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.

376 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',
  89. 'action': 'store', 'default': False,
  90. 'help': 'DNS or IP address of the OpenERP server. Default = none '
  91. '(will not try to connect to OpenERP)'},
  92. {'names': ('-p', '--port'), 'dest': 'port', 'type': 'int',
  93. 'action': 'store', 'default': 8069,
  94. 'help': "Port of OpenERP's XML-RPC interface. Default = 8069"},
  95. {'names': ('-e', '--ssl'), 'dest': 'ssl',
  96. 'help': "Use XML-RPC secure i.e. with SSL instead of clear XML-RPC. "
  97. "Default = no, use clear XML-RPC",
  98. 'action': 'store_true', 'default': False},
  99. {'names': ('-d', '--database'), 'dest': 'database', 'type': 'string',
  100. 'action': 'store', 'default': 'openerp',
  101. 'help': "OpenERP database name. Default = 'openerp'"},
  102. {'names': ('-u', '--user-id'), 'dest': 'user', 'type': 'int',
  103. 'action': 'store', 'default': 2,
  104. 'help': "OpenERP user ID to use when connecting to OpenERP. "
  105. "Default = 2"},
  106. {'names': ('-w', '--password'), 'dest': 'password', 'type': 'string',
  107. 'action': 'store', 'default': 'demo',
  108. 'help': "Password of the OpenERP user. Default = 'demo'"},
  109. {'names': ('-a', '--ascii'), 'dest': 'ascii',
  110. 'action': 'store_true', 'default': False,
  111. 'help': "Convert name from UTF-8 to ASCII. Default = no, keep UTF-8"},
  112. {'names': ('-n', '--notify'), 'dest': 'notify',
  113. 'action': 'store_true', 'default': False,
  114. 'help': "Notify OpenERP users via a pop-up (requires the OpenERP "
  115. "module 'base_phone_popup'). If you use this option, you must pass "
  116. "the logins of the OpenERP users to notify as argument to the "
  117. "script. Default = no"},
  118. {'names': ('-g', '--geoloc'), 'dest': 'geoloc',
  119. 'action': 'store_true', 'default': False,
  120. 'help': "Try to geolocate phone numbers unknown to OpenERP. This "
  121. "features requires the 'phonenumbers' Python lib. To install it, "
  122. "run 'sudo pip install phonenumbers' Default = no"},
  123. {'names': ('-l', '--geoloc-lang'), 'dest': 'lang', 'type': 'string',
  124. 'action': 'store', 'default': "en",
  125. 'help': "Language in which the name of the country and city name "
  126. "will be displayed by the geolocalisation database. Use the 2 "
  127. "letters ISO code of the language. Default = 'en'"},
  128. {'names': ('-c', '--geoloc-country'), 'dest': 'country', 'type': 'string',
  129. 'action': 'store', 'default': "FR",
  130. 'help': "2 letters ISO code for your country e.g. 'FR' for France. "
  131. "This will be used by the geolocalisation system to parse the phone "
  132. "number of the calling party. Default = 'FR'"},
  133. {'names': ('-o', '--outgoing'), 'dest': 'outgoing',
  134. 'action': 'store_true', 'default': False,
  135. 'help': "Update the Connected Line ID name on outgoing calls via a "
  136. "call to the Asterisk function CONNECTEDLINE(), instead of updating "
  137. "the Caller ID name on incoming calls. Default = no."},
  138. {'names': ('-i', '--outgoing-agi-variable'), 'dest': 'outgoing_agi_var',
  139. 'type': 'string', 'action': 'store', 'default': "extension",
  140. 'help': "Enter the name of the AGI variable (without the 'agi_' "
  141. "prefix) from which the script will get the phone number dialed by "
  142. "the user on outgoing calls. For example, with Xivo, you should "
  143. "specify 'dnid' as the AGI variable. Default = 'extension'"},
  144. {'names': ('-m', '--max-size'), 'dest': 'max_size', 'type': 'int',
  145. 'action': 'store', 'default': 40,
  146. 'help': "If the name has more characters this maximum size, cut it "
  147. "to this maximum size. Default = 40"},
  148. ]
  149. def stdout_write(string):
  150. '''Wrapper on sys.stdout.write'''
  151. sys.stdout.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  152. sys.stdout.flush()
  153. # When we output a command, we get an answer "200 result=1" on stdin
  154. # Purge stdin to avoid these Asterisk error messages :
  155. # utils.c ast_carefulwrite: write() returned error: Broken pipe
  156. sys.stdin.readline()
  157. return True
  158. def stderr_write(string):
  159. '''Wrapper on sys.stderr.write'''
  160. sys.stderr.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  161. sys.stdout.flush()
  162. return True
  163. def geolocate_phone_number(number, my_country_code, lang):
  164. import phonenumbers
  165. import phonenumbers.geocoder
  166. res = ''
  167. phonenum = phonenumbers.parse(number, my_country_code.upper())
  168. city = phonenumbers.geocoder.description_for_number(phonenum, lang.lower())
  169. country_code = phonenumbers.region_code_for_number(phonenum)
  170. # We don't display the country name when it's my own country
  171. if country_code == my_country_code.upper():
  172. if city:
  173. res = city
  174. else:
  175. # Convert country code to country name
  176. country = phonenumbers.geocoder._region_display_name(
  177. country_code, lang.lower())
  178. if country and city:
  179. res = country + ' ' + city
  180. elif country and not city:
  181. res = country
  182. return res
  183. def convert_to_ascii(my_unicode):
  184. '''Convert to ascii, with clever management of accents (é -> e, è -> e)'''
  185. import unicodedata
  186. if isinstance(my_unicode, unicode):
  187. my_unicode_with_ascii_chars_only = ''.join((
  188. char for char in unicodedata.normalize('NFD', my_unicode)
  189. if unicodedata.category(char) != 'Mn'))
  190. return str(my_unicode_with_ascii_chars_only)
  191. # If the argument is already of string type, return it with the same value
  192. elif isinstance(my_unicode, str):
  193. return my_unicode
  194. else:
  195. return False
  196. def main(options, arguments):
  197. # print 'options = %s' % options
  198. # print 'arguments = %s' % arguments
  199. # AGI passes parameters to the script on standard input
  200. stdinput = {}
  201. while 1:
  202. input_line = sys.stdin.readline()
  203. if not input_line:
  204. break
  205. line = input_line.strip()
  206. try:
  207. variable, value = line.split(':')
  208. except:
  209. break
  210. if variable[:4] != 'agi_': # All AGI parameters start with 'agi_'
  211. stderr_write("bad stdin variable : %s\n" % variable)
  212. continue
  213. variable = variable.strip()
  214. value = value.strip()
  215. if variable and value:
  216. stdinput[variable] = value
  217. stderr_write("full AGI environnement :\n")
  218. for variable in stdinput.keys():
  219. stderr_write("%s = %s\n" % (variable, stdinput.get(variable)))
  220. if options.outgoing:
  221. phone_number = stdinput.get('agi_%s' % options.outgoing_agi_var)
  222. stdout_write('VERBOSE "Dialed phone number is %s"\n' % phone_number)
  223. else:
  224. # If we already have a "True" caller ID name
  225. # i.e. not just digits, but a real name, then we don't try to
  226. # connect to OpenERP or geoloc, we just keep it
  227. if (
  228. stdinput.get('agi_calleridname')
  229. and not stdinput.get('agi_calleridname').isdigit()
  230. and stdinput.get('agi_calleridname').lower()
  231. not in ['asterisk', 'unknown', 'anonymous']
  232. and not options.notify):
  233. stdout_write(
  234. 'VERBOSE "Incoming CallerID name is %s"\n'
  235. % stdinput.get('agi_calleridname'))
  236. stdout_write(
  237. 'VERBOSE "As it is a real name, we do not change it"\n')
  238. return True
  239. phone_number = stdinput.get('agi_callerid')
  240. stderr_write('stdout encoding = %s\n' % sys.stdout.encoding or 'utf-8')
  241. if not isinstance(phone_number, str):
  242. stdout_write('VERBOSE "Phone number is empty"\n')
  243. exit(0)
  244. # Match for particular cases and anonymous phone calls
  245. # To test anonymous call in France, dial 3651 + number
  246. if not phone_number.isdigit():
  247. stdout_write(
  248. 'VERBOSE "Phone number (%s) is not a digit"\n' % phone_number)
  249. exit(0)
  250. stdout_write('VERBOSE "Phone number = %s"\n' % phone_number)
  251. res = False
  252. # Yes, this script can be used without "-s openerp_server" !
  253. if options.server:
  254. if options.ssl:
  255. stdout_write(
  256. 'VERBOSE "Starting XML-RPC secure request on OpenERP %s:%s"\n'
  257. % (options.server, str(options.port)))
  258. protocol = 'https'
  259. else:
  260. stdout_write(
  261. 'VERBOSE "Starting clear XML-RPC request on OpenERP %s:%s"\n'
  262. % (options.server, str(options.port)))
  263. protocol = 'http'
  264. sock = xmlrpclib.ServerProxy(
  265. '%s://%s:%s/xmlrpc/object'
  266. % (protocol, options.server, str(options.port)))
  267. try:
  268. if options.notify and arguments:
  269. res = sock.execute(
  270. options.database, options.user, options.password,
  271. 'phone.common', 'incall_notify_by_login',
  272. phone_number, arguments)
  273. stdout_write('VERBOSE "Calling incall_notify_by_login"\n')
  274. else:
  275. res = sock.execute(
  276. options.database, options.user, options.password,
  277. 'phone.common', 'get_name_from_phone_number',
  278. phone_number)
  279. stdout_write('VERBOSE "Calling get_name_from_phone_number"\n')
  280. stdout_write('VERBOSE "End of XML-RPC request on OpenERP"\n')
  281. if not res:
  282. stdout_write('VERBOSE "Phone number not found in OpenERP"\n')
  283. except:
  284. stdout_write('VERBOSE "Could not connect to OpenERP"\n')
  285. res = False
  286. # To simulate a long execution of the XML-RPC request
  287. # import time
  288. # time.sleep(5)
  289. # Function to limit the size of the name
  290. if res:
  291. if len(res) > options.max_size:
  292. res = res[0:options.max_size]
  293. elif options.geoloc:
  294. # if the number is not found in OpenERP, we try to geolocate
  295. stdout_write(
  296. 'VERBOSE "Trying to geolocate with country %s and lang %s"\n'
  297. % (options.country, options.lang))
  298. res = geolocate_phone_number(
  299. phone_number, options.country, options.lang)
  300. else:
  301. # if the number is not found in OpenERP and geoloc is off,
  302. # we put 'not_found_name' as Name
  303. res = not_found_name
  304. # All SIP phones should support UTF-8...
  305. # but in case you have analog phones over TDM
  306. # or buggy phones, you should use the command line option --ascii
  307. if options.ascii:
  308. res = convert_to_ascii(res)
  309. stdout_write('VERBOSE "Name = %s"\n' % res)
  310. if options.outgoing:
  311. stdout_write('SET VARIABLE connectedlinename "%s"\n' % res)
  312. else:
  313. stdout_write('SET CALLERID "%s"<%s>\n' % (res, phone_number))
  314. return True
  315. if __name__ == '__main__':
  316. usage = "Usage: get_name_agi.py [options] login1 login2 login3 ..."
  317. epilog = "Script written by Alexis de Lattre. "
  318. "Published under the GNU AGPL licence."
  319. description = "This is an AGI script that sends a query to OpenERP. "
  320. "It can also be used without OpenERP as to geolocate phone numbers "
  321. "of incoming calls."
  322. parser = OptionParser(usage=usage, epilog=epilog, description=description)
  323. for option in options:
  324. param = option['names']
  325. del option['names']
  326. parser.add_option(*param, **option)
  327. options, arguments = parser.parse_args()
  328. sys.argv[:] = arguments
  329. main(options, arguments)