summaryrefslogtreecommitdiff
path: root/misc/freeswitch/scripts/dialplan/acd.lua
diff options
context:
space:
mode:
Diffstat (limited to 'misc/freeswitch/scripts/dialplan/acd.lua')
-rw-r--r--misc/freeswitch/scripts/dialplan/acd.lua484
1 files changed, 484 insertions, 0 deletions
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