summaryrefslogtreecommitdiff
path: root/misc/freeswitch/scripts/common
diff options
context:
space:
mode:
Diffstat (limited to 'misc/freeswitch/scripts/common')
-rw-r--r--misc/freeswitch/scripts/common/call_forwarding.lua47
-rw-r--r--misc/freeswitch/scripts/common/call_history.lua140
-rw-r--r--misc/freeswitch/scripts/common/conference.lua239
-rw-r--r--misc/freeswitch/scripts/common/configuration_file.lua70
-rw-r--r--misc/freeswitch/scripts/common/database.lua151
-rw-r--r--misc/freeswitch/scripts/common/fapi.lua80
-rw-r--r--misc/freeswitch/scripts/common/ipcalc.lua27
-rw-r--r--misc/freeswitch/scripts/common/log.lua69
-rw-r--r--misc/freeswitch/scripts/common/node.lua73
-rw-r--r--misc/freeswitch/scripts/common/phone_number.lua359
-rw-r--r--misc/freeswitch/scripts/common/routing_tables.lua66
-rw-r--r--misc/freeswitch/scripts/common/sip_account.lua137
-rw-r--r--misc/freeswitch/scripts/common/str.lua136
-rw-r--r--misc/freeswitch/scripts/common/sync_log.lua39
14 files changed, 1633 insertions, 0 deletions
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