diff options
author | Stefan Wintermeyer <stefan.wintermeyer@amooma.de> | 2013-03-25 10:27:27 +0100 |
---|---|---|
committer | Stefan Wintermeyer <stefan.wintermeyer@amooma.de> | 2013-03-25 10:27:27 +0100 |
commit | df6e17e48995f25e72509986f30700d778b179b6 (patch) | |
tree | f432c24b8e4ad81009188650dabfd99194883265 /misc/freeswitch/scripts | |
parent | 11f186a118285fbc87a536af26730780a9ad01f5 (diff) | |
parent | cce94a74aa5c9691f9b37cd9be5a6831f8063812 (diff) |
Merge branch 'develop'5.1.2
Diffstat (limited to 'misc/freeswitch/scripts')
25 files changed, 1198 insertions, 467 deletions
diff --git a/misc/freeswitch/scripts/common/array.lua b/misc/freeswitch/scripts/common/array.lua new file mode 100644 index 0000000..b1b7a71 --- /dev/null +++ b/misc/freeswitch/scripts/common/array.lua @@ -0,0 +1,97 @@ +-- Gemeinschaft 5 module: array functions +-- (c) AMOOMA GmbH 2013 +-- + +module(...,package.seeall) + +function try(array, arguments) + if type(arguments) ~= 'string' or type(array) ~= 'table' then + return nil; + end + + local result = array; + + arguments:gsub('([^%.]+)', function(entry) + local success, result = pcall(function() result = (result[tonumber(entry) or entry]); end); + end); + + return result; +end + + +function set(array, arguments, value) + local nop, arguments_count = arguments:gsub('%.', ''); + local structure = array; + arguments:gsub('([^%.]+)', function(entry) + if arguments_count <= 0 then + structure[entry] = value; + elseif type(structure[entry]) == 'table' then + structure = structure[entry]; + else + structure[entry] = {}; + structure = structure[entry]; + end + arguments_count = arguments_count - 1; + end); +end + + +function expand_variable(variable_path, variable_sets) + for index=1, #variable_sets do + local result = try(variable_sets[index], variable_path); + if result then + return result; + end + end +end + +-- replace variables in a string by array values +function expand_variables(line, ...) + local variable_sets = {...}; + return (line:gsub('{([%a%d%._]+)}', function(captured) + return expand_variable(captured, variable_sets); + end)) +end + + +-- concatenate array values +function to_s(array, separator, prefix, suffix) + require 'common.str'; + + local buffer = ''; + for key, value in pairs(array) do + buffer = common.str.append(buffer, value, separator, prefix, suffix); + end + + return buffer; +end + +-- concatenate array keys +function keys_to_s(array, separator, prefix, suffix) + require 'common.str'; + + local buffer = ''; + for key, value in pairs(array) do + buffer = common.str.append(buffer, key, separator, prefix, suffix); + end + + return buffer; +end + +-- convert to JSON +function to_json(array) + require 'common.str'; + local buffer = '{'; + for key, value in pairs(array) do + if type(value) == 'table' then + buffer = buffer .. '"' .. key .. '":' .. to_json(value) .. ','; + else + buffer = buffer .. '"' .. key .. '":' .. common.str.to_json(value) .. ','; + end + end + if buffer:sub(-1) == ',' then + buffer = buffer:sub(1, -2); + end + buffer = buffer .. '}'; + return buffer; +end diff --git a/misc/freeswitch/scripts/common/conference.lua b/misc/freeswitch/scripts/common/conference.lua index b56cba9..5694f62 100644 --- a/misc/freeswitch/scripts/common/conference.lua +++ b/misc/freeswitch/scripts/common/conference.lua @@ -1,19 +1,11 @@ -- Gemeinschaft 5 module: conference class --- (c) AMOOMA GmbH 2012-2013 +-- (c) AMOOMA GmbH 2013 -- module(...,package.seeall) Conference = {} -MEMBERS_MAX = 100; -PIN_LENGTH_MAX = 10; -PIN_LENGTH_MIN = 2; -PIN_TIMEOUT = 4000; -ANNOUNCEMENT_MAX_LEN = 10 -ANNOUNCEMENT_SILENCE_THRESHOLD = 500 -ANNOUNCEMENT_SILENCE_LEN = 3 - -- create conference object function Conference.new(self, arg) arg = arg or {} @@ -24,215 +16,271 @@ function Conference.new(self, arg) self.log = arg.log; self.database = arg.database; self.record = arg.record; - self.max_members = 0; return object; end --- find conference by id + +function Conference.settings_get(self) + require 'common.array'; + require 'common.configuration_table'; + + local configuration = common.configuration_table.get(self.database, 'conferences'); + if not configuration then + return nil; + end + + local parameters = configuration.parameters or {}; + local settings = configuration.settings or {}; + local sounds = configuration.sounds or {}; + + settings.members_max = settings.members_max or tonumber(parameters['max-members']) or 100; + settings.pin_length_max = tonumber(settings.pin_length_max) or 10; + settings.pin_length_min = tonumber(settings.pin_length_min) or 2; + settings.pin_timeout = tonumber(settings.pin_timeout) or 4000; + settings.announcement_max_length = tonumber(settings.announcement_max_length) or 10; + settings.announcement_silence_threshold = tonumber(settings.announcement_silence_threshold) or 500; + settings.announcement_silence_length = tonumber(settings.announcement_silence_length) or 3; + settings.flags = settings.flags or { waste = true }; + sounds.pin = sounds.pin or 'conference/conf-pin.wav'; + sounds.has_joined = sounds.has_joined or 'conference/conf-has_joined.wav'; + sounds.has_left = sounds.has_left or 'conference/conf-has_left.wav'; + sounds.alone = sounds.has_alone or 'conference/conf-alone.wav'; + + settings.key_enter = parameters.key_enter or '#'; + settings.spool_dir = settings.spool_dir or '/var/spool/freeswitch'; + return settings, sounds; +end + + function Conference.find_by_id(self, id) - local sql_query = 'SELECT * FROM `conferences` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + local sql_query = 'SELECT *, (NOW() >= `start` AND NOW() <= `end`) AS `open_now` FROM `conferences` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; local conference = nil; self.database:query(sql_query, function(conference_entry) conference = Conference:new(self); conference.record = conference_entry; conference.id = tonumber(conference_entry.id); + conference.identifier = 'conference' .. conference.id; conference.uuid = conference_entry.uuid; - conference.max_members = tonumber(conference.record.max_members) or MEMBERS_MAX; - end) + conference.pin = conference_entry.pin; + conference.open_for_public = common.str.to_b(conference_entry.open_for_anybody); + conference.announce_entering = common.str.to_b(conference_entry.announce_new_member_by_name); + conference.announce_leaving = common.str.to_b(conference_entry.announce_left_member_by_name); + if not common.str.blank(conference_entry.open_now) then + conference.open_now = common.str.to_b(conference_entry.open_now); + end + conference.settings, conference.sounds = self:settings_get(); + if conference.settings then + conference.settings.members_max = tonumber(conference.record.max_members) or conference.settings.members_max; + else + conference.log:error('CONFERENCE - no basic configuration'); + end + end) + return conference; end --- find invitee by phone numbers + function Conference.find_invitee_by_numbers(self, phone_numbers) if not self.record then - return false + return false; end - local sql_query = string.format( - "SELECT `conference_invitees`.`pin` AS `pin`, `conference_invitees`.`speaker` AS `speaker`, `conference_invitees`.`moderator` AS `moderator` " .. - "FROM `conference_invitees` JOIN `phone_numbers` ON `phone_numbers`.`phone_numberable_id` = `conference_invitees`.`id` " .. - "WHERE `phone_numbers`.`phone_numberable_type` = 'ConferenceInvitee' AND `conference_invitees`.`conference_id` = %d " .. - "AND `phone_numbers`.`number` IN ('%s') LIMIT 1", self.record.id, table.concat(phone_numbers, "','")); + local sql_query = 'SELECT `a`.* \ + FROM `conference_invitees` `a` \ + JOIN `phone_numbers` `b` ON `b`.`phone_numberable_id` = `a`.`id` \ + WHERE `b`.`phone_numberable_type` = "ConferenceInvitee" \ + AND `a`.`conference_id` = ' .. self.id .. ' \ + AND `b`.`number` IN ("' .. table.concat(phone_numbers, "','") .. '") \ + LIMIT 1'; local invitee = nil; - self.database:query(sql_query, function(conference_entry) - invitee = conference_entry; + self.database:query(sql_query, function(invitee_entry) + invitee = invitee_entry; end) - return invitee; + return invitee; end -function Conference.count(self) - return tonumber(self.caller:result('conference ' .. self.record.id .. ' list count')) or 0; + +function Conference.members_count(self) + return tonumber(self.caller:result('conference ' .. self.identifier .. ' list count')) or 0; end --- Try to enter a conference -function Conference.enter(self, caller, domain) - local cause = "NORMAL_CLEARING"; - local pin = nil; - local flags = {'waste'}; - self.caller = caller; +function Conference.check_pin(self, pin) + local digits = ''; + for i = 1, 3 do + if digits == pin then + self.caller:send_display('PIN: OK'); + break + elseif digits ~= "" then + self.caller:send_display('PIN: wrong'); + self.caller.session:sayPhrase('conference_bad_pin'); + end + self.caller:send_display('Enter PIN'); + digits = self.caller.session:read(self.settings.pin_length_min, self.settings.pin_length_max, self.sounds.pin, self.settings.pin_timeout, self.settings.key_enter); + end - require "common.phone_number" - local phone_number_class = common.phone_number.PhoneNumber:new{log = self.log, database = self.database} - local phone_numbers = phone_number_class:list_by_owner(self.record.id, "Conference"); + if digits ~= pin then + return false + end - -- Set conference presence - require "dialplan.presence" - local presence = dialplan.presence.Presence:new(); - presence:init{ log = log, accounts = phone_numbers, domain = domain, uuid = "conference_" .. self.record.id }; + return true; +end - local conference_count = self:count(); - -- Check if conference is full - if conference_count >= self.max_members then - presence:early(); - self.log:debug(string.format("full conference %s (\"%s\"), members: %d, members allowed: %d", self.record.id, self.record.name, conference_count, self.max_members)); +function Conference.check_ownership(self, check_caller) + local owner = nil; - if (tonumber(self.record.conferenceable_id) == caller.account_owner_id) - and (self.record.conferenceable_type == caller.account_owner_type) then - self.log:debug("Allow owner of this conterence to enter a full conference"); - else - cause = "CALL_REJECTED"; - caller:hangup(cause); - return cause; - end; - end - - require 'common.str' - -- Check if conference is within time frame - if not common.str.blank(self.record.start) and not common.str.blank(self.record['end']) then - local d = {} - _,_,d.year,d.month,d.day,d.hour,d.min,d.sec=string.find(self.record.start, "(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)"); - - local conference_start = os.time(d); - _,_,d.year,d.month,d.day,d.hour,d.min,d.sec=string.find(self.record['end'], "(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)"); - local conference_end = os.time(d); - local now = os.time(os.date("!*t", os.time())); - - log:debug("conference - open: " .. os.date("%c",conference_start) .. " by " .. os.date("%c",conference_end) .. ", now: " .. os.date("%c",now)); - - if now < conference_start or now > conference_end then - cause = "CALL_REJECTED"; - caller:hangup(cause); - return cause; - end + if check_caller then + owner = common.array.try(self.caller, 'account.owner'); + else + owner = common.array.try(self.caller, 'auth_account.owner'); + end + if not owner then + return false; end - -- Owner ist always moderator - if (tonumber(self.record.conferenceable_id) == caller.account_owner_id) and (self.record.conferenceable_type == caller.account_owner_type) then - table.insert(flags, 'moderator'); - log:debug("is owner - conference: " .. self.record.id .. ", owner: " .. caller.account_owner_type .. ":" .. caller.account_owner_id); - else - local invitee = self:find_invitee_by_numbers(caller.caller_phone_numbers); + if tonumber(self.record.conferenceable_id) == owner.id and self.record.conferenceable_type:lower() == owner.class then + return true; + end +end - if not common.str.to_b(self.record.open_for_anybody) and not invitee then - log:debug(string.format("conference %s (\"%s\"), caller %s not allowed to enter this conference", self.record.id, self.record.name, caller.caller_phone_number)); - cause = "CALL_REJECTED"; - caller:hangup(cause); - return cause; - end - if invitee then - log:debug("conference " .. self.record.id .. " member invited - speaker: " .. invitee.speaker .. ", moderator: " .. invitee.moderator); - if common.str.to_b(invitee.moderator) then - table.insert(flags, 'moderator'); - end - if not common.str.to_b(invitee.speaker) then - table.insert(flags, 'mute'); - end - pin = invitee.pin; - else - log:debug("conference " .. self.record.id .. " caller not invited"); - end +function Conference.account_name_file(self) + if not self.caller.account or tostring(self.caller.account.class):lower() ~= 'sipaccount' then + return; end - if not pin and self.record.pin then - pin = self.record.pin + require 'dialplan.voicemail' + local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(self.caller.account.id); + if voicemail_account and not common.str.blank(voicemail_account.record.name_path) then + self.log:debug('CONFERENCE ', self.id, ' - caller_name_file: ', voicemail_account.record.name_path); + return voicemail_account.record.name_path; end +end - caller:answer(); - caller:sleep(1000); - caller.session:sayPhrase('conference_welcome'); - if not common.str.blank(pin) then - local digits = ""; - for i = 1, 3, 1 do - if digits == pin then - break - elseif digits ~= "" then - caller.session:sayPhrase('conference_bad_pin'); - end - digits = caller.session:read(PIN_LENGTH_MIN, PIN_LENGTH_MAX, 'conference/conf-pin.wav', PIN_TIMEOUT, '#'); +function Conference.record_name(self) + self.caller:send_display('Record name'); + local name_file = self.settings.spool_dir .. '/conference_caller_name_' .. self.caller.uuid .. '.wav'; + self.caller.session:sayPhrase('conference_record_name'); + self.caller.session:recordFile(name_file, self.settings.announcement_max_length, self.settings.announcement_silence_threshold, self.settings.announcement_max_length); + self.caller:send_display('Playback name'); + self.caller:playback(name_file); + + return name_file; +end + + +function Conference.playback(self, ...) + local sound_files = {...}; + for index=1, #sound_files do + self.caller:execute('set',"result=${conference(" .. self.identifier .. " play ".. tostring(sound_files[index]) .. ")}"); + end +end + +function Conference.phrase(self, phrase, file_name) + self.caller:execute('set',"result=${conference(" .. self.identifier .. " phrase ".. phrase .. ':' .. file_name .. ")}"); +end + +function Conference.enter(self, caller, domain) + self.caller = caller; + local members = self:members_count(); + + self.log:info('CONFERENCE ', self.id, ' - open_for_public: ', self.open_for_public, ', open_now: ', self.open_now, ', members: ', members, ', members_max: ', self.settings.members_max); + + if self.open_now == false then + self.log:notice('CONFERENCE ', self.id, ' - currently closed, start: ', self.record.start, ', end: ', self.record['end']); + return { continue = false, code = 493, phrase = 'Conference closed' }; + end + + if members >= self.settings.members_max then + self.log:notice('CONFERENCE ', self.id, ' - full, members: ', members, ', members_max: ', self.settings.members_max); + return { continue = false, code = 493, phrase = 'Conference closed' }; + end + + local invitee = self:find_invitee_by_numbers(caller.caller_phone_numbers); + if invitee then + if common.str.to_b(invitee.speaker) then + self.settings.flags.mute = nil; + end + if common.str.to_b(invitee.moderator) then + self.settings.flags.moderator = true; end - if digits ~= pin then - caller.session:sayPhrase('conference_goodbye'); - return "CALL_REJECTED"; + self.log:info('CONFERENCE ', self.id, ' - invitee=', invitee.id, '/', invitee.uuid, ', speaker: ', not self.settings.flags.mute, ', moderator: ', self.settings.flags.moderator); + self.pin = invitee.pin; + elseif self:check_ownership() then + self.pin = nil; + local caller_owner = false; + if self:check_ownership(true) then + self.settings.flags.moderator = true; + self.settings.flags.dtmf = true; + caller_owner = true; end + self.log:info('CONFERENCE ', self.id, ' - owner authenticated: ', self.caller.auth_account.owner.class,'=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid, ', owner: ', caller_owner, ', speaker: ', not self.settings.flags.mute, ', moderator: ', self.settings.flags.moderator); + elseif not self.open_for_public then + self.log:notice('CONFERENCE ', self.id, ' - not open for public'); + return { continue = false, code = 493, phrase = 'Conference closed' }; end - self.log:debug(string.format("entering conference %s - name: \"%s\", flags: %s, members: %d, max. members: %d", - self.record.id, self.record.name, table.concat(flags, ','), conference_count, self.max_members)); - - -- Members count will be incremented in a few milliseconds, set presence - if (conference_count + 1) >= self.max_members then - presence:early(); - else - presence:confirmed(); + caller:answer(); + if not common.str.blank(self.pin) and not self:check_pin(self.pin) then + self.log:notice('CONFERENCE ', self.id, ' - PIN wrong'); + caller.session:sayPhrase('conference_goodbye'); + return { continue = false, code = 493, phrase = 'Not authorized' }; end - -- Enter the conference + self.caller:send_display(tostring(self.record.name) .. ', members: ' .. tostring(members)); + caller:sleep(1000); + caller.session:sayPhrase('conference_welcome'); + + local name_file = nil; + local name_file_delete = nil; - -- Record caller's name - if common.str.to_b(self.record.announce_new_member_by_name) or common.str.to_b(self.record.announce_left_member_by_name) then - local uid = session:get_uuid(); - name_file = "/var/spool/freeswitch/conference_caller_name_" .. uid .. ".wav"; - caller.session:sayPhrase('conference_record_name'); - session:recordFile(name_file, ANNOUNCEMENT_MAX_LEN, ANNOUNCEMENT_SILENCE_THRESHOLD, ANNOUNCEMENT_SILENCE_LEN); - caller.session:streamFile(name_file); + if self.announce_entering or self.announce_leaving then + name_file = self:account_name_file(); + if not name_file then + name_file = self:record_name(caller); + name_file_delete = true; + end end - -- Play entering caller's name if recorded - if name_file and (self:count() > 0) and common.str.to_b(self.record.announce_new_member_by_name) then - caller.session:execute('set',"result=${conference(" .. self.record.id .. " play ".. name_file .. ")}"); - caller.session:execute('set',"result=${conference(" .. self.record.id .. " play conference/conf-has_joined.wav)}"); - else - -- Ensure a surplus "#" digit is not passed to the conference - caller.session:read(1, 1, '', 1000, "#"); + members = self:members_count(); + if self.announce_entering and name_file then + if members > 0 then + self:playback(name_file, self.sounds.has_joined); + end end - local result = caller.session:execute('conference', self.record.id .. "@profile_" .. self.record.id .. "++flags{" .. table.concat(flags, '|') .. "}"); - self.log:debug('exited conference - result: ' .. tostring(result)); + if members == 0 then + caller.session:sayPhrase('conference_alone'); + end + + self.caller:send_display(tostring(self.record.name)); + local result = caller:execute('conference', self.identifier .. "@profile_" .. self.identifier .. "++flags{" .. common.array.keys_to_s(self.settings.flags, '|') .. "}"); + + self.caller:send_display('Goodbye'); caller.session:sayPhrase('conference_goodbye'); - -- Play leaving caller's name if recorded if name_file then - if (self:count() > 0) and common.str.to_b(self.record.announce_left_member_by_name) then - if (self:count() == 1) then - caller.session:sleep(3000); + if self.announce_leaving then + members = self:members_count(); + if members > 0 then + self:playback(name_file, self.sounds.has_left); + if members == 1 then + self:playback(self.sounds.alone); + end end - caller.session:execute('set',"result=${conference(" .. self.record.id .. " play ".. name_file .. ")}"); - caller.session:execute('set',"result=${conference(" .. self.record.id .. " play conference/conf-has_left.wav)}"); end - os.remove(name_file); - end - - -- Set presence according to member count - conference_count = self:count(); - if conference_count >= self.max_members then - presence:early(); - elseif conference_count > 0 then - presence:confirmed(); - else - presence:terminated(); + if name_file_delete then + os.remove(name_file); + end end - cause = "NORMAL_CLEARING"; - caller.session:hangup(cause); - return cause; + return { continue = false, code = 200, phrase = 'OK' } end diff --git a/misc/freeswitch/scripts/common/database.lua b/misc/freeswitch/scripts/common/database.lua index 8aed1ac..be32ad7 100644 --- a/misc/freeswitch/scripts/common/database.lua +++ b/misc/freeswitch/scripts/common/database.lua @@ -67,6 +67,29 @@ function Database.query_return_value(self, sql_query) end +function Database.query_return_first(self, sql_query) + local result = nil; + + self.conn:query(sql_query, function(row) + result = row; + return result; + end); + + return result; +end + + +function Database.query_return_all(self, sql_query) + local result = {}; + + self.conn:query(sql_query, function(row) + table.insert(result, row); + end); + + return result; +end + + function Database.last_insert_id(self) return self:query_return_value('SELECT LAST_INSERT_ID()'); end diff --git a/misc/freeswitch/scripts/common/gateway.lua b/misc/freeswitch/scripts/common/gateway.lua index c1b50a7..ac38326 100644 --- a/misc/freeswitch/scripts/common/gateway.lua +++ b/misc/freeswitch/scripts/common/gateway.lua @@ -34,7 +34,11 @@ end function Gateway.find_by_id(self, id) - local sql_query = 'SELECT * FROM `gateways` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + local sql_query = 'SELECT `a`.*, `c`.`sip_host` AS `domain`, `c`.`contact` AS `contact_full`, `c`.`network_ip`, `c`.`network_port` \ + FROM `gateways` `a` \ + LEFT JOIN `gateway_settings` `b` ON `a`.`id` = `b`.`gateway_id` AND `b`.`name` = "inbound_username" \ + LEFT JOIN `sip_registrations` `c` ON `b`.`value` = `c`.`sip_user` \ + WHERE `a`.`id`= ' .. tonumber(id) .. ' LIMIT 1'; local gateway = nil; self.database:query(sql_query, function(entry) @@ -46,6 +50,9 @@ function Gateway.find_by_id(self, id) gateway.technology = entry.technology; gateway.outbound = common.str.to_b(entry.outbound); gateway.inbound = common.str.to_b(entry.inbound); + gateway.domain = entry.domain; + gateway.network_ip = entry.network_ip; + gateway.network_port = tonumber(entry.network_port) or 5060; end) if gateway then @@ -59,7 +66,45 @@ end function Gateway.find_by_name(self, name) local gateway_name = name:gsub('([^%a%d%._%+])', ''); - local sql_query = 'SELECT * FROM `gateways` WHERE `name`= "' .. gateway_name .. '" LIMIT 1'; + local sql_query = 'SELECT `a`.*, `c`.`sip_host` `domain`, `c`.`contact` AS `contact_full`, `c`.`network_ip`, `c`.`network_port`\ + FROM `gateways` `a` \ + LEFT JOIN `gateway_settings` `b` ON `a`.`id` = `b`.`gateway_id` AND `b`.`name` = "inbound_username" \ + LEFT JOIN `sip_registrations` `c` ON `b`.`value` = `c`.`sip_user` \ + WHERE `a`.`name`= ' .. self.database:escape(gateway_name, '"') .. ' LIMIT 1'; + + local gateway = nil; + self.database:query(sql_query, function(entry) + require 'common.str'; + gateway = Gateway:new(self); + gateway.record = entry; + gateway.id = tonumber(entry.id); + gateway.name = entry.name; + gateway.technology = entry.technology; + gateway.outbound = common.str.to_b(entry.outbound); + gateway.inbound = common.str.to_b(entry.inbound); + gateway.domain = entry.domain; + gateway.network_ip = entry.network_ip; + gateway.network_port = tonumber(entry.network_port) or 5060; + end) + + if gateway then + gateway.settings = self:config_table_get('gateway_settings', gateway.id); + end + + return gateway; +end + + +function Gateway.find_by_auth_name(self, name) + local auth_name = name:gsub('([^%a%d%._%+])', ''); + + local sql_query = 'SELECT `c`.*, `a`.`value` `password`, `b`.`value` `username` \ + FROM `gateway_settings` `a` \ + INNER JOIN `gateway_settings` `b` \ + ON (`a`.`gateway_id` = `b`.`gateway_id` AND `a`.`name` = "inbound_password" AND `b`.`name` = "inbound_username" AND `b`.`value` = ' .. self.database:escape(auth_name, '"') .. ') \ + LEFT JOIN `gateways` `c` \ + ON (`a`.`gateway_id` = `c`.`id`) \ + WHERE `c`.`inbound` IS TRUE OR `c`.`outbound` IS TRUE LIMIT 1'; local gateway = nil; self.database:query(sql_query, function(entry) @@ -82,14 +127,26 @@ end function Gateway.call_url(self, destination_number) - if self.technology == 'sip' then - return 'sofia/gateway/' .. self.GATEWAY_PREFIX .. self.id .. '/' .. tostring(destination_number); - elseif self.technology == 'xmpp' then - local destination_str = tostring(destination_number); - if self.settings.destination_domain then - destination_str = destination_str .. '@' .. self.settings.destination_domain; + require 'common.str'; + + if common.str.blank(self.settings.dial_string) then + if self.technology == 'sip' then + if self.settings.inbound_username and self.settings.inbound_password and not common.str.blank(self.record.domain) then + return 'sofia/' .. (self.settings.profile or 'gemeinschaft') .. '/' .. self.settings.inbound_username .. '%' .. self.record.domain; + else + return 'sofia/gateway/' .. self.GATEWAY_PREFIX .. self.id .. '/' .. tostring(destination_number); + end + + elseif self.technology == 'xmpp' then + local destination_str = tostring(destination_number); + if self.settings.destination_domain then + destination_str = destination_str .. '@' .. self.settings.destination_domain; + end + return 'dingaling/' .. self.GATEWAY_PREFIX .. self.id .. '/' .. destination_str; end - return 'dingaling/' .. self.GATEWAY_PREFIX .. self.id .. '/' .. destination_str; + else + require 'common.array'; + return tostring(common.array.expand_variables(self.settings.dial_string, self, { destination_number = destination_number })); end return ''; @@ -174,9 +231,7 @@ function Gateway.parameters_build(self, gateway_id, technology) parameters.register = common.str.to_b(settings.register); end - if common.str.blank(settings.password) then - parameters.password = 'gateway' .. gateway_id; - else + if not common.str.blank(settings.password) then parameters.password = settings.password; end diff --git a/misc/freeswitch/scripts/common/intruder.lua b/misc/freeswitch/scripts/common/intruder.lua index 083ec37..7d12155 100644 --- a/misc/freeswitch/scripts/common/intruder.lua +++ b/misc/freeswitch/scripts/common/intruder.lua @@ -49,3 +49,46 @@ function Intruder.update_blacklist(self, event) self.database:insert_or_update('intruders', intruder_record, { created_at = false, comment = false }); end + + +function Intruder.sources_list(self, key) + local sql_query = nil; + + if key then + sql_query = 'SELECT * FROM `intruders` WHERE `key` = ' .. self.database:escape(key, '"') .. ' LIMIT 1'; + else + sql_query = 'SELECT * FROM `intruders`'; + end + + local sources = {}; + local sources_count = 0; + local blacklist_count = 0; + local whitelist_count = 0; + + self.database:query(sql_query, function(record) + sources[record.key] = { + ignore = (record.list_type == 'whitelist'), + contact_first = 0, + contact_last = 0, + contact_count = tonumber(record.contact_count) or 0, + span_contact_count = 0, + span_start = 0, + points = tonumber(record.points) or 0, + banned = tonumber(record.bans) or 0, + }; + sources_count = sources_count + 1; + if record.list_type == 'whitelist' then + whitelist_count = whitelist_count + 1; + elseif record.list_type == 'blacklist' then + blacklist_count = blacklist_count + 1; + end + end); + + self.log:info('[intruder] INTRUDER_LIST - entries loaded: ', sources_count, ', blacklist: ', blacklist_count, ', whitelist: ', whitelist_count); + + if key then + return sources[key]; + end + + return sources; +end diff --git a/misc/freeswitch/scripts/common/log.lua b/misc/freeswitch/scripts/common/log.lua index b7c8d09..b9893ac 100644 --- a/misc/freeswitch/scripts/common/log.lua +++ b/misc/freeswitch/scripts/common/log.lua @@ -12,6 +12,8 @@ function Log.new(self, arg) object = arg.object or {} setmetatable(object, self); self.__index = self; + self.disabled = arg.disabled or false; + self.buffer = arg.buffer; self.prefix = arg.prefix or '### '; self.level_console = arg.level_console or 0; @@ -22,18 +24,31 @@ function Log.new(self, arg) self.level_notice = arg.level_notice or 5; self.level_info = arg.level_info or 6; self.level_debug = arg.level_debug or 7; + self.level_devel = arg.level_devel or 4; return object; end function Log.message(self, log_level, message_arguments ) + if self.disabled then + return + end local message = tostring(self.prefix); for index, value in pairs(message_arguments) do if type(index) == 'number' then - message = message .. tostring(value); + if type(value) == 'table' then + require 'common.array'; + message = message .. common.array.to_json(value); + else + message = message .. tostring(value); + end end end - freeswitch.consoleLog(log_level, message .. '\n'); + if self.buffer then + table.insert(self.buffer, message); + elseif freeswitch then + freeswitch.consoleLog(log_level, message .. '\n'); + end end function Log.console(self, ...) @@ -67,3 +82,9 @@ end function Log.debug(self, ...) self:message(self.level_debug, {...}); end + +function Log.devel(self, ...) + local arguments = {...}; + table.insert(arguments, 1, '**'); + self:message(self.level_devel, arguments); +end diff --git a/misc/freeswitch/scripts/common/object.lua b/misc/freeswitch/scripts/common/object.lua index 8c195e5..68e1361 100644 --- a/misc/freeswitch/scripts/common/object.lua +++ b/misc/freeswitch/scripts/common/object.lua @@ -95,6 +95,18 @@ function Object.find(self, attributes) if object then object.owner = self:find{class = object.record.fax_accountable_type, id = tonumber(object.record.fax_accountable_id)}; end + elseif class == 'conference' then + require 'common.conference'; + + if tonumber(attributes.id) then + object = common.conference.Conference:new{ log = self.log, database = self.database }:find_by_id(attributes.id); + elseif not common.str.blank(attributes.uuid) then + object = common.conference.Conference:new{ log = self.log, database = self.database }:find_by_uuid(attributes.uuid); + end + + if object then + object.owner = self:find{class = object.record.conferenceable_type, id = tonumber(object.record.conferenceable_id)}; + end end if object then diff --git a/misc/freeswitch/scripts/common/perimeter.lua b/misc/freeswitch/scripts/common/perimeter.lua index 0815d33..d3b601c 100644 --- a/misc/freeswitch/scripts/common/perimeter.lua +++ b/misc/freeswitch/scripts/common/perimeter.lua @@ -9,6 +9,9 @@ Perimeter = {} function Perimeter.new(self, arg) + require 'common.str'; + require 'common.array'; + arg = arg or {} object = arg.object or {} setmetatable(object, self); @@ -94,12 +97,24 @@ end function Perimeter.check(self, event) - if not event or not event.key then - self.log:warning('[perimeter] PERIMETER_CHECK - no event/key'); + if not type(event) == 'list' then + self.log:warning('[perimeter] PERIMETER_CHECK - no event data'); + return; + end + if not event.key then + self.log:warning('[perimeter] PERIMETER_CHECK - no key'); + for key, value in pairs(event) do + self.log:debug('[perimeter] PERIMETER_CHECK event_data - "', key, '" = "', value, '"'); + end return; end - event.record = self:record_load(event); + event.record = self:record_load(event); + + if event.record.ignore then + return + end + if event.record.banned <= self.ban_tries then for check_name, check_points in pairs(self.checks[event.action]) do if self.checks_available[check_name] then @@ -191,7 +206,7 @@ end function Perimeter.check_bad_headers(self, event) local points = nil; for name, pattern in pairs(self.bad_headers[event.action]) do - pattern = self:expand_variables(pattern, event); + pattern = common.array.expand_variables(pattern, event); local success, result = pcall(string.find, event[name], pattern); if success and result then self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_BAD_HEADERS - ', name, '=', event[name], ' ~= ', pattern); @@ -213,29 +228,36 @@ function Perimeter.append_blacklist_file(self, event) event.date = self:format_date(event.timestamp); if self.blacklist_file_comment then - blacklist:write(self:expand_variables(self.blacklist_file_comment, event), '\n'); + blacklist:write(common.array.expand_variables(self.blacklist_file_comment, event), '\n'); end self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_APPEND_BLACKLIST - file: ', self.blacklist_file); - blacklist:write(self:expand_variables(self.blacklist_file_entry, event), '\n'); + blacklist:write(common.array.expand_variables(self.blacklist_file_entry, event), '\n'); blacklist:close(); end function Perimeter.execute_ban(self, event) - local command = self:expand_variables(self.ban_command, event); + local command = common.array.expand_variables(self.ban_command, event); self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_EXECUTE_BAN - command: ', command); local result = os.execute(command); end + function Perimeter.update_intruder(self, event) require 'common.intruder'; local result = common.intruder.Intruder:new{ log = self.log, database = self.database }:update_blacklist(event); end -function Perimeter.expand_variables(self, line, variables) - return (line:gsub('{([%a%d%._]+)}', function(captured) - return variables[captured] or ''; - end)) +function Perimeter.action_db_rescan(self, record) + require 'common.intruder'; + + if common.str.blank(record.key) then + self.log:info('[perimeter] PERIMETER rescan entire sources database'); + self.sources = common.intruder.Intruder:new{ log = self.log, database = self.database }:sources_list(); + else + self.log:info('[perimeter] PERIMETER rescan sources database - key: ', record.key); + self.sources[record.key] = common.intruder.Intruder:new{ log = self.log, database = self.database }:sources_list(record.key); + end end diff --git a/misc/freeswitch/scripts/common/sip_account.lua b/misc/freeswitch/scripts/common/sip_account.lua index 6cc7d25..e7ee0d7 100644 --- a/misc/freeswitch/scripts/common/sip_account.lua +++ b/misc/freeswitch/scripts/common/sip_account.lua @@ -130,9 +130,9 @@ end function SipAccount.call_state(self) - local sql_query = 'SELECT `callstate` FROM `detailed_calls` \ - WHERE `presence_id` LIKE "' .. self.record.auth_name .. '@%" \ - OR `b_presence_id` LIKE "' .. self.record.auth_name .. '@%" \ + local sql_query = 'SELECT `callstate` FROM `calls_active` \ + WHERE `sip_account_id` = ' .. self.id .. ' \ + OR `b_sip_account_id` = ' .. self.id .. ' \ LIMIT 1'; return self.database:query_return_value(sql_query); diff --git a/misc/freeswitch/scripts/common/str.lua b/misc/freeswitch/scripts/common/str.lua index 541199f..3fd8fde 100644 --- a/misc/freeswitch/scripts/common/str.lua +++ b/misc/freeswitch/scripts/common/str.lua @@ -4,37 +4,6 @@ module(...,package.seeall) -function try(array, arguments) - if type(arguments) ~= 'string' or type(array) ~= 'table' then - return nil; - end - - local result = array; - - arguments:gsub('([^%.]+)', function(entry) - local success, result = pcall(function() result = (result[tonumber(entry) or entry]); end); - end); - - return result; -end - - -function set(array, arguments, value) - local nop, arguments_count = arguments:gsub('%.', ''); - local structure = array; - arguments:gsub('([^%.]+)', function(entry) - if arguments_count <= 0 then - structure[entry] = value; - elseif type(structure[entry]) == 'table' then - structure = structure[entry]; - else - structure[entry] = {}; - structure = structure[entry]; - end - arguments_count = arguments_count - 1; - end); -end - -- to number function to_n(value) value = tostring(value):gsub('[^%d%.%+%-]', ''); @@ -160,23 +129,3 @@ function append(buffer, value, separator, prefix, suffix) return buffer; end - --- concatenate array values to string -function concat(array, separator, prefix, suffix) - local buffer = ''; - for key, value in pairs(array) do - buffer = append(buffer, value, separator, prefix, suffix); - end - - return buffer; -end - --- concatenate array keys to string -function concat_keys(array, separator, prefix, suffix) - local buffer = ''; - for key, value in pairs(array) do - buffer = append(buffer, key, separator, prefix, suffix); - end - - return buffer; -end diff --git a/misc/freeswitch/scripts/configuration.lua b/misc/freeswitch/scripts/configuration.lua index 6660c3d..88ef452 100644 --- a/misc/freeswitch/scripts/configuration.lua +++ b/misc/freeswitch/scripts/configuration.lua @@ -196,7 +196,7 @@ function conf_conference(database) if conf_name then require 'common.conference' - conference = common.conference.Conference:new{log=log, database=database}:find_by_id(conf_name); + conference = common.conference.Conference:new{log=log, database=database}:find_by_id(common.str.to_i(conf_name)); if conference then log:debug('CONFIG_CONFERENCE ', conf_name, ' name: ', conference.record.name, ', profile: ', profile_name); config.parameters['caller-id-name'] = conference.record.name or ''; @@ -248,6 +248,7 @@ function conf_conference(database) }; end + function conf_voicemail(database) require 'configuration.simple_xml' local xml = configuration.simple_xml.SimpleXml:new(); @@ -283,6 +284,35 @@ function conf_voicemail(database) }; end + +function conf_event_socket(database) + require 'configuration.simple_xml' + local xml = configuration.simple_xml.SimpleXml:new(); + + require 'common.configuration_table'; + local settings = common.configuration_table.get(database, 'event_socket', 'settings'); + + XML_STRING = xml:element{ + 'document', + ['type'] = 'freeswitch/xml', + xml:element{ + 'section', + name = 'configuration', + description = 'Gemeinschaft 5 FreeSWITCH configuration', + xml:element{ + 'configuration', + name = 'event_socket.conf', + description = 'Event socket configuration', + xml:element{ + 'settings', + xml:from_hash('param', settings, 'name', 'value'), + }, + }, + }, + }; +end + + function conf_post_switch(database) require 'configuration.simple_xml' local xml = configuration.simple_xml.SimpleXml:new(); @@ -371,6 +401,7 @@ end function directory_sip_account(database) + require 'common.str'; require 'configuration.simple_xml' local xml = configuration.simple_xml.SimpleXml:new(); @@ -381,21 +412,82 @@ function directory_sip_account(database) local user_xml = nil; - if auth_name and auth_name ~= '' then - -- sip account or gateway - if string.len(auth_name) > 3 and auth_name:sub(1, 3) == 'gw+' then - local gateway_name = auth_name:sub(4); - domain = domain or freeswitch.API():execute('global_getvar', 'domain'); + if not common.str.blank(auth_name) then + require 'common.sip_account' + local sip_account = common.sip_account.SipAccount:new{ log = log, database = database}:find_by_auth_name(auth_name); + + require 'common.configuration_table' + local user_parameters = common.configuration_table.get(database, 'sip_accounts', 'parameters'); + + if sip_account ~= nil then + user_parameters['password'] = sip_account.record.password; + user_parameters['vm-password'] = sip_account.record.voicemail_pin; + + local user_variables = { + user_context = "default", + gs_from_gateway = "false", + gs_account_id = sip_account.record.id, + gs_account_uuid = sip_account.record.uuid, + gs_account_type = "SipAccount", + gs_account_state = sip_account.record.state, + gs_account_caller_name = sip_account.record.caller_name, + gs_account_owner_type = sip_account.record.sip_accountable_type, + gs_account_owner_id = sip_account.record.sip_accountable_id + } + + if tostring(purpose) == 'publish-vm' then + log:debug('DIRECTORY_SIP_ACCOUNT - purpose: VoiceMail, auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain); + user_xml = xml:element{ + 'groups', + xml:element{ + 'group', + name = 'default', + xml:element{ + 'users', + xml:element{ + 'user', + id = sip_account.record.auth_name, + xml:element{ + 'params', + xml:from_hash('param', user_parameters, 'name', 'value'), + }, + xml:element{ + 'variables', + xml:from_hash('variable', user_variables, 'name', 'value'), + }, + }, + }, + }, + }; + else + log:debug('DIRECTORY_SIP_ACCOUNT - auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain); + + user_xml = xml:element{ + 'user', + id = sip_account.record.auth_name, + xml:element{ + 'params', + xml:from_hash('param', user_parameters, 'name', 'value'), + }, + xml:element{ + 'variables', + xml:from_hash('variable', user_variables, 'name', 'value'), + }, + }; + end + else require 'common.gateway' - local sip_gateway = common.gateway.Gateway:new{ log = self.log, database = self.database }:find_by_name(gateway_name); + local sip_gateway = common.gateway.Gateway:new{ log = log, database = database }:find_by_auth_name(auth_name); + if sip_gateway then - log:debug('DIRECTORY_GATEWAY - name: ', gateway_name, ', auth_name: ', auth_name); + log:debug('DIRECTORY_GATEWAY - name: ', sip_gateway.name, ', auth_name: ', auth_name); local user_variables = { - user_context = "default", - gs_from_gateway = "true", - gs_gateway_name = gateway_name, - gs_gateway_id = sip_gateway.id, + user_context = 'default', + gs_from_gateway = 'true', + gs_gateway_name = sip_gateway.name, + gs_gateway_id = sip_gateway.id, + gs_gateway_domain = domain, } user_xml = xml:element{ @@ -414,73 +506,7 @@ function directory_sip_account(database) }, }; else - log:debug('DIRECTORY_GATEWAY - gateway not found - name: ', gateway_name, ', auth_name: ', auth_name); - end - else - require 'common.sip_account' - local sip_account = common.sip_account.SipAccount:new{ log = log, database = database}:find_by_auth_name(auth_name); - - require 'common.configuration_table' - local user_parameters = common.configuration_table.get(database, 'sip_accounts', 'parameters'); - - if sip_account ~= nil then - user_parameters['password'] = sip_account.record.password; - user_parameters['vm-password'] = sip_account.record.voicemail_pin; - - local user_variables = { - user_context = "default", - gs_from_gateway = "false", - gs_account_id = sip_account.record.id, - gs_account_uuid = sip_account.record.uuid, - gs_account_type = "SipAccount", - gs_account_state = sip_account.record.state, - gs_account_caller_name = sip_account.record.caller_name, - gs_account_owner_type = sip_account.record.sip_accountable_type, - gs_account_owner_id = sip_account.record.sip_accountable_id - } - - if tostring(purpose) == 'publish-vm' then - log:debug('DIRECTORY_SIP_ACCOUNT - purpose: VoiceMail, auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain); - user_xml = xml:element{ - 'groups', - xml:element{ - 'group', - name = 'default', - xml:element{ - 'users', - xml:element{ - 'user', - id = sip_account.record.auth_name, - xml:element{ - 'params', - xml:from_hash('param', user_parameters, 'name', 'value'), - }, - xml:element{ - 'variables', - xml:from_hash('variable', user_variables, 'name', 'value'), - }, - }, - }, - }, - }; - else - log:debug('DIRECTORY_SIP_ACCOUNT - auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain); - - user_xml = xml:element{ - 'user', - id = sip_account.record.auth_name, - xml:element{ - 'params', - xml:from_hash('param', user_parameters, 'name', 'value'), - }, - xml:element{ - 'variables', - xml:from_hash('variable', user_variables, 'name', 'value'), - }, - }; - end - else - log:debug('DIRECTORY_SIP_ACCOUNT - sip account not found - auth_name: ', auth_name, ', domain: ', domain); + log:debug('DIRECTORY_SIP_ACCOUNT - neither a sip account nor a gateway found by SIP user name: ', auth_name, ', domain: ', domain); -- fake sip_account configuration user_parameters['password'] = tostring(math.random(0, 65534)); user_parameters['vm-password'] = ''; @@ -562,6 +588,8 @@ if XML_REQUEST.section == 'configuration' and XML_REQUEST.tag_name == 'configura conf_voicemail(database); elseif XML_REQUEST.key_value == "post_load_switch.conf" then conf_post_switch(database); + elseif XML_REQUEST.key_value == "event_socket.conf" then + conf_event_socket(database); end elseif XML_REQUEST.section == 'directory' and XML_REQUEST.tag_name == '' then log:debug('SIP_ACCOUNT_DIRECTORY - initialization phase'); diff --git a/misc/freeswitch/scripts/dialplan/callback.lua b/misc/freeswitch/scripts/dialplan/callback.lua new file mode 100644 index 0000000..f6fd48e --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/callback.lua @@ -0,0 +1,85 @@ +-- Gemeinschaft 5 module: callback class +-- (c) AMOOMA GmbH 2013 +-- + +module(...,package.seeall) + +Callback = {} + +-- create callback callback ;) +function Callback.new(self, arg) + arg = arg or {} + callback = arg.callback or {} + setmetatable(callback, self); + self.__index = self; + self.class = 'callback'; + self.log = arg.log; + self.session = arg.session; + self.sessions = {}; + local id = common.str.to_i(self.session); + self.sessions[id] = { id = id, session = self.session, dtmf = {} }; + return callback; +end + + +function Callback.callback(self, class, identifier, callback_function, callback_instance, callback_session) + local id = common.str.to_i(callback_session or self.session); + local session_record = self.sessions[id]; + + if not session_record and callback_session then + self.sessions[id] = { id = id, session = callback_session, dtmf = {} }; + session_record = self.sessions[id]; + end + + if not session_record then + return false; + end + + + _G['global_callback_record_' .. id] = session_record; + + session_record.session:setInputCallback('global_callback_handler', 'global_callback_record_' .. id); + session_record[class][identifier] = { method = callback_function, instance = callback_instance }; + + return true; +end + + +function Callback.callback_unset(self, class, identifier, callback_session) + local session_record = self.sessions[tostring(callback_session or self.session)]; + if not session_record then + return false; + end + + -- session_record.session:unsetInputCallback(); + session_record[class][identifier] = nil; + + return true; +end + + +function Callback.run(self, arg) + local identifier = arg[2]; + local data = arg[3]; + local session_record = arg[4]; + local callbacks = session_record[identifier]; + + local return_value = nil; + if callbacks then + for identifier, callback in pairs(callbacks) do + if callback.method then + local result = nil; + if callback.instance then + result = callback.method(callback.instance, data); + else + result = callback.method(data.digit, data); + end + if result == false then + return_value = 'break'; + end + end + end + end + + return return_value; +end diff --git a/misc/freeswitch/scripts/dialplan/dialplan.lua b/misc/freeswitch/scripts/dialplan/dialplan.lua index ffad4da..5335328 100644 --- a/misc/freeswitch/scripts/dialplan/dialplan.lua +++ b/misc/freeswitch/scripts/dialplan/dialplan.lua @@ -22,6 +22,9 @@ local CALL_FORWARDING_SERVICES = { -- create dialplan object function Dialplan.new(self, arg) + require 'common.str'; + require 'common.array'; + arg = arg or {} object = arg.object or {} setmetatable(object, self); @@ -35,7 +38,6 @@ 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 @@ -74,8 +76,7 @@ end function Dialplan.configuration_read(self) - require 'common.str' - require 'common.configuration_table' + require 'common.configuration_table'; -- dialplan configuration self.config = common.configuration_table.get(self.database, 'dialplan'); @@ -124,7 +125,6 @@ end function Dialplan.auth_sip_account(self) - require 'common.str' if not common.str.blank(self.caller.auth_account_type) then self.log:info('AUTH_SIP_ACCOUNT - ', self.caller.auth_account_type, '=', self.caller.account_id, '/', self.caller.account_uuid); return true; @@ -135,10 +135,22 @@ end function Dialplan.auth_gateway(self) require 'common.gateway' local gateway_class = common.gateway.Gateway:new{ log = self.log, database = self.database}; - local gateway = gateway_class:authenticate(self.caller); + + local gateway = false; + + if self.caller:to_b('gs_from_gateway') then + gateway = { + name = self.caller:to_s('gs_gateway_name'), + id = self.caller:to_i('gs_gateway_id'), + } + log:info('AUTH_GATEWAY - authenticaded by password and username: ', self.caller:to_s('username'), ', gateway=', gateway.id, '|', gateway.name, ', ip: ', self.caller.sip_contact_host); + return gateway_class:find_by_id(gateway.id); + else + gateway = gateway_class:authenticate(self.caller); + end if gateway then - log:info('AUTH_GATEWAY - ', gateway.auth_source, ' ~ ', gateway.auth_pattern, ', gateway=', gateway.id, ', name: ', gateway.name, ', ip: ', self.caller.sip_contact_host); + log:info('AUTH_GATEWAY - ', gateway.auth_source, ' ~ ', gateway.auth_pattern, ', gateway=', gateway.id, '|', gateway.name, ', ip: ', self.caller.sip_contact_host); return gateway_class:find_by_id(gateway.id); end end @@ -151,20 +163,15 @@ end function Dialplan.retrieve_caller_data(self) - require 'common.str' self.caller.caller_phone_numbers_hash = {}; - -- TODO: Set auth_account on transfer initiated by calling party - if not common.str.blank(self.caller.dialed_sip_user) then - self.caller.auth_account = self:object_find{class = 'sipaccount', domain = self.caller.dialed_domain, auth_account = self.caller.dialed_sip_user}; - if self.caller.set_auth_account then - self.caller:set_auth_account(self.caller.auth_account); - end + if not common.str.blank(self.caller.previous_destination_type) and not common.str.blank(self.caller.previous_destination_uuid) then + self.log:debug('CALLER_DATA - authenticate by previous destination: ', self.caller.previous_destination_type, '=', self.caller.previous_destination_id, '/', self.caller.previous_destination_uuid); + self.caller.auth_account = self:object_find{class = self.caller.previous_destination_type, uuid = self.caller.previous_destination_uuid}; 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{class = self.caller.auth_account_type, uuid = self.caller.auth_account_uuid}; - if self.caller.set_auth_account then - self.caller:set_auth_account(self.caller.auth_account); - end + elseif not common.str.blank(self.caller.dialed_sip_user) then + self.caller.auth_account = self:object_find{class = 'sipaccount', domain = self.caller.dialed_domain, auth_account = self.caller.dialed_sip_user}; end if self.caller.auth_account then @@ -174,13 +181,19 @@ function Dialplan.retrieve_caller_data(self) else self.log:error('CALLER_DATA - auth owner not found'); end + if self.caller.set_auth_account then + self.caller:set_auth_account(self.caller.auth_account); + end else - self.log:info('CALLER_DATA - no data - unauthenticated call: ', self.caller.auth_account_type, '/', self.caller.auth_account_uuid); + self.log:info('CALLER_DATA - no data - unauthenticated call: ', self.caller.auth_account_type, '=', self.caller.auth_account_id, '/', 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{class = self.caller.account_type, uuid = self.caller.account_uuid}; if self.caller.account then + self.caller.clir = common.str.to_b(common.array.try(self.caller, 'account.record.clir')); + self.caller.clip = common.str.to_b(common.array.try(self.caller, 'account.record.clip')); + 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 @@ -207,8 +220,6 @@ end function Dialplan.destination_new(self, arg) - require 'common.str' - local destination = { number = arg.number or '', type = arg.type or 'unknown', @@ -288,7 +299,6 @@ function Dialplan.dial(self, destination) local user_id = nil; local tenant_id = nil; - require 'common.str' destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1]; if destination.node_local and destination.type == 'sipaccount' then @@ -296,6 +306,7 @@ function Dialplan.dial(self, destination) destination.account = self:object_find{class = destination.type, id = destination.id}; if destination.account then + destination.uuid = destination.account.uuid; 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); @@ -306,7 +317,7 @@ function Dialplan.dial(self, destination) self.log:debug('DESTINATION_GROUPS - pickup_groups: ', table.concat(group_names, ',')); for index=1, #group_ids do table.insert(destination.pickup_groups, 'g' .. group_ids[index]); - end + end end if destination.account and destination.account.owner then @@ -317,12 +328,14 @@ function Dialplan.dial(self, destination) elseif destination.account.owner.class == 'tenant' then tenant_id = destination.account.owner.id; end + self.caller:set_variable('gs_destination_owner_type', destination.account.owner.class); + self.caller:set_variable('gs_destination_owner_id', destination.account.owner.id); + self.caller:set_variable('gs_destination_owner_uuid', destination.account.owner.uuid); end end if not self.caller.clir then if user_id or tenant_id then - require 'common.str' if self.phonebook_number_lookup then require 'dialplan.phone_book' @@ -455,16 +468,14 @@ 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); + require 'common.conference'; + local 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 } + return conference:enter(self.caller, self.domain); end @@ -579,13 +590,16 @@ 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); + + local voicemail_account = nil; + + local sip_account_id + if not common.str.blank(destination.number) and false then + voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(destination.number); + elseif self.caller.auth_account and self.caller.auth_account.class == 'sipaccount' then + voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(self.caller.auth_account.id); + end if not voicemail_account then self.log:error('VOICEMAIL - no mailbox'); @@ -609,7 +623,6 @@ end function Dialplan.switch(self, destination) - require 'common.str' local result = nil; self.dial_timeout_active = self.dial_timeout; @@ -703,7 +716,7 @@ function Dialplan.switch(self, destination) elseif not common.str.blank(destination.number) then local result = { continue = false, code = 404, phrase = 'No route' } - local clip_no_screening = common.str.try(self.caller, 'account.record.clip_no_screening'); + local clip_no_screening = common.array.try(self.caller, 'account.record.clip_no_screening'); self.caller.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 @@ -713,8 +726,8 @@ function Dialplan.switch(self, destination) for index, number in ipairs(self.caller.caller_phone_numbers) do table.insert(self.caller.caller_id_numbers, number); end - self.log:info('CALLER_ID_NUMBERS - clir: ', self.caller.clir, ', numbers: ', table.concat(self.caller.caller_id_numbers, ',')); + self.log:info('SWITCH - clir: ', self.caller.clir, ', caller_id_numbers: ', table.concat(self.caller.caller_id_numbers, ',')); destination.callee_id_number = destination.number; destination.callee_id_name = nil; @@ -727,9 +740,8 @@ function Dialplan.switch(self, destination) end 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'); + local user_id = common.array.try(self.caller, 'account.owner.id'); + local tenant_id = common.array.try(self.caller, 'account.owner.record.current_tenant_id'); if user_id or tenant_id then require 'dialplan.phone_book' @@ -745,7 +757,6 @@ function Dialplan.switch(self, destination) 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); @@ -796,7 +807,6 @@ end function Dialplan.run(self, destination) - require 'common.str'; require 'dialplan.router'; self.caller:set_variable('hangup_after_bridge', false); @@ -881,14 +891,21 @@ function Dialplan.run(self, destination) end self.caller:set_variable('default_language', self.caller.language); - self.caller:set_variable('sound_prefix', common.str.try(self.config, 'sounds.' .. tostring(self.caller.language))); + self.caller:set_variable('sound_prefix', common.array.try(self.config, 'sounds.' .. tostring(self.caller.language))); self.log:info('DIALPLAN start - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name, '" , number: ', destination.number, ', language: ', self.caller.language); + + self.caller.static_caller_id_number = self.caller.caller_id_number; + self.caller.static_caller_id_name = self.caller.caller_id_name; + 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.caller.caller_id_number = self.caller.static_caller_id_number; + self.caller.caller_id_name = self.caller.static_caller_id_name; self.log:info('LOOP ', loop, ' - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, @@ -900,6 +917,7 @@ function Dialplan.run(self, destination) 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); + self.caller:set_variable('gs_destination_node_id, ', destination.node_id); result = self:switch(destination); result = result or { continue = false, code = 502, cause = 'DESTINATION_OUT_OF_ORDER', phrase = 'Destination out of order' } diff --git a/misc/freeswitch/scripts/dialplan/fax.lua b/misc/freeswitch/scripts/dialplan/fax.lua index 6dce0a9..dc52d16 100644 --- a/misc/freeswitch/scripts/dialplan/fax.lua +++ b/misc/freeswitch/scripts/dialplan/fax.lua @@ -231,8 +231,8 @@ function Fax.insert_document(self, record) return self.database:query(sql_query); end -function Fax.trigger_notification(self, fax_document_id, uuid) - local command = 'http_request.lua ' .. uuid .. ' http://127.0.0.1/trigger/fax?fax_account_id=' .. tostring(fax_document_id); +function Fax.trigger_notification(self, fax_account_id, uuid) + local command = 'http_request.lua ' .. uuid .. ' http://127.0.0.1/trigger/fax?fax_account_id=' .. tostring(fax_account_id); require 'common.fapi' return common.fapi.FApi:new():execute('luarun', command); diff --git a/misc/freeswitch/scripts/dialplan/functions.lua b/misc/freeswitch/scripts/dialplan/functions.lua index 780e3be..a47d9d3 100644 --- a/misc/freeswitch/scripts/dialplan/functions.lua +++ b/misc/freeswitch/scripts/dialplan/functions.lua @@ -23,7 +23,6 @@ function Functions.ensure_caller_sip_account(self, caller) 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 }; @@ -120,35 +119,32 @@ function Functions.dialplan_function(self, caller, dialed_number) return result or { continue = false, code = 505, phrase = 'Error executing function', no_cdr = true }; 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.log:info('TRANSFER_ALL - initiator: ', caller.account.class, '=', caller.account.id, '/', caller.account.uuid, ', number: ', destination_number); + local sql_query = 'SELECT `uuid`, `b_uuid`, `callee_number`, `caller_id_number`, `sip_account_id`, `b_sip_account_id` \ + FROM `calls_active` WHERE `sip_account_id` = '.. caller.account.id .. ' OR `b_sip_account_id` = '.. caller.account.id; + local index = 1; 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); + if not common.str.blank(call_entry.uuid) and tostring(caller.account.id) ~= tostring(call_entry.sip_account_id) then + self.log:info('TRANSFER_ALEG ', index, ' - channel/', call_entry.uuid, '|', call_entry.caller_id_number); + freeswitch.API():execute("uuid_transfer", call_entry.uuid .. " " .. destination_number); + end + if not common.str.blank(call_entry.b_uuid) and tostring(caller.account.id) ~= tostring(call_entry.b_sip_account_id) then + self.log:info('TRANSFER_BLEG ', index, ' - channel/', call_entry.b_uuid, '|', call_entry.callee_number); + freeswitch.API():execute("uuid_transfer", call_entry.b_uuid .. " " .. destination_number); end - freeswitch.API():execute("uuid_transfer", uid .. " " .. destination_number); + index = index + 1; end) - return destination_number; + return { continue = true, number = destination_number } end @@ -165,19 +161,18 @@ function Functions.intercept_any_number(self, caller, destination_number) local phone_numberable = common.object.Object:new{ log = self.log, database = self.database}:find{class = phone_number.record.phone_numberable_type, id = phone_number.record.phone_numberable_id}; if not phone_numberable then - self.log:notice('FUNCTION_INTERCEPT_ANY_NUMBER - numberable not found: ', dphone_number.record.phone_numberable_type, '=', phone_number.record.phone_numberable_id); + self.log:notice('FUNCTION_INTERCEPT_ANY_NUMBER - numberable not found: ', phone_number.record.phone_numberable_type, '=', phone_number.record.phone_numberable_id); return { continue = false, code = 404, phrase = 'Destination not found', no_cdr = true }; end - require 'common.str'; require 'common.group'; local group_class = common.group.Group:new{ log = self.log, database = self.database }; - local group_ids = group_class:union(common.str.try(caller, 'auth_account.group_ids'), common.str.try(caller, 'auth_account.owner.group_ids')); + local group_ids = group_class:union(common.array.try(caller, 'auth_account.group_ids'), common.array.try(caller, 'auth_account.owner.group_ids')); local target_groups, target_group_ids = group_class:permission_targets(group_ids, 'pickup'); - local destination_group_ids = group_class:union(common.str.try(phone_numberable, 'group_ids'), common.str.try(phone_numberable, 'owner.group_ids')); + local destination_group_ids = group_class:union(common.array.try(phone_numberable, 'group_ids'), common.array.try(phone_numberable, 'owner.group_ids')); if #group_class:intersection(destination_group_ids, target_group_ids) == 0 then - self.log:notice('FUNCTION_INTERCEPT_ANY_NUMBER - Groups not found or insufficient permissions'); + self.log:notice('FUNCTION_INTERCEPT_ANY_NUMBER - Groups not found or insufficient permissions, destination:', destination_group_ids, ', target: ', target_group_ids); return { continue = false, code = 402, phrase = '"Insufficient permissions', no_cdr = true }; end @@ -195,10 +190,9 @@ function Functions.group_pickup(self, caller, group_id) return { continue = false, code = 505, phrase = 'Incompatible destination', no_cdr = true }; end - require 'common.str'; require 'common.group'; local group_class = common.group.Group:new{ log = self.log, database = self.database }; - local group_ids = group_class:union(common.str.try(caller, 'auth_account.group_ids'), common.str.try(caller, 'auth_account.owner.group_ids')); + local group_ids = group_class:union(common.array.try(caller, 'auth_account.group_ids'), common.array.try(caller, 'auth_account.owner.group_ids')); local target_group = group_class:is_target(group_id, 'pickup'); if not target_group then @@ -250,8 +244,6 @@ 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; @@ -367,7 +359,6 @@ 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 @@ -845,8 +836,6 @@ end function Functions.hangup(self, caller, code, phrase) - require 'common.str' - if not tonumber(code) then code = 403; phrase = 'Forbidden'; @@ -885,8 +874,7 @@ function Functions.call_parking_inout_index(self, caller, stall_index) return { continue = false, code = 404, phrase = 'No parkings stall specified', no_cdr = true } end - require 'common.str'; - local owner = common.str.try(caller, 'auth_account.owner'); + local owner = common.array.try(caller, 'auth_account.owner'); if not owner then self.log:notice('FUNCTION_CALL_PARKING_INOUT_INDEX - stall owner not specified'); diff --git a/misc/freeswitch/scripts/dialplan/hunt_group.lua b/misc/freeswitch/scripts/dialplan/hunt_group.lua index fa3c05b..b1728c3 100644 --- a/misc/freeswitch/scripts/dialplan/hunt_group.lua +++ b/misc/freeswitch/scripts/dialplan/hunt_group.lua @@ -38,7 +38,7 @@ end function HuntGroup.find_by_uuid(self, uuid) - local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= "'.. uuid .. '" LIMIT 1'; + local sql_query = 'SELECT * FROM `hunt_groups` WHERE `uuid`= "'.. uuid .. '" LIMIT 1'; local hunt_group = nil; self.database:query(sql_query, function(entry) @@ -98,25 +98,26 @@ function HuntGroup.run(self, dialplan_object, caller, destination) self.log:info('HUNTGROUP ', self.record.id, ' - name: ', self.record.name, ', strategy: ', self.record.strategy,', members: ', #hunt_group_members); - local clip_no_screening = common.str.try(caller, 'account.record.clip_no_screening'); caller.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.caller_id_numbers, number); - end - end for index, number in ipairs(caller.caller_phone_numbers) do table.insert(caller.caller_id_numbers, number); end - self.log:info('CALLER_ID_NUMBERS - clir: ', caller.clir, ', numbers: ', table.concat(caller.caller_id_numbers, ',')); - local save_destination = caller.destination; + local phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(self.id, self.class); + for index, number in ipairs(phone_numbers) do + table.insert(caller.caller_id_numbers, number); + end + + self.log:debug('HUNTGROUP ', self.record.id, ' - auth: ', caller.auth_account.class, '=', caller.auth_account.id, '/', caller.auth_account.uuid, ', caller: ', caller.account.class, '=', caller.account.id, '/', caller.account.uuid); + self.log:info('HUNTGROUP ', self.record.id, ' - clir: ', caller.clir, ', caller_id_numbers: ', table.concat(caller.caller_id_numbers, ',')); + + local hunt_group_destination = caller.destination; 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 - + caller.destination = destination; caller.destination_number = destination.number; require 'dialplan.router' @@ -144,8 +145,8 @@ function HuntGroup.run(self, dialplan_object, caller, destination) end end - caller.destination = save_destination; - caller.destination_number = save_destination.number; + caller.destination = hunt_group_destination; + caller.destination_number = hunt_group_destination.number; local forwarding_destination = nil; if caller.forwarding_service == 'assistant' and caller.auth_account then @@ -165,7 +166,7 @@ function HuntGroup.run(self, dialplan_object, caller, destination) 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 }); + result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( recursive_destinations, { callee_id_number = hunt_group_destination.number, timeout = member_timeout }); if result.disposition == 'SUCCESS' then if result.fork_index then result.destination = recursive_destinations[result.fork_index]; diff --git a/misc/freeswitch/scripts/dialplan/ivr.lua b/misc/freeswitch/scripts/dialplan/ivr.lua new file mode 100644 index 0000000..f8b8a2d --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/ivr.lua @@ -0,0 +1,119 @@ +-- Gemeinschaft 5 module: ivr class +-- (c) AMOOMA GmbH 2013 +-- + +module(...,package.seeall) + +Ivr = {} + +-- create ivr ivr ;) +function Ivr.new(self, arg) + arg = arg or {} + ivr = arg.ivr or {} + setmetatable(ivr, self); + self.__index = self; + self.class = 'ivr'; + self.log = arg.log; + self.caller = arg.caller; + self.dtmf_threshold = arg.dtmf_threshold or 500; + + return ivr; +end + + +function Ivr.ivr_phrase(self, phrase, keys, timeout, ivr_repeat) + ivr_repeat = ivr_repeat or 3; + timeout = timeout or 30; + self.digit = ''; + self.exit = false; + + self.break_keys = {}; + for index=1, #keys do + self.break_keys[keys[index]] = true; + end + + global_callback:callback('dtmf', 'ivr_ivr_phrase', self.ivr_phrase_dtmf, self); + + for index=0, ivr_repeat do + self.caller.session:sayPhrase(phrase, table.concat(keys, ':')); + self.caller:sleep(timeout * 1000); + if self.exit then + break; + end + end + + global_callback:callback_unset('dtmf', 'ivr_ivr_phrase'); + + return self.digit; +end + + +function Ivr.ivr_phrase_dtmf(self, dtmf) + if self.break_keys[dtmf.digit] then + self.digit = dtmf.digit; + self.exit = true; + return false; + end +end + + +function Ivr.read_phrase(self, phrase, phrase_data, max_keys, min_keys, timeout, enter_key) + self.max_keys = max_keys or 64; + self.min_keys = min_keys or 1; + self.enter_key = enter_key or '#'; + self.digits = ''; + self.exit = false; + timeout = timeout or 30; + + global_callback:callback('dtmf', 'ivr_read_phrase', self.read_phrase_dtmf, self); + self.caller.session:sayPhrase(phrase, phrase_data or enter_key or ''); + self.caller:sleep(timeout * 1000); + global_callback:callback_unset('dtmf', 'ivr_read_phrase'); + + return self.digits; +end + + +function Ivr.read_phrase_dtmf(self, dtmf) + if dtmf.duration < self.dtmf_threshold then + return nil; + end + + if self.enter_key == dtmf.digit then + self.exit = true; + return false; + end + + self.digits = self.digits .. dtmf.digit; +end + + +function Ivr.check_pin(self, phrase, pin, pin_timeout, pin_repeat, key_enter) + if not pin then + return nil; + end + + pin_timeout = pin_timeout or 30; + pin_repeat = pin_repeat or 3; + key_enter = key_enter or '#'; + + local digits = ''; + for i = 1, pin_repeat do + if digits == pin then + self.caller:send_display('PIN: OK'); + break + elseif digits ~= "" then + self.caller:send_display('PIN: wrong'); + end + self.caller:send_display('Enter PIN'); + digits = ivr:read_phrase(phrase, nil, 0, pin:len() + 1, pin_timeout, key_enter); + end + + if digits ~= pin then + self.caller:send_display('PIN: wrong'); + return false + end + self.caller:send_display('PIN: OK'); + + return true; +end diff --git a/misc/freeswitch/scripts/dialplan/router.lua b/misc/freeswitch/scripts/dialplan/router.lua index 8473c2b..322c748 100644 --- a/misc/freeswitch/scripts/dialplan/router.lua +++ b/misc/freeswitch/scripts/dialplan/router.lua @@ -9,6 +9,7 @@ Router = {} -- create route object function Router.new(self, arg) require 'common.str'; + require 'common.array'; arg = arg or {} object = arg.object or {} setmetatable(object, self); @@ -19,6 +20,7 @@ function Router.new(self, arg) self.routes = arg.routes or {}; self.caller = arg.caller; self.variables = arg.variables or {}; + self.log_details = arg.log_details; self.routing_tables = {}; return object; end @@ -60,31 +62,37 @@ function Router.read_table(self, table_name, force_reload) end -function Router.expand_variables(self, line, variables, variables2) - return (line:gsub('{([%a%d%._]+)}', function(captured) - return common.str.try(variables, captured) or common.str.try(variables2, captured) or ''; - end)) -end - - function Router.element_match(self, pattern, search_string, replacement, route_variables) local success, result = pcall(string.find, search_string, pattern); if not success then - self.log:error('ELEMENT_MATCH - table error - pattern: ', pattern, ', search_string: ', search_string); + self.log:error('ELEMENT_ERROR - table error - pattern: ', pattern, ', search_string: ', search_string); elseif result then - return true, search_string:gsub(pattern, self:expand_variables(replacement, route_variables, self.variables)); + local replace_by = common.array.expand_variables(replacement, route_variables, self.variables) + result = search_string:gsub(pattern, replace_by); + if self.log_details then + self.log:debug('ELEMENT_MATCH - ', search_string, ' ~= ', pattern, ' => ', replacement, ' => ', replace_by); + end + return true, result; end + if self.log_details then + self.log:debug('ELEMENT_NO_MATCH - ', search_string, ' != ', pattern); + end return false; end -function Router.element_match_group(self, pattern, groups, replacement, use_key, route_variables) +function Router.element_match_group(self, pattern, groups, replacement, use_key, route_variables, variable_name) if type(groups) ~= 'table' then + self.log:debug('ELEMENT_FIND_IN_ARRAY - no such array: ', variable_name, ', use_keys: ', tostring(use_key)); return false; end + if self.log_details then + self.log:debug('ELEMENT_FIND_IN_ARRAY - array: ', variable_name, ', use_keys: ', tostring(use_key)); + end + for key, value in pairs(groups) do if use_key then value = key; @@ -102,8 +110,9 @@ function Router.route_match(self, route) gateway = 'gateway' .. route.endpoint_id, ['type'] = route.endpoint_type, id = route.endpoint_id, - destination_number = common.str.try(self, 'caller.destination_number'), + destination_number = common.array.try(self, 'caller.destination_number'), channel_variables = {}, + route_id = route.id, }; local route_matches = false; @@ -114,22 +123,26 @@ function Router.route_match(self, route) local element = route.elements[index]; + if self.log_details then + self.log:debug('ROUTE_ELEMENT ', element.id, ' - var_in: ', element.var_in, ', var_out: ', element.var_out, ', action: ', element.action, ', mandatory: ', element.mandatory); + end + if element.action ~= 'none' then if common.str.blank(element.var_in) or common.str.blank(element.pattern) and element.action == 'set' then result = true; - replacement = self:expand_variables(element.replacement, destination, self.variables); + replacement = common.array.expand_variables(element.replacement, destination, self.variables); else local command, variable_name = common.str.partition(element.var_in, ':'); if not command or not variable_name then - local search_string = tostring(common.str.try(destination, element.var_in) or common.str.try(self.caller, element.var_in)); + local search_string = tostring(common.array.try(destination, element.var_in) or common.array.try(self.caller, element.var_in)); result, replacement = self:element_match(tostring(element.pattern), search_string, tostring(element.replacement), destination); elseif command == 'var' then - local search_string = tostring(common.str.try(self.caller, element.var_in)); + local search_string = tostring(common.array.try(self.caller, variable_name)); result, replacement = self:element_match(tostring(element.pattern), search_string, tostring(element.replacement)); elseif command == 'key' or command == 'val' then - local groups = common.str.try(self.caller, variable_name); - result, replacement = self:element_match_group(tostring(element.pattern), groups, tostring(element.replacement), command == 'key'); + local groups = common.array.try(destination, variable_name) or common.array.try(self.caller, variable_name); + result, replacement = self:element_match_group(tostring(element.pattern), groups, tostring(element.replacement), command == 'key', destination, variable_name); elseif command == 'chv' then local search_string = self.caller:to_s(variable_name); result, replacement = self:element_match(tostring(element.pattern), search_string, tostring(element.replacement)); @@ -151,7 +164,7 @@ function Router.route_match(self, route) if not common.str.blank(element.var_out) then local command, variable_name = common.str.partition(element.var_out, ':'); if not command or not variable_name or command == 'var' then - common.str.set(destination, element.var_out, replacement); + common.array.set(destination, element.var_out, replacement); elseif command == 'chv' then destination.channel_variables[variable_name] = replacement; elseif command == 'hdr' then @@ -180,7 +193,10 @@ function Router.route_run(self, table_name, find_first) local routes = {}; if type(routing_table) == 'table' then - for index=1, #routing_table do + for index=1, #routing_table do + if self.log_details then + self.log:info('ROUTE_',table_name:upper(),' ', index,' - ', table_name,'=', routing_table[index].id, '/', routing_table[index].name); + end local route = self:route_match(routing_table[index]); if route then table.insert(routes, route); diff --git a/misc/freeswitch/scripts/dialplan/session.lua b/misc/freeswitch/scripts/dialplan/session.lua index 7de85ca..9c43e74 100644 --- a/misc/freeswitch/scripts/dialplan/session.lua +++ b/misc/freeswitch/scripts/dialplan/session.lua @@ -58,6 +58,13 @@ function Session.init_channel_variables(self) self.node_id = self:to_i('sip_h_X-GS_node_id'); self.loop_count = self:to_i('sip_h_X-GS_loop_count'); + self.previous_destination_type = self:to_s('gs_destination_type'); + self.previous_destination_id = self:to_i('gs_destination_id'); + self.previous_destination_uuid = self:to_s('gs_destination_uuid'); + self.previous_destination_owner_type = self:to_s('gs_destination_owner_type'); + self.previous_destination_owner_id = self:to_i('gs_destination_owner_id'); + self.previous_destination_owner_uuid = self:to_s('gs_destination_owner_uuid'); + if self.node_id > 0 and self.node_id ~= self.local_node_id then self.from_node = true; else @@ -166,6 +173,12 @@ function Session.answer(self) return self.session:answer(); end +-- Is answered? +function Session.answered(self) + return self.session:answered(); +end + + function Session.intercept(self, uid) self.session:execute("intercept", uid); end @@ -226,3 +239,8 @@ function Session.expand_variables(self, line) return self.session:getVariable(captured) or ''; end)) end + + +function Session.playback(self, file) + self.session:streamFile(file); +end diff --git a/misc/freeswitch/scripts/dialplan/sip_call.lua b/misc/freeswitch/scripts/dialplan/sip_call.lua index 5c98792..0cde601 100644 --- a/misc/freeswitch/scripts/dialplan/sip_call.lua +++ b/misc/freeswitch/scripts/dialplan/sip_call.lua @@ -135,6 +135,17 @@ function SipCall.fork(self, destinations, arg ) if destination.alert_info then table.insert(origination_variables, "alert_info='" .. destination.alert_info .. "'"); end + if destination.account then + table.insert(origination_variables, "gs_account_type='" .. common.str.to_s(destination.account.class) .. "'"); + table.insert(origination_variables, "gs_account_id='" .. common.str.to_i(destination.account.id) .. "'"); + table.insert(origination_variables, "gs_account_uuid='" .. common.str.to_s(destination.account.uuid) .. "'"); + end + if self.caller.auth_account then + table.insert(origination_variables, "gs_auth_account_type='" .. common.str.to_s(self.caller.auth_account.class) .. "'"); + table.insert(origination_variables, "gs_auth_account_id='" .. common.str.to_i(self.caller.auth_account.id) .. "'"); + table.insert(origination_variables, "gs_auth_account_uuid='" .. common.str.to_s(self.caller.auth_account.uuid) .. "'"); + end + table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/' .. sip_account.record.profile_name .. '/' .. sip_account.record.auth_name .. '%' .. sip_account.record.sip_host); if destination.pickup_groups and #destination.pickup_groups > 0 then for key=1, #destination.pickup_groups do diff --git a/misc/freeswitch/scripts/dialplan/voicemail.lua b/misc/freeswitch/scripts/dialplan/voicemail.lua index ae7d0a1..caeeb48 100644 --- a/misc/freeswitch/scripts/dialplan/voicemail.lua +++ b/misc/freeswitch/scripts/dialplan/voicemail.lua @@ -66,15 +66,14 @@ function Voicemail.find_by_name(self, account_name) return voicemail_account end --- Find Voicemail account by name +-- Find Voicemail account by number 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); + local destination_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(phone_number); + if destination_number_object and destination_number_object.record.phone_numberable_type:lower() == "sipaccount" then + return self:find_by_sip_account_id(destination_number_object.record.phone_numberable_id); end return false; diff --git a/misc/freeswitch/scripts/event/perimeter_defense.lua b/misc/freeswitch/scripts/event/perimeter_defense.lua index acdfa8d..5bd124b 100644 --- a/misc/freeswitch/scripts/event/perimeter_defense.lua +++ b/misc/freeswitch/scripts/event/perimeter_defense.lua @@ -31,9 +31,10 @@ end function PerimeterDefense.event_handlers(self) return { CUSTOM = { - ['sofia::pre_register'] = self.sofia_pre_register, - ['sofia::register_attempt'] = self.sofia_register_attempt, - ['sofia::register_failure'] = self.sofia_register_failure, + ['sofia::pre_register'] = self.sofia_pre_register, + ['sofia::register_attempt'] = self.sofia_register_attempt, + ['sofia::register_failure'] = self.sofia_register_failure, + ['perimeter::control'] = self.perimeter_control, }, CHANNEL_HANGUP = { [true] = self.channel_hangup }, }; @@ -112,3 +113,20 @@ function PerimeterDefense.channel_hangup(self, event) local record = self:to_call_record(event, 'channel_hangup'); self.perimeter:check(record); end + + +function PerimeterDefense.control_to_action(self, event) + return { + key = event:getHeader('key'), + }; +end + + +function PerimeterDefense.perimeter_control(self, event) + local action = event:getHeader('action'); + if self.perimeter['action_' .. action] then + self.perimeter['action_' .. action](self.perimeter, self:control_to_action(event)) + else + self.log:error('[perimeter_defense] PERIMETER_DEFENSE - could not execute action: ', action); + end +end diff --git a/misc/freeswitch/scripts/event/presence_update.lua b/misc/freeswitch/scripts/event/presence_update.lua index 01ec17b..f9d8ee7 100644 --- a/misc/freeswitch/scripts/event/presence_update.lua +++ b/misc/freeswitch/scripts/event/presence_update.lua @@ -10,6 +10,13 @@ ACCOUNT_RECORD_TIMEOUT = 120; PresenceUpdate = {} function PresenceUpdate.new(self, arg) + require 'common.sip_account'; + require 'dialplan.presence'; + require 'common.str'; + require 'common.phone_number'; + require 'common.fapi'; + require 'common.configuration_table'; + arg = arg or {} object = arg.object or {} setmetatable(object, self); @@ -21,6 +28,13 @@ function PresenceUpdate.new(self, arg) self.presence_accounts = {} self.account_record = {} + self.config = common.configuration_table.get(self.database, 'presence_update'); + self.config = self.config or {}; + self.config.trigger = self.config.trigger or { + sip_account_presence = true, + sip_account_register = true, + sip_account_unregister = true, + }; return object; end @@ -34,10 +48,29 @@ function PresenceUpdate.event_handlers(self) end +function PresenceUpdate.retrieve_sip_account(self, account, uuid) + uuid = uuid or 'presence_update'; + + if not self.account_record[account] or ((os.time() - self.account_record[account].created_at) > ACCOUNT_RECORD_TIMEOUT) then + self.log:debug('[', uuid,'] PRESENCE - retrieve account data - account: ', account); + + local sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(account); + if not sip_account then + return + end + + local phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(sip_account.id, sip_account.class); + + self.account_record[account] = { id = sip_account.id, class = sip_account.class, phone_numbers = phone_numbers, created_at = os.time() } + end + + return self.account_record[account]; +end + + function PresenceUpdate.presence_probe(self, event) local DIALPLAN_FUNCTION_PATTERN = '^f[_%-].*'; - require 'common.str' local event_to = event:getHeader('to'); local event_from = event:getHeader('from'); local probe_type = event:getHeader('probe-type'); @@ -59,6 +92,13 @@ end function PresenceUpdate.sofia_register(self, event) local account = event:getHeader('from-user'); + local timestamp = event:getHeader('Event-Date-Timestamp'); + + local sip_account = self:retrieve_sip_account(account, account); + if sip_account and common.str.to_b(self.config.trigger.sip_account_register) then + self:trigger_rails(sip_account, 'register', timestamp, account) + end + self.log:debug('[', account, '] PRESENCE_UPDATE - flushing account cache on register'); self.presence_accounts[account] = nil; end @@ -66,28 +106,48 @@ end function PresenceUpdate.sofia_ungerister(self, event) local account = event:getHeader('from-user'); + local timestamp = event:getHeader('Event-Date-Timestamp'); + + local sip_account = self:retrieve_sip_account(account, account); + if sip_account and common.str.to_b(self.config.trigger.sip_account_unregister) then + self:trigger_rails(sip_account, 'unregister', timestamp, account) + end + self.log:debug('[', account, '] PRESENCE_UPDATE - flushing account cache on unregister'); self.presence_accounts[account] = nil; end function PresenceUpdate.presence_in(self, event) - if not event:getHeader('status') then - return - end - local account, domain = common.str.partition(event:getHeader('from'), '@'); - local direction = tostring(event:getHeader('presence-call-direction')) + local call_direction = tostring(event:getHeader('presence-call-direction') or event:getHeader('call-direction')) local state = event:getHeader('presence-call-info-state'); local uuid = event:getHeader('Unique-ID'); local caller_id = event:getHeader('Caller-Caller-ID-Number'); + local protocol = tostring(event:getHeader('proto')); + local timestamp = event:getHeader('Event-Date-Timestamp'); + local direction = nil; + + if call_direction == 'inbound' then + direction = true; + elseif call_direction == 'outbound' then + direction = false; + end - if direction == 'inbound' then - self.log:info('[', uuid,'] PRESENCE_INBOUND: account: ', account, ', state: ', state); - self:sip_account(true, account, domain, state, uuid); - elseif direction == 'outbound' then - self.log:info('[', uuid,'] PRESENCE_OUTBOUND: account: ', account, ', state: ', state, ', caller: ', caller_id); - self:sip_account(false, account, domain, state, uuid, caller_id); + if protocol == 'conf' then + state = event:getHeader('answer-state'); + local login = tostring(event:getHeader('proto')); + self.log:info('[', uuid,'] PRESENCE_CONFERENCE_', call_direction:upper(), ' ', common.str.to_i(account), ' - identifier: ', account, ', state: ', state); + self:conference(direction, account, domain, state, uuid); + elseif protocol == 'sip' or protocol == 'any' then + if protocol == 'sip' and common.str.blank(state) then + self.log:debug('[', uuid,'] PRESENCE_', call_direction:upper(),' no state - protocol: ', protocol, ', account: ', account); + return; + end + self.log:info('[', uuid,'] PRESENCE_', call_direction:upper(),' - protocol: ', protocol, ', account: ', account, ', state: ', state); + self:sip_account(direction, account, domain, state, uuid, caller_id, timestamp); + else + self.log:info('[', uuid,'] PRESENCE_', call_direction:upper(),' unhandled protocol: ', protocol, ', account: ', account, ', state: ', state); end end @@ -114,10 +174,9 @@ end function PresenceUpdate.call_forwarding(self, account, domain, call_forwarding_id) - require 'common.call_forwarding' - local call_forwarding = common.call_forwarding.CallForwarding:new{ log=self.log, database=self.database, domain=domain }:find_by_id(call_forwarding_id); - - require 'common.str' + require 'common.call_forwarding'; + + local call_forwarding = common.call_forwarding.CallForwarding:new{ log=self.log, database=self.database, domain=domain }:find_by_id(call_forwarding_id); if call_forwarding and common.str.to_b(call_forwarding.record.active) then local destination_type = tostring(call_forwarding.record.call_forwardable_type):lower() @@ -138,7 +197,6 @@ function PresenceUpdate.hunt_group_membership(self, account, domain, member_id) if status then self.log:debug('[', account, '] PRESENCE_UPDATE - updating hunt group membership presence - id: ', member_id); - require 'dialplan.presence' local presence_class = dialplan.presence.Presence:new{ log = self.log, database = self.database, @@ -156,7 +214,6 @@ function PresenceUpdate.acd_membership(self, account, domain, member_id) if status then self.log:debug('[', account, '] PRESENCE_UPDATE - updating ACD membership presence - id: ', member_id); - require 'dialplan.presence' local presence_class = dialplan.presence.Presence:new{ log = self.log, database = self.database, @@ -168,32 +225,45 @@ function PresenceUpdate.acd_membership(self, account, domain, member_id) end -function PresenceUpdate.sip_account(self, inbound, account, domain, status, uuid, caller_id) +function PresenceUpdate.sip_account(self, inbound, account, domain, status, uuid, caller_id, timestamp) local status_map = { progressing = 'early', alerting = 'confirmed', active = 'confirmed' } + + local sip_account = self:retrieve_sip_account(account, uuid); + if not sip_account then + return; + end - if not self.account_record[account] or ((os.time() - self.account_record[account].created_at) > ACCOUNT_RECORD_TIMEOUT) then - self.log:debug('[', uuid,'] PRESENCE - retrieve account data - account: ', account); - - require 'common.sip_account' - local sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(account); - - if not sip_account then - return - end - - require 'common.phone_number' - local phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(sip_account.id, sip_account.class); + dialplan.presence.Presence:new{ + log = self.log, + database = self.database, + inbound = inbound, + domain = domain, + accounts = sip_account.phone_numbers, + uuid = uuid + }:set(status_map[status] or 'terminated', caller_id); - self.account_record[account] = { id = sip_account.id, class = sip_account.class, phone_numbers = phone_numbers, created_at = os.time() } + if common.str.to_b(self.config.trigger.sip_account_presence) then + self:trigger_rails(sip_account, status_map[status] or 'terminated', timestamp, uuid); end +end - require 'dialplan.presence' - local result = dialplan.presence.Presence:new{ + +function PresenceUpdate.conference(self, inbound, account, domain, status, uuid) + + dialplan.presence.Presence:new{ log = self.log, database = self.database, inbound = inbound, domain = domain, - accounts = self.account_record[account].phone_numbers, + accounts = { account }, uuid = uuid - }:set(status_map[status] or 'terminated', caller_id); + }:set(status or 'terminated'); +end + + +function PresenceUpdate.trigger_rails(self, account, status, timestamp, uuid) + if account.class == 'sipaccount' then + local command = 'http_request.lua ' .. tostring(uuid) .. ' http://127.0.0.1/trigger/sip_account_update/' .. tostring(account.id) .. '?timestamp=' .. timestamp .. '&status=' .. tostring(status); + common.fapi.FApi:new():execute('luarun', command); + end end diff --git a/misc/freeswitch/scripts/send_fax.lua b/misc/freeswitch/scripts/send_fax.lua index d717f93..ded8c65 100644 --- a/misc/freeswitch/scripts/send_fax.lua +++ b/misc/freeswitch/scripts/send_fax.lua @@ -173,6 +173,12 @@ if session and session:answered() then cause = session:hangupCause(); log:info('FAX_SEND - end - fax_document=', fax_document.id, ', success: ', fax_state.state, ', cause: ', cause, ', result: ', fax_state.result_code, ' ', session:getVariable('fax_result_text')); + + local command = 'http_request.lua sendfax http://127.0.0.1/trigger/fax_has_been_sent/' .. tostring(fax_document.id); + + require 'common.fapi' + common.fapi.FApi:new():execute('luarun', command); + else if session then cause = session:hangupCause(); diff --git a/misc/freeswitch/scripts/test_route.lua b/misc/freeswitch/scripts/test_route.lua new file mode 100644 index 0000000..98dfda9 --- /dev/null +++ b/misc/freeswitch/scripts/test_route.lua @@ -0,0 +1,84 @@ +-- Gemeinschaft 5 routing test module +-- (c) AMOOMA GmbH 2013 +-- + +require 'common.array'; + +local arguments = {}; +local value = nil; + +for index=1, #argv do + if math.mod(index, 2) == 0 then + common.array.set(arguments, argv[index], value); + else + value = argv[index]; + end +end + +local caller = arguments.caller or {}; +local channel_variables = arguments.chv or {}; + +function caller.to_s(variable) + return common.str.to_s(arguments[variable]) +end + +local log_buffer = {}; +-- initialize logging +require 'common.log'; +log = common.log.Log:new{ buffer = log_buffer, prefix = '' }; + +-- connect to database +require 'common.database'; +local database = common.database.Database:new{ log = log }:connect(); +if not database:connected() then + log:critical('TEST_ROUTE - database connection failed'); + return; +end + +-- dialplan object +require 'dialplan.dialplan' +local dialplan_object = dialplan.dialplan.Dialplan:new{ log = log, caller = caller, database = database }; +dialplan_object:configuration_read(); +caller.dialplan = dialplan_object; +caller.local_node_id = dialplan_object.node_id; + +dialplan_object:retrieve_caller_data(); +local destination = arguments.destination or dialplan_object:destination_new{ number = caller.destination_number }; +local routes = {}; + +if destination and destination.type == 'unknown' then + local clip_no_screening = common.array.try(caller, 'account.record.clip_no_screening'); + caller.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.caller_id_numbers, number); + end + end + if caller.caller_phone_numbers then + for index, number in ipairs(caller.caller_phone_numbers) do + table.insert(caller.caller_id_numbers, number); + end + end + log:info('CALLER_ID_NUMBERS - clir: ', caller.clir, ', numbers: ', table.concat(caller.caller_id_numbers, ',')); + + destination.callee_id_number = destination.number; + destination.callee_id_name = nil; +end + +caller.destination = destination; + +require 'dialplan.router'; +routes = dialplan.router.Router:new{ log = log, database = database, caller = caller, variables = caller, log_details = true }:route_run(arguments.table or 'outbound'); + +local result = { + routes = routes, + destination = destination, + log = log_buffer +} + +stream:write(common.array.to_json(result)); + +-- release database handle +if database then + database:release(); +end |