Mercurial > prosody-hg
diff plugins/mod_offline.lua @ 13995:30d457b2b0c4
mod_offline: Support for expiring and cleaning up old offline messages
This is achieved using the new `offline_expires_after` option, which is
practically identical to the same option used for mod_mam expiry (the code is
also practically identical).
With MAM-only clients, it's possible that messages could end up in the offline
queue, but never collected (they would sit there waiting for a legacy client
to come online). This doesn't happen much so far because most clients are
"legacy" by the heuristic mod_mam is using (MAM request before initial
presence), but that is changing.
This option also allows simpler retention policies for server operators.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Thu, 04 Dec 2025 14:07:41 +0000 |
| parents | 74b9e05af71e |
| children |
line wrap: on
line diff
--- a/plugins/mod_offline.lua Thu Dec 04 11:39:34 2025 +0000 +++ b/plugins/mod_offline.lua Thu Dec 04 14:07:41 2025 +0000 @@ -9,11 +9,22 @@ local datetime = require "prosody.util.datetime"; local jid_split = require "prosody.util.jid".split; +local datestamp = require "prosody.util.datetime".date; + +local cleanup_after = module:get_option_period("offline_expires_after", "never"); local offline_messages = module:open_store("offline", "archive"); module:add_feature("msgoffline"); +function schedule_cleanup(_username, _date) -- luacheck: ignore 212 + -- Called to make a note of which users have messages on which days, which in + -- turn is used to optimize the message expiry routine. + -- + -- This noop is conditionally replaced later depending on retention settings + -- and storage backend capabilities. +end + module:hook("message/offline/handle", function(event) local origin, stanza = event.origin, event.stanza; local to = stanza.attr.to; @@ -26,6 +37,7 @@ local ok = offline_messages:append(node, nil, stanza, os.time(), ""); if ok then + schedule_cleanup(node); module:log("debug", "Saved to offline storage: %s", stanza:top_tag()); end return ok; @@ -49,3 +61,93 @@ end return true; end, -1); + +if cleanup_after ~= math.huge then + local cleanup_storage = module:open_store("offline_cleanup"); + local cleanup_map = module:open_store("offline_cleanup", "map"); + + module:log("debug", "offline_expires_after = %d -- in seconds", cleanup_after); + + if not offline_messages.delete then + module:log("error", "offline_expires_after set but mod_%s does not support deleting", offline_messages._provided_by); + return false; + end + + -- For each day, store a set of users that have new messages. To expire + -- messages, we collect the union of sets of users from dates that fall + -- outside the cleanup range. + + if not (offline_messages.caps and offline_messages.caps.wildcard_delete) then + local last_date = require "prosody.util.cache".new(module:get_option_integer("archive_cleanup_date_cache_size", 1000, 1)); + function schedule_cleanup(username, date) + date = date or datestamp(); + if last_date:get(username) == date then return end + local ok = cleanup_map:set(date, username, true); + if ok then + last_date:set(username, date); + end + end + end + + local cleanup_time = module:measure("cleanup", "times"); + + local async = require "prosody.util.async"; + module:daily("Remove expired offline messages", function () + local cleanup_done = cleanup_time(); + + if offline_messages.caps and offline_messages.caps.wildcard_delete then + local ok, err = offline_messages:delete(true, { ["end"] = os.time() - cleanup_after }) + if ok then + local sum = tonumber(ok); + if sum then + module:log("info", "Deleted %d expired messages", sum); + else + -- driver did not tell + module:log("info", "Deleted all expired messages"); + end + else + module:log("error", "Could not delete messages: %s", err); + end + cleanup_done(); + return; + end + + local users = {}; + local cut_off = datestamp(os.time() - cleanup_after); + for date in cleanup_storage:users() do + if date <= cut_off then + module:log("debug", "Messages from %q should be expired", date); + local messages_this_day = cleanup_storage:get(date); + if messages_this_day then + for user in pairs(messages_this_day) do + users[user] = true; + end + if date < cut_off then + -- Messages from the same day as the cut-off might not have expired yet, + -- but all earlier will have, so clear storage for those days. + cleanup_storage:set(date, nil); + end + end + end + end + local sum, num_users = 0, 0; + for user in pairs(users) do + local ok, err = offline_messages:delete(user, { ["end"] = os.time() - cleanup_after; }) + if ok then + num_users = num_users + 1; + sum = sum + (tonumber(ok) or 0); + else + cleanup_map:set(cut_off, user, true); + module:log("error", "Could not delete messages for user '%s': %s", user, err); + end + local wait, done = async.waiter(); + module:add_timer(0.01, done); + wait(); + end + module:log("info", "Deleted %d expired messages for %d users", sum, num_users); + cleanup_done(); + end); + +else + module:log("debug", "Offline message expiry disabled"); +end
