You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
15 KiB

  1. #! /usr/bin/python
  2. # -*- encoding: utf-8 -*-
  3. """
  4. Name lookup in OpenERP for incoming and outgoing calls with an
  5. Asterisk IPBX
  6. This script is designed to be used as an AGI on an Asterisk IPBX...
  7. BUT I advise you to use a wrapper around this script to control the
  8. execution time. Why ? Because if the script takes too much time to
  9. execute or get stucks (in the XML-RPC request for example), then the
  10. incoming phone call will also get stucks and you will miss a call !
  11. The simplest solution I found is to use the "timeout" shell command to
  12. call this script, for example :
  13. # timeout 2s get_name_agi.py <OPTIONS>
  14. See my 2 sample wrappers "set_name_incoming_timeout.sh" and
  15. "set_name_outgoing_timeout.sh"
  16. It's probably a good idea to create a user in OpenERP dedicated to this task.
  17. This user only needs to be part of the group "Phone CallerID", which has
  18. read access on the 'res.partner' and other objects with phone numbers and
  19. names.
  20. Note that this script can be used without OpenERP, with just the
  21. geolocalisation feature : for that, don't use option --server ;
  22. only use --geoloc
  23. This script can be used both on incoming and outgoing calls :
  24. 1) INCOMING CALLS
  25. When executed from the dialplan on an incoming phone call, it will
  26. lookup in OpenERP's partners and other objects with phone numbers
  27. (leads, employees, etc...), and, if it finds the phone number, it will
  28. get the corresponding name of the person and use this name as CallerID
  29. name for the incoming call.
  30. Requires the "base_phone" module
  31. available from https://code.launchpad.net/openerp-asterisk-connector
  32. for OpenERP version >= 7.0
  33. Asterisk dialplan example :
  34. [from-extern]
  35. exten = _0141981242,1,AGI(/usr/local/bin/set_name_incoming_timeout.sh)
  36. same = n,Dial(SIP/10, 30)
  37. same = n,Answer
  38. same = n,Voicemail(10@default,u)
  39. same = n,Hangup
  40. 2) OUTGOING CALLS
  41. When executed from the dialplan on an outgoing call, it will
  42. lookup in OpenERP the name corresponding to the phone number
  43. that is called by the user and it will update the name of the
  44. callee on the screen of the phone of the caller.
  45. For that, it uses the CONNECTEDLINE dialplan function of Asterisk
  46. See the following page for more info:
  47. https://wiki.asterisk.org/wiki/display/AST/Manipulating+Party+ID+Information
  48. It is not possible to set the CONNECTEDLINE directly from an AGI script,
  49. (at least not with Asterisk 11) so the AGI script sets a variable
  50. "connectedlinename" that can then be read from the dialplan and passed
  51. as parameter to the CONNECTEDLINE function.
  52. Here is the code that I used on the pre-process subroutine
  53. "openerp-out-call" of the Outgoing Call of my Xivo server :
  54. [openerp-out-call]
  55. exten = s,1,NoOp(X
  56. same = n,AGI(/var/lib/asterisk/agi-bin/set_name_outgoing_timeout.sh)
  57. same = n,Set(CONNECTEDLINE(name,i)=${connectedlinename})
  58. same = n,Set(CONNECTEDLINE(name-pres,i)=allowed)
  59. same = n,Set(CONNECTEDLINE(num,i)=${XIVO_DSTNUM})
  60. same = n,Set(CONNECTEDLINE(num-pres)=allowed)
  61. same = n,Return()
  62. Of course, you should adapt this example to the Asterisk server you are using.
  63. """
  64. __author__ = "Alexis de Lattre <alexis.delattre@akretion.com>"
  65. __date__ = "August 2014"
  66. __version__ = "0.5"
  67. # Copyright (C) 2010-2014 Alexis de Lattre <alexis.delattre@akretion.com>
  68. #
  69. # This program is free software: you can redistribute it and/or modify
  70. # it under the terms of the GNU Affero General Public License as
  71. # published by the Free Software Foundation, either version 3 of the
  72. # License, or (at your option) any later version.
  73. #
  74. # This program is distributed in the hope that it will be useful,
  75. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  76. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  77. # GNU Affero General Public License for more details.
  78. #
  79. # You should have received a copy of the GNU Affero General Public License
  80. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  81. import xmlrpclib
  82. import sys
  83. from optparse import OptionParser
  84. # Name that will be displayed if there is no match
  85. # and no geolocalisation
  86. not_found_name = "Not in OpenERP"
  87. # Define command line options
  88. options = [
  89. {'names': ('-s', '--server'), 'dest': 'server', 'type': 'string', 'help': 'DNS or IP address of the OpenERP server. Default = none (will not try to connect to OpenERP)', 'action': 'store', 'default': False},
  90. {'names': ('-p', '--port'), 'dest': 'port', 'type': 'int', 'help': "Port of OpenERP's XML-RPC interface. Default = 8069", 'action': 'store', 'default': 8069},
  91. {'names': ('-e', '--ssl'), 'dest': 'ssl', 'help': "Use XML-RPC secure i.e. with SSL instead of clear XML-RPC. Default = no, use clear XML-RPC", 'action': 'store_true', 'default': False},
  92. {'names': ('-d', '--database'), 'dest': 'database', 'type': 'string', 'help': "OpenERP database name. Default = 'openerp'", 'action': 'store', 'default': 'openerp'},
  93. {'names': ('-u', '--user-id'), 'dest': 'user', 'type': 'int', 'help': "OpenERP user ID to use when connecting to OpenERP. Default = 2", 'action': 'store', 'default': 2},
  94. {'names': ('-w', '--password'), 'dest': 'password', 'type': 'string', 'help': "Password of the OpenERP user. Default = 'demo'", 'action': 'store', 'default': 'demo'},
  95. {'names': ('-a', '--ascii'), 'dest': 'ascii', 'help': "Convert name from UTF-8 to ASCII. Default = no, keep UTF-8", 'action': 'store_true', 'default': False},
  96. {'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},
  97. {'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},
  98. {'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"},
  99. {'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"},
  100. {'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},
  101. {'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"},
  102. {'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},
  103. ]
  104. def stdout_write(string):
  105. '''Wrapper on sys.stdout.write'''
  106. sys.stdout.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  107. sys.stdout.flush()
  108. # When we output a command, we get an answer "200 result=1" on stdin
  109. # Purge stdin to avoid these Asterisk error messages :
  110. # utils.c ast_carefulwrite: write() returned error: Broken pipe
  111. sys.stdin.readline()
  112. return True
  113. def stderr_write(string):
  114. '''Wrapper on sys.stderr.write'''
  115. sys.stderr.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  116. sys.stdout.flush()
  117. return True
  118. def geolocate_phone_number(number, my_country_code, lang):
  119. import phonenumbers
  120. import phonenumbers.geocoder
  121. res = ''
  122. phonenum = phonenumbers.parse(number, my_country_code.upper())
  123. city = phonenumbers.geocoder.description_for_number(phonenum, lang.lower())
  124. country_code = phonenumbers.region_code_for_number(phonenum)
  125. # We don't display the country name when it's my own country
  126. if country_code == my_country_code.upper():
  127. if city:
  128. res = city
  129. else:
  130. # Convert country code to country name
  131. country = phonenumbers.geocoder._region_display_name(
  132. country_code, lang.lower())
  133. if country and city:
  134. res = country + ' ' + city
  135. elif country and not city:
  136. res = country
  137. return res
  138. def convert_to_ascii(my_unicode):
  139. '''Convert to ascii, with clever management of accents (é -> e, è -> e)'''
  140. import unicodedata
  141. if isinstance(my_unicode, unicode):
  142. my_unicode_with_ascii_chars_only = ''.join((
  143. char for char in unicodedata.normalize('NFD', my_unicode)
  144. if unicodedata.category(char) != 'Mn'))
  145. return str(my_unicode_with_ascii_chars_only)
  146. # If the argument is already of string type, return it with the same value
  147. elif isinstance(my_unicode, str):
  148. return my_unicode
  149. else:
  150. return False
  151. def main(options, arguments):
  152. # print 'options = %s' % options
  153. # print 'arguments = %s' % arguments
  154. # AGI passes parameters to the script on standard input
  155. stdinput = {}
  156. while 1:
  157. input_line = sys.stdin.readline()
  158. if not input_line:
  159. break
  160. line = input_line.strip()
  161. try:
  162. variable, value = line.split(':')
  163. except:
  164. break
  165. if variable[:4] != 'agi_': # All AGI parameters start with 'agi_'
  166. stderr_write("bad stdin variable : %s\n" % variable)
  167. continue
  168. variable = variable.strip()
  169. value = value.strip()
  170. if variable and value:
  171. stdinput[variable] = value
  172. stderr_write("full AGI environnement :\n")
  173. for variable in stdinput.keys():
  174. stderr_write("%s = %s\n" % (variable, stdinput.get(variable)))
  175. if options.outgoing:
  176. phone_number = stdinput.get('agi_%s' % options.outgoing_agi_var)
  177. stdout_write('VERBOSE "Dialed phone number is %s"\n' % phone_number)
  178. else:
  179. # If we already have a "True" caller ID name
  180. # i.e. not just digits, but a real name, then we don't try to
  181. # connect to OpenERP or geoloc, we just keep it
  182. if (
  183. stdinput.get('agi_calleridname')
  184. and not stdinput.get('agi_calleridname').isdigit()
  185. and stdinput.get('agi_calleridname').lower()
  186. not in ['asterisk', 'unknown', 'anonymous']):
  187. stdout_write(
  188. 'VERBOSE "Incoming CallerID name is %s"\n'
  189. % stdinput.get('agi_calleridname'))
  190. stdout_write(
  191. 'VERBOSE "As it is a real name, we do not change it"\n')
  192. return True
  193. phone_number = stdinput.get('agi_callerid')
  194. stderr_write('stdout encoding = %s\n' % sys.stdout.encoding or 'utf-8')
  195. if not isinstance(phone_number, str):
  196. stdout_write('VERBOSE "Phone number is empty"\n')
  197. exit(0)
  198. # Match for particular cases and anonymous phone calls
  199. # To test anonymous call in France, dial 3651 + number
  200. if not phone_number.isdigit():
  201. stdout_write(
  202. 'VERBOSE "Phone number (%s) is not a digit"\n' % phone_number)
  203. exit(0)
  204. stdout_write('VERBOSE "Phone number = %s"\n' % phone_number)
  205. res = False
  206. # Yes, this script can be used without "-s openerp_server" !
  207. if options.server:
  208. if options.ssl:
  209. stdout_write(
  210. 'VERBOSE "Starting XML-RPC secure request on OpenERP %s:%s"\n'
  211. % (options.server, str(options.port)))
  212. protocol = 'https'
  213. else:
  214. stdout_write(
  215. 'VERBOSE "Starting clear XML-RPC request on OpenERP %s:%s"\n'
  216. % (options.server, str(options.port)))
  217. protocol = 'http'
  218. sock = xmlrpclib.ServerProxy(
  219. '%s://%s:%s/xmlrpc/object'
  220. % (protocol, options.server, str(options.port)))
  221. try:
  222. if options.notify and arguments:
  223. res = sock.execute(
  224. options.database, options.user, options.password,
  225. 'phone.common', 'incall_notify_by_login',
  226. phone_number, arguments)
  227. stdout_write('VERBOSE "Calling incall_notify_by_login"\n')
  228. else:
  229. res = sock.execute(
  230. options.database, options.user, options.password,
  231. 'phone.common', 'get_name_from_phone_number',
  232. phone_number)
  233. stdout_write('VERBOSE "Calling get_name_from_phone_number"\n')
  234. stdout_write('VERBOSE "End of XML-RPC request on OpenERP"\n')
  235. if not res:
  236. stdout_write('VERBOSE "Phone number not found in OpenERP"\n')
  237. except:
  238. stdout_write('VERBOSE "Could not connect to OpenERP"\n')
  239. res = False
  240. # To simulate a long execution of the XML-RPC request
  241. # import time
  242. # time.sleep(5)
  243. # Function to limit the size of the name
  244. if res:
  245. if len(res) > options.max_size:
  246. res = res[0:options.max_size]
  247. elif options.geoloc:
  248. # if the number is not found in OpenERP, we try to geolocate
  249. stdout_write(
  250. 'VERBOSE "Trying to geolocate with country %s and lang %s"\n'
  251. % (options.country, options.lang))
  252. res = geolocate_phone_number(
  253. phone_number, options.country, options.lang)
  254. else:
  255. # if the number is not found in OpenERP and geoloc is off,
  256. # we put 'not_found_name' as Name
  257. res = not_found_name
  258. # All SIP phones should support UTF-8...
  259. # but in case you have analog phones over TDM
  260. # or buggy phones, you should use the command line option --ascii
  261. if options.ascii:
  262. res = convert_to_ascii(res)
  263. stdout_write('VERBOSE "Name = %s"\n' % res)
  264. if options.outgoing:
  265. stdout_write('SET VARIABLE connectedlinename "%s"\n' % res)
  266. else:
  267. stdout_write('SET CALLERID "%s"<%s>\n' % (res, phone_number))
  268. return True
  269. if __name__ == '__main__':
  270. usage = "Usage: get_name_agi.py [options] login1 login2 login3 ..."
  271. epilog = "Script written by Alexis de Lattre. "
  272. "Published under the GNU AGPL licence."
  273. description = "This is an AGI script that sends a query to OpenERP. "
  274. "It can also be used without OpenERP as to geolocate phone numbers "
  275. "of incoming calls."
  276. parser = OptionParser(usage=usage, epilog=epilog, description=description)
  277. for option in options:
  278. param = option['names']
  279. del option['names']
  280. parser.add_option(*param, **option)
  281. options, arguments = parser.parse_args()
  282. sys.argv[:] = arguments
  283. main(options, arguments)