From b80bd744ad873f6fc43018bc4bfb90677de167bd Mon Sep 17 00:00:00 2001 From: Stefan Wintermeyer Date: Mon, 17 Dec 2012 12:01:45 +0100 Subject: Start of GS5. --- .../scripts/dialplan/access_authorizations.lua | 52 ++ misc/freeswitch/scripts/dialplan/acd.lua | 484 ++++++++++ misc/freeswitch/scripts/dialplan/callthrough.lua | 148 +++ misc/freeswitch/scripts/dialplan/cdr.lua | 71 ++ misc/freeswitch/scripts/dialplan/dialplan.lua | 996 +++++++++++++++++++++ misc/freeswitch/scripts/dialplan/fax.lua | 232 +++++ misc/freeswitch/scripts/dialplan/functions.lua | 839 +++++++++++++++++ misc/freeswitch/scripts/dialplan/geo_number.lua | 89 ++ misc/freeswitch/scripts/dialplan/hunt_group.lua | 202 +++++ misc/freeswitch/scripts/dialplan/phone_book.lua | 63 ++ misc/freeswitch/scripts/dialplan/presence.lua | 84 ++ misc/freeswitch/scripts/dialplan/route.lua | 265 ++++++ misc/freeswitch/scripts/dialplan/session.lua | 224 +++++ misc/freeswitch/scripts/dialplan/sip_call.lua | 266 ++++++ misc/freeswitch/scripts/dialplan/tenant.lua | 51 ++ misc/freeswitch/scripts/dialplan/user.lua | 91 ++ misc/freeswitch/scripts/dialplan/voicemail.lua | 155 ++++ 17 files changed, 4312 insertions(+) create mode 100644 misc/freeswitch/scripts/dialplan/access_authorizations.lua create mode 100644 misc/freeswitch/scripts/dialplan/acd.lua create mode 100644 misc/freeswitch/scripts/dialplan/callthrough.lua create mode 100644 misc/freeswitch/scripts/dialplan/cdr.lua create mode 100644 misc/freeswitch/scripts/dialplan/dialplan.lua create mode 100644 misc/freeswitch/scripts/dialplan/fax.lua create mode 100644 misc/freeswitch/scripts/dialplan/functions.lua create mode 100644 misc/freeswitch/scripts/dialplan/geo_number.lua create mode 100644 misc/freeswitch/scripts/dialplan/hunt_group.lua create mode 100644 misc/freeswitch/scripts/dialplan/phone_book.lua create mode 100644 misc/freeswitch/scripts/dialplan/presence.lua create mode 100644 misc/freeswitch/scripts/dialplan/route.lua create mode 100644 misc/freeswitch/scripts/dialplan/session.lua create mode 100644 misc/freeswitch/scripts/dialplan/sip_call.lua create mode 100644 misc/freeswitch/scripts/dialplan/tenant.lua create mode 100644 misc/freeswitch/scripts/dialplan/user.lua create mode 100644 misc/freeswitch/scripts/dialplan/voicemail.lua (limited to 'misc/freeswitch/scripts/dialplan') 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 -- cgit v1.2.3