Changeset

157:aa0f11fb166c

clix.avatar: Publish and fetch XEP-0084 Avatars
author Kim Alvefur <zash@zash.se>
date Sun, 18 Apr 2021 23:36:18 +0200
parents 156:3369ae4ff520
children 158:703fa6922493
files clix/avatar.lua squishy
diffstat 2 files changed, 132 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/clix/avatar.lua	Sun Apr 18 23:36:18 2021 +0200
@@ -0,0 +1,131 @@
+local b64 = require"util.encodings".base64.encode;
+local unb64 = require"util.encodings".base64.decode;
+local st = require "util.stanza";
+local sha1 = require "util.hashes".sha1;
+
+return function(opts, arg)
+	if opts.short_help then
+		print("Manage PEP avatars");
+		return;
+	end
+
+	local subcommands = {};
+
+	function subcommands.fetch(conn)
+		local waiting = {[true] = true};
+		for _, userjid in ipairs(arg) do
+			waiting[userjid] = true;
+			local userpep = conn.pubsub:service(userjid);
+			userpep:node("urn:xmpp:avatar:metadata"):items(true, function(result)
+				local metadata_tag = result:find("{http://jabber.org/protocol/pubsub}pubsub/items/item/{urn:xmpp:avatar:metadata}metadata");
+				if not metadata_tag or not metadata_tag:get_child("info") then
+					if result.attr.type == "error" then
+						conn:error("Got error from %s: %s:%s", userjid, result:get_error());
+					else
+						conn:error("%s has no avatar", userjid)
+					end
+					waiting[userjid] = nil;
+					if next(waiting) == nil then
+						conn:close();
+					end
+					return;
+				end
+
+				for info_tag in metadata_tag:childtags("info") do
+					conn:info("Has avatar with type %s", info_tag.attr.type or "?")
+					local filename = (opts.output or (userjid .. "_" .. info_tag.attr.id)) .. "." .. (info_tag.attr.type or "/dat"):match("/([^./+]+)");
+					local output = assert(io.open(filename, "w"));
+					waiting[info_tag.attr.id] = true;
+					conn:debug("Writing to %s", filename);
+					userpep:node("urn:xmpp:avatar:data"):item(info_tag.attr.id, function(dataresult)
+						local data = unb64(dataresult:find("{http://jabber.org/protocol/pubsub}pubsub/items/item/{urn:xmpp:avatar:data}data#"));
+						if data then
+							assert(output:write(data));
+							assert(output:close());
+							conn:info("Avatar of %s written to %s", userjid, filename);
+						else
+							conn:error("Got no data for %s id %s", userjid, info_tag.attr.id);
+						end
+						waiting[info_tag.attr.id] = nil;
+						if next(waiting) == nil then
+							conn:close();
+						end
+					end);
+
+					if not opts.all then
+						break
+					end
+				end
+
+				waiting[userjid] = nil;
+			end)
+		end
+		waiting[true] = nil;
+	end
+
+	function subcommands.publish(conn)
+		local waiting = {meta=true};
+		local userpep = conn.pubsub:service(nil);
+		local metadata_tag = st.stanza("metadata", { xmlns = "urn:xmpp:avatar:metadata" });
+		local metadata_node = userpep:node("urn:xmpp:avatar:metadata");
+		local data_node = userpep:node("urn:xmpp:avatar:data");
+		local first_h = nil;
+		local sha1pat = string.rep("%x", #sha1("",true));
+
+		for _, file in ipairs(arg) do
+			local h, width, height, typ = file:match("_("..sha1pat..")_(%d+)x(%d+)%.(%w+)$");
+			if not h then h, typ = file:match("_("..sha1pat..")%.(%w+)$"); end
+			if not h then typ = file:match("%.(%w+)$"); end
+
+			local f = assert(io.open(file));
+			local data = f:read("*a");
+			f:close();
+			local bytes = string.format("%d", #data);
+
+			if not h then h = sha1(data, true); end
+			if not first_h then first_h = h; end
+			if typ == "jpg" then typ = "jpeg"; end
+
+			local data_tag = st.stanza("data", { xmlns = "urn:xmpp:avatar:data" }):text(b64(data));
+			waiting[h] = true;
+			data_node:publish(h, nil, data_tag, function(ok)
+				waiting[h] = nil;
+				if next(waiting) == nil then
+					conn:close();
+				end
+
+			end);
+
+			metadata_tag:tag("info", {id = h; type = "image/" .. typ; bytes = bytes; width = width; height = height}):up();
+		end
+
+		metadata_node:publish(first_h, nil, metadata_tag, function(ok)
+			waiting.meta = nil;
+			if next(waiting) == nil then
+				conn:close();
+			end
+		end);
+
+	end
+
+	if ((#arg == 0) or opts.help) then
+		print("Subcommands:");
+		print("  publish file_HASH_WxH.png");
+		print("  fetch user@example.com");
+		return 0;
+	end
+	if opts.output and opts.all then
+		print("Can't download multiple avatars to a single file")
+		return 1;
+	end
+
+	local subcommand = table.remove(arg, 1);
+	local on_connect = subcommands[subcommand];
+
+	if not on_connect then
+		print("No such command: " .. subcommand);
+		return 1;
+	end
+
+	return clix_connect(opts, on_connect, {"pubsub"})
+end
--- a/squishy	Sat Apr 10 01:00:00 2021 +0200
+++ b/squishy	Sun Apr 18 23:36:18 2021 +0200
@@ -15,6 +15,7 @@
 	"archive";
 	"presence";
 	"watch_pep";
+	"avatar";
 }
 
 for _, cmd in ipairs(commands) do