view mod_storage_metronome_readonly/mod_storage_metronome_readonly.lua @ 6319:04c3273cb81f

mod_auth_cyrus: Add empty 'profile' table to SASL handler objects This is for compatibility with Prosody's built-in util.sasl objects. A SASL profile table usually includes methods supported by the backend, which can be used by SASL mechanism handlers to perform operations (such as testing the password). It also optionally contains a 'cb' field with channel binding method handlers. The Cyrus backend doesn't support channel binding, and doesn't have the same concept of auth backend methods (it handles all that internally, and Prosody has no insight or control over it). Thus, we create an empty profile which informs Prosody that the SASL handler does not support any of the auth or channel binding methods. Some features will not work, but they didn't work anyway. This just makes it explicit. This fixes a traceback in mod_sasl2_fast, which expected SASL handlers to always contain a 'profile' field.
author Matthew Wild <mwild1@gmail.com>
date Thu, 04 Sep 2025 10:14:46 +0100
parents 877416deacfa
children
line wrap: on
line source

local datamanager = require "prosody.core.storagemanager".olddm;
local datetime = require "prosody.util.datetime";
local st = require "prosody.util.stanza";
local now = require "prosody.util.time".now;
local gen_id = require "prosody.util.id".medium;
local set = require "prosody.util.set";
local envloadfile = require"prosody.util.envload".envloadfile;
local dir = require "lfs".dir;

local host = module.host;

local archive_item_limit = module:get_option_integer("storage_archive_item_limit", 10000, 0);

-- Metronome doesn’t store the item publish time, so fallback to the migration time.
local time_now = math.floor(now());

local function encode (s)
        return s and (s:gsub("%W", function (c) return string.format("%%%02x", c:byte()); end));
end

local file = io.open("/etc/mime.types");
local mimes = {};
while true do
	local line = file:read("*l");
	if not line then
		break;
	end
	if line ~= "" then
		local first_char = line:sub(1, 1);
		if first_char ~= "#" then
			--local line:match("(%S+)%s+"));
			local match = line:gmatch("%S+");
			local mime = match();
			for ext in match do
				mimes[ext] = mime;
			end
		end
	end
end
file:close();

local driver = {};

function driver:open(store, typ)
	local mt = self[typ or "keyval"]
	if not mt then
		return nil, "unsupported-store";
	end
	return setmetatable({ store = store, type = typ }, mt);
end

function driver:stores(username) -- luacheck: ignore 212/self
	if username == true then
		local nodes = set.new();
		for user in datamanager.users(host, "pep") do
			local data = datamanager.load(user, host, "pep");

			for _, node in ipairs(data["nodes"]) do
				nodes:add("pep_" .. node);
			end
		end
		return function()
			-- luacheck: ignore 512
			for node in nodes do
				nodes:remove(node);
				return node;
			end
		end;
	end
end

function driver:purge(user) -- luacheck: ignore 212/self user
	return nil, "unsupported-store";
end

local keyval = { };
driver.keyval = { __index = keyval };

function keyval:get(user)
	if self.store == "pep" then
		local ret = datamanager.load(user, host, self.store);
		local nodes = ret["nodes"];
		local result = {};

		local pep_base_path = datamanager.getpath(user, host, self.store):sub(1, -5);

		for _, node in ipairs(nodes) do
			local path = ("%s/%s.dat"):format(pep_base_path, encode(node));
			local get_data = envloadfile(path, {});
			if not get_data then
				module:log("error", "Failed to load metronome storage");
				return nil, "Error reading storage";
			end
			local success, data = pcall(get_data);
			if not success then
				module:log("error", "Unable to load metronome storage");
				return nil, "Error reading storage";
			end
			local new_node = {};
			new_node["name"] = node;
			new_node["subscribers"] = data["subscribers"];
			new_node["affiliations"] = data["affiliations"];
			new_node["config"] = data["config"];
			result[node] = new_node;
		end
		return result;
	elseif self.store == "cloud_notify" then
		local ret = datamanager.load(user, host, "push");
		local result = {};
		for jid, data in pairs(ret) do
			local secret = data["secret"];
			for node in pairs(data["nodes"]) do
				-- TODO: Does Metronome store more info than that?
				local options;
				if secret then
					options = st.preserialize(st.stanza("x", { xmlns = "jabber:x:data", type = "submit" })
						:tag("field", { var = "FORM_TYPE" })
							:text_tag("value", "http://jabber.org/protocol/pubsub#publish-options")
						:up()
						:tag("field", { var = "secret" })
							:text_tag("value", secret));
				end
				result[jid.."<"..node] = {
					jid = jid,
					node = node,
					options = options,
				};
			end
		end
		return result;
	elseif self.store == "roster" then
		return datamanager.load(user, host, self.store);
	elseif self.store == "vcard" then
		return datamanager.load(user, host, self.store);
	elseif self.store == "private" then
		return datamanager.load(user, host, self.store);

	-- After that, handle MUC specific stuff, not tested yet whatsoever.
	elseif self.store == "persistent" then
		return datamanager.load(user, host, self.store);
	elseif self.store == "config" then
		return datamanager.load(user, host, self.store);
	elseif self.store == "vcard_muc" then
		local data = datamanager.load(user, host, "room_icons");
		return data and data["photo"];
	else
		return nil, "unsupported-store";
	end
end

function keyval:set(user, data) -- luacheck: ignore 212/self user data
	return nil, "unsupported-store";
end

function keyval:users()
	local store;
	if self.store == "vcard_muc" then
		store = "room_icons";
	elseif self.store == "cloud_notify" then
		store = "push";
	else
		store = self.store;
	end
	return datamanager.users(host, store, self.type);
end

local function parse_logs(logs, jid)
	local iter = ipairs(logs);
	local i = 0;
	local message;
	return function()
		i, message = iter(logs, i);
		if not message then
			return;
		end

		local with;
		local bare_to = message["bare_to"];
		local bare_from = message["bare_from"];
		if jid == bare_to then
			-- received
			with = bare_from;
		else
			-- sent
			with = bare_to;
		end

		local to = message["to"];
		local from = message["from"];
		local id = message["id"];
		local type = message["type"];

		local key = message["uid"];
		local when = message["timestamp"];
		local item = st.message({ to = to, from = from, id = id, type = type }, message["body"]);
		if message["tags"] then
			for _, tag in ipairs(message["tags"]) do
				setmetatable(tag, st.stanza_mt);
				item:add_direct_child(tag);
			end
		end
		if message["marker"] then
			item:tag(message["marker"], { xmlns = "urn:xmpp:chat-markers:0", id = message["marker_id"] });
		end
		return key, item, when, with;
	end;
end

local archive = {};
driver.archive = { __index = archive };

archive.caps = {
	total = true;
	quota = archive_item_limit;
	full_id_range = true;
	ids = true;
};

function archive:append(username, key, value, when, with) -- luacheck: ignore 212/self username key value when with
	return nil, "unsupported-store";
end

function archive:find(username, query) -- luacheck: ignore 212/self query
	if self.store == "archive" then
		local jid = username.."@"..host;
		local data = datamanager.load(username, host, "archiving");
		return parse_logs(data["logs"], jid);

	elseif self.store:sub(1, 4) == "pep_" then
		local node = self.store:sub(5);

		local pep_base_path = datamanager.getpath(username, host, "pep"):sub(1, -5);
		local path = ("%s/%s.dat"):format(pep_base_path, encode(node));
		local get_data = envloadfile(path, {});
		if not get_data then
			module:log("debug", "Failed to load metronome storage");
			return {};
		end
		local success, data = pcall(get_data);
		if not success then
			module:log("error", "Unable to load metronome storage");
			return nil, "Error reading storage";
		end

		local iter = pairs(data["data"]);
		local key = nil;
		local payload;
		return function()
			key, payload = iter(data["data"], key);
			if not key then
				return;
			end
			local item = st.deserialize(payload[1]);
			local with = data["data_author"][key];
			return key, item, time_now, with;
		end;

	elseif self.store == "offline" then
		-- This is mostly copy/pasted from mod_storage_internal.
		local list, err = datamanager.list_open(username, host, self.store);
		if not list then
			if err then
				return list, err;
			end
			return function()
			end;
		end

		local i = 0;
		local iter = function()
			i = i + 1;
			return list[i];
		end

		return function()
			local item = iter();
			if item == nil then
				if list.close then
					list:close();
				end
				return
			end
			local key = gen_id();
			local when = item.attr and datetime.parse(item.attr.stamp);
			local with = "";
			item.key, item.when, item.with = nil, nil, nil;
			item.attr.stamp = nil;
			-- COMPAT Stored data may still contain legacy XEP-0091 timestamp
			item.attr.stamp_legacy = nil;
			item = st.deserialize(item);
			return key, item, when, with;
		end

	elseif self.store == "uploads" then
		local list = {};

		for user in datamanager.users(host, "http_upload", "list") do
			local data, err = datamanager.list_open(user, host, "http_upload");
			if not data then
				if err then
					return data, err;
				end
				return function()
				end;
			end

			for _, stuff in ipairs(data) do
				local key = stuff.dir;
				local size = tostring(stuff.size);
				local time = stuff.time;
				local filename = stuff.filename;
				local ext = filename:match(".*%.(%S+)"):lower();
				local mime = mimes[ext] or "application/octet-stream";
				local stanza = st.stanza("request", { xmlns = "urn:xmpp:http:upload:0", size = size, ["content-type"] = mime, filename = filename })
				list[key] = {user.."@"..host, time, stanza};
			end
		end

		local iter = pairs(list);
		local key = nil;
		local payload;
		return function()
			key, payload = iter(list, key);
			if not key then
				return;
			end
			local with = payload[1];
			local when = payload[2];
			local stanza = payload[3];
			return key, stanza, when, with;
		end;

	elseif self.store == "muc_log" then
		local base_path = datamanager.getpath("", host, "stanza_log"):sub(1, -5);
		local days = {};
		for date in dir(base_path) do
			if date ~= "." and date ~= ".." then
				table.insert(days, date);
			end
		end
		table.sort(days);
		local list = {};
		for _, date in ipairs(days) do
			local path = base_path..date.."/"..encode(username)..".dat";
			local get_data = envloadfile(path, {});
			if get_data then
				local success, data = pcall(get_data);
				if not success then
					module:log("error", "Unable to load metronome storage");
					return nil, "Error reading storage";
				end
				for key, item, when in parse_logs(data) do
					table.insert(list, {key, item, when});
				end
			end
		end

		local i = 0;
		local iter = function()
			i = i + 1;
			return list[i];
		end

		return function()
			local item = iter();
			if item == nil then
				if list.close then
					list:close();
				end
				return
			end
			return item[1], item[2], item[3], "message<groupchat"
		end

	else
		return nil, "unsupported-store";
	end
end

function archive:get(username, wanted_key)
	local iter, err = self:find(username, { key = wanted_key })
	if not iter then return iter, err; end
	for key, stanza, when, with in iter do
		if key == wanted_key then
			return stanza, when, with;
		end
	end
	return nil, "item-not-found";
end

function archive:set(username, key, new_value, new_when, new_with) -- luacheck: ignore 212/self username key new_value new_when new_with
	return nil, "unsupported-store";
end

function archive:dates(username) -- luacheck: ignore 212/self username
	return nil, "unsupported-store";
end

function archive:summary(username, query) -- luacheck: ignore 212/self username query
	return nil, "unsupported-store";
end

function archive:users()
	if self.store == "archive" then
		return datamanager.users(host, "archiving");
	elseif self.store:sub(1, 4) == "pep_" then
		local wanted_node = self.store:sub(5);
		local iter, tbl = datamanager.users(host, "pep");
		return function()
			while true do
				local user = iter(tbl);
				if not user then
					return;
				end
				local data = datamanager.load(user, host, "pep");
				for _, node in ipairs(data["nodes"]) do
					if node == wanted_node then
						return user;
					end
				end
			end
		end;
	elseif self.store == "offline" then
		return datamanager.users(host, self.store, "list");
	elseif self.store == "uploads" then
		local done = false;
		return function()
			if not done then
				done = true;
				return "";
			end
		end;
	elseif self.store == "muc_log" then
		local iter, tbl = pairs(datamanager.load(nil, host, "persistent"));
		local jid = nil;
		return function()
			jid = iter(tbl, jid);
			if not jid then
				return;
			end
			local user = jid:gsub("@.*", "");
			return user;
		end;
	else
		return nil, "unsupported-store";
	end
end

function archive:trim(username, to_when) -- luacheck: ignore 212/self username to_when
	return nil, "unsupported-store";
end

function archive:delete(username, query) -- luacheck: ignore 212/self username query
	return nil, "unsupported-store";
end

module:provides("storage", driver);