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