rtbl_guard: New plugin to subscribe to RTBLs and act on them in MUCs

Wed, 15 Mar 2023 17:38:56 +0000

author
Matthew Wild <mwild1@gmail.com>
date
Wed, 15 Mar 2023 17:38:56 +0000
changeset 173
e46ac57fa60b
parent 172
3420808b8d3f
child 174
56316e345595

rtbl_guard: New plugin to subscribe to RTBLs and act on them in MUCs

This allows applying an RTBL to a MUC that doesn't have built-in RTBL support.

Note that it doesn't currently take any action when an entry is removed from
an RTBL.

plugins/rtbl_guard.lua file | annotate | diff | comparison | revisions
squishy file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/rtbl_guard.lua	Wed Mar 15 17:38:56 2023 +0000
@@ -0,0 +1,201 @@
+local jid_bare = require "util.jid".bare;
+local jid_prep = require "util.jid".prep;
+local sha256 = require "util.hashes".sha256;
+
+local function get_ban_reason(blocklist, entry)
+	local msg = { ("Banned by %s"):format(blocklist.name or "RTBL"), nil, nil };
+
+	local reason = entry.reason:match("^urn:xmpp:reporting:(%w+)$");
+	if reason then
+		msg[2] = "for "..reason;
+	end
+
+	if entry.text then
+		msg[#msg+1] = "("..entry.text..")";
+	end
+
+	return table.concat(msg, " ");
+end
+
+function riddim.plugins.rtbl_guard(bot)
+	bot.stream:add_plugin("pubsub");
+
+	local config = bot.config.rtbl_guard;
+	if not config then return; end
+
+	-- {
+	--   [rtbl_name] = {
+	--     entries = {
+	--       [jid/hash] = {
+	--         reason = <string>
+	--         text = <string>
+	--       }
+	--     }
+	--   }
+	local blocklists = {};
+
+	local function check_blocklists(jid, only_blocklist, only_entry_id)
+		jid = jid_bare(jid_prep(jid));
+		local hash = sha256(jid, true);
+
+		if only_blocklist and only_entry_id then
+			if only_entry_id ~= jid and only_entry_id ~= hash then
+				bot.stream:debug("No match for single entry %q == (%q | %q)", only_entry_id, jid, hash)
+				return;
+			end
+			return only_blocklist.entries[only_entry_id], only_blocklist;
+		end
+
+		for rtbl_id, blocklist in pairs(only_blocklist and {only_blocklist} or blocklists) do
+			bot.stream:debug("Checking %s for (%q | %q)", rtbl_id, jid, hash);
+			for k in pairs(blocklist.entries) do
+				bot.stream:debug("[%q]", k);
+			end
+			local entry = blocklist.entries[hash] or blocklist.entries[jid];
+			if entry then
+				return entry, blocklist;
+			end
+		end
+	end
+
+	local function guard_room(room)
+
+		local function process_occupant(occupant, only_blocklist, only_entry_id)
+			if not occupant.real_jid then
+				bot.stream:debug("Unable to determine real JID for %s - skipping RTBL checks", occupant.nick);
+				return;
+			end
+
+			local matched_entry, matched_blocklist = check_blocklists(occupant.real_jid, only_blocklist, only_entry_id);
+			if not matched_entry then
+				bot.stream:debug("%s is not on any RTBLs", occupant.nick);
+				return;
+			end
+
+			room:ban(occupant.nick, get_ban_reason(matched_blocklist, matched_entry));
+		end
+
+		-- Check future occupants when they join
+		room:hook("occupant-joined", process_occupant);
+
+		local function process_all_occupants(blocklist, entry_id)
+			bot.stream:debug("Checking all occupants of %s against RTBL (%s)", room.jid, entry_id or "all entries");
+			for _, occupant in pairs(room.occupants) do
+				process_occupant(occupant, blocklist, entry_id);
+			end
+		end
+
+		-- Check existing occupants against all existing entries
+		process_all_occupants();
+
+		-- Check existing occupants when new entries are added
+		room:hook("rtbl-entry-added", function (event)
+			process_all_occupants(event.blocklist, event.entry_id);
+		end);
+		room:hook("rtbl-updated", function (event)
+			process_all_occupants(event.blocklist);
+		end);
+	end
+
+	local function handle_entry(blocklist, item)
+		local report = item:get_child("report", "urn:xmpp:reporting:1");
+		if not report then return; end
+		blocklist.entries[item.attr.id] = {
+			reason = report.attr.reason;
+			text = report:get_child_text("text");
+		};
+		return blocklist.entries[item.attr.id];
+	end
+
+	local function clear_entry(blocklist, retract)
+		local cleared_entry = blocklist.entries[retract.attr.id];
+		if not cleared_entry then
+			return;
+		end
+		blocklist.entries[retract.attr.id] = nil;
+		return cleared_entry;
+	end
+
+	bot.stream:hook("pubsub/event", function (event)
+		local blocklist_id = event.from.."::"..event.node;
+		local blocklist = blocklists[blocklist_id];
+		if not blocklist then return; end
+		local entry = handle_entry(blocklist, event.item);
+		for _, room in pairs(bot.rooms) do
+			room:event("rtbl-entry-added", {
+				blocklist = blocklist;
+				entry = entry;
+				entry_id = event.item.attr.id;
+			});
+		end
+	end);
+
+	-- COMPAT: current verse does not emit an event for retraction
+	bot.stream:hook("message", function (message)
+		local m_from = message.attr.from;
+		for pubsub_event in message:childtags("event", "http://jabber.org/protocol/pubsub#event") do
+			local items = pubsub_event:get_child("items");
+			if items then
+				local node = items.attr.node;
+				local blocklist_id = m_from.."::"..node;
+				local blocklist = blocklists[blocklist_id];
+				if not blocklist then return; end
+
+				for retract in items:childtags("retract") do
+					local entry = clear_entry(blocklist, retract);
+					if entry then
+						for _, room in pairs(bot.rooms) do
+							room:event("rtbl-entry-removed", {
+								blocklist = blocklist;
+								entry = entry;
+							});
+						end
+					end
+				end
+			end
+		end
+	end);
+
+	bot:hook("started", function ()
+		for _, rtbl_config in ipairs(config) do
+			local host, node = rtbl_config.host, rtbl_config.node;
+			if host and node then
+				bot.stream.pubsub(host, node):subscribe(nil, nil, function (response)
+					if response.attr.type ~= "result" then
+						bot.stream:warn("Failed to subscribe to RTBL %s::%s", host, node);
+						return;
+					end
+					bot.stream:info("Subscribed to RTBL %s::%s", host, node);
+
+					local blocklist = { name = rtbl_config.name, entries = {} };
+					bot.stream.pubsub(host, node):items(true, function (items_response)
+						if items_response.attr.type ~= "result" then
+							bot.stream:warn("Failed to synchronize with RTBL: %s", tostring(items_response));
+							return;
+						end
+						local items = items_response.tags[1].tags[1];
+						bot.stream:debug("RTBL sync: %s", items);
+						for item in items:childtags("item") do
+							bot.stream:debug("RTBL item: %s", item);
+							handle_entry(blocklist, item);
+						end
+
+						for _, room in pairs(bot.rooms) do
+							room:event("rtbl-updated", {
+								blocklist = blocklist;
+							});
+						end
+					end);
+					local blocklist_id = host.."::"..node;
+					blocklists[blocklist_id] = blocklist;
+				end);
+			end
+		end
+
+		for _, room in pairs(bot.rooms) do
+			guard_room(room);
+		end
+
+		bot:hook("groupchat/joined", guard_room);
+	end);
+end
--- a/squishy	Wed Mar 15 12:19:39 2023 +0000
+++ b/squishy	Wed Mar 15 17:38:56 2023 +0000
@@ -19,6 +19,7 @@
 	"opdown",
 	"pubsub2room",
 	"rtbl_admin",
+	"rtbl_guard",
 	"simple_commands",
 	"slap",
 	"topic",

mercurial