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.

415 lines
17 KiB

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