File

mod_storage_metronome_readonly/mod_storage_metronome_readonly.lua @ 6154:160d1bcfe341

mod_anti_spam: Optimization to avoid needless string concatenation
author Matthew Wild <mwild1@gmail.com>
date Fri, 17 Jan 2025 10:32:34 +0000 (2 months ago)
parent 6147:6ba0489e4828
child 6165:11650fe276c0
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 id = require "prosody.util.id".medium;
local set = require "prosody.util.set";
local envloadfile = require"prosody.util.envload".envloadfile;

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 when = math.floor(now());

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

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()
			for node in nodes do
				nodes:remove(node);
				return node;
			end
		end;
	end
end

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

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

function keyval:get(user)
	if self.store == "pep" then
		local data = datamanager.load(user, host, self.store);
		local nodes = data["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 data = envloadfile(path, {});
			if not data then
				log("error", "Failed to load metronome storage");
				return nil, "Error reading storage";
			end
			local success, data = pcall(data);
			if not success then
				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 data = datamanager.load(user, host, "push");
		local result = {};
		for jid, data in pairs(data) 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)
	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 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)
	return nil, "unsupported-store";
end

function archive:find(username, query)
	if self.store == "archive" then
		local jid = username.."@"..host;
		local data = datamanager.load(username, host, "archiving");
		local iter = ipairs(data["logs"]);
		local i = 0;
		local message;
		return function()
			i, message = iter(data["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;

	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 data = envloadfile(path, {});
		if not data then
			log("debug", "Failed to load metronome storage");
			return {};
		end
		local success, data = pcall(data);
		if not success then
			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, when, 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();
			print(item)
			if item == nil then
				if list.close then
					list:close();
				end
				return
			end
			print(0)
			local key = id();
			local when = item.attr and datetime.parse(item.attr.stamp);
			local with = "";
			print(1)
			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);
			print(key, when, with, item)
			return key, item, when, with;
		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)
	return nil, "unsupported-store";
end

function archive:dates(username)
	return nil, "unsupported-store";
end

function archive:summary(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");
	else
		return nil, "unsupported-store";
	end
end

function archive:trim(username, to_when)
	return nil, "unsupported-store";
end

function archive:delete(username, query)
	return nil, "unsupported-store";
end

module:provides("storage", driver);