Software /
code /
prosody-modules
File
mod_rest/jsonmap.lib.lua @ 4421:94805a7e7b30
mod_invites: rework CLI parsing to support groups
To make this sensible, the code had to move from rather simple
parsing to something which looks more like getopt or your typical
shell script.
author | Jonas Schäfer <jonas@wielicki.name> |
---|---|
date | Sun, 31 Jan 2021 19:16:36 +0100 |
parent | 4372:78de3c7acf58 |
child | 4473:3b50a9a75fb6 |
line wrap: on
line source
local array = require "util.array"; local jid = require "util.jid"; local json = require "util.json"; local st = require "util.stanza"; local xml = require "util.xml"; local field_mappings; -- in scope for "func" mappings field_mappings = { -- top level stanza attributes -- needed here to mark them as known fields kind = "attr", type = "attr", to = "attr", from = "attr", id = "attr", lang = "attr", -- basic message body = "text_tag", subject = "text_tag", thread = "text_tag", -- basic presence show = "text_tag", status = "text_tag", priority = "text_tag", state = { type = "name", xmlns = "http://jabber.org/protocol/chatstates" }, nick = { type = "text_tag", xmlns = "http://jabber.org/protocol/nick", tagname = "nick" }, delay = { type = "attr", xmlns = "urn:xmpp:delay", tagname = "delay", attr = "stamp" }, replace = { type = "attr", xmlns = "urn:xmpp:message-correct:0", tagname = "replace", attr = "id" }, -- XEP-0045 MUC -- TODO history, password, ??? join = { type = "bool_tag", xmlns = "http://jabber.org/protocol/muc", tagname = "x" }, -- XEP-0071 html = { type = "func", xmlns = "http://jabber.org/protocol/xhtml-im", tagname = "html", st2json = function (s) --> json string return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'", "", 1)); end; json2st = function (s) --> xml if type(s) == "string" then return assert(xml.parse("<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>" .. s .. "</x:html>")); end end; }; -- XEP-0199: XMPP Ping ping = { type = "bool_tag", xmlns = "urn:xmpp:ping", tagname = "ping" }, -- XEP-0092: Software Version version = { type = "func", xmlns = "jabber:iq:version", tagname = "query", st2json = function (s) return { name = s:get_child_text("name"); version = s:get_child_text("version"); os = s:get_child_text("os"); } end, json2st = function (s) local v = st.stanza("query", { xmlns = "jabber:iq:version" }); if type(s) == "table" then v:text_tag("name", s.name); v:text_tag("version", s.version); if s.os then v:text_tag("os", s.os); end end return v; end }; -- XEP-0030 disco = { type = "func", xmlns = "http://jabber.org/protocol/disco#info", tagname = "query", st2json = function (s) --> array of features local identities, features, extensions = 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 for form in s:childtags("x", "jabber:x:data") do local jform = field_mappings.formdata.st2json(form); local form_type = jform["FORM_TYPE"]; if jform then jform["FORM_TYPE"] = nil; extensions[form_type] = jform; end end if next(extensions) == nil then extensions = nil; end return { node = s.attr.node, identities = identities, features = features, extensions = extensions }; end; json2st = function (s) if type(s) == "table" and s ~= json.null then local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); if s.identities then for _, identity in ipairs(s.identities) do disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); end end if s.features then for _, feature in ipairs(s.features) do disco:tag("feature", { var = feature }):up(); end end if s.extensions then for form_type, extension in pairs(s.extensions) do extension["FORM_TYPE"] = form_type; disco:add_child(field_mappings.formdata.json2st(extension)); end end return disco; else return st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", }); end end; }; items = { type = "func", xmlns = "http://jabber.org/protocol/disco#items", tagname = "query", st2json = function (s) --> array of features | map with node if s.attr.node and s.tags[1] == nil then return { node = s.attr.node }; end 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; json2st = function (s) if type(s) == "table" and s ~= json.null then local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s.node }); for _, item in ipairs(s) do if type(item) == "string" then disco:tag("item", { jid = item }); elseif type(item) == "table" then disco:tag("item", { jid = item.jid, node = item.node, name = item.name }); end end return disco; else return st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", }); end end; }; -- XEP-0050: Ad-Hoc Commands command = { type = "func", xmlns = "http://jabber.org/protocol/commands", tagname = "command", st2json = function (s) local cmd = { action = s.attr.action, node = s.attr.node, sessionid = s.attr.sessionid, status = s.attr.status, }; local actions = s:get_child("actions"); local note = s:get_child("note"); local form = s:get_child("x", "jabber:x:data"); if actions then cmd.actions = { execute = actions.attr.execute, }; for action in actions:childtags() do cmd.actions[action.name] = true end elseif note then cmd.note = { type = note.attr.type; text = note:get_text(); }; end if form then cmd.form = field_mappings.dataform.st2json(form); end return cmd; end; json2st = function (s) if type(s) == "table" and s ~= json.null then local cmd = st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", action = s.action, node = s.node, sessionid = s.sessionid, status = s.status, }); if type(s.actions) == "table" then cmd:tag("actions", { execute = s.actions.execute }); do if s.actions.next == true then cmd:tag("next"):up(); end if s.actions.prev == true then cmd:tag("prev"):up(); end if s.actions.complete == true then cmd:tag("complete"):up(); end end cmd:up(); elseif type(s.note) == "table" then cmd:text_tag("note", s.note.text, { type = s.note.type }); end if s.form then cmd:add_child(field_mappings.dataform.json2st(s.form)); elseif s.data then cmd:add_child(field_mappings.formdata.json2st(s.data)); end return cmd; elseif type(s) == "string" then -- assume node return st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", node = s }); end -- else .. missing required attribute end; }; -- XEP-0066: Out of Band Data oob_url = { type = "func", xmlns = "jabber:iq:oob", tagname = "query", st2json = function (s) return s:get_child_text("url"); end; json2st = function (s) if type(s) == "string" then return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); end end; }; -- XEP-0432: Simple JSON Messaging payload = { type = "func", xmlns = "urn:xmpp:json-msg:0", tagname = "payload", st2json = function (s) local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); if not rawjson then return nil, "missing-json-payload"; end local parsed, err = json.decode(rawjson); if not parsed then return nil, err; end return { datatype = s.attr.datatype; data = parsed; }; end; json2st = function (s) if type(s) == "table" then return st.stanza("payload", { xmlns = "urn:xmpp:json-msg:0", datatype = s.datatype }) :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); end; end }; -- XEP-0004: Data Forms dataform = { -- Generic and complete dataforms mapping type = "func", xmlns = "jabber:x:data", tagname = "x", st2json = function (s) local fields = array(); local form = { type = s.attr.type; title = s:get_child_text("title"); instructions = s:get_child_text("instructions"); fields = fields; }; for field in s:childtags("field") do local i = { var = field.attr.var; type = field.attr.type; label = field.attr.label; desc = field:get_child_text("desc"); required = field:get_child("required") and true or nil; value = field:get_child_text("value"); }; if field.attr.type == "jid-multi" or field.attr.type == "list-multi" or field.attr.type == "text-multi" then local value = array(); for v in field:childtags("value") do value:push(v:get_text()); end if field.attr.type == "text-multi" then i.value = value:concat("\n"); else i.value = value; end end if field.attr.type == "list-single" or field.attr.type == "list-multi" then local options = array(); for o in field:childtags("option") do options:push({ label = o.attr.label, value = o:get_child_text("value") }); end i.options = options; end fields:push(i); end return form; end; json2st = function (x) if type(x) == "table" and x ~= json.null then local form = st.stanza("x", { xmlns = "jabber:x:data", type = x.type }); if x.title then form:text_tag("title", x.title); end if x.instructions then form:text_tag("instructions", x.instructions); end if type(x.fields) == "table" then for _, f in ipairs(x.fields) do if type(f) == "table" then form:tag("field", { var = f.var, type = f.type, label = f.label }); if f.desc then form:text_tag("desc", f.desc); end if f.required == true then form:tag("required"):up(); end if type(f.value) == "string" then form:text_tag("value", f.value); elseif type(f.value) == "table" then for _, v in ipairs(f.value) do form:text_tag("value", v); end end if type(f.options) == "table" then for _, o in ipairs(f.value) do if type(o) == "table" then form:tag("option", { label = o.label }); form:text_tag("value", o.value); form:up(); end end end end end end return form; end end; }; -- Simpler mapping of dataform from JSON map formdata = { type = "func", xmlns = "jabber:x:data", tagname = "", st2json = function (s) local r = {}; for field in s:childtags("field") do if field.attr.var then local values = array(); for value in field:childtags("value") do values:push(value:get_text()); end if field.attr.type == "list-single" or field.attr.type == "list-multi" then r[field.attr.var] = values; elseif field.attr.type == "text-multi" then r[field.attr.var] = values:concat("\n"); elseif field.attr.type == "boolean" then r[field.attr.var] = values[1] == "1" or values[1] == "true"; elseif field.attr.type then r[field.attr.var] = values[1] or json.null; else -- type is optional, no way to know if multiple or single value is expected r[field.attr.var] = values; end end end return r; end, json2st = function (s, t) local form = st.stanza("x", { xmlns = "jabber:x:data", type = t }); for k, v in pairs(s) do form:tag("field", { var = k }); if type(v) == "string" then form:text_tag("value", v); elseif type(v) == "table" then for _, v_ in ipairs(v) do form:text_tag("value", v_); end end form:up(); end return form; end }; -- XEP-0039: Statistics Gathering stats = { type = "func", xmlns = "http://jabber.org/protocol/stats", tagname = "query", st2json = function (s) local o = array(); for stat in s:childtags("stat") do o:push({ name = stat.attr.name; unit = stat.attr.unit; value = stat.attr.value; }); end return o; end; json2st = function (j) local stats = st.stanza("query", { xmlns = "http://jabber.org/protocol/stats" }); if type(j) == "table" then for _, stat in ipairs(j) do stats:tag("stat", { name = stat.name, unit = stat.unit, value = stat.value }):up(); end end return stats; end; }; }; local byxmlname = {}; for k, spec in pairs(field_mappings) do if type(spec) == "table" then spec.key = k; if spec.xmlns and spec.tagname then byxmlname["{" .. spec.xmlns .. "}" .. spec.tagname] = spec; elseif spec.type == "name" then byxmlname["{" .. spec.xmlns .. "}"] = spec; end elseif type(spec) == "string" then byxmlname["{jabber:client}" .. k] = {key = k; type = spec}; end end local implied_kinds = { disco = "iq", items = "iq", ping = "iq", version = "iq", command = "iq", body = "message", html = "message", replace = "message", state = "message", subject = "message", thread = "message", join = "presence", priority = "presence", show = "presence", status = "presence", } local implied_types = { command = "set", } 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, lang = s.attr["xml:lang"], }; 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 error = s:get_child("error"); local err_typ, err_condition, err_text = s:get_error(); t.error = { type = err_typ, condition = err_condition, text = err_text, by = error and error.attr.by or nil, }; return t; end for _, tag in ipairs(s.tags) do local prefix = "{" .. (tag.attr.xmlns or "jabber:client") .. "}"; local mapping = byxmlname[prefix .. tag.name]; if not mapping then mapping = byxmlname[prefix]; end if not mapping then -- luacheck: ignore 542 -- pass elseif mapping.type == "text_tag" then t[mapping.key] = tag:get_text(); elseif mapping.type == "name" then t[mapping.key] = tag.name; elseif mapping.type == "attr" then t[mapping.key] = tag.attr[mapping.attr]; elseif mapping.type == "bool_tag" then t[mapping.key] = true; elseif mapping.type == "func" and mapping.st2json then t[mapping.key] = mapping.st2json(tag); end end return t; end local function str(s) if type(s) == "string" then return s; end end local function json2st(t) if type(t) ~= "table" or not str(next(t)) then return nil, "invalid-json"; end local t_type = str(t.type); if t_type == nil then for k, implied in pairs(implied_types) do if t[k] then t_type = implied; end end end local kind = str(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 if t_type == "available" then t_type = nil; end local s = st.stanza(kind or "message", { type = t_type; to = str(t.to) and jid.prep(t.to); from = str(t.to) and jid.prep(t.from); id = str(t.id), ["xml:lang"] = str(t.lang), }); 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 kind == "iq" and not s.attr.type then s.attr.type = "get"; end if type(t.error) == "table" then return st.error_reply(st.reply(s), str(t.error.type), str(t.error.condition), str(t.error.text), str(t.error.by)); 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 mapping = field_mappings[k]; if mapping then if mapping == "text_tag" then s:text_tag(k, v); elseif mapping == "attr" then -- luacheck: ignore 542 -- handled already elseif mapping.type == "text_tag" then s:text_tag(mapping.tagname or k, v, mapping.xmlns and { xmlns = mapping.xmlns }); elseif mapping.type == "name" then s:tag(v, { xmlns = mapping.xmlns }):up(); elseif mapping.type == "attr" then s:tag(mapping.tagname or k, { xmlns = mapping.xmlns, [mapping.attr or k] = v }):up(); elseif mapping.type == "bool_tag" then s:tag(mapping.tagname or k, { xmlns = mapping.xmlns }):up(); elseif mapping.type == "func" and mapping.json2st then s:add_child(mapping.json2st(v)):up(); end else return nil, "unknown-field"; end end s:reset(); return s; end return { st2json = st2json; json2st = json2st; };