diff --git a/resources/install/scripts/call_forward.lua b/resources/install/scripts/call_forward.lua index 2c1fc34ff9..47cf539748 100644 --- a/resources/install/scripts/call_forward.lua +++ b/resources/install/scripts/call_forward.lua @@ -45,6 +45,7 @@ local log = require "resources.functions.log".call_forward local cache = require "resources.functions.cache" local Database = require "resources.functions.database" + local route_to_bridge = require "resources.functions.route_to_bridge" local function opt(t, ...) if select('#', ...) == 0 then @@ -265,7 +266,20 @@ dial_string = dial_string .. "user/"..forward_all_destination.."@"..domain_name; end else - dial_string = dial_string .. "loopback/"..forward_all_destination; + local mode = opt(settings(domain_uuid), 'domain', 'bridge', 'text') + if mode == "outbound" or mode == "bridge" then + local bridge = route_to_bridge(dbh, domain_uuid, { + destination_number = forward_all_destination; + ['${toll_allow}'] = toll_allow; + }) + if bridge and bridge.bridge then + dial_string = dial_string .. bridge.bridge + else + log.warning('Can not build dialstring for call forward number.') + end + else + dial_string = dial_string .. "loopback/"..forward_all_destination; + 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 new file mode 100644 index 0000000000..f7098a8b7d --- /dev/null +++ b/resources/install/scripts/resources/functions/route_to_bridge.lua @@ -0,0 +1,282 @@ +local log = require "resources.functions.log".route_to_bridge + +local pcre_match + +if freeswitch then + --! @todo find better way to extract captures + api = api or freeswitch.API() + local unpack = unpack or table.unpack + function pcre_match(str, pat) + local a = str .. "|/" .. pat .."/" + if api:execute("regex", a) == 'false' then return end + local t = {} + for i = 1, 5 do + t[i] = api:execute("regex", a .. '|$' .. i) + end + return unpack(t) + end +else + local pcre = require "rex_pcre" + function pcre_match(str, pat) + return pcre.match(str, pat) + end +end + +local function pcre_self_test() + local a, b, c + a,b,c = pcre_match('abcd', '(\\d{3})(\\d{3})') + assert(a == nil) + + a,b,c = pcre_match('123456', '(\\d{3})(\\d{3})') + assert(a == '123', a) + assert(b == '456', b) +end + +local select_routes_sql = [[ +select * +from v_dialplans +where (domain_uuid = '%s' or domain_uuid 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 = '%s' +order by dialplan_detail_group asc, dialplan_detail_order asc +]] + +local function append(t, v) + t[#t + 1] = v + return t +end + +local function order_keys(t) + local o = {} + for k in pairs(t) do append(o, k) end + table.sort(o) + local i = 0 + return function(o) + i = i + 1 + return o[i], t[o[i]] + end, o +end + +local function check_conditions(group, fields) + local matches, pass, last, break_on + + for n, condition in ipairs(group.conditions) do + last = (n == #group.conditions) + + local value = fields[condition.type] + if (not value) and (condition.type ~= '') then -- skip unkonw fields + log.errf('Unsupportded condition: %s', condition.type) + matches, pass = {}, false + else + if condition.type == '' then + matches, pass = {}, true + else + matches = {pcre_match(value, condition.data)} + pass = #matches > 0 + end + log.debugf('%s condition %s(%s) to `%s`', pass and 'PASS' or 'FAIL', condition.type, condition.data, value or '') + 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 + end + + break_on = nil + end + + -- we shuld 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 + return act, not not break_on, matches +end + +local function apply_match(s, match) + return string.gsub(s, "%$(%d)", function(i) + return match[tonumber(i)] or '' + end) +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) + end + + -- we only support action/export + if element.type == 'export' or element.type == 'set' then + value = apply_match(value, matches) + append(actions, value) + end + + if element.type == 'bridge' then + actions.bridge = apply_match(value, matches) + break + end + end + end + + return do_break +end + +local function extension_to_bridge(extension, actions, fields) + for _, group in order_keys(extension) do + local do_break = group_to_bridge(actions, group, fields) + if do_break then break end + end +end + +local function self_test() + pcre_self_test() + + local test_conditions = { + { + {conditions={ + {type='destination_number', data='100', break_on=''}; + }}; + {destination_number = 100}; + {'action', false}; + }; + + { + {conditions={ + {type='destination_number', data='100', break_on='on-true'}; + }}; + {destination_number = 100}; + {'action', true}; + }; + + { + {conditions={ + {type='destination_number', data='101', break_on=''}; + }}; + {destination_number = 100}; + {'anti-action', true}; + }; + + { + {conditions={ + {type='destination_number', data='100', break_on=''}; + {type='destination_number', data='101', break_on=''}; + {type='destination_number', data='102', break_on=''}; + }}; + {destination_number = 100}; + {nil, true}; + }; + + { + {conditions={ + {type='destination_number', data='100', break_on='never'}; + {type='destination_number', data='101', break_on='never'}; + {type='destination_number', data='102', break_on='never'}; + }}; + {destination_number = 102}; + {'action', false}; + }; + + { + {conditions={ + {type='destination_number', data='100', break_on='never'}; + {type='destination_number', data='101', break_on='never'}; + {type='destination_number', data='102', break_on='never'}; + }}; + {destination_number = 103}; + {'anti-action', false}; + }; + + { + {conditions={ + {type='destination_number', data='100', break_on=''}; + {type='destination_number', data='101', break_on=''}; + {type='destination_number', data='102', break_on=''}; + }}; + {destination_number = 102}; + {nil, true}; + }; + + { + {conditions={ + {type='', data='', break_on=''}; + }}; + {}; + {'action', false}; + }; + + { + {conditions={ + {type='caller_id_number', data='123456', break_on=''}; + }}; + {}; + {'anti-action', true}; + }; + + } + + for i, test in ipairs(test_conditions) do + io.write('Test conditions #' .. i) + local group, fields, result = test[1], test[2], test[3] + local action, do_break, matches = check_conditions(group, fields) + assert(action == result[1], tostring(action)) + assert(do_break == result[2]) + io.write(' - ok\n') + end +end + +local function outbound_route_to_bridge(dbh, domain_uuid, fields) + local actions, dial_string = {} + + dbh:query(select_routes_sql:format(domain_uuid), function(route) + local extension = {} + dbh:query(select_extensions_sql:format(route.dialplan_uuid), 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 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) + + local n = #actions + + extension_to_bridge(extension, actions, fields) + + if actions.bridge or (n > #actions and route.dialplan_continue == 'false') then + return 1 + end + end) + + if actions.bridge then return actions end +end + +return setmetatable({ + __self_test = self_test; +}, {__call = function(_, ...) + return outbound_route_to_bridge(...) +end}) \ No newline at end of file