Comparison

plugins/mod_muc_mam.lua @ 10563:e8db377a2983

Merge 0.11->trunk
author Kim Alvefur <zash@zash.se>
date Tue, 24 Dec 2019 00:39:45 +0100
parent 10562:670afc079f68
parent 10560:3adb6c46fbf4
child 10568:b6f93babebe8
comparison
equal deleted inserted replaced
10562:670afc079f68 10563:e8db377a2983
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);
51 57
52 if archive.name == "null" or not archive.find then 58 if archive.name == "null" or not archive.find then
53 if not archive.find then 59 if not archive.find then
54 module:log("error", "Attempt to open archive storage returned a driver without archive API support"); 60 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", 61 module:log("error", "mod_%s does not support archiving",
61 return false; 67 return false;
62 end 68 end
63 69
64 local function archiving_enabled(room) 70 local function archiving_enabled(room)
65 if log_all_rooms then 71 if log_all_rooms then
72 module:log("debug", "Archiving all rooms");
66 return true; 73 return true;
67 end 74 end
68 local enabled = room._data.archiving; 75 local enabled = room._data.archiving;
69 if enabled == nil then 76 if enabled == nil then
77 module:log("debug", "Default is %s (for %s)", log_by_default, room.jid);
70 return log_by_default; 78 return log_by_default;
71 end 79 end
80 module:log("debug", "Logging in room %s is %s", room.jid, enabled);
72 return enabled; 81 return enabled;
73 end 82 end
74 83
75 if not log_all_rooms then 84 if not log_all_rooms then
76 module:hook("muc-config-form", function(event) 85 module:hook("muc-config-form", function(event)
133 142
134 -- Search query parameters 143 -- Search query parameters
135 local qstart, qend; 144 local qstart, qend;
136 local form = query:get_child("x", "jabber:x:data"); 145 local form = query:get_child("x", "jabber:x:data");
137 if form then 146 if form then
138 local err; 147 local form_type, err = get_form_type(form);
148 if not form_type then
149 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
150 return true;
151 elseif form_type ~= xmlns_mam then
152 origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'"));
153 return true;
154 end
139 form, err = query_form:data(form); 155 form, err = query_form:data(form);
140 if err then 156 if err then
141 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); 157 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
142 return true; 158 return true;
143 end 159 end
151 return true; 167 return true;
152 end 168 end
153 qstart, qend = vstart, vend; 169 qstart, qend = vstart, vend;
154 end 170 end
155 171
156 module:log("debug", "Archive query id %s from %s until %s)", 172 module:log("debug", "Archive query by %s id=%s when=%s...%s",
157 tostring(qid), 173 origin.username,
158 qstart and timestamp(qstart) or "the dawn of time", 174 qid or stanza.attr.id,
159 qend and timestamp(qend) or "now"); 175 qstart and timestamp(qstart) or "",
176 qend and timestamp(qend) or "");
160 177
161 -- RSM stuff 178 -- RSM stuff
162 local qset = rsm.get(query); 179 local qset = rsm.get(query);
163 local qmax = m_min(qset and qset.max or default_max_items, max_max_items); 180 local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
164 local reverse = qset and qset.before or false; 181 local reverse = qset and qset.before or false;
165 182
166 local before, after = qset and qset.before, qset and qset.after; 183 local before, after = qset and qset.before, qset and qset.after;
167 if type(before) ~= "string" then before = nil; end 184 if type(before) ~= "string" then before = nil; end
185 if qset then
186 module:log("debug", "Archive query id=%s rsm=%q", qid or stanza.attr.id, qset);
187 end
168 188
169 -- Load all the data! 189 -- Load all the data!
170 local data, err = archive:find(room_node, { 190 local data, err = archive:find(room_node, {
171 start = qstart; ["end"] = qend; -- Time range 191 start = qstart; ["end"] = qend; -- Time range
172 limit = qmax + 1; 192 limit = qmax + 1;
174 reverse = reverse; 194 reverse = reverse;
175 with = "message<groupchat"; 195 with = "message<groupchat";
176 }); 196 });
177 197
178 if not data then 198 if not data then
179 origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); 199 module:log("debug", "Archive query id=%s failed: %s", qid or stanza.attr.id, err);
200 if err == "item-not-found" then
201 origin.send(st.error_reply(stanza, "modify", "item-not-found"));
202 else
203 origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
204 end
180 return true; 205 return true;
181 end 206 end
182 local total = tonumber(err); 207 local total = tonumber(err);
183 208
184 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; 209 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
231 origin.send(results[i]); 256 origin.send(results[i]);
232 end 257 end
233 first, last = last, first; 258 first, last = last, first;
234 end 259 end
235 260
236 -- That's all folks!
237 module:log("debug", "Archive query %s completed", tostring(qid));
238 261
239 origin.send(st.reply(stanza) 262 origin.send(st.reply(stanza)
240 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) 263 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
241 :add_child(rsm.generate { 264 :add_child(rsm.generate {
242 first = first, last = last, count = total })); 265 first = first, last = last, count = total }));
266
267 -- That's all folks!
268 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; 269 return true;
244 end); 270 end);
245 271
246 module:hook("muc-get-history", function (event) 272 module:hook("muc-get-history", function (event)
247 local room = event.room; 273 local room = event.room;
272 with = "message<groupchat"; 298 with = "message<groupchat";
273 } 299 }
274 local data, err = archive:find(jid_split(room_jid), query); 300 local data, err = archive:find(jid_split(room_jid), query);
275 301
276 if not data then 302 if not data then
277 module:log("error", "Could not fetch history: %s", tostring(err)); 303 module:log("error", "Could not fetch history: %s", err);
278 return 304 return
279 end 305 end
280 306
281 local history, i = {}, 1; 307 local history, i = {}, 1;
282 308
298 break 324 break
299 end 325 end
300 maxchars = maxchars - chars; 326 maxchars = maxchars - chars;
301 end 327 end
302 history[i], i = item, i+1; 328 history[i], i = item, i+1;
303 -- module:log("debug", tostring(item)); 329 -- module:log("debug", item);
304 end 330 end
305 function event.next_stanza() 331 function event.next_stanza()
306 i = i - 1; 332 i = i - 1;
307 return history[i]; 333 return history[i];
308 end 334 end
326 352
327 end, 0); 353 end, 0);
328 354
329 -- Handle messages 355 -- Handle messages
330 local function save_to_history(self, stanza) 356 local function save_to_history(self, stanza)
331 local room_node, room_host = jid_split(self.jid); 357 local room_node = jid_split(self.jid);
332 358
333 local stored_stanza = stanza; 359 local stored_stanza = stanza;
334 360
335 if stanza.name == "message" and self:get_whois() == "anyone" then 361 if stanza.name == "message" and self:get_whois() == "anyone" then
336 stored_stanza = st.clone(stanza); 362 stored_stanza = st.clone(stanza);
350 if stanza.attr.type then 376 if stanza.attr.type then
351 with = with .. "<" .. stanza.attr.type 377 with = with .. "<" .. stanza.attr.type
352 end 378 end
353 379
354 -- And stash it 380 -- And stash it
355 local id = archive:append(room_node, nil, stored_stanza, time_now(), with); 381 local time = time_now();
382 local id, err = archive:append(room_node, nil, stored_stanza, time, with);
383
384 if not id and err == "quota-limit" then
385 if type(cleanup_after) == "number" then
386 module:log("debug", "Room '%s' over quota, cleaning archive", room_node);
387 local cleaned = archive:delete(room_node, {
388 ["end"] = (os.time() - cleanup_after);
389 });
390 if cleaned then
391 id, err = archive:append(room_node, nil, stored_stanza, time, with);
392 end
393 end
394 if not id and (archive.caps and archive.caps.truncate) then
395 module:log("debug", "User '%s' over quota, truncating archive", room_node);
396 local truncated = archive:delete(room_node, {
397 truncate = archive_item_limit - 1;
398 });
399 if truncated then
400 id, err = archive:append(room_node, nil, stored_stanza, time, with);
401 end
402 end
403 end
356 404
357 if id then 405 if id then
358 schedule_cleanup(room_node); 406 schedule_cleanup(room_node);
359 stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id })); 407 stanza:add_direct_child(st.stanza("stanza-id", { xmlns = xmlns_st_id, by = self.jid, id = id }));
360 else 408 else
389 -- And role/affiliation changes? 437 -- And role/affiliation changes?
390 438
391 module:add_feature(xmlns_mam); 439 module:add_feature(xmlns_mam);
392 440
393 module:hook("muc-disco#info", function(event) 441 module:hook("muc-disco#info", function(event)
394 event.reply:tag("feature", {var=xmlns_mam}):up(); 442 if archiving_enabled(event.room) then
443 event.reply:tag("feature", {var=xmlns_mam}):up();
444 end
395 end); 445 end);
396 446
397 -- Cleanup 447 -- Cleanup
398
399 local cleanup_after = module:get_option_string("muc_log_expires_after", "1w");
400 local cleanup_interval = module:get_option_number("muc_log_cleanup_interval", 4 * 60 * 60);
401 448
402 if cleanup_after ~= "never" then 449 if cleanup_after ~= "never" then
403 local cleanup_storage = module:open_store("muc_log_cleanup"); 450 local cleanup_storage = module:open_store("muc_log_cleanup");
404 local cleanup_map = module:open_store("muc_log_cleanup", "map"); 451 local cleanup_map = module:open_store("muc_log_cleanup", "map");
405 452