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