plugins/rtbl_guard.lua

Thu, 23 Mar 2023 09:54:45 +0000

author
Matthew Wild <mwild1@gmail.com>
date
Thu, 23 Mar 2023 09:54:45 +0000
changeset 174
56316e345595
parent 173
e46ac57fa60b
permissions
-rw-r--r--

squishy: Add missing servercontact plugin

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

mercurial