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

# -*- coding: utf-8 -*-
# © 2010-2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# © 2014-2016 Trever L. Adams
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openerp import models, fields, api, _
from openerp.exceptions import UserError, ValidationError
import logging
try:
from freeswitchESL import ESL
except ImportError:
import ESL
import StringIO
import re
import json
_logger = logging.getLogger(__name__)
class FreeSWITCHServer(models.Model):
'''FreeSWITCH server object, stores the parameters of the FreeSWITCH Servers'''
_name = "freeswitch.server"
_description = "FreeSWITCH Servers"
name = fields.Char(string='FreeSWITCH Server Name', required=True)
active = fields.Boolean(
string='Active', help="The active field allows you to hide the "
"FreeSWITCH server without deleting it.", default=True)
ip_address = fields.Char(
string='FreeSWITCH IP address or DNS', required=True,
help="IP address or DNS name of the FreeSWITCH server.")
port = fields.Integer(
string='Port', required=True, default=8021,
help="TCP port on which the FreeSWITCH Event Socket listens. "
"Defined in "
"/etc/freeswitch/autoload_configs/event_socket.conf.xml on "
"FreeSWITCH.")
out_prefix = fields.Char(
string='Out Prefix', size=4, help="Prefix to dial to make outgoing "
"calls. If you don't use a prefix to make outgoing calls, "
"leave empty.")
password = fields.Char(
string='Event Socket Password', required=True,
help="Password that OpenERP/Odoo will use to communicate with the "
"FreeSWITCH Event Socket. Refer to "
"/etc/freeswitch/autoload_configs/event_socket.conf.xml "
"on your FreeSWITCH server.")
context = fields.Char(
string='Dialplan Context', required=True, default="XML default",
help="FreeSWITCH dialplan context from which the calls will be "
"made; e.g. 'XML default'. Refer to /etc/freeswitch/dialplan/* "
"on your FreeSWITCH server.")
wait_time = fields.Integer(
string='Wait Time (sec)', required=True,
help="Amount of time (in seconds) FreeSWITCH will try to reach "
"the user's phone before hanging up.", default=60)
alert_info = fields.Char(
string='Alert-Info SIP Header',
help="Set Alert-Info header in SIP request to user's IP Phone "
"for the click2dial feature. If empty, the Alert-Info header "
"will not be added. You can use it to have a special ring tone "
"for click2dial (a silent one !) or to activate auto-answer "
"for example.")
company_id = fields.Many2one(
'res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get(
'freeswitch.server'),
help="Company who uses the FreeSWITCH server.")
@api.multi
@api.constrains(
'out_prefix', 'wait_time', 'port', 'context', 'alert_info', 'password')
def _check_validity(self):
for server in self:
out_prefix = ('Out prefix', server.out_prefix)
dialplan_context = ('Dialplan context', server.context)
alert_info = ('Alert-Info SIP header', server.alert_info)
password = ('Event Socket password', server.password)
if out_prefix[1] and not out_prefix[1].isdigit():
raise ValidationError(
_("Only use digits for the '%s' on the FreeSWITCH server "
"'%s'" % (out_prefix[0], server.name)))
if server.wait_time < 1 or server.wait_time > 120:
raise ValidationError(
_("You should set a 'Wait time' value between 1 and 120 "
"seconds for the FreeSWITCH server '%s'"
% server.name))
if server.port > 65535 or server.port < 1:
raise ValidationError(
_("You should set a TCP port between 1 and 65535 for the "
"FreeSWITCH server '%s'" % server.name))
for check_str in [dialplan_context, alert_info, password]:
if check_str[1]:
try:
check_str[1].encode('ascii')
except UnicodeEncodeError:
raise ValidationError(
_("The '%s' should only have ASCII characters for "
"the FreeSWITCH server '%s'"
% (check_str[0], server.name)))
@api.model
def _connect_to_freeswitch(self):
'''
Open the connection to the FreeSWITCH Event Socket
Returns an instance of the FreeSWITCH Event Socket
'''
user = self.env.user
fs_server = user.get_freeswitch_server_from_user()
# We check if the current user has a chan type
if not user.freeswitch_chan_type:
raise UserError(
_('No channel type configured for the current user.'))
# We check if the current user has an internal number
if not user.resource:
raise UserError(
_('No resource name configured for the current user.'))
_logger.debug(
"User's phone: %s/%s", user.freeswitch_chan_type, user.resource)
_logger.debug(
"FreeSWITCH server: %s:%d", fs_server.ip_address, fs_server.port)
# Connect to the FreeSWITCH Event Socket
try:
fs_manager = ESL.ESLconnection(
str(fs_server.ip_address),
str(fs_server.port), str(fs_server.password))
except Exception, e:
_logger.error(
"Error in the request to the FreeSWITCH Event Socket %s",
fs_server.ip_address)
_logger.error("Here is the error message: %s", e)
raise UserError(
_("Problem in the request from Odoo to FreeSWITCH. "
"Here is the error message: %s" % e))
return (user, fs_server, fs_manager)
@api.multi
def test_es_connection(self):
self.ensure_one()
fs_manager = False
try:
fs_manager = ESL.ESLconnection(
str(self.ip_address),
str(self.port), str(self.password))
except Exception, e:
raise UserError(
_("Connection Test Failed! The error message is: %s" % e))
finally:
try:
if fs_manager.connected() is not 1:
raise UserError(
_("Connection Test Failed! Check Host, Port and "
"Password."))
else:
fs_manager.disconnect()
except Exception, e:
pass
raise UserError(_(
"Connection Test Successfull! Odoo can successfully login to "
"the FreeSWITCH Event Socket."))
@api.model
def _get_calling_number(self):
user, fs_server, fs_manager = self._connect_to_freeswitch()
calling_party_number = False
try:
is_fq_res = user.resource.rfind('@')
if is_fq_res > 0:
resource = user.resource[0:is_fq_res]
else:
resource = user.resource
request = "channels like /" + re.sub(r'/', r':', resource) + \
("/" if user.freeswitch_chan_type == "FreeTDM" else "@") + \
" as json"
ret = fs_manager.api('show', str(request))
f = json.load(StringIO.StringIO(ret.getBody()))
if int(f['row_count']) > 0:
for x in range(0, int(f['row_count'])):
if (is_fq_res > 0 and f['rows'][x]['presence_id'] !=
user.resource):
continue
if (f['rows'][x]['cid_num'] == user.internal_number or
len(f['rows'][x]['cid_num']) < 3):
calling_party_number = f['rows'][x]['dest']
else:
calling_party_number = f['rows'][x]['cid_num']
except Exception, e:
_logger.error(
"Error in the Status request to FreeSWITCH server %s",
fs_server.ip_address)
_logger.error(
"Here are the details of the error: '%s'" % unicode(e))
raise UserError(
_("Can't get calling number from FreeSWITCH.\nHere is the "
"error: '%s'" % unicode(e)))
finally:
fs_manager.disconnect()
_logger.debug("Calling party number: '%s'", calling_party_number)
if calling_party_number and calling_party_number.isdigit():
return calling_party_number
else:
return False
@api.model
def get_record_from_my_channel(self):
calling_number = self.env['freeswitch.server']._get_calling_number()
# calling_number = "0641981246"
if calling_number:
record = self.env['phone.common'].get_record_from_phone_number(
calling_number)
if record:
return record
else:
return calling_number
else:
return False
class ResUsers(models.Model):
_inherit = "res.users"
internal_number = fields.Char(
string='Internal Number',
help="User's internal phone number.")
dial_suffix = fields.Char(
string='User-specific Dial Suffix',
help="User-specific dial suffix such as aa=2wb for SCCP "
"auto answer.")
callerid = fields.Char(
string='Caller ID', copy=False,
help="Caller ID used for the calls initiated by this user. "
"This must be in the form of 'Name <NUMBER>'.")
cdraccount = fields.Char(
string='CDR Account',
help="Call Detail Record (CDR) account used for billing this "
"user.")
freeswitch_chan_type = fields.Selection([
('user', 'SIP'),
('FreeTDM', 'FreeTDM'),
('verto.rtc', 'Verto'),
('skinny', 'Skinny'),
('h323', 'H323'),
('dingaling', 'XMPP/JINGLE'),
('gsmopen', 'GSM SMS/Voice'),
('skypeopen', 'SkypeOpen'),
('Khomp', 'Khomp'),
('opal', 'Opal Multi-protocol'),
('portaudio', 'Portaudio'),
], string='FreeSWITCH Channel Type', default='user',
help="FreeSWITCH channel type, as used in the FreeSWITCH "
"dialplan. If the user has a regular IP phone, the channel type "
"is 'SIP'. Use Verto for verto.rtc connections only if you "
"haven't added '${verto_contact ${dialed_user}@${dialed_domain}}' "
"to the default dial string. Otherwise, use SIP. (This better "
"allows for changes to the user directory and changes in type of "
"phone without the need for further changes in OpenERP/Odoo.)")
resource = fields.Char(
string='Resource Name',
help="Resource name for the channel type selected. For example, "
"if you use 'user/phone1' in your FreeSWITCH dialplan to ring "
"the SIP phone of this user, then the resource name for this user "
"is 'phone1'. For a SIP phone, the phone number is often used as "
"resource name, but not always. FreeTDM will be the span followed "
"by the port (i.e. 1/5).")
alert_info = fields.Char(
string='User-specific Alert-Info SIP Header',
help="Set a user-specific Alert-Info header in SIP request to "
"user's IP Phone for the click2dial feature. If empty, the "
"Alert-Info header will not be added. You can use it to have a "
"special ring tone for click2dial (a silent one !) or to "
"activate auto-answer for example.")
variable = fields.Char(
string='User-specific Variable',
help="Set a user-specific 'Variable' field in the FreeSWITCH "
"Event Socket 'originate' request for the click2dial "
"feature. If you want to have several variable headers, separate "
"them with '|'.")
freeswitch_server_id = fields.Many2one(
'freeswitch.server', string='FreeSWITCH Server',
help="FreeSWITCH server on which the user's phone is connected. "
"If you leave this field empty, it will use the first FreeSWITCH "
"server of the user's company.")
@api.multi
@api.constrains('resource', 'internal_number', 'callerid')
def _check_validity(self):
for user in self:
strings_to_check = [
(_('Resource Name'), user.resource),
(_('Internal Number'), user.internal_number),
(_('Caller ID'), user.callerid),
]
for check_string in strings_to_check:
if check_string[1]:
try:
check_string[1].encode('ascii')
except UnicodeEncodeError:
raise ValidationError(_(
"The '%s' for the user '%s' should only have "
"ASCII caracters."),
check_string[0], user.name)
@api.multi
def get_freeswitch_server_from_user(self):
'''Returns an freeswitch.server recordset'''
self.ensure_one()
# We check if the user has an FreeSWITCH server configured
if self.freeswitch_server_id.id:
fs_server = self.freeswitch_server_id
else:
freeswitch_servers = self.env['freeswitch.server'].search(
[('company_id', '=', self.company_id.id)])
# If the user doesn't have an FreeSWITCH server,
# we take the first one of the user's company
if not freeswitch_servers:
raise UserError(
_("No FreeSWITCH server configured for the company '%s'.")
% self.company_id.name)
else:
fs_server = freeswitch_servers[0]
return fs_server
class PhoneCommon(models.AbstractModel):
_inherit = 'phone.common'
@api.model
def click2dial(self, erp_number):
res = super(PhoneCommon, self).click2dial(erp_number)
if not erp_number:
raise UserError(_('Missing phone number.'))
user, fs_server, fs_manager = \
self.env['freeswitch.server']._connect_to_freeswitch()
fs_number = self.convert_to_dial_number(erp_number)
# Add 'out prefix'
if fs_server.out_prefix:
_logger.debug('Out prefix = %s', fs_server.out_prefix)
fs_number = '%s%s' % (fs_server.out_prefix, fs_number)
_logger.debug('Number to be sent to FreeSWITCH = %s', fs_number)
# The user should have a CallerID
if not user.callerid:
raise UserError(_('No callerID configured for the current user.'))
variable = ""
if user.freeswitch_chan_type == 'user':
# We can only have one alert-info header in a SIP request
if user.alert_info:
variable += 'alert_info=' + user.alert_info
elif fs_server.alert_info:
variable += 'alert_info=' + fs_server.alert_info
if user.variable:
for user_variable in user.variable.split('|'):
if len(variable) and len(user_variable):
variable += ','
variable += user_variable.strip()
if user.callerid:
caller_name = re.search(r'([^<]*).*',
user.callerid).group(1).strip()
caller_number = re.search(r'.*<(.*)>.*',
user.callerid).group(1).strip()
if caller_name:
if len(variable):
variable += ','
caller_name = caller_name.replace(",", r"\,")
variable += 'effective_caller_id_name=\'' + caller_name + '\''
if caller_number:
if len(variable):
variable += ','
variable += 'effective_caller_id_number=\'' + \
caller_number + '\''
if fs_server.wait_time != 60:
if len(variable):
variable += ','
variable += 'ignore_early_media=true' + ','
variable += 'originate_timeout=' + str(fs_server.wait_time)
if len(variable):
variable += ','
variable += 'odoo_connector=true'
channel = '%s/%s' % (user.freeswitch_chan_type, user.resource)
if user.dial_suffix:
channel += '/%s' % user.dial_suffix
try:
# originate <csv global vars>user/2005 1005 DP_TYPE DP_NAME
# 'Caller ID name showed to aleg' 90125
dial_string = (('<' + variable + '>') if variable else '') + \
channel + ' ' + fs_number + ' ' + fs_server.context + ' ' + \
'\'' + self.get_name_from_phone_number(fs_number) + '\' ' + \
fs_number
fs_manager.api('originate', dial_string.encode('utf-8'))
except Exception, e:
_logger.error(
"Error in the Originate request to FreeSWITCH server %s",
fs_server.ip_address)
_logger.error(
"Here are the details of the error: '%s'", unicode(e))
raise UserError(
_("Click to dial with FreeSWITCH failed.\nHere is the error: "
"'%s'")
% unicode(e))
finally:
fs_manager.disconnect()
res['dialed_number'] = fs_number
return res