diff --git a/resources/install/scripts/app/ring_groups/index.lua b/resources/install/scripts/app/ring_groups/index.lua index 6f7304355f..db7a8caf7f 100644 --- a/resources/install/scripts/app/ring_groups/index.lua +++ b/resources/install/scripts/app/ring_groups/index.lua @@ -30,6 +30,9 @@ --include the log local log = require "resources.functions.log".ring_group +-- include libs + local route_to_bridge = require "resources.functions.route_to_bridge" + --connect to the database local Database = require "resources.functions.database"; dbh = Database.new('system'); @@ -118,6 +121,7 @@ uuid = session:getVariable("uuid"); context = session:getVariable("context"); call_direction = session:getVariable("call_direction"); + accountcode = session:getVariable("accountcode"); end --default to local if nil @@ -170,6 +174,9 @@ -- error(); --end +--get current switchname + hostname = trim(api:execute("switchname", "")) + --get the ring group ring_group_forward_enabled = ""; ring_group_forward_destination = ""; @@ -187,7 +194,7 @@ missed_call_app = row["ring_group_missed_call_app"]; missed_call_data = row["ring_group_missed_call_data"]; end); - + --get the ring group user sql = "SELECT r.*, u.user_uuid FROM v_ring_groups as r, v_ring_group_users as u "; sql = sql .. "where r.ring_group_uuid = :ring_group_uuid "; @@ -304,7 +311,7 @@ --process the ring group if (ring_group_forward_enabled == "true" and string.len(ring_group_forward_destination) > 0) then --forward the ring group - session:setVariable("toll_allow",ring_group_forward_toll_allow); + session:setVariable("toll_allow",ring_group_forward_toll_allow); session:execute("transfer", ring_group_forward_destination.." XML "..context); else --get the strategy of the ring group, if random, we use random() to order the destinations @@ -422,33 +429,9 @@ --get the dialplan data and save it to a table if (external) then - sql = [[select * from v_dialplans as d, v_dialplan_details as s - where (d.domain_uuid = :domain_uuid or d.domain_uuid is null) - and d.app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3' - and d.dialplan_enabled = 'true' - and d.dialplan_uuid = s.dialplan_uuid - order by - d.dialplan_order asc, - d.dialplan_name asc, - d.dialplan_uuid asc, - s.dialplan_detail_group asc, - CASE s.dialplan_detail_tag - WHEN 'condition' THEN 1 - WHEN 'action' THEN 2 - WHEN 'anti-action' THEN 3 - ELSE 100 END, - s.dialplan_detail_order asc - ]]; - params = {domain_uuid = domain_uuid}; - if debug["sql"] then - freeswitch.consoleLog("notice", "[ring group] SQL:" .. sql .. "; params:" .. json.encode(params) .. "\n"); - end - dialplans = {}; - x = 1; - assert(dbh:query(sql, params, function(row) - dialplans[x] = row; - x = x + 1; - end)); + dialplans = route_to_bridge.preload_dialplan( + dbh, domain_uuid, {hostname = hostname, context = context} + ) end --process the destinations @@ -580,77 +563,65 @@ extension_uuid = trim(api:executeString(cmd)); --send to user local dial_string_to_user = "[sip_invite_domain="..domain_name..",call_direction="..call_direction..","..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay..",dialed_extension=" .. row.destination_number .. ",extension_uuid="..extension_uuid .. row.record_session .. "]user/" .. row.destination_number .. "@" .. domain_name; - dial_string = dial_string_to_user; + dial_string = dial_string_to_user; elseif (tonumber(destination_number) == nil) then --sip uri dial_string = "[sip_invite_domain="..domain_name..",call_direction="..call_direction..","..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay.."]" .. row.destination_number; else --external number - y = 0; - dial_string = ''; - previous_dialplan_uuid = ''; - regex_match = false; - for k, r in pairs(dialplans) do - if (y > 0) then - if (previous_dialplan_uuid ~= r.dialplan_uuid) then - regex_match = false; - bridge_match = false; - square = square .. "]"; - y = 0; + dial_string = nil + + local session_mt = {__index = function(_, k) return session:getVariable(k) end} + local params = setmetatable({ + __api__ = api, + destination_number = destination_number, + user_exists = 'false', + call_direction = 'outbound', + domain_name = domain_name, + domain_uuid = domain_uuid, + destination_timeout = destination_timeout, + destination_delay = destination_delay, + }, session_mt) + + local confirm = string.gsub(group_confirm, ',$', '') -- remove `,` from end of string + local route = route_to_bridge.apply_vars({ -- predefined actions + "domain_name=${domain_name}", + "domain_uuid=${domain_uuid}", + "sip_invite_domain=${domain_name}", + "leg_timeout=${destination_timeout}", + delay_name .. "=${destination_delay}", + "ignore_early_media=true", + confirm, + }, params) + + route = route_to_bridge(dialplans, domain_uuid, params, route) + + if route and route.bridge then + local remove_actions = { + ["effective_caller_id_name="] = true; + ["effective_caller_id_number="] = true; + ['sip_h_X-accountcode='] = true; + } + + -- cleanup variables + local i = 1 while i < #route do + -- remove vars from prev variant + if remove_actions[ route[i] ] then + table.remove(route, i) + i = i - 1 + -- remove vars with unresolved vars + elseif string.find(route[i], '%${.+}') then + table.remove(route, i) + i = i - 1 + -- remove vars with empty values + elseif string.find(route[i], '=$') then + table.remove(route, i) + i = i - 1 end + i = i + 1 end - - if (r.dialplan_detail_tag == "condition") then - if (r.dialplan_detail_type == "destination_number") then - dial_string = "regex m:~"..destination_number.."~"..r.dialplan_detail_data - if (api:execute("regex", "m:~"..destination_number.."~"..r.dialplan_detail_data) == "true") then - --get the regex result - destination_result = trim(api:execute("regex", "m:~"..destination_number.."~"..r.dialplan_detail_data.."~$1")); - --set match equal to true - regex_match = true; - end - end - end ---regex_match = true; ---dial_string = r.dialplan_detail_data; - if (r.dialplan_detail_tag == "action") then - if (regex_match) then ---dial_string = 'match'; - --replace $1 - dialplan_detail_data = r.dialplan_detail_data:gsub("$1", destination_result); - --if the session is set then process the actions - if (y == 0) then - square = "[domain_name="..domain_name..",domain_uuid="..domain_uuid..",sip_invite_domain="..domain_name..",call_direction=outbound,"..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay..",ignore_early_media=true,"; - end - if (r.dialplan_detail_type == "set") then - --session:execute("eval", dialplan_detail_data); - if (dialplan_detail_data == "sip_h_X-accountcode=${accountcode}") then - if (session) then - accountcode = session:getVariable("accountcode"); - if (accountcode) then - square = square .. "sip_h_X-accountcode="..accountcode..","; - end - end - elseif (dialplan_detail_data == "effective_caller_id_name=${outbound_caller_id_name}") then - elseif (dialplan_detail_data == "effective_caller_id_number=${outbound_caller_id_number}") then - else - square = square .. dialplan_detail_data..","; - end - elseif (r.dialplan_detail_type == "bridge") then - if (bridge_match) then - dial_string = dial_string .. delimiter .. square .."]"..dialplan_detail_data; - square = "["; - else - dial_string = square .."]"..dialplan_detail_data; - end - bridge_match = true; - break; - end - --increment the value - y = y + 1; - end - end - previous_dialplan_uuid = r.dialplan_uuid; + + dial_string = '[' .. table.concat(route, ',') .. ']' .. route.bridge end end @@ -776,12 +747,16 @@ or session:getVariable("originate_disposition") == "failure" ) then --execute the time out action - session:execute(ring_group_timeout_app, ring_group_timeout_data); + if ring_group_timeout_app and #ring_group_timeout_app > 0 then + session:execute(ring_group_timeout_app, ring_group_timeout_data); + end end else if (ring_group_timeout_app ~= nil) then --execute the time out action - session:execute(ring_group_timeout_app, ring_group_timeout_data); + if ring_group_timeout_app and #ring_group_timeout_app > 0 then + session:execute(ring_group_timeout_app, ring_group_timeout_data); + end else local sql = "SELECT ring_group_timeout_app, ring_group_timeout_data FROM v_ring_groups "; sql = sql .. "where ring_group_uuid = :ring_group_uuid"; @@ -791,7 +766,9 @@ end dbh:query(sql, params, function(row) --execute the time out action - session:execute(row.ring_group_timeout_app, row.ring_group_timeout_data); + if row.ring_group_timeout_app and #row.ring_group_timeout_app > 0 then + session:execute(row.ring_group_timeout_app, row.ring_group_timeout_data); + end end); end end diff --git a/resources/install/scripts/resources/functions/route_to_bridge.lua b/resources/install/scripts/resources/functions/route_to_bridge.lua index 5415aa0246..66c63b0c5e 100644 --- a/resources/install/scripts/resources/functions/route_to_bridge.lua +++ b/resources/install/scripts/resources/functions/route_to_bridge.lua @@ -1,4 +1,9 @@ local log = require "resources.functions.log".route_to_bridge +require "resources.functions.split" + +local allows_functions = { + ['user_data'] = true, +} local pcre_match @@ -48,20 +53,35 @@ local function pcre_self_test() io.write(' - ok\n') end -local select_routes_sql = [[ -select * -from v_dialplans -where (domain_uuid = :domain_uuid or domain_uuid is null) - and (hostname = :hostname or hostname is null) - and app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3' - and dialplan_enabled = 'true' -order by dialplan_order asc -]] - -local select_extensions_sql = [[ -select * from v_dialplan_details -where dialplan_uuid = :dialplan_uuid -order by dialplan_detail_group asc, dialplan_detail_order asc +local select_outbound_dialplan_sql = [[ +SELECT + d.dialplan_uuid, + d.dialplan_context, + d.dialplan_continue, + s.dialplan_detail_group, + s.dialplan_detail_break, + s.dialplan_detail_data, + s.dialplan_detail_inline, + s.dialplan_detail_tag, + s.dialplan_detail_type +FROM v_dialplans as d, v_dialplan_details as s +WHERE (d.domain_uuid = :domain_uuid OR d.domain_uuid IS NULL) + AND (d.hostname = :hostname OR d.hostname IS NULL) + AND d.app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3' + AND d.dialplan_enabled = 'true' + AND d.dialplan_uuid = s.dialplan_uuid +ORDER BY + d.dialplan_order ASC, + d.dialplan_name ASC, + d.dialplan_uuid ASC, + s.dialplan_detail_group ASC, + CASE s.dialplan_detail_tag + WHEN 'condition' THEN 1 + WHEN 'action' THEN 2 + WHEN 'anti-action' THEN 3 + ELSE 100 + END, + s.dialplan_detail_order ASC ]] local function append(t, v) @@ -87,6 +107,11 @@ local function check_conditions(group, fields) last = (n == #group.conditions) local value = fields[condition.type] + if (not value) and (condition_type ~= '') then -- try var name + local condition_type = string.match(condition.type, '^%${(.*)}$') + if condition_type then value = fields[condition_type] end + end + if (not value) and (condition.type ~= '') then -- skip unkonw fields log.errf('Unsupportded condition: %s', condition.type) matches, pass = {}, false @@ -101,20 +126,20 @@ local function check_conditions(group, fields) end break_on = condition.break_on - if break_on == 'always' then break end - if break_on ~= 'never' then - if pass and break_on == 'on-true' then break end - if not pass and (break_on == 'on-false' or break_on == '') then break end + if break_on == 'always' then break + elseif break_on ~= 'never' then + if pass then if break_on == 'on-true' then break end + elseif break_on == 'on-false' or break_on == '' then break end end break_on = nil end - -- we shuld execute action/anti-action only if we check ALL conditions + -- we should execute action/anti-action only if we check ALL conditions local act if last then act = pass and 'action' or 'anti-action' end - -- we shuld break + -- we should break return act, not not break_on, matches end @@ -124,23 +149,67 @@ local function apply_match(s, match) end) end +local function apply_var(s, fields) + local str = string.gsub(s, "%$?%${([^$%(%){}= ]-)}", function(var) + return fields[var] + end) + + if fields.__api__ then + local api = fields.__api__ + -- try call functions like ('set result=${user_data(args)}') + str = string.gsub(str, "%${([^$%(%){}= ]+)%s*%((.-)%)%s*}", function(fn, par) + if allows_functions[fn] then + return api:execute(fn, par) or '' + end + log.warningf('try call not allowed function %s', tostring(fn)) + end) + + -- try call functions like 'set result=${user_data args}' + str = string.gsub(str, "%${([^$%(%){}= ]+)%s+(%S.-)%s*}", function(fn, par) + if allows_functions[fn] then + return api:execute(fn, par) or '' + end + log.warningf('try call not allowed function %s', tostring(fn)) + end) + end + + if string.find(str, '%${.+}') then + log.warningf('can not resolve vars inside `%s`', tostring(str)) + end + return str +end + local function group_to_bridge(actions, group, fields) - local action, do_break, matches = check_conditions(group, fields) - if action then - local t = (action == 'action') and group.actions or group.anti_actions - for _, element in ipairs(t) do - local value = element.data - if element.type == 'export' and string.sub(value, 1, 8) == 'nolocal:' then - value = string.sub(value, 9) + local action_type, do_break, matches = check_conditions(group, fields) + if action_type then + local t = (action_type == 'action') and group.actions or group.anti_actions + for _, action in ipairs(t) do + local value = action.data + + -- we only support set/export actions + if action.type == 'export' or action.type == 'set' then + local key + + key, value = split_first(value, '=', true) + if key then + local bleg_only = (action.type == 'export') and (string.sub(key, 1, 8) == 'nolocal:') + if bleg_only then key = string.sub(key, 9) end + + value = apply_match(value, matches) + value = apply_var(value, fields) + + if action.inline and not bleg_only then + fields[key] = value + end + + --! @todo do value escape? + append(actions, key .. '=' .. value) + end end - -- we only support action/export - if element.type == 'export' or element.type == 'set' then + if action.type == 'bridge' then value = apply_match(value, matches) - append(actions, value) - end - - if element.type == 'bridge' then + value = apply_var(value, fields) actions.bridge = apply_match(value, matches) break end @@ -158,6 +227,69 @@ local function extension_to_bridge(extension, actions, fields) end local function self_test() + local unpack = unpack or table.unpack + + local function assert_equal(expected, actions) + for i = 1, math.max(#expected, #actions) do + local e, v, msg = expected[i], actions[i] + if not e then + msg = string.format("unexpected value #%d - `%s`", i, v) + elseif not v then + msg = string.format("expected value `%s` at position #%d, but got no value", e, i) + elseif e ~= v then + msg = string.format("expected value `%s` at position #%d but got: `%s`", e, i, v) + end + assert(not msg, msg) + end + + for name, e in pairs(expected) do + local v, msg = actions[name] + if not v then + msg = string.format("%s expected as `%s`, but got no value", name, e) + elseif e ~= v then + msg = string.format("expected value for %s is `%s`, but got: `%s`", name, e, v) + end + assert(not msg, msg) + end + + for name, v in pairs(actions) do + local e, msg = expected[name] + if not e then + msg = string.format("expected value %s = `%s`", name, v) + end + assert(not msg, msg) + end + + end + + local function test_grout_to_bridge(group, params, ret, expected) + local actions = {} + local result = group_to_bridge(actions, group, params) + if result ~= ret then + local msg = string.format('expected `%s` but got `%s`', tostring(ret), tostring(result)) + assert(false, msg) + end + assert_equal(expected, actions) + end + + -- mock for API + local function API(t) + local api = { + execute = function(self, cmd, args) + cmd = assert(t[cmd]) + return cmd[args] + end; + } + return api + end + + local old_log = log + log = { + errf = function() end; + warningf = function() end; + debugf = function() end; + } + pcre_self_test() local test_conditions = { @@ -251,53 +383,296 @@ local function self_test() assert(do_break == result[2]) io.write(' - ok\n') end + + local test_actions = { + { -- should not touch unknown vars + {actions={ + {type='set', data='a=${b}'} + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + }, + { -- result + 'a=${b}' + } + }, + { -- should call execute command with braces + {actions={ + {type='set', data='a=${user_data(a b c)}'} + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['a b c'] = 'value'}} + }, + { -- result + 'a=value' + } + }, + { -- should call execute command with spaces + {actions={ + {type='set', data='a=${user_data a b c }'} + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['a b c'] = 'value'}} + }, + { -- result + 'a=value' + } + }, + { -- should not call not allowed function + {actions={ + {type='set', data='a=${user_exists( a b c )}'} + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['a b c'] = 'value'}} + }, + { -- result + 'a=${user_exists( a b c )}' + } + }, + { -- should set inline vars + {actions={ + {type='set', data='a=hello', inline=true}, + {type='set', data='b=${a}'}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['a b c'] = 'value'}} + }, + { -- result + 'a=hello', + 'b=hello', + } + }, + { -- should not set not inline vars + {actions={ + {type='set', data='a=hello'}, + {type='set', data='b=${a}'}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['a b c'] = 'value'}} + }, + { -- result + 'a=hello', + 'b=${a}', + } + }, + { -- should expand vars inside call + {actions={ + {type='set', data='a=${user_data(${a}${b})}'}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + __api__ = API{user_data={['helloworld'] = 'value'}}, + a = 'hello', + b = 'world', + }, + { -- result + 'a=value', + } + }, + { -- should export nolocal + {actions={ + {type='export', data='a=nolocal:value', inline=true}, + {type='export', data='b=${a}'}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + }, + { -- result + 'a=value', + 'b=${a}', + } + }, + { -- should handle bridge as last action + {actions={ + {type='bridge', data='sofia/gateway/${a}'}, + {type='set', data='a=123', inline=true}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + a='gw' + }, + { -- result + bridge = 'sofia/gateway/gw' + } + }, + { -- should ingnore `nolocal` for set + {actions={ + {type='set', data='a=nolocal:123', inline=true}, + {type='export', data='b=${a}'}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + }, + { -- result + 'a=nolocal:123'; + 'b=nolocal:123'; + } + }, + { -- should ingnore unsupportded actions + {actions={ + {type='ring_ready', data=''}, + {type='answer', data=''}, + }; + conditions={{type='', data='', break_on='on-true'}}; + }, + { -- parameters + }, + { -- result + } + }, + } + + for i, test_case in ipairs(test_actions) do + local group, params, expected = unpack(test_case) + io.write('Test execute #' .. i) + test_grout_to_bridge(group, params, true, expected) + io.write(' - ok\n') + end + + log = old_log end -local function outbound_route_to_bridge(dbh, domain_uuid, fields) - local actions, dial_string = {} - require "resources.functions.trim"; - local hostname = trim(api:execute("switchname", "")); +-- Returns array of set/export actions and bridge command. +-- +-- This function does not set any var to session. +-- +-- @param dbh database connection +-- @param domain_uuid +-- @param fields list of avaliable channel variables. +-- if `context` provided then dialplan will be filtered by this var +-- `__api__` key can be used to pass freeswitch.API object for execute +-- some functions in actions (e.g. `s=${user_data ...}`) +-- @param actions optional list of predefined actions +-- @return array part of table will contain list of actions. +-- `bridge` key will contain bridge statement +local function outbound_route_to_bridge(dbh, domain_uuid, fields, actions) + actions = actions or {} - local params = {} - dbh:query(select_routes_sql, {domain_uuid=domain_uuid,hostname=hostname}, function(route) - local extension = {} - params.dialplan_uuid = route.dialplan_uuid - dbh:query(select_extensions_sql, params, function(ext) - local group_no = tonumber(ext.dialplan_detail_group) - local tag = ext.dialplan_detail_tag - local element = { - type = ext.dialplan_detail_type; - data = ext.dialplan_detail_data; - break_on = ext.dialplan_detail_break; - inline = ext.dialplan_detail_inline; - } + local hostname = fields.hostname + if not hostname then + require "resources.functions.trim"; + hostname = trim(api:execute("switchname", "")) + end - local group = extension[ group_no ] or { - conditions = {}; - actions = {}; - anti_actions = {}; - } - extension[ group_no ] = group + -- try filter by context + local context = fields.context + if context == '' then context = nil end - if tag == 'condition' then append(group.conditions, element) end - if tag == 'action' then append(group.actions, element) end - if tag == 'anti-action' then append(group.anti_actions, element) end - end) - - local n = #actions - - extension_to_bridge(extension, actions, fields) - - if actions.bridge or (n > #actions and route.dialplan_continue == 'false') then - return 1 + local current_dialplan_uuid, extension + dbh:query(select_outbound_dialplan_sql, {domain_uuid=domain_uuid, hostname=hostname}, function(route) + if context and context ~= route.dialplan_context then + -- skip dialplan for wrong contexts + return end + + if current_dialplan_uuid ~= route.dialplan_uuid then + if extension then + local n = #actions + extension_to_bridge(extension, actions, fields) + -- if we found bridge or add any action and there no continue flag + if actions.bridge or (n > #actions and route.dialplan_continue == 'false') then + extension = nil + return 1 + end + end + extension = {} + current_dialplan_uuid = route.dialplan_uuid + end + + local group_no = tonumber(route.dialplan_detail_group) + local tag = route.dialplan_detail_tag + local element = { + type = route.dialplan_detail_type; + data = route.dialplan_detail_data; + break_on = route.dialplan_detail_break; + inline = route.dialplan_detail_inline; + } + + local group = extension[ group_no ] or { + conditions = {}; + actions = {}; + anti_actions = {}; + } + extension[ group_no ] = group + + if tag == 'condition' then append(group.conditions, element) end + if tag == 'action' then append(group.actions, element) end + if tag == 'anti-action' then append(group.anti_actions, element) end end) + if extension and next(extension) then + extension_to_bridge(extension, actions, fields) + end + if actions.bridge then return actions end end +local function apply_vars(actions, fields) + for i, action in ipairs(actions) do + actions[i] = apply_var(action, fields) + end + return actions +end + +local function wrap_dbh(t) + local i = 0 + return {query = function(self, sql, params, callback) + while true do + i = i + 1 + local row = t[i] + if not row then break end + + local result = callback(row) + if result == 1 then break end + end + end} +end + +-- Load all extension for outbound routes and +-- returns object which can be used instead real DBH object to build +-- dialplan for specific destination_number +local function preload_dialplan(dbh, domain_uuid, fields) + local hostname = fields and fields.hostname + if not hostname then + require "resources.functions.trim"; + hostname = trim(api:execute("switchname", "")) + end + + -- try filter by context + local context = fields and fields.context + if context == '' then context = nil end + + local dialplan = {} + dbh:query(select_outbound_dialplan_sql, {domain_uuid=domain_uuid, hostname=hostname}, function(route) + if context and context ~= route.dialplan_context then + -- skip dialplan for wrong contexts + return + end + dialplan[#dialplan + 1] = route + end) + + return wrap_dbh(dialplan), dialplan +end + return setmetatable({ __self_test = self_test; + apply_vars = apply_vars; + preload_dialplan = preload_dialplan; }, {__call = function(_, ...) return outbound_route_to_bridge(...) end})