File

mod_storage_metronome_readonly/mod_storage_metronome_readonly.lua @ 6192:76ae646563ea

Backed out changeset 94399ad6b5ab Unintentional committed changes
author Matthew Wild <mwild1@gmail.com>
date Thu, 06 Feb 2025 10:23:08 +0000 (7 weeks ago)
parent 6174:877416deacfa
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);