Mercurial > prosody-modules
view mod_voipms/mod_voipms.lua @ 6556:7477e97a9045
mod_firewall: Apply pre-reload state before re-reading config
This change makes load/reload a bit more robust. module.load() runs before
module.restore() and it reads from the config and updates the state (if
needed).
However, after this, module.restore() could run and apply the old state again.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Sun, 24 May 2026 20:03:20 +0100 |
| parents | fa976338928c |
| children |
line wrap: on
line source
local http = require "net.http" local json = require "util.json" local st = require "util.stanza" local api_username = module:get_option_string("voipms_api_username") local api_password = module:get_option_string("voipms_api_password") local query_key = module:get_option("voipms_query_key") local jid_map = module:get_option("voipms_jid_map") or {} local rest_endpoint = "https://voip.ms/api/v1/rest.php" if not api_username or not api_password or not query_key then module:log("error", "Missing required config values (voipms_api_username, voipms_api_password, voipms_query_key)") return end for jid, dids in pairs(jid_map) do if type(dids) ~= "table" then module:log("error", "Invalid voipms_jid_map entry for %s: must be a list of DIDs", jid) return end end module:depends("http") local function normalize_number(num) if not num then return nil end if num:sub(1, 1) ~= "+" then return "+1" .. num end return num end local function extract_query_key(event) return (event.request.url.query or ""):match("key=([^&]+)") end local function send_message(query, stanza) local query_str = http.formencode(query) http.request(rest_endpoint .. "?" .. query_str, { method = "GET"; }, function(response_body, code) if code == 200 then local resp, err = json.decode(response_body) if not resp or resp.status ~= "success" then local err_msg = string.format("Failed to send %s: %s", query.method, err or (resp and resp.status) or "unknown") module:log("error", "%s", err_msg) local err_reply = st.error_reply(stanza, "cancel", "remote-server-error", err_msg) module:send(err_reply) else module:log("debug", "Sent %s from %s to %s", query.method, query.did, query.dst) end else local err_msg = string.format("HTTP error sending %s: code %s", method, tostring(code)) module:log("error", "%s", err_msg) local err_reply = st.error_reply(stanza, "cancel", "remote-server-error", err_msg) module:send(err_reply) end end) end module:provides("http", { route = { ["POST"] = function(event) local req = event.request local body = req.body or "" if extract_query_key(event) ~= query_key then module:log("warn", "Unauthorized webhook: missing or invalid key") return { status_code = 403 } end local json_payload, err = json.decode(body) if not json_payload then module:log("warn", "Invalid JSON: %s", err or "unknown error") return { status_code = 400 } end local payload = json_payload.data and json_payload.data.payload if not payload then module:log("warn", "Missing payload in JSON") return { status_code = 400 } end local from = payload.from and payload.from.phone_number local to_list = payload.to or {} local to = #to_list > 0 and to_list[1].phone_number or nil if not from or not to then module:log("warn", "Missing phone numbers (from: %s, to: %s)", tostring(from), tostring(to)) return { status_code = 400 } end local normalized_from = normalize_number(from) local normalized_to = normalize_number(to) local target_jid = nil for jid, dids in pairs(jid_map) do for _, did in ipairs(dids) do if normalize_number(did) == normalized_to then target_jid = jid break end end if target_jid then break end end if not target_jid then module:log("warn", "No JID mapping for DID %s", normalized_to) return { status_code = 404 } end local message_text = payload.text or "" local from_jid = normalized_from .. "%" .. normalized_to .. "@" .. module.host if message_text ~= "" then local text_message = st.message({ from = from_jid, to = target_jid, type = "chat" }):tag("body"):text(message_text):up() module:send(text_message) module:log("debug", "Delivered text SMS from %s to %s", normalized_from, target_jid) end if payload.media and #payload.media > 0 then for _, media_item in ipairs(payload.media) do if media_item.url then local media_message = st.message({ from = from_jid, to = target_jid, type = "chat" }) :tag("body"):text(media_item.url):up() :tag("x", { xmlns = "jabber:x:oob" }) :tag("url"):text(media_item.url):up() :up() module:send(media_message) module:log("debug", "Delivered media URL from %s to %s: %s", normalized_from, target_jid, media_item.url) end end end return { status_code = 204 } end } }) module:hook("message/bare", function(event) local stanza = event.stanza if stanza.attr.type ~= "chat" then return end local from_jid = stanza.attr.from local to_jid = stanza.attr.to local body = stanza:get_child_text("body") if not body or body == "" then return end local to_node = to_jid:match("^(.-)@") if not to_node then module:log("warn", "Malformed JID in message.to: %s", to_jid) return end local dst_number, from_number = to_node:match("([^%%]+)%%(.+)") if not dst_number or not from_number then module:log("warn", "Malformed to JID node: %s", to_node) return end dst_number = normalize_number(dst_number) from_number = normalize_number(from_number) local media_urls = {} for line in body:gmatch("[^\r\n]+") do if line:match("^https?://") then table.insert(media_urls, line) end end local method = (#media_urls > 0) and "sendMMS" or "sendSMS" local query = { api_username = api_username, api_password = api_password, method = method, did = from_number, dst = dst_number, message = body } if method == "sendMMS" then for i, url in ipairs(media_urls) do query["media_url[" .. (i - 1) .. "]"] = url end send_message(query, stanza) else for i = 1, #body, 160 do local chunked_query = { api_username = query.api_username, api_password = query.api_password, method = method, did = from_number, dst = dst_number, message = body:sub(i, i + 159) } send_message(chunked_query, stanza) end end return true end)
