Diff

mod_rest/jsonmap.lib.lua @ 3813:aa1ad69c7c10

mod_rest: Add JSON support
author Kim Alvefur <zash@zash.se>
date Wed, 01 Jan 2020 16:21:28 +0100
child 3817:937f8c463be6
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_rest/jsonmap.lib.lua	Wed Jan 01 16:21:28 2020 +0100
@@ -0,0 +1,250 @@
+local array = require "util.array";
+local jid = require "util.jid";
+local st = require "util.stanza";
+local xml = require "util.xml";
+
+local simple_types = {
+	-- basic message
+	body = "text_tag",
+	subject = "text_tag",
+	thread = "text_tag",
+
+	-- basic presence
+	show = "text_tag",
+	status = "text_tag",
+	priority = "text_tag",
+
+	state = {"name", "http://jabber.org/protocol/chatstates"},
+	nick = {"text_tag", "http://jabber.org/protocol/nick", "nick"},
+	delay = {"attr", "urn:xmpp:delay", "delay", "stamp"},
+	replace = {"attr", "urn:xmpp:message-correct:0", "replace", "id"},
+
+	-- XEP-0045 MUC
+	-- TODO history, password, ???
+	join = {"bool_tag", "http://jabber.org/protocol/muc", "x"},
+
+	-- XEP-0071
+	-- FIXME xmlns is awkward
+	html = {
+		"func", "http://jabber.org/protocol/xhtml-im", "html",
+		function (s) --> json string
+			return tostring(s:get_child("body", "http://www.w3.org/1999/xhtml"));
+		end;
+		function (s) --> xml
+			return xml.parse([[<html xmlns='http://jabber.org/protocol/xhtml-im'>]]..s..[[</html>]]);
+		end;
+	};
+
+	-- XEP-0199
+	ping = {"bool_tag", "urn:xmpp:ping", "ping"},
+
+	-- XEP-0030
+	disco = {
+		"func", "http://jabber.org/protocol/disco#info", "query",
+		function (s) --> array of features
+			local identities, features = array(), array();
+			for tag in s:childtags() do
+				if tag.name == "identity" and tag.attr.category and tag.attr.type then
+					identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name });
+				elseif tag.name == "feature" and tag.attr.var then
+					features:push(tag.attr.var);
+				end
+			end
+			return { identities = identities, features = features, };
+		end;
+		function  (s)
+			local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
+			if type(s) == "table" then
+				if s.identities then
+					for identity in ipairs(s.identities) do
+						disco:tag("identity", { category = identity[1], type = identity[2] }):up();
+					end
+				end
+				if s.features then
+					for feature in ipairs(s.features) do
+						disco:tag("feature", { var = feature }):up();
+					end
+				end
+			end
+			return disco;
+		end;
+	};
+
+	items = {
+		"func", "http://jabber.org/protocol/disco#items", "query",
+		function (s) --> array of features
+			local items = array();
+			for item in s:childtags("item") do
+				items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name });
+			end
+			return items;
+		end;
+		function  (s)
+			local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items" });
+			if type(s) == "table" then
+				for _, item in ipairs(s) do
+					disco:tag("item", item);
+				end
+			end
+			return disco;
+		end;
+	};
+
+	oob_url = {"func", "jabber:iq:oob", "query",
+		function (s)
+			return s:get_child_text("url");
+		end;
+		function (s)
+			return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s);
+		end;
+	};
+};
+
+local implied_kinds = {
+	disco = "iq",
+	items = "iq",
+	ping = "iq",
+
+	body = "message",
+	html = "message",
+	replace = "message",
+	state = "message",
+	subject = "message",
+	thread = "message",
+
+	join = "presence",
+	priority = "presence",
+	show = "presence",
+	status = "presence",
+}
+
+local kind_by_type = {
+	get = "iq", set = "iq", result = "iq",
+	normal = "message", chat = "message", headline = "message", groupchat = "message",
+	available = "presence", unavailable = "presence",
+	subscribe = "presence", unsubscribe = "presence",
+	subscribed = "presence", unsubscribed = "presence",
+}
+
+local function st2json(s)
+	local t = {
+		kind = s.name,
+		type = s.attr.type,
+		to = s.attr.to,
+		from = s.attr.from,
+		id = s.attr.id,
+	};
+	if s.name == "presence" and not s.attr.type then
+		t.type = "available";
+	end
+
+	if t.to then
+		t.to = jid.prep(t.to);
+		if not t.to then return nil, "invalid-jid-to"; end
+	end
+	if t.from then
+		t.from = jid.prep(t.from);
+		if not t.from then return nil, "invalid-jid-from"; end
+	end
+
+	if t.type == "error" then
+		local err_typ, err_condition, err_text = s:get_error();
+		t.error = {
+			type = err_typ,
+			condition = err_condition,
+			text = err_text
+		};
+		return t;
+	end
+
+	for k, typ in pairs(simple_types) do
+		if typ == "text_tag" then
+			t[k] = s:get_child_text(k);
+		elseif typ[1] == "text_tag" then
+			t[k] = s:get_child_text(typ[3], typ[2]);
+		elseif typ[1] == "name" then
+			local child = s:get_child(nil, typ[2]);
+			if child then
+				t[k] = child.name;
+			end
+		elseif typ[1] == "attr" then
+			local child = s:get_child(typ[3], typ[2])
+			if child then
+				t[k] = child.attr[typ[4]];
+			end
+		elseif typ[1] == "bool_tag" then
+			if s:get_child(typ[3], typ[2]) then
+				t[k] = true;
+			end
+		elseif typ[1] == "func" then
+			local child = s:get_child(typ[3], typ[2] or k);
+			-- TODO handle err
+			if child then
+				t[k] = typ[4](child);
+			end
+		end
+	end
+
+	return t;
+end
+
+local function json2st(t)
+	local kind = t.kind or kind_by_type[t.type];
+	if not kind then
+		for k, implied in pairs(implied_kinds) do
+			if t[k] then
+				kind = implied;
+				break
+			end
+		end
+	end
+
+	local s = st.stanza(kind or "message", {
+		type = t.type ~= "available" and t.type or nil,
+		to = jid.prep(t.to);
+		from = jid.prep(t.from);
+		id = t.id,
+	});
+
+	if t.to and not s.attr.to then
+		return nil, "invalid-jid-to";
+	end
+	if t.from and not s.attr.from then
+		return nil, "invalid-jid-from";
+	end
+
+	if t.error then
+		return st.error_reply(st.reply(s), t.error.type, t.error.condition, t.error.text);
+	elseif t.type == "error" then
+		s:text_tag("error", t.body, { code = t.error_code and tostring(t.error_code) });
+		return s;
+	end
+
+	for k, v in pairs(t) do
+		local typ = simple_types[k];
+		if typ then
+			if typ == "text_tag" then
+				s:text_tag(k, v);
+			elseif typ[1] == "text_tag" then
+				s:text_tag(typ[3] or k, v, typ[2] and { xmlns = typ[2] });
+			elseif typ[1] == "name" then
+				s:tag(v, { xmlns = typ[2] }):up();
+			elseif typ[1] == "attr" then
+				s:tag(typ[3] or k, { xmlns = typ[2], [ typ[4] or k ] = v }):up();
+			elseif typ[1] == "bool_tag" then
+				s:tag(typ[3] or k, { xmlns = typ[2] }):up();
+			elseif typ[1] == "func" then
+				s:add_child(typ[5](v)):up();
+			end
+		end
+	end
+
+	s:reset();
+
+	return s;
+end
+
+return {
+	st2json = st2json;
+	json2st = json2st;
+};