plugins/rtbl_guard.lua

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