File

mod_profile/mod_profile.lua @ 4935:a85efae90e21

mod_rest: Expand mapping of XEP-0045 join stanza The previous 'join' mapping was apparently lost in translation when swithing to datamapper, so might as well map some properties allowing history control. Usually you probably want either zero history or history since the last known time of being joined. Maybe that the former should be the default?
author Kim Alvefur <zash@zash.se>
date Sat, 30 Apr 2022 01:00:01 +0200
parent 3322:682b05b4017e
line wrap: on
line source

-- mod_profile
-- Copyright (C) 2014-2018 Kim Alvefur
--
-- This file is MIT licensed.

local st = require"util.stanza";
local jid_split = require"util.jid".split;
local jid_bare = require"util.jid".bare;
local is_admin = require"core.usermanager".is_admin;
local vcard = require"util.vcard";
local base64 = require"util.encodings".base64;
local sha1 = require"util.hashes".sha1;
local t_insert, t_remove = table.insert, table.remove;

local pep_plus;
if module:get_host_type() == "local" and module:get_option_boolean("vcard_to_pep", true) then
	pep_plus = module:depends"pep";
	assert(pep_plus.get_pep_service, "Wrong version of mod_pep loaded, you need to update Prosody");
end

local storage = module:open_store();
local legacy_storage = module:open_store("vcard");

module:hook("account-disco-info", function (event)
	event.reply:tag("feature", { var = "urn:xmpp:pep-vcard-conversion:0" }):up();
end);

local function get_item(vcard, name) -- luacheck: ignore 431
	local item;
	for i=1, #vcard do
		item=vcard[i];
		if item.name == name then
			return item, i;
		end
	end
end

local magic_mime = {
	["\137PNG\r\n\026\n"] = "image/png";
	["\255\216"] = "image/jpeg";
	["GIF87a"] = "image/gif";
	["GIF89a"] = "image/gif";
	["<?xml"] = "image/svg+xml";
}
local function identify(data)
	for magic, mime in pairs(magic_mime) do
		if data:sub(1, #magic) == magic then
			return mime;
		end
	end
	return "application/octet-stream";
end

local function item_container(id, payload)
	return id, st.stanza("item", { id = id or "current", xmlns = "http://jabber.org/protocol/pubsub"; })
		:add_child(payload);
end

local function update_pep(username, data, pep)
	pep = pep or pep_plus.get_pep_service(username);
	local photo, p = get_item(data, "PHOTO");
	if vcard.to_vcard4 then
		if p then t_remove(data, p); end
		pep:purge("urn:xmpp:vcard4", true)
		pep:publish("urn:xmpp:vcard4", true, item_container("current", vcard.to_vcard4(data)));
		if p then t_insert(data, p, photo); end
	end

	local nickname = get_item(data, "NICKNAME");
	if nickname and nickname[1] then
		pep:purge("http://jabber.org/protocol/nick", true);
		pep:publish("http://jabber.org/protocol/nick", true, item_container("current",
			st.stanza("nick", { xmlns="http://jabber.org/protocol/nick" }):text(nickname[1])));
	end

	if photo and photo[1] then
		local photo_raw = base64.decode(photo[1]);
		local photo_hash = sha1(photo_raw, true);
		local photo_type = photo.TYPE and photo.TYPE[1];

		pep:purge("urn:xmpp:avatar:metadata", true);
		pep:purge("urn:xmpp:avatar:data", true);
		pep:publish("urn:xmpp:avatar:metadata", true, item_container(photo_hash,
			st.stanza("metadata", { xmlns="urn:xmpp:avatar:metadata" })
				:tag("info", {
					bytes = tostring(#photo_raw),
					id = photo_hash,
					type = photo_type or identify(photo_raw),
				})));
		pep:publish("urn:xmpp:avatar:data", true, item_container(photo_hash,
			st.stanza("data", { xmlns="urn:xmpp:avatar:data" }):text(photo[1])));
	end
end

-- The "temporary" vCard XEP-0054 part
module:add_feature("vcard-temp");

local function handle_get(event)
	local origin, stanza = event.origin, event.stanza;
	local username = origin.username;
	local to = stanza.attr.to;
	if to then username = jid_split(to); end
	local data, err = storage:get(username);
	if not data then
		if err then
			origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
			return true;
		end
		data = legacy_storage:get(username);
		data = data and st.deserialize(data);
		if data then
			origin.send(st.reply(stanza):add_child(data));
			return true;
		end
	end
	if not data then
		origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
		return true;
	end
	origin.send(st.reply(stanza):add_child(vcard.to_xep54(data)));
	return true;
end

local function handle_set(event)
	local origin, stanza = event.origin, event.stanza;
	local data = vcard.from_xep54(stanza.tags[1]);
	local username = origin.username;
	local to = stanza.attr.to;
	if to then
		if not is_admin(jid_bare(stanza.attr.from), module.host) then
			origin.send(st.error_reply(stanza, "auth", "forbidden"));
			return true;
		end
		username = jid_split(to);
	end
	local ok, err = storage:set(username, data);
	if not ok then
		origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
		return true;
	end

	if pep_plus and username then
		update_pep(username, data);
	end

	origin.send(st.reply(stanza));
	return true;
end

module:hook("iq-get/bare/vcard-temp:vCard", handle_get);
module:hook("iq-get/host/vcard-temp:vCard", handle_get);

module:hook("iq-set/bare/vcard-temp:vCard", handle_set);
module:hook("iq-set/host/vcard-temp:vCard", handle_set);

local function on_publish(event)
	if event.actor == true then return end -- Not from a client
	local node, item = event.node, event.item;
	local username, host = jid_split(event.actor);
	if host ~= module.host then
		module:log("warn", "on_publish() called for non-local actor");
		for k,v in pairs(event) do
			module:log("debug", "event[%q] = %q", k, v);
		end
		return;
	end
	local data = storage:get(username) or {};
	if node == "urn:xmpp:avatar:data" then
		local new_photo = item:get_child_text("data", "urn:xmpp:avatar:data");
		new_photo = new_photo and { name = "PHOTO"; ENCODING = { "b" }; new_photo } or nil;
		local _, i = get_item(data, "PHOTO")
		if new_photo then
			data[i or #data+1] = new_photo;
		elseif i then
			table.remove(data, i);
		end
	elseif node == "http://jabber.org/protocol/nick" then
		local new_nick = item:get_child_text("nick", "http://jabber.org/protocol/nick");
		new_nick = new_nick and new_nick ~= "" and { name = "NICKNAME"; new_nick } or nil;
		local _, i = get_item(data, "NICKNAME")
		if new_nick then
			data[i or #data+1] = new_nick;
		elseif i then
			table.remove(data, i);
		end
	else
		return;
	end
	storage:set(username, data);
end

local function pep_service_added(event)
	local item = event.item;
	local service, username = item.service, jid_split(item.jid);
	module:hook_object_event(service.events, "item-published", on_publish);
	local data = storage:get(username);
	if data then
		update_pep(username, data, service);
	end
end

local function pep_service_removed()
	-- This would happen when mod_pep_plus gets unloaded, but this module gets unloaded before that
end

function module.load()
	module:handle_items("pep-service", pep_service_added, pep_service_removed, true);
end

-- The vCard4 part
if vcard.to_vcard4 then
	module:add_feature("urn:ietf:params:xml:ns:vcard-4.0");

	module:hook("iq-get/bare/urn:ietf:params:xml:ns:vcard-4.0:vcard", function(event)
		local origin, stanza = event.origin, event.stanza;
		local username = jid_split(stanza.attr.to) or origin.username;
		local data = storage:get(username);
		if not data then
			origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
			return true;
		end
		origin.send(st.reply(stanza):add_child(vcard.to_vcard4(data)));
		return true;
	end);

	if vcard.from_vcard4 then
		module:hook("iq-set/self/urn:ietf:params:xml:ns:vcard-4.0:vcard", function(event)
			local origin, stanza = event.origin, event.stanza;
			local ok, err = storage:set(origin.username, vcard.from_vcard4(stanza.tags[1]));
			if not ok then
				origin.send(st.error_reply(stanza, "cancel", "internal-server-error", err));
				return true;
			end
			origin.send(st.reply(stanza));
			return true;
		end);
	else
		module:hook("iq-set/self/urn:ietf:params:xml:ns:vcard-4.0:vcard", function(event)
			local origin, stanza = event.origin, event.stanza;
			origin.send(st.error_reply(stanza, "cancel", "feature-not-implemented"));
			return true;
		end);
	end
end

local function inject_xep153(event)
	local origin, stanza = event.origin, event.stanza;
	local username = origin.username;
	if not username then return end
	local pep = pep_plus.get_pep_service(username);

	local ok, avatar_hash = pep:get_last_item("urn:xmpp:avatar:metadata", true);
	if ok and avatar_hash then
		stanza:remove_children("x", "vcard-temp:x:update");
		local x_update = st.stanza("x", { xmlns = "vcard-temp:x:update" });
		x_update:text_tag("photo", avatar_hash);
		stanza:add_direct_child(x_update);
	end
end

if pep_plus then
	module:hook("pre-presence/full", inject_xep153, 1)
	module:hook("pre-presence/bare", inject_xep153, 1)
	module:hook("pre-presence/host", inject_xep153, 1)
end