plugins/mod_vcard_legacy.lua
author Kim Alvefur <zash@zash.se>
Mon, 12 Dec 2022 07:03:31 +0100
branch0.11
changeset 12802 c4b1b5cbc20b
parent 10273 4701415f5b0e
child 10274 c2b9ff42db03
permissions -rw-r--r--
Tag 0.11.14

local st = require "util.stanza";
local jid_split = require "util.jid".split;

local mod_pep = module:depends("pep");

local sha1 = require "util.hashes".sha1;
local base64_decode = require "util.encodings".base64.decode;

local vcards = module:open_store("vcard");

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

local function handle_error(origin, stanza, err)
	if err == "forbidden" then
		origin.send(st.error_reply(stanza, "auth", "forbidden"));
	elseif err == "internal-server-error" then
		origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
	else
		origin.send(st.error_reply(stanza, "modify", "undefined-condition", err));
	end
end

-- Simple translations
-- <foo><text>hey</text></foo> -> <FOO>hey</FOO>
local simple_map = {
	nickname = "text";
	title = "text";
	role = "text";
	categories = "text";
	note = "text";
	url = "uri";
	bday = "date";
}

module:hook("iq-get/bare/vcard-temp:vCard", function (event)
	local origin, stanza = event.origin, event.stanza;
	local pep_service = mod_pep.get_pep_service(jid_split(stanza.attr.to) or origin.username);
	local ok, id, vcard4_item = pep_service:get_last_item("urn:xmpp:vcard4", stanza.attr.from);

	local vcard_temp = st.stanza("vCard", { xmlns = "vcard-temp" });
	if ok and vcard4_item then
		local vcard4 = vcard4_item.tags[1];

		local fn = vcard4:get_child("fn");
		vcard_temp:text_tag("FN", fn and fn:get_child_text("text"));

		local v4n = vcard4:get_child("n");
		vcard_temp:tag("N")
			:text_tag("FAMILY", v4n and v4n:get_child_text("surname"))
			:text_tag("GIVEN", v4n and v4n:get_child_text("given"))
			:text_tag("MIDDLE", v4n and v4n:get_child_text("additional"))
			:text_tag("PREFIX", v4n and v4n:get_child_text("prefix"))
			:text_tag("SUFFIX", v4n and v4n:get_child_text("suffix"))
			:up();

		for tag in vcard4:childtags() do
			local typ = simple_map[tag.name];
			if typ then
				local text = tag:get_child_text(typ);
				if text then
					vcard_temp:text_tag(tag.name:upper(), text);
				end
			elseif tag.name == "email" then
				local text = tag:get_child_text("text");
				if text then
					vcard_temp:tag("EMAIL")
						:text_tag("USERID", text)
						:tag("INTERNET"):up();
					if tag:find"parameters/type/text#" == "home" then
						vcard_temp:tag("HOME"):up();
					elseif tag:find"parameters/type/text#" == "work" then
						vcard_temp:tag("WORK"):up();
					end
					vcard_temp:up();
				end
			elseif tag.name == "tel" then
				local text = tag:get_child_text("uri");
				if text then
					if text:sub(1, 4) == "tel:" then
						text = text:sub(5)
					end
					vcard_temp:tag("TEL"):text_tag("NUMBER", text);
					if tag:find"parameters/type/text#" == "home" then
						vcard_temp:tag("HOME"):up();
					elseif tag:find"parameters/type/text#" == "work" then
						vcard_temp:tag("WORK"):up();
					end
					vcard_temp:up();
				end
			elseif tag.name == "adr" then
				vcard_temp:tag("ADR")
					:text_tag("POBOX", tag:get_child_text("pobox"))
					:text_tag("EXTADD", tag:get_child_text("ext"))
					:text_tag("STREET", tag:get_child_text("street"))
					:text_tag("LOCALITY", tag:get_child_text("locality"))
					:text_tag("REGION", tag:get_child_text("region"))
					:text_tag("PCODE", tag:get_child_text("code"))
					:text_tag("CTRY", tag:get_child_text("country"));
				if tag:find"parameters/type/text#" == "home" then
					vcard_temp:tag("HOME"):up();
				elseif tag:find"parameters/type/text#" == "work" then
					vcard_temp:tag("WORK"):up();
				end
				vcard_temp:up();
			end
		end
	end

	local meta_ok, avatar_meta = pep_service:get_items("urn:xmpp:avatar:metadata", stanza.attr.from);
	local data_ok, avatar_data = pep_service:get_items("urn:xmpp:avatar:data", stanza.attr.from);

	if data_ok then
		for _, hash in ipairs(avatar_data) do
			local meta = meta_ok and avatar_meta[hash];
			local data = avatar_data[hash];
			local info = meta and meta.tags[1]:get_child("info");
			vcard_temp:tag("PHOTO");
			if info and info.attr.type then
				vcard_temp:text_tag("TYPE", info.attr.type);
			end
			if data then
				vcard_temp:text_tag("BINVAL", data.tags[1]:get_text());
			elseif info and info.attr.url then
				vcard_temp:text_tag("EXTVAL", info.attr.url);
			end
			vcard_temp:up();
		end
	end

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

local node_defaults = {
	access_model = "open";
	_defaults_only = true;
};

function vcard_to_pep(vcard_temp)
	local avatars = {};

	local vcard4 = st.stanza("item", { xmlns = "http://jabber.org/protocol/pubsub", id = "current" })
		:tag("vcard", { xmlns = 'urn:ietf:params:xml:ns:vcard-4.0' });

	vcard4:tag("fn"):text_tag("text", vcard_temp:get_child_text("FN")):up();

	local N = vcard_temp:get_child("N");

	vcard4:tag("n")
		:text_tag("surname", N and N:get_child_text("FAMILY"))
		:text_tag("given", N and N:get_child_text("GIVEN"))
		:text_tag("additional", N and N:get_child_text("MIDDLE"))
		:text_tag("prefix", N and N:get_child_text("PREFIX"))
		:text_tag("suffix", N and N:get_child_text("SUFFIX"))
	:up();

	for tag in vcard_temp:childtags() do
		local typ = simple_map[tag.name:lower()];
		if typ then
			local text = tag:get_text();
			if text then
				vcard4:tag(tag.name:lower()):text_tag(typ, text):up();
			end
		elseif tag.name == "EMAIL" then
			local text = tag:get_child_text("USERID");
			if text then
				vcard4:tag("email")
				vcard4:text_tag("text", text)
				vcard4:tag("parameters"):tag("type");
				if tag:get_child("HOME") then
					vcard4:text_tag("text", "home");
				elseif tag:get_child("WORK") then
					vcard4:text_tag("text", "work");
				end
				vcard4:up():up():up();
			end
		elseif tag.name == "TEL" then
			local text = tag:get_child_text("NUMBER");
			if text then
				vcard4:tag("tel"):text_tag("uri", "tel:"..text);
			end
			vcard4:tag("parameters"):tag("type");
			if tag:get_child("HOME") then
				vcard4:text_tag("text", "home");
			elseif tag:get_child("WORK") then
				vcard4:text_tag("text", "work");
			end
			vcard4:up():up():up();
		elseif tag.name == "ORG" then
			local text = tag:get_child_text("ORGNAME");
			if text then
				vcard4:tag("org"):text_tag("text", text):up();
			end
		elseif tag.name == "DESC" then
			local text = tag:get_text();
			if text then
				vcard4:tag("note"):text_tag("text", text):up();
			end
			-- <note> gets mapped into <NOTE> in the other direction
		elseif tag.name == "ADR" then
			vcard4:tag("adr")
				:text_tag("pobox", tag:get_child_text("POBOX"))
				:text_tag("ext", tag:get_child_text("EXTADD"))
				:text_tag("street", tag:get_child_text("STREET"))
				:text_tag("locality", tag:get_child_text("LOCALITY"))
				:text_tag("region", tag:get_child_text("REGION"))
				:text_tag("code", tag:get_child_text("PCODE"))
				:text_tag("country", tag:get_child_text("CTRY"));
			vcard4:tag("parameters"):tag("type");
			if tag:get_child("HOME") then
				vcard4:text_tag("text", "home");
			elseif tag:get_child("WORK") then
				vcard4:text_tag("text", "work");
			end
			vcard4:up():up():up();
		elseif tag.name == "PHOTO" then
			local avatar_type = tag:get_child_text("TYPE");
			local avatar_payload = tag:get_child_text("BINVAL");
			-- Can EXTVAL be translated? No way to know the sha1 of the data?

			if avatar_payload then
				local avatar_raw = base64_decode(avatar_payload);
				local avatar_hash = sha1(avatar_raw, true);

				local avatar_meta = st.stanza("item", { id = avatar_hash, xmlns = "http://jabber.org/protocol/pubsub" })
					:tag("metadata", { xmlns="urn:xmpp:avatar:metadata" })
						:tag("info", {
							bytes = tostring(#avatar_raw),
							id = avatar_hash,
							type = avatar_type,
						});

				local avatar_data = st.stanza("item", { id = avatar_hash, xmlns = "http://jabber.org/protocol/pubsub" })
					:tag("data", { xmlns="urn:xmpp:avatar:data" })
						:text(avatar_payload);

				table.insert(avatars, { hash = avatar_hash, meta = avatar_meta, data = avatar_data });
			end
		end
	end
	return vcard4, avatars;
end

function save_to_pep(pep_service, actor, vcard4, avatars)
	if avatars then

		if pep_service:purge("urn:xmpp:avatar:metadata", actor) then
			pep_service:purge("urn:xmpp:avatar:data", actor);
		end

		local avatar_defaults = node_defaults;
		if #avatars > 1 then
			avatar_defaults = {};
			for k,v in pairs(node_defaults) do
				avatar_defaults[k] = v;
			end
			avatar_defaults.max_items = #avatars;
		end
		for _, avatar in ipairs(avatars) do
			local ok, err = pep_service:publish("urn:xmpp:avatar:data", actor, avatar.hash, avatar.data, avatar_defaults);
			if ok then
				ok, err = pep_service:publish("urn:xmpp:avatar:metadata", actor, avatar.hash, avatar.meta, avatar_defaults);
			end
			if not ok then
				return ok, err;
			end
		end
	end

	if vcard4 then
		return pep_service:publish("urn:xmpp:vcard4", actor, "current", vcard4, node_defaults);
	end

	return true;
end

module:hook("iq-set/self/vcard-temp:vCard", function (event)
	local origin, stanza = event.origin, event.stanza;
	local pep_service = mod_pep.get_pep_service(origin.username);

	local vcard_temp = stanza.tags[1];

	local ok, err = save_to_pep(pep_service, origin.full_jid, vcard_to_pep(vcard_temp));
	if ok then
		origin.send(st.reply(stanza));
	else
		handle_error(origin, stanza, err);
	end

	return true;
end);

local function inject_xep153(event)
	local origin, stanza = event.origin, event.stanza;
	local username = origin.username;
	if not username then return end
	if stanza.attr.type then return end
	local pep_service = mod_pep.get_pep_service(username);

	local x_update = stanza:get_child("x", "vcard-temp:x:update");
	if not x_update then
		x_update = st.stanza("x", { xmlns = "vcard-temp:x:update" }):tag("photo");
		stanza:add_direct_child(x_update);
	elseif x_update:get_child("photo") then
		return; -- XEP implies that these should be left alone
	else
		x_update:tag("photo");
	end
	local ok, avatar_hash = pep_service:get_last_item("urn:xmpp:avatar:metadata", true);
	if ok and avatar_hash then
		x_update:text(avatar_hash);
	end
end

module:hook("pre-presence/full", inject_xep153, 1);
module:hook("pre-presence/bare", inject_xep153, 1);
module:hook("pre-presence/host", inject_xep153, 1);

if module:get_option_boolean("upgrade_legacy_vcards", true) then
module:hook("resource-bind", function (event)
	local session = event.session;
	local username = session.username;
	local vcard_temp = vcards:get(username);
	if not vcard_temp then
		session.log("debug", "No legacy vCard to migrate or already migrated");
		return;
	end
	local pep_service = mod_pep.get_pep_service(username);
	vcard_temp = st.deserialize(vcard_temp);
	local vcard4, avatars = vcard_to_pep(vcard_temp);
	if pep_service:get_last_item("urn:xmpp:vcard4", true) then
		vcard4 = nil;
	end
	if pep_service:get_last_item("urn:xmpp:avatar:metadata", true)
	or pep_service:get_last_item("urn:xmpp:avatar:data", true) then
		avatars = nil;
	end
	if not (vcard4 or avatars) then
		session.log("debug", "Already PEP data, not overwriting with migrated data");
		vcards:set(username, nil);
		return;
	end
	local ok, err = save_to_pep(pep_service, true, vcard4, avatars);
	if ok and vcards:set(username, nil) then
		session.log("info", "Migrated vCard-temp to PEP");
	else
		session.log("info", "Failed to migrate vCard-temp to PEP: %s", err or "problem emptying 'vcard' store");
	end
end);
end