mirror of
https://github.com/fusionpbx/fusionpbx.git
synced 2026-01-06 11:43:50 +00:00
Add. Use route_to_bridge module to build routes for ring groups. (#2907)
* Add. Use `route_to_bridge` module to build routes fro ring groups. This commit has several improvements 1. Select only needed fields. (do not select quite big XML text strings) 2. Filter routes also by context name 3. Filter dialplans also by hostname 4. Handle conditions based not only `destination_number` 5. Handle `break` and `continue` attributes for extensions 6. Escape vars inside dial-string 7. Add log messages similar as FS dialplan do * Add. `route_to_bridge` set inline vars so it possible use then in next conditions. Add. `route_to_bridge` can execute basic api commands from allowed lists. `route_to_bridge` expand all known vars. If var is unknown then it pass as is. Fix. `export nolocal:` action. * Fix. Short variable names * Add. some comments * Fix. Do not try execute empty string This produce error messages `[ERR] switch_cpp.cpp:759 No application specified` * Fix. Export nolocal values.
This commit is contained in:
committed by
FusionPBX
parent
8e0cf6ae12
commit
679d4e1fb5
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user