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.

459 lines
20 KiB

  1. # -*- encoding: 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. import ESL
  26. import sys
  27. import csv
  28. import StringIO
  29. import re
  30. _logger = logging.getLogger(__name__)
  31. class freeswitch_server(orm.Model):
  32. '''FreeSWITCH server object, stores the parameters of the FreeSWITCH IPBXs'''
  33. _name = "freeswitch.server"
  34. _description = "FreeSWITCH Servers"
  35. _columns = {
  36. 'name': fields.char('FreeSWITCH Server Name', size=50, required=True),
  37. 'active': fields.boolean(
  38. 'Active', help="The active field allows you to hide the FreeSWITCH "
  39. "server without deleting it."),
  40. 'ip_address': fields.char(
  41. 'FreeSWITCH IP address or DNS', size=50, required=True,
  42. help="IP address or DNS name of the FreeSWITCH server."),
  43. 'port': fields.integer(
  44. 'Port', required=True,
  45. help="TCP port on which the FreeSWITCH Event Socket listens. "
  46. "Defined in /etc/freeswitch/autoload_configs/event_socket.conf.xml on FreeSWITCH."),
  47. 'out_prefix': fields.char(
  48. 'Out Prefix', size=4, help="Prefix to dial to make outgoing "
  49. "calls. If you don't use a prefix to make outgoing calls, "
  50. "leave empty."),
  51. 'password': fields.char(
  52. 'Event Socket Password', size=30, required=True,
  53. help="Password that OpenERP will use to communicate with the "
  54. "FreeSWITCH Event Socket. Refer to "
  55. "/etc/freeswitch/autoload_configs/event_socket.conf.xml "
  56. "on your FreeSWITCH server."),
  57. 'context': fields.char(
  58. 'Dialplan Context', size=50, required=True,
  59. help="FreeSWITCH dialplan context from which the calls will be "
  60. "made; e.g. 'XML default'. Refer to /etc/freeswitch/dialplan/* on your FreeSWITCH "
  61. "server."),
  62. 'wait_time': fields.integer(
  63. 'Wait Time (sec)', required=True,
  64. help="Amount of time (in seconds) FreeSWITCH will try to reach "
  65. "the user's phone before hanging up."),
  66. 'alert_info': fields.char(
  67. 'Alert-Info SIP Header', size=255,
  68. help="Set Alert-Info header in SIP request to user's IP Phone "
  69. "for the click2dial feature. If empty, the Alert-Info header "
  70. "will not be added. You can use it to have a special ring tone "
  71. "for click2dial (a silent one !) or to activate auto-answer "
  72. "for example."),
  73. 'company_id': fields.many2one(
  74. 'res.company', 'Company',
  75. help="Company who uses the FreeSWITCH server."),
  76. }
  77. _defaults = {
  78. 'active': True,
  79. 'port': 8021, # Default Event Socket port
  80. 'context': 'XML default',
  81. 'wait_time': 60,
  82. 'company_id': lambda self, cr, uid, context:
  83. self.pool['res.company']._company_default_get(
  84. cr, uid, 'freeswitch.server', context=context),
  85. }
  86. def _check_validity(self, cr, uid, ids):
  87. for server in self.browse(cr, uid, ids):
  88. out_prefix = ('Out prefix', server.out_prefix)
  89. dialplan_context = ('Dialplan context', server.context)
  90. alert_info = ('Alert-Info SIP header', server.alert_info)
  91. password = ('Event Socket password', server.password)
  92. if out_prefix[1] and not out_prefix[1].isdigit():
  93. raise orm.except_orm(
  94. _('Error:'),
  95. _("Only use digits for the '%s' on the FreeSWITCH server "
  96. "'%s'" % (out_prefix[0], server.name)))
  97. if server.wait_time < 1 or server.wait_time > 120:
  98. raise orm.except_orm(
  99. _('Error:'),
  100. _("You should set a 'Wait time' value between 1 and 120 "
  101. "seconds for the FreeSWITCH server '%s'" % server.name))
  102. if server.port > 65535 or server.port < 1:
  103. raise orm.except_orm(
  104. _('Error:'),
  105. _("You should set a TCP port between 1 and 65535 for the "
  106. "FreeSWITCH server '%s'" % server.name))
  107. for check_str in [dialplan_context, alert_info, password]:
  108. if check_str[1]:
  109. try:
  110. check_str[1].encode('ascii')
  111. except UnicodeEncodeError:
  112. raise orm.except_orm(
  113. _('Error:'),
  114. _("The '%s' should only have ASCII caracters for "
  115. "the FreeSWITCH server '%s'"
  116. % (check_str[0], server.name)))
  117. return True
  118. _constraints = [(
  119. _check_validity,
  120. "Error message in raise",
  121. [
  122. 'out_prefix', 'wait_time', 'port',
  123. 'context', 'alert_info', 'password']
  124. )]
  125. def _get_freeswitch_server_from_user(self, cr, uid, context=None):
  126. '''Returns an freeswitch.server browse object'''
  127. # We check if the user has an FreeSWITCH server configured
  128. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  129. if user.freeswitch_server_id.id:
  130. fs_server = user.freeswitch_server_id
  131. else:
  132. freeswitch_server_ids = self.search(
  133. cr, uid, [('company_id', '=', user.company_id.id)],
  134. context=context)
  135. # If the user doesn't have an freeswitch server,
  136. # we take the first one of the user's company
  137. if not freeswitch_server_ids:
  138. raise orm.except_orm(
  139. _('Error:'),
  140. _("No FreeSWITCH server configured for the company '%s'.")
  141. % user.company_id.name)
  142. else:
  143. fs_server = self.browse(
  144. cr, uid, freeswitch_server_ids[0], context=context)
  145. servers = self.pool.get('freeswitch.server')
  146. server_ids = servers.search(cr, uid, [('id', '=', fs_server.id)], context=context)
  147. fake_fs_server = servers.browse(cr, uid, server_ids, context=context)
  148. for rec in fake_fs_server:
  149. fs_server = rec
  150. break
  151. return fs_server
  152. def _connect_to_freeswitch(self, cr, uid, context=None):
  153. '''
  154. Open the connection to the FreeSWITCH Event Socket
  155. Returns an instance of the FreeSWITCH Event Socket
  156. '''
  157. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  158. fs_server = self._get_freeswitch_server_from_user(
  159. cr, uid, context=context)
  160. # We check if the current user has a chan type
  161. if not user.freeswitch_chan_type:
  162. raise orm.except_orm(
  163. _('Error:'),
  164. _('No channel type configured for the current user.'))
  165. # We check if the current user has an internal number
  166. if not user.resource:
  167. raise orm.except_orm(
  168. _('Error:'),
  169. _('No resource name configured for the current user'))
  170. _logger.debug(
  171. "User's phone: %s/%s" % (user.freeswitch_chan_type, user.resource))
  172. _logger.debug(
  173. "FreeSWITCH server: %s:%d"
  174. % (fs_server.ip_address, fs_server.port))
  175. # Connect to the FreeSWITCH Event Socket
  176. try:
  177. fs_manager = ESL.ESLconnection(str(fs_server.ip_address),
  178. str(fs_server.port), str(fs_server.password))
  179. except Exception, e:
  180. _logger.error(
  181. "Error in the request to the FreeSWITCH Event Socket %s"
  182. % fs_server.ip_address)
  183. _logger.error("Here is the error message: %s" % e)
  184. raise orm.except_orm(
  185. _('Error:'),
  186. _("Problem in the request from OpenERP to FreeSWITCH. "
  187. "Here is the error message: %s" % e))
  188. # return (False, False, False)
  189. return (user, fs_server, fs_manager)
  190. def test_es_connection(self, cr, uid, ids, context=None):
  191. assert len(ids) == 1, 'Only 1 ID'
  192. fs_server = self.browse(cr, uid, ids[0], context=context)
  193. fs_manager = False
  194. try:
  195. fs_manager = ESL.ESLconnection(str(fs_server.ip_address),
  196. str(fs_server.port), str(fs_server.password))
  197. except Exception, e:
  198. raise orm.except_orm(
  199. _("Connection Test Failed!"),
  200. _("Here is the error message: %s" % e))
  201. finally:
  202. if fs_manager.connected() is not 1:
  203. raise orm.except_orm(
  204. _("Connection Test Failed!"),
  205. _("Check Host, Port and Password"))
  206. else:
  207. try:
  208. if fs_manager:
  209. fs_manager.disconnect()
  210. raise orm.except_orm(
  211. _("Connection Test Successfull!"),
  212. _("OpenERP can successfully login to the FreeSWITCH Event "
  213. "Socket."))
  214. def _get_calling_number(self, cr, uid, context=None):
  215. user, fs_server, fs_manager = self._connect_to_freeswitch(
  216. cr, uid, context=context)
  217. calling_party_number = False
  218. try:
  219. ret = fs_manager.api('show', "calls as delim | like callee_cid_num " +
  220. str(user.internal_number))
  221. f = StringIO.StringIO(ret.getBody())
  222. reader = csv.DictReader(f, delimiter='|')
  223. for row in reader:
  224. if "uuid" not in row or row["uuid"] == "" or row["uuid"] == "uuid":
  225. break
  226. if row["callstate"] not in ["EARLY", "ACTIVE", "RINGING"]:
  227. continue
  228. if row["b_cid_num"] and row["b_cid_num"] is not None:
  229. calling_party_number = row["b_cid_num"]
  230. elif row["cid_num"] and row["cid_num"] is not None:
  231. calling_party_number = row["cid_num"]
  232. except Exception, e:
  233. _logger.error(
  234. "Error in the Status request to FreeSWITCH server %s"
  235. % fs_server.ip_address)
  236. _logger.error(
  237. "Here are the details of the error: '%s'" % unicode(e))
  238. raise orm.except_orm(
  239. _('Error:'),
  240. _("Can't get calling number from FreeSWITCH.\nHere is the "
  241. "error: '%s'" % unicode(e)))
  242. finally:
  243. fs_manager.disconnect()
  244. _logger.debug("Calling party number: '%s'" % calling_party_number)
  245. return calling_party_number
  246. def get_record_from_my_channel(self, cr, uid, context=None):
  247. calling_number = self.pool['freeswitch.server']._get_calling_number(
  248. cr, uid, context=context)
  249. # calling_number = "0641981246"
  250. if calling_number:
  251. record = self.pool['phone.common'].get_record_from_phone_number(
  252. cr, uid, calling_number, context=context)
  253. if record:
  254. return record
  255. else:
  256. return calling_number
  257. else:
  258. return False
  259. class res_users(orm.Model):
  260. _inherit = "res.users"
  261. _columns = {
  262. 'internal_number': fields.char(
  263. 'Internal Number', size=15,
  264. help="User's internal phone number."),
  265. 'dial_suffix': fields.char(
  266. 'User-specific Dial Suffix', size=15,
  267. help="User-specific dial suffix such as aa=2wb for SCCP "
  268. "auto answer."),
  269. 'callerid': fields.char(
  270. 'Caller ID', size=50,
  271. help="Caller ID used for the calls initiated by this user."),
  272. # You'd probably think: FreeSWITCH should reuse the callerID of sip.conf!
  273. # But it cannot, cf
  274. # http://lists.digium.com/pipermail/freeswitch-users/
  275. # 2012-January/269787.html
  276. 'cdraccount': fields.char(
  277. 'CDR Account', size=50,
  278. help="Call Detail Record (CDR) account used for billing this "
  279. "user."),
  280. 'freeswitch_chan_type': fields.selection([
  281. ('user', 'SIP'),
  282. ('FreeTDM', 'FreeTDM'),
  283. ('Skinny', 'Skinny'),
  284. ('MGCP', 'MGCP'),
  285. ('mISDN', 'mISDN'),
  286. ('H323', 'H323'),
  287. ('SCCP', 'SCCP'),
  288. ('Local', 'Local'),
  289. ], 'FreeSWITCH Channel Type',
  290. help="FreeSWITCH channel type, as used in the FreeSWITCH dialplan. "
  291. "If the user has a regular IP phone, the channel type is 'SIP'."),
  292. 'resource': fields.char(
  293. 'Resource Name', size=64,
  294. help="Resource name for the channel type selected. For example, "
  295. "if you use 'Dial(SIP/phone1)' in your FreeSWITCH dialplan to ring "
  296. "the SIP phone of this user, then the resource name for this user "
  297. "is 'phone1'. For a SIP phone, the phone number is often used as "
  298. "resource name, but not always."),
  299. 'alert_info': fields.char(
  300. 'User-specific Alert-Info SIP Header', size=255,
  301. help="Set a user-specific Alert-Info header in SIP request to "
  302. "user's IP Phone for the click2dial feature. If empty, the "
  303. "Alert-Info header will not be added. You can use it to have a "
  304. "special ring tone for click2dial (a silent one !) or to "
  305. "activate auto-answer for example."),
  306. 'variable': fields.char(
  307. 'User-specific Variable', size=255,
  308. help="Set a user-specific 'Variable' field in the FreeSWITCH "
  309. "Event Socket 'originate' request for the click2dial "
  310. "feature. If you want to have several variable headers, separate "
  311. "them with '|'."),
  312. 'freeswitch_server_id': fields.many2one(
  313. 'freeswitch.server', 'FreeSWITCH Server',
  314. help="FreeSWITCH server on which the user's phone is connected. "
  315. "If you leave this field empty, it will use the first FreeSWITCH "
  316. "server of the user's company."),
  317. }
  318. _defaults = {
  319. 'freeswitch_chan_type': 'SIP',
  320. }
  321. def _check_validity(self, cr, uid, ids):
  322. for user in self.browse(cr, uid, ids):
  323. strings_to_check = [
  324. (_('Resource Name'), user.resource),
  325. (_('Internal Number'), user.internal_number),
  326. (_('Caller ID'), user.callerid),
  327. ]
  328. for check_string in strings_to_check:
  329. if check_string[1]:
  330. try:
  331. check_string[1].encode('ascii')
  332. except UnicodeEncodeError:
  333. raise orm.except_orm(
  334. _('Error:'),
  335. _("The '%s' for the user '%s' should only have "
  336. "ASCII caracters")
  337. % (check_string[0], user.name))
  338. return True
  339. _constraints = [(
  340. _check_validity,
  341. "Error message in raise",
  342. ['resource', 'internal_number', 'callerid']
  343. )]
  344. class PhoneCommon(orm.AbstractModel):
  345. _inherit = 'phone.common'
  346. def click2dial(self, cr, uid, erp_number, context=None):
  347. res = super(PhoneCommon, self).click2dial(
  348. cr, uid, erp_number, context=context)
  349. if not erp_number:
  350. raise orm.except_orm(
  351. _('Error:'),
  352. _('Missing phone number'))
  353. user, fs_server, fs_manager = \
  354. self.pool['freeswitch.server']._connect_to_freeswitch(
  355. cr, uid, context=context)
  356. fs_number = self.convert_to_dial_number(
  357. cr, uid, erp_number, context=context)
  358. # Add 'out prefix'
  359. if fs_server.out_prefix:
  360. _logger.debug('Out prefix = %s' % fs_server.out_prefix)
  361. fs_number = '%s%s' % (fs_server.out_prefix, fs_number)
  362. _logger.debug('Number to be sent to FreeSWITCH = %s' % fs_number)
  363. # The user should have a CallerID
  364. if not user.callerid:
  365. raise orm.except_orm(
  366. _('Error:'),
  367. _('No callerID configured for the current user'))
  368. variable = ""
  369. if user.freeswitch_chan_type == 'SIP':
  370. # We can only have one alert-info header in a SIP request
  371. if user.alert_info:
  372. variable += 'alert_info=' + user.alert_info
  373. elif fs_server.alert_info:
  374. variable += 'alert_info=' + fs_server.alert_info
  375. if user.variable:
  376. for user_variable in user.variable.split('|'):
  377. if len(variable) and len(user_variable):
  378. variable += ','
  379. variable += user_variable.strip()
  380. if user.callerid:
  381. caller_name = re.search(r'([^<]*).*', user.callerid).group(1).strip()
  382. caller_number = re.search(r'.*<(.*)>.*', user.callerid).group(1).strip()
  383. if caller_name:
  384. if len(variable):
  385. variable += ','
  386. caller_name = caller_name.replace(",", r"\,")
  387. variable += 'effective_caller_id_name=' + caller_name
  388. if caller_number:
  389. if len(variable):
  390. variable += ','
  391. variable += 'effective_caller_id_number=' + caller_number
  392. if fs_server.wait_time != 60:
  393. if len(variable):
  394. variable += ','
  395. variable += 'ignore_early_media=true' + ','
  396. variable += 'originate_timeout=' + str(fs_server.wait_time)
  397. channel = '%s/%s' % (user.freeswitch_chan_type, user.resource)
  398. if user.dial_suffix:
  399. channel += '/%s' % user.dial_suffix
  400. try:
  401. # originate FreeTDM/1/3 1234567 XML Internal-FXS
  402. # originate user/2005 1003 XML Internal-FXS
  403. # originate <effective_caller_id_number=1234,originate_timeout=7,call_timeout=7>user/2005 1005 XML Internal-FXS 'Caller ID showed to OpenERP user' 90125
  404. dial_string = (('<' + variable + '>') if variable else '') + \
  405. channel + ' ' + fs_number + ' ' + fs_server.context + ' ' + \
  406. '\'FreeSWITCH/Odoo Connector\' ' + fs_number
  407. # raise orm.except_orm(_('Error :'), dial_string)
  408. fs_manager.api('originate', dial_string.encode("ascii"))
  409. except Exception, e:
  410. _logger.error(
  411. "Error in the Originate request to FreeSWITCH server %s"
  412. % fs_server.ip_address)
  413. _logger.error(
  414. "Here are the details of the error: '%s'" % unicode(e))
  415. raise orm.except_orm(
  416. _('Error:'),
  417. _("Click to dial with FreeSWITCH failed.\nHere is the error: "
  418. "'%s'")
  419. % unicode(e))
  420. finally:
  421. fs_manager.disconnect()
  422. res['dialed_number'] = fs_number
  423. return res