445 lines
20 KiB
Python
445 lines
20 KiB
Python
#This file is part asterisk module for Tryton.
|
|
#The COPYRIGHT file at the top level of this repository contains
|
|
#the full copyright notices and license terms.
|
|
from trytond.model import ModelView, ModelSQL, ModelSingleton, fields
|
|
from trytond.pool import Pool
|
|
from trytond.transaction import Transaction
|
|
import logging
|
|
import socket
|
|
import unicodedata
|
|
import time
|
|
|
|
__all__ = [ 'AsteriskConfiguration', 'AsteriskConfigurationCompany']
|
|
|
|
|
|
class AsteriskConfiguration(ModelSingleton, ModelSQL, ModelView):
|
|
'Asterisk Configuration'
|
|
__name__ = 'asterisk.configuration'
|
|
name = fields.Function(fields.Char('Asterisk server name', required=True,
|
|
help="Asterisk server name."), 'get_fields', setter='set_fields')
|
|
ip_address = fields.Function(fields.Char('Asterisk IP addr. or DNS',
|
|
required=True,
|
|
help="IPv4 address or DNS name of the Asterisk server."),
|
|
'get_fields', setter='set_fields')
|
|
port = fields.Function(fields.Integer('Port', required=True,
|
|
help="TCP port on which the Asterisk Manager Interface listens. "
|
|
"Defined in /etc/asterisk/manager.conf on Asterisk."),
|
|
'get_fields', setter='set_fields')
|
|
out_prefix = fields.Function(fields.Char('Out prefix',
|
|
help="Prefix to dial to place outgoing calls. If you don't use a "
|
|
"prefix to place outgoing calls, leave empty."),
|
|
'get_fields', setter='set_fields')
|
|
national_prefix = fields.Function(fields.Char('National prefix',
|
|
help="Prefix for national phone calls (don't include the 'out"
|
|
" prefix'). For example, in France, the phone numbers looks like "
|
|
"'01 41 98 12 42': the National prefix is '0'."),
|
|
'get_fields', setter='set_fields')
|
|
international_prefix = fields.Function(fields.Char('International prefix',
|
|
help="Prefix to add to make international phone calls (don't "
|
|
"include the 'out prefix'). For example, in France, the "
|
|
"International prefix is '00'."),
|
|
'get_fields', setter='set_fields')
|
|
country_prefix = fields.Function(fields.Char('My country prefix',
|
|
required=True,
|
|
help="Prefix to add to make international phone calls (don't "
|
|
"include the 'out prefix'). For example, the phone prefix for "
|
|
"France is '33'. If the phone number to dial starts with the 'My "
|
|
"country prefix', Tryton will remove the country prefix from "
|
|
"the phone number and add the 'out prefix' followed by the "
|
|
"'national prefix'. If the phone number to dial doesn't start "
|
|
"with the 'My country prefix', Tryton will add the 'out "
|
|
"prefix' followed by the 'international prefix'."),
|
|
'get_fields', setter='set_fields')
|
|
national_format_allowed = fields.Function(fields.Boolean(
|
|
'National format allowed?',
|
|
help="Do we allow to use click2dial on phone numbers written in "
|
|
"national format, e.g. 01 41 98 12 42, or only in the "
|
|
"international format, e.g. +34 1 41 98 12 42 ?"),
|
|
'get_fields', setter='set_fields')
|
|
login = fields.Function(fields.Char('AMI login', required=True,
|
|
help="Login that Tryton will use to communicate with the Asterisk "
|
|
"Manager Interface. Refer to /etc/asterisk/manager.conf on "
|
|
"your Asterisk server."),
|
|
'get_fields', setter='set_fields')
|
|
password = fields.Function(fields.Char('AMI password', required=True,
|
|
help="Password that Asterisk will use to communicate with the "
|
|
"Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf "
|
|
"on your Asterisk server."),
|
|
'get_fields', setter='set_fields')
|
|
context = fields.Function(fields.Char('Dialplan context', required=True,
|
|
help="Asterisk dialplan context from which the calls will be "
|
|
"made. Refer to /etc/asterisk/extensions.conf on your Asterisk "
|
|
"server."),
|
|
'get_fields', setter='set_fields')
|
|
wait_time = fields.Function(fields.Integer('Wait time (sec)',
|
|
required=True,
|
|
help="Amount of time (in seconds) Asterisk will try to reach the "
|
|
"user's phone before hanging up."),
|
|
'get_fields', setter='set_fields')
|
|
extension_priority = fields.Function(fields.Integer('Extension priority',
|
|
required=True,
|
|
help="Priority of the extension in the Asterisk dialplan. Refer "
|
|
"to /etc/asterisk/extensions.conf on your Asterisk server."),
|
|
'get_fields', setter='set_fields')
|
|
alert_info = fields.Function(fields.Char('Alert-Info SIP header',
|
|
help="Set Alert-Info header in SIP request to user's IP Phone. If "
|
|
"empty, the Alert-Info header will not be added. You can use "
|
|
"it to have a special ring tone for click2dial, for example "
|
|
"you could choose a silent ring tone."),
|
|
'get_fields', setter='set_fields')
|
|
|
|
@classmethod
|
|
def get_fields(cls, configurations, names):
|
|
res = {}
|
|
ConfigurationCompany = Pool().get('asterisk.configuration.company')
|
|
company_id = Transaction().context.get('company')
|
|
conf_id = configurations[0].id
|
|
if company_id:
|
|
confs = ConfigurationCompany.search([
|
|
('company', '=', company_id),
|
|
], limit=1)
|
|
for conf in confs:
|
|
for field_name in names:
|
|
value = getattr(conf, field_name)
|
|
res[field_name] = {conf_id: value}
|
|
else:
|
|
cls.raise_user_error('not_company')
|
|
return res
|
|
|
|
@classmethod
|
|
def set_fields(cls, configurations, name, value):
|
|
if value:
|
|
ConfigurationCompany = Pool().get('asterisk.configuration.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id:
|
|
configuration = ConfigurationCompany.search([
|
|
('company', '=', company_id),
|
|
], limit=1)
|
|
if not configuration:
|
|
ConfigurationCompany.create([{
|
|
'company': company_id,
|
|
name: value,
|
|
}])
|
|
else:
|
|
ConfigurationCompany.write([configuration[0]], {
|
|
name: value
|
|
})
|
|
|
|
def _only_digits(self, prefix, can_be_empty):
|
|
prefix_to_check = self.read([self.id], [prefix])[0]
|
|
if prefix_to_check:
|
|
prefix_to_check = prefix_to_check[prefix]
|
|
if not prefix_to_check:
|
|
if not can_be_empty:
|
|
return False
|
|
else:
|
|
if not prefix_to_check.isdigit():
|
|
return False
|
|
return True
|
|
|
|
def _only_digits_out_prefix(self):
|
|
return self._only_digits('out_prefix', True)
|
|
|
|
def _only_digits_country_prefix(self):
|
|
return self._only_digits('country_prefix', False)
|
|
|
|
def _only_digits_national_prefix(self):
|
|
return self._only_digits('national_prefix', True)
|
|
|
|
def _only_digits_international_prefix(self):
|
|
return self._only_digits('international_prefix', False)
|
|
|
|
def _check_wait_time(self):
|
|
wait_time_to_check = self.read([self.id], ['wait_time'])[0]\
|
|
['wait_time']
|
|
if wait_time_to_check < 1 or wait_time_to_check > 120:
|
|
return False
|
|
return True
|
|
|
|
def _check_extension_priority(self):
|
|
extension_priority_to_check = self.read([self.id],
|
|
['extension_priority'])[0]['extension_priority']
|
|
if extension_priority_to_check < 1:
|
|
return False
|
|
return True
|
|
|
|
def _check_port(self):
|
|
port_to_check = self.read([self.id], ['port'])[0]['port']
|
|
if int(port_to_check) > 65535 or port_to_check < 1:
|
|
return False
|
|
return True
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(AsteriskConfiguration, cls).__setup__()
|
|
cls._constraints += [
|
|
('_only_digits_out_prefix', 'out_prefix'),
|
|
('_only_digits_country_prefix', 'country_prefix'),
|
|
('_only_digits_national_prefix', 'national_prefix'),
|
|
('_only_digits_international_prefix', 'international_prefix'),
|
|
('_check_wait_time', 'wait_time'),
|
|
('_check_extension_priority', 'extension_priority'),
|
|
('_check_port', 'port'),
|
|
]
|
|
cls._error_messages.update({
|
|
'not_company': "You have not got the default company configured.",
|
|
'out_prefix': "Use only digits for the 'Out prefix' or leave "
|
|
"it empty.",
|
|
'country_prefix': "Use only digits for the 'Country prefix'.",
|
|
'national_prefix': "Use only digits for the 'National prefix' "
|
|
"or leave it empty.",
|
|
'international_prefix': "Use only digits for 'International "
|
|
"prefix'.",
|
|
'wait_time': "You should enter a 'Wait time' value between 1 "
|
|
"and 120 seconds.",
|
|
'extension_priority': "The 'Extension priority' must be a "
|
|
"positive value.",
|
|
'port': 'TCP ports range from 1 to 65535.',
|
|
'error': 'Error',
|
|
'invalid_phone': 'Invalid phone number',
|
|
'invalid_international_format': "The phone number is not "
|
|
"written in a valid international format. Example of valid "
|
|
"international format: +33 1 41 98 12 42.",
|
|
'invalid_national_format': "The phone number is not written "
|
|
"in a valid national format.",
|
|
'invalid_format': "The phone number is not written in a valid "
|
|
"format.",
|
|
'no_phone_number': "There is no phone number.",
|
|
'no_asterisk_configuration': "Not available Asterisk Server "
|
|
"configured for the current user.",
|
|
'no_channel_type': "There isn't a channel type configured for "
|
|
"the current user",
|
|
'no_internal_phone': "There isn't a internal phone number "
|
|
"configured for the current user",
|
|
'cant_resolve_dns': "Can't resolve the DNS of the Asterisk "
|
|
"server:",
|
|
'connection_failed': "The connection from Tryton to the "
|
|
"Asterisk server has failed. Please check the configuration "
|
|
"on Tryton and Asterisk.",
|
|
})
|
|
|
|
@staticmethod
|
|
def default_port():
|
|
return 5038
|
|
|
|
@staticmethod
|
|
def default_out_prefix():
|
|
return '0'
|
|
|
|
@staticmethod
|
|
def default_national_prefix():
|
|
return '0'
|
|
|
|
@staticmethod
|
|
def default_international_prefix():
|
|
return '00'
|
|
|
|
@staticmethod
|
|
def default_extension_priority():
|
|
return 1
|
|
|
|
@staticmethod
|
|
def default_wait_time():
|
|
return 5
|
|
|
|
@staticmethod
|
|
def unaccent(text):
|
|
if isinstance(text, str):
|
|
text = unicode(text, 'utf-8')
|
|
return unicodedata.normalize('NFKD', text).encode('ASCII',
|
|
'ignore')
|
|
|
|
@classmethod
|
|
def reformat_number(cls, tryton_number, ast_server):
|
|
'''
|
|
This method transforms the number available in Tryton to the number
|
|
that Asterisk should dial.
|
|
'''
|
|
logger = logging.getLogger('asterisk')
|
|
|
|
# Let's call the variable tmp_number now
|
|
tmp_number = tryton_number
|
|
logger.debug('Number before reformat = %s' % tmp_number)
|
|
|
|
# Check if empty
|
|
if not tmp_number:
|
|
cls.raise_user_error(error='invalid_phone',
|
|
error_description='invalid_format')
|
|
|
|
# First, we remove all stupid characters and spaces
|
|
for i in [' ', '.', '(', ')', '[', ']', '-', '/']:
|
|
tmp_number = tmp_number.replace(i, '')
|
|
|
|
# Before starting to use prefix, we convert empty prefix whose value
|
|
# is False to an empty string
|
|
country_prefix = (ast_server.country_prefix or '')
|
|
national_prefix = (ast_server.national_prefix or '')
|
|
international_prefix = (ast_server.international_prefix or '')
|
|
out_prefix = (ast_server.out_prefix or '')
|
|
|
|
# International format
|
|
if tmp_number[0] == '+':
|
|
# Remove the starting '+' of the number
|
|
tmp_number = tmp_number.replace('+','')
|
|
logger.debug('Number after removal of special char = %s' % \
|
|
tmp_number)
|
|
|
|
# At this stage, 'tmp_number' should only contain digits
|
|
if not tmp_number.isdigit():
|
|
cls.raise_user_error(error='invalid_phone',
|
|
error_description='invalid_format_msg')
|
|
|
|
logger.debug('Country prefix = ' + country_prefix)
|
|
if country_prefix == tmp_number[0:len(country_prefix)]:
|
|
# If the number is a national number,
|
|
# remove 'my country prefix' and add 'national prefix'
|
|
tmp_number = (national_prefix) + tmp_number[
|
|
len(country_prefix):len(tmp_number)]
|
|
logger.debug('National prefix = %s - Number with national '
|
|
'prefix = %s' % (national_prefix, tmp_number))
|
|
else:
|
|
# If the number is an international number,
|
|
# add 'international prefix'
|
|
tmp_number = international_prefix + tmp_number
|
|
logger.debug('International prefix = %s - Number with '
|
|
'international prefix = %s' % (international_prefix,
|
|
tmp_number))
|
|
# National format, allowed
|
|
elif ast_server.national_format_allowed:
|
|
# No treatment required
|
|
if not tmp_number.isdigit():
|
|
cls.raise_user_error(error='invalid_phone',
|
|
error_description='invalid_national_format')
|
|
|
|
# National format, disallowed
|
|
elif not ast_server.national_format_allowed:
|
|
cls.raise_user_error(error='invalid_phone',
|
|
error_description='invalid_international_format')
|
|
# Add 'out prefix' to all numbers
|
|
tmp_number = out_prefix + tmp_number
|
|
logger.debug('Out prefix = %s - Number to be sent to Asterisk = %s' % \
|
|
(out_prefix, tmp_number))
|
|
return tmp_number
|
|
|
|
@classmethod
|
|
def dial(cls, party, tryton_number):
|
|
'''
|
|
Open the socket to the Asterisk Manager Interface (AMI)
|
|
and send instructions to Dial to Asterisk.
|
|
'''
|
|
logger = logging.getLogger('asterisk')
|
|
User = Pool().get('res.user')
|
|
user_id = Transaction().user
|
|
if user_id == 0 and 'user' in Transaction().context:
|
|
user_id = Transaction().context['user']
|
|
user = User(user_id)
|
|
|
|
# Check if the number to dial is not empty
|
|
if not tryton_number:
|
|
cls.raise_user_error(error='error',
|
|
error_description='no_phone_number')
|
|
|
|
# We check if the user has an Asterisk server configured
|
|
if not user.asterisk_server:
|
|
cls.raise_user_error(error='error',
|
|
error_description='no_asterisk_configuration')
|
|
else:
|
|
ast_server = user.asterisk_server
|
|
|
|
# We check if the current user has a chan type
|
|
if not user.asterisk_chan_type:
|
|
cls.raise_user_error(error='error',
|
|
error_description='no_channel_type')
|
|
|
|
# We check if the current user has an internal number
|
|
if not user.internal_number:
|
|
cls.raise_user_error(error='error',
|
|
error_description='no_internal_phone')
|
|
|
|
# The user should also have a CallerID, but in Spain that will
|
|
# be the name of the address that we call.
|
|
if not user.callerid:
|
|
#Party = Pool().get('party.party')
|
|
#callerid = Party.search(party).get_name_for_display
|
|
callerid = party.display_name
|
|
else:
|
|
callerid = user.CallerId
|
|
|
|
# Convert the phone number in the format that will be sent to Asterisk
|
|
ast_number = cls.reformat_number(tryton_number, ast_server)
|
|
logger.info('User dialing: channel = %s/%s - Callerid = %s' % \
|
|
(user.asterisk_chan_type, user.internal_number, user.callerid))
|
|
logger.info('Asterisk server = %s:%s' % \
|
|
(ast_server.ip_address, ast_server.port))
|
|
|
|
# Connect to the Asterisk Manager Interface, using IPv6-ready code
|
|
try:
|
|
res = socket.getaddrinfo(ast_server.ip_address, ast_server.port,
|
|
socket.AF_UNSPEC, socket.SOCK_STREAM)
|
|
except:
|
|
logger.error("Can't resolve the DNS of the Asterisk server : %s" %\
|
|
str(ast_server.ip_address))
|
|
cls.raise_user_error(error='error',
|
|
error_description='cant_resolve_dns')
|
|
for result in res:
|
|
af, socktype, proto, _, sockaddr = result
|
|
try:
|
|
sock = socket.socket(af, socktype, proto)
|
|
sock.connect(sockaddr)
|
|
sock.send('Action: login\r\n')
|
|
sock.send('Events: off\r\n')
|
|
sock.send('Username: %s\r\n' % str(ast_server.login))
|
|
sock.send('Secret: %s\r\n\r\n' % str(ast_server.password))
|
|
sock.send('Action: originate\r\n')
|
|
sock.send('Channel: %s/%s\r\n' % (str(user.asterisk_chan_type),
|
|
str(user.internal_number)))
|
|
sock.send('Timeout: %s\r\n' % str(ast_server.wait_time*1000))
|
|
sock.send('CallerId: %s\r\n' % cls.unaccent(callerid))
|
|
sock.send('Exten: %s\r\n' % str(ast_number))
|
|
sock.send('Context: %s\r\n' % str(ast_server.context))
|
|
if ast_server.alert_info and user.asterisk_chan_type == 'SIP':
|
|
sock.send('Variable: SIPAddHeader=Alert-Info: %s\r\n' % \
|
|
str(ast_server.alert_info))
|
|
sock.send('Priority: %s\r\n\r\n' % \
|
|
str(ast_server.extension_priority))
|
|
time.sleep(1)
|
|
sock.send('Action: Logoff\r\n\r\n')
|
|
sock.close()
|
|
except:
|
|
logger.debug("Click2dial failed: unable to connect to "
|
|
"Asterisk")
|
|
cls.raise_user_error(error='error',
|
|
error_description='connection_failed')
|
|
logger.info("Asterisk Click2Dial from %s to %s" % \
|
|
(user.internal_number, ast_number))
|
|
|
|
|
|
class AsteriskConfigurationCompany(ModelSQL, ModelView):
|
|
'Asterisk Configuration Company'
|
|
__name__ = 'asterisk.configuration.company'
|
|
|
|
company = fields.Many2One('company.company', 'Company', readonly=True)
|
|
name = fields.Char('Asterisk server name')
|
|
ip_address = fields.Char('Asterisk IP addr. or DNS')
|
|
port = fields.Integer('Port')
|
|
out_prefix = fields.Char('Out prefix')
|
|
national_prefix = fields.Char('National prefix')
|
|
international_prefix = fields.Char('International prefix')
|
|
country_prefix = fields.Char('My country prefix')
|
|
national_format_allowed = fields.Boolean('National format allowed?')
|
|
login = fields.Char('AMI login')
|
|
#TODO: Make not visible the password
|
|
password = fields.Char('AMI password')
|
|
context = fields.Char('Dialplan context')
|
|
wait_time = fields.Integer('Wait time (sec)')
|
|
extension_priority = fields.Integer('Extension priority')
|
|
alert_info = fields.Char('Alert-Info SIP header')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(AsteriskConfigurationCompany, cls).__setup__()
|
|
cls._sql_constraints += [
|
|
('company_uniq', 'UNIQUE(company)',
|
|
'There is already one configuration for this company.'),
|
|
]
|