blob: 3539d89f7298570211674f8dcffb2fec2472a569 [file] [log] [blame]
#
# Copyright (C) 2015-2018 Savoir-faire Linux Inc
#
# Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""DRing controling class through DBUS"""
import sys
import os
import random
import time
import hashlib
from threading import Thread
from functools import partial
try:
from gi.repository import GObject
except ImportError as e:
import gobject as GObject
except Exception as e:
print(str(e))
exit(1)
from errors import *
try:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
except ImportError as e:
raise DRingCtrlError("No python-dbus module found")
DBUS_DEAMON_OBJECT = 'cx.ring.Ring'
DBUS_DEAMON_PATH = '/cx/ring/Ring'
class DRingCtrl(Thread):
def __init__(self, name, autoAnswer):
if sys.version_info[0] < 3:
super(DRingCtrl, self).__init__()
else:
super().__init__()
self.activeCalls = {} # list of active calls (known by the client)
self.activeConferences = {} # list of active conferences
self.account = None # current active account
self.name = name # client name
self.autoAnswer = autoAnswer
self.currentCallId = ""
self.currentConfId = ""
self.isStop = False
# Glib MainLoop for processing callbacks
self.loop = GObject.MainLoop()
GObject.threads_init()
# client registered to sflphoned ?
self.registered = False
self.register()
def __del__(self):
self.unregister()
#self.loop.quit() # causes exception
def stopThread(self):
self.isStop = True
def register(self):
if self.registered:
return
try:
# register the main loop for d-bus events
DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
except dbus.DBusException as e:
raise DRingCtrlDBusError("Unable to connect DBUS session bus")
if not bus.name_has_owner(DBUS_DEAMON_OBJECT) :
raise DRingCtrlDBusError(("Unable to find %s in DBUS." % DBUS_DEAMON_OBJECT)
+ " Check if dring is running")
try:
proxy_instance = bus.get_object(DBUS_DEAMON_OBJECT,
DBUS_DEAMON_PATH+'/Instance', introspect=False)
proxy_callmgr = bus.get_object(DBUS_DEAMON_OBJECT,
DBUS_DEAMON_PATH+'/CallManager', introspect=False)
proxy_confmgr = bus.get_object(DBUS_DEAMON_OBJECT,
DBUS_DEAMON_PATH+'/ConfigurationManager', introspect=False)
proxy_videomgr = bus.get_object(DBUS_DEAMON_OBJECT,
DBUS_DEAMON_PATH+'/VideoManager', introspect=False)
self.instance = dbus.Interface(proxy_instance,
DBUS_DEAMON_OBJECT+'.Instance')
self.callmanager = dbus.Interface(proxy_callmgr,
DBUS_DEAMON_OBJECT+'.CallManager')
self.configurationmanager = dbus.Interface(proxy_confmgr,
DBUS_DEAMON_OBJECT+'.ConfigurationManager')
if proxy_videomgr:
self.videomanager = dbus.Interface(proxy_videomgr,
DBUS_DEAMON_OBJECT+'.VideoManager')
except dbus.DBusException as e:
raise DRingCtrlDBusError("Unable to bind to dring DBus API")
try:
self.instance.Register(os.getpid(), self.name)
self.registered = True
except dbus.DBusException as e:
raise DRingCtrlDeamonError("Client registration failed")
try:
proxy_callmgr.connect_to_signal('incomingCall', self.onIncomingCall)
proxy_callmgr.connect_to_signal('callStateChanged', self.onCallStateChanged)
proxy_callmgr.connect_to_signal('conferenceCreated', self.onConferenceCreated)
proxy_confmgr.connect_to_signal('accountsChanged', self.onAccountsChanged)
proxy_confmgr.connect_to_signal('dataTransferEvent', self.onDataTransferEvent)
except dbus.DBusException as e:
raise DRingCtrlDBusError("Unable to connect to dring DBus signals")
def unregister(self):
if not self.registered:
return
try:
self.instance.Unregister(os.getpid())
self.registered = False
except:
raise DRingCtrlDeamonError("Client unregistration failed")
def isRegistered(self):
return self.registered
#
# Signal handling
#
def onIncomingCall_cb(self, callId):
if self.autoAnswer:
self.Accept(callId)
pass
def onCallHangup_cb(self, callId):
pass
def onCallRinging_cb(self):
pass
def onCallHold_cb(self):
pass
def onCallCurrent_cb(self):
pass
def onCallBusy_cb(self):
pass
def onCallFailure_cb(self):
pass
def onIncomingCall(self, account, callid, to):
""" On incoming call event, add the call to the list of active calls """
self.activeCalls[callid] = {'Account': account,
'To': to,
'State': ''}
self.currentCallId = callid
self.onIncomingCall_cb(callid)
def onCallHangUp(self, callid):
""" Remove callid from call list """
self.onCallHangup_cb(callid)
self.currentCallId = ""
del self.activeCalls[callid]
def onCallRinging(self, callid, state):
""" Update state for this call to Ringing """
self.activeCalls[callid]['State'] = state
self.onCallRinging_cb(callid)
def onCallHold(self, callid, state):
""" Update state for this call to Hold """
self.activeCalls[callid]['State'] = state
self.onCallHold_cb()
def onCallCurrent(self, callid, state):
""" Update state for this call to current """
self.activeCalls[callid]['State'] = state
self.onCallCurrent_cb()
def onCallBusy(self, callid, state):
""" Update state for this call to busy """
self.activeCalls[callid]['State'] = state
self.onCallBusy_cb()
def onCallFailure(self, callid, state):
""" Handle call failure """
self.onCallFailure_cb()
del self.activeCalls[callid]
def onCallStateChanged(self, callid, state, code):
""" On call state changed event, set the values for new calls,
or delete the call from the list of active calls
"""
print(("On call state changed " + callid + " " + state))
if callid not in self.activeCalls:
print("This call didn't exist!: " + callid + ". Adding it to the list.")
callDetails = self.getCallDetails(callid)
self.activeCalls[callid] = {'Account': callDetails['ACCOUNTID'],
'To': callDetails['PEER_NUMBER'],
'State': state,
'Code': code }
self.currentCallId = callid
if state == "HUNGUP":
self.onCallHangUp(callid)
elif state == "RINGING":
self.onCallRinging(callid, state)
elif state == "CURRENT":
self.onCallCurrent(callid, state)
elif state == "HOLD":
self.onCallHold(callid, state)
elif state == "BUSY":
self.onCallBusy(callid, state)
elif state == "FAILURE":
self.onCallFailure(callid, state)
else:
print("unknown state:" + str(state))
def onConferenceCreated_cb(self):
pass
def onConferenceCreated(self, confId):
self.currentConfId = confId
self.onConferenceCreated_cb()
def onDataTransferEvent(self, transferId, code):
pass
#
# Account management
#
def _valid_account(self, account):
account = account or self.account
if account is None:
raise DRingCtrlError("No provided or current account!")
return account
def isAccountExists(self, account):
""" Checks if the account exists"""
return account in self.getAllAccounts()
def isAccountEnable(self, account=None):
"""Return True if the account is enabled. If no account is provided, active account is used"""
return self.getAccountDetails(self._valid_account(account))['Account.enable'] == "true"
def isAccountRegistered(self, account=None):
"""Return True if the account is registered. If no account is provided, active account is used"""
return self.getVolatileAccountDetails(self._valid_account(account))['Account.registrationStatus'] in ('READY', 'REGISTERED')
def isAccountOfType(self, account_type, account=None):
"""Return True if the account type is the given one. If no account is provided, active account is used"""
return self.getAccountDetails(self._valid_account(account))['Account.type'] == account_type
def getAllAccounts(self, account_type=None):
"""Return a list with all accounts"""
acclist = map(str, self.configurationmanager.getAccountList())
if account_type:
acclist = filter(partial(self.isAccountOfType, account_type), acclist)
return list(acclist)
def getAllEnabledAccounts(self):
"""Return a list with all enabled-only accounts"""
return [x for x in self.getAllAccounts() if self.isAccountEnable(x)]
def getAllRegisteredAccounts(self):
"""Return a list with all registered-only accounts"""
return [x for x in self.getAllAccounts() if self.isAccountRegistered(x)]
def getAccountDetails(self, account=None):
"""Return a list of string. If no account is provided, active account is used"""
account = self._valid_account(account)
if self.isAccountExists(account):
return self.configurationmanager.getAccountDetails(account)
return []
def getVolatileAccountDetails(self, account=None):
"""Return a list of string. If no account is provided, active account is used"""
account = self._valid_account(account)
if self.isAccountExists(account):
return self.configurationmanager.getVolatileAccountDetails(account)
return []
def setActiveCodecList(self, account=None, codec_list=''):
"""Activate given codecs on an account. If no account is provided, active account is used"""
account = self._valid_account(account)
if self.isAccountExists(account):
codec_list = [dbus.UInt32(x) for x in codec_list.split(',')]
self.configurationmanager.setActiveCodecList(account, codec_list)
def addAccount(self, details=None):
"""Add a new account account
Add a new account to the Ring-daemon. Default parameters are \
used for missing account configuration field.
Required parameters are type, alias, hostname, username and password
input details
"""
if details is None:
raise DRingCtrlAccountError("Must specifies type, alias, hostname, \
username and password in \
order to create a new account")
return str(self.configurationmanager.addAccount(details))
def removeAccount(self, accountID=None):
"""Remove an account from internal list"""
if accountID is None:
raise DRingCtrlAccountError("Account ID must be specified")
self.configurationmanager.removeAccount(accountID)
def setAccountByAlias(self, alias):
"""Define as active the first account who match with the alias"""
for testedaccount in self.getAllAccounts():
details = self.getAccountDetails(testedaccount)
if (details['Account.enable'] == 'true' and
details['Account.alias'] == alias):
self.account = testedaccount
return
raise DRingCtrlAccountError("No enabled account matched with alias")
def getAccountByAlias(self, alias):
"""Get account name having its alias"""
for account in self.getAllAccounts():
details = self.getAccountDetails(account)
if details['Account.alias'] == alias:
return account
raise DRingCtrlAccountError("No account matched with alias")
def setAccount(self, account):
"""Define the active account
The active account will be used when sending a new call
"""
if account in self.getAllAccounts():
self.account = account
else:
print(account)
raise DRingCtrlAccountError("Not a valid account")
def setFirstRegisteredAccount(self):
"""Find the first enabled account and define it as active"""
rAccounts = self.getAllRegisteredAccounts()
if 0 == len(rAccounts):
raise DRingCtrlAccountError("No registered account !")
self.account = rAccounts[0]
def setFirstActiveAccount(self):
"""Find the first enabled account and define it as active"""
aAccounts = self.getAllEnabledAccounts()
if 0 == len(aAccounts):
raise DRingCtrlAccountError("No active account !")
self.account = aAccounts[0]
def getAccount(self):
"""Return the active account"""
return self.account
def setAccountEnable(self, account=None, enable=False):
"""Set account enabled"""
account = self._valid_account(account)
if enable == True:
details = self.getAccountDetails(account)
details['Account.enable'] = "true"
self.configurationmanager.setAccountDetails(account, details)
else:
details = self.getAccountDetails(account)
details['Account.enable'] = "false"
self.configurationmanager.setAccountDetails(account, details)
def setAccountRegistered(self, account=None, register=False):
""" Tries to register the account"""
account = self._valid_account(account)
self.configurationmanager.sendRegister(account, register)
def onAccountsChanged(self):
print("Accounts changed")
#
# Codec manager
#
def getAllCodecs(self):
""" Return all codecs"""
return [int(x) for x in self.configurationmanager.getCodecList()]
def getCodecDetails(self, account, codecId):
""" Return codec details"""
codecId=dbus.UInt32(codecId)
return self.configurationmanager.getCodecDetails(account, codecId)
def getActiveCodecs(self, account=None):
""" Return all active codecs on given account"""
account = self._valid_account(account)
return [int(x) for x in self.configurationmanager.getActiveCodecList(account)]
def setVideoCodecBitrate(self, account, bitrate):
""" Change bitrate for all codecs on given account"""
for codecId in self.configurationmanager.getActiveCodecList(account):
details = self.configurationmanager.getCodecDetails(account, codecId)
details['CodecInfo.bitrate'] = str(bitrate)
if details['CodecInfo.type'] == 'VIDEO':
self.configurationmanager.setCodecDetails(account, codecId, details)
#
# Call management
#
def getAllCalls(self):
"""Return all calls handled by the daemon"""
return [str(x) for x in self.callmanager.getCallList()]
def getCallDetails(self, callid):
"""Return informations on this call if exists"""
return self.callmanager.getCallDetails(callid)
def printClientCallList(self):
print("Client active call list:")
print("------------------------")
for call in self.activeCalls:
print("\t" + call)
def Call(self, dest):
"""Start a call and return a CallID
Use the current account previously set using setAccount().
If no account specified, first registered one in account list is used.
return callID Newly generated callidentifier for this call
"""
if dest is None or dest == "":
raise SflPhoneError("Invalid call destination")
# Set the account to be used for this call
if not self.account:
self.setFirstRegisteredAccount()
if self.account is not "IP2IP" and not self.isAccountRegistered():
raise DRingCtrlAccountError("Can't place a call without a registered account")
# Send the request to the CallManager
callid = self.callmanager.placeCall(self.account, dest)
if callid:
# Add the call to the list of active calls and set status to SENT
self.activeCalls[callid] = {'Account': self.account, 'To': dest, 'State': 'SENT' }
return callid
def HangUp(self, callid):
"""End a call identified by a CallID"""
if not self.account:
self.setFirstRegisteredAccount()
if callid is None or callid == "":
pass # just to see
self.callmanager.hangUp(callid)
def Transfer(self, callid, to):
"""Transfert a call identified by a CallID"""
if callid is None or callid == "":
raise DRingCtrlError("Invalid callID")
self.callmanager.transfert(callid, to)
def Refuse(self, callid):
"""Refuse an incoming call identified by a CallID"""
print("Refuse call " + callid)
if callid is None or callid == "":
raise DRingCtrlError("Invalid callID")
self.callmanager.refuse(callid)
def Accept(self, callid):
"""Accept an incoming call identified by a CallID"""
print("Accept call " + callid)
if not self.account:
self.setFirstRegisteredAccount()
if not self.isAccountRegistered():
raise DRingCtrlAccountError("Can't accept a call without a registered account")
if callid is None or callid == "":
raise DRingCtrlError("Invalid callID")
self.callmanager.accept(callid)
def Hold(self, callid):
"""Hold a call identified by a CallID"""
if callid is None or callid == "":
raise DRingCtrlError("Invalid callID")
self.callmanager.hold(callid)
def UnHold(self, callid):
"""Unhold an incoming call identified by a CallID"""
if callid is None or callid == "":
raise DRingCtrlError("Invalid callID")
self.callmanager.unhold(callid)
def Dtmf(self, key):
"""Send a DTMF"""
self.callmanager.playDTMF(key)
def _GenerateCallID(self):
"""Generate Call ID"""
m = hashlib.md5()
t = int( time.time() * 1000 )
r = int( random.random()*100000000000000000 )
m.update(str(t) + str(r))
callid = m.hexdigest()
return callid
def createConference(self, call1Id, call2Id):
""" Create a conference given the two call ids """
self.callmanager.joinParticipant(call1Id, call2Id)
def hangupConference(self, confId):
""" Hang up each call for this conference """
self.callmanager.hangUpConference(confId)
def switchInput(self, callid, inputName):
"""switch to input if exist"""
return self.callmanager.switchInput(callid, inputName)
def interruptHandler(self, signum, frame):
print('Signal handler called with signal ' + str(signum))
self.stopThread()
def printAccountDetails(self, account):
details = self.getAccountDetails(account)
print(account)
for k in sorted(details.keys()):
print(" %s: %s" % (k, details[k]))
print()
def sendFile(self, *args, **kwds):
return self.configurationmanager.sendFile(*args, **kwds)
def run(self):
"""Processing method for this thread"""
context = self.loop.get_context()
while True:
context.iteration(True)
if self.isStop:
print("++++++++++++++++++ EXIT ++++++++++++++++++++++")
return