@ -1,17 +1,8 @@
#! /usr/bin/python
#! /usr/bin/python
# -*- encoding: utf-8 -*-
# -*- 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 . . .
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
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
The simplest solution I found is to use the " timeout " shell command to
call this script , for example :
call this script , for example :
# timeout 2s get_cid_ name.py <OPTIONS>
# timeout 2s get_name_agi .py <OPTIONS>
See my sample wrapper " get_cid_name_timeout.sh "
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 .
Requires the " base_phone " module
available from https : / / code . launchpad . net / openerp - asterisk - connector
for OpenERP version > = 7.0
Asterisk dialplan example :
Asterisk dialplan example :
[ from - extern ]
[ 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 ( )
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 .
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 .
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 <alexis.delattre@akretion.com> "
__author__ = " Alexis de Lattre <alexis.delattre@akretion.com> "
__date__ = " July 2014 "
__version__ = " 0.4 "
__date__ = " August 2014 "
__version__ = " 0.5 "
# Copyright (C) 2010-2014 Alexis de Lattre <alexis.delattre@akretion.com>
# Copyright (C) 2010-2014 Alexis de Lattre <alexis.delattre@akretion.com>
#
#
@ -66,9 +103,9 @@ import sys
from optparse import OptionParser
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
# and no geolocalisation
default_ci d_name = " Not in OpenERP "
not_foun d_name = " Not in OpenERP "
# Define command line options
# Define command line options
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 ' : ( ' -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 ' : ( ' -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 ' : ( ' -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 ' : ( ' -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 ' : ( ' -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 ' : ( ' -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 ) :
def stdout_write ( string ) :
''' Wrapper on sys.stdout.write '''
''' Wrapper on sys.stdout.write '''
sys . stdout . write ( string . encode ( sys . stdout . encoding or ' utf-8 ' , ' replace ' ) )
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
# When we output a command, we get an answer "200 result=1" on stdin
# Purge stdin to avoid these Asterisk error messages :
# Purge stdin to avoid these Asterisk error messages :
# utils.c ast_carefulwrite: write() returned error: Broken pipe
# utils.c ast_carefulwrite: write() returned error: Broken pipe
input_line = sys . stdin . readline ( )
sys . stdin . readline ( )
return True
return True
def stderr_write ( string ) :
def stderr_write ( string ) :
''' Wrapper on sys.stderr.write '''
''' Wrapper on sys.stderr.write '''
sys . stderr . write ( string . encode ( sys . stdout . encoding or ' utf-8 ' , ' replace ' ) )
sys . stderr . write ( string . encode ( sys . stdout . encoding or ' utf-8 ' , ' replace ' ) )
sys . stdout . flush ( )
sys . stdout . flush ( )
return True
return True
def geolocate_phone_number ( number , my_country_code , lang ) :
def geolocate_phone_number ( number , my_country_code , lang ) :
import phonenumbers
import phonenumbers
import phonenumbers.geocoder
import phonenumbers.geocoder
res = ' '
res = ' '
phonenum = phonenumbers . parse ( number , my_country_code . upper ( ) )
phonenum = phonenumbers . parse ( number , my_country_code . upper ( ) )
city = phonenumbers . geocoder . description_for_number ( phonenum , lang . lower ( ) )
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 )
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
# We don't display the country name when it's my own country
if country_code == my_country_code . upper ( ) :
if city :
if city :
res = city
res = city
else :
else :
# Convert country code to country name
# 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 :
if country and city :
res = country + ' ' + city
res = country + ' ' + city
elif country and not city :
elif country and not city :
res = country
res = country
return res
return res
def convert_to_ascii ( my_unicode ) :
def convert_to_ascii ( my_unicode ) :
''' Convert to ascii, with clever management of accents (é -> e, è -> e) '''
''' Convert to ascii, with clever management of accents (é -> e, è -> e) '''
import unicodedata
import unicodedata
if isinstance ( my_unicode , unicode ) :
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 )
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 ) :
elif isinstance ( my_unicode , str ) :
return my_unicode
return my_unicode
else :
else :
return False
return False
def main ( options , arguments ) :
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
# AGI passes parameters to the script on standard input
stdinput = { }
stdinput = { }
@ -161,35 +208,53 @@ def main(options, arguments):
for variable in stdinput . keys ( ) :
for variable in stdinput . keys ( ) :
stderr_write ( " %s = %s \n " % ( variable , stdinput . get ( variable ) ) )
stderr_write ( " %s = %s \n " % ( variable , stdinput . get ( variable ) ) )
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
# If we already have a "True" caller ID name
# i.e. not just digits, but a real name, then we don't try to
# i.e. not just digits, but a real name, then we don't try to
# connect to OpenERP or geoloc, we just keep it
# 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 ' )
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
return True
input_cid_number = stdinput . get ( ' agi_callerid ' )
phone_number = stdinput . get ( ' agi_callerid ' )
stderr_write ( ' stdout encoding = %s \n ' % sys . stdout . encoding or ' utf-8 ' )
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 )
exit ( 0 )
# Match for particular cases and anonymous phone calls
# Match for particular cases and anonymous phone calls
# To test anonymous call in France, dial 3651 + number
# 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 )
exit ( 0 )
stdout_write ( ' VERBOSE " CallerID number = %s " \n ' % input_cid _number)
stdout_write ( ' VERBOSE " Phone number = %s " \n ' % phone _number)
res = False
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 :
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 '
protocol = ' https '
else :
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 '
protocol = ' http '
sock = xmlrpclib . ServerProxy (
sock = xmlrpclib . ServerProxy (
@ -201,13 +266,13 @@ def main(options, arguments):
res = sock . execute (
res = sock . execute (
options . database , options . user , options . password ,
options . database , options . user , options . password ,
' phone.common ' , ' incall_notify_by_login ' ,
' phone.common ' , ' incall_notify_by_login ' ,
input_cid _number, arguments )
phone _number, arguments )
stdout_write ( ' VERBOSE " Calling incall_notify_by_login " \n ' )
stdout_write ( ' VERBOSE " Calling incall_notify_by_login " \n ' )
else :
else :
res = sock . execute (
res = sock . execute (
options . database , options . user , options . password ,
options . database , options . user , options . password ,
' phone.common ' , ' get_name_from_phone_number ' ,
' 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 " Calling get_name_from_phone_number " \n ' )
stdout_write ( ' VERBOSE " End of XML-RPC request on OpenERP " \n ' )
stdout_write ( ' VERBOSE " End of XML-RPC request on OpenERP " \n ' )
if not res :
if not res :
@ -216,34 +281,45 @@ def main(options, arguments):
stdout_write ( ' VERBOSE " Could not connect to OpenERP " \n ' )
stdout_write ( ' VERBOSE " Could not connect to OpenERP " \n ' )
res = False
res = False
# To simulate a long execution of the XML-RPC request
# 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 res :
if len ( res ) > 40 :
res = res [ 0 : 40 ]
if len ( res ) > options . max_size :
res = res [ 0 : options . max_size ]
elif options . geoloc :
elif options . geoloc :
# if the number is not found in OpenERP, we try to geolocate
# 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 :
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
# or buggy phones, you should use the command line option --ascii
if options . ascii :
if options . ascii :
res = convert_to_ascii ( res )
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
return True
if __name__ == ' __main__ ' :
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 )
parser = OptionParser ( usage = usage , epilog = epilog , description = description )
for option in options :
for option in options :
param = option [ ' names ' ]
param = option [ ' names ' ]