comparison plugins/mod_muc_mam.lua @ 11200:bf8f2da84007

Merge 0.11->trunk
author Kim Alvefur <zash@zash.se>
date Thu, 05 Nov 2020 22:31:25 +0100
parents cb52cb8aa706
children cabb022f31c0
comparison
equal deleted inserted replaced
11199:6c7c50a4de32 11200:bf8f2da84007
2 -- Copyright (C) 2011-2017 Kim Alvefur 2 -- Copyright (C) 2011-2017 Kim Alvefur
3 -- 3 --
4 -- This file is MIT/X11 licensed. 4 -- This file is MIT/X11 licensed.
5 5
6 if module:get_host_type() ~= "component" then 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); 7 module:log_status("error", "mod_%s should be loaded only on a MUC component, not normal hosts", module.name);
8 return; 8 return;
9 end 9 end
10 10
11 local xmlns_mam = "urn:xmpp:mam:2"; 11 local xmlns_mam = "urn:xmpp:mam:2";
12 local xmlns_delay = "urn:xmpp:delay"; 12 local xmlns_delay = "urn:xmpp:delay";
19 local rsm = require "util.rsm"; 19 local rsm = require "util.rsm";
20 local jid_bare = require "util.jid".bare; 20 local jid_bare = require "util.jid".bare;
21 local jid_split = require "util.jid".split; 21 local jid_split = require "util.jid".split;
22 local jid_prep = require "util.jid".prep; 22 local jid_prep = require "util.jid".prep;
23 local dataform = require "util.dataforms".new; 23 local dataform = require "util.dataforms".new;
24 local get_form_type = require "util.dataforms".get_type;
24 25
25 local mod_muc = module:depends"muc"; 26 local mod_muc = module:depends"muc";
26 local get_room_from_jid = mod_muc.get_room_from_jid; 27 local get_room_from_jid = mod_muc.get_room_from_jid;
27 28
28 local is_stanza = st.is_stanza; 29 local is_stanza = st.is_stanza;
30 local time_now = os.time; 31 local time_now = os.time;
31 local m_min = math.min; 32 local m_min = math.min;
32 local timestamp, timestamp_parse, datestamp = import( "util.datetime", "datetime", "parse", "date"); 33 local timestamp, timestamp_parse, datestamp = import( "util.datetime", "datetime", "parse", "date");
33 local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); 34 local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
34 35
36 local cleanup_after = module:get_option_string("muc_log_expires_after", "1w");
37 local cleanup_interval = module:get_option_number("muc_log_cleanup_interval", 4 * 60 * 60);
38
35 local default_history_length = 20; 39 local default_history_length = 20;
36 local max_history_length = module:get_option_number("max_history_messages", math.huge); 40 local max_history_length = module:get_option_number("max_history_messages", math.huge);
37 41
38 local function get_historylength(room) 42 local function get_historylength(room)
39 return math.min(room._data.history_length or default_history_length, max_history_length); 43 return math.min(room._data.history_length or default_history_length, max_history_length);
46 local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false); 50 local log_all_rooms = module:get_option_boolean("muc_log_all_rooms", false);
47 local log_by_default = module:get_option_boolean("muc_log_by_default", true); 51 local log_by_default = module:get_option_boolean("muc_log_by_default", true);
48 52
49 local archive_store = "muc_log"; 53 local archive_store = "muc_log";
50 local archive = module:open_store(archive_store, "archive"); 54 local archive = module:open_store(archive_store, "archive");
55
56 local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000);
57 local archive_truncate = math.floor(archive_item_limit * 0.99);
51 58
52 if archive.name == "null" or not archive.find then 59 if archive.name == "null" or not archive.find then
53 if not archive.find then 60 if not archive.find then
54 module:log("error", "Attempt to open archive storage returned a driver without archive API support"); 61 module:log("error", "Attempt to open archive storage returned a driver without archive API support");
55 module:log("error", "mod_%s does not support archiving", 62 module:log("error", "mod_%s does not support archiving",
61 return false; 68 return false;
62 end 69 end
63 70
64 local function archiving_enabled(room) 71 local function archiving_enabled(room)
65 if log_all_rooms then 72 if log_all_rooms then
73 module:log("debug", "Archiving all rooms");
66 return true; 74 return true;
67 end 75 end
68 local enabled = room._data.archiving; 76 local enabled = room._data.archiving;
69 if enabled == nil then 77 if enabled == nil then
78 module:log("debug", "Default is %s (for %s)", log_by_default, room.jid);
70 return log_by_default; 79 return log_by_default;
71 end 80 end
81 module:log("debug", "Logging in room %s is %s", room.jid, enabled);
72 return enabled; 82 return enabled;
73 end 83 end
74 84
75 if not log_all_rooms then 85 if not log_all_rooms then
76 module:hook("muc-config-form", function(event) 86 module:hook("muc-config-form", function(event)
133 143
134 -- Search query parameters 144 -- Search query parameters
135 local qstart, qend; 145 local qstart, qend;
136 local form = query:get_child("x", "jabber:x:data"); 146 local form = query:get_child("x", "jabber:x:data");
137 if form then 147 if form then
138 local err; 148 local form_type, err = get_form_type(form);
149 if not form_type then
150 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
151 return true;
152 elseif form_type ~= xmlns_mam then
153 origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'"));
154 return true;
155 end
139 form, err = query_form:data(form); 156 form, err = query_form:data(form);
140 if err then 157 if err then
141 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); 158 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
142 return true; 159 return true;
143 end 160 end
151 return true; 168 return true;
152 end 169 end
153 qstart, qend = vstart, vend; 170 qstart, qend = vstart, vend;
154 end 171 end
155 172
156 module:log("debug", "Archive query id %s from %s until %s)", 173 module:log("debug", "Archive query by %s id=%s when=%s...%s",
157 tostring(qid), 174 origin.username,
158 qstart and timestamp(qstart) or "the dawn of time", 175 qid or stanza.attr.id,
159 qend and timestamp(qend) or "now"); 176 qstart and timestamp(qstart) or "",
177 qend and timestamp(qend) or "");
160 178
161 -- RSM stuff 179 -- RSM stuff
162 local qset = rsm.get(query); 180 local qset = rsm.get(query);
163 local qmax = m_min(qset and qset.max or default_max_items, max_max_items); 181 local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
164 local reverse = qset and qset.before or false; 182 local reverse = qset and qset.before or false;
165 183
166 local before, after = qset and qset.before, qset and qset.after; 184 local before, after = qset and qset.before, qset and qset.after;
167 if type(before) ~= "string" then before = nil; end 185 if type(before) ~= "string" then before = nil; end
186 if qset then
187 module:log("debug", "Archive query id=%s rsm=%q", qid or stanza.attr.id, qset);
188 end
168 189
169 -- Load all the data! 190 -- Load all the data!
170 local data, err = archive:find(room_node, { 191 local data, err = archive:find(room_node, {
171 start = qstart; ["end"] = qend; -- Time range 192 start = qstart; ["end"] = qend; -- Time range
172 limit = qmax + 1; 193 limit = qmax + 1;
174 reverse = reverse; 195 reverse = reverse;
175 with = "message<groupchat"; 196 with = "message<groupchat";
176 }); 197 });
177 198
178 if not data then 199 if not data then
179 origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); 200 module:log("debug", "Archive query id=%s failed: %s", qid or stanza.attr.id, err);
201 if err == "item-not-found" then
202 origin.send(st.error_reply(stanza, "modify", "item-not-found"));
203 else
204 origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
205 end
180 return true; 206 return true;
181 end 207 end
182 local total = tonumber(err); 208 local total = tonumber(err);
183 209
184 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; 210 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
231 origin.send(results[i]); 257 origin.send(results[i]);
232 end 258 end
233 first, last = last, first; 259 first, last = last, first;
234 end 260 end
235 261
236 -- That's all folks!
237 module:log("debug", "Archive query %s completed", tostring(qid));
238 262
239 origin.send(st.reply(stanza) 263 origin.send(st.reply(stanza)
240 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) 264 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
241 :add_child(rsm.generate { 265 :add_child(rsm.generate {
242 first = first, last = last, count = total })); 266 first = first, last = last, count = total }));
267
268 -- That's all folks!
269 module:log("debug", "Archive query id=%s completed, %d items returned", qid or stanza.attr.id, complete and count or count - 1);
243 return true; 270 return true;
244 end); 271 end);
245 272
246 module:hook("muc-get-history", function (event) 273 module:hook("muc-get-history", function (event)
247 local room = event.room; 274 local room = event.room;
272 with = "message<groupchat"; 299 with = "message<groupchat";
273 } 300 }
274 local data, err = archive:find(jid_split(room_jid), query); 301 local data, err = archive:find(jid_split(room_jid), query);
275 302
276 if not data then 303 if not data then
277 module:log("error", "Could not fetch history: %s", tostring(err)); 304 module:log("error", "Could not fetch history: %s", err);
278 return 305 return
279 end 306 end
280 307
281 local history, i = {}, 1; 308 local history, i = {}, 1;
282 309
298 break 325 break
299 end 326 end
300 maxchars = maxchars - chars; 327 maxchars = maxchars - chars;
301 end 328 end
302 history[i], i = item, i+1; 329 history[i], i = item, i+1;
303 -- module:log("debug", tostring(item)); 330 -- module:log("debug", item);
304 end 331 end
305 function event.next_stanza() 332 function event.next_stanza()
306 i = i - 1; 333 i = i - 1;
307 return history[i]; 334 return history[i];
308 end 335 end
323 350
324 end, 1); 351 end, 1);
325 352
326 -- Handle messages 353 -- Handle messages
327 local function save_to_history(self, stanza) 354 local function save_to_history(self, stanza)
328 local room_node, room_host = jid_split(self.jid); 355 local room_node = jid_split(self.jid);
329 356
330 local stored_stanza = stanza; 357 local stored_stanza = stanza;
331 358
332 if stanza.name == "message" and self:get_whois() == "anyone" then 359 if stanza.name == "message" and self:get_whois() == "anyone" then
333 stored_stanza = st.clone(stanza); 360 stored_stanza = st.clone(stanza);
350 if stanza.attr.type then 377 if stanza.attr.type then
351 with = with .. "<" .. stanza.attr.type 378 with = with .. "<" .. stanza.attr.type
352 end 379 end
353 380
354 -- And stash it 381 -- And stash it
355 local id, err = archive:append(room_node, nil, stored_stanza, time_now(), with); 382 local time = time_now();
383 local id, err = archive:append(room_node, nil, stored_stanza, time, with);
384
385 if not id and err == "quota-limit" then
386 if type(cleanup_after) == "number" then
387 module:log("debug", "Room '%s' over quota, cleaning archive", room_node);
388 local cleaned = archive:delete(room_node, {
389 ["end"] = (os.time() - cleanup_after);
390 });
391 if cleaned then
392 id, err = archive:append(room_node, nil, stored_stanza, time, with);
393 end
394 end
395 if not id and (archive.caps and archive.caps.truncate) then
396 module:log("debug", "User '%s' over quota, truncating archive", room_node);
397 local truncated = archive:delete(room_node, {
398 truncate = archive_truncate;
399 });
400 if truncated then
401 id, err = archive:append(room_node, nil, stored_stanza, time, with);
402 end
403 end
404 end
356 405
357 if id then 406 if id then
358 schedule_cleanup(room_node); 407 schedule_cleanup(room_node);
359 stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id })); 408 stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id }));
360 else 409 else
389 -- And role/affiliation changes? 438 -- And role/affiliation changes?
390 439
391 module:add_feature(xmlns_mam); 440 module:add_feature(xmlns_mam);
392 441
393 module:hook("muc-disco#info", function(event) 442 module:hook("muc-disco#info", function(event)
394 event.reply:tag("feature", {var=xmlns_mam}):up(); 443 if archiving_enabled(event.room) then
444 event.reply:tag("feature", {var=xmlns_mam}):up();
445 end
395 event.reply:tag("feature", {var=xmlns_st_id}):up(); 446 event.reply:tag("feature", {var=xmlns_st_id}):up();
396 end); 447 end);
397 448
398 -- Cleanup 449 -- Cleanup
399
400 local cleanup_after = module:get_option_string("muc_log_expires_after", "1w");
401 local cleanup_interval = module:get_option_number("muc_log_cleanup_interval", 4 * 60 * 60);
402 450
403 if cleanup_after ~= "never" then 451 if cleanup_after ~= "never" then
404 local cleanup_storage = module:open_store("muc_log_cleanup"); 452 local cleanup_storage = module:open_store("muc_log_cleanup");
405 local cleanup_map = module:open_store("muc_log_cleanup", "map"); 453 local cleanup_map = module:open_store("muc_log_cleanup", "map");
406 454
433 if ok then 481 if ok then
434 last_date:set(roomname, date); 482 last_date:set(roomname, date);
435 end 483 end
436 end 484 end
437 485
486 local cleanup_time = module:measure("cleanup", "times");
487
438 local async = require "util.async"; 488 local async = require "util.async";
439 cleanup_runner = async.runner(function () 489 cleanup_runner = async.runner(function ()
490 local cleanup_done = cleanup_time();
440 local rooms = {}; 491 local rooms = {};
441 local cut_off = datestamp(os.time() - cleanup_after); 492 local cut_off = datestamp(os.time() - cleanup_after);
442 for date in cleanup_storage:users() do 493 for date in cleanup_storage:users() do
443 if date <= cut_off then 494 if date <= cut_off then
444 module:log("debug", "Messages from %q should be expired", date); 495 module:log("debug", "Messages from %q should be expired", date);
468 local wait, done = async.waiter(); 519 local wait, done = async.waiter();
469 module:add_timer(0.01, done); 520 module:add_timer(0.01, done);
470 wait(); 521 wait();
471 end 522 end
472 module:log("info", "Deleted %d expired messages for %d rooms", sum, num_rooms); 523 module:log("info", "Deleted %d expired messages for %d rooms", sum, num_rooms);
524 cleanup_done();
473 end); 525 end);
474 526
475 cleanup_task = module:add_timer(1, function () 527 cleanup_task = module:add_timer(1, function ()
476 cleanup_runner:run(true); 528 cleanup_runner:run(true);
477 return cleanup_interval; 529 return cleanup_interval;