summaryrefslogtreecommitdiff
path: root/misc/freeswitch/scripts/dialplan
diff options
context:
space:
mode:
Diffstat (limited to 'misc/freeswitch/scripts/dialplan')
-rw-r--r--misc/freeswitch/scripts/dialplan/access_authorizations.lua52
-rw-r--r--misc/freeswitch/scripts/dialplan/acd.lua484
-rw-r--r--misc/freeswitch/scripts/dialplan/callthrough.lua148
-rw-r--r--misc/freeswitch/scripts/dialplan/cdr.lua71
-rw-r--r--misc/freeswitch/scripts/dialplan/dialplan.lua996
-rw-r--r--misc/freeswitch/scripts/dialplan/fax.lua232
-rw-r--r--misc/freeswitch/scripts/dialplan/functions.lua839
-rw-r--r--misc/freeswitch/scripts/dialplan/geo_number.lua89
-rw-r--r--misc/freeswitch/scripts/dialplan/hunt_group.lua202
-rw-r--r--misc/freeswitch/scripts/dialplan/phone_book.lua63
-rw-r--r--misc/freeswitch/scripts/dialplan/presence.lua84
-rw-r--r--misc/freeswitch/scripts/dialplan/route.lua265
-rw-r--r--misc/freeswitch/scripts/dialplan/session.lua224
-rw-r--r--misc/freeswitch/scripts/dialplan/sip_call.lua266
-rw-r--r--misc/freeswitch/scripts/dialplan/tenant.lua51
-rw-r--r--misc/freeswitch/scripts/dialplan/user.lua91
-rw-r--r--misc/freeswitch/scripts/dialplan/voicemail.lua155
17 files changed, 4312 insertions, 0 deletions
diff --git a/misc/freeswitch/scripts/dialplan/access_authorizations.lua b/misc/freeswitch/scripts/dialplan/access_authorizations.lua
new file mode 100644
index 0000000..dbacf20
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/access_authorizations.lua
@@ -0,0 +1,52 @@
+-- CommonModule: AccessAuthorization
+--
+module(...,package.seeall)
+
+AccessAuthorization = {}
+
+-- Create AccessAuthorization object
+function AccessAuthorization.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.record = arg.record
+ self.session = arg.session
+ return object
+end
+
+-- Find AccessAuthorization by ID
+function AccessAuthorization.find_by_id(self, id)
+ local sql_query = string.format("SELECT * FROM `access_authorizations` WHERE `id`=%d LIMIT 1", id)
+ local record = nil
+
+ self.database:query(sql_query, function(access_authorization_entry)
+ record = access_authorization_entry
+ end)
+
+ if record then
+ access_authorization = AccessAuthorization:new(self)
+ access_authorization.record = record
+ return access_authorization
+ end
+
+ return nil
+end
+
+-- list accessauthorization by owner
+function AccessAuthorization.list_by_owner(self, owner_id, owner_type)
+ local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`login`, `a`.`pin`, `a`.`sip_account_id`, `b`.`number` AS `phone_number` \
+ FROM `access_authorizations` `a` \
+ LEFT JOIN `phone_numbers` `b` ON `b`.`phone_numberable_id` = `a`.`id` AND `b`.`phone_numberable_type` = "AccessAuthorization" \
+ WHERE `a`.`access_authorizationable_type` = "' .. owner_type .. '" AND `access_authorizationable_id`= ' .. tonumber(owner_id);
+
+ local access_authorizations = {}
+
+ self.database:query(sql_query, function(access_authorization_entry)
+ table.insert(access_authorizations, access_authorization_entry);
+ end);
+
+ return access_authorizations;
+end
diff --git a/misc/freeswitch/scripts/dialplan/acd.lua b/misc/freeswitch/scripts/dialplan/acd.lua
new file mode 100644
index 0000000..563d836
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/acd.lua
@@ -0,0 +1,484 @@
+-- Gemeinschaft 5 module: acd class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+AutomaticCallDistributor = {}
+
+local DEFAULT_AGENT_TIMEOUT = 20;
+local DEFAULT_TIME_RES = 5;
+local DEFAULT_WAIT_TIMEOUT = 360;
+local DEFAULT_RETRY_TIME = 2;
+local DEFAULT_MUSIC_ON_WAIT = 'tone_stream://%(2000,4000,440.0,480.0);loops=-1';
+
+-- create acd object
+function AutomaticCallDistributor.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'automaticcalldistributor';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.acd_caller_id = arg.acd_caller_id;
+ self.domain = arg.domain;
+ return object;
+end
+
+
+function AutomaticCallDistributor.find_by_sql(self, sql_query)
+ local acd = nil;
+
+ require 'common.str'
+
+ self.database:query(sql_query, function(entry)
+ acd = AutomaticCallDistributor:new(self);
+ acd.record = entry;
+ acd.id = tonumber(entry.id);
+ acd.uuid = entry.uuid;
+ acd.agent_timeout = tonumber(entry.agent_timeout) or DEFAULT_AGENT_TIMEOUT;
+ acd.announce_position = tonumber(entry.announce_position);
+ acd.announce_call_agents = common.str.to_s(entry.announce_call_agents);
+ acd.greeting = common.str.to_s(entry.greeting);
+ acd.goodbye = common.str.to_s(entry.goodbye);
+ acd.music = common.str.to_s(entry.music);
+ acd.strategy = common.str.to_s(entry.strategy);
+ acd.join = common.str.to_s(entry.join);
+ acd.leave = common.str.to_s(entry.leave);
+ end)
+
+ return acd;
+end
+
+
+function AutomaticCallDistributor.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1';
+ return self:find_by_sql(sql_query);
+end
+
+
+function AutomaticCallDistributor.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `uuid`= "'.. tostring(uuid) .. '" LIMIT 1';
+ return self:find_by_sql(sql_query);
+end
+
+
+function AutomaticCallDistributor.callers_count(self)
+ return self.database:query_return_value('SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` WHERE `automatic_call_distributor_id` = ' .. self.id);
+end
+
+
+function AutomaticCallDistributor.caller_new(self, uuid)
+ local sql_query = 'INSERT INTO `acd_callers` \
+ (`enter_time`, `created_at`, `updated_at`, `status`, `automatic_call_distributor_id`, `channel_uuid`) \
+ VALUES (NOW(), NOW(), NOW(), "enter", ' .. self.id .. ', "' .. uuid .. '")';
+
+ if self.database:query(sql_query) then
+ self.acd_caller_id = self.database:last_insert_id();
+ end
+end
+
+
+function AutomaticCallDistributor.caller_update(self, attributes)
+ local attributes_sql = { '`updated_at` = NOW()' };
+ for key, value in pairs(attributes) do
+ table.insert(attributes_sql, '`' .. key .. '` = "' .. value .. '"');
+ end
+
+ local sql_query = 'UPDATE `acd_callers` \
+ SET '.. table.concat(attributes_sql, ',') .. '\
+ WHERE `id` = ' .. tonumber(self.acd_caller_id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.caller_delete(self, id)
+ id = id or self.acd_caller_id;
+ local sql_query = 'DELETE FROM `acd_callers` \
+ WHERE `id` = ' .. tonumber(id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.agent_find_by_acd_and_destination(self, acd_id, destination_type, destination_id)
+ local sql_query = 'SELECT * FROM `acd_agents` \
+ WHERE `automatic_call_distributor_id` = ' .. acd_id .. ' \
+ AND `destination_type` = "' .. destination_type .. '" \
+ AND `destination_id` = ' .. destination_id;
+
+ local agent = nil;
+ self.database:query(sql_query, function(entry)
+ agent = entry;
+ end)
+
+ return agent;
+end
+
+
+function AutomaticCallDistributor.agent_status_presence_set(self, agent_id, presence_state)
+ require "dialplan.presence"
+ local presence = dialplan.presence.Presence:new();
+
+ presence:init{log = self.log, accounts = { 'f-acdmtg-' .. tostring(agent_id) }, domain = self.domain, uuid = 'acd_agent_' .. tostring(agent_id)};
+ return presence:set(presence_state);
+end
+
+
+function AutomaticCallDistributor.agent_status_get(self, agent_id)
+ local sql_query = 'SELECT `status` FROM `acd_agents` WHERE `id` = ' .. agent_id;
+ return self.database:query_return_value(sql_query);
+end
+
+
+function AutomaticCallDistributor.agent_status_toggle(self, agent_id, destination_type, destination_id)
+ local sql_query = 'UPDATE `acd_agents` SET `status` = IF(`status` = "active", "inactive", "active") \
+ WHERE `id` = ' .. agent_id .. ' \
+ AND `destination_type` = "' .. destination_type .. '" \
+ AND `destination_id` = ' .. destination_id;
+
+ if not self.database:query(sql_query) then
+ return nil;
+ end
+
+ local status = self:agent_status_get(agent_id);
+
+ if tostring(status) == 'active' then
+ self:agent_status_presence_set(agent_id, 'confirmed');
+ else
+ self:agent_status_presence_set(agent_id, 'terminated');
+ end
+
+ return status;
+end
+
+
+function AutomaticCallDistributor.agents_active(self)
+ local sql_query = 'SELECT * FROM `acd_agents` \
+ WHERE `status` = "active" AND destination_type != "SipAccount" AND `automatic_call_distributor_id` = ' .. tonumber(self.id);
+
+ local agents = {}
+ self.database:query(sql_query, function(entry)
+ table.insert(agents, entry);
+ end);
+
+ local sql_query = 'SELECT `a`.* FROM `acd_agents` `a` \
+ JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \
+ JOIN `sip_registrations` `c` ON `b`.`auth_name` = `c`.`sip_user` \
+ WHERE `a`.`status` = "active" AND `a`.destination_type = "SipAccount" AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id);
+
+ self.database:query(sql_query, function(entry)
+ table.insert(agents, entry);
+ end);
+
+ return agents;
+end
+
+
+function AutomaticCallDistributor.agents_available(self, strategy)
+ local order_by = '`a`.`id` DESC';
+
+ if strategy then
+ if strategy == 'round_robin' then
+ order_by = '`a`.`last_call` ASC, `a`.`id` DESC';
+ end
+ end
+
+ local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`destination_type`, `a`.`destination_id`, `b`.`auth_name`, `b`.`gs_node_id`, `c`.`callstate` \
+ FROM `acd_agents` `a` LEFT JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \
+ JOIN `sip_registrations` `d` ON `b`.`auth_name` = `d`.`sip_user` \
+ LEFT JOIN `channels` `c` ON `c`.`name` LIKE CONCAT("%", `b`.`auth_name`, "@%") \
+ WHERE `a`.`status` = "active" AND `a`.`destination_id` IS NOT NULL AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' \
+ ORDER BY ' .. order_by;
+
+ local accounts = {}
+ self.database:query(sql_query, function(entry)
+ if not entry.callstate then
+ table.insert(accounts, entry);
+ end
+ end);
+
+ return accounts;
+end
+
+
+function AutomaticCallDistributor.agent_update_call(self, agent_id)
+
+ local sql_query = 'UPDATE `acd_agents` \
+ SET `last_call` = NOW(), `calls_answered` = IFNULL(`calls_answered`, 0) + 1 \
+ WHERE `id` = ' .. tonumber(agent_id);
+ return self.database:query(sql_query);
+end
+
+
+function AutomaticCallDistributor.call_position(self)
+ local sql_query = 'SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` \
+ WHERE `automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' AND `status` = "waiting" AND `id` < ' .. tonumber(self.acd_caller_id);
+
+ return tonumber(self.database:query_return_value(sql_query));
+end
+
+
+function AutomaticCallDistributor.wait_turn(self, caller_uuid, acd_caller_id, timeout, retry_timeout)
+ self.acd_caller_id = acd_caller_id or self.acd_caller_id;
+ timeout = timeout or DEFAULT_WAIT_TIMEOUT;
+ local available_agents = {};
+ local active_agents = {};
+ local position = self:call_position();
+
+ self.log:info('ACD ', self.id, ' WAIT - timeout: ', timeout, ', res: ', DEFAULT_TIME_RES, ', retry_timeout: ', retry_timeout, ', position: ', position + 1);
+
+ require 'common.fapi'
+ local fapi = common.fapi.FApi:new{ log = self.log, uuid = caller_uuid }
+
+ local acd_status = nil;
+ local start_time = os.time();
+ local exit_time = start_time + timeout;
+
+ if tonumber(retry_timeout) then
+ self.log:info('ACD ', self.id, ' WAIT - retry_timeout: ', retry_timeout);
+ fapi:sleep(retry_timeout * 1000);
+ end
+
+ while (exit_time > os.time() and fapi:channel_exists()) do
+ available_agents = self:agents_available();
+ active_agents = self:agents_active();
+ local current_position = self:call_position();
+
+ if position ~= current_position then
+ position = current_position;
+ self.log:info('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+
+ if tostring(self.announce_position) >= '0' and position > 0 then
+ acd_status = 'announce_position';
+ fapi:set_variable('acd_position', position + 1);
+ break;
+ end
+ else
+ self.log:debug('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+ end
+
+ if #available_agents == 0 and self.leave:find('no_agents_available') then
+ acd_status = 'no_agents';
+ break;
+ elseif #active_agents == 0 and self.leave:find('no_agents_active') then
+ acd_status = 'no_agents';
+ break;
+ elseif position == 0 and #available_agents > 0 then
+ acd_status = 'call_agents';
+ break;
+ end
+
+ if tonumber(self.announce_position) and tonumber(self.announce_position) > 0 and tonumber(self.announce_position) <= os.time()-start_time then
+ acd_status = 'announce_position';
+ fapi:set_variable('acd_position', position + 1);
+ break;
+ end
+
+ fapi:sleep(DEFAULT_TIME_RES * 1000);
+ end
+
+ if not acd_status then
+ if (exit_time <= os.time()) then
+ acd_status = 'timeout';
+ else
+ acd_status = 'unspecified';
+ end
+ end
+
+ self.log:info('ACD ', self.id, ' WAIT END - status: ', acd_status, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time);
+
+ fapi:set_variable('acd_status', acd_status);
+ if tostring(fapi:get_variable('acd_waiting')) == 'true' then
+ fapi:continue();
+ end
+end
+
+
+function AutomaticCallDistributor.wait_play_music(self, caller, timeout, retry_timeout, music)
+ local result = caller:result('luarun(acd_wait.lua ' .. caller.uuid .. ' ' .. tostring(self.id) .. ' ' .. tostring(timeout) .. ' ' .. tostring(retry_timeout) .. ' ' .. self.acd_caller_id .. ')');
+ if not tostring(result):match('^+OK') then
+ self.log:error('ACD ', self.id,' WAIT_PLAY_MUSIC - error starting acd thread');
+ return 'error';
+ end
+
+ caller:set_variable('acd_waiting', true);
+ caller.session:streamFile(music or DEFAULT_MUSIC_ON_WAIT);
+ caller:set_variable('acd_waiting', false);
+
+ local acd_status = caller:to_s('acd_status');
+ if acd_status == '' then
+ acd_status = 'abandoned';
+ end
+
+ return acd_status;
+end
+
+
+function AutomaticCallDistributor.on_answer(self, destination)
+ self.log:info('ACD ', self.id, ' ANSWERED - agent: ', destination.type, '=', destination.id, '/', destination.uuid)
+ self:caller_update({status = 'answered'});
+end
+
+
+function AutomaticCallDistributor.call_agents(self, dialplan_object, caller, destination)
+ local available_agents = self:agents_available(self.strategy);
+
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - strategy: ', self.strategy, ', available_agents: ', #available_agents);
+
+ caller:set_variable('ring_ready', true);
+
+ local destinations = {}
+ for index, agent in ipairs(available_agents) do
+ self.log:info('ACD ', self.id, ' AGENT - name: ', agent.name, ', destination: ', agent.destination_type, '=', agent.destination_id, '@', agent.gs_node_id, ', local_node: ', dialplan_object.node_id);
+ table.insert(destinations, dialplan_object:destination_new{ type = agent.destination_type, id = agent.destination_id, node_id = agent.gs_node_id, data = agent.id });
+ end
+
+ local result = { continue = false };
+ local start_time = os.time();
+
+ require 'dialplan.sip_call'
+ if self.strategy == 'ring_all' then
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork(destinations,
+ {
+ callee_id_number = destination.number,
+ timeout = self.agent_timeout,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - success, fork_index: ', result.fork_index);
+ if result.fork_index then
+ result.destination = destinations[result.fork_index];
+ end
+ return result;
+ else
+ for index, destination in ipairs(destinations) do
+ if os.time() > (self.start_time + self.timeout) and caller.session:ready() then
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - timeout');
+ return { disposition = 'ACD_TIMEOUT', code = 480, phrase = 'Timeout' }
+ end
+
+ self.log:info('ACD ', self.id, ' CALL_AGENT - ', destination.type, '=', destination.id, ', timeout: ', self.agent_timeout);
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork({ destination },
+ {
+ callee_id_number = destination.number,
+ timeout = self.agent_timeout,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ if result.disposition == 'SUCCESS' then
+ self.log:info('ACD ', self.id, ' CALL_AGENTS - success, agent_id: ', destination.data);
+ self:agent_update_call(destination.data);
+ result.destination = destination;
+ return result;
+ end
+ end
+ end
+
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' }
+end
+
+
+function AutomaticCallDistributor.run(self, dialplan_object, caller, destination)
+ require 'common.str'
+
+ local callers_count = self:callers_count();
+ local active_agents = self:agents_active();
+ local available_agents = self:agents_available();
+ local position = self:call_position();
+
+ if self.leave:find('timeout') then
+ self.timeout = dialplan_object.dial_timeout_active;
+ else
+ self.timeout = 86400;
+ end
+
+ self.log:info('ACD ', self.id,' - ', self.class, '=', self.id, '/', self.uuid, ', acd_caller=', self.acd_caller_id, ', callers: ', callers_count, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', music: ', tostring(self.music));
+
+ if self.join == 'agents_active' and #active_agents == 0 then
+ self.log:info('ACD ', self.id, ' - no agents active');
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No agents' }
+ end
+
+ if self.join == 'agents_available' and #available_agents == 0 then
+ self.log:info('ACD ', self.id, ' - no agents available');
+ return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'All agents busy' }
+ end
+
+ if not common.str.blank(self.music) then
+ caller:set_variable('ringback', self.music);
+ else
+ self.music = false;
+ end
+
+ if self.music then
+ caller.session:answer();
+ else
+ caller:set_variable('instant_ringback', true);
+ end
+
+ self.start_time = os.time();
+ caller:sleep(500);
+ local acd_status = 'waiting';
+ self:caller_update({status = acd_status});
+
+ local retry_timeout = nil;
+ local result = { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' }
+
+ if self.greeting then
+ caller.session:sayPhrase('acd_greeting', self.greeting);
+ end
+
+ if self.announce_position then
+ local current_position = self:call_position();
+ if tonumber(current_position) then
+ caller.session:sayPhrase('acd_announce_position_enter', tonumber(current_position) + 1);
+ end
+ end
+
+ while acd_status == 'waiting' and caller.session:ready() do
+ acd_status = self:wait_play_music(caller, self.timeout - (os.time() - self.start_time), retry_timeout, self.music);
+ self.log:info('ACD ', self.id, ' PROCESS - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time));
+
+ if not caller.session:ready() then
+ acd_status = 'abandoned';
+ break;
+ elseif os.time() >= (self.start_time + self.timeout) then
+ acd_status = 'timeout';
+ break;
+ elseif acd_status == 'no_agents' then
+ break;
+ elseif acd_status == 'call_agents' then
+ if self.announce_call_agents ~= '' then
+ caller.session:sayPhrase('acd_announce_call_agents', self.announce_call_agents);
+ end
+
+ result = self:call_agents(dialplan_object, caller, destination);
+ self.log:info('ACD ', self.id, ' PROCESS - result: ', result.disposition, ', code: ', result.code, ', wait_time: ', (os.time() - self.start_time));
+
+ if result.disposition == 'SUCCESS' then
+ acd_status = 'success';
+ break;
+ elseif os.time() < (self.start_time + self.timeout) then
+ acd_status = 'waiting';
+ else
+ break;
+ end
+ elseif acd_status == 'announce_position' then
+ acd_status = 'waiting';
+ if tostring(self.announce_position) == '0' then
+ caller.session:sayPhrase('acd_announce_position_change', caller:to_i('acd_position'));
+ else
+ caller.session:sayPhrase('acd_announce_position_periodic', caller:to_i('acd_position'));
+ end
+ end
+
+ retry_timeout = tonumber(self.record.retry_timeout);
+ end
+
+ if self.goodbye and caller.session:ready() then
+ caller.session:sayPhrase('acd_goodbye', self.goodbye);
+ end
+ self.log:info('ACD ', self.id, ' EXIT - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time));
+
+ return result;
+end
diff --git a/misc/freeswitch/scripts/dialplan/callthrough.lua b/misc/freeswitch/scripts/dialplan/callthrough.lua
new file mode 100644
index 0000000..69a0611
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/callthrough.lua
@@ -0,0 +1,148 @@
+-- CommonModule: Callthrough
+--
+module(...,package.seeall)
+
+Callthrough = {}
+
+-- Create Callthrough object
+function Callthrough.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.record = arg.record
+ self.access_authorizations = arg.access_authorizations
+ return object
+end
+
+-- Find Callthrough by ID
+function Callthrough.find_by_id(self, id)
+ local sql_query = string.format("SELECT * FROM `callthroughs` WHERE `id`=%d LIMIT 1", id)
+ local record = nil
+
+ self.database:query(sql_query, function(callthrough_entry)
+ record = callthrough_entry
+ end)
+
+ if record then
+ local callthrough = Callthrough:new(self);
+ callthrough.record = record
+
+ require 'dialplan.access_authorizations'
+ callthrough.access_authorizations = dialplan.access_authorizations.AccessAuthorization:new{ log = self.log, database = self.database }:list_by_owner(record.id, 'Callthrough');
+ return callthrough
+ end
+
+ return nil
+end
+
+function Callthrough.authenticate(self, caller)
+ local authorizations = {}
+ local logins = {}
+ local pins = {}
+
+ caller:answer();
+ caller:sleep(1000);
+
+ if not self.access_authorizations or table.getn(self.access_authorizations) == 0 then
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - authorization disabled');
+ return true;
+ end
+
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorizations: ', #self.access_authorizations);
+ for index, authorization in ipairs(self.access_authorizations) do
+ if authorization.phone_number then
+ if authorization.phone_number == caller.caller_phone_number then
+ if authorization.pin and authorization.pin ~= "" then
+ if caller.session:read(authorization.pin:len(), authorization.pin:len(), "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#") ~= authorization.pin then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN");
+ return false;
+ else
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number .. " and PIN");
+ return authorization;
+ end
+ end
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number);
+ return authorization;
+ end
+ else
+ self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorization=', authorization.id);
+ if authorization.id then
+ authorizations[authorization.id] = authorization;
+ if authorization.login and authorization.login ~= "" then
+ logins[authorization.login] = authorization;
+ elseif authorization.pin and authorization.pin ~= "" then
+ pins[authorization.pin] = authorization;
+ end
+ end
+ end
+ end
+
+ local login = nil;
+ local pin = nil;
+
+
+ if next(logins) ~= nil then
+
+ caller.session:streamFile('ivr/ivr-please_enter_the.wav');
+ caller.session:streamFile('ivr/ivr-id_number.wav');
+ login = caller.session:read(2, 10, 'ivr/ivr-followed_by_pound.wav', 3000, '#');
+ end
+
+ if login and logins[tostring(login)] then
+ if not logins[tostring(login)].pin or logins[tostring(login)].pin == '' then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " without PIN");
+ return logins[tostring(login)];
+ end
+ pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#");
+ if logins[tostring(login)].pin == pin then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " and PIN");
+ return logins[tostring(login)];
+ else
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN");
+ return false
+ end
+ end
+
+ if next(pins) ~= nil then
+ pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#");
+ end
+
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - No such login, try PIN");
+
+ if pin and pins[tostring(pin)] then
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by PIN");
+ return pins[tostring(pin)];
+ end
+
+ self.log:debug("CALLTHROUGH_AUTHENTICATE - No login, wrong PIN - giving up");
+
+ return false;
+end
+
+function Callthrough.whitelist(self, number)
+ local sql_query = 'SELECT `id` FROM `whitelists` WHERE `whitelistable_type` = "Callthrough" AND `whitelistable_id` = ' .. self.record.id;
+ local whitelist_ids = {}
+
+ self.database:query(sql_query, function(entry)
+ table.insert(whitelist_ids, entry.id);
+ end)
+
+ if next(whitelist_ids) == nil then
+ return true;
+ end
+
+ -- OPTIMIZE Make sure number contains only valid characters
+ local sql_query = 'SELECT `id` FROM `phone_numbers` WHERE \
+ `number` = "' .. number .. '" AND \
+ `phone_numberable_type` = "Whitelist" AND `phone_numberable_id` IN (' .. table.concat(whitelist_ids, ',') .. ') LIMIT 1';
+
+ local authorized = false
+ self.database:query(sql_query, function(entry)
+ authorized = true
+ end)
+
+ return authorized;
+end
diff --git a/misc/freeswitch/scripts/dialplan/cdr.lua b/misc/freeswitch/scripts/dialplan/cdr.lua
new file mode 100644
index 0000000..55a7889
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/cdr.lua
@@ -0,0 +1,71 @@
+-- Gemeinschaft 5 module: cdr class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Cdr = {}
+
+local DEFAULT_MEMBER_TIMEOUT = 20;
+
+-- Create Cdr object
+function Cdr.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+function Cdr.save(self, caller, destination)
+ require 'common.str'
+ local cdr = {}
+ cdr.uuid = common.str.to_sql(caller.uuid);
+ cdr.bleg_uuid = common.str.to_sql(caller:to_s('bridge_uuid'));
+ cdr.dialed_number = common.str.to_sql(caller.called_number);
+ cdr.destination_number = common.str.to_sql(destination.number);
+ cdr.caller_id_number = common.str.to_sql(caller:to_s('effective_caller_id_number'));
+ cdr.caller_id_name = common.str.to_sql(caller:to_s('effective_caller_id_name'));
+ cdr.callee_id_number = common.str.to_sql(caller:to_s('effective_callee_id_number'));
+ cdr.callee_id_name = common.str.to_sql(caller:to_s('effective_callee_id_name'));
+ cdr.start_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('created_time') / 1000000) .. ')';
+ cdr.answer_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('answered_time') / 1000000) .. ')';
+ cdr.end_stamp = 'NOW()';
+ cdr.duration = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('created_time') / 1000000);
+ cdr.hangup_cause = common.str.to_sql(caller.session:hangupCause());
+ cdr.dialstatus = common.str.to_sql(caller:to_s('DIALSTATUS'));
+ cdr.forwarding_number = common.str.to_sql(caller.forwarding_number);
+ cdr.forwarding_service = common.str.to_sql(caller.forwarding_service);
+
+ if caller.auth_account then
+ cdr.forwarding_account_id = common.str.to_sql(caller.auth_account.id);
+ cdr.forwarding_account_type = common.str.to_sql(caller.auth_account.class);
+ end
+
+ if caller.account then
+ cdr.account_id = common.str.to_sql(caller.account.id);
+ cdr.account_type = common.str.to_sql(caller.account.class);
+ end
+
+ if caller:to_i('answered_time') > 0 then
+ cdr.billsec = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('answered_time') / 1000000);
+ end
+
+ cdr.bleg_account_id = common.str.to_sql(tonumber(destination.id));
+ cdr.bleg_account_type = common.str.to_sql(destination.type);
+
+ local keys = {}
+ local values = {}
+
+ for key, value in pairs(cdr) do
+ table.insert(keys, key);
+ table.insert(values, value);
+ end
+
+ local sql_query = 'INSERT INTO `cdrs` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')';
+ self.log:info('CDR_SAVE - caller: ', cdr.account_type, '=', cdr.account_id, ', callee: ',cdr.bleg_account_type, '=', cdr.bleg_account_id,', dialstatus: ', cdr.dialstatus);
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/dialplan/dialplan.lua b/misc/freeswitch/scripts/dialplan/dialplan.lua
new file mode 100644
index 0000000..f4dca9e
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/dialplan.lua
@@ -0,0 +1,996 @@
+-- Gemeinschaft 5 module: dialplan class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Dialplan = {}
+
+-- local constants
+local CONFIG_FILE_NAME = '/opt/freeswitch/scripts/ini/dialplan.ini';
+local DIAL_TIMEOUT = 120;
+local MAX_LOOPS = 20;
+local DIALPLAN_FUNCTION_PATTERN = '^f[_%-].*';
+local CALL_FORWARDING_SERVICES = {
+ USER_BUSY = 'busy',
+ CALL_REJECTED = 'busy',
+ NO_ANSWER = 'noanswer',
+ USER_NOT_REGISTERED = 'offline',
+ HUNT_GROUP_EMPTY = 'offline',
+ ACD_NO_AGENTS = 'offline',
+ ACD_TIMEOUT = 'noanswer',
+}
+
+-- create dialplan object
+function Dialplan.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.caller = arg.caller;
+
+ return object;
+end
+
+
+function Dialplan.domain_get(self, domain)
+ require 'common.str'
+ local global_domain = freeswitch.API():execute('global_getvar', 'domain');
+
+ if common.str.blank(global_domain) then
+ if common.str.blank(domain) then
+ require 'common.database'
+ local database = common.database.Database:new{ log = self.log }:connect();
+ if not database:connected() then
+ self.log:error('[', uuid,'] DIALPLAN_DOMAIN - cannot connect to Gemeinschaft database');
+ else
+ require 'configuration.sip'
+ local domains = configuration.sip.Sip:new{ log = self.log, database = database }:domains();
+ if domains[1] then
+ domain = domains[1]['host'];
+ end
+ end
+ end
+
+ if database then
+ database:release();
+ end
+
+ if not common.str.blank(domain) then
+ self.log:notice('DIALPLAN_DOMAIN - setting default domain: ', domain);
+ freeswitch.API():execute('global_setvar', 'domain=' .. tostring(domain));
+ end
+ else
+ domain = global_domain;
+ end
+
+ if common.str.blank(domain) then
+ self.log:error('DIALPLAN_DOMAIN - no domain found');
+ end
+
+ return domain;
+end
+
+
+function Dialplan.configuration_read(self, file_name)
+ require 'common.str'
+ require 'common.configuration_file'
+
+ -- dialplan configuration
+ self.config = common.configuration_file.get(file_name or CONFIG_FILE_NAME);
+ self.node_id = common.str.to_i(self.config.parameters.node_id);
+ self.domain = self:domain_get(self.config.parameters.domain);
+ self.dial_timeout = tonumber(self.config.parameters.dial_timeout) or DIAL_TIMEOUT;
+ self.max_loops = tonumber(self.config.parameters.max_loops) or MAX_LOOPS;
+ self.user_image_url = common.str.to_s(self.config.parameters.user_image_url);
+ self.phone_book_entry_image_url = common.str.to_s(self.config.parameters.phone_book_entry_image_url);
+ self.phonebook_number_lookup = common.str.to_b(self.config.parameters.phonebook_number_lookup);
+ self.geo_number_lookup = common.str.to_b(self.config.parameters.geo_number_lookup);
+ self.default_language = self.config.parameters.default_language or 'en';
+ self.send_ringing_to_gateways = common.str.to_b(self.config.parameters.send_ringing_to_gateways);
+
+ if tonumber(self.config.parameters.default_ringtone) then
+ self.default_ringtone = 'http://amooma.de;info=Ringer' .. self.config.parameters.default_ringtone .. ';x-line-id=0';
+ else
+ self.default_ringtone = 'http://amooma.de;info=Ringer1;x-line-id=0';
+ end
+
+ return (self.config ~= nil);
+end
+
+
+function Dialplan.hangup(self, code, phrase, cause)
+ if self.caller:ready() then
+ if tonumber(code) then
+ self.caller:respond(code, phrase or 'Thank you for flying Gemeinschaft5');
+ end
+ self.caller:hangup(cause or 16);
+ else
+ self.log:info('HANGUP - caller sesson down - cause: ', self.caller.session:hangupCause());
+ end
+end
+
+
+function Dialplan.check_auth(self)
+ local authenticated = false;
+
+ require 'common.str'
+ if self.caller.from_node then
+ self.log:info('AUTH_FIRST_STAGE - node authenticated - node_id: ', self.caller.node_id);
+ authenticated = true;
+ elseif not common.str.blank(self.caller.auth_account_type) then
+ self.log:info('AUTH_FIRST_STAGE - sipaccount autheticated by name/password: ', self.caller.auth_account_type, '=', self.caller.account_id, '/', self.caller.account_uuid);
+ authenticated = true;
+ elseif self.caller.from_gateway then
+ self.log:info('AUTH_FIRST_STAGE - gateway autheticated by name/password: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name);
+ authenticated = true;
+ else
+ local gateways = common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', false);
+ if not gateways then
+ return false;
+ end
+ for gateway, gateway_parameters in pairs(gateways) do
+ if common.str.to_s(gateway_parameters.proxy) == self.caller.sip_contact_host then
+ self.caller.gateway_name = gateway;
+ self.caller.from_gateway = true;
+ self.log:info('AUTH_FIRST_STAGE - gateway autheticated by ip: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name, ', ip: ', self.caller.sip_contact_host);
+ authenticated = true;
+ end
+ end
+ end
+
+ return authenticated;
+end
+
+
+function Dialplan.check_auth_node(self)
+ require 'common.node'
+ local node = common.node.Node:new{ log = self.log, database = self.database }:find_by_address(self.caller.sip_contact_host);
+
+ return (node ~= nil);
+end
+
+
+function Dialplan.check_auth_ip(self)
+ self.log:info('AUTH - node: ', self.caller.from_node, ', auth_account: ', self.caller.auth_account_type, ', gateway: ', self.caller.from_gateway);
+ require 'common.str'
+ if self.caller.from_node then
+ return true;
+ elseif not common.str.blank(self.caller.auth_account_type) then
+ return true;
+ elseif self.caller.from_gateway then
+ return true;
+ else
+ return nil;
+ end
+end
+
+
+function Dialplan.object_find(self, class, identifier, auth_name)
+ require 'common.str'
+ class = common.str.downcase(class);
+
+ if class == 'user' then
+ require 'dialplan.user'
+ local user = nil;
+ if type(identifier) == 'number' then
+ user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ if user then
+ user.groups = user:list_groups();
+ end
+
+ return user;
+ elseif class == 'tenant' then
+ require 'dialplan.tenant'
+ local tenant = nil;
+ if type(identifier) == 'number' then
+ tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ return tenant;
+ elseif class == 'sipaccount' then
+ require 'common.sip_account'
+ local sip_account = nil;
+ if auth_name then
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(auth_name, identifier);
+ elseif type(identifier) == 'number' then
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+ if sip_account then
+ sip_account.owner = self:object_find(sip_account.record.sip_accountable_type, tonumber(sip_account.record.sip_accountable_id));
+ end
+ return sip_account;
+ elseif class == 'huntgroup' then
+ require 'dialplan.hunt_group'
+
+ local hunt_group = nil;
+ if type(identifier) == 'number' then
+ hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+
+ if hunt_group then
+ hunt_group.owner = self:object_find('tenant', tonumber(hunt_group.record.tenant_id));
+ end
+
+ return hunt_group;
+ elseif class == 'automaticcalldistributor' then
+ require 'dialplan.acd'
+
+ local acd = nil;
+ if type(identifier) == 'number' then
+ acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_id(identifier);
+ else
+ acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_uuid(identifier);
+ end
+
+ if acd then
+ acd.owner = self:object_find(acd.record.automatic_call_distributorable_type, tonumber(acd.record.automatic_call_distributorable_id));
+ end
+
+ return acd;
+ elseif class == 'faxaccount' then
+ require 'dialplan.fax'
+ local fax_account = nil;
+ if type(identifier) == 'number' then
+ fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(identifier);
+ else
+ fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_uuid(identifier);
+ end
+ if fax_account then
+ fax_account.owner = self:object_find(fax_account.record.fax_accountable_type, tonumber(fax_account.record.fax_accountable_id));
+ end
+
+ return fax_account;
+ end
+end
+
+
+function Dialplan.retrieve_caller_data(self)
+ self.caller.caller_phone_numbers_hash = {}
+
+ require 'common.str'
+
+ local dialed_sip_user = self.caller:to_s('dialed_user');
+
+ -- TODO: Set auth_account on transfer initiated by calling party
+ if not common.str.blank(dialed_sip_user) then
+ self.caller.auth_account = self:object_find('sipaccount', self.caller:to_s('dialed_domain'), dialed_sip_user);
+ self.caller:set_auth_account(self.caller.auth_account);
+ elseif not common.str.blank(self.caller.auth_account_type) and not common.str.blank(self.caller.auth_account_uuid) then
+ self.caller.auth_account = self:object_find(self.caller.auth_account_type, self.caller.auth_account_uuid);
+ self.caller:set_auth_account(self.caller.auth_account);
+ end
+
+ if self.caller.auth_account then
+ self.log:info('CALLER_DATA - auth account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('CALLER_DATA - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('CALLER_DATA - auth owner not found');
+ end
+ else
+ self.log:info('CALLER_DATA - no data - unauthenticated call: ', self.caller.auth_account_type, '/', self.caller.auth_account_uuid);
+ end
+
+ if not common.str.blank(self.caller.account_type) and not common.str.blank(self.caller.account_uuid) then
+ self.caller.account = self:object_find(self.caller.account_type, self.caller.account_uuid);
+ if self.caller.account then
+ require 'common.phone_number'
+ self.caller.caller_phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(self.caller.account.id, self.caller.account.class);
+ for index, caller_number in ipairs(self.caller.caller_phone_numbers) do
+ self.caller.caller_phone_numbers_hash[caller_number] = true;
+ end
+ self.log:info('CALLER_DATA - caller account: ', self.caller.account.class, '=', self.caller.account.id, '/', self.caller.account.uuid, ', phone_numbers: ', #self.caller.caller_phone_numbers);
+ if self.caller.account.owner then
+ self.log:info('CALLER_DATA - caller owner: ', self.caller.account.owner.class, '=', self.caller.account.owner.id, '/', self.caller.account.owner.uuid);
+ else
+ self.log:error('CALLER_DATA - caller owner not found');
+ end
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(self.caller.caller_phone_numbers[1], self.caller.account.record.caller_name or self.caller.account.record.name);
+ end
+ else
+ self.log:error('CALLER_DATA - caller account not found: ', self.caller.account_type, '/', self.caller.account_uuid);
+ end
+ end
+end
+
+
+function Dialplan.destination_new(self, arg)
+ require 'common.str'
+
+ local destination = {
+ number = arg.number or '',
+ type = arg.type or 'unknown',
+ id = common.str.to_i(arg.id),
+ uuid = arg.uuid or '',
+ phone_number = arg.phone_number,
+ node_id = common.str.to_i(arg.node_id),
+ call_forwarding = {},
+ data = arg.data,
+ }
+
+ destination.type = common.str.downcase(destination.type);
+
+ if not common.str.blank(destination.number) then
+ if destination.type == 'unknown' and destination.number:find(DIALPLAN_FUNCTION_PATTERN) then
+ destination.type = 'dialplanfunction';
+ elseif destination.type == 'phonenumber' or destination.type == 'unknown' then
+ require 'common.phone_number'
+ destination.phone_number = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination.number);
+
+ if destination.phone_number then
+ destination.type = common.str.downcase(destination.phone_number.record.phone_numberable_type);
+ destination.id = common.str.to_i(destination.phone_number.record.phone_numberable_id);
+ destination.uuid = common.str.to_s(destination.phone_number.record.phone_numberable_uuid);
+ destination.node_id = common.str.to_i(destination.phone_number.record.gs_node_id);
+ if self.caller then
+ destination.call_forwarding = destination.phone_number:call_forwarding(self.caller.caller_phone_numbers);
+ end
+ elseif destination.type == 'unknown' then
+ require 'common.sip_account'
+ destination.account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(destination.number);
+ if destination.account then
+ destination.type = 'sipaccount';
+ destination.id = common.str.to_i(destination.account.record.id);
+ destination.uuid = common.str.to_s(destination.account.record.uuid);
+ destination.node_id = common.str.to_i(destination.account.record.gs_node_id);
+ end
+ end
+ end
+ end
+
+ if destination.node_id == 0 then
+ destination.node_id = self.node_id;
+ destination.node_local = true;
+ else
+ destination.node_local = (destination.node_id == self.node_id);
+ end
+
+ self.log:info('DESTINATION_NEW - ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, ', number: ', destination.number);
+
+ return destination;
+end
+
+
+function Dialplan.routes_get(self, destination)
+ require 'dialplan.route'
+ return dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:outbound(self.caller, destination.number);
+end
+
+
+function Dialplan.set_caller_picture(self, entry_id, entry_type, image)
+ entry_type = entry_type:lower();
+ if entry_type == 'user' then
+ require 'dialplan.user'
+ local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(entry_id);
+ if user then
+ self.caller:set_variable('sip_h_Call-Info', '<' .. self.user_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(user.record.image) .. '>;purpose=icon');
+ end
+ elseif entry_type == 'phonebookentry' and image then
+ self.caller:set_variable('sip_h_Call-Info', '<' .. self.phone_book_entry_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(image) .. '>;purpose=icon');
+ end
+end
+
+
+function Dialplan.dial(self, destination)
+ require 'common.str'
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ if destination.node_local and destination.type == 'sipaccount' then
+ local user_id = nil;
+ local tenant_id = nil;
+
+ destination.account = self:object_find(destination.type, destination.id);
+ if destination.account then
+ if destination.account.class == 'sipaccount' then
+ destination.callee_id_name = destination.account.record.caller_name;
+ self.caller:set_callee_id(destination.number, destination.account.record.caller_name);
+ end
+ end
+
+ if destination.account and destination.account.owner then
+ if destination.account.owner.class == 'user' then
+ user_id = destination.account.owner.id;
+ tenant_id = tonumber(destination.account.owner.record.current_tenant_id);
+ elseif destination.account.owner.class == 'tenant' then
+ tenant_id = destination.account.owner.id;
+ end
+ end
+
+ if user_id or tenant_id then
+ require 'common.str'
+ local phone_book_entry = nil;
+
+ if self.phonebook_number_lookup then
+ require 'dialplan.phone_book'
+ phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant(self.caller.caller_phone_numbers, user_id, tenant_id);
+ end
+
+ if phone_book_entry then
+ self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), caller_id_name: ', phone_book_entry.caller_id_name, ', ringtone: ', phone_book_entry.bellcore_id);
+ destination.caller_id_name = common.str.to_ascii(phone_book_entry.caller_id_name);
+ if tonumber(phone_book_entry.bellcore_id) then
+ self.log:debug('RINGTONE - phonebookentry, index: ', phone_book_entry.bellcore_id);
+ self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. phone_book_entry.bellcore_id .. ';x-line-id=0');
+ end
+ if phone_book_entry.image then
+ self:set_caller_picture(phone_book_entry.id, 'phonebookentry', phone_book_entry.image);
+ elseif self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ elseif self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ elseif self.geo_number_lookup then
+ require 'dialplan.geo_number'
+ local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.caller_id_number);
+ if geo_number then
+ self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country);
+ if geo_number.name then
+ destination.caller_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country);
+ else
+ destination.caller_id_name = common.str.to_ascii(geo_number.country);
+ end
+ end
+ end
+ end
+ end
+ self.caller:set_caller_id(destination.caller_id_number, destination.caller_id_name or self.caller.caller_id_name);
+ else
+ self.caller:set_caller_id('anonymous', 'Unknown');
+ self.caller:set_privacy(true);
+ end
+
+ local destinations = { destination };
+
+ if self.caller.forwarding_service == 'assistant' and self.caller.auth_account and self.caller.auth_account.class == 'sipaccount' then
+ self.caller.auth_account.type = self.caller.auth_account.class;
+ local forwarding_destination = self:destination_new( self.caller.auth_account );
+ if forwarding_destination then
+ forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0'
+ table.insert(destinations, forwarding_destination);
+ end
+ end
+
+ if common.str.to_b(self.config.parameters.bypass_media) then
+ self.caller:set_variable('bypass_media', true);
+ end
+
+ require 'dialplan.sip_call'
+ return dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = self.caller }:fork(
+ destinations,
+ { timeout = self.dial_timeout_active,
+ send_ringing = ( self.send_ringing_to_gateways and self.caller.from_gateway ),
+ bypass_media_network = self.config.parameters.bypass_media_network,
+ }
+ );
+end
+
+
+function Dialplan.huntgroup(self, destination)
+ local hunt_group = self:object_find('huntgroup', tonumber(destination.id));
+
+ if not hunt_group then
+ self.log:error('DIALPLAN_HUNTGROUP - huntgroup not found');
+ return { continue = true, code = 404, phrase = 'Huntgroup not found' }
+ end
+
+ self.caller:set_callee_id(destination.number, hunt_group.record.name);
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(destination.caller_id_number, tostring(hunt_group.record.name) .. ' '.. tostring(self.caller.caller_id_name));
+ if self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ else
+ self.caller:set_caller_id('anonymous', tostring(hunt_group.record.name));
+ self.caller:set_privacy(true);
+ end
+
+ self.caller.auth_account = hunt_group;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'huntgroup';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+ return hunt_group:run(self, self.caller, destination);
+end
+
+
+function Dialplan.acd(self, destination)
+ local acd = self:object_find('automaticcalldistributor', tonumber(destination.id));
+
+ if not acd then
+ self.log:error('DIALPLAN_ACD - acd not found');
+ return { continue = true, code = 404, phrase = 'ACD not found' }
+ end
+
+ self.caller:set_callee_id(destination.number, acd.record.name);
+ destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1];
+
+ if not self.caller.clir then
+ self.caller:set_caller_id(destination.caller_id_number, tostring(acd.record.name) .. ' '.. tostring(self.caller.caller_id_name));
+ if self.caller.account and self.caller.account.owner then
+ self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class);
+ end
+ else
+ self.caller:set_caller_id('anonymous', tostring(acd.record.name));
+ self.caller:set_privacy(true);
+ end
+
+ self.caller.auth_account = acd;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'automaticcalldistributor';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ acd:caller_new(self.caller.uuid);
+ local result = acd:run(self, self.caller, destination);
+ acd:caller_delete();
+
+ return result;
+end
+
+
+function Dialplan.conference(self, destination)
+ -- call local conference
+ require 'common.conference'
+ conference = common.conference.Conference:new{ log = self.log, database = self.database }:find_by_id(destination.id);
+
+ if not conference then
+ return { continue = false, code = 404, phrase = 'Conference not found' }
+ end
+
+ local cause = conference:enter(self.caller, self.domain);
+ return { continue = false, cause = cause }
+end
+
+
+function Dialplan.faxaccount(self, destination)
+ require 'dialplan.fax'
+ local fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(destination.id);
+
+ if not fax_account then
+ return { continue = false, code = 404, phrase = 'Fax not found' }
+ end
+
+ self.log:info('FAX_RECEIVE start - fax_account=', fax_account.id, '/', fax_account.uuid, ', name: ', fax_account.record.name, ', station_id: ', fax_account.record.station_id);
+
+ self.caller:set_caller_id(self.caller.caller_phone_number);
+ self.caller:set_callee_id(destination.number, fax_account.record.name);
+
+ local fax_document = fax_account:receive(self.caller);
+
+ if not fax_document then
+ self.log:error('FAX_RECEIVE - error receiving fax document - fax_account=', fax_account.id, '/', fax_account.uuid);
+ return { continue = false, code = 500, phrase = 'Error receiving fax' };
+ end
+
+ fax_document.caller_id_number = self.caller.caller_phone_number;
+ fax_document.caller_id_name = self.caller.caller_id_name;
+ fax_document.uuid = self.caller.uuid;
+
+ self.log:info('FAX_RECEIVE end - success: ', fax_document.success,
+ ', remote: ', fax_document.remote_station_id,
+ ', pages: ', fax_document.transferred_pages, '/', fax_document.total_pages,
+ ', result: ', fax_document.result_code, ' ', fax_document.result_text);
+
+ if fax_document.success then
+ self.log:notice('FAX_RECEIVE - saving fax document: ', fax_document.filename );
+ if not fax_account:insert_document(fax_document) then
+ self.log:error('FAX_RECEIVE - error inserting fax document to database - fax_account=', fax_account.id, '/', fax_account.uuid, ', file: ', fax_document.filename);
+ end
+ end
+
+ return { continue = false, code = 200, phrase = 'OK' }
+end
+
+
+function Dialplan.callthrough(self, destination)
+ -- Callthrough
+ require 'dialplan.callthrough'
+ callthrough = dialplan.callthrough.Callthrough:new{ log = self.log, database = self.database }:find_by_id(destination.id)
+
+ if not callthrough then
+ self.log:error('CALLTHROUGH - no callthrough for destination number: ', destination.number);
+ return { continue = false, code = 404, phrase = 'Fax not found' }
+ end
+ self.log:info('CALLTHROUGH - number: ' .. destination.number .. ', name: ' .. callthrough.record.name);
+
+ local authorization = callthrough:authenticate(self.caller);
+
+ if not authorization then
+ self.log:notice('CALLTHROUGH - authentication failed');
+ return { continue = false, code = 403, phrase = 'Authentication failed' }
+ end
+
+ if type(authorization) == 'table' and tonumber(authorization.sip_account_id) and tonumber(authorization.sip_account_id) > 0 then
+ local auth_account = self:object_find('sipaccount', tonumber(authorization.sip_account_id));
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = 'callthrough';
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ if auth_account then
+ self.caller.auth_account = auth_account;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found');
+ end
+ self.log:info('CALLTHROUGH - use sip account: ', auth_account.id, ' (', auth_account.record.caller_name, ')');
+ end
+ else
+ self.log:info('CALLTHROUGH - no sip account');
+ end
+
+ local destination_number = '';
+ for i = 1, 3, 1 do
+ if destination_number ~= '' then
+ break;
+ end
+ destination_number = session:read(2, 16, "ivr/ivr-enter_destination_telephone_number.wav", 3000, "#");
+ end
+
+ if destination_number == '' then
+ self.log:debug("no callthrough destination - hangup call");
+ return { continue = false, code = 404, phrase = 'No destination' }
+ end
+
+ local route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, destination_number);
+ if route and route.value then
+ destination_number = route.value;
+ end
+
+ if not callthrough:whitelist(destination_number) then
+ self.log:debug('caller not authorized to call destination number: ' .. destination_number .. ' - hangup call');
+ return { continue = false, code = 403, phrase = 'Unauthorized' }
+ end
+
+ return { continue = true, code = 302, number = destination_number }
+end
+
+
+function Dialplan.voicemail(self, destination)
+ if not self.caller.auth_account or self.caller.auth_account.class ~= 'sipaccount' then
+ self.log:error('VOICEMAIL - incompatible destination');
+ return { continue = false, code = 404, phrase = 'Mailbox not found' }
+ end
+
+ require 'dialplan.voicemail'
+ local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(self.caller.auth_account.id);
+
+ if not voicemail_account then
+ self.log:error('VOICEMAIL - no mailbox');
+ return { continue = false, code = 404, phrase = 'Mailbox not found' }
+ end
+
+ voicemail_account:leave(self.caller, self.caller.forwarding_number);
+
+ if self.caller:to_s("voicemail_message_len") == '' then
+ self.log:info('VOICEMAIL - no message saved');
+ end
+
+ return { continue = false, code = 200 }
+end
+
+
+function Dialplan.dialplanfunction(self, destination)
+ require 'dialplan.functions'
+ return dialplan.functions.Functions:new{ log = self.log, database = self.database, domain = self.domain }:dialplan_function(self.caller, destination.number);
+end
+
+
+function Dialplan.switch(self, destination)
+ require 'common.str'
+ local result = nil;
+ self.dial_timeout_active = self.dial_timeout;
+
+ if not destination.node_local then
+ return self:dial(destination);
+ end
+
+ for service, call_forwarding in pairs(destination.call_forwarding) do
+ if self.caller.caller_phone_numbers_hash[call_forwarding.number] then
+ self.log:info('CALL_FORWARDING - caller number equals destination: ', call_forwarding.number,' - ignore service: ', service);
+ destination.call_forwarding[service] = nil;
+ end
+ end
+
+ if destination.call_forwarding.noanswer then
+ self.dial_timeout_active = tonumber(destination.call_forwarding.noanswer.timeout) or self.dial_timeout;
+ end
+
+ if destination.call_forwarding.always then
+ return { continue = true, call_forwarding = destination.call_forwarding.always }
+ elseif destination.call_forwarding.assistant then
+ if common.str.downcase(destination.call_forwarding.assistant.type) == 'huntgroup' then
+ require 'dialplan.hunt_group'
+ local hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(destination.call_forwarding.assistant.id);
+ self.log:info('CALL_FORWARDING - huntgroup - auth_account: ', self.caller.auth_account_type, '=', self.caller.auth_account_uuid);
+ if hunt_group and (hunt_group:is_member_by_numbers(self.caller.caller_phone_numbers)) then
+ self.log:info('CALL_FORWARDING - caller is huntgroup member - ignore service: ', destination.call_forwarding.assistant.service);
+ else
+ return { continue = true, call_forwarding = destination.call_forwarding.assistant }
+ end
+ else
+ return { continue = true, call_forwarding = destination.call_forwarding.assistant }
+ end
+ end
+
+ -- reset ringtone
+ self.caller:export_variable('alert_info', self.default_ringtone);
+
+ if destination.phone_number then
+ local ringtone = destination.phone_number:ringtone();
+ if ringtone and ringtone.bellcore_id then
+ self.log:debug('RINGTONE - ', ringtone.ringtoneable_type .. ', index: ' .. ringtone.bellcore_id);
+ self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. tonumber(ringtone.bellcore_id) .. ';x-line-id=0');
+ end
+ end
+
+ if destination.type == 'sipaccount' then
+ result = self:dial(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'conference' then
+ return self:conference(destination);
+ elseif destination.type == 'faxaccount' then
+ return self:faxaccount(destination);
+ elseif destination.type == 'callthrough' then
+ return self:callthrough(destination);
+ elseif destination.type == 'huntgroup' then
+ result = self:huntgroup(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'automaticcalldistributor' then
+ result = self:acd(destination);
+ if CALL_FORWARDING_SERVICES[result.disposition] then
+ result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]];
+ if result.call_forwarding then
+ result.continue = true;
+ end
+ end
+ return result;
+ elseif destination.type == 'voicemail' then
+ return self:voicemail(destination);
+ elseif destination.type == 'dialplanfunction' then
+ return self:dialplanfunction(destination);
+ elseif not common.str.blank(destination.number) then
+ local result = { continue = false, code = 404, phrase = 'No route' }
+ local routes = self:routes_get(destination);
+
+ if not routes or #routes == 0 then
+ self.log:notice('SWITCH - no route - number: ', destination.number);
+ return { continue = false, code = 404, phrase = 'No route' }
+ end
+
+ destination.callee_id_number = destination.number;
+ destination.callee_id_name = nil;
+
+ if self.phonebook_number_lookup then
+ require 'common.str'
+ local user_id = common.str.try(self.caller, 'account.owner.id');
+ local tenant_id = common.str.try(self.caller, 'account.owner.record.current_tenant_id');
+
+ if user_id or tenant_id then
+ require 'dialplan.phone_book'
+ local phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant({ destination.number }, user_id, tenant_id);
+ if phone_book_entry then
+ self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), callee_id_name: ', common.str.to_ascii(phone_book_entry.caller_id_name));
+ destination.callee_id_name = common.str.to_ascii(phone_book_entry.caller_id_name);
+ end
+ end
+ end
+
+ if self.geo_number_lookup and not destination.callee_id_name then
+ require 'dialplan.geo_number'
+ local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.number);
+ if geo_number then
+ require 'common.str'
+ self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country);
+ if geo_number.name then
+ destination.callee_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country);
+ else
+ destination.callee_id_name = common.str.to_ascii(geo_number.country);
+ end
+ end
+ end
+
+ self.caller:set_callee_id(destination.callee_id_number, destination.callee_id_name);
+
+ for index, route in ipairs(routes) do
+ if route.class == 'hangup' then
+ return { continue = false, code = route.endpoint, phrase = route.phrase, cause = route.value }
+ end
+ if route.class == 'forward' then
+ return { continue = true, call_forwarding = { number = route.value, service = 'route', type = 'phonenumber' }}
+ end
+ destination.gateway = route.endpoint;
+ destination.type = route.class;
+ destination.number = route.value;
+ destination.caller_id_number = route.caller_id_number;
+ destination.caller_id_name = route.caller_id_name;
+ result = self:dial(destination);
+
+ if result.continue == false then
+ break;
+ end
+
+ if common.str.to_b(self.routes.failover[tostring(result.code)]) == true then
+ self.log:info('SWITCH - failover - code: ', result.code);
+ elseif common.str.to_b(self.routes.failover[tostring(result.cause)]) == true then
+ self.log:info('SWITCH - failover - cause: ', result.cause);
+ else
+ self.log:info('SWITCH - no failover - cause: ', result.cause, ', code: ', result.code);
+ break;
+ end
+ end
+
+ return result;
+ end
+
+ self.log:error('SWITCH - destination not found - type: ', destination.type);
+ return { continue = true, code = 404, phrase = destination.type .. ' not found' }
+end
+
+
+function Dialplan.run(self, destination)
+ self.caller:set_variable('hangup_after_bridge', false);
+ self.caller:set_variable('ringback', self.config.parameters.ringback);
+ self.caller:set_variable('bridge_early_media', 'true');
+ self.caller:set_variable('send_silence_when_idle', 0);
+ self.caller:set_variable('default_language', self.default_language);
+ self.caller:set_variable('gs_save_cdr', true);
+ self.caller:set_variable('gs_call_service', 'dial');
+ self.caller.session:setAutoHangup(false);
+
+ self.routes = common.configuration_file.get('/opt/freeswitch/scripts/ini/routes.ini');
+ self.caller.domain_local = self.domain;
+ self:retrieve_caller_data();
+
+ if not destination or destination.type == 'unknown' then
+ require 'dialplan.route'
+ local route = nil;
+
+ if self.caller.from_gateway then
+ local route_object = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes };
+ route = route_object:inbound(self.caller, self.caller.destination_number);
+ local inbound_caller_id_number = route_object:inbound_cid_number(self.caller, self.caller.gateway_name, 'gateway');
+ route_object.expandable.caller_id_number = inbound_caller_id_number;
+ local inbound_caller_id_name = route_object:inbound_cid_name(self.caller, self.caller.gateway_name, 'gateway');
+ self.log:info('INBOUND_CALLER_ID_REWRITE - number: ', inbound_caller_id_number, ', name: ', inbound_caller_id_name);
+ self.caller.caller_id_number = inbound_caller_id_number or self.caller.caller_id_number;
+ self.caller.caller_id_name = inbound_caller_id_name or self.caller.caller_id_name;
+ self.caller.caller_phone_numbers[1] = self.caller.caller_id_number;
+ else
+ route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, self.caller.destination_number);
+ end
+
+ if route then
+ destination = self:destination_new{ number = route.value }
+ self.caller.destination_number = destination.number;
+ self.caller.destination = destination;
+ elseif not destination or destination.type == 'unknown' then
+ destination = self:destination_new{ number = self.caller.destination_number }
+ self.caller.destination = destination;
+ end
+ end
+
+ self.log:info('DIALPLAN start - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"',
+ ', number: ', destination.number);
+
+ local result = { continue = false };
+ local loop = self.caller.loop_count;
+ while self.caller:ready() and loop < self.max_loops do
+ loop = loop + 1;
+ self.caller.loop_count = loop;
+
+ self.log:info('LOOP ', loop,
+ ' - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number);
+
+ self.caller:set_variable('gs_destination_type', destination.type);
+ self.caller:set_variable('gs_destination_id', destination.id);
+ self.caller:set_variable('gs_destination_uuid', destination.uuid);
+ self.caller:set_variable('gs_destination_number', destination.number);
+ self.caller:set_variable('gs_destination_node_local', destination.node_local);
+
+ result = self:switch(destination);
+ result = result or { continue = false, code = 502, cause = 'DESTINATION_OUT_OF_ORDER', phrase = 'Destination out of order' }
+
+ if result.call_service then
+ self.caller:set_variable('gs_call_service', result.call_service);
+ end
+
+ if not result.continue then
+ break;
+ end
+
+ if result.call_forwarding then
+ self.log:info('LOOP ', loop, ' CALL_FORWARDING - service: ', result.call_forwarding.service,
+ ', destination: ', result.call_forwarding.type, '=', result.call_forwarding.id,
+ ', number: ', result.call_forwarding.number);
+
+ local auth_account = self:object_find(destination.type, destination.id);
+ self.caller.forwarding_number = destination.number;
+ self.caller.forwarding_service = result.call_forwarding.service;
+ self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service);
+ self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number);
+
+ if auth_account then
+ self.caller.auth_account = auth_account;
+ self.caller:set_auth_account(self.caller.auth_account);
+ self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid);
+ if self.caller.auth_account.owner then
+ self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid);
+ else
+ self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found');
+ end
+ end
+
+ destination = self:destination_new(result.call_forwarding);
+ self.caller.destination = destination;
+
+ if not result.no_cdr and auth_account then
+ require 'common.call_history'
+ common.call_history.CallHistory:new{ log = self.log, database = self.database }:insert_forwarded(
+ self.caller.uuid,
+ auth_account.class,
+ auth_account.id,
+ self.caller,
+ destination,
+ result
+ );
+ end
+ end
+
+ if result.number then
+ self.log:info('LOOP ', loop, ' NEW_DESTINATION_NUMBER - number: ', result.number );
+ destination = self:destination_new{ number = result.number }
+ self.caller.destination = destination;
+ end
+ end
+
+ if loop >= self.max_loops then
+ result = { continue = false, code = 483, cause = 'EXCHANGE_ROUTING_ERROR', phrase = 'Too many hops' }
+ end
+
+ self.log:info('DIALPLAN end - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"',
+ ', destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number, ', result: ', result.code, ' ', result.phrase);
+
+ if self.caller:ready() then
+ self:hangup(result.code, result.phrase, result.cause);
+ end
+
+ self.caller:set_variable('gs_save_cdr', not result.no_cdr);
+end
diff --git a/misc/freeswitch/scripts/dialplan/fax.lua b/misc/freeswitch/scripts/dialplan/fax.lua
new file mode 100644
index 0000000..2a40620
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/fax.lua
@@ -0,0 +1,232 @@
+-- Gemeinschaft 5 module: fax class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+FAX_DOCUMENTS_DIRECTORY = '/tmp/'
+FAX_PARALLEL_MAX = 8;
+Fax = {}
+
+-- Create Fax object
+function Fax.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.class = 'faxaccount';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ self.fax_directory = arg.fax_directory or FAX_DOCUMENTS_DIRECTORY;
+ return object;
+end
+
+-- find fax account by id
+function Fax.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `fax_accounts` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1';
+ local fax_account = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ fax_account = Fax:new(self);
+ fax_account.record = fax_entry;
+ fax_account.id = tonumber(fax_entry.id);
+ fax_account.uuid = fax_entry.uuid;
+ end)
+
+ return fax_account;
+end
+
+
+-- find fax account by uuid
+function Fax.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `fax_accounts` WHERE `uuid` = "' .. uuid .. '" LIMIT 1';
+ local fax_account = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ fax_account = Fax:new(self);
+ fax_account.record = fax_entry;
+ fax_account.id = tonumber(fax_entry.id);
+ fax_account.uuid = fax_entry.uuid;
+ end)
+
+ return fax_account;
+end
+
+
+function Fax.destination_numbers(self, id)
+ local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id` = ' .. tonumber(id);
+ local destination_numbers = {}
+
+ self.database:query(sql_query, function(fax_entry)
+ table.insert(destination_numbers, fax_entry.number);
+ end)
+
+ return destination_numbers;
+end
+
+function Fax.destination_number(self, id)
+ local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local destination_number = nil;
+
+ self.database:query(sql_query, function(fax_entry)
+ destination_number = fax_entry.number;
+ end)
+
+ return destination_number;
+end
+
+-- List waiting fax documents
+function Fax.queued_for_sending(self, limit)
+ limit = limit or FAX_PARALLEL_MAX;
+ local sql_query = 'SELECT * FROM `fax_documents` WHERE `state` IN ("queued_for_sending","unsuccessful") AND `retry_counter` > 0 ORDER BY `sent_at` ASC LIMIT ' .. limit;
+ local fax_documents = {}
+ self.database:query(sql_query, function(fax_entry)
+ fax_entry['destination_numbers'] = Fax:destination_numbers(fax_entry.id)
+ table.insert(fax_documents, fax_entry);
+ end)
+
+ return fax_documents;
+end
+
+-- Update fax document sending status
+function Fax.document_update(self, id, params)
+ require 'common.str'
+ local params_sql = {}
+
+ for name, value in pairs(params) do
+ table.insert(params_sql, '`' .. name .. '`=' .. common.str.to_sql(value));
+ end
+
+ if not params['sent_at'] then
+ table.insert(params_sql, '`sent_at`=NOW()');
+ end
+
+ if not params['updated_at'] then
+ table.insert(params_sql, '`updated_at`=NOW()');
+ end
+
+ local sql_query = 'UPDATE `fax_documents` SET ' .. table.concat(params_sql, ',') .. ' WHERE `id` = ' .. tonumber(id);
+
+ return self.database:query(sql_query);
+end
+
+
+function Fax.get_parameters(self, caller)
+ local fax_parameters = {
+ bad_rows = caller:to_i('fax_bad_rows'),
+ total_pages = caller:to_i('fax_document_total_pages'),
+ transferred_pages = caller:to_i('fax_document_transferred_pages'),
+ ecm_requested = caller:to_b('fax_ecm_requested'),
+ ecm_used = caller:to_b('fax_ecm_used'),
+ filename = caller:to_s('fax_filename'),
+ image_resolution = caller:to_s('fax_image_resolution'),
+ image_size = caller:to_i('fax_image_size'),
+ local_station_id = caller:to_s('fax_local_station_id'),
+ result_code = caller:to_i('fax_result_code'),
+ result_text = caller:to_s('fax_result_text'),
+ remote_station_id = caller:to_s('fax_remote_station_id'),
+ success = caller:to_b('fax_success'),
+ transfer_rate = caller:to_i('fax_transfer_rate'),
+ v17_disabled = caller:to_b('fax_v17_disabled'),
+ }
+
+ return fax_parameters;
+end
+
+-- Receive Fax
+function Fax.receive(self, caller, file_name)
+ file_name = file_name or self.fax_directory .. 'fax_in_' .. caller.uuid .. '.tiff';
+
+ caller:set_variable('fax_ident', self.record.station_id)
+ caller:set_variable('fax_verbose', 'false')
+
+ caller:answer();
+ local start_time = os.time();
+ caller:execute('rxfax', file_name);
+ local record = self:get_parameters(caller);
+ record.transmission_time = os.time() - start_time;
+ return record;
+end
+
+-- Send Fax
+function Fax.send(self, caller, file_name)
+ caller:set_variable('fax_ident', self.record.station_id)
+ caller:set_variable('fax_header', self.record.name)
+ caller:set_variable('fax_verbose', 'false')
+ local start_time = os.time();
+ caller:execute('txfax', file_name);
+ local record = self:get_parameters(caller);
+ record.transmission_time = os.time() - start_time;
+ return record;
+end
+
+-- find fax document by id
+function Fax.find_document_by_id(self, id)
+ local sql_query = 'SELECT * FROM `fax_documents` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1'
+ local record = nil
+
+ self.database:query(sql_query, function(fax_entry)
+ record = fax_entry;
+ end);
+
+ return record;
+end
+
+-- save fax document to database
+function Fax.insert_document(self, record)
+ require 'common.str'
+ local sql_query = 'INSERT INTO `fax_documents` ( \
+ inbound, \
+ retry_counter, \
+ fax_resolution_id, \
+ state, \
+ transmission_time, \
+ sent_at, \
+ document_total_pages, \
+ document_transferred_pages, \
+ ecm_requested, \
+ ecm_used, \
+ image_resolution, \
+ image_size, \
+ local_station_id, \
+ result_code, \
+ remote_station_id, \
+ success, \
+ transfer_rate, \
+ created_at, \
+ updated_at, \
+ fax_account_id, \
+ caller_id_number, \
+ caller_id_name, \
+ tiff, \
+ uuid \
+ ) VALUES ( \
+ true, \
+ 0, \
+ 1, \
+ "received", \
+ ' .. common.str.to_sql(record.transmission_time) .. ', \
+ NOW(), \
+ ' .. common.str.to_sql(record.total_pages) .. ', \
+ ' .. common.str.to_sql(record.transferred_pages) .. ', \
+ ' .. common.str.to_sql(record.ecm_requested) .. ', \
+ ' .. common.str.to_sql(record.ecm_used) .. ', \
+ ' .. common.str.to_sql(record.image_resolution) .. ', \
+ ' .. common.str.to_sql(record.image_size) .. ', \
+ ' .. common.str.to_sql(record.local_station_id) .. ', \
+ ' .. common.str.to_sql(record.result_code) .. ', \
+ ' .. common.str.to_sql(record.remote_station_id) .. ', \
+ ' .. common.str.to_sql(record.success) .. ', \
+ ' .. common.str.to_sql(record.transfer_rate) .. ', \
+ NOW(), \
+ NOW(), \
+ ' .. common.str.to_sql(self.id) .. ', \
+ ' .. common.str.to_sql(record.caller_id_number) .. ', \
+ ' .. common.str.to_sql(record.caller_id_name) .. ', \
+ ' .. common.str.to_sql(record.filename) .. ', \
+ ' .. common.str.to_sql(record.uuid) .. ' \
+ )';
+
+ return self.database:query(sql_query);
+end
diff --git a/misc/freeswitch/scripts/dialplan/functions.lua b/misc/freeswitch/scripts/dialplan/functions.lua
new file mode 100644
index 0000000..c104f89
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/functions.lua
@@ -0,0 +1,839 @@
+-- DialplanModule: Functions
+--
+module(...,package.seeall)
+
+Functions = {}
+
+-- Create Functions object
+function Functions.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log
+ self.database = arg.database
+ self.domain = arg.domain
+ return object
+end
+
+function Functions.ensure_caller_sip_account(self, caller)
+ if caller.account and caller.account.class == 'sipaccount' then
+ return caller.account;
+ end
+end
+
+function Functions.dialplan_function(self, caller, dialed_number)
+ require 'common.str'
+ local parameters = common.str.to_a(dialed_number, '%-');
+ if not parameters[2] then
+ return { continue = false, code = 484, phrase = 'Malformed function parameters', no_cdr = true };
+ end
+ local fid = tostring(parameters[2]);
+ local result = { continue = false, code = 404, phrase = 'Function not found', no_cdr = true };
+
+ self.log:debug('DIALPLAN_DUNCTION - execute: ', dialed_number);
+
+ if fid == "ta" then
+ result = self:transfer_all(caller, parameters[3]);
+ elseif fid == "in" then
+ result = self:intercept_extensions(caller, parameters[3]);
+ elseif fid == "ia" then
+ result = self:intercept_any_extension(caller, parameters[3]);
+ elseif fid == "anc" then
+ result = self:account_node_change(caller);
+ elseif fid == "li" then
+ result = self:user_login(caller, parameters[3], parameters[4]);
+ elseif fid == "lo" then
+ result = self:user_logout(caller);
+ elseif fid == "lir" then
+ result = self:user_login_redirect(caller, parameters[3], parameters[4]);
+ elseif fid == "loaon" then
+ result = self:user_auto_logout(caller, true);
+ elseif fid == "loaoff" then
+ result = self:user_auto_logout(caller, false);
+ elseif fid == "dcliroff" then
+ result = self:dial_clir_off(caller, parameters[3]);
+ elseif fid == "dcliron" then
+ result = self:dial_clir_on(caller, parameters[3]);
+ elseif fid == "clipon" then
+ result = self:clip_on(caller);
+ elseif fid == "clipoff" then
+ result = self:clip_off(caller);
+ elseif fid == "cwaoff" then
+ result = self:callwaiting_off(caller);
+ elseif fid == "cwaon" then
+ result = self:callwaiting_on(caller);
+ elseif fid == "cfoff" then
+ result = self:call_forwarding_off(caller);
+ elseif fid == "cfdel" then
+ result = self:call_forwarding_off(caller, nil, true);
+ elseif fid == "cfu" then
+ result = self:call_forwarding_on(caller, 'always', parameters[3], 'PhoneNumber');
+ elseif fid == "cfuoff" then
+ result = self:call_forwarding_off(caller, 'always');
+ elseif fid == "cfudel" then
+ result = self:call_forwarding_off(caller, 'always', true);
+ elseif fid == "cfutg" then
+ result = self:call_forwarding_toggle(caller, 'always', parameters[3]);
+ elseif fid == "cfn" then
+ result = self:call_forwarding_on(caller, 'noanswer', parameters[3], 'PhoneNumber', parameters[4]);
+ elseif fid == "cfnoff" then
+ result = self:call_forwarding_off(caller, 'noanswer');
+ elseif fid == "cfndel" then
+ result = self:call_forwarding_off(caller, 'noanswer', true);
+ elseif fid == "cfo" then
+ result = self:call_forwarding_on(caller, 'offline', parameters[3], 'PhoneNumber');
+ elseif fid == "cfooff" then
+ result = self:call_forwarding_off(caller, 'offline');
+ elseif fid == "cfodel" then
+ result = self:call_forwarding_off(caller, 'offline', true);
+ elseif fid == "cfb" then
+ result = self:call_forwarding_on(caller, 'busy', parameters[3], 'PhoneNumber');
+ elseif fid == "cfboff" then
+ result = self:call_forwarding_off(caller, 'busy');
+ elseif fid == "cfbdel" then
+ result = self:call_forwarding_off(caller, 'busy', true);
+ elseif fid == "vmleave" then
+ result = self:voicemail_message_leave(caller, parameters[3]);
+ elseif fid == "vmcheck" then
+ result = self:voicemail_check(caller, parameters[3]);
+ elseif fid == "vmtg" then
+ result = self:call_forwarding_toggle(caller, nil, parameters[3]);
+ elseif fid == "acdmtg" then
+ result = self:acd_membership_toggle(caller, parameters[3], parameters[4]);
+ elseif fid == "e164" then
+ result = "+" .. tostring(parameters[3]);
+ elseif fid == "hangup" then
+ result = self:hangup(caller, parameters[3], parameters[4]);
+ end
+
+ return result;
+end
+
+-- Transfer all calls to a conference
+function Functions.transfer_all(self, caller, destination_number)
+ self.log:info('TRANSFER_ALL - caller: ', caller.account_type, '/', caller.account_uuid, ' number: ', destination_number);
+
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ self.log:error('TRANSFER_ALL - incompatible caller');
+ return { continue = false, code = 403, phrase = 'Incompatible caller' }
+ end
+
+ -- Query call and channel table for channel IDs
+ local sql_query = 'SELECT `b`.`name` AS `caller_chan_name`, `a`.`caller_uuid`, `a`.`callee_uuid` \
+ FROM `calls` `a` JOIN `channels` `b` ON `a`.`caller_uuid` = `b`.`uuid` JOIN `channels` `c` \
+ ON `a`.`callee_uuid` = `c`.`uuid` WHERE `b`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") \
+ OR `c`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") LIMIT 100';
+
+ self.database:query(sql_query, function(call_entry)
+ local uid = nil
+ if call_entry.caller_chan_name:find(caller_sip_account.record.auth_name .. "@") then
+ uid = call_entry.callee_uuid;
+ self.log:debug("Transfering callee channel with uid: " .. uid);
+ else
+ uid = call_entry.caller_uuid;
+ self.log:debug("Transfering caller channel with uid: " .. uid);
+ end
+ freeswitch.API():execute("uuid_transfer", uid .. " " .. destination_number);
+ end)
+
+ return destination_number;
+end
+
+-- Intercept Extensions
+function Functions.intercept_extensions(self, caller, destination_numbers)
+ if type(destination_numbers) == "string" then
+ destination_numbers = "\"" .. destination_numbers .. "\"";
+ else
+ destination_numbers = "\"" .. table.concat(destination_numbers, "\",\"") .. "\"";
+ end
+
+ self.log:debug("Intercept call to number(s): " .. destination_numbers);
+
+ if caller.account_type ~= "SipAccount" then
+ self.log:error("caller is not a SipAccount");
+ return { continue = false, code = 403, phrase = 'Incompatible caller' }
+ end
+
+ local sql_query = 'SELECT * FROM `channels` WHERE `callstate` IN ("EARLY", "ACTIVE") AND `dest` IN (' .. destination_numbers .. ') LIMIT 1';
+
+ self.database:query(sql_query, function(call_entry)
+ self.log:debug("intercepting call with uid: " .. call_entry.uuid);
+ caller:intercept(call_entry.uuid);
+ end)
+
+ return nil;
+end
+
+-- intercept call to destination (e.g. sip_account)
+function Functions.intercept_destination(self, caller, destination)
+ self.log:debug("Intercept call to destination " .. destination);
+ local result = false;
+ local sql_query = 'SELECT `call_uuid`, `uuid` FROM `channels` WHERE `callstate` = "RINGING" AND `dest` = "' .. destination .. '" LIMIT 1';
+
+ caller:set_caller_id(caller.caller_phone_numbers[1] ,caller.caller_id_name);
+ self.database:query(sql_query, function(call_entry)
+ if call_entry.call_uuid and tostring(call_entry.call_uuid) then
+ self.log:debug("intercepting call - uuid: " .. call_entry.call_uuid);
+ caller:intercept(call_entry.call_uuid);
+ result = { continue = false, code = 200, call_service = 'pickup' }
+ require 'common.str'
+ require 'common.fapi'
+ local fapi = common.fapi.FApi:new{ log = self.log, uuid = call_entry.call_uuid }
+ if fapi:channel_exists() then
+ caller:set_caller_id(
+ common.str.to_s(fapi:get_variable('effective_caller_id_number')),
+ common.str.to_s(fapi:get_variable('effective_caller_id_name'))
+ );
+ caller:set_callee_id(
+ common.str.to_s(fapi:get_variable('effective_callee_id_number')),
+ common.str.to_s(fapi:get_variable('effective_callee_id_name'))
+ );
+
+ caller:set_variable('gs_destination_type', fapi:get_variable('gs_destination_type'));
+ caller:set_variable('gs_destination_id', fapi:get_variable('gs_destination_id'));
+ caller:set_variable('gs_destination_uuid', fapi:get_variable('gs_destination_uuid'));
+
+ caller:set_variable('gs_caller_account_type', fapi:get_variable('gs_account_type'));
+ caller:set_variable('gs_caller_account_id', fapi:get_variable('gs_account_id'));
+ caller:set_variable('gs_caller_account_uuid', fapi:get_variable('gs_account_uuid'));
+
+ caller:set_variable('gs_auth_account_type', fapi:get_variable('gs_auth_account_type'));
+ caller:set_variable('gs_auth_account_id', fapi:get_variable('gs_auth_account_id'));
+ caller:set_variable('gs_auth_account_uuid', fapi:get_variable('gs_auth_account_uuid'));
+ end
+ else
+ self.log:error('FUNCTION - failed to intercept call - no caller uuid for callee uuid: ', call_entry.uuid);
+ end
+ end)
+
+ return result;
+end
+
+-- intercept call to owner of destination_number
+function Functions.intercept_any_extension(self, caller, destination_number)
+ require 'common.phone_number'
+ local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination_number);
+
+ if not phone_number_object or not phone_number_object.record then
+ self.log:notice("unallocated number: " .. tostring(destination_number));
+ return false;
+ end
+
+ if phone_number_object.record.phone_numberable_type == 'SipAccount' then
+ require "common.sip_account"
+ local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database }
+ local sip_account = sip_account_class:find_by_id(phone_number_object.record.phone_numberable_id)
+ if sip_account then
+ return self:intercept_destination(caller, sip_account.record.auth_name);
+ end
+ end
+end
+
+
+function Functions.account_node_change(self, caller)
+ self.log:info('NODE_CHANGE - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number);
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGIN - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ -- logout caller phones if caller account is hot-deskable
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.record.id);
+ for index, phone_caller in ipairs(caller_phones) do
+ phone_caller:logout(caller_sip_account.record.id);
+ end
+
+ self:update_node_change(caller_sip_account, caller.local_node_id);
+ caller:answer();
+ caller:send_display('Change successful');
+ caller.session:sayPhrase('logged_in');
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('NODE_CHANGE - resync phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.user_login(self, caller, number, pin)
+ require 'common.str'
+
+ local PHONE_NUMBER_LEN_MIN = 4;
+ local PHONE_NUMBER_LEN_MAX = 12;
+ local PIN_LEN_MIN = 4;
+ local PIN_LEN_MAX = 12;
+
+ caller:set_variable('destination_number', 'f-li-' .. common.str.to_s(number) .. '-PIN');
+ self.log:info('LOGIN - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number, ', number: ', number);
+
+ if common.str.blank(number) then
+ number = caller.session:read(PHONE_NUMBER_LEN_MIN, PHONE_NUMBER_LEN_MAX, 'ivr/ivr-please_enter_extension_followed_by_pound.wav', 3000, '#');
+ end
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGIN - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id);
+ local caller_phone = caller_phones[1];
+
+ if not caller_phone then
+ self.log:notice('LOGIN - caller phone not found or not hot-deskable');
+ return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ require 'common.phone_number'
+ local phone_number = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_by_number(number, {"SipAccount"});
+
+ if not phone_number then
+ self.log:notice('LOGIN - number not found or not linked to a sip account - number: ', number);
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ require 'common.sip_account'
+ local destination_sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(phone_number.record.phone_numberable_id);
+
+ if not destination_sip_account then
+ self.log:notice('LOGIN - account not found - ', phone_number.record.phone_numberable_type, '=', phone_number.record.phone_numberable_id, ', number: ', number);
+ return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true }
+ end
+
+ self.log:info('LOGIN - destination: ', phone_number.record.phone_numberable_type, '=', destination_sip_account.record.id,
+ ', caller_name: ', destination_sip_account.record.caller_name, ', hotdeskable: ', destination_sip_account.record.hotdeskable);
+
+ if not common.str.to_b(destination_sip_account.record.hotdeskable) then
+ self.log:notice('LOGIN - destination sip_account not hot-deskable');
+ return { continue = false, code = 404, phrase = 'Destination not hot-deskable', no_cdr = true }
+ end
+
+ require 'dialplan.user'
+ local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(destination_sip_account.record.sip_accountable_id);
+
+ if common.str.blank(pin) then
+ pin = caller.session:read(PIN_LEN_MIN, PIN_LEN_MAX, 'ivr/ivr-please_enter_pin_followed_by_pound.wav', 3000, '#');
+ end
+
+ if not user then
+ self.log:notice('LOGIN - user not found - ', destination_sip_account.record.sip_accountable_type, '=',destination_sip_account.record.sip_accountable_id);
+ return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true }
+ end
+
+ if not user:check_pin(pin) then
+ self.log:notice('LOGIN - authentication failed');
+ return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true }
+ end
+
+ -- logout caller phones if caller account is hot-deskable
+ if common.str.to_b(caller_sip_account.record.hotdeskable) then
+ for index, phone_caller in ipairs(caller_phones) do
+ phone_caller:logout(caller_sip_account.record.id);
+ end
+ end
+
+ local destination_phones = phone_class:find_all_hot_deskable_by_account(destination_sip_account.record.id);
+ -- logout destination phones
+ for index, phone_destination in ipairs(destination_phones) do
+ phone_destination:logout(destination_sip_account.record.id);
+ end
+
+ local result = caller_phone:login(destination_sip_account.record.id, destination_sip_account.record.sip_accountable_id, destination_sip_account.record.sip_accountable_type);
+ self.log:info('LOGIN - account login - mac: ', caller_phone.record.mac_address, ', ip_address: ', caller_phone.record.ip_address, ', result: ', result);
+
+ if not result then
+ return { continue = false, code = 403, phrase = 'Login failed', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Login successful');
+
+ self:update_node_change(destination_sip_account, caller.local_node_id);
+ caller:sleep(1000);
+
+ -- resync destination phones
+ for index, phone_destination in ipairs(destination_phones) do
+ local result = phone_destination:resync{ auth_name = destination_sip_account.auth_name, domain = caller.domain_local };
+ self.log:info('LOGIN - resync destination phone - mac: ', phone_destination.record.mac_address, ', ip_address: ', phone_destination.record.ip_address, ', result: ', result);
+ end
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.user_logout(self, caller)
+ require 'common.str'
+ self.log:info('LOGOUT - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number);
+
+ -- find caller's sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ self.log:notice('LOGOUT - caller sip_account not found');
+ return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true }
+ end
+
+ if not common.str.to_b(caller_sip_account.record.hotdeskable) then
+ self.log:notice('LOGOUT - caller sip_account not hot-deskable');
+ return { continue = false, code = 404, phrase = 'Caller not hot-deskable', no_cdr = true }
+ end
+
+ require 'phones.phone'
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id);
+
+ if caller_phones == 0 then
+ self.log:notice('LOGOUT - caller phones not found or not hot-deskable');
+ return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ local result = false;
+ for index, phone_caller in ipairs(caller_phones) do
+ result = phone_caller:logout(caller_sip_account.record.id);
+ self.log:info('LOGOUT - account logout - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ caller:answer();
+ caller:send_display('Logout successful');
+ caller:sleep(1000);
+
+ -- resync caller phones
+ for index, phone_caller in ipairs(caller_phones) do
+ local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain };
+ self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result);
+ end
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.update_node_change(self, sip_account, node_id)
+ require 'common.sync_log'
+ local sync_log_class = common.sync_log.SyncLog:new{ log = self.log, database = self.database, homebase_ip_address = sip_account.record.host }
+
+ if tostring(sip_account.record.gs_node_id) ~= tostring(node_id) then
+ self.log:info('UPDATE_NODE - from: ', sip_account.record.gs_node_id, ', to: ', node_id, ', sipaccount=', sip_account.record.id, '/', sip_account.record.uuid, '@', node_id, ', caller_name: ', sip_account.record.caller_name);
+ sql_query = 'UPDATE `sip_accounts` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(sip_account.record.id);
+ if self.database:query(sql_query) then
+ sync_log_class:insert('SipAccount', { uuid = sip_account.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' });
+ end
+ end
+
+ require 'common.phone_number'
+ local phone_numbers = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_all_by_owner(sip_account.record.id, 'SipAccount');
+ for number_id, phone_number in pairs(phone_numbers) do
+ if tostring(phone_number.record.gs_node_id) ~= tostring(node_id) then
+ self.log:info('UPDATE_NODE - from: ', phone_number.record.gs_node_id, ', to: ', node_id, ', phonenumber=', phone_number.record.id, '/', phone_number.record.uuid, '@', node_id, ', number: ', phone_number.record.number);
+ sql_query = 'UPDATE `phone_numbers` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(number_id);
+
+ if self.database:query(sql_query) then
+ sync_log_class:insert('PhoneNumber', { uuid = phone_number.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' });
+ end
+ end
+ end
+end
+
+
+function Functions.user_login_redirect(self, caller, phone_number, pin)
+ -- Remove PIN from destination_number
+ caller.session:setVariable("destination_number", "f-li-" .. tostring(phone_number) .. "-PIN");
+
+ -- Redirect to f-li function
+ caller.session:execute("redirect", "sip:f-li-" .. tostring(phone_number) .. "-" .. tostring(pin) .. "@" .. caller.domain);
+end
+
+-- Set nightly_reboot flag
+function Functions.user_auto_logout(self, caller, auto_logout)
+ local nightly_reboot = 'FALSE';
+ if auto_logout then
+ nightly_reboot = 'TRUE';
+ end
+
+ -- Ensure a valid sip account
+ local caller_sip_account = caller.account;
+ if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "phones.phone"
+ local phone_class = phones.phone.Phone:new{log = self.log, database = self.database}
+
+ -- Get caller phone
+ local caller_phone = phone_class:find_hot_deskable_by_account(caller_sip_account.id);
+ if not caller_phone then
+ self.log:notice("Caller phone not found or not hot-deskable");
+ return { continue = false, code = 401, phrase = 'Phone not hot-deskable', no_cdr = true }
+ end
+
+ log:debug("Hot-desking auto log off - caller phone: " .. caller_phone.record.id .. ", mac: " .. caller_phone.record.mac_address);
+
+ sql_query = 'UPDATE `phones` SET `nightly_reboot` = ' .. nightly_reboot .. ' WHERE `id` = ' .. tonumber(caller_phone.record.id);
+
+ if not self.database:query(sql_query) then
+ self.log:error('Hot-desking auto log off status could not be changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot);
+ return { continue = false, code = 401, phrase = 'Value could not be changed', no_cdr = true }
+
+ end
+
+ self.log:debug('Hot-desking auto log off changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot);
+
+ caller:answer();
+ caller:send_display('Logout successful');
+ caller:sleep(1000);
+end
+
+function Functions.dial_clir_off(self, caller, phone_number)
+ -- Ensure a valid sip account
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ caller.clir = false;
+ return { continue = true, number = phone_number }
+end
+
+function Functions.dial_clir_on(self, caller, phone_number)
+ -- Ensure a valid sip account
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ caller.clir = true;
+ return { continue = true, number = phone_number }
+end
+
+function Functions.callwaiting_on(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = TRUE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("Call Waiting could not be set");
+ return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Call waiting on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.callwaiting_off(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = FALSE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("Call Waiting could not be set");
+ return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true }
+ end
+
+ caller:answer();
+ caller:send_display('Call waiting off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.clip_on(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `clip` = TRUE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("CLIP could not be set");
+ return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('CLIP on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.clip_off(self, caller)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ local sql_query = 'UPDATE `sip_accounts` SET `clip` = FALSE WHERE `id` = ' .. caller_sip_account.record.id;
+
+ if not self.database:query(sql_query) then
+ self.log:notice("CLIP could not be set");
+ return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('CLIP off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_off(self, caller, call_forwarding_service, delete)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require 'common.phone_number'
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local success = false;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if phone_number_object:call_forwarding_off(call_forwarding_service, nil, delete) then
+ success = true;
+ end
+ end
+ end
+
+ if not success then
+ self.log:notice("call forwarding could not be deactivated");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be deactivated', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding off');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_on(self, caller, call_forwarding_service, destination, destination_type, timeout)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ if not call_forwarding_service then
+ self.log:notice('no call forwarding service specified');
+ end
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local success = false;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if phone_number_object:call_forwarding_on(call_forwarding_service, destination, timeout) then
+ success = true;
+ end
+ end
+ end
+
+ if not success then
+ self.log:notice("call forwarding could not be activated");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be activated', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding on');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.call_forwarding_toggle(self, caller, call_forwarding_service, phone_number_id)
+ local defaults = {log = self.log, database = self.database, domain = caller.domain}
+
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain };
+ local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount');
+
+ local result = nil;
+ for index, phone_number in pairs(phone_numbers) do
+ phone_number_object = phone_number_class:find_by_number(phone_number);
+ if phone_number_object then
+ if not result then
+ result = phone_number_object:call_forwarding_toggle(call_forwarding_service);
+ elseif result.active then
+ phone_number_object:call_forwarding_on(call_forwarding_service, result.destination, result.destination_type, result.timeout);
+ else
+ phone_number_object:call_forwarding_off(call_forwarding_service);
+ end
+ end
+ end
+
+ if not result then
+ self.log:notice("call forwarding could not be toggled");
+ return { continue = false, code = 500, phrase = 'Call Forwarding could not be toggled', no_cdr = true }
+
+ end
+
+ caller:answer();
+ caller:send_display('Call forwarding toggled');
+ caller:sleep(1000);
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.voicemail_message_leave(self, caller, phone_number)
+ require 'dialplan.voicemail'
+ local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number);
+
+ if not voicemail_account then
+ return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true }
+ end
+
+ voicemail_account:leave(caller, phone_number);
+
+ if caller:to_s("voicemail_message_len") ~= '' then
+ voicemail_account:send_notify(caller);
+ else
+ self.log:debug("voicemail - no message saved");
+ end
+
+ return { continue = false, code = 200, phrase = 'OK' }
+end
+
+
+function Functions.voicemail_check(self, caller, phone_number)
+ local voicemail_account = nil;
+ local voicemail_authorized = false;
+
+ require 'dialplan.voicemail'
+
+ if phone_number then
+ voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number);
+ else
+ if caller.auth_account_type == 'SipAccount' then
+ voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(caller.auth_account.id);
+ voicemail_authorized = true;
+ end
+ end
+
+ if not voicemail_account then
+ return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true }
+ end
+
+ voicemail_account:menu(caller, voicemail_authorized);
+
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+
+function Functions.acd_membership_toggle(self, caller, agent_id, phone_number)
+ -- Find caller's SipAccount
+ local caller_sip_account = self:ensure_caller_sip_account(caller);
+ if not caller_sip_account then
+ return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true }
+ end
+
+ require 'dialplan.acd'
+ local acd_class = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain };
+
+ self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', ACD phone number: ', phone_number);
+
+ if not tonumber(agent_id) or tonumber(agent_id) == 0 then
+
+ if not phone_number then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - neither agent_id nor phone_number specified');
+ return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true }
+ end
+
+ require "common.phone_number"
+ local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }:find_by_number(phone_number, {'AutomaticCallDistributor'});
+
+ if not phone_number_object or not tonumber(phone_number_object.record.phone_numberable_id) then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - ACD not found');
+ return { continue = false, code = 404, phrase = 'ACD not found', no_cdr = true }
+ end
+
+ local agent = acd_class:agent_find_by_acd_and_destination(phone_number_object.record.phone_numberable_id, caller_sip_account.class, caller_sip_account.id);
+
+ if not agent or not tonumber(agent.id) then
+ self.log:notice('ACD_MEMBERSHIP_TOGGLE - agent not found');
+ return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true }
+ end
+
+ agent_id = agent.id;
+ end
+
+ local status = acd_class:agent_status_toggle(agent_id, 'sipaccount', caller_sip_account.id);
+
+ if not status then
+ self.log:error('ACD_MEMBERSHIP_TOGGLE - error toggling ACD membership');
+ return { continue = false, code = 500, phrase = 'Error toggling ACD membership', no_cdr = true }
+ end
+
+ self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', status: ', status);
+
+ caller:answer();
+ caller:send_display('ACD membership toggled: ' .. status);
+ caller:sleep(500);
+ caller.session:sayPhrase('acd_agent_status', tostring(status));
+ return { continue = false, code = 200, phrase = 'OK', no_cdr = true }
+end
+
+function Functions.hangup(self, caller, code, phrase)
+ require 'common.str'
+
+ if not tonumber(code) then
+ code = 403;
+ phrase = 'Forbidden';
+ end
+
+ if common.str.blank(phrase) then
+ phrase = 'Hangup here';
+ end
+
+ self.log:info("FUNCTION_HANGUP code: ", code, ', phrase: ', phrase);
+ return { continue = false, code = code, phrase = phrase:gsub('_', ' '), no_cdr = true }
+end
diff --git a/misc/freeswitch/scripts/dialplan/geo_number.lua b/misc/freeswitch/scripts/dialplan/geo_number.lua
new file mode 100644
index 0000000..06bfd62
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/geo_number.lua
@@ -0,0 +1,89 @@
+-- Gemeinschaft 5 module: geonumber class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+GeoNumber = {}
+
+-- create phone_book object
+function GeoNumber.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'geonumber';
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+function GeoNumber.country(self, phone_number)
+ if phone_number:match('^%+1') then
+ return { id = 0, name = 'NANP', country_code = '1' }
+ end
+
+ local country_codes = {};
+ for i = 2, 4, 1 do
+ table.insert(country_codes, phone_number:sub(2, i));
+ end
+
+ local sql_query = 'SELECT * FROM `countries` WHERE `country_code` IN ("' .. table.concat(country_codes, '","') .. '") ORDER BY LENGTH(`country_code`) DESC LIMIT 1';
+
+ local country = nil;
+ self.database:query(sql_query, function(entry)
+ country = entry;
+ end)
+
+ return country;
+end
+
+
+function GeoNumber.area_code(self, phone_number, country_code)
+ local sql_query = nil;
+ local area_code = nil;
+
+ if country_code == '1' then
+ area_code = {}
+ area_code.area_code, area_code.central_office_code, area_code.subscriber_number, area_code.extension = phone_number:match('%+1(%d%d%d)(%d%d%d)(%d%d%d%d)(%d*)');
+ sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \
+ JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \
+ WHERE `b`.`country_code` = "' .. tostring(country_code) .. '"\
+ AND `a`.`area_code` = "' .. tostring(area_code.area_code) .. '" \
+ AND `a`.`central_office_code` = "' .. tostring(area_code.central_office_code) .. '" LIMIT 1';
+ else
+ local offset = #country_code;
+ area_codes = {};
+ for i = (3 + offset), (6 + offset), 1 do
+ table.insert(area_codes, phone_number:sub((2 + offset), i));
+ end
+
+ sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \
+ JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \
+ WHERE `b`.`country_code` = "' .. country_code .. '"\
+ AND `a`.`area_code` IN ("' .. table.concat(area_codes, '","') .. '") ORDER BY LENGTH(`a`.`area_code`) DESC LIMIT 1';
+ end
+
+ self.database:query(sql_query, function(entry)
+ area_code = entry;
+ end)
+
+ return area_code;
+end
+
+
+function GeoNumber.find(self, phone_number)
+ if not phone_number:match('^%+%d+') then
+ return nil;
+ end
+
+ local country = self:country(phone_number);
+ if country then
+ local area_code = self:area_code(phone_number, country.country_code);
+ if area_code then
+ return area_code;
+ else
+ return { country = country.name };
+ end
+ end
+end
diff --git a/misc/freeswitch/scripts/dialplan/hunt_group.lua b/misc/freeswitch/scripts/dialplan/hunt_group.lua
new file mode 100644
index 0000000..87f86f1
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/hunt_group.lua
@@ -0,0 +1,202 @@
+-- Gemeinschaft 5 module: hunt group class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+HuntGroup = {}
+
+local DEFAULT_MEMBER_TIMEOUT = 20;
+
+-- Create HuntGroup object
+function HuntGroup.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'huntgroup';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+
+function HuntGroup.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1';
+ local hunt_group = nil;
+
+ self.database:query(sql_query, function(entry)
+ hunt_group = HuntGroup:new(self);
+ hunt_group.record = entry;
+ hunt_group.id = tonumber(entry.id);
+ hunt_group.uuid = entry.uuid;
+ end)
+
+ return hunt_group;
+end
+
+
+function HuntGroup.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= "'.. uuid .. '" LIMIT 1';
+ local hunt_group = nil;
+
+ self.database:query(sql_query, function(entry)
+ hunt_group = HuntGroup:new(self);
+ hunt_group.record = entry;
+ hunt_group.id = tonumber(entry.id);
+ hunt_group.uuid = entry.uuid;
+ end)
+
+ return hunt_group;
+end
+
+
+function HuntGroup.list_active_members(self)
+ local sql_query = 'SELECT `a`.`number`, `b`.`name` \
+ FROM `phone_numbers` `a` \
+ LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \
+ WHERE `a`.`phone_numberable_type` = "huntgroupmember" \
+ AND `b`.`active` IS TRUE \
+ AND `b`.`hunt_group_id` = ' .. self.record.id;
+
+ local hunt_group_members = {}
+
+ self.database:query(sql_query, function(hunt_group_members_entry)
+ table.insert(hunt_group_members, hunt_group_members_entry);
+ end)
+
+ return hunt_group_members;
+end
+
+
+function HuntGroup.is_member_by_numbers(self, numbers)
+ local sql_query = 'SELECT `a`.`number`, `b`.`name` \
+ FROM `phone_numbers` `a` \
+ LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \
+ WHERE `a`.`phone_numberable_type` = "huntgroupmember" \
+ AND `b`.`active` IS TRUE \
+ AND `b`.`hunt_group_id` = ' .. self.record.id .. '\
+ AND `a`.`number` IN ("' .. table.concat( numbers, '","') .. '") LIMIT 1';
+
+ local hunt_group_member = false;
+
+ self.database:query(sql_query, function(hunt_group_members_entry)
+ hunt_group_member = true;
+ end)
+
+ return hunt_group_member;
+end
+
+
+function HuntGroup.run(self, dialplan_object, caller, destination)
+ local hunt_group_members = self:list_active_members();
+
+ if #hunt_group_members == 0 then
+ return { disposition = 'HUNT_GROUP_EMPTY', code = 480, phrase = 'No active users' }
+ end
+
+ self.log:info('HUNTGROUP ', self.record.id, ' - name: ', self.record.name, ', strategy: ', self.record.strategy,', members: ', #hunt_group_members);
+
+ local destinations = {}
+ for index, hunt_group_member in ipairs(hunt_group_members) do
+ local destination = dialplan_object:destination_new{ number = hunt_group_member.number };
+ if destination.type == 'unknown' then
+ require 'dialplan.route'
+ local routes = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = dialplan_object.routes }:outbound(caller, destination.number);
+ if routes and #routes > 0 then
+ destination.callee_id_number = destination.number;
+ destination.callee_id_name = nil;
+ local route = routes[1];
+ destination.gateway = route.endpoint;
+ destination.type = route.class;
+ destination.number = route.value;
+ destination.caller_id_number = route.caller_id_number;
+ destination.caller_id_name = route.caller_id_name;
+ table.insert(destinations, destination);
+ end
+ else
+ table.insert(destinations, destination);
+ end
+ end
+
+ local forwarding_destination = nil;
+ if caller.forwarding_service == 'assistant' and caller.auth_account then
+ forwarding_destination = dialplan_object:destination_new{ type = caller.auth_account.class, id = caller.auth_account.id, number = forwarding_number }
+ forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0';
+ end
+
+ local result = { continue = false };
+ local start_time = os.time();
+ if self.record.strategy == 'ring_recursively' then
+ local member_timeout = tonumber(self.record.seconds_between_jumps) or DEFAULT_MEMBER_TIMEOUT;
+ local run_queue = true;
+ while run_queue do
+ for index, member_destination in ipairs(destinations) do
+ local recursive_destinations = { member_destination }
+ if forwarding_destination then
+ table.insert(recursive_destinations, forwarding_destination);
+ end
+ require 'dialplan.sip_call'
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( recursive_destinations, { callee_id_number = destination.number, timeout = member_timeout });
+ if result.disposition == 'SUCCESS' then
+ if result.fork_index then
+ result.destination = recursive_destinations[result.fork_index];
+ end
+ run_queue = false;
+ break;
+ elseif os.time() > start_time + dialplan_object.dial_timeout_active then
+ run_queue = false;
+ break;
+ elseif not caller:ready() then
+ run_queue = false;
+ break;
+ end
+ end
+ if tostring(result.code) == '486' then
+ self.log:info('HUNTGROUP ', self.record.id, ' - all members busy');
+ run_queue = false;
+ end
+ end
+ else
+ if forwarding_destination then
+ table.insert(destinations, forwarding_destination);
+ end
+
+ require 'dialplan.sip_call'
+ result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( destinations,
+ {
+ callee_id_number = destination.number,
+ timeout = dialplan_object.dial_timeout_active,
+ send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ),
+ });
+ if result.fork_index then
+ result.destination = destinations[result.fork_index];
+ end
+
+ return result;
+ end
+
+ return result;
+end
+
+
+function HuntGroup.list_destination_numbers(self)
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new(defaults)
+
+ local sql_query = string.format("SELECT * FROM `phone_numbers` WHERE `state`='active' AND `phone_numberable_type` = 'HuntGroupMember' AND `phone_numberable_id` IN ( \
+ SELECT `id` FROM `hunt_group_members` WHERE `active` IS TRUE AND `hunt_group_id`=%d ) ORDER BY `position` ASC", tonumber(self.record.id));
+ local phone_numbers = {}
+
+ self.database:query(sql_query, function(hunt_group_number_entry)
+ local number_object = phone_number_class:find_by_number(hunt_group_number_entry.number)
+ if number_object and number_object.record then
+ table.insert(phone_numbers, {number = hunt_group_number_entry.number, destination_type = number_object.record.phone_numberable_type, destination_id = number_object.record.phone_numberable_id});
+ else
+ table.insert(phone_numbers, {number = hunt_group_number_entry.number});
+ end
+ end)
+
+ return phone_numbers ;
+end
diff --git a/misc/freeswitch/scripts/dialplan/phone_book.lua b/misc/freeswitch/scripts/dialplan/phone_book.lua
new file mode 100644
index 0000000..089f115
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/phone_book.lua
@@ -0,0 +1,63 @@
+-- Gemeinschaft 5 module: phone book class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+PhoneBook = {}
+
+-- create phone_book object
+function PhoneBook.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'phonebook';
+ self.log = arg.log;
+ self.database = arg.database;
+ return object;
+end
+
+
+function PhoneBook.find_entry_by_number_user_tenant(self, numbers, user_id, tenant_id)
+ user_id = tonumber(user_id) or 0;
+ tenant_id = tonumber(tenant_id) or 0;
+
+ if not numbers or #numbers == 0 then
+ return nil;
+ end
+ local numbers_sql = '"' .. table.concat(numbers, '","') .. '"';
+
+ local sql_query = 'SELECT `a`.`name` AS `number_name`, \
+ `a`.`number`, \
+ `b`.`id`, \
+ `b`.`value_of_to_s`, \
+ `b`.`phone_book_id`, \
+ `b`.`image`, \
+ `c`.`name` AS `phone_book_name`, \
+ `d`.`bellcore_id` \
+ FROM `phone_numbers` `a` \
+ JOIN `phone_book_entries` `b` ON `a`.`phone_numberable_id` = `b`.`id` AND `a`.`phone_numberable_type` = "PhoneBookENtry" \
+ JOIN `phone_books` `c` ON `b`.`phone_book_id` = `c`.`id` \
+ LEFT JOIN `ringtones` `d` ON `a`.`id` = `d`.`ringtoneable_id` AND `d`.`ringtoneable_type` = "PhoneNumber" \
+ WHERE ((`c`.`phone_bookable_type` = "User" AND `c`.`phone_bookable_id` = ' .. user_id .. ') \
+ OR (`c`.`phone_bookable_type` = "Tenant" AND `c`.`phone_bookable_id` = ' .. tenant_id .. ')) \
+ AND `a`.`number` IN (' .. numbers_sql .. ') \
+ AND `a`.`state` = "active" \
+ AND `b`.`state` = "active" \
+ AND `c`.`state` = "active" \
+ ORDER BY `c`.`phone_bookable_type` DESC LIMIT 1';
+
+ local phone_book_entry = nil;
+
+ self.database:query(sql_query, function(entry)
+ phone_book_entry = entry;
+ if entry.number_name then
+ phone_book_entry.caller_id_name = tostring(entry.value_of_to_s) .. ' (' .. entry.number_name .. ')';
+ else
+ phone_book_entry.caller_id_name = entry.value_of_to_s;
+ end
+ end)
+
+ return phone_book_entry;
+end
diff --git a/misc/freeswitch/scripts/dialplan/presence.lua b/misc/freeswitch/scripts/dialplan/presence.lua
new file mode 100644
index 0000000..234b908
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/presence.lua
@@ -0,0 +1,84 @@
+-- Gemeinschaft 5 module: presence class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Presence = {}
+
+-- Create Presence object
+function Presence.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self
+ self.log = arg.log;
+ self.domain = arg.domain;
+ self.uuid = arg.uuid;
+ self.inbound = arg.inbound;
+ self.accounts = arg.accounts;
+
+ return object
+end
+
+
+function Presence.init(self, arg)
+ self.log = arg.log or self.log;
+ self.domain = arg.domain or self.domain;
+ self.uuid = arg.uuid or self.uuid;
+ self.inbound = arg.inbound or self.inbound;
+ self.accounts = arg.accounts or self.accounts;
+end
+
+
+function Presence.set(self, state, caller_number)
+ if not self.accounts or #self.accounts == 0 then
+ return nil;
+ end
+
+ state = state or "terminated";
+ local direction = "outbound";
+
+ if self.inbound then
+ direction = "inbound";
+ end
+
+ for index, account in pairs(self.accounts) do
+ if account ~= '' then
+ local event = freeswitch.Event('PRESENCE_IN');
+ event:addHeader('proto', 'sip');
+ event:addHeader('from', account .. '@' .. self.domain);
+ event:addHeader('event_type', 'presence');
+ event:addHeader('alt_event_type', 'dialog');
+ event:addHeader('presence-call-direction', direction);
+ event:addHeader('answer-state', state);
+ event:addHeader('unique-id', self.uuid);
+ if caller_number then
+ if self.inbound then
+ event:addHeader('Caller-Destination-Number', caller_number);
+ else
+ event:addHeader('Other-Leg-Caller-ID-Number', caller_number);
+ end
+ end
+ event:fire();
+ self.log:debug('PRESENCE - account: ' .. account .. '@' .. self.domain .. ', state: ' .. state .. ', direction: ' .. direction .. ', uid: ' ..self.uuid);
+ end
+ end
+
+ return true;
+end
+
+
+function Presence.early(self, caller_number)
+ return self:set("early", caller_number);
+end
+
+
+function Presence.confirmed(self, caller_number)
+ return self:set("confirmed", caller_number);
+end
+
+
+function Presence.terminated(self, caller_number)
+ return self:set("terminated", caller_number);
+end
diff --git a/misc/freeswitch/scripts/dialplan/route.lua b/misc/freeswitch/scripts/dialplan/route.lua
new file mode 100644
index 0000000..2243cbe
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/route.lua
@@ -0,0 +1,265 @@
+-- Gemeinschaft 5 module: routing class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Route = {}
+
+-- create route object
+function Route.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.database = arg.database;
+ self.routing_table = arg.routing_table;
+ self.expandable = arg.expandable or {};
+ return object;
+end
+
+-- find matching routes
+function Route.prerouting(self, caller, number)
+ require 'common.routing_tables'
+
+ for index, routing_entry in pairs(self.routing_table.prerouting) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('PREROUTE - error: ', route.error);
+ elseif route.value then
+ self.log:info('ROUTE_PREROUTING - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern);
+ return route;
+ end
+ end
+end
+
+-- find matching routes
+function Route.outbound(self, caller, number)
+ local routes = {};
+ require 'common.routing_tables'
+ require 'common.str'
+
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ local clip_no_screening = common.str.try(caller, 'account.record.clip_no_screening');
+ local caller_id_numbers = {}
+ if not common.str.blank(clip_no_screening) then
+ for index, number in ipairs(common.str.strip_to_a(clip_no_screening, ',')) do
+ table.insert(caller_id_numbers, number);
+ end
+ end
+ for index, number in ipairs(caller.caller_phone_numbers) do
+ table.insert(caller_id_numbers, number);
+ end
+ self.log:info('CALLER_ID_NUMBER - caller_id_numbers: ', table.concat(caller_id_numbers, ','));
+
+ for index, routing_entry in pairs(self.routing_table.outbound) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('ROUTE_OUTBOUND - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.group then
+ if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - group=', route.group, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - group=', route.group, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if route.cidn then
+ if caller.caller_id_number:match(route.cidn) then
+ self.log:info('ROUTE_OUTBOUND_POSITIVE - cidn=', route.cidn, ' ~ ', caller.caller_id_number,', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_OUTBOUND_NEGATIVE - cidn=', route.cidn, ' !~ ', caller.caller_id_number, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ if route.class ~= 'hangup' then
+ route.caller_id_number = self:outbound_cid_number(caller, caller_id_numbers, route.endpoint, route.class);
+ self.expandable.caller_id_number = route.caller_id_number;
+ route.caller_id_name = self:outbound_cid_name(caller, route.endpoint, route.class);
+ end
+ table.insert(routes, route);
+ self.log:info('ROUTE_OUTBOUND ', #routes,' - ', route.class, '=', route.endpoint, ', value: ', route.value, ', caller_id_number: ', route.caller_id_number, ', caller_id_name: ', route.caller_id_name);
+ end
+ end
+ end
+
+ return routes;
+end
+
+
+function Route.inbound(self, caller, number)
+ require 'common.routing_tables'
+
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ for index, routing_entry in pairs(self.routing_table.inbound) do
+ local route = common.routing_tables.match_route(routing_entry, number);
+ if route.error then
+ self.log:error('ROUTE_INBOUND - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:info('ROUTE_INBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_INBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.class and route.endpoint then
+ if route.class == 'gateway' and caller.gateway_name:match(route.endpoint) then
+ self.log:info('ROUTE_INBOUND_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', caller.gateway_name, ', pattern: ', route.pattern);
+ else
+ self.log:info('ROUTE_INBOUND_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', caller.gateway_name, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ self.log:info('ROUTE_INBOUND - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern);
+ return route;
+ end
+ end
+ end
+end
+
+-- find caller id
+function Route.caller_id(self, caller, cid_entry, search_str, endpoint, class)
+ local ignore_arguments = {
+ class=true,
+ endpoint=true,
+ pattern=true,
+ value=true,
+ group=true,
+ phrase=true,
+ }
+
+ local route = common.routing_tables.match_route(cid_entry, search_str, self.expandable);
+ if route.error then
+ self.log:error('CALLER_ID - error: ', route.error);
+ elseif route.value then
+ local valid_route = true;
+
+ for argument, value in pairs(route) do
+ if not ignore_arguments[argument] then
+ local table_value = common.str.downcase(tostring(common.str.try(caller, argument)));
+ value = common.str.downcase(tostring(value));
+ if table_value:match(value) then
+ self.log:debug('CALLER_ID_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+ end
+
+ if route.group then
+ if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then
+ self.log:debug('CALLER_ID_POSITIVE - group=', route.group, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - group=', route.group, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ endpoint = tostring(endpoint);
+ if route.class and route.endpoint then
+ if route.class == 'gateway' and endpoint:match(route.endpoint) then
+ self.log:debug('CALLER_ID_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', endpoint, ', pattern: ', route.pattern);
+ else
+ self.log:debug('CALLER_ID_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', endpoint, ', pattern: ', route.pattern);
+ valid_route = false;
+ end
+ end
+
+ if valid_route then
+ self.log:debug('CALLER_ID ', route.class, '=', route.endpoint, ', value: ', route.value);
+ return route.value;
+ end
+ end
+
+ return nil;
+end
+
+-- find matching caller id number
+function Route.outbound_cid_number(self, caller, caller_id_numbers, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.outbound_cid_number) do
+ for index, number in ipairs(caller_id_numbers) do
+ local route = self:caller_id(caller, cid_entry, number, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+ end
+end
+
+-- find matching caller id name
+function Route.outbound_cid_name(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.outbound_cid_name) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
+
+-- find matching caller id number
+function Route.inbound_cid_number(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.inbound_cid_number) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_number, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
+
+-- find matching caller id name
+function Route.inbound_cid_name(self, caller, endpoint, class)
+ for route_index, cid_entry in pairs(self.routing_table.inbound_cid_name) do
+ local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class);
+ if route then
+ return route;
+ end
+ end
+end
diff --git a/misc/freeswitch/scripts/dialplan/session.lua b/misc/freeswitch/scripts/dialplan/session.lua
new file mode 100644
index 0000000..7174b24
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/session.lua
@@ -0,0 +1,224 @@
+-- Gemeinschaft 5 module: caller session class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Session = {}
+
+-- create session object
+function Session.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.session = arg.session;
+
+ if not self.session then
+ return nil;
+ end
+
+ return object;
+end
+
+function Session.init_channel_variables(self)
+ self.cause = "UNSPECIFIED"
+
+ self.uuid = self.session:get_uuid();
+ self.destination_number = self:expand_variables(self:to_s('destination_number'));
+ self.called_number = self.destination_number;
+
+ self.caller_id_number = self:to_s('caller_id_number');
+ self.caller_id_name = self:to_s('caller_id_name');
+ self.caller_phone_number = self.caller_id_number;
+ self.caller_phone_numbers = {self.caller_id_number};
+
+ self.domain = self:to_s('domain_name');
+ self.gateway_name = self:to_s('sip_gateway');
+ self.from_gateway = self:to_b('gs_from_gateway');
+ if self.from_gateway then
+ self.gateway_name = self:to_s('gs_gateway_name');
+ elseif self.gateway_name ~= '' then
+ self.from_gateway = true;
+ end
+
+ self.account_uuid = self:to_s('gs_account_uuid');
+ self.account_type = self:to_s('gs_account_type');
+ self.sip_contact_host = self:to_s('sip_contact_host');
+ self.clir = self:to_b('gs_clir');
+ self.call_timeout = self:to_i('gs_call_timeout');
+ self.auth_account_type = self:to_s('gs_auth_account_type');
+ self.auth_account_uuid = self:to_s('gs_auth_account_uuid');
+
+ self.node_id = self:to_i('sip_h_X-GS_node_id');
+ self.loop_count = self:to_i('sip_h_X-GS_loop_count');
+
+ if self.node_id > 0 and self.node_id ~= self.local_node_id then
+ self.from_node = true;
+ else
+ self.from_node = false;
+ end
+ self:set_variable('gs_account_node_local', not self.from_node);
+
+ if self.from_node then
+ self.account_uuid = self:to_s('sip_h_X-GS_account_uuid');
+ self.account_type = self:to_s('sip_h_X-GS_account_type');
+ self.auth_account_uuid = self:to_s('sip_h_X-GS_auth_account_uuid');
+ self.auth_account_type = self:to_s('sip_h_X-GS_auth_account_type');
+ end
+
+ if self.auth_account_type == '' then
+ self.auth_account_type = self.account_type;
+ self.auth_account_uuid = self.account_uuid;
+ end
+
+ self.forwarding_number = nil;
+ self.forwarding_service = nil;
+
+ return true;
+end
+
+
+-- Cast channel variable to string
+function Session.to_s(self, variable_name)
+ require 'common.str'
+ return common.str.to_s(self.session:getVariable(variable_name));
+end
+
+-- Cast channel variable to integer
+function Session.to_i(self, variable_name)
+ require 'common.str'
+ return common.str.to_i(self.session:getVariable(variable_name));
+end
+
+-- Cast channel variable to boolean
+function Session.to_b(self, variable_name)
+ require 'common.str'
+ return common.str.to_b(self.session:getVariable(variable_name));
+end
+
+-- Split channel variable to table
+function Session.to_a(self, variable_name)
+ require 'common.str'
+ return common.str.to_a(self.session:getVariable(variable_name));
+end
+
+-- Check if session is active
+function Session.ready(self, command, parameters)
+ return self.session:ready();
+end
+
+-- Wait milliseconds
+function Session.sleep(self, milliseconds)
+ return self.session:sleep(milliseconds);
+end
+
+-- Execute command
+function Session.execute(self, command, parameters)
+ parameters = parameters or '';
+ self.session:execute(command, parameters);
+end
+
+-- Execute and return result
+function Session.result(self, command_line)
+ self.session:execute('set', 'result=${' .. command_line .. '}');
+ return self.session:getVariable('result');
+end
+
+-- Set cause code
+function Session.set_cause(self, cause)
+ self.cause = cause
+end
+
+-- Set channel variable
+function Session.set_variable(self, name, value)
+ self.session:setVariable(name, tostring(value));
+end
+
+-- Set and export channel variable
+function Session.export_variable(self, name, value)
+ self.session:execute('export', tostring(name) .. '=' .. tostring(value));
+end
+
+-- Set SIP header
+function Session.set_header(self, name, value)
+ self.session:setVariable('sip_h_' .. name, tostring(value));
+end
+
+-- Hangup a call
+function Session.hangup(self, cause)
+ return self.session:hangup(cause);
+end
+
+-- Respond a call
+function Session.respond(self, code, text)
+ self.session:execute('respond', tostring(code) .. ' ' .. text);
+ return self.session:hangupCause();
+end
+
+-- Answer a call
+function Session.answer(self)
+ return self.session:answer();
+end
+
+function Session.intercept(self, uid)
+ self.session:execute("intercept", uid);
+end
+
+function Session.send_display(self, ... )
+ self:execute('send_display', table.concat( arg, '|'));
+end
+
+-- Set caller ID
+function Session.set_caller_id(self, number, name)
+ if number then
+ self.caller_id_number = tostring(number);
+ self.session:setVariable('effective_caller_id_number', tostring(number))
+ end
+ if name then
+ self.caller_id_name = tostring(name);
+ self.session:setVariable('effective_caller_id_name', tostring(name))
+ end
+end
+
+-- Set callee ID
+function Session.set_callee_id(self, number, name)
+ if number ~= nil then
+ self.callee_id_number = tostring(number);
+ self.session:execute('export', 'effective_callee_id_number=' .. number);
+ end
+ if name ~= nil then
+ self.callee_id_name = tostring(name);
+ self.session:execute('export', 'effective_callee_id_name=' .. name);
+ end
+end
+
+-- Set caller Privacy header
+function Session.set_privacy(self, privacy)
+ if privacy then
+ self.session:setVariable('cid_type', 'none');
+ self.session:setVariable('sip_h_Privacy', 'id');
+ else
+ self.session:setVariable('cid_type', 'none');
+ self.session:setVariable('sip_h_Privacy', 'none');
+ end
+end
+
+
+function Session.set_auth_account(self, auth_account)
+ if auth_account then
+ self:set_variable('gs_auth_account_type', auth_account.class);
+ self:set_variable('gs_auth_account_id', auth_account.id);
+ self:set_variable('gs_auth_account_uuid', auth_account.uuid);
+ end
+
+ return auth_account;
+end
+
+
+function Session.expand_variables(self, line)
+ return (line:gsub('{([%a%d_-]+)}', function(captured)
+ return self.session:getVariable(captured) or '';
+ end))
+end
diff --git a/misc/freeswitch/scripts/dialplan/sip_call.lua b/misc/freeswitch/scripts/dialplan/sip_call.lua
new file mode 100644
index 0000000..57f92c6
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/sip_call.lua
@@ -0,0 +1,266 @@
+-- Gemeinschaft 5 module: sip call class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall);
+
+SipCall = {}
+
+-- Create SipCall object
+function SipCall.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.log = arg.log;
+ self.session = arg.session;
+ self.record = arg.record;
+ self.database = arg.database;
+ self.domain = arg.domain;
+ self.caller = arg.caller;
+ self.on_answer = arg.on_answer;
+ self.calling_object = arg.calling_object;
+ return object;
+end
+
+
+function SipCall.wait_answer(self, caller_session, callee_session, timeout, start_time)
+ if caller_session:ready() and callee_session:ready() then
+ callee_session:waitForAnswer(caller_session);
+ end
+
+ while true do
+ if not caller_session:ready() then
+ return 'ORIGINATOR_CANCEL';
+ elseif not callee_session:ready() then
+ return 'UNSPECIFIED';
+ elseif (os.time() - start_time) > timeout then
+ return 'NO_ANSWER';
+ elseif callee_session:answered() then
+ return 'SUCCESS';
+ end
+
+ self.caller:sleep(500);
+ end
+end
+
+
+function SipCall.wait_hangup(self, caller_session, callee_session)
+ local hangup_on = {
+ CS_HANGUP = true,
+ CS_DESTROY = true,
+ }
+
+ while true do
+ local state_caller = caller_session:getState();
+ local state_callee = callee_session:getState();
+ if hangup_on[state_caller] or hangup_on[state_callee] then
+ break;
+ end
+ caller_session:sleep(500);
+ end
+end
+
+
+function SipCall.call_waiting_busy(self, sip_account)
+ require 'common.str'
+ if common.str.to_b(sip_account.record.call_waiting) then
+ self.log:info('CALL_WAITING - status: enabled');
+ return false;
+ else
+ local state = sip_account:call_state();
+ self.log:info('CALL_WAITING - status: disabled, sip_account state: ', state);
+ return state;
+ end
+end
+
+
+function SipCall.fork(self, destinations, arg )
+ local dial_strings = {}
+
+ require 'common.sip_account'
+ local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database };
+
+ local call_result = { code = 404, phrase = 'No destination' };
+ local some_destinations_busy = false;
+
+ for index, destination in ipairs(destinations) do
+ local origination_variables = { 'gs_fork_index=' .. index }
+
+ self.log:info('FORK ', index, '/', #destinations, ' - ', destination.type, '=', destination.id, '/', destination.gateway or destination.uuid, '@', destination.node_id, ', number: ', destination.number);
+ if not destination.node_local or destination.type == 'node' then
+ require 'common.node'
+ local node = nil;
+ if tonumber(destination.gateway) then
+ node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(tonumber(destination.gateway));
+ else
+ node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(destination.node_id);
+ end
+ if node then
+ table.insert(origination_variables, 'sip_h_X-GS_node_id=' .. self.caller.local_node_id);
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. node.record.name .. '/' .. destination.number);
+ end
+ elseif destination.type == 'sipaccount' then
+ local callee_id_params = '';
+ local sip_account = sip_account_class:find_by_id(destination.id);
+ local call_waiting = self:call_waiting_busy(sip_account);
+ if not call_waiting then
+ destinations[index].numbers = sip_account:phone_numbers();
+
+ if not arg.callee_id_name then
+ table.insert(origination_variables, "effective_callee_id_name='" .. sip_account.record.caller_name .. "'");
+ end
+ if not arg.callee_id_number then
+ table.insert(origination_variables, "effective_callee_id_number='" .. destination.number .. "'");
+ end
+ if destination.alert_info then
+ table.insert(origination_variables, "alert_info='" .. destination.alert_info .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']user/' .. sip_account.record.auth_name);
+ else
+ some_destinations_busy = true;
+ call_result = { code = 486, phrase = 'User busy', disposition = 'USER_BUSY' };
+ end
+ elseif destination.type == 'gateway' then
+ if destination.caller_id_number then
+ table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'");
+ end
+ if destination.caller_id_name then
+ table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. destination.gateway .. '/' .. destination.number);
+ elseif destination.type == 'dial' then
+ if destination.caller_id_number then
+ table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'");
+ end
+ if destination.caller_id_name then
+ table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'");
+ end
+ table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']' .. destination.number);
+ else
+ self.log:info('FORK ', index, '/', #destinations, ' - unhandled destination type: ', destination.type, ', number: ', destination.number);
+ end
+ end
+
+ if #dial_strings == 0 then
+ self.log:notice('FORK - no active destinations - result: ', call_result.code, ' ', call_result.phrase);
+ return call_result;
+ end
+
+ self.caller:set_callee_id(arg.callee_id_number, arg.callee_id_name);
+ self.caller:set_header('X-GS_account_uuid', self.caller.account_uuid);
+ self.caller:set_header('X-GS_account_type', self.caller.account_type);
+ self.caller:set_header('X-GS_auth_account_type', self.caller.auth_account_type);
+ self.caller:set_header('X-GS_auth_account_uuid', self.caller.auth_account_uuid);
+ self.caller:set_header('X-GS_loop_count', self.caller.loop_count);
+
+ self.caller:set_variable('call_timeout', arg.timeout );
+ self.log:info('FORK DIAL - destinations: ', #dial_strings, ', timeout: ', arg.timeout);
+
+ if arg.send_ringing then
+ self.caller:execute('ring_ready');
+ end
+
+ local start_time = os.time();
+ local session_callee = freeswitch.Session('{local_var_clobber=true}' .. table.concat(dial_strings, ','), self.caller.session);
+ self.log:debug('FORK SESSION_INIT - dial_time: ', os.time() - start_time);
+ local answer_result = self:wait_answer(self.caller.session, session_callee, arg.timeout, start_time);
+ local fork_index = nil;
+ self.log:info('FORK ANSWER - status: ', answer_result, ', dial_time: ', os.time() - start_time);
+ if answer_result == 'SUCCESS' then
+ session_callee:setAutoHangup(false);
+ fork_index = tonumber(session_callee:getVariable('gs_fork_index')) or 0;
+ local destination = destinations[fork_index];
+
+ if arg.bypass_media_network then
+ local callee_uuid = session_callee:get_uuid();
+
+ if callee_uuid and self.caller.uuid and freeswitch then
+ require 'common.ipcalc'
+ local callee_network_str = self.caller:to_s('bleg_network_addr');
+ local caller_network_str = self.caller:to_s('network_addr');
+ local callee_network_addr = common.ipcalc.ipv4_to_i(callee_network_str);
+ local caller_network_addr = common.ipcalc.ipv4_to_i(caller_network_str);
+ local network, netmask = common.ipcalc.ipv4_to_network_netmask(arg.bypass_media_network);
+ if network and netmask and callee_network_addr and caller_network_addr
+ and common.ipcalc.ipv4_in_network(callee_network_addr, network, netmask)
+ and common.ipcalc.ipv4_in_network(caller_network_addr, network, netmask) then
+ self.log:info('FORK ', fork_index, ' BYPASS_MEDIA - caller_ip: ', caller_network_str,
+ ', callee_ip: ', callee_network_str,
+ ', subnet: ', arg.bypass_media_network,
+ ', uuid: ', self.caller.uuid, ', bleg_uuid: ', callee_uuid);
+ freeswitch.API():execute('uuid_media', 'off ' .. self.caller.uuid);
+ freeswitch.API():execute('uuid_media', 'off ' .. callee_uuid);
+ end
+ end
+ end
+
+ if self.on_answer then
+ self.on_answer(self.calling_object, destination);
+ end
+
+ self.caller:set_variable('gs_destination_type', destination.type);
+ self.caller:set_variable('gs_destination_id', destination.id);
+ self.caller:set_variable('gs_destination_uuid', destination.uuid);
+
+ self.log:info('FORK ', fork_index,
+ ' BRIDGE - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id,
+ ', number: ', destination.number,
+ ', dial_time: ', os.time() - start_time);
+ freeswitch.bridge(self.caller.session, session_callee);
+ self:wait_hangup(self.caller.session, session_callee);
+ end
+
+ -- if session_callee:ready() then
+ -- self.log:info('FORK - hangup destination channel');
+ -- session_callee:hangup('ORIGINATOR_CANCEL');
+ -- end
+
+ call_result = {};
+ call_result.disposition = session_callee:hangupCause();
+ call_result.fork_index = fork_index;
+
+ if some_destinations_busy and call_result.disposition == 'USER_NOT_REGISTERED' then
+ call_result.phrase = 'User busy';
+ call_result.code = 486;
+ call_result.disposition = 'USER_BUSY';
+ elseif call_result.disposition == 'USER_NOT_REGISTERED' then
+ call_result.phrase = 'User offline';
+ call_result.code = 480;
+ elseif call_result.disposition == 'NO_ANSWER' then
+ call_result.phrase = 'No answer';
+ call_result.code = 408;
+ elseif call_result.disposition == 'NORMAL_TEMPORARY_FAILURE' then
+ call_result.phrase = 'User offline';
+ call_result.code = 480;
+ else
+ call_result.cause = self.caller:to_s('last_bridge_hangup_cause');
+ call_result.code = self.caller:to_i('last_bridge_proto_specific_hangup_cause');
+ call_result.phrase = self.caller:to_s('sip_hangup_phrase');
+ end
+
+ self.log:info('FORK EXIT - disposition: ', call_result.disposition,
+ ', cause: ', call_result.cause,
+ ', code: ', call_result.code,
+ ', phrase: ', call_result.phrase,
+ ', dial_time: ', os.time() - start_time);
+
+ return call_result;
+end
+
+-- Return call forwarding settngs
+function SipCall.conditional_call_forwarding(self, cause, call_forwarding)
+ local condition_map = {USER_NOT_REGISTERED="offline", NO_ANSWER="noanswer", USER_BUSY="busy"}
+ local condition = condition_map[cause]
+ if call_forwarding and condition and call_forwarding[condition] then
+ log:debug('call forwarding on ' .. condition .. ' - destination: ' .. call_forwarding[condition].destination .. ', type: ' .. call_forwarding[condition].call_forwardable_type);
+ return call_forwarding[condition]
+ end
+end
+
+function SipCall.set_callee_variables(self, sip_account)
+ self.session:setVariable("gs_callee_account_id", sip_account.id);
+ self.session:setVariable("gs_callee_account_type", "SipAccount");
+ self.session:setVariable("gs_callee_account_owner_type", sip_account.sip_accountable_type);
+ self.session:setVariable("gs_callee_account_owner_id", sip_account.sip_accountable_id);
+end
diff --git a/misc/freeswitch/scripts/dialplan/tenant.lua b/misc/freeswitch/scripts/dialplan/tenant.lua
new file mode 100644
index 0000000..8d6436c
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/tenant.lua
@@ -0,0 +1,51 @@
+-- Gemeinschaft 5 module: user class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Tenant = {}
+
+-- Create Tenant object
+function Tenant.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self)
+ self.__index = self;
+ self.class = 'tenant';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+-- find tenant by id
+function Tenant.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `tenants` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local tenant = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ tenant = Tenant:new(self);
+ tenant.record = account_entry;
+ tenant.id = tonumber(account_entry.id);
+ tenant.uuid = account_entry.uuid;
+ end);
+
+ return tenant;
+end
+
+-- find tenant by uuid
+function Tenant.find_by_uuid(self, uuid)
+ tenant_id = tonumber(tenant_id)
+ local sql_query = 'SELECT * FROM `tenants` WHERE `id`= "' .. uuid .. '" LIMIT 1';
+ local tenant = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ tenant = Tenant:new(self);
+ tenant.record = account_entry;
+ tenant.id = tonumber(account_entry.id);
+ tenant.uuid = account_entry.uuid;
+ end);
+
+ return tenant;
+end
diff --git a/misc/freeswitch/scripts/dialplan/user.lua b/misc/freeswitch/scripts/dialplan/user.lua
new file mode 100644
index 0000000..3b483c8
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/user.lua
@@ -0,0 +1,91 @@
+-- Gemeinschaft 5 module: user class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+User = {}
+
+MAX_GROUP_MEMBERSHIPS = 256;
+
+-- create user object
+function User.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'user';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object;
+end
+
+-- find user by id
+function User.find_by_id(self, id)
+ local sql_query = 'SELECT * FROM `users` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1';
+ local user = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ user = User:new(self);
+ user.record = account_entry;
+ user.id = tonumber(account_entry.id);
+ user.uuid = account_entry.uuid;
+ end);
+
+ return user;
+end
+
+-- find user by uuid
+function User.find_by_uuid(self, uuid)
+ local sql_query = 'SELECT * FROM `users` WHERE `id`= "' .. uuid .. '" LIMIT 1';
+ local user = nil;
+
+ self.database:query(sql_query, function(account_entry)
+ user = User:new(self);
+ user.record = account_entry;
+ user.id = tonumber(account_entry.id);
+ user.uuid = account_entry.uuid;
+ end);
+
+ return user;
+end
+
+
+function User.list_groups(self, id)
+ require 'common.str'
+ id = id or self.id;
+ local sql_query = 'SELECT `b`.`name` FROM `user_group_memberships` `a` \
+ JOIN `user_groups` `b` ON `a`.`user_group_id` = `b`.`id` \
+ WHERE `a`.`state` = "active" AND `a`.`user_id`= ' .. tonumber(id) .. ' ORDER BY `b`.`position` LIMIT ' .. MAX_GROUP_MEMBERSHIPS;
+
+ local groups = {};
+
+ self.database:query(sql_query, function(entry)
+ groups[common.str.downcase(entry.name)] = true;
+ end);
+
+ return groups;
+end
+
+
+function User.check_pin(self, pin_to_check)
+ if not self.record then
+ return nil
+ end
+
+ local str_to_hash = tostring(self.record.pin_salt) .. tostring(pin_to_check);
+
+ local file = io.popen("echo -n " .. str_to_hash .. "|sha256sum");
+ local pin_to_check_hash = file:read("*a");
+ file:close();
+
+ pin_to_check_hash = pin_to_check_hash:sub(1, 64);
+
+ if pin_to_check_hash == self.record.pin_hash then
+ return true;
+ end
+
+ return false;
+end
+
diff --git a/misc/freeswitch/scripts/dialplan/voicemail.lua b/misc/freeswitch/scripts/dialplan/voicemail.lua
new file mode 100644
index 0000000..b9dab79
--- /dev/null
+++ b/misc/freeswitch/scripts/dialplan/voicemail.lua
@@ -0,0 +1,155 @@
+-- Gemeinschaft 5 module: voicemail class
+-- (c) AMOOMA GmbH 2012
+--
+
+module(...,package.seeall)
+
+Voicemail = {}
+
+MESSAGE_LENGTH_MIN = 3;
+MESSAGE_LENGTH_MAX = 120;
+SILENCE_LENGTH_ABORT = 5;
+SILENCE_LEVEL = 500;
+BEEP = 'tone_stream://%(1000,0,500)';
+RECORD_FILE_PREFIX = '/tmp/voicemail_';
+
+-- create voicemail object
+function Voicemail.new(self, arg)
+ arg = arg or {}
+ object = arg.object or {}
+ setmetatable(object, self);
+ self.__index = self;
+ self.class = 'voicemail';
+ self.log = arg.log;
+ self.database = arg.database;
+ self.record = arg.record;
+ return object
+end
+
+-- find voicemail account by sip account id
+function Voicemail.find_by_sip_account_id(self, id)
+ local sql_query = 'SELECT `a`.`id`, `a`.`uuid`, `a`.`auth_name`, `a`.`caller_name`, `b`.`name_path`, `b`.`greeting_path`, `a`.`voicemail_pin`, `b`.`password`, `c`.`host` AS `domain` \
+ FROM `sip_accounts` `a` LEFT JOIN `voicemail_prefs` `b` ON `a`.`auth_name` = `b`.`username` \
+ JOIN `sip_domains` `c` ON `a`.`sip_domain_id` = `c`.`id` \
+ WHERE `a`.`id` = ' .. tonumber(id);
+
+ local voicemail_account = nil;
+ self.database:query(sql_query, function(entry)
+ voicemail_account = Voicemail:new(self);
+ voicemail_account.record = entry;
+ voicemail_account.id = tonumber(entry.id);
+ voicemail_account.uuid = entry.uuid;
+ end)
+
+ return voicemail_account;
+end
+
+-- Find Voicemail account by name
+function Voicemail.find_by_name(self, account_name)
+ id = tonumber(id) or 0;
+ local sql_query = string.format('SELECT * FROM `voicemail_prefs` WHERE `username`= "%s" LIMIT 1', account_name)
+ local record = nil
+
+ self.database:query(sql_query, function(voicemail_entry)
+ record = voicemail_entry
+ end)
+
+ if voicemail_account then
+ voicemail_account.account_name = account_name;
+ if record then
+ voicemail_account.name_path = record.name_path;
+ voicemail_account.greeting_path = record.greeting_path;
+ voicemail_account.password = record.password;
+ end
+ end
+
+ return voicemail_account
+end
+
+-- Find Voicemail account by name
+function Voicemail.find_by_number(self, phone_number)
+ local sip_account = nil;
+
+ require "common.phone_number"
+ local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database };
+ local destination_number_object = phone_number_class:find_by_number(phone_number);
+ if destination_number_object and destination_number_object.record.phone_numberable_type == "SipAccount" then
+ return Voicemail:find_by_sip_account_id(destination_number_object.record.phone_numberable_id);
+ end
+
+ return false;
+end
+
+
+function Voicemail.leave(self, caller, phone_number)
+ require 'common.str'
+
+ self.log:info('VOICEMAIL_LEAVE - account=', self.record.id, '/', self.record.uuid, ', auth_name: ', self.record.auth_name, ', caller_name: ', self.record.caller_name);
+
+ caller:set_callee_id(phone_number, self.record.caller_name);
+ caller:answer();
+ caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number));
+ caller:sleep(1000);
+
+ if not common.str.blank(self.record.greeting_path) then
+ caller.session:sayPhrase('voicemail_play_greeting', 'greeting:' .. tostring(self.record.greeting_path));
+ elseif not common.str.blank(self.record.name_path) then
+ caller.session:sayPhrase('voicemail_play_greeting', 'name:' .. tostring(self.record.name_path));
+ elseif not common.str.blank(phone_number) then
+ caller.session:sayPhrase('voicemail_play_greeting', (tostring(phone_number):gsub('[%D]', '')));
+ end
+
+ local record_file_name = RECORD_FILE_PREFIX .. caller.uuid .. '.wav';
+ caller.session:streamFile(BEEP);
+ self.log:info('VOICEMAIL_LEAVE - recording to file: ', tostring(record_file_name));
+ local result = caller.session:recordFile(record_file_name, MESSAGE_LENGTH_MAX, SILENCE_LEVEL, SILENCE_LENGTH_ABORT);
+ local duration = caller:to_i('record_seconds');
+
+ if duration >= MESSAGE_LENGTH_MIN then
+ self.log:info('VOICEMAIL_LEAVE - saving recorded message to voicemail, duration: ', duration);
+ require 'common.fapi'
+ common.fapi.FApi:new{ log = self.log, uuid = caller.uuid }:execute('vm_inject',
+ self.record.auth_name ..
+ '@' .. self.record.domain .. " '" ..
+ record_file_name .. "' '" ..
+ caller.caller_id_number .. "' '" ..
+ caller.caller_id_name .. "' '" ..
+ caller.uuid .. "'"
+ );
+ caller:set_variable('voicemail_message_len', duration);
+ else
+ caller:set_variable('voicemail_message_len');
+ end
+ os.remove(record_file_name);
+ return true;
+end
+
+
+function Voicemail.send_notify(self, caller)
+ self.log:debug('VOICEMAIL_NOTIFY - account: ' .. self.record.auth_name .. ", id: " .. tostring(caller.uuid));
+
+ local file = io.popen("/opt/GS5/script/voicemail_new.sh '" .. tostring(self.record.auth_name) .. "' '" .. tostring(caller.uuid) .. "' 2>&1");
+ self.log:debug('VOICEMAIL_NOTIFY - result: ' .. tostring(file:read("*a")));
+ file:close();
+
+ return true;
+end
+
+
+function Voicemail.menu(self, caller, authorized)
+ self.log:info('VOICEMAIL_MENU - account: ', self.record.auth_name);
+
+ if authorized then
+ caller:set_variable('voicemail_authorized', true);
+ end
+
+ caller:set_callee_id(phone_number, self.record.caller_name);
+ caller:answer();
+ caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number));
+
+ caller:sleep(1000);
+ caller:set_variable('skip_greeting', true);
+ caller:set_variable('skip_instructions', true);
+
+ caller:execute('voicemail', 'check default ' .. self.record.domain .. ' ' .. self.record.auth_name);
+end