Browse Source

Use py-asterisk from http://code.google.com/p/py-asterisk/ (up-to-date mercurial version required) instead of our own code

Add a field 'resource' on res.users, which is used in "SIP/resource", instead of "SIP/internal_number", which was not correct. YOU NEED TO UPDATE YOUR DATA !
pull/26/head
Alexis de Lattre 12 years ago
parent
commit
602abe0639
  1. 201
      asterisk_click2dial/asterisk_click2dial.py
  2. 5
      asterisk_click2dial/res_users_view.xml
  3. 2
      asterisk_click2dial/wizard/open_calling_partner.py

201
asterisk_click2dial/asterisk_click2dial.py

@ -28,6 +28,10 @@ import logging
from tools.translate import _ from tools.translate import _
# Lib for phone number reformating -> pip install phonenumbers # Lib for phone number reformating -> pip install phonenumbers
import phonenumbers import phonenumbers
# Lib py-asterisk from http://code.google.com/p/py-asterisk/
# We need a version which has this commit : http://code.google.com/p/py-asterisk/source/detail?r=8d0e1c941cce727c702582f3c9fcd49beb4eeaa4
# so a version after Nov 20th, 2012
from Asterisk import Manager
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -49,7 +53,7 @@ class asterisk_server(osv.osv):
'context': fields.char('Dialplan context', size=50, required=True, help="Asterisk dialplan context from which the calls will be made. Refer to /etc/asterisk/extensions.conf on your Asterisk server."), 'context': fields.char('Dialplan context', size=50, required=True, help="Asterisk dialplan context from which the calls will be made. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
'wait_time': fields.integer('Wait time (sec)', required=True, help="Amount of time (in seconds) Asterisk will try to reach the user's phone before hanging up."), 'wait_time': fields.integer('Wait time (sec)', required=True, help="Amount of time (in seconds) Asterisk will try to reach the user's phone before hanging up."),
'extension_priority': fields.integer('Extension priority', required=True, help="Priority of the extension in the Asterisk dialplan. Refer to /etc/asterisk/extensions.conf on your Asterisk server."), 'extension_priority': fields.integer('Extension priority', required=True, help="Priority of the extension in the Asterisk dialplan. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
'alert_info': fields.char('Alert-Info SIP header', size=255, help="Set Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example. If you want to have several variable headers, separate them with '|'."),
'alert_info': fields.char('Alert-Info SIP header', size=255, help="Set Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example."),
'company_id': fields.many2one('res.company', 'Company', help="Company who uses the Asterisk server."), 'company_id': fields.many2one('res.company', 'Company', help="Company who uses the Asterisk server."),
} }
@ -188,28 +192,10 @@ class asterisk_server(osv.osv):
return ast_server return ast_server
def _parse_asterisk_answer(self, cr, uid, sock, end_string='\r\n\r\n', context=None):
'''Parse the answer of the Asterisk Manager Interface'''
answer = ''
data = ''
# TODO : if there is an error, we will stay in the while loop
# ex :
# Response: Error
# Message: Permission denied
while end_string not in data:
data = sock.recv(1024)
if data:
answer += data
# remove end_string from answer
if answer[-len(end_string):] == end_string:
answer = answer[:-len(end_string)]
return answer
def _connect_to_asterisk(self, cr, uid, method='dial', options=None, context=None):
def _connect_to_asterisk(self, cr, uid, context=None):
''' '''
Open the socket to the Asterisk Manager Interface (AMI)
and send instructions to Dial to Asterisk. That's the important function !
Open the connection to the asterisk manager
Returns an instance of the Asterisk Manager
''' '''
user = self.pool.get('res.users').browse(cr, uid, uid, context=context) user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
@ -223,125 +209,97 @@ class asterisk_server(osv.osv):
raise osv.except_osv(_('Error :'), _('No channel type configured for the current user.')) raise osv.except_osv(_('Error :'), _('No channel type configured for the current user.'))
# We check if the current user has an internal number # We check if the current user has an internal number
if not user.internal_number:
raise osv.except_osv(_('Error :'), _('No internal phone number configured for the current user'))
if not user.resource:
raise osv.except_osv(_('Error :'), _('No resource name configured for the current user'))
_logger.debug("User's phone : %s/%s" % (user.asterisk_chan_type, user.internal_number))
_logger.debug("User's phone : %s/%s" % (user.asterisk_chan_type, user.resource))
_logger.debug("Asterisk server = %s:%d" % (ast_server.ip_address, ast_server.port)) _logger.debug("Asterisk server = %s:%d" % (ast_server.ip_address, ast_server.port))
# Connect to the Asterisk Manager Interface, using IPv6-ready code
# Connect to the Asterisk Manager Interface
try: try:
res = socket.getaddrinfo(str(ast_server.ip_address), ast_server.port, socket.AF_UNSPEC, socket.SOCK_STREAM)
except:
_logger.warning("Can't resolve the DNS of the Asterisk server '%s'" % ast_server.ip_address)
raise osv.except_osv(_('Error :'), _("Can't resolve the DNS of the Asterisk server : '%s'" % ast_server.ip_address))
for result in res:
af, socktype, proto, canonname, sockaddr = result
sock = socket.socket(af, socktype, proto)
try:
sock.connect(sockaddr)
header_received = sock.recv(1024)
_logger.debug('Header received from Asterisk : %s' % header_received)
# Login to Asterisk
login_act = 'Action: login\r\n' + \
'Events: off\r\n' + \
'Username: ' + ast_server.login + '\r\n' + \
'Secret: ' + ast_server.password + '\r\n\r\n'
sock.send(login_act.encode('ascii'))
login_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
if 'Response: Success' in login_answer:
_logger.debug("Successful authentification to Asterisk :\n%s" % login_answer)
else:
raise osv.except_osv(_('Error :'), _("Authentification to Asterisk failed :\n%s" % login_answer))
ast_manager = Manager.Manager((ast_server.ip_address, ast_server.port), ast_server.login, ast_server.password)
except Exception, e:
_logger.error("Error in the Originate request to Asterisk server %s" % ast_server.ip_address)
_logger.error("Here is the detail of the error : '%s'" % unicode(e))
raise osv.except_osv(_('Error :'), _("Problem in the request from OpenERP to Asterisk. Here is the detail of the error: '%s'" % unicode(e)))
return False
return (user, ast_server, ast_manager)
if method == 'dial':
# Convert the phone number in the format that will be sent to Asterisk
erp_number = options.get('erp_number')
def _dial_with_asterisk(self, cr, uid, erp_number, context=None):
print "_dial_with_asterisk erp_number=", erp_number
if not erp_number: if not erp_number:
raise osv.except_osv(_('Error :'), "Hara kiri : you must call the function with erp_number in the options")
raise osv.except_osv(_('Error :'), "Hara kiri : you must call the function with erp_number")
user, ast_server, ast_manager = self._connect_to_asterisk(cr, uid, context=context)
ast_number = self._reformat_number(cr, uid, erp_number, ast_server, context=context) ast_number = self._reformat_number(cr, uid, erp_number, ast_server, context=context)
# The user should have a CallerID # The user should have a CallerID
if not user.callerid: if not user.callerid:
raise osv.except_osv(_('Error :'), _('No callerID configured for the current user')) raise osv.except_osv(_('Error :'), _('No callerID configured for the current user'))
# Dial with Asterisk
originate_act = 'Action: originate\r\n' + \
'Channel: ' + user.asterisk_chan_type + '/' + user.internal_number + ( ('/' + user.dial_suffix) if user.dial_suffix else '') + '\r\n' + \
'Priority: ' + str(ast_server.extension_priority) + '\r\n' + \
'Timeout: ' + str(ast_server.wait_time*1000) + '\r\n' + \
'CallerId: ' + user.callerid + '\r\n' + \
'Exten: ' + ast_number + '\r\n' + \
'Context: ' + ast_server.context + '\r\n'
if ast_server.alert_info and user.asterisk_chan_type == 'SIP':
for server_alertinfo in ast_server.alert_info.split('|'):
originate_act += 'Variable: SIPAddHeader=Alert-Info: ' + server_alertinfo.strip() + '\r\n'
if user.alert_info and user.asterisk_chan_type == 'SIP':
for user_alertinfo in user.alert_info.split('|'):
originate_act += 'Variable: SIPAddHeader=Alert-Info: ' + user_alertinfo.strip() + '\r\n'
variable = []
if user.asterisk_chan_type == 'SIP':
# We can only have one alert-info header in a SIP request
if user.alert_info:
variable.append('SIPAddHeader=Alert-Info: ' + user.alert_info)
elif ast_server.alert_info:
variable.append('SIPAddHeader=Alert-Info: ' + ast_server.alert_info)
if user.variable: if user.variable:
for user_variable in user.variable.split('|'): for user_variable in user.variable.split('|'):
originate_act += 'Variable: ' + user_variable.strip() + '\r\n'
variable.append(user_variable.strip())
originate_act += '\r\n'
sock.send(originate_act.encode('ascii'))
originate_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
if 'Response: Success' in originate_answer:
_logger.debug('Successfull originate command : %s' % originate_answer)
else:
raise osv.except_osv(_('Error :'), _("Click to dial with Asterisk failed :\n%s" % originate_answer))
try:
ast_manager.Originate(
user.asterisk_chan_type + '/' + user.resource + ( ('/' + user.dial_suffix) if user.dial_suffix else ''),
context = ast_server.context,
extension = ast_number,
priority = str(ast_server.extension_priority),
timeout = str(ast_server.wait_time*1000),
caller_id = user.callerid,
variable = variable)
except Exception, e:
_logger.error("Error in the Originate request to Asterisk server %s" % ast_server.ip_address)
_logger.error("Here is the detail of the error : '%s'" % unicode(e))
raise osv.except_osv(_('Error :'), _("Click to dial with Asterisk failed.\nHere is the error: '%s'" % unicode(e)))
elif method == "get_calling_number":
status_act = 'Action: Status\r\n\r\n' # TODO : add ActionID
sock.send(status_act.encode('ascii'))
status_answer = self._parse_asterisk_answer(cr, uid, sock, end_string='Event: StatusComplete', context=context).decode('utf-8')
finally:
ast_manager.Logoff()
if 'Response: Success' in status_answer:
_logger.debug('Successfull Status command :\n%s' % status_answer)
else:
raise osv.except_osv(_('Error :'), _("Status command to Asterisk failed :\n%s" % status_answer))
return True
def _get_calling_number(self, cr, uid, context=None):
# Parse answer
user, ast_server, ast_manager = self._connect_to_asterisk(cr, uid, context=context)
calling_party_number = False calling_party_number = False
status_answer_split = status_answer.split('\r\n\r\n')
for event in status_answer_split:
string_bridge_match = 'BridgedChannel: ' + user.asterisk_chan_type + '/' + user.internal_number
string_link_match = 'Link: ' + user.asterisk_chan_type + '/' + user.internal_number # Asterisk 1.4 ? Or is it related to the fact that it's an IAX trunk ?
if string_bridge_match in event or string_link_match in event:
_logger.debug("Found a matching Event")
event_split = event.split('\r\n')
for event_line in event_split:
if 'CallerIDNum' in event_line:
line_detail = event_line.split(': ')
if len(line_detail) <> 2:
raise osv.except_osv('Error :', "Hara kiri... this is not possible")
calling_party_number = line_detail[1]
_logger.debug("The calling party number is '%s'" % calling_party_number)
try:
list_chan = ast_manager.Status()
#from pprint import pprint
#pprint(list_chan)
_logger.debug("Result of Status AMI request: %s", list_chan)
for chan in list_chan.values():
sip_account = user.asterisk_chan_type + '/' + user.resource
if chan.get('ChannelState') == '4' and chan.get('ConnectedLineNum') == user.internal_number: # 4 = Ring
_logger.debug("Found a matching Event in 'Ring' state")
calling_party_number = chan.get('CallerIDNum')
break
if chan.get('ChannelState') == '6' and sip_account in chan.get('BridgedChannel'): # 6 = Up
_logger.debug("Found a matching Event in 'Up' state")
calling_party_number = chan.get('CallerIDNum')
break
except Exception, e:
_logger.error("Error in the Status request to Asterisk server %s" % ast_server.ip_address)
_logger.error("Here is the detail of the error : '%s'" % unicode(e))
raise osv.except_osv(_('Error :'), _("Can't get calling number from Asterisk.\nHere is the error: '%s'" % unicode(e)))
# Logout of Asterisk
sock.send(('Action: Logoff\r\n\r\n').encode('ascii'))
logout_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
if 'Response: Goodbye' in logout_answer:
_logger.debug('Successfull logout from Asterisk :\n%s' % logout_answer)
else:
_logger.warning('Logout from Asterisk failed :\n%s' % logout_answer)
# we catch only network problems here
except socket.error:
_logger.warning("Unable to connect to the Asterisk server '%s' IP '%s:%d'" % (ast_server.name, ast_server.ip_address, ast_server.port))
raise osv.except_osv(_('Error :'), _("The connection from OpenERP to the Asterisk server failed. Please check the configuration on OpenERP and on Asterisk."))
finally: finally:
sock.close()
if method == 'dial':
_logger.info("Asterisk Click2Dial from %s/%s to %s" % (user.asterisk_chan_type, user.internal_number, ast_number))
return True
ast_manager.Logoff()
_logger.debug("The calling party number is '%s'" % calling_party_number)
elif method == "get_calling_number":
return calling_party_number return calling_party_number
else:
return False
asterisk_server() asterisk_server()
@ -372,7 +330,9 @@ class res_users(osv.osv):
('SCCP', 'SCCP'), ('SCCP', 'SCCP'),
], 'Asterisk channel type', ], 'Asterisk channel type',
help="Asterisk channel type, as used in the Asterisk dialplan. If the user has a regular IP phone, the channel type is 'SIP'."), help="Asterisk channel type, as used in the Asterisk dialplan. If the user has a regular IP phone, the channel type is 'SIP'."),
'alert_info': fields.char('User-specific Alert-Info SIP header', size=255, help="Set a user-specific Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example. If you want to have several variable headers, separate them with '|'."),
'resource': fields.char('Resource name', size=64,
help="Resource name for the channel type selected. For example, if you use 'Dial(SIP/phone1)' in your Asterisk dialplan to ring the SIP phone of this user, then the resource name for this user is 'phone1'. For a SIP phone, the phone number is often used as resource name, but not always."),
'alert_info': fields.char('User-specific Alert-Info SIP header', size=255, help="Set a user-specific Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example."),
'variable': fields.char('User-specific Variable', size=255, help="Set a user-specific 'Variable' field in the Asterisk Manager Interface 'originate' request for the click2dial feature. If you want to have several variable headers, separate them with '|'."), 'variable': fields.char('User-specific Variable', size=255, help="Set a user-specific 'Variable' field in the Asterisk Manager Interface 'originate' request for the click2dial feature. If you want to have several variable headers, separate them with '|'."),
'asterisk_server_id': fields.many2one('asterisk.server', 'Asterisk server', 'asterisk_server_id': fields.many2one('asterisk.server', 'Asterisk server',
help="Asterisk server on which the user's phone is connected. If you leave this field empty, it will use the first Asterisk server of the user's company."), help="Asterisk server on which the user's phone is connected. If you leave this field empty, it will use the first Asterisk server of the user's company."),
@ -384,7 +344,7 @@ class res_users(osv.osv):
def _check_validity(self, cr, uid, ids): def _check_validity(self, cr, uid, ids):
for user in self.browse(cr, uid, ids): for user in self.browse(cr, uid, ids):
for check_string in [('Internal number', user.internal_number), ('Caller ID', user.callerid)]:
for check_string in [('Resource name', user.resource), ('Internal number', user.internal_number), ('Caller ID', user.callerid)]:
if check_string[1]: if check_string[1]:
try: try:
plom = check_string[1].encode('ascii') plom = check_string[1].encode('ascii')
@ -393,7 +353,7 @@ class res_users(osv.osv):
return True return True
_constraints = [ _constraints = [
(_check_validity, "Error message in raise", ['internal_number', 'callerid']),
(_check_validity, "Error message in raise", ['resource', 'internal_number', 'callerid']),
] ]
res_users() res_users()
@ -481,8 +441,7 @@ class res_partner_address(osv.osv):
raise osv.except_osv(_('Error :'), _('There is no phone number !')) raise osv.except_osv(_('Error :'), _('There is no phone number !'))
elif erp_number_display and not erp_number_e164: elif erp_number_display and not erp_number_e164:
raise osv.except_osv(_('Error :'), _("The phone number isn't stored in the standard E.164 format. Try to run the wizard 'Reformat all phone numbers' from the menu Settings > Configuration > Asterisk.")) raise osv.except_osv(_('Error :'), _("The phone number isn't stored in the standard E.164 format. Try to run the wizard 'Reformat all phone numbers' from the menu Settings > Configuration > Asterisk."))
options = {'erp_number': erp_number_e164}
return self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='dial', options=options, context=context)
return self.pool.get('asterisk.server')._dial_with_asterisk(cr, uid, erp_number_e164, context=context)
def action_dial_phone(self, cr, uid, ids, context=None): def action_dial_phone(self, cr, uid, ids, context=None):

5
asterisk_click2dial/res_users_view.xml

@ -11,15 +11,16 @@
<data> <data>
<record id="asterisk_res_users_internal_number" model="ir.ui.view"> <record id="asterisk_res_users_internal_number" model="ir.ui.view">
<field name="name">asterisk.res.users.internal.number</field>
<field name="name">asterisk.res.users.telephony_tab</field>
<field name="model">res.users</field> <field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/> <field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<page string="User" position="after"> <page string="User" position="after">
<page string="Telephony"> <page string="Telephony">
<separator string="Standard parameters" colspan="4"/> <separator string="Standard parameters" colspan="4"/>
<field name="internal_number"/>
<field name="asterisk_chan_type"/> <field name="asterisk_chan_type"/>
<field name="resource"/>
<field name="internal_number"/>
<field name="callerid"/> <field name="callerid"/>
<field name="asterisk_server_id"/> <field name="asterisk_server_id"/>
<separator string="Optional parameters - for experts only, can be left empty" colspan="4"/> <separator string="Optional parameters - for experts only, can be left empty" colspan="4"/>

2
asterisk_click2dial/wizard/open_calling_partner.py

@ -49,7 +49,7 @@ class wizard_open_calling_partner(osv.osv_memory):
'''Thanks to the default_get method, we are able to query Asterisk and '''Thanks to the default_get method, we are able to query Asterisk and
get the corresponding partner when we launch the wizard''' get the corresponding partner when we launch the wizard'''
res = {} res = {}
calling_number = self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='get_calling_number', context=context)
calling_number = self.pool.get('asterisk.server')._get_calling_number(cr, uid, context=context)
#To test the code without Asterisk server #To test the code without Asterisk server
#calling_number = "0141981242" #calling_number = "0141981242"
if calling_number: if calling_number:

Loading…
Cancel
Save