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.

364 lines
12 KiB

10 years ago
10 years ago
10 years ago
12 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. #! /usr/bin/python
  2. # -*- encoding: utf-8 -*-
  3. """
  4. CallerID name lookup in OpenERP for Asterisk IPBX
  5. When executed from the dialplan on an incoming phone call, it will lookup in
  6. OpenERP's partners, and, if it finds the phone number, it will get the
  7. corresponding name of the person and use this name as CallerID name for the
  8. incoming call.
  9. Requires the "asterisk_click2dial" module
  10. available from https://code.launchpad.net/openerp-asterisk-connector
  11. for OpenERP version >= 5.0
  12. This script is designed to be used as an AGI on an Asterisk IPBX...
  13. BUT I advise you to use a wrapper around this script to control the
  14. execution time. Why ? Because if the script takes too much time to
  15. execute or get stucks (in the XML-RPC request for example), then the
  16. incoming phone call will also get stucks and you will miss a call !
  17. The simplest solution I found is to use the "timeout" shell command to
  18. call this script, for example :
  19. # timeout 1s get_cid_name.py <OPTIONS>
  20. See my sample wrapper "get_cid_name_timeout.sh"
  21. Asterisk dialplan example :
  22. [from-extern]
  23. exten => _0141981242,1,AGI(/usr/local/bin/get_cid_name_timeout.sh)
  24. exten => _0141981242,n,Dial(SIP/10, 30)
  25. exten => _0141981242,n,Answer()
  26. exten => _0141981242,n,Voicemail(10@default,u)
  27. exten => _0141981242,n,Hangup()
  28. It's probably a good idea to create a user in OpenERP dedicated to this task.
  29. This user only needs to be part of the group "Asterisk CallerID", which has
  30. read access on the 'res.partner.address' object, nothing more.
  31. Note that this script can be used without OpenERP, with just the
  32. geolocalisation feature : for that, don't use option --server ; only use
  33. --geoloc
  34. """
  35. __author__ = "Alexis de Lattre <alexis.delattre@akretion.com>"
  36. __date__ = "December 2010"
  37. __version__ = "0.3"
  38. # Copyright (C) 2010-2012 Alexis de Lattre <alexis.delattre@akretion.com>
  39. #
  40. # This program is free software: you can redistribute it and/or modify
  41. # it under the terms of the GNU Affero General Public License as
  42. # published by the Free Software Foundation, either version 3 of the
  43. # License, or (at your option) any later version.
  44. #
  45. # This program is distributed in the hope that it will be useful,
  46. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  47. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  48. # GNU Affero General Public License for more details.
  49. #
  50. # You should have received a copy of the GNU Affero General Public License
  51. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  52. import xmlrpclib
  53. import sys
  54. from optparse import OptionParser
  55. # CID Name that will be displayed if there is no match in res.partner.address
  56. # and no geolocalisation
  57. default_cid_name = "Not in OpenERP"
  58. # Define command line options
  59. option_server = {
  60. 'names': ('-s', '--server'),
  61. 'dest': 'server',
  62. 'type': 'string',
  63. 'help': 'DNS or IP address of the OpenERP server. Default = none '
  64. '(will not try to connect to OpenERP)',
  65. 'action': 'store',
  66. 'default': False,
  67. }
  68. option_port = {
  69. 'names': ('-p', '--port'),
  70. 'dest': 'port',
  71. 'type': 'int',
  72. 'help': "Port of OpenERP's XML-RPC interface. Default = 8069",
  73. 'action': 'store',
  74. 'default': 8069,
  75. }
  76. option_ssl = {
  77. 'names': ('-e', '--ssl'),
  78. 'dest': 'ssl',
  79. 'help': "Use XML-RPC secure i.e. with SSL instead of clear XML-RPC. "
  80. "Default = no, use clear XML-RPC",
  81. 'action': 'store_true',
  82. 'default': False,
  83. }
  84. option_database = {
  85. 'names': ('-d', '--database'),
  86. 'dest': 'database',
  87. 'type': 'string',
  88. 'help': "OpenERP database name. Default = 'openerp'",
  89. 'action': 'store',
  90. 'default': 'openerp',
  91. }
  92. option_user = {
  93. 'names': ('-u', '--user-id'),
  94. 'dest': 'user',
  95. 'type': 'int',
  96. 'help': "OpenERP user ID to use when connecting to OpenERP. Default = 2",
  97. 'action': 'store',
  98. 'default': 2,
  99. }
  100. option_password = {
  101. 'names': ('-w', '--password'),
  102. 'dest': 'password',
  103. 'type': 'string',
  104. 'help': "Password of the OpenERP user. Default = 'demo'",
  105. 'action': 'store',
  106. 'default': 'demo',
  107. }
  108. option_ascii = {
  109. 'names': ('-a', '--ascii'),
  110. 'dest': 'ascii',
  111. 'help': "Convert name from UTF-8 to ASCII. Default = no, keep UTF-8",
  112. 'action': 'store_true',
  113. 'default': False,
  114. }
  115. option_geoloc = {
  116. 'names': ('-g', '--geoloc'),
  117. 'dest': 'geoloc',
  118. 'help': "Try to geolocate phone numbers unknown to OpenERP. This features "
  119. "requires the 'phonenumbers' Python lib. To install it, run "
  120. "'sudo pip install phonenumbers' Default = no",
  121. 'action': 'store_true',
  122. 'default': False,
  123. }
  124. option_geoloc_lang = {
  125. 'names': ('-l', '--geoloc-lang'),
  126. 'dest': 'lang',
  127. 'help': "Language in which the name of the country and city name will be "
  128. "displayed by the geolocalisation database. Use the 2 letters ISO "
  129. "code of the language. Default = 'en'",
  130. 'action': 'store',
  131. 'default': "en",
  132. }
  133. option_geoloc_country = {
  134. 'names': ('-c', '--geoloc-country'),
  135. 'dest': 'country',
  136. 'help': "2 letters ISO code for your country e.g. 'FR' for France. This "
  137. "will be used by the geolocalisation system to parse the phone "
  138. "number of the calling party. Default = 'FR'",
  139. 'action': 'store',
  140. 'default': "FR",
  141. }
  142. options = [
  143. option_server,
  144. option_port,
  145. option_ssl,
  146. option_database,
  147. option_user,
  148. option_password,
  149. option_ascii,
  150. option_geoloc,
  151. option_geoloc_lang,
  152. option_geoloc_country,
  153. ]
  154. def stdout_write(string):
  155. '''Wrapper on sys.stdout.write'''
  156. sys.stdout.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  157. sys.stdout.flush()
  158. # When we output a command, we get an answer "200 result=1" on stdin
  159. # Purge stdin to avoid these Asterisk error messages :
  160. # utils.c ast_carefulwrite: write() returned error: Broken pipe
  161. sys.stdin.readline()
  162. return True
  163. def stderr_write(string):
  164. '''Wrapper on sys.stderr.write'''
  165. sys.stderr.write(string.encode(sys.stdout.encoding or 'utf-8', 'replace'))
  166. sys.stdout.flush()
  167. return True
  168. def geolocate_phone_number(number, my_country_code, lang):
  169. import phonenumbers
  170. import phonenumbers.geocoder
  171. res = ''
  172. phonenum = phonenumbers.parse(number, my_country_code.upper())
  173. city = phonenumbers.area_description_for_number(phonenum, lang.lower())
  174. # country = phonenumbers.country_name_for_number(phonenum, lang.lower())
  175. country_code = phonenumbers.region_code_for_number(phonenum)
  176. if country_code == my_country_code.upper():
  177. # We don't display the country name when it's my own country
  178. if city:
  179. res = city
  180. else:
  181. # Convert country code to country name
  182. country = phonenumbers.geocoder._region_display_name(
  183. country_code, lang.lower()
  184. )
  185. if country and city:
  186. res = country + ' ' + city
  187. elif country and not city:
  188. res = country
  189. return res
  190. def convert_to_ascii(my_unicode):
  191. """Convert to ascii, with clever management of accents (é -> e, è -> e)"""
  192. import unicodedata
  193. if isinstance(my_unicode, unicode):
  194. my_unicode_with_ascii_chars_only = ''.join(
  195. (char
  196. for char in unicodedata.normalize('NFD', my_unicode)
  197. if unicodedata.category(char) != 'Mn')
  198. )
  199. return str(my_unicode_with_ascii_chars_only)
  200. # If the argument is already of string type, we return it with the same
  201. # value
  202. elif isinstance(my_unicode, str):
  203. return my_unicode
  204. else:
  205. return False
  206. def main(options, arguments):
  207. # print 'options = %s' % options
  208. # print 'arguments = %s' % arguments
  209. # AGI passes parameters to the script on standard input
  210. stdinput = {}
  211. while 1:
  212. input_line = sys.stdin.readline()
  213. if not input_line:
  214. break
  215. line = input_line.strip()
  216. try:
  217. variable, value = line.split(':')
  218. except:
  219. break
  220. if variable[:4] != 'agi_': # All AGI parameters start with 'agi_'
  221. stderr_write("bad stdin variable : %s\n" % variable)
  222. continue
  223. variable = variable.strip()
  224. value = value.strip()
  225. if variable and value:
  226. stdinput[variable] = value
  227. stderr_write("full AGI environnement :\n")
  228. for variable in stdinput.keys():
  229. stderr_write("%s = %s\n" % (variable, stdinput.get(variable)))
  230. # If we already have a "True" caller ID name
  231. # i.e. not just digits, but a real name, then we don't try to
  232. # connect to OpenERP or geoloc, we just keep it
  233. if (stdinput.get('agi_calleridname')
  234. and not stdinput.get('agi_calleridname').isdigit()
  235. and stdinput.get('agi_calleridname').lower()
  236. not in ['asterisk', 'unknown', 'anonymous']):
  237. stdout_write('VERBOSE "Incoming CallerID name is %s"\n'
  238. % stdinput.get('agi_calleridname'))
  239. stdout_write('VERBOSE "As it is a real name, we do not change it"\n')
  240. return True
  241. input_cid_number = stdinput.get('agi_callerid')
  242. stderr_write('stdout encoding = %s\n' % sys.stdout.encoding or 'utf-8')
  243. if not isinstance(input_cid_number, str):
  244. stdout_write('VERBOSE "CallerID number is empty"\n')
  245. exit(0)
  246. # Match for particular cases and anonymous phone calls
  247. # To test anonymous call in France, dial 3651 + number
  248. if not input_cid_number.isdigit():
  249. stdout_write('VERBOSE "CallerID number (%s) is not a digit"\n'
  250. % input_cid_number)
  251. exit(0)
  252. stdout_write('VERBOSE "CallerID number = %s"\n' % input_cid_number)
  253. res = False
  254. # Yes, this script can be used without "-s openerp_server" !
  255. if options.server:
  256. if options.ssl:
  257. stdout_write(
  258. 'VERBOSE "Starting XML-RPC secure request on OpenERP %s:%s"\n'
  259. % (options.server, str(options.port))
  260. )
  261. protocol = 'https'
  262. else:
  263. stdout_write(
  264. 'VERBOSE "Starting clear XML-RPC request on OpenERP %s:%s"\n'
  265. % (options.server, str(options.port))
  266. )
  267. protocol = 'http'
  268. sock = xmlrpclib.ServerProxy(
  269. '%s://%s:%s/xmlrpc/object'
  270. % (protocol, options.server, str(options.port))
  271. )
  272. try:
  273. res = sock.execute(
  274. options.database,
  275. options.user,
  276. options.password,
  277. 'res.partner.address',
  278. 'get_name_from_phone_number',
  279. input_cid_number
  280. )
  281. stdout_write('VERBOSE "End of XML-RPC request on OpenERP"\n')
  282. if not res:
  283. stdout_write('VERBOSE "Phone number not found in OpenERP"\n')
  284. except:
  285. stdout_write('VERBOSE "Could not connect to OpenERP"\n')
  286. res = False
  287. # To simulate a long execution of the XML-RPC request
  288. # import time
  289. # time.sleep(5)
  290. # Function to limit the size of the CID name to 40 chars
  291. if res:
  292. if len(res) > 40:
  293. res = res[0:40]
  294. elif options.geoloc:
  295. # if the number is not found in OpenERP, we try to geolocate
  296. stdout_write(
  297. 'VERBOSE "Trying to geolocate with country %s and lang %s"\n'
  298. % (options.country, options.lang)
  299. )
  300. res = geolocate_phone_number(
  301. input_cid_number,
  302. options.country,
  303. options.lang
  304. )
  305. else:
  306. # if the number is not found in OpenERP and geoloc is off,
  307. # we put 'default_cid_name' as CID Name
  308. res = default_cid_name
  309. # All SIP phones should support UTF-8... but in case you have analog
  310. # phones over TDM or buggy phones, you should use the command line option
  311. # --ascii
  312. if options.ascii:
  313. res = convert_to_ascii(res)
  314. stdout_write('VERBOSE "CallerID Name = %s"\n' % res)
  315. stdout_write('SET CALLERID "%s"<%s>\n' % (res, input_cid_number))
  316. return True
  317. if __name__ == '__main__':
  318. parser = OptionParser()
  319. for option in options:
  320. param = option['names']
  321. del option['names']
  322. parser.add_option(*param, **option)
  323. options, arguments = parser.parse_args()
  324. sys.argv[:] = arguments
  325. main(options, arguments)