Wed, 15 Mar 2023 17:38:56 +0000
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.
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