diff options
Diffstat (limited to 'misc/freeswitch/scripts')
57 files changed, 8026 insertions, 0 deletions
diff --git a/misc/freeswitch/scripts/acd_wait.lua b/misc/freeswitch/scripts/acd_wait.lua new file mode 100644 index 0000000..fd16bea --- /dev/null +++ b/misc/freeswitch/scripts/acd_wait.lua @@ -0,0 +1,45 @@ +-- Gemeinschaft 5: acd call handler +-- (c) AMOOMA GmbH 2012 +-- + +local caller_uuid = argv[1]; +local acd_id = tonumber(argv[2]); +local timeout = tonumber(argv[3]); +local retry_timeout = tonumber(argv[4]); +local acd_caller_id = tonumber(argv[5]); + +-- initialize logging +require 'common.log' +local log = common.log.Log:new{ prefix = '### [' .. caller_uuid .. '] ' }; + +if not acd_id then + log:error('ACD_WAIT - automaticcalldistributor=', acd_id, ' not specified'); + return; +end + +-- connect to database +require 'common.database' +local database = common.database.Database:new{ log = log }:connect(); +if not database:connected() then + log:critical('ACD_WAIT - database connect failed'); + database:release(); + return; +end + +require 'dialplan.acd' +local acd = dialplan.acd.AutomaticCallDistributor:new{ log = log, database = database }:find_by_id(acd_id); + +if not acd then + log:error('ACD_WAIT - automaticcalldistributor=', acd_id, ' not found'); + database:release(); + return; +end + +log:debug('ACD_WAIT ', acd_id, ' - start'); +acd:wait_turn(caller_uuid, acd_caller_id, timeout, retry_timeout); +log:debug('ACD_WAIT ', acd_id, ' - end'); + +-- release database +if database then + database:release(); +end diff --git a/misc/freeswitch/scripts/common/call_forwarding.lua b/misc/freeswitch/scripts/common/call_forwarding.lua new file mode 100644 index 0000000..3942d05 --- /dev/null +++ b/misc/freeswitch/scripts/common/call_forwarding.lua @@ -0,0 +1,47 @@ +-- Gemeinschaft 5 module: call forwarding class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +CallForwarding = {} + +-- Create CallForwarding object +function CallForwarding.new(self, arg, object) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + self.domain = arg.domain; + return object; +end + +-- Find call forwarding by id +function CallForwarding.find_by_id(self, id) + local sql_query = 'SELECT * FROM `call_forwards` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + local record = nil + + self.database:query(sql_query, function(entry) + record = entry; + end) + + if record then + call_forwarding = CallForwarding:new(self) + call_forwarding.record = record + return call_forwarding + end + + return nil +end + +function CallForwarding.presence_set(self, presence_state) + require 'dialplan.presence' + local presence = dialplan.presence.Presence:new(); + + presence:init{log = self.log, accounts = { 'f-cftg-' .. tostring(self.record.id) }, domain = self.domain, uuid = 'call_forwarding_' .. tostring(self.record.id)}; + + return presence:set(presence_state); +end diff --git a/misc/freeswitch/scripts/common/call_history.lua b/misc/freeswitch/scripts/common/call_history.lua new file mode 100644 index 0000000..c5bc0bf --- /dev/null +++ b/misc/freeswitch/scripts/common/call_history.lua @@ -0,0 +1,140 @@ +-- Gemeinschaft 5 module: call_history class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + + +function camelize_type(account_type) + ACCOUNT_TYPES = { + sipaccount = 'SipAccount', + conference = 'Conference', + faxaccount = 'FaxAccount', + callthrough = 'Callthrough', + huntgroup = 'HuntGroup', + automaticcalldistributor = 'AutomaticCallDistributor', + } + + return ACCOUNT_TYPES[account_type] or account_type; +end + + +CallHistory = {} + +-- Create CallHistory object +function CallHistory.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'callhistory'; + self.log = arg.log; + self.database = arg.database; + return object; +end + + +function CallHistory.insert_entry(self, call_history) + local keys = {} + local values = {} + + call_history.created_at = 'NOW()'; + call_history.updated_at = 'NOW()'; + + for key, value in pairs(call_history) do + table.insert(keys, key); + table.insert(values, value); + end + + local sql_query = 'INSERT INTO `call_histories` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')'; + local result = self.database:query(sql_query); + if not result then + self.log:error('[', call_history.caller_channel_uuid, '] CALL_HISTORY_SAVE - SQL: ', sql_query); + end + return result; +end + + +function CallHistory.insert_event(self, uuid, account_type, account_id, entry_type, event) + require 'common.str' + local call_history = {} + + call_history.entry_type = common.str.to_sql(entry_type); + call_history.call_historyable_type = common.str.to_sql(camelize_type(account_type)); + call_history.call_historyable_id = common.str.to_sql(account_id); + call_history.caller_channel_uuid = common.str.to_sql(uuid); + call_history.duration = common.str.to_sql(event:getHeader('variable_billsec')); + call_history.caller_id_number = common.str.to_sql(event:getHeader('variable_effective_caller_id_number')); + call_history.caller_id_name = common.str.to_sql(event:getHeader('variable_effective_caller_id_name')); + call_history.callee_id_number = common.str.to_sql(event:getHeader('variable_effective_callee_id_number')); + call_history.callee_id_name = common.str.to_sql(event:getHeader('variable_effective_callee_id_name')); + call_history.result = common.str.to_sql(event:getHeader('variable_hangup_cause')); + call_history.start_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Created-Time')) / 1000000) .. ')'; + call_history.caller_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_caller_account_type') or event:getHeader('variable_gs_account_type'))); + call_history.caller_account_id = common.str.to_sql(event:getHeader('variable_gs_caller_account_id') or event:getHeader('variable_gs_account_id')); + call_history.auth_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_auth_account_type'))); + call_history.auth_account_id = common.str.to_sql(event:getHeader('variable_gs_auth_account_id')); + call_history.callee_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_destination_type'))); + call_history.callee_account_id = common.str.to_sql(event:getHeader('variable_gs_destination_id')); + call_history.destination_number = common.str.to_sql(event:getHeader('variable_gs_destination_number')); + call_history.forwarding_service = common.str.to_sql(event:getHeader('variable_gs_forwarding_service')); + + if common.str.to_s(event:getHeader('variable_gs_call_service')) == 'pickup' then + call_history.forwarding_service = common.str.to_sql('pickup'); + end + + self.log:info('[', uuid,'] CALL_HISTORY_SAVE ', entry_type,' - account: ', account_type, '=', account_id, + ', caller: ', call_history.caller_id_number, ' ', call_history.caller_id_name, + ', callee: ', call_history.callee_id_number, ' ', call_history.callee_id_name, + ', result: ', call_history.result + ); + + return self:insert_entry(call_history); +end + + +function CallHistory.insert_forwarded(self, uuid, account_type, account_id, caller, destination, result) + require 'common.str' + + local call_history = {} + + call_history.entry_type = common.str.to_sql('forwarded'); + call_history.call_historyable_type = common.str.to_sql(camelize_type(account_type)); + call_history.call_historyable_id = common.str.to_sql(account_id); + call_history.caller_channel_uuid = common.str.to_sql(uuid); + + call_history.duration = common.str.to_sql(caller:to_i('billsec')); + call_history.caller_id_number = common.str.to_sql(caller.caller_id_number); + call_history.caller_id_name = common.str.to_sql(caller.caller_id_name); + call_history.callee_id_number = common.str.to_sql(caller.callee_id_number); + call_history.callee_id_name = common.str.to_sql(caller.callee_id_name); + call_history.result = common.str.to_sql(result.cause or 'UNSPECIFIED'); + call_history.start_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('created_time') / 1000000) .. ')'; + + if caller.account then + call_history.caller_account_type = common.str.to_sql(camelize_type(caller.account.class)); + call_history.caller_account_id = common.str.to_sql(caller.account.id); + end + + if caller.auth_account then + call_history.auth_account_type = common.str.to_sql(camelize_type(caller.auth_account.class)); + call_history.auth_account_id = common.str.to_sql(caller.auth_account.id); + end + + if destination then + call_history.callee_account_type = common.str.to_sql(camelize_type(destination.type)); + call_history.callee_account_id = common.str.to_sql(destination.id); + call_history.destination_number = common.str.to_sql(destination.number); + end + + call_history.forwarding_service = common.str.to_sql(caller.forwarding_service); + + self.log:info('CALL_HISTORY_SAVE forwarded - account: ', account_type, '=', account_id, + ', service: ', call_history.forwarding_service, + ', caller: ', call_history.caller_id_number, ' ', call_history.caller_id_name, + ', callee: ', call_history.callee_id_number, ' ', call_history.callee_id_name, + ', result: ', call_history.result + ); + + return self:insert_entry(call_history); +end diff --git a/misc/freeswitch/scripts/common/conference.lua b/misc/freeswitch/scripts/common/conference.lua new file mode 100644 index 0000000..d2bf829 --- /dev/null +++ b/misc/freeswitch/scripts/common/conference.lua @@ -0,0 +1,239 @@ +-- Gemeinschaft 5 module: conference class +-- (c) AMOOMA GmbH 2012 +-- + +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 {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'conference'; + 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.find_by_id(self, id) + local sql_query = 'SELECT * 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.uuid = conference_entry.uuid; + conference.max_members = tonumber(conference.record.max_members) or MEMBERS_MAX; + 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 + 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 invitee = nil; + + self.database:query(sql_query, function(conference_entry) + invitee = conference_entry; + end) + + return invitee; +end + +function Conference.count(self) + return tonumber(self.caller:result('conference ' .. self.record.id .. ' 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; + + 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"); + + -- 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 }; + + 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)); + + 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 + + -- Check if conference is within time frame + if self.record.start and 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 + end + + require 'common.str' + -- 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 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 + end + + if not pin and self.record.pin then + pin = self.record.pin + end + + caller:answer(); + caller:sleep(1000); + caller.session:streamFile('conference/conf-welcome.wav'); + + if pin and pin ~= "" then + local digits = ""; + for i = 1, 3, 1 do + if digits == pin then + break + elseif digits ~= "" then + caller.session:streamFile('conference/conf-bad-pin.wav'); + end + digits = caller.session:read(PIN_LENGTH_MIN, PIN_LENGTH_MAX, 'conference/conf-enter_conf_pin.wav', PIN_TIMEOUT, '#'); + end + if digits ~= pin then + caller.session:streamFile("conference/conf-goodbye.wav"); + return "CALL_REJECTED"; + end + 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(); + end + + -- Enter the conference + local name_file = 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 = "/tmp/conference_caller_name_" .. uid .. ".wav"; + caller.session:streamFile("voicemail/vm-record_name1.wav"); + caller.session:execute("playback", "tone_stream://%(1000,0,500)"); + session:recordFile(name_file, ANNOUNCEMENT_MAX_LEN, ANNOUNCEMENT_SILENCE_THRESHOLD, ANNOUNCEMENT_SILENCE_LEN); + caller.session:streamFile(name_file); + 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, "#"); + 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)); + caller.session:streamFile("conference/conf-goodbye.wav") + + -- 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); + 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(); + end + + cause = "NORMAL_CLEARING"; + caller.session:hangup(cause); + return cause; +end diff --git a/misc/freeswitch/scripts/common/configuration_file.lua b/misc/freeswitch/scripts/common/configuration_file.lua new file mode 100644 index 0000000..67e1f3b --- /dev/null +++ b/misc/freeswitch/scripts/common/configuration_file.lua @@ -0,0 +1,70 @@ +-- Gemeinschaft 5 module: configuration file +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +function ignore_comments(line) + return line:gsub(';+([^;]*)', function(entry) + return ''; + end); +end + +-- parse configuration +function parse(lines, filter_section_name) + require 'common.str' + local section = {} + local root = { [true] = section } + + for line in lines do + if line then + local ignore_line = false; + line = ignore_comments(line); + + line:gsub('^%s*%[(.-)%]%s*$', function(section_name) + if tostring(section_name):match('%=false$') then + section = {} + else + root[common.str.strip(section_name)] = {}; + section = root[common.str.strip(section_name)]; + end + ignore_line = true; + end); + + if not ignore_line then + key, value = common.str.partition(line, '='); + if value and key and not common.str.strip(key):match('%s') then + section[common.str.strip(key)] = common.str.strip(value); + else + line = common.str.strip(line); + if not common.str.blank(line) then + if line:match(',') then + table.insert(section, common.str.strip_to_a(line, ',')); + else + table.insert(section, line); + end + end + end + end + end + end + + if filter_section_name == false then + root[true] = nil; + elseif filter_section_name then + return root[filter_section_name]; + end + + return root; +end + +-- retrieve configuration from file +function get(file_name, filter_section_name) + local file = io.open(file_name); + + if file then + local result = parse(file:lines(), filter_section_name); + file:close(); + return result; + end +end diff --git a/misc/freeswitch/scripts/common/database.lua b/misc/freeswitch/scripts/common/database.lua new file mode 100644 index 0000000..3692f84 --- /dev/null +++ b/misc/freeswitch/scripts/common/database.lua @@ -0,0 +1,151 @@ +-- Gemeinschaft 5 module: database class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Database = {} + +DATABASE_DRIVER = 'mysql' + +function Database.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'database'; + self.log = arg.log; + self.conn = nil; + return object; +end + + +function Database.connect(self, database_name, user_name, password, host_name) + local database_driver = nil; + if not (database_name and user_name and password) then + require 'common.configuration_file' + local config = common.configuration_file.get('/opt/freeswitch/scripts/ini/database.ini'); + if config then + database_driver = config[true].driver + database_name = config[database_driver].database + user_name = config[database_driver].user + password = config[database_driver].password + host_name = config[database_driver].host + end + end + + host_name = host_name or 'localhost'; + database_driver = database_driver or DATABASE_DRIVER; + + if database_driver == 'mysql' then + require "luasql.mysql" + self.env = luasql.mysql(); + elseif database_driver == 'odbc' then + require "luasql.odbc" + self.env = luasql.odbc(); + end + + self.conn = self.env:connect(database_name, user_name, password, host_name); + self.conn_id = tostring(self.conn); + self.database_name = database_name; + self.user_name = user_name; + self.password = password; + self.host_name = host_name; + + -- self.log:debug('DATABASE_CONNECT - connection: ', self.conn_id, ', environment: ', self.env); + + return self; +end + + +function Database.reconnect(self) + self.conn = self.env:connect(self.database_name, self.user_name, self.password, self.host_name); + self.conn_id = tostring(self.conn); + + if self.log then + self.log:info('DATABASE_RECONNECT - connection: ', self.conn_id, ', environment: ', self.env); + end + + return self; +end + + +function Database.connected(self) + return self.conn; +end + + +function Database.query(self, sql_query, call_function) + local cursor = self.conn:execute(sql_query); + + if cursor == nil and not self.conn:execute('SELECT @@VERSION') then + if self.log then + self.log:error('DATABASE_QUERY - lost connection: ', self.conn_id, ', environment: ', self.env, ', query: ', sql_query); + end + self:reconnect(); + + if call_function then + cursor = self.conn:execute(sql_query); + self.log:notice('DATABASE_QUERY - retry: ', sql_query); + end + end + + if cursor and call_function then + repeat + row = cursor:fetch({}, 'a'); + if row then + call_function(row); + end + until not row; + end + + if type(cursor) == 'userdata' then + cursor:close(); + end + + return cursor; +end + + +function Database.query_return_value(self, sql_query) + local cursor = self.conn:execute(sql_query); + + if cursor == nil and not self.conn:execute('SELECT @@VERSION') then + if self.log then + self.log:error('DATABASE_QUERY - lost connection: ', self.conn_id, ', environment: ', self.env, ', query: ', sql_query); + end + self:reconnect(); + cursor = self.conn:execute(sql_query); + self.log:notice('DATABASE_QUERY - retry: ', sql_query); + end + + if type(cursor) == 'userdata' then + local row = cursor:fetch({}, 'n'); + cursor:close(); + + if not row then + return row; + else + return row[1]; + end + end + + return cursor; +end + + +function Database.last_insert_id(self) + return self:query_return_value('SELECT LAST_INSERT_ID()'); +end + + +function Database.release(self, sql_query, call_function) + if self.conn then + self.conn:close(); + end + if self.env then + self.env:close(); + end + + -- self.log:debug('DATABASE_RELEASE - connection: ', self.conn_id, ', status: ', self.env, ', ', self.conn); +end diff --git a/misc/freeswitch/scripts/common/fapi.lua b/misc/freeswitch/scripts/common/fapi.lua new file mode 100644 index 0000000..0a05155 --- /dev/null +++ b/misc/freeswitch/scripts/common/fapi.lua @@ -0,0 +1,80 @@ +-- Gemeinschaft 5 module: FS api class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +FApi = {} + +-- create fapi object +function FApi.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'fapi'; + self.log = arg.log; + self.uuid = arg.uuid; + self.fs_api = freeswitch.API(); + return object; +end + + +function FApi.return_result(self, result, positive, negative, unspecified) + if not result then + return negative; + end + result = tostring(result); + + if result:match('^-ERR') then + return negative; + elseif result:match('^_undef_') then + return negative; + elseif result:match('^+OK') then + return positive; + else + return unspecified; + end +end + + +function FApi.sleep(self, value) + freeswitch.msleep(value); +end + + +function FApi.channel_exists(self, uuid) + require 'common.str' + uuid = uuid or self.uuid; + return common.str.to_b(freeswitch.API():execute('uuid_exists', tostring(uuid))); +end + + +function FApi.get_variable(self, variable_name) + local result = freeswitch.API():execute('uuid_getvar', tostring(self.uuid) .. ' ' .. tostring(variable_name)); + return self:return_result(result, result, nil, result); +end + + +function FApi.set_variable(self, variable_name, value) + value = value or ''; + + local result = freeswitch.API():execute('uuid_setvar', tostring(self.uuid) .. ' ' .. tostring(variable_name) .. ' ' .. tostring(value)); + return self:return_result(result, true); +end + + +function FApi.continue(self) + local result = freeswitch.API():execute('break', tostring(self.uuid)); + return self:return_result(result, true, false); +end + +function FApi.create_uuid(self, uuid) + local result = self.fs_api:execute('create_uuid', uuid); + return result; +end + +function FApi.execute(self, function_name, function_parameters) + local result = self.fs_api:execute(function_name, function_parameters); + return self:return_result(result, true); +end diff --git a/misc/freeswitch/scripts/common/ipcalc.lua b/misc/freeswitch/scripts/common/ipcalc.lua new file mode 100644 index 0000000..5c19d20 --- /dev/null +++ b/misc/freeswitch/scripts/common/ipcalc.lua @@ -0,0 +1,27 @@ +-- Gemeinschaft 5 module: ip calculation functions +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +function ipv4_to_i(ip_address_str) + local octet4, octet3, octet2, octet1 = ip_address_str:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)'); + if octet4 and octet3 and octet2 and octet1 then + return (2^24*octet4 + 2^16*octet3 + 2^8*octet2 + octet1); + end +end + +function ipv4_to_network_netmask(ip_address_str) + local octet4, octet3, octet2, octet1, netmask = ip_address_str:match('(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)/(%d%d?)'); + if octet4 and octet3 and octet2 and octet1 and netmask then + return (2^24*octet4 + 2^16*octet3 + 2^8*octet2 + octet1), tonumber(netmask); + end +end + +function ipv4_network(ip_address, netmask) + return math.floor(ip_address / 2^(32-netmask)); +end + +function ipv4_in_network(ip_address, network, netmask) + return ipv4_network(ip_address, netmask) == ipv4_network(network, netmask); +end diff --git a/misc/freeswitch/scripts/common/log.lua b/misc/freeswitch/scripts/common/log.lua new file mode 100644 index 0000000..d0d13dc --- /dev/null +++ b/misc/freeswitch/scripts/common/log.lua @@ -0,0 +1,69 @@ +-- Gemeinschaft 5 module: log +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Log = {} + +-- Create logger object +function Log.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.prefix = arg.prefix or '### '; + + self.level_console = arg.level_console or 0; + self.level_alert = arg.level_alert or 1; + self.level_critical = arg.level_critical or 2; + self.level_error = arg.level_error or 3; + self.level_warning = arg.level_warning or 4; + self.level_notice = arg.level_notice or 5; + self.level_info = arg.level_info or 6; + self.level_debug = arg.level_debug or 7; + + return object; +end + +function Log.message(self, log_level, message_arguments ) + local message = tostring(self.prefix); + for index, value in pairs(message_arguments) do + if type(index) == 'number' then + message = message .. tostring(value); + end + end + freeswitch.consoleLog(log_level, message .. '\n'); +end + +function Log.console(self, ...) + self:message(self.level_console, arg); +end + +function Log.alert(self, ...) + self:message(self.level_alert, arg); +end + +function Log.critical(self, ...) + self:message(self.level_critical, arg); +end + +function Log.error(self, ...) + self:message(self.level_error, arg); +end + +function Log.warning(self, ...) + self:message(self.level_warning, arg); +end + +function Log.notice(self, ...) + self:message(self.level_notice, arg); +end + +function Log.info(self, ...) + self:message(self.level_info, arg); +end + +function Log.debug(self, ...) + self:message(self.level_debug, arg); +end diff --git a/misc/freeswitch/scripts/common/node.lua b/misc/freeswitch/scripts/common/node.lua new file mode 100644 index 0000000..544ede9 --- /dev/null +++ b/misc/freeswitch/scripts/common/node.lua @@ -0,0 +1,73 @@ +-- CommonModule: Node +-- +module(...,package.seeall) + +Node = {} + +-- Create Node object +function Node.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log + self.database = arg.database + self.record = arg.record + self.session = arg.session + return object +end + +-- Find Node account by name +function Node.find_by_id(self, node_id) + + if not tonumber(node_id) then + return nil + end + + local sql_query = 'SELECT * FROM `gs_nodes` WHERE `id`= ' .. node_id .. ' LIMIT 1'; + local record = nil + + self.database:query(sql_query, function(node_entry) + record = node_entry + end) + + if record then + local node_object = Node:new(self); + node_object.record = record + + return node_object + end + + return nil +end + +-- Find Node account by name +function Node.find_by_address(self, address) + local sql_query = 'SELECT * FROM `gs_nodes` WHERE `ip_address`= "' .. tostring(address):gsub('[^A-F0-9%.%:]', '') .. '" LIMIT 1'; + local record = nil + + self.database:query(sql_query, function(node_entry) + record = node_entry + end) + + if record then + local node_object = Node:new(self); + node_object.record = record + + return node_object + end + + return nil +end + +-- List Nodes +function Node.all(self) + local sql_query = 'SELECT * FROM `gs_nodes`'; + nodes = {}; + + self.database:query(sql_query, function(node_entry) + nodes[tonumber(node_entry.id)] = node_entry; + end) + + return nodes +end
\ No newline at end of file diff --git a/misc/freeswitch/scripts/common/phone_number.lua b/misc/freeswitch/scripts/common/phone_number.lua new file mode 100644 index 0000000..f4f4bfe --- /dev/null +++ b/misc/freeswitch/scripts/common/phone_number.lua @@ -0,0 +1,359 @@ +-- Gemeinschaft 5 module: phone number class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +PhoneNumber = {} + +PHONE_NUMBER_INTERNAL_TYPES = { 'SipAccount', 'Conference', 'FaxAccount', 'Callthrough', 'HuntGroup', 'AutomaticCallDistributor' } + +-- create phone number object +function PhoneNumber.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'phonenumber'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + self.domain = arg.domain; + self.DEFAULT_CALL_FORWARDING_DEPTH = 20; + return object; +end + +-- find phone number by id +function PhoneNumber.find_by_id(self, id) + local sql_query = 'SELECT * FROM `phone_numbers` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + + local phone_number = nil; + + self.database:query(sql_query, function(number_entry) + phone_number = PhoneNumber:new(self); + phone_number.record = number_entry; + phone_number.id = tonumber(number_entry.id); + phone_number.uuid = number_entry.uuid; + end) + + return phone_number; +end + +-- find phone number by number +function PhoneNumber.find_by_number(self, number, phone_numberable_types) + require 'common.str' + + phone_numberable_types = phone_numberable_types or PHONE_NUMBER_INTERNAL_TYPES + + local sql_query = 'SELECT * FROM `phone_numbers` \ + WHERE `number`= ' .. common.str.to_sql(number) .. ' \ + AND `phone_numberable_type` IN ("' .. table.concat(phone_numberable_types, '","') .. '") \ + AND `state` = "active" LIMIT 1'; + + local phone_number = nil; + + self.database:query(sql_query, function(number_entry) + phone_number = PhoneNumber:new(self); + phone_number.record = number_entry; + end) + + return phone_number; +end + +-- Find numbers by owner id and type +function PhoneNumber.find_all_by_owner(self, owner_id, owner_type) + local sql_query = 'SELECT * FROM `phone_numbers` WHERE `phone_numberable_type`="' .. owner_type .. '" AND `phone_numberable_id`= ' .. tonumber(owner_id) ..' ORDER BY `position`'; + local phone_numbers = {} + + self.database:query(sql_query, function(number_entry) + phone_numbers[tonumber(number_entry.id)] = PhoneNumber:new(self); + phone_numbers[tonumber(number_entry.id)].record = number_entry; + end) + + return phone_numbers; +end + +-- List numbers by owner id and type +function PhoneNumber.list_by_owner(self, owner_id, owner_type) + local sql_query = 'SELECT * FROM `phone_numbers` WHERE `phone_numberable_type`="' .. owner_type .. '" AND `phone_numberable_id`= ' .. tonumber(owner_id) ..' ORDER BY `position`'; + local phone_numbers = {} + + self.database:query(sql_query, function(number_entry) + table.insert(phone_numbers, number_entry.number) + end) + + return phone_numbers; +end + +-- List numbers by same owner +function PhoneNumber.list_by_same_owner(self, number, owner_types) + local phone_number = self:find_by_number(number, owner_types) + + if phone_number then + return self:list_by_owner(phone_number.record.phone_numberable_id, phone_number.record.phone_numberable_type); + end +end + +-- Retrieve call forwarding +function PhoneNumber.call_forwarding(self, sources) + require 'common.str' + + sources = sources or {}; + table.insert(sources, ''); + + local sql_query = 'SELECT \ + `a`.`destination` AS `number`, \ + `a`.`call_forwardable_id` AS `id`, \ + `a`.`call_forwardable_type` AS `type`, \ + `a`.`timeout`, `a`.`depth`, \ + `b`.`value` AS `service` \ + FROM `call_forwards` `a` JOIN `call_forward_cases` `b` ON `a`.`call_forward_case_id` = `b`.`id` \ + WHERE `a`.`phone_number_id`= ' .. tonumber(self.record.id) .. ' \ + AND `a`.`active` IS TRUE \ + AND (`a`.`source` IS NULL OR `a`.`source` IN ("' .. table.concat( sources, '","') .. '"))'; + + local call_forwarding = {} + + self.database:query(sql_query, function(forwarding_entry) + call_forwarding[forwarding_entry.service] = forwarding_entry; + self.log:debug('CALL_FORWARDING_GET - PhoneNumber=', self.record.id, '/', self.record.uuid, '@', self.record.gs_node_id, + ', number: ', self.record.number, + ', service: ', forwarding_entry.service, + ', destination: ',forwarding_entry.type, '=', forwarding_entry.id, + ', number: ', forwarding_entry.number); + end) + + return call_forwarding; +end + + +function PhoneNumber.call_forwarding_effective(self, service, source) + local conditions = {} + table.insert(conditions, '`phone_number_id` = ' .. self.record.id); + + if source then + table.insert(conditions, '`source` = "' .. source); + else + table.insert(conditions, '(`source` = "" OR `source` IS NULL)'); + end + + if service then + table.insert(conditions, '`call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '")'); + end + + -- get call forwarding entry + local sql_query = 'SELECT `destination`,`active`,`timeout`,`call_forwardable_type`, `call_forwardable_id` FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND ') .. ' ORDER BY `active` DESC LIMIT 1'; + local call_forwarding = nil; + + self.database:query(sql_query, function(entry) + call_forwarding = entry; + end) + + return call_forwarding; +end + + +function PhoneNumber.call_forwarding_off(self, service, source, delete) + local conditions = {} + table.insert(conditions, '`phone_number_id` = ' .. self.record.id); + + if source then + table.insert(conditions, '`source` = "' .. source); + else + table.insert(conditions, '(`source` = "" OR `source` IS NULL)'); + end + + if service then + table.insert(conditions, '`call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '")'); + end + + self.log:info('PHONE_NUMBER_CALL_FORWARDING_OFF - service: ', service, ', number: ', self.record.number); + + local call_forwarding_ids = {} + + local sql_query = 'SELECT `id` FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND '); + self.database:query(sql_query, function(record) + table.insert(call_forwarding_ids, record.id); + end) + + require 'common.call_forwarding' + local call_forwarding_class = common.call_forwarding.CallForwarding:new{ log = self.log, database = self.database, domain = self.domain }; + + for index, call_forwarding_id in ipairs(call_forwarding_ids) do + if tonumber(call_forwarding_id) then + local call_forwarding = call_forwarding_class:find_by_id(call_forwarding_id); + call_forwarding:presence_set('terminated'); + end + end + + -- set call forwarding entry inactive + local sql_query = 'UPDATE `call_forwards` SET `active` = FALSE, `updated_at` = NOW() WHERE ' .. table.concat(conditions, ' AND '); + + local call_forwards = {}; + + -- or delete call forwarding entry + if delete then + sql_query = 'SELECT * FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND '); + self.database:query(sql_query, function(forwarding_entry) + table.insert(call_forwards, forwarding_entry) + end) + sql_query = 'DELETE FROM `call_forwards` WHERE ' .. table.concat(conditions, ' AND '); + end + + if not self.database:query(sql_query) then + self.log:notice('PHONE_NUMBER_CALL_FORWARDING_OFF - call forwarding could not be deactivated - number: ', self.record.number); + return false; + end + + if delete then + require 'common.sync_log' + local sync_log_class = common.sync_log.SyncLog:new{ log = self.log, database = self.database, homebase_ip_address = '' } + + for index, call_forward in ipairs(call_forwards) do + sync_log_class:insert('CallForward', call_forward, 'destroy', nil); + end + end + + return true; +end + + +function PhoneNumber.call_forwarding_on(self, service, destination, destination_type, timeout, source) + require 'common.str' + if call_forwarding_service == 'noanswer' then + timeout = tonumber(timeout) or '30'; + else + timeout = 'NULL'; + end + + if source then + sql_query = 'SELECT `id`, `destination`, `call_forwardable_type`, `call_forward_case_id` FROM `call_forwards` \ + WHERE `phone_number_id` = ' .. self.record.id .. ' \ + AND `call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '") \ + AND `source` = "' .. source .. '" AND `phone_number_id` = ' .. self.record.id .. ' ORDER BY `active` DESC LIMIT 1'; + else + sql_query = 'SELECT `id`, `destination`, `call_forwardable_type`, `call_forward_case_id` FROM `call_forwards` \ + WHERE `phone_number_id` = ' .. self.record.id .. ' \ + AND `call_forward_case_id` IN (SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '") \ + AND (`source` = "" OR `source` IS NULL) AND `phone_number_id` = ' .. self.record.id .. ' ORDER BY `active` DESC LIMIT 1'; + end + + destination_type = destination_type or ''; + destination = destination or ''; + local service_id = nil; + local entry_id = 'NULL'; + + self.database:query(sql_query, function(record) + entry_id = record.id; + service_id = record.call_forward_case_id; + if common.str.blank(destination) then + if not common.str.blank(record.call_forwardable_type) then + destination_type = common.str.downcase(record.call_forwardable_type); + end + if not common.str.blank(record.destination) then + destination = record.destination; + end + end + end) + + if destination == '' and destination_type:lower() ~= 'voicemail' then + self.log:notice('PHONE_NUMBER_CALL_FORWARDING_ON - destination not specified - destination: ', destination, ', type: ', destination_type,', number: ' .. self.record.number); + return false; + end + + if destination_type == '' then + destination_type = 'PhoneNumber'; + end + + self.log:info('PHONE_NUMBER_CALL_FORWARDING_ON - service: ', service, ', number: ', self.record.number, ', destination: ', destination, ', type: ', destination_type, ', timeout: ', timeout); + + if not service_id then + sql_query = 'SELECT `id` FROM `call_forward_cases` WHERE `value` = "' .. service .. '"'; + self.database:query(sql_query, function(record) + service_id = tonumber(record.id); + end); + end + + sql_query = 'REPLACE INTO `call_forwards` \ + (`active`, `uuid`, `depth`, `updated_at`, `id`, `phone_number_id`, `call_forward_case_id`, `destination`, `call_forwardable_type`, `timeout`) \ + VALUES \ + (TRUE, UUID(), ' .. self.DEFAULT_CALL_FORWARDING_DEPTH .. ', NOW(), ' .. entry_id .. ', ' .. self.record.id .. ', ' .. service_id .. ', "' .. destination .. '", "' .. destination_type .. '", ' .. timeout .. ')' + + if not self.database:query(sql_query) then + self.log:error('PHONE_NUMBER_CALL_FORWARDING_ON - could not be activated - destination: ', destination, ', type: ', destination_type,', number: ' .. self.record.number); + return false; + end + + require 'common.call_forwarding' + local call_forwarding_class = common.call_forwarding.CallForwarding:new{ log = self.log, database = self.database, domain = self.domain }; + if tonumber(entry_id) then + local call_forwarding = call_forwarding_class:find_by_id(entry_id); + end + + if call_forwarding then + if destination_type:lower() == 'voicemail' then + call_forwarding:presence_set('early'); + else + call_forwarding:presence_set('confirmed'); + end + end + + return true; +end + + +function PhoneNumber.call_forwarding_toggle(self, service, source) + local call_forwarding = self:call_forwarding_effective(service, source); + + -- no call_forwarding entry: all forwarding is deactivated + if not call_forwarding then + return false; + end + + if tostring(call_forwarding.active) == '1' then + if self:call_forwarding_off(service, source) then + return {destination = call_forwarding.destination, destination_type = call_forwarding.destination_type, active = false}; + end + end + + if self:call_forwarding_on(service, call_forwarding.destination, call_forwarding.destination_type, call_forwarding.timeout, source) then + return {destination = call_forwarding.destination, destination_type = call_forwarding.destination_type, active = true}; + end + + return nil; +end + + +function PhoneNumber.call_forwarding_presence_set(self, presence_state, service) + service = service or 'always'; + local dialplan_function = 'f-cfutg'; + + if service == 'assistant' then + dialplan_function = 'f-cfatg'; + end + + require "dialplan.presence" + local presence = dialplan.presence.Presence:new(); + + presence:init{log = self.log, accounts = { dialplan_function .. '-' .. tostring(self.record.id) }, domain = self.domain, uuid = 'call_forwarding_number_' .. tostring(self.record.id)}; + + return presence:set(presence_state); +end + + +-- Retrieve ringtone +function PhoneNumber.ringtone(self, id) + id = id or self.record.id; + if not id then + return false; + end + + local sql_query = "SELECT * FROM `ringtones` WHERE `ringtoneable_type` = \"PhoneNumber\" AND `ringtoneable_id`=" .. self.record.id .. " LIMIT 1"; + local ringtone = nil; + + self.database:query(sql_query, function(entry) + ringtone = entry; + end) + + return ringtone; +end diff --git a/misc/freeswitch/scripts/common/routing_tables.lua b/misc/freeswitch/scripts/common/routing_tables.lua new file mode 100644 index 0000000..34d0143 --- /dev/null +++ b/misc/freeswitch/scripts/common/routing_tables.lua @@ -0,0 +1,66 @@ +-- Gemeinschaft 5 module: routing table functions +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +function expand_variables(line, variables_list) + variables_list = variables_list or {}; + + return (line:gsub('{([%a%d_]+)}', function(captured) + return variables_list[captured] or ''; + end)) +end + + +function match_route(entry, search_str, variables_list) + if not entry or not search_str then + return { error = 'No input values' }; + end + + local result = nil; + local success = nil; + success, result = pcall(string.find, search_str, entry[1]); + + if not success then + return { error = result, line = line } + elseif result then + local route = { + pattern = entry[1], + value = search_str:gsub(entry[1], expand_variables(entry[#entry], variables_list)), + } + + for index = 2, #entry-1 do + local attribute = entry[index]:match('^(.-)%s*='); + if attribute then + route[attribute] = entry[index]:match('=%s*(.-)$'); + end + end + + return route; + end + + return {}; +end + + +function match_caller_id(entry, search_str, variables_list) + if not entry or not search_str then + return { error = 'No input values' }; + end + local result = nil; + local success = nil; + success, result = pcall(string.find, search_str, entry[1]); + if not success then + return { error = result, line = line } + elseif result then + return { + value = search_str:gsub(entry[1], expand_variables(entry[4], variables_list)), + class = entry[2], + endpoint = entry[3], + pattern = entry[1], + } + end + + return {}; +end diff --git a/misc/freeswitch/scripts/common/sip_account.lua b/misc/freeswitch/scripts/common/sip_account.lua new file mode 100644 index 0000000..28a00df --- /dev/null +++ b/misc/freeswitch/scripts/common/sip_account.lua @@ -0,0 +1,137 @@ +-- Gemeinschaft 5 module: sip account class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +SipAccount = {} + +-- Create SipAccount object +function SipAccount.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'sipaccount'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object; +end + + +function SipAccount.find_by_sql(self, where) + local sql_query = 'SELECT \ + `a`.`id`, \ + `a`.`uuid`, \ + `a`.`auth_name`, \ + `a`.`caller_name`, \ + `a`.`password`, \ + `a`.`voicemail_pin`, \ + `a`.`tenant_id`, \ + `a`.`sip_domain_id`, \ + `a`.`call_waiting`, \ + `a`.`clir`, \ + `a`.`clip`, \ + `a`.`clip_no_screening`, \ + `a`.`sip_accountable_type`, \ + `a`.`sip_accountable_id`, \ + `a`.`hotdeskable`, \ + `a`.`gs_node_id`, \ + `b`.`host` \ + FROM `sip_accounts` `a` JOIN `sip_domains` `b` ON `a`.`sip_domain_id` = `b`.`id` \ + WHERE ' .. where .. ' LIMIT 1'; + + local sip_account = nil; + self.database:query(sql_query, function(account_entry) + sip_account = SipAccount:new(self); + sip_account.record = account_entry; + sip_account.id = tonumber(account_entry.id); + sip_account.uuid = account_entry.uuid; + end) + + return sip_account; +end + + +-- find sip account by id +function SipAccount.find_by_id(self, id) + local sql_query = '`a`.`id`= ' .. tonumber(id); + return self:find_by_sql(sql_query); +end + +-- find sip account by uuid +function SipAccount.find_by_uuid(self, uuid) + local sql_query = '`a`.`uuid`= "' .. uuid .. '"'; + return self:find_by_sql(sql_query); +end + +-- Find SIP Account by auth_name +function SipAccount.find_by_auth_name(self, auth_name, domain) + local sql_query = '`a`.`auth_name`= "' .. auth_name .. '"'; + + if domain then + sql_query = sql_query .. ' AND `b`.`host` = "' .. domain .. '"'; + end + + return self:find_by_sql(sql_query); +end + +-- retrieve Phone Numbers for SIP Account +function SipAccount.phone_numbers(self, id) + id = id or self.record.id; + if not id then + return false; + end + + local sql_query = "SELECT * FROM `phone_numbers` WHERE `phone_numberable_type` = \"SipAccount\" AND `phone_numberable_id`=" .. self.record.id; + local phone_numbers = {} + + self.database:query(sql_query, function(entry) + table.insert(phone_numbers,entry.number); + end) + + return phone_numbers; +end + +-- retrieve Ringtone for SIP Account +function SipAccount.ringtone(self, id) + id = id or self.record.id; + if not id then + return false; + end + + local sql_query = "SELECT * FROM `ringtones` WHERE `ringtoneable_type` = \"SipAccount\" AND `ringtoneable_id`=" .. self.record.id .. " LIMIT 1"; + local ringtone = nil; + + self.database:query(sql_query, function(entry) + ringtone = entry; + end) + + return ringtone; +end + +function SipAccount.send_text(self, text) + local event = freeswitch.Event("NOTIFY"); + event:addHeader("profile", "gemeinschaft"); + event:addHeader("event-string", "text"); + event:addHeader("user", self.record.auth_name); + event:addHeader("host", self.record.host); + event:addHeader("content-type", "text/plain"); + event:addBody(text); + event:fire(); +end + + +function SipAccount.call_state(self) + local state = nil + local sql_query = "SELECT `callstate` FROM `channels` \ + WHERE `name` LIKE (\"\%" .. self.record.auth_name .. "@%\") \ + OR `name` LIKE (\"\%" .. self.record.auth_name .. "@%\") LIMIT 1"; + + self.database:query(sql_query, function(channel_entry) + state = channel_entry.callstate; + end) + + return state; +end diff --git a/misc/freeswitch/scripts/common/str.lua b/misc/freeswitch/scripts/common/str.lua new file mode 100644 index 0000000..b19f299 --- /dev/null +++ b/misc/freeswitch/scripts/common/str.lua @@ -0,0 +1,136 @@ +-- Gemeinschaft 5 module: string functions +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +function try(array, arguments) + local argument = arguments:match('^(.-)%.') or arguments; + local remaining_arguments = arguments:match('%.(.-)$'); + + if argument and type(array) == 'table' then + if remaining_arguments then + if type(array[argument]) == 'table' then + return try(array[argument], remaining_arguments); + else + return nil; + end + else + return array[argument]; + end + end + + return nil; +end + +-- to number +function to_n(value) + value = tostring(value):gsub('[^%d%.%+%-]', ''); + return tonumber(value) or 0; +end + +-- to integer +function to_i(value) + return math.floor(to_n(value)); +end + +-- to string +function to_s(value) + if value == nil then + return ''; + end + + return tostring(value); +end + +-- to boolean +function to_b(value) + if type(value) == 'boolean' then + return value; + elseif tonumber(value) then + return (tonumber(value) > 0); + else + return (tostring(value) == 'yes' or tostring(value) == 'true'); + end +end + +-- to array +function to_a(line, separator) + line = line or ''; + separator = separator or ';'; + local result = {} + line:gsub('([^' .. separator .. ']+)', function(entry) + table.insert(result, entry); + end); + + return result; +end + +-- stripped to array +function strip_to_a(line, separator) + + local result = {} + line:gsub('([^' .. separator .. ']+)', function(entry) + table.insert(result, (entry:gsub('^%s+', ''):gsub('%s+$', ''))); + end); + + return result; +end + +-- downcase +function downcase(value) + if value == nil then + return ''; + end + + return tostring(value):lower(); +end + +-- remove special characters +function to_ascii(value) + return (to_s(value):gsub('[^A-Za-z0-9%-%_ %(%)]', '')); +end + +-- to SQL +function to_sql(value) + if type(value) == 'boolean' then + return tostring(value):upper(); + elseif type(value) == 'number' then + return tostring(value); + elseif type(value) == 'string' then + return '"' .. value:gsub('"', '\\"'):gsub("'", "\\'") .. '"'; + else + return 'NULL'; + end +end + +-- to JSON +function to_json(value) + if type(value) == 'boolean' then + return tostring(value):lower(); + elseif type(value) == 'number' then + return tostring(value); + elseif type(value) == 'string' then + return '"' .. value:gsub('"', '\\"'):gsub("'", "\\'") .. '"'; + else + return 'null'; + end +end + +-- remove leading/trailing whitespace +function strip(value) + return (tostring(value):gsub('^%s+', ''):gsub('%s+$', '')); +end + +-- split string +function partition(value, separator) + value = tostring(value); + separator = separator or ':' + + return value:match('^(.-)' .. separator), value:match(separator .. '(.-)$'); +end + +-- check if value is empty string or nil +function blank(value) + return (value == nil or to_s(value) == ''); +end diff --git a/misc/freeswitch/scripts/common/sync_log.lua b/misc/freeswitch/scripts/common/sync_log.lua new file mode 100644 index 0000000..05b0dcf --- /dev/null +++ b/misc/freeswitch/scripts/common/sync_log.lua @@ -0,0 +1,39 @@ +-- Gemeinschaft 5 module: sync log class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +SyncLog = {} + +-- create sync log object +function SyncLog.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + self.homebase_ip_address = arg.homebase_ip_address; + return object; +end + +-- create new entry +function SyncLog.insert(self, entry_name, entry_record, action, history_entries) + local content = {} + for key, value in pairs(entry_record) do + require 'common.str' + table.insert(content, '"'.. key ..'":' .. common.str.to_json(value)); + end + + local history = ''; + if action == 'update' then + history = 'Changed: ["' .. table.concat(history_entries, '","') .. '"]'; + end + + local sql_query = 'INSERT INTO `gs_cluster_sync_log_entries` (`waiting_to_be_synced`,`created_at`,`updated_at`,`class_name`,`action`,`content`,`history`,`homebase_ip_address`) \ + VALUES \ + (TRUE, NOW(), NOW(), \'' .. entry_name .. '\', \'' .. action .. '\', \'{' .. table.concat(content, ',') .. '}\', \'' .. history .. '\', \'' .. self.homebase_ip_address .. '\')'; + + return self.database:query(sql_query); +end diff --git a/misc/freeswitch/scripts/configuration.lua b/misc/freeswitch/scripts/configuration.lua new file mode 100644 index 0000000..906d3f8 --- /dev/null +++ b/misc/freeswitch/scripts/configuration.lua @@ -0,0 +1,229 @@ +-- Gemeinschaft 5 dynamic freeswitch configuration +-- (c) AMOOMA GmbH 2012 +-- + +function nodes(database, local_node_id) + local gateways_xml = ''; + + require 'common.node' + for node_id, node_record in pairs(common.node.Node:new{log=log, database=database}:all()) do + if node_id ~= local_node_id then + local node_parameters = {} + node_parameters['username'] = node_record.name; + node_parameters['password'] = 'gemeinschaft'; + node_parameters['proxy'] = node_record.ip_address; + node_parameters['register'] = 'false'; + log:debug('NODE_GATEWAY ', node_record.id, ' - name: ', node_record.name, ', address: ', node_record.ip_address); + gateways_xml = gateways_xml .. xml:gateway(node_record.name, node_parameters); + end + end + + return gateways_xml; +end + +function gateways(profile_name) + local gateways_xml = ''; + local gateways = common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', false); + + if not gateways then + return ''; + end + + for sofia_gateway, gateway_parameters in pairs(gateways) do + if tostring(gateway_parameters.profile) == profile_name then + log:debug('GATEWAY - name: ', sofia_gateway, ', address: ', gateway_parameters.proxy); + gateways_xml = gateways_xml .. xml:gateway(sofia_gateway, gateway_parameters); + end + end + + return gateways_xml; +end + +function profile(database, sofia_ini, profile_name, index, domains, node_id) + local profile_parameters = sofia_ini['profile:' .. profile_name]; + + if not profile_parameters then + log:error('SOFIA_PROFILE ', index,' - name: ', profile_name, ' - no parameters'); + return ''; + end + -- set local bind address + if domains[index] then + profile_parameters['sip-ip'] = domains[index]['host']; + profile_parameters['rtp-ip'] = domains[index]['host']; + profile_parameters['force-register-domain'] = domains[index]['host']; + profile_parameters['force-subscription-domain'] = domains[index]['host']; + profile_parameters['force-register-db-domain'] = domains[index]['host']; + log:debug('SOFIA_PROFILE ', index,' - name: ', profile_name, ', domain: ', domains[index]['host'], ', sip_bind: ', profile_parameters['sip-ip'], ':', profile_parameters['sip-port']); + else + log:error('SOFIA_PROFILE ', index,' - name: ', profile_name, ' - no domains'); + end + + local gateways_xml = gateways(profile_name); + + if index == 1 then + gateways_xml = gateways_xml .. nodes(database, node_id); + end + + return xml:sofia_profile(profile_name, profile_parameters, gateways_xml); +end + +-- generate sofia.conf +function conf_sofia(database) + local sofia_profile = "gemeinschaft"; + + require 'common.configuration_file' + local sofia_ini = common.configuration_file.get('/opt/freeswitch/scripts/ini/sofia.ini'); + local dialplan_parameters = common.configuration_file.get('/opt/freeswitch/scripts/ini/dialplan.ini', 'parameters'); + + local local_node_id = tonumber(dialplan_parameters['node_id']) or 1; + + require 'configuration.sip' + local domains = configuration.sip.Sip:new{ log = log, database = database}:domains(); + + sofia_profiles_xml = ''; + for index, profile_name in ipairs(sofia_ini.profiles) do + sofia_profiles_xml = sofia_profiles_xml .. profile(database, sofia_ini, profile_name, index, domains, local_node_id); + end + + XML_STRING = xml:document(xml:sofia(sofia_ini.parameters, sofia_profiles_xml)) +end + +function conf_conference(database) + XML_STRING = xml:document(xml:conference()); + + require 'common.configuration_file' + local conference_ini = common.configuration_file.get('/opt/freeswitch/scripts/ini/conferences.ini'); + local conference_parameters = conference_ini.parameters; + + local event_name = params:getHeader("Event-Name") + if event_name == 'COMMAND' then + local conf_name = params:getHeader('conf_name'); + local profile_name = params:getHeader('profile_name'); + + if conf_name then + require 'common.conference' + conference = common.conference.Conference:new{log=log, database=database}:find_by_id(conf_name); + if conference then + log:debug('CONFIG_CONFERENCE ', conf_name, ' name: ', conference.record.name, ', profile: ', profile_name); + conference_parameters['caller-id-name'] = conference.record.name or ''; + XML_STRING = xml:document(xml:conference(xml:conference_profile(profile_name, conference_parameters))); + else + log:error('CONFIG_CONFERENCE ', conf_name, ' - conference not found'); + end + else + log:notice('CONFIG_CONFERENCE - no conference name'); + end + else + log:debug('CONFIG_CONFERENCE ', conf_name, ' - event: ', event_name); + end +end + + +function directory_sip_account(database) + local key = params:getHeader('key'); + local auth_name = params:getHeader('user'); + local domain = params:getHeader('domain'); + local purpose = params:getHeader('purpose'); + + 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'); + require 'configuration.sip' + log:notice('DATABASE: ', database); + local sip_gateway = configuration.sip.Sip:new{ log = log, database = database}:find_gateway_by_name(gateway_name); + if sip_gateway ~= nil and next(sip_gateway) ~= nil then + log:debug('DIRECTORY_GATEWAY - name: ', gateway_name, ', auth_name: ', auth_name); + XML_STRING = xml:document(xml:directory(xml:gateway_user(sip_gateway, gateway_name, auth_name), domain)); + 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, domain); + if sip_account ~= nil then + 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); + XML_STRING = xml:document(xml:directory(xml:group_default(xml:user(sip_account.record)), domain)); + else + log:debug('DIRECTORY_SIP_ACCOUNT - auth_name: ', sip_account.record.auth_name, ', caller_name: ', sip_account.record.caller_name, ', domain: ', domain); + XML_STRING = xml:document(xml:directory(xml:user(sip_account.record), domain)); + end + else + log:debug('DIRECTORY_SIP_ACCOUNT - sip account not found - auth_name: ', auth_name, ', domain: ', domain); + -- fake sip_account configuration + sip_account = { + auth_name = auth_name, + id = 0, + uuid = '', + password = tostring(math.random(0, 65534)), + voicemail_pin = '', + state = 'inactive', + caller_name = '', + sip_accountable_type = 'none', + sip_accountable_id = 0, + } + XML_STRING = xml:document(xml:directory(xml:user(sip_account), domain)) + end + end + elseif tostring(XML_REQUEST.key_name) == 'name' and tostring(XML_REQUEST.key_value) ~= '' then + log:debug('DOMAIN_DIRECTORY - domain: ', XML_REQUEST.key_value); + XML_STRING = xml:document(xml:directory(nil, XML_REQUEST.key_value)); + end +end + + +local log_identifier = XML_REQUEST.key_value or 'CONFIG'; + +-- set logger +require 'common.log' +log = common.log.Log:new(); +log.prefix = '#C# [' .. log_identifier .. '] '; + +-- return a valid xml document +require 'configuration.freeswitch_xml' +xml = configuration.freeswitch_xml.FreeSwitchXml:new(); +XML_STRING = xml:document(); + +local database = nil; + +-- log:debug('CONFIG_REQUEST section: ', XML_REQUEST.section, ', tag: ', XML_REQUEST.tag_name, ', key: ', XML_REQUEST.key_value); + +if XML_REQUEST.section == 'configuration' and XML_REQUEST.tag_name == 'configuration' then + -- database connection + require 'common.database' + database = common.database.Database:new{ log = log }:connect(); + if database:connected() == false then + log:error('CONFIG_REQUEST - cannot connect to Gemeinschaft database'); + return false; + end + + if XML_REQUEST.key_value == 'sofia.conf' then + conf_sofia(database); + elseif XML_REQUEST.key_value == "conference.conf" then + conf_conference(database); + end +elseif XML_REQUEST.section == 'directory' and XML_REQUEST.tag_name == '' then + log:debug('SIP_ACCOUNT_DIRECTORY - initialization phase'); +elseif XML_REQUEST.section == 'directory' and XML_REQUEST.tag_name == 'domain' then + if params == nil then + log:error('SIP_ACCOUNT_DIRECTORY - no parameters'); + return false; + end + + require 'common.database' + database = common.database.Database:new{ log = log }:connect(); + if not database:connected() then + log:error('CONFIG_REQUEST - cannot connect to Gemeinschaft database'); + return false; + end + directory_sip_account(database); +else + log:error('CONFIG_REQUEST - no configuration handler, section: ', XML_REQUEST.section, ', tag: ', XML_REQUEST.tag_name); +end + +-- ensure database handler is released on exit +if database then + database:release(); +end diff --git a/misc/freeswitch/scripts/configuration/freeswitch_xml.lua b/misc/freeswitch/scripts/configuration/freeswitch_xml.lua new file mode 100644 index 0000000..c81bf50 --- /dev/null +++ b/misc/freeswitch/scripts/configuration/freeswitch_xml.lua @@ -0,0 +1,307 @@ +-- ConfigurationModule: FreeSwitchXml +-- +module(...,package.seeall) + +FreeSwitchXml = {} + +-- Create FreeSwitchXml object +function FreeSwitchXml.new(self, object) + object = object or {} + setmetatable(object, self) + self.__index = self + return object +end + +function FreeSwitchXml.param(self, name, value) + return '<param name="' .. name .. '" value="' .. value .. '"/>' +end + +function FreeSwitchXml.variable(self, name, value) + return '<variable name="' .. name .. '" value="' .. value .. '"/>' +end + +function FreeSwitchXml.document(self, sections_xml) + if type(sections_xml) == "string" then + sections_xml = { sections_xml } + elseif type(sections_xml) == "nil" then + sections_xml = { "" } + end + + local xml_string= +[[<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="freeswitch/xml"> +]] .. table.concat(sections_xml, "\n") .. [[ + +</document>]] + + return xml_string +end + +function FreeSwitchXml.directory(self, entries_xml, domain) + if type(entries_xml) == "string" then + entries_xml = { entries_xml } + elseif type(entries_xml) == "nil" then + entries_xml = { "" } + end + + local xml_string = +[[ +<section name="directory"> +<domain name="]] .. domain .. [["> +<params> +<param name="dial-string" value="${sofia_contact(${dialed_user}@${dialed_domain})}"/> +</params> +]] .. table.concat(entries_xml, "\n") .. [[ + +</domain> +</section>]] + return xml_string +end + +function FreeSwitchXml.group_default(self, entries_xml) + if type(entries_xml) == "string" then + entries_xml = { entries_xml } + elseif type(entries_xml) == "nil" then + entries_xml = { "" } + end + + local xml_string = +[[ +<groups> +<group name="default"> +<users> +]] .. table.concat(entries_xml, "\n") .. [[ + +</users> +</group> +</groups>]] + return xml_string +end + +function FreeSwitchXml.user(self, user) + require 'common.configuration_file' + local params = common.configuration_file.get('/opt/freeswitch/scripts/ini/sip_accounts.ini', 'parameters'); + + params['password'] = user.password; + params['vm-password'] = user.voicemail_pin; + + local variables = { + user_context = "default", + gs_from_gateway = "false", + gs_account_id = user.id, + gs_account_uuid = user.uuid, + gs_account_type = "SipAccount", + gs_account_state = user.state, + gs_account_caller_name = user.caller_name, + gs_account_owner_type = user.sip_accountable_type, + gs_account_owner_id = user.sip_accountable_id + } + + local params_xml = {} + for name, value in pairs(params) do + params_xml[#params_xml+1] = self:param(name, value) + end + + local variables_xml = {} + for name, value in pairs(variables) do + variables_xml[#variables_xml+1] = self:variable(name, value) + end + + local xml_string = +[[ +<user id="]] .. user.auth_name .. [["> +<params> +]] .. table.concat(params_xml, "\n") .. [[ + +</params> +<variables> +]] .. table.concat(variables_xml, "\n") .. [[ + +</variables> +</user>]] + return xml_string +end + +function FreeSwitchXml.gateway_user(self, user, gateway_name, auth_name) + user.id = user.id or 0 + + local params = { + ['password'] = user.password, + } + + local variables = { + user_context = "default", + gs_from_gateway = "true", + gs_gateway_name = gateway_name, + gs_gateway_id = user.id + } + + local params_xml = {} + for name, value in pairs(params) do + params_xml[#params_xml+1] = self:param(name, value) + end + + local variables_xml = {} + for name, value in pairs(variables) do + variables_xml[#variables_xml+1] = self:variable(name, value) + end + + local xml_string = +[[ +<user id="]] .. auth_name .. [["> +<params> +]] .. table.concat(params_xml, "\n") .. [[ + +</params> +<variables> +]] .. table.concat(variables_xml, "\n") .. [[ + +</variables> +</user>]] + return xml_string +end + +function FreeSwitchXml.sofia(self, parameters, profiles_xml) + if type(profiles_xml) == "string" then + profiles_xml = { profiles_xml } + elseif type(profiles_xml) == "nil" then + profiles_xml = { "" } + end + + local params_xml = {} + for name, value in pairs(parameters) do + params_xml[#params_xml+1] = self:param(name, value) + end + + local xml_string = +[[ +<section name="configuration" description="FreeSwitch configuration for Sofia Profile"> +<configuration name="sofia.conf" description="Sofia SIP Configuration"> +<global_settings> +]] .. table.concat(params_xml, "\n") .. [[ + +</global_settings> +<profiles> +]] .. table.concat(profiles_xml, "\n") .. [[ + +</profiles> +</configuration> +</section>]] + return xml_string +end + +function FreeSwitchXml.sofia_profile(self, profile_name, parameters, gateways_xml) + params_xml = {} + for name, value in pairs(parameters) do + params_xml[#params_xml+1] = self:param(name, value) + end + + if type(gateways_xml) == "string" then + gateways_xml = { gateways_xml } + elseif type(gateways_xml) == "nil" then + gateways_xml = { "" } + end + + local xml_string = +[[ +<profile name="]] .. profile_name .. [["> +<aliases> +</aliases> +<gateways> +]] .. table.concat(gateways_xml, "\n") .. [[ + +</gateways> +<domains> +<domain name="all" alias="true" parse="false"/> +</domains> +<settings> +]] .. table.concat(params_xml, "\n") .. [[ + +</settings> +</profile>]] + return xml_string +end + +function FreeSwitchXml.gateway(self, gateway_name, parameters) + local params_xml = {} + if parameters then + for name, value in pairs(parameters) do + params_xml[#params_xml+1] = self:param(name, value) + end + end + + local xml_string = +[[ +<gateway name="]] .. gateway_name .. [["> +]] .. table.concat(params_xml, "\n") .. [[ + +</gateway>]] + return xml_string +end + +function FreeSwitchXml.conference(self, profiles_xml) + if type(profiles_xml) == "string" then + profiles_xml = { profiles_xml } + elseif type(profiles_xml) == "nil" then + profiles_xml = { "" } + end + + local xml_string = +[[ +<section name="configuration" description="FreeSwitch configuration for Sofia Profile"> +<configuration name="conference.conf" description="Conference configuration"> +<advertise> +</advertise> +<caller-controls> +<group name="speaker"> +<control action="mute"/> +<control action="deaf mute" digits="*"/> +<control action="energy up" digits="9"/> +<control action="energy equ" digits="8"/> +<control action="energy dn" digits="7"/> +<control action="vol talk up" digits="3"/> +<control action="vol talk zero" digits="2"/> +<control action="vol talk dn" digits="1"/> +<control action="vol listen up" digits="6"/> +<control action="vol listen zero" digits="5"/> +<control action="vol listen dn" digits="4"/> +<control action="hangup" digits="#"/> +</group> +<group name="moderator"> +<control action="mute" digits="0"/> +<control action="deaf mute" digits="*"/> +<control action="energy up" digits="9"/> +<control action="energy equ" digits="8"/> +<control action="energy dn" digits="7"/> +<control action="vol talk up" digits="3"/> +<control action="vol talk zero" digits="2"/> +<control action="vol talk dn" digits="1"/> +<control action="vol listen up" digits="6"/> +<control action="vol listen zero" digits="5"/> +<control action="vol listen dn" digits="4"/> +<control action="hangup" digits="#"/> +</group> +</caller-controls> +<profiles> +]] .. table.concat(profiles_xml, "\n") .. [[ + +</profiles> +</configuration> +</section>]] + return xml_string +end + +function FreeSwitchXml.conference_profile(self, profile_name, parameters) + params_xml = {} + for name, value in pairs(parameters) do + params_xml[#params_xml+1] = self:param(name, value) + end + + local xml_string = +[[ +<profile name="]] .. profile_name .. [["> +]] .. table.concat(params_xml, "\n") .. [[ + +</profile>]] + return xml_string +end diff --git a/misc/freeswitch/scripts/configuration/sip.lua b/misc/freeswitch/scripts/configuration/sip.lua new file mode 100644 index 0000000..78143bc --- /dev/null +++ b/misc/freeswitch/scripts/configuration/sip.lua @@ -0,0 +1,37 @@ +-- Gemeinschaft 5 module: sip configuration class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Sip = {} + +-- create sip configuration object +function Sip.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object; +end + +-- find gateway by name +function Sip.find_gateway_by_name(self, name) + require 'common.configuration_file' + return common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', name); +end + +-- list sip domains +function Sip.domains(self) + local sql_query = 'SELECT * FROM `sip_domains`'; + local sip_domains = {} + + self.database:query(sql_query, function(sip_domain) + table.insert(sip_domains, sip_domain); + end) + + return sip_domains; +end diff --git a/misc/freeswitch/scripts/dialplan/access_authorizations.lua b/misc/freeswitch/scripts/dialplan/access_authorizations.lua new file mode 100644 index 0000000..dbacf20 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/access_authorizations.lua @@ -0,0 +1,52 @@ +-- CommonModule: AccessAuthorization +-- +module(...,package.seeall) + +AccessAuthorization = {} + +-- Create AccessAuthorization object +function AccessAuthorization.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log + self.database = arg.database + self.record = arg.record + self.session = arg.session + return object +end + +-- Find AccessAuthorization by ID +function AccessAuthorization.find_by_id(self, id) + local sql_query = string.format("SELECT * FROM `access_authorizations` WHERE `id`=%d LIMIT 1", id) + local record = nil + + self.database:query(sql_query, function(access_authorization_entry) + record = access_authorization_entry + end) + + if record then + access_authorization = AccessAuthorization:new(self) + access_authorization.record = record + return access_authorization + end + + return nil +end + +-- list accessauthorization by owner +function AccessAuthorization.list_by_owner(self, owner_id, owner_type) + local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`login`, `a`.`pin`, `a`.`sip_account_id`, `b`.`number` AS `phone_number` \ + FROM `access_authorizations` `a` \ + LEFT JOIN `phone_numbers` `b` ON `b`.`phone_numberable_id` = `a`.`id` AND `b`.`phone_numberable_type` = "AccessAuthorization" \ + WHERE `a`.`access_authorizationable_type` = "' .. owner_type .. '" AND `access_authorizationable_id`= ' .. tonumber(owner_id); + + local access_authorizations = {} + + self.database:query(sql_query, function(access_authorization_entry) + table.insert(access_authorizations, access_authorization_entry); + end); + + return access_authorizations; +end diff --git a/misc/freeswitch/scripts/dialplan/acd.lua b/misc/freeswitch/scripts/dialplan/acd.lua new file mode 100644 index 0000000..563d836 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/acd.lua @@ -0,0 +1,484 @@ +-- Gemeinschaft 5 module: acd class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +AutomaticCallDistributor = {} + +local DEFAULT_AGENT_TIMEOUT = 20; +local DEFAULT_TIME_RES = 5; +local DEFAULT_WAIT_TIMEOUT = 360; +local DEFAULT_RETRY_TIME = 2; +local DEFAULT_MUSIC_ON_WAIT = 'tone_stream://%(2000,4000,440.0,480.0);loops=-1'; + +-- create acd object +function AutomaticCallDistributor.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'automaticcalldistributor'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + self.acd_caller_id = arg.acd_caller_id; + self.domain = arg.domain; + return object; +end + + +function AutomaticCallDistributor.find_by_sql(self, sql_query) + local acd = nil; + + require 'common.str' + + self.database:query(sql_query, function(entry) + acd = AutomaticCallDistributor:new(self); + acd.record = entry; + acd.id = tonumber(entry.id); + acd.uuid = entry.uuid; + acd.agent_timeout = tonumber(entry.agent_timeout) or DEFAULT_AGENT_TIMEOUT; + acd.announce_position = tonumber(entry.announce_position); + acd.announce_call_agents = common.str.to_s(entry.announce_call_agents); + acd.greeting = common.str.to_s(entry.greeting); + acd.goodbye = common.str.to_s(entry.goodbye); + acd.music = common.str.to_s(entry.music); + acd.strategy = common.str.to_s(entry.strategy); + acd.join = common.str.to_s(entry.join); + acd.leave = common.str.to_s(entry.leave); + end) + + return acd; +end + + +function AutomaticCallDistributor.find_by_id(self, id) + local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1'; + return self:find_by_sql(sql_query); +end + + +function AutomaticCallDistributor.find_by_uuid(self, uuid) + local sql_query = 'SELECT * FROM `automatic_call_distributors` WHERE `uuid`= "'.. tostring(uuid) .. '" LIMIT 1'; + return self:find_by_sql(sql_query); +end + + +function AutomaticCallDistributor.callers_count(self) + return self.database:query_return_value('SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` WHERE `automatic_call_distributor_id` = ' .. self.id); +end + + +function AutomaticCallDistributor.caller_new(self, uuid) + local sql_query = 'INSERT INTO `acd_callers` \ + (`enter_time`, `created_at`, `updated_at`, `status`, `automatic_call_distributor_id`, `channel_uuid`) \ + VALUES (NOW(), NOW(), NOW(), "enter", ' .. self.id .. ', "' .. uuid .. '")'; + + if self.database:query(sql_query) then + self.acd_caller_id = self.database:last_insert_id(); + end +end + + +function AutomaticCallDistributor.caller_update(self, attributes) + local attributes_sql = { '`updated_at` = NOW()' }; + for key, value in pairs(attributes) do + table.insert(attributes_sql, '`' .. key .. '` = "' .. value .. '"'); + end + + local sql_query = 'UPDATE `acd_callers` \ + SET '.. table.concat(attributes_sql, ',') .. '\ + WHERE `id` = ' .. tonumber(self.acd_caller_id); + return self.database:query(sql_query); +end + + +function AutomaticCallDistributor.caller_delete(self, id) + id = id or self.acd_caller_id; + local sql_query = 'DELETE FROM `acd_callers` \ + WHERE `id` = ' .. tonumber(id); + return self.database:query(sql_query); +end + + +function AutomaticCallDistributor.agent_find_by_acd_and_destination(self, acd_id, destination_type, destination_id) + local sql_query = 'SELECT * FROM `acd_agents` \ + WHERE `automatic_call_distributor_id` = ' .. acd_id .. ' \ + AND `destination_type` = "' .. destination_type .. '" \ + AND `destination_id` = ' .. destination_id; + + local agent = nil; + self.database:query(sql_query, function(entry) + agent = entry; + end) + + return agent; +end + + +function AutomaticCallDistributor.agent_status_presence_set(self, agent_id, presence_state) + require "dialplan.presence" + local presence = dialplan.presence.Presence:new(); + + presence:init{log = self.log, accounts = { 'f-acdmtg-' .. tostring(agent_id) }, domain = self.domain, uuid = 'acd_agent_' .. tostring(agent_id)}; + return presence:set(presence_state); +end + + +function AutomaticCallDistributor.agent_status_get(self, agent_id) + local sql_query = 'SELECT `status` FROM `acd_agents` WHERE `id` = ' .. agent_id; + return self.database:query_return_value(sql_query); +end + + +function AutomaticCallDistributor.agent_status_toggle(self, agent_id, destination_type, destination_id) + local sql_query = 'UPDATE `acd_agents` SET `status` = IF(`status` = "active", "inactive", "active") \ + WHERE `id` = ' .. agent_id .. ' \ + AND `destination_type` = "' .. destination_type .. '" \ + AND `destination_id` = ' .. destination_id; + + if not self.database:query(sql_query) then + return nil; + end + + local status = self:agent_status_get(agent_id); + + if tostring(status) == 'active' then + self:agent_status_presence_set(agent_id, 'confirmed'); + else + self:agent_status_presence_set(agent_id, 'terminated'); + end + + return status; +end + + +function AutomaticCallDistributor.agents_active(self) + local sql_query = 'SELECT * FROM `acd_agents` \ + WHERE `status` = "active" AND destination_type != "SipAccount" AND `automatic_call_distributor_id` = ' .. tonumber(self.id); + + local agents = {} + self.database:query(sql_query, function(entry) + table.insert(agents, entry); + end); + + local sql_query = 'SELECT `a`.* FROM `acd_agents` `a` \ + JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \ + JOIN `sip_registrations` `c` ON `b`.`auth_name` = `c`.`sip_user` \ + WHERE `a`.`status` = "active" AND `a`.destination_type = "SipAccount" AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id); + + self.database:query(sql_query, function(entry) + table.insert(agents, entry); + end); + + return agents; +end + + +function AutomaticCallDistributor.agents_available(self, strategy) + local order_by = '`a`.`id` DESC'; + + if strategy then + if strategy == 'round_robin' then + order_by = '`a`.`last_call` ASC, `a`.`id` DESC'; + end + end + + local sql_query = 'SELECT `a`.`id`, `a`.`name`, `a`.`destination_type`, `a`.`destination_id`, `b`.`auth_name`, `b`.`gs_node_id`, `c`.`callstate` \ + FROM `acd_agents` `a` LEFT JOIN `sip_accounts` `b` ON `a`.`destination_id` = `b`.`id` \ + JOIN `sip_registrations` `d` ON `b`.`auth_name` = `d`.`sip_user` \ + LEFT JOIN `channels` `c` ON `c`.`name` LIKE CONCAT("%", `b`.`auth_name`, "@%") \ + WHERE `a`.`status` = "active" AND `a`.`destination_id` IS NOT NULL AND `a`.`automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' \ + ORDER BY ' .. order_by; + + local accounts = {} + self.database:query(sql_query, function(entry) + if not entry.callstate then + table.insert(accounts, entry); + end + end); + + return accounts; +end + + +function AutomaticCallDistributor.agent_update_call(self, agent_id) + + local sql_query = 'UPDATE `acd_agents` \ + SET `last_call` = NOW(), `calls_answered` = IFNULL(`calls_answered`, 0) + 1 \ + WHERE `id` = ' .. tonumber(agent_id); + return self.database:query(sql_query); +end + + +function AutomaticCallDistributor.call_position(self) + local sql_query = 'SELECT COUNT(*) FROM `acd_callers` `a` JOIN `channels` `b` ON `a`.`channel_uuid` = `b`.`uuid` \ + WHERE `automatic_call_distributor_id` = ' .. tonumber(self.id) .. ' AND `status` = "waiting" AND `id` < ' .. tonumber(self.acd_caller_id); + + return tonumber(self.database:query_return_value(sql_query)); +end + + +function AutomaticCallDistributor.wait_turn(self, caller_uuid, acd_caller_id, timeout, retry_timeout) + self.acd_caller_id = acd_caller_id or self.acd_caller_id; + timeout = timeout or DEFAULT_WAIT_TIMEOUT; + local available_agents = {}; + local active_agents = {}; + local position = self:call_position(); + + self.log:info('ACD ', self.id, ' WAIT - timeout: ', timeout, ', res: ', DEFAULT_TIME_RES, ', retry_timeout: ', retry_timeout, ', position: ', position + 1); + + require 'common.fapi' + local fapi = common.fapi.FApi:new{ log = self.log, uuid = caller_uuid } + + local acd_status = nil; + local start_time = os.time(); + local exit_time = start_time + timeout; + + if tonumber(retry_timeout) then + self.log:info('ACD ', self.id, ' WAIT - retry_timeout: ', retry_timeout); + fapi:sleep(retry_timeout * 1000); + end + + while (exit_time > os.time() and fapi:channel_exists()) do + available_agents = self:agents_available(); + active_agents = self:agents_active(); + local current_position = self:call_position(); + + if position ~= current_position then + position = current_position; + self.log:info('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time); + + if tostring(self.announce_position) >= '0' and position > 0 then + acd_status = 'announce_position'; + fapi:set_variable('acd_position', position + 1); + break; + end + else + self.log:debug('ACD ', self.id, ' WAIT - agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time); + end + + if #available_agents == 0 and self.leave:find('no_agents_available') then + acd_status = 'no_agents'; + break; + elseif #active_agents == 0 and self.leave:find('no_agents_active') then + acd_status = 'no_agents'; + break; + elseif position == 0 and #available_agents > 0 then + acd_status = 'call_agents'; + break; + end + + if tonumber(self.announce_position) and tonumber(self.announce_position) > 0 and tonumber(self.announce_position) <= os.time()-start_time then + acd_status = 'announce_position'; + fapi:set_variable('acd_position', position + 1); + break; + end + + fapi:sleep(DEFAULT_TIME_RES * 1000); + end + + if not acd_status then + if (exit_time <= os.time()) then + acd_status = 'timeout'; + else + acd_status = 'unspecified'; + end + end + + self.log:info('ACD ', self.id, ' WAIT END - status: ', acd_status, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', wait_time: ', os.time()-start_time); + + fapi:set_variable('acd_status', acd_status); + if tostring(fapi:get_variable('acd_waiting')) == 'true' then + fapi:continue(); + end +end + + +function AutomaticCallDistributor.wait_play_music(self, caller, timeout, retry_timeout, music) + local result = caller:result('luarun(acd_wait.lua ' .. caller.uuid .. ' ' .. tostring(self.id) .. ' ' .. tostring(timeout) .. ' ' .. tostring(retry_timeout) .. ' ' .. self.acd_caller_id .. ')'); + if not tostring(result):match('^+OK') then + self.log:error('ACD ', self.id,' WAIT_PLAY_MUSIC - error starting acd thread'); + return 'error'; + end + + caller:set_variable('acd_waiting', true); + caller.session:streamFile(music or DEFAULT_MUSIC_ON_WAIT); + caller:set_variable('acd_waiting', false); + + local acd_status = caller:to_s('acd_status'); + if acd_status == '' then + acd_status = 'abandoned'; + end + + return acd_status; +end + + +function AutomaticCallDistributor.on_answer(self, destination) + self.log:info('ACD ', self.id, ' ANSWERED - agent: ', destination.type, '=', destination.id, '/', destination.uuid) + self:caller_update({status = 'answered'}); +end + + +function AutomaticCallDistributor.call_agents(self, dialplan_object, caller, destination) + local available_agents = self:agents_available(self.strategy); + + self.log:info('ACD ', self.id, ' CALL_AGENTS - strategy: ', self.strategy, ', available_agents: ', #available_agents); + + caller:set_variable('ring_ready', true); + + local destinations = {} + for index, agent in ipairs(available_agents) do + self.log:info('ACD ', self.id, ' AGENT - name: ', agent.name, ', destination: ', agent.destination_type, '=', agent.destination_id, '@', agent.gs_node_id, ', local_node: ', dialplan_object.node_id); + table.insert(destinations, dialplan_object:destination_new{ type = agent.destination_type, id = agent.destination_id, node_id = agent.gs_node_id, data = agent.id }); + end + + local result = { continue = false }; + local start_time = os.time(); + + require 'dialplan.sip_call' + if self.strategy == 'ring_all' then + result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork(destinations, + { + callee_id_number = destination.number, + timeout = self.agent_timeout, + send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ), + }); + self.log:info('ACD ', self.id, ' CALL_AGENTS - success, fork_index: ', result.fork_index); + if result.fork_index then + result.destination = destinations[result.fork_index]; + end + return result; + else + for index, destination in ipairs(destinations) do + if os.time() > (self.start_time + self.timeout) and caller.session:ready() then + self.log:info('ACD ', self.id, ' CALL_AGENTS - timeout'); + return { disposition = 'ACD_TIMEOUT', code = 480, phrase = 'Timeout' } + end + + self.log:info('ACD ', self.id, ' CALL_AGENT - ', destination.type, '=', destination.id, ', timeout: ', self.agent_timeout); + result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller, calling_object = self, on_answer = self.on_answer }:fork({ destination }, + { + callee_id_number = destination.number, + timeout = self.agent_timeout, + send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ), + }); + if result.disposition == 'SUCCESS' then + self.log:info('ACD ', self.id, ' CALL_AGENTS - success, agent_id: ', destination.data); + self:agent_update_call(destination.data); + result.destination = destination; + return result; + end + end + end + + return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' } +end + + +function AutomaticCallDistributor.run(self, dialplan_object, caller, destination) + require 'common.str' + + local callers_count = self:callers_count(); + local active_agents = self:agents_active(); + local available_agents = self:agents_available(); + local position = self:call_position(); + + if self.leave:find('timeout') then + self.timeout = dialplan_object.dial_timeout_active; + else + self.timeout = 86400; + end + + self.log:info('ACD ', self.id,' - ', self.class, '=', self.id, '/', self.uuid, ', acd_caller=', self.acd_caller_id, ', callers: ', callers_count, ', agents: ', #available_agents, '/', #active_agents, ', position: ', position + 1, ', music: ', tostring(self.music)); + + if self.join == 'agents_active' and #active_agents == 0 then + self.log:info('ACD ', self.id, ' - no agents active'); + return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No agents' } + end + + if self.join == 'agents_available' and #available_agents == 0 then + self.log:info('ACD ', self.id, ' - no agents available'); + return { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'All agents busy' } + end + + if not common.str.blank(self.music) then + caller:set_variable('ringback', self.music); + else + self.music = false; + end + + if self.music then + caller.session:answer(); + else + caller:set_variable('instant_ringback', true); + end + + self.start_time = os.time(); + caller:sleep(500); + local acd_status = 'waiting'; + self:caller_update({status = acd_status}); + + local retry_timeout = nil; + local result = { disposition = 'ACD_NO_AGENTS', code = 480, phrase = 'No active agents' } + + if self.greeting then + caller.session:sayPhrase('acd_greeting', self.greeting); + end + + if self.announce_position then + local current_position = self:call_position(); + if tonumber(current_position) then + caller.session:sayPhrase('acd_announce_position_enter', tonumber(current_position) + 1); + end + end + + while acd_status == 'waiting' and caller.session:ready() do + acd_status = self:wait_play_music(caller, self.timeout - (os.time() - self.start_time), retry_timeout, self.music); + self.log:info('ACD ', self.id, ' PROCESS - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time)); + + if not caller.session:ready() then + acd_status = 'abandoned'; + break; + elseif os.time() >= (self.start_time + self.timeout) then + acd_status = 'timeout'; + break; + elseif acd_status == 'no_agents' then + break; + elseif acd_status == 'call_agents' then + if self.announce_call_agents ~= '' then + caller.session:sayPhrase('acd_announce_call_agents', self.announce_call_agents); + end + + result = self:call_agents(dialplan_object, caller, destination); + self.log:info('ACD ', self.id, ' PROCESS - result: ', result.disposition, ', code: ', result.code, ', wait_time: ', (os.time() - self.start_time)); + + if result.disposition == 'SUCCESS' then + acd_status = 'success'; + break; + elseif os.time() < (self.start_time + self.timeout) then + acd_status = 'waiting'; + else + break; + end + elseif acd_status == 'announce_position' then + acd_status = 'waiting'; + if tostring(self.announce_position) == '0' then + caller.session:sayPhrase('acd_announce_position_change', caller:to_i('acd_position')); + else + caller.session:sayPhrase('acd_announce_position_periodic', caller:to_i('acd_position')); + end + end + + retry_timeout = tonumber(self.record.retry_timeout); + end + + if self.goodbye and caller.session:ready() then + caller.session:sayPhrase('acd_goodbye', self.goodbye); + end + self.log:info('ACD ', self.id, ' EXIT - status: ', acd_status, ', wait_time: ', (os.time() - self.start_time)); + + return result; +end diff --git a/misc/freeswitch/scripts/dialplan/callthrough.lua b/misc/freeswitch/scripts/dialplan/callthrough.lua new file mode 100644 index 0000000..69a0611 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/callthrough.lua @@ -0,0 +1,148 @@ +-- CommonModule: Callthrough +-- +module(...,package.seeall) + +Callthrough = {} + +-- Create Callthrough object +function Callthrough.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log + self.database = arg.database + self.record = arg.record + self.access_authorizations = arg.access_authorizations + return object +end + +-- Find Callthrough by ID +function Callthrough.find_by_id(self, id) + local sql_query = string.format("SELECT * FROM `callthroughs` WHERE `id`=%d LIMIT 1", id) + local record = nil + + self.database:query(sql_query, function(callthrough_entry) + record = callthrough_entry + end) + + if record then + local callthrough = Callthrough:new(self); + callthrough.record = record + + require 'dialplan.access_authorizations' + callthrough.access_authorizations = dialplan.access_authorizations.AccessAuthorization:new{ log = self.log, database = self.database }:list_by_owner(record.id, 'Callthrough'); + return callthrough + end + + return nil +end + +function Callthrough.authenticate(self, caller) + local authorizations = {} + local logins = {} + local pins = {} + + caller:answer(); + caller:sleep(1000); + + if not self.access_authorizations or table.getn(self.access_authorizations) == 0 then + self.log:debug('CALLTHROUGH_AUTHENTICATE - authorization disabled'); + return true; + end + + self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorizations: ', #self.access_authorizations); + for index, authorization in ipairs(self.access_authorizations) do + if authorization.phone_number then + if authorization.phone_number == caller.caller_phone_number then + if authorization.pin and authorization.pin ~= "" then + if caller.session:read(authorization.pin:len(), authorization.pin:len(), "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#") ~= authorization.pin then + self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN"); + return false; + else + self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number .. " and PIN"); + return authorization; + end + end + self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by caller id: " .. caller.caller_phone_number); + return authorization; + end + else + self.log:debug('CALLTHROUGH_AUTHENTICATE - access_authorization=', authorization.id); + if authorization.id then + authorizations[authorization.id] = authorization; + if authorization.login and authorization.login ~= "" then + logins[authorization.login] = authorization; + elseif authorization.pin and authorization.pin ~= "" then + pins[authorization.pin] = authorization; + end + end + end + end + + local login = nil; + local pin = nil; + + + if next(logins) ~= nil then + + caller.session:streamFile('ivr/ivr-please_enter_the.wav'); + caller.session:streamFile('ivr/ivr-id_number.wav'); + login = caller.session:read(2, 10, 'ivr/ivr-followed_by_pound.wav', 3000, '#'); + end + + if login and logins[tostring(login)] then + if not logins[tostring(login)].pin or logins[tostring(login)].pin == '' then + self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " without PIN"); + return logins[tostring(login)]; + end + pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#"); + if logins[tostring(login)].pin == pin then + self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by login: " .. login .. " and PIN"); + return logins[tostring(login)]; + else + self.log:debug("CALLTHROUGH_AUTHENTICATE - Wrong PIN"); + return false + end + end + + if next(pins) ~= nil then + pin = caller.session:read(2, 10, "ivr/ivr-please_enter_pin_followed_by_pound.wav", 3000, "#"); + end + + self.log:debug("CALLTHROUGH_AUTHENTICATE - No such login, try PIN"); + + if pin and pins[tostring(pin)] then + self.log:debug("CALLTHROUGH_AUTHENTICATE - Caller was authenticated by PIN"); + return pins[tostring(pin)]; + end + + self.log:debug("CALLTHROUGH_AUTHENTICATE - No login, wrong PIN - giving up"); + + return false; +end + +function Callthrough.whitelist(self, number) + local sql_query = 'SELECT `id` FROM `whitelists` WHERE `whitelistable_type` = "Callthrough" AND `whitelistable_id` = ' .. self.record.id; + local whitelist_ids = {} + + self.database:query(sql_query, function(entry) + table.insert(whitelist_ids, entry.id); + end) + + if next(whitelist_ids) == nil then + return true; + end + + -- OPTIMIZE Make sure number contains only valid characters + local sql_query = 'SELECT `id` FROM `phone_numbers` WHERE \ + `number` = "' .. number .. '" AND \ + `phone_numberable_type` = "Whitelist" AND `phone_numberable_id` IN (' .. table.concat(whitelist_ids, ',') .. ') LIMIT 1'; + + local authorized = false + self.database:query(sql_query, function(entry) + authorized = true + end) + + return authorized; +end diff --git a/misc/freeswitch/scripts/dialplan/cdr.lua b/misc/freeswitch/scripts/dialplan/cdr.lua new file mode 100644 index 0000000..55a7889 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/cdr.lua @@ -0,0 +1,71 @@ +-- Gemeinschaft 5 module: cdr class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Cdr = {} + +local DEFAULT_MEMBER_TIMEOUT = 20; + +-- Create Cdr object +function Cdr.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + return object; +end + + +function Cdr.save(self, caller, destination) + require 'common.str' + local cdr = {} + cdr.uuid = common.str.to_sql(caller.uuid); + cdr.bleg_uuid = common.str.to_sql(caller:to_s('bridge_uuid')); + cdr.dialed_number = common.str.to_sql(caller.called_number); + cdr.destination_number = common.str.to_sql(destination.number); + cdr.caller_id_number = common.str.to_sql(caller:to_s('effective_caller_id_number')); + cdr.caller_id_name = common.str.to_sql(caller:to_s('effective_caller_id_name')); + cdr.callee_id_number = common.str.to_sql(caller:to_s('effective_callee_id_number')); + cdr.callee_id_name = common.str.to_sql(caller:to_s('effective_callee_id_name')); + cdr.start_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('created_time') / 1000000) .. ')'; + cdr.answer_stamp = 'FROM_UNIXTIME(' .. math.floor(caller:to_i('answered_time') / 1000000) .. ')'; + cdr.end_stamp = 'NOW()'; + cdr.duration = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('created_time') / 1000000); + cdr.hangup_cause = common.str.to_sql(caller.session:hangupCause()); + cdr.dialstatus = common.str.to_sql(caller:to_s('DIALSTATUS')); + cdr.forwarding_number = common.str.to_sql(caller.forwarding_number); + cdr.forwarding_service = common.str.to_sql(caller.forwarding_service); + + if caller.auth_account then + cdr.forwarding_account_id = common.str.to_sql(caller.auth_account.id); + cdr.forwarding_account_type = common.str.to_sql(caller.auth_account.class); + end + + if caller.account then + cdr.account_id = common.str.to_sql(caller.account.id); + cdr.account_type = common.str.to_sql(caller.account.class); + end + + if caller:to_i('answered_time') > 0 then + cdr.billsec = 'UNIX_TIMESTAMP(NOW()) - ' .. math.floor(caller:to_i('answered_time') / 1000000); + end + + cdr.bleg_account_id = common.str.to_sql(tonumber(destination.id)); + cdr.bleg_account_type = common.str.to_sql(destination.type); + + local keys = {} + local values = {} + + for key, value in pairs(cdr) do + table.insert(keys, key); + table.insert(values, value); + end + + local sql_query = 'INSERT INTO `cdrs` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')'; + self.log:info('CDR_SAVE - caller: ', cdr.account_type, '=', cdr.account_id, ', callee: ',cdr.bleg_account_type, '=', cdr.bleg_account_id,', dialstatus: ', cdr.dialstatus); + return self.database:query(sql_query); +end diff --git a/misc/freeswitch/scripts/dialplan/dialplan.lua b/misc/freeswitch/scripts/dialplan/dialplan.lua new file mode 100644 index 0000000..f4dca9e --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/dialplan.lua @@ -0,0 +1,996 @@ +-- Gemeinschaft 5 module: dialplan class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Dialplan = {} + +-- local constants +local CONFIG_FILE_NAME = '/opt/freeswitch/scripts/ini/dialplan.ini'; +local DIAL_TIMEOUT = 120; +local MAX_LOOPS = 20; +local DIALPLAN_FUNCTION_PATTERN = '^f[_%-].*'; +local CALL_FORWARDING_SERVICES = { + USER_BUSY = 'busy', + CALL_REJECTED = 'busy', + NO_ANSWER = 'noanswer', + USER_NOT_REGISTERED = 'offline', + HUNT_GROUP_EMPTY = 'offline', + ACD_NO_AGENTS = 'offline', + ACD_TIMEOUT = 'noanswer', +} + +-- create dialplan object +function Dialplan.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + self.caller = arg.caller; + + return object; +end + + +function Dialplan.domain_get(self, domain) + require 'common.str' + local global_domain = freeswitch.API():execute('global_getvar', 'domain'); + + if common.str.blank(global_domain) then + if common.str.blank(domain) then + require 'common.database' + local database = common.database.Database:new{ log = self.log }:connect(); + if not database:connected() then + self.log:error('[', uuid,'] DIALPLAN_DOMAIN - cannot connect to Gemeinschaft database'); + else + require 'configuration.sip' + local domains = configuration.sip.Sip:new{ log = self.log, database = database }:domains(); + if domains[1] then + domain = domains[1]['host']; + end + end + end + + if database then + database:release(); + end + + if not common.str.blank(domain) then + self.log:notice('DIALPLAN_DOMAIN - setting default domain: ', domain); + freeswitch.API():execute('global_setvar', 'domain=' .. tostring(domain)); + end + else + domain = global_domain; + end + + if common.str.blank(domain) then + self.log:error('DIALPLAN_DOMAIN - no domain found'); + end + + return domain; +end + + +function Dialplan.configuration_read(self, file_name) + require 'common.str' + require 'common.configuration_file' + + -- dialplan configuration + self.config = common.configuration_file.get(file_name or CONFIG_FILE_NAME); + self.node_id = common.str.to_i(self.config.parameters.node_id); + self.domain = self:domain_get(self.config.parameters.domain); + self.dial_timeout = tonumber(self.config.parameters.dial_timeout) or DIAL_TIMEOUT; + self.max_loops = tonumber(self.config.parameters.max_loops) or MAX_LOOPS; + self.user_image_url = common.str.to_s(self.config.parameters.user_image_url); + self.phone_book_entry_image_url = common.str.to_s(self.config.parameters.phone_book_entry_image_url); + self.phonebook_number_lookup = common.str.to_b(self.config.parameters.phonebook_number_lookup); + self.geo_number_lookup = common.str.to_b(self.config.parameters.geo_number_lookup); + self.default_language = self.config.parameters.default_language or 'en'; + self.send_ringing_to_gateways = common.str.to_b(self.config.parameters.send_ringing_to_gateways); + + if tonumber(self.config.parameters.default_ringtone) then + self.default_ringtone = 'http://amooma.de;info=Ringer' .. self.config.parameters.default_ringtone .. ';x-line-id=0'; + else + self.default_ringtone = 'http://amooma.de;info=Ringer1;x-line-id=0'; + end + + return (self.config ~= nil); +end + + +function Dialplan.hangup(self, code, phrase, cause) + if self.caller:ready() then + if tonumber(code) then + self.caller:respond(code, phrase or 'Thank you for flying Gemeinschaft5'); + end + self.caller:hangup(cause or 16); + else + self.log:info('HANGUP - caller sesson down - cause: ', self.caller.session:hangupCause()); + end +end + + +function Dialplan.check_auth(self) + local authenticated = false; + + require 'common.str' + if self.caller.from_node then + self.log:info('AUTH_FIRST_STAGE - node authenticated - node_id: ', self.caller.node_id); + authenticated = true; + elseif not common.str.blank(self.caller.auth_account_type) then + self.log:info('AUTH_FIRST_STAGE - sipaccount autheticated by name/password: ', self.caller.auth_account_type, '=', self.caller.account_id, '/', self.caller.account_uuid); + authenticated = true; + elseif self.caller.from_gateway then + self.log:info('AUTH_FIRST_STAGE - gateway autheticated by name/password: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name); + authenticated = true; + else + local gateways = common.configuration_file.get('/opt/freeswitch/scripts/ini/gateways.ini', false); + if not gateways then + return false; + end + for gateway, gateway_parameters in pairs(gateways) do + if common.str.to_s(gateway_parameters.proxy) == self.caller.sip_contact_host then + self.caller.gateway_name = gateway; + self.caller.from_gateway = true; + self.log:info('AUTH_FIRST_STAGE - gateway autheticated by ip: gateway=', self.caller.gateway_id, ', name: ', self.caller.gateway_name, ', ip: ', self.caller.sip_contact_host); + authenticated = true; + end + end + end + + return authenticated; +end + + +function Dialplan.check_auth_node(self) + require 'common.node' + local node = common.node.Node:new{ log = self.log, database = self.database }:find_by_address(self.caller.sip_contact_host); + + return (node ~= nil); +end + + +function Dialplan.check_auth_ip(self) + self.log:info('AUTH - node: ', self.caller.from_node, ', auth_account: ', self.caller.auth_account_type, ', gateway: ', self.caller.from_gateway); + require 'common.str' + if self.caller.from_node then + return true; + elseif not common.str.blank(self.caller.auth_account_type) then + return true; + elseif self.caller.from_gateway then + return true; + else + return nil; + end +end + + +function Dialplan.object_find(self, class, identifier, auth_name) + require 'common.str' + class = common.str.downcase(class); + + if class == 'user' then + require 'dialplan.user' + local user = nil; + if type(identifier) == 'number' then + user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(identifier); + else + user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_uuid(identifier); + end + + if user then + user.groups = user:list_groups(); + end + + return user; + elseif class == 'tenant' then + require 'dialplan.tenant' + local tenant = nil; + if type(identifier) == 'number' then + tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_id(identifier); + else + tenant = dialplan.tenant.Tenant:new{ log = self.log, database = self.database }:find_by_uuid(identifier); + end + + return tenant; + elseif class == 'sipaccount' then + require 'common.sip_account' + local sip_account = nil; + if auth_name then + sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(auth_name, identifier); + elseif type(identifier) == 'number' then + sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(identifier); + else + sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_uuid(identifier); + end + if sip_account then + sip_account.owner = self:object_find(sip_account.record.sip_accountable_type, tonumber(sip_account.record.sip_accountable_id)); + end + return sip_account; + elseif class == 'huntgroup' then + require 'dialplan.hunt_group' + + local hunt_group = nil; + if type(identifier) == 'number' then + hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(identifier); + else + hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_uuid(identifier); + end + + if hunt_group then + hunt_group.owner = self:object_find('tenant', tonumber(hunt_group.record.tenant_id)); + end + + return hunt_group; + elseif class == 'automaticcalldistributor' then + require 'dialplan.acd' + + local acd = nil; + if type(identifier) == 'number' then + acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_id(identifier); + else + acd = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }:find_by_uuid(identifier); + end + + if acd then + acd.owner = self:object_find(acd.record.automatic_call_distributorable_type, tonumber(acd.record.automatic_call_distributorable_id)); + end + + return acd; + elseif class == 'faxaccount' then + require 'dialplan.fax' + local fax_account = nil; + if type(identifier) == 'number' then + fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(identifier); + else + fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_uuid(identifier); + end + if fax_account then + fax_account.owner = self:object_find(fax_account.record.fax_accountable_type, tonumber(fax_account.record.fax_accountable_id)); + end + + return fax_account; + end +end + + +function Dialplan.retrieve_caller_data(self) + self.caller.caller_phone_numbers_hash = {} + + require 'common.str' + + local dialed_sip_user = self.caller:to_s('dialed_user'); + + -- TODO: Set auth_account on transfer initiated by calling party + if not common.str.blank(dialed_sip_user) then + self.caller.auth_account = self:object_find('sipaccount', self.caller:to_s('dialed_domain'), dialed_sip_user); + self.caller:set_auth_account(self.caller.auth_account); + elseif not common.str.blank(self.caller.auth_account_type) and not common.str.blank(self.caller.auth_account_uuid) then + self.caller.auth_account = self:object_find(self.caller.auth_account_type, self.caller.auth_account_uuid); + self.caller:set_auth_account(self.caller.auth_account); + end + + if self.caller.auth_account then + self.log:info('CALLER_DATA - auth account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid); + if self.caller.auth_account.owner then + self.log:info('CALLER_DATA - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid); + else + self.log:error('CALLER_DATA - auth owner not found'); + end + else + self.log:info('CALLER_DATA - no data - unauthenticated call: ', self.caller.auth_account_type, '/', self.caller.auth_account_uuid); + end + + if not common.str.blank(self.caller.account_type) and not common.str.blank(self.caller.account_uuid) then + self.caller.account = self:object_find(self.caller.account_type, self.caller.account_uuid); + if self.caller.account then + require 'common.phone_number' + self.caller.caller_phone_numbers = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:list_by_owner(self.caller.account.id, self.caller.account.class); + for index, caller_number in ipairs(self.caller.caller_phone_numbers) do + self.caller.caller_phone_numbers_hash[caller_number] = true; + end + self.log:info('CALLER_DATA - caller account: ', self.caller.account.class, '=', self.caller.account.id, '/', self.caller.account.uuid, ', phone_numbers: ', #self.caller.caller_phone_numbers); + if self.caller.account.owner then + self.log:info('CALLER_DATA - caller owner: ', self.caller.account.owner.class, '=', self.caller.account.owner.id, '/', self.caller.account.owner.uuid); + else + self.log:error('CALLER_DATA - caller owner not found'); + end + + if not self.caller.clir then + self.caller:set_caller_id(self.caller.caller_phone_numbers[1], self.caller.account.record.caller_name or self.caller.account.record.name); + end + else + self.log:error('CALLER_DATA - caller account not found: ', self.caller.account_type, '/', self.caller.account_uuid); + end + end +end + + +function Dialplan.destination_new(self, arg) + require 'common.str' + + local destination = { + number = arg.number or '', + type = arg.type or 'unknown', + id = common.str.to_i(arg.id), + uuid = arg.uuid or '', + phone_number = arg.phone_number, + node_id = common.str.to_i(arg.node_id), + call_forwarding = {}, + data = arg.data, + } + + destination.type = common.str.downcase(destination.type); + + if not common.str.blank(destination.number) then + if destination.type == 'unknown' and destination.number:find(DIALPLAN_FUNCTION_PATTERN) then + destination.type = 'dialplanfunction'; + elseif destination.type == 'phonenumber' or destination.type == 'unknown' then + require 'common.phone_number' + destination.phone_number = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination.number); + + if destination.phone_number then + destination.type = common.str.downcase(destination.phone_number.record.phone_numberable_type); + destination.id = common.str.to_i(destination.phone_number.record.phone_numberable_id); + destination.uuid = common.str.to_s(destination.phone_number.record.phone_numberable_uuid); + destination.node_id = common.str.to_i(destination.phone_number.record.gs_node_id); + if self.caller then + destination.call_forwarding = destination.phone_number:call_forwarding(self.caller.caller_phone_numbers); + end + elseif destination.type == 'unknown' then + require 'common.sip_account' + destination.account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_auth_name(destination.number); + if destination.account then + destination.type = 'sipaccount'; + destination.id = common.str.to_i(destination.account.record.id); + destination.uuid = common.str.to_s(destination.account.record.uuid); + destination.node_id = common.str.to_i(destination.account.record.gs_node_id); + end + end + end + end + + if destination.node_id == 0 then + destination.node_id = self.node_id; + destination.node_local = true; + else + destination.node_local = (destination.node_id == self.node_id); + end + + self.log:info('DESTINATION_NEW - ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, ', number: ', destination.number); + + return destination; +end + + +function Dialplan.routes_get(self, destination) + require 'dialplan.route' + return dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:outbound(self.caller, destination.number); +end + + +function Dialplan.set_caller_picture(self, entry_id, entry_type, image) + entry_type = entry_type:lower(); + if entry_type == 'user' then + require 'dialplan.user' + local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(entry_id); + if user then + self.caller:set_variable('sip_h_Call-Info', '<' .. self.user_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(user.record.image) .. '>;purpose=icon'); + end + elseif entry_type == 'phonebookentry' and image then + self.caller:set_variable('sip_h_Call-Info', '<' .. self.phone_book_entry_image_url .. '/' .. tonumber(entry_id) .. '/snom_caller_picture_' .. tostring(image) .. '>;purpose=icon'); + end +end + + +function Dialplan.dial(self, destination) + require 'common.str' + destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1]; + + if not self.caller.clir then + if destination.node_local and destination.type == 'sipaccount' then + local user_id = nil; + local tenant_id = nil; + + destination.account = self:object_find(destination.type, destination.id); + if destination.account then + if destination.account.class == 'sipaccount' then + destination.callee_id_name = destination.account.record.caller_name; + self.caller:set_callee_id(destination.number, destination.account.record.caller_name); + end + end + + if destination.account and destination.account.owner then + if destination.account.owner.class == 'user' then + user_id = destination.account.owner.id; + tenant_id = tonumber(destination.account.owner.record.current_tenant_id); + elseif destination.account.owner.class == 'tenant' then + tenant_id = destination.account.owner.id; + end + end + + if user_id or tenant_id then + require 'common.str' + local phone_book_entry = nil; + + if self.phonebook_number_lookup then + require 'dialplan.phone_book' + phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant(self.caller.caller_phone_numbers, user_id, tenant_id); + end + + if phone_book_entry then + self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), caller_id_name: ', phone_book_entry.caller_id_name, ', ringtone: ', phone_book_entry.bellcore_id); + destination.caller_id_name = common.str.to_ascii(phone_book_entry.caller_id_name); + if tonumber(phone_book_entry.bellcore_id) then + self.log:debug('RINGTONE - phonebookentry, index: ', phone_book_entry.bellcore_id); + self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. phone_book_entry.bellcore_id .. ';x-line-id=0'); + end + if phone_book_entry.image then + self:set_caller_picture(phone_book_entry.id, 'phonebookentry', phone_book_entry.image); + elseif self.caller.account and self.caller.account.owner then + self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class); + end + elseif self.caller.account and self.caller.account.owner then + self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class); + elseif self.geo_number_lookup then + require 'dialplan.geo_number' + local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.caller_id_number); + if geo_number then + self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country); + if geo_number.name then + destination.caller_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country); + else + destination.caller_id_name = common.str.to_ascii(geo_number.country); + end + end + end + end + end + self.caller:set_caller_id(destination.caller_id_number, destination.caller_id_name or self.caller.caller_id_name); + else + self.caller:set_caller_id('anonymous', 'Unknown'); + self.caller:set_privacy(true); + end + + local destinations = { destination }; + + if self.caller.forwarding_service == 'assistant' and self.caller.auth_account and self.caller.auth_account.class == 'sipaccount' then + self.caller.auth_account.type = self.caller.auth_account.class; + local forwarding_destination = self:destination_new( self.caller.auth_account ); + if forwarding_destination then + forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0' + table.insert(destinations, forwarding_destination); + end + end + + if common.str.to_b(self.config.parameters.bypass_media) then + self.caller:set_variable('bypass_media', true); + end + + require 'dialplan.sip_call' + return dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = self.caller }:fork( + destinations, + { timeout = self.dial_timeout_active, + send_ringing = ( self.send_ringing_to_gateways and self.caller.from_gateway ), + bypass_media_network = self.config.parameters.bypass_media_network, + } + ); +end + + +function Dialplan.huntgroup(self, destination) + local hunt_group = self:object_find('huntgroup', tonumber(destination.id)); + + if not hunt_group then + self.log:error('DIALPLAN_HUNTGROUP - huntgroup not found'); + return { continue = true, code = 404, phrase = 'Huntgroup not found' } + end + + self.caller:set_callee_id(destination.number, hunt_group.record.name); + destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1]; + + if not self.caller.clir then + self.caller:set_caller_id(destination.caller_id_number, tostring(hunt_group.record.name) .. ' '.. tostring(self.caller.caller_id_name)); + if self.caller.account and self.caller.account.owner then + self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class); + end + else + self.caller:set_caller_id('anonymous', tostring(hunt_group.record.name)); + self.caller:set_privacy(true); + end + + self.caller.auth_account = hunt_group; + self.caller:set_auth_account(self.caller.auth_account); + self.caller.forwarding_number = destination.number; + self.caller.forwarding_service = 'huntgroup'; + self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service); + self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number); + return hunt_group:run(self, self.caller, destination); +end + + +function Dialplan.acd(self, destination) + local acd = self:object_find('automaticcalldistributor', tonumber(destination.id)); + + if not acd then + self.log:error('DIALPLAN_ACD - acd not found'); + return { continue = true, code = 404, phrase = 'ACD not found' } + end + + self.caller:set_callee_id(destination.number, acd.record.name); + destination.caller_id_number = destination.caller_id_number or self.caller.caller_phone_numbers[1]; + + if not self.caller.clir then + self.caller:set_caller_id(destination.caller_id_number, tostring(acd.record.name) .. ' '.. tostring(self.caller.caller_id_name)); + if self.caller.account and self.caller.account.owner then + self:set_caller_picture(self.caller.account.owner.id, self.caller.account.owner.class); + end + else + self.caller:set_caller_id('anonymous', tostring(acd.record.name)); + self.caller:set_privacy(true); + end + + self.caller.auth_account = acd; + self.caller:set_auth_account(self.caller.auth_account); + self.caller.forwarding_number = destination.number; + self.caller.forwarding_service = 'automaticcalldistributor'; + self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service); + self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number); + + acd:caller_new(self.caller.uuid); + local result = acd:run(self, self.caller, destination); + acd:caller_delete(); + + return result; +end + + +function Dialplan.conference(self, destination) + -- call local conference + require 'common.conference' + conference = common.conference.Conference:new{ log = self.log, database = self.database }:find_by_id(destination.id); + + if not conference then + return { continue = false, code = 404, phrase = 'Conference not found' } + end + + local cause = conference:enter(self.caller, self.domain); + return { continue = false, cause = cause } +end + + +function Dialplan.faxaccount(self, destination) + require 'dialplan.fax' + local fax_account = dialplan.fax.Fax:new{ log = self.log, database = self.database }:find_by_id(destination.id); + + if not fax_account then + return { continue = false, code = 404, phrase = 'Fax not found' } + end + + self.log:info('FAX_RECEIVE start - fax_account=', fax_account.id, '/', fax_account.uuid, ', name: ', fax_account.record.name, ', station_id: ', fax_account.record.station_id); + + self.caller:set_caller_id(self.caller.caller_phone_number); + self.caller:set_callee_id(destination.number, fax_account.record.name); + + local fax_document = fax_account:receive(self.caller); + + if not fax_document then + self.log:error('FAX_RECEIVE - error receiving fax document - fax_account=', fax_account.id, '/', fax_account.uuid); + return { continue = false, code = 500, phrase = 'Error receiving fax' }; + end + + fax_document.caller_id_number = self.caller.caller_phone_number; + fax_document.caller_id_name = self.caller.caller_id_name; + fax_document.uuid = self.caller.uuid; + + self.log:info('FAX_RECEIVE end - success: ', fax_document.success, + ', remote: ', fax_document.remote_station_id, + ', pages: ', fax_document.transferred_pages, '/', fax_document.total_pages, + ', result: ', fax_document.result_code, ' ', fax_document.result_text); + + if fax_document.success then + self.log:notice('FAX_RECEIVE - saving fax document: ', fax_document.filename ); + if not fax_account:insert_document(fax_document) then + self.log:error('FAX_RECEIVE - error inserting fax document to database - fax_account=', fax_account.id, '/', fax_account.uuid, ', file: ', fax_document.filename); + end + end + + return { continue = false, code = 200, phrase = 'OK' } +end + + +function Dialplan.callthrough(self, destination) + -- Callthrough + require 'dialplan.callthrough' + callthrough = dialplan.callthrough.Callthrough:new{ log = self.log, database = self.database }:find_by_id(destination.id) + + if not callthrough then + self.log:error('CALLTHROUGH - no callthrough for destination number: ', destination.number); + return { continue = false, code = 404, phrase = 'Fax not found' } + end + self.log:info('CALLTHROUGH - number: ' .. destination.number .. ', name: ' .. callthrough.record.name); + + local authorization = callthrough:authenticate(self.caller); + + if not authorization then + self.log:notice('CALLTHROUGH - authentication failed'); + return { continue = false, code = 403, phrase = 'Authentication failed' } + end + + if type(authorization) == 'table' and tonumber(authorization.sip_account_id) and tonumber(authorization.sip_account_id) > 0 then + local auth_account = self:object_find('sipaccount', tonumber(authorization.sip_account_id)); + self.caller.forwarding_number = destination.number; + self.caller.forwarding_service = 'callthrough'; + self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service); + self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number); + + if auth_account then + self.caller.auth_account = auth_account; + self.caller:set_auth_account(self.caller.auth_account); + self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid); + if self.caller.auth_account.owner then + self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid); + else + self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found'); + end + self.log:info('CALLTHROUGH - use sip account: ', auth_account.id, ' (', auth_account.record.caller_name, ')'); + end + else + self.log:info('CALLTHROUGH - no sip account'); + end + + local destination_number = ''; + for i = 1, 3, 1 do + if destination_number ~= '' then + break; + end + destination_number = session:read(2, 16, "ivr/ivr-enter_destination_telephone_number.wav", 3000, "#"); + end + + if destination_number == '' then + self.log:debug("no callthrough destination - hangup call"); + return { continue = false, code = 404, phrase = 'No destination' } + end + + local route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, destination_number); + if route and route.value then + destination_number = route.value; + end + + if not callthrough:whitelist(destination_number) then + self.log:debug('caller not authorized to call destination number: ' .. destination_number .. ' - hangup call'); + return { continue = false, code = 403, phrase = 'Unauthorized' } + end + + return { continue = true, code = 302, number = destination_number } +end + + +function Dialplan.voicemail(self, destination) + if not self.caller.auth_account or self.caller.auth_account.class ~= 'sipaccount' then + self.log:error('VOICEMAIL - incompatible destination'); + return { continue = false, code = 404, phrase = 'Mailbox not found' } + end + + require 'dialplan.voicemail' + local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(self.caller.auth_account.id); + + if not voicemail_account then + self.log:error('VOICEMAIL - no mailbox'); + return { continue = false, code = 404, phrase = 'Mailbox not found' } + end + + voicemail_account:leave(self.caller, self.caller.forwarding_number); + + if self.caller:to_s("voicemail_message_len") == '' then + self.log:info('VOICEMAIL - no message saved'); + end + + return { continue = false, code = 200 } +end + + +function Dialplan.dialplanfunction(self, destination) + require 'dialplan.functions' + return dialplan.functions.Functions:new{ log = self.log, database = self.database, domain = self.domain }:dialplan_function(self.caller, destination.number); +end + + +function Dialplan.switch(self, destination) + require 'common.str' + local result = nil; + self.dial_timeout_active = self.dial_timeout; + + if not destination.node_local then + return self:dial(destination); + end + + for service, call_forwarding in pairs(destination.call_forwarding) do + if self.caller.caller_phone_numbers_hash[call_forwarding.number] then + self.log:info('CALL_FORWARDING - caller number equals destination: ', call_forwarding.number,' - ignore service: ', service); + destination.call_forwarding[service] = nil; + end + end + + if destination.call_forwarding.noanswer then + self.dial_timeout_active = tonumber(destination.call_forwarding.noanswer.timeout) or self.dial_timeout; + end + + if destination.call_forwarding.always then + return { continue = true, call_forwarding = destination.call_forwarding.always } + elseif destination.call_forwarding.assistant then + if common.str.downcase(destination.call_forwarding.assistant.type) == 'huntgroup' then + require 'dialplan.hunt_group' + local hunt_group = dialplan.hunt_group.HuntGroup:new{ log = self.log, database = self.database }:find_by_id(destination.call_forwarding.assistant.id); + self.log:info('CALL_FORWARDING - huntgroup - auth_account: ', self.caller.auth_account_type, '=', self.caller.auth_account_uuid); + if hunt_group and (hunt_group:is_member_by_numbers(self.caller.caller_phone_numbers)) then + self.log:info('CALL_FORWARDING - caller is huntgroup member - ignore service: ', destination.call_forwarding.assistant.service); + else + return { continue = true, call_forwarding = destination.call_forwarding.assistant } + end + else + return { continue = true, call_forwarding = destination.call_forwarding.assistant } + end + end + + -- reset ringtone + self.caller:export_variable('alert_info', self.default_ringtone); + + if destination.phone_number then + local ringtone = destination.phone_number:ringtone(); + if ringtone and ringtone.bellcore_id then + self.log:debug('RINGTONE - ', ringtone.ringtoneable_type .. ', index: ' .. ringtone.bellcore_id); + self.caller:export_variable('alert_info', 'http://amooma.de;info=Ringer' .. tonumber(ringtone.bellcore_id) .. ';x-line-id=0'); + end + end + + if destination.type == 'sipaccount' then + result = self:dial(destination); + if CALL_FORWARDING_SERVICES[result.disposition] then + result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]]; + if result.call_forwarding then + result.continue = true; + end + end + return result; + elseif destination.type == 'conference' then + return self:conference(destination); + elseif destination.type == 'faxaccount' then + return self:faxaccount(destination); + elseif destination.type == 'callthrough' then + return self:callthrough(destination); + elseif destination.type == 'huntgroup' then + result = self:huntgroup(destination); + if CALL_FORWARDING_SERVICES[result.disposition] then + result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]]; + if result.call_forwarding then + result.continue = true; + end + end + return result; + elseif destination.type == 'automaticcalldistributor' then + result = self:acd(destination); + if CALL_FORWARDING_SERVICES[result.disposition] then + result.call_forwarding = destination.call_forwarding[CALL_FORWARDING_SERVICES[result.disposition]]; + if result.call_forwarding then + result.continue = true; + end + end + return result; + elseif destination.type == 'voicemail' then + return self:voicemail(destination); + elseif destination.type == 'dialplanfunction' then + return self:dialplanfunction(destination); + elseif not common.str.blank(destination.number) then + local result = { continue = false, code = 404, phrase = 'No route' } + local routes = self:routes_get(destination); + + if not routes or #routes == 0 then + self.log:notice('SWITCH - no route - number: ', destination.number); + return { continue = false, code = 404, phrase = 'No route' } + end + + destination.callee_id_number = destination.number; + destination.callee_id_name = nil; + + if self.phonebook_number_lookup then + require 'common.str' + local user_id = common.str.try(self.caller, 'account.owner.id'); + local tenant_id = common.str.try(self.caller, 'account.owner.record.current_tenant_id'); + + if user_id or tenant_id then + require 'dialplan.phone_book' + local phone_book_entry = dialplan.phone_book.PhoneBook:new{ log = self.log, database = self.database }:find_entry_by_number_user_tenant({ destination.number }, user_id, tenant_id); + if phone_book_entry then + self.log:info('PHONE_BOOK_ENTRY - phone_book=', phone_book_entry.phone_book_id, ' (', phone_book_entry.phone_book_name, '), callee_id_name: ', common.str.to_ascii(phone_book_entry.caller_id_name)); + destination.callee_id_name = common.str.to_ascii(phone_book_entry.caller_id_name); + end + end + end + + if self.geo_number_lookup and not destination.callee_id_name then + require 'dialplan.geo_number' + local geo_number = dialplan.geo_number.GeoNumber:new{ log = self.log, database = self.database }:find(destination.number); + if geo_number then + require 'common.str' + self.log:info('GEO_NUMBER - found: ', geo_number.name, ', ', geo_number.country); + if geo_number.name then + destination.callee_id_name = common.str.to_ascii(geo_number.name) .. ', ' .. common.str.to_ascii(geo_number.country); + else + destination.callee_id_name = common.str.to_ascii(geo_number.country); + end + end + end + + self.caller:set_callee_id(destination.callee_id_number, destination.callee_id_name); + + for index, route in ipairs(routes) do + if route.class == 'hangup' then + return { continue = false, code = route.endpoint, phrase = route.phrase, cause = route.value } + end + if route.class == 'forward' then + return { continue = true, call_forwarding = { number = route.value, service = 'route', type = 'phonenumber' }} + end + destination.gateway = route.endpoint; + destination.type = route.class; + destination.number = route.value; + destination.caller_id_number = route.caller_id_number; + destination.caller_id_name = route.caller_id_name; + result = self:dial(destination); + + if result.continue == false then + break; + end + + if common.str.to_b(self.routes.failover[tostring(result.code)]) == true then + self.log:info('SWITCH - failover - code: ', result.code); + elseif common.str.to_b(self.routes.failover[tostring(result.cause)]) == true then + self.log:info('SWITCH - failover - cause: ', result.cause); + else + self.log:info('SWITCH - no failover - cause: ', result.cause, ', code: ', result.code); + break; + end + end + + return result; + end + + self.log:error('SWITCH - destination not found - type: ', destination.type); + return { continue = true, code = 404, phrase = destination.type .. ' not found' } +end + + +function Dialplan.run(self, destination) + self.caller:set_variable('hangup_after_bridge', false); + self.caller:set_variable('ringback', self.config.parameters.ringback); + self.caller:set_variable('bridge_early_media', 'true'); + self.caller:set_variable('send_silence_when_idle', 0); + self.caller:set_variable('default_language', self.default_language); + self.caller:set_variable('gs_save_cdr', true); + self.caller:set_variable('gs_call_service', 'dial'); + self.caller.session:setAutoHangup(false); + + self.routes = common.configuration_file.get('/opt/freeswitch/scripts/ini/routes.ini'); + self.caller.domain_local = self.domain; + self:retrieve_caller_data(); + + if not destination or destination.type == 'unknown' then + require 'dialplan.route' + local route = nil; + + if self.caller.from_gateway then + local route_object = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }; + route = route_object:inbound(self.caller, self.caller.destination_number); + local inbound_caller_id_number = route_object:inbound_cid_number(self.caller, self.caller.gateway_name, 'gateway'); + route_object.expandable.caller_id_number = inbound_caller_id_number; + local inbound_caller_id_name = route_object:inbound_cid_name(self.caller, self.caller.gateway_name, 'gateway'); + self.log:info('INBOUND_CALLER_ID_REWRITE - number: ', inbound_caller_id_number, ', name: ', inbound_caller_id_name); + self.caller.caller_id_number = inbound_caller_id_number or self.caller.caller_id_number; + self.caller.caller_id_name = inbound_caller_id_name or self.caller.caller_id_name; + self.caller.caller_phone_numbers[1] = self.caller.caller_id_number; + else + route = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = self.routes }:prerouting(self.caller, self.caller.destination_number); + end + + if route then + destination = self:destination_new{ number = route.value } + self.caller.destination_number = destination.number; + self.caller.destination = destination; + elseif not destination or destination.type == 'unknown' then + destination = self:destination_new{ number = self.caller.destination_number } + self.caller.destination = destination; + end + end + + self.log:info('DIALPLAN start - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"', + ', number: ', destination.number); + + local result = { continue = false }; + local loop = self.caller.loop_count; + while self.caller:ready() and loop < self.max_loops do + loop = loop + 1; + self.caller.loop_count = loop; + + self.log:info('LOOP ', loop, + ' - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, + ', number: ', destination.number); + + self.caller:set_variable('gs_destination_type', destination.type); + self.caller:set_variable('gs_destination_id', destination.id); + self.caller:set_variable('gs_destination_uuid', destination.uuid); + self.caller:set_variable('gs_destination_number', destination.number); + self.caller:set_variable('gs_destination_node_local', destination.node_local); + + result = self:switch(destination); + result = result or { continue = false, code = 502, cause = 'DESTINATION_OUT_OF_ORDER', phrase = 'Destination out of order' } + + if result.call_service then + self.caller:set_variable('gs_call_service', result.call_service); + end + + if not result.continue then + break; + end + + if result.call_forwarding then + self.log:info('LOOP ', loop, ' CALL_FORWARDING - service: ', result.call_forwarding.service, + ', destination: ', result.call_forwarding.type, '=', result.call_forwarding.id, + ', number: ', result.call_forwarding.number); + + local auth_account = self:object_find(destination.type, destination.id); + self.caller.forwarding_number = destination.number; + self.caller.forwarding_service = result.call_forwarding.service; + self.caller:set_variable('gs_forwarding_service', self.caller.forwarding_service); + self.caller:set_variable('gs_forwarding_number', self.caller.forwarding_number); + + if auth_account then + self.caller.auth_account = auth_account; + self.caller:set_auth_account(self.caller.auth_account); + self.log:info('AUTH_ACCOUNT_UPDATE - account: ', self.caller.auth_account.class, '=', self.caller.auth_account.id, '/', self.caller.auth_account.uuid); + if self.caller.auth_account.owner then + self.log:info('AUTH_ACCOUNT_UPDATE - auth owner: ', self.caller.auth_account.owner.class, '=', self.caller.auth_account.owner.id, '/', self.caller.auth_account.owner.uuid); + else + self.log:error('AUTH_ACCOUNT_UPDATE - auth owner not found'); + end + end + + destination = self:destination_new(result.call_forwarding); + self.caller.destination = destination; + + if not result.no_cdr and auth_account then + require 'common.call_history' + common.call_history.CallHistory:new{ log = self.log, database = self.database }:insert_forwarded( + self.caller.uuid, + auth_account.class, + auth_account.id, + self.caller, + destination, + result + ); + end + end + + if result.number then + self.log:info('LOOP ', loop, ' NEW_DESTINATION_NUMBER - number: ', result.number ); + destination = self:destination_new{ number = result.number } + self.caller.destination = destination; + end + end + + if loop >= self.max_loops then + result = { continue = false, code = 483, cause = 'EXCHANGE_ROUTING_ERROR', phrase = 'Too many hops' } + end + + self.log:info('DIALPLAN end - caller_id: ',self.caller.caller_id_number, ' "', self.caller.caller_id_name,'"', + ', destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, + ', number: ', destination.number, ', result: ', result.code, ' ', result.phrase); + + if self.caller:ready() then + self:hangup(result.code, result.phrase, result.cause); + end + + self.caller:set_variable('gs_save_cdr', not result.no_cdr); +end diff --git a/misc/freeswitch/scripts/dialplan/fax.lua b/misc/freeswitch/scripts/dialplan/fax.lua new file mode 100644 index 0000000..2a40620 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/fax.lua @@ -0,0 +1,232 @@ +-- Gemeinschaft 5 module: fax class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +FAX_DOCUMENTS_DIRECTORY = '/tmp/' +FAX_PARALLEL_MAX = 8; +Fax = {} + +-- Create Fax object +function Fax.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.class = 'faxaccount'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + self.fax_directory = arg.fax_directory or FAX_DOCUMENTS_DIRECTORY; + return object; +end + +-- find fax account by id +function Fax.find_by_id(self, id) + local sql_query = 'SELECT * FROM `fax_accounts` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1'; + local fax_account = nil; + + self.database:query(sql_query, function(fax_entry) + fax_account = Fax:new(self); + fax_account.record = fax_entry; + fax_account.id = tonumber(fax_entry.id); + fax_account.uuid = fax_entry.uuid; + end) + + return fax_account; +end + + +-- find fax account by uuid +function Fax.find_by_uuid(self, uuid) + local sql_query = 'SELECT * FROM `fax_accounts` WHERE `uuid` = "' .. uuid .. '" LIMIT 1'; + local fax_account = nil; + + self.database:query(sql_query, function(fax_entry) + fax_account = Fax:new(self); + fax_account.record = fax_entry; + fax_account.id = tonumber(fax_entry.id); + fax_account.uuid = fax_entry.uuid; + end) + + return fax_account; +end + + +function Fax.destination_numbers(self, id) + local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id` = ' .. tonumber(id); + local destination_numbers = {} + + self.database:query(sql_query, function(fax_entry) + table.insert(destination_numbers, fax_entry.number); + end) + + return destination_numbers; +end + +function Fax.destination_number(self, id) + local sql_query = 'SELECT `number` FROM `phone_numbers` WHERE `phone_numberable_type` = "FaxDocument" AND `phone_numberable_id`= ' .. tonumber(id) .. ' LIMIT 1'; + local destination_number = nil; + + self.database:query(sql_query, function(fax_entry) + destination_number = fax_entry.number; + end) + + return destination_number; +end + +-- List waiting fax documents +function Fax.queued_for_sending(self, limit) + limit = limit or FAX_PARALLEL_MAX; + local sql_query = 'SELECT * FROM `fax_documents` WHERE `state` IN ("queued_for_sending","unsuccessful") AND `retry_counter` > 0 ORDER BY `sent_at` ASC LIMIT ' .. limit; + local fax_documents = {} + self.database:query(sql_query, function(fax_entry) + fax_entry['destination_numbers'] = Fax:destination_numbers(fax_entry.id) + table.insert(fax_documents, fax_entry); + end) + + return fax_documents; +end + +-- Update fax document sending status +function Fax.document_update(self, id, params) + require 'common.str' + local params_sql = {} + + for name, value in pairs(params) do + table.insert(params_sql, '`' .. name .. '`=' .. common.str.to_sql(value)); + end + + if not params['sent_at'] then + table.insert(params_sql, '`sent_at`=NOW()'); + end + + if not params['updated_at'] then + table.insert(params_sql, '`updated_at`=NOW()'); + end + + local sql_query = 'UPDATE `fax_documents` SET ' .. table.concat(params_sql, ',') .. ' WHERE `id` = ' .. tonumber(id); + + return self.database:query(sql_query); +end + + +function Fax.get_parameters(self, caller) + local fax_parameters = { + bad_rows = caller:to_i('fax_bad_rows'), + total_pages = caller:to_i('fax_document_total_pages'), + transferred_pages = caller:to_i('fax_document_transferred_pages'), + ecm_requested = caller:to_b('fax_ecm_requested'), + ecm_used = caller:to_b('fax_ecm_used'), + filename = caller:to_s('fax_filename'), + image_resolution = caller:to_s('fax_image_resolution'), + image_size = caller:to_i('fax_image_size'), + local_station_id = caller:to_s('fax_local_station_id'), + result_code = caller:to_i('fax_result_code'), + result_text = caller:to_s('fax_result_text'), + remote_station_id = caller:to_s('fax_remote_station_id'), + success = caller:to_b('fax_success'), + transfer_rate = caller:to_i('fax_transfer_rate'), + v17_disabled = caller:to_b('fax_v17_disabled'), + } + + return fax_parameters; +end + +-- Receive Fax +function Fax.receive(self, caller, file_name) + file_name = file_name or self.fax_directory .. 'fax_in_' .. caller.uuid .. '.tiff'; + + caller:set_variable('fax_ident', self.record.station_id) + caller:set_variable('fax_verbose', 'false') + + caller:answer(); + local start_time = os.time(); + caller:execute('rxfax', file_name); + local record = self:get_parameters(caller); + record.transmission_time = os.time() - start_time; + return record; +end + +-- Send Fax +function Fax.send(self, caller, file_name) + caller:set_variable('fax_ident', self.record.station_id) + caller:set_variable('fax_header', self.record.name) + caller:set_variable('fax_verbose', 'false') + local start_time = os.time(); + caller:execute('txfax', file_name); + local record = self:get_parameters(caller); + record.transmission_time = os.time() - start_time; + return record; +end + +-- find fax document by id +function Fax.find_document_by_id(self, id) + local sql_query = 'SELECT * FROM `fax_documents` WHERE `id` = ' .. tonumber(id) .. ' LIMIT 1' + local record = nil + + self.database:query(sql_query, function(fax_entry) + record = fax_entry; + end); + + return record; +end + +-- save fax document to database +function Fax.insert_document(self, record) + require 'common.str' + local sql_query = 'INSERT INTO `fax_documents` ( \ + inbound, \ + retry_counter, \ + fax_resolution_id, \ + state, \ + transmission_time, \ + sent_at, \ + document_total_pages, \ + document_transferred_pages, \ + ecm_requested, \ + ecm_used, \ + image_resolution, \ + image_size, \ + local_station_id, \ + result_code, \ + remote_station_id, \ + success, \ + transfer_rate, \ + created_at, \ + updated_at, \ + fax_account_id, \ + caller_id_number, \ + caller_id_name, \ + tiff, \ + uuid \ + ) VALUES ( \ + true, \ + 0, \ + 1, \ + "received", \ + ' .. common.str.to_sql(record.transmission_time) .. ', \ + NOW(), \ + ' .. common.str.to_sql(record.total_pages) .. ', \ + ' .. common.str.to_sql(record.transferred_pages) .. ', \ + ' .. common.str.to_sql(record.ecm_requested) .. ', \ + ' .. common.str.to_sql(record.ecm_used) .. ', \ + ' .. common.str.to_sql(record.image_resolution) .. ', \ + ' .. common.str.to_sql(record.image_size) .. ', \ + ' .. common.str.to_sql(record.local_station_id) .. ', \ + ' .. common.str.to_sql(record.result_code) .. ', \ + ' .. common.str.to_sql(record.remote_station_id) .. ', \ + ' .. common.str.to_sql(record.success) .. ', \ + ' .. common.str.to_sql(record.transfer_rate) .. ', \ + NOW(), \ + NOW(), \ + ' .. common.str.to_sql(self.id) .. ', \ + ' .. common.str.to_sql(record.caller_id_number) .. ', \ + ' .. common.str.to_sql(record.caller_id_name) .. ', \ + ' .. common.str.to_sql(record.filename) .. ', \ + ' .. common.str.to_sql(record.uuid) .. ' \ + )'; + + return self.database:query(sql_query); +end diff --git a/misc/freeswitch/scripts/dialplan/functions.lua b/misc/freeswitch/scripts/dialplan/functions.lua new file mode 100644 index 0000000..c104f89 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/functions.lua @@ -0,0 +1,839 @@ +-- DialplanModule: Functions +-- +module(...,package.seeall) + +Functions = {} + +-- Create Functions object +function Functions.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log + self.database = arg.database + self.domain = arg.domain + return object +end + +function Functions.ensure_caller_sip_account(self, caller) + if caller.account and caller.account.class == 'sipaccount' then + return caller.account; + end +end + +function Functions.dialplan_function(self, caller, dialed_number) + require 'common.str' + local parameters = common.str.to_a(dialed_number, '%-'); + if not parameters[2] then + return { continue = false, code = 484, phrase = 'Malformed function parameters', no_cdr = true }; + end + local fid = tostring(parameters[2]); + local result = { continue = false, code = 404, phrase = 'Function not found', no_cdr = true }; + + self.log:debug('DIALPLAN_DUNCTION - execute: ', dialed_number); + + if fid == "ta" then + result = self:transfer_all(caller, parameters[3]); + elseif fid == "in" then + result = self:intercept_extensions(caller, parameters[3]); + elseif fid == "ia" then + result = self:intercept_any_extension(caller, parameters[3]); + elseif fid == "anc" then + result = self:account_node_change(caller); + elseif fid == "li" then + result = self:user_login(caller, parameters[3], parameters[4]); + elseif fid == "lo" then + result = self:user_logout(caller); + elseif fid == "lir" then + result = self:user_login_redirect(caller, parameters[3], parameters[4]); + elseif fid == "loaon" then + result = self:user_auto_logout(caller, true); + elseif fid == "loaoff" then + result = self:user_auto_logout(caller, false); + elseif fid == "dcliroff" then + result = self:dial_clir_off(caller, parameters[3]); + elseif fid == "dcliron" then + result = self:dial_clir_on(caller, parameters[3]); + elseif fid == "clipon" then + result = self:clip_on(caller); + elseif fid == "clipoff" then + result = self:clip_off(caller); + elseif fid == "cwaoff" then + result = self:callwaiting_off(caller); + elseif fid == "cwaon" then + result = self:callwaiting_on(caller); + elseif fid == "cfoff" then + result = self:call_forwarding_off(caller); + elseif fid == "cfdel" then + result = self:call_forwarding_off(caller, nil, true); + elseif fid == "cfu" then + result = self:call_forwarding_on(caller, 'always', parameters[3], 'PhoneNumber'); + elseif fid == "cfuoff" then + result = self:call_forwarding_off(caller, 'always'); + elseif fid == "cfudel" then + result = self:call_forwarding_off(caller, 'always', true); + elseif fid == "cfutg" then + result = self:call_forwarding_toggle(caller, 'always', parameters[3]); + elseif fid == "cfn" then + result = self:call_forwarding_on(caller, 'noanswer', parameters[3], 'PhoneNumber', parameters[4]); + elseif fid == "cfnoff" then + result = self:call_forwarding_off(caller, 'noanswer'); + elseif fid == "cfndel" then + result = self:call_forwarding_off(caller, 'noanswer', true); + elseif fid == "cfo" then + result = self:call_forwarding_on(caller, 'offline', parameters[3], 'PhoneNumber'); + elseif fid == "cfooff" then + result = self:call_forwarding_off(caller, 'offline'); + elseif fid == "cfodel" then + result = self:call_forwarding_off(caller, 'offline', true); + elseif fid == "cfb" then + result = self:call_forwarding_on(caller, 'busy', parameters[3], 'PhoneNumber'); + elseif fid == "cfboff" then + result = self:call_forwarding_off(caller, 'busy'); + elseif fid == "cfbdel" then + result = self:call_forwarding_off(caller, 'busy', true); + elseif fid == "vmleave" then + result = self:voicemail_message_leave(caller, parameters[3]); + elseif fid == "vmcheck" then + result = self:voicemail_check(caller, parameters[3]); + elseif fid == "vmtg" then + result = self:call_forwarding_toggle(caller, nil, parameters[3]); + elseif fid == "acdmtg" then + result = self:acd_membership_toggle(caller, parameters[3], parameters[4]); + elseif fid == "e164" then + result = "+" .. tostring(parameters[3]); + elseif fid == "hangup" then + result = self:hangup(caller, parameters[3], parameters[4]); + end + + return result; +end + +-- Transfer all calls to a conference +function Functions.transfer_all(self, caller, destination_number) + self.log:info('TRANSFER_ALL - caller: ', caller.account_type, '/', caller.account_uuid, ' number: ', destination_number); + + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + self.log:error('TRANSFER_ALL - incompatible caller'); + return { continue = false, code = 403, phrase = 'Incompatible caller' } + end + + -- Query call and channel table for channel IDs + local sql_query = 'SELECT `b`.`name` AS `caller_chan_name`, `a`.`caller_uuid`, `a`.`callee_uuid` \ + FROM `calls` `a` JOIN `channels` `b` ON `a`.`caller_uuid` = `b`.`uuid` JOIN `channels` `c` \ + ON `a`.`callee_uuid` = `c`.`uuid` WHERE `b`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") \ + OR `c`.`name` LIKE ("%' .. caller_sip_account.record.auth_name .. '@%") LIMIT 100'; + + self.database:query(sql_query, function(call_entry) + local uid = nil + if call_entry.caller_chan_name:find(caller_sip_account.record.auth_name .. "@") then + uid = call_entry.callee_uuid; + self.log:debug("Transfering callee channel with uid: " .. uid); + else + uid = call_entry.caller_uuid; + self.log:debug("Transfering caller channel with uid: " .. uid); + end + freeswitch.API():execute("uuid_transfer", uid .. " " .. destination_number); + end) + + return destination_number; +end + +-- Intercept Extensions +function Functions.intercept_extensions(self, caller, destination_numbers) + if type(destination_numbers) == "string" then + destination_numbers = "\"" .. destination_numbers .. "\""; + else + destination_numbers = "\"" .. table.concat(destination_numbers, "\",\"") .. "\""; + end + + self.log:debug("Intercept call to number(s): " .. destination_numbers); + + if caller.account_type ~= "SipAccount" then + self.log:error("caller is not a SipAccount"); + return { continue = false, code = 403, phrase = 'Incompatible caller' } + end + + local sql_query = 'SELECT * FROM `channels` WHERE `callstate` IN ("EARLY", "ACTIVE") AND `dest` IN (' .. destination_numbers .. ') LIMIT 1'; + + self.database:query(sql_query, function(call_entry) + self.log:debug("intercepting call with uid: " .. call_entry.uuid); + caller:intercept(call_entry.uuid); + end) + + return nil; +end + +-- intercept call to destination (e.g. sip_account) +function Functions.intercept_destination(self, caller, destination) + self.log:debug("Intercept call to destination " .. destination); + local result = false; + local sql_query = 'SELECT `call_uuid`, `uuid` FROM `channels` WHERE `callstate` = "RINGING" AND `dest` = "' .. destination .. '" LIMIT 1'; + + caller:set_caller_id(caller.caller_phone_numbers[1] ,caller.caller_id_name); + self.database:query(sql_query, function(call_entry) + if call_entry.call_uuid and tostring(call_entry.call_uuid) then + self.log:debug("intercepting call - uuid: " .. call_entry.call_uuid); + caller:intercept(call_entry.call_uuid); + result = { continue = false, code = 200, call_service = 'pickup' } + require 'common.str' + require 'common.fapi' + local fapi = common.fapi.FApi:new{ log = self.log, uuid = call_entry.call_uuid } + if fapi:channel_exists() then + caller:set_caller_id( + common.str.to_s(fapi:get_variable('effective_caller_id_number')), + common.str.to_s(fapi:get_variable('effective_caller_id_name')) + ); + caller:set_callee_id( + common.str.to_s(fapi:get_variable('effective_callee_id_number')), + common.str.to_s(fapi:get_variable('effective_callee_id_name')) + ); + + caller:set_variable('gs_destination_type', fapi:get_variable('gs_destination_type')); + caller:set_variable('gs_destination_id', fapi:get_variable('gs_destination_id')); + caller:set_variable('gs_destination_uuid', fapi:get_variable('gs_destination_uuid')); + + caller:set_variable('gs_caller_account_type', fapi:get_variable('gs_account_type')); + caller:set_variable('gs_caller_account_id', fapi:get_variable('gs_account_id')); + caller:set_variable('gs_caller_account_uuid', fapi:get_variable('gs_account_uuid')); + + caller:set_variable('gs_auth_account_type', fapi:get_variable('gs_auth_account_type')); + caller:set_variable('gs_auth_account_id', fapi:get_variable('gs_auth_account_id')); + caller:set_variable('gs_auth_account_uuid', fapi:get_variable('gs_auth_account_uuid')); + end + else + self.log:error('FUNCTION - failed to intercept call - no caller uuid for callee uuid: ', call_entry.uuid); + end + end) + + return result; +end + +-- intercept call to owner of destination_number +function Functions.intercept_any_extension(self, caller, destination_number) + require 'common.phone_number' + local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }:find_by_number(destination_number); + + if not phone_number_object or not phone_number_object.record then + self.log:notice("unallocated number: " .. tostring(destination_number)); + return false; + end + + if phone_number_object.record.phone_numberable_type == 'SipAccount' then + require "common.sip_account" + local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database } + local sip_account = sip_account_class:find_by_id(phone_number_object.record.phone_numberable_id) + if sip_account then + return self:intercept_destination(caller, sip_account.record.auth_name); + end + end +end + + +function Functions.account_node_change(self, caller) + self.log:info('NODE_CHANGE - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number); + + -- find caller's sip account + local caller_sip_account = caller.account; + if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then + self.log:notice('LOGIN - caller sip_account not found'); + return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true } + end + + require 'phones.phone' + local phone_class = phones.phone.Phone:new{log = self.log, database = self.database} + + -- logout caller phones if caller account is hot-deskable + local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.record.id); + for index, phone_caller in ipairs(caller_phones) do + phone_caller:logout(caller_sip_account.record.id); + end + + self:update_node_change(caller_sip_account, caller.local_node_id); + caller:answer(); + caller:send_display('Change successful'); + caller.session:sayPhrase('logged_in'); + + -- resync caller phones + for index, phone_caller in ipairs(caller_phones) do + local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain }; + self.log:info('NODE_CHANGE - resync phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result); + end + + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.user_login(self, caller, number, pin) + require 'common.str' + + local PHONE_NUMBER_LEN_MIN = 4; + local PHONE_NUMBER_LEN_MAX = 12; + local PIN_LEN_MIN = 4; + local PIN_LEN_MAX = 12; + + caller:set_variable('destination_number', 'f-li-' .. common.str.to_s(number) .. '-PIN'); + self.log:info('LOGIN - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number, ', number: ', number); + + if common.str.blank(number) then + number = caller.session:read(PHONE_NUMBER_LEN_MIN, PHONE_NUMBER_LEN_MAX, 'ivr/ivr-please_enter_extension_followed_by_pound.wav', 3000, '#'); + end + + -- find caller's sip account + local caller_sip_account = caller.account; + if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then + self.log:notice('LOGIN - caller sip_account not found'); + return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true } + end + + require 'phones.phone' + local phone_class = phones.phone.Phone:new{log = self.log, database = self.database} + + local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id); + local caller_phone = caller_phones[1]; + + if not caller_phone then + self.log:notice('LOGIN - caller phone not found or not hot-deskable'); + return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true } + end + + require 'common.phone_number' + local phone_number = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_by_number(number, {"SipAccount"}); + + if not phone_number then + self.log:notice('LOGIN - number not found or not linked to a sip account - number: ', number); + return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true } + end + + require 'common.sip_account' + local destination_sip_account = common.sip_account.SipAccount:new{ log = self.log, database = self.database }:find_by_id(phone_number.record.phone_numberable_id); + + if not destination_sip_account then + self.log:notice('LOGIN - account not found - ', phone_number.record.phone_numberable_type, '=', phone_number.record.phone_numberable_id, ', number: ', number); + return { continue = false, code = 404, phrase = 'Account not found', no_cdr = true } + end + + self.log:info('LOGIN - destination: ', phone_number.record.phone_numberable_type, '=', destination_sip_account.record.id, + ', caller_name: ', destination_sip_account.record.caller_name, ', hotdeskable: ', destination_sip_account.record.hotdeskable); + + if not common.str.to_b(destination_sip_account.record.hotdeskable) then + self.log:notice('LOGIN - destination sip_account not hot-deskable'); + return { continue = false, code = 404, phrase = 'Destination not hot-deskable', no_cdr = true } + end + + require 'dialplan.user' + local user = dialplan.user.User:new{ log = self.log, database = self.database }:find_by_id(destination_sip_account.record.sip_accountable_id); + + if common.str.blank(pin) then + pin = caller.session:read(PIN_LEN_MIN, PIN_LEN_MAX, 'ivr/ivr-please_enter_pin_followed_by_pound.wav', 3000, '#'); + end + + if not user then + self.log:notice('LOGIN - user not found - ', destination_sip_account.record.sip_accountable_type, '=',destination_sip_account.record.sip_accountable_id); + return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true } + end + + if not user:check_pin(pin) then + self.log:notice('LOGIN - authentication failed'); + return { continue = false, code = 403, phrase = 'Authentication failed', no_cdr = true } + end + + -- logout caller phones if caller account is hot-deskable + if common.str.to_b(caller_sip_account.record.hotdeskable) then + for index, phone_caller in ipairs(caller_phones) do + phone_caller:logout(caller_sip_account.record.id); + end + end + + local destination_phones = phone_class:find_all_hot_deskable_by_account(destination_sip_account.record.id); + -- logout destination phones + for index, phone_destination in ipairs(destination_phones) do + phone_destination:logout(destination_sip_account.record.id); + end + + local result = caller_phone:login(destination_sip_account.record.id, destination_sip_account.record.sip_accountable_id, destination_sip_account.record.sip_accountable_type); + self.log:info('LOGIN - account login - mac: ', caller_phone.record.mac_address, ', ip_address: ', caller_phone.record.ip_address, ', result: ', result); + + if not result then + return { continue = false, code = 403, phrase = 'Login failed', no_cdr = true } + end + + caller:answer(); + caller:send_display('Login successful'); + + self:update_node_change(destination_sip_account, caller.local_node_id); + caller:sleep(1000); + + -- resync destination phones + for index, phone_destination in ipairs(destination_phones) do + local result = phone_destination:resync{ auth_name = destination_sip_account.auth_name, domain = caller.domain_local }; + self.log:info('LOGIN - resync destination phone - mac: ', phone_destination.record.mac_address, ', ip_address: ', phone_destination.record.ip_address, ', result: ', result); + end + + -- resync caller phones + for index, phone_caller in ipairs(caller_phones) do + local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain }; + self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result); + end + + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.user_logout(self, caller) + require 'common.str' + self.log:info('LOGOUT - caller: ', caller.account_type, '/', caller.account_uuid, ', caller_id: ', caller.caller_id_number); + + -- find caller's sip account + local caller_sip_account = caller.account; + if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then + self.log:notice('LOGOUT - caller sip_account not found'); + return { continue = false, code = 404, phrase = 'Caller not found', no_cdr = true } + end + + if not common.str.to_b(caller_sip_account.record.hotdeskable) then + self.log:notice('LOGOUT - caller sip_account not hot-deskable'); + return { continue = false, code = 404, phrase = 'Caller not hot-deskable', no_cdr = true } + end + + require 'phones.phone' + local phone_class = phones.phone.Phone:new{log = self.log, database = self.database} + + local caller_phones = phone_class:find_all_hot_deskable_by_account(caller_sip_account.id); + + if caller_phones == 0 then + self.log:notice('LOGOUT - caller phones not found or not hot-deskable'); + return { continue = false, code = 403, phrase = 'Phone not hot-deskable', no_cdr = true } + end + + local result = false; + for index, phone_caller in ipairs(caller_phones) do + result = phone_caller:logout(caller_sip_account.record.id); + self.log:info('LOGOUT - account logout - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result); + end + + caller:answer(); + caller:send_display('Logout successful'); + caller:sleep(1000); + + -- resync caller phones + for index, phone_caller in ipairs(caller_phones) do + local result = phone_caller:resync{ auth_name = caller_sip_account.auth_name, domain = caller.domain }; + self.log:info('LOGIN - resync caller phone - mac: ', phone_caller.record.mac_address, ', ip_address: ', phone_caller.record.ip_address, ', result: ', result); + end + + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.update_node_change(self, sip_account, node_id) + require 'common.sync_log' + local sync_log_class = common.sync_log.SyncLog:new{ log = self.log, database = self.database, homebase_ip_address = sip_account.record.host } + + if tostring(sip_account.record.gs_node_id) ~= tostring(node_id) then + self.log:info('UPDATE_NODE - from: ', sip_account.record.gs_node_id, ', to: ', node_id, ', sipaccount=', sip_account.record.id, '/', sip_account.record.uuid, '@', node_id, ', caller_name: ', sip_account.record.caller_name); + sql_query = 'UPDATE `sip_accounts` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(sip_account.record.id); + if self.database:query(sql_query) then + sync_log_class:insert('SipAccount', { uuid = sip_account.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' }); + end + end + + require 'common.phone_number' + local phone_numbers = common.phone_number.PhoneNumber:new{log = self.log, database = self.database}:find_all_by_owner(sip_account.record.id, 'SipAccount'); + for number_id, phone_number in pairs(phone_numbers) do + if tostring(phone_number.record.gs_node_id) ~= tostring(node_id) then + self.log:info('UPDATE_NODE - from: ', phone_number.record.gs_node_id, ', to: ', node_id, ', phonenumber=', phone_number.record.id, '/', phone_number.record.uuid, '@', node_id, ', number: ', phone_number.record.number); + sql_query = 'UPDATE `phone_numbers` SET `updated_at` = NOW(), `gs_node_id` = ' .. tonumber(node_id) .. ' WHERE id = ' .. tonumber(number_id); + + if self.database:query(sql_query) then + sync_log_class:insert('PhoneNumber', { uuid = phone_number.record.uuid, gs_node_id = tonumber(node_id), updated_at = os.date('!%Y-%m-%d %H:%M:%S %Z') }, 'update', { 'gs_node_id', 'updated_at' }); + end + end + end +end + + +function Functions.user_login_redirect(self, caller, phone_number, pin) + -- Remove PIN from destination_number + caller.session:setVariable("destination_number", "f-li-" .. tostring(phone_number) .. "-PIN"); + + -- Redirect to f-li function + caller.session:execute("redirect", "sip:f-li-" .. tostring(phone_number) .. "-" .. tostring(pin) .. "@" .. caller.domain); +end + +-- Set nightly_reboot flag +function Functions.user_auto_logout(self, caller, auto_logout) + local nightly_reboot = 'FALSE'; + if auto_logout then + nightly_reboot = 'TRUE'; + end + + -- Ensure a valid sip account + local caller_sip_account = caller.account; + if not caller_sip_account or not caller_sip_account.class == 'sipaccount' then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + require "phones.phone" + local phone_class = phones.phone.Phone:new{log = self.log, database = self.database} + + -- Get caller phone + local caller_phone = phone_class:find_hot_deskable_by_account(caller_sip_account.id); + if not caller_phone then + self.log:notice("Caller phone not found or not hot-deskable"); + return { continue = false, code = 401, phrase = 'Phone not hot-deskable', no_cdr = true } + end + + log:debug("Hot-desking auto log off - caller phone: " .. caller_phone.record.id .. ", mac: " .. caller_phone.record.mac_address); + + sql_query = 'UPDATE `phones` SET `nightly_reboot` = ' .. nightly_reboot .. ' WHERE `id` = ' .. tonumber(caller_phone.record.id); + + if not self.database:query(sql_query) then + self.log:error('Hot-desking auto log off status could not be changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot); + return { continue = false, code = 401, phrase = 'Value could not be changed', no_cdr = true } + + end + + self.log:debug('Hot-desking auto log off changed from ' .. tostring(caller_phone.record.nightly_reboot) .. ' to ' .. nightly_reboot); + + caller:answer(); + caller:send_display('Logout successful'); + caller:sleep(1000); +end + +function Functions.dial_clir_off(self, caller, phone_number) + -- Ensure a valid sip account + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + caller.clir = false; + return { continue = true, number = phone_number } +end + +function Functions.dial_clir_on(self, caller, phone_number) + -- Ensure a valid sip account + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + caller.clir = true; + return { continue = true, number = phone_number } +end + +function Functions.callwaiting_on(self, caller) + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = TRUE WHERE `id` = ' .. caller_sip_account.record.id; + + if not self.database:query(sql_query) then + self.log:notice("Call Waiting could not be set"); + return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true } + end + + caller:answer(); + caller:send_display('Call waiting on'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + +function Functions.callwaiting_off(self, caller) + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + local sql_query = 'UPDATE `sip_accounts` SET `call_waiting` = FALSE WHERE `id` = ' .. caller_sip_account.record.id; + + if not self.database:query(sql_query) then + self.log:notice("Call Waiting could not be set"); + return { continue = false, code = 500, phrase = 'Call Waiting could not be set', no_cdr = true } + end + + caller:answer(); + caller:send_display('Call waiting off'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + +function Functions.clip_on(self, caller) + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + local sql_query = 'UPDATE `sip_accounts` SET `clip` = TRUE WHERE `id` = ' .. caller_sip_account.record.id; + + if not self.database:query(sql_query) then + self.log:notice("CLIP could not be set"); + return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true } + + end + + caller:answer(); + caller:send_display('CLIP on'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + +function Functions.clip_off(self, caller) + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + local sql_query = 'UPDATE `sip_accounts` SET `clip` = FALSE WHERE `id` = ' .. caller_sip_account.record.id; + + if not self.database:query(sql_query) then + self.log:notice("CLIP could not be set"); + return { continue = false, code = 500, phrase = 'CLIP could not be set', no_cdr = true } + + end + + caller:answer(); + caller:send_display('CLIP off'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.call_forwarding_off(self, caller, call_forwarding_service, delete) + local defaults = {log = self.log, database = self.database, domain = caller.domain} + + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + require 'common.phone_number' + local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }; + local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount'); + + local success = false; + for index, phone_number in pairs(phone_numbers) do + phone_number_object = phone_number_class:find_by_number(phone_number); + if phone_number_object then + if phone_number_object:call_forwarding_off(call_forwarding_service, nil, delete) then + success = true; + end + end + end + + if not success then + self.log:notice("call forwarding could not be deactivated"); + return { continue = false, code = 500, phrase = 'Call Forwarding could not be deactivated', no_cdr = true } + + end + + caller:answer(); + caller:send_display('Call forwarding off'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.call_forwarding_on(self, caller, call_forwarding_service, destination, destination_type, timeout) + local defaults = {log = self.log, database = self.database, domain = caller.domain} + + if not call_forwarding_service then + self.log:notice('no call forwarding service specified'); + end + + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + require "common.phone_number" + local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }; + local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount'); + + local success = false; + for index, phone_number in pairs(phone_numbers) do + phone_number_object = phone_number_class:find_by_number(phone_number); + if phone_number_object then + if phone_number_object:call_forwarding_on(call_forwarding_service, destination, timeout) then + success = true; + end + end + end + + if not success then + self.log:notice("call forwarding could not be activated"); + return { continue = false, code = 500, phrase = 'Call Forwarding could not be activated', no_cdr = true } + + end + + caller:answer(); + caller:send_display('Call forwarding on'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.call_forwarding_toggle(self, caller, call_forwarding_service, phone_number_id) + local defaults = {log = self.log, database = self.database, domain = caller.domain} + + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + require "common.phone_number" + local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }; + local phone_numbers = phone_number_class:list_by_owner(caller_sip_account.record.id, 'SipAccount'); + + local result = nil; + for index, phone_number in pairs(phone_numbers) do + phone_number_object = phone_number_class:find_by_number(phone_number); + if phone_number_object then + if not result then + result = phone_number_object:call_forwarding_toggle(call_forwarding_service); + elseif result.active then + phone_number_object:call_forwarding_on(call_forwarding_service, result.destination, result.destination_type, result.timeout); + else + phone_number_object:call_forwarding_off(call_forwarding_service); + end + end + end + + if not result then + self.log:notice("call forwarding could not be toggled"); + return { continue = false, code = 500, phrase = 'Call Forwarding could not be toggled', no_cdr = true } + + end + + caller:answer(); + caller:send_display('Call forwarding toggled'); + caller:sleep(1000); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.voicemail_message_leave(self, caller, phone_number) + require 'dialplan.voicemail' + local voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number); + + if not voicemail_account then + return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true } + end + + voicemail_account:leave(caller, phone_number); + + if caller:to_s("voicemail_message_len") ~= '' then + voicemail_account:send_notify(caller); + else + self.log:debug("voicemail - no message saved"); + end + + return { continue = false, code = 200, phrase = 'OK' } +end + + +function Functions.voicemail_check(self, caller, phone_number) + local voicemail_account = nil; + local voicemail_authorized = false; + + require 'dialplan.voicemail' + + if phone_number then + voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_number(phone_number); + else + if caller.auth_account_type == 'SipAccount' then + voicemail_account = dialplan.voicemail.Voicemail:new{ log = self.log, database = self.database }:find_by_sip_account_id(caller.auth_account.id); + voicemail_authorized = true; + end + end + + if not voicemail_account then + return { continue = false, code = 404, phrase = 'Mailbox not found', no_cdr = true } + end + + voicemail_account:menu(caller, voicemail_authorized); + + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + + +function Functions.acd_membership_toggle(self, caller, agent_id, phone_number) + -- Find caller's SipAccount + local caller_sip_account = self:ensure_caller_sip_account(caller); + if not caller_sip_account then + return { continue = false, code = 403, phrase = 'Incompatible caller', no_cdr = true } + end + + require 'dialplan.acd' + local acd_class = dialplan.acd.AutomaticCallDistributor:new{ log = self.log, database = self.database, domain = self.domain }; + + self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', ACD phone number: ', phone_number); + + if not tonumber(agent_id) or tonumber(agent_id) == 0 then + + if not phone_number then + self.log:notice('ACD_MEMBERSHIP_TOGGLE - neither agent_id nor phone_number specified'); + return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true } + end + + require "common.phone_number" + local phone_number_object = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database, domain = caller.domain }:find_by_number(phone_number, {'AutomaticCallDistributor'}); + + if not phone_number_object or not tonumber(phone_number_object.record.phone_numberable_id) then + self.log:notice('ACD_MEMBERSHIP_TOGGLE - ACD not found'); + return { continue = false, code = 404, phrase = 'ACD not found', no_cdr = true } + end + + local agent = acd_class:agent_find_by_acd_and_destination(phone_number_object.record.phone_numberable_id, caller_sip_account.class, caller_sip_account.id); + + if not agent or not tonumber(agent.id) then + self.log:notice('ACD_MEMBERSHIP_TOGGLE - agent not found'); + return { continue = false, code = 404, phrase = 'Agent not found', no_cdr = true } + end + + agent_id = agent.id; + end + + local status = acd_class:agent_status_toggle(agent_id, 'sipaccount', caller_sip_account.id); + + if not status then + self.log:error('ACD_MEMBERSHIP_TOGGLE - error toggling ACD membership'); + return { continue = false, code = 500, phrase = 'Error toggling ACD membership', no_cdr = true } + end + + self.log:info('ACD_MEMBERSHIP_TOGGLE - sipaccount=', caller_sip_account.id, '/', caller_sip_account.uuid, ', agent=', agent_id, ', status: ', status); + + caller:answer(); + caller:send_display('ACD membership toggled: ' .. status); + caller:sleep(500); + caller.session:sayPhrase('acd_agent_status', tostring(status)); + return { continue = false, code = 200, phrase = 'OK', no_cdr = true } +end + +function Functions.hangup(self, caller, code, phrase) + require 'common.str' + + if not tonumber(code) then + code = 403; + phrase = 'Forbidden'; + end + + if common.str.blank(phrase) then + phrase = 'Hangup here'; + end + + self.log:info("FUNCTION_HANGUP code: ", code, ', phrase: ', phrase); + return { continue = false, code = code, phrase = phrase:gsub('_', ' '), no_cdr = true } +end diff --git a/misc/freeswitch/scripts/dialplan/geo_number.lua b/misc/freeswitch/scripts/dialplan/geo_number.lua new file mode 100644 index 0000000..06bfd62 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/geo_number.lua @@ -0,0 +1,89 @@ +-- Gemeinschaft 5 module: geonumber class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +GeoNumber = {} + +-- create phone_book object +function GeoNumber.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'geonumber'; + self.log = arg.log; + self.database = arg.database; + return object; +end + +function GeoNumber.country(self, phone_number) + if phone_number:match('^%+1') then + return { id = 0, name = 'NANP', country_code = '1' } + end + + local country_codes = {}; + for i = 2, 4, 1 do + table.insert(country_codes, phone_number:sub(2, i)); + end + + local sql_query = 'SELECT * FROM `countries` WHERE `country_code` IN ("' .. table.concat(country_codes, '","') .. '") ORDER BY LENGTH(`country_code`) DESC LIMIT 1'; + + local country = nil; + self.database:query(sql_query, function(entry) + country = entry; + end) + + return country; +end + + +function GeoNumber.area_code(self, phone_number, country_code) + local sql_query = nil; + local area_code = nil; + + if country_code == '1' then + area_code = {} + area_code.area_code, area_code.central_office_code, area_code.subscriber_number, area_code.extension = phone_number:match('%+1(%d%d%d)(%d%d%d)(%d%d%d%d)(%d*)'); + sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \ + JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \ + WHERE `b`.`country_code` = "' .. tostring(country_code) .. '"\ + AND `a`.`area_code` = "' .. tostring(area_code.area_code) .. '" \ + AND `a`.`central_office_code` = "' .. tostring(area_code.central_office_code) .. '" LIMIT 1'; + else + local offset = #country_code; + area_codes = {}; + for i = (3 + offset), (6 + offset), 1 do + table.insert(area_codes, phone_number:sub((2 + offset), i)); + end + + sql_query = 'SELECT `a`.`name`, `b`.`name` AS `country` FROM `area_codes` `a` \ + JOIN `countries` `b` ON `a`.`country_id` = `b`.`id` \ + WHERE `b`.`country_code` = "' .. country_code .. '"\ + AND `a`.`area_code` IN ("' .. table.concat(area_codes, '","') .. '") ORDER BY LENGTH(`a`.`area_code`) DESC LIMIT 1'; + end + + self.database:query(sql_query, function(entry) + area_code = entry; + end) + + return area_code; +end + + +function GeoNumber.find(self, phone_number) + if not phone_number:match('^%+%d+') then + return nil; + end + + local country = self:country(phone_number); + if country then + local area_code = self:area_code(phone_number, country.country_code); + if area_code then + return area_code; + else + return { country = country.name }; + end + end +end diff --git a/misc/freeswitch/scripts/dialplan/hunt_group.lua b/misc/freeswitch/scripts/dialplan/hunt_group.lua new file mode 100644 index 0000000..87f86f1 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/hunt_group.lua @@ -0,0 +1,202 @@ +-- Gemeinschaft 5 module: hunt group class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +HuntGroup = {} + +local DEFAULT_MEMBER_TIMEOUT = 20; + +-- Create HuntGroup object +function HuntGroup.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'huntgroup'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object; +end + + +function HuntGroup.find_by_id(self, id) + local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= '.. tonumber(id) .. ' LIMIT 1'; + local hunt_group = nil; + + self.database:query(sql_query, function(entry) + hunt_group = HuntGroup:new(self); + hunt_group.record = entry; + hunt_group.id = tonumber(entry.id); + hunt_group.uuid = entry.uuid; + end) + + return hunt_group; +end + + +function HuntGroup.find_by_uuid(self, uuid) + local sql_query = 'SELECT * FROM `hunt_groups` WHERE `id`= "'.. uuid .. '" LIMIT 1'; + local hunt_group = nil; + + self.database:query(sql_query, function(entry) + hunt_group = HuntGroup:new(self); + hunt_group.record = entry; + hunt_group.id = tonumber(entry.id); + hunt_group.uuid = entry.uuid; + end) + + return hunt_group; +end + + +function HuntGroup.list_active_members(self) + local sql_query = 'SELECT `a`.`number`, `b`.`name` \ + FROM `phone_numbers` `a` \ + LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \ + WHERE `a`.`phone_numberable_type` = "huntgroupmember" \ + AND `b`.`active` IS TRUE \ + AND `b`.`hunt_group_id` = ' .. self.record.id; + + local hunt_group_members = {} + + self.database:query(sql_query, function(hunt_group_members_entry) + table.insert(hunt_group_members, hunt_group_members_entry); + end) + + return hunt_group_members; +end + + +function HuntGroup.is_member_by_numbers(self, numbers) + local sql_query = 'SELECT `a`.`number`, `b`.`name` \ + FROM `phone_numbers` `a` \ + LEFT JOIN `hunt_group_members` `b` ON `a`.`phone_numberable_type` = "huntgroupmember" AND `a`.`phone_numberable_id` = `b`.`id` \ + WHERE `a`.`phone_numberable_type` = "huntgroupmember" \ + AND `b`.`active` IS TRUE \ + AND `b`.`hunt_group_id` = ' .. self.record.id .. '\ + AND `a`.`number` IN ("' .. table.concat( numbers, '","') .. '") LIMIT 1'; + + local hunt_group_member = false; + + self.database:query(sql_query, function(hunt_group_members_entry) + hunt_group_member = true; + end) + + return hunt_group_member; +end + + +function HuntGroup.run(self, dialplan_object, caller, destination) + local hunt_group_members = self:list_active_members(); + + if #hunt_group_members == 0 then + return { disposition = 'HUNT_GROUP_EMPTY', code = 480, phrase = 'No active users' } + end + + self.log:info('HUNTGROUP ', self.record.id, ' - name: ', self.record.name, ', strategy: ', self.record.strategy,', members: ', #hunt_group_members); + + local destinations = {} + for index, hunt_group_member in ipairs(hunt_group_members) do + local destination = dialplan_object:destination_new{ number = hunt_group_member.number }; + if destination.type == 'unknown' then + require 'dialplan.route' + local routes = dialplan.route.Route:new{ log = self.log, database = self.database, routing_table = dialplan_object.routes }:outbound(caller, destination.number); + if routes and #routes > 0 then + destination.callee_id_number = destination.number; + destination.callee_id_name = nil; + local route = routes[1]; + destination.gateway = route.endpoint; + destination.type = route.class; + destination.number = route.value; + destination.caller_id_number = route.caller_id_number; + destination.caller_id_name = route.caller_id_name; + table.insert(destinations, destination); + end + else + table.insert(destinations, destination); + end + end + + local forwarding_destination = nil; + if caller.forwarding_service == 'assistant' and caller.auth_account then + forwarding_destination = dialplan_object:destination_new{ type = caller.auth_account.class, id = caller.auth_account.id, number = forwarding_number } + forwarding_destination.alert_info = 'http://amooma.com;info=Ringer0;x-line-id=0'; + end + + local result = { continue = false }; + local start_time = os.time(); + if self.record.strategy == 'ring_recursively' then + local member_timeout = tonumber(self.record.seconds_between_jumps) or DEFAULT_MEMBER_TIMEOUT; + local run_queue = true; + while run_queue do + for index, member_destination in ipairs(destinations) do + local recursive_destinations = { member_destination } + if forwarding_destination then + table.insert(recursive_destinations, forwarding_destination); + end + require 'dialplan.sip_call' + result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( recursive_destinations, { callee_id_number = destination.number, timeout = member_timeout }); + if result.disposition == 'SUCCESS' then + if result.fork_index then + result.destination = recursive_destinations[result.fork_index]; + end + run_queue = false; + break; + elseif os.time() > start_time + dialplan_object.dial_timeout_active then + run_queue = false; + break; + elseif not caller:ready() then + run_queue = false; + break; + end + end + if tostring(result.code) == '486' then + self.log:info('HUNTGROUP ', self.record.id, ' - all members busy'); + run_queue = false; + end + end + else + if forwarding_destination then + table.insert(destinations, forwarding_destination); + end + + require 'dialplan.sip_call' + result = dialplan.sip_call.SipCall:new{ log = self.log, database = self.database, caller = caller }:fork( destinations, + { + callee_id_number = destination.number, + timeout = dialplan_object.dial_timeout_active, + send_ringing = ( dialplan_object.send_ringing_to_gateways and caller.from_gateway ), + }); + if result.fork_index then + result.destination = destinations[result.fork_index]; + end + + return result; + end + + return result; +end + + +function HuntGroup.list_destination_numbers(self) + require "common.phone_number" + local phone_number_class = common.phone_number.PhoneNumber:new(defaults) + + local sql_query = string.format("SELECT * FROM `phone_numbers` WHERE `state`='active' AND `phone_numberable_type` = 'HuntGroupMember' AND `phone_numberable_id` IN ( \ + SELECT `id` FROM `hunt_group_members` WHERE `active` IS TRUE AND `hunt_group_id`=%d ) ORDER BY `position` ASC", tonumber(self.record.id)); + local phone_numbers = {} + + self.database:query(sql_query, function(hunt_group_number_entry) + local number_object = phone_number_class:find_by_number(hunt_group_number_entry.number) + if number_object and number_object.record then + table.insert(phone_numbers, {number = hunt_group_number_entry.number, destination_type = number_object.record.phone_numberable_type, destination_id = number_object.record.phone_numberable_id}); + else + table.insert(phone_numbers, {number = hunt_group_number_entry.number}); + end + end) + + return phone_numbers ; +end diff --git a/misc/freeswitch/scripts/dialplan/phone_book.lua b/misc/freeswitch/scripts/dialplan/phone_book.lua new file mode 100644 index 0000000..089f115 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/phone_book.lua @@ -0,0 +1,63 @@ +-- Gemeinschaft 5 module: phone book class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +PhoneBook = {} + +-- create phone_book object +function PhoneBook.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'phonebook'; + self.log = arg.log; + self.database = arg.database; + return object; +end + + +function PhoneBook.find_entry_by_number_user_tenant(self, numbers, user_id, tenant_id) + user_id = tonumber(user_id) or 0; + tenant_id = tonumber(tenant_id) or 0; + + if not numbers or #numbers == 0 then + return nil; + end + local numbers_sql = '"' .. table.concat(numbers, '","') .. '"'; + + local sql_query = 'SELECT `a`.`name` AS `number_name`, \ + `a`.`number`, \ + `b`.`id`, \ + `b`.`value_of_to_s`, \ + `b`.`phone_book_id`, \ + `b`.`image`, \ + `c`.`name` AS `phone_book_name`, \ + `d`.`bellcore_id` \ + FROM `phone_numbers` `a` \ + JOIN `phone_book_entries` `b` ON `a`.`phone_numberable_id` = `b`.`id` AND `a`.`phone_numberable_type` = "PhoneBookENtry" \ + JOIN `phone_books` `c` ON `b`.`phone_book_id` = `c`.`id` \ + LEFT JOIN `ringtones` `d` ON `a`.`id` = `d`.`ringtoneable_id` AND `d`.`ringtoneable_type` = "PhoneNumber" \ + WHERE ((`c`.`phone_bookable_type` = "User" AND `c`.`phone_bookable_id` = ' .. user_id .. ') \ + OR (`c`.`phone_bookable_type` = "Tenant" AND `c`.`phone_bookable_id` = ' .. tenant_id .. ')) \ + AND `a`.`number` IN (' .. numbers_sql .. ') \ + AND `a`.`state` = "active" \ + AND `b`.`state` = "active" \ + AND `c`.`state` = "active" \ + ORDER BY `c`.`phone_bookable_type` DESC LIMIT 1'; + + local phone_book_entry = nil; + + self.database:query(sql_query, function(entry) + phone_book_entry = entry; + if entry.number_name then + phone_book_entry.caller_id_name = tostring(entry.value_of_to_s) .. ' (' .. entry.number_name .. ')'; + else + phone_book_entry.caller_id_name = entry.value_of_to_s; + end + end) + + return phone_book_entry; +end diff --git a/misc/freeswitch/scripts/dialplan/presence.lua b/misc/freeswitch/scripts/dialplan/presence.lua new file mode 100644 index 0000000..234b908 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/presence.lua @@ -0,0 +1,84 @@ +-- Gemeinschaft 5 module: presence class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Presence = {} + +-- Create Presence object +function Presence.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log; + self.domain = arg.domain; + self.uuid = arg.uuid; + self.inbound = arg.inbound; + self.accounts = arg.accounts; + + return object +end + + +function Presence.init(self, arg) + self.log = arg.log or self.log; + self.domain = arg.domain or self.domain; + self.uuid = arg.uuid or self.uuid; + self.inbound = arg.inbound or self.inbound; + self.accounts = arg.accounts or self.accounts; +end + + +function Presence.set(self, state, caller_number) + if not self.accounts or #self.accounts == 0 then + return nil; + end + + state = state or "terminated"; + local direction = "outbound"; + + if self.inbound then + direction = "inbound"; + end + + for index, account in pairs(self.accounts) do + if account ~= '' then + local event = freeswitch.Event('PRESENCE_IN'); + event:addHeader('proto', 'sip'); + event:addHeader('from', account .. '@' .. self.domain); + event:addHeader('event_type', 'presence'); + event:addHeader('alt_event_type', 'dialog'); + event:addHeader('presence-call-direction', direction); + event:addHeader('answer-state', state); + event:addHeader('unique-id', self.uuid); + if caller_number then + if self.inbound then + event:addHeader('Caller-Destination-Number', caller_number); + else + event:addHeader('Other-Leg-Caller-ID-Number', caller_number); + end + end + event:fire(); + self.log:debug('PRESENCE - account: ' .. account .. '@' .. self.domain .. ', state: ' .. state .. ', direction: ' .. direction .. ', uid: ' ..self.uuid); + end + end + + return true; +end + + +function Presence.early(self, caller_number) + return self:set("early", caller_number); +end + + +function Presence.confirmed(self, caller_number) + return self:set("confirmed", caller_number); +end + + +function Presence.terminated(self, caller_number) + return self:set("terminated", caller_number); +end diff --git a/misc/freeswitch/scripts/dialplan/route.lua b/misc/freeswitch/scripts/dialplan/route.lua new file mode 100644 index 0000000..2243cbe --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/route.lua @@ -0,0 +1,265 @@ +-- Gemeinschaft 5 module: routing class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Route = {} + +-- create route object +function Route.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + self.routing_table = arg.routing_table; + self.expandable = arg.expandable or {}; + return object; +end + +-- find matching routes +function Route.prerouting(self, caller, number) + require 'common.routing_tables' + + for index, routing_entry in pairs(self.routing_table.prerouting) do + local route = common.routing_tables.match_route(routing_entry, number); + if route.error then + self.log:error('PREROUTE - error: ', route.error); + elseif route.value then + self.log:info('ROUTE_PREROUTING - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern); + return route; + end + end +end + +-- find matching routes +function Route.outbound(self, caller, number) + local routes = {}; + require 'common.routing_tables' + require 'common.str' + + local ignore_arguments = { + class=true, + endpoint=true, + pattern=true, + value=true, + group=true, + phrase=true, + } + + local clip_no_screening = common.str.try(caller, 'account.record.clip_no_screening'); + local caller_id_numbers = {} + if not common.str.blank(clip_no_screening) then + for index, number in ipairs(common.str.strip_to_a(clip_no_screening, ',')) do + table.insert(caller_id_numbers, number); + end + end + for index, number in ipairs(caller.caller_phone_numbers) do + table.insert(caller_id_numbers, number); + end + self.log:info('CALLER_ID_NUMBER - caller_id_numbers: ', table.concat(caller_id_numbers, ',')); + + for index, routing_entry in pairs(self.routing_table.outbound) do + local route = common.routing_tables.match_route(routing_entry, number); + if route.error then + self.log:error('ROUTE_OUTBOUND - error: ', route.error); + elseif route.value then + local valid_route = true; + + for argument, value in pairs(route) do + if not ignore_arguments[argument] then + local table_value = common.str.downcase(tostring(common.str.try(caller, argument))); + value = common.str.downcase(tostring(value)); + if table_value:match(value) then + self.log:info('ROUTE_OUTBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern); + else + self.log:info('ROUTE_OUTBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern); + valid_route = false; + end + end + end + + if route.group then + if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then + self.log:info('ROUTE_OUTBOUND_POSITIVE - group=', route.group, ', pattern: ', route.pattern); + else + self.log:info('ROUTE_OUTBOUND_NEGATIVE - group=', route.group, ', pattern: ', route.pattern); + valid_route = false; + end + end + + if route.cidn then + if caller.caller_id_number:match(route.cidn) then + self.log:info('ROUTE_OUTBOUND_POSITIVE - cidn=', route.cidn, ' ~ ', caller.caller_id_number,', pattern: ', route.pattern); + else + self.log:info('ROUTE_OUTBOUND_NEGATIVE - cidn=', route.cidn, ' !~ ', caller.caller_id_number, ', pattern: ', route.pattern); + valid_route = false; + end + end + + if valid_route then + if route.class ~= 'hangup' then + route.caller_id_number = self:outbound_cid_number(caller, caller_id_numbers, route.endpoint, route.class); + self.expandable.caller_id_number = route.caller_id_number; + route.caller_id_name = self:outbound_cid_name(caller, route.endpoint, route.class); + end + table.insert(routes, route); + self.log:info('ROUTE_OUTBOUND ', #routes,' - ', route.class, '=', route.endpoint, ', value: ', route.value, ', caller_id_number: ', route.caller_id_number, ', caller_id_name: ', route.caller_id_name); + end + end + end + + return routes; +end + + +function Route.inbound(self, caller, number) + require 'common.routing_tables' + + local ignore_arguments = { + class=true, + endpoint=true, + pattern=true, + value=true, + group=true, + phrase=true, + } + + for index, routing_entry in pairs(self.routing_table.inbound) do + local route = common.routing_tables.match_route(routing_entry, number); + if route.error then + self.log:error('ROUTE_INBOUND - error: ', route.error); + elseif route.value then + local valid_route = true; + + for argument, value in pairs(route) do + if not ignore_arguments[argument] then + local table_value = common.str.downcase(tostring(common.str.try(caller, argument))); + value = common.str.downcase(tostring(value)); + if table_value:match(value) then + self.log:info('ROUTE_INBOUND_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern); + else + self.log:info('ROUTE_INBOUND_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern); + valid_route = false; + end + end + end + + if route.class and route.endpoint then + if route.class == 'gateway' and caller.gateway_name:match(route.endpoint) then + self.log:info('ROUTE_INBOUND_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', caller.gateway_name, ', pattern: ', route.pattern); + else + self.log:info('ROUTE_INBOUND_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', caller.gateway_name, ', pattern: ', route.pattern); + valid_route = false; + end + end + + if valid_route then + self.log:info('ROUTE_INBOUND - called number: ', number, ', value: ', route.value, ', pattern: ', route.pattern); + return route; + end + end + end +end + +-- find caller id +function Route.caller_id(self, caller, cid_entry, search_str, endpoint, class) + local ignore_arguments = { + class=true, + endpoint=true, + pattern=true, + value=true, + group=true, + phrase=true, + } + + local route = common.routing_tables.match_route(cid_entry, search_str, self.expandable); + if route.error then + self.log:error('CALLER_ID - error: ', route.error); + elseif route.value then + local valid_route = true; + + for argument, value in pairs(route) do + if not ignore_arguments[argument] then + local table_value = common.str.downcase(tostring(common.str.try(caller, argument))); + value = common.str.downcase(tostring(value)); + if table_value:match(value) then + self.log:debug('CALLER_ID_POSITIVE - ', argument, '=', value, ' ~ ', table_value, ', pattern: ', route.pattern); + else + self.log:debug('CALLER_ID_NEGATIVE - ', argument, '=', value, ' !~ ', table_value, ', pattern: ', route.pattern); + valid_route = false; + end + end + end + + if route.group then + if common.str.try(caller.auth_account, 'owner.groups.' .. tostring(route.group)) then + self.log:debug('CALLER_ID_POSITIVE - group=', route.group, ', pattern: ', route.pattern); + else + self.log:debug('CALLER_ID_NEGATIVE - group=', route.group, ', pattern: ', route.pattern); + valid_route = false; + end + end + + endpoint = tostring(endpoint); + if route.class and route.endpoint then + if route.class == 'gateway' and endpoint:match(route.endpoint) then + self.log:debug('CALLER_ID_POSITIVE - ', route.class, '=', route.endpoint, ' ~ ', endpoint, ', pattern: ', route.pattern); + else + self.log:debug('CALLER_ID_NEGATIVE - ', route.class, '=', route.endpoint, ' !~ ', endpoint, ', pattern: ', route.pattern); + valid_route = false; + end + end + + if valid_route then + self.log:debug('CALLER_ID ', route.class, '=', route.endpoint, ', value: ', route.value); + return route.value; + end + end + + return nil; +end + +-- find matching caller id number +function Route.outbound_cid_number(self, caller, caller_id_numbers, endpoint, class) + for route_index, cid_entry in pairs(self.routing_table.outbound_cid_number) do + for index, number in ipairs(caller_id_numbers) do + local route = self:caller_id(caller, cid_entry, number, endpoint, class); + if route then + return route; + end + end + end +end + +-- find matching caller id name +function Route.outbound_cid_name(self, caller, endpoint, class) + for route_index, cid_entry in pairs(self.routing_table.outbound_cid_name) do + local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class); + if route then + return route; + end + end +end + +-- find matching caller id number +function Route.inbound_cid_number(self, caller, endpoint, class) + for route_index, cid_entry in pairs(self.routing_table.inbound_cid_number) do + local route = self:caller_id(caller, cid_entry, caller.caller_id_number, endpoint, class); + if route then + return route; + end + end +end + +-- find matching caller id name +function Route.inbound_cid_name(self, caller, endpoint, class) + for route_index, cid_entry in pairs(self.routing_table.inbound_cid_name) do + local route = self:caller_id(caller, cid_entry, caller.caller_id_name, endpoint, class); + if route then + return route; + end + end +end diff --git a/misc/freeswitch/scripts/dialplan/session.lua b/misc/freeswitch/scripts/dialplan/session.lua new file mode 100644 index 0000000..7174b24 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/session.lua @@ -0,0 +1,224 @@ +-- Gemeinschaft 5 module: caller session class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Session = {} + +-- create session object +function Session.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.session = arg.session; + + if not self.session then + return nil; + end + + return object; +end + +function Session.init_channel_variables(self) + self.cause = "UNSPECIFIED" + + self.uuid = self.session:get_uuid(); + self.destination_number = self:expand_variables(self:to_s('destination_number')); + self.called_number = self.destination_number; + + self.caller_id_number = self:to_s('caller_id_number'); + self.caller_id_name = self:to_s('caller_id_name'); + self.caller_phone_number = self.caller_id_number; + self.caller_phone_numbers = {self.caller_id_number}; + + self.domain = self:to_s('domain_name'); + self.gateway_name = self:to_s('sip_gateway'); + self.from_gateway = self:to_b('gs_from_gateway'); + if self.from_gateway then + self.gateway_name = self:to_s('gs_gateway_name'); + elseif self.gateway_name ~= '' then + self.from_gateway = true; + end + + self.account_uuid = self:to_s('gs_account_uuid'); + self.account_type = self:to_s('gs_account_type'); + self.sip_contact_host = self:to_s('sip_contact_host'); + self.clir = self:to_b('gs_clir'); + self.call_timeout = self:to_i('gs_call_timeout'); + self.auth_account_type = self:to_s('gs_auth_account_type'); + self.auth_account_uuid = self:to_s('gs_auth_account_uuid'); + + self.node_id = self:to_i('sip_h_X-GS_node_id'); + self.loop_count = self:to_i('sip_h_X-GS_loop_count'); + + if self.node_id > 0 and self.node_id ~= self.local_node_id then + self.from_node = true; + else + self.from_node = false; + end + self:set_variable('gs_account_node_local', not self.from_node); + + if self.from_node then + self.account_uuid = self:to_s('sip_h_X-GS_account_uuid'); + self.account_type = self:to_s('sip_h_X-GS_account_type'); + self.auth_account_uuid = self:to_s('sip_h_X-GS_auth_account_uuid'); + self.auth_account_type = self:to_s('sip_h_X-GS_auth_account_type'); + end + + if self.auth_account_type == '' then + self.auth_account_type = self.account_type; + self.auth_account_uuid = self.account_uuid; + end + + self.forwarding_number = nil; + self.forwarding_service = nil; + + return true; +end + + +-- Cast channel variable to string +function Session.to_s(self, variable_name) + require 'common.str' + return common.str.to_s(self.session:getVariable(variable_name)); +end + +-- Cast channel variable to integer +function Session.to_i(self, variable_name) + require 'common.str' + return common.str.to_i(self.session:getVariable(variable_name)); +end + +-- Cast channel variable to boolean +function Session.to_b(self, variable_name) + require 'common.str' + return common.str.to_b(self.session:getVariable(variable_name)); +end + +-- Split channel variable to table +function Session.to_a(self, variable_name) + require 'common.str' + return common.str.to_a(self.session:getVariable(variable_name)); +end + +-- Check if session is active +function Session.ready(self, command, parameters) + return self.session:ready(); +end + +-- Wait milliseconds +function Session.sleep(self, milliseconds) + return self.session:sleep(milliseconds); +end + +-- Execute command +function Session.execute(self, command, parameters) + parameters = parameters or ''; + self.session:execute(command, parameters); +end + +-- Execute and return result +function Session.result(self, command_line) + self.session:execute('set', 'result=${' .. command_line .. '}'); + return self.session:getVariable('result'); +end + +-- Set cause code +function Session.set_cause(self, cause) + self.cause = cause +end + +-- Set channel variable +function Session.set_variable(self, name, value) + self.session:setVariable(name, tostring(value)); +end + +-- Set and export channel variable +function Session.export_variable(self, name, value) + self.session:execute('export', tostring(name) .. '=' .. tostring(value)); +end + +-- Set SIP header +function Session.set_header(self, name, value) + self.session:setVariable('sip_h_' .. name, tostring(value)); +end + +-- Hangup a call +function Session.hangup(self, cause) + return self.session:hangup(cause); +end + +-- Respond a call +function Session.respond(self, code, text) + self.session:execute('respond', tostring(code) .. ' ' .. text); + return self.session:hangupCause(); +end + +-- Answer a call +function Session.answer(self) + return self.session:answer(); +end + +function Session.intercept(self, uid) + self.session:execute("intercept", uid); +end + +function Session.send_display(self, ... ) + self:execute('send_display', table.concat( arg, '|')); +end + +-- Set caller ID +function Session.set_caller_id(self, number, name) + if number then + self.caller_id_number = tostring(number); + self.session:setVariable('effective_caller_id_number', tostring(number)) + end + if name then + self.caller_id_name = tostring(name); + self.session:setVariable('effective_caller_id_name', tostring(name)) + end +end + +-- Set callee ID +function Session.set_callee_id(self, number, name) + if number ~= nil then + self.callee_id_number = tostring(number); + self.session:execute('export', 'effective_callee_id_number=' .. number); + end + if name ~= nil then + self.callee_id_name = tostring(name); + self.session:execute('export', 'effective_callee_id_name=' .. name); + end +end + +-- Set caller Privacy header +function Session.set_privacy(self, privacy) + if privacy then + self.session:setVariable('cid_type', 'none'); + self.session:setVariable('sip_h_Privacy', 'id'); + else + self.session:setVariable('cid_type', 'none'); + self.session:setVariable('sip_h_Privacy', 'none'); + end +end + + +function Session.set_auth_account(self, auth_account) + if auth_account then + self:set_variable('gs_auth_account_type', auth_account.class); + self:set_variable('gs_auth_account_id', auth_account.id); + self:set_variable('gs_auth_account_uuid', auth_account.uuid); + end + + return auth_account; +end + + +function Session.expand_variables(self, line) + return (line:gsub('{([%a%d_-]+)}', function(captured) + return self.session:getVariable(captured) or ''; + end)) +end diff --git a/misc/freeswitch/scripts/dialplan/sip_call.lua b/misc/freeswitch/scripts/dialplan/sip_call.lua new file mode 100644 index 0000000..57f92c6 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/sip_call.lua @@ -0,0 +1,266 @@ +-- Gemeinschaft 5 module: sip call class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall); + +SipCall = {} + +-- Create SipCall object +function SipCall.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.session = arg.session; + self.record = arg.record; + self.database = arg.database; + self.domain = arg.domain; + self.caller = arg.caller; + self.on_answer = arg.on_answer; + self.calling_object = arg.calling_object; + return object; +end + + +function SipCall.wait_answer(self, caller_session, callee_session, timeout, start_time) + if caller_session:ready() and callee_session:ready() then + callee_session:waitForAnswer(caller_session); + end + + while true do + if not caller_session:ready() then + return 'ORIGINATOR_CANCEL'; + elseif not callee_session:ready() then + return 'UNSPECIFIED'; + elseif (os.time() - start_time) > timeout then + return 'NO_ANSWER'; + elseif callee_session:answered() then + return 'SUCCESS'; + end + + self.caller:sleep(500); + end +end + + +function SipCall.wait_hangup(self, caller_session, callee_session) + local hangup_on = { + CS_HANGUP = true, + CS_DESTROY = true, + } + + while true do + local state_caller = caller_session:getState(); + local state_callee = callee_session:getState(); + if hangup_on[state_caller] or hangup_on[state_callee] then + break; + end + caller_session:sleep(500); + end +end + + +function SipCall.call_waiting_busy(self, sip_account) + require 'common.str' + if common.str.to_b(sip_account.record.call_waiting) then + self.log:info('CALL_WAITING - status: enabled'); + return false; + else + local state = sip_account:call_state(); + self.log:info('CALL_WAITING - status: disabled, sip_account state: ', state); + return state; + end +end + + +function SipCall.fork(self, destinations, arg ) + local dial_strings = {} + + require 'common.sip_account' + local sip_account_class = common.sip_account.SipAccount:new{ log = self.log, database = self.database }; + + local call_result = { code = 404, phrase = 'No destination' }; + local some_destinations_busy = false; + + for index, destination in ipairs(destinations) do + local origination_variables = { 'gs_fork_index=' .. index } + + self.log:info('FORK ', index, '/', #destinations, ' - ', destination.type, '=', destination.id, '/', destination.gateway or destination.uuid, '@', destination.node_id, ', number: ', destination.number); + if not destination.node_local or destination.type == 'node' then + require 'common.node' + local node = nil; + if tonumber(destination.gateway) then + node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(tonumber(destination.gateway)); + else + node = common.node.Node:new{ log = self.log, database = self.database }:find_by_id(destination.node_id); + end + if node then + table.insert(origination_variables, 'sip_h_X-GS_node_id=' .. self.caller.local_node_id); + table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. node.record.name .. '/' .. destination.number); + end + elseif destination.type == 'sipaccount' then + local callee_id_params = ''; + local sip_account = sip_account_class:find_by_id(destination.id); + local call_waiting = self:call_waiting_busy(sip_account); + if not call_waiting then + destinations[index].numbers = sip_account:phone_numbers(); + + if not arg.callee_id_name then + table.insert(origination_variables, "effective_callee_id_name='" .. sip_account.record.caller_name .. "'"); + end + if not arg.callee_id_number then + table.insert(origination_variables, "effective_callee_id_number='" .. destination.number .. "'"); + end + if destination.alert_info then + table.insert(origination_variables, "alert_info='" .. destination.alert_info .. "'"); + end + table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']user/' .. sip_account.record.auth_name); + else + some_destinations_busy = true; + call_result = { code = 486, phrase = 'User busy', disposition = 'USER_BUSY' }; + end + elseif destination.type == 'gateway' then + if destination.caller_id_number then + table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'"); + end + if destination.caller_id_name then + table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'"); + end + table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']sofia/gateway/' .. destination.gateway .. '/' .. destination.number); + elseif destination.type == 'dial' then + if destination.caller_id_number then + table.insert(origination_variables, "origination_caller_id_number='" .. destination.caller_id_number .. "'"); + end + if destination.caller_id_name then + table.insert(origination_variables, "origination_caller_id_name='" .. destination.caller_id_name .. "'"); + end + table.insert(dial_strings, '[' .. table.concat(origination_variables , ',') .. ']' .. destination.number); + else + self.log:info('FORK ', index, '/', #destinations, ' - unhandled destination type: ', destination.type, ', number: ', destination.number); + end + end + + if #dial_strings == 0 then + self.log:notice('FORK - no active destinations - result: ', call_result.code, ' ', call_result.phrase); + return call_result; + end + + self.caller:set_callee_id(arg.callee_id_number, arg.callee_id_name); + self.caller:set_header('X-GS_account_uuid', self.caller.account_uuid); + self.caller:set_header('X-GS_account_type', self.caller.account_type); + self.caller:set_header('X-GS_auth_account_type', self.caller.auth_account_type); + self.caller:set_header('X-GS_auth_account_uuid', self.caller.auth_account_uuid); + self.caller:set_header('X-GS_loop_count', self.caller.loop_count); + + self.caller:set_variable('call_timeout', arg.timeout ); + self.log:info('FORK DIAL - destinations: ', #dial_strings, ', timeout: ', arg.timeout); + + if arg.send_ringing then + self.caller:execute('ring_ready'); + end + + local start_time = os.time(); + local session_callee = freeswitch.Session('{local_var_clobber=true}' .. table.concat(dial_strings, ','), self.caller.session); + self.log:debug('FORK SESSION_INIT - dial_time: ', os.time() - start_time); + local answer_result = self:wait_answer(self.caller.session, session_callee, arg.timeout, start_time); + local fork_index = nil; + self.log:info('FORK ANSWER - status: ', answer_result, ', dial_time: ', os.time() - start_time); + if answer_result == 'SUCCESS' then + session_callee:setAutoHangup(false); + fork_index = tonumber(session_callee:getVariable('gs_fork_index')) or 0; + local destination = destinations[fork_index]; + + if arg.bypass_media_network then + local callee_uuid = session_callee:get_uuid(); + + if callee_uuid and self.caller.uuid and freeswitch then + require 'common.ipcalc' + local callee_network_str = self.caller:to_s('bleg_network_addr'); + local caller_network_str = self.caller:to_s('network_addr'); + local callee_network_addr = common.ipcalc.ipv4_to_i(callee_network_str); + local caller_network_addr = common.ipcalc.ipv4_to_i(caller_network_str); + local network, netmask = common.ipcalc.ipv4_to_network_netmask(arg.bypass_media_network); + if network and netmask and callee_network_addr and caller_network_addr + and common.ipcalc.ipv4_in_network(callee_network_addr, network, netmask) + and common.ipcalc.ipv4_in_network(caller_network_addr, network, netmask) then + self.log:info('FORK ', fork_index, ' BYPASS_MEDIA - caller_ip: ', caller_network_str, + ', callee_ip: ', callee_network_str, + ', subnet: ', arg.bypass_media_network, + ', uuid: ', self.caller.uuid, ', bleg_uuid: ', callee_uuid); + freeswitch.API():execute('uuid_media', 'off ' .. self.caller.uuid); + freeswitch.API():execute('uuid_media', 'off ' .. callee_uuid); + end + end + end + + if self.on_answer then + self.on_answer(self.calling_object, destination); + end + + self.caller:set_variable('gs_destination_type', destination.type); + self.caller:set_variable('gs_destination_id', destination.id); + self.caller:set_variable('gs_destination_uuid', destination.uuid); + + self.log:info('FORK ', fork_index, + ' BRIDGE - destination: ', destination.type, '=', destination.id, '/', destination.uuid,'@', destination.node_id, + ', number: ', destination.number, + ', dial_time: ', os.time() - start_time); + freeswitch.bridge(self.caller.session, session_callee); + self:wait_hangup(self.caller.session, session_callee); + end + + -- if session_callee:ready() then + -- self.log:info('FORK - hangup destination channel'); + -- session_callee:hangup('ORIGINATOR_CANCEL'); + -- end + + call_result = {}; + call_result.disposition = session_callee:hangupCause(); + call_result.fork_index = fork_index; + + if some_destinations_busy and call_result.disposition == 'USER_NOT_REGISTERED' then + call_result.phrase = 'User busy'; + call_result.code = 486; + call_result.disposition = 'USER_BUSY'; + elseif call_result.disposition == 'USER_NOT_REGISTERED' then + call_result.phrase = 'User offline'; + call_result.code = 480; + elseif call_result.disposition == 'NO_ANSWER' then + call_result.phrase = 'No answer'; + call_result.code = 408; + elseif call_result.disposition == 'NORMAL_TEMPORARY_FAILURE' then + call_result.phrase = 'User offline'; + call_result.code = 480; + else + call_result.cause = self.caller:to_s('last_bridge_hangup_cause'); + call_result.code = self.caller:to_i('last_bridge_proto_specific_hangup_cause'); + call_result.phrase = self.caller:to_s('sip_hangup_phrase'); + end + + self.log:info('FORK EXIT - disposition: ', call_result.disposition, + ', cause: ', call_result.cause, + ', code: ', call_result.code, + ', phrase: ', call_result.phrase, + ', dial_time: ', os.time() - start_time); + + return call_result; +end + +-- Return call forwarding settngs +function SipCall.conditional_call_forwarding(self, cause, call_forwarding) + local condition_map = {USER_NOT_REGISTERED="offline", NO_ANSWER="noanswer", USER_BUSY="busy"} + local condition = condition_map[cause] + if call_forwarding and condition and call_forwarding[condition] then + log:debug('call forwarding on ' .. condition .. ' - destination: ' .. call_forwarding[condition].destination .. ', type: ' .. call_forwarding[condition].call_forwardable_type); + return call_forwarding[condition] + end +end + +function SipCall.set_callee_variables(self, sip_account) + self.session:setVariable("gs_callee_account_id", sip_account.id); + self.session:setVariable("gs_callee_account_type", "SipAccount"); + self.session:setVariable("gs_callee_account_owner_type", sip_account.sip_accountable_type); + self.session:setVariable("gs_callee_account_owner_id", sip_account.sip_accountable_id); +end diff --git a/misc/freeswitch/scripts/dialplan/tenant.lua b/misc/freeswitch/scripts/dialplan/tenant.lua new file mode 100644 index 0000000..8d6436c --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/tenant.lua @@ -0,0 +1,51 @@ +-- Gemeinschaft 5 module: user class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Tenant = {} + +-- Create Tenant object +function Tenant.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self; + self.class = 'tenant'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object; +end + +-- find tenant by id +function Tenant.find_by_id(self, id) + local sql_query = 'SELECT * FROM `tenants` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + local tenant = nil; + + self.database:query(sql_query, function(account_entry) + tenant = Tenant:new(self); + tenant.record = account_entry; + tenant.id = tonumber(account_entry.id); + tenant.uuid = account_entry.uuid; + end); + + return tenant; +end + +-- find tenant by uuid +function Tenant.find_by_uuid(self, uuid) + tenant_id = tonumber(tenant_id) + local sql_query = 'SELECT * FROM `tenants` WHERE `id`= "' .. uuid .. '" LIMIT 1'; + local tenant = nil; + + self.database:query(sql_query, function(account_entry) + tenant = Tenant:new(self); + tenant.record = account_entry; + tenant.id = tonumber(account_entry.id); + tenant.uuid = account_entry.uuid; + end); + + return tenant; +end diff --git a/misc/freeswitch/scripts/dialplan/user.lua b/misc/freeswitch/scripts/dialplan/user.lua new file mode 100644 index 0000000..3b483c8 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/user.lua @@ -0,0 +1,91 @@ +-- Gemeinschaft 5 module: user class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +User = {} + +MAX_GROUP_MEMBERSHIPS = 256; + +-- create user object +function User.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'user'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object; +end + +-- find user by id +function User.find_by_id(self, id) + local sql_query = 'SELECT * FROM `users` WHERE `id`= ' .. tonumber(id) .. ' LIMIT 1'; + local user = nil; + + self.database:query(sql_query, function(account_entry) + user = User:new(self); + user.record = account_entry; + user.id = tonumber(account_entry.id); + user.uuid = account_entry.uuid; + end); + + return user; +end + +-- find user by uuid +function User.find_by_uuid(self, uuid) + local sql_query = 'SELECT * FROM `users` WHERE `id`= "' .. uuid .. '" LIMIT 1'; + local user = nil; + + self.database:query(sql_query, function(account_entry) + user = User:new(self); + user.record = account_entry; + user.id = tonumber(account_entry.id); + user.uuid = account_entry.uuid; + end); + + return user; +end + + +function User.list_groups(self, id) + require 'common.str' + id = id or self.id; + local sql_query = 'SELECT `b`.`name` FROM `user_group_memberships` `a` \ + JOIN `user_groups` `b` ON `a`.`user_group_id` = `b`.`id` \ + WHERE `a`.`state` = "active" AND `a`.`user_id`= ' .. tonumber(id) .. ' ORDER BY `b`.`position` LIMIT ' .. MAX_GROUP_MEMBERSHIPS; + + local groups = {}; + + self.database:query(sql_query, function(entry) + groups[common.str.downcase(entry.name)] = true; + end); + + return groups; +end + + +function User.check_pin(self, pin_to_check) + if not self.record then + return nil + end + + local str_to_hash = tostring(self.record.pin_salt) .. tostring(pin_to_check); + + local file = io.popen("echo -n " .. str_to_hash .. "|sha256sum"); + local pin_to_check_hash = file:read("*a"); + file:close(); + + pin_to_check_hash = pin_to_check_hash:sub(1, 64); + + if pin_to_check_hash == self.record.pin_hash then + return true; + end + + return false; +end + diff --git a/misc/freeswitch/scripts/dialplan/voicemail.lua b/misc/freeswitch/scripts/dialplan/voicemail.lua new file mode 100644 index 0000000..b9dab79 --- /dev/null +++ b/misc/freeswitch/scripts/dialplan/voicemail.lua @@ -0,0 +1,155 @@ +-- Gemeinschaft 5 module: voicemail class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Voicemail = {} + +MESSAGE_LENGTH_MIN = 3; +MESSAGE_LENGTH_MAX = 120; +SILENCE_LENGTH_ABORT = 5; +SILENCE_LEVEL = 500; +BEEP = 'tone_stream://%(1000,0,500)'; +RECORD_FILE_PREFIX = '/tmp/voicemail_'; + +-- create voicemail object +function Voicemail.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.class = 'voicemail'; + self.log = arg.log; + self.database = arg.database; + self.record = arg.record; + return object +end + +-- find voicemail account by sip account id +function Voicemail.find_by_sip_account_id(self, id) + local sql_query = 'SELECT `a`.`id`, `a`.`uuid`, `a`.`auth_name`, `a`.`caller_name`, `b`.`name_path`, `b`.`greeting_path`, `a`.`voicemail_pin`, `b`.`password`, `c`.`host` AS `domain` \ + FROM `sip_accounts` `a` LEFT JOIN `voicemail_prefs` `b` ON `a`.`auth_name` = `b`.`username` \ + JOIN `sip_domains` `c` ON `a`.`sip_domain_id` = `c`.`id` \ + WHERE `a`.`id` = ' .. tonumber(id); + + local voicemail_account = nil; + self.database:query(sql_query, function(entry) + voicemail_account = Voicemail:new(self); + voicemail_account.record = entry; + voicemail_account.id = tonumber(entry.id); + voicemail_account.uuid = entry.uuid; + end) + + return voicemail_account; +end + +-- Find Voicemail account by name +function Voicemail.find_by_name(self, account_name) + id = tonumber(id) or 0; + local sql_query = string.format('SELECT * FROM `voicemail_prefs` WHERE `username`= "%s" LIMIT 1', account_name) + local record = nil + + self.database:query(sql_query, function(voicemail_entry) + record = voicemail_entry + end) + + if voicemail_account then + voicemail_account.account_name = account_name; + if record then + voicemail_account.name_path = record.name_path; + voicemail_account.greeting_path = record.greeting_path; + voicemail_account.password = record.password; + end + end + + return voicemail_account +end + +-- Find Voicemail account by name +function Voicemail.find_by_number(self, phone_number) + local sip_account = nil; + + require "common.phone_number" + local phone_number_class = common.phone_number.PhoneNumber:new{ log = self.log, database = self.database }; + local destination_number_object = phone_number_class:find_by_number(phone_number); + if destination_number_object and destination_number_object.record.phone_numberable_type == "SipAccount" then + return Voicemail:find_by_sip_account_id(destination_number_object.record.phone_numberable_id); + end + + return false; +end + + +function Voicemail.leave(self, caller, phone_number) + require 'common.str' + + self.log:info('VOICEMAIL_LEAVE - account=', self.record.id, '/', self.record.uuid, ', auth_name: ', self.record.auth_name, ', caller_name: ', self.record.caller_name); + + caller:set_callee_id(phone_number, self.record.caller_name); + caller:answer(); + caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number)); + caller:sleep(1000); + + if not common.str.blank(self.record.greeting_path) then + caller.session:sayPhrase('voicemail_play_greeting', 'greeting:' .. tostring(self.record.greeting_path)); + elseif not common.str.blank(self.record.name_path) then + caller.session:sayPhrase('voicemail_play_greeting', 'name:' .. tostring(self.record.name_path)); + elseif not common.str.blank(phone_number) then + caller.session:sayPhrase('voicemail_play_greeting', (tostring(phone_number):gsub('[%D]', ''))); + end + + local record_file_name = RECORD_FILE_PREFIX .. caller.uuid .. '.wav'; + caller.session:streamFile(BEEP); + self.log:info('VOICEMAIL_LEAVE - recording to file: ', tostring(record_file_name)); + local result = caller.session:recordFile(record_file_name, MESSAGE_LENGTH_MAX, SILENCE_LEVEL, SILENCE_LENGTH_ABORT); + local duration = caller:to_i('record_seconds'); + + if duration >= MESSAGE_LENGTH_MIN then + self.log:info('VOICEMAIL_LEAVE - saving recorded message to voicemail, duration: ', duration); + require 'common.fapi' + common.fapi.FApi:new{ log = self.log, uuid = caller.uuid }:execute('vm_inject', + self.record.auth_name .. + '@' .. self.record.domain .. " '" .. + record_file_name .. "' '" .. + caller.caller_id_number .. "' '" .. + caller.caller_id_name .. "' '" .. + caller.uuid .. "'" + ); + caller:set_variable('voicemail_message_len', duration); + else + caller:set_variable('voicemail_message_len'); + end + os.remove(record_file_name); + return true; +end + + +function Voicemail.send_notify(self, caller) + self.log:debug('VOICEMAIL_NOTIFY - account: ' .. self.record.auth_name .. ", id: " .. tostring(caller.uuid)); + + local file = io.popen("/opt/GS5/script/voicemail_new.sh '" .. tostring(self.record.auth_name) .. "' '" .. tostring(caller.uuid) .. "' 2>&1"); + self.log:debug('VOICEMAIL_NOTIFY - result: ' .. tostring(file:read("*a"))); + file:close(); + + return true; +end + + +function Voicemail.menu(self, caller, authorized) + self.log:info('VOICEMAIL_MENU - account: ', self.record.auth_name); + + if authorized then + caller:set_variable('voicemail_authorized', true); + end + + caller:set_callee_id(phone_number, self.record.caller_name); + caller:answer(); + caller:send_display(common.str.to_s(self.record.caller_name), common.str.to_s(phone_number)); + + caller:sleep(1000); + caller:set_variable('skip_greeting', true); + caller:set_variable('skip_instructions', true); + + caller:execute('voicemail', 'check default ' .. self.record.domain .. ' ' .. self.record.auth_name); +end diff --git a/misc/freeswitch/scripts/dialplan_default.lua b/misc/freeswitch/scripts/dialplan_default.lua new file mode 100644 index 0000000..ee4a88f --- /dev/null +++ b/misc/freeswitch/scripts/dialplan_default.lua @@ -0,0 +1,64 @@ +-- Gemeinschaft 5 default dialplan +-- (c) AMOOMA GmbH 2012 +-- + + +function hangup_hook_caller(s, status, arg) + log:info('HANGUP_HOOK: ', status) + if tostring(status) == 'transfer' then + if start_caller and start_caller.destination then + log:info('CALL_TRANSFERRED - destination was: ', start_caller.destination.type, '=', start_caller.destination.id,', number: ' .. tostring(start_caller.destination.number) .. ', to: ' .. start_caller:to_s('sip_refer_to')); + start_caller.auth_account = start_dialplan:object_find(start_caller.destination.type, start_caller.destination.id); + start_caller.forwarding_number = start_caller.destination.number; + start_caller.forwarding_service = 'transfer'; + end + end +end + +-- initialize logging +require 'common.log' +log = common.log.Log:new{ prefix = '### [' .. session:get_uuid() .. '] ' }; + +-- caller session object +require 'dialplan.session' +start_caller = dialplan.session.Session:new{ log = log, session = session }; + +-- dialplan object +require 'dialplan.dialplan' + +start_dialplan = dialplan.dialplan.Dialplan:new{ log = log, caller = start_caller }; +start_dialplan:configuration_read(); +start_caller.local_node_id = start_dialplan.node_id; +start_caller:init_channel_variables(); + +-- session:execute('info','notice'); + +if not start_dialplan:check_auth() then + log:debug('AUTHENTICATION_REQUIRED - domain: ', start_dialplan.domain); + start_dialplan:hangup(407, start_dialplan.domain); + return false; +end + +-- connect to database +require 'common.database' +local database = common.database.Database:new{ log = log }:connect(); +if not database:connected() then + log:critical('DIALPLAN_DEFAULT - database connect failed'); + return; +end + +start_dialplan.database = database; + +if start_caller.from_node and not start_dialplan:check_auth_node() then + log:debug('AUTHENTICATION_REQUIRED_NODE - node_id: ', start_caller.node_id, ', domain: ', start_dialplan.domain); + start_dialplan:hangup(407, start_dialplan.domain); +else + start_destination = { type = 'unknown' } + start_caller.session:setHangupHook('hangup_hook_caller', 'destination_number'); + start_dialplan:run(start_destination); +end + +-- release database handle +if database then + database:release(); +end diff --git a/misc/freeswitch/scripts/event/call_history_save.lua b/misc/freeswitch/scripts/event/call_history_save.lua new file mode 100644 index 0000000..057ca16 --- /dev/null +++ b/misc/freeswitch/scripts/event/call_history_save.lua @@ -0,0 +1,74 @@ +-- Gemeinschaft 5 module: call_history event handler class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +function handler_class() + return CallHistorySave +end + +CallHistorySave = {} + + +function CallHistorySave.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'callhistorysave' + self.database = arg.database; + self.domain = arg.domain; + + return object; +end + + +function CallHistorySave.event_handlers(self) + return { CHANNEL_DESTROY = { [true] = self.channel_destroy } } +end + + +function CallHistorySave.channel_destroy(self, event) + local uuid = event:getHeader('Unique-ID'); + local direction = event:getHeader('variable_direction'); + + require 'common.str' + local save_cdr = common.str.to_b(event:getHeader('variable_gs_save_cdr')); + + if not save_cdr then + self.log:debug('[', uuid,'] CALL_HISTORY_SAVE - event: CHANNEL_DESTROY, direction: ', direction, ', save_cdr: ', save_cdr); + return false; + end + + require 'common.call_history' + call_history_class = common.call_history.CallHistory:new{ log = self.log, database = self.database } + + -- caller entry + local account_type = event:getHeader('variable_gs_account_type'); + local account_id = common.str.to_i(event:getHeader('variable_gs_account_id')); + + if account_type and account_id > 0 and common.str.to_b(event:getHeader('variable_gs_account_node_local')) then + call_history_class:insert_event(uuid, account_type, account_id, 'dialed', event); + else + self.log:info('[', uuid,'] CALL_HISTORY_SAVE - ignore caller entry - account: ', account_type, '=', account_id, ', local: ', event:getHeader('variable_gs_account_node_local')); + end + + -- callee entry + local account_type = event:getHeader('variable_gs_destination_type'); + local account_id = common.str.to_i(event:getHeader('variable_gs_destination_id')); + + if account_type and account_id > 0 + and common.str.to_b(event:getHeader('variable_gs_destination_node_local')) + and tostring(event:getHeader('variable_gs_call_service')) ~= 'pickup' then + + if tostring(event:getHeader('variable_endpoint_disposition')) == 'ANSWER' then + call_history_class:insert_event(uuid, account_type, account_id, 'received', event); + else + call_history_class:insert_event(uuid, account_type, account_id, 'missed', event); + end + else + self.log:info('[', uuid,'] CALL_HISTORY_SAVE - ignore callee entry - account: ', account_type, '=', account_id, ', local: ', event:getHeader('variable_gs_destination_node_local')); + end +end diff --git a/misc/freeswitch/scripts/event/cdr_save.lua b/misc/freeswitch/scripts/event/cdr_save.lua new file mode 100644 index 0000000..ed53aa3 --- /dev/null +++ b/misc/freeswitch/scripts/event/cdr_save.lua @@ -0,0 +1,105 @@ +-- Gemeinschaft 5 module: cdr event handler class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + + +function handler_class() + return CdrSave +end + + +function camelize_type(account_type) + ACCOUNT_TYPES = { + sipaccount = 'SipAccount', + conference = 'Conference', + faxaccount = 'FaxAccount', + callthrough = 'Callthrough', + huntgroup = 'HuntGroup', + automaticcalldistributor = 'AutomaticCallDistributor', + } + + return ACCOUNT_TYPES[account_type] or account_type; +end + + +CdrSave = {} + + +function CdrSave.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'cdrsave' + self.database = arg.database; + self.domain = arg.domain; + + return object; +end + + +function CdrSave.event_handlers(self) + return { CHANNEL_DESTROY = { [true] = self.channel_destroy } } +end + + +function CdrSave.channel_destroy(self, event) + local uuid = event:getHeader('Unique-ID'); + local direction = event:getHeader('variable_direction'); + + require 'common.str' + local save_cdr = common.str.to_b(event:getHeader('variable_gs_save_cdr')); + + if not save_cdr then + self.log:debug('[', uuid,'] CDR_SAVE - event: CHANNEL_DESTROY, direction: ', direction, ', save_cdr: ', save_cdr); + return false; + end + + require 'common.str' + local cdr = {} + + cdr.uuid = common.str.to_sql(uuid); + cdr.bleg_uuid = common.str.to_sql(event:getHeader('variable_bridge_uuid')); + cdr.dialed_number = common.str.to_sql(event:getHeader('Caller-Destination-Number')); + cdr.destination_number = common.str.to_sql(event:getHeader('variable_gs_destination_number')); + cdr.caller_id_number = common.str.to_sql(event:getHeader('variable_effective_caller_id_number')); + cdr.caller_id_name = common.str.to_sql(event:getHeader('variable_effective_caller_id_name')); + cdr.callee_id_number = common.str.to_sql(event:getHeader('variable_effective_callee_id_number')); + cdr.callee_id_name = common.str.to_sql(event:getHeader('variable_effective_callee_id_name')); + cdr.start_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Created-Time')) / 1000000) .. ')'; + cdr.answer_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Answered-Time')) / 1000000) .. ')'; + cdr.end_stamp = 'FROM_UNIXTIME(' .. math.floor(common.str.to_i(event:getHeader('Caller-Channel-Hangup-Time')) / 1000000) .. ')'; + cdr.bridge_stamp = common.str.to_sql(event:getHeader('variable_bridge_stamp')); + cdr.duration = common.str.to_sql(event:getHeader('variable_duration')); + cdr.billsec = common.str.to_sql(event:getHeader('variable_billsec')); + cdr.hangup_cause = common.str.to_sql(event:getHeader('variable_hangup_cause')); + cdr.dialstatus = common.str.to_sql(event:getHeader('variable_DIALSTATUS')); + cdr.forwarding_number = common.str.to_sql(event:getHeader('variable_gs_forwarding_number')); + cdr.forwarding_service = common.str.to_sql(event:getHeader('variable_gs_forwarding_service')); + cdr.forwarding_account_id = common.str.to_sql(event:getHeader('variable_gs_auth_account_id')); + cdr.forwarding_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_auth_account_type'))); + cdr.account_id = common.str.to_sql(event:getHeader('variable_gs_account_id')); + cdr.account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_account_type'))); + cdr.bleg_account_id = common.str.to_sql(event:getHeader('variable_gs_destination_id')); + cdr.bleg_account_type = common.str.to_sql(camelize_type(event:getHeader('variable_gs_destination_type'))); + + local keys = {} + local values = {} + + for key, value in pairs(cdr) do + table.insert(keys, key); + table.insert(values, value); + end + + self.log:info('[', uuid,'] CDR_SAVE - account: ', cdr.account_type, '=', cdr.account_id, + ', caller: ', cdr.caller_id_number, ' ', cdr.caller_id_name, + ', callee: ', cdr.callee_id_number, ' ', cdr.callee_id_name, + ', cause: ', cdr.hangup_cause + ); + + local sql_query = 'INSERT INTO `cdrs` (`' .. table.concat(keys, "`, `") .. '`) VALUES (' .. table.concat(values, ", ") .. ')'; + return self.database:query(sql_query); +end diff --git a/misc/freeswitch/scripts/event/event.lua b/misc/freeswitch/scripts/event/event.lua new file mode 100644 index 0000000..8e67bc9 --- /dev/null +++ b/misc/freeswitch/scripts/event/event.lua @@ -0,0 +1,109 @@ +-- Gemeinschaft 5 module: event manager class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +EventManager = {} + +-- create event manager object +function EventManager.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'eventmanager' + self.database = arg.database; + self.domain = arg.domain; + + return object; +end + + +function EventManager.register(self) + self.consumer = freeswitch.EventConsumer('all'); + return (self.consumer ~= nil); +end + + +function EventManager.load_event_modules(self) + local CONFIG_FILE_NAME = '/opt/freeswitch/scripts/ini/events.ini'; + + require 'common.configuration_file' + self.config = common.configuration_file.get(CONFIG_FILE_NAME); + + return self.config.modules; +end + + +function EventManager.load_event_handlers(self, event_modules) + event_handlers = {} + + for index, event_module_name in ipairs(event_modules) do + event_module = require('event.' .. event_module_name); + if event_module then + self.log:info('[event] EVENT_MANAGER - loading handler module: ', event_module_name); + handler_class = event_module.handler_class(); + + if handler_class then + module_event_handlers = handler_class:new{ log = self.log, database = self.database, domain = self.domain }:event_handlers(); + if module_event_handlers then + for event_name, event_subclasses in pairs(module_event_handlers) do + if not event_handlers[event_name] then + event_handlers[event_name] = {}; + end + + for event_subclass, module_event_handler in pairs(event_subclasses) do + if not event_handlers[event_name][event_subclass] then + event_handlers[event_name][event_subclass] = {}; + end + + table.insert(event_handlers[event_name][event_subclass], { class = handler_class, method = module_event_handler } ); + self.log:info('[event] EVENT_MANAGER - module: ', event_module_name, ', handling events: ', event_name, ', subclass:', event_subclass); + end + end + end + end + end + end + + return event_handlers; +end + + +function EventManager.run(self) + + local event_modules = self:load_event_modules(); + local event_handlers = self:load_event_handlers(event_modules); + + if not event_handlers then + self.log:error('[event] EVENT_MANAGER - no handlers specified'); + return nil; + end + + if not self:register() then + return nil; + end + + freeswitch.setGlobalVariable('gs_event_manager', 'true'); + while freeswitch.getGlobalVariable('gs_event_manager') == 'true' do + local event = self.consumer:pop(1, 100); + if event then + local event_type = event:getType(); + local event_subclass = event:getHeader('Event-Subclass'); + if event_handlers[event_type] then + if event_handlers[event_type][event_subclass] and #event_handlers[event_type][event_subclass] > 0 then + for index, event_handler in ipairs(event_handlers[event_type][event_subclass]) do + event_handler.method(event_handler.class, event); + end + end + if event_handlers[event_type][true] and #event_handlers[event_type][true] > 0 then + for index, event_handler in ipairs(event_handlers[event_type][true]) do + event_handler.method(event_handler.class, event); + end + end + end + end + end +end diff --git a/misc/freeswitch/scripts/event/perimeter.lua b/misc/freeswitch/scripts/event/perimeter.lua new file mode 100644 index 0000000..3babba6 --- /dev/null +++ b/misc/freeswitch/scripts/event/perimeter.lua @@ -0,0 +1,106 @@ +-- Gemeinschaft 5 module: cdr event handler class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + + +function handler_class() + return Perimeter +end + + + +Perimeter = {} + +MALICIOUS_CONTACT_COUNT = 20; +MALICIOUS_CONTACT_TIME_SPAN = 2; +BAN_FUTILE = 2; + +function Perimeter.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'cdrsave' + self.database = arg.database; + self.domain = arg.domain; + + self.ip_address_table = {} + self:init(); + + return object; +end + + +function Perimeter.event_handlers(self) + return { CUSTOM = { ['sofia::pre_register'] = self.sofia_pre_register } } +end + + +function Perimeter.init(self) + local config = common.configuration_file.get('/opt/freeswitch/scripts/ini/perimeter.ini'); + if config and config.general then + self.malicious_contact_count = tonumber(config.general.malicious_contact_count) or MALICIOUS_CONTACT_COUNT; + self.malicious_contact_time_span = tonumber(config.general.malicious_contact_time_span) or MALICIOUS_CONTACT_TIME_SPAN; + self.ban_futile = tonumber(config.general.ban_futile) or BAN_FUTILE; + self.execute = config.general.execute; + end + + self.log:info('[perimeter] PERIMETER - setup perimeter defense - config: ', self.malicious_contact_count, '/', self.malicious_contact_time_span, ', execute: ', self.execute); +end + + +function Perimeter.sofia_pre_register(self, event) + local ip_address = event:getHeader('network-ip'); + self:check_ip(ip_address); +end + + +function Perimeter.check_ip(self, ip_address) + local event_time = os.time(); + + if not self.ip_address_table[ip_address] then + self.ip_address_table[ip_address] = { last_contact = event_time, contact_count = 0, start_stamp = event_time, banned = 0 } + end + + local ip_record = self.ip_address_table[ip_address]; + ip_record.last_contact = event_time; + ip_record.contact_count = ip_record.contact_count + 1; + + if ip_record.contact_count > MALICIOUS_CONTACT_COUNT then + if (event_time - ip_record.start_stamp) <= MALICIOUS_CONTACT_TIME_SPAN then + self.log:warning('[', ip_address, '] PERIMETER - too many registration attempts'); + ip_record.start_stamp = event_time; + ip_record.contact_count = 0; + if ip_record.banned < BAN_FUTILE then + ip_record.banned = ip_record.banned + 1; + self:ban_ip(ip_address); + else + self.log:error('[', ip_address, '] PERIMETER - ban futile'); + end + end + end +end + + +function Perimeter.ban_ip(self, ip_address) + self.ip_address = ip_address; + + if self.execute then + local command = self:expand_variables(self.execute); + self.log:debug('[', ip_address, '] PERIMETER - execute: ', command); + local result = os.execute(command); + if tostring(result) == '0' then + self.log:warning('[', ip_address, '] PERIMETER - IP banned'); + end + end +end + + +function Perimeter.expand_variables(self, line) + return (line:gsub('{([%a%d_-]+)}', function(captured) + return self[captured]; + end)) +end diff --git a/misc/freeswitch/scripts/event/presence_update.lua b/misc/freeswitch/scripts/event/presence_update.lua new file mode 100644 index 0000000..01ec17b --- /dev/null +++ b/misc/freeswitch/scripts/event/presence_update.lua @@ -0,0 +1,199 @@ + +module(...,package.seeall) + +function handler_class() + return PresenceUpdate +end + +ACCOUNT_RECORD_TIMEOUT = 120; + +PresenceUpdate = {} + +function PresenceUpdate.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'presenceupdate' + self.database = arg.database; + self.domain = arg.domain; + self.presence_accounts = {} + self.account_record = {} + + return object; +end + + +function PresenceUpdate.event_handlers(self) + return { + PRESENCE_PROBE = { [true] = self.presence_probe }, + CUSTOM = { ['sofia::register'] = self.sofia_register, ['sofia::unregister'] = self.sofia_ungerister }, + PRESENCE_IN = { [true] = self.presence_in }, + } +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'); + local account, domain = common.str.partition(event_from, '@'); + local subscription, domain = common.str.partition(event_to, '@'); + + self.log:debug('[', account, '] PRESENCE_UPDATE - subscription: ', subscription,', type: ', probe_type); + if (not self.presence_accounts[account] or not self.presence_accounts[account][subscription]) and subscription:find(DIALPLAN_FUNCTION_PATTERN) then + if not self.presence_accounts[account] then + self.presence_accounts[account] = {}; + end + if not self.presence_accounts[account][subscription] then + self.presence_accounts[account][subscription] = {}; + end + self:update_function_presence(account, domain, subscription); + end +end + + +function PresenceUpdate.sofia_register(self, event) + local account = event:getHeader('from-user'); + self.log:debug('[', account, '] PRESENCE_UPDATE - flushing account cache on register'); + self.presence_accounts[account] = nil; +end + + +function PresenceUpdate.sofia_ungerister(self, event) + local account = event:getHeader('from-user'); + 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 state = event:getHeader('presence-call-info-state'); + local uuid = event:getHeader('Unique-ID'); + local caller_id = event:getHeader('Caller-Caller-ID-Number'); + + 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); + end +end + + +function PresenceUpdate.update_function_presence(self, account, domain, subscription) + local parameters = common.str.to_a(subscription, '_%-'); + local fid = parameters[2]; + local function_parameter = parameters[3]; + + if not fid then + self.log:error('[', account, '] PRESENCE_UPDATE - no function specified'); + return; + end + + if fid == 'cftg' and tonumber(function_parameter) then + self:call_forwarding(account, domain, function_parameter); + elseif fid == 'hgmtg' then + self:hunt_group_membership(account, domain, function_parameter); + elseif fid == 'acdmtg' then + self:acd_membership(account, domain, function_parameter); + end + +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' + if call_forwarding and common.str.to_b(call_forwarding.record.active) then + local destination_type = tostring(call_forwarding.record.call_forwardable_type):lower() + + self.log:debug('[', account, '] PRESENCE_UPDATE - updating call forwarding presence - id: ', call_forwarding_id, ', destination: ', destination_type); + + if destination_type == 'voicemail' then + call_forwarding:presence_set('early'); + else + call_forwarding:presence_set('confirmed'); + end + end +end + + +function PresenceUpdate.hunt_group_membership(self, account, domain, member_id) + local sql_query = 'SELECT `active` FROM `hunt_group_members` WHERE `active` IS TRUE AND `id`=' .. tonumber(member_id) .. ' LIMIT 1'; + local status = self.database:query_return_value(sql_query); + + 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, + domain = domain, + accounts = {'f-hgmtg-' .. member_id}, + uuid = 'hunt_group_member_' .. member_id + }:set('confirmed'); + end +end + + +function PresenceUpdate.acd_membership(self, account, domain, member_id) + local sql_query = 'SELECT `status` FROM `acd_agents` WHERE `status` = "active" AND `id`=' .. tonumber(member_id) .. ' LIMIT 1'; + local status = self.database:query_return_value(sql_query); + + 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, + domain = domain, + accounts = {'f-acdmtg-' .. member_id}, + uuid = 'acd_agent_' .. member_id + }:set(status); + end +end + + +function PresenceUpdate.sip_account(self, inbound, account, domain, status, uuid, caller_id) + local status_map = { progressing = 'early', alerting = 'confirmed', active = 'confirmed' } + + 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); + + self.account_record[account] = { id = sip_account.id, class = sip_account.class, phone_numbers = phone_numbers, created_at = os.time() } + end + + require 'dialplan.presence' + local result = dialplan.presence.Presence:new{ + log = self.log, + database = self.database, + inbound = inbound, + domain = domain, + accounts = self.account_record[account].phone_numbers, + uuid = uuid + }:set(status_map[status] or 'terminated', caller_id); +end diff --git a/misc/freeswitch/scripts/event_manager.lua b/misc/freeswitch/scripts/event_manager.lua new file mode 100644 index 0000000..0e3c0e0 --- /dev/null +++ b/misc/freeswitch/scripts/event_manager.lua @@ -0,0 +1,39 @@ +-- Gemeinschaft 5.0 event handler +-- (c) AMOOMA GmbH 2012 +-- + +-- Set logger +require "common.log" +local log = common.log.Log:new() +log.prefix = "#E# " + +log:info('[event] EVENT_MANAGER start'); + +require 'common.database' +local database = common.database.Database:new{ log = log }:connect(); +if not database:connected() then + log:error('[event] EVENT_MANAGER - cannot connect to Gemeinschaft database'); + return; +end + +require "configuration.sip" +local sip = configuration.sip.Sip:new{ log = log, database = database } + +local domain = '127.0.0.1'; +local domains = sip:domains(); +if domains[1] then + domain = domains[1]['host']; +else + log:error('[event] EVENT_MANAGER - No SIP domains found!'); +end + +require 'event.event' +local event_manager = event.event.EventManager:new{ log = log, database = database, domain = domain } +event_manager:run(); + +-- ensure database handle is released on exit +if database then + database:release(); +end + +log:info('[event] EVENT_MANAGER exit'); diff --git a/misc/freeswitch/scripts/fax_daemon.lua b/misc/freeswitch/scripts/fax_daemon.lua new file mode 100644 index 0000000..cfe7c4e --- /dev/null +++ b/misc/freeswitch/scripts/fax_daemon.lua @@ -0,0 +1,42 @@ +-- Gemeinschaft 5.0 fax daemon +-- (c) AMOOMA GmbH 2012 +-- + +local MAIN_LOOP_SLEEP_TIME = 30; + +-- Set logger +require "common.log" +local log = common.log.Log:new() +log.prefix = "### [faxdaemon] " + +log:debug('Starting fax daemon'); + +local database = nil; +local api = freeswitch.API(); + +freeswitch.setGlobalVariable('gs_fax_daemon', 'true'); +while freeswitch.getGlobalVariable("gs_fax_daemon") == 'true' do + require 'common.database' + local database = common.database.Database:new{ log = log }:connect(); + + if not database:connected() then + log:error("connection to Gemeinschaft database lost - retry in " .. MAIN_LOOP_SLEEP_TIME .. " seconds") + else + require 'dialplan.fax' + local fax_documents = dialplan.fax.Fax:new{log=log, database=database}:queued_for_sending(); + + for key, fax_document in pairs(fax_documents) do + if table.getn(fax_document.destination_numbers) > 0 and tonumber(fax_document.retry_counter) > 0 then + log:debug('FAX_DAEMON_LOOP - fax_document=', fax_document.id, '/', fax_document.uuid, ', number: ' .. fax_document.destination_numbers[1]); + local result = api:executeString('luarun send_fax.lua ' .. fax_document.id); + end + end + end + database:release(); + + if freeswitch.getGlobalVariable("gs_fax_daemon") == 'true' then + freeswitch.msleep(MAIN_LOOP_SLEEP_TIME * 1000); + end +end + +log:debug('Exiting fax daemon'); diff --git a/misc/freeswitch/scripts/ini/conferences.ini b/misc/freeswitch/scripts/ini/conferences.ini new file mode 100644 index 0000000..d8d0817 --- /dev/null +++ b/misc/freeswitch/scripts/ini/conferences.ini @@ -0,0 +1,27 @@ +; Gemeinschaft 5 conferences configuration file +; (c) AMOOMA GmbH 2012 +; + +[parameters] +caller-controls = speaker +moderator-controls = moderator +max-members = 100 +rate = 16000 +interval = 20 +energy-level = 300 +sound-prefix = /opt/freeswitch/sounds/en/us/callie +muted-sound = conference/conf-muted.wav +unmuted-sound = conference/conf-unmuted.wav +alone-sound = conference/conf-alone.wav +moh-sound = local_stream://moh +enter-sound = tone_stream://%(200,0,500,600,700) +exit-sound = tone_stream://%(500,0,300,200,100,50,25) +kicked-sound = conference/conf-kicked.wav +locked-sound = conference/conf-locked.wav +is-locked-sound = conference/conf-is-locked.wav +is-unlocked-sound = conference/conf-is-unlocked.wav +pin-sound = conference/conf-pin.wav +bad-pin-sound = conference/conf-bad-pin.wav +caller-id-name = Conference +caller-id-number = +comfort-noise = true diff --git a/misc/freeswitch/scripts/ini/database.ini b/misc/freeswitch/scripts/ini/database.ini new file mode 100644 index 0000000..1652118 --- /dev/null +++ b/misc/freeswitch/scripts/ini/database.ini @@ -0,0 +1,11 @@ +; Gemeinschaft 5 database configuration +; (c) AMOOMA GmbH 2012 +; + +driver = mysql + +[mysql] +host = localhost +database = gemeinschaft +user = gemeinschaft +password = gemeinschaft diff --git a/misc/freeswitch/scripts/ini/dialplan.ini b/misc/freeswitch/scripts/ini/dialplan.ini new file mode 100644 index 0000000..f4a6b66 --- /dev/null +++ b/misc/freeswitch/scripts/ini/dialplan.ini @@ -0,0 +1,11 @@ +; Gemeinschaft 5 dialplan configuration file +; (c) AMOOMA GmbH 2012 +; + +[parameters] +node_id = 1 +phone_book_entry_image_url = http://192.168.0.150/uploads/phone_book_entry/image +user_image_url = http://192.168.0.150/uploads/user/image +ringtone_url = http://192.168.0.150 +ringback = %(2000,4000,440.0,480.0) +tone_busy = %(500,500,480,620);loops=4 diff --git a/misc/freeswitch/scripts/ini/events.ini b/misc/freeswitch/scripts/ini/events.ini new file mode 100644 index 0000000..e63eb73 --- /dev/null +++ b/misc/freeswitch/scripts/ini/events.ini @@ -0,0 +1,8 @@ +; Gemeinschaft 5 routing configuration file +; (c) AMOOMA GmbH 2012 +; + +[modules] +cdr_save +call_history_save +presence_update diff --git a/misc/freeswitch/scripts/ini/gateways.ini.example b/misc/freeswitch/scripts/ini/gateways.ini.example new file mode 100644 index 0000000..b6ae018 --- /dev/null +++ b/misc/freeswitch/scripts/ini/gateways.ini.example @@ -0,0 +1,23 @@ +; Gemeinschaft 5 gateways configuration file +; (c) AMOOMA GmbH 2012 +; + +[gateway1] +profile = gemeinschaft +name = gateway1 +username = gateway1 +realm = gemeinschaft +password = freeswitch +extension = default +proxy = 192.168.0.1 +expire-seconds = 600 +register = true + +[gateway2] +profile = gemeinschaft +name = sipgate +username = 1234567e0 +password = ABCdeF +proxy = sipgate.com +register = true +extension = {sip_to_user} diff --git a/misc/freeswitch/scripts/ini/perimeter.ini b/misc/freeswitch/scripts/ini/perimeter.ini new file mode 100644 index 0000000..ecbb032 --- /dev/null +++ b/misc/freeswitch/scripts/ini/perimeter.ini @@ -0,0 +1,9 @@ +; Gemeinschaft 5 perimeter defense configuration file +; (c) AMOOMA GmbH 2012 +; + +[general] +malicious_contact_count = 20 +malicious_contact_time_span = 2 +ban_futile = 5 +execute = sudo /usr/local/bin/ban_ip.sh {ip_address} diff --git a/misc/freeswitch/scripts/ini/routes.ini b/misc/freeswitch/scripts/ini/routes.ini new file mode 100644 index 0000000..1334e7b --- /dev/null +++ b/misc/freeswitch/scripts/ini/routes.ini @@ -0,0 +1,77 @@ +; Gemeinschaft 5 routing configuration file +; (c) AMOOMA GmbH 2012 +; + +[general] + + +[prerouting] +^%*0%*$ , f-li +^%*0%*(%d+)#*$ , f-li-%1 +^%*0%*(%d+)%*(%d+)#*$ , f-li-%1-%2 +^#0#$ , f-lo +^%*30#$ , f-clipon +^#30#$ , f-clipoff +^%*31#(%d+)$ , f-dclirof, f-%1 +^#31#(%d+)$ , f-dcliron-%1 +^%*43#$ , f-cwaon +^#43#$ , f-cwaoff +^#002#$ , f-cfoff +^##002#$ , f-cfdel +^%*21#$ , f-cfu +^%*21%*(%d+)#$ , f-cfu-%1 +^%*%*21%*(%d+)#$ , f-cfu-%1 +^#21#$ , f-cfuoff +^##21#$ , f-cfudel +^%*61#$ , f-cfn +^%*61%*(%d+)#$ , f-cfn-%1 +^%*%*61%*(%d+)#$ , f-cfn-%1 +^%*61%*(%d+)%*(%d+)#$ , f-cfn-%1-%2 +^%*%*61%*(%d+)%*(%d+)#$ , f-cfn-%1-%2 +^#61#$ , f-cfnoff +^##61#$ , f-cfndel +^%*62#$ , f-cfo +^%*62%*(%d+)#$ , f-cfo-%1 +^%*%*62%*(%d+)#$ , f-cfo-%1 +^#62#$ , f-cfooff +^##62#$ , f-cfodel +^%*67#$ , f-cfb +^%*67%*(%d+)#$ , f-cfb-%1 +^%*%*67%*(%d+)#$ , f-cfb-%1 +^#67#$ , f-cfboff +^##67#$ , f-cfbdel +^%*98$ , f-vmcheck +^%*98#$ , f-vmcheck +^%*98%*(%d+)#$ , f-vmcheck-%1 +^%*1337%*1%*1#$ , f-loaon +^%*1337%*1%*0#$ , f-loaoff + +^00(%d+)$ , +%1 +^0(%d+)$ , +49%1 + + +[outbound] +^%+(%d+)$ , class=gateway, endpoint=gateway1, group=users, %1 +^([1-9]%d+)$ , class=gateway, endpoint=gateway1, group=users, %1 + + +[failover] +UNALLOCATED_NUMBER = true +NORMAL_TEMPORARY_FAILURE = true + + +[outbound_cid_number] + + +[outbound_cid_name] + + +[inbound] +^00(%d+)$ , +%1 +^0(%d+)$ , +49%1 + +[inbound_cid_number] +^00(%d+)$ , +%1 +^0(%d+)$ , +49%1 + +[inbound_cid_name] diff --git a/misc/freeswitch/scripts/ini/sip_accounts.ini b/misc/freeswitch/scripts/ini/sip_accounts.ini new file mode 100644 index 0000000..73a5fae --- /dev/null +++ b/misc/freeswitch/scripts/ini/sip_accounts.ini @@ -0,0 +1,10 @@ +; Gemeinschaft 5 sip accounts default parameters +; (c) AMOOMA GmbH 2012 +; + +[parameters] +vm-enabled = true +vm-email-all-messages = false +vm-attach-file = false +vm-mailto = + diff --git a/misc/freeswitch/scripts/ini/sofia.ini b/misc/freeswitch/scripts/ini/sofia.ini new file mode 100644 index 0000000..9c73990 --- /dev/null +++ b/misc/freeswitch/scripts/ini/sofia.ini @@ -0,0 +1,55 @@ +; Gemeinschaft 5 sofia configuration file +; (c) AMOOMA GmbH 2012 +; + +[profiles] +gemeinschaft + +[parameters] +log-level = 3 +debug-presence = 0 + +[profile:gemeinschaft] +user-agent-string = Gemeinschaft5 +debug = 0 +sip-trace = no +log-auth-failures = false +context = default +rfc2833-pt = 101 +pass-rfc2833 = true +sip-port = 5060 +dialplan = XML +dtmf-duration = 2000 +rtp-timer-name = soft +inbound-codec-prefs = PCMA,G7221@32000h,G7221@16000h,G722,PCMU,GSM +outbound-codec-prefs = PCMA,G7221@32000h,G7221@16000h,G722,PCMU,GSM +inbound-codec-negotiation = greedy +ext-rtp-ip = auto-nat +ext-sip-ip = auto-nat +hold-music = local_stream://moh +manage-presence = true +tls = false +tls-sip-port = 5061 +tls-cert-dir = /opt/freeswitch/conf/ssl +accept-blind-reg = false +accept-blind-auth = false +nonce-ttl = 60 +disable-transcoding = false +manual-redirect = true +disable-transfer = false +disable-register = false +auth-calls = false +inbound-reg-force-matching-username = true +auth-all-packets = false +rtp-timeout-sec = 300 +rtp-hold-timeout-sec = 1800 +force-subscription-expires = 3600 +sip-force-expires = 3000 +sip-expires-max-deviation = 600; +challenge-realm = auto_from +rtp-rewrite-timestamps = true +inbound-use-callid-as-uuid = false +outbound-use-callid-as-uuid = false +context = default +record-template = /${record_file} +odbc-dsn = gemeinschaft:gemeinschaft:gemeinschaft diff --git a/misc/freeswitch/scripts/phones/phone.lua b/misc/freeswitch/scripts/phones/phone.lua new file mode 100644 index 0000000..5cd210b --- /dev/null +++ b/misc/freeswitch/scripts/phones/phone.lua @@ -0,0 +1,114 @@ +-- Gemeinschaft 5 module: phone class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Phone = {} + +-- create phone object +function Phone.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.database = arg.database; + return object; +end + + + +-- Find a hot-deskable phone by sip-account +function Phone.find_all_hot_deskable_by_account(self, account_id) + require 'common.str' + + local sql_query = 'SELECT \ + `b`.`id`, `b`.`mac_address`, `b`.`ip_address`, `b`.`http_user`, `b`.`http_password`, `b`.`phoneable_type`, `b`.`phoneable_id`, \ + `d`.`ieee_name` \ + FROM `phone_sip_accounts` `a` \ + JOIN `phones` `b` ON `a`.`phone_id` = `b`.`id` \ + JOIN `phone_models` `c` ON `b`.`phone_model_id` = `c`.`id` \ + JOIN `manufacturers` `d` ON `c`.`manufacturer_id` = `d`.`id` \ + WHERE `b`.`hot_deskable` IS TRUE \ + AND `c`.`state` = "active" \ + AND `d`.`state` = "active" \ + AND `a`.`sip_account_id` = ' .. tonumber(account_id); + + local account_phones = {}; + + self.database:query(sql_query, function(account_entry) + local phone = Phone:new(self, {object = parent_class}); + phone.record = account_entry; + phone.record.ieee_name = common.str.downcase(account_entry.ieee_name); + + if phone.record.ieee_name == 'snom technology ag' then + require 'phones.snom' + phone.model = phones.snom.Snom:new(); + elseif account_entry.ieee_name == 'siemens enterprise communicationsgmbh & co. kg' then + require 'phones.siemens' + phone.model = phones.siemens.Siemens:new(); + end + table.insert(account_phones, phone); + end) + + return account_phones; +end + + +function Phone.find_hot_deskable_by_account(self, account_id) + return self:find_all_hot_deskable_by_account(account_id)[1]; +end + + +function Phone.tenant_id_get(self) + local sql_query = 'SELECT `c`.`sip_accountable_id` \ + FROM `phones` `a` LEFT JOIN `phone_sip_accounts` `b` ON `a`.`id` = `b`.`phone_id` \ + JOIN `sip_accounts` `c` ON `b`.`sip_account_id` = `c`.`id` AND `sip_accountable_type` = "Tenant" \ + WHERE `a`.`id` = ' .. tonumber(self.record.id) .. ' LIMIT 1'; + + local tenant_id = nil; + self.database:query(sql_query, function(tenant_entry) + tenant_id = tenant_entry.sip_accountable_id; + end) + + return tenant_id; +end + +function Phone.phoneable_set(self, phoneable_id, phoneable_type) + sql_query = 'UPDATE `phones` SET `phoneable_type` = "' .. phoneable_type ..'", `phoneable_id` = ' .. phoneable_id .. ' \ + WHERE `id` = ' .. tonumber(self.record.id); + self.database:query(sql_query); +end + +function Phone.logout(self, account_id) + local tenant_id = self:tenant_id_get(); + + if not tenant_id then + self.log:info('PHONE_LOGOUT - tenant not found'); + return false; + end + + self:phoneable_set(tenant_id, 'Tenant'); + + sql_query = 'DELETE FROM `phone_sip_accounts` WHERE `sip_account_id` = ' .. tonumber(account_id); + return self.database:query(sql_query); +end + +function Phone.login(self, account_id, owner_id, owner_type) + self:phoneable_set(owner_id, owner_type); + sql_query = 'INSERT INTO `phone_sip_accounts` (`phone_id`, `sip_account_id`, `position`, `created_at`, `updated_at`) \ + VALUES ('.. tonumber(self.record.id) .. ', '.. tonumber(account_id) .. ', 1, NOW(), NOW())'; + + return self.database:query(sql_query); +end + +function Phone.resync(self, arg) + if not self.model then + self.log:notice('PHONE_RESYNC - unsupported phone model'); + return false; + end + + arg.ip_address = arg.ip_address or self.record.ip_address; + return self.model:resync(arg); +end
\ No newline at end of file diff --git a/misc/freeswitch/scripts/phones/siemens.lua b/misc/freeswitch/scripts/phones/siemens.lua new file mode 100644 index 0000000..71bb40a --- /dev/null +++ b/misc/freeswitch/scripts/phones/siemens.lua @@ -0,0 +1,45 @@ +-- Gemeinschaft 5 module: general siemens model class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Siemens = {} + +-- create siemens object +function Siemens.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.PHONE_HTTP_PORT = 8085; + return object; +end + +-- send reload message to phone +function Siemens.resync(self, arg) + if arg.ip_address then + return self:resync_http(arg.ip_address, arg.http_user, arg.http_password, arg.http_port); + end + + return false; +end + +-- send reload message to ip +function Siemens.resync_http(self, ip_address, http_user, http_password, http_port) + local port_str = ''; + if tonumber(http_port) then + port_str = ':' .. http_port; + end + + get_command = 'wget --no-proxy -q -O /dev/null -o /dev/null -b --tries=2 --timeout=10 --user="' .. (http_user or '') .. '" --password="' .. (http_password or '') .. '"' .. + ' wget http://' .. tostring(ip_address):gsub('[^0-9%.]', '') .. ':' .. (tonumber(http_port) or self.PHONE_HTTP_PORT) .. '/contact_dls.html/ContactDLS' .. + ' 1>>/dev/null 2>>/dev/null &'; + + result = os.execute(get_command); + + if result and tonumber(result) == 0 then + return true; + end +end diff --git a/misc/freeswitch/scripts/phones/snom.lua b/misc/freeswitch/scripts/phones/snom.lua new file mode 100644 index 0000000..80d1fce --- /dev/null +++ b/misc/freeswitch/scripts/phones/snom.lua @@ -0,0 +1,65 @@ +-- Gemeinschaft 5 module: general snom model class +-- (c) AMOOMA GmbH 2012 +-- + +module(...,package.seeall) + +Snom = {} + +-- Create Snom object +function Snom.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.reboot = arg.reboot or true; + return object; +end + +-- send reload message to phone +function Snom.resync(self, arg) + if arg.reboot == nil then + arg.reboot = self.reboot; + end + + local success = nil; + if arg.auth_name and arg.domain then + success = self:resync_sip(arg.auth_name, arg.domain, arg.reboot); + end + + if arg.ip_address and arg.reboot then + success = self:resync_http(arg.ip_address, arg.http_user, arg.http_password, arg.http_port); + end + + return success; +end + +-- send reload message to sip_account +function Snom.resync_sip(self, sip_account, domain, reboot) + local event = freeswitch.Event('NOTIFY'); + event:addHeader('profile', 'gemeinschaft'); + event:addHeader('event-string', 'check-sync;reboot=' .. tostring(reboot)); + event:addHeader('user', sip_account); + event:addHeader('host', domain); + event:addHeader('content-type', 'application/simple-message-summary'); + return event:fire(); +end + +-- send reload message to ip +function Snom.resync_http(self, ip_address, http_user, http_password, http_port) + local port_str = ''; + if tonumber(http_port) then + port_str = ':' .. http_port; + end + + get_command = 'wget --no-proxy -q -O /dev/null -o /dev/null -b --tries=2 --timeout=10 --user="' .. (http_user or '') .. '" --password="' .. (http_password or '') .. '"' .. + ' wget http://' .. tostring(ip_address):gsub('[^0-9%.]', '') .. port_str .. '/advanced.htm?reboot=Reboot' .. + ' 1>>/dev/null 2>>/dev/null &'; + + result = os.execute(get_command); + + if result and tonumber(result) == 0 then + return true; + end +end diff --git a/misc/freeswitch/scripts/phones/uacsta.lua b/misc/freeswitch/scripts/phones/uacsta.lua new file mode 100644 index 0000000..61cb788 --- /dev/null +++ b/misc/freeswitch/scripts/phones/uacsta.lua @@ -0,0 +1,100 @@ +-- CommonModule: Uacsta +-- +module(...,package.seeall) + +Uacsta = {} + +-- Create Uacsta object +function Uacsta.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self) + self.__index = self + self.log = arg.log; + + return object +end + +function Uacsta.send(self, sip_account, domain, body) + local event = freeswitch.Event("NOTIFY"); + event:addHeader("profile", "gemeinschaft"); + event:addHeader("event-string", "uaCSTA"); + event:addHeader("user", sip_account); + event:addHeader("host", domain); + event:addHeader("content-type", "application/csta+xml"); + event:addBody(body); + event:fire(); +end + +function Uacsta.make_call(self, sip_account, domain, number) + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<MakeCall xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed4"> + <callingDevice>]] .. sip_account .. [[</callingDevice> + <calledDirectoryNumber>]] .. number .. [[</calledDirectoryNumber> + <autoOriginate>doNotPrompt</autoOriginate> +</MakeCall>]] + + self:send(sip_account, domain, body); +end + +function Uacsta.answer_call(self, sip_account, domain) + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<AnswerCall xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed4"> + <callToBeAnswered> + <deviceID>]] .. sip_account .. [[</deviceID> + </callToBeAnswered> +</AnswerCall>]] + + self:send(sip_account, domain, body); +end + +function Uacsta.set_microphone_mute(self, sip_account, domain, value) + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<SetMicrophoneMute xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3"> + <device>]] .. sip_account .. [[</device> + <auditoryApparatus>1</auditoryApparatus> + <microphoneMuteOn>]] .. tostring(value) .. [[</microphoneMuteOn> +</SetMicrophoneMute>]] + + self:send(sip_account, domain, body); +end + +function Uacsta.set_speaker_volume(self, sip_account, domain, value) + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<SetSpeakerVolume xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3"> + <device>]] .. sip_account .. [[</device> + <auditoryApparatus>1</auditoryApparatus> + <speakerVolume>]] .. tonumber(value) .. [[</speakerVolume> +</SetSpeakerVolume>]] + + self:send(sip_account, domain, body); +end + +function Uacsta.set_do_not_disturb(self, sip_account, domain, value) + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<SetDoNotDisturb xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3"> + <device>]] .. sip_account .. [[</device> + <doNotDisturbOn>]] .. tostring(value) .. [[</doNotDisturbOn> +</SetDoNotDisturb>]] + + self:send(sip_account, domain, body); +end + +function Uacsta.set_forwarding(self, sip_account, domain, forwarding_type, number, activate) + local forwarding_types = { "forwardImmediate", "forwardBusy", "forwardNoAns" } + local body = +[[<?xml version="1.0" encoding="UTF-8"?> +<SetForwarding xmlns="http://www.ecma-international.org/standards/ecma-323/csta/ed3"> + <device>]] .. sip_account .. [[</device> + <forwardingType>]] .. tostring(forwarding_types[tonumber(forwarding_type)]) .. [[</forwardingType> + <forwardDN>]] .. number .. [[</forwardDN> + <activateForward>]] .. tostring(activate) .. [[</activateForward> +</SetForwarding>]] + + self:send(sip_account, domain, body); +end diff --git a/misc/freeswitch/scripts/send_fax.lua b/misc/freeswitch/scripts/send_fax.lua new file mode 100644 index 0000000..321a5b1 --- /dev/null +++ b/misc/freeswitch/scripts/send_fax.lua @@ -0,0 +1,170 @@ +-- Gemeinschaft 5.0 +-- (c) AMOOMA GmbH 2012 +-- + +local FAX_FILE_PATH = "/opt/GS5/public/uploads/fax_document/tiff/"; +local FAX_ANSWERING_TIMEOUT = 20; + +-- Set logger +require "common.log" +local log = common.log.Log:new() +log.prefix = "### [sendfax] " + +local document_id = argv[1]; + +require 'common.database' +local database = common.database.Database:new{ log = log }:connect(); + +if not database:connected() then + log:error('cannot connect to Gemeinschaft database'); + return +end + +if not tonumber(document_id) then + log:error('document id not specified'); + return +end + +local defaults = {log=log, database=database} +require "dialplan.fax" +local fax_class = dialplan.fax.Fax:new(defaults); + +local fax_document = fax_class:find_document_by_id(document_id); + +if not fax_document then + log:error('document ' .. document_id .. ' not found'); + return +end + +if tonumber(fax_document.retry_counter) > 0 then + fax_class:document_update(document_id, {state = 'sending', retry_counter = fax_document.retry_counter - 1}); +else + fax_class:document_update(document_id, {state = 'sending'}); +end + +local fax_account = fax_class:find_by_id(fax_document.fax_account_id); + +if not fax_account then + log:error('fax account ' .. fax_document.fax_account_id .. ' not found'); + return +end + +local destination_number = fax_class:destination_number(document_id); + +if not destination_number or tostring(destination_number) == '' then + log:error('destination number not found'); + return +end + +require 'common.str' +destination_number = common.str.strip(destination_number); + +log:info('FAX_SEND - fax_document=' .. document_id .. ', destination number: ' .. destination_number .. ', retries: ' .. fax_document.retry_counter); + +require "common.phone_number" +local phone_number_class = common.phone_number.PhoneNumber:new(defaults); + +phone_number = phone_number_class:find_by_number(destination_number); + +local origination_variables = { + 'gs_account_id=' .. fax_account.record.id, + 'gs_account_uuid=' .. fax_account.record.uuid, + 'gs_account_type=' .. 'faxaccount', + 'gs_auth_account_id=' .. fax_account.record.id, + 'gs_auth_account_uuid=' .. fax_account.record.uuid, + 'gs_auth_account_type=' .. 'faxaccount', +} + +local session = nil + +if phone_number then + session = freeswitch.Session("[" .. table.concat(origination_variables, ",") .. "]loopback/" .. destination_number .. "/default"); +else + local owner_class = common.str.downcase(fax_account.record.fax_accountable_type); + + local caller = {} + caller.caller_phone_numbers = phone_number_class:list_by_owner(fax_account.record.id, 'FaxAccount'); + caller.account = fax_account; + caller.auth_account = fax_account; + caller.caller_id_name = fax_account.record.station_id; + + if owner_class == 'user' then + require 'dialplan.user' + caller.auth_account.owner = dialplan.user.User:new{ log = log, database = database }:find_by_id(fax_account.record.fax_accountable_id); + if caller.auth_account.owner then + caller.auth_account.owner.groups = caller.auth_account.owner:list_groups(); + end + elseif owner_class == 'tenant' then + require 'dialplan.tenant' + caller.auth_account.owner = dialplan.tenant.Tenant:new{ log = log, database = database }:find_by_id(fax_account.record.fax_accountable_id); + end + + require 'common.configuration_file' + local routing_table = common.configuration_file.get('/opt/freeswitch/scripts/ini/routes.ini'); + require 'dialplan.route' + local routes = dialplan.route.Route:new{ log = log, database = database, routing_table = routing_table }:outbound(caller, destination_number); + + for index, route in ipairs(routes) do + log:info('FAX_SEND - ', route.class, '=', route.endpoint, ', number: ', route.value); + if route.class == 'gateway' then + table.insert(origination_variables, "origination_caller_id_number='" .. (route.caller_id_number or caller.caller_phone_numbers[1]) .. "'"); + table.insert(origination_variables, "origination_caller_id_name='" .. (route.caller_id_name or fax_account.record.station_id) .. "'"); + session = freeswitch.Session('[' .. table.concat(origination_variables, ',') .. ']sofia/gateway/' .. route.endpoint .. '/' .. route.value); + break; + end + end +end + +local loop_count = FAX_ANSWERING_TIMEOUT; +local cause = "UNSPECIFIED" + +while session and session:ready() and not session:answered() and loop_count >= 0 do + log:debug('waiting for answer: ' .. loop_count) + loop_count = loop_count - 1; + freeswitch.msleep(1000); +end + +if session and session:answered() then + log:info('FAX_SEND - sending fax_document=' .. fax_document.id .. ' (' .. fax_document.tiff .. ')'); + + local file_name = FAX_FILE_PATH .. fax_document.id .. "/" .. fax_document.tiff; + + session:setVariable('fax_ident', fax_account.record.station_id) + session:setVariable('fax_header', fax_account.record.name) + session:setVariable('fax_verbose', 'false') + local start_time = os.time(); + session:execute('txfax', file_name); + + fax_state = { + state = nil, + transmission_time = os.time() - start_time, + document_total_pages = common.str.to_i(session:getVariable('fax_document_total_pages')), + document_transferred_pages = common.str.to_i(session:getVariable('fax_document_transferred_pages')), + ecm_requested = common.str.to_b(session:getVariable('fax_ecm_requested')), + ecm_used = common.str.to_b(session:getVariable('fax_ecm_used')), + image_resolution = common.str.to_s(session:getVariable('fax_image_resolution')), + image_size = common.str.to_i(session:getVariable('fax_image_size')), + local_station_id = common.str.to_s(session:getVariable('fax_local_station_id')), + result_code = common.str.to_i(session:getVariable('fax_result_code')), + remote_station_id = common.str.to_s(session:getVariable('fax_remote_station_id')), + success = common.str.to_b(session:getVariable('fax_success')), + transfer_rate = common.str.to_i(session:getVariable('fax_transfer_rate')), + } + + if fax_state.success then + fax_state.state = 'successful'; + else + fax_state.state = 'unsuccessful'; + end + + fax_account:document_update(fax_document.id, fax_state) + + 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')); +else + if session then + cause = session:hangupCause(); + end + log:debug('Destination "', destination_number, '" could not be reached, cause: ', cause) + fax_account:document_update(fax_document.id, {state = 'unsuccessful', result_code = "129"}) +end |