Mercurial > prosody-hg
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 13994:8baaefc4db79 | 13995:30d457b2b0c4 |
|---|---|
| 7 -- | 7 -- |
| 8 | 8 |
| 9 | 9 |
| 10 local datetime = require "prosody.util.datetime"; | 10 local datetime = require "prosody.util.datetime"; |
| 11 local jid_split = require "prosody.util.jid".split; | 11 local jid_split = require "prosody.util.jid".split; |
| 12 local datestamp = require "prosody.util.datetime".date; | |
| 13 | |
| 14 local cleanup_after = module:get_option_period("offline_expires_after", "never"); | |
| 12 | 15 |
| 13 local offline_messages = module:open_store("offline", "archive"); | 16 local offline_messages = module:open_store("offline", "archive"); |
| 14 | 17 |
| 15 module:add_feature("msgoffline"); | 18 module:add_feature("msgoffline"); |
| 19 | |
| 20 function schedule_cleanup(_username, _date) -- luacheck: ignore 212 | |
| 21 -- Called to make a note of which users have messages on which days, which in | |
| 22 -- turn is used to optimize the message expiry routine. | |
| 23 -- | |
| 24 -- This noop is conditionally replaced later depending on retention settings | |
| 25 -- and storage backend capabilities. | |
| 26 end | |
| 16 | 27 |
| 17 module:hook("message/offline/handle", function(event) | 28 module:hook("message/offline/handle", function(event) |
| 18 local origin, stanza = event.origin, event.stanza; | 29 local origin, stanza = event.origin, event.stanza; |
| 19 local to = stanza.attr.to; | 30 local to = stanza.attr.to; |
| 20 local node; | 31 local node; |
| 24 node = origin.username; | 35 node = origin.username; |
| 25 end | 36 end |
| 26 | 37 |
| 27 local ok = offline_messages:append(node, nil, stanza, os.time(), ""); | 38 local ok = offline_messages:append(node, nil, stanza, os.time(), ""); |
| 28 if ok then | 39 if ok then |
| 40 schedule_cleanup(node); | |
| 29 module:log("debug", "Saved to offline storage: %s", stanza:top_tag()); | 41 module:log("debug", "Saved to offline storage: %s", stanza:top_tag()); |
| 30 end | 42 end |
| 31 return ok; | 43 return ok; |
| 32 end, -1); | 44 end, -1); |
| 33 | 45 |
| 47 if type(ok) == "number" and ok > 0 then | 59 if type(ok) == "number" and ok > 0 then |
| 48 origin.log("debug", "%d offline messages consumed"); | 60 origin.log("debug", "%d offline messages consumed"); |
| 49 end | 61 end |
| 50 return true; | 62 return true; |
| 51 end, -1); | 63 end, -1); |
| 64 | |
| 65 if cleanup_after ~= math.huge then | |
| 66 local cleanup_storage = module:open_store("offline_cleanup"); | |
| 67 local cleanup_map = module:open_store("offline_cleanup", "map"); | |
| 68 | |
| 69 module:log("debug", "offline_expires_after = %d -- in seconds", cleanup_after); | |
| 70 | |
| 71 if not offline_messages.delete then | |
| 72 module:log("error", "offline_expires_after set but mod_%s does not support deleting", offline_messages._provided_by); | |
| 73 return false; | |
| 74 end | |
| 75 | |
| 76 -- For each day, store a set of users that have new messages. To expire | |
| 77 -- messages, we collect the union of sets of users from dates that fall | |
| 78 -- outside the cleanup range. | |
| 79 | |
| 80 if not (offline_messages.caps and offline_messages.caps.wildcard_delete) then | |
| 81 local last_date = require "prosody.util.cache".new(module:get_option_integer("archive_cleanup_date_cache_size", 1000, 1)); | |
| 82 function schedule_cleanup(username, date) | |
| 83 date = date or datestamp(); | |
| 84 if last_date:get(username) == date then return end | |
| 85 local ok = cleanup_map:set(date, username, true); | |
| 86 if ok then | |
| 87 last_date:set(username, date); | |
| 88 end | |
| 89 end | |
| 90 end | |
| 91 | |
| 92 local cleanup_time = module:measure("cleanup", "times"); | |
| 93 | |
| 94 local async = require "prosody.util.async"; | |
| 95 module:daily("Remove expired offline messages", function () | |
| 96 local cleanup_done = cleanup_time(); | |
| 97 | |
| 98 if offline_messages.caps and offline_messages.caps.wildcard_delete then | |
| 99 local ok, err = offline_messages:delete(true, { ["end"] = os.time() - cleanup_after }) | |
| 100 if ok then | |
| 101 local sum = tonumber(ok); | |
| 102 if sum then | |
| 103 module:log("info", "Deleted %d expired messages", sum); | |
| 104 else | |
| 105 -- driver did not tell | |
| 106 module:log("info", "Deleted all expired messages"); | |
| 107 end | |
| 108 else | |
| 109 module:log("error", "Could not delete messages: %s", err); | |
| 110 end | |
| 111 cleanup_done(); | |
| 112 return; | |
| 113 end | |
| 114 | |
| 115 local users = {}; | |
| 116 local cut_off = datestamp(os.time() - cleanup_after); | |
| 117 for date in cleanup_storage:users() do | |
| 118 if date <= cut_off then | |
| 119 module:log("debug", "Messages from %q should be expired", date); | |
| 120 local messages_this_day = cleanup_storage:get(date); | |
| 121 if messages_this_day then | |
| 122 for user in pairs(messages_this_day) do | |
| 123 users[user] = true; | |
| 124 end | |
| 125 if date < cut_off then | |
| 126 -- Messages from the same day as the cut-off might not have expired yet, | |
| 127 -- but all earlier will have, so clear storage for those days. | |
| 128 cleanup_storage:set(date, nil); | |
| 129 end | |
| 130 end | |
| 131 end | |
| 132 end | |
| 133 local sum, num_users = 0, 0; | |
| 134 for user in pairs(users) do | |
| 135 local ok, err = offline_messages:delete(user, { ["end"] = os.time() - cleanup_after; }) | |
| 136 if ok then | |
| 137 num_users = num_users + 1; | |
| 138 sum = sum + (tonumber(ok) or 0); | |
| 139 else | |
| 140 cleanup_map:set(cut_off, user, true); | |
| 141 module:log("error", "Could not delete messages for user '%s': %s", user, err); | |
| 142 end | |
| 143 local wait, done = async.waiter(); | |
| 144 module:add_timer(0.01, done); | |
| 145 wait(); | |
| 146 end | |
| 147 module:log("info", "Deleted %d expired messages for %d users", sum, num_users); | |
| 148 cleanup_done(); | |
| 149 end); | |
| 150 | |
| 151 else | |
| 152 module:log("debug", "Offline message expiry disabled"); | |
| 153 end |
