--[[ 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()