Changeset

6138:9db1529c06c2

Merge upstream
author tmolitor <thilo@eightysoft.de>
date Sun, 05 Jan 2025 17:50:02 +0100
parents 6137:4cb1cad2badd (current diff) 6135:c42419d73737 (diff)
children 6139:ce755c661036
files
diffstat 54 files changed, 1378 insertions(+), 377 deletions(-) [+]
line wrap: on
line diff
--- a/mod_admin_blocklist/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_admin_blocklist/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,5 +1,5 @@
 ---
-summary: Block s2s connections based on admin blocklists
+summary: Block s2s connections based on admin blocklist
 labels:
 - 'Stage-Beta'
 ...
@@ -21,11 +21,11 @@
 admin_blocklist_roles = { "prosody:operator", "prosody:admin" }
 ```
 
-#Compatibility
+# Compatibility
 
-  ------- -------
-  trunk*   Works
-  0.12     Works
-  ------- -------
+  Prosody-Version Status
+  --------------- ------
+  trunk*          Works
+  0.12            Works
 
-*as of 2024-10-22
+*as of 2024-12-21
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_anti_spam/README.markdown	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,96 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: 'Spam filtering'
+rockspec:
+  build:
+    modules:
+      mod_anti_spam.rtbl: rtbl.lib.lua
+      mod_anti_spam.trie: trie.lib.lua
+---
+
+This module aims to provide an all-in-one spam filter for any kind of Prosody
+deployment.
+
+## What is spam?
+
+You're lucky if you have to ask! But it's worth explaining, so we can be clear
+about what the module does (and does not).
+
+Similar to every other popular communication network, there are people who try
+to exploit XMPP for sending unsolicited messages - usually advertisements
+for products and services. These people have gathered large lists of user
+addresses, e.g. by searching and "scraping" websites for contact info.
+
+If your address has not been discovered by the spammers, you won't receive any
+spam. Prosody does not reveal user addresses (except, obviously, to people who
+you communicate with). So to avoid it being picked up by spammers, be careful
+about posting it unprotected on websites, etc.
+
+However, if get unlucky and your address is discovered by spammers, you may
+receive dozens of spam messages per day. mod_anti_spam is designed to filter
+these annoying messages to prevent them from reaching you.
+
+## How does it work?
+
+mod_anti_spam uses a variety of techniques to identify likely spam. Just as
+the behaviour of spammers changes, The exact methods used to detect spam may
+evolve over time in future updates.
+
+If the sender is in the recipient's contact list already, no filtering will be
+performed.
+
+Otherwise, if the sender is a "stranger" to the recipient, the module will
+perform some checks, and decide whether to let the message or contact request
+through.
+
+### Shared block lists
+
+mod_anti_spam can subscribe to Real-Time Block Lists (RTBLs) such as those
+published by [xmppbl.org](https://xmppbl.org). This is a highly effective
+measure to reduce spam from the network.
+
+To enable this feature, you need to specify one or more compatible spam
+services in the config file:
+
+```lua
+anti_spam_services = { "xmppbl.org" }
+```
+
+### Content filters
+
+mod_anti_spam also supports optionally filtering messages with specific
+content or matching certain patterns.
+
+A list of strings to block can be specified in the config file like so:
+
+```lua
+anti_spam_block_strings = {
+  -- Block messages containing the text "exploit"
+  "exploit";
+}
+```
+
+Alternatively, you can specify a list of [Lua patterns](https://lua.org/manual/5.4/manual.html#6.4.1).
+These are similar to regular expressions you may be familiar with from tools
+like grep, but differ in a number of ways. Lua patterns are faster, but have
+fewer features. The syntax is not fully compatible with other more widely-used
+regular expression syntaxes. Read the Lua manual for full details.
+
+```lua
+anti_spam_block_patterns = {
+  -- Block OTR handshake greetings (modern XMPP clients do not use OTR)
+  "^%?OTRv2?3?%?";
+}
+```
+
+There are no string or pattern filters in the module by default.
+
+## Handling reports
+
+We recommend setting up Prosody to allow spam reporting, in case any spam
+still gets through. Documentation can be found on [xmppbl.org's site](https://xmppbl.org/reports#server-operators).
+
+## Compatibility
+
+Compatible with Prosody 0.12 and later.
--- a/mod_anti_spam/mod_anti_spam.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_anti_spam/mod_anti_spam.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -18,6 +18,13 @@
 
 local count_spam_blocked = module:metric("counter", "anti_spam_blocked", "stanzas", "Stanzas blocked as spam", {"reason"});
 
+local hosts = prosody.hosts;
+
+local reason_messages = {
+	default = "Rejected as spam";
+	["known-spam-source"] = "Rejected as spam. Your server is listed as a known source of spam. Please contact your server operator.";
+};
+
 function block_spam(event, reason, action)
 	event.spam_reason = reason;
 	event.spam_action = action;
@@ -30,7 +37,7 @@
 
 	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"));
+		event.origin.send(st.error_reply("cancel", "policy-violation", reason_messages[reason] or reason_messages.default));
 	else
 		module:log("debug", "Discarding likely spam %s from %s (%s)", event.stanza.name, event.stanza.attr.from, reason);
 	end
@@ -63,8 +70,11 @@
 	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
+	local raw_ip = session.ip;
+	local parsed_ip = raw_ip and ip.new_ip(session.ip);
+	-- Not every session has an ip - for example, stanzas sent from a
+	-- local host session
+	if parsed_ip and spam_source_ips:contains_ip(parsed_ip) then
 		return true;
 	end
 end
@@ -82,6 +92,8 @@
 	if not (spammy_strings or spammy_patterns) then return; end
 
 	local body = stanza:get_child_text("body");
+	if not body then return; end
+
 	if spammy_strings then
 		for _, s in ipairs(spammy_strings) do
 			if body:find(s, 1, true) then
@@ -100,7 +112,7 @@
 
 -- Set up RTBLs
 
-local anti_spam_services = module:get_option_array("anti_spam_services");
+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", {
@@ -113,10 +125,18 @@
 	});
 	new_rtbl_subscription(rtbl_service_jid, "spam_source_ips", {
 		added = function (item)
-			spam_source_ips:add_subnet(ip.parse_cidr(item));
+			local subnet_ip, subnet_bits = ip.parse_cidr(item);
+			if not subnet_ip then
+				return;
+			end
+			spam_source_ips:add_subnet(subnet_ip, subnet_bits);
 		end;
 		removed = function (item)
-			spam_source_ips:remove_subnet(ip.parse_cidr(item));
+			local subnet_ip, subnet_bits = ip.parse_cidr(item);
+			if not subnet_ip then
+				return;
+			end
+			spam_source_ips:remove_subnet(subnet_ip, subnet_bits);
 		end;
 	});
 	new_rtbl_subscription(rtbl_service_jid, "spam_source_jids_sha256", {
@@ -130,13 +150,20 @@
 end
 
 module:hook("message/bare", function (event)
-	local to_bare = jid_bare(event.stanza.attr.to);
+	local to_user, to_host = jid_split(event.stanza.attr.to);
 
-	if not user_exists(to_bare) then return; end
+	if not hosts[to_host] then
+		module:log("warn", "Skipping filtering of message to unknown host <%s>", to_host);
+		return;
+	end
+
+	if not user_exists(to_user, to_host) then return; end
 
 	local from_bare = jid_bare(event.stanza.attr.from);
 	if not is_from_stranger(from_bare, event) then return; end
 
+	module:log("debug", "Processing message from stranger...");
+
 	if is_spammy_server(event.origin) then
 		return block_spam(event, "known-spam-source", "drop");
 	end
@@ -148,18 +175,28 @@
 	if is_spammy_content(event.stanza) then
 		return block_spam(event, "spam-content", "drop");
 	end
+
+	module:log("debug", "Allowing message through");
 end, 500);
 
 module:hook("presence/bare", function (event)
-	if event.stanza.type ~= "subscribe" then
+	if event.stanza.attr.type ~= "subscribe" then
 		return;
 	end
 
+	module:log("debug", "Processing subscription request...");
+
 	if is_spammy_server(event.origin) then
 		return block_spam(event, "known-spam-source", "drop");
 	end
 
-	if is_spammy_sender(event.stanza) then
+	module:log("debug", "Not from known spam source server");
+
+	if is_spammy_sender(jid_bare(event.stanza.attr.from)) then
 		return block_spam(event, "known-spam-jid", "drop");
 	end
+
+	module:log("debug", "Not from known spam source JID");
+
+	module:log("debug", "Allowing subscription request through");
 end, 500);
--- a/mod_anti_spam/trie.lib.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_anti_spam/trie.lib.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -120,7 +120,7 @@
 	end
 end
 
-function trie_methods:has_ip(item)
+function trie_methods:contains_ip(item)
 	item = item.packed;
 	local node = self.root;
 	local len = #item;
--- a/mod_auto_accept_subscriptions/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_auto_accept_subscriptions/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -32,8 +32,8 @@
 Compatibility
 =============
 
-  ------- -------
-  trunk   Works
-  0.9     Works
-  0.8     Works
-  ------- -------
+  Prosody-Version Status
+  --------------- -----------
+  0.12            Should work
+  0.11            Should work
+  0.10            Works
--- a/mod_auto_moved/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_auto_moved/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -18,14 +18,12 @@
 Configuration
 =============
 
-There is no configuration for this module, just add it to
-modules\_enabled as normal.
+There is no configuration for this module, just add it to `modules_enabled` as normal.
 
 Compatibility
 =============
 
-  ----- -------
-  0.11  Does not work
-  ----- -------
-  trunk Works
-  ----- -------
+  Prosody-Version Status
+  --------------- -----------
+  trunk           Should Work
+  0.12            Works
--- a/mod_bind2/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
----
-labels:
-- Stage-Alpha
-rockspec:
-  dependencies:
-  - mod_sasl2
-summary: "XEP-0386: Bind 2.0"
----
-
-Experimental early implementation of [XEP-0386: Bind 2.0] for use with
-[mod_sasl2].
--- a/mod_bind2/mod_bind2.lua	Wed Nov 20 05:07:11 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-local mm = require "core.modulemanager";
-local sm = require "core.sessionmanager";
-
-local xmlns_sasl2 --[[<const>]] = "urn:xmpp:sasl:1";
-local xmlns_bind2 --[[<const>]] = "urn:xmpp:bind2:0";
-local xmlns_carbons --[[<const>]] = "urn:xmpp:carbons:2";
-
-module:depends("sasl2");
-module:depends("carbons");
-
-module:hook("stream-features", function(event)
-	local origin, features = event.origin, event.features;
-	if origin.type ~= "c2s_unauthed" then return end
-	features:tag("bind", xmlns_bind2):up();
-end);
-
-module:hook_tag(xmlns_sasl2, "authenticate", function (session, auth)
-	session.bind2 = auth:get_child("bind", xmlns_bind2);
-end, 1);
-
-module:hook("sasl2/c2s/success", function (event)
-	local session = event.session;
-	if not session.bind2 then return end
-
-	-- When it receives a bind 2.0 on an authenticated not-yet-bound session, the
-	-- server MUST:
-
-	-- Clear the offline messages for this user, if any, without sending them (as
-	-- they will be provided by MAM).
-	if mm.is_loaded(module.host, "offline") then -- luacheck: ignore 542
-		-- TODO
-	end
-
-	-- Perform resource binding to a random resource (see 6120)
-	if not sm.bind_resource(session, nil) then
-		-- FIXME How should this be handled even?
-		session:close("reset");
-		return true;
-	end
-
-	-- Work out which contacts have unread messages in the user's MAM archive,
-	-- how many, and what the id of the last read message is
-	-- XXX How do we know what the last read message was?
-	-- TODO archive:summary(session.username, { after = ??? });
-
-	-- Get the id of the newest stanza in the user's MAM archive
-	-- TODO archive:find(session.username, { reverse = true, limit = 1 });
-
-	-- Silently enable carbons for this session
-	session.carbons = xmlns_carbons;
-
-	-- After processing the bind stanza, as above, the server MUST respond with
-	-- an element of type 'bound' in the namespace 'urn:xmpp:bind2:0', as in the
-	-- below example
-	event.success:tag("bound", xmlns_bind2):text_tag("jid", session.full_jid):up();
-
-	session.bind2 = nil;
-end);
--- a/mod_checkcerts/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_checkcerts/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,8 +1,13 @@
 ---
 labels:
+- Stage-Broken
 summary: Certificate expiry reminder
 ...
 
+::: {.alert .alert-info} 
+This module is incompatible with prosody since version 0.10.
+:::
+
 Introduction
 ============
 
@@ -29,4 +34,4 @@
 Needs LuaSec 0.5+
 
 Originally written for Prosody 0.9.x, apparently incompatible with
-0.10.x.
+0.10 or greater
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compliance_2023/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,22 @@
+---
+summary: XMPP Compliance Suites 2023 self-test
+labels:
+- Stage-Beta
+rockspec:
+  dependencies:
+  - mod_cloud_notify
+
+...
+
+Compare the list of enabled modules with
+[XEP-0479: XMPP Compliance Suites 2023] and produce basic report to the
+Prosody log file.
+
+If installed with the Prosody plugin installer then all modules needed for a green checkmark should be included. (With prosody 0.12 only [mod_cloud_notify] is not included with prosody and we need the community module) 
+
+# Compatibility
+
+  Prosody-Version Status
+  --------------- ----------------------
+  trunk           Works as of 2024-12-21
+  0.12            Works
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compliance_2023/mod_compliance_2023.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,79 @@
+-- Copyright (c) 2021 Kim Alvefur
+--
+-- This module is MIT licensed.
+
+local hostmanager = require "core.hostmanager";
+
+local array = require "util.array";
+local set = require "util.set";
+
+local modules_enabled = module:get_option_inherited_set("modules_enabled");
+
+for host in pairs(hostmanager.get_children(module.host)) do
+	local component = module:context(host):get_option_string("component_module");
+	if component then
+		modules_enabled:add(component);
+		modules_enabled:include(module:context(host):get_option_set("modules_enabled", {}));
+	end
+end
+
+local function check(suggested, alternate, ...)
+	if set.intersection(modules_enabled, set.new({suggested; alternate; ...})):empty() then return suggested; end
+	return false;
+end
+
+local compliance = {
+	array {"Server"; check("tls"); check("disco")};
+
+	array {"Advanced Server"; check("pep", "pep_simple")};
+
+	array {"Web"; check("bosh"); check("websocket")};
+
+	-- No Server requirements for Advanced Web
+
+	array {"IM"; check("vcard_legacy", "vcard"); check("carbons"); check("http_file_share", "http_upload")};
+
+	array {
+		"Advanced IM";
+		check("vcard_legacy", "vcard");
+		check("blocklist");
+		check("muc");
+		check("private");
+		check("smacks");
+		check("mam");
+		check("bookmarks");
+	};
+
+	array {"Mobile"; check("smacks"); check("csi_simple", "csi_battery_saver")};
+
+	array {"Advanced Mobile"; check("cloud_notify")};
+
+	array {"A/V Calling"; check("turn_external", "external_services", "turncredentials", "extdisco")};
+
+};
+
+function check_compliance()
+	local compliant = true;
+	for _, suite in ipairs(compliance) do
+		local section = suite:pop(1);
+		if module:get_option_boolean("compliance_" .. section:lower():gsub("%A", "_"), true) then
+			local missing = set.new(suite:filter(function(m) return type(m) == "string" end):map(function(m) return "mod_" .. m end));
+			if suite[1] then
+				if compliant then
+					compliant = false;
+					module:log("warn", "Missing some modules for XMPP Compliance 2023");
+				end
+				module:log("info", "%s Compliance: %s", section, missing);
+			end
+		end
+	end
+
+	if compliant then module:log("info", "XMPP Compliance 2023: Compliant ✔️"); end
+end
+
+if prosody.start_time then
+	check_compliance()
+else
+	module:hook_global("server-started", check_compliance);
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compliance_latest/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,27 @@
+---
+summary: XMPP Compliance Suites self-test
+labels:
+- Stage-Beta
+rockspec:
+  dependencies:
+  - mod_compliance_2023
+...
+
+# Introduction
+
+This meta-module will always `require` (and therefore auto-load) the lastest compliance tester we have in the community modules.
+Currently this is [mod_compliance_2023]. See the linked module for further details.
+
+If you do not use the *Prosody plugin installer* this module will likely have limited value to you.
+You can also just install the current compliance tester manually.
+
+# Configuration
+
+Just load this module as any other module and it will automatically install and load [mod_compliance_2023] if you use the *Prosody plugin installer*. 
+
+# Compatibility
+
+  Prosody-Version Status
+  --------------- ----------------------
+  trunk           Works as of 2024-12-22
+  0.12            Works
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compliance_latest/mod_compliance_latest.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,6 @@
+local success, err = pcall(function() module:depends("compliance_2023") end)
+
+if not success then
+module:log_status( "error", "Error, can't load module: mod_compliance_2023. Is this module downloaded into a folder readable by prosody?" )
+return false
+end
--- a/mod_filter_chatstates/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_filter_chatstates/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -2,6 +2,12 @@
 summary: Drop chat states from messages to inactive sessions
 ...
 
+::: {.alert .alert-info}
+This module discards certain kinds of stanzas that are unnecessary to deliver to inactive clients. This is technically against the XMPP specification, and has the potential to cause bugs. However it is being used by some people successfully, and reduces the overall bandwidth usage for mobile devices.
+On the other hand it does not save battery usage in a relevant way compared to other `csi` modules.
+Consider using [mod_csi_simple][doc:modules:mod_csi_simple] that is incuded in prosody since Version 0.11.
+:::
+
 Introduction
 ============
 
@@ -22,5 +28,6 @@
 =============
 
   ----- -------
-  0.9   Works
+  0.11   Works
+  0.10   Works
   ----- -------
--- a/mod_firewall/conditions.lib.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_firewall/conditions.lib.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -123,7 +123,7 @@
 end
 
 function condition_handlers.SUBSCRIBED()
-	return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
+	return "(bare_to == bare_from or to_node and rostermanager.is_user_subscribed(to_node, to_host, bare_from))",
 	       { "rostermanager", "split_to", "bare_to", "bare_from" };
 end
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_flags/mod_flags.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,194 @@
+-- This module is only for 0.12, later versions have mod_flags bundled
+--% conflicts: mod_flags
+
+local flags_map;
+if prosody.process_type ~= "prosodyctl" then
+	flags_map = module:open_store("account_flags", "map");
+end
+
+-- API
+
+function add_flag(username, flag, comment) -- luacheck: ignore 131/add_flag
+	local flag_data = {
+		when = os.time();
+		comment = comment;
+	};
+
+	local ok, err = flags_map:set(username, flag, flag_data);
+	if not ok then
+		return nil, err;
+	end
+
+	module:fire_event("user-flag-added/"..flag, {
+		user = username;
+		flag = flag;
+		data = flag_data;
+	});
+
+	return true;
+end
+
+function remove_flag(username, flag) -- luacheck: ignore 131/remove_flag
+	local ok, err = flags_map:set(username, flag, nil);
+	if not ok then
+		return nil, err;
+	end
+
+	module:fire_event("user-flag-removed/"..flag, {
+		user = username;
+		flag = flag;
+	});
+
+	return true;
+end
+
+function has_flag(username, flag) -- luacheck: ignore 131/has_flag
+	local ok, err = flags_map:get(username, flag);
+	if not ok and err then
+		error("Failed to check flags for user: "..err);
+	end
+	return not not ok;
+end
+
+function get_flag_info(username, flag) -- luacheck: ignore 131/get_flag_info
+	return flags_map:get(username, flag);
+end
+
+
+-- Migration from mod_firewall marks
+
+local function migrate_marks(host)
+	local usermanager = require "core.usermanager";
+
+	local flag_storage = module:open_store("account_flags");
+	local mark_storage = module:open_store("firewall_marks");
+
+	local migration_comment = "Migrated from mod_firewall marks at "..os.date("%Y-%m-%d %R");
+
+	local migrated, empty, errors = 0, 0, 0;
+	for username in usermanager.users(host) do
+		local marks, err = mark_storage:get(username);
+		if marks then
+			local flags = {};
+			for mark_name, mark_timestamp in pairs(marks) do
+				flags[mark_name] = {
+					when = mark_timestamp;
+					comment = migration_comment;
+				};
+			end
+			local saved_ok, saved_err = flag_storage:set(username, flags);
+			if saved_ok then
+				prosody.log("error", "Failed to save flags for %s: %s", username, saved_err);
+				migrated = migrated + 1;
+			else
+				errors = errors + 1;
+			end
+		elseif err then
+			prosody.log("error", "Failed to load marks for %s: %s", username, err);
+			errors = errors + 1;
+		else
+			empty = empty + 1;
+		end
+	end
+
+	print(("Finished - %d migrated, %d users with no marks, %d errors"):format(migrated, empty, errors));
+end
+
+function module.command(arg)
+	local storagemanager = require "core.storagemanager";
+	local usermanager = require "core.usermanager";
+	local jid = require "util.jid";
+	local warn = require"util.prosodyctl".show_warning;
+
+	local command = arg[1];
+	if not command then
+		warn("Valid subcommands: migrate_marks");
+		return 0;
+	end
+	table.remove(arg, 1);
+
+	local node, host = jid.prepped_split(arg[1]);
+	if not host then
+		warn("Please specify a host or JID after the command");
+		return 1;
+	elseif not prosody.hosts[host] then
+		warn("Unknown host: "..host);
+		return 1;
+	end
+
+	table.remove(arg, 1);
+
+	module.host = host; -- luacheck: ignore 122
+	storagemanager.initialize_host(host);
+	usermanager.initialize_host(host);
+
+	flags_map = module:open_store("account_flags", "map");
+
+	if command == "migrate_marks" then
+		migrate_marks(host);
+		return 0;
+	elseif command == "find" then
+		local flag = assert(arg[1], "expected argument: flag");
+		local flags = module:open_store("account_flags", "map");
+		local users_with_flag = flags:get_all(flag);
+
+		local c = 0;
+		for user, flag_data in pairs(users_with_flag) do
+			print(user, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment or "");
+			c = c + 1;
+		end
+
+		print(("%d accounts listed"):format(c));
+		return 1;
+	elseif command == "add" then
+		local username = assert(node, "expected a user JID, got "..host);
+		local flag = assert(arg[1], "expected argument: flag");
+		local comment = arg[2];
+
+		local ok, err = add_flag(username, flag, comment);
+		if not ok then
+			print("Failed to add flag: "..err);
+			return 1;
+		end
+
+		print("Flag added");
+		return 1;
+	elseif command == "remove" then
+		local username = assert(node, "expected a user JID, got "..host);
+		local flag = assert(arg[1], "expected argument: flag");
+
+		local ok, err = remove_flag(username, flag);
+		if not ok then
+			print("Failed to remove flag: "..err);
+			return 1;
+		end
+
+		print("Flag removed");
+		return 1;
+	elseif command == "list" then
+		local username = assert(node, "expected a user JID, got "..host);
+
+		local c = 0;
+
+		local flags = module:open_store("account_flags");
+		local user_flags, err = flags:get(username);
+
+		if not user_flags and err then
+			print("Unable to list flags: "..err);
+			return 1;
+		end
+
+		if user_flags then
+			for flag_name, flag_data in pairs(user_flags) do
+				print(flag_name, os.date("%Y-%m-%d %R", flag_data.when), flag_data.comment or "");
+				c = c + 1;
+			end
+		end
+
+		print(("%d flags listed"):format(c));
+		return 0;
+	else
+		warn("Unknown command: %s", command);
+		return 1;
+	end
+end
--- a/mod_groups_internal/mod_groups_internal.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_groups_internal/mod_groups_internal.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -11,7 +11,7 @@
 local group_members_store = module:open_store("groups");
 local group_memberships = module:open_store("groups", "map");
 
-local muc_host_name = module:get_option("groups_muc_host", "groups."..host);
+local muc_host_name = module:get_option("groups_muc_host");
 local muc_host = nil;
 
 local is_contact_subscribed = rostermanager.is_contact_subscribed;
--- a/mod_http_libjs/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_http_libjs/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -11,7 +11,7 @@
 filesystem, allowing other HTTP modules to easily reference them.
 
 The default configuration works out of the box with Debian (and derivatives)
-`libjs-*` packages, such as libjs-jquery and libjs-bootstrap.
+`libjs-*` packages, such as `libjs-jquery` and `libjs-bootstrap`.
 
 You can override the filesystem location using the `libjs_path` configuration
 option. The default is `/usr/share/javascript`.
@@ -19,6 +19,7 @@
 Compatibility
 =============
 
-  ----- -------
-  0.11   Works
-  ----- -------
+  Prosody-Version Status
+  --------------- --------------------
+  trunk           Works as of 24-12-08
+  0.12            Works
--- a/mod_http_upload_external/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_http_upload_external/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,7 +1,7 @@
 ---
 description: HTTP File Upload (external service)
 labels:
-- Stage-Alpha
+- Stage-Beta
 ---
 
 Introduction
@@ -19,6 +19,7 @@
 * [PHP implementation](https://hg.prosody.im/prosody-modules/raw-file/tip/mod_http_upload_external/share.php)
 * [Python3+Flask implementation](https://github.com/horazont/xmpp-http-upload)
 * [Go implementation, Prosody Filer](https://github.com/ThomasLeister/prosody-filer)
+* [Go implementation, HMAC File Server](https://github.com/PlusOne/hmac-file-server)
 * [Perl implementation for nginx](https://github.com/weiss/ngx_http_upload)
 * [Rust implementation](https://gitlab.com/nyovaya/xmpp-http-upload)
 
@@ -86,7 +87,10 @@
 Compatibility
 =============
 
-Works with Prosody 0.9.x and later.
+  Prosody-Version   Status
+  ----------------  --------------------
+  trunk             Works as of 24-12-12
+  0.12              Works
 
 Implementation
 ==============
--- a/mod_invites_page/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_invites_page/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -41,6 +41,9 @@
 configurable list of XMPP clients (to configure the list, see mod_register_apps
 documentation).
 
+For a complete experience one should also load
+[mod_invites_register], [mod_invites_register_web], [mod_register_apps] and [mod_http_libjs] see [mod_invites]
+
 Configuration
 =============
 
@@ -65,3 +68,11 @@
 invitation page generator (such as [ge0rg/easy-xmpp-invitation](https://github.com/ge0rg/easy-xmpp-invitation)
 then set `invites_page_external = true` and set `invites_page` to the
 appropriate URL for your installation.
+
+Compatibility
+=============
+
+  Prosody-Version Status
+  --------------- ---------------------
+  trunk           Works as of 24-12-08
+  0.12            Works
--- a/mod_invites_register_web/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_invites_register_web/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -49,3 +49,11 @@
 loaded). As a consequence of this module being loaded, the default password
 policies will be enforced for all registrations on the server if not
 explicitly loaded or configured.
+
+Compatibility
+=============
+
+Prosody-Version Status
+--------------- ---------------------
+trunk           Works as of 24-12-08
+0.12            Works
--- a/mod_invites_tracking/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_invites_tracking/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -20,11 +20,21 @@
 Details
 =======
 
-Add to `modules_enabled`.
+Add it to `modules_enabled`.
+
+Assuming file based storage the information will be stored at your storage location under `./invites_tracking/` 
 
 Caveats
 =======
 
 - The information is not deleted even when the associated user accounts are
   deleted.
-- Currently, there is no way to make any use of that information.
+- Currently, there is no integrated way to make use of that information.
+
+Compatibility
+=============
+
+Prosody-Version Status
+--------------- ---------------------
+trunk           Works as of 24-12-08
+0.12            unknown
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_activity/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,17 @@
+---
+summary: 'XEP-0502 (MUC Activity Indicator) implementation'
+...
+
+This module provides an implementation of [XEP-0502 (MUC Activity Indicator)](https://xmpp.org/extensions/xep-0502.html) for Prosody.
+
+To enable it, load it on a MUC host, for example:
+
+```lua
+Component "chat.domain.example" "muc"
+    modules_enabled = { "muc_activity" }
+```
+
+When this module is loaded, it will expose the average number of messages per hour for all public MUCs.
+The number is calculated over a 24 hour window.
+
+Note that this module may impact server performance on servers with many MUCs.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_muc_activity/mod_muc_activity.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,87 @@
+local os_time = os.time;
+local math_floor = math.floor;
+local store = module:open_store("muc_activity", "keyval");
+
+local accumulator = {};
+
+local field_var = "{urn:xmpp:muc-activity}message-activity";
+
+module:hook("muc-room-destroyed", function (event)
+	local jid = event.room.jid;
+	module:log("debug", "deleting activity data for destroyed muc %s", jid);
+	store:set_keys(jid, {});
+	accumulator[jid] = nil;
+end)
+
+module:hook("muc-occupant-groupchat", function (event)
+	local jid = event.room.jid;
+	if not event.room:get_persistent() then
+		-- we do not count stanzas in non-persistent rooms
+		if accumulator[jid] then
+			-- if we have state for the room, drop it.
+			store:set_keys(jid, {});
+			accumulator[jid] = nil;
+		end
+
+		return
+	end
+
+	if event.stanza:get_child("body") == nil then
+		-- we do not count stanzas without body.
+		return
+	end
+
+	module:log("debug", "counting stanza for MUC activity in %s", jid);
+	accumulator[jid] = (accumulator[jid] or 0) + 1;
+end)
+
+local function shift(data)
+	for i = 1, 23 do
+		data[i] = data[i+1]
+	end
+end
+
+local function accumulate(data)
+	if data == nil then
+		return 0;
+	end
+	local accum = 0;
+	for i = 1, 24 do
+		local v = data[i];
+		if v ~= nil then
+			accum = accum + v
+		end
+	end
+	return accum;
+end
+
+module:hourly("muc-activity-shift", function ()
+	module:log("info", "shifting MUC activity store forward by one hour");
+	for jid in store:users() do
+		local data = store:get(jid);
+		local new = accumulator[jid] or 0;
+		shift(data);
+		data[24] = new;
+		accumulator[jid] = nil;
+		store:set(jid, data);
+	end
+
+	-- All remaining entries in the accumulator are non-existent in the store,
+	-- otherwise they would have been removed earlier.
+	for jid, count in pairs(accumulator) do
+		store:set(jid, { [24] = count });
+	end
+	accumulator = {};
+end)
+
+module:hook("muc-disco#info", function(event)
+	local room = event.room;
+	local jid = room.jid;
+	if not room:get_persistent() or not room:get_public() or room:get_members_only() or room:get_password() ~= nil then
+		module:log("debug", "%s is not persistent or not public, not injecting message activity", jid);
+		return;
+	end
+	local count = accumulate(store:get(jid)) / 24.0;
+	table.insert(event.form, { name = field_var, label = "Message activity" });
+	event.formdata[field_var] = tostring(count);
+end);
--- a/mod_muc_restrict_pm/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_muc_restrict_pm/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,9 +1,15 @@
 ---
 labels:
-- 'Stage-Alpha'
+- 'Stage-Obsolete'
 summary: Limit who may send and recieve MUC PMs
 ...
 
+::: {.alert .alert-warning}
+This feature has been merged into
+[mod_muc][doc:modules:mod_muc] in trunk and is therefore obsolete when used with a version >0.12.x or trunk.
+It can still be used with Prosody 0.12.
+:::
+
 # Introduction
 
 This module adds configurable MUC options that restrict and limit who may send MUC PMs to other users.
@@ -23,8 +29,7 @@
 Compatibility
 =============
 
-  ----- -----
-  0.12  Works
-  0.11  Probably does not work
-  ----- -----
-
+  version   note
+  --------- ---------------------------------------------------------------------------
+  trunk     [Integrated](https://hg.prosody.im/trunk/rev/47e1df2d0a37) into `mod_muc`
+  0.12      Works
--- a/mod_pubsub_serverinfo/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_pubsub_serverinfo/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -50,6 +50,12 @@
 
     pubsub_serverinfo_cache_ttl = 1800 -- or any other number of seconds
 
+To include the count of active (within the past 30 days) users:
+
+    pubsub_serverinfo_publish_user_count = true
+
+Enabling this option will automatically load mod_measure_active_users.
+
 Compatibility
 =============
 
--- a/mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -12,6 +12,7 @@
 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);
+local include_user_count = module:get_option_boolean(module.name.."_publish_user_count", false);
 
 if not service and prosody.hosts["pubsub."..module.host] then
 	service = "pubsub."..module.host;
@@ -21,6 +22,11 @@
 	return;
 end
 
+local metric_registry = require "core.statsmanager".get_metric_registry();
+if include_user_count then
+	module:depends("measure_active_users");
+end
+
 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
 
 -- Needed to publish server-info-fields
@@ -184,6 +190,10 @@
 	return domains_by_host
 end
 
+local function get_gauge_metric(name)
+	return (metric_registry.families[name].data:get(module.host) or {}).value;
+end
+
 function publish_serverinfo()
 	module:log("debug", "Publishing server info...");
 	local domains_by_host = get_remote_domain_names()
@@ -211,7 +221,18 @@
 		end
 	end
 
-	request:up():up()
+	request:up();
+
+	if include_user_count then
+		local mau = get_gauge_metric("prosody_mod_measure_active_users/active_users_30d");
+		request:tag("users", { xmlns = "xmpp:prosody.im/protocol/serverinfo" });
+		if mau then
+			request:text_tag("active", ("%d"):format(mau));
+		end
+		request:up();
+	end
+
+	request:up()
 
 	module:send_iq(request):next(
 		function(response)
--- a/mod_pubsub_subscription/mod_pubsub_subscription.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_pubsub_subscription/mod_pubsub_subscription.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -1,3 +1,4 @@
+local id = require "util.id";
 local st = require "util.stanza";
 local uuid = require "util.uuid";
 local mt = require "util.multitable";
@@ -37,7 +38,7 @@
 	end
 
 	item._id = uuid.generate();
-	local iq_id = uuid.generate();
+	local iq_id = "pubsub-sub-"..id.short();
 	pending_subscription:set(iq_id, item._id);
 	active_subscriptions:set(item.service, item.node, item.from, item._id, item);
 
@@ -51,12 +52,14 @@
 for _, event_name in ipairs(valid_events) do
 	module:hook("pubsub-event/host/"..event_name, function (event)
 		for _, _, _, _, _, cb in active_subscriptions:iter(event.service, event.node, event.stanza.attr.to, nil, "on_"..event_name) do
+			event.handled = true;
 			pcall(cb, event);
 		end
 	end);
 
 	module:hook("pubsub-event/bare/"..event_name, function (event)
 		for _, _, _, _, _, cb in active_subscriptions:iter(event.service, event.node, event.stanza.attr.to, nil, "on_"..event_name) do
+			event.handled = true;
 			pcall(cb, event);
 		end
 	end);
@@ -67,6 +70,7 @@
 	local service = stanza.attr.from;
 
 	if not stanza.attr.id then return end -- shouldn't be possible
+	if not stanza.attr.id:match("^pubsub%-sub%-") then return end
 
 	local subscribed_node = pending_subscription:get(stanza.attr.id);
 	pending_subscription:set(stanza.attr.id, nil);
@@ -118,7 +122,7 @@
 	local node_subs = active_subscriptions:get(item.service, item.node, item.from);
 	if node_subs and next(node_subs) then return end
 
-	local iq_id = uuid.generate();
+	local iq_id = "pubsub-sub-"..id.short();
 	pending_unsubscription:set(iq_id, item._id);
 
 	module:send(st.iq({ type = "set", id = iq_id, from = item.from, to = item.service })
@@ -130,24 +134,33 @@
 
 function handle_message(context, event)
 	local origin, stanza = event.origin, event.stanza;
-	local ret = nil;
+	local handled = nil;
 	local service = stanza.attr.from;
 	module:log("debug", "Got message/%s: %s", context, stanza:top_tag());
 	for event_container in stanza:childtags("event", xmlns_pubsub_event) do
 		for pubsub_event in event_container:childtags() do
 			module:log("debug", "Got pubsub event %s", pubsub_event:top_tag());
 			local node = pubsub_event.attr.node;
-			module:fire_event("pubsub-event/" .. context .. "/"..pubsub_event.name, {
-					stanza = stanza;
-					origin = origin;
-					event = pubsub_event;
-					service = service;
-					node = node;
-				});
-			ret = true;
+			local event_data = {
+				stanza = stanza;
+				origin = origin;
+				event = pubsub_event;
+				service = service;
+				node = node;
+				handled = false;
+			};
+			module:fire_event("pubsub-event/" .. context .. "/"..pubsub_event.name, event_data);
+			if not handled and event_data.handled then
+				handled = true;
+			end
 		end
 	end
-	return ret;
+	-- If not addressed to the host, let it fall through to normal handling
+	-- (it may be on its way to a local client), otherwise, we'll mark the
+	-- event as handled to suppress an error response if we handled it.
+	if context == "host" and handled then
+		return true;
+	end
 end
 
 module:hook("message/host", function(event)
--- a/mod_push2/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_push2/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,13 +1,13 @@
 ---
 labels:
 - Stage-Alpha
-summary: 'Push 2.0'
+summary: 'Push 2.0 - New Cloud-Notify'
 ---
 
 The way forward for push notifications?  You are probably looking for
 `mod_cloud_notify` for now though
 
-See also https://hg.prosody.im/prosody-modules/file/tip/mod_push2/push2.markdown
+See also [push2.md](https://hg.prosody.im/prosody-modules/file/tip/mod_push2/push2.md)
 
 Configuration
 =============
@@ -41,6 +41,7 @@
 
 **Note:** This module should be used with Lua 5.3 and higher.
 
------- -----------------------------------------------------------------------------
-  trunk  Works
------- -----------------------------------------------------------------------------
+  ----- ----------------------
+  trunk Works
+  0.12  Does probably not work 
+  ----- ----------------------
--- a/mod_register_apps/mod_register_apps.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_register_apps/mod_register_apps.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -54,7 +54,7 @@
 		download = {
 			buttons = {
 				{
-					image = "https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2017-05-31&kind=iossoftware&bubble=ios_apps";
+					image = "https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1245024000";
 					url = "https://apps.apple.com/us/app/siskin-im/id1153516838";
 					target = "_blank";
 				};
@@ -120,7 +120,7 @@
 		download = {
 			buttons = {
 				{
-					image = "https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2017-05-31&kind=iossoftware&bubble=ios_apps";
+					image = "https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1245024000";
 					url = "https://apps.apple.com/app/id317711500";
 					target = "_blank";
 				};
@@ -137,7 +137,7 @@
 		download = {
 			buttons = {
 				{
-					image = "https://linkmaker.itunes.apple.com/en-us/badge-lrg.svg?releaseDate=2017-05-31&kind=macossoftware&bubble=macos_apps";
+					image = "https://toolbox.marketingtools.apple.com/api/v2/badges/download-on-the-app-store/black/en-us?releaseDate=1245024000";
 					url = "https://apps.apple.com/app/id1637078500";
 					target = "_blank";
 				};
--- a/mod_register_dnsbl_firewall_mark/mod_register_dnsbl_firewall_mark.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_register_dnsbl_firewall_mark/mod_register_dnsbl_firewall_mark.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -17,6 +17,8 @@
 	return ("%d.%d.%d.%d.%s"):format(d,c,b,a, suffix);
 end
 
+local store = module:open_store("firewall_marks", "map");
+
 module:hook("user-registered", function (event)
 	local session = event.session;
 	local ip = session and session.ip and cleanup_ip(session.ip);
@@ -31,7 +33,7 @@
 				if user and user.firewall_marks then
 					user.firewall_marks.dnsbl_hit = registration_time;
 				else
-					module:open_store("firewall_marks", "map"):set(event.username, "dnsbl_hit", registration_time);
+					store:set(event.username, "dnsbl_hit", registration_time);
 				end
 				if rbl_message then
 					module:log("debug", "Warning RBL registered user %s@%s", event.username, event.host);
@@ -45,3 +47,13 @@
 		end, rbl_ip);
 	end
 end);
+
+module:add_item("account-trait", {
+	name = "register-dnsbl-hit";
+	prob_bad_true = 0.6;
+	prob_bad_false = 0.4;
+});
+
+module:hook("get-account-traits", function (event)
+	event.traits["register-dnsbl-hit"] = not not store:get(event.username, "dnsbl_hit");
+end);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_report_affiliations/README.markdown	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,108 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: 'XEP-0489: Reporting Account Affiliations'
+rockspec:
+  build:
+    modules:
+      mod_report_affiliations.traits: traits.lib.lua
+---
+
+
+This module implements [XEP-0489: Reporting Account Affiliations](https://xmpp.org/extensions/xep-0489.html).
+It can help with spam on the network, especially if you run a public server
+that allows registration.
+
+## How it works
+
+Here is the scenario: you run a public server. Despite your best efforts, and
+following the [best practices](https://prosody.im/doc/public_servers), some
+spammers still occasionally manage to register on your server. Because of
+this, other servers on the network start filtering messages from all accounts
+on your server.
+
+Enabling this module will include additional information in certain kinds of
+outgoing traffic, which allows other servers to judge the sending account,
+rather than the whole server.
+
+### When is affiliation information shared?
+
+Affiliation is shared when a user on your server:
+
+- sends a message to a user that has not (yet) authorized them
+- sends a subscription request to a user
+- sends a "directed presence" to a remote JID (for example, when joining a
+  group chat).
+
+### What information is shared?
+
+The following information is included in matching traffic:
+
+- The affiliation of the account:
+  - "guest" (the account is anonymous/temporary)
+  - "registered" (the account was self-registered)
+  - "member" (the account belongs to a recognised/trusted member of the server)
+  - "admin" (the account belongs to a server administrator)
+
+For the "registered" affiliation, the following additional items are included:
+
+- When the account was created
+- The "trust level" of the account
+
+### What is the trust level?
+
+This is a score out of 100 which indicates how trusted the account is. It is
+automatically calculated, and the calculation may include various factors
+provided by installed modules. At this time, in a default installation, the
+reported value is always 50.
+
+## Configuration
+
+### Allowing queries
+
+In most cases, Prosody will automatically include the affiliation information
+when necessary. However it is also possible to provide affiliation on-demand,
+in response to queries.
+
+To avoid leaking information about the server's registered users, queries are
+restricted by default.
+
+You can configure a list of servers from which queries are permitted, by using
+the 'report_affiliations_trusted_servers' option:
+
+```lua
+report_affiliations_trusted_servers = { "rtbl.example.net" }
+```
+
+In this example, permission has been granted to an RTBL service, so that it
+can query the server and avoid adding legitimate users to the blocklist, even
+if it receives reports about them (obviously this is just an example, RTBLs
+will decide their own policies).
+
+### Tweaking roles
+
+Prosody automatically maps its standard roles to the affiliations defined by
+the XEP. If your deployment uses custom roles, you can customize the mapping
+by specifying the list of roles that should be mapped to a given affiliation.
+This can be done using the following options:
+
+- report_affiliations_admin_roles
+- report_affiliations_member_roles
+- report_affiliations_registered_roles
+- report_affiliations_anonymous_roles
+
+For example, to consider the 'company:staff' role as members, as well as the
+built-in prosody:member role, you might set the following:
+
+```lua
+report_affiliations_member_roles = { "prosody:member", "company:staff" }
+```
+
+## Compatibility
+
+Should work with 0.12, but has not been tested. 0.12 does not support the
+"member" role, so all non-anonymous/non-admin accounts will be reported as
+"registered".
+
+Tested with trunk (2024-11-22).
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_report_affiliations/mod_report_affiliations.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,166 @@
+local dt = require "util.datetime";
+local jid = require "util.jid";
+local st = require "util.stanza";
+
+local rm = require "core.rostermanager";
+local um = require "core.usermanager";
+
+local traits = module:require("traits");
+
+local xmlns_aff = "urn:xmpp:raa:0";
+
+local is_host_anonymous = module:get_option_string("authentication") == "anonymous";
+
+local trusted_servers = module:get_option_inherited_set("report_affiliations_trusted_servers", {});
+
+local roles = {
+	-- These affiliations are defined by XEP-0489, and we map a set of Prosody roles to each one
+	admin = module:get_option_set("report_affiliations_admin_roles", { "prosody:admin", "prosody:operator" });
+	member = module:get_option_set("report_affiliations_member_roles", { "prosody:member" });
+	registered = module:get_option_set("report_affiliations_registered", { "prosody:user", "prosody:registered" });
+	guest = module:get_option_set("report_affiliations_anonymous", { "prosody:guest" });
+};
+
+-- Map of role to affiliation
+local role_affs = {
+	-- [role (e.g. "prosody:guest")] = affiliation ("admin"|"member"|"registered"|"guest");
+};
+
+--Build the role->affiliation map based on the config
+for aff, aff_roles in pairs(roles) do
+	for role in aff_roles do
+		role_affs[role] = aff;
+	end
+end
+
+local account_details_store = module:open_store("account_details");
+local lastlog2_store = module:open_store("lastlog2");
+
+module:add_feature(xmlns_aff);
+module:add_feature(xmlns_aff.."#embed-presence-sub");
+module:add_feature(xmlns_aff.."#embed-presence-directed");
+
+local function get_registered_timestamp(username)
+	if um.get_account_info then
+		local ts = um.get_account_info(username, module.host);
+		if ts then return ts.created; end
+	end
+
+	local account_details = account_details_store:get(username);
+	if account_details and account_details.registered then
+		return account_details.registered;
+	end
+
+	local lastlog2 = lastlog2_store:get(username);
+	if lastlog2 and lastlog2.registered then
+		return lastlog2.registered.timestamp;
+	end
+
+	return nil;
+end
+
+local function get_trust_score(username)
+	return math.floor(100 * (1 - traits.get_probability_bad(username)));
+end
+
+
+local function get_account_type(username)
+	if is_host_anonymous then
+		return "anonymous";
+	end
+
+	if not um.get_user_role then
+		return "registered"; -- COMPAT w/0.12
+	end
+
+	local user_role = um.get_user_role(username, module.host);
+
+	return role_affs[user_role] or "registered";
+end
+
+function get_info_element(username)
+	local account_type = get_account_type(username);
+
+	local since, trust;
+
+	if account_type == "registered" then
+		since = get_registered_timestamp(username);
+		trust = get_trust_score(username);
+	end
+
+	return st.stanza("info", {
+		affiliation = account_type;
+		since = since and dt.datetime(since - (since%86400)) or nil;
+		trust = ("%d"):format(trust);
+		xmlns = xmlns_aff;
+	});
+end
+
+-- Outgoing presence
+
+local function embed_in_outgoing_presence(pres_type)
+	return function (event)
+		local origin, stanza = event.origin, event.stanza;
+
+		stanza:remove_children("info", xmlns_aff);
+
+		-- Unavailable presence is pretty harmless, and blocking it may cause
+		-- weird issues.
+		if (pres_type == "bare" and stanza.attr.type == "unavailable")
+		or (pres_type == "full" and stanza.attr.type ~= nil) then
+			return;
+		end
+
+		-- Only attach info to stanzas sent to "strangers" (users that have not
+		-- approved us to see their presence)
+		if rm.is_user_subscribed(origin.username, origin.host, stanza.attr.to) then
+			return;
+		end
+
+		local info = get_info_element(origin.username);
+		if not info then return; end
+
+		stanza:add_direct_child(info);
+	end;
+end
+
+module:hook("pre-presence/bare", embed_in_outgoing_presence("bare"));
+module:hook("pre-presence/full", embed_in_outgoing_presence("full"));
+
+-- Handle direct queries
+
+local function should_permit_query(from_jid, to_username) --luacheck: ignore 212/to_username
+	local from_node, from_host = jid.split(from_jid);
+	if from_node then
+		return false;
+	end
+
+	-- Who should we respond to?
+	-- Only respond to domains
+	-- Does user have a JID with this domain in directed presence? (doesn't work with bare JIDs)
+	-- Does this user have a JID with domain in pending subscription requests?
+
+	if trusted_servers:contains(from_host) then
+		return true;
+	end
+
+	return false;
+end
+
+module:hook("iq-get/bare/urn:xmpp:raa:0:query", function (event)
+	local origin, stanza = event.origin, event.stanza;
+	local username = jid.node(stanza.attr.to);
+
+	if not should_permit_query(stanza.attr.from, username) then
+		origin.send(st.error_reply(stanza, "auth", "forbidden"));
+		return true;
+	end
+
+	local info = get_info_element(username);
+
+	local reply = st.reply(stanza)
+		:add_child(info);
+	origin.send(reply);
+
+	return true;
+end);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_report_affiliations/traits.lib.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,58 @@
+local known_traits = {};
+
+local function trait_added(event)
+	local trait = event.item;
+	local name = trait.name;
+	if known_traits[name] then return; end
+
+	known_traits[name] = trait.probabilities;
+end
+
+local function trait_removed(event)
+	local trait = event.item;
+	known_traits[trait.name] = nil;
+end
+
+module:handle_items("account-trait", trait_added, trait_removed);
+
+local function bayes_probability(prior, prob_given_true, prob_given_false)
+	local numerator = prob_given_true * prior;
+	local denominator = numerator + prob_given_false * (1 - prior);
+	return numerator / denominator;
+end
+
+local function prob_is_bad(traits, prior)
+	prior = prior or 0.50;
+
+	for trait, state in pairs(traits) do
+		local probabilities = known_traits[trait];
+		if probabilities then
+			if state then
+				prior = bayes_probability(
+					prior,
+					probabilities.prob_bad_true,
+					probabilities.prob_bad_false
+				);
+			else
+				prior = bayes_probability(
+					prior,
+					1 - probabilities.prob_bad_true,
+					1 - probabilities.prob_bad_false
+				);
+			end
+		end
+	end
+
+	return prior;
+end
+
+local function get_probability_bad(username, prior)
+	local user_traits = {};
+	module:fire_event("get-account-traits", { username = username, host = module.host, traits = user_traits });
+	local result = prob_is_bad(user_traits, prior);
+	return result;
+end
+
+return {
+	get_probability_bad = get_probability_bad;
+};
--- a/mod_report_forward/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_report_forward/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -23,7 +23,7 @@
 list of JIDs to send all reports to (default is empty):
 
 ```lua
-report_forward_to = { "antispam.example.com" }
+report_forward_to = { "admin@example.net", "antispam.example2.com" }
 ```
 
 You can also control whether the module sends a report to the server from
@@ -35,7 +35,9 @@
 
 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.
+fall back to sending the report to the domain itself unless `report_forward_to_origin_fallback`
+is disabled (set to `false`). If the fallback is disabled, it will log a
+warning and not send the report.
 
 
 
@@ -75,3 +77,10 @@
   </forwarded>
 </message>
 ```
+
+## Compability
+
+  Prosody-Version   Status
+  ----------------- ----------------------
+  trunk             Works as of 07.12.22
+  0.12              Works
--- a/mod_report_forward/mod_report_forward.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_report_forward/mod_report_forward.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -14,6 +14,7 @@
 
 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 report_to_origin_fallback = module:get_option_boolean("report_forward_to_origin_fallback", 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", [[
@@ -30,7 +31,7 @@
 --
 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
+to a different address by setting 'report-addresses' in your server
 contact info configuration. For more information, see https://xmppbl.org/reports/
 ]]):gsub("^%s+", ""):gsub("(%S)\n(%S)", "%1 %2");
 
@@ -65,18 +66,27 @@
 		: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;
+			if response.attr.type == "result" then
+				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, "report-addresses", "abuse-addresses");
+						break;
+					end
+				end
 			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;
+			if not address then
+				if report_to_origin_fallback then
+					-- If no contact address found, but fallback is enabled,
+					-- just send the report to the domain
+					module:log("debug", "Falling back to domain to send report to %s", host);
+					address = host;
+				else
+					module:log("warn", "Failed to query contact addresses of %s: %s", host, response);
 				end
 			end
+
 			return address;
 		end);
 end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_report_tracker/README.markdown	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,32 @@
+---
+labels:
+- 'Stage-Alpha'
+summary: 'Track abuse/spam reports from remote servers'
+---
+
+This module tracks reports received from remote servers about local user
+accounts. The count of reports and the servers they came from is stored for
+inspection by the admin or for use by other modules which might take action
+against the reported accounts.
+
+## Configuration
+
+### Trusted reporters
+
+You can configure which servers the module will trust reports from:
+
+```
+trusted_reporters = { "example.com", "example.net" }
+```
+
+Reports from non-domain JIDs are currently always ignored (even if listed).
+
+Reports from domain JIDs which are not listed here are logged so the admin
+can decide whether to add them to the configured list.
+
+## Compatibility
+
+Should work with 0.12, but has not been tested.
+
+Tested with trunk (2024-11-22).
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_report_tracker/mod_report_tracker.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,93 @@
+local um = require "core.usermanager";
+local cache = require "util.cache";
+local jid = require "util.jid";
+local trusted_reporters = module:get_option_inherited_set("trusted_reporters", {});
+
+local reports_received = module:open_store("reports_received");
+
+local xmlns_reporting = "urn:xmpp:reporting:1";
+
+local reported_users = cache.new(256);
+
+local function is_trusted_reporter(reporter_jid)
+	return trusted_reporters:contains(reporter_jid);
+end
+
+function handle_report(event)
+	local stanza = event.stanza;
+	local report = stanza:get_child("report", xmlns_reporting);
+	if not report then
+		return;
+	end
+	local reported_jid = report:get_child_text("jid", "urn:xmpp:jid:0")
+		or stanza:find("{urn:xmpp:forward:0}forwarded/{jabber:client}message@from");
+	if not reported_jid then
+		module:log("debug", "Discarding report with no JID");
+		return;
+	elseif jid.host(reported_jid) ~= module.host then
+		module:log("debug", "Discarding report about non-local user");
+		return;
+	end
+
+	local reporter_jid = stanza.attr.from;
+	if jid.node(reporter_jid) then
+		module:log("debug", "Discarding report from non-server JID");
+		return;
+	end
+
+	local reported_user = jid.node(reported_jid);
+	if not um.user_exists(reported_user, module.host) then
+		module:log("debug", "Discarding report about non-existent user");
+		return;
+	end
+
+	if is_trusted_reporter(reporter_jid) then
+		local current_reports = reports_received:get(reported_user, reporter_jid);
+
+		if not current_reports then
+			current_reports = {
+				first = os.time();
+				last = os.time();
+				count = 1;
+			};
+		else
+			current_reports.last = os.time();
+			current_reports.count = current_reports.count + 1;
+		end
+
+		reports_received:set(reported_user, reporter_jid, current_reports);
+		reported_users:set(reported_user, true);
+
+		module:log("info", "Received abuse report about <%s> from <%s>", reported_jid, reporter_jid);
+
+		module:fire_event(module.name.."/account-reported", {
+			report_from = reporter_jid;
+			reported_user = reported_user;
+			report = report;
+		});
+	else
+		module:log("warn", "Discarding abuse report about <%s> from untrusted source <%s>", reported_jid, reporter_jid);
+	end
+
+	-- Message was handled
+	return true;
+end
+
+module:hook("message/host", handle_report);
+
+module:add_item("account-trait", {
+	name = "reported-by-trusted-server";
+	prob_bad_true = 0.80;
+	prob_bad_false = 0.50;
+});
+
+module:hook("get-account-traits", function (event)
+	local username = event.username;
+	local reported = reported_users:get(username);
+	if reported == nil then
+		-- Check storage, update cache
+		reported = not not reports_received:get(username);
+		reported_users:set(username, reported);
+	end
+	event.traits["reported-by-trusted-server"] = reported;
+end);
--- a/mod_roster_allinall/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_roster_allinall/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,5 +1,7 @@
 ---
 labels:
+- 'Stage-Beta'
+summary: 'Add everyone to everyones roster on the server'
 ...
 
 Introduction
@@ -19,3 +21,11 @@
 
 Just add it to the modules\_enabled, after that there is no further
 configuration.
+
+Compatibility
+=============
+
+  version   note
+  --------- ----------------------
+  trunk     Works as of 07.12.22
+  0.12      Works
--- a/mod_sasl2/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -4,10 +4,7 @@
 summary: "XEP-0388: Extensible SASL Profile"
 ---
 
-Implementation of [XEP-0388: Extensible SASL Profile]. **Note: At the time of
-writing (Nov 2022) the version of the XEP implemented by this module is still
-working its way through the XSF standards process. See [PR #1214](https://github.com/xsf/xeps/pull/1214)
-for the current status.**
+Implementation of [XEP-0388: Extensible SASL Profile]. 
 
 ## Configuration
 
@@ -31,8 +28,9 @@
 
 This module requires Prosody **trunk** and is not compatible with 0.12 or older versions.
 
-  ------- ---------------
-  trunk   Works
-  0.12    Does not work
-  0.11    Does not work
-  ------- ---------------
+
+     Prosody Version           Status
+  -----------------------  ----------------
+  trunk as of 2024-11-24   Works
+  0.12                     Does not work
+  -----------------------  ----------------
--- a/mod_sasl2_bind2/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2_bind2/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -8,9 +8,13 @@
 ---
 
 Add support for [XEP-0386: Bind 2], which is a new method for clients to bind
-resources and establish sessions in XMPP, using SASL2. **Note: At the time of
-writing (November 2022), this plugin implements a version of XEP-0386 that is
-still working its way through the XSF standards process. See [PR #1217](https://github.com/xsf/xeps/pull/1217)
-for more information and current status.**
+resources and establish sessions in XMPP, using SASL2. 
 
 This module depends on [mod_sasl2]. It exposes no configuration options.
+
+# Compatibility
+
+  Prosody-Version Status
+  --------------- ----------------------
+  trunk           Works as of 2024-12-21
+  0.12            Does not work
--- a/mod_sasl2_fast/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2_fast/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -16,8 +16,8 @@
 
 | Name                      | Description                                            | Default               |
 |---------------------------|--------------------------------------------------------|-----------------------|
-| sasl2_fast_token_ttl      | Default token expiry (seconds)                         | `86400*21` (21 days)  |
-| sasl2_fast_token_min_ttl  | Time before tokens are eligible for rotation (seconds) | `86400` (1 day)       |
+| sasl2_fast_token_ttl      | Default token expiry (seconds)                         | 86400*21 (21 days)  |
+| sasl2_fast_token_min_ttl  | Time before tokens are eligible for rotation (seconds) | 86400 (1 day)       |
 
 The `sasl2_fast_token_ttl` option determines the length of time a client can
 remain disconnected before being "logged out" and needing to authenticate with
@@ -28,3 +28,10 @@
 rotated by the server. By default a token is rotated if it is older than 24
 hours. This value should be less than `sasl2_fast_token_ttl` to prevent
 clients being logged out unexpectedly.
+
+# Compatibility
+
+  Prosody-Version Status
+  --------------- ----------------------
+  trunk           Works as of 2024-12-21
+  0.12            Does not work
--- a/mod_sasl2_fast/mod_sasl2_fast.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2_fast/mod_sasl2_fast.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -8,6 +8,11 @@
 local now = require "util.time".now;
 local hash = require "util.hashes";
 
+local sasl_mt = getmetatable(sasl.new("", { mechanisms = {} }));
+local function is_util_sasl(sasl_handler)
+	return getmetatable(sasl_handler) == sasl_mt;
+end
+
 module:depends("sasl2");
 
 -- Tokens expire after 21 days by default
@@ -113,9 +118,11 @@
 	local sasl_handler = get_sasl_handler(username);
 	if not sasl_handler then return; end
 	sasl_handler.fast_auth = true; -- For informational purposes
-	-- Copy channel binding info from primary SASL handler
-	sasl_handler.profile.cb = session.sasl_handler.profile.cb;
-	sasl_handler.userdata = session.sasl_handler.userdata;
+	-- Copy channel binding info from primary SASL handler if it's compatible
+	if is_util_sasl(session.sasl_handler) then
+		sasl_handler.profile.cb = session.sasl_handler.profile.cb;
+		sasl_handler.userdata = session.sasl_handler.userdata;
+	end
 	-- Store this handler, in case we later want to use it for authenticating
 	session.fast_sasl_handler = sasl_handler;
 	local fast = st.stanza("fast", { xmlns = xmlns_fast });
--- a/mod_sasl2_sm/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2_sm/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -5,13 +5,17 @@
 rockspec:
   dependencies:
   - mod_sasl2
+  - mod_sasl2_bind2
 ---
 
-Add support for inlining stream management negotiation into the SASL2 process.
-
-**Note: At the time of writing (November 2022), this module implements a
-version of XEP-0198 that is still working its way through the XSF standards
-process. For more information and current status, see [PR #1215](https://github.com/xsf/xeps/pull/1215).**
+Add support for inlining stream management negotiation into the SASL2 process. (See [XEP-0388: Extensible SASL Profile])
 
 This module depends on [mod_sasl2] and [mod_sasl2_bind2]. It exposes no
 configuration options.
+
+# Compatibility
+
+  Prosody-Version Status
+  --------------- ----------------------
+  trunk           Works as of 2024-12-21
+  0.12            Does not work
--- a/mod_sasl2_sm/mod_sasl2_sm.lua	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_sasl2_sm/mod_sasl2_sm.lua	Sun Jan 05 17:50:02 2025 +0100
@@ -6,6 +6,7 @@
 local xmlns_sm = "urn:xmpp:sm:3";
 
 module:depends("sasl2");
+module:depends("sasl2_bind2");
 
 -- Advertise what we can do
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_sslv3_warn/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,9 @@
+---
+summary: 'Warn clients that try sslv3'
+labels:
+- 'Stage-Obsolete'
+...
+
+::: {.alert .alert-warning}
+Prosody does not allow any sslv3 connection anymore, making this module obsolete.
+:::
--- a/mod_stanzadebug/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_stanzadebug/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,9 +1,25 @@
 ---
 summary: Extra verbose stanza logging
----
+labels:
+- Stage-Merged
+- Type-Logging
+...
+
+::: {.alert .alert-info}
+This module has been merged into Prosody as [mod_stanza_debug][doc:modules:mod_stanza_debug].
+:::
 
 Summary
 =======
 
 This module logs the full stanzas that are sent and received into debug
 logs, for debugging purposes.
+
+
+Compatibility
+=============
+
+Prosody Version  Status
+---------------  ------
+trunk            Merged
+0.12             Merged
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_statistics/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -0,0 +1,18 @@
+---
+labels:
+- Stage-Broken
+- Statistics
+summary: A rough 'prosodyctl mod_statistics top'
+...
+
+::: {.alert .alert-warning}
+This module was an early protoype and is [not compatible with prosody anymore.](https://issues.prosody.im/647)
+:::
+
+# Compatibility
+
+  version  status
+  -------- -------
+  trunk    broken
+  0.12     broken
+  0.11     works
--- a/mod_storage_memory/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_storage_memory/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -6,6 +6,13 @@
 summary: 'Simple memory-only storage module'
 ...
 
+
+::: {.alert .alert-warning}
+This module has been merged into
+[prosodys storage backend as "memory"][doc:storage] and is therefore obsolete.
+You will be redirected shortly.
+:::
+
 Introduction
 ============
 
--- a/mod_storage_memory/mod_storage_memory.lua	Wed Nov 20 05:07:11 2024 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,178 +0,0 @@
-local serialize = require "util.serialization".serialize;
-local envload = require "util.envload".envload;
-local st = require "util.stanza";
-local is_stanza = st.is_stanza or function (s) return getmetatable(s) == st.stanza_mt end
-
-local auto_purge_enabled = module:get_option_boolean("storage_memory_temporary", false);
-local auto_purge_stores = module:get_option_set("storage_memory_temporary_stores", {});
-
-local memory = setmetatable({}, {
-	__index = function(t, k)
-		local store = module:shared(k)
-		t[k] = store;
-		return store;
-	end
-});
-
-local function NULL() return nil end
-
-local function _purge_store(self, username)
-	self.store[username or NULL] = nil;
-	return true;
-end
-
-local keyval_store = {};
-keyval_store.__index = keyval_store;
-
-function keyval_store:get(username)
-	return (self.store[username or NULL] or NULL)();
-end
-
-function keyval_store:set(username, data)
-	if data ~= nil then
-		data = envload("return "..serialize(data), "@data", {});
-	end
-	self.store[username or NULL] = data;
-	return true;
-end
-
-keyval_store.purge = _purge_store;
-
-local archive_store = {};
-archive_store.__index = archive_store;
-
-function archive_store:append(username, key, value, when, with)
-	if type(when) ~= "number" then
-		when, with, value = value, when, with;
-	end
-	if is_stanza(value) then
-		value = st.preserialize(value);
-		value = envload("return xml"..serialize(value), "@stanza", { xml = st.deserialize })
-	else
-		value = envload("return "..serialize(value), "@data", {});
-	end
-	local a = self.store[username or NULL];
-	if not a then
-		a = {};
-		self.store[username or NULL] = a;
-	end
-	local i = #a+1;
-	local v = { key = key, when = when, with = with, value = value };
-	if not key then
-		key = tostring(a):match"%x+$"..tostring(v):match"%x+$";
-		v.key = key;
-	end
-	if a[key] then
-		table.remove(a, a[key]);
-	end
-	a[i] = v;
-	a[key] = i;
-	return key;
-end
-
-local function archive_iter (a, start, stop, step, limit, when_start, when_end, match_with)
-	local item, when, with;
-	local count = 0;
-	coroutine.yield(true); -- Ready
-	for i = start, stop, step do
-		item = a[i];
-		when, with = item.when, item.with;
-		if when >= when_start and when_end >= when and (not match_with or match_with == with) then
-			coroutine.yield(item.key, item.value(), when, with);
-			count = count + 1;
-			if limit and count >= limit then return end
-		end
-	end
-end
-
-function archive_store:find(username, query)
-	local a = self.store[username or NULL] or {};
-	local start, stop, step = 1, #a, 1;
-	local qstart, qend, qwith = -math.huge, math.huge;
-	local limit;
-	if query then
-		module:log("debug", "query included")
-		if query.reverse then
-			start, stop, step = stop, start, -1;
-			if query.before then
-				start = a[query.before];
-			end
-		elseif query.after then
-			start = a[query.after];
-		end
-		limit = query.limit;
-		qstart = query.start or qstart;
-		qend = query["end"] or qend;
-		qwith = query.with;
-	end
-	if not start then return nil, "invalid-key"; end
-	local iter = coroutine.wrap(archive_iter);
-	iter(a, start, stop, step, limit, qstart, qend, qwith);
-	return iter;
-end
-
-function archive_store:delete(username, query)
-	if not query or next(query) == nil then
-		self.store[username or NULL] = nil;
-		return true;
-	end
-	local old = self.store[username or NULL];
-	if not old then return true; end
-	local qstart = query.start or -math.huge;
-	local qend = query["end"] or math.huge;
-	local qwith = query.with;
-	local new = {};
-	self.store[username or NULL] = new;
-	local t;
-	for i = 1, #old do
-		i = old[i];
-		t = i.when;
-		if not(qstart >= t and qend <= t and (not qwith or i.with == qwith)) then
-			self:append(username, i.key, i.value(), t, i.with);
-		end
-	end
-	if #new == 0 then
-		self.store[username or NULL] = nil;
-	end
-	return true;
-end
-
-archive_store.purge = _purge_store;
-
-local stores = {
-	keyval = keyval_store;
-	archive = archive_store;
-}
-
-local driver = {};
-
-function driver:open(store, typ) -- luacheck: ignore 212/self
-	local store_mt = stores[typ or "keyval"];
-	if store_mt then
-		return setmetatable({ store = memory[store] }, store_mt);
-	end
-	return nil, "unsupported-store";
-end
-
-if auto_purge_enabled then
-	module:hook("resource-unbind", function (event)
-		local user_bare_jid = event.session.username.."@"..event.session.host;
-		if not prosody.bare_sessions[user_bare_jid] then -- User went offline
-			module:log("debug", "Clearing store for offline user %s", user_bare_jid);
-			local f, s, v;
-			if auto_purge_stores:empty() then
-				f, s, v = pairs(memory);
-			else
-				f, s, v = auto_purge_stores:items();
-			end
-
-			for store_name in f, s, v do
-				if memory[store_name] then
-					memory[store_name][event.session.username] = nil;
-				end
-			end
-		end
-	end);
-end
-
-module:provides("storage", driver);
--- a/mod_swedishchef/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_swedishchef/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,14 +1,13 @@
 ---
 labels:
-- 'Stage-Beta'
-summary: 'Silly little module to convert your conversations to "swedish"'
+- Stage-Beta
+summary: Silly little module to convert your conversations to "swedish"
 ...
 
 Introduction
 ============
 
-This module does some conversions on message bodys passed through it
-causing them to look like our beloved swedish chef had typed them.
+This module does some conversions on message bodys passed through it causing them to look like our beloved swedish chef had typed them.
 
 Details
 =======
@@ -19,16 +18,15 @@
         modules_enabled = { "swedishchef" }
         swedishchef_trigger = "!chef"; -- optional, converts only when the message starts with "!chef"
 
-In theory this also works for whole servers, but that is untested and
-not recommended ;)
+This also works for whole servers, it is not recommended ;)
 
 Compatibility
 =============
 
-  ----- -------
-  0.6   Works
-  0.5   Works
-  ----- -------
+Prosody-Version Status
+--------------- --------------------
+trunk           Works as of 24-12-20
+0.12            Works
 
 Todo
 ====
--- a/mod_throttle_presence/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_throttle_presence/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -4,6 +4,10 @@
 summary: Limit presence stanzas to save traffic
 ...
 
+::: {.alert .alert-info} 
+This module violates the xmpp protocol by discarding stanzas, some people reportet issues with it. In reality the bandwith saving is not relevant for most poeple, consider using [mod_csi_simple][doc:modules:mod_csi_simple] that is incuded in prosody since Version 0.11.
+:::
+
 Introduction
 ============
 
--- a/mod_unified_push/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_unified_push/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,7 +1,7 @@
 ---
 labels:
 - Stage-Alpha
-summary: "Unified Push provider"
+summary: Unified Push provider
 ---
 
 This module implements a [Unified Push](https://unifiedpush.org/) Provider
@@ -72,28 +72,29 @@
 `openssl rand -base64 32`. Changing the secret will invalidate all existing
 push registrations.
 
-### HTTP configuration
+## HTTP configuration
 
-This module exposes a HTTP endpoint (to receive push notifications from app
-servers). For more information on configuring HTTP services in Prosody, see
+This module exposes a HTTP endpoint, by default at the path `/push` (to receive push notifications from app
+servers). **If you use a reverse proxy, make sure you proxy this path too.**
+For more information on configuring HTTP services and reverse proxying in Prosody, see
 [Prosody HTTP documentation](https://prosody.im/doc/http).
 
-#### Example configuration
+## Example configuration
 
-##### Normal method
+### Recommended: load on Virtualhost(s)
 
 Just add just add `"unified_push"` to your `modules_enabled` option.
 This is the easiest and **recommended** configuration.
 
-``` {.lua}
+``` lua
   modules_enabled = {
-    ---
+    -- ...
     "unified_push";
-    ---
+    -- ...
   }
 ```
 
-##### Component method
+#### Component method
 
 This is an example of how to configure the module as an internal component,
 e.g. on a subdomain or other non-user domain.
@@ -105,7 +106,7 @@
 on the 'example.com' host, which avoids needing to create/update DNS records
 and HTTPS certificates if example.com is already set up.
 
-``` {.lua}
+``` lua
 Component "notify.example.com" "unified_push"
     unified_push_secret = "<secret string here>"
     http_host = "example.com"
@@ -113,5 +114,7 @@
 
 ## Compatibility
 
-| trunk | Works |
-| 0.12  | Works |
+  Prosody-Version   Status
+  ----------------- ----------------------
+  trunk             Works as of 24-12-08
+  0.12              Works
--- a/mod_warn_legacy_tls/README.md	Wed Nov 20 05:07:11 2024 +0100
+++ b/mod_warn_legacy_tls/README.md	Sun Jan 05 17:50:02 2025 +0100
@@ -1,5 +1,13 @@
-TLS 1.0 and TLS 1.1 are about to be obsolete. This module warns clients
-if they are using those versions, to prepare for disabling them.
+---
+labels:
+- Stage-Alpha
+summary: Warn users of obsolete TLS Versions in clients
+---
+
+
+TLS 1.0 and TLS 1.1 are obsolete. This module warns clients if they are using those versions, to prepare for disabling them. (If you use the default prosody config, this module will be unnessesary in its default setting, since these protocols are not allowed anymore by any supported prosody version.)
+
+This module can be used to warn from TLS1.2 if you want to switch to modern security in the near future.
 
 # Configuration
 
@@ -15,6 +23,10 @@
 legacy_tls_warning = [[
 Your connection is encrypted using the %s protocol, which has been demonstrated to be insecure and will be disabled soon.  Please upgrade your client.
 ]]
+
+--You may want to warn about TLS1.2 these days too (This note added 2024), by default prosody will not even allow connections from TLS <1.2
+--Example:
+legacy_tls_versions = { "TLSv1", "TLSv1.1", "TLSv1.2" }
 ```
 
 ## Options
@@ -26,3 +38,10 @@
 `legacy_tls_versions`
 :   Set of TLS versions, defaults to
     `{ "SSLv3", "TLSv1", "TLSv1.1" }`{.lua}, i.e. TLS \< 1.2.
+
+# Compatibility
+
+Prosody-Version Status
+--------------- ---------------------
+trunk           Works as of 24-12-16
+0.12            Works