-- ----------------------------------------------------------------------- -- portabilidad1.lua -- Versión Consolidada + Logs de Diagnóstico Avanzado -- ----------------------------------------------------------------------- local enable_cache = true local cache = nil -- Declaramos la variable fuera del IF if enable_cache then cache = require("portabilidad_cache") end -- -------------------------------- -- 1. CARGA DE LIBRERÍAS (Global) -- -------------------------------- local ok_cjson, cjson = pcall(require, "cjson") local TOKEN = "dxz1BIVjRSmOZHB4gQv_XnkLr_QosOJXi1ZBvBZvdIA" local LOG_DIR = "/usr/share/freeswitch/scripts/logs" local LOG_FILE = LOG_DIR .. "/nuetel_responses.json" local ani if argv[1] ~= nil and argv[1] ~= "" then ani = argv[1] end if ani == nil or ani == "" then if freeswitch then freeswitch.consoleLog("WARNING", "ANI vacío o no provisto\n") end return end local url = string.format("https://api.nuetel.com.ar/info?ani=%s", ani) local function is_userdata_nil(v) return type(v) == "userdata" and tostring(v) == "userdata: (nil)" end local function safe_value(v, default) if v == nil then return default end if v == "userdata: (nil)" then return default end if is_userdata_nil(v) then return default end if type(v) == "table" then return default end return v end -- ----------------------------------------- -- -- ----------------------------------------- local function sql_datetime(val) if val == nil or val == "" or val == "userdata: (nil)" then -- return "NULL" return "2000-01-01 00:00:00" else -- return "'" .. val .. "'" -- Ojo: Asegúrate de escapar val para evitar SQL Injection return val -- Ojo: Asegúrate de escapar val para evitar SQL Injection end end -- ------------------------------------------------------------------------ -- Helper para setear variable de canal + log -- ------------------------------------------------------------------------ local function format_number(n) if type(n) == "number" and n == math.floor(n) then return string.format("%d", n) end return tostring(n) end local function set_var(name, value) local str_value = "" if value ~= nil then if ok_cjson and value == cjson.null then str_value = "" else str_value = format_number(value) end end freeswitch.consoleLog("NOTICE", string.format("%s = %s\n", name, str_value)) if session ~= nil then session:setVariable(name, str_value) end end -- ------------ -- Consultar -- ------------ local function Consultar(Url, token) local cmd = string.format( 'curl -s --max-time 5 -w "|HTTP_CODE:%%{http_code}" -H "Authorization: Bearer %s" "%s"', token, Url ) local handle = io.popen(cmd) local result = handle:read("*a") handle:close() local body, status = result:match("(.*)|HTTP_CODE:(%d+)%s*$") if not body then body, status = result, "desconocido" end return body, status end -- ------------------------------------------------------------------------ -- Graba la respuesta cruda en formato JSON Lines -- ------------------------------------------------------------------------ local function log_raw_response(cjson_lib, ani_val, status_val, body_val) local entry = { ts = os.date("!%Y-%m-%dT%H:%M:%SZ"), ani = ani_val, http_status = status_val, } local parsed_ok, parsed_body = pcall(cjson_lib.decode, body_val or "") if parsed_ok then entry.body = parsed_body else entry.body_raw = body_val end local encode_ok, line = pcall(cjson_lib.encode, entry) if not encode_ok then return end local f = io.open(LOG_FILE, "a") if f then f:write(line .. "\n") f:close() end end local dbh = nil if enable_cache then dbh = freeswitch.Dbh("odbc://billing") if not dbh:connected() then freeswitch.consoleLog("ERR", "NO SE PUDO CONECTAR A LA BASE DE DATOS 'Billing'\n") return end -- ----------------------------------------------- -- 1) Chequear cache antes de pegarle a la API -- ----------------------------------------------- local cached = cache.get(dbh, ani) if cached then freeswitch.consoleLog("WARNING", "[CACHE] EL NUMERO ESTA EN CACHE\n") -- ---------------------------------------------------------------------------- -- Seteo ÚNICO final en variables de FreeSWITCH -- ---------------------------------------------------------------------------- set_var("api_root_ani", cached.api_root_ani) set_var("api_ts", cached.api_ts) set_var("api_http_status", cached.api_http_status) body_id = safe_value(cached.currentProvider_ID, "999") set_var("currentProvider_ID", format_number(cached.currentProvider_ID) ) set_var("ani", cached.ani ) set_var("currentProvider_Name", cached.currentProvider_Name ) set_var("currentProvider_RoutingNumber", cached.currentProvider_RoutingNumber) set_var("prefix", cached.prefix) set_var("block", cached.block) set_var("localNumber", cached.localNumber) set_var("geo", cached.geo) set_var("type", cached.type ) dbh:release() return end end -- ------------------------------------------------------------------------ -- Ejecución Principal -- ------------------------------------------------------------------------ local body, status = Consultar(url, TOKEN) freeswitch.consoleLog("NOTICE", "ANI consultado: " .. ani .. "\n") freeswitch.consoleLog("NOTICE", "HTTP status: " .. tostring(status) .. "\n") if not ok_cjson then freeswitch.consoleLog("ERR", "cjson no disponible\n") set_var("portabilidad_status", "false") return end if status == "sin_respuesta" or status == "sin_status" or status == "desconocido" then set_var("portabilidad_status", "false") set_var("status_error", "network_error") return end if body then log_raw_response(cjson, ani, status, body) end -- ---------------------------------------------------------------------------- -- [EL CAMBIO VA ACÁ] Sanitización Agresiva y Parseo del Body con Diagnóstico -- ---------------------------------------------------------------------------- local cleaned_body = body or "" -- 1. Eliminamos posibles caracteres de control o invisibles en los extremos cleaned_body = cleaned_body:match("^%s*(.*)%s*$") or cleaned_body -- 2. Limpieza estricta de saltos de línea físicos y tabulaciones molestos cleaned_body = cleaned_body:gsub("\r", ""):gsub("\n", " "):gsub("\t", " ") -- Logs de Debug Directo para ver qué le pasa a cjson -- freeswitch.consoleLog("NOTICE", ">>> [DEBUG STRING CLEANED]: " .. cleaned_body .. "\n") local parsed, data = pcall(cjson.decode, cleaned_body) if parsed then freeswitch.consoleLog("NOTICE", ">>> [DEBUG TYPE OF DATA]: " .. type(data) .. "\n") if type(data) == "table" then freeswitch.consoleLog("NOTICE", ">>> [DEBUG DATA.BODY EXISTS?]: " .. tostring(data.body ~= nil) .. "\n") end else freeswitch.consoleLog("WARNING", "La respuesta no es JSON válido. Status=" .. tostring(status) .. "\n") set_var("portabilidad_status", "false") set_var("status_error", "invalid_json") return end set_var("portabilidad_status", "true") -- ---------------------------------------------------------------------------- -- Inicialización de variables de extracción -- ---------------------------------------------------------------------------- local body_id = "" local body_number = "" local body_type = "" local body_operator = "" local body_routingNumber = "" local body_miliseconds = "" local sub_prefix = "" local sub_block = "" local sub_localNumber = "" local sub_geo = "" local orig_id = "" local orig_type = "" local orig_operator = "" local lp_type = "" local lp_requestTs = "" local lp_changeWindowTs = "" local lp_fromId = "" local lp_toId = "" local lp_routingNumber = "" -- ?? DETECTOR HÍBRIDO: Apuntamos dinámicamente al bloque correcto local root = data if data and data.body then root = data.body end -- Extracción segura desde el puntero inteligente (root) if root then if root.id then body_id = root.id end if root.number then body_number = format_number(root.number) end if root.type then body_type = tostring(root.type) end if root.operator then body_operator = tostring(root.operator) end if root.routingNumber then body_routingNumber = tostring(root.routingNumber) end if root.miliseconds then body_miliseconds = tostring(root.miliseconds) end -- subscriberInfo local s = root.subscriberInfo if s then if s.prefix then sub_prefix = format_number(s.prefix) end if s.block then sub_block = format_number(s.block) end if s.localNumber then sub_localNumber = format_number(s.localNumber) end if s.geo then sub_geo = tostring(s.geo) end end -- original local o = root.original if o then if o.id then orig_id = format_number(o.id) end if o.type then orig_type = tostring(o.type) end if o.operator then orig_operator = tostring(o.operator) end end -- lastPorting local lp = root.lastPorting if lp then if lp.type and lp.type ~= cjson.null then lp_type = tostring(lp.type) end if lp.requestTs and lp.requestTs ~= cjson.null then lp_requestTs = tostring(lp.requestTs) end if lp.changeWindowTs and lp.changeWindowTs ~= cjson.null then lp_changeWindowTs = tostring(lp.changeWindowTs) end if lp.fromId and lp.fromId ~= cjson.null then lp_fromId = format_number(lp.fromId) end if lp.toId and lp.toId ~= cjson.null then lp_toId = format_number(lp.toId) end if lp.routingNumber and lp.routingNumber ~= cjson.null then lp_routingNumber = tostring(lp.routingNumber) end end end -- Cálculo de portabilidad automático y dinámico if body_id ~= "" and orig_id ~= "" then local is_ported = (body_id ~= orig_id) set_var("ported", tostring(is_ported)) else set_var("ported", "false") end if enable_cache then -- --------------------------------------------------------- -- 2) Guardar/actualizar cache con el resultado fresco -- --------------------------------------------------------- cache.upsert(dbh, { ani = ani, api_root_ani = data and data.ani or "", api_ts = data and data.ts or "", api_http_status = status, api_miliseconds = body_miliseconds, type = body_type, currentProvider_ID = format_number(body_id) or "999", currentProvider_Name = safe_value( (body_operator ~= "" and body_operator) or orig_operator, 0 ), currentProvider_RoutingNumber = safe_value( body_routingNumber, ""), prefix = safe_value( sub_prefix, "" ), block = safe_value( sub_block, "" ), localNumber = safe_value( sub_localNumber, "" ), geo = safe_value( sub_geo, "" ), originalProvider_ID = safe_value( orig_id, "" ), originalProvider_Type = safe_value( orig_type, ""), originalProvider_Name = safe_value( orig_operator, "" ), lastPorting_Type = safe_value( lp_type, "" ), lastPorting_RequestTs = sql_datetime(lp_requestTs), lastPorting_ChangeWindowTs = sql_datetime(lp_changeWindosTs), lastPorting_FromId = lp_fromId, lastPorting_ToId = lp_toId, lastPorting_RoutingNumber = lp_routingNumber, }, 168) -- TTL en horas, opcional dbh:release() end -- ---------------------------------------------------------------------------- -- Seteo ÚNICO final en variables de FreeSWITCH -- ---------------------------------------------------------------------------- set_var("api_root_ani", data and data.ani or "") set_var("api_ts", data and data.ts or "") set_var("api_http_status", status) body_id = safe_value(body_id, "999") set_var("currentProvider_ID", format_number(body_id)) set_var("ani", body_number) set_var("type", body_type) if body_operator ~= "" then set_var("currentProvider_Name", body_operator) else set_var("currentProvider_Name", orig_operator) end set_var("currentProvider_RoutingNumber", body_routingNumber) set_var("api_miliseconds", body_miliseconds) set_var("prefix", sub_prefix) set_var("block", sub_block) set_var("localNumber", sub_localNumber) set_var("geo", sub_geo) set_var("originalProvider_ID", orig_id) set_var("originalProvider_Type", orig_type) set_var("originalProvider_Name", orig_operator) set_var("lastPorting_Type", lp_type) set_var("lastPorting_RequestTs", lp_requestTs) set_var("lastPorting_ChangeWindowTs", lp_changeWindowTs) set_var("lastPorting_FromId", lp_fromId) set_var("lastPorting_ToId", lp_toId) set_var("lastPorting_RoutingNumber", lp_routingNumber) freeswitch.consoleLog("NOTICE", ">>> [EXTRACCION COMPLETA OK] ID: " .. body_id .. " | OP: " .. body_operator .. " | GEO: " .. sub_geo .. "\n")