Software /
code /
prosody-modules
Changeset
5942:abd1bbe5006e draft default tip
Merge
author | Trần H. Trung <xmpp:trần.h.trung@trung.fun> |
---|---|
date | Sun, 16 Feb 2025 16:09:03 +0700 |
parents | 5856:75dee6127829 (current diff) 5941:5c4e102e2563 (diff) |
children | |
files | |
diffstat | 70 files changed, 1489 insertions(+), 264 deletions(-) [+] |
line wrap: on
line diff
--- a/misc/systemd/prosody.service Tue Feb 06 18:32:01 2024 +0700 +++ b/misc/systemd/prosody.service Sun Feb 16 16:09:03 2025 +0700 @@ -1,3 +1,5 @@ +# This is an example service file. For some time there's now also one in used in our Debian releases at https://hg.prosody.im/debian/ + [Unit] ### see man systemd.unit Description=Prosody XMPP Server @@ -30,7 +32,7 @@ User=prosody Group=prosody -Umask=0027 +UMask=0027 # Nice=0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_anti_spam/mod_anti_spam.lua Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,165 @@ +local ip = require "util.ip"; +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; +local set = require "util.set"; +local sha256 = require "util.hashes".sha256; +local st = require"util.stanza"; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local full_sessions = prosody.full_sessions; + +local user_exists = require "core.usermanager".user_exists; + +local new_rtbl_subscription = module:require("rtbl").new_rtbl_subscription; +local trie = module:require("trie"); + +local spam_source_domains = set.new(); +local spam_source_ips = trie.new(); +local spam_source_jids = set.new(); + +local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"}); + +function block_spam(event, reason, action) + event.spam_reason = reason; + event.spam_action = action; + if module:fire_event("spam-blocked", event) == false then + module:log("debug", "Spam allowed by another module"); + return; + end + + count_spam_blocked:with_labels(reason):add(1); + + if action == "bounce" then + module:log("debug", "Bouncing likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason); + event.origin.send(st.error_reply("cancel", "policy-violation", "Rejected as spam")); + else + module:log("debug", "Discarding likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason); + end + + return true; +end + +function is_from_stranger(from_jid, event) + local stanza = event.stanza; + local to_user, to_host, to_resource = jid_split(stanza.attr.to); + + if not to_user then return false; end + + local to_session = full_sessions[stanza.attr.to]; + if to_session then return false; end + + if not is_contact_subscribed(to_user, to_host, from_jid) then + -- Allow all messages from your own jid + if from_jid == to_user.."@"..to_host then + return false; -- Pass through + end + if to_resource and stanza.attr.type == "groupchat" then + return false; -- Pass through + end + return true; -- Stranger danger + end +end + +function is_spammy_server(session) + if spam_source_domains:contains(session.from_host) then + return true; + end + local origin_ip = ip.new(session.ip); + if spam_source_ips:contains_ip(origin_ip) then + return true; + end +end + +function is_spammy_sender(sender_jid) + return spam_source_jids:contains(sha256(sender_jid, true)); +end + +local spammy_strings = module:get_option_array("anti_spam_block_strings"); +local spammy_patterns = module:get_option_array("anti_spam_block_patterns"); + +function is_spammy_content(stanza) + -- Only support message content + if stanza.name ~= "message" then return; end + if not (spammy_strings or spammy_patterns) then return; end + + local body = stanza:get_child_text("body"); + if spammy_strings then + for _, s in ipairs(spammy_strings) do + if body:find(s, 1, true) then + return true; + end + end + end + if spammy_patterns then + for _, s in ipairs(spammy_patterns) do + if body:find(s) then + return true; + end + end + end +end + +-- Set up RTBLs + +local anti_spam_services = module:get_option_array("anti_spam_services"); + +for _, rtbl_service_jid in ipairs(anti_spam_services) do + new_rtbl_subscription(rtbl_service_jid, "spam_source_domains", { + added = function (item) + spam_source_domains:add(item); + end; + removed = function (item) + spam_source_domains:remove(item); + end; + }); + new_rtbl_subscription(rtbl_service_jid, "spam_source_ips", { + added = function (item) + spam_source_ips:add_subnet(ip.parse_cidr(item)); + end; + removed = function (item) + spam_source_ips:remove_subnet(ip.parse_cidr(item)); + end; + }); + new_rtbl_subscription(rtbl_service_jid, "spam_source_jids_sha256", { + added = function (item) + spam_source_jids:add(item); + end; + removed = function (item) + spam_source_jids:remove(item); + end; + }); +end + +module:hook("message/bare", function (event) + local to_bare = jid_bare(event.stanza.attr.to); + + if not user_exists(to_bare) then return; end + + local from_bare = jid_bare(event.stanza.attr.from); + if not is_from_stranger(from_bare, event) then return; end + + if is_spammy_server(event.origin) then + return block_spam(event, "known-spam-source", "drop"); + end + + if is_spammy_sender(from_bare) then + return block_spam(event, "known-spam-jid", "drop"); + end + + if is_spammy_content(event.stanza) then + return block_spam(event, "spam-content", "drop"); + end +end, 500); + +module:hook("presence/bare", function (event) + if event.stanza.type ~= "subscribe" then + return; + end + + if is_spammy_server(event.origin) then + return block_spam(event, "known-spam-source", "drop"); + end + + if is_spammy_sender(event.stanza) then + return block_spam(event, "known-spam-jid", "drop"); + end +end, 500);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_anti_spam/rtbl.lib.lua Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,122 @@ +local array = require "util.array"; +local id = require "util.id"; +local it = require "util.iterators"; +local set = require "util.set"; +local st = require "util.stanza"; + +module:depends("pubsub_subscription"); + +local function new_rtbl_subscription(rtbl_service_jid, rtbl_node, handlers) + local items = {}; + + local function notify(event_type, hash) + local handler = handlers[event_type]; + if not handler then return; end + handler(hash); + end + + module:add_item("pubsub-subscription", { + service = rtbl_service_jid; + node = rtbl_node; + + -- Callbacks: + on_subscribed = function() + module:log("info", "RTBL active: %s:%s", rtbl_service_jid, rtbl_node); + end; + + on_error = function(err) + module:log( + "error", + "Failed to subscribe to RTBL: %s:%s %s::%s: %s", + rtbl_service_jid, + rtbl_node, + err.type, + err.condition, + err.text + ); + end; + + on_item = function(event) + local hash = event.item.attr.id; + if not hash then return; end + module:log("debug", "Received new hash from %s:%s: %s", rtbl_service_jid, rtbl_node, hash); + items[hash] = true; + notify("added", hash); + end; + + on_retract = function (event) + local hash = event.item.attr.id; + if not hash then return; end + module:log("debug", "Retracted hash from %s:%s: %s", rtbl_service_jid, rtbl_node, hash); + items[hash] = nil; + notify("removed", hash); + end; + + purge = function() + module:log("debug", "Purge all hashes from %s:%s", rtbl_service_jid, rtbl_node); + for hash in pairs(items) do + items[hash] = nil; + notify("removed", hash); + end + end; + }); + + local request_id = "rtbl-request-"..id.short(); + + local function request_list() + local items_request = st.iq({ to = rtbl_service_jid, from = module.host, type = "get", id = request_id }) + :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) + :tag("items", { node = rtbl_node }):up() + :up(); + module:send(items_request); + end + + local function update_list(event) + local from_jid = event.stanza.attr.from; + if from_jid ~= rtbl_service_jid then + module:log("debug", "Ignoring RTBL response from unknown sender: %s", from_jid); + return; + end + local items_el = event.stanza:find("{http://jabber.org/protocol/pubsub}pubsub/items"); + if not items_el then + module:log("warn", "Invalid items response from RTBL service %s:%s", rtbl_service_jid, rtbl_node); + return; + end + + local old_entries = set.new(array.collect(it.keys(items))); + + local n_added, n_removed, n_total = 0, 0, 0; + for item in items_el:childtags("item") do + local hash = item.attr.id; + if hash then + n_total = n_total + 1; + if not old_entries:contains(hash) then + -- New entry + n_added = n_added + 1; + items[hash] = true; + notify("added", hash); + else + -- Entry already existed + old_entries:remove(hash); + end + end + end + + -- Remove old entries that weren't in the received list + for hash in old_entries do + n_removed = n_removed + 1; + items[hash] = nil; + notify("removed", hash); + end + + module:log("info", "%d RTBL entries received from %s:%s (%d added, %d removed)", n_total, from_jid, rtbl_node, n_added, n_removed); + return true; + end + + module:hook("iq-result/host/"..request_id, update_list); + module:add_timer(0, request_list); +end + +return { + new_rtbl_subscription = new_rtbl_subscription; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_anti_spam/trie.lib.lua Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,168 @@ +local bit = require "prosody.util.bitcompat"; + +local trie_methods = {}; +local trie_mt = { __index = trie_methods }; + +local function new_node() + return {}; +end + +function trie_methods:set(item, value) + local node = self.root; + for i = 1, #item do + local c = item:byte(i); + if not node[c] then + node[c] = new_node(); + end + node = node[c]; + end + node.terminal = true; + node.value = value; +end + +local function _remove(node, item, i) + if i > #item then + if node.terminal then + node.terminal = nil; + node.value = nil; + end + if next(node) ~= nil then + return node; + end + return nil; + end + local c = item:byte(i); + local child = node[c]; + local ret; + if child then + ret = _remove(child, item, i+1); + node[c] = ret; + end + if ret == nil and next(node) == nil then + return nil; + end + return node; +end + +function trie_methods:remove(item) + return _remove(self.root, item, 1); +end + +function trie_methods:get(item, partial) + local value; + local node = self.root; + local len = #item; + for i = 1, len do + if partial and node.terminal then + value = node.value; + end + local c = item:byte(i); + node = node[c]; + if not node then + return value, i - 1; + end + end + return node.value, len; +end + +function trie_methods:add(item) + return self:set(item, true); +end + +function trie_methods:contains(item, partial) + return self:get(item, partial) ~= nil; +end + +function trie_methods:longest_prefix(item) + return select(2, self:get(item)); +end + +function trie_methods:add_subnet(item, bits) + item = item.packed:sub(1, math.ceil(bits/8)); + local existing = self:get(item); + if not existing then + existing = { bits }; + return self:set(item, existing); + end + + -- Simple insertion sort + for i = 1, #existing do + local v = existing[i]; + if v == bits then + return; -- Already in there + elseif v > bits then + table.insert(existing, v, i); + return; + end + end +end + +function trie_methods:remove_subnet(item, bits) + item = item.packed:sub(1, math.ceil(bits/8)); + local existing = self:get(item); + if not existing then + return; + end + + -- Simple insertion sort + for i = 1, #existing do + local v = existing[i]; + if v == bits then + table.remove(existing, i); + break; + elseif v > bits then + return; -- Stop search + end + end + + if #existing == 0 then + self:remove(item); + end +end + +function trie_methods:has_ip(item) + item = item.packed; + local node = self.root; + local len = #item; + for i = 1, len do + if node.terminal then + return true; + end + + local c = item:byte(i); + local child = node[c]; + if not child then + for child_byte, child_node in pairs(node) do + if type(child_byte) == "number" and child_node.terminal then + local bits = child_node.value; + for j = #bits, 1, -1 do + local b = bits[j]-((i-1)*8); + if b ~= 8 then + local mask = bit.bnot(2^b-1); + if bit.band(bit.bxor(c, child_byte), mask) == 0 then + return true; + end + end + end + end + end + return false; + end + node = child; + end +end + +local function new() + return setmetatable({ + root = new_node(); + }, trie_mt); +end + +local function is_trie(o) + return getmetatable(o) == trie_mt; +end + +return { + new = new; + is_trie = is_trie; +};
--- a/mod_audit_auth/mod_audit_auth.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_audit_auth/mod_audit_auth.lua Sun Feb 16 16:09:03 2025 +0700 @@ -1,25 +1,55 @@ -local jid = require"util.jid"; +local cache = require "util.cache"; +local jid = require "util.jid"; local st = require "util.stanza"; module:depends("audit"); -- luacheck: read globals module.audit local only_passwords = module:get_option_boolean("audit_auth_passwords_only", true); +local cache_size = module:get_option_number("audit_auth_cache_size", 128); +local repeat_failure_timeout = module:get_option_number("audit_auth_repeat_failure_timeout"); +local repeat_success_timeout = module:get_option_number("audit_auth_repeat_success_timeout"); +local failure_cache = cache.new(cache_size); module:hook("authentication-failure", function(event) local session = event.session; - module:audit(jid.join(session.sasl_handler.username, module.host), "authentication-failure", { - session = session, + + local username = session.sasl_handler.username; + if repeat_failure_timeout then + local cache_key = ("%s\0%s"):format(username, session.ip); + local last_failure = failure_cache:get(cache_key); + local now = os.time(); + if last_failure and (now - last_failure) > repeat_failure_timeout then + return; + end + failure_cache:set(cache_key, now); + end + + module:audit(jid.join(username, module.host), "authentication-failure", { + session = session; }); end) +local success_cache = cache.new(cache_size); module:hook("authentication-success", function(event) local session = event.session; if only_passwords and session.sasl_handler.fast then return; end - module:audit(jid.join(session.sasl_handler.username, module.host), "authentication-success", { - session = session, + + local username = session.sasl_handler.username; + if repeat_success_timeout then + local cache_key = ("%s\0%s"):format(username, session.ip); + local last_success = success_cache:get(cache_key); + local now = os.time(); + if last_success and (now - last_success) > repeat_success_timeout then + return; + end + success_cache:set(cache_key, now); + end + + module:audit(jid.join(username, module.host), "authentication-success", { + session = session; }); end)
--- a/mod_audit_status/mod_audit_status.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_audit_status/mod_audit_status.lua Sun Feb 16 16:09:03 2025 +0700 @@ -9,10 +9,14 @@ local store = module:open_store(nil, "keyval+"); +-- This is global, to make it available to other modules +crashed = false; --luacheck: ignore 131/crashed + module:hook_global("server-started", function () local recorded_status = store:get(); if recorded_status and recorded_status.status == "started" then module:audit(nil, "server-crashed", { timestamp = recorded_status.heartbeat }); + crashed = true; end module:audit(nil, "server-started"); store:set_key(nil, "status", "started");
--- a/mod_auth_oauth_external/README.md Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_auth_oauth_external/README.md Sun Feb 16 16:09:03 2025 +0700 @@ -4,7 +4,7 @@ - Stage-Alpha --- -This module provides external authentication via an external [AOuth +This module provides external authentication via an external [OAuth 2](https://datatracker.ietf.org/doc/html/rfc7628) authorization server and supports the [SASL OAUTHBEARER authentication][rfc7628] mechanism as well as PLAIN for legacy clients (this is all of them).
--- a/mod_blocking/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_blocking/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,8 +1,17 @@ --- labels: -- 'Stage-Alpha' -summary: 'XEP-0191: Simple Communications Blocking support' -... +- Stage-Deprecated +rockspec: + dependencies: + - mod_privacy_lists +summary: "XEP-0191: Simple Communications Blocking support" +--- + +::: {.alert .alert-warning} +This module is deprecated as it depends on the deprecated +[mod_privacy_lists], use the core module +[mod_blocklist][doc:modules:mod_blocklist] instead. +::: Introduction ============ @@ -33,12 +42,12 @@ Configuration ============= -Simply ensure that mod\_privacy (or [mod\_privacy\_lists] in 0.10+) and -mod\_blocking are loaded in your modules\_enabled list: +Simply ensure that [mod_privacy_lists] and mod_blocking are loaded in +your modules_enabled list: modules_enabled = { -- ... - "privacy", -- or privacy_lists in Prosody 0.10+ + "privacy_lists", "blocking", -- ...
--- a/mod_client_management/mod_client_management.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_client_management/mod_client_management.lua Sun Feb 16 16:09:03 2025 +0700 @@ -116,17 +116,22 @@ end -- Update state - local legacy_info = session.client_management_info; client_state.full_jid = session.full_jid; client_state.last_seen = now; - client_state.mechanisms[legacy_info.mechanism] = now; - if legacy_info.fast_auth then - client_state.fast_auth = now; - end - local token_id = legacy_info.token_info and legacy_info.token_info.id; - if token_id then - client_state.auth_token_id = token_id; + local legacy_info = session.client_management_info; + if legacy_info then + client_state.mechanisms[legacy_info.mechanism] = now; + if legacy_info.fast_auth then + client_state.fast_auth = now; + end + + local token_id = legacy_info.token_info and legacy_info.token_info.id; + if token_id then + client_state.auth_token_id = token_id; + end + else + session.log("warn", "Missing client management info") end -- Store updated state
--- a/mod_compat_roles/mod_compat_roles.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_compat_roles/mod_compat_roles.lua Sun Feb 16 16:09:03 2025 +0700 @@ -50,8 +50,14 @@ if not role_permissions then return false; end + if role_permissions[permission] then + return true; + end local next_role = role_inheritance[role_name]; - return not not permissions[role_name][permission] or (next_role and role_may(host, next_role, permission)); + if not next_role then + return false; + end + return role_may(host, next_role, permission); end function moduleapi.may(self, action, context)
--- a/mod_conversejs/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_conversejs/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -158,6 +158,34 @@ The example above uses the `[[` and `]]` syntax simply because it will not conflict with any embedded quotes. +Custimizing the generated PWA options +------------------------------------- + +``` {.lua} +conversejs_name = "Service name" -- Also used as the web page title +conversejs_short_name = "Shorter name" +conversejs_description = "Description of the service" +conversejs_manifest_icons = { + { + src = "https://example.com/logo/512.png", + sizes = "512x512", + }, + { + src = "https://example.com/logo/192.png", + sizes = "192x192", + }, + { + src = "https://example.com/logo/192.svg", + sizes = "192x192", + }, + { + src = "https://example.com/logo/512.svg", + sizes = "512x512", + }, +} +conversejs_pwa_color = "#397491" +``` + Compatibility =============
--- a/mod_conversejs/mod_conversejs.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_conversejs/mod_conversejs.lua Sun Feb 16 16:09:03 2025 +0700 @@ -118,6 +118,11 @@ local add_tags = module:get_option_array("conversejs_tags", {}); +local service_name = module:get_option_string("name", "Prosody IM and Converse.js"); +local service_short_name = module:get_option_string("short_name", "Converse"); +local service_description = module:get_option_string("description", "Messaging Freedom") +local pwa_color = module:get_option_string("pwa_color", "#397491") + module:provides("http", { title = "Converse.js"; route = { @@ -126,7 +131,9 @@ event.response.headers.content_type = "text/html"; return render(html_template, { - service_name = module:get_option_string("name"); + service_name = service_name; + -- note that using a relative path won’t work as this URL doesn’t end in a / + manifest_url = module:http_url().."/manifest.json", header_scripts = { js_url }; header_style = { css_url }; header_tags = add_tags; @@ -143,6 +150,42 @@ event.response.headers.content_type = "application/javascript"; return js_template:format(json_encode(converse_options)); end; + ["GET /manifest.json"] = function (event) + -- See manifest.json in the root of Converse.js’s git repository + local data = { + short_name = service_short_name, + name = service_name, + description = service_description, + categories = {"social"}, + icons = module:get_option_array("manifest_icons", { + { + src = cdn_url..version.."/dist/images/logo/conversejs-filled-512.png", + sizes = "512x512", + }, + { + src = cdn_url..version.."/dist/images/logo/conversejs-filled-192.png", + sizes = "192x192", + }, + { + src = cdn_url..version.."/dist/images/logo/conversejs-filled-192.svg", + sizes = "192x192", + }, + { + src = cdn_url..version.."/dist/images/logo/conversejs-filled-512.svg", + sizes = "512x512", + }, + }), + start_url = module:http_url(), + background_color = pwa_color, + display = "standalone", + scope = module:http_url().."/", + theme_color = pwa_color, + } + return { + headers = { content_type = "application/schema+json" }, + body = json_encode(data), + } + end; ["GET /dist/*"] = serve_dist; } });
--- a/mod_conversejs/templates/template.html Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_conversejs/templates/template.html Sun Feb 16 16:09:03 2025 +0700 @@ -5,9 +5,10 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> {header_style# <link rel="stylesheet" type="text/css" media="screen" href="{item}"/>} +<link rel="manifest" href="{manifest_url}"> {header_scripts# <script charset="utf-8" src="{item}"></script>} -<title>{service_name?Prosody IM and Converse.js}</title> +<title>{service_name}</title> {header_tags# {item!}} </head>
--- a/mod_csi_battery_saver/mod_csi_battery_saver.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_csi_battery_saver/mod_csi_battery_saver.lua Sun Feb 16 16:09:03 2025 +0700 @@ -85,6 +85,17 @@ end local function is_important(stanza, session) + -- some special handlings + if stanza == " " then -- whitespace keepalive + return true; + elseif type(stanza) == "string" then -- raw data + return true; + elseif not st.is_stanza(stanza) then -- this should probably never happen + return true; + end + if stanza.attr.xmlns ~= nil then -- nonzas (stream errors, stream management etc.) + return true; + end local st_name = stanza and stanza.name or nil; if not st_name then return true; end -- nonzas are always important if st_name == "presence" then @@ -104,8 +115,19 @@ local st_type = stanza.attr.type; - -- headline message are always not important - if st_type == "headline" then return false; end + -- errors are always important + if st_type == "error" then return true; end; + + -- headline message are always not important, with some exceptions + if st_type == "headline" then + -- allow headline pushes of mds updates (XEP-0490) + if stanza:find("{http://jabber.org/protocol/pubsub#event}event/items@node") == "urn:xmpp:mds:displayed:0" then return true; end; + return false + end + + -- mediated muc invites + if stanza:find("{http://jabber.org/protocol/muc#user}x/invite") then return true; end; + if stanza:get_child("x", "jabber:x:conference") then return true; end; -- chat markers (XEP-0333) are important, too, because some clients use them to update their notifications if stanza:child_with_ns("urn:xmpp:chat-markers:0") then return true; end;
--- a/mod_debug_traceback/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_debug_traceback/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -22,4 +22,4 @@ # Compatibility -Prosody 0.11 or later. +Prosody 0.12 or later.
--- a/mod_debug_traceback/mod_debug_traceback.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_debug_traceback/mod_debug_traceback.lua Sun Feb 16 16:09:03 2025 +0700 @@ -46,9 +46,4 @@ count = count + 1; end -local mod_posix = module:depends("posix"); -if rawget(mod_posix, "features") and mod_posix.features.signal_events then - module:hook("signal/"..signal_name, dump_traceback); -else - require"util.signal".signal(signal_name, dump_traceback); -end +module:hook("signal/"..signal_name, dump_traceback);
--- a/mod_file_management/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_file_management/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,6 +1,7 @@ --- description: File management for uploaded files -labels: 'Stage-Alpha' +labels: +- Stage-Alpha --- Introduction
--- a/mod_firewall/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_firewall/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -662,9 +662,9 @@ ### Reporting - Action Description - ------------------------ --------------------------------------------------------------------------------------------------------------------------------------------------------- - `REPORT=jid reason text` Forwards the full stanza to `jid` with a XEP-0377 abuse report attached. + Action Description + ------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------- + `REPORT TO=jid [reason] [text]` Forwards the full stanza to `jid` with a XEP-0377 abuse report attached. Only the `jid` is mandatory. The `reason` parameter should be either `abuse`, `spam` or a custom URI. If not specified, it defaults to `abuse`. After the reason, some human-readable text may be included to explain the report.
--- a/mod_firewall/actions.lib.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_firewall/actions.lib.lua Sun Feb 16 16:09:03 2025 +0700 @@ -263,18 +263,18 @@ local where, reason, text = spec:match("^%s*(%S+) *(%S*) *(.*)$"); if reason == "spam" then reason = "urn:xmpp:reporting:spam"; - elseif reason == "abuse" or not reason then + elseif reason == "abuse" or not reason or reason == "" then reason = "urn:xmpp:reporting:abuse"; end local code = [[ - local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" }); + local newstanza = st.stanza("message", { to = %q, from = current_host, id = new_short_id() }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" }); local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza):up(); newstanza:tag("report", { xmlns = "urn:xmpp:reporting:1", reason = %q }) do local text = %q; if text ~= "" then newstanza:text_tag("text", text); end end newstanza:up(); core_post_stanza(session, newstanza); ]]; - return code:format(where, reason, text), { "core_post_stanza", "current_host", "st" }; + return code:format(where, reason, text), { "core_post_stanza", "current_host", "st", "new_short_id" }; end return action_handlers;
--- a/mod_firewall/mod_firewall.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_firewall/mod_firewall.lua Sun Feb 16 16:09:03 2025 +0700 @@ -306,6 +306,15 @@ "iplib" } }; + new_short_id = { + global_code = [[local new_short_id = require "util.id".short;]]; + }; + new_medium_id = { + global_code = [[local new_medium_id = require "util.id".medium;]]; + }; + new_long_id = { + global_code = [[local new_long_id = require "util.id".long;]]; + }; }; local function include_dep(dependency, code) @@ -612,7 +621,7 @@ local function resolve_script_path(script_path) local relative_to = prosody.paths.config; if script_path:match("^module:") then - relative_to = module.path:sub(1, -#("/mod_"..module.name..".lua")); + relative_to = module:get_directory(); script_path = script_path:match("^module:(.+)$"); end return resolve_relative_path(relative_to, script_path);
--- a/mod_host_status_check/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_host_status_check/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: Stage-Beta +labels: +- Stage-Beta summary: Host status check ...
--- a/mod_host_status_heartbeat/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_host_status_heartbeat/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: Stage-Beta +labels: +- Stage-Beta summary: Host status heartbeat ...
--- a/mod_http_admin_api/mod_http_admin_api.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_admin_api/mod_http_admin_api.lua Sun Feb 16 16:09:03 2025 +0700 @@ -80,7 +80,9 @@ local function token_info_to_invite_info(token_info) local additional_data = token_info.additional_data; local groups = additional_data and additional_data.groups or nil; + local roles = additional_data and additional_data.roles or nil; local source = additional_data and additional_data.source or nil; + local note = additional_data and additional_data.note or nil; local reset = not not (additional_data and additional_data.allow_reset or nil); return { id = token_info.token; @@ -93,8 +95,10 @@ created_at = token_info.created_at; expires = token_info.expires; groups = groups; + roles = roles; source = source; reset = reset; + note = note; }; end @@ -153,11 +157,15 @@ end invite = invites.create_group(options.groups, { source = source; + roles = options.roles; + note = options.note; }, options.ttl); elseif invite_type == "account" then invite = invites.create_account(options.username, { source = source; groups = options.groups; + roles = options.roles; + note = options.note; }, options.ttl); else return 400; @@ -762,6 +770,11 @@ result.cpu = maybe_export_plain_counter(families.process_cpu_seconds); result.c2s = maybe_export_summed_gauge(families["prosody_mod_c2s/connections"]) result.uploads = maybe_export_summed_gauge(families["prosody_mod_http_file_share/total_storage_bytes"]); + result.users = { + active_1d = maybe_export_summed_gauge(families["prosody_mod_measure_active_users/active_users_1d"]); + active_7d = maybe_export_summed_gauge(families["prosody_mod_measure_active_users/active_users_7d"]); + active_30d = maybe_export_summed_gauge(families["prosody_mod_measure_active_users/active_users_30d"]); + }; return json.encode(result); end @@ -790,9 +803,13 @@ if body.recipients == "online" then announce.send_to_online(message, host); elseif body.recipients == "all" then - for username in usermanager.users(host) do - message.attr.to = username .. "@" .. host - module:send(st.clone(message)) + if announce.send_to_all then + announce.send_to_all(message, host); + else -- COMPAT w/ 0.12 and trunk before e22609460975 + for username in usermanager.users(host) do + message.attr.to = username .. "@" .. host + module:send(st.clone(message)) + end end else for _, addr in ipairs(body.recipients) do
--- a/mod_http_admin_api/openapi.yaml Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_admin_api/openapi.yaml Sun Feb 16 16:09:03 2025 +0700 @@ -545,6 +545,10 @@ type: string description: HTTPS URL of invite page (use in preference to XMPP URI when available) nullable: true + note: + type: string + nullable: true + description: Free-form text note/annotation to help identify the invitation created_at: type: integer description: Unix timestamp of invite creation @@ -557,6 +561,12 @@ items: type: string description: Group ID + roles: + type: array + description: Array of role names that accepting users will have (primary first) + items: + type: string + description: Role name source: type: string description: | @@ -586,6 +596,17 @@ items: type: string description: "Group ID" + roles: + type: array + nullable: true + description: "List of roles the new account should have (primary role first)" + items: + type: string + description: "Role name" + note: + type: string + nullable: true + description: Free-form text note/annotation to help identify the invitation NewGroupInvite: type: object properties: @@ -601,6 +622,17 @@ description: "IDs of existing group to add the new accounts to" group_options: $ref: '#/components/schemas/NewGroup' + roles: + type: array + nullable: true + description: "List of roles the new accounts should have (primary role first)" + items: + type: string + description: "Role name" + note: + type: string + nullable: true + description: Free-form text note/annotation to help identify the invitation NewResetInvite: type: object properties:
--- a/mod_http_host_status_check/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_host_status_check/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: Stage-Beta +labels: +- Stage-Beta summary: HTTP Host Status Check ...
--- a/mod_http_muc_log/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_muc_log/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -13,7 +13,7 @@ ============ This module provides a built-in web interface to view chatroom logs -stored by [mod\_mam\_muc]. +stored by [mod\_muc\_mam]. Installation ============ @@ -29,7 +29,7 @@ ``` lua Component "conference.example.com" "muc" modules_enabled = { - "mam_muc"; + "muc_mam"; "http_muc_log"; } storage = {
--- a/mod_http_oauth2/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_oauth2/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -102,7 +102,7 @@ client registration. Dynamic client registration can be enabled by configuring a JWT key. Algorithm -defaults to *HS256* lifetime defaults to forever. +defaults to *HS256*, lifetime defaults to forever. ```lua oauth2_registration_key = "securely generated JWT key here" @@ -202,7 +202,7 @@ - Authorization Code grant, optionally with Proof Key for Code Exchange - Device Authorization Grant -- Resource owner password grant *(likely to be phased out in the future)* +- Resource owner password grant *(disabled by default)* - Implicit flow *(disabled by default)* - Refresh Token grants @@ -214,7 +214,7 @@ allowed_oauth2_grant_types = { "authorization_code"; -- authorization code grant "device_code"; - "password"; -- resource owner password grant + -- "password"; -- resource owner password grant disabled by default } allowed_oauth2_response_types = {
--- a/mod_http_oauth2/mod_http_oauth2.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_oauth2/mod_http_oauth2.lua Sun Feb 16 16:09:03 2025 +0700 @@ -1128,7 +1128,7 @@ headers = { content_type = "application/json" }; body = json.encode { active = true; - client_id = credentials.username; -- We don't really know for sure + client_id = credentials.username; -- Verified via client hash username = jid.node(token_info.jid); scope = token_info.grant.data.oauth2_scopes; token_type = purpose_map[token_info.purpose];
--- a/mod_http_upload/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_upload/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,6 +1,7 @@ --- description: HTTP File Upload -labels: 'Stage-Alpha' +labels: +- Stage-Alpha --- Introduction
--- a/mod_http_upload_external/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_http_upload_external/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,6 +1,7 @@ --- description: HTTP File Upload (external service) -labels: 'Stage-Alpha' +labels: +- Stage-Alpha --- Introduction @@ -19,6 +20,7 @@ * [Python3+Flask implementation](https://github.com/horazont/xmpp-http-upload) * [Go implementation, Prosody Filer](https://github.com/ThomasLeister/prosody-filer) * [Perl implementation for nginx](https://github.com/weiss/ngx_http_upload) +* [Rust implementation](https://gitlab.com/nyovaya/xmpp-http-upload) To implement your own service compatible with this module, check out the implementation notes below (and if you publish your implementation - let us know!). @@ -75,10 +77,10 @@ ------ You may want to give upload access to additional entities such as components -by using the `http_upload_access` config option. +by using the `http_upload_external_access` config option. ``` {.lua} -http_upload_access = {"gateway.example.com"}; +http_upload_external_access = {"gateway.example.com"}; ``` Compatibility
--- a/mod_invites_tracking/mod_invites_tracking.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_invites_tracking/mod_invites_tracking.lua Sun Feb 16 16:09:03 2025 +0700 @@ -4,6 +4,11 @@ local validated_invite = event.validated_invite or (event.session and event.session.validated_invite); local new_username = event.username; + if not validated_invite then + module:log("debug", "No invitation found for registration of %s", new_username); + return; + end + local invite_id = nil; local invite_source = nil; if validated_invite then @@ -11,7 +16,7 @@ invite_id = validated_invite.token; end - tracking_store:set(new_username, {invite_id = validated_invite.token, invite_source = invite_source}); + tracking_store:set(new_username, {invite_id = invite_id, invite_source = invite_source}); module:log("debug", "recorded that invite from %s was used to create %s", invite_source, new_username) end);
--- a/mod_lastlog2/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_lastlog2/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -34,7 +34,7 @@ You can check a user's last activity by running: - prosodyctl mod_lastlog username@example.com + prosodyctl mod_lastlog2 username@example.com # Compatibility
--- a/mod_lastlog2/mod_lastlog2.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_lastlog2/mod_lastlog2.lua Sun Feb 16 16:09:03 2025 +0700 @@ -69,7 +69,7 @@ function module.command(arg) if not arg[1] or arg[1] == "--help" then - require"util.prosodyctl".show_usage([[mod_lastlog <user@host>]], [[Show when user last logged in or out]]); + require"util.prosodyctl".show_usage([[mod_lastlog2 <user@host>]], [[Show when user last logged in or out]]); return 1; end local user, host = jid.prepped_split(table.remove(arg, 1));
--- a/mod_log_ringbuffer/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_log_ringbuffer/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -120,4 +120,4 @@ # Compatibility -0.11 and later. +0.12 and later.
--- a/mod_log_ringbuffer/mod_log_ringbuffer.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_log_ringbuffer/mod_log_ringbuffer.lua Sun Feb 16 16:09:03 2025 +0700 @@ -90,6 +90,8 @@ return write, dump; end +local event_hooks = {}; + local function ringbuffer_log_sink_maker(sink_config) local write, dump = new_buffer(sink_config); @@ -106,9 +108,11 @@ end if sink_config.signal then - require "util.signal".signal(sink_config.signal, handler); + module:hook_global("signal/"..sink_config.signal, handler); + event_hooks[handler] = "signal/"..sink_config.signal; elseif sink_config.event then module:hook_global(sink_config.event, handler); + event_hooks[handler] = sink_config.event; end return function (name, level, message, ...) @@ -117,4 +121,11 @@ end; end +module:hook_global("reopen-log-files", function() + for handler, event_name in pairs(event_hooks) do + module:unhook_object_event(prosody.events, event_name, handler); + event_hooks[handler] = nil; + end +end, 1); + loggingmanager.register_sink_type("ringbuffer", ringbuffer_log_sink_maker);
--- a/mod_mam_archive/mod_mam_archive.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_mam_archive/mod_mam_archive.lua Sun Feb 16 16:09:03 2025 +0700 @@ -20,12 +20,6 @@ local resolve_relative_path = require "core.configmanager".resolve_relative_path; -- Feature discovery -local xmlns_archive = "urn:xmpp:archive" -local feature_archive = st.stanza("feature", {xmlns=xmlns_archive}):tag("optional"); -if(global_default_policy) then - feature_archive:tag("default"); -end -module:add_extension(feature_archive); module:add_feature("urn:xmpp:archive:auto"); module:add_feature("urn:xmpp:archive:manage"); module:add_feature("urn:xmpp:archive:pref");
--- a/mod_measure_active_users/mod_measure_active_users.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_measure_active_users/mod_measure_active_users.lua Sun Feb 16 16:09:03 2025 +0700 @@ -41,8 +41,10 @@ measure_d1(active_d1); measure_d7(active_d7); measure_d30(active_d30); - - return 3600 + (300*math.random()); end +-- Schedule at startup module:add_timer(15, update_calculations); + +-- Recalculate hourly +module:hourly(update_calculations);
--- a/mod_muc_adhoc_bots/mod_muc_adhoc_bots.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_muc_adhoc_bots/mod_muc_adhoc_bots.lua Sun Feb 16 16:09:03 2025 +0700 @@ -37,7 +37,7 @@ for j,item in ipairs(query.tags) do item.attr.node = json.encode({ jid = item.attr.jid, node = item.attr.node }) item.attr.jid = event.stanza.attr.to - reply:add_child(item):up() + reply:add_child(item) end end end
--- a/mod_muc_eventsource/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_muc_eventsource/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: 'Stage-Beta' +labels: +- Stage-Beta summary: Subscribe to MUC rooms using the HTML5 EventSource API ...
--- a/mod_muc_moderation/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_muc_moderation/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -16,7 +16,7 @@ Example [MUC component][doc:chatrooms] configuration: ``` {.lua} -VirtualHost "channels.example.com" "muc" +Component "channels.example.com" "muc" modules_enabled = { "muc_mam", "muc_moderation", @@ -40,5 +40,4 @@ - [Conversations](https://codeberg.org/iNPUTmice/Conversations/issues/20) - [Dino](https://github.com/dino/dino/issues/1133) -- [Poezio](https://lab.louiz.org/poezio/poezio/-/issues/3543) - [Profanity](https://github.com/profanity-im/profanity/issues/1336)
--- a/mod_muc_restrict_avatars/mod_muc_restrict_avatars.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_muc_restrict_avatars/mod_muc_restrict_avatars.lua Sun Feb 16 16:09:03 2025 +0700 @@ -9,11 +9,46 @@ return tag; end +-- Function to determine if avatar restriction is enabled +local function is_avatar_restriction_enabled(room) + return room._data.restrict_avatars; +end + +-- Add MUC configuration form option for avatar restriction +module:hook("muc-config-form", function(event) + local room, form = event.room, event.form; + table.insert(form, { + name = "restrict_avatars", + type = "boolean", + label = "Restrict avatars to members only", + value = is_avatar_restriction_enabled(room) + }); +end); + +-- Handle MUC configuration form submission +module:hook("muc-config-submitted", function(event) + local room, fields, changed = event.room, event.fields, event.changed; + local restrict_avatars = fields["restrict_avatars"]; + + if room and restrict_avatars ~= is_avatar_restriction_enabled(room) then + -- Update room settings based on the submitted value + room._data.restrict_avatars = restrict_avatars; + -- Mark the configuration as changed + if type(changed) == "table" then + changed["restrict_avatars"] = true; + else + event.changed = true; + end + end +end); + +-- Handle presence/full events to filter avatar advertisements module:hook("presence/full", function(event) local stanza = event.stanza; local room = mod_muc.get_room_from_jid(bare_jid(stanza.attr.to)); - - if not room:get_affiliation(stanza.attr.from) then - stanza:maptags(filter_avatar_advertisement); + if room and not room:get_affiliation(stanza.attr.from) then + if is_avatar_restriction_enabled(room) then + stanza:maptags(filter_avatar_advertisement); + end end end, 1);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_restrict_pm/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,30 @@ +--- +labels: +- 'Stage-Alpha' +summary: Limit who may send and recieve MUC PMs +... + +# Introduction + +This module adds configurable MUC options that restrict and limit who may send MUC PMs to other users. + +If a user does not have permissions to send a MUC PM, the MUC will send a policy violation stanza. + +# Setup + +```lua +Component "conference.example.org" "muc" + +modules_enabled = { + "muc_restrict_pm"; +} +``` + +Compatibility +============= + + ----- ----- + 0.12 Works + 0.11 Probably does not work + ----- ----- +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_muc_restrict_pm/mod_muc_restrict_pm.lua Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,99 @@ +local st = require "util.stanza"; +local muc_util = module:require "muc/util"; +local valid_roles = muc_util.valid_roles; + +-- Backported backwards compatibility map (Thanks MattJ) +local compat_map = { + everyone = "visitor"; + participants = "participant"; + moderators = "moderator"; + members = "affiliated"; +}; + +local function get_allow_pm(room) + local val = room._data.allow_pm; + return compat_map[val] or val or 'visitor'; +end + +local function set_allow_pm(room, val) + if get_allow_pm(room) == val then return false; end + room._data.allow_pm = val; + return true; +end + +local function get_allow_modpm(room) + return room._data.allow_modpm or false; +end + +local function set_allow_modpm(room, val) + if get_allow_modpm(room) == val then return false; end + room._data.allow_modpm = val; + return true; +end + +module:hook("muc-config-form", function(event) + local pmval = get_allow_pm(event.room); + table.insert(event.form, { + name = 'muc#allow_pm'; + type = 'list-single'; + label = 'Allow PMs from'; + options = { + { value = 'visitor', label = 'Everyone', default = pmval == 'visitor' }, + { value = 'participant', label = 'Participants', default = pmval == 'participant' }, + { value = 'affiliated', label = 'Members', default = pmval == 'affiliated' }, + { value = 'moderator', label = 'Moderators', default = pmval == 'moderator' }, + { value = 'none', label = 'No one', default = pmval == 'none' } + } + }); + table.insert(event.form, { + name = 'muc#allow_modpm'; + type = 'boolean'; + label = 'Allow PMs to moderators'; + value = get_allow_modpm(event.room) + }); +end); + +module:hook("muc-config-submitted/muc#allow_pm", function(event) + if set_allow_pm(event.room, event.value) then + event.status_codes["104"] = true; + end +end); + +module:hook("muc-config-submitted/muc#allow_modpm", function(event) + if set_allow_modpm(event.room, event.value) then + event.status_codes["104"] = true; + end +end); + +module:hook("muc-private-message", function(event) + local stanza, room = event.stanza, event.room; + local from_occupant = room:get_occupant_by_nick(stanza.attr.from); + local to_occupant = room:get_occupant_by_nick(stanza.attr.to); + + -- To self is always okay + if to_occupant.bare_jid == from_occupant.bare_jid then return; end + + if get_allow_modpm(room) then + if to_occupant and to_occupant.role == 'moderator' + or from_occupant and from_occupant.role == "moderator" then + return; -- Allow to/from moderators + end + end + + local pmval = get_allow_pm(room); + + -- Backported improved handling (Thanks MattJ) + if pmval ~= "none" then + if pmval == "affiliated" and room:get_affiliation(from_occupant.bare_jid) then + return; -- Allow from affiliated users + elseif valid_roles[from_occupant.role] >= valid_roles[pmval] then + return; -- Allow from a permitted role + end + end + + room:route_to_occupant( + from_occupant, + st.error_reply(stanza, "cancel", "policy-violation", "Private messages are restricted", room.jid) + ); + return false; +end, 1);
--- a/mod_muc_rtbl/mod_muc_rtbl.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_muc_rtbl/mod_muc_rtbl.lua Sun Feb 16 16:09:03 2025 +0700 @@ -164,7 +164,7 @@ module:log("debug", "Blocked private message from user <%s> from room <%s> due to RTBL match", occupant.bare_jid, event.stanza.attr.to); local error_reply = st.error_reply(event.stanza, "cancel", "forbidden", "You are banned from this service", event.room.jid); event.origin.send(error_reply); - return true; + return false; -- Don't route it end end);
--- a/mod_nodeinfo2/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_nodeinfo2/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,6 +1,6 @@ --- -description: -labels: 'Stage-Alpha' +labels: +- Stage-Alpha --- Introduction
--- a/mod_pastebin/mod_pastebin.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pastebin/mod_pastebin.lua Sun Feb 16 16:09:03 2025 +0700 @@ -100,8 +100,6 @@ end end -local line_count_pattern = string.rep("[^\n]*\n", line_threshold + 1):sub(1,-2); - function check_message(data) local stanza = data.stanza; @@ -122,7 +120,8 @@ if ( #body > length_threshold and utf8_length(body) > length_threshold ) or (trigger_string and body:find(trigger_string, 1, true) == 1) or - body:find(line_count_pattern) then + (select(2, body:gsub("\n", "%0")) >= line_threshold) + then if trigger_string and body:sub(1, #trigger_string) == trigger_string then body = body:sub(#trigger_string+1); end
--- a/mod_privacy_lists/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_privacy_lists/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,8 +1,13 @@ --- labels: -- 'Stage-Beta' -summary: 'Privacy lists (XEP-0016) support' -... +- Stage-Deprecated +summary: Privacy lists (XEP-0016) support +--- + +::: {.alert .alert-warning} +[XEP-0016 Privacy Lists] and this module has been deprecated, instead +use [mod_blocklist][doc:modules:mod_blocklist], included with Prosody. +::: Introduction ------------ @@ -10,7 +15,7 @@ Privacy lists are a flexible method for blocking communications. Originally known as mod\_privacy and bundled with Prosody, this module -is being phased out in favour of the newer simpler blocking (XEP-0191) +was phased out in favour of the newer simpler blocking (XEP-0191) protocol, implemented in [mod\_blocklist][doc:modules:mod_blocklist]. Configuration
--- a/mod_privilege/mod_privilege.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_privilege/mod_privilege.lua Sun Feb 16 16:09:03 2025 +0700 @@ -69,7 +69,7 @@ end local iq_perm = perms["iq"] if iq_perm ~= nil then - message:tag("perm", {access="iq"}) + local perm_el = st.stanza("perm", {access="iq"}) for namespace, ns_perm in pairs(iq_perm) do local perm_type if ns_perm.set and ns_perm.get then @@ -81,8 +81,9 @@ else perm_type = nil end - message:tag("namespace", {ns=namespace, type=perm_type}) + perm_el:tag("namespace", {ns=namespace, type=perm_type}):up() end + message:add_child(perm_el) end session.send(message) end
--- a/mod_proxy65_whitelist/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_proxy65_whitelist/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: 'Stage-Alpha' +labels: +- Stage-Alpha summary: Limit which file transfer users can use ...
--- a/mod_pubsub_eventsource/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_eventsource/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -1,5 +1,6 @@ --- -labels: 'Stage-Beta' +labels: +- Stage-Beta summary: Subscribe to pubsub nodes using the HTML5 EventSource API ...
--- a/mod_pubsub_mqtt/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_mqtt/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -11,35 +11,38 @@ to embedded devices. This module provides a way for MQTT clients to connect to Prosody and publish or subscribe to local pubsub nodes. +The module currently implements MQTT version 3.1.1. + Details ------- MQTT has the concept of 'topics' (similar to XMPP's pubsub 'nodes'). mod\_pubsub\_mqtt maps pubsub nodes to MQTT topics of the form -`HOST/NODE`, e.g.`pubsub.example.org/mynode`. +`<HOST>/<TYPE>/<NODE>`, e.g.`pubsub.example.org/json/mynode`. + +The 'TYPE' parameter in the topic allows the client to choose the payload +format it will send/receive. For the supported values of 'TYPE' see the +'Payloads' section below. ### Limitations The current implementation is quite basic, and in particular: - Authentication is not supported -- SSL/TLS is not supported - Only QoS level 0 is supported ### Payloads XMPP payloads are always XML, but MQTT does not define a payload format. -Therefore mod\_pubsub\_mqtt will attempt to convert data of certain -recognised payload types. Currently supported: +Therefore mod\_pubsub\_mqtt has some built-in data format translators. + +Currently supported data types: -- JSON (see [XEP-0335](http://xmpp.org/extensions/xep-0335.html) for - the format) -- Plain UTF-8 text (wrapped inside +- `json`: See [XEP-0335](http://xmpp.org/extensions/xep-0335.html) for + the format. +- `utf8`: Plain UTF-8 text (wrapped inside `<data xmlns="https://prosody.im/protocol/mqtt"/>`) - -All other XMPP payload types are sent to the client directly as XML. -Data published by MQTT clients is currently never translated, and always -treated as UTF-8 text. +- `atom_title`: Returns the title of an Atom entry as UTF-8 data Configuration ------------- @@ -51,16 +54,15 @@ modules_enabled = { "pubsub_mqtt" } You may also configure which port(s) mod\_pubsub\_mqtt listens on using -Prosody's standard config directives, such as `mqtt_ports`. Network -settings **must** be specified in the global section of the config file, -not under any particular pubsub component. The default port is 1883 -(MQTT's standard port number). +Prosody's standard config directives, such as `mqtt_ports` and +`mqtt_tls_ports`. Network settings **must** be specified in the global section +of the config file, not under any particular pubsub component. The default +port is 1883 (MQTT's standard port number) and 8883 for TLS connections. Compatibility ------------- ------- -------------- trunk Works - 0.9 Works - 0.8 Doesn't work + 0.12 Works ------- --------------
--- a/mod_pubsub_mqtt/mod_pubsub_mqtt.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_mqtt/mod_pubsub_mqtt.lua Sun Feb 16 16:09:03 2025 +0700 @@ -59,6 +59,15 @@ end function packet_handlers.connect(session, packet) + module:log("info", "MQTT client connected (sending connack)"); + module:log("debug", "MQTT version: %02x", packet.version); + if packet.version ~= 0x04 then -- Version mismatch + session.conn:write(mqtt.serialize_packet{ + type = "connack"; + data = string.char(0x00, 0x01); + }); + return; + end session.conn:write(mqtt.serialize_packet{ type = "connack"; data = string.char(0x00, 0x00); @@ -96,27 +105,33 @@ end function packet_handlers.subscribe(session, packet) - for _, topic in ipairs(packet.topics) do + local results = {}; + for i, topic in ipairs(packet.topics) do module:log("info", "SUBSCRIBE to %s", topic); local host, payload_type, node = topic:match("^([^/]+)/([^/]+)/(.+)$"); if not host then module:log("warn", "Invalid topic format - expected: HOST/TYPE/NODE"); - return; - end - local pubsub = pubsub_subscribers[host]; - if not pubsub then - module:log("warn", "Unable to locate host/node: %s", topic); - return; + results[i] = 0x80; -- Failure + else + local pubsub = pubsub_subscribers[host]; + if not pubsub then + module:log("warn", "Unable to locate host/node: %s", topic); + results[i] = 0x80; -- Failure + else + local node_subs = pubsub[node]; + if not node_subs then + node_subs = {}; + pubsub[node] = node_subs; + end + session.subscriptions[topic] = payload_type; + node_subs[session] = payload_type; + module:log("debug", "Successfully subscribed to %s", topic); + results[i] = 0x00; -- Success + end end - local node_subs = pubsub[node]; - if not node_subs then - node_subs = {}; - pubsub[node] = node_subs; - end - session.subscriptions[topic] = payload_type; - node_subs[session] = payload_type; end - + local ack = mqtt.serialize_packet{ type = "suback", id = packet.id, results = results }; + session.conn:write(ack); end function packet_handlers.pingreq(session, packet) @@ -191,7 +206,7 @@ topic = module.host.."/"..payload_type.."/"..event.node; data = data_translators[payload_type].from_item(event.item) or ""; }; - rawset(self, packet); + rawset(self, payload_type, packet); return packet; end; });
--- a/mod_pubsub_mqtt/mqtt.lib.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_mqtt/mqtt.lib.lua Sun Feb 16 16:09:03 2025 +0700 @@ -1,4 +1,4 @@ -local bit = require "bit"; +local bit = require "util.bitcompat"; local stream_mt = {}; stream_mt.__index = stream_mt; @@ -29,10 +29,25 @@ return self:read_bytes(len), len+2; end +function stream_mt:read_word() + local len1, len2 = self:read_bytes(2):byte(1,2); + local result = bit.lshift(len1, 8) + len2; + module:log("debug", "read_word(%02x, %02x) = %04x (%d)", len1, len2, result, result); + return result; +end + +local function hasbit(byte, n_bit) + return bit.band(byte, 2^n_bit) ~= 0; +end + +local function encode_string(str) + return string.char(bit.band(#str, 0xff00), bit.band(#str, 0x00ff))..str; +end + local packet_type_codes = { "connect", "connack", "publish", "puback", "pubrec", "pubrel", "pubcomp", - "subscribe", "subak", "unsubscribe", "unsuback", + "subscribe", "suback", "unsubscribe", "unsuback", "pingreq", "pingresp", "disconnect" }; @@ -59,9 +74,46 @@ packet.type = nil; -- Invalid packet else packet.version = self:read_bytes(1):byte(); - packet.connect_flags = self:read_bytes(1):byte(); - packet.keepalive_timer = self:read_bytes(1):byte(); + module:log("debug", "ver: %02x", packet.version); + if packet.version ~= 0x04 then + module:log("warn", "MQTT version mismatch (got %02x, we support %02x", packet.version, 0x04); + end + local flags = self:read_bytes(1):byte(); + module:log("debug", "flags: %02x", flags); + packet.keepalive_timer = self:read_bytes(2):byte(); + module:log("debug", "keepalive: %d", packet.keepalive_timer); + packet.connect_flags = {}; length = length - 11; + packet.connect_flags = { + clean_session = hasbit(flags, 1); + will = hasbit(flags, 2); + will_qos = bit.band(bit.rshift(flags, 2), 0x02); + will_retain = hasbit(flags, 5); + user_name = hasbit(flags, 7); + password = hasbit(flags, 6); + }; + module:log("debug", "%s", require "util.serialization".serialize(packet.connect_flags, "debug")); + module:log("debug", "Reading client_id..."); + packet.client_id = self:read_string(); + if packet.connect_flags.will then + module:log("debug", "Reading will..."); + packet.will = { + topic = self:read_string(); + message = self:read_string(); + qos = packet.connect_flags.will_qos; + retain = packet.connect_flags.will_retain; + }; + end + if packet.connect_flags.user_name then + module:log("debug", "Reading username..."); + packet.username = self:read_string(); + end + if packet.connect_flags.password then + module:log("debug", "Reading password..."); + packet.password = self:read_string(); + end + module:log("debug", "Done parsing connect!"); + length = 0; -- No payload left end elseif packet.type == "publish" then packet.topic = self:read_string(); @@ -87,6 +139,7 @@ if length > 0 then packet.data = self:read_bytes(length); end + module:log("debug", "MQTT packet complete!"); return packet; end @@ -102,7 +155,6 @@ end function stream_mt:feed(data) - module:log("debug", "Feeding %d bytes", #data); local packets = {}; local packet = self.parser(data); while packet do @@ -135,10 +187,10 @@ packet.data = string.char(bit.band(#topic, 0xff00), bit.band(#topic, 0x00ff))..topic..packet.data; elseif packet.type == "suback" then local t = {}; - for _, topic in ipairs(packet.topics) do - table.insert(t, string.char(bit.band(#topic, 0xff00), bit.band(#topic, 0x00ff))..topic.."\000"); + for i, result_code in ipairs(packet.results) do + table.insert(t, string.char(result_code)); end - packet.data = table.concat(t); + packet.data = packet.id..table.concat(t); end -- Get length
--- a/mod_pubsub_serverinfo/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_serverinfo/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -3,12 +3,16 @@ - 'Statistics' ... -Exposes server information over Pub/Sub per ProtoXEP: PubSub Server Information. +Exposes server information over Pub/Sub per [XEP-0485: PubSub Server Information](https://xmpp.org/extensions/xep-0485.html). The module announces support (used to 'opt-in', per the XEP) and publishes the name of the local domain via a Pub/Sub node. The published data will contain a 'remote-domain' element for inbound and outgoing s2s connections. These elements will be named only when the remote domain announces support ('opts in') too. +**Known issues:** + +- [Issue #1841](https://issues.prosody.im/1841): This module conflicts with mod_server_contact_info (both will run, but it may affect the ability of some implementations to read the server/contact information provided). + Installation ============
--- a/mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua Sun Feb 16 16:09:03 2025 +0700 @@ -2,18 +2,25 @@ local json = require "util.json"; local st = require "util.stanza"; local new_id = require"util.id".medium; -local dataform = require "util.dataforms".new; local local_domain = module:get_host(); -local service = module:get_option(module.name .. "_service") or "pubsub." .. local_domain; -local node = module:get_option(module.name .. "_node") or "serverinfo"; +local service = module:get_option_string(module.name .. "_service"); +local node = module:get_option_string(module.name .. "_node", "serverinfo"); local actor = module.host .. "/modules/" .. module.name; -local publication_interval = module:get_option(module.name .. "_publication_interval") or 300; -local cache_ttl = module:get_option(module.name .. "_cache_ttl") or 3600; +local publication_interval = module:get_option_number(module.name .. "_publication_interval", 300); +local cache_ttl = module:get_option_number(module.name .. "_cache_ttl", 3600); local public_providers_url = module:get_option_string(module.name.."_public_providers_url", "https://data.xmpp.net/providers/v2/providers-Ds.json"); local delete_node_on_unload = module:get_option_boolean(module.name.."_delete_node_on_unload", false); local persist_items = module:get_option_boolean(module.name.."_persist_items", true); +if not service and prosody.hosts["pubsub."..module.host] then + service = "pubsub."..module.host; +end +if not service + module:log_status("warn", "No pubsub service specified - module not activated"); + return; +end + local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; function module.load() @@ -29,10 +36,9 @@ module:add_feature("urn:xmpp:serverinfo:0"); - module:add_extension(dataform { - { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/network/serverinfo" }, - { name = "serverinfo-pubsub-node", type = "text-single" }, - }:form({ ["serverinfo-pubsub-node"] = ("xmpp:%s?;node=%s"):format(service, node) }, "result")); + module:add_item("server-info-fields", { + { name = "serverinfo-pubsub-node", type = "text-single", value = ("xmpp:%s?;node=%s"):format(service, node) }; + }); if cache_ttl < publication_interval then module:log("warn", "It is recommended to have a cache interval higher than the publication interval");
--- a/mod_push2/mod_push2.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_push2/mod_push2.lua Sun Feb 16 16:09:03 2025 +0700 @@ -536,7 +536,7 @@ -- only notify if the stanza destination is the mam user we store it for if event.for_user == to then - local user_push_services = push2_registrations:get(to) + local user_push_services = push2_registrations:get(to) or {} -- Urgent stanzas are time-sensitive (e.g. calls) and should -- be pushed immediately to avoid getting stuck in the smacks
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_report_forward/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,77 @@ +--- +labels: +- 'Stage-Beta' +summary: 'Forward spam/abuse reports to a JID' +--- + +This module forwards spam/abuse reports (e.g. those submitted by users via +XEP-0377 via mod_spam_reporting) to one or more JIDs. + +## Configuration + +Install and enable the module the same as any other: + +```lua +modules_enabled = { + --- + "report_forward"; + --- +} +``` + +There are two main options. You can set `report_forward_to` which accepts a +list of JIDs to send all reports to (default is empty): + +```lua +report_forward_to = { "antispam.example.com" } +``` + +You can also control whether the module sends a report to the server from +which the spam/abuse originated (default is `true`): + +```lua +report_forward_to_origin = false +``` + +The module looks up an abuse report address using XEP-0157 (only XMPP +addresses are accepted). If it fails to find any suitable destination, it will +log a warning and not send the report. + + + +## Protocol + +This section is intended for developers. + +XEP-0377 assumes the report is embedded within another protocol such as +XEP-0191, and doesn't specify a format for communicating "standalone" reports. +This module transmits them inside a `<message>` stanza, and adds a `<jid/>` +element (borrowed from XEP-0268): + +```xml +<message from="prosody.example" to="destination.example"> + <report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:spam"> + <jid xmlns="urn:xmpp:jid:0">spammer@bad.example</jid> + <text> + Never came trouble to my house like this. + </text> + </report> +</message> +``` + +It may also include the reported message, if this has been indicated by the +user, wrapped in a XEP-0297 `<forwarded/>` element: + +```xml +<message from="prosody.example" to="destination.example"> + <report reason="urn:xmpp:reporting:spam" xmlns="urn:xmpp:reporting:1"> + <jid xmlns="urn:xmpp:jid:0">spammer@bad.example</jid> + <text>Never came trouble to my house like this.</text> + </report> + <forwarded xmlns="urn:xmpp:forward:0"> + <message from="spammer@bad.example" to="victim@prosody.example" type="chat" xmlns="jabber:client"> + <body>Spam, Spam, Spam, Spam, Spam, Spam, baked beans, Spam, Spam and Spam!</body> + </message> + </forwarded> +</message> +```
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_report_forward/mod_report_forward.lua Sun Feb 16 16:09:03 2025 +0700 @@ -0,0 +1,152 @@ +local dt = require "util.datetime"; +local jid = require "util.jid"; +local st = require "util.stanza"; +local url = require "socket.url"; + +local new_id = require "util.id".short; +local render = require"util.interpolation".new("%b{}", function (s) return s; end); + +module:depends("spam_reporting"); + +local destinations = module:get_option_set("report_forward_to", {}); + +local archive = module:open_store("archive", "archive"); + +local cache_size = module:get_option_number("report_forward_contact_cache_size", 256); +local report_to_origin = module:get_option_boolean("report_forward_to_origin", true); +local contact_lookup_timeout = module:get_option_number("report_forward_contact_lookup_timeout", 180); + +local body_template = module:get_option_string("report_forward_body_template", [[ +SPAM/ABUSE REPORT +----------------- + +Reported JID: {reported_jid} + +A user on our service has reported a message originating from the above JID on +your server. + +{reported_message_time&The reported message was sent at: {reported_message_time}} + +-- +This message contains also machine-readable payloads, including XEP-0377, in case +you want to automate handling of these reports. You can receive these reports +to a different address by setting 'spam-report-addresses' in your server +contact info configuration. For more information, see https://xmppbl.org/reports/ +]]):gsub("^%s+", ""):gsub("(%S)\n(%S)", "%1 %2"); + +local report_addresses = require "util.cache".new(cache_size); + +local function get_address(form, ...) + for i = 1, select("#", ...) do + local field_var = select(i, ...); + local field = form:get_child_with_attr("field", nil, "var", field_var); + if field then + for value in field:childtags("value") do + local parsed = url.parse(value:get_text()); + if parsed.scheme == "xmpp" and parsed.path and not parsed.query then + return parsed.path; + end + end + else + module:log("debug", "No field '%s'", field_var); + end + end +end + +local function get_origin_report_address(reported_jid) + local host = jid.host(reported_jid); + local address = report_addresses:get(host); + if address then return address; end + + local contact_query = st.iq({ type = "get", to = host, from = module.host, id = new_id() }) + :query("http://jabber.org/protocol/disco#info"); + + return module:send_iq(contact_query, prosody.hosts[module.host], contact_lookup_timeout) + :next(function (result) + module:log("debug", "Processing contact form..."); + local response = result.stanza; + if response.attr.type ~= "result" then + module:log("warn", "Failed to query contact addresses of %s: %s", host, response); + return; + end + + for form in response.tags[1]:childtags("x", "jabber:x:data") do + local form_type = form:get_child_with_attr("field", nil, "var", "FORM_TYPE"); + if form_type and form_type:get_child_text("value") == "http://jabber.org/network/serverinfo" then + address = get_address(form, "spam-report-addresses", "abuse-addresses"); + break; + end + end + return address; + end); +end + +local function send_report(to, message) + local m = st.clone(message); + m.attr.to = to; + module:send(m); +end + +function forward_report(event) + local reporter_username = event.origin.username; + local reporter_jid = jid.join(reporter_username, module.host); + local reported_jid = event.jid; + + local report = st.clone(event.report); + report:text_tag("jid", reported_jid, { xmlns = "urn:xmpp:jid:0" }); + + local reported_message_el = report:get_child_with_attr( + "stanza-id", + "urn:xmpp:sid:0", + "by", + reported_jid, + jid.prep + ); + + local reported_message, reported_message_time, reported_message_with; + if reported_message_el then + reported_message, reported_message_time, reported_message_with = archive:get(reporter_username, reported_message_el.attr.id); + if jid.bare(reported_message_with) ~= event.jid then + reported_message = nil; + end + end + + local body_text = render(body_template, { + reporter_jid = reporter_jid; + reported_jid = event.jid; + reported_message_time = dt.datetime(reported_message_time); + }); + + local message = st.message({ from = module.host, id = new_id() }) + :text_tag("body", body_text) + :add_child(report); + + if reported_message then + reported_message.attr.xmlns = "jabber:client"; + local fwd = st.stanza("forwarded", { xmlns = "urn:xmpp:forward:0" }) + :tag("delay", { xmlns = "urn:xmpp:delay", stamp = dt.datetime(reported_message_time) }):up() + :add_child(reported_message); + message:add_child(fwd); + end + + for destination in destinations do + send_report(destination, message); + end + + if report_to_origin then + module:log("debug", "Sending report to origin server..."); + get_origin_report_address(event.jid):next(function (origin_report_address) + if not origin_report_address then + module:log("warn", "Couldn't report to origin: no contact address found for %s", jid.host(event.jid)); + return; + end + send_report(origin_report_address, message); + end):catch(function (e) + module:log("error", "Failed to report to origin server: %s", e); + end); + end +end + +module:hook("spam_reporting/abuse-report", forward_report, -1); +module:hook("spam_reporting/spam-report", forward_report, -1); +module:hook("spam_reporting/unknown-report", forward_report, -1);
--- a/mod_rest/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_rest/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -21,21 +21,55 @@ # Usage +You make a choice: install via VirtualHosts or as a Component. User authentication can +be used when installed via VirtualHost, and OAuth2 can be used for either. + ## On VirtualHosts +This enables rest on the VirtualHost domain, enabling user authentication to secure +the endpoint. Make sure that the modules_enabled section is immediately below the +VirtualHost entry so that it's not under any Component sections. EG: + ```lua -VirtualHost "example.com" +VirtualHost "chat.example.com" modules_enabled = {"rest"} ``` ## As a Component +If you install this as a component, you won't be able to use user authentication above, +and must use OAuth2 authentication outlined below. + ``` {.lua} -Component "rest.example.net" "rest" +Component "chat.example.com" "rest" component_secret = "dmVyeSBzZWNyZXQgdG9rZW4K" modules_enabled = {"http_oauth2"} ``` +## User authentication + +To enable user authentication, edit the "admins = { }" section in prosody.cfg.lua, EG: + +```lua +admins = { "admin@chat.example.com" } +``` + +To set up the admin user account: + +```lua +prosodyctl adduser admin@chat.example.com +``` + +and lastly, drop the "@host" from the username in your http queries, EG: + +```lua +curl \ + https://chat.example.com:5281/rest/version/chat.example.com \ + -k \ + --user admin \ + -H 'Accept: application/json' +``` + ## OAuth2 [mod_http_oauth2] can be used to grant bearer tokens which are accepted @@ -45,7 +79,7 @@ ## Sending stanzas The API endpoint becomes available at the path `/rest`, so the full URL -will be something like `https://your-prosody.example:5281/rest`. +will be something like `https://conference.chat.example.com:5281/rest`. To try it, simply `curl` an XML stanza payload:
--- a/mod_rest/res/schema-xmpp.json Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_rest/res/schema-xmpp.json Sun Feb 16 16:09:03 2025 +0700 @@ -109,6 +109,9 @@ }, "delay" : { "description" : "Timestamp of when a stanza was delayed, in ISO 8601 / XEP-0082 format.", + "examples" : [ + "2002-09-10T23:08:25Z" + ], "format" : "date-time", "title" : "XEP-0203: Delayed Delivery", "type" : "string", @@ -120,7 +123,9 @@ }, "from" : { "description" : "the sender of the stanza", - "example" : "bob@example.net", + "examples" : [ + "bob@example.net" + ], "format" : "xmpp-jid", "type" : "string", "xml" : { @@ -129,6 +134,10 @@ }, "id" : { "description" : "Reasonably unique id. mod_rest generates one if left out.", + "examples" : [ + "c6d02db2-5c1f-4e00-9014-4dd3e21309b0", + "EBRthZvxaAEXoTJ77w692pQW" + ], "type" : "string", "xml" : { "attribute" : true @@ -136,7 +145,14 @@ }, "lang" : { "description" : "Language code", - "example" : "en", + "examples" : [ + "de", + "en", + "en-UK", + "en-US", + "fr", + "sv-SE" + ], "type" : "string", "xml" : { "attribute" : true, @@ -144,6 +160,9 @@ } }, "nick" : { + "examples" : [ + "CallMeIshmael" + ], "type" : "string", "xml" : { "name" : "nick", @@ -206,6 +225,9 @@ "to" : { "description" : "the intended recipient for the stanza", "example" : "alice@example.com", + "examples" : [ + "alice@example.com" + ], "format" : "xmpp-jid", "type" : "string", "xml" : { @@ -342,7 +364,7 @@ "gateway" : { "properties" : { "desc" : { - "type" : "text" + "type" : "string" }, "jid" : { "type" : "string" @@ -662,14 +684,23 @@ "properties" : { "name" : { "example" : "My Software", + "examples" : [ + "My Software" + ], "type" : "string" }, "os" : { "example" : "Linux", + "examples" : [ + "Linux" + ], "type" : "string" }, "version" : { "example" : "1.0.0", + "examples" : [ + "1.0.0" + ], "type" : "string" } }, @@ -730,6 +761,9 @@ "body" : { "description" : "Human-readable chat message", "example" : "Hello, World!", + "examples" : [ + "Hello, World!" + ], "type" : "string" }, "dataform" : { @@ -864,6 +898,9 @@ "url" : { "description" : "The URL of the attached media file", "example" : "https://media.example.net/thisfile.jpg", + "examples" : [ + "https://media.example.net/thisfile.jpg" + ], "format" : "uri", "type" : "string" } @@ -1031,6 +1068,10 @@ "subject" : { "description" : "Subject of message or group chat", "example" : "Talking about stuff", + "examples" : [ + "I implore you!", + "Talking about stuff" + ], "type" : "string" }, "thread" : {
--- a/mod_sasl2/mod_sasl2.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_sasl2/mod_sasl2.lua Sun Feb 16 16:09:03 2025 +0700 @@ -65,6 +65,8 @@ log("debug", "Channel binding 'tls-exporter' supported"); sasl_handler:add_cb_handler("tls-exporter", sasl_tls_exporter); channel_bindings:add("tls-exporter"); + else + log("debug", "Channel binding 'tls-exporter' not supported"); end elseif origin.conn.ssl_peerfinished and origin.conn:ssl_peerfinished() then log("debug", "Channel binding 'tls-unique' supported");
--- a/mod_sasl2_fast/README.md Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_sasl2_fast/README.md Sun Feb 16 16:09:03 2025 +0700 @@ -7,12 +7,8 @@ - mod_sasl2 --- -This module implements a mechanism via which clients can exchange a password -for a secure token, improving security and streamlining future reconnections. - -At the time of writing, the XEP that describes the FAST protocol is still -working its way through the XSF standards process. You can [view the FAST XEP -proposal here](https://xmpp.org/extensions/inbox/xep-fast.html). +This module implements a mechanism described in [XEP-0484: Fast Authentication Streamlining Tokens] via which clients can exchange a +password for a secure token, improving security and streamlining future reconnections. This module depends on [mod_sasl2].
--- a/mod_sasl2_fast/mod_sasl2_fast.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_sasl2_fast/mod_sasl2_fast.lua Sun Feb 16 16:09:03 2025 +0700 @@ -196,6 +196,13 @@ if not authc_username then return "failure", "malformed-request"; end + if not sasl_handler.profile.cb then + module:log("warn", "Attempt to use channel binding %s with SASL profile that does not support any channel binding (FAST: %s)", cb_name, sasl_handler.fast); + return "failure", "malformed-request"; + elseif not sasl_handler.profile.cb[cb_name] then + module:log("warn", "SASL profile does not support %s channel binding (FAST: %s)", cb_name, sasl_handler.fast); + return "failure", "malformed-request"; + end local cb_data = cb_name and sasl_handler.profile.cb[cb_name](sasl_handler) or ""; local ok, authz_username, response, rotation_needed = backend( mechanism_name,
--- a/mod_sasl_ssdp/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_sasl_ssdp/README.markdown Sun Feb 16 16:09:03 2025 +0700 @@ -14,8 +14,8 @@ **Note:** This module implements version 0.3.0 of XEP-0474. As of 2023-12-05, this version is not yet published on xmpp.org. Version 0.3.0 of the XEP is -implemented in Monal 6.0.1. No other clients are currently known to implement -the XEP at the time of writing. +implemented in Monal 6.0.1 and go-sendxmpp 0.8.0. No other clients are currently +known to implement the XEP at the time of writing. # Configuration
--- a/mod_server_info/README.md Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_server_info/README.md Sun Feb 16 16:09:03 2025 +0700 @@ -14,37 +14,52 @@ Everything configured here is publicly visible to other XMPP entities. +**Note:** This module was rewritten in February 2024, the configuration is not +compatible with the previous version of the module. + ## Configuration -The `server_info` option accepts a list of dataforms. A dataform is an array -of fields. A field has three required properties: +The `server_info_extensions` option accepts a list of custom fields to include +in the server info form. + +A field has three required properties: - `type` - usually `text-single` or `list-multi` -- `var` - the field name +- `var` - the field name (see below) - `value` the field value Example configuration: ``` lua server_info = { - - -- Our custom form - { - -- Conventionally XMPP dataforms have a 'FORM_TYPE' field to - -- indicate what type of form it is - { type = "hidden", var = "FORM_TYPE", value = "urn:example:foo" }; + -- Advertise that our maximum speed is 88 mph + { type = "text-single", var = "speed", value = "88" }; - -- Advertise that our maximum speed is 88 mph - { type = "text-single", var = "speed", value = "88" }; - - -- Advertise that the time is 1:20 AM and zero seconds - { type = "text-single", var = "time", value = "01:21:00" }; - }; - + -- Advertise that the time is 1:20 AM and zero seconds + { type = "text-single", var = "time", value = "01:21:00" }; } ``` +The `var` attribute is used to uniquely identify fields. Every `var` should be +registered with the XSF [form registry](https://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo), +or prefixed with a custom namespace using Clark notation, e.g. `{https://example.com}my-field-name`. This is to prevent +collisions. + +## Developers + +Developers of other modules can add fields to the form at runtime: + +```lua +module:depends("server_info"); + +module:add_item("server-info-fields", { + { type = "text-single", var = "speed", value = "88" }; + { type = "text-single", var = "time", value = "01:21:00" }; +}); +``` + +Prosody will ensure they are removed if your module is unloaded. + ## Compatibility -This module should be compatible with Prosody 0.12, and possibly earlier -versions. +This module should be compatible with Prosody 0.12 and later.
--- a/mod_server_info/mod_server_info.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_server_info/mod_server_info.lua Sun Feb 16 16:09:03 2025 +0700 @@ -1,18 +1,60 @@ --- XEP-0128: Service Discovery Extensions (manual config) --- --- Copyright (C) 2023 Matthew Wild --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- +-- mod_server_info imported from Prosody commit 1ce18cb3e6cc for the benefit +-- of 0.12 deployments. This community version of the module will not load in +-- newer Prosody versions, which include their own copy of the module. +--% conflicts: mod_server_info + +local dataforms = require "prosody.util.dataforms"; + +local server_info_config = module:get_option("server_info", {}); +local server_info_custom_fields = module:get_option_array("server_info_extensions"); -local dataforms = require "util.dataforms"; - -local config = module:get_option("server_info"); +-- Source: http://xmpp.org/registrar/formtypes.html#http:--jabber.org-network-serverinfo +local form_layout = dataforms.new({ + { var = "FORM_TYPE"; type = "hidden"; value = "http://jabber.org/network/serverinfo" }; +}); -if not config or next(config) == nil then return; end -- Nothing to do - -for _, form in ipairs(config) do - module:add_extension(dataforms.new(form):form({}, "result")); +if server_info_custom_fields then + for _, field in ipairs(server_info_custom_fields) do + table.insert(form_layout, field); + end end +local generated_form; + +function update_form() + local new_form = form_layout:form(server_info_config, "result"); + if generated_form then + module:remove_item("extension", generated_form); + end + generated_form = new_form; + module:add_item("extension", generated_form); +end + +function add_fields(event) + local fields = event.item; + for _, field in ipairs(fields) do + table.insert(form_layout, field); + end + update_form(); +end + +function remove_fields(event) + local removed_fields = event.item; + for _, removed_field in ipairs(removed_fields) do + local removed_var = removed_field.var or removed_field.name; + for i, field in ipairs(form_layout) do + local var = field.var or field.name + if var == removed_var then + table.remove(form_layout, i); + break; + end + end + end + update_form(); +end + +module:handle_items("server-info-fields", add_fields, remove_fields); + +function module.load() + update_form(); +end
--- a/mod_spam_report_forwarder/README.markdown Tue Feb 06 18:32:01 2024 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,48 +0,0 @@ ---- -labels: -- 'Stage-Beta' -summary: 'Forward spam/abuse reports to a JID' ---- - -This module forwards spam/abuse reports (e.g. those submitted by users via -XEP-0377 via mod_spam_reporting) to one or more JIDs. - -## Configuration - -Install and enable the module the same as any other. - -There is a single option, `spam_report_destinations` which accepts a list of -JIDs to send reports to. - -For example: - -```lua -modules_enabled = { - --- - "spam_reporting"; - "spam_report_forwarder"; - --- -} - -spam_report_destinations = { "antispam.example.com" } -``` - -## Protocol - -This section is intended for developers. - -XEP-0377 assumes the report is embedded within another protocol such as -XEP-0191, and doesn't specify a format for communicating "standalone" reports. -This module transmits them inside a `<message>` stanza, and adds a `<jid/>` -element (borrowed from XEP-0268): - -```xml -<message from="prosody.example" to="destination.example"> - <report xmlns="urn:xmpp:reporting:1" reason="urn:xmpp:reporting:spam"> - <jid xmlns="urn:xmpp:jid:0">spammer@bad.example</jid> - <text> - Never came trouble to my house like this. - </text> - </report> -</message> -```
--- a/mod_spam_report_forwarder/mod_spam_report_forwarder.lua Tue Feb 06 18:32:01 2024 +0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,21 +0,0 @@ -local st = require "util.stanza"; - -local destinations = module:get_option_set("spam_report_destinations", {}); - -function forward_report(event) - local report = st.clone(event.report); - report:text_tag("jid", event.jid, { xmlns = "urn:xmpp:jid:0" }); - - local message = st.message({ from = module.host }) - :add_child(report); - - for destination in destinations do - local m = st.clone(message); - m.attr.to = destination; - module:send(m); - end -end - -module:hook("spam_reporting/abuse-report", forward_report, -1); -module:hook("spam_reporting/spam-report", forward_report, -1); -module:hook("spam_reporting/unknown-report", forward_report, -1);
--- a/mod_traceback/mod_traceback.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_traceback/mod_traceback.lua Sun Feb 16 16:09:03 2025 +0700 @@ -2,9 +2,10 @@ local traceback = require "util.debug".traceback; -require"util.signal".signal(module:get_option_string(module.name, "SIGUSR1"), function () - module:log("info", "Received SIGUSR1, writing traceback"); - local f = io.open(prosody.paths.data.."/traceback.txt", "a+"); +local signal = module:get_option_string(module.name, "SIGUSR1"); +module:hook("signal/" .. signal, function() + module:log("info", "Received %s, writing traceback", signal); + local f = io.open(prosody.paths.data .. "/traceback.txt", "a+"); f:write(traceback(), "\n"); f:close(); end);
--- a/mod_vcard_muc/mod_vcard_muc.lua Tue Feb 06 18:32:01 2024 +0700 +++ b/mod_vcard_muc/mod_vcard_muc.lua Sun Feb 16 16:09:03 2025 +0700 @@ -103,10 +103,10 @@ event.reply:tag("feature", { var = "vcard-temp" }):up(); table.insert(event.form, { - name = "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", - type = "text-single", + name = "muc#roominfo_avatarhash", + type = "text-multi", }); - event.formdata["{http://modules.prosody.im/mod_vcard_muc}avatar#sha1"] = get_photo_hash(event.room); + event.formdata["muc#roominfo_avatarhash"] = get_photo_hash(event.room); end); module:hook("muc-occupant-session-new", function(event)