view plugins/mod_offline.lua @ 14016:1df1782ccdfe

prosodyctl shell: Improve process exit codes This fixes a bug introduced in 3326c8a29a86 where prosodyctl shell would always exit with non-zero error codes, even on success. Now it will exit with 0 for success, or 1 if any errors were printed. It also updates the other exit codes in the shell command to help distinguish between different error cases. These are based on the sysexits.h codes, although it is not recommended to rely on that interface.
author Matthew Wild <mwild1@gmail.com>
date Fri, 19 Dec 2025 15:32:26 +0000
parents 30d457b2b0c4
children
line wrap: on
line source

-- Prosody IM
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--


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;
	local node;
	if to then
		node = jid_split(to)
	else
		node = origin.username;
	end

	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;
end, -1);

module:hook("message/offline/broadcast", function(event)
	local origin = event.origin;
	origin.log("debug", "Broadcasting offline messages");

	local node, host = origin.username, origin.host;

	local data = offline_messages:find(node);
	if not data then return true; end
	for _, stanza, when in data do
		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = host, stamp = datetime.datetime(when)}):up(); -- XEP-0203
		origin.send(stanza);
	end
	local ok = offline_messages:delete(node);
	if type(ok) == "number" and ok > 0 then
		origin.log("debug", "%d offline messages consumed");
	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