mod_pep: Wipe pubsub service on user deletion
Data is already wiped from storage, but this ensures everything is
properly unsubscribed, possibly with notifications etc.
Clears recipient cache as well, since it is no longer relevant.
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