comparison plugins/mod_mam/mod_mam.lua @ 11200:bf8f2da84007

Merge 0.11->trunk
author Kim Alvefur <zash@zash.se>
date Thu, 05 Nov 2020 22:31:25 +0100
parents 30fc1ed5647a
children 08b397c21805
comparison
equal deleted inserted replaced
11199:6c7c50a4de32 11200:bf8f2da84007
23 local prefs_from_stanza = module:require"mamprefsxml".fromstanza; 23 local prefs_from_stanza = module:require"mamprefsxml".fromstanza;
24 local jid_bare = require "util.jid".bare; 24 local jid_bare = require "util.jid".bare;
25 local jid_split = require "util.jid".split; 25 local jid_split = require "util.jid".split;
26 local jid_prepped_split = require "util.jid".prepped_split; 26 local jid_prepped_split = require "util.jid".prepped_split;
27 local dataform = require "util.dataforms".new; 27 local dataform = require "util.dataforms".new;
28 local get_form_type = require "util.dataforms".get_type;
28 local host = module.host; 29 local host = module.host;
29 30
30 local rm_load_roster = require "core.rostermanager".load_roster; 31 local rm_load_roster = require "core.rostermanager".load_roster;
31 32
32 local is_stanza = st.is_stanza; 33 local is_stanza = st.is_stanza;
37 local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50); 38 local default_max_items, max_max_items = 20, module:get_option_number("max_archive_query_results", 50);
38 local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" }); 39 local strip_tags = module:get_option_set("dont_archive_namespaces", { "http://jabber.org/protocol/chatstates" });
39 40
40 local archive_store = module:get_option_string("archive_store", "archive"); 41 local archive_store = module:get_option_string("archive_store", "archive");
41 local archive = module:open_store(archive_store, "archive"); 42 local archive = module:open_store(archive_store, "archive");
43
44 local cleanup_after = module:get_option_string("archive_expires_after", "1w");
45 local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
46 local archive_item_limit = module:get_option_number("storage_archive_item_limit", archive.caps and archive.caps.quota or 1000);
47 local archive_truncate = math.floor(archive_item_limit * 0.99);
42 48
43 if not archive.find then 49 if not archive.find then
44 error("mod_"..(archive._provided_by or archive.name and "storage_"..archive.name).." does not support archiving\n" 50 error("mod_"..(archive._provided_by or archive.name and "storage_"..archive.name).." does not support archiving\n"
45 .."See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information"); 51 .."See https://prosody.im/doc/storage and https://prosody.im/doc/archiving for more information");
46 end 52 end
96 102
97 -- Search query parameters 103 -- Search query parameters
98 local qwith, qstart, qend; 104 local qwith, qstart, qend;
99 local form = query:get_child("x", "jabber:x:data"); 105 local form = query:get_child("x", "jabber:x:data");
100 if form then 106 if form then
101 local err; 107 local form_type, err = get_form_type(form);
108 if not form_type then
109 origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid dataform: "..err));
110 return true;
111 elseif form_type ~= xmlns_mam then
112 origin.send(st.error_reply(stanza, "modify", "bad-request", "Unexpected FORM_TYPE, expected '"..xmlns_mam.."'"));
113 return true;
114 end
102 form, err = query_form:data(form); 115 form, err = query_form:data(form);
103 if err then 116 if err then
104 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err)))); 117 origin.send(st.error_reply(stanza, "modify", "bad-request", select(2, next(err))));
105 return true; 118 return true;
106 end 119 end
115 return true; 128 return true;
116 end 129 end
117 qstart, qend = vstart, vend; 130 qstart, qend = vstart, vend;
118 end 131 end
119 132
120 module:log("debug", "Archive query, id %s with %s from %s until %s", 133 module:log("debug", "Archive query by %s id=%s with=%s when=%s...%s",
121 tostring(qid), qwith or "anyone", 134 origin.username,
122 qstart and timestamp(qstart) or "the dawn of time", 135 qid or stanza.attr.id,
123 qend and timestamp(qend) or "now"); 136 qwith or "*",
137 qstart and timestamp(qstart) or "",
138 qend and timestamp(qend) or "");
124 139
125 -- RSM stuff 140 -- RSM stuff
126 local qset = rsm.get(query); 141 local qset = rsm.get(query);
127 local qmax = m_min(qset and qset.max or default_max_items, max_max_items); 142 local qmax = m_min(qset and qset.max or default_max_items, max_max_items);
128 local reverse = qset and qset.before or false; 143 local reverse = qset and qset.before or false;
129 local before, after = qset and qset.before, qset and qset.after; 144 local before, after = qset and qset.before, qset and qset.after;
130 if type(before) ~= "string" then before = nil; end 145 if type(before) ~= "string" then before = nil; end
146 if qset then
147 module:log("debug", "Archive query id=%s rsm=%q", qid or stanza.attr.id, qset);
148 end
131 149
132 -- Load all the data! 150 -- Load all the data!
133 local data, err = archive:find(origin.username, { 151 local data, err = archive:find(origin.username, {
134 start = qstart; ["end"] = qend; -- Time range 152 start = qstart; ["end"] = qend; -- Time range
135 with = qwith; 153 with = qwith;
138 reverse = reverse; 156 reverse = reverse;
139 total = use_total or qmax == 0; 157 total = use_total or qmax == 0;
140 }); 158 });
141 159
142 if not data then 160 if not data then
143 origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err)); 161 module:log("debug", "Archive query id=%s failed: %s", qid or stanza.attr.id, err);
162 if err == "item-not-found" then
163 origin.send(st.error_reply(stanza, "modify", "item-not-found"));
164 else
165 origin.send(st.error_reply(stanza, "cancel", "internal-server-error"));
166 end
144 return true; 167 return true;
145 end 168 end
146 local total = tonumber(err); 169 local total = tonumber(err);
147 170
148 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to }; 171 local msg_reply_attr = { to = stanza.attr.from, from = stanza.attr.to };
187 origin.send(results[i]); 210 origin.send(results[i]);
188 end 211 end
189 first, last = last, first; 212 first, last = last, first;
190 end 213 end
191 214
192 -- That's all folks!
193 module:log("debug", "Archive query %s completed", tostring(qid));
194
195 origin.send(st.reply(stanza) 215 origin.send(st.reply(stanza)
196 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete }) 216 :tag("fin", { xmlns = xmlns_mam, queryid = qid, complete = complete })
197 :add_child(rsm.generate { 217 :add_child(rsm.generate {
198 first = first, last = last, count = total })); 218 first = first, last = last, count = total }));
219
220 -- That's all folks!
221 module:log("debug", "Archive query id=%s completed, %d items returned", qid or stanza.attr.id, complete and count or count - 1);
199 return true; 222 return true;
200 end); 223 end);
201 224
202 local function has_in_roster(user, who) 225 local function has_in_roster(user, who)
203 local roster = rm_load_roster(user, host); 226 local roster = rm_load_roster(user, host);
211 module:log("debug", "%s@%s does not exist", user, host) 234 module:log("debug", "%s@%s does not exist", user, host)
212 return false; 235 return false;
213 end 236 end
214 local prefs = get_prefs(user); 237 local prefs = get_prefs(user);
215 local rule = prefs[who]; 238 local rule = prefs[who];
216 module:log("debug", "%s's rule for %s is %s", user, who, tostring(rule)); 239 module:log("debug", "%s's rule for %s is %s", user, who, rule);
217 if rule ~= nil then 240 if rule ~= nil then
218 return rule; 241 return rule;
219 end 242 end
220 -- Below could be done by a metatable 243 -- Below could be done by a metatable
221 local default = prefs[false]; 244 local default = prefs[false];
222 module:log("debug", "%s's default rule is %s", user, tostring(default)); 245 module:log("debug", "%s's default rule is %s", user, default);
223 if default == "roster" then 246 if default == "roster" then
224 return has_in_roster(user, who); 247 return has_in_roster(user, who);
225 end 248 end
226 return default; 249 return default;
227 end 250 end
240 end); 263 end);
241 end 264 end
242 return stanza; 265 return stanza;
243 end 266 end
244 267
268 local function should_store(stanza, c2s) --> boolean, reason: string
269 local st_type = stanza.attr.type or "normal";
270 -- FIXME pass direction of stanza and use that along with bare/full JID addressing
271 -- for more accurate MUC / type=groupchat check
272
273 if st_type == "headline" then
274 -- Headline messages are ephemeral by definition
275 return false, "headline";
276 end
277 if st_type == "error" and not c2s then
278 -- Store delivery failure notifications so you know if your own messages were not delivered
279 return true, "bounce";
280 end
281 if st_type == "groupchat" then
282 -- MUC messages always go to the full JID, usually archived by the MUC
283 return false, "groupchat";
284 end
285 if stanza:get_child("no-store", "urn:xmpp:hints")
286 or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then
287 -- XXX Experimental XEP
288 return false, "hint";
289 end
290 if stanza:get_child("store", "urn:xmpp:hints") then
291 return true, "hint";
292 end
293 if stanza:get_child("body") then
294 return true, "body";
295 end
296 if stanza:get_child("subject") then
297 -- XXX Who would send a message with a subject but without a body?
298 return true, "subject";
299 end
300 if stanza:get_child("encryption", "urn:xmpp:eme:0") then
301 -- Since we can't know what an encrypted message contains, we assume it's important
302 -- XXX Experimental XEP
303 return true, "encrypted";
304 end
305 if stanza:get_child(nil, "urn:xmpp:receipts") then
306 -- If it's important enough to ask for a receipt then it's important enough to archive
307 -- and the same applies to the receipt
308 return true, "receipt";
309 end
310 if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then
311 -- XXX Experimental XEP
312 return true, "marker";
313 end
314 if stanza:get_child("x", "jabber:x:conference")
315 or stanza:find("{http://jabber.org/protocol/muc#user}x/invite") then
316 return true, "invite";
317 end
318 if stanza:get_child(nil, "urn:xmpp:jingle-message:0") then
319 -- XXX Experimental XEP stuck in Proposed for almost a year at the time of this comment
320 return true, "jingle call";
321 end
322
323 -- The IM-NG thing to do here would be to return `not st_to_full`
324 -- One day ...
325 return false, "default";
326 end
327
245 -- Handle messages 328 -- Handle messages
246 local function message_handler(event, c2s) 329 local function message_handler(event, c2s)
247 local origin, stanza = event.origin, event.stanza; 330 local origin, stanza = event.origin, event.stanza;
248 local log = c2s and origin.log or module._log; 331 local log = c2s and origin.log or module._log;
249 local orig_type = stanza.attr.type or "normal";
250 local orig_from = stanza.attr.from; 332 local orig_from = stanza.attr.from;
251 local orig_to = stanza.attr.to or orig_from; 333 local orig_to = stanza.attr.to or orig_from;
252 -- Stanza without 'to' are treated as if it was to their own bare jid 334 -- Stanza without 'to' are treated as if it was to their own bare jid
253 335
254 -- Whos storage do we put it in? 336 -- Whos storage do we put it in?
257 local with = jid_bare(c2s and orig_to or orig_from); 339 local with = jid_bare(c2s and orig_to or orig_from);
258 340
259 -- Filter out <stanza-id> that claim to be from us 341 -- Filter out <stanza-id> that claim to be from us
260 event.stanza = strip_stanza_id(stanza, store_user); 342 event.stanza = strip_stanza_id(stanza, store_user);
261 343
262 -- We store chat messages or normal messages that have a body 344 local should, why = should_store(stanza, c2s);
263 if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body")) ) then 345 if not should then
264 log("debug", "Not archiving stanza: %s (type)", stanza:top_tag()); 346 log("debug", "Not archiving stanza: %s (%s)", stanza:top_tag(), why);
265 return; 347 return;
266 end
267
268 -- or if hints suggest we shouldn't
269 if not stanza:get_child("store", "urn:xmpp:hints") then -- No hint telling us we should store
270 if stanza:get_child("no-permanent-store", "urn:xmpp:hints")
271 or stanza:get_child("no-store", "urn:xmpp:hints") then -- Hint telling us we should NOT store
272 log("debug", "Not archiving stanza: %s (hint)", stanza:top_tag());
273 return;
274 end
275 end 348 end
276 349
277 local clone_for_storage; 350 local clone_for_storage;
278 if not strip_tags:empty() then 351 if not strip_tags:empty() then
279 clone_for_storage = st.clone(stanza); 352 clone_for_storage = st.clone(stanza);
292 clone_for_storage = stanza; 365 clone_for_storage = stanza;
293 end 366 end
294 367
295 -- Check with the users preferences 368 -- Check with the users preferences
296 if shall_store(store_user, with) then 369 if shall_store(store_user, with) then
297 log("debug", "Archiving stanza: %s", stanza:top_tag()); 370 log("debug", "Archiving stanza: %s (%s)", stanza:top_tag(), why);
298 371
299 -- And stash it 372 -- And stash it
300 local ok, err = archive:append(store_user, nil, clone_for_storage, time_now(), with); 373 local time = time_now();
374 local ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
375 if not ok and err == "quota-limit" then
376 if type(cleanup_after) == "number" then
377 module:log("debug", "User '%s' over quota, cleaning archive", store_user);
378 local cleaned = archive:delete(store_user, {
379 ["end"] = (os.time() - cleanup_after);
380 });
381 if cleaned then
382 ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
383 end
384 end
385 if not ok and (archive.caps and archive.caps.truncate) then
386 module:log("debug", "User '%s' over quota, truncating archive", store_user);
387 local truncated = archive:delete(store_user, {
388 truncate = archive_truncate;
389 });
390 if truncated then
391 ok, err = archive:append(store_user, nil, clone_for_storage, time, with);
392 end
393 end
394 end
301 if ok then 395 if ok then
302 local clone_for_other_handlers = st.clone(stanza); 396 local clone_for_other_handlers = st.clone(stanza);
303 local id = ok; 397 local id = ok;
304 clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up(); 398 clone_for_other_handlers:tag("stanza-id", { xmlns = xmlns_st_id, by = store_user.."@"..host, id = id }):up();
305 event.stanza = clone_for_other_handlers; 399 event.stanza = clone_for_other_handlers;
323 end 417 end
324 418
325 module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1); 419 module:hook("pre-message/bare", strip_stanza_id_after_other_events, -1);
326 module:hook("pre-message/full", strip_stanza_id_after_other_events, -1); 420 module:hook("pre-message/full", strip_stanza_id_after_other_events, -1);
327 421
328 local cleanup_after = module:get_option_string("archive_expires_after", "1w");
329 local cleanup_interval = module:get_option_number("archive_cleanup_interval", 4 * 60 * 60);
330 if cleanup_after ~= "never" then 422 if cleanup_after ~= "never" then
331 local cleanup_storage = module:open_store("archive_cleanup"); 423 local cleanup_storage = module:open_store("archive_cleanup");
332 local cleanup_map = module:open_store("archive_cleanup", "map"); 424 local cleanup_map = module:open_store("archive_cleanup", "map");
333 425
334 local day = 86400; 426 local day = 86400;
359 local ok = cleanup_map:set(date, username, true); 451 local ok = cleanup_map:set(date, username, true);
360 if ok then 452 if ok then
361 last_date:set(username, date); 453 last_date:set(username, date);
362 end 454 end
363 end 455 end
456 local cleanup_time = module:measure("cleanup", "times");
364 457
365 local async = require "util.async"; 458 local async = require "util.async";
366 cleanup_runner = async.runner(function () 459 cleanup_runner = async.runner(function ()
460 local cleanup_done = cleanup_time();
367 local users = {}; 461 local users = {};
368 local cut_off = datestamp(os.time() - cleanup_after); 462 local cut_off = datestamp(os.time() - cleanup_after);
369 for date in cleanup_storage:users() do 463 for date in cleanup_storage:users() do
370 if date <= cut_off then 464 if date <= cut_off then
371 module:log("debug", "Messages from %q should be expired", date); 465 module:log("debug", "Messages from %q should be expired", date);
395 local wait, done = async.waiter(); 489 local wait, done = async.waiter();
396 module:add_timer(0.01, done); 490 module:add_timer(0.01, done);
397 wait(); 491 wait();
398 end 492 end
399 module:log("info", "Deleted %d expired messages for %d users", sum, num_users); 493 module:log("info", "Deleted %d expired messages for %d users", sum, num_users);
494 cleanup_done();
400 end); 495 end);
401 496
402 cleanup_task = module:add_timer(1, function () 497 cleanup_task = module:add_timer(1, function ()
403 cleanup_runner:run(true); 498 cleanup_runner:run(true);
404 return cleanup_interval; 499 return cleanup_interval;