mod_smacks: Fix traceback due to session being destroyed in send()
Sending something can cause the OS to notice that the connection is dead
and then the connection can be dead at this point. More likely if
opportunistic_writes is enabled.
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;
};