changeset 6304:6e2458b69e33

mod_muc_rtbl: Use mod_rtbl, adds support for multiple list subscriptions
author Matthew Wild <mwild1@gmail.com>
date Wed, 30 Jul 2025 14:16:55 +0100
parents d1d774f79ea8
children 1c0e9486ba23
files mod_muc_rtbl/README.md mod_muc_rtbl/mod_muc_rtbl.lua
diffstat 2 files changed, 63 insertions(+), 108 deletions(-) [+]
line wrap: on
line diff
--- a/mod_muc_rtbl/README.md	Thu Jul 31 15:07:30 2025 +0100
+++ b/mod_muc_rtbl/README.md	Wed Jul 30 14:16:55 2025 +0100
@@ -2,7 +2,7 @@
 summary: 
 rockspec:
   dependencies:
-  - mod_pubsub_subscription
+  - mod_rtbl
 labels:
 - Stage-Alpha
 ...
@@ -23,17 +23,24 @@
 }
 ```
 
-Then there are two options, which must be set under the component or in the
-global section of your config:
+Then you should configure one or more lists you want to subscribe to:
 
+```lua
+muc_rtbls = {
+	"xmpp:rtbl.example?;node=muc_bans_sha256";
+}
 ```
-muc_rtbl_jid = "rtbl.example"
-muc_rtbl_node = "muc_bans_sha256"
-```
+
+If an unaffiliated JID matches an entry found in any of the configured lists,
+this module will block:
+
+- Joins
+- Group chat messages
+- Private messages
 
 # Compatibility
 
-Should work with Prosody >= 0.11.x
+Should work with Prosody >= 0.12.x
 
 # Developers
 
--- a/mod_muc_rtbl/mod_muc_rtbl.lua	Thu Jul 31 15:07:30 2025 +0100
+++ b/mod_muc_rtbl/mod_muc_rtbl.lua	Wed Jul 30 14:16:55 2025 +0100
@@ -1,104 +1,52 @@
-local array = require "util.array";
-local it = require "util.iterators";
 local jid = require "util.jid";
 local sha256 = require "util.hashes".sha256;
-local set = require "util.set";
 local st = require "util.stanza";
 
-local rtbl_service_jid = assert(module:get_option_string("muc_rtbl_jid"), "No RTBL JID supplied");
+local url = require "socket.url";
+
+-- Legacy config (single list only)
+local rtbl_service_jid = module:get_option_string("muc_rtbl_jid");
 local rtbl_node = module:get_option_string("muc_rtbl_node", "muc_bans_sha256");
 
-local banned_hashes = module:shared("banned_hashes");
-
-module:depends("pubsub_subscription");
-
-module:add_item("pubsub-subscription", {
-	service = rtbl_service_jid;
-	node = rtbl_node;
-
-	-- Callbacks:
-	on_subscribed = function()
-		module:log("info", "RTBL active");
-	end;
-
-	on_error = function(err)
-		module:log("error", "Failed to subscribe to RTBL: %s::%s:  %s", 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: %s", hash);
-		banned_hashes[hash] = true;
-	end;
-
-	on_retract = function (event)
-		local hash = event.item.attr.id;
-		if not hash then return; end
-		module:log("debug", "Retracted hash: %s", hash);
-		banned_hashes[hash] = nil;
-	end;
-
-	purge = function()
-		module:log("debug", "Purge all hashes");
-		for hash in pairs(banned_hashes) do
-			banned_hashes[hash] = nil;
-		end
-	end;
+-- Multi-list config
+local rtbl_config = module:get_option_array("muc_rtbls", {
+	rtbl_service_jid and rtbl_node and ("xmpp:"..rtbl_service_jid.."?;node="..rtbl_node) or nil;
 });
 
-function request_list()
-	local items_request = st.iq({ to = rtbl_service_jid, from = module.host, type = "get", id = "rtbl-request" })
-		:tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" })
-			:tag("items", { node = rtbl_node }):up()
-		:up();
-
-	module:send(items_request);
+if #rtbl_config == 0 then
+	return error("No RTBLs configured");
 end
 
-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");
-		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");
-		return;
+local mod_rtbl = module:depends("rtbl");
+
+local lists = {};
+
+local function parse_xmpp_uri(uri)
+	local parsed = url.parse(uri);
+	if parsed.scheme ~= "xmpp" then
+		return nil, "unexpected-scheme";
 	end
 
-	local old_entries = set.new(array.collect(it.keys(banned_hashes)));
-
-	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;
-				banned_hashes[hash] = true;
-			else
-				-- Entry already existed
-				old_entries:remove(hash);
-			end
-		end
+	local parsed_query = {};
+	for kv in (parsed.query or ""):gmatch("[^?;]+") do
+		local k, v = kv:match("^([^=]+)=?(.*)$");
+		parsed_query[k] = v;
 	end
 
-	-- Remove old entries that weren't in the received list
-	for hash in old_entries do
-		n_removed = n_removed + 1;
-		banned_hashes[hash] = nil;
-	end
-
-	module:log("info", "%d RTBL entries received from %s (%d added, %d removed)", n_total, from_jid, n_added, n_removed);
-	return true;
+	return {
+		jid = jid.prep(parsed.path);
+		params = parsed_query;
+	};
 end
 
-module:hook("iq-result/host/rtbl-request", update_list);
+for _, rtbl_uri in ipairs(rtbl_config) do
+	local uri = parse_xmpp_uri(rtbl_uri);
+	module:log("debug", "Subscribing to %q node %q", uri.jid, uri.params.node);
+	local rtbl = mod_rtbl.new_rtbl_subscription(uri.jid, uri.params.node, {});
+	table.insert(lists, rtbl);
+end
 
-function update_hashes(occupant)
+local function update_occupant_hashes(occupant)
 	local bare_hash, host_hash;
 	if not occupant.mod_muc_rtbl_bare_hash then
 		bare_hash = sha256(jid.bare(occupant.bare_jid), true);
@@ -112,12 +60,20 @@
 	else
 		host_hash = occupant.mod_muc_rtbl_host_hash;
 	end
-	return bare_hash, host_hash
+	return bare_hash, host_hash;
+end
+
+local function is_banned_occupant(occupant)
+	local bare_hash, host_hash = update_occupant_hashes(occupant);
+	for _, list in ipairs(lists) do
+		local items = list.items;
+		if items[bare_hash] or items[host_hash] then
+			return true;
+		end
+	end
 end
 
 module:hook("muc-occupant-pre-join", function (event)
-	if next(banned_hashes) == nil then return end
-
 	local from_bare = jid.bare(event.stanza.attr.from);
 
 	local affiliation = event.room:get_affiliation(from_bare);
@@ -126,8 +82,7 @@
 		return;
 	end
 
-	local bare_hash, host_hash = update_hashes(event.occupant);
-	if banned_hashes[bare_hash] or banned_hashes[host_hash] then
+	if is_banned_occupant(event.occupant) then
 		module:log("info", "Blocked user <%s> from room <%s> due to RTBL match", from_bare, 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);
@@ -136,14 +91,14 @@
 end);
 
 module:hook("muc-occupant-groupchat", function(event)
-	local affiliation = event.room:get_affiliation(event.occupant.bare_jid);
+	local occupant = event.occupant;
+	local affiliation = event.room:get_affiliation(occupant.bare_jid);
 	if affiliation and affiliation ~= "none" then
 		-- Skip check for affiliated users
 		return;
 	end
 
-	local bare_hash, host_hash = update_hashes(event.occupant);
-	if banned_hashes[bare_hash] or banned_hashes[host_hash] then
+	if is_banned_occupant(occupant) then
 		module:log("debug", "Blocked message from user <%s> to room <%s> due to RTBL match", event.stanza.attr.from, 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);
@@ -159,8 +114,7 @@
 		return;
 	end
 
-	local bare_hash, host_hash = update_hashes(occupant);
-	if banned_hashes[bare_hash] or banned_hashes[host_hash] then
+	if is_banned_occupant(occupant) then
 		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);
@@ -168,10 +122,4 @@
 	end
 end);
 
-if prosody.start_time then
-	request_list();
-else
-	module:hook_global("server-started", function ()
-		request_list();
-	end);
-end
+module.environment.lists = lists