2026-06-25 14:27:41 +07:00

157 lines
7.7 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[
route.lua — запрашивает pulse-lets-go для маршрутизации вызова.
Телефонный балансировщик нагрузки на базе FreeSWITCH + NATS.
┌─ Размещение ─────────────────────────────────────────────────────┐
│ /etc/freeswitch/scripts/route.lua │
│ │
│ ┌─ Использование в dialplan ────────────────────────────────────┐│
│ │ <extension name="route_call"> ││
│ │ <condition field="destination_number" expression="^.*$"> ││
│ │ <action application="lua" data="route.lua"/> ││
│ │ </condition> ││
│ │ </extension> ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Переменные канала (опционально) ─────────────────────────────┐│
│ │ balancer_url — URL pulse-lets-go (default: localhost:8080) ││
│ │ ingress_trunk — ID входящего транка ││
│ └───────────────────────────────────────────────────────────────┘│
│ │
│ ┌─ Требования FreeSWITCH ───────────────────────────────────────┐│
│ │ mod_curl — <load module="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()