Comparison

plugins/mod_muc_mam.lua @ 8722:7ee93b3fa160

mod_muc_mam: Import mod_mam_muc from prosody-modules ba6466fa6823
author Kim Alvefur <zash@zash.se>
date Mon, 02 Apr 2018 19:44:44 +0200
child 8723:f3bdb20214ab
comparison
equal deleted inserted replaced
8721:b773b15fee71 8722:7ee93b3fa160
1 -- XEP-0313: Message Archive Management for Prosody MUC
2 -- Copyright (C) 2011-2017 Kim Alvefur
3 --
4 -- This file is MIT/X11 licensed.
5
6 if module:get_host_type() ~= "component" then
7 module:log("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name);
8 return;
9 end
10
11 local xmlns_mam = "urn:xmpp:mam:2";
12 local xmlns_delay = "urn:xmpp:delay";
13 local xmlns_forward = "urn:xmpp:forward:0";
14 local xmlns_st_id = "urn:xmpp:sid:0";
15 local xmlns_muc_user = "http://jabber.org/protocol/muc#user";
16 local muc_form_enable = "muc#roomconfig_enablearchiving"
17
18 local st = require "util.stanza";
19 local rsm = require "util.rsm";
20 local jid_bare = require "util.jid".bare;
21 local jid_split = require "util.jid".split;
22 local jid_prep = require "util.jid".prep;
23 local dataform = require "util.dataforms".new;
24 local it = require"util.iterators";
25
26 -- Support both old and new MUC code
27 local mod_muc = module:depends"muc";
28 local rooms = rawget(mod_muc, "rooms");
29 local each_room = rawget(mod_muc, "each_room") or function() return it.values(rooms); end;
30 local new_muc = not rooms;
31 if new_muc then
32 rooms = module:shared"muc/rooms";
33 else
34 -- COMPAT: We don't (currently?) support injecting stanza-id
35 -- on Prosody 0.10 and prior, which is required by mam:2
36 xmlns_mam = "urn:xmpp:mam:1";
37 end
38 local get_room_from_jid = rawget(mod_muc, "get_room_from_jid") or
39 function (jid)
40 return rooms[jid];
41 end
42
43 local is_stanza = st.is_stanza;
44 local tostring = tostring;
45 local time_now = os.time;
46 local m_min = math.min;
47 local timestamp, timestamp_parse = require "util.datetime".datetime, require "util.datetime".parse;
48 local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
49
50 local default_history_length = 20;
51 local max_history_length = module:get_option_number("max_history_messages", math.huge);
52
53 local function get_historylength(room)
54 return math.min(room._data.history_length or default_history_length, max_history_length);
55 end
56
57 local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false);
58 local log_by_default = module:get_option_boolean("muc_log_by_default", true);
59
60 local archive_store = "muc_log";
61 local archive = module:open_store(archive_store, "archive");
62
63 if archive.name == "null" or not archive.find then
64 if not archive.find then
65 module:log("error", "Attempt to open archive storage returned a driver without archive API support");
66 module:log("error", "mod_%s does not support archiving",
67 archive._provided_by or archive.name and "storage_"..archive.name.."(?)" or "<unknown>");
68 else
69 module:log("error", "Attempt to open archive storage returned null driver");
70 end
71 module:log("info", "See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
72 return false;
73 end
74
75 local function archiving_enabled(room)
76 if log_all_rooms then
77 return true;
78 end
79 local enabled = room._data.archiving;
80 if enabled == nil then
81 return log_by_default;
82 end
83 return enabled;
84 end
85
86 local send_history, save_to_history;
87
88 -- Override history methods for all rooms.
89 if not new_muc then -- 0.10 or older
90 module:hook("muc-room-created", function (event)
91 local room = event.room;
92 if archiving_enabled(room) then
93 room.send_history = send_history;
94 room.save_to_history = save_to_history;
95 end
96 end);
97
98 function module.load()
99 for room in each_room() do
100 if archiving_enabled(room) then
101 room.send_history = send_history;
102 room.save_to_history = save_to_history;
103 end
104 end
105 end
106 function module.unload()
107 for room in each_room() do
108 if room.send_history == send_history then
109 room.send_history = nil;
110 room.save_to_history = nil;
111 end
112 end
113 end
114 end
115
116 if not log_all_rooms then
117 module:hook("muc-config-form", function(event)
118 local room, form = event.room, event.form;
119 table.insert(form,
120 {
121 name = muc_form_enable,
122 type = "boolean",
123 label = "Enable archiving?",
124 value = archiving_enabled(room),
125 }
126 );
127 end);
128
129 module:hook("muc-config-submitted", function(event)
130 local room, fields, changed = event.room, event.fields, event.changed;
131 local new = fields[muc_form_enable];
132 if new ~= room._data.archiving then
133 room._data.archiving = new;
134 if type(changed) == "table" then
135 changed[muc_form_enable] = true;
136 else
137 event.changed = true;
138 end
139 if new then
140 room.send_history = send_history;
141 room.save_to_history = save_to_history;
142 else
143 room.send_history = nil;
144 room.save_to_history = nil;
145 end
146 end
147 end);
148 end
149
150 -- Note: We ignore the 'with' field as this is internally used for stanza types
151 local query_form = dataform {
152 { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; };
153 { name = "with"; type = "jid-single"; };
154 { name = "start"; type = "text-single" };
155 { name = "end"; type = "text-single"; };
156 };
157
158 -- Serve form
159 module:hook("iq-get/bare/"..xmlns_mam..":query", function(event)
160 local origin, stanza = event.origin, event.stanza;
161 origin.send(st.reply(stanza):add_child(query_form:form()));
162 return true;
163 end);
164
165 -- Handle archive queries
166 module:hook("iq-set/bare/"..xmlns_mam..":query", function(event)
167 local origin, stanza = event.origin, event.stanza;
168 local room_jid = stanza.attr.to;
169 local room_node = jid_split(room_jid);
170 local orig_from = stanza.attr.from;
171 local query = stanza.tags[1];
172
173 local room = get_room_from_jid(room_jid);
174 if not room then
175 origin.send(st.error_reply(stanza, "cancel", "item-not-found"))
176 return true;
177 end
178 local from = jid_bare(orig_from);
179
180 -- Banned or not a member of a members-only room?
181 local from_affiliation = room:get_affiliation(from);
182 if from_affiliation == "outcast" -- banned
183 or room:get_members_only() and not from_affiliation then -- members-only, not a member
184 origin.send(st.error_reply(stanza, "auth", "forbidden"))
185 return true;
186 end
187
188 local qid = query.attr.queryid;
189
190 -- Search query parameters
191 local qstart, qend;
192 local form = query:get_child("x", "jabber:x:data");
193 if form then
194 local err;
195 form, err = query_form:data(form);
196 if err then
197 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
198 return true;
199 end
200 qstart, qend = form["start"], form["end"];
201 end
202
203 if qstart or qend then -- Validate timestamps
204 local vstart, vend = (qstart and timestamp_parse(qstart)), (qend and timestamp_parse(qend))
205 if (qstart and not vstart) or (qend and not vend) then
206 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid timestamp"))
207 return true;
208 end
209 qstart, qend = vstart, vend;
210 end
211
212 module:log("debug", "Archive query id %s from %s until %s)",
213 tostring(qid),
214 qstart and timestamp(qstart) or "the dawn of time",
215 qend and timestamp(qend) or "now");
216
217 -- RSM stuff
218 local qset = rsm.get(query);
219 local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
220 local reverse = qset and qset.before or false;
221
222 local before, after = qset and qset.before, qset and qset.after;
223 if type(before) ~= "string" then before = nil; end
224
225 -- Load all the data!
226 local data, err = archive:find(room_node, {
227 start = qstart; ["end"] = qend; -- Time range
228 limit = qmax + 1;
229 before = before; after = after;
230 reverse = reverse;
231 with = "message<groupchat";
232 });
233
234 if not data then
235 origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
236 return true;
237 end
238 local total = tonumber(err);
239
240 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
241
242 local results = {};
243
244 -- Wrap it in stuff and deliver
245 local first, last;
246 local count = 0;
247 local complete = "true";
248 for id, item, when in data do
249 count = count + 1;
250 if count > qmax then
251 complete = nil;
252 break;
253 end
254 local fwd_st = st.message(msg_reply_attr)
255 :tag("result", { xmlns = xmlns_mam, queryid = qid, id = id })
256 :tag("forwarded", { xmlns = xmlns_forward })
257 :tag("delay", { xmlns = xmlns_delay, stamp = timestamp(when) }):up();
258
259 -- Strip <x> tag, containing the original senders JID, unless the room makes this public
260 if room:get_whois() ~= "anyone" then
261 item:maptags(function (tag)
262 if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then
263 return nil;
264 end
265 return tag;
266 end);
267 end
268 if not is_stanza(item) then
269 item = st.deserialize(item);
270 end
271 item.attr.xmlns = "jabber:client";
272 fwd_st:add_child(item);
273
274 if not first then first = id; end
275 last = id;
276
277 if reverse then
278 results[count] = fwd_st;
279 else
280 origin.send(fwd_st);
281 end
282 end
283
284 if reverse then
285 for i = #results, 1, -1 do
286 origin.send(results[i]);
287 end
288 first, last = last, first;
289 end
290
291 -- That's all folks!
292 module:log("debug", "Archive query %s completed", tostring(qid));
293
294 origin.send(st.reply(stanza)
295 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
296 :add_child(rsm.generate {
297 first = first, last = last, count = total }));
298 return true;
299 end);
300
301 module:hook("muc-get-history", function (event)
302 local room = event.room;
303 if not archiving_enabled(room) then return end
304 local room_jid = room.jid;
305 local maxstanzas = event.maxstanzas or math.huge;
306 local maxchars = event.maxchars;
307 local since = event.since;
308 local to = event.to;
309
310 -- Load all the data!
311 local query = {
312 limit = math.min(maxstanzas, get_historylength(room));
313 start = since;
314 reverse = true;
315 with = "message<groupchat";
316 }
317 local data, err = archive:find(jid_split(room_jid), query);
318
319 if not data then
320 module:log("error", "Could not fetch history: %s", tostring(err));
321 return
322 end
323
324 local history, i = {}, 1;
325
326 for id, item, when in data do
327 item.attr.to = to;
328 item:tag("delay", { xmlns = "urn:xmpp:delay", from = room_jid, stamp = timestamp(when) }):up(); -- XEP-0203
329 item:tag("stanza-id", { xmlns = xmlns_st_id, by = room_jid, id = id }):up();
330 if room:get_whois() ~= "anyone" then
331 item:maptags(function (tag)
332 if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then
333 return nil;
334 end
335 return tag;
336 end);
337 end
338 if maxchars then
339 local chars = #tostring(item);
340 if maxchars - chars < 0 then
341 break
342 end
343 maxchars = maxchars - chars;
344 end
345 history[i], i = item, i+1;
346 -- module:log("debug", tostring(item));
347 end
348 function event.next_stanza()
349 i = i - 1;
350 return history[i];
351 end
352 return true;
353 end, 1);
354
355 function send_history(self, to, stanza)
356 local maxchars, maxstanzas, seconds, since;
357 local history_tag = stanza:find("{http://jabber.org/protocol/muc}x/history")
358 if history_tag then
359 module:log("debug", tostring(history_tag));
360 local history_attr = history_tag.attr;
361 maxchars = tonumber(history_attr.maxchars);
362 maxstanzas = tonumber(history_attr.maxstanzas);
363 seconds = tonumber(history_attr.seconds);
364 since = history_attr.since;
365 if since then
366 since = timestamp_parse(since);
367 end
368 if seconds then
369 since = math.max(os.time() - seconds, since or 0);
370 end
371 end
372
373 local event = {
374 room = self;
375 to = to; -- `to` is required to calculate the character count for `maxchars`
376 maxchars = maxchars, maxstanzas = maxstanzas, since = since;
377 next_stanza = function() end; -- events should define this iterator
378 };
379
380 module:fire_event("muc-get-history", event);
381
382 for msg in event.next_stanza, event do
383 self:_route_stanza(msg);
384 end
385 end
386
387 -- Handle messages
388 function save_to_history(self, stanza)
389 local room_node, room_host = jid_split(self.jid);
390
391 -- Filter out <stanza-id> that claim to be from us
392 stanza:maptags(function (tag)
393 if tag.name == "stanza-id" and tag.attr.xmlns == xmlns_st_id
394 and jid_prep(tag.attr.by) == self.jid then
395 return nil;
396 end
397 if tag.name == "x" and tag.attr.xmlns == xmlns_muc_user then
398 return nil;
399 end
400 return tag;
401 end);
402
403 local stored_stanza = stanza;
404
405 if stanza.name == "message" and self:get_whois() == "anyone" then
406 stored_stanza = st.clone(stanza);
407 local actor = jid_bare(self._occupants[stanza.attr.from].jid);
408 local affiliation = self:get_affiliation(actor) or "none";
409 local role = self:get_role(actor) or self:get_default_role(affiliation);
410 stored_stanza:add_direct_child(st.stanza("x", { xmlns = xmlns_muc_user })
411 :tag("item", { affiliation = affiliation; role = role; jid = actor }));
412 end
413
414 -- Policy check
415 if not archiving_enabled(self) then return end -- Don't log
416
417 -- And stash it
418 local with = stanza.name
419 if stanza.attr.type then
420 with = with .. "<" .. stanza.attr.type
421 end
422
423 local id = archive:append(room_node, nil, stored_stanza, time_now(), with);
424
425 if id then
426 stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id }));
427 end
428 end
429
430 module:hook("muc-broadcast-message", function (event)
431 local room, stanza = event.room, event.stanza;
432 if stanza:get_child("body") then
433 save_to_history(room, stanza);
434 end
435 end);
436
437 if module:get_option_boolean("muc_log_presences", true) then
438 module:hook("muc-occupant-joined", function (event)
439 save_to_history(event.room, st.stanza("presence", { from = event.nick }));
440 end);
441 module:hook("muc-occupant-left", function (event)
442 save_to_history(event.room, st.stanza("presence", { type = "unavailable", from = event.nick }));
443 end);
444 end
445
446 if not archive.delete then
447 module:log("warn", "Storage driver %s does not support deletion", archive._provided_by);
448 module:log("warn", "Archived message will persist after a room has been destroyed");
449 else
450 module:hook("muc-room-destroyed", function(event)
451 local room_node = jid_split(event.room.jid);
452 archive:delete(room_node);
453 end);
454 end
455
456 -- And role/affiliation changes?
457
458 module:add_feature(xmlns_mam);
459
460 module:hook("muc-disco#info", function(event)
461 event.reply:tag("feature", {var=xmlns_mam}):up();
462 end);