From 6d92318bd02b0c799c7e8568b1a1ba5b0a7976a0 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 1 Aug 2014 21:19:47 +0200 Subject: [PATCH] Add support for update of the name on OUTGOING calls (for far, it was only possible on incoming calls). --- asterisk_click2dial/__openerp__.py | 9 +- .../{get_cid_name.py => set_name_agi.py} | 220 ++++++++++++------ ...imeout.sh => set_name_incoming_timeout.sh} | 17 +- .../scripts/set_name_outgoing_timeout.sh | 32 +++ 4 files changed, 195 insertions(+), 83 deletions(-) rename asterisk_click2dial/scripts/{get_cid_name.py => set_name_agi.py} (56%) rename asterisk_click2dial/scripts/{get_cid_name_timeout.sh => set_name_incoming_timeout.sh} (62%) create mode 100755 asterisk_click2dial/scripts/set_name_outgoing_timeout.sh diff --git a/asterisk_click2dial/__openerp__.py b/asterisk_click2dial/__openerp__.py index 8836b03..f75eb5c 100644 --- a/asterisk_click2dial/__openerp__.py +++ b/asterisk_click2dial/__openerp__.py @@ -36,11 +36,12 @@ . If the remote party answers, the user can talk to his correspondent. 2) It adds the ability to show the name of the calling party on the screen of your IP phone on incoming phone calls if the presented -phone number is present in the partners of OpenERP. Here is how it works : -. On incoming phone calls, the Asterisk dialplan executes an AGI script "get_cid_name_timeout.sh". -. The "get_cid_name_timeout.sh" script calls the "get_cid_name.py" script with a short timeout. -. The "get_cid_name.py" script will make an XML-RPC request on the OpenERP server to try to find the name of the person corresponding to the phone number presented by the calling party. +phone number is present in the partner/leads/employees/... of OpenERP. Here is how it works : +. On incoming phone calls, the Asterisk dialplan executes an AGI script "set_name_incoming_timeout.sh". +. The "set_name_incoming_timeout.sh" script calls the "set_name_agi.py" script with a short timeout. +. The "set_name_agi.py" script will make an XML-RPC request on the OpenERP server to try to find the name of the person corresponding to the phone number presented by the calling party. . If it finds the name, it is set as the CallerID name of the call, so as to be presented on the IP phone of the user. +It also works on outgoing calls, so as to display the name of the callee on the SIP phone of the caller. For that, you should use the script "set_name_outgoing_timeout.sh". 3) It adds a button "Open calling partner" in the menu "Sales > Address book" to get the partner corresponding to the calling party in one click. Here is how it works : . When the user clicks on the "Open calling partner" button, OpenERP sends a query to the Asterisk Manager Interface to get a list of the current phone calls diff --git a/asterisk_click2dial/scripts/get_cid_name.py b/asterisk_click2dial/scripts/set_name_agi.py similarity index 56% rename from asterisk_click2dial/scripts/get_cid_name.py rename to asterisk_click2dial/scripts/set_name_agi.py index 44680df..5222913 100755 --- a/asterisk_click2dial/scripts/get_cid_name.py +++ b/asterisk_click2dial/scripts/set_name_agi.py @@ -1,17 +1,8 @@ #! /usr/bin/python # -*- encoding: utf-8 -*- """ - CallerID name lookup in OpenERP for Asterisk IPBX - - When executed from the dialplan on an incoming phone call, it will - lookup in OpenERP's partners and other objects with phone numbers - (leads, employees, etc...), and, if it finds the phone number, it will - get the corresponding name of the person and use this name as CallerID - name for the incoming call. - - Requires the "base_phone" module - available from https://code.launchpad.net/openerp-asterisk-connector - for OpenERP version >= 7.0 + Name lookup in OpenERP for incoming and outgoing calls with an + Asterisk IPBX This script is designed to be used as an AGI on an Asterisk IPBX... BUT I advise you to use a wrapper around this script to control the @@ -21,30 +12,76 @@ The simplest solution I found is to use the "timeout" shell command to call this script, for example : - # timeout 2s get_cid_name.py + # timeout 2s get_name_agi.py + + See my 2 sample wrappers "set_name_incoming_timeout.sh" and + "set_name_outgoing_timeout.sh" + + It's probably a good idea to create a user in OpenERP dedicated to this task. + This user only needs to be part of the group "Phone CallerID", which has + read access on the 'res.partner' and other objects with phone numbers and + names. + + Note that this script can be used without OpenERP, with just the + geolocalisation feature : for that, don't use option --server ; + only use --geoloc + + This script can be used both on incoming and outgoing calls : + + 1) INCOMING CALLS + When executed from the dialplan on an incoming phone call, it will + lookup in OpenERP's partners and other objects with phone numbers + (leads, employees, etc...), and, if it finds the phone number, it will + get the corresponding name of the person and use this name as CallerID + name for the incoming call. - See my sample wrapper "get_cid_name_timeout.sh" + Requires the "base_phone" module + available from https://code.launchpad.net/openerp-asterisk-connector + for OpenERP version >= 7.0 Asterisk dialplan example : [from-extern] - exten => _0141981242,1,AGI(/usr/local/bin/get_cid_name_timeout.sh) - exten => _0141981242,n,Dial(SIP/10, 30) - exten => _0141981242,n,Answer() - exten => _0141981242,n,Voicemail(10@default,u) - exten => _0141981242,n,Hangup() - - It's probably a good idea to create a user in OpenERP dedicated to this task. - This user only needs to be part of the group "Asterisk CallerID", which has - read access on the 'res.partner' object, nothing more. + exten = _0141981242,1,AGI(/usr/local/bin/set_name_incoming_timeout.sh) + same = n,Dial(SIP/10, 30) + same = n,Answer + same = n,Voicemail(10@default,u) + same = n,Hangup + + 2) OUTGOING CALLS + When executed from the dialplan on an outgoing call, it will + lookup in OpenERP the name corresponding to the phone number + that is called by the user and it will update the name of the + callee on the screen of the phone of the caller. + + For that, it uses the CONNECTEDLINE dialplan function of Asterisk + See the following page for more info: + https://wiki.asterisk.org/wiki/display/AST/Manipulating+Party+ID+Information + + It is not possible to set the CONNECTEDLINE directly from an AGI script, + (at least not with Asterisk 11) so the AGI script sets a variable + "connectedlinename" that can then be read from the dialplan and passed + as parameter to the CONNECTEDLINE function. + + Here is the code that I used on the pre-process subroutine + "openerp-out-call" of the Outgoing Call of my Xivo server : + + [openerp-out-call] + exten = s,1,NoOp(X + same = n,AGI(/var/lib/asterisk/agi-bin/set_name_outgoing_timeout.sh) + same = n,Set(CONNECTEDLINE(name,i)=${connectedlinename}) + same = n,Set(CONNECTEDLINE(name-pres,i)=allowed) + same = n,Set(CONNECTEDLINE(num,i)=${XIVO_DSTNUM}) + same = n,Set(CONNECTEDLINE(num-pres)=allowed) + same = n,Return() + + Of course, you should adapt this example to the Asterisk server you are using. - Note that this script can be used without OpenERP, with just the geolocalisation - feature : for that, don't use option --server ; only use --geoloc """ __author__ = "Alexis de Lattre " -__date__ = "July 2014" -__version__ = "0.4" +__date__ = "August 2014" +__version__ = "0.5" # Copyright (C) 2010-2014 Alexis de Lattre # @@ -66,9 +103,9 @@ import sys from optparse import OptionParser -# CID Name that will be displayed if there is no match +# Name that will be displayed if there is no match # and no geolocalisation -default_cid_name = "Not in OpenERP" +not_found_name = "Not in OpenERP" # Define command line options options = [ @@ -79,12 +116,16 @@ options = [ {'names': ('-u', '--user-id'), 'dest': 'user', 'type': 'int', 'help': "OpenERP user ID to use when connecting to OpenERP. Default = 2", 'action': 'store', 'default': 2}, {'names': ('-w', '--password'), 'dest': 'password', 'type': 'string', 'help': "Password of the OpenERP user. Default = 'demo'", 'action': 'store', 'default': 'demo'}, {'names': ('-a', '--ascii'), 'dest': 'ascii', 'help': "Convert name from UTF-8 to ASCII. Default = no, keep UTF-8", 'action': 'store_true', 'default': False}, - {'names': ('-n', '--notify'), 'dest': 'notify', 'help': "Notify OpenERP users via a pop-up (requires the OpenERP module 'asterisk_popup'). If you use this option, you must pass the logins of the OpenERP users to notify as argument to the script. Default = no", 'action': 'store_true', 'default': False}, + {'names': ('-n', '--notify'), 'dest': 'notify', 'help': "Notify OpenERP users via a pop-up (requires the OpenERP module 'base_phone_popup'). If you use this option, you must pass the logins of the OpenERP users to notify as argument to the script. Default = no", 'action': 'store_true', 'default': False}, {'names': ('-g', '--geoloc'), 'dest': 'geoloc', 'help': "Try to geolocate phone numbers unknown to OpenERP. This features requires the 'phonenumbers' Python lib. To install it, run 'sudo pip install phonenumbers' Default = no", 'action': 'store_true', 'default': False}, {'names': ('-l', '--geoloc-lang'), 'dest': 'lang', 'help': "Language in which the name of the country and city name will be displayed by the geolocalisation database. Use the 2 letters ISO code of the language. Default = 'en'", 'action': 'store', 'default': "en"}, {'names': ('-c', '--geoloc-country'), 'dest': 'country', 'help': "2 letters ISO code for your country e.g. 'FR' for France. This will be used by the geolocalisation system to parse the phone number of the calling party. Default = 'FR'", 'action': 'store', 'default': "FR"}, + {'names': ('-o', '--outgoing'), 'dest': 'outgoing', 'help': "Update the Connected Line ID name on outgoing calls via a call to the Asterisk function CONNECTEDLINE(), instead of updating the Caller ID name on incoming calls. Default = no.", 'action': 'store_true', 'default': False}, + {'names': ('-i', '--outgoing-agi-variable'), 'dest': 'outgoing_agi_var', 'help': "Enter the name of the AGI variable (without the 'agi_' prefix) from which the script will get the phone number dialed by the user on outgoing calls. For example, with Xivo, you should specify 'dnid' as the AGI variable. Default = 'extension'", 'action': 'store', 'default': "extension"}, + {'names': ('-m', '--max-size'), 'dest': 'max_size', 'type': 'int', 'help': "If the name has more characters this maximum size, cut it to this maximum size. Default = 40", 'action': 'store', 'default': 40}, ] + def stdout_write(string): '''Wrapper on sys.stdout.write''' sys.stdout.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace')) @@ -92,51 +133,57 @@ def stdout_write(string): # When we output a command, we get an answer "200 result=1" on stdin # Purge stdin to avoid these Asterisk error messages : # utils.c ast_carefulwrite: write() returned error: Broken pipe - input_line = sys.stdin.readline() + sys.stdin.readline() return True + def stderr_write(string): '''Wrapper on sys.stderr.write''' sys.stderr.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace')) sys.stdout.flush() return True + def geolocate_phone_number(number, my_country_code, lang): import phonenumbers import phonenumbers.geocoder res = '' phonenum = phonenumbers.parse(number, my_country_code.upper()) city = phonenumbers.geocoder.description_for_number(phonenum, lang.lower()) - #country = phonenumbers.country_name_for_number(phonenum, lang.lower()) country_code = phonenumbers.region_code_for_number(phonenum) - if country_code == my_country_code.upper(): # We don't display the country name when it's my own country + if country_code == my_country_code.upper(): if city: res = city else: # Convert country code to country name - country = phonenumbers.geocoder._region_display_name(country_code, lang.lower()) + country = phonenumbers.geocoder._region_display_name( + country_code, lang.lower()) if country and city: res = country + ' ' + city elif country and not city: res = country return res + def convert_to_ascii(my_unicode): '''Convert to ascii, with clever management of accents (é -> e, è -> e)''' import unicodedata if isinstance(my_unicode, unicode): - my_unicode_with_ascii_chars_only = ''.join((char for char in unicodedata.normalize('NFD', my_unicode) if unicodedata.category(char) != 'Mn')) + my_unicode_with_ascii_chars_only = ''.join(( + char for char in unicodedata.normalize('NFD', my_unicode) + if unicodedata.category(char) != 'Mn')) return str(my_unicode_with_ascii_chars_only) - # If the argument is already of string type, we return it with the same value + # If the argument is already of string type, return it with the same value elif isinstance(my_unicode, str): return my_unicode else: return False + def main(options, arguments): - #print 'options = %s' % options - #print 'arguments = %s' % arguments + # print 'options = %s' % options + # print 'arguments = %s' % arguments # AGI passes parameters to the script on standard input stdinput = {} @@ -148,8 +195,8 @@ def main(options, arguments): try: variable, value = line.split(':') except: - break - if variable[:4] != 'agi_': # All AGI parameters start with 'agi_' + break + if variable[:4] != 'agi_': # All AGI parameters start with 'agi_' stderr_write("bad stdin variable : %s\n" % variable) continue variable = variable.strip() @@ -161,35 +208,53 @@ def main(options, arguments): for variable in stdinput.keys(): stderr_write("%s = %s\n" % (variable, stdinput.get(variable))) - # If we already have a "True" caller ID name - # i.e. not just digits, but a real name, then we don't try to - # connect to OpenERP or geoloc, we just keep it - if stdinput.get('agi_calleridname') and not stdinput.get('agi_calleridname').isdigit() and stdinput.get('agi_calleridname').lower() not in ['asterisk', 'unknown', 'anonymous']: - stdout_write('VERBOSE "Incoming CallerID name is %s"\n' % stdinput.get('agi_calleridname')) - stdout_write('VERBOSE "As it is a real name, we do not change it"\n') - return True + if options.outgoing: + phone_number = stdinput.get('agi_%s' % options.outgoing_agi_var) + stdout_write('VERBOSE "Dialed phone number is %s"\n' % phone_number) + else: + # If we already have a "True" caller ID name + # i.e. not just digits, but a real name, then we don't try to + # connect to OpenERP or geoloc, we just keep it + if ( + stdinput.get('agi_calleridname') + and not stdinput.get('agi_calleridname').isdigit() + and stdinput.get('agi_calleridname').lower() + not in ['asterisk', 'unknown', 'anonymous']): + stdout_write( + 'VERBOSE "Incoming CallerID name is %s"\n' + % stdinput.get('agi_calleridname')) + stdout_write( + 'VERBOSE "As it is a real name, we do not change it"\n') + return True + + phone_number = stdinput.get('agi_callerid') - input_cid_number = stdinput.get('agi_callerid') stderr_write('stdout encoding = %s\n' % sys.stdout.encoding or 'utf-8') - if not isinstance(input_cid_number, str): - stdout_write('VERBOSE "CallerID number is empty"\n') + if not isinstance(phone_number, str): + stdout_write('VERBOSE "Phone number is empty"\n') exit(0) # Match for particular cases and anonymous phone calls # To test anonymous call in France, dial 3651 + number - if not input_cid_number.isdigit(): - stdout_write('VERBOSE "CallerID number (%s) is not a digit"\n' % input_cid_number) + if not phone_number.isdigit(): + stdout_write( + 'VERBOSE "Phone number (%s) is not a digit"\n' % phone_number) exit(0) - stdout_write('VERBOSE "CallerID number = %s"\n' % input_cid_number) + stdout_write('VERBOSE "Phone number = %s"\n' % phone_number) res = False - if options.server: # Yes, this script can be used without "-s openerp_server" ! + # Yes, this script can be used without "-s openerp_server" ! + if options.server: if options.ssl: - stdout_write('VERBOSE "Starting XML-RPC secure request on OpenERP %s:%s"\n' % (options.server, str(options.port))) + stdout_write( + 'VERBOSE "Starting XML-RPC secure request on OpenERP %s:%s"\n' + % (options.server, str(options.port))) protocol = 'https' else: - stdout_write('VERBOSE "Starting clear XML-RPC request on OpenERP %s:%s"\n' % (options.server, str(options.port))) + stdout_write( + 'VERBOSE "Starting clear XML-RPC request on OpenERP %s:%s"\n' + % (options.server, str(options.port))) protocol = 'http' sock = xmlrpclib.ServerProxy( @@ -201,13 +266,13 @@ def main(options, arguments): res = sock.execute( options.database, options.user, options.password, 'phone.common', 'incall_notify_by_login', - input_cid_number, arguments) + phone_number, arguments) stdout_write('VERBOSE "Calling incall_notify_by_login"\n') else: res = sock.execute( options.database, options.user, options.password, 'phone.common', 'get_name_from_phone_number', - input_cid_number) + phone_number) stdout_write('VERBOSE "Calling get_name_from_phone_number"\n') stdout_write('VERBOSE "End of XML-RPC request on OpenERP"\n') if not res: @@ -216,34 +281,45 @@ def main(options, arguments): stdout_write('VERBOSE "Could not connect to OpenERP"\n') res = False # To simulate a long execution of the XML-RPC request - #import time - #time.sleep(5) + # import time + # time.sleep(5) - # Function to limit the size of the CID name to 40 chars + # Function to limit the size of the name if res: - if len(res) > 40: - res = res[0:40] + if len(res) > options.max_size: + res = res[0:options.max_size] elif options.geoloc: # if the number is not found in OpenERP, we try to geolocate - stdout_write('VERBOSE "Trying to geolocate with country %s and lang %s"\n' % (options.country, options.lang)) - res = geolocate_phone_number(input_cid_number, options.country, options.lang) + stdout_write( + 'VERBOSE "Trying to geolocate with country %s and lang %s"\n' + % (options.country, options.lang)) + res = geolocate_phone_number( + phone_number, options.country, options.lang) else: - # if the number is not found in OpenERP and geoloc is off, we put 'default_cid_name' as CID Name - res = default_cid_name + # if the number is not found in OpenERP and geoloc is off, + # we put 'not_found_name' as Name + res = not_found_name - # All SIP phones should support UTF-8... but in case you have analog phones over TDM + # All SIP phones should support UTF-8... + # but in case you have analog phones over TDM # or buggy phones, you should use the command line option --ascii if options.ascii: res = convert_to_ascii(res) - stdout_write('VERBOSE "CallerID Name = %s"\n' % res) - stdout_write('SET CALLERID "%s"<%s>\n' % (res, input_cid_number)) + stdout_write('VERBOSE "Name = %s"\n' % res) + if options.outgoing: + stdout_write('SET VARIABLE connectedlinename "%s"\n' % res) + else: + stdout_write('SET CALLERID "%s"<%s>\n' % (res, phone_number)) return True if __name__ == '__main__': - usage = "Usage: get_cid_name.py [options] login1 login2 login3 ..." - epilog = "Script written by Alexis de Lattre. Published under the GNU AGPL licence." - description = "This is an AGI script that sends a query to OpenERP. It can also be used without OpenERP as to geolocate phone numbers of incoming calls." + usage = "Usage: get_name_agi.py [options] login1 login2 login3 ..." + epilog = "Script written by Alexis de Lattre. " + "Published under the GNU AGPL licence." + description = "This is an AGI script that sends a query to OpenERP. " + "It can also be used without OpenERP as to geolocate phone numbers " + "of incoming calls." parser = OptionParser(usage=usage, epilog=epilog, description=description) for option in options: param = option['names'] diff --git a/asterisk_click2dial/scripts/get_cid_name_timeout.sh b/asterisk_click2dial/scripts/set_name_incoming_timeout.sh similarity index 62% rename from asterisk_click2dial/scripts/get_cid_name_timeout.sh rename to asterisk_click2dial/scripts/set_name_incoming_timeout.sh index ec59877..9b264f2 100755 --- a/asterisk_click2dial/scripts/get_cid_name_timeout.sh +++ b/asterisk_click2dial/scripts/set_name_incoming_timeout.sh @@ -3,13 +3,13 @@ # # Written by Alexis de Lattre -# Example of wrapper for get_cid_name.py which makes sure that the +# Example of wrapper for set_name_agi.py which makes sure that the # script doesn't take too much time to execute -# Limiting the execution time of get_cid_name.py is important because +# Limiting the execution time of set_name_agi.py is important because # the script is designed to be executed at the beginning of each -# incoming phone call... and if the script get stucks, the phone call -# will also get stucks and you will miss the call ! +# incoming or outgoing phone call... and if the script get stucks, the +# phone call will also get stucks ! # For Debian Lenny and Ubuntu Lucid, you need to install the package "timeout" # For Ubuntu >= Maverick and Debian >= Squeeze, the "timeout" command is shipped @@ -19,11 +19,14 @@ # In this example, we chose 2 seconds. Note that geolocalisation takes about # 1 second on an small machine ; so if you enable the --geoloc option, # don't put a 1 sec timeout ! +# NOTE : with recent version of the phonenumbers lib, the loading time +# is extremely high, about 3 seconds ! I don't know if it's a bug +# or if it will stay like that. # To test this script manually (i.e. outside of Asterisk), run : -# echo "agi_callerid:0141401242"|get_cid_name_timeout.sh +# echo "agi_callerid:0141401242"|set_name_incoming_timeout.sh # where 0141401242 is a phone number that could be presented by the calling party -PATH=/usr/local/sbin:/usr/local/bin:/var/lib/asterisk/agi-bin:/sbin:/bin:/usr/sbin:/usr/bin +PATH=/usr/local/sbin:/usr/local/bin:/var/lib/asterisk/agi-bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/share/asterisk/agi-bin -timeout 2s get_cid_name.py --server openerp.mycompany.com --database erp_prod --user-id 12 --password "thepasswd" --geoloc --geoloc-country "FR" --geoloc-lang "fr" +timeout 2s set_name_agi.py --server openerp.mycompany.com --database erp_prod --user-id 12 --password "thepasswd" --geoloc --geoloc-country "FR" --geoloc-lang "fr" diff --git a/asterisk_click2dial/scripts/set_name_outgoing_timeout.sh b/asterisk_click2dial/scripts/set_name_outgoing_timeout.sh new file mode 100755 index 0000000..47b194b --- /dev/null +++ b/asterisk_click2dial/scripts/set_name_outgoing_timeout.sh @@ -0,0 +1,32 @@ +#! /bin/sh +# -*- encoding: utf-8 -*- +# +# Written by Alexis de Lattre + +# Example of wrapper for set_name_agi.py which makes sure that the +# script doesn't take too much time to execute + +# Limiting the execution time of set_name_agi.py is important because +# the script is designed to be executed at the beginning of each +# incoming or outgoing phone call... and if the script get stucks, the +# phone call will also get stucks ! + +# For Debian Lenny and Ubuntu Lucid, you need to install the package "timeout" +# For Ubuntu >= Maverick and Debian >= Squeeze, the "timeout" command is shipped +# in the "coreutils" package + +# The first argument of the "timeout" command is the maximum execution time +# In this example, we chose 2 seconds. Note that geolocalisation takes about +# 1 second on an small machine ; so if you enable the --geoloc option, +# don't put a 1 sec timeout ! +# NOTE : with recent version of the phonenumbers lib, the loading time +# is extremely high, about 3 seconds ! I don't know if it's a bug +# or if it will stay like that. + +# To test this script manually (i.e. outside of Asterisk), run : +# echo "agi_extension:0141401242"|set_name_outgoing_timeout.sh +# where 0141401242 is a phone number that could be presented by the calling party + +PATH=/usr/local/sbin:/usr/local/bin:/var/lib/asterisk/agi-bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/share/asterisk/agi-bin + +timeout 2s set_name_agi.py --server openerp.mycompany.com --database erp_prod --user-id 12 --password "thepasswd" --outgoing --outgoing-agi-variable extension