--[[
route.lua — запрашивает pulse-lets-go для маршрутизации вызова.
Телефонный балансировщик нагрузки на базе FreeSWITCH + NATS.
┌─ Размещение ─────────────────────────────────────────────────────┐
│ /etc/freeswitch/scripts/route.lua │
│ │
│ ┌─ Использование в dialplan ────────────────────────────────────┐│
│ │ ││
│ │ ││
│ │ ││
│ │ ││
│ │ ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Переменные канала (опционально) ─────────────────────────────┐│
│ │ balancer_url — URL pulse-lets-go (default: localhost:8080) ││
│ │ ingress_trunk — ID входящего транка ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Требования FreeSWITCH ───────────────────────────────────────┐│
│ │ mod_curl — в modules.conf.xml ││
│ │ cjson — входит в стандартную поставку FS Lua ││
│ └───────────────────────────────────────────────────────────────┘│
└───────────────────────────────────────────────────────────────────┘
Логика:
1. Извлечь caller_id + destination из переменных сессии
2. HTTP GET /api/route?caller_id=X&dest=Y&ingress_trunk=Z
3. Разобрать JSON-ответ
4. При fallback=true → bridge на fallback gateway (оператор)
5. При ошибке → кладём трубку с NORMAL_TEMPORARY_FAILURE
6. Нормальный маршрут → bridge на sip_gateway выбранной ноды
]]--
-- ===================================================================
-- Конфигурация
-- ===================================================================
-- URL балансировщика (переопределяется через переменную канала)
local BALANCER_URL = session:getVariable("balancer_url") or "http://localhost:8080"
-- Таймаут HTTP-запроса в секундах (через параметр curl)
local HTTP_TIMEOUT = tonumber(session:getVariable("route_timeout") or "3")
-- JSON-парсер
local cjson = require("cjson")
-- API FreeSWITCH для вызова mod_curl
local api = freeswitch.API()
-- ===================================================================
-- Вспомогательные функции
-- ===================================================================
-- Логирование с префиксом [route]
local function log(level, msg)
freeswitch.consoleLog(level, "[route] " .. msg .. "\n")
end
-- Выполнить HTTP GET и вернуть тело ответа (без заголовков).
-- mod_curl API возвращает сырой HTTP-ответ: заголовки + \r?\n\r?\n + тело.
local function http_get(url)
local cmd = "curl " .. url
local raw = api:execute("curl", cmd)
if not raw or raw == "" then
return nil, "пустой ответ от curl"
end
-- Отделяем заголовки от тела
-- Ищем двойной перенос строки (CRLF или LF)
local body = raw:match("\r?\n\r?\n(.+)")
if not body then
-- Если нет заголовков — весь ответ это тело
return raw, nil
end
return body, nil
end
-- Безопасная отправка трубки с кодом причины
local function safe_hangup(cause)
if session:ready() then
session:hangup(cause or "NORMAL_TEMPORARY_FAILURE")
end
end
-- ===================================================================
-- Основная логика
-- ===================================================================
-- Извлекаем параметры вызова
local caller_id = session:getVariable("caller_id_number") or ""
local dest = session:getVariable("destination_number") or ""
local ing_trunk = session:getVariable("ingress_trunk") or ""
-- Собираем URL
local query = string.format("caller_id=%s&dest=%s", caller_id, dest)
if ing_trunk ~= "" then
query = query .. "&ingress_trunk=" .. ing_trunk
end
local url = BALANCER_URL .. "/api/route?" .. query
log("info", "запрос маршрута: " .. url)
-- Запрашиваем маршрут у pulse-lets-go
local body, curl_err = http_get(url)
if not body then
log("err", "ошибка HTTP: " .. (curl_err or "unknown"))
safe_hangup()
return
end
-- Парсим JSON
local ok, route = pcall(cjson.decode, body)
if not ok or type(route) ~= "table" then
log("err", "ошибка парсинга JSON: " .. tostring(route))
safe_hangup()
return
end
-- ── Фолбэк (все ноды unhealthy) ──────────────────────────────────
if route.fallback and route.sip_gateway then
log("info", string.format("FALLBACK → %s (причина: %s)",
route.sip_gateway, route.reason or "all_nodes_unhealthy"))
if route.nodes then
for _, n in ipairs(route.nodes) do
log("info", string.format(" %s: score=%d reason=%s",
n.id or "?", n.score or 0, n.reason or "-"))
end
end
session:execute("bridge", route.sip_gateway)
return
end
-- ── Ошибка API (нет зарегистрированных нод) ──────────────────────
if route.error then
log("err", "ошибка балансировщика: " .. route.error)
safe_hangup()
return
end
-- ── Успешный маршрут ─────────────────────────────────────────────
if route.sip_gateway then
log("info", string.format("ROUTE: %s score=%d → %s",
route.node_id or "?", route.score or 0, route.sip_gateway))
session:execute("bridge", route.sip_gateway)
return
end
-- ── Непредвиденная ситуация ──────────────────────────────────────
log("err", "нет sip_gateway в ответе балансировщика")
safe_hangup()