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.

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