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.

475 lines
20 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # FreeSWITCH Click2dial module for OpenERP
  5. # Copyright (C) 2014 Trever L. Adams
  6. # Copyright (C) 2010-2013 Alexis de Lattre <alexis@via.ecp.fr>
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Affero General Public License as
  10. # published by the Free Software Foundation, either version 3 of the
  11. # License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Affero General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Affero General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ##############################################################################
  22. from openerp.osv import fields, orm
  23. from openerp.tools.translate import _
  24. import logging
  25. try:
  26. from freeswitchESL import ESL
  27. except ImportError:
  28. import ESL
  29. # import sys
  30. import StringIO
  31. import re
  32. import json
  33. _logger = logging.getLogger(__name__)
  34. class freeswitch_server(orm.Model):
  35. '''FreeSWITCH server object, stores the parameters of the FreeSWITCH Servers'''
  36. _name = "freeswitch.server"
  37. _description = "FreeSWITCH Servers"
  38. _columns = {
  39. 'name': fields.char('FreeSWITCH Server Name', size=50, required=True),
  40. 'active': fields.boolean(
  41. 'Active', help="The active field allows you to hide the "
  42. "FreeSWITCH server without deleting it."),
  43. 'ip_address': fields.char(
  44. 'FreeSWITCH IP address or DNS', size=50, required=True,
  45. help="IP address or DNS name of the FreeSWITCH server."),
  46. 'port': fields.integer(
  47. 'Port', required=True,
  48. help="TCP port on which the FreeSWITCH Event Socket listens. "
  49. "Defined in "
  50. "/etc/freeswitch/autoload_configs/event_socket.conf.xml on "
  51. "FreeSWITCH."),
  52. 'out_prefix': fields.char(
  53. 'Out Prefix', size=4, help="Prefix to dial to make outgoing "
  54. "calls. If you don't use a prefix to make outgoing calls, "
  55. "leave empty."),
  56. 'password': fields.char(
  57. 'Event Socket Password', size=30, required=True,
  58. help="Password that OpenERP will use to communicate with the "
  59. "FreeSWITCH Event Socket. Refer to "
  60. "/etc/freeswitch/autoload_configs/event_socket.conf.xml "
  61. "on your FreeSWITCH server."),
  62. 'context': fields.char(
  63. 'Dialplan Context', size=50, required=True,
  64. help="FreeSWITCH dialplan context from which the calls will be "
  65. "made; e.g. 'XML default'. Refer to /etc/freeswitch/dialplan/* "
  66. "on your FreeSWITCH server."),
  67. 'wait_time': fields.integer(
  68. 'Wait Time (sec)', required=True,
  69. help="Amount of time (in seconds) FreeSWITCH will try to reach "
  70. "the user's phone before hanging up."),
  71. 'alert_info': fields.char(
  72. 'Alert-Info SIP Header', size=255,
  73. help="Set Alert-Info header in SIP request to user's IP Phone "
  74. "for the click2dial feature. If empty, the Alert-Info header "
  75. "will not be added. You can use it to have a special ring tone "
  76. "for click2dial (a silent one !) or to activate auto-answer "
  77. "for example."),
  78. 'company_id': fields.many2one(
  79. 'res.company', 'Company',
  80. help="Company who uses the FreeSWITCH server."),
  81. }
  82. _defaults = {
  83. 'active': True,
  84. 'port': 8021, # Default Event Socket port
  85. 'context': 'XML default',
  86. 'wait_time': 60,
  87. 'company_id': lambda self, cr, uid, context:
  88. self.pool['res.company']._company_default_get(
  89. cr, uid, 'freeswitch.server', context=context),
  90. }
  91. def _check_validity(self, cr, uid, ids):
  92. for server in self.browse(cr, uid, ids):
  93. out_prefix = ('Out prefix', server.out_prefix)
  94. dialplan_context = ('Dialplan context', server.context)
  95. alert_info = ('Alert-Info SIP header', server.alert_info)
  96. password = ('Event Socket password', server.password)
  97. if out_prefix[1] and not out_prefix[1].isdigit():
  98. raise orm.except_orm(
  99. _('Error:'),
  100. _("Only use digits for the '%s' on the FreeSWITCH server "
  101. "'%s'" % (out_prefix[0], server.name)))
  102. if server.wait_time < 1 or server.wait_time > 120:
  103. raise orm.except_orm(
  104. _('Error:'),
  105. _("You should set a 'Wait time' value between 1 and 120 "
  106. "seconds for the FreeSWITCH server '%s'"
  107. % server.name))
  108. if server.port > 65535 or server.port < 1:
  109. raise orm.except_orm(
  110. _('Error:'),
  111. _("You should set a TCP port between 1 and 65535 for the "
  112. "FreeSWITCH server '%s'" % server.name))
  113. for check_str in [dialplan_context, alert_info, password]:
  114. if check_str[1]:
  115. try:
  116. check_str[1].encode('ascii')
  117. except UnicodeEncodeError:
  118. raise orm.except_orm(
  119. _('Error:'),
  120. _("The '%s' should only have ASCII caracters for "
  121. "the FreeSWITCH server '%s'"
  122. % (check_str[0], server.name)))
  123. return True
  124. _constraints = [(
  125. _check_validity,
  126. "Error message in raise",
  127. [
  128. 'out_prefix', 'wait_time', 'port',
  129. 'context', 'alert_info', 'password']
  130. )]
  131. def _get_freeswitch_server_from_user(self, cr, uid, context=None):
  132. '''Returns an freeswitch.server browse object'''
  133. # We check if the user has an FreeSWITCH server configured
  134. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  135. if user.freeswitch_server_id.id:
  136. fs_server = user.freeswitch_server_id
  137. else:
  138. freeswitch_server_ids = self.search(
  139. cr, uid, [('company_id', '=', user.company_id.id)],
  140. context=context)
  141. # If the user doesn't have an freeswitch server,
  142. # we take the first one of the user's company
  143. if not freeswitch_server_ids:
  144. raise orm.except_orm(
  145. _('Error:'),
  146. _("No FreeSWITCH server configured for the company '%s'.")
  147. % user.company_id.name)
  148. else:
  149. fs_server = self.browse(
  150. cr, uid, freeswitch_server_ids[0], context=context)
  151. servers = self.pool.get('freeswitch.server')
  152. server_ids = servers.search(cr, uid, [('id', '=', fs_server.id)],
  153. context=context)
  154. fake_fs_server = servers.browse(cr, uid, server_ids, context=context)
  155. for rec in fake_fs_server:
  156. fs_server = rec
  157. break
  158. return fs_server
  159. def _connect_to_freeswitch(self, cr, uid, context=None):
  160. '''
  161. Open the connection to the FreeSWITCH Event Socket
  162. Returns an instance of the FreeSWITCH Event Socket
  163. '''
  164. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  165. fs_server = self._get_freeswitch_server_from_user(
  166. cr, uid, context=context)
  167. # We check if the current user has a chan type
  168. if not user.freeswitch_chan_type:
  169. raise orm.except_orm(
  170. _('Error:'),
  171. _('No channel type configured for the current user.'))
  172. # We check if the current user has an internal number
  173. if not user.resource:
  174. raise orm.except_orm(
  175. _('Error:'),
  176. _('No resource name configured for the current user'))
  177. _logger.debug(
  178. "User's phone: %s/%s" % (user.freeswitch_chan_type, user.resource))
  179. _logger.debug(
  180. "FreeSWITCH server: %s:%d"
  181. % (fs_server.ip_address, fs_server.port))
  182. # Connect to the FreeSWITCH Event Socket
  183. try:
  184. fs_manager = ESL.ESLconnection(
  185. str(fs_server.ip_address),
  186. str(fs_server.port), str(fs_server.password))
  187. except Exception, e:
  188. _logger.error(
  189. "Error in the request to the FreeSWITCH Event Socket %s"
  190. % fs_server.ip_address)
  191. _logger.error("Here is the error message: %s" % e)
  192. raise orm.except_orm(
  193. _('Error:'),
  194. _("Problem in the request from OpenERP to FreeSWITCH. "
  195. "Here is the error message: %s" % e))
  196. # return (False, False, False)
  197. return (user, fs_server, fs_manager)
  198. def test_es_connection(self, cr, uid, ids, context=None):
  199. assert len(ids) == 1, 'Only 1 ID'
  200. fs_server = self.browse(cr, uid, ids[0], context=context)
  201. fs_manager = False
  202. try:
  203. fs_manager = ESL.ESLconnection(
  204. str(fs_server.ip_address),
  205. str(fs_server.port), str(fs_server.password))
  206. except Exception, e:
  207. raise orm.except_orm(
  208. _("Connection Test Failed!"),
  209. _("Here is the error message: %s" % e))
  210. finally:
  211. try:
  212. if fs_manager.connected() is not 1:
  213. raise orm.except_orm(
  214. _("Connection Test Failed!"),
  215. _("Check Host, Port and Password"))
  216. else:
  217. fs_manager.disconnect()
  218. raise orm.except_orm(
  219. _("Connection Test Successfull!"),
  220. _("OpenERP can successfully login to the FreeSWITCH Event "
  221. "Socket."))
  222. except Exception, e:
  223. raise orm.except_orm(
  224. _("Connection Test Failed!"),
  225. _("Check Host, Port and Password"))
  226. def _get_calling_number(self, cr, uid, context=None):
  227. user, fs_server, fs_manager = self._connect_to_freeswitch(
  228. cr, uid, context=context)
  229. calling_party_number = False
  230. try:
  231. request = "channels like /" + re.sub(r'/', r':', user.resource) + \
  232. ("/" if user.freeswitch_chan_type == "FreeTDM" else "@") + \
  233. " as json"
  234. ret = fs_manager.api('show', str(request))
  235. f = json.load(StringIO.StringIO(ret.getBody()))
  236. if int(f['row_count']) > 0:
  237. if (f['rows'][0]['initial_cid_name'] == 'Odoo Connector' or
  238. f['rows'][0]['direction'] == 'inbound'):
  239. calling_party_number = f['rows'][0]['dest']
  240. else:
  241. calling_party_number = f['rows'][0]['cid_num']
  242. except Exception, e:
  243. _logger.error(
  244. "Error in the Status request to FreeSWITCH server %s"
  245. % fs_server.ip_address)
  246. _logger.error(
  247. "Here are the details of the error: '%s'" % unicode(e))
  248. raise orm.except_orm(
  249. _('Error:'),
  250. _("Can't get calling number from FreeSWITCH.\nHere is the "
  251. "error: '%s'" % unicode(e)))
  252. finally:
  253. fs_manager.disconnect()
  254. _logger.debug("Calling party number: '%s'" % calling_party_number)
  255. return calling_party_number
  256. def get_record_from_my_channel(self, cr, uid, context=None):
  257. calling_number = self.pool['freeswitch.server']._get_calling_number(
  258. cr, uid, context=context)
  259. # calling_number = "0641981246"
  260. if calling_number:
  261. record = self.pool['phone.common'].get_record_from_phone_number(
  262. cr, uid, calling_number, context=context)
  263. if record:
  264. return record
  265. else:
  266. return calling_number
  267. else:
  268. return False
  269. class res_users(orm.Model):
  270. _inherit = "res.users"
  271. _columns = {
  272. 'internal_number': fields.char(
  273. 'Internal Number', size=15,
  274. help="User's internal phone number."),
  275. 'dial_suffix': fields.char(
  276. 'User-specific Dial Suffix', size=15,
  277. help="User-specific dial suffix such as aa=2wb for SCCP "
  278. "auto answer."),
  279. 'callerid': fields.char(
  280. 'Caller ID', size=50,
  281. help="Caller ID used for the calls initiated by this user."),
  282. 'cdraccount': fields.char(
  283. 'CDR Account', size=50,
  284. help="Call Detail Record (CDR) account used for billing this "
  285. "user."),
  286. 'freeswitch_chan_type': fields.selection([
  287. ('user', 'SIP'),
  288. ('FreeTDM', 'FreeTDM'),
  289. ('verto.rtc', 'Verto'),
  290. ('skinny', 'Skinny'),
  291. ('h323', 'H323'),
  292. ('dingaling', 'XMPP/JINGLE'),
  293. ('gsmopen', 'GSM SMS/Voice'),
  294. ('skypeopen', 'SkypeOpen'),
  295. ('Khomp', 'Khomp'),
  296. ('opal', 'Opal Multi-protocol'),
  297. ('portaudio', 'Portaudio'),
  298. ], 'FreeSWITCH Channel Type',
  299. help="FreeSWITCH channel type, as used in the FreeSWITCH "
  300. "dialplan. If the user has a regular IP phone, the channel type "
  301. "is 'SIP'. Use Verto for verto.rtc connections only if you "
  302. "haven't added '${verto_contact ${dialed_user}@${dialed_domain}}' "
  303. "to the default dial string. Otherwise, use SIP. (This better "
  304. "allows for changes to the user directory and changes in type of "
  305. "phone without the need for further changes in OpenERP/Odoo.)"),
  306. 'resource': fields.char(
  307. 'Resource Name', size=64,
  308. help="Resource name for the channel type selected. For example, "
  309. "if you use 'user/phone1' in your FreeSWITCH dialplan to ring "
  310. "the SIP phone of this user, then the resource name for this user "
  311. "is 'phone1'. For a SIP phone, the phone number is often used as "
  312. "resource name, but not always. FreeTDM will be the span followed "
  313. "by the port (i.e. 1/5)."),
  314. 'alert_info': fields.char(
  315. 'User-specific Alert-Info SIP Header', size=255,
  316. help="Set a user-specific Alert-Info header in SIP request to "
  317. "user's IP Phone for the click2dial feature. If empty, the "
  318. "Alert-Info header will not be added. You can use it to have a "
  319. "special ring tone for click2dial (a silent one !) or to "
  320. "activate auto-answer for example."),
  321. 'variable': fields.char(
  322. 'User-specific Variable', size=255,
  323. help="Set a user-specific 'Variable' field in the FreeSWITCH "
  324. "Event Socket 'originate' request for the click2dial "
  325. "feature. If you want to have several variable headers, separate "
  326. "them with '|'."),
  327. 'freeswitch_server_id': fields.many2one(
  328. 'freeswitch.server', 'FreeSWITCH Server',
  329. help="FreeSWITCH server on which the user's phone is connected. "
  330. "If you leave this field empty, it will use the first FreeSWITCH "
  331. "server of the user's company."),
  332. }
  333. _defaults = {
  334. 'freeswitch_chan_type': 'user',
  335. }
  336. def _check_validity(self, cr, uid, ids):
  337. for user in self.browse(cr, uid, ids):
  338. strings_to_check = [
  339. (_('Resource Name'), user.resource),
  340. (_('Internal Number'), user.internal_number),
  341. (_('Caller ID'), user.callerid),
  342. ]
  343. for check_string in strings_to_check:
  344. if check_string[1]:
  345. try:
  346. check_string[1].encode('ascii')
  347. except UnicodeEncodeError:
  348. raise orm.except_orm(
  349. _('Error:'),
  350. _("The '%s' for the user '%s' should only have "
  351. "ASCII caracters")
  352. % (check_string[0], user.name))
  353. return True
  354. _constraints = [(
  355. _check_validity,
  356. "Error message in raise",
  357. ['resource', 'internal_number', 'callerid']
  358. )]
  359. class PhoneCommon(orm.AbstractModel):
  360. _inherit = 'phone.common'
  361. def click2dial(self, cr, uid, erp_number, context=None):
  362. res = super(PhoneCommon, self).click2dial(
  363. cr, uid, erp_number, context=context)
  364. if not erp_number:
  365. raise orm.except_orm(
  366. _('Error:'),
  367. _('Missing phone number'))
  368. user, fs_server, fs_manager = \
  369. self.pool['freeswitch.server']._connect_to_freeswitch(
  370. cr, uid, context=context)
  371. fs_number = self.convert_to_dial_number(
  372. cr, uid, erp_number, context=context)
  373. # Add 'out prefix'
  374. if fs_server.out_prefix:
  375. _logger.debug('Out prefix = %s' % fs_server.out_prefix)
  376. fs_number = '%s%s' % (fs_server.out_prefix, fs_number)
  377. _logger.debug('Number to be sent to FreeSWITCH = %s' % fs_number)
  378. # The user should have a CallerID
  379. if not user.callerid:
  380. raise orm.except_orm(
  381. _('Error:'),
  382. _('No callerID configured for the current user'))
  383. variable = ""
  384. if user.freeswitch_chan_type == 'user':
  385. # We can only have one alert-info header in a SIP request
  386. if user.alert_info:
  387. variable += 'alert_info=' + user.alert_info
  388. elif fs_server.alert_info:
  389. variable += 'alert_info=' + fs_server.alert_info
  390. if user.variable:
  391. for user_variable in user.variable.split('|'):
  392. if len(variable) and len(user_variable):
  393. variable += ','
  394. variable += user_variable.strip()
  395. if user.callerid:
  396. caller_name = re.search(r'([^<]*).*',
  397. user.callerid).group(1).strip()
  398. caller_number = re.search(r'.*<(.*)>.*',
  399. user.callerid).group(1).strip()
  400. if caller_name:
  401. if len(variable):
  402. variable += ','
  403. caller_name = caller_name.replace(",", r"\,")
  404. variable += 'effective_caller_id_name=' + caller_name
  405. if caller_number:
  406. if len(variable):
  407. variable += ','
  408. variable += 'effective_caller_id_number=' + caller_number
  409. if fs_server.wait_time != 60:
  410. if len(variable):
  411. variable += ','
  412. variable += 'ignore_early_media=true' + ','
  413. variable += 'originate_timeout=' + str(fs_server.wait_time)
  414. channel = '%s/%s' % (user.freeswitch_chan_type, user.resource)
  415. if user.dial_suffix:
  416. channel += '/%s' % user.dial_suffix
  417. try:
  418. # originate <csv global vars>user/2005 1005 DP_TYPE DP_NAME
  419. # 'Caller ID name showed to aleg' 90125
  420. dial_string = (('<' + variable + '>') if variable else '') + \
  421. channel + ' ' + fs_number + ' ' + fs_server.context + ' ' + \
  422. '\'FreeSWITCH/Odoo Connector\' ' + fs_number
  423. # raise orm.except_orm(_('Error :'), dial_string)
  424. fs_manager.api('originate', dial_string.encode("ascii"))
  425. except Exception, e:
  426. _logger.error(
  427. "Error in the Originate request to FreeSWITCH server %s"
  428. % fs_server.ip_address)
  429. _logger.error(
  430. "Here are the details of the error: '%s'" % unicode(e))
  431. raise orm.except_orm(
  432. _('Error:'),
  433. _("Click to dial with FreeSWITCH failed.\nHere is the error: "
  434. "'%s'")
  435. % unicode(e))
  436. finally:
  437. fs_manager.disconnect()
  438. res['dialed_number'] = fs_number
  439. return res