Mercurial > prosody-modules
view mod_rest/jsonmap.lib.lua @ 4646:072d078be095
mod_storage_xmlarchive: Advertise capabilities (none atm)
Both 'total' and 'quota' would be prohibitively expensive for the kind
of archives this module targets as it would require counting items
across the entire archive.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 13 Aug 2021 01:18:32 +0200 |
parents | b68b801ddc50 |
children | d71beacaec3b |
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 map = require "util.datamapper"; local schema do local f = assert(module:load_resource("res/schema-xmpp.json")); schema = json.decode(f:read("*a")) f:close(); -- Copy common properties to all stanza kinds if schema._common then for key, prop in pairs(schema._common) do for _, copyto in pairs(schema.properties) do copyto.properties[key] = prop; end end schema._common = nil; end end -- Some mappings that are still hard to do in a nice way with util.datamapper local field_mappings; -- in scope for "func" mappings field_mappings = { -- 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-0030 disco = { type = "func", xmlns = "http://jabber.org/protocol/disco#info", tagname = "query", st2json = function (s) --> array of features if s.tags[1] == nil then return s.attr.node or true; end 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; elseif type(s) == "string" then return st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s }); 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.tags[1] == nil then return s.attr.node or true; 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; elseif type(s) == "string" then return st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s }); 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 -- TODO Replace by oob.url in datamapper schema oob_url = { type = "func", xmlns = "jabber:x:oob", tagname = "x", -- XXX namespace depends on whether it's in an iq or message stanza st2json = function (s) return s:get_child_text("url"); end; json2st = function (s) if type(s) == "string" then return st.stanza("x", { xmlns = "jabber:x: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 }; }; local byxmlname = {}; for k, spec in pairs(field_mappings) do for _, replace in pairs(schema.properties) do replace.properties[k] = nil end 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 = map.parse(schema.properties[s.name], s); t.kind = s.name; 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 mapping and 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 kind == "presence" and t_type == "available" then t_type = nil; elseif kind == "iq" and not t_type then t_type = "get"; end local s = map.unparse(schema.properties[kind or "message"], t); s.attr.type = t_type; s.attr.to = str(t.to) and jid.prep(t.to); s.attr.from = str(t.to) and jid.prep(t.from); 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 and mapping.type == "func" and mapping.json2st then s:add_child(mapping.json2st(v)):up(); end end s:reset(); return s; end return { st2json = st2json; json2st = json2st; };