| 1 | local jid_bare = require "prosody.util.jid".bare; |
| 2 | local jid_prep = require "prosody.util.jid".prep; |
| 3 | local sha256 = require "prosody.util.hashes".sha256; |
| 4 | |
| 5 | local function get_ban_reason(blocklist, entry) |
| 6 | local msg = { ("Banned by %s"):format(blocklist.name or "RTBL"), nil, nil }; |
| 7 | |
| 8 | local reason = entry.reason:match("^urn:xmpp:reporting:(%w+)$"); |
| 9 | if reason then |
| 10 | msg[2] = "for "..reason; |
| 11 | end |
| 12 | |
| 13 | if entry.text then |
| 14 | msg[#msg+1] = "("..entry.text..")"; |
| 15 | end |
| 16 | |
| 17 | return table.concat(msg, " "); |
| 18 | end |
| 19 | |
| 20 | function riddim.plugins.rtbl_guard(bot) |
| 21 | bot.stream:add_plugin("pubsub"); |
| 22 | |
| 23 | local config = bot.config.rtbl_guard; |
| 24 | if not config then return; end |
| 25 | |
| 26 | -- { |
| 27 | -- [rtbl_name] = { |
| 28 | -- entries = { |
| 29 | -- [jid/hash] = { |
| 30 | -- reason = <string> |
| 31 | -- text = <string> |
| 32 | -- } |
| 33 | -- } |
| 34 | -- } |
| 35 | local blocklists = {}; |
| 36 | |
| 37 | local function check_blocklists(jid, only_blocklist, only_entry_id) |
| 38 | jid = jid_bare(jid_prep(jid)); |
| 39 | local hash = sha256(jid, true); |
| 40 | |
| 41 | if only_blocklist and only_entry_id then |
| 42 | if only_entry_id ~= jid and only_entry_id ~= hash then |
| 43 | bot.stream:debug("No match for single entry %q == (%q | %q)", only_entry_id, jid, hash) |
| 44 | return; |
| 45 | end |
| 46 | return only_blocklist.entries[only_entry_id], only_blocklist; |
| 47 | end |
| 48 | |
| 49 | for rtbl_id, blocklist in pairs(only_blocklist and {only_blocklist} or blocklists) do |
| 50 | bot.stream:debug("Checking %s for (%q | %q)", rtbl_id, jid, hash); |
| 51 | for k in pairs(blocklist.entries) do |
| 52 | bot.stream:debug("[%q]", k); |
| 53 | end |
| 54 | local entry = blocklist.entries[hash] or blocklist.entries[jid]; |
| 55 | if entry then |
| 56 | return entry, blocklist; |
| 57 | end |
| 58 | end |
| 59 | end |
| 60 | |
| 61 | local function guard_room(room) |
| 62 | |
| 63 | local function process_occupant(occupant, only_blocklist, only_entry_id) |
| 64 | if not occupant.real_jid then |
| 65 | bot.stream:debug("Unable to determine real JID for %s - skipping RTBL checks", occupant.nick); |
| 66 | return; |
| 67 | end |
| 68 | |
| 69 | local matched_entry, matched_blocklist = check_blocklists(occupant.real_jid, only_blocklist, only_entry_id); |
| 70 | if not matched_entry then |
| 71 | bot.stream:debug("%s is not on any RTBLs", occupant.nick); |
| 72 | return; |
| 73 | end |
| 74 | |
| 75 | room:ban(occupant.nick, get_ban_reason(matched_blocklist, matched_entry)); |
| 76 | end |
| 77 | |
| 78 | -- Check future occupants when they join |
| 79 | room:hook("occupant-joined", process_occupant); |
| 80 | |
| 81 | local function process_all_occupants(blocklist, entry_id) |
| 82 | bot.stream:debug("Checking all occupants of %s against RTBL (%s)", room.jid, entry_id or "all entries"); |
| 83 | for _, occupant in pairs(room.occupants) do |
| 84 | process_occupant(occupant, blocklist, entry_id); |
| 85 | end |
| 86 | end |
| 87 | |
| 88 | -- Check existing occupants against all existing entries |
| 89 | process_all_occupants(); |
| 90 | |
| 91 | -- Check existing occupants when new entries are added |
| 92 | room:hook("rtbl-entry-added", function (event) |
| 93 | process_all_occupants(event.blocklist, event.entry_id); |
| 94 | end); |
| 95 | room:hook("rtbl-updated", function (event) |
| 96 | process_all_occupants(event.blocklist); |
| 97 | end); |
| 98 | end |
| 99 | |
| 100 | local function handle_entry(blocklist, item) |
| 101 | local report = item:get_child("report", "urn:xmpp:reporting:1"); |
| 102 | if not report then return; end |
| 103 | blocklist.entries[item.attr.id] = { |
| 104 | reason = report.attr.reason; |
| 105 | text = report:get_child_text("text"); |
| 106 | }; |
| 107 | return blocklist.entries[item.attr.id]; |
| 108 | end |
| 109 | |
| 110 | local function clear_entry(blocklist, retract) |
| 111 | local cleared_entry = blocklist.entries[retract.attr.id]; |
| 112 | if not cleared_entry then |
| 113 | return; |
| 114 | end |
| 115 | blocklist.entries[retract.attr.id] = nil; |
| 116 | return cleared_entry; |
| 117 | end |
| 118 | |
| 119 | bot.stream:hook("pubsub/event", function (event) |
| 120 | local blocklist_id = event.from.."::"..event.node; |
| 121 | local blocklist = blocklists[blocklist_id]; |
| 122 | if not blocklist then return; end |
| 123 | local entry = handle_entry(blocklist, event.item); |
| 124 | for _, room in pairs(bot.rooms) do |
| 125 | room:event("rtbl-entry-added", { |
| 126 | blocklist = blocklist; |
| 127 | entry = entry; |
| 128 | entry_id = event.item.attr.id; |
| 129 | }); |
| 130 | end |
| 131 | end); |
| 132 | |
| 133 | -- COMPAT: current verse does not emit an event for retraction |
| 134 | bot.stream:hook("message", function (message) |
| 135 | local m_from = message.attr.from; |
| 136 | for pubsub_event in message:childtags("event", "http://jabber.org/protocol/pubsub#event") do |
| 137 | local items = pubsub_event:get_child("items"); |
| 138 | if items then |
| 139 | local node = items.attr.node; |
| 140 | local blocklist_id = m_from.."::"..node; |
| 141 | local blocklist = blocklists[blocklist_id]; |
| 142 | if not blocklist then return; end |
| 143 | |
| 144 | for retract in items:childtags("retract") do |
| 145 | local entry = clear_entry(blocklist, retract); |
| 146 | if entry then |
| 147 | for _, room in pairs(bot.rooms) do |
| 148 | room:event("rtbl-entry-removed", { |
| 149 | blocklist = blocklist; |
| 150 | entry = entry; |
| 151 | }); |
| 152 | end |
| 153 | end |
| 154 | end |
| 155 | end |
| 156 | end |
| 157 | end); |
| 158 | |
| 159 | bot:hook("started", function () |
| 160 | for _, rtbl_config in ipairs(config) do |
| 161 | local host, node = rtbl_config.host, rtbl_config.node; |
| 162 | if host and node then |
| 163 | bot.stream.pubsub(host, node):subscribe(nil, nil, function (response) |
| 164 | if response.attr.type ~= "result" then |
| 165 | bot.stream:warn("Failed to subscribe to RTBL %s::%s", host, node); |
| 166 | return; |
| 167 | end |
| 168 | bot.stream:info("Subscribed to RTBL %s::%s", host, node); |
| 169 | |
| 170 | local blocklist = { name = rtbl_config.name, entries = {} }; |
| 171 | bot.stream.pubsub(host, node):items(true, function (items_response) |
| 172 | if items_response.attr.type ~= "result" then |
| 173 | bot.stream:warn("Failed to synchronize with RTBL: %s", tostring(items_response)); |
| 174 | return; |
| 175 | end |
| 176 | local items = items_response.tags[1].tags[1]; |
| 177 | bot.stream:debug("RTBL sync: %s", items); |
| 178 | for item in items:childtags("item") do |
| 179 | bot.stream:debug("RTBL item: %s", item); |
| 180 | handle_entry(blocklist, item); |
| 181 | end |
| 182 | |
| 183 | for _, room in pairs(bot.rooms) do |
| 184 | room:event("rtbl-updated", { |
| 185 | blocklist = blocklist; |
| 186 | }); |
| 187 | end |
| 188 | end); |
| 189 | local blocklist_id = host.."::"..node; |
| 190 | blocklists[blocklist_id] = blocklist; |
| 191 | end); |
| 192 | end |
| 193 | end |
| 194 | |
| 195 | for _, room in pairs(bot.rooms) do |
| 196 | guard_room(room); |
| 197 | end |
| 198 | |
| 199 | bot:hook("groupchat/joined", guard_room); |
| 200 | end); |
| 201 | end |