Software / code / prosody-modules
Comparison
mod_rest/jsonmap.lib.lua @ 3896:987b203bb091
mod_rest: Restructure JSON / Stanza mapping definitions
It was a pain to remember which index had the mapping function for which
direction. This is more readable.
| author | Kim Alvefur <zash@zash.se> |
|---|---|
| date | Sat, 22 Feb 2020 14:08:19 +0100 |
| parent | 3895:25a3ad36ef3e |
| child | 3906:dbebc9226597 |
comparison
equal
deleted
inserted
replaced
| 3895:25a3ad36ef3e | 3896:987b203bb091 |
|---|---|
| 5 local xml = require "util.xml"; | 5 local xml = require "util.xml"; |
| 6 | 6 |
| 7 -- Reused in many XEPs so declared up here | 7 -- Reused in many XEPs so declared up here |
| 8 local dataform = { | 8 local dataform = { |
| 9 -- Generic and complete dataforms mapping | 9 -- Generic and complete dataforms mapping |
| 10 "func", "jabber:x:data", "x", | 10 type = "func", xmlns = "jabber:x:data", tagname = "x", |
| 11 function (s) | 11 st2json = function (s) |
| 12 local fields = array(); | 12 local fields = array(); |
| 13 local form = { | 13 local form = { |
| 14 type = s.attr.type; | 14 type = s.attr.type; |
| 15 title = s:get_child_text("title"); | 15 title = s:get_child_text("title"); |
| 16 instructions = s:get_child_text("instructions"); | 16 instructions = s:get_child_text("instructions"); |
| 45 end | 45 end |
| 46 fields:push(i); | 46 fields:push(i); |
| 47 end | 47 end |
| 48 return form; | 48 return form; |
| 49 end; | 49 end; |
| 50 function (x) | 50 json2st = function (x) |
| 51 if type(x) == "table" and x ~= json.null then | 51 if type(x) == "table" and x ~= json.null then |
| 52 local form = st.stanza("x", { xmlns = "jabber:x:data", type = x.type }); | 52 local form = st.stanza("x", { xmlns = "jabber:x:data", type = x.type }); |
| 53 if x.title then | 53 if x.title then |
| 54 form:text_tag("title", x.title); | 54 form:text_tag("title", x.title); |
| 55 end | 55 end |
| 88 return form; | 88 return form; |
| 89 end | 89 end |
| 90 end; | 90 end; |
| 91 }; | 91 }; |
| 92 | 92 |
| 93 local function formdata(s,t) | 93 local function formdata(s, t) |
| 94 local form = st.stanza("x", { xmlns = "jabber:x:data", type = t }); | 94 local form = st.stanza("x", { xmlns = "jabber:x:data", type = t }); |
| 95 for k,v in pairs(s) do | 95 for k, v in pairs(s) do |
| 96 form:tag("field", { var = k }); | 96 form:tag("field", { var = k }); |
| 97 if type(v) == "string" then | 97 if type(v) == "string" then |
| 98 form:text_tag("value", v); | 98 form:text_tag("value", v); |
| 99 elseif type(v) == "table" then | 99 elseif type(v) == "table" then |
| 100 for _, v_ in ipairs(v) do | 100 for _, v_ in ipairs(v) do |
| 122 -- basic presence | 122 -- basic presence |
| 123 show = "text_tag", | 123 show = "text_tag", |
| 124 status = "text_tag", | 124 status = "text_tag", |
| 125 priority = "text_tag", | 125 priority = "text_tag", |
| 126 | 126 |
| 127 state = {"name", "http://jabber.org/protocol/chatstates"}, | 127 state = { type = "name", xmlns = "http://jabber.org/protocol/chatstates" }, |
| 128 nick = {"text_tag", "http://jabber.org/protocol/nick", "nick"}, | 128 nick = { type = "text_tag", xmlns = "http://jabber.org/protocol/nick", tagname = "nick" }, |
| 129 delay = {"attr", "urn:xmpp:delay", "delay", "stamp"}, | 129 delay = { type = "attr", xmlns = "urn:xmpp:delay", tagname = "delay", attr = "stamp" }, |
| 130 replace = {"attr", "urn:xmpp:message-correct:0", "replace", "id"}, | 130 replace = { type = "attr", xmlns = "urn:xmpp:message-correct:0", tagname = "replace", attr = "id" }, |
| 131 | 131 |
| 132 -- XEP-0045 MUC | 132 -- XEP-0045 MUC |
| 133 -- TODO history, password, ??? | 133 -- TODO history, password, ??? |
| 134 join = {"bool_tag", "http://jabber.org/protocol/muc", "x"}, | 134 join = { type = "bool_tag", xmlns = "http://jabber.org/protocol/muc", tagname = "x" }, |
| 135 | 135 |
| 136 -- XEP-0071 | 136 -- XEP-0071 |
| 137 html = { | 137 html = { |
| 138 "func", "http://jabber.org/protocol/xhtml-im", "html", | 138 type = "func", xmlns = "http://jabber.org/protocol/xhtml-im", tagname = "html", |
| 139 function (s) --> json string | 139 st2json = function (s) --> json string |
| 140 return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'","", 1)); | 140 return (tostring(s:get_child("body", "http://www.w3.org/1999/xhtml")):gsub(" xmlns='[^']*'", "", 1)); |
| 141 end; | 141 end; |
| 142 function (s) --> xml | 142 json2st = function (s) --> xml |
| 143 if type(s) == "string" then | 143 if type(s) == "string" then |
| 144 return assert(xml.parse([[<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>]]..s..[[</x:html>]])); | 144 return assert(xml.parse("<x:html xmlns:x='http://jabber.org/protocol/xhtml-im' xmlns='http://www.w3.org/1999/xhtml'>" .. s .. "</x:html>")); |
| 145 end | 145 end |
| 146 end; | 146 end; |
| 147 }; | 147 }; |
| 148 | 148 |
| 149 -- XEP-0199: XMPP Ping | 149 -- XEP-0199: XMPP Ping |
| 150 ping = {"bool_tag", "urn:xmpp:ping", "ping"}, | 150 ping = { type = "bool_tag", xmlns = "urn:xmpp:ping", tagname = "ping" }, |
| 151 | 151 |
| 152 -- XEP-0092: Software Version | 152 -- XEP-0092: Software Version |
| 153 version = {"func", "jabber:iq:version", "query", | 153 version = { type = "func", xmlns = "jabber:iq:version", tagname = "query", |
| 154 function (s) | 154 st2json = function (s) |
| 155 return { | 155 return { |
| 156 name = s:get_child_text("name"); | 156 name = s:get_child_text("name"); |
| 157 version = s:get_child_text("version"); | 157 version = s:get_child_text("version"); |
| 158 os = s:get_child_text("os"); | 158 os = s:get_child_text("os"); |
| 159 } | 159 } |
| 160 end, | 160 end, |
| 161 function (s) | 161 json2st = function (s) |
| 162 local v = st.stanza("query", { xmlns = "jabber:iq:version" }); | 162 local v = st.stanza("query", { xmlns = "jabber:iq:version" }); |
| 163 if type(s) == "table" then | 163 if type(s) == "table" then |
| 164 v:text_tag("name", s.name); | 164 v:text_tag("name", s.name); |
| 165 v:text_tag("version", s.version); | 165 v:text_tag("version", s.version); |
| 166 if s.os then | 166 if s.os then |
| 171 end | 171 end |
| 172 }; | 172 }; |
| 173 | 173 |
| 174 -- XEP-0030 | 174 -- XEP-0030 |
| 175 disco = { | 175 disco = { |
| 176 "func", "http://jabber.org/protocol/disco#info", "query", | 176 type = "func", xmlns = "http://jabber.org/protocol/disco#info", tagname = "query", |
| 177 function (s) --> array of features | 177 st2json = function (s) --> array of features |
| 178 local identities, features = array(), array(); | 178 local identities, features = array(), array(); |
| 179 for tag in s:childtags() do | 179 for tag in s:childtags() do |
| 180 if tag.name == "identity" and tag.attr.category and tag.attr.type then | 180 if tag.name == "identity" and tag.attr.category and tag.attr.type then |
| 181 identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name }); | 181 identities:push({ category = tag.attr.category, type = tag.attr.type, name = tag.attr.name }); |
| 182 elseif tag.name == "feature" and tag.attr.var then | 182 elseif tag.name == "feature" and tag.attr.var then |
| 183 features:push(tag.attr.var); | 183 features:push(tag.attr.var); |
| 184 end | 184 end |
| 185 end | 185 end |
| 186 return { node = s.attr.node, identities = identities, features = features, }; | 186 return { node = s.attr.node, identities = identities, features = features, }; |
| 187 end; | 187 end; |
| 188 function (s) | 188 json2st = function (s) |
| 189 if type(s) == "table" and s ~= json.null then | 189 if type(s) == "table" and s ~= json.null then |
| 190 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); | 190 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info", node = s.node }); |
| 191 if s.identities then | 191 if s.identities then |
| 192 for _, identity in ipairs(s.identities) do | 192 for _, identity in ipairs(s.identities) do |
| 193 disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); | 193 disco:tag("identity", { category = identity.category, type = identity.type, name = identity.name }):up(); |
| 204 end | 204 end |
| 205 end; | 205 end; |
| 206 }; | 206 }; |
| 207 | 207 |
| 208 items = { | 208 items = { |
| 209 "func", "http://jabber.org/protocol/disco#items", "query", | 209 type = "func", xmlns = "http://jabber.org/protocol/disco#items", tagname = "query", |
| 210 function (s) --> array of features | map with node | 210 st2json = function (s) --> array of features | map with node |
| 211 if s.attr.node and s.tags[1] == nil then | 211 if s.attr.node and s.tags[1] == nil then |
| 212 return { node = s.attr. node }; | 212 return { node = s.attr.node }; |
| 213 end | 213 end |
| 214 | 214 |
| 215 local items = array(); | 215 local items = array(); |
| 216 for item in s:childtags("item") do | 216 for item in s:childtags("item") do |
| 217 items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name }); | 217 items:push({ jid = item.attr.jid, node = item.attr.node, name = item.attr.name }); |
| 218 end | 218 end |
| 219 return items; | 219 return items; |
| 220 end; | 220 end; |
| 221 function (s) | 221 json2st = function (s) |
| 222 if type(s) == "table" and s ~= json.null then | 222 if type(s) == "table" and s ~= json.null then |
| 223 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s.node }); | 223 local disco = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#items", node = s.node }); |
| 224 for _, item in ipairs(s) do | 224 for _, item in ipairs(s) do |
| 225 if type(item) == "string" then | 225 if type(item) == "string" then |
| 226 disco:tag("item", { jid = item }); | 226 disco:tag("item", { jid = item }); |
| 234 end | 234 end |
| 235 end; | 235 end; |
| 236 }; | 236 }; |
| 237 | 237 |
| 238 -- XEP-0050: Ad-Hoc Commands | 238 -- XEP-0050: Ad-Hoc Commands |
| 239 command = {"func", "http://jabber.org/protocol/commands", "command", | 239 command = { type = "func", xmlns = "http://jabber.org/protocol/commands", tagname = "command", |
| 240 function (s) | 240 st2json = function (s) |
| 241 local cmd = { | 241 local cmd = { |
| 242 action = s.attr.action, | 242 action = s.attr.action, |
| 243 node = s.attr.node, | 243 node = s.attr.node, |
| 244 sessionid = s.attr.sessionid, | 244 sessionid = s.attr.sessionid, |
| 245 status = s.attr.status, | 245 status = s.attr.status, |
| 259 type = note.attr.type; | 259 type = note.attr.type; |
| 260 text = note:get_text(); | 260 text = note:get_text(); |
| 261 }; | 261 }; |
| 262 end | 262 end |
| 263 if form then | 263 if form then |
| 264 cmd.form = dataform[4](form); | 264 cmd.form = dataform[4] (form); |
| 265 end | 265 end |
| 266 return cmd; | 266 return cmd; |
| 267 end; | 267 end; |
| 268 function (s) | 268 json2st = function (s) |
| 269 if type(s) == "table" and s ~= json.null then | 269 if type(s) == "table" and s ~= json.null then |
| 270 local cmd = st.stanza("command", { | 270 local cmd = st.stanza("command", { |
| 271 xmlns = "http://jabber.org/protocol/commands", | 271 xmlns = "http://jabber.org/protocol/commands", |
| 272 action = s.action, | 272 action = s.action, |
| 273 node = s.node, | 273 node = s.node, |
| 274 sessionid = s.sessionid, | 274 sessionid = s.sessionid, |
| 275 status = s.status, | 275 status = s.status, |
| 276 }); | 276 }); |
| 277 if type(s.actions) == "table" then | 277 if type(s.actions) == "table" then |
| 278 cmd:tag("actions", { execute = s.actions.execute }); | 278 cmd:tag("actions", { execute = s.actions.execute }); |
| 279 do | 279 do |
| 280 if s.actions.next == true then | 280 if s.actions.next == true then |
| 281 cmd:tag("next"):up(); | 281 cmd:tag("next"):up(); |
| 290 cmd:up(); | 290 cmd:up(); |
| 291 elseif type(s.note) == "table" then | 291 elseif type(s.note) == "table" then |
| 292 cmd:text_tag("note", s.note.text, { type = s.note.type }); | 292 cmd:text_tag("note", s.note.text, { type = s.note.type }); |
| 293 end | 293 end |
| 294 if s.form then | 294 if s.form then |
| 295 cmd:add_child(dataform[5](s.form)); | 295 cmd:add_child(dataform[5] (s.form)); |
| 296 elseif s.data then | 296 elseif s.data then |
| 297 cmd:add_child(formdata(s.data)); | 297 cmd:add_child(formdata(s.data)); |
| 298 end | 298 end |
| 299 return cmd; | 299 return cmd; |
| 300 elseif type(s) == "string" then -- assume node | 300 elseif type(s) == "string" then -- assume node |
| 301 return st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", node = s }); | 301 return st.stanza("command", { xmlns = "http://jabber.org/protocol/commands", node = s }); |
| 302 end | 302 end |
| 303 -- else .. missing required attribute | 303 -- else .. missing required attribute |
| 304 end; | 304 end; |
| 305 }; | 305 }; |
| 306 | 306 |
| 307 -- XEP-0066: Out of Band Data | 307 -- XEP-0066: Out of Band Data |
| 308 oob_url = {"func", "jabber:iq:oob", "query", | 308 oob_url = { type = "func", xmlns = "jabber:iq:oob", tagname = "query", |
| 309 function (s) | 309 st2json = function (s) |
| 310 return s:get_child_text("url"); | 310 return s:get_child_text("url"); |
| 311 end; | 311 end; |
| 312 function (s) | 312 json2st = function (s) |
| 313 if type(s) == "string" then | 313 if type(s) == "string" then |
| 314 return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); | 314 return st.stanza("query", { xmlns = "jabber:iq:oob" }):text_tag("url", s); |
| 315 end | 315 end |
| 316 end; | 316 end; |
| 317 }; | 317 }; |
| 318 | 318 |
| 319 -- XEP-XXXX: User-defined Data Transfer | 319 -- XEP-XXXX: User-defined Data Transfer |
| 320 payload = {"func", "urn:xmpp:udt:0", "payload", | 320 payload = { type = "func", xmlns = "urn:xmpp:udt:0", tagname = "payload", |
| 321 function (s) | 321 st2json = function (s) |
| 322 local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); | 322 local rawjson = s:get_child_text("json", "urn:xmpp:json:0"); |
| 323 if not rawjson then return nil, "missing-json-payload"; end | 323 if not rawjson then return nil, "missing-json-payload"; end |
| 324 local parsed, err = json.decode(rawjson); | 324 local parsed, err = json.decode(rawjson); |
| 325 if not parsed then return nil, err; end | 325 if not parsed then return nil, err; end |
| 326 return { | 326 return { |
| 327 datatype = s.attr.datatype; | 327 datatype = s.attr.datatype; |
| 328 data = parsed; | 328 data = parsed; |
| 329 }; | 329 }; |
| 330 end; | 330 end; |
| 331 function (s) | 331 json2st = function (s) |
| 332 if type(s) == "table" then | 332 if type(s) == "table" then |
| 333 return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype }) | 333 return st.stanza("payload", { xmlns = "urn:xmpp:udt:0", datatype = s.datatype }) |
| 334 :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); | 334 :tag("json", { xmlns = "urn:xmpp:json:0" }):text(json.encode(s.data)); |
| 335 end; | 335 end; |
| 336 end | 336 end |
| 337 }; | 337 }; |
| 338 | 338 |
| 339 -- XEP-0004: Data Forms | 339 -- XEP-0004: Data Forms |
| 340 dataform = dataform; | 340 dataform = dataform; |
| 341 | 341 |
| 342 -- Simpler mapping from JSON map | 342 -- Simpler mapping from JSON map |
| 343 formdata = {"func", "jabber:x:data", "", | 343 formdata = { type = "func", xmlns = "jabber:x:data", tagname = "", |
| 344 function () | 344 st2json = function () |
| 345 -- Tricky to do in a generic way without each form layout | 345 -- Tricky to do in a generic way without each form layout |
| 346 -- In the future, some well-known layouts might be understood | 346 -- In the future, some well-known layouts might be understood |
| 347 return nil, "not-implemented"; | 347 return nil, "not-implemented"; |
| 348 end, | 348 end, |
| 349 formdata, | 349 json2st = formdata, |
| 350 }; | 350 }; |
| 351 }; | 351 }; |
| 352 | 352 |
| 353 local implied_kinds = { | 353 local implied_kinds = { |
| 354 disco = "iq", | 354 disco = "iq", |
| 412 end | 412 end |
| 413 | 413 |
| 414 for k, mapping in pairs(field_mappings) do | 414 for k, mapping in pairs(field_mappings) do |
| 415 if mapping == "text_tag" then | 415 if mapping == "text_tag" then |
| 416 t[k] = s:get_child_text(k); | 416 t[k] = s:get_child_text(k); |
| 417 elseif mapping[1] == "text_tag" then | 417 elseif mapping.type == "text_tag" then |
| 418 t[k] = s:get_child_text(mapping[3], mapping[2]); | 418 t[k] = s:get_child_text(mapping.tagname, mapping.xmlns); |
| 419 elseif mapping[1] == "name" then | 419 elseif mapping.type == "name" then |
| 420 local child = s:get_child(nil, mapping[2]); | 420 local child = s:get_child(nil, mapping.xmlns); |
| 421 if child then | 421 if child then |
| 422 t[k] = child.name; | 422 t[k] = child.name; |
| 423 end | 423 end |
| 424 elseif mapping[1] == "attr" then | 424 elseif mapping.type == "attr" then |
| 425 local child = s:get_child(mapping[3], mapping[2]) | 425 local child = s:get_child(mapping.tagname, mapping.xmlns); |
| 426 if child then | 426 if child then |
| 427 t[k] = child.attr[mapping[4]]; | 427 t[k] = child.attr[mapping.attr]; |
| 428 end | 428 end |
| 429 elseif mapping[1] == "bool_tag" then | 429 elseif mapping.type == "bool_tag" then |
| 430 if s:get_child(mapping[3], mapping[2]) then | 430 if s:get_child(mapping.tagname, mapping.xmlns) then |
| 431 t[k] = true; | 431 t[k] = true; |
| 432 end | 432 end |
| 433 elseif mapping[1] == "func" then | 433 elseif mapping.type == "func" and mapping.st2json then |
| 434 local child = s:get_child(mapping[3], mapping[2] or k); | 434 local child = s:get_child(mapping.tagname, mapping.xmlns or k); |
| 435 -- TODO handle err | 435 -- TODO handle err |
| 436 if child then | 436 if child then |
| 437 t[k] = mapping[4](child); | 437 t[k] = mapping.st2json(child); |
| 438 end | 438 end |
| 439 end | 439 end |
| 440 end | 440 end |
| 441 | 441 |
| 442 return t; | 442 return t; |
| 491 if mapping then | 491 if mapping then |
| 492 if mapping == "text_tag" then | 492 if mapping == "text_tag" then |
| 493 s:text_tag(k, v); | 493 s:text_tag(k, v); |
| 494 elseif mapping == "attr" then -- luacheck: ignore 542 | 494 elseif mapping == "attr" then -- luacheck: ignore 542 |
| 495 -- handled already | 495 -- handled already |
| 496 elseif mapping[1] == "text_tag" then | 496 elseif mapping.type == "text_tag" then |
| 497 s:text_tag(mapping[3] or k, v, mapping[2] and { xmlns = mapping[2] }); | 497 s:text_tag(mapping.tagname or k, v, mapping.xmlns and { xmlns = mapping.xmlns }); |
| 498 elseif mapping[1] == "name" then | 498 elseif mapping.type == "name" then |
| 499 s:tag(v, { xmlns = mapping[2] }):up(); | 499 s:tag(v, { xmlns = mapping.xmlns }):up(); |
| 500 elseif mapping[1] == "attr" then | 500 elseif mapping.type == "attr" then |
| 501 s:tag(mapping[3] or k, { xmlns = mapping[2], [ mapping[4] or k ] = v }):up(); | 501 s:tag(mapping.tagname or k, { xmlns = mapping.xmlns, [mapping.attr or k] = v }):up(); |
| 502 elseif mapping[1] == "bool_tag" then | 502 elseif mapping.type == "bool_tag" then |
| 503 s:tag(mapping[3] or k, { xmlns = mapping[2] }):up(); | 503 s:tag(mapping.tagname or k, { xmlns = mapping.xmlns }):up(); |
| 504 elseif mapping[1] == "func" then | 504 elseif mapping.type == "func" and mapping.json2st then |
| 505 s:add_child(mapping[5](v)):up(); | 505 s:add_child(mapping.json2st(v)):up(); |
| 506 end | 506 end |
| 507 else | 507 else |
| 508 return nil, "unknown-field"; | 508 return nil, "unknown-field"; |
| 509 end | 509 end |
| 510 end | 510 end |