annotate mod_muc_http_auth/README.md @ 6299:5cf5ee23b361

mod_voipms: New Module to send and receive SMS/MMS via VoIP.ms APIs. diff --git a/mod_voipms/README.md b/mod_voipms/README.md new file mode 100644 --- /dev/null +++ b/mod_voipms/README.md @@ -0,0 +1,51 @@ +--- +labels: +- 'Stage-Alpha' +- 'Type-Web' +summary: Send and receive SMS/MMS via VoIP.ms APIs. +rockspec: + build: + modules: + mod_voipms: mod_voipms.lua +... + +Introduction +============ + +This is a Prosody module to map JIDs to DIDs on VoIP.ms and support sending/receiving SMS/MMS. + +Configuration +============= + +| option | type | default | +|-----------------------|--------|---------| +| voipms\_api\_username | string | nil | +| voipms\_api\_password | string | nil | +| voipms\_query\_key | string | nil | +| voipms\_jid\_map | table | nil | +``` +VirtualHost "sms.example.com" +modules_enabled = { + "voipms"; +} +voipms_api_username = john@example.com -- E-mail registered with VoIP.ms +voipms_api_password = abcd1234 -- API password configured in VoIP.ms +voipms_query_key = some_query_key -- query param 'key' part of your URL callback +voipms_jid_map = { + ["your_jid@your_domain.com"] = "+1234567890" +} +``` + +HTTP +==== + +The module is served on Prosody's default HTTP ports at the path /voipms. More details on configuring HTTP modules in Prosody can be found in the HTTP documentation. + +VoIP.ms Webhook URL +=================== + +This module receives the VoIP.ms Webhook URL (POST) at the /voipms endpoint. It uses the sendSMS/sendMMS GET methods against the VoIP.ms APIs. This is an example webhook to use in VoIP.ms: + +``` +https://sms.example.com/voipms?key=some_query_key +``` diff --git a/mod_voipms/mod_voipms.lua b/mod_voipms/mod_voipms.lua new file mode 100644 --- /dev/null +++ b/mod_voipms/mod_voipms.lua @@ -0,0 +1,166 @@ +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 + +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 + +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_to = normalize_number(to) + local target_jid = nil + + for jid, did in pairs(jid_map) do + if normalize_number(did) == normalized_to then + target_jid = jid + 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 normalized_from = normalize_number(from) + local message_text = payload.text or "" + + local message = st.message({ + from = normalized_from .. "@" .. module.host, + to = target_jid, + type = "chat" + }):tag("body"):text(message_text):up() + + if payload.media and #payload.media > 0 then + for _, media_item in ipairs(payload.media) do + if media_item.url then + message_text = message_text .. "\n" .. media_item.url + message:tag("x", { xmlns = "jabber:x:oob" }) + :tag("url"):text(media_item.url):up():up() + end + end + end + + module:send(message) + module:log("info", "Delivered SMS from %s to %s", normalized_from, target_jid) + + 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 from_number = jid_map[from_jid] + if not from_number then + module:log("warn", "No DID mapping for JID %s", from_jid) + return + end + + local to_number = to_jid:match("^([^@]+)") + if not to_number then + module:log("warn", "Malformed JID in message.to: %s", to_jid) + return + end + + to_number = normalize_number(to_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 = to_number, + message = body + } + + if method == "sendMMS" then + for i, url in ipairs(media_urls) do + query["media_url[" .. (i - 1) .. "]"] = url + end + end + + 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 + module:log("error", "Failed to send %s: %s", method, err or (resp and resp.status) or "unknown") + else + module:log("info", "Sent %s from %s to %s", method, from_number, to_number) + end + else + module:log("error", "HTTP error sending %s: code %s", method, tostring(code)) + end + end) +end)
author Chaz <menel@snikket.de>
date Mon, 21 Jul 2025 23:57:37 +0200
parents 0a0334a3a784
children
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
1 # Introduction
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
2
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
3 This module externalizes MUC authorization via HTTP.
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
4 Whenever a user wants to join a MUC, an HTTP GET request is made to `authorization_url`
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
5 with the user's bare jid (`userJID`), the MUC jid (`mucJID`) and the user's nickname (`nickname`) as GET parameters.
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
6 Example:
4695
4b3f054666e6 mod_muc_http_auth: External auth services might need to check on the nickname as well
Seve Ferrer <seve@delape.net>
parents: 4322
diff changeset
7 `https://www.prosody.im/users/can-join/?userJID=romeo@example.com&mucJID=teaparty@chat.example.com&nickname=Romeo`
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
8
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
9 This allows an external service to decide whether a user is authorized to join a MUC or not.
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
10
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
11 When a user is authorized to join a MUC, this module expects the following JSON payload:
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
12 ```
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
13 {
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
14 allowed: true,
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
15 error: "",
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
16 }
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
17 ```
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
18 Otherwise, either the user not being authorized or some failure in the external service:
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
19 ```
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
20 {
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
21 allowed: false,
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
22 error: "Some error message to be displayed in this module's logs",
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
23 }
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
24 ```
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
25
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
26 # Configuring
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
27
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
28 ## Enabling
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
29
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
30 ``` {.lua}
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
31 Component "rooms.example.net" "muc"
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
32
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
33 modules_enabled = {
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
34 "muc_http_auth";
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
35 }
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
36
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
37 ```
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
38
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
39
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
40 ## Settings
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
41
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
42 | Name | Description | Default |
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
43 |------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------|
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
44 | muc_http_auth_url | URL of the external HTTP service to which send `userJID`, `mucJID` and `nickname` in a GET request | "" |
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
45 | muc_http_auth_enabled_for | A map of user hostnames to an array of MUC names (node part) to enable this module for. To enable for all hostnames, use `"all"` as key. | nil |
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
46 | muc_http_auth_disabled_for | A map of user hostnames to an array of MUC names (node part) to disable this module for. To disable for all hostnames, use `"all"` as key. | nil |
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
47 | muc_http_auth_insecure | Disable certificate verification for request. Only intended for development of the external service. | false |
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
48 | muc_http_auth_authorization_header | Value of the Authorization header if requested by the external HTTP service. Example: `Basic dXNlcm5hbWU6cGFzc3dvcmQ=` | nil |
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
49
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
50
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
51 This module can be enabled/disabled for specific rooms. Only one of the following settings must be set.
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
52 ```
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
53 -- muc_http_auth_enabled_for = {["all"] = {"teaparty"}}
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
54 -- muc_http_auth_disabled_for = {["all"] = {"teaparty"}}
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
55 ```
4303
d261233f7ced Improve UX by providing defaults users expect
Seve Ferrer <seve@delape.net>
parents: 4296
diff changeset
56 If none is set, all rooms in the MUC component will have this module enabled.
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
57
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
58 Note: Use the node part of the MUC jid for these lists. Example:
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
59
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
60 Wrong:
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
61 `muc_http_auth_enabled_for = {["all"] = {"teaparty@rooms.example.net"}}`
4296
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
62
08138de4cb88 Prosodoy module to externalize MUC authorization via HTTP
Seve Ferrer <seve@delape.net>
parents:
diff changeset
63 Correct:
4723
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
64 `muc_http_auth_enabled_for = {["all"] = {"teaparty"}}`
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
65
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
66 It's also possible to disable/enable checking for a particular host, for example:
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
67
0a0334a3a784 mod_muc_http_auth: Allow for enabling/disabling per user host
JC Brand <jc@opkode.com>
parents: 4695
diff changeset
68 `muc_http_auth_enabled_for = {["jabber.org"] = {"teaparty"}, ["prosody.org] = {"orchard"}}`