Mercurial > prosody-hg
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; |
