From d0e23d516bf6a44da81e589e54a20c4303315024 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Wed, 25 Nov 2015 13:42:19 -0500 Subject: [PATCH] initial --- dev/mod_ircd.old_comments | 508 + ircd.sh | 8 + mod_ircd.in.lua | 598 ++ mod_ircd.lua | 8174 +++++++++++++++++ squishy | 12 + verse.orig/verse.lua | 580 ++ verse/.hg/00changelog.i | Bin 0 -> 57 bytes verse/.hg/branch | 1 + verse/.hg/cache/branchheads-served | 2 + verse/.hg/dirstate | Bin 0 -> 2026 bytes verse/.hg/hgrc | 2 + verse/.hg/requires | 4 + verse/.hg/store/00changelog.i | Bin 0 -> 77591 bytes verse/.hg/store/00manifest.i | Bin 0 -> 65562 bytes verse/.hg/store/data/_l_i_c_e_n_s_e.i | Bin 0 -> 696 bytes verse/.hg/store/data/bosh.lua.i | Bin 0 -> 3389 bytes verse/.hg/store/data/client.lua.i | Bin 0 -> 8157 bytes verse/.hg/store/data/component.lua.i | Bin 0 -> 2196 bytes verse/.hg/store/data/doc/example.lua.i | Bin 0 -> 1096 bytes verse/.hg/store/data/doc/example__adhoc.lua.i | Bin 0 -> 911 bytes verse/.hg/store/data/doc/example__bosh.lua.i | Bin 0 -> 1041 bytes .../.hg/store/data/doc/example__carbons.lua.i | Bin 0 -> 996 bytes .../store/data/doc/example__component.lua.i | Bin 0 -> 1073 bytes .../.hg/store/data/doc/example__jingle.lua.i | Bin 0 -> 1124 bytes .../data/doc/example__jingle__send.lua.i | Bin 0 -> 923 bytes verse/.hg/store/data/doc/example__pep.lua.i | Bin 0 -> 1679 bytes .../.hg/store/data/doc/example__pubsub.lua.i | Bin 0 -> 1225 bytes verse/.hg/store/data/init.lua.i | Bin 0 -> 10627 bytes verse/.hg/store/data/libs/adhoc.lib.lua.i | Bin 0 -> 1228 bytes verse/.hg/store/data/libs/bit.lua.i | Bin 0 -> 3156 bytes verse/.hg/store/data/libs/encodings.lua.i | Bin 0 -> 507 bytes verse/.hg/store/data/libs/hashes.lua.i | Bin 0 -> 122 bytes verse/.hg/store/data/libs/logger.lua.i | Bin 0 -> 632 bytes verse/.hg/store/data/libs/xstanza.lua.i | Bin 0 -> 575 bytes verse/.hg/store/data/plugins/adhoc.lua.i | Bin 0 -> 2155 bytes verse/.hg/store/data/plugins/archive.lua.i | Bin 0 -> 4011 bytes verse/.hg/store/data/plugins/bind.lua.i | Bin 0 -> 1857 bytes verse/.hg/store/data/plugins/blocking.lua.i | Bin 0 -> 545 bytes verse/.hg/store/data/plugins/carbons.lua.i | Bin 0 -> 1829 bytes .../.hg/store/data/plugins/compression.lua.i | Bin 0 -> 2166 bytes verse/.hg/store/data/plugins/disco.lua.i | Bin 0 -> 7295 bytes verse/.hg/store/data/plugins/groupchat.lua.i | Bin 0 -> 4314 bytes verse/.hg/store/data/plugins/jingle.lua.i | Bin 0 -> 3680 bytes verse/.hg/store/data/plugins/jingle__ft.lua.i | Bin 0 -> 1246 bytes .../.hg/store/data/plugins/jingle__ibb.lua.i | Bin 0 -> 2179 bytes .../.hg/store/data/plugins/jingle__s5b.lua.i | Bin 0 -> 3004 bytes verse/.hg/store/data/plugins/keepalive.lua.i | Bin 0 -> 299 bytes verse/.hg/store/data/plugins/legacy.lua.i | Bin 0 -> 1875 bytes verse/.hg/store/data/plugins/pep.lua.i | Bin 0 -> 1601 bytes verse/.hg/store/data/plugins/ping.lua.i | Bin 0 -> 1062 bytes verse/.hg/store/data/plugins/presence.lua.i | Bin 0 -> 835 bytes verse/.hg/store/data/plugins/private.lua.i | Bin 0 -> 1124 bytes verse/.hg/store/data/plugins/proxy65.lua.i | Bin 0 -> 2297 bytes verse/.hg/store/data/plugins/pubsub.lua.i | Bin 0 -> 5826 bytes verse/.hg/store/data/plugins/receipts.lua.i | Bin 0 -> 309 bytes verse/.hg/store/data/plugins/register.lua.i | Bin 0 -> 1044 bytes verse/.hg/store/data/plugins/roster.lua.i | Bin 0 -> 2988 bytes verse/.hg/store/data/plugins/sasl.lua.i | Bin 0 -> 2454 bytes verse/.hg/store/data/plugins/session.lua.i | Bin 0 -> 2122 bytes verse/.hg/store/data/plugins/smacks.lua.i | Bin 0 -> 3793 bytes verse/.hg/store/data/plugins/tls.lua.i | Bin 0 -> 1424 bytes verse/.hg/store/data/plugins/uptime.lua.i | Bin 0 -> 784 bytes verse/.hg/store/data/plugins/vcard.lua.i | Bin 0 -> 1565 bytes .../store/data/plugins/vcard__update.lua.i | Bin 0 -> 1582 bytes verse/.hg/store/data/plugins/version.lua.i | Bin 0 -> 1496 bytes verse/.hg/store/data/squishy.i | Bin 0 -> 7744 bytes verse/.hg/store/data/util/dataforms.lua.i | Bin 0 -> 3105 bytes .../.hg/store/data/util/sasl/anonymous.lua.i | Bin 0 -> 391 bytes verse/.hg/store/data/util/sasl/plain.lua.i | Bin 0 -> 416 bytes verse/.hg/store/data/util/sasl/scram.lua.i | Bin 0 -> 2697 bytes verse/.hg/store/data/util/sha1.lua.i | Bin 0 -> 1480 bytes verse/.hg/store/data/util/vcard.lua.i | Bin 0 -> 5469 bytes verse/.hg/store/fncache | 58 + verse/.hg/store/phaseroots | 0 verse/.hg/store/undo | Bin 0 -> 1643 bytes verse/.hg/store/undo.phaseroots | 0 verse/.hg/undo.bookmarks | 0 verse/.hg/undo.branch | 1 + verse/.hg/undo.desc | 3 + verse/.hg/undo.dirstate | 0 verse/LICENSE | 19 + verse/bosh.lua | 209 + verse/client.lua | 219 + verse/component.lua | 149 + verse/doc/example.lua | 39 + verse/doc/example_adhoc.lua | 48 + verse/doc/example_bosh.lua | 40 + verse/doc/example_carbons.lua | 46 + verse/doc/example_component.lua | 43 + verse/doc/example_jingle.lua | 56 + verse/doc/example_jingle_send.lua | 52 + verse/doc/example_pep.lua | 56 + verse/doc/example_pubsub.lua | 56 + verse/init.lua | 250 + verse/libs/adhoc.lib.lua | 85 + verse/libs/bit.lua | 145 + verse/libs/encodings.lua | 12 + verse/libs/hashes.lua | 3 + verse/libs/xstanza.lua | 27 + verse/plugins/adhoc.lua | 115 + verse/plugins/archive.lua | 139 + verse/plugins/bind.lua | 28 + verse/plugins/blocking.lua | 46 + verse/plugins/carbons.lua | 67 + verse/plugins/compression.lua | 122 + verse/plugins/disco.lua | 369 + verse/plugins/groupchat.lua | 177 + verse/plugins/jingle.lua | 301 + verse/plugins/jingle_ft.lua | 74 + verse/plugins/jingle_ibb.lua | 214 + verse/plugins/jingle_s5b.lua | 220 + verse/plugins/keepalive.lua | 9 + verse/plugins/legacy.lua | 65 + verse/plugins/pep.lua | 34 + verse/plugins/ping.lua | 24 + verse/plugins/presence.lua | 36 + verse/plugins/private.lua | 35 + verse/plugins/proxy65.lua | 186 + verse/plugins/pubsub.lua | 267 + verse/plugins/receipts.lua | 16 + verse/plugins/register.lua | 32 + verse/plugins/roster.lua | 161 + verse/plugins/sasl.lua | 80 + verse/plugins/session.lua | 30 + verse/plugins/smacks.lua | 144 + verse/plugins/tls.lua | 36 + verse/plugins/uptime.lua | 42 + verse/plugins/vcard.lua | 36 + verse/plugins/vcard_update.lua | 103 + verse/plugins/version.lua | 57 + verse/squishy | 84 + verse/util/dataforms.lua | 282 + verse/util/sasl/anonymous.lua | 8 + verse/util/sasl/plain.lua | 9 + verse/util/sasl/scram.lua | 109 + verse/util/sha1.lua | 146 + verse/util/vcard.lua | 464 + verse/verse.lua | 7669 ++++++++++++++++ 138 files changed, 23541 insertions(+) create mode 100644 dev/mod_ircd.old_comments create mode 100755 ircd.sh create mode 100644 mod_ircd.in.lua create mode 100644 mod_ircd.lua create mode 100644 squishy create mode 100644 verse.orig/verse.lua create mode 100644 verse/.hg/00changelog.i create mode 100644 verse/.hg/branch create mode 100644 verse/.hg/cache/branchheads-served create mode 100644 verse/.hg/dirstate create mode 100644 verse/.hg/hgrc create mode 100644 verse/.hg/requires create mode 100644 verse/.hg/store/00changelog.i create mode 100644 verse/.hg/store/00manifest.i create mode 100644 verse/.hg/store/data/_l_i_c_e_n_s_e.i create mode 100644 verse/.hg/store/data/bosh.lua.i create mode 100644 verse/.hg/store/data/client.lua.i create mode 100644 verse/.hg/store/data/component.lua.i create mode 100644 verse/.hg/store/data/doc/example.lua.i create mode 100644 verse/.hg/store/data/doc/example__adhoc.lua.i create mode 100644 verse/.hg/store/data/doc/example__bosh.lua.i create mode 100644 verse/.hg/store/data/doc/example__carbons.lua.i create mode 100644 verse/.hg/store/data/doc/example__component.lua.i create mode 100644 verse/.hg/store/data/doc/example__jingle.lua.i create mode 100644 verse/.hg/store/data/doc/example__jingle__send.lua.i create mode 100644 verse/.hg/store/data/doc/example__pep.lua.i create mode 100644 verse/.hg/store/data/doc/example__pubsub.lua.i create mode 100644 verse/.hg/store/data/init.lua.i create mode 100644 verse/.hg/store/data/libs/adhoc.lib.lua.i create mode 100644 verse/.hg/store/data/libs/bit.lua.i create mode 100644 verse/.hg/store/data/libs/encodings.lua.i create mode 100644 verse/.hg/store/data/libs/hashes.lua.i create mode 100644 verse/.hg/store/data/libs/logger.lua.i create mode 100644 verse/.hg/store/data/libs/xstanza.lua.i create mode 100644 verse/.hg/store/data/plugins/adhoc.lua.i create mode 100644 verse/.hg/store/data/plugins/archive.lua.i create mode 100644 verse/.hg/store/data/plugins/bind.lua.i create mode 100644 verse/.hg/store/data/plugins/blocking.lua.i create mode 100644 verse/.hg/store/data/plugins/carbons.lua.i create mode 100644 verse/.hg/store/data/plugins/compression.lua.i create mode 100644 verse/.hg/store/data/plugins/disco.lua.i create mode 100644 verse/.hg/store/data/plugins/groupchat.lua.i create mode 100644 verse/.hg/store/data/plugins/jingle.lua.i create mode 100644 verse/.hg/store/data/plugins/jingle__ft.lua.i create mode 100644 verse/.hg/store/data/plugins/jingle__ibb.lua.i create mode 100644 verse/.hg/store/data/plugins/jingle__s5b.lua.i create mode 100644 verse/.hg/store/data/plugins/keepalive.lua.i create mode 100644 verse/.hg/store/data/plugins/legacy.lua.i create mode 100644 verse/.hg/store/data/plugins/pep.lua.i create mode 100644 verse/.hg/store/data/plugins/ping.lua.i create mode 100644 verse/.hg/store/data/plugins/presence.lua.i create mode 100644 verse/.hg/store/data/plugins/private.lua.i create mode 100644 verse/.hg/store/data/plugins/proxy65.lua.i create mode 100644 verse/.hg/store/data/plugins/pubsub.lua.i create mode 100644 verse/.hg/store/data/plugins/receipts.lua.i create mode 100644 verse/.hg/store/data/plugins/register.lua.i create mode 100644 verse/.hg/store/data/plugins/roster.lua.i create mode 100644 verse/.hg/store/data/plugins/sasl.lua.i create mode 100644 verse/.hg/store/data/plugins/session.lua.i create mode 100644 verse/.hg/store/data/plugins/smacks.lua.i create mode 100644 verse/.hg/store/data/plugins/tls.lua.i create mode 100644 verse/.hg/store/data/plugins/uptime.lua.i create mode 100644 verse/.hg/store/data/plugins/vcard.lua.i create mode 100644 verse/.hg/store/data/plugins/vcard__update.lua.i create mode 100644 verse/.hg/store/data/plugins/version.lua.i create mode 100644 verse/.hg/store/data/squishy.i create mode 100644 verse/.hg/store/data/util/dataforms.lua.i create mode 100644 verse/.hg/store/data/util/sasl/anonymous.lua.i create mode 100644 verse/.hg/store/data/util/sasl/plain.lua.i create mode 100644 verse/.hg/store/data/util/sasl/scram.lua.i create mode 100644 verse/.hg/store/data/util/sha1.lua.i create mode 100644 verse/.hg/store/data/util/vcard.lua.i create mode 100644 verse/.hg/store/fncache create mode 100644 verse/.hg/store/phaseroots create mode 100644 verse/.hg/store/undo create mode 100644 verse/.hg/store/undo.phaseroots create mode 100644 verse/.hg/undo.bookmarks create mode 100644 verse/.hg/undo.branch create mode 100644 verse/.hg/undo.desc create mode 100644 verse/.hg/undo.dirstate create mode 100644 verse/LICENSE create mode 100644 verse/bosh.lua create mode 100644 verse/client.lua create mode 100644 verse/component.lua create mode 100644 verse/doc/example.lua create mode 100644 verse/doc/example_adhoc.lua create mode 100644 verse/doc/example_bosh.lua create mode 100644 verse/doc/example_carbons.lua create mode 100644 verse/doc/example_component.lua create mode 100644 verse/doc/example_jingle.lua create mode 100644 verse/doc/example_jingle_send.lua create mode 100644 verse/doc/example_pep.lua create mode 100644 verse/doc/example_pubsub.lua create mode 100644 verse/init.lua create mode 100644 verse/libs/adhoc.lib.lua create mode 100644 verse/libs/bit.lua create mode 100644 verse/libs/encodings.lua create mode 100644 verse/libs/hashes.lua create mode 100644 verse/libs/xstanza.lua create mode 100644 verse/plugins/adhoc.lua create mode 100644 verse/plugins/archive.lua create mode 100644 verse/plugins/bind.lua create mode 100644 verse/plugins/blocking.lua create mode 100644 verse/plugins/carbons.lua create mode 100644 verse/plugins/compression.lua create mode 100644 verse/plugins/disco.lua create mode 100644 verse/plugins/groupchat.lua create mode 100644 verse/plugins/jingle.lua create mode 100644 verse/plugins/jingle_ft.lua create mode 100644 verse/plugins/jingle_ibb.lua create mode 100644 verse/plugins/jingle_s5b.lua create mode 100644 verse/plugins/keepalive.lua create mode 100644 verse/plugins/legacy.lua create mode 100644 verse/plugins/pep.lua create mode 100644 verse/plugins/ping.lua create mode 100644 verse/plugins/presence.lua create mode 100644 verse/plugins/private.lua create mode 100644 verse/plugins/proxy65.lua create mode 100644 verse/plugins/pubsub.lua create mode 100644 verse/plugins/receipts.lua create mode 100644 verse/plugins/register.lua create mode 100644 verse/plugins/roster.lua create mode 100644 verse/plugins/sasl.lua create mode 100644 verse/plugins/session.lua create mode 100644 verse/plugins/smacks.lua create mode 100644 verse/plugins/tls.lua create mode 100644 verse/plugins/uptime.lua create mode 100644 verse/plugins/vcard.lua create mode 100644 verse/plugins/vcard_update.lua create mode 100644 verse/plugins/version.lua create mode 100644 verse/squishy create mode 100644 verse/util/dataforms.lua create mode 100644 verse/util/sasl/anonymous.lua create mode 100644 verse/util/sasl/plain.lua create mode 100644 verse/util/sasl/scram.lua create mode 100644 verse/util/sha1.lua create mode 100644 verse/util/vcard.lua create mode 100644 verse/verse.lua diff --git a/dev/mod_ircd.old_comments b/dev/mod_ircd.old_comments new file mode 100644 index 0000000..063acc0 --- /dev/null +++ b/dev/mod_ircd.old_comments @@ -0,0 +1,508 @@ +-- README +-- Squish verse into this dir, then squish them into one, which you move +-- and rename to mod_ircd.lua in your prosody modules/plugins dir. +-- +-- IRC spec: +-- http://tools.ietf.org/html/rfc2812 +local _module = module +module = _G.module +local module = _module +-- +local component_jid, component_secret, muc_server, port_number = + module.host, nil, module:get_option_string("conference_server"), module:get_option_number("listener_port", 7000); + +if not muc_server then + module:log ("error", "You need to set the MUC server! halting.") + return false; +end + +package.loaded["util.sha1"] = require "util.encodings"; +local verse = require "verse" +require "verse.component" +require "socket" +c = verse.new();--verse.logger()) +c:add_plugin("groupchat"); + +local function verse2prosody(e) + return c:event("stanza", e.stanza) or true; +end +module:hook("message/bare", verse2prosody); +module:hook("message/full", verse2prosody); +module:hook("presence/bare", verse2prosody); +module:hook("presence/full", verse2prosody); +c.type = "component"; +c.send = core_post_stanza; + +-- This plugin is actually a verse based component, but that mode is currently commented out + +-- Add some hooks for debugging +--c:hook("opened", function () print("Stream opened!") end); +--c:hook("closed", function () print("Stream closed!") end); +--c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +--c:hook("incoming-raw", print, 1000); +--c:hook("stanza", print, 1000); +--c:hook("outgoing-raw", print, 1000); + +-- Print a message after authentication +--c:hook("authentication-success", function () print("Logged in!"); end); +--c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +--c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +--c.connect_host = "127.0.0.1" +--c:connect_component(component_jid, component_secret); + +local jid = require "util.jid"; +local nodeprep = require "util.encodings".stringprep.nodeprep; + +local function utf8_clean (s) + local push, join = table.insert, table.concat; + local r, i = {}, 1; + if not(s and #s > 0) then + return "" + end + while true do + local c = s:sub(i,i) + local b = c:byte(); + local w = ( + (b >= 9 and b <= 10 and 0) or + (b >= 32 and b <= 126 and 0) or + (b >= 192 and b <= 223 and 1) or + (b >= 224 and b <= 239 and 2) or + (b >= 240 and b <= 247 and 3) or + (b >= 248 and b <= 251 and 4) or + (b >= 251 and b <= 252 and 5) or nil + ) + if not w then + push(r, "?") + else + local n = i + w; + if w == 0 then + push(r, c); + elseif n > #s then + push(r, ("?"):format(b)); + else + local e = s:sub(i+1,n); + if e:match('^[\128-\191]*$') then + push(r, c); + push(r, e); + i = n; + else + push(r, ("?"):format(b)); + end + end + end + i = i + 1; + if i > #s then + break + end + end + return join(r); +end + +local function parse_line(line) + local ret = {}; + if line:sub(1,1) == ":" then + ret.from, line = line:match("^:(%w+)%s+(.*)$"); + end + for part in line:gmatch("%S+") do + if part:sub(1,1) == ":" then + ret[#ret+1] = line:match(":(.*)$"); + break + end + ret[#ret+1]=part; + end + return ret; +end + +local function build_line(parts) + if #parts > 1 then + parts[#parts] = ":" .. parts[#parts]; + end + return (parts.from and ":"..parts.from.." " or "")..table.concat(parts, " "); +end + +local function irc2muc(channel, nick) + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + return jid.join(room, muc_server, nick) +end +local function muc2irc(room) + local channel, _, nick = jid.split(room); + return "#"..channel, nick; +end +local role_map = { + moderator = "@", + participant = "", + visitor = "", + none = "" +} +local aff_map = { + owner = "~", + administrator = "&", + member = "+", + none = "" +} +local role_modemap = { + moderator = "o", + participant = "", + visitor = "", + none = "" +} +local aff_modemap = { + owner = "q", + administrator = "a", + member = "v", + none = "" +} + +local irc_listener = { default_port = port_number, default_mode = "*l" }; + +local sessions = {}; +local jids = {}; +local commands = {}; + +local nicks = {}; + +local st = require "util.stanza"; + +local conference_server = muc_server; + +local function irc_close_session(session) + session.conn:close(); +end + +function irc_listener.onincoming(conn, data) + local session = sessions[conn]; + if not session then + session = { conn = conn, host = component_jid, reset_stream = function () end, + close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), + rooms = {}, + roster = {} }; + sessions[conn] = session; + function session.data(data) + local parts = parse_line(data); + module:log("debug", require"util.serialization".serialize(parts)); + local command = table.remove(parts, 1); + if not command then + return; + end + command = command:upper(); + if not session.nick then + if not (command == "USER" or command == "NICK") then + module:log("debug", "Client tried to send command %s before registering", command); + return session.send{from=muc_server, "451", command, "You have not registered"} + end + end + if commands[command] then + local ret = commands[command](session, parts); + if ret then + return session.send(ret); + end + else + session.send{from=muc_server, "421", session.nick, command, "Unknown command"}; + return module:log("debug", "Unknown command: %s", command); + end + end + function session.send(data) + if type(data) == "string" then + return conn:write(data.."\r\n"); + elseif type(data) == "table" then + local line = build_line(data); + module:log("debug", line); + conn:write(line.."\r\n"); + end + end + end + if data then + session.data(data); + end +end + +function irc_listener.ondisconnect(conn, error) + local session = sessions[conn]; + if session then + for _, room in pairs(session.rooms) do + room:leave("Disconnected"); + end + if session.nick then + nicks[session.nick] = nil; + end + if session.full_jid then + jids[session.full_jid] = nil; + end + end + sessions[conn] = nil; +end + +function commands.NICK(session, args) + if session.nick then + session.send{from = muc_server, "484", "*", nick, "I'm afraid I can't let you do that"}; + --TODO Loop throug all rooms and change nick, with help from Verse. + return; + end + local nick = args[1]; + nick = nick:gsub("[^%w_]",""); + if nicks[nick] then + session.send{from=muc_server, "433", nick, "The nickname "..nick.." is already in use"}; + return; + end + local full_jid = jid.join(nick, component_jid, "ircd"); + jids[full_jid] = session; + jids[full_jid]["ar_last"] = {}; + nicks[nick] = session; + session.nick = nick; + session.full_jid = full_jid; + session.type = "c2s"; + + session.send{from = muc_server, "001", nick, "Welcome in the IRC to MUC XMPP Gateway, "..nick}; + session.send{from = muc_server, "002", nick, "Your host is "..muc_server.." running Prosody "..prosody.version}; + session.send{from = muc_server, "003", nick, "This server was created the "..os.date(nil, prosody.start_time)} + session.send{from = muc_server, "004", nick, table.concat({muc_server, "mod_ircd(alpha-0.8)", "i", "aoqv"}, " ")}; + session.send{from = muc_server, "375", nick, "- "..muc_server.." Message of the day -"}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Please be warned that this is only a partial irc implementation,"}; + session.send{from = muc_server, "372", nick, "- it's made to facilitate users transiting away from irc to XMPP."}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Prosody is _NOT_ an IRC Server and it never will."}; + session.send{from = muc_server, "372", nick, "- We also would like to remind you that this plugin is provided as is,"}; + session.send{from = muc_server, "372", nick, "- it's still an Alpha and it's still a work in progress, use it at your sole"}; + session.send{from = muc_server, "372", nick, "- risk as there's a not so little chance something will break."}; + + session.send{from = nick, "MODE", nick, "+i"}; -- why -> Invisible mode setting, + -- enforce by default on most servers (since the source host doesn't show it's sensible to have it "set") +end + +function commands.USER(session, params) + -- FIXME + -- Empty command for now +end + +local function mode_map(am, rm, nicks) + local rnick; + local c_modes; + c_modes = aff_modemap[am]..role_modemap[rm] + rnick = string.rep(nicks.." ", c_modes:len()) + if c_modes == "" then return nil, nil end + return c_modes, rnick +end + +function commands.JOIN(session, args) + local channel = args[1]; + if not channel then return end + local room_jid = irc2muc(channel); + print(session.full_jid); + if not jids[session.full_jid].ar_last[room_jid] then jids[session.full_jid].ar_last[room_jid] = {}; end + local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); + if not room then + return ":"..muc_server.." ERR :Could not join room: "..err + end + session.rooms[channel] = room; + room.channel = channel; + room.session = session; + session.send{from=session.nick, "JOIN", channel}; + if room.subject then + session.send{from=muc_server, 332, session.nick, channel ,room.subject}; + end + commands.NAMES(session, channel); + + room:hook("subject-changed", function(changed) + session.send((":%s TOPIC %s :%s"):format(changed.by.nick, channel, changed.to or "")); + end); + + room:hook("message", function(event) + if not event.body then return end + local nick, body = event.nick, event.body; + if nick ~= session.nick then + if body:sub(1,4) == "/me " then + body = "\1ACTION ".. body:sub(5) .. "\1" + end + local type = event.stanza.attr.type; + session.send{from=nick, "PRIVMSG", type == "groupchat" and channel or nick, body}; + --FIXME PM's probably won't work + end + end); + + room:hook("presence", function(ar) + local c_modes; + local rnick; + if ar.nick and not jids[session.full_jid].ar_last[ar.room_jid][ar.nick] then jids[session.full_jid].ar_last[ar.room_jid][ar.nick] = {} end + local x_ar = ar.stanza:get_child("x", "http://jabber.org/protocol/muc#user") + if x_ar then + local xar_item = x_ar:get_child("item") + if xar_item and xar_item.attr and ar.stanza.attr.type ~= "unavailable" then + if xar_item.attr.affiliation and xar_item.attr.role then + if not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] and + not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] then + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + else + c_modes, rnick = mode_map(jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"], jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"], ar.nick); + if c_modes and rnick then session.send((":%s MODE %s -%s"):format(muc_server, channel, c_modes.." "..rnick)); end + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + end + end + end + end + end, -1); +end + +c:hook("groupchat/joined", function(room) + local session = room.session or jids[room.opts.source]; + local channel = "#"..room.jid:match("^(.*)@"); + session.send{from=session.nick.."!"..session.nick, "JOIN", channel}; + if room.topic then + session.send{from=muc_server, 332, room.topic}; + end + commands.NAMES(session, channel) + room:hook("occupant-joined", function(nick) + session.send{from=nick.nick.."!"..nick.nick, "JOIN", channel}; + end); + room:hook("occupant-left", function(nick) + jids[session.full_jid].ar_last[nick.jid:match("^(.*)/")][nick.nick] = nil; -- ugly + session.send{from=nick.nick.."!"..nick.nick, "PART", channel}; + end); +end); + +function commands.NAMES(session, channel) + local nicks = { }; + local room = session.rooms[channel]; + local symbols_map = { + owner = "~", + administrator = "&", + moderator = "@", + member = "+" + } + + if not room then return end + -- TODO Break this out into commands.NAMES + for nick, n in pairs(room.occupants) do + if n.affiliation == "owner" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "administrator" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "member" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + elseif n.affiliation == "member" and n.role == "partecipant" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "none" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + end + table.insert(nicks, nick); + end + nicks = table.concat(nicks, " "); + session.send((":%s 353 %s = %s :%s"):format(muc_server, session.nick, channel, nicks)); + session.send((":%s 366 %s %s :End of /NAMES list."):format(muc_server, session.nick, channel)); + session.send(":"..muc_server.." 353 "..session.nick.." = "..channel.." :"..nicks); +end + +function commands.PART(session, args) + local channel, part_message = unpack(args); + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not room then return end + channel = channel:match("^([%S]*)"); + session.rooms[channel]:leave(part_message); + jids[session.full_jid].ar_last[room.."@"..muc_server] = nil; + session.send(":"..session.nick.." PART :"..channel); +end + +function commands.PRIVMSG(session, args) + local channel, message = unpack(args); + if message and #message > 0 then + if message:sub(1,8) == "\1ACTION " then + message = "/me ".. message:sub(9,-2) + end + message = utf8_clean(message); + if channel:sub(1,1) == "#" then + if session.rooms[channel] then + module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); + session.rooms[channel]:send_message(message); + end + else -- private message + local nick = channel; + module:log("debug", "PM to %s", nick); + for channel, room in pairs(session.rooms) do + module:log("debug", "looking for %s in %s", nick, channel); + if room.occupants[nick] then + module:log("debug", "found %s in %s", nick, channel); + local who = room.occupants[nick]; + -- FIXME PMs in verse + --room:send_private_message(nick, message); + local pm = st.message({type="chat",to=who.jid}, message); + module:log("debug", "sending PM to %s: %s", nick, tostring(pm)); + room:send(pm) + break + end + end + end + end +end + +function commands.PING(session, args) + session.send{from=muc_server, "PONG", args[1]}; +end + +function commands.TOPIC(session, message) + if not message then return end + local channel, topic = message[1], message[2]; + channel = utf8_clean(channel); + topic = utf8_clean(topic); + if not channel then return end + local room = session.rooms[channel]; + + if topic then room:set_subject(topic); end +end + +function commands.WHO(session, args) + local channel = args[1]; + if session.rooms[channel] then + local room = session.rooms[channel] + for nick in pairs(room.occupants) do + --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild + session.send{from=muc_server, 352, session.nick, channel, nick, nick, muc_server, nick, "H", "0 "..nick} + end + session.send{from=muc_server, 315, session.nick, channel, "End of /WHO list"}; + end +end + +function commands.MODE(session, args) -- FIXME + -- emptied for the time being, until something sane which works is available. +end + +function commands.QUIT(session, args) + session.send{"ERROR", "Closing Link: "..session.nick}; + for _, room in pairs(session.rooms) do + room:leave(args[1]); + end + jids[session.full_jid] = nil; + nicks[session.nick] = nil; + sessions[session.conn] = nil; + session:close(); +end + +function commands.RAW(session, data) + --c:send(data) +end + +local function desetup() + require "net.connlisteners".deregister("irc"); +end + +--c:hook("ready", function () + require "net.connlisteners".register("irc", irc_listener); + require "net.connlisteners".start("irc"); +--end); + +module:hook("module-unloaded", desetup) + + +--print("Starting loop...") +--verse.loop() diff --git a/ircd.sh b/ircd.sh new file mode 100755 index 0000000..23b4a8c --- /dev/null +++ b/ircd.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +squish +echo -e 'config:reload()\nmodule:reload("ircd", "burtrum.org")' | telnet localhost 5582 + +exit + +echo -e 'module:load("ircd", "burtrum.org")' | telnet localhost 5582 \ No newline at end of file diff --git a/mod_ircd.in.lua b/mod_ircd.in.lua new file mode 100644 index 0000000..5b5b014 --- /dev/null +++ b/mod_ircd.in.lua @@ -0,0 +1,598 @@ +-- README +-- Squish verse into this dir, then squish them into one, which you move +-- and rename to mod_ircd.lua in your prosody modules/plugins dir. +-- +-- IRC spec: +-- http://tools.ietf.org/html/rfc2812 + +local client_xmlns = "jabber:client" + +--module:set_global(); + +local server = require "net.server"; +local portmanager = require "core.portmanager"; + +local component_jid, component_secret, muc_server, port_number = + module.host, nil, module:get_option_string("conference_server"), module:get_option_number("listener_port", 7000); + +if not muc_server then + module:log ("error", "You need to set the MUC server in the configuration (conference_server)!") + module:log ("error", "Be a good boy or girl and go read the wiki at: http://code.google.com/p/prosody-modules/wiki/mod_ircd") + return false; +end + +package.loaded["util.sha1"] = require "util.encodings"; +--local verse = require "verse" +--require "verse.component" +--require "socket" +require "verse".init("component"); + +c = verse.new(); -- something interferes with prosody's console logging +c:add_plugin("groupchat"); + +local function verse2prosody(e) + return c:event("stanza", e.stanza) or true; +end +module:hook("message/bare", verse2prosody); +module:hook("message/full", verse2prosody); +module:hook("presence/bare", verse2prosody); +module:hook("presence/full", verse2prosody); +c.type = "component"; +c.send = core_post_stanza; + +local jid = require "util.jid"; +local nodeprep = require "util.encodings".stringprep.nodeprep; + +local function utf8_clean (s) + local push, join = table.insert, table.concat; + local r, i = {}, 1; + if not(s and #s > 0) then + return "" + end + while true do + local c = s:sub(i,i) + local b = c:byte(); + local w = ( + (b >= 9 and b <= 10 and 0) or + (b >= 32 and b <= 126 and 0) or + (b >= 192 and b <= 223 and 1) or + (b >= 224 and b <= 239 and 2) or + (b >= 240 and b <= 247 and 3) or + (b >= 248 and b <= 251 and 4) or + (b >= 251 and b <= 252 and 5) or nil + ) + if not w then + push(r, "?") + else + local n = i + w; + if w == 0 then + push(r, c); + elseif n > #s then + push(r, ("?"):format(b)); + else + local e = s:sub(i+1,n); + if e:match('^[\128-\191]*$') then + push(r, c); + push(r, e); + i = n; + else + push(r, ("?"):format(b)); + end + end + end + i = i + 1; + if i > #s then + break + end + end + return join(r); +end + +local function parse_line(line) + local ret = {}; + if line:sub(1,1) == ":" then + ret.from, line = line:match("^:(%w+)%s+(.*)$"); + end + for part in line:gmatch("%S+") do + if part:sub(1,1) == ":" then + ret[#ret+1] = line:match(":(.*)$"); + break + end + ret[#ret+1]=part; + end + return ret; +end + +local function build_line(parts) + if #parts > 1 then + parts[#parts] = ":" .. parts[#parts]; + end + return (parts.from and ":"..parts.from.." " or "")..table.concat(parts, " "); +end + +local function irc2muc(channel, nick) + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not nick then + return jid.join(room, muc_server); + else + return jid.join(room, muc_server, nick); + end +end +local function muc2irc(room) + local channel, _, nick = jid.split(room); + return "#"..channel, nick; +end +local role_map = { + moderator = "@", + participant = "", + visitor = "", + none = "" +} +local aff_map = { + owner = "~", + administrator = "&", + member = "+", + none = "" +} +local role_modemap = { + moderator = "o", + participant = "", + visitor = "", + none = "" +} +local aff_modemap = { + owner = "q", + administrator = "a", + member = "v", + none = "" +} + +local irc_listener = { default_port = port_number, default_mode = "*l"; interface = "192.168.1.3" }; + +local sessions = module:shared("sessions"); +local jids = {}; +local commands = {}; + +local nicks = {}; +local usernames = {}; + +local st = require "util.stanza"; + +local conference_server = muc_server; + +local function irc_close_session(session) + session.conn:close(); +end + +function irc_listener.onincoming(conn, data) + local session = sessions[conn]; + if not session then + session = { conn = conn, host = component_jid, reset_stream = function () end, + close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), + rooms = {}, roster = {}, has_un = false }; + sessions[conn] = session; + + function session.data(data) + local parts = parse_line(data); + module:log("debug", require"util.serialization".serialize(parts)); + local command = table.remove(parts, 1); + if not command then + return; + end + command = command:upper(); + if not session.username and not session.nick then + if not (command == "USER" or command == "NICK") then + module:log("debug", "Client tried to send command %s before registering", command); + return session.send{from=muc_server, "451", command, "You have not completed the registration."} + end + end + if commands[command] then + local ret = commands[command](session, parts); + if ret then + return session.send(ret); + end + else + session.send{from=muc_server, "421", session.nick, command, "Unknown command"}; + return module:log("debug", "Unknown command: %s", command); + end + end + + function session.send(data) + if type(data) == "string" then + return conn:write(data.."\r\n"); + elseif type(data) == "table" then + local line = build_line(data); + module:log("debug", line); + conn:write(line.."\r\n"); + end + end + end + + if data then + session.data(data); + end +end + +function irc_listener.ondisconnect(conn, error) + local session = sessions[conn]; + + if session then + for _, room in pairs(session.rooms) do + room:leave("Disconnected"); + end + if session.nick then + nicks[session.nick] = nil; + end + if session.full_jid then + jids[session.full_jid] = nil; + end + if session.username then + usernames[session.username] = nil; + end + end + sessions[conn] = nil; +end + +local function nick_inuse(nick) + if nicks[nick] then return true else return false end +end +local function check_username(un) + local count = 0; + local result; + + for name, given in pairs(usernames) do + if un == given then count = count + 1; end + end + + result = count + 1; + + if count > 0 then return tostring(un)..tostring(result); else return tostring(un); end +end +local function set_t_data(session, full_jid) + session.full_jid = full_jid; + jids[full_jid] = session; + jids[full_jid]["ar_last"] = {}; + jids[full_jid]["nicks_changing"] = {}; + + if session.nick then nicks[session.nick] = session; end +end +local function send_motd(session) + local nick = session.nick; + + if session.username and session.nick then -- send MOTD only if username and nick are set + session.send{from = muc_server, "001", nick, "Welcome in the IRC to MUC XMPP Gateway, "..nick}; + session.send{from = muc_server, "002", nick, "Your host is "..muc_server.." running Prosody "..prosody.version}; + session.send{from = muc_server, "003", nick, "This server was created the "..os.date(nil, prosody.start_time)} + session.send{from = muc_server, "004", nick, table.concat({muc_server, "mod_ircd(alpha-0.8)", "i", "aoqv"}, " ")}; + session.send((":%s %s %s %s :%s"):format(muc_server, "005", nick, "CHANTYPES=# PREFIX=(qaov)~&@+", "are supported by this server")); + session.send((":%s %s %s %s :%s"):format(muc_server, "005", nick, "STATUSMSG=~&@+", "are supported by this server")); + session.send{from = muc_server, "375", nick, "- "..muc_server.." Message of the day -"}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Please be warned that this is only a partial irc implementation,"}; + session.send{from = muc_server, "372", nick, "- it's made to facilitate users transiting away from irc to XMPP."}; + session.send{from = muc_server, "372", nick, "-"}; + session.send{from = muc_server, "372", nick, "- Prosody is _NOT_ an IRC Server and it never will."}; + session.send{from = muc_server, "372", nick, "- We also would like to remind you that this plugin is provided as is,"}; + session.send{from = muc_server, "372", nick, "- it's still an Alpha and it's still a work in progress, use it at your sole"}; + session.send{from = muc_server, "372", nick, "- risk as there's a not so little chance something will break."}; + session.send{from = nick, "MODE", nick, "+i"}; -- why -> Invisible mode setting, + end -- enforce by default on most servers (since the source host doesn't show it's sensible to have it "set") +end + +function commands.NICK(session, args) + local nick = args[1]; + nick = nick:gsub("[^%w_]",""); + + if session.nick and not nick_inuse(nick) then -- changing nick + local oldnick = session.nick; + + -- update and replace session data + session.nick = nick; + nicks[oldnick] = nil; + nicks[nick] = session; + + session.send{from=oldnick.."!"..nicks[nick].username, "NICK", nick}; + + -- broadcast changes if required + if session.rooms then + session.nicks_changing[nick] = { oldnick, session.username }; + + for id, room in pairs(session.rooms) do room:change_nick(nick); end + + session.nicks_changing[nick] = nil; + end + + return; + elseif nick_inuse(nick) then + session.send{from=muc_server, "433", nick, "The nickname "..nick.." is already in use"}; return; + end + + session.nick = nick; + session.type = "c2s"; + nicks[nick] = session; + + -- Some choppy clients send in NICK before USER, that needs to be handled + if session.username then + set_t_data(session, jid.join(session.username, component_jid, "ircd")); + end + + send_motd(session); +end + +function commands.USER(session, params) + local username = params[1]; + + if not session.has_un then + local un_checked = check_username(username); + + usernames[un_checked] = username; + session.username = un_checked; + session.has_un = true; + + if not session.full_jid then + set_t_data(session, jid.join(session.username, component_jid, "ircd")); + end + else + return session.send{from=muc_server, "462", "USER", "You may not re-register."} + end + + send_motd(session); +end + +function commands.USERHOST(session, params) -- can show only users on the gateway. Needed for some clients to determinate self hostmask. + local nick = params[1]; + + if not nick then session.send{from=muc_server, "461", "USERHOST", "Not enough parameters"}; return; end + + if nicks[nick] and nicks[nick].nick and nicks[nick].username then + session.send{from=muc_server, "302", session.nick, nick.."=+"..nicks[nick].username}; return; + else + return; + end +end + +local function mode_map(am, rm, nicks) + local rnick; + local c_modes; + c_modes = aff_modemap[am]..role_modemap[rm] + rnick = string.rep(nicks.." ", c_modes:len()) + if c_modes == "" then return nil, nil end + return c_modes, rnick +end + +function commands.JOIN(session, args) + local channel = args[1]; + if not channel then return end + local room_jid = irc2muc(channel); + + if not jids[session.full_jid].ar_last[room_jid] then jids[session.full_jid].ar_last[room_jid] = {}; end + local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); + if not room then + return ":"..muc_server.." ERR :Could not join room: "..err + end + + session.rooms[channel] = room; + room.session = session; + + if session.nicks_changing[session.nick] then -- my own nick is changing + commands.NAMES(session, channel); + else + session.send{from=session.nick.."!"..session.username, "JOIN", channel}; + if room.subject then + session.send{from=muc_server, 332, session.nick, channel, room.subject}; + end + commands.NAMES(session, channel); + end + + room:hook("subject-changed", function(changed) + session.send{from=changed.by.nick, "TOPIC", channel, changed.to or ""} + end); + + room:hook("message", function(event) + if not event.body then return end + local nick, body = event.nick, event.body; + if nick ~= session.nick then + if body:sub(1,4) == "/me " then + body = "\1ACTION ".. body:sub(5) .. "\1" + end + local type = event.stanza.attr.type; + session.send{from=nick, "PRIVMSG", type == "groupchat" and channel or nick, body}; + --FIXME PM's probably won't work + end + end); + + room:hook("presence", function(ar) + local c_modes; + local rnick; + if ar.nick and not jids[session.full_jid].ar_last[ar.room_jid][ar.nick] then jids[session.full_jid].ar_last[ar.room_jid][ar.nick] = {} end + local x_ar = ar.stanza:get_child("x", "http://jabber.org/protocol/muc#user") + if x_ar then + local xar_item = x_ar:get_child("item") + if xar_item and xar_item.attr and ar.stanza.attr.type ~= "unavailable" then + if xar_item.attr.affiliation and xar_item.attr.role then + if not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] and + not jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] then + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + n_self_changing = nicks[ar.nick] and nicks[ar.nick].nicks_changing and nicks[ar.nick].nicks_changing[ar.nick] + if n_self_changing then return; end + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + else + c_modes, rnick = mode_map(jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"], jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"], ar.nick); + if c_modes and rnick then session.send((":%s MODE %s -%s"):format(muc_server, channel, c_modes.." "..rnick)); end + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["affiliation"] = xar_item.attr.affiliation + jids[session.full_jid].ar_last[ar.room_jid][ar.nick]["role"] = xar_item.attr.role + n_self_changing = nicks[ar.nick] and nicks[ar.nick].nicks_changing and nicks[ar.nick].nicks_changing[ar.nick] + if n_self_changing then return; end + c_modes, rnick = mode_map(xar_item.attr.affiliation, xar_item.attr.role, ar.nick); + if c_modes and rnick then session.send((":%s MODE %s +%s"):format(muc_server, channel, c_modes.." "..rnick)); end + end + end + end + end + end, -1); +end + +c:hook("groupchat/joined", function(room) + local session = room.session or jids[room.opts.source]; + local channel = "#"..room.jid:match("^(.*)@"); + + room:hook("occupant-joined", function(nick) + if session.nicks_changing[nick.nick] then + session.send{from=session.nicks_changing[nick.nick][1].."!"..(session.nicks_changing[nick.nick][2] or "xmpp"), "NICK", nick.nick}; + session.nicks_changing[nick.nick] = nil; + else + session.send{from=nick.nick.."!"..(nicks[nick.nick] and nicks[nick.nick].username or "xmpp"), "JOIN", channel}; + end + end); + room:hook("occupant-left", function(nick) + if jids[session.full_jid] then jids[session.full_jid].ar_last[nick.jid:match("^(.*)/")][nick.nick] = nil; end + local status_code = + nick.presence:get_child("x","http://jabber.org/protocol/muc#user") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status").attr.code; + + + if status_code == "303" then + local newnick = + nick.presence:get_child("x","http://jabber.org/protocol/muc#user") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item") and + nick.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item").attr.nick; + + session.nicks_changing[newnick] = { nick.nick, (nicks[nick.nick] and nicks[nick.nick].username or "xmpp") }; return; + end + + for id, data in pairs(session.nicks_changing) do + if data[1] == nick.nick then return; end + end + session.send{from=nick.nick.."!"..(nicks[nick.nick] and nicks[nick.nick].username or "xmpp"), "PART", channel}; + end); +end); + +function commands.NAMES(session, channel) + local nicks = { }; + if type(channel) == "table" then channel = channel[1] end + + local room = session.rooms[channel]; + + local symbols_map = { + owner = "~", + administrator = "&", + moderator = "@", + member = "+" + } + + if not room then return end + -- TODO Break this out into commands.NAMES + for nick, n in pairs(room.occupants) do + if n.affiliation == "owner" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "administrator" and n.role == "moderator" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "member" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + elseif n.affiliation == "member" and n.role == "partecipant" then + nick = symbols_map[n.affiliation]..nick; + elseif n.affiliation == "none" and n.role == "moderator" then + nick = symbols_map[n.role]..nick; + end + table.insert(nicks, nick); + end + nicks = table.concat(nicks, " "); + session.send((":%s 353 %s = %s :%s"):format(muc_server, session.nick, channel, nicks)); + session.send((":%s 366 %s %s :End of /NAMES list."):format(muc_server, session.nick, channel)); + session.send(":"..muc_server.." 353 "..session.nick.." = "..channel.." :"..nicks); +end + +function commands.PART(session, args) + local channel, part_message = unpack(args); + local room = channel and nodeprep(channel:match("^#(%w+)")) or nil; + if not room then return end + channel = channel:match("^([%S]*)"); + session.rooms[channel]:leave(part_message); + jids[session.full_jid].ar_last[room.."@"..muc_server] = nil; + session.send{from=session.nick.."!"..session.username, "PART", channel}; +end + +function commands.PRIVMSG(session, args) + local channel, message = unpack(args); + if message and #message > 0 then + if message:sub(1,8) == "\1ACTION " then + message = "/me ".. message:sub(9,-2) + end + message = utf8_clean(message); + if channel:sub(1,1) == "#" then + if session.rooms[channel] then + module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); + session.rooms[channel]:send_message(message); + end + else -- private message + local nick = channel; + module:log("debug", "PM to %s", nick); + for channel, room in pairs(session.rooms) do + module:log("debug", "looking for %s in %s", nick, channel); + if room.occupants[nick] then + module:log("debug", "found %s in %s", nick, channel); + local who = room.occupants[nick]; + -- FIXME PMs in verse + --room:send_private_message(nick, message); + local pm = st.message({type="chat",to=who.jid}, message); + module:log("debug", "sending PM to %s: %s", nick, tostring(pm)); + room:send(pm) + break + end + end + end + end +end + +function commands.PING(session, args) + session.send{from=muc_server, "PONG", args[1]}; +end + +function commands.TOPIC(session, message) + if not message then return end + local channel, topic = message[1], message[2]; + channel = utf8_clean(channel); + topic = utf8_clean(topic); + if not channel then return end + local room = session.rooms[channel]; + + if topic then room:set_subject(topic); end +end + +function commands.WHO(session, args) + local channel = args[1]; + if session.rooms[channel] then + local room = session.rooms[channel] + for nick in pairs(room.occupants) do + session.send{from=muc_server, 352, session.nick, channel, nick, nick, muc_server, nick, "H", "0 "..nick} + end + session.send{from=muc_server, 315, session.nick, channel, "End of /WHO list"}; + end +end + +function commands.MODE(session, args) -- Empty command +end + +function commands.QUIT(session, args) + session.send{"ERROR", "Closing Link: "..session.nick}; + for _, room in pairs(session.rooms) do + room:leave(args[1]); + end + jids[session.full_jid] = nil; + nicks[session.nick] = nil; + usernames[session.username] = nil; + sessions[session.conn] = nil; + session:close(); +end + +function commands.RAW(session, data) -- Empty command +end + +module:provides("net", { + name = "ircd"; + listener = irc_listener; + default_port = 7000; +}); diff --git a/mod_ircd.lua b/mod_ircd.lua new file mode 100644 index 0000000..f7507a4 --- /dev/null +++ b/mod_ircd.lua @@ -0,0 +1,8174 @@ +package.preload['verse']=(function(...) +package.preload['util.encodings']=(function(...) +local function e() +error("Function not implemented"); +end +local t=require"mime"; +module"encodings" +stringprep={}; +base64={encode=t.b64,decode=e}; +return _M; +end) +package.preload['util.hashes']=(function(...) +local e=require"util.sha1"; +return{sha1=e.sha1}; +end) +package.preload['util.sha1']=(function(...) +local c=string.len +local a=string.char +local b=string.byte +local g=string.sub +local m=math.floor +local t=require"bit" +local k=t.bnot +local e=t.band +local y=t.bor +local n=t.bxor +local i=t.lshift +local o=t.rshift +local u,l,d,h,r +local function p(e,t) +return i(e,t)+o(e,32-t) +end +local function s(i) +local t,o +local t="" +for n=1,8 do +o=e(i,15) +if(o<10)then +t=a(o+48)..t +else +t=a(o+87)..t +end +i=m(i/16) +end +return t +end +local function j(t) +local i,o +local n="" +i=c(t)*8 +t=t..a(128) +o=56-e(c(t),63) +if(o<0)then +o=o+64 +end +for e=1,o do +t=t..a(0) +end +for t=1,8 do +n=a(e(i,255))..n +i=m(i/256) +end +return t..n +end +local function q(w) +local m,t,i,o,f,s,c,v +local a,a +local a={} +while(w~="")do +for e=0,15 do +a[e]=0 +for t=1,4 do +a[e]=a[e]*256+b(w,e*4+t) +end +end +for e=16,79 do +a[e]=p(n(n(a[e-3],a[e-8]),n(a[e-14],a[e-16])),1) +end +m=u +t=l +i=d +o=h +f=r +for h=0,79 do +if(h<20)then +s=y(e(t,i),e(k(t),o)) +c=1518500249 +elseif(h<40)then +s=n(n(t,i),o) +c=1859775393 +elseif(h<60)then +s=y(y(e(t,i),e(t,o)),e(i,o)) +c=2400959708 +else +s=n(n(t,i),o) +c=3395469782 +end +v=p(m,5)+s+f+c+a[h] +f=o +o=i +i=p(t,30) +t=m +m=v +end +u=e(u+m,4294967295) +l=e(l+t,4294967295) +d=e(d+i,4294967295) +h=e(h+o,4294967295) +r=e(r+f,4294967295) +w=g(w,65) +end +end +local function a(e,t) +e=j(e) +u=1732584193 +l=4023233417 +d=2562383102 +h=271733878 +r=3285377520 +q(e) +local e=s(u)..s(l)..s(d) +..s(h)..s(r); +if t then +return e; +else +return(e:gsub("..",function(e) +return string.char(tonumber(e,16)); +end)); +end +end +_G.sha1={sha1=a}; +return _G.sha1; +end) +package.preload['lib.adhoc']=(function(...) +local n,r=require"util.stanza",require"util.uuid"; +local h="http://jabber.org/protocol/commands"; +local i={} +local s={}; +function _cmdtag(e,o,t,a) +local e=n.stanza("command",{xmlns=h,node=e.node,status=o}); +if t then e.attr.sessionid=t;end +if a then e.attr.action=a;end +return e; +end +function s.new(e,a,t,o) +return{name=e,node=a,handler=t,cmdtag=_cmdtag,permission=(o or"user")}; +end +function s.handle_cmd(o,s,a) +local e=a.tags[1].attr.sessionid or r.generate(); +local t={}; +t.to=a.attr.to; +t.from=a.attr.from; +t.action=a.tags[1].attr.action or"execute"; +t.form=a.tags[1]:child_with_ns("jabber:x:data"); +local t,h=o:handler(t,i[e]); +i[e]=h; +local a=n.reply(a); +if t.status=="completed"then +i[e]=nil; +cmdtag=o:cmdtag("completed",e); +elseif t.status=="canceled"then +i[e]=nil; +cmdtag=o:cmdtag("canceled",e); +elseif t.status=="error"then +i[e]=nil; +a=n.error_reply(a,t.error.type,t.error.condition,t.error.message); +s.send(a); +return true; +else +cmdtag=o:cmdtag("executing",e); +end +for t,e in pairs(t)do +if t=="info"then +cmdtag:tag("note",{type="info"}):text(e):up(); +elseif t=="warn"then +cmdtag:tag("note",{type="warn"}):text(e):up(); +elseif t=="error"then +cmdtag:tag("note",{type="error"}):text(e.message):up(); +elseif t=="actions"then +local t=n.stanza("actions"); +for a,e in ipairs(e)do +if(e=="prev")or(e=="next")or(e=="complete")then +t:tag(e):up(); +else +module:log("error",'Command "'..o.name.. +'" at node "'..o.node..'" provided an invalid action "'..e..'"'); +end +end +cmdtag:add_child(t); +elseif t=="form"then +cmdtag:add_child((e.layout or e):form(e.values)); +elseif t=="result"then +cmdtag:add_child((e.layout or e):form(e.values,"result")); +elseif t=="other"then +cmdtag:add_child(e); +end +end +a:add_child(cmdtag); +s.send(a); +return true; +end +return s; +end) +package.preload['util.rsm']=(function(...) +local h=require"util.stanza".stanza; +local a,i=tostring,tonumber; +local n=type; +local r=pairs; +local s='http://jabber.org/protocol/rsm'; +local o={}; +do +local e=o; +local function t(e) +return i((e:get_text())); +end +local function a(t) +return t:get_text(); +end +e.after=a; +e.before=function(e) +local e=e:get_text(); +return e==""or e; +end; +e.max=t; +e.index=t; +e.first=function(e) +return{index=i(e.attr.index);e:get_text()}; +end; +e.last=a; +e.count=t; +end +local d=setmetatable({ +first=function(t,e) +if n(e)=="table"then +t:tag("first",{index=e.index}):text(e[1]):up(); +else +t:tag("first"):text(a(e)):up(); +end +end; +before=function(t,e) +if e==true then +t:tag("before"):up(); +else +t:tag("before"):text(a(e)):up(); +end +end +},{ +__index=function(t,e) +return function(t,o) +t:tag(e):text(a(o)):up(); +end +end; +}); +local function n(e) +local i={}; +for t in e:childtags()do +local e=t.name; +local a=e and o[e]; +if a then +i[e]=a(t); +end +end +return i; +end +local function i(e) +local t=h("set",{xmlns=s}); +for e,a in r(e)do +if o[e]then +d[e](t,a); +end +end +return t; +end +local function t(e) +local e=e:get_child("set",s); +if e and#e.tags>0 then +return n(e); +end +end +return{parse=n,generate=i,get=t}; +end) +package.preload['util.stanza']=(function(...) +local t=table.insert; +local d=table.remove; +local w=table.concat; +local s=string.format; +local l=string.match; +local f=tostring; +local u=setmetatable; +local n=pairs; +local i=ipairs; +local o=type; +local v=string.gsub; +local y=string.sub; +local c=string.find; +local e=os; +local m=not e.getenv("WINDIR"); +local r,a; +if m then +local t,e=pcall(require,"util.termcolours"); +if t then +r,a=e.getstyle,e.getstring; +else +m=nil; +end +end +local p="urn:ietf:params:xml:ns:xmpp-stanzas"; +module"stanza" +stanza_mt={__type="stanza"}; +stanza_mt.__index=stanza_mt; +local e=stanza_mt; +function stanza(t,a) +local t={name=t,attr=a or{},tags={}}; +return u(t,e); +end +local h=stanza; +function e:query(e) +return self:tag("query",{xmlns=e}); +end +function e:body(t,e) +return self:tag("body",e):text(t); +end +function e:tag(a,e) +local a=h(a,e); +local e=self.last_add; +if not e then e={};self.last_add=e;end +(e[#e]or self):add_direct_child(a); +t(e,a); +return self; +end +function e:text(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:up() +local e=self.last_add; +if e then d(e);end +return self; +end +function e:reset() +self.last_add=nil; +return self; +end +function e:add_direct_child(e) +if o(e)=="table"then +t(self.tags,e); +end +t(self,e); +end +function e:add_child(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:get_child(a,t) +for o,e in i(self.tags)do +if(not a or e.name==a) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +return e; +end +end +end +function e:get_child_text(e,t) +local e=self:get_child(e,t); +if e then +return e:get_text(); +end +return nil; +end +function e:child_with_name(t) +for a,e in i(self.tags)do +if e.name==t then return e;end +end +end +function e:child_with_ns(t) +for a,e in i(self.tags)do +if e.attr.xmlns==t then return e;end +end +end +function e:children() +local e=0; +return function(t) +e=e+1 +return t[e]; +end,self,e; +end +function e:childtags(i,t) +local e=self.tags; +local a,o=1,#e; +return function() +for o=a,o do +local e=e[o]; +if(not i or e.name==i) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +a=o+1; +return e; +end +end +end; +end +function e:maptags(i) +local a,t=self.tags,1; +local n,o=#self,#a; +local e=1; +while t<=o and o>0 do +if self[e]==a[t]then +local i=i(self[e]); +if i==nil then +d(self,e); +d(a,t); +n=n-1; +o=o-1; +e=e-1; +t=t-1; +else +self[e]=i; +a[t]=i; +end +t=t+1; +end +e=e+1; +end +return self; +end +function e:find(a) +local e=1; +local s=#a+1; +repeat +local o,t,i; +local n=y(a,e,e); +if n=="@"then +return self.attr[y(a,e+1)]; +elseif n=="{"then +o,e=l(a,"^([^}]+)}()",e+1); +end +t,i,e=l(a,"^([^@/#]*)([/#]?)()",e); +t=t~=""and t or nil; +if e==s then +if i=="#"then +return self:get_child_text(t,o); +end +return self:get_child(t,o); +end +self=self:get_child(t,o); +until not self +end +local d +do +local t={["'"]="'",["\""]=""",["<"]="<",[">"]=">",["&"]="&"}; +function d(e)return(v(e,"['&<>\"]",t));end +_M.xml_escape=d; +end +local function y(a,e,h,o,r) +local i=0; +local s=a.name +t(e,"<"..s); +for a,n in n(a.attr)do +if c(a,"\1",1,true)then +local a,s=l(a,"^([^\1]*)\1?(.*)$"); +i=i+1; +t(e," xmlns:ns"..i.."='"..o(a).."' ".."ns"..i..":"..s.."='"..o(n).."'"); +elseif not(a=="xmlns"and n==r)then +t(e," "..a.."='"..o(n).."'"); +end +end +local i=#a; +if i==0 then +t(e,"/>"); +else +t(e,">"); +for i=1,i do +local i=a[i]; +if i.name then +h(i,e,h,o,a.attr.xmlns); +else +t(e,o(i)); +end +end +t(e,""); +end +end +function e.__tostring(t) +local e={}; +y(t,e,y,d,nil); +return w(e); +end +function e.top_tag(e) +local t=""; +if e.attr then +for e,a in n(e.attr)do if o(e)=="string"then t=t..s(" %s='%s'",e,d(f(a)));end end +end +return s("<%s%s>",e.name,t); +end +function e.get_text(e) +if#e.tags==0 then +return w(e); +end +end +function e.get_error(a) +local o,e,t; +local a=a:get_child("error"); +if not a then +return nil,nil,nil; +end +o=a.attr.type; +for o,a in i(a.tags)do +if a.attr.xmlns==p then +if not t and a.name=="text"then +t=a:get_text(); +elseif not e then +e=a.name; +end +if e and t then +break; +end +end +end +return o,e or"undefined-condition",t; +end +do +local e=0; +function new_id() +e=e+1; +return"lx"..e; +end +end +function preserialize(e) +local a={name=e.name,attr=e.attr}; +for i,e in i(e)do +if o(e)=="table"then +t(a,preserialize(e)); +else +t(a,e); +end +end +return a; +end +function deserialize(a) +if a then +local s=a.attr; +for e=1,#s do s[e]=nil;end +local h={}; +for e in n(s)do +if c(e,"|",1,true)and not c(e,"\1",1,true)then +local t,a=l(e,"^([^|]+)|(.+)$"); +h[t.."\1"..a]=s[e]; +s[e]=nil; +end +end +for t,e in n(h)do +s[t]=e; +end +u(a,e); +for t,e in i(a)do +if o(e)=="table"then +deserialize(e); +end +end +if not a.tags then +local n={}; +for i,e in i(a)do +if o(e)=="table"then +t(n,e); +end +end +a.tags=n; +end +end +return a; +end +local function l(a) +local o,i={},{}; +for t,e in n(a.attr)do o[t]=e;end +local o={name=a.name,attr=o,tags=i}; +for e=1,#a do +local e=a[e]; +if e.name then +e=l(e); +t(i,e); +end +t(o,e); +end +return u(o,e); +end +clone=l; +function message(e,t) +if not t then +return h("message",e); +else +return h("message",e):tag("body"):text(t):up(); +end +end +function iq(e) +if e and not e.id then e.id=new_id();end +return h("iq",e or{id=new_id()}); +end +function reply(e) +return h(e.name,e.attr and{to=e.attr.from,from=e.attr.to,id=e.attr.id,type=((e.name=="iq"and"result")or e.attr.type)}); +end +do +local a={xmlns=p}; +function error_reply(e,o,i,t) +local e=reply(e); +e.attr.type="error"; +e:tag("error",{type=o}) +:tag(i,a):up(); +if(t)then e:tag("text",a):text(t):up();end +return e; +end +end +function presence(e) +return h("presence",e); +end +if m then +local u=r("yellow"); +local h=r("red"); +local l=r("red"); +local t=r("magenta"); +local r=" "..a(u,"%s")..a(t,"=")..a(h,"'%s'"); +local h=a(t,"<")..a(l,"%s").."%s"..a(t,">"); +local l=h.."%s"..a(t,""); +function e.pretty_print(e) +local t=""; +for a,e in i(e)do +if o(e)=="string"then +t=t..d(e); +else +t=t..e:pretty_print(); +end +end +local a=""; +if e.attr then +for e,t in n(e.attr)do if o(e)=="string"then a=a..s(r,e,f(t));end end +end +return s(l,e.name,a,t,e.name); +end +function e.pretty_top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if o(t)=="string"then e=e..s(r,t,f(a));end end +end +return s(h,t.name,e); +end +else +e.pretty_print=e.__tostring; +e.pretty_top_tag=e.top_tag; +end +return _M; +end) +package.preload['util.timer']=(function(...) +local a=require"net.server"; +local d=math.min +local l=math.huge +local n=require"socket".gettime; +local r=table.insert; +local h=pairs; +local s=type; +local i={}; +local e={}; +module"timer" +local t; +if not a.event then +function t(o,i) +local n=n(); +o=o+n; +if o>=n then +r(e,{o,i}); +else +local e=i(n); +if e and s(e)=="number"then +return t(e,i); +end +end +end +a._addtimer(function() +local a=n(); +if#e>0 then +for a,t in h(e)do +r(i,t); +end +e={}; +end +local e=l; +for h,o in h(i)do +local o,n=o[1],o[2]; +if o<=a then +i[h]=nil; +local a=n(a); +if s(a)=="number"then +t(a,n); +e=d(e,a); +end +else +e=d(e,o-a); +end +end +return e; +end); +else +local e=a.event; +local a=a.event_base; +local o=(e.core and e.core.LEAVE)or-1; +function t(i,e) +local t; +t=a:addevent(nil,0,function() +local e=e(n()); +if e then +return 0,e; +elseif t then +return o; +end +end +,i); +end +end +add_task=t; +return _M; +end) +package.preload['util.termcolours']=(function(...) +local i,n=table.concat,table.insert; +local t,a=string.char,string.format; +local h=tonumber; +local s=ipairs; +local r=io.write; +local e; +if os.getenv("WINDIR")then +e=require"util.windows"; +end +local o=e and e.get_consolecolor and e.get_consolecolor(); +module"termcolours" +local d={ +reset=0;bright=1,dim=2,underscore=4,blink=5,reverse=7,hidden=8; +black=30;red=31;green=32;yellow=33;blue=34;magenta=35;cyan=36;white=37; +["black background"]=40;["red background"]=41;["green background"]=42;["yellow background"]=43;["blue background"]=44;["magenta background"]=45;["cyan background"]=46;["white background"]=47; +bold=1,dark=2,underline=4,underlined=4,normal=0; +} +local u={ +["0"]=o, +["1"]=7+8, +["1;33"]=2+4+8, +["1;31"]=4+8 +} +local l={ +[1]="font-weight: bold",[2]="opacity: 0.5",[4]="text-decoration: underline",[8]="visibility: hidden", +[30]="color:black",[31]="color:red",[32]="color:green",[33]="color:#FFD700", +[34]="color:blue",[35]="color: magenta",[36]="color:cyan",[37]="color: white", +[40]="background-color:black",[41]="background-color:red",[42]="background-color:green", +[43]="background-color:yellow",[44]="background-color:blue",[45]="background-color: magenta", +[46]="background-color:cyan",[47]="background-color: white"; +}; +local c=t(27).."[%sm%s"..t(27).."[0m"; +function getstring(t,e) +if t then +return a(c,t,e); +else +return e; +end +end +function getstyle(...) +local e,t={...},{}; +for a,e in s(e)do +e=d[e]; +if e then +n(t,e); +end +end +return i(t,";"); +end +local a="0"; +function setstyle(e) +e=e or"0"; +if e~=a then +r("\27["..e.."m"); +a=e; +end +end +if e then +function setstyle(t) +t=t or"0"; +if t~=a then +e.set_consolecolor(u[t]or o); +a=t; +end +end +if not o then +function setstyle(e)end +end +end +local function a(e) +if e=="0"then return"";end +local t={}; +for e in e:gmatch("[^;]+")do +n(t,l[h(e)]); +end +return""; +end +function tohtml(e) +return e:gsub("\027%[(.-)m",a); +end +return _M; +end) +package.preload['util.uuid']=(function(...) +local e=math.random; +local o=tostring; +local e=os.time; +local n=os.clock; +local i=require"util.hashes".sha1; +module"uuid" +local t=0; +local function a() +local e=e(); +if t>=e then e=t+1;end +t=e; +return e; +end +local function t(e) +return i(e..n()..o({}),true); +end +local e=t(a()); +local function o(a) +e=t(e..a); +end +local function t(t) +if#e"; +end +return e; +end +local k={ +LOC=e.LOC_tostring; +MX=function(e) +return a.format('%2i %s',e.pref,e.mx); +end; +SRV=function(e) +local e=e.srv; +return a.format('%5d %5d %5d %s',e.priority,e.weight,e.port,e.target); +end; +}; +local q={}; +function q.__tostring(e) +local t=(k[e.type]or w)(e); +return a.format('%2s %-5s %6i %-28s %s',e.class,e.type,e.ttl,e.name,t); +end +local k={}; +function k.__tostring(t) +local e={}; +for a,t in c(t)do +h(e,p(t)..'\n'); +end +return i.concat(e); +end +local w={}; +function w.__tostring(t) +local a=s.gettime(); +local e={}; +for i,t in o(t)do +for i,t in o(t)do +for o,t in o(t)do +y(t,a); +h(e,p(t)); +end +end +end +return i.concat(e); +end +function e:new() +local t={active={},cache={},unsorted={}}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +function t.random(...) +b.randomseed(b.floor(1e4*s.gettime())%2147483648); +t.random=b.random; +return t.random(...); +end +local function E(e) +e=e or{}; +e.id=e.id or t.random(0,65535); +e.rd=e.rd or 1; +e.tc=e.tc or 0; +e.aa=e.aa or 0; +e.opcode=e.opcode or 0; +e.qr=e.qr or 0; +e.rcode=e.rcode or 0; +e.z=e.z or 0; +e.ra=e.ra or 0; +e.qdcount=e.qdcount or 1; +e.ancount=e.ancount or 0; +e.nscount=e.nscount or 0; +e.arcount=e.arcount or 0; +local t=a.char( +m(e.id),e.id%256, +e.rd+2*e.tc+4*e.aa+8*e.opcode+128*e.qr, +e.rcode+16*e.z+128*e.ra, +m(e.qdcount),e.qdcount%256, +m(e.ancount),e.ancount%256, +m(e.nscount),e.nscount%256, +m(e.arcount),e.arcount%256 +); +return t,e.id; +end +local function m(t) +local e={}; +for t in a.gmatch(t,'[^.]+')do +h(e,a.char(a.len(t))); +h(e,t); +end +h(e,a.char(0)); +return i.concat(e); +end +local function b(o,a,e) +o=m(o); +a=t.typecode[a or'a']; +e=t.classcode[e or'in']; +return o..a..e; +end +function e:byte(e) +e=e or 1; +local o=self.offset; +local t=o+e-1; +if t>#self.packet then +x(a.format('out of bounds: %i>%i',t,#self.packet)); +end +self.offset=o+e; +return a.byte(self.packet,o,t); +end +function e:word() +local t,e=self:byte(2); +return 256*t+e; +end +function e:dword() +local o,t,a,e=self:byte(4); +return 16777216*o+65536*t+256*a+e; +end +function e:sub(e) +e=e or 1; +local t=a.sub(self.packet,self.offset,self.offset+e-1); +self.offset=self.offset+e; +return t; +end +function e:header(t) +local e=self:word(); +if not self.active[e]and not t then return nil;end +local e={id=e}; +local t,a=self:byte(2); +e.rd=t%2; +e.tc=t/2%2; +e.aa=t/4%2; +e.opcode=t/8%16; +e.qr=t/128; +e.rcode=a%16; +e.z=a/16%8; +e.ra=a/128; +e.qdcount=self:word(); +e.ancount=self:word(); +e.nscount=self:word(); +e.arcount=self:word(); +for a,t in o(e)do e[a]=t-t%1;end +return e; +end +function e:name() +local a,t=nil,0; +local e=self:byte(); +local o={}; +if e==0 then return"."end +while e>0 do +if e>=192 then +t=t+1; +if t>=20 then x('dns error: 20 pointers');end; +local e=((e-192)*256)+self:byte(); +a=a or self.offset; +self.offset=e+1; +else +h(o,self:sub(e)..'.'); +end +e=self:byte(); +end +self.offset=a or self.offset; +return i.concat(o); +end +function e:question() +local e={}; +e.name=self:name(); +e.type=t.type[self:word()]; +e.class=t.class[self:word()]; +return e; +end +function e:A(e) +local o,t,n,i=self:byte(4); +e.a=a.format('%i.%i.%i.%i',o,t,n,i); +end +function e:AAAA(a) +local e={}; +for t=1,a.rdlength,2 do +local a,t=self:byte(2); +i.insert(e,("%02x%02x"):format(a,t)); +end +e=i.concat(e,":"):gsub("%f[%x]0+(%x)","%1"); +local t={}; +for e in e:gmatch(":[0:]+:")do +i.insert(t,e) +end +if#t==0 then +a.aaaa=e; +return +elseif#t>1 then +i.sort(t,function(t,e)return#t>#e end); +end +a.aaaa=e:gsub(t[1],"::",1):gsub("^0::","::"):gsub("::0$","::"); +end +function e:CNAME(e) +e.cname=self:name(); +end +function e:MX(e) +e.pref=self:word(); +e.mx=self:name(); +end +function e:LOC_nibble_power() +local e=self:byte(); +return((e-(e%16))/16)*(10^(e%16)); +end +function e:LOC(e) +e.version=self:byte(); +if e.version==0 then +e.loc=e.loc or{}; +e.loc.size=self:LOC_nibble_power(); +e.loc.horiz_pre=self:LOC_nibble_power(); +e.loc.vert_pre=self:LOC_nibble_power(); +e.loc.latitude=self:dword(); +e.loc.longitude=self:dword(); +e.loc.altitude=self:dword(); +end +end +local function m(e,i,t) +e=e-2147483648; +if e<0 then i=t;e=-e;end +local n,t,o; +o=e%6e4; +e=(e-o)/6e4; +t=e%60; +n=(e-t)/60; +return a.format('%3d %2d %2.3f %s',n,t,o/1e3,i); +end +function e.LOC_tostring(e) +local t={}; +h(t,a.format( +'%s %s %.2fm %.2fm %.2fm %.2fm', +m(e.loc.latitude,'N','S'), +m(e.loc.longitude,'E','W'), +(e.loc.altitude-1e7)/100, +e.loc.size/100, +e.loc.horiz_pre/100, +e.loc.vert_pre/100 +)); +return i.concat(t); +end +function e:NS(e) +e.ns=self:name(); +end +function e:SOA(e) +end +function e:SRV(e) +e.srv={}; +e.srv.priority=self:word(); +e.srv.weight=self:word(); +e.srv.port=self:word(); +e.srv.target=self:name(); +end +function e:PTR(e) +e.ptr=self:name(); +end +function e:TXT(e) +e.txt=self:sub(self:byte()); +end +function e:rr() +local e={}; +r(e,q); +e.name=self:name(self); +e.type=t.type[self:word()]or e.type; +e.class=t.class[self:word()]or e.class; +e.ttl=65536*self:word()+self:word(); +e.rdlength=self:word(); +if e.ttl<=0 then +e.tod=self.time+30; +else +e.tod=self.time+e.ttl; +end +local a=self.offset; +local t=self[t.type[e.type]]; +if t then t(self,e);end +self.offset=a; +e.rdata=self:sub(e.rdlength); +return e; +end +function e:rrs(t) +local e={}; +for t=1,t do h(e,self:rr());end +return e; +end +function e:decode(t,o) +self.packet,self.offset=t,1; +local t=self:header(o); +if not t then return nil;end +local t={header=t}; +t.question={}; +local i=self.offset; +for e=1,t.header.qdcount do +h(t.question,self:question()); +end +t.question.raw=a.sub(self.packet,i,self.offset-1); +if not o then +if not self.active[t.header.id]or not self.active[t.header.id][t.question.raw]then +self.active[t.header.id]=nil; +return nil; +end +end +t.answer=self:rrs(t.header.ancount); +t.authority=self:rrs(t.header.nscount); +t.additional=self:rrs(t.header.arcount); +return t; +end +e.delays={1,3}; +function e:addnameserver(e) +self.server=self.server or{}; +h(self.server,e); +end +function e:setnameserver(e) +self.server={}; +self:addnameserver(e); +end +function e:adddefaultnameservers() +if _ then +if v and v.get_nameservers then +for t,e in c(v.get_nameservers())do +self:addnameserver(e); +end +end +if not self.server or#self.server==0 then +self:addnameserver("208.67.222.222"); +self:addnameserver("208.67.220.220"); +end +else +local e=z.open("/etc/resolv.conf"); +if e then +for e in e:lines()do +e=e:gsub("#.*$","") +:match('^%s*nameserver%s+(.*)%s*$'); +if e then +e:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]",function(e) +self:addnameserver(e) +end); +end +end +end +if not self.server or#self.server==0 then +self:addnameserver("127.0.0.1"); +end +end +end +function e:getsocket(a) +self.socket=self.socket or{}; +self.socketset=self.socketset or{}; +local e=self.socket[a]; +if e then return e;end +local o,t; +e,t=s.udp(); +if e and self.socket_wrapper then e,t=self.socket_wrapper(e,self);end +if not e then +return nil,t; +end +e:settimeout(0); +self.socket[a]=e; +self.socketset[e]=a; +o,t=e:setsockname('*',0); +if not o then return self:servfail(e,t);end +o,t=e:setpeername(self.server[a],53); +if not o then return self:servfail(e,t);end +return e; +end +function e:voidsocket(e) +if self.socket[e]then +self.socketset[self.socket[e]]=nil; +self.socket[e]=nil; +elseif self.socketset[e]then +self.socket[self.socketset[e]]=nil; +self.socketset[e]=nil; +end +e:close(); +end +function e:socket_wrapper_set(e) +self.socket_wrapper=e; +end +function e:closeall() +for t,e in c(self.socket)do +self.socket[t]=nil; +self.socketset[e]=nil; +e:close(); +end +end +function e:remember(e,t) +local i,o,a=g(e.name,e.type,e.class); +if t~='*'then +t=o; +local t=d(self.cache,a,'*',i); +if t then h(t,e);end +end +self.cache=self.cache or r({},w); +local a=d(self.cache,a,t,i)or +l(self.cache,a,t,i,r({},k)); +if not a[e[o:lower()]]then +a[e[o:lower()]]=true; +h(a,e); +end +if t=='MX'then self.unsorted[a]=true;end +end +local function m(e,t) +return(e.pref==t.pref)and(e.mx#self.server then +e.server=1; +end +e.retries=(e.retries or 0)+1; +if e.retries>=#self.server then +a[o]=nil; +else +t,i=self:getsocket(e.server); +if t then t:send(e.packet);end +end +end +end +if n(a)==nil then +self.active[s]=nil; +end +end +if h==self.best_server then +self.best_server=self.best_server+1; +if self.best_server>#self.server then +self.best_server=1; +end +end +return t,i; +end +function e:settimeout(e) +self.timeout=e; +end +function e:receive(t) +self.time=s.gettime(); +t=t or self.socket; +local e; +for a,t in o(t)do +if self.socketset[t]then +local t=t:receive(); +if t then +e=self:decode(t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type) +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not n(t)then self.active[e.header.id]=nil;end +if not n(self.active)then self:closeall();end +local e=e.question[1]; +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,e.class,e.type,e.name,nil); +end +end +end +end +end +return e; +end +function e:feed(a,e,t) +self.time=s.gettime(); +local e=self:decode(e,t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type); +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not n(t)then self.active[e.header.id]=nil;end +if not n(self.active)then self:closeall();end +local e=e.question[1]; +if e then +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,e.class,e.type,e.name,nil); +end +end +end +return e; +end +function e:cancel(i,t,e) +local a=d(self.wanted,i,t,e); +if a then +for e in o(a)do +if u.status(e)=="suspended"then u.resume(e);end +end +l(self.wanted,i,t,e,nil); +end +end +function e:pulse() +while self:receive()do end +if not n(self.active)then return nil;end +self.time=s.gettime(); +for i,t in o(self.active)do +for a,e in o(t)do +if self.time>=e.retry then +e.server=e.server+1; +if e.server>#self.server then +e.server=1; +e.delay=e.delay+1; +end +if e.delay>#self.delays then +t[a]=nil; +if not n(t)then self.active[i]=nil;end +if not n(self.active)then return nil;end +else +local t=self.socket[e.server]; +if t then t:send(e.packet);end +e.retry=self.time+self.delays[e.delay]; +end +end +end +end +if n(self.active)then return true;end +return nil; +end +function e:lookup(t,o,a) +self:query(t,o,a) +while self:pulse()do +local e={} +for a,t in c(self.socket)do +e[a]=t +end +s.select(e,nil,4) +end +return self:peek(t,o,a); +end +function e:lookupex(o,e,t,a) +return self:peek(e,t,a)or self:query(e,t,a); +end +function e:tohostname(e) +return t.lookup(e:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)","%4.%3.%2.%1.in-addr.arpa."),"PTR"); +end +local i={ +qr={[0]='query','response'}, +opcode={[0]='query','inverse query','server status request'}, +aa={[0]='non-authoritative','authoritative'}, +tc={[0]='complete','truncated'}, +rd={[0]='recursion not desired','recursion desired'}, +ra={[0]='recursion not available','recursion available'}, +z={[0]='(reserved)'}, +rcode={[0]='no error','format error','server failure','name error','not implemented'}, +type=t.type, +class=t.class +}; +local function n(t,e) +return(i[e]and i[e][t[e]])or''; +end +function e.print(e) +for o,t in o{'id','qr','opcode','aa','tc','rd','ra','z', +'rcode','qdcount','ancount','nscount','arcount'}do +f(a.format('%-30s','header.'..t),e.header[t],n(e.header,t)); +end +for t,e in c(e.question)do +f(a.format('question[%i].name ',t),e.name); +f(a.format('question[%i].type ',t),e.type); +f(a.format('question[%i].class ',t),e.class); +end +local h={name=1,type=1,class=1,ttl=1,rdlength=1,rdata=1}; +local t; +for s,i in o({'answer','authority','additional'})do +for s,e in o(e[i])do +for h,o in o({'name','type','class','ttl','rdlength'})do +t=a.format('%s[%i].%s',i,s,o); +f(a.format('%-30s',t),e[o],n(e,o)); +end +for e,o in o(e)do +if not h[e]then +t=a.format('%s[%i].%s',i,s,e); +f(a.format('%-30s %s',p(t),p(o))); +end +end +end +end +end +function t.resolver() +local t={active={},cache={},unsorted={},wanted={},best_server=1}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +local e=t.resolver(); +t._resolver=e; +function t.lookup(...) +return e:lookup(...); +end +function t.tohostname(...) +return e:tohostname(...); +end +function t.purge(...) +return e:purge(...); +end +function t.peek(...) +return e:peek(...); +end +function t.query(...) +return e:query(...); +end +function t.feed(...) +return e:feed(...); +end +function t.cancel(...) +return e:cancel(...); +end +function t.settimeout(...) +return e:settimeout(...); +end +function t.cache() +return e.cache; +end +function t.socket_wrapper_set(...) +return e:socket_wrapper_set(...); +end +return t; +end) +package.preload['net.adns']=(function(...) +local c=require"net.server"; +local a=require"net.dns"; +local e=require"util.logger".init("adns"); +local t,t=table.insert,table.remove; +local o,r,l=coroutine,tostring,pcall; +local function u(a,a,e,t)return(t-e)+1;end +module"adns" +function lookup(d,t,h,s) +return o.wrap(function(i) +if i then +e("debug","Records for %s already cached, using those...",t); +d(i); +return; +end +e("debug","Records for %s not in cache, sending query (%s)...",t,r(o.running())); +local i,n=a.query(t,h,s); +if i then +o.yield({s or"IN",h or"A",t,o.running()}); +e("debug","Reply for %s (%s)",t,r(o.running())); +end +if i then +i,n=l(d,a.peek(t,h,s)); +else +e("error","Error sending DNS query: %s",n); +i,n=l(d,nil,n); +end +if not i then +e("error","Error in DNS response handler: %s",r(n)); +end +end)(a.peek(t,h,s)); +end +function cancel(t,o,i) +e("warn","Cancelling DNS lookup for %s",r(t[3])); +a.cancel(t[1],t[2],t[3],t[4],o); +end +function new_async_socket(o,i) +local n=""; +local s={}; +local t={}; +local h; +function s.onincoming(o,e) +if e then +a.feed(t,e); +end +end +function s.ondisconnect(a,o) +if o then +e("warn","DNS socket for %s disconnected: %s",n,o); +local t=i.server; +if i.socketset[a]==i.best_server and i.best_server==#t then +e("error","Exhausted all %d configured DNS servers, next lookup will try %s again",#t,t[1]); +end +i:servfail(a); +end +end +t,h=c.wrapclient(o,"dns",53,s); +if not t then +return nil,h; +end +t.settimeout=function()end +t.setsockname=function(e,...)return o:setsockname(...);end +t.setpeername=function(i,...)n=(...);local a,e=o:setpeername(...);i:set_send(u);return a,e;end +t.connect=function(e,...)return o:connect(...)end +t.send=function(a,t) +e("debug","Sending DNS query to %s",n); +return o:send(t); +end +return t; +end +a.socket_wrapper_set(new_async_socket); +return _M; +end) +package.preload['net.server']=(function(...) +local l=function(e) +return _G[e] +end +local M,e=require("util.logger").init("socket"),table.concat; +local i=function(...)return M("debug",e{...});end +local H=function(...)return M("warn",e{...});end +local ce=1 +local F=l"type" +local W=l"pairs" +local de=l"ipairs" +local y=l"tonumber" +local h=l"tostring" +local t=l"os" +local o=l"table" +local a=l"string" +local e=l"coroutine" +local Z=t.difftime +local X=math.min +local re=math.huge +local fe=o.concat +local me=a.sub +local we=e.wrap +local ye=e.yield +local x=l"ssl" +local b=l"socket"or require"socket" +local J=b.gettime +local ve=(x and x.wrap) +local he=b.bind +local pe=b.sleep +local be=b.select +local G +local B +local ae +local K +local te +local c +local ee +local oe +local ne +local ie +local se +local Q +local r +local le +local D +local ue +local p +local s +local R +local d +local n +local S +local g +local f +local m +local a +local o +local v +local U +local C +local T +local E +local k +local V +local u +local _ +local z +local A +local I +local O +local L +local j +local q +local N +p={} +s={} +d={} +R={} +n={} +g={} +f={} +S={} +a=0 +o=0 +v=0 +U=0 +C=0 +T=1 +E=0 +k=128 +_=51e3*1024 +z=25e3*1024 +A=12e5 +I=6e4 +O=6*60*60 +local e=package.config:sub(1,1)=="\\" +q=(e and math.huge)or b._SETSIZE or 1024 +j=b._SETSIZE or 1024 +N=30 +ie=function(w,t,f,l,v,u) +if t:getfd()>=q then +H("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +return nil,"fd-too-large" +end +local m=0 +local y,e=w.onconnect,w.ondisconnect +local b=t.accept +local e={} +e.shutdown=function()end +e.ssl=function() +return u~=nil +end +e.sslctx=function() +return u +end +e.remove=function() +m=m-1 +if e then +e.resume() +end +end +e.close=function() +t:close() +o=r(d,t,o) +a=r(s,t,a) +p[f..":"..l]=nil; +n[t]=nil +e=nil +t=nil +i"server.lua: closed server handler and removed sockets from list" +end +e.pause=function(o) +if not e.paused then +a=r(s,t,a) +if o then +n[t]=nil +t:close() +t=nil; +end +e.paused=true; +end +end +e.resume=function() +if e.paused then +if not t then +t=he(f,l,k); +t:settimeout(0) +end +a=c(s,t,a) +n[t]=e +e.paused=false; +end +end +e.ip=function() +return f +end +e.serverport=function() +return l +end +e.socket=function() +return t +end +e.readbuffer=function() +if a>=j or o>=j then +e.pause() +i("server.lua: refused new client connection: server full") +return false +end +local t,o=b(t) +if t then +local o,a=t:getpeername() +local t,n,e=D(e,w,t,o,l,a,v,u) +if e then +return false +end +m=m+1 +i("server.lua: accepted new client connection from ",h(o),":",h(a)," to ",h(l)) +if y and not u then +return y(t); +end +return; +elseif o then +i("server.lua: error with new client connection: ",h(o)) +return false +end +end +return e +end +D=function(E,y,t,I,Q,O,T,j) +if t:getfd()>=q then +H("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +if E then +E.pause() +end +return nil,nil,"fd-too-large" +end +t:settimeout(0) +local p +local A +local k +local P +local W=y.onincoming +local Y=y.onstatus +local b=y.ondisconnect +local F=y.ondrain +local M=y.ondetach +local v={} +local l=0 +local V +local B +local L +local w=0 +local q=false +local H=false +local D,R=0,0 +local _=_ +local z=z +local e=v +e.dispatch=function() +return W +end +e.disconnect=function() +return b +end +e.setlistener=function(a,t) +if M then +M(a) +end +W=t.onincoming +b=t.ondisconnect +Y=t.onstatus +F=t.ondrain +M=t.ondetach +end +e.getstats=function() +return R,D +end +e.ssl=function() +return P +end +e.sslctx=function() +return j +end +e.send=function(n,o,i,a) +return p(t,o,i,a) +end +e.receive=function(o,a) +return A(t,o,a) +end +e.shutdown=function(a) +return k(t,a) +end +e.setoption=function(i,a,o) +if t.setoption then +return t:setoption(a,o); +end +return false,"setoption not implemented"; +end +e.force_close=function(t,a) +if l~=0 then +i("server.lua: discarding unwritten data for ",h(I),":",h(O)) +l=0; +end +return t:close(a); +end +e.close=function(u,h) +if not e then return true;end +a=r(s,t,a) +g[e]=nil +if l~=0 then +e.sendbuffer() +if l~=0 then +if e then +e.write=nil +end +V=true +return false +end +end +if t then +m=k and k(t) +t:close() +o=r(d,t,o) +n[t]=nil +t=nil +else +i"server.lua: socket already closed" +end +if e then +f[e]=nil +S[e]=nil +local t=e; +e=nil +if b then +b(t,h or false); +b=nil +end +end +if E then +E.remove() +end +i"server.lua: closed client handler and removed socket from list" +return true +end +e.ip=function() +return I +end +e.serverport=function() +return Q +end +e.clientport=function() +return O +end +e.port=e.clientport +local b=function(i,a) +w=w+#a +if w>_ then +S[e]="send buffer exceeded" +e.write=K +return false +elseif t and not d[t]then +o=c(d,t,o) +end +l=l+1 +v[l]=a +if e then +f[e]=f[e]or u +end +return true +end +e.write=b +e.bufferqueue=function(t) +return v +end +e.socket=function(a) +return t +end +e.set_mode=function(a,t) +T=t or T +return T +end +e.set_send=function(a,t) +p=t or p +return p +end +e.bufferlen=function(o,a,t) +_=t or _ +z=a or z +return w,z,_ +end +e.lock_read=function(i,o) +if o==true then +local o=a +a=r(s,t,a) +g[e]=nil +if a~=o then +q=true +end +elseif o==false then +if q then +q=false +a=c(s,t,a) +g[e]=u +end +end +return q +end +e.pause=function(t) +return t:lock_read(true); +end +e.resume=function(t) +return t:lock_read(false); +end +e.lock=function(i,a) +e.lock_read(a) +if a==true then +e.write=K +local a=o +o=r(d,t,o) +f[e]=nil +if o~=a then +H=true +end +elseif a==false then +e.write=b +if H then +H=false +b("") +end +end +return q,H +end +local b=function() +local a,t,o=A(t,T) +if not t or(t=="wantread"or t=="timeout")then +local o=a or o or"" +local a=#o +if a>z then +e:close("receive buffer exceeded") +return false +end +local a=a*ce +R=R+a +C=C+a +g[e]=u +return W(e,o,t) +else +i("server.lua: client ",h(I),":",h(O)," read error: ",h(t)) +B=true +m=e and e:force_close(t) +return false +end +end +local w=function() +local c,a,n,s,y; +if t then +s=fe(v,"",1,l) +c,a,n=p(t,s,1,w) +y=(c or n or 0)*ce +D=D+y +U=U+y +for e=l,1,-1 do +v[e]=nil +end +else +c,a,y=false,"unexpected close",0; +end +if c then +l=0 +w=0 +o=r(d,t,o) +f[e]=nil +if F then +F(e) +end +m=L and e:starttls(nil) +m=V and e:force_close() +return true +elseif n and(a=="timeout"or a=="wantwrite")then +s=me(s,n+1,w) +v[1]=s +l=1 +w=w-n +f[e]=u +return true +else +i("server.lua: client ",h(I),":",h(O)," write error: ",h(a)) +B=true +m=e and e:force_close(a) +return false +end +end +local u; +function e.set_sslctx(p,t) +j=t; +local l,f +u=we(function(n) +local t +for h=1,N do +o=(f and r(d,n,o))or o +a=(l and r(s,n,a))or a +l,f=nil,nil +m,t=n:dohandshake() +if not t then +i("server.lua: ssl handshake done") +e.readbuffer=b +e.sendbuffer=w +m=Y and Y(e,"ssl-handshake-complete") +if p.autostart_ssl and y.onconnect then +y.onconnect(p); +end +a=c(s,n,a) +return true +else +if t=="wantwrite"then +o=c(d,n,o) +f=true +elseif t=="wantread"then +a=c(s,n,a) +l=true +else +break; +end +t=nil; +ye() +end +end +i("server.lua: ssl handshake error: ",h(t or"handshake too long")) +m=e and e:force_close("ssl handshake failed") +return false,t +end +) +end +if x then +e.starttls=function(f,m) +if m then +e:set_sslctx(m); +end +if l>0 then +i"server.lua: we need to do tls, but delaying until send buffer empty" +L=true +return +end +i("server.lua: attempting to start tls on "..h(t)) +local m,l=t +t,l=ve(t,j) +if not t then +i("server.lua: error while starting tls on client: ",h(l or"unknown error")) +return nil,l +end +t:settimeout(0) +p=t.send +A=t.receive +k=G +n[t]=e +a=c(s,t,a) +a=r(s,m,a) +o=r(d,m,o) +n[m]=nil +e.starttls=nil +L=nil +P=true +e.readbuffer=u +e.sendbuffer=u +return u(t) +end +end +e.readbuffer=b +e.sendbuffer=w +p=t.send +A=t.receive +k=(P and G)or t.shutdown +n[t]=e +a=c(s,t,a) +if j and x then +i"server.lua: auto-starting ssl negotiation..." +e.autostart_ssl=true; +local t,e=e:starttls(j); +if t==false then +return nil,nil,e +end +end +return e,t +end +G=function() +end +K=function() +return false +end +c=function(t,a,e) +if not t[a]then +e=e+1 +t[e]=a +t[a]=e +end +return e; +end +r=function(e,a,t) +local o=e[a] +if o then +e[a]=nil +local i=e[t] +e[t]=nil +if i~=a then +e[i]=o +e[o]=i +end +return t-1 +end +return t +end +Q=function(e) +o=r(d,e,o) +a=r(s,e,a) +n[e]=nil +e:close() +end +local function w(e,a,o) +local t; +local i=a.sendbuffer; +function a.sendbuffer() +i(); +if t and a.bufferlen()=o then +t=true; +e:lock_read(true); +end +end +e:set_mode("*a"); +end +ee=function(t,e,d,l,r) +local o +if F(d)~="table"then +o="invalid listener table" +end +if F(e)~="number"or not(e>=0 and e<=65535)then +o="invalid port" +elseif p[t..":"..e]then +o="listeners on '["..t.."]:"..e.."' already exist" +elseif r and not x then +o="luasec not found" +end +if o then +H("server.lua, [",t,"]:",e,": ",o) +return nil,o +end +t=t or"*" +local o,h=he(t,e,k) +if h then +H("server.lua, [",t,"]:",e,": ",h) +return nil,h +end +local h,d=ie(d,o,t,e,l,r) +if not h then +o:close() +return nil,d +end +o:settimeout(0) +a=c(s,o,a) +p[t..":"..e]=h +n[o]=h +i("server.lua: new "..(r and"ssl "or"").."server listener on '[",t,"]:",e,"'") +return h +end +ne=function(t,e) +return p[t..":"..e]; +end +le=function(t,e) +local a=p[t..":"..e] +if not a then +return nil,"no server found on '["..t.."]:"..h(e).."'" +end +a:close() +p[t..":"..e]=nil +return true +end +te=function() +for e,t in W(n)do +t:close() +n[e]=nil +end +a=0 +o=0 +v=0 +p={} +s={} +d={} +R={} +n={} +end +se=function() +return{ +select_timeout=T; +select_sleep_time=E; +tcp_backlog=k; +max_send_buffer_size=_; +max_receive_buffer_size=z; +select_idle_check_interval=A; +send_timeout=I; +read_timeout=O; +max_connections=j; +max_ssl_handshake_roundtrips=N; +highest_allowed_fd=q; +} +end +ue=function(e) +if F(e)~="table"then +return nil,"invalid settings table" +end +T=y(e.select_timeout)or T +E=y(e.select_sleep_time)or E +_=y(e.max_send_buffer_size)or _ +z=y(e.max_receive_buffer_size)or z +A=y(e.select_idle_check_interval)or A +k=y(e.tcp_backlog)or k +I=y(e.send_timeout)or I +O=y(e.read_timeout)or O +j=e.max_connections or j +N=e.max_ssl_handshake_roundtrips or N +q=e.highest_allowed_fd or q +return true +end +oe=function(e) +if F(e)~="function"then +return nil,"invalid listener function" +end +v=v+1 +R[v]=e +return true +end +ae=function() +return C,U,a,o,v +end +local t; +local function h(e) +t=not not e; +end +B=function(a) +if t then return"quitting";end +if a then t="once";end +local e=re; +repeat +local o,a,s=be(s,d,X(T,e)) +for t,e in de(a)do +local t=n[e] +if t then +t.sendbuffer() +else +Q(e) +i"server.lua: found no handler and closed socket (writelist)" +end +end +for e,t in de(o)do +local e=n[t] +if e then +e.readbuffer() +else +Q(t) +i"server.lua: found no handler and closed socket (readlist)" +end +end +for e,t in W(S)do +e.disconnect()(e,t) +e:force_close() +S[e]=nil; +end +u=J() +local a=Z(u-V) +if a>A then +V=u +for e,t in W(f)do +if Z(u-t)>I then +e.disconnect()(e,"send timeout") +e:force_close() +end +end +for e,t in W(g)do +if Z(u-t)>O then +e.disconnect()(e,"read timeout") +e:close() +end +end +end +if u-L>=X(e,1)then +e=re; +for t=1,v do +local t=R[t](u) +if t then e=X(e,t);end +end +L=u +else +e=e-(u-L); +end +pe(E) +until t; +if a and t=="once"then t=nil;return;end +return"quitting" +end +local function u() +return B(true); +end +local function r() +return"select"; +end +local i=function(e,t,s,a,h,i) +local e,t,s=D(nil,a,e,t,s,"clientport",h,i) +if not e then return nil,s end +n[t]=e +if not i then +o=c(d,t,o) +if a.onconnect then +local t=e.sendbuffer; +e.sendbuffer=function() +e.sendbuffer=t; +a.onconnect(e); +return t(); +end +end +end +return e,t +end +local o=function(a,o,n,h,s) +local e,t=b.tcp() +if t then +return nil,t +end +e:settimeout(0) +m,t=e:connect(a,o) +if t then +local e=i(e,a,o,n) +else +D(nil,n,e,a,o,"clientport",h,s) +end +end +l"setmetatable"(n,{__mode="k"}) +l"setmetatable"(g,{__mode="k"}) +l"setmetatable"(f,{__mode="k"}) +L=J() +V=J() +local function a(e) +local t=M; +if e then +M=e; +end +return t; +end +return{ +_addtimer=oe, +addclient=o, +wrapclient=i, +loop=B, +link=w, +step=u, +stats=ae, +closeall=te, +addserver=ee, +getserver=ne, +setlogger=a, +getsettings=se, +setquitting=h, +removeserver=le, +get_backend=r, +changesettings=ue, +} +end) +package.preload['util.xmppstream']=(function(...) +local e=require"lxp"; +local t=require"util.stanza"; +local b=t.stanza_mt; +local f=error; +local t=tostring; +local l=table.insert; +local y=table.concat; +local x=table.remove; +local v=setmetatable; +local j=pcall(e.new,{StartDoctypeDecl=false}); +local k=pcall(e.new,{XmlDecl=false}); +local a=not not e.new({}).getcurrentbytecount; +local q=1024*1024*10; +module"xmppstream" +local p=e.new; +local z={ +["http://www.w3.org/XML/1998/namespace\1lang"]="xml:lang"; +["http://www.w3.org/XML/1998/namespace\1space"]="xml:space"; +["http://www.w3.org/XML/1998/namespace\1base"]="xml:base"; +["http://www.w3.org/XML/1998/namespace\1id"]="xml:id"; +}; +local h="http://etherx.jabber.org/streams"; +local d="\1"; +local g="^([^"..d.."]*)"..d.."?(.*)$"; +_M.ns_separator=d; +_M.ns_pattern=g; +local function o()end +function new_sax_handlers(n,e,s) +local i={}; +local p=e.streamopened; +local w=e.streamclosed; +local u=e.error or function(o,a,e)f("XML stream error: "..t(a)..(e and": "..t(e)or""),2);end; +local q=e.handlestanza; +s=s or o; +local t=e.stream_ns or h; +local m=e.stream_tag or"stream"; +if t~=""then +m=t..d..m; +end +local _=t..d..(e.error_tag or"error"); +local E=e.default_ns; +local d={}; +local h,e={}; +local t=0; +local r=0; +function i:StartElement(c,o) +if e and#h>0 then +l(e,y(h)); +h={}; +end +local h,i=c:match(g); +if i==""then +h,i="",h; +end +if h~=E or r>0 then +o.xmlns=h; +r=r+1; +end +for t=1,#o do +local e=o[t]; +o[t]=nil; +local t=z[e]; +if t then +o[t]=o[e]; +o[e]=nil; +end +end +if not e then +if a then +t=self:getcurrentbytecount(); +end +if n.notopen then +if c==m then +r=0; +if p then +if a then +s(t); +t=0; +end +p(n,o); +end +else +u(n,"no-stream",c); +end +return; +end +if h=="jabber:client"and i~="iq"and i~="presence"and i~="message"then +u(n,"invalid-top-level-element"); +end +e=v({name=i,attr=o,tags={}},b); +else +if a then +t=t+self:getcurrentbytecount(); +end +l(d,e); +local t=e; +e=v({name=i,attr=o,tags={}},b); +l(t,e); +l(t.tags,e); +end +end +if k then +function i:XmlDecl(e,e,e) +if a then +s(self:getcurrentbytecount()); +end +end +end +function i:StartCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +s(self:getcurrentbytecount()); +end +end +end +function i:EndCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +s(self:getcurrentbytecount()); +end +end +end +function i:CharacterData(o) +if e then +if a then +t=t+self:getcurrentbytecount(); +end +l(h,o); +elseif a then +s(self:getcurrentbytecount()); +end +end +function i:EndElement(o) +if a then +t=t+self:getcurrentbytecount() +end +if r>0 then +r=r-1; +end +if e then +if#h>0 then +l(e,y(h)); +h={}; +end +if#d==0 then +if a then +s(t); +end +t=0; +if o~=_ then +q(n,e); +else +u(n,"stream-error",e); +end +e=nil; +else +e=x(d); +end +else +if w then +w(n); +end +end +end +local function a(e) +u(n,"parse-error","restricted-xml","Restricted XML, see RFC 6120 section 11.1."); +if not e.stop or not e:stop()then +f("Failed to abort parsing"); +end +end +if j then +i.StartDoctypeDecl=a; +end +i.Comment=a; +i.ProcessingInstruction=a; +local function a() +e,h,t=nil,{},0; +d={}; +end +local function e(t,e) +n=e; +end +return i,{reset=a,set_session=e}; +end +function new(n,i,o) +local e=0; +local t; +if a then +function t(t) +e=e-t; +end +o=o or q; +elseif o then +f("Stanza size limits are not supported on this version of LuaExpat") +end +local i,n=new_sax_handlers(n,i,t); +local t=p(i,d,false); +local s=t.parse; +return{ +reset=function() +t=p(i,d,false); +s=t.parse; +e=0; +n.reset(); +end, +feed=function(n,i) +if a then +e=e+#i; +end +local i,t=s(t,i); +if a and e>o then +return nil,"stanza-too-large"; +end +return i,t; +end, +set_session=n.set_session; +}; +end +return _M; +end) +package.preload['util.jid']=(function(...) +local t,i=string.match,string.sub; +local d=require"util.encodings".stringprep.nodeprep; +local r=require"util.encodings".stringprep.nameprep; +local h=require"util.encodings".stringprep.resourceprep; +local n={ +[" "]="\\20";['"']="\\22"; +["&"]="\\26";["'"]="\\27"; +["/"]="\\2f";[":"]="\\3a"; +["<"]="\\3c";[">"]="\\3e"; +["@"]="\\40";["\\"]="\\5c"; +}; +local s={}; +for t,e in pairs(n)do s[e]=t;end +module"jid" +local function a(e) +if not e then return;end +local i,a=t(e,"^([^@/]+)@()"); +local a,o=t(e,"^([^@/]+)()",a) +if i and not a then return nil,nil,nil;end +local t=t(e,"^/(.+)$",o); +if(not a)or((not t)and#e>=o)then return nil,nil,nil;end +return i,a,t; +end +split=a; +function bare(e) +local t,e=a(e); +if t and e then +return t.."@"..e; +end +return e; +end +local function o(e) +local a,e,t=a(e); +if e then +if i(e,-1,-1)=="."then +e=i(e,1,-2); +end +e=r(e); +if not e then return;end +if a then +a=d(a); +if not a then return;end +end +if t then +t=h(t); +if not t then return;end +end +return a,e,t; +end +end +prepped_split=o; +function prep(e) +local t,e,a=o(e); +if e then +if t then +e=t.."@"..e; +end +if a then +e=e.."/"..a; +end +end +return e; +end +function join(t,e,a) +if t and e and a then +return t.."@"..e.."/"..a; +elseif t and e then +return t.."@"..e; +elseif e and a then +return e.."/"..a; +elseif e then +return e; +end +return nil; +end +function compare(e,t) +local o,i,n=a(e); +local e,t,a=a(t); +if((e~=nil and e==o)or e==nil)and +((t~=nil and t==i)or t==nil)and +((a~=nil and a==n)or a==nil)then +return true +end +return false +end +function escape(e)return e and(e:gsub(".",n));end +function unescape(e)return e and(e:gsub("\\%x%x",s));end +return _M; +end) +package.preload['util.events']=(function(...) +local i=pairs; +local s=table.insert; +local o=table.sort; +local d=setmetatable; +local n=next; +module"events" +function new() +local t={}; +local e={}; +local function r(h,a) +local e=e[a]; +if not e or n(e)==nil then return;end +local t={}; +for e in i(e)do +s(t,e); +end +o(t,function(t,a)return e[t]>e[a];end); +h[a]=t; +return t; +end; +d(t,{__index=r}); +local function s(o,i,n) +local a=e[o]; +if a then +a[i]=n or 0; +else +a={[i]=n or 0}; +e[o]=a; +end +t[o]=nil; +end; +local function h(a,i) +local o=e[a]; +if o then +o[i]=nil; +t[a]=nil; +if n(o)==nil then +e[a]=nil; +end +end +end; +local function n(e) +for e,t in i(e)do +s(e,t); +end +end; +local function o(e) +for t,e in i(e)do +h(t,e); +end +end; +local function a(e,...) +local e=t[e]; +if e then +for t=1,#e do +local e=e[t](...); +if e~=nil then return e;end +end +end +end; +return{ +add_handler=s; +remove_handler=h; +add_handlers=n; +remove_handlers=o; +fire_event=a; +_handlers=t; +_event_map=e; +}; +end +return _M; +end) +package.preload['util.dataforms']=(function(...) +local a=setmetatable; +local e,i=pairs,ipairs; +local r,h,l=tostring,type,next; +local n=table.concat; +local c=require"util.stanza"; +local d=require"util.jid".prep; +module"dataforms" +local u='jabber:x:data'; +local s={}; +local e={__index=s}; +function new(t) +return a(t,e); +end +function from_stanza(e) +local o={ +title=e:get_child_text("title"); +instructions=e:get_child_text("instructions"); +}; +for e in e:childtags("field")do +local a={ +name=e.attr.var; +label=e.attr.label; +type=e.attr.type; +required=e:get_child("required")and true or nil; +value=e:get_child_text("value"); +}; +o[#o+1]=a; +if a.type then +local t={}; +if a.type:match"list%-"then +for e in e:childtags("option")do +t[#t+1]={label=e.attr.label,value=e:get_child_text("value")}; +end +for e in e:childtags("value")do +t[#t+1]={label=e.attr.label,value=e:get_text(),default=true}; +end +elseif a.type:match"%-multi"then +for e in e:childtags("value")do +t[#t+1]=e.attr.label and{label=e.attr.label,value=e:get_text()}or e:get_text(); +end +if a.type=="text-multi"then +a.value=n(t,"\n"); +else +a.value=t; +end +end +end +end +return new(o); +end +function s.form(t,a,e) +local e=c.stanza("x",{xmlns=u,type=e or"form"}); +if t.title then +e:tag("title"):text(t.title):up(); +end +if t.instructions then +e:tag("instructions"):text(t.instructions):up(); +end +for t,o in i(t)do +local t=o.type or"text-single"; +e:tag("field",{type=t,var=o.name,label=o.label}); +local a=(a and a[o.name])or o.value; +if a then +if t=="hidden"then +if h(a)=="table"then +e:tag("value") +:add_child(a) +:up(); +else +e:tag("value"):text(r(a)):up(); +end +elseif t=="boolean"then +e:tag("value"):text((a and"1")or"0"):up(); +elseif t=="fixed"then +elseif t=="jid-multi"then +for a,t in i(a)do +e:tag("value"):text(t):up(); +end +elseif t=="jid-single"then +e:tag("value"):text(a):up(); +elseif t=="text-single"or t=="text-private"then +e:tag("value"):text(a):up(); +elseif t=="text-multi"then +for t in a:gmatch("([^\r\n]+)\r?\n*")do +e:tag("value"):text(t):up(); +end +elseif t=="list-single"then +local o=false; +for a,t in i(a)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default and(not o)then +e:tag("value"):text(t.value):up(); +o=true; +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +elseif t=="list-multi"then +for a,t in i(a)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default then +e:tag("value"):text(t.value):up(); +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +end +end +if o.required then +e:tag("required"):up(); +end +e:up(); +end +return e; +end +local e={}; +function s.data(t,n) +local o={}; +local a={}; +for i,t in i(t)do +local i; +for e in n:childtags()do +if t.name==e.attr.var then +i=e; +break; +end +end +if not i then +if t.required then +a[t.name]="Required value missing"; +end +else +local e=e[t.type]; +if e then +o[t.name],a[t.name]=e(i,t.required); +end +end +end +if l(a)then +return o,a; +end +return o; +end +e["text-single"]= +function(t,a) +local t=t:get_child_text("value"); +if t and#t>0 then +return t +elseif a then +return nil,"Required value missing"; +end +end +e["text-private"]= +e["text-single"]; +e["jid-single"]= +function(t,o) +local a=t:get_child_text("value") +local t=d(a); +if t and#t>0 then +return t +elseif a then +return nil,"Invalid JID: "..a; +elseif o then +return nil,"Required value missing"; +end +end +e["jid-multi"]= +function(o,i) +local t={}; +local a={}; +for e in o:childtags("value")do +local e=e:get_text(); +local o=d(e); +t[#t+1]=o; +if e and not o then +a[#a+1]=("Invalid JID: "..e); +end +end +if#t>0 then +return t,(#a>0 and n(a,"\n")or nil); +elseif i then +return nil,"Required value missing"; +end +end +e["list-multi"]= +function(o,a) +local t={}; +for e in o:childtags("value")do +t[#t+1]=e:get_text(); +end +return t,(a and#t==0 and"Required value missing"or nil); +end +e["text-multi"]= +function(a,t) +local t,a=e["list-multi"](a,t); +if t then +t=n(t,"\n"); +end +return t,a; +end +e["list-single"]= +e["text-single"]; +local a={ +["1"]=true,["true"]=true, +["0"]=false,["false"]=false, +}; +e["boolean"]= +function(t,o) +local t=t:get_child_text("value"); +local a=a[t~=nil and t]; +if a~=nil then +return a; +elseif t then +return nil,"Invalid boolean representation"; +elseif o then +return nil,"Required value missing"; +end +end +e["hidden"]= +function(e) +return e:get_child_text("value"); +end +return _M; +end) +package.preload['util.caps']=(function(...) +local l=require"util.encodings".base64.encode; +local d=require"util.hashes".sha1; +local n,s,h=table.insert,table.sort,table.concat; +local r=ipairs; +module"caps" +function calculate_hash(e) +local i,o,a={},{},{}; +for t,e in r(e)do +if e.name=="identity"then +n(i,(e.attr.category or"").."\0"..(e.attr.type or"").."\0"..(e.attr["xml:lang"]or"").."\0"..(e.attr.name or"")); +elseif e.name=="feature"then +n(o,e.attr.var or""); +elseif e.name=="x"and e.attr.xmlns=="jabber:x:data"then +local t={}; +local o; +for a,e in r(e.tags)do +if e.name=="field"and e.attr.var then +local a={}; +for t,e in r(e.tags)do +e=#e.tags==0 and e:get_text(); +if e then n(a,e);end +end +s(a); +if e.attr.var=="FORM_TYPE"then +o=a[1]; +elseif#a>0 then +n(t,e.attr.var.."\0"..h(a,"<")); +else +n(t,e.attr.var); +end +end +end +s(t); +t=h(t,"<"); +if o then t=o.."\0"..t;end +n(a,t); +end +end +s(i); +s(o); +s(a); +if#i>0 then i=h(i,"<"):gsub("%z","/").."<";else i="";end +if#o>0 then o=h(o,"<").."<";else o="";end +if#a>0 then a=h(a,"<"):gsub("%z","<").."<";else a="";end +local e=i..o..a; +local t=l(d(e)); +return t,e; +end +return _M; +end) +package.preload['util.vcard']=(function(...) +local i=require"util.stanza"; +local a,r=table.insert,table.concat; +local s=type; +local e,n,c=next,pairs,ipairs; +local d,h,l,u; +local f="\n"; +local o; +local function e() +error"Not implemented" +end +local function e() +error"Not implemented" +end +local function w(e) +return e:gsub("[,:;\\]","\\%1"):gsub("\n","\\n"); +end +local function y(e) +return e:gsub("\\?[\\nt:;,]",{ +["\\\\"]="\\", +["\\n"]="\n", +["\\r"]="\r", +["\\t"]="\t", +["\\:"]=":", +["\\;"]=";", +["\\,"]=",", +[":"]="\29", +[";"]="\30", +[","]="\31", +}); +end +local function p(e) +local a=i.stanza(e.name,{xmlns="vcard-temp"}); +local t=o[e.name]; +if t=="text"then +a:text(e[1]); +elseif s(t)=="table"then +if t.types and e.TYPE then +if s(e.TYPE)=="table"then +for o,t in n(t.types)do +for o,e in n(e.TYPE)do +if e:upper()==t then +a:tag(t):up(); +break; +end +end +end +else +a:tag(e.TYPE:upper()):up(); +end +end +if t.props then +for o,t in n(t.props)do +if e[t]then +a:tag(t):up(); +end +end +end +if t.value then +a:tag(t.value):text(e[1]):up(); +elseif t.values then +local o=t.values; +local i=o.behaviour=="repeat-last"and o[#o]; +for o=1,#e do +a:tag(t.values[o]or i):text(e[o]):up(); +end +end +end +return a; +end +local function m(t) +local e=i.stanza("vCard",{xmlns="vcard-temp"}); +for a=1,#t do +e:add_child(p(t[a])); +end +return e; +end +function u(e) +if not e[1]or e[1].name then +return m(e) +else +local t=i.stanza("xCard",{xmlns="vcard-temp"}); +for a=1,#e do +t:add_child(m(e[a])); +end +return t; +end +end +function d(t) +t=t +:gsub("\r\n","\n") +:gsub("\n ","") +:gsub("\n\n+","\n"); +local s={}; +local e; +for t in t:gmatch("[^\n]+")do +local t=y(t); +local n,t,i=t:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); +i=i:gsub("\29",":"); +if#t>0 then +local o={}; +for a,n,i in t:gmatch("\30([^=]+)(=?)([^\30]*)")do +a=a:upper(); +local e={}; +for t in i:gmatch("[^\31]+")do +e[#e+1]=t +e[t]=true; +end +if n=="="then +o[a]=e; +else +o[a]=true; +end +end +t=o; +end +if n=="BEGIN"and i=="VCARD"then +e={}; +s[#s+1]=e; +elseif n=="END"and i=="VCARD"then +e=nil; +elseif e and o[n]then +local o=o[n]; +local n={name=n}; +e[#e+1]=n; +local s=e; +e=n; +if o.types then +for o,a in c(o.types)do +local a=a:lower(); +if(t.TYPE and t.TYPE[a]==true) +or t[a]==true then +e.TYPE=a; +end +end +end +if o.props then +for o,a in c(o.props)do +if t[a]then +if t[a]==true then +e[a]=true; +else +for o,t in c(t[a])do +e[a]=t; +end +end +end +end +end +if o=="text"or o.value then +a(e,i); +elseif o.values then +local t="\30"..i; +for t in t:gmatch("\30([^\30]*)")do +a(e,t); +end +end +e=s; +end +end +return s; +end +local function c(t) +local e={}; +for a=1,#t do +e[a]=w(t[a]); +end +e=r(e,";"); +local a=""; +for e,t in n(t)do +if s(e)=="string"and e~="name"then +a=a..(";%s=%s"):format(e,s(t)=="table"and r(t,",")or t); +end +end +return("%s%s:%s"):format(t.name,a,e) +end +local function i(t) +local e={}; +a(e,"BEGIN:VCARD") +for o=1,#t do +a(e,c(t[o])); +end +a(e,"END:VCARD") +return r(e,f); +end +function h(e) +if e[1]and e[1].name then +return i(e) +else +local t={}; +for a=1,#e do +t[a]=i(e[a]); +end +return r(t,f); +end +end +local function r(i) +local e=i.name; +local t=o[e]; +local e={name=e}; +if t=="text"then +e[1]=i:get_text(); +elseif s(t)=="table"then +if t.value then +e[1]=i:get_child_text(t.value)or""; +elseif t.values then +local t=t.values; +if t.behaviour=="repeat-last"then +for t=1,#i.tags do +a(e,i.tags[t]:get_text()or""); +end +else +for o=1,#t do +a(e,i:get_child_text(t[o])or""); +end +end +elseif t.names then +local t=t.names; +for a=1,#t do +if i:get_child(t[a])then +e[1]=t[a]; +break; +end +end +end +if t.props_verbatim then +for a,t in n(t.props_verbatim)do +e[a]=t; +end +end +if t.types then +local t=t.types; +e.TYPE={}; +for o=1,#t do +if i:get_child(t[o])then +a(e.TYPE,t[o]:lower()); +end +end +if#e.TYPE==0 then +e.TYPE=nil; +end +end +if t.props then +local t=t.props; +for o=1,#t do +local t=t[o] +local o=i:get_child_text(t); +if o then +e[t]=e[t]or{}; +a(e[t],o); +end +end +end +else +return nil +end +return e; +end +local function i(e) +local e=e.tags; +local t={}; +for o=1,#e do +a(t,r(e[o])); +end +return t +end +function l(e) +if e.attr.xmlns~="vcard-temp"then +return nil,"wrong-xmlns"; +end +if e.name=="xCard"then +local t={}; +local a=e.tags; +for e=1,#a do +t[e]=i(a[e]); +end +return t +elseif e.name=="vCard"then +return i(e) +end +end +o={ +VERSION="text", +FN="text", +N={ +values={ +"FAMILY", +"GIVEN", +"MIDDLE", +"PREFIX", +"SUFFIX", +}, +}, +NICKNAME="text", +PHOTO={ +props_verbatim={ENCODING={"b"}}, +props={"TYPE"}, +value="BINVAL", +}, +BDAY="text", +ADR={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +values={ +"POBOX", +"EXTADD", +"STREET", +"LOCALITY", +"REGION", +"PCODE", +"CTRY", +} +}, +LABEL={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +value="LINE", +}, +TEL={ +types={ +"HOME", +"WORK", +"VOICE", +"FAX", +"PAGER", +"MSG", +"CELL", +"VIDEO", +"BBS", +"MODEM", +"ISDN", +"PCS", +"PREF", +}, +value="NUMBER", +}, +EMAIL={ +types={ +"HOME", +"WORK", +"INTERNET", +"PREF", +"X400", +}, +value="USERID", +}, +JABBERID="text", +MAILER="text", +TZ="text", +GEO={ +values={ +"LAT", +"LON", +}, +}, +TITLE="text", +ROLE="text", +LOGO="copy of PHOTO", +AGENT="text", +ORG={ +values={ +behaviour="repeat-last", +"ORGNAME", +"ORGUNIT", +} +}, +CATEGORIES={ +values="KEYWORD", +}, +NOTE="text", +PRODID="text", +REV="text", +SORTSTRING="text", +SOUND="copy of PHOTO", +UID="text", +URL="text", +CLASS={ +names={ +"PUBLIC", +"PRIVATE", +"CONFIDENTIAL", +}, +}, +KEY={ +props={"TYPE"}, +value="CRED", +}, +DESC="text", +}; +o.LOGO=o.PHOTO; +o.SOUND=o.PHOTO; +return{ +from_text=d; +to_text=h; +from_xep54=l; +to_xep54=u; +lua_to_text=h; +lua_to_xep54=u; +text_to_lua=d; +text_to_xep54=function(...)return u(d(...));end; +xep54_to_lua=l; +xep54_to_text=function(...)return h(l(...))end; +}; +end) +package.preload['util.logger']=(function(...) +local e=pcall; +local e=string.find; +local e,n,e=ipairs,pairs,setmetatable; +module"logger" +local e={}; +local t; +function init(e) +local a=t(e,"debug"); +local i=t(e,"info"); +local o=t(e,"warn"); +local n=t(e,"error"); +return function(e,t,...) +if e=="debug"then +return a(t,...); +elseif e=="info"then +return i(t,...); +elseif e=="warn"then +return o(t,...); +elseif e=="error"then +return n(t,...); +end +end +end +function t(i,a) +local t=e[a]; +if not t then +t={}; +e[a]=t; +end +local e=function(o,...) +for e=1,#t do +t[e](i,a,o,...); +end +end +return e; +end +function reset() +for t,e in n(e)do +for t=1,#e do +e[t]=nil; +end +end +end +function add_level_sink(t,a) +if not e[t]then +e[t]={a}; +else +e[t][#e[t]+1]=a; +end +end +_M.new=t; +return _M; +end) +package.preload['util.datetime']=(function(...) +local e=os.date; +local i=os.time; +local u=os.difftime; +local t=error; +local l=tonumber; +module"datetime" +function date(t) +return e("!%Y-%m-%d",t); +end +function datetime(t) +return e("!%Y-%m-%dT%H:%M:%SZ",t); +end +function time(t) +return e("!%H:%M:%S",t); +end +function legacy(t) +return e("!%Y%m%dT%H:%M:%S",t); +end +function parse(t) +if t then +local n,r,s,h,d,a,o; +n,r,s,h,d,a,o=t:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); +if n then +local u=u(i(e("*t")),i(e("!*t"))); +local t=0; +if o~=""and o~="Z"then +local o,a,e=o:match("([+%-])(%d%d):?(%d*)"); +if not o then return;end +if#e~=2 then e="0";end +a,e=l(a),l(e); +t=a*60*60+e*60; +if o=="-"then t=-t;end +end +a=(a+u)-t; +return i({year=n,month=r,day=s,hour=h,min=d,sec=a,isdst=false}); +end +end +end +return _M; +end) +package.preload['util.sasl.scram']=(function(...) +local i,c=require"mime".b64,require"mime".unb64; +local a=require"crypto"; +local t=require"bit"; +local p=tonumber; +local o,e=string.char,string.byte; +local n=string.gsub; +local s=t.bxor; +local function m(t,a) +return(n(t,"()(.)",function(i,t) +return o(s(e(t),e(a,i))) +end)); +end +local function w(e) +return a.digest("sha1",e,true); +end +local t=a.hmac.digest; +local function e(e,a) +return t("sha1",a,e,true); +end +local function y(o,t,i) +local t=e(o,t.."\0\0\0\1"); +local a=t; +for i=2,i do +t=e(o,t); +a=m(a,t); +end +return a; +end +local function f(e) +return e; +end +local function t(e) +return(n(e,"[,=]",{[","]="=2C",["="]="=3D"})); +end +local function n(o,d) +local t="n="..t(o.username); +local n=i(a.rand.bytes(15)); +local r="r="..n; +local h=t..","..r; +local s=""; +local t=o.conn:ssl()and"y"or"n"; +if d=="SCRAM-SHA-1-PLUS"then +s=o.conn:socket():getfinished(); +t="p=tls-unique"; +end +local d=t..",,"; +local t=d..h; +local t,u=coroutine.yield(t); +if t~="challenge"then return false end +local a,t,l=u:match("(r=[^,]+),s=([^,]*),i=(%d+)"); +local l=p(l); +t=c(t); +if not a or not t or not l then +return false,"Could not parse server_first_message"; +elseif a:find(n,3,true)~=3 then +return false,"nonce sent by server does not match our nonce"; +elseif a==r then +return false,"server did not append s-nonce to nonce"; +end +local n=d..s; +local n="c="..i(n); +local n=n..","..a; +local o=y(f(o.password),t,l); +local t=e(o,"Client Key"); +local s=w(t); +local a=h..","..u..","..n; +local s=e(s,a); +local t=m(t,s); +local o=e(o,"Server Key"); +local e=e(o,a); +local t="p="..i(t); +local t=n..","..t; +local t,a=coroutine.yield(t); +if t~="success"then return false,"success-expected"end +local t=a:match("v=([^,]+)"); +if c(t)~=e then +return false,"server signature did not match"; +end +return true; +end +return function(e,t) +if e.username and(e.password or(e.client_key or e.server_key))then +if t=="SCRAM-SHA-1"then +return n,99; +elseif t=="SCRAM-SHA-1-PLUS"then +local e=e.conn:ssl()and e.conn:socket(); +if e and e.getfinished then +return n,100; +end +end +end +end +end) +package.preload['util.sasl.plain']=(function(...) +return function(e,t) +if t=="PLAIN"and e.username and e.password then +return function(e) +return"success"==coroutine.yield("\0"..e.username.."\0"..e.password); +end,5; +end +end +end) +package.preload['util.sasl.anonymous']=(function(...) +return function(t,e) +if e=="ANONYMOUS"then +return function() +return coroutine.yield()=="success"; +end,0; +end +end +end) +package.preload['verse.plugins.tls']=(function(...) +local a=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-tls"; +function a.plugins.tls(e) +local function i(o) +if e.authenticated then return;end +if o:get_child("starttls",t)and e.conn.starttls then +e:debug("Negotiating TLS..."); +e:send(a.stanza("starttls",{xmlns=t})); +return true; +elseif not e.conn.starttls and not e.secure then +e:warn("SSL libary (LuaSec) not loaded, so TLS not available"); +elseif not e.secure then +e:debug("Server doesn't offer TLS :("); +end +end +local function o(t) +if t.name=="proceed"then +e:debug("Server says proceed, handshake starting..."); +e.conn:starttls({mode="client",protocol="sslv23",options="no_sslv2"},true); +end +end +local function a(t) +if t=="ssl-handshake-complete"then +e.secure=true; +e:debug("Re-opening stream..."); +e:reopen(); +end +end +e:hook("stream-features",i,400); +e:hook("stream/"..t,o); +e:hook("status",a,400); +return true; +end +end) +package.preload['verse.plugins.sasl']=(function(...) +local n,h=require"mime".b64,require"mime".unb64; +local a="urn:ietf:params:xml:ns:xmpp-sasl"; +function verse.plugins.sasl(e) +local function s(t) +if e.authenticated then return;end +e:debug("Authenticating with SASL..."); +local t=t:get_child("mechanisms",a); +if not t then return end +local o={}; +local i={}; +for t in t:childtags("mechanism")do +t=t:get_text(); +e:debug("Server offers %s",t); +if not o[t]then +local n=t:match("[^-]+"); +local s,a=pcall(require,"util.sasl."..n:lower()); +if s then +e:debug("Loaded SASL %s module",n); +o[t],i[t]=a(e,t); +elseif not tostring(a):match("not found")then +e:debug("Loading failed: %s",tostring(a)); +end +end +end +local t={}; +for e in pairs(o)do +table.insert(t,e); +end +if not t[1]then +e:event("authentication-failure",{condition="no-supported-sasl-mechanisms"}); +e:close(); +return; +end +table.sort(t,function(e,t)return i[e]>i[t];end); +local t,i=t[1]; +e:debug("Selecting %s mechanism...",t); +e.sasl_mechanism=coroutine.wrap(o[t]); +i=e:sasl_mechanism(t); +local t=verse.stanza("auth",{xmlns=a,mechanism=t}); +if i then +t:text(n(i)); +end +e:send(t); +return true; +end +local function o(t) +if t.name=="failure"then +local a=t.tags[1]; +local t=t:get_child_text("text"); +e:event("authentication-failure",{condition=a.name,text=t}); +e:close(); +return false; +end +local t,o=e.sasl_mechanism(t.name,h(t:get_text())); +if not t then +e:event("authentication-failure",{condition=o}); +e:close(); +return false; +elseif t==true then +e:event("authentication-success"); +e.authenticated=true +e:reopen(); +else +e:send(verse.stanza("response",{xmlns=a}):text(n(t))); +end +return true; +end +e:hook("stream-features",s,300); +e:hook("stream/"..a,o); +return true; +end +end) +package.preload['verse.plugins.bind']=(function(...) +local t=require"verse"; +local i=require"util.jid"; +local a="urn:ietf:params:xml:ns:xmpp-bind"; +function t.plugins.bind(e) +local function o(o) +if e.bound then return;end +e:debug("Binding resource..."); +e:send_iq(t.iq({type="set"}):tag("bind",{xmlns=a}):tag("resource"):text(e.resource), +function(t) +if t.attr.type=="result"then +local t=t +:get_child("bind",a) +:get_child_text("jid"); +e.username,e.host,e.resource=i.split(t); +e.jid,e.bound=t,true; +e:event("bind-success",{jid=t}); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("bind-failure",{error=t,text=a,type=o}); +end +end); +end +e:hook("stream-features",o,200); +return true; +end +end) +package.preload['verse.plugins.session']=(function(...) +local o=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-session"; +function o.plugins.session(e) +local function n(a) +local a=a:get_child("session",t); +if a and not a:get_child("optional")then +local function i(a) +e:debug("Establishing Session..."); +e:send_iq(o.iq({type="set"}):tag("session",{xmlns=t}), +function(t) +if t.attr.type=="result"then +e:event("session-success"); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("session-failure",{error=t,text=a,type=o}); +end +end); +return true; +end +e:hook("bind-success",i); +end +end +e:hook("stream-features",n); +return true; +end +end) +package.preload['verse.plugins.legacy']=(function(...) +local i=require"verse"; +local n=require"util.uuid".generate; +local o="jabber:iq:auth"; +function i.plugins.legacy(e) +function handle_auth_form(t) +local a=t:get_child("query",o); +if t.attr.type~="result"or not a then +local a,o,t=t:get_error(); +e:debug("warn","%s %s: %s",a,o,t); +end +local t={ +username=e.username; +password=e.password; +resource=e.resource or n(); +digest=false,sequence=false,token=false; +}; +local o=i.iq({to=e.host,type="set"}) +:tag("query",{xmlns=o}); +if#a>0 then +for a in a:childtags()do +local a=a.name; +local i=t[a]; +if i then +o:tag(a):text(t[a]):up(); +elseif i==nil then +local t="feature-not-implemented"; +e:event("authentication-failure",{condition=t}); +return false; +end +end +else +for t,e in pairs(t)do +if e then +o:tag(t):text(e):up(); +end +end +end +e:send_iq(o,function(a) +if a.attr.type=="result"then +e.resource=t.resource; +e.jid=t.username.."@"..e.host.."/"..t.resource; +e:event("authentication-success"); +e:event("bind-success",e.jid); +else +local a,t,a=a:get_error(); +e:event("authentication-failure",{condition=t}); +end +end); +end +function handle_opened(t) +if not t.version then +e:send_iq(i.iq({type="get"}) +:tag("query",{xmlns="jabber:iq:auth"}) +:tag("username"):text(e.username), +handle_auth_form); +end +end +e:hook("opened",handle_opened); +end +end) +package.preload['verse.plugins.compression']=(function(...) +local a=require"verse"; +local o=require"zlib"; +local e="http://jabber.org/features/compress" +local t="http://jabber.org/protocol/compress" +local e="http://etherx.jabber.org/streams"; +local i=9; +local function s(e) +local i,o=pcall(o.deflate,i); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +e:send(t); +e:error("Failed to create zlib.deflate filter: %s",tostring(o)); +return +end +return o +end +local function h(e) +local i,o=pcall(o.inflate); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +e:send(t); +e:error("Failed to create zlib.inflate filter: %s",tostring(o)); +return +end +return o +end +local function r(e,i) +function e:send(o) +local i,o,n=pcall(i,tostring(o),'sync'); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +e:warn("Compressed send failed: %s",tostring(o)); +return; +end +e.conn:write(o); +end; +end +local function d(e,o) +local n=e.data +e.data=function(s,i) +e:debug("Decompressing data..."); +local i,o,h=pcall(o,i); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +stream:warn("%s",tostring(o)); +return; +end +return n(s,o); +end; +end +function a.plugins.compression(e) +local function n(o) +if not e.compressed then +local o=o:child_with_name("compression"); +if o then +for o in o:children()do +local o=o[1] +if o=="zlib"then +e:send(a.stanza("compress",{xmlns=t}):tag("method"):text("zlib")) +e:debug("Enabled compression using zlib.") +return true; +end +end +session:debug("Remote server supports no compression algorithm we support.") +end +end +end +local function i(o) +if o.name=="compressed"then +e:debug("Activating compression...") +local a=s(e); +if not a then return end +local t=h(e); +if not t then return end +r(e,a); +d(e,t); +e.compressed=true; +e:reopen(); +elseif o.name=="failure"then +e:warn("Failed to establish compression"); +end +end +e:hook("stream-features",n,250); +e:hook("stream/"..t,i); +end +end) +package.preload['verse.plugins.smacks']=(function(...) +local i=require"verse"; +local h=socket.gettime; +local n="urn:xmpp:sm:2"; +function i.plugins.smacks(e) +local t={}; +local a=0; +local r=h(); +local o; +local s=0; +local function d(t) +if t.attr.xmlns=="jabber:client"or not t.attr.xmlns then +s=s+1; +e:debug("Increasing handled stanzas to %d for %s",s,t:top_tag()); +end +end +function outgoing_stanza(a) +if a.name and not a.attr.xmlns then +t[#t+1]=tostring(a); +r=h(); +if not o then +o=true; +e:debug("Waiting to send ack request..."); +i.add_task(1,function() +if#t==0 then +o=false; +return; +end +local a=h()-r; +if a<1 and#t<10 then +return 1-a; +end +e:debug("Time up, sending ..."); +o=false; +e:send(i.stanza("r",{xmlns=n})); +end); +end +end +end +local function h() +e:debug("smacks: connection lost"); +e.stream_management_supported=nil; +if e.resumption_token then +e:debug("smacks: have resumption token, reconnecting in 1s..."); +e.authenticated=nil; +i.add_task(1,function() +e:connect(e.connect_host or e.host,e.connect_port or 5222); +end); +return true; +end +end +local function r() +e.resumption_token=nil; +e:unhook("disconnected",h); +end +local function l(o) +if o.name=="r"then +e:debug("Ack requested... acking %d handled stanzas",s); +e:send(i.stanza("a",{xmlns=n,h=tostring(s)})); +elseif o.name=="a"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +else +e:warn("Received bad ack for "..o.." when last ack was "..a); +end +elseif o.name=="enabled"then +if o.attr.id then +e.resumption_token=o.attr.id; +e:hook("closed",r,100); +e:hook("disconnected",h,100); +end +elseif o.name=="resumed"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +end +for a=1,#t do +e:send(t[a]); +end +t={}; +e:debug("Resumed successfully"); +e:event("resumed"); +else +e:warn("Don't know how to handle "..n.."/"..o.name); +end +end +local function a() +if not e.smacks then +e:debug("smacks: sending enable"); +e:send(i.stanza("enable",{xmlns=n,resume="true"})); +e.smacks=true; +e:hook("stanza",d); +e:hook("outgoing",outgoing_stanza); +end +end +local function t(t) +if t:get_child("sm",n)then +e.stream_management_supported=true; +if e.smacks and e.bound then +e:debug("Resuming stream with %d handled stanzas",s); +e:send(i.stanza("resume",{xmlns=n, +h=s,previd=e.resumption_token})); +return true; +else +e:hook("bind-success",a,1); +end +end +end +e:hook("stream-features",t,250); +e:hook("stream/"..n,l); +end +end) +package.preload['verse.plugins.keepalive']=(function(...) +local t=require"verse"; +function t.plugins.keepalive(e) +e.keepalive_timeout=e.keepalive_timeout or 300; +t.add_task(e.keepalive_timeout,function() +e.conn:write(" "); +return e.keepalive_timeout; +end); +end +end) +package.preload['verse.plugins.disco']=(function(...) +local a=require"verse"; +local h=require("mime").b64; +local n=require("util.sha1").sha1; +local s="http://jabber.org/protocol/caps"; +local e="http://jabber.org/protocol/disco"; +local o=e.."#info"; +local i=e.."#items"; +function a.plugins.disco(e) +e:add_plugin("presence"); +local t={ +__index=function(a,e) +local t={identities={},features={}}; +if e=="identities"or e=="features"then +return a[false][e] +end +a[e]=t; +return t; +end, +}; +local r={ +__index=function(t,a) +local e={}; +t[a]=e; +return e; +end, +}; +e.disco={ +cache={}, +info=setmetatable({ +[false]={ +identities={ +{category='client',type='pc',name='Verse'}, +}, +features={ +[s]=true, +[o]=true, +[i]=true, +}, +}, +},t); +items=setmetatable({[false]={}},r); +}; +e.caps={} +e.caps.node='http://code.matthewwild.co.uk/verse/' +local function d(t,e) +if t.category0 then +self.connecting_peer_candidates=true; +local function n(e,t) +self.jingle:send_command("transport-info",a.stanza("content",{creator=self.creator,name=self.name}) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("candidate-used",{cid=e.cid})); +self.onconnect_callback=i; +self.conn=t; +end +local e=s(self.s5b_sid..self.peer..e.jid,true); +h(n,t,e); +else +e:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); +self.onconnect_callback=i; +end +end +function t:info_received(t) +e:warn("Info received"); +local n=t:child_with_name("content"); +local i=n:child_with_name("transport"); +if i:get_child("candidate-used")and not self.connecting_peer_candidates then +local t=i:child_with_name("candidate-used"); +if t then +local function d(i,e) +if self.jingle.role=="initiator"then +self.jingle.stream:send_iq(a.iq({to=i.jid,type="set"}) +:tag("query",{xmlns=r,sid=self.s5b_sid}) +:tag("activate"):text(self.jingle.peer),function(i) +if i.attr.type=="result"then +self.jingle:send_command("transport-info",a.stanza("content",n.attr) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("activated",{cid=t.attr.cid})); +self.conn=e; +self.onconnect_callback(e); +else +self.jingle.stream:error("Failed to activate bytestream"); +end +end); +end +end +self.jingle.stream:debug("CID: %s",self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]); +local t={ +self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]; +}; +local e=s(self.s5b_sid..e.jid..self.peer,true); +h(d,t,e); +end +elseif i:get_child("activated")then +self.onconnect_callback(self.conn); +end +end +function t:disconnect() +if self.conn then +self.conn:close(); +end +end +function t:handle_accepted(e) +end +local t={__index=t}; +e:hook("jingle/transport/"..o,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.proxy65']=(function(...) +local e=require"util.events"; +local r=require"util.uuid"; +local h=require"util.sha1"; +local i={}; +i.__index=i; +local o="http://jabber.org/protocol/bytestreams"; +local n; +function verse.plugins.proxy65(t) +t.proxy65=setmetatable({stream=t},i); +t.proxy65.available_streamhosts={}; +local e=0; +t:hook("disco/service-discovered/proxy",function(a) +if a.type=="bytestreams"then +e=e+1; +t:send_iq(verse.iq({to=a.jid,type="get"}) +:tag("query",{xmlns=o}),function(a) +e=e-1; +if a.attr.type=="result"then +local e=a:get_child("query",o) +:get_child("streamhost").attr; +t.proxy65.available_streamhosts[e.jid]={ +jid=e.jid; +host=e.host; +port=tonumber(e.port); +}; +end +if e==0 then +t:event("proxy65/discovered-proxies",t.proxy65.available_streamhosts); +end +end); +end +end); +t:hook("iq/"..o,function(a) +local e=verse.new(nil,{ +initiator_jid=a.attr.from, +streamhosts={}, +current_host=0; +}); +for t in a.tags[1]:childtags()do +if t.name=="streamhost"then +table.insert(e.streamhosts,t.attr); +end +end +local function o() +if e.current_host<#e.streamhosts then +e.current_host=e.current_host+1; +e:connect( +e.streamhosts[e.current_host].host, +e.streamhosts[e.current_host].port +); +n(t,e,a.tags[1].attr.sid,a.attr.from,t.jid); +return true; +end +e:unhook("disconnected",o); +t:send(verse.error_reply(a,"cancel","item-not-found")); +end +function e:accept() +e:hook("disconnected",o,100); +e:hook("connected",function() +e:unhook("disconnected",o); +local e=verse.reply(a) +:tag("query",a.tags[1].attr) +:tag("streamhost-used",{jid=e.streamhosts[e.current_host].jid}); +t:send(e); +end,100); +o(); +end +function e:refuse() +end +t:event("proxy65/request",e); +end); +end +function i:new(t,s) +local e=verse.new(nil,{ +target_jid=t; +bytestream_sid=r.generate(); +}); +local a=verse.iq{type="set",to=t} +:tag("query",{xmlns=o,mode="tcp",sid=e.bytestream_sid}); +for t,e in ipairs(s or self.proxies)do +a:tag("streamhost",e):up(); +end +self.stream:send_iq(a,function(a) +if a.attr.type=="error"then +local t,a,o=a:get_error(); +e:event("connection-failed",{conn=e,type=t,condition=a,text=o}); +else +local a=a.tags[1]:get_child("streamhost-used"); +if not a then +end +e.streamhost_jid=a.attr.jid; +local a,i; +for o,t in ipairs(s or self.proxies)do +if t.jid==e.streamhost_jid then +a,i=t.host,t.port; +break; +end +end +if not(a and i)then +end +e:connect(a,i); +local function a() +e:unhook("connected",a); +local t=verse.iq{to=e.streamhost_jid,type="set"} +:tag("query",{xmlns=o,sid=e.bytestream_sid}) +:tag("activate"):text(t); +self.stream:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("connected",e); +else +end +end); +return true; +end +e:hook("connected",a,100); +n(self.stream,e,e.bytestream_sid,self.stream.jid,t); +end +end); +return e; +end +function n(i,e,o,a,t) +local a=h.sha1(o..a..t); +local function t() +e:unhook("connected",t); +return true; +end +local function n(t) +e:unhook("incoming-raw",n); +if t:sub(1,2)~="\005\000"then +return e:event("error","connection-failure"); +end +e:event("connected"); +return true; +end +local function o(i) +e:unhook("incoming-raw",o); +if i~="\005\000"then +local t="version-mismatch"; +if i:sub(1,1)=="\005"then +t="authentication-failure"; +end +return e:event("error",t); +end +e:send(string.char(5,1,0,3,#a)..a.."\0\0"); +e:hook("incoming-raw",n,100); +return true; +end +e:hook("connected",t,200); +e:hook("incoming-raw",o,100); +e:send("\005\001\000"); +end +end) +package.preload['verse.plugins.jingle_ibb']=(function(...) +local e=require"verse"; +local i=require"util.encodings".base64; +local s=require"util.uuid".generate; +local n="urn:xmpp:jingle:transports:ibb:1"; +local o="http://jabber.org/protocol/ibb"; +assert(i.encode("This is a test.")=="VGhpcyBpcyBhIHRlc3Qu","Base64 encoding failed"); +assert(i.decode("VGhpcyBpcyBhIHRlc3Qu")=="This is a test.","Base64 decoding failed"); +local t=table.concat +local a={}; +local t={__index=a}; +local function h(a) +local t=setmetatable({stream=a},t) +t=e.eventable(t); +return t; +end +function a:initiate(e,a,t) +self.block=2048; +self.stanza=t or'iq'; +self.peer=e; +self.sid=a or tostring(self):match("%x+$"); +self.iseq=0; +self.oseq=0; +local e=function(e) +return self:feed(e) +end +self.feeder=e; +print("Hooking incomming IQs"); +local a=self.stream; +a:hook("iq/"..o,e) +if t=="message"then +a:hook("message",e) +end +end +function a:open(t) +self.stream:send_iq(e.iq{to=self.peer,type="set"} +:tag("open",{ +xmlns=o, +["block-size"]=self.block, +sid=self.sid, +stanza=self.stanza +}) +,function(e) +if t then +if e.attr.type~="error"then +t(true) +else +t(false,e:get_error()) +end +end +end); +end +function a:send(n) +local a=self.stanza; +local t; +if a=="iq"then +t=e.iq{type="set",to=self.peer} +elseif a=="message"then +t=e.message{to=self.peer} +end +local e=self.oseq; +self.oseq=e+1; +t:tag("data",{xmlns=o,sid=self.sid,seq=e}) +:text(i.encode(n)); +if a=="iq"then +self.stream:send_iq(t,function(e) +self:event(e.attr.type=="result"and"drained"or"error"); +end) +else +stream:send(t) +self:event("drained"); +end +end +function a:feed(t) +if t.attr.from~=self.peer then return end +local a=t[1]; +if a.attr.sid~=self.sid then return end +local n; +if a.name=="open"then +self:event("connected"); +self.stream:send(e.reply(t)) +return true +elseif a.name=="data"then +local o=t:get_child_text("data",o); +local a=tonumber(a.attr.seq); +local n=self.iseq; +if o and a then +if a~=n then +self.stream:send(e.error_reply(t,"cancel","not-acceptable","Wrong sequence. Packet lost?")) +self:close(); +self:event("error"); +return true; +end +self.iseq=a+1; +local a=i.decode(o); +if self.stanza=="iq"then +self.stream:send(e.reply(t)) +end +self:event("incoming-raw",a); +return true; +end +elseif a.name=="close"then +self.stream:send(e.reply(t)) +self:close(); +return true +end +end +function a:close() +self.stream:unhook("iq/"..o,self.feeder) +self:event("disconnected"); +end +function e.plugins.jingle_ibb(a) +a:hook("ready",function() +a:add_disco_feature(n); +end,10); +local t={}; +function t:_setup() +local e=h(self.stream); +e.sid=self.sid or e.sid; +e.stanza=self.stanza or e.stanza; +e.block=self.block or e.block; +e:initiate(self.peer,self.sid,self.stanza); +self.conn=e; +end +function t:generate_initiate() +print("ibb:generate_initiate() as "..self.role); +local t=s(); +self.sid=t; +self.stanza='iq'; +self.block=2048; +local e=e.stanza("transport",{xmlns=n, +sid=self.sid,stanza=self.stanza,["block-size"]=self.block}); +return e; +end +function t:generate_accept(t) +print("ibb:generate_accept() as "..self.role); +local e=t.attr; +self.sid=e.sid or self.sid; +self.stanza=e.stanza or self.stanza; +self.block=e["block-size"]or self.block; +self:_setup(); +return t; +end +function t:connect(t) +if not self.conn then +self:_setup(); +end +local e=self.conn; +print("ibb:connect() as "..self.role); +if self.role=="initiator"then +e:open(function(a,...) +assert(a,table.concat({...},", ")); +t(e); +end); +else +t(e); +end +end +function t:info_received(e) +print("ibb:info_received()"); +end +function t:disconnect() +if self.conn then +self.conn:close() +end +end +function t:handle_accepted(e)end +local t={__index=t}; +a:hook("jingle/transport/"..n,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.pubsub']=(function(...) +local h=require"verse"; +local e=require"util.jid".bare; +local s=table.insert; +local o="http://jabber.org/protocol/pubsub"; +local n="http://jabber.org/protocol/pubsub#owner"; +local a="http://jabber.org/protocol/pubsub#event"; +local e="http://jabber.org/protocol/pubsub#errors"; +local e={}; +local i={__index=e}; +function h.plugins.pubsub(e) +e.pubsub=setmetatable({stream=e},i); +e:hook("message",function(t) +local o=t.attr.from; +for t in t:childtags("event",a)do +local t=t:get_child("items"); +if t then +local a=t.attr.node; +for t in t:childtags("item")do +e:event("pubsub/event",{ +from=o; +node=a; +item=t; +}); +end +end +end +end); +return true; +end +function e:create(t,e,a) +return self:service(t):node(e):create(nil,a); +end +function e:subscribe(e,t,a,o) +return self:service(e):node(t):subscribe(a,nil,o); +end +function e:publish(a,e,i,o,t) +return self:service(a):node(e):publish(i,nil,o,t); +end +local a={}; +local t={__index=a}; +function e:service(e) +return setmetatable({stream=self.stream,service=e},t) +end +local function t(r,e,s,a,i,n,t) +local e=h.iq{type=r or"get",to=e} +:tag("pubsub",{xmlns=s or o}) +if a then e:tag(a,{node=i,jid=n});end +if t then e:tag("item",{id=t~=true and t or nil});end +return e; +end +function a:subscriptions(a) +self.stream:send_iq(t(nil,self.service,nil,"subscriptions") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",o); +local e=e and e:get_child("subscriptions"); +local o={}; +if e then +for e in e:childtags("subscription")do +local t=self:node(e.attr.node) +t.subscription=e; +t.subscribed_jid=e.attr.jid; +s(o,t); +end +end +a(o); +else +a(false,e:get_error()); +end +end or nil); +end +function a:affiliations(a) +self.stream:send_iq(t(nil,self.service,nil,"affiliations") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",o); +local e=e and e:get_child("affiliations")or{}; +local o={}; +if e then +for t in e:childtags("affiliation")do +local e=self:node(t.attr.node) +e.affiliation=t; +s(o,e); +end +end +a(o); +else +a(false,e:get_error()); +end +end or nil); +end +function a:nodes(a) +self.stream:disco_items(self.service,nil,function(e,...) +if e then +for t=1,#e do +e[t]=self:node(e[t].node); +end +end +a(e,...) +end); +end +local e={}; +local o={__index=e}; +function a:node(e) +return setmetatable({stream=self.stream,service=self.service,node=e},o) +end +function i:__call(t,e) +local t=self:service(t); +return e and t:node(e)or t; +end +function e:hook(a,o) +self._hooks=self._hooks or setmetatable({},{__mode='kv'}); +local function t(e) +if(not e.service or e.from==self.service)and e.node==self.node then +return a(e) +end +end +self._hooks[a]=t; +self.stream:hook("pubsub/event",t,o); +return t; +end +function e:unhook(e) +if e then +local e=self._hooks[e]; +self.stream:unhook("pubsub/event",e); +elseif self._hooks then +for e in pairs(self._hooks)do +self.stream:unhook("pubsub/event",e); +end +end +end +function e:create(a,e) +if a~=nil then +error("Not implemented yet."); +else +self.stream:send_iq(t("set",self.service,nil,"create",self.node),e); +end +end +function e:configure(e,a) +if e~=nil then +error("Not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,e==nil and"default"or"configure",self.node),a); +end +function e:publish(i,o,e,a) +if o~=nil then +error("Node configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"publish",self.node,nil,i or true) +:add_child(e) +,a); +end +function e:subscribe(e,o,a) +e=e or self.stream.jid; +if o~=nil then +error("Subscription configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"subscribe",self.node,e,id) +,a); +end +function e:subscription(e) +error("Not implemented yet."); +end +function e:affiliation(e) +error("Not implemented yet."); +end +function e:unsubscribe(e,a) +e=e or self.subscribed_jid or self.stream.jid; +self.stream:send_iq(t("set",self.service,nil,"unsubscribe",self.node,e) +,a); +end +function e:configure_subscription(e,e) +error("Not implemented yet."); +end +function e:items(a,e) +if a then +self.stream:send_iq(t("get",self.service,nil,"items",self.node) +,e); +else +self.stream:disco_items(self.service,self.node,e); +end +end +function e:item(e,a) +self.stream:send_iq(t("get",self.service,nil,"items",self.node,nil,e) +,a); +end +function e:retract(a,e) +self.stream:send_iq(t("set",self.service,nil,"retract",self.node,nil,a) +,e); +end +function e:purge(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,n,"purge",self.node) +,a); +end +function e:delete(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,n,"delete",self.node) +,a); +end +end) +package.preload['verse.plugins.pep']=(function(...) +local e=require"verse"; +local t="http://jabber.org/protocol/pubsub"; +local t=t.."#event"; +function e.plugins.pep(e) +e:add_plugin("disco"); +e:add_plugin("pubsub"); +e.pep={}; +e:hook("pubsub/event",function(t) +return e:event("pep/"..t.node,{from=t.from,item=t.item.tags[1]}); +end); +function e:hook_pep(t,o,i) +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:add_disco_feature(t.."+notify"); +end +e:hook("pep/"..t,o,i); +end +function e:unhook_pep(t,a) +e:unhook("pep/"..t,a); +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:remove_disco_feature(t.."+notify"); +end +end +function e:publish_pep(t,a) +return e.pubsub:service(nil):node(a or t.attr.xmlns):publish(nil,nil,t) +end +end +end) +package.preload['verse.plugins.adhoc']=(function(...) +local o=require"verse"; +local n=require"lib.adhoc"; +local t="http://jabber.org/protocol/commands"; +local h="jabber:x:data"; +local a={}; +a.__index=a; +local i={}; +function o.plugins.adhoc(e) +e:add_plugin("disco"); +e:add_disco_feature(t); +function e:query_commands(a,o) +e:disco_items(a,t,function(t) +e:debug("adhoc list returned") +local a={}; +for o,t in ipairs(t)do +a[t.node]=t.name; +end +e:debug("adhoc calling callback") +return o(a); +end); +end +function e:execute_command(t,o,i) +local e=setmetatable({ +stream=e,jid=t, +command=o,callback=i +},a); +return e:execute(); +end +local function s(t,e) +if not(e)or e=="user"then return true;end +if type(e)=="function"then +return e(t); +end +end +function e:add_adhoc_command(o,a,s,h) +i[a]=n.new(o,a,s,h); +e:add_disco_item({jid=e.jid,node=a,name=o},t); +return i[a]; +end +local function h(a) +local t=a.tags[1]; +local t=t.attr.node; +local t=i[t]; +if not t then return;end +if not s(a.attr.from,t.permission)then +e:send(o.error_reply(a,"auth","forbidden","You don't have permission to execute this command"):up() +:add_child(t:cmdtag("canceled") +:tag("note",{type="error"}):text("You don't have permission to execute this command"))); +return true +end +return n.handle_cmd(t,{send=function(t)return e:send(t)end},a); +end +e:hook("iq/"..t,function(e) +local t=e.attr.type; +local a=e.tags[1].name; +if t=="set"and a=="command"then +return h(e); +end +end); +end +function a:_process_response(e) +if e.attr.type=="error"then +self.status="canceled"; +self.callback(self,{}); +return; +end +local e=e:get_child("command",t); +self.status=e.attr.status; +self.sessionid=e.attr.sessionid; +self.form=e:get_child("x",h); +self.note=e:get_child("note"); +self.callback(self); +end +function a:execute() +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{xmlns=t,node=self.command}); +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +function a:next(a) +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{ +xmlns=t, +node=self.command, +sessionid=self.sessionid +}); +if a then e:add_child(a);end +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +end) +package.preload['verse.plugins.presence']=(function(...) +local a=require"verse"; +function a.plugins.presence(e) +e.last_presence=nil; +e:hook("presence-out",function(t) +if not t.attr.to then +e.last_presence=t; +end +end,1); +function e:resend_presence() +if last_presence then +e:send(last_presence); +end +end +function e:set_status(t) +local a=a.presence(); +if type(t)=="table"then +if t.show then +a:tag("show"):text(t.show):up(); +end +if t.prio then +a:tag("priority"):text(tostring(t.prio)):up(); +end +if t.msg then +a:tag("status"):text(t.msg):up(); +end +end +e:send(a); +end +end +end) +package.preload['verse.plugins.private']=(function(...) +local o=require"verse"; +local a="jabber:iq:private"; +function o.plugins.private(i) +function i:private_set(i,n,e,s) +local t=o.iq({type="set"}) +:tag("query",{xmlns=a}); +if e then +if e.name==i and e.attr and e.attr.xmlns==n then +t:add_child(e); +else +t:tag(i,{xmlns=n}) +:add_child(e); +end +end +self:send_iq(t,s); +end +function i:private_get(e,t,i) +self:send_iq(o.iq({type="get"}) +:tag("query",{xmlns=a}) +:tag(e,{xmlns=t}), +function(o) +if o.attr.type=="result"then +local a=o:get_child("query",a); +local e=a:get_child(e,t); +i(e); +end +end); +end +end +end) +package.preload['verse.plugins.roster']=(function(...) +local o=require"verse"; +local r=require"util.jid".bare; +local a="jabber:iq:roster"; +local i="urn:xmpp:features:rosterver"; +local n=table.insert; +function o.plugins.roster(t) +local s=false; +local e={ +items={}; +ver=""; +}; +t.roster=e; +t:hook("stream-features",function(e) +if e:get_child("ver",i)then +s=true; +end +end); +local function h(t) +local e=o.stanza("item",{xmlns=a}); +for a,t in pairs(t)do +if a~="groups"then +e.attr[a]=t; +else +for a=1,#t do +e:tag("group"):text(t[a]):up(); +end +end +end +return e; +end +local function d(e) +local t={}; +local a={}; +t.groups=a; +local o=e.attr.jid; +for e,a in pairs(e.attr)do +if e~="xmlns"then +t[e]=a +end +end +for e in e:childtags("group")do +n(a,e:get_text()) +end +return t; +end +function e:load(t) +e.ver,e.items=t.ver,t.items; +end +function e:dump() +return{ +ver=e.ver, +items=e.items, +}; +end +function e:add_contact(s,i,n,e) +local i={jid=s,name=i,groups=n}; +local a=o.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(i)); +t:send_iq(a,function(t) +if not e then return end +if t.attr.type=="result"then +e(true); +else +local t,a,o=t:get_error(); +e(nil,{t,a,o}); +end +end); +end +function e:delete_contact(i,n) +i=(type(i)=="table"and i.jid)or i; +local s={jid=i,subscription="remove"} +if not e.items[i]then return false,"item-not-found";end +t:send_iq(o.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(s)), +function(e) +if not n then return end +if e.attr.type=="result"then +n(true); +else +local a,t,e=e:get_error(); +n(nil,{a,t,e}); +end +end); +end +local function h(t) +local t=d(t); +e.items[t.jid]=t; +end +local function d(t) +local a=e.items[t]; +e.items[t]=nil; +return a; +end +function e:fetch(i) +t:send_iq(o.iq({type="get"}):tag("query",{xmlns=a,ver=s and e.ver or nil}), +function(t) +if t.attr.type=="result"then +local t=t:get_child("query",a); +if t then +e.items={}; +for t in t:childtags("item")do +h(t) +end +e.ver=t.attr.ver or""; +end +i(e); +else +local t,e,a=stanza:get_error(); +i(nil,{t,e,a}); +end +end); +end +t:hook("iq/"..a,function(i) +local s,n=i.attr.type,i.attr.from; +if s=="set"and(not n or n==r(t.jid))then +local s=i:get_child("query",a); +local n=s and s:get_child("item"); +if n then +local i,a; +local o=n.attr.jid; +if n.attr.subscription=="remove"then +i="removed" +a=d(o); +else +i=e.items[o]and"changed"or"added"; +h(n) +a=e.items[o]; +end +e.ver=s.attr.ver; +if a then +t:event("roster/item-"..i,a); +end +end +t:send(o.reply(i)) +return true; +end +end); +end +end) +package.preload['verse.plugins.register']=(function(...) +local t=require"verse"; +local i="jabber:iq:register"; +function t.plugins.register(e) +local function a(o) +if o:get_child("register","http://jabber.org/features/iq-register")then +local t=t.iq({to=e.host_,type="set"}) +:tag("query",{xmlns=i}) +:tag("username"):text(e.username):up() +:tag("password"):text(e.password):up(); +if e.register_email then +t:tag("email"):text(e.register_email):up(); +end +e:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("registration-success"); +else +local o,t,a=t:get_error(); +e:debug("Registration failed: %s",t); +e:event("registration-failure",{type=o,condition=t,text=a}); +end +end); +else +e:debug("In-band registration not offered by server"); +e:event("registration-failure",{condition="service-unavailable"}); +end +e:unhook("stream-features",a); +return true; +end +e:hook("stream-features",a,310); +end +end) +package.preload['verse.plugins.groupchat']=(function(...) +local i=require"verse"; +local e=require"events"; +local n=require"util.jid"; +local a={}; +a.__index=a; +local h="urn:xmpp:delay"; +local s="http://jabber.org/protocol/muc"; +function i.plugins.groupchat(o) +o:add_plugin("presence") +o.rooms={}; +o:hook("stanza",function(e) +local a=n.bare(e.attr.from); +if not a then return end +local t=o.rooms[a] +if not t and e.attr.to and a then +t=o.rooms[e.attr.to.." "..a] +end +if t and t.opts.source and e.attr.to~=t.opts.source then return end +if t then +local i=select(3,n.split(e.attr.from)); +local n=e:get_child_text("body"); +local o=e:get_child("delay",h); +local a={ +room_jid=a; +room=t; +sender=t.occupants[i]; +nick=i; +body=n; +stanza=e; +delay=(o and o.attr.stamp); +}; +local t=t:event(e.name,a); +return t or(e.name=="message")or nil; +end +end,500); +function o:join_room(n,h,t) +if not h then +return false,"no nickname supplied" +end +t=t or{}; +local e=setmetatable(i.eventable{ +stream=o,jid=n,nick=h, +subject=nil, +occupants={}, +opts=t, +},a); +if t.source then +self.rooms[t.source.." "..n]=e; +else +self.rooms[n]=e; +end +local a=e.occupants; +e:hook("presence",function(o) +local t=o.nick or h; +if not a[t]and o.stanza.attr.type~="unavailable"then +a[t]={ +nick=t; +jid=o.stanza.attr.from; +presence=o.stanza; +}; +local o=o.stanza:get_child("x",s.."#user"); +if o then +local e=o:get_child("item"); +if e and e.attr then +a[t].real_jid=e.attr.jid; +a[t].affiliation=e.attr.affiliation; +a[t].role=e.attr.role; +end +end +if t==e.nick then +e.stream:event("groupchat/joined",e); +else +e:event("occupant-joined",a[t]); +end +elseif a[t]and o.stanza.attr.type=="unavailable"then +if t==e.nick then +e.stream:event("groupchat/left",e); +if e.opts.source then +self.rooms[e.opts.source.." "..n]=nil; +else +self.rooms[n]=nil; +end +else +a[t].presence=o.stanza; +e:event("occupant-left",a[t]); +a[t]=nil; +end +end +end); +e:hook("message",function(a) +local t=a.stanza:get_child_text("subject"); +if not t then return end +t=#t>0 and t or nil; +if t~=e.subject then +local o=e.subject; +e.subject=t; +return e:event("subject-changed",{from=o,to=t,by=a.sender,event=a}); +end +end,2e3); +local t=i.presence():tag("x",{xmlns=s}):reset(); +self:event("pre-groupchat/joining",t); +e:send(t) +self:event("groupchat/joining",e); +return e; +end +o:hook("presence-out",function(e) +if not e.attr.to then +for a,t in pairs(o.rooms)do +t:send(e); +end +e.attr.to=nil; +end +end); +end +function a:send(e) +if e.name=="message"and not e.attr.type then +e.attr.type="groupchat"; +end +if e.name=="presence"then +e.attr.to=self.jid.."/"..self.nick; +end +if e.attr.type=="groupchat"or not e.attr.to then +e.attr.to=self.jid; +end +if self.opts.source then +e.attr.from=self.opts.source +end +self.stream:send(e); +end +function a:send_message(e) +self:send(i.message():tag("body"):text(e)); +end +function a:set_subject(e) +self:send(i.message():tag("subject"):text(e)); +end +function a:leave(t) +self.stream:event("groupchat/leaving",self); +local e=i.presence({type="unavailable"}); +if t then +e:tag("status"):text(t); +end +self:send(e); +end +function a:admin_set(e,t,a,o) +self:send(i.iq({type="set"}) +:query(s.."#admin") +:tag("item",{nick=e,[t]=a}) +:tag("reason"):text(o or"")); +end +function a:set_role(e,t,a) +self:admin_set(e,"role",t,a); +end +function a:set_affiliation(a,t,e) +self:admin_set(a,"affiliation",t,e); +end +function a:kick(e,t) +self:set_role(e,"none",t); +end +function a:ban(t,e) +self:set_affiliation(t,"outcast",e); +end +end) +package.preload['verse.plugins.vcard']=(function(...) +local i=require"verse"; +local o=require"util.vcard"; +local n="vcard-temp"; +function i.plugins.vcard(a) +function a:get_vcard(t,e) +a:send_iq(i.iq({to=t,type="get"}) +:tag("vCard",{xmlns=n}),e and function(t) +local a,a; +vCard=t:get_child("vCard",n); +if t.attr.type=="result"and vCard then +vCard=o.from_xep54(vCard) +e(vCard) +else +e(false) +end +end or nil); +end +function a:set_vcard(e,n) +local t; +if type(e)=="table"and e.name then +t=e; +elseif type(e)=="string"then +t=o.to_xep54(o.from_text(e)[1]); +elseif type(e)=="table"then +t=o.to_xep54(e); +error("Converting a table to vCard not implemented") +end +if not t then return false end +a:debug("setting vcard to %s",tostring(t)); +a:send_iq(i.iq({type="set"}) +:add_child(t),n); +end +end +end) +package.preload['verse.plugins.vcard_update']=(function(...) +local i=require"verse"; +local e,n="vcard-temp","vcard-temp:x:update"; +local e,t=pcall(function()return require("util.hashes").sha1;end); +if not e then +e,t=pcall(function()return require("util.sha1").sha1;end); +if not e then +error("Could not find a sha1()") +end +end +local h=t; +local e,t=pcall(function() +local e=require("util.encodings").base64.decode; +assert(e("SGVsbG8=")=="Hello") +return e; +end); +if not e then +e,t=pcall(function()return require("mime").unb64;end); +if not e then +error("Could not find a base64 decoder") +end +end +local s=t; +function i.plugins.vcard_update(e) +e:add_plugin("vcard"); +e:add_plugin("presence"); +local t; +function update_vcard_photo(a) +local o; +for e=1,#a do +if a[e].name=="PHOTO"then +o=a[e][1]; +break +end +end +if o then +local a=h(s(o),true); +t=i.stanza("x",{xmlns=n}) +:tag("photo"):text(a); +e:resend_presence() +else +t=nil; +end +end +local a=e.set_vcard; +local a; +e:hook("ready",function(t) +if a then return;end +a=true; +e:get_vcard(nil,function(t) +if t then +update_vcard_photo(t) +end +e:event("ready"); +end); +return true; +end,3); +e:hook("presence-out",function(e) +if t and not e:get_child("x",n)then +e:add_child(t); +end +end,10); +end +end) +package.preload['verse.plugins.carbons']=(function(...) +local a=require"verse"; +local o="urn:xmpp:carbons:2"; +local h="urn:xmpp:forward:0"; +local r=os.time; +local s=require"util.datetime".parse; +local n=require"util.jid".bare; +function a.plugins.carbons(e) +local t={}; +t.enabled=false; +e.carbons=t; +function t:enable(i) +e:send_iq(a.iq{type="set"} +:tag("enable",{xmlns=o}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=true; +end +if i then +i(e); +end +end or nil); +end +function t:disable(i) +e:send_iq(a.iq{type="set"} +:tag("disable",{xmlns=o}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=false; +end +if i then +i(e); +end +end or nil); +end +local a; +e:hook("bind-success",function() +a=n(e.jid); +end); +e:hook("message",function(i) +local t=i:get_child(nil,o); +if i.attr.from==a and t then +local o=t.name; +local t=t:get_child("forwarded",h); +local a=t and t:get_child("message","jabber:client"); +local t=t:get_child("delay","urn:xmpp:delay"); +local t=t and t.attr.stamp; +t=t and s(t); +if a then +return e:event("carbon",{ +dir=o, +stanza=a, +timestamp=t or r(), +}); +end +end +end,1); +end +end) +package.preload['verse.plugins.archive']=(function(...) +local e=require"verse"; +local a=require"util.stanza"; +local t="urn:xmpp:mam:0" +local r="urn:xmpp:forward:0"; +local l="urn:xmpp:delay"; +local s=require"util.uuid".generate; +local u=require"util.datetime".parse; +local i=require"util.datetime".datetime; +local o=require"util.dataforms".new; +local d=require"util.rsm"; +local m={}; +local c=o{ +{name="FORM_TYPE";type="hidden";value=t;}; +{name="with";type="jid-single";}; +{name="start";type="text-single"}; +{name="end";type="text-single";}; +}; +function e.plugins.archive(n) +function n:query_archive(o,e,h) +local s=s(); +local n=a.iq{type="set",to=o} +:tag("query",{xmlns=t,queryid=s}); +local o,a=tonumber(e["start"]),tonumber(e["end"]); +e["start"]=o and i(o); +e["end"]=a and i(a); +n:add_child(c:form(e,"submit")); +n:add_child(d.generate(e)); +local a={}; +local function o(i) +local e=i:get_child("fin",t) +if e and e.attr.queryid==s then +local e=d.get(e); +for e,t in pairs(e or m)do a[e]=t;end +self:unhook("message",o); +h(a); +return true +end +local e=i:get_child("result",t); +if e and e.attr.queryid==s then +local t=e:get_child("forwarded",r); +t=t or i:get_child("forwarded",r); +local o=e.attr.id; +local e=t:get_child("delay",l); +local i=e and u(e.attr.stamp)or nil; +local e=t:get_child("message","jabber:client") +a[#a+1]={id=o,stamp=i,message=e}; +return true +end +end +self:hook("message",o,1); +self:send_iq(n,function(e) +if e.attr.type=="error"then +self:warn(table.concat({e:get_error()}," ")) +self:unhook("message",o); +h(false,e:get_error()) +end +return true +end); +end +local i={ +always=true,[true]="always", +never=false,[false]="never", +roster="roster", +} +local function h(t) +local e={}; +local a=t.attr.default; +if a then +e[false]=i[a]; +end +local a=t:get_child("always"); +if a then +for t in a:childtags("jid")do +local t=t:get_text(); +e[t]=true; +end +end +local t=t:get_child("never"); +if t then +for t in t:childtags("jid")do +local t=t:get_text(); +e[t]=false; +end +end +return e; +end +local function s(o) +local e +e,o[false]=o[false],nil; +if e~=nil then +e=i[e]; +end +local i=a.stanza("prefs",{xmlns=t,default=e}) +local t=a.stanza("always"); +local e=a.stanza("never"); +for o,a in pairs(o)do +(a and t or e):tag("jid"):text(o):up(); +end +return i:add_child(t):add_child(e); +end +function n:archive_prefs_get(o) +self:send_iq(a.iq{type="get"}:tag("prefs",{xmlns=t}), +function(e) +if e and e.attr.type=="result"and e.tags[1]then +local t=h(e.tags[1]); +o(t,e); +else +o(nil,e); +end +end); +end +function n:archive_prefs_set(t,e) +self:send_iq(a.iq{type="set"}:add_child(s(t)),e); +end +end +end) +package.preload['util.http']=(function(...) +local t,s=string.format,string.char; +local i,n,h=pairs,ipairs,tonumber; +local o,r=table.insert,table.concat; +local function d(e) +return e and(e:gsub("[^a-zA-Z0-9.~_-]",function(e)return t("%%%02x",e:byte());end)); +end +local function a(e) +return e and(e:gsub("%%(%x%x)",function(e)return s(h(e,16));end)); +end +local function e(e) +return e and(e:gsub("%W",function(e) +if e~=" "then +return t("%%%02x",e:byte()); +else +return"+"; +end +end)); +end +local function s(t) +local a={}; +if t[1]then +for i,t in n(t)do +o(a,e(t.name).."="..e(t.value)); +end +else +for t,i in i(t)do +o(a,e(t).."="..e(i)); +end +end +return r(a,"&"); +end +local function n(e) +if not e:match("=")then return a(e);end +local i={}; +for e,t in e:gmatch("([^=&]*)=([^&]*)")do +e,t=e:gsub("%+","%%20"),t:gsub("%+","%%20"); +e,t=a(e),a(t); +o(i,{name=e,value=t}); +i[e]=t; +end +return i; +end +local function o(e,t) +e=","..e:gsub("[ \t]",""):lower()..","; +return e:find(","..t:lower()..",",1,true)~=nil; +end +return{ +urlencode=d,urldecode=a; +formencode=s,formdecode=n; +contains_token=o; +}; +end) +package.preload['net.http.parser']=(function(...) +local w=tonumber; +local a=assert; +local b=require"socket.url".parse; +local t=require"util.http".urldecode; +local function g(e) +e=t((e:gsub("//+","/"))); +if e:sub(1,1)~="/"then +e="/"..e; +end +local t=0; +for e in e:gmatch("([^/]+)/")do +if e==".."then +t=t-1; +elseif e~="."then +t=t+1; +end +if t<0 then +return nil; +end +end +return e; +end +local v={}; +function v.new(u,s,e,y) +local d=true; +if not e or e=="server"then d=false;else a(e=="client","Invalid parser type");end +local e=""; +local p,a,r; +local h=nil; +local t; +local o; +local c; +local n; +return{ +feed=function(l,i) +if n then return nil,"parse has failed";end +if not i then +if h and d and not o then +t.body=e; +u(t); +elseif e~=""then +n=true;return s(); +end +return; +end +e=e..i; +while#e>0 do +if h==nil then +local f=e:find("\r\n\r\n",nil,true); +if not f then return;end +local m,r,l,i,v; +local u; +local a={}; +for t in e:sub(1,f+1):gmatch("([^\r\n]+)\r\n")do +if u then +local e,t=t:match("^([^%s:]+): *(.*)$"); +if not e then n=true;return s("invalid-header-line");end +e=e:lower(); +a[e]=a[e]and a[e]..","..t or t; +else +u=t; +if d then +l,i,v=t:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); +i=w(i); +if not i then n=true;return s("invalid-status-line");end +c=not +((y and y().method=="HEAD") +or(i==204 or i==304 or i==301) +or(i>=100 and i<200)); +else +m,r,l=t:match("^(%w+) (%S+) HTTP/(1%.[01])$"); +if not m then n=true;return s("invalid-status-line");end +end +end +end +if not u then n=true;return s("invalid-status-line");end +p=c and a["transfer-encoding"]=="chunked"; +o=w(a["content-length"]); +if d then +if not c then o=0;end +t={ +code=i; +httpversion=l; +headers=a; +body=c and""or nil; +responseversion=l; +responseheaders=a; +}; +else +local e; +if r:byte()==47 then +local a,t=r:match("([^?]*).?(.*)"); +if t==""then t=nil;end +e={path=a,query=t}; +else +e=b(r); +if not(e and e.path)then n=true;return s("invalid-url");end +end +r=g(e.path); +a.host=e.host or a.host; +o=o or 0; +t={ +method=m; +url=e; +path=r; +httpversion=l; +headers=a; +body=nil; +}; +end +e=e:sub(f+4); +h=true; +end +if h then +if d then +if p then +if not e:find("\r\n",nil,true)then +return; +end +if not a then +a,r=e:match("^(%x+)[^\r\n]*\r\n()"); +a=a and w(a,16); +if not a then n=true;return s("invalid-chunk-size");end +end +if a==0 and e:find("\r\n\r\n",r-2,true)then +h,a=nil,nil; +e=e:gsub("^.-\r\n\r\n",""); +u(t); +elseif#e-r-2>=a then +t.body=t.body..e:sub(r,r+(a-1)); +e=e:sub(r+a+2); +a,r=nil,nil; +else +break; +end +elseif o and#e>=o then +if t.code==101 then +t.body,e=e,""; +else +t.body,e=e:sub(1,o),e:sub(o+1); +end +h=nil;u(t); +else +break; +end +elseif#e>=o then +t.body,e=e:sub(1,o),e:sub(o+1); +h=nil;u(t); +else +break; +end +end +end +end; +}; +end +return v; +end) +package.preload['net.http']=(function(...) +local x=require"socket" +local j=require"util.encodings".base64.encode; +local c=require"socket.url" +local d=require"net.http.parser".new; +local h=require"util.http"; +local w=pcall(require,"ssl"); +local f=require"net.server" +local r,i=table.insert,table.concat; +local m=pairs; +local y,u,b,p,s= +tonumber,tostring,xpcall,select,debug.traceback; +local g,v=assert,error +local l=require"util.logger".init("http"); +module"http" +local o={}; +local n={default_port=80,default_mode="*a"}; +function n.onconnect(t) +local e=o[t]; +local a={e.method or"GET"," ",e.path," HTTP/1.1\r\n"}; +if e.query then +r(a,4,"?"..e.query); +end +t:write(i(a)); +local a={[2]=": ",[4]="\r\n"}; +for o,e in m(e.headers)do +a[1],a[3]=o,e; +t:write(i(a)); +end +t:write("\r\n"); +if e.body then +t:write(e.body); +end +end +function n.onincoming(t,a) +local e=o[t]; +if not e then +l("warn","Received response from connection %s with no request attached!",u(t)); +return; +end +if a and e.reader then +e:reader(a); +end +end +function n.ondisconnect(t,a) +local e=o[t]; +if e and e.conn then +e:reader(nil,a); +end +o[t]=nil; +end +function n.ondetach(e) +o[e]=nil; +end +local function q(e,a,i) +if not e.parser then +local function o(t) +if e.callback then +e.callback(t or"connection-closed",0,e); +e.callback=nil; +end +destroy_request(e); +end +if not a then +o(i); +return; +end +local function a(t) +if e.callback then +e.callback(t.body,t.code,t,e); +e.callback=nil; +end +destroy_request(e); +end +local function t() +return e; +end +e.parser=d(a,o,"client",t); +end +e.parser:feed(a); +end +local function k(e)l("error","Traceback[http]: %s",s(u(e),2));end +function request(e,t,d) +local e=c.parse(e); +if not(e and e.host)then +d(nil,0,e); +return nil,"invalid-url"; +end +if not e.path then +e.path="/"; +end +local r,i,s; +local c,a=e.host,e.port; +local h=c; +if(a=="80"and e.scheme=="http") +or(a=="443"and e.scheme=="https")then +a=nil; +elseif a then +h=h..":"..a; +end +i={ +["Host"]=h; +["User-Agent"]="Prosody XMPP Server"; +}; +if e.userinfo then +i["Authorization"]="Basic "..j(e.userinfo); +end +if t then +e.onlystatus=t.onlystatus; +s=t.body; +if s then +r="POST"; +i["Content-Length"]=u(#s); +i["Content-Type"]="application/x-www-form-urlencoded"; +end +if t.method then r=t.method;end +if t.headers then +for e,t in m(t.headers)do +i[e]=t; +end +end +end +e.method,e.headers,e.body=r,i,s; +local i=e.scheme=="https"; +if i and not w then +v("SSL not available, unable to contact https URL"); +end +local h=a and y(a)or(i and 443 or 80); +local a=x.tcp(); +a:settimeout(10); +local r,s=a:connect(c,h); +if not r and s~="timeout"then +d(nil,0,e); +return nil,s; +end +local s=false; +if i then +s=t and t.sslctx or{mode="client",protocol="sslv23",options={"no_sslv2","no_sslv3"}}; +end +e.handler,e.conn=g(f.wrapclient(a,c,h,n,"*a",s)); +e.write=function(...)return e.handler:write(...);end +e.callback=function(o,t,i,a)l("debug","Calling callback, status %s",t or"---");return p(2,b(function()return d(o,t,i,a)end,k));end +e.reader=q; +e.state="status"; +o[e.handler]=e; +return e; +end +function destroy_request(e) +if e.conn then +e.conn=nil; +e.handler:close() +end +end +local t,o=h.urlencode,h.urldecode; +local a,e=h.formencode,h.formdecode; +_M.urlencode,_M.urldecode=t,o; +_M.formencode,_M.formdecode=a,e; +return _M; +end) +package.preload['verse.bosh']=(function(...) +local n=require"util.xmppstream".new; +local i=require"util.stanza"; +require"net.httpclient_listener"; +local o=require"net.http"; +local e=setmetatable({},{__index=verse.stream_mt}); +e.__index=e; +local s="http://etherx.jabber.org/streams"; +local h="http://jabber.org/protocol/httpbind"; +local a=5; +function verse.new_bosh(a,t) +local t={ +bosh_conn_pool={}; +bosh_waiting_requests={}; +bosh_rid=math.random(1,999999); +bosh_outgoing_buffer={}; +bosh_url=t; +conn={}; +}; +function t:reopen() +self.bosh_need_restart=true; +self:flush(); +end +local t=verse.new(a,t); +return setmetatable(t,e); +end +function e:connect() +self:_send_session_request(); +end +function e:send(e) +self:debug("Putting into BOSH send buffer: %s",tostring(e)); +self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1]=i.clone(e); +self:flush(); +end +function e:flush() +if self.connected +and#self.bosh_waiting_requests0 +or self.bosh_need_restart)then +self:debug("Flushing..."); +local t=self:_make_body(); +local e=self.bosh_outgoing_buffer; +for a,o in ipairs(e)do +t:add_child(o); +e[a]=nil; +end +self:_make_request(t); +else +self:debug("Decided not to flush."); +end +end +function e:_make_request(i) +local e,t=o.request(self.bosh_url,{body=tostring(i)},function(o,e,t) +if e~=0 then +self.inactive_since=nil; +return self:_handle_response(o,e,t); +end +local e=os.time(); +if not self.inactive_since then +self.inactive_since=e; +elseif e-self.inactive_since>self.bosh_max_inactivity then +return self:_disconnected(); +else +self:debug("%d seconds left to reconnect, retrying in %d seconds...", +self.bosh_max_inactivity-(e-self.inactive_since),a); +end +timer.add_task(a,function() +self:debug("Retrying request..."); +for e,a in ipairs(self.bosh_waiting_requests)do +if a==t then +table.remove(self.bosh_waiting_requests,e); +break; +end +end +self:_make_request(i); +end); +end); +if e then +table.insert(self.bosh_waiting_requests,e); +else +self:warn("Request failed instantly: %s",t); +end +end +function e:_disconnected() +self.connected=nil; +self:event("disconnected"); +end +function e:_send_session_request() +local e=self:_make_body(); +e.attr.hold="1"; +e.attr.wait="60"; +e.attr["xml:lang"]="en"; +e.attr.ver="1.6"; +e.attr.from=self.jid; +e.attr.to=self.host; +e.attr.secure='true'; +o.request(self.bosh_url,{body=tostring(e)},function(e,t) +if t==0 then +return self:_disconnected(); +end +local e=self:_parse_response(e) +if not e then +self:warn("Invalid session creation response"); +self:_disconnected(); +return; +end +self.bosh_sid=e.attr.sid; +self.bosh_wait=tonumber(e.attr.wait); +self.bosh_hold=tonumber(e.attr.hold); +self.bosh_max_inactivity=tonumber(e.attr.inactivity); +self.bosh_max_requests=tonumber(e.attr.requests)or self.bosh_hold; +self.connected=true; +self:event("connected"); +self:_handle_response_payload(e); +end); +end +function e:_handle_response(t,a,e) +if self.bosh_waiting_requests[1]~=e then +self:warn("Server replied to request that wasn't the oldest"); +for a,t in ipairs(self.bosh_waiting_requests)do +if t==e then +self.bosh_waiting_requests[a]=nil; +break; +end +end +else +table.remove(self.bosh_waiting_requests,1); +end +local e=self:_parse_response(t); +if e then +self:_handle_response_payload(e); +end +self:flush(); +end +function e:_handle_response_payload(t) +local e=t.tags; +for t=1,#e do +local e=e[t]; +if e.attr.xmlns==s then +self:event("stream-"..e.name,e); +elseif e.attr.xmlns then +self:event("stream/"..e.attr.xmlns,e); +else +self:event("stanza",e); +end +end +if t.attr.type=="terminate"then +self:_disconnected({reason=t.attr.condition}); +end +end +local a={ +stream_ns="http://jabber.org/protocol/httpbind",stream_tag="body", +default_ns="jabber:client", +streamopened=function(e,t)e.notopen=nil;e.payload=verse.stanza("body",t);return true;end; +handlestanza=function(e,t)e.payload:add_child(t);end; +}; +function e:_parse_response(e) +self:debug("Parsing response: %s",e); +if e==nil then +self:debug("%s",debug.traceback()); +self:_disconnected(); +return; +end +local t={notopen=true,stream=self}; +local a=n(t,a); +a:feed(e); +return t.payload; +end +function e:_make_body() +self.bosh_rid=self.bosh_rid+1; +local e=verse.stanza("body",{ +xmlns=h; +content="text/xml; charset=utf-8"; +sid=self.bosh_sid; +rid=self.bosh_rid; +}); +if self.bosh_need_restart then +self.bosh_need_restart=nil; +e.attr.restart='true'; +end +return e; +end +end) +package.preload['verse.client']=(function(...) +local t=require"verse"; +local i=t.stream_mt; +local s=require"util.jid".split; +local r=require"net.adns"; +local e=require"lxp"; +local a=require"util.stanza"; +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local h=require"util.xmppstream".new; +local n="http://etherx.jabber.org/streams"; +local function d(t,e) +return t.prioritye.weight); +end +local o={ +stream_ns=n, +stream_tag="stream", +default_ns="jabber:client"}; +function o.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function o.streamclosed(e) +e.notopen=true; +if not e.closed then +e:send(""); +e.closed=true; +end +e:event("closed"); +return e:close("stream closed") +end +function o.handlestanza(t,e) +if e.attr.xmlns==n then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns then +return t:event("stream/"..e.attr.xmlns,e); +end +return t:event("stanza",e); +end +function o.error(a,t,e) +if a:event(t,e)==nil then +if e then +local t=e:get_child(nil,"urn:ietf:params:xml:ns:xmpp-streams"); +local e=e:get_child_text("text","urn:ietf:params:xml:ns:xmpp-streams"); +error(t.name..(e and": "..e or"")); +else +error(e and e.name or t or"unknown-error"); +end +end +end +function i:reset() +if self.stream then +self.stream:reset(); +else +self.stream=h(self,o); +end +self.notopen=true; +return true; +end +function i:connect_client(e,a) +self.jid,self.password=e,a; +self.username,self.host,self.resource=s(e); +self:add_plugin("tls"); +self:add_plugin("sasl"); +self:add_plugin("bind"); +self:add_plugin("session"); +function self.data(t,e) +local t,a=self.stream:feed(e); +if t then return;end +self:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(a),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +self:close("xml-not-well-formed"); +end +self:hook("connected",function()self:reopen();end); +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(e) +local t,a=e.attr.id,e.attr.type; +if t and e.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[t]then +self.tracked_iqs[t](e); +self.tracked_iqs[t]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local a; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local o=e.tags[1]and e.tags[1].attr.xmlns; +if o then +a=self:event("iq/"..o,e); +if not a then +a=self:event("iq",e); +end +end +if a==nil then +self:send(t.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +a=self:event(e.name,e); +end +end +return a; +end,-1); +self:hook("outgoing",function(e) +if e.name then +self:event("stanza-out",e); +end +end); +self:hook("stanza-out",function(e) +if not e.attr.xmlns then +self:event(e.name.."-out",e); +end +end); +local function e() +self:event("ready"); +end +self:hook("session-success",e,-1) +self:hook("bind-success",e,-1); +local e=self.close; +function self:close(t) +self.close=e; +if not self.closed then +self:send(""); +self.closed=true; +else +return self:close(t); +end +end +local function t() +self:connect(self.connect_host or self.host,self.connect_port or 5222); +end +if not(self.connect_host or self.connect_port)then +r.lookup(function(a) +if a then +local e={}; +self.srv_hosts=e; +for a,t in ipairs(a)do +table.insert(e,t.srv); +end +table.sort(e,d); +local a=e[1]; +self.srv_choice=1; +if a then +self.connect_host,self.connect_port=a.target,a.port; +self:debug("Best record found, will connect to %s:%d",self.connect_host or self.host,self.connect_port or 5222); +end +self:hook("disconnected",function() +if self.srv_hosts and self.srv_choice<#self.srv_hosts then +self.srv_choice=self.srv_choice+1; +local e=e[self.srv_choice]; +self.connect_host,self.connect_port=e.target,e.port; +t(); +return true; +end +end,1e3); +self:hook("connected",function() +self.srv_hosts=nil; +end,1e3); +end +t(); +end,"_xmpp-client._tcp."..(self.host)..".","SRV"); +else +t(); +end +end +function i:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.host,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns="jabber:client",version="1.0"}):top_tag()); +end +function i:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function i:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +package.preload['verse.component']=(function(...) +local o=require"verse"; +local a=o.stream_mt; +local h=require"util.jid".split; +local e=require"lxp"; +local t=require"util.stanza"; +local d=require"util.sha1".sha1; +o.message,o.presence,o.iq,o.stanza,o.reply,o.error_reply= +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply; +local r=require"util.xmppstream".new; +local s="http://etherx.jabber.org/streams"; +local i="jabber:component:accept"; +local n={ +stream_ns=s, +stream_tag="stream", +default_ns=i}; +function n.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function n.streamclosed(e) +return e:event("closed"); +end +function n.handlestanza(t,e) +if e.attr.xmlns==s then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns or e.name=="handshake"then +return t:event("stream/"..(e.attr.xmlns or i),e); +end +return t:event("stanza",e); +end +function a:reset() +if self.stream then +self.stream:reset(); +else +self.stream=r(self,n); +end +self.notopen=true; +return true; +end +function a:connect_component(e,n) +self.jid,self.password=e,n; +self.username,self.host,self.resource=h(e); +function self.data(t,e) +local o,t=self.stream:feed(e); +if o then return;end +a:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(t),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +a:close("xml-not-well-formed"); +end +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local t; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local a=e.tags[1]and e.tags[1].attr.xmlns; +if a then +t=self:event("iq/"..a,e); +if not t then +t=self:event("iq",e); +end +end +if t==nil then +self:send(o.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +t=self:event(e.name,e); +end +end +return t; +end,-1); +self:hook("opened",function(e) +print(self.jid,self.stream_id,e.id); +local e=d(self.stream_id..n,true); +self:send(t.stanza("handshake",{xmlns=i}):text(e)); +self:hook("stream/"..i,function(e) +if e.name=="handshake"then +self:event("authentication-success"); +end +end); +end); +local function e() +self:event("ready"); +end +self:hook("authentication-success",e,-1); +self:connect(self.connect_host or self.host,self.connect_port or 5347); +self:reopen(); +end +function a:reopen() +self:reset(); +self:send(t.stanza("stream:stream",{to=self.jid,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns=i,version="1.0"}):top_tag()); +end +function a:close(t) +if not self.notopen then +self:send(""); +end +local e=self.conn.disconnect(); +self.conn:close(); +e(conn,t); +end +function a:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function a:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +pcall(require,"luarocks.require"); +local s=require"socket"; +pcall(require,"ssl"); +local a=require"net.server"; +local n=require"util.events"; +local o=require"util.logger"; +module("verse",package.seeall); +local e=_M; +_M.server=a; +local t={}; +t.__index=t; +stream_mt=t; +e.plugins={}; +function e.init(...) +for e=1,select("#",...)do +local t=pcall(require,"verse."..select(e,...)); +if not t then +error("Verse connection module not found: verse."..select(e,...)); +end +end +return e; +end +local i=0; +function e.new(a,o) +local t=setmetatable(o or{},t); +i=i+1; +t.id=tostring(i); +t.logger=a or e.new_logger("stream"..t.id); +t.events=n.new(); +t.plugins={}; +t.verse=e; +return t; +end +e.add_task=require"util.timer".add_task; +e.logger=o.init; +e.new_logger=o.init; +e.log=e.logger("verse"); +local function h(o,...) +local e,t,a=0,{...},select('#',...); +return(o:gsub("%%(.)",function(o)if e<=a then e=e+1;return tostring(t[e]);end end)); +end +function e.set_log_handler(e,t) +t=t or{"debug","info","warn","error"}; +o.reset(); +if io.type(e)=="file"then +local o=e; +function e(a,t,e) +o:write(a,"\t",t,"\t",e,"\n"); +end +end +if e then +local function i(a,t,o,...) +return e(a,t,h(o,...)); +end +for t,e in ipairs(t)do +o.add_level_sink(e,i); +end +end +end +function _default_log_handler(a,t,o) +return io.stderr:write(a,"\t",t,"\t",o,"\n"); +end +e.set_log_handler(_default_log_handler,{"error"}); +local function o(t) +e.log("error","Error: %s",t); +e.log("error","Traceback: %s",debug.traceback()); +end +function e.set_error_handler(e) +o=e; +end +function e.loop() +return xpcall(a.loop,o); +end +function e.step() +return xpcall(a.step,o); +end +function e.quit() +return a.setquitting(true); +end +function t:listen(e,t) +e=e or"localhost"; +t=t or 0; +local a,o=a.addserver(e,t,new_listener(self,"server"),"*a"); +if a then +self:debug("Bound to %s:%s",e,t); +self.server=a; +end +return a,o; +end +function t:connect(t,o) +t=t or"localhost"; +o=tonumber(o)or 5222; +local i=s.tcp() +i:settimeout(0); +local n,e=i:connect(t,o); +if not n and e~="timeout"then +self:warn("connect() to %s:%d failed: %s",t,o,e); +return self:event("disconnected",{reason=e})or false,e; +end +local t=a.wrapclient(i,t,o,new_listener(self),"*a"); +if not t then +self:warn("connection initialisation failed: %s",e); +return self:event("disconnected",{reason=e})or false,e; +end +self:set_conn(t); +return true; +end +function t:set_conn(t) +self.conn=t; +self.send=function(a,e) +self:event("outgoing",e); +e=tostring(e); +self:event("outgoing-raw",e); +return t:write(e); +end; +end +function t:close(t) +if not self.conn then +e.log("error","Attempt to close disconnected connection - possibly a bug"); +return; +end +local e=self.conn.disconnect(); +self.conn:close(); +e(self.conn,t); +end +function t:debug(...) +return self.logger("debug",...); +end +function t:info(...) +return self.logger("info",...); +end +function t:warn(...) +return self.logger("warn",...); +end +function t:error(...) +return self.logger("error",...); +end +function t:event(e,...) +self:debug("Firing event: "..tostring(e)); +return self.events.fire_event(e,...); +end +function t:hook(e,...) +return self.events.add_handler(e,...); +end +function t:unhook(e,t) +return self.events.remove_handler(e,t); +end +function e.eventable(e) +e.events=n.new(); +e.hook,e.unhook=t.hook,t.unhook; +local t=e.events.fire_event; +function e:event(e,...) +return t(e,...); +end +return e; +end +function t:add_plugin(t) +if self.plugins[t]then return true;end +if require("verse.plugins."..t)then +local e,a=e.plugins[t](self); +if e~=false then +self:debug("Loaded %s plugin",t); +self.plugins[t]=true; +else +self:warn("Failed to load %s plugin: %s",t,a); +end +end +return self; +end +function new_listener(t) +local a={}; +function a.onconnect(a) +if t.server then +local e=e.new(); +a:setlistener(new_listener(e)); +e:set_conn(a); +t:event("connected",{client=e}); +else +t.connected=true; +t:event("connected"); +end +end +function a.onincoming(a,e) +t:event("incoming-raw",e); +end +function a.ondisconnect(e,a) +if e~=t.conn then return end +t.connected=false; +t:event("disconnected",{reason=a}); +end +function a.ondrain(e) +t:event("drained"); +end +function a.onstatus(a,e) +t:event("status",e); +end +return a; +end +return e; +end) +local e="jabber:client" +local e=require"net.server"; +local e=require"core.portmanager"; +local h,t,e,d= +module.host,nil,module:get_option_string("conference_server"),module:get_option_number("listener_port",7e3); +if not e then +module:log("error","You need to set the MUC server in the configuration (conference_server)!") +module:log("error","Be a good boy or girl and go read the wiki at: http://code.google.com/p/prosody-modules/wiki/mod_ircd") +return false; +end +package.loaded["util.sha1"]=require"util.encodings"; +require"verse".init("component"); +c=verse.new(); +c:add_plugin("groupchat"); +local function t(e) +return c:event("stanza",e.stanza)or true; +end +module:hook("message/bare",t); +module:hook("message/full",t); +module:hook("presence/bare",t); +module:hook("presence/full",t); +c.type="component"; +c.send=core_post_stanza; +local s=require"util.jid"; +local w=require"util.encodings".stringprep.nodeprep; +local function u(o) +local i,r=table.insert,table.concat; +local a,t={},1; +if not(o and#o>0)then +return"" +end +while true do +local n=o:sub(t,t) +local e=n:byte(); +local s=( +(e>=9 and e<=10 and 0)or +(e>=32 and e<=126 and 0)or +(e>=192 and e<=223 and 1)or +(e>=224 and e<=239 and 2)or +(e>=240 and e<=247 and 3)or +(e>=248 and e<=251 and 4)or +(e>=251 and e<=252 and 5)or nil +) +if not s then +i(a,"?") +else +local h=t+s; +if s==0 then +i(a,n); +elseif h>#o then +i(a,("?"):format(e)); +else +local o=o:sub(t+1,h); +if o:match('^[\128-\191]*$')then +i(a,n); +i(a,o); +t=h; +else +i(a,("?"):format(e)); +end +end +end +t=t+1; +if t>#o then +break +end +end +return r(a); +end +local function r(t) +local e={}; +if t:sub(1,1)==":"then +e.from,t=t:match("^:(%w+)%s+(.*)$"); +end +for a in t:gmatch("%S+")do +if a:sub(1,1)==":"then +e[#e+1]=t:match(":(.*)$"); +break +end +e[#e+1]=a; +end +return e; +end +local function m(e) +if#e>1 then +e[#e]=":"..e[#e]; +end +return(e.from and":"..e.from.." "or"")..table.concat(e," "); +end +local function b(t,a) +local t=t and w(t:match("^#(%w+)"))or nil; +if not a then +return s.join(t,e); +else +return s.join(t,e,a); +end +end +local function t(e) +local e,a,t=s.split(e); +return"#"..e,t; +end +local t={ +moderator="@", +participant="", +visitor="", +none="" +} +local t={ +owner="~", +administrator="&", +member="+", +none="" +} +local k={ +moderator="o", +participant="", +visitor="", +none="" +} +local g={ +owner="q", +administrator="a", +member="v", +none="" +} +local f={default_port=d,default_mode="*l";interface="192.168.1.3"}; +local d=module:shared("sessions"); +local i={}; +local n={}; +local o={}; +local l={}; +local p=require"util.stanza"; +local t=e; +local function a(e) +e.conn:close(); +end +function f.onincoming(o,i) +local t=d[o]; +if not t then +t={conn=o,host=h,reset_stream=function()end, +close=a,log=logger.init("irc"..(o.id or"1")), +rooms={},roster={},has_un=false}; +d[o]=t; +function t.data(a) +local o=r(a); +module:log("debug",require"util.serialization".serialize(o)); +local a=table.remove(o,1); +if not a then +return; +end +a=a:upper(); +if not t.username and not t.nick then +if not(a=="USER"or a=="NICK")then +module:log("debug","Client tried to send command %s before registering",a); +return t.send{from=e,"451",a,"You have not completed the registration."} +end +end +if n[a]then +local e=n[a](t,o); +if e then +return t.send(e); +end +else +t.send{from=e,"421",t.nick,a,"Unknown command"}; +return module:log("debug","Unknown command: %s",a); +end +end +function t.send(e) +if type(e)=="string"then +return o:write(e.."\r\n"); +elseif type(e)=="table"then +local e=m(e); +module:log("debug",e); +o:write(e.."\r\n"); +end +end +end +if i then +t.data(i); +end +end +function f.ondisconnect(t,e) +local e=d[t]; +if e then +for t,e in pairs(e.rooms)do +e:leave("Disconnected"); +end +if e.nick then +o[e.nick]=nil; +end +if e.full_jid then +i[e.full_jid]=nil; +end +if e.username then +l[e.username]=nil; +end +end +d[t]=nil; +end +local function r(e) +if o[e]then return true else return false end +end +local function v(t) +local e=0; +local a; +for i,o in pairs(l)do +if t==o then e=e+1;end +end +a=e+1; +if e>0 then return tostring(t)..tostring(a);else return tostring(t);end +end +local function m(e,t) +e.full_jid=t; +i[t]=e; +i[t]["ar_last"]={}; +i[t]["nicks_changing"]={}; +if e.nick then o[e.nick]=e;end +end +local function y(t) +local a=t.nick; +if t.username and t.nick then +t.send{from=e,"001",a,"Welcome in the IRC to MUC XMPP Gateway, "..a}; +t.send{from=e,"002",a,"Your host is "..e.." running Prosody "..prosody.version}; +t.send{from=e,"003",a,"This server was created the "..os.date(nil,prosody.start_time)} +t.send{from=e,"004",a,table.concat({e,"mod_ircd(alpha-0.8)","i","aoqv"}," ")}; +t.send((":%s %s %s %s :%s"):format(e,"005",a,"CHANTYPES=# PREFIX=(qaov)~&@+","are supported by this server")); +t.send((":%s %s %s %s :%s"):format(e,"005",a,"STATUSMSG=~&@+","are supported by this server")); +t.send{from=e,"375",a,"- "..e.." Message of the day -"}; +t.send{from=e,"372",a,"-"}; +t.send{from=e,"372",a,"- Please be warned that this is only a partial irc implementation,"}; +t.send{from=e,"372",a,"- it's made to facilitate users transiting away from irc to XMPP."}; +t.send{from=e,"372",a,"-"}; +t.send{from=e,"372",a,"- Prosody is _NOT_ an IRC Server and it never will."}; +t.send{from=e,"372",a,"- We also would like to remind you that this plugin is provided as is,"}; +t.send{from=e,"372",a,"- it's still an Alpha and it's still a work in progress, use it at your sole"}; +t.send{from=e,"372",a,"- risk as there's a not so little chance something will break."}; +t.send{from=a,"MODE",a,"+i"}; +end +end +function n.NICK(t,a) +local a=a[1]; +a=a:gsub("[^%w_]",""); +if t.nick and not r(a)then +local e=t.nick; +t.nick=a; +o[e]=nil; +o[a]=t; +t.send{from=e.."!"..o[a].username,"NICK",a}; +if t.rooms then +t.nicks_changing[a]={e,t.username}; +for o,e in pairs(t.rooms)do e:change_nick(a);end +t.nicks_changing[a]=nil; +end +return; +elseif r(a)then +t.send{from=e,"433",a,"The nickname "..a.." is already in use"};return; +end +t.nick=a; +t.type="c2s"; +o[a]=t; +if t.username then +m(t,s.join(t.username,h,"ircd")); +end +y(t); +end +function n.USER(t,a) +local a=a[1]; +if not t.has_un then +local e=v(a); +l[e]=a; +t.username=e; +t.has_un=true; +if not t.full_jid then +m(t,s.join(t.username,h,"ircd")); +end +else +return t.send{from=e,"462","USER","You may not re-register."} +end +y(t); +end +function n.USERHOST(a,t) +local t=t[1]; +if not t then a.send{from=e,"461","USERHOST","Not enough parameters"};return;end +if o[t]and o[t].nick and o[t].username then +a.send{from=e,"302",a.nick,t.."=+"..o[t].username};return; +else +return; +end +end +local function m(a,o,i) +local t; +local e; +e=g[a]..k[o] +t=string.rep(i.." ",e:len()) +if e==""then return nil,nil end +return e,t +end +function n.JOIN(a,t) +local s=t[1]; +if not s then return end +local t=b(s); +if not i[a.full_jid].ar_last[t]then i[a.full_jid].ar_last[t]={};end +local t,h=c:join_room(t,a.nick,{source=a.full_jid}); +if not t then +return":"..e.." ERR :Could not join room: "..h +end +a.rooms[s]=t; +t.session=a; +if a.nicks_changing[a.nick]then +n.NAMES(a,s); +else +a.send{from=a.nick.."!"..a.username,"JOIN",s}; +if t.subject then +a.send{from=e,332,a.nick,s,t.subject}; +end +n.NAMES(a,s); +end +t:hook("subject-changed",function(e) +a.send{from=e.by.nick,"TOPIC",s,e.to or""} +end); +t:hook("message",function(e) +if not e.body then return end +local o,t=e.nick,e.body; +if o~=a.nick then +if t:sub(1,4)=="/me "then +t="\1ACTION "..t:sub(5).."\1" +end +local e=e.stanza.attr.type; +a.send{from=o,"PRIVMSG",e=="groupchat"and s or o,t}; +end +end); +t:hook("presence",function(t) +local h; +local r; +if t.nick and not i[a.full_jid].ar_last[t.room_jid][t.nick]then i[a.full_jid].ar_last[t.room_jid][t.nick]={}end +local n=t.stanza:get_child("x","http://jabber.org/protocol/muc#user") +if n then +local n=n:get_child("item") +if n and n.attr and t.stanza.attr.type~="unavailable"then +if n.attr.affiliation and n.attr.role then +if not i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]and +not i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]then +i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]=n.attr.affiliation +i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]=n.attr.role +n_self_changing=o[t.nick]and o[t.nick].nicks_changing and o[t.nick].nicks_changing[t.nick] +if n_self_changing then return;end +h,r=m(n.attr.affiliation,n.attr.role,t.nick); +if h and r then a.send((":%s MODE %s +%s"):format(e,s,h.." "..r));end +else +h,r=m(i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"],i[a.full_jid].ar_last[t.room_jid][t.nick]["role"],t.nick); +if h and r then a.send((":%s MODE %s -%s"):format(e,s,h.." "..r));end +i[a.full_jid].ar_last[t.room_jid][t.nick]["affiliation"]=n.attr.affiliation +i[a.full_jid].ar_last[t.room_jid][t.nick]["role"]=n.attr.role +n_self_changing=o[t.nick]and o[t.nick].nicks_changing and o[t.nick].nicks_changing[t.nick] +if n_self_changing then return;end +h,r=m(n.attr.affiliation,n.attr.role,t.nick); +if h and r then a.send((":%s MODE %s +%s"):format(e,s,h.." "..r));end +end +end +end +end +end,-1); +end +c:hook("groupchat/joined",function(a) +local t=a.session or i[a.opts.source]; +local n="#"..a.jid:match("^(.*)@"); +a:hook("occupant-joined",function(e) +if t.nicks_changing[e.nick]then +t.send{from=t.nicks_changing[e.nick][1].."!"..(t.nicks_changing[e.nick][2]or"xmpp"),"NICK",e.nick}; +t.nicks_changing[e.nick]=nil; +else +t.send{from=e.nick.."!"..(o[e.nick]and o[e.nick].username or"xmpp"),"JOIN",n}; +end +end); +a:hook("occupant-left",function(e) +if i[t.full_jid]then i[t.full_jid].ar_last[e.jid:match("^(.*)/")][e.nick]=nil;end +local a= +e.presence:get_child("x","http://jabber.org/protocol/muc#user")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("status").attr.code; +if a=="303"then +local a= +e.presence:get_child("x","http://jabber.org/protocol/muc#user")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item")and +e.presence:get_child("x","http://jabber.org/protocol/muc#user"):get_child("item").attr.nick; +t.nicks_changing[a]={e.nick,(o[e.nick]and o[e.nick].username or"xmpp")};return; +end +for o,a in pairs(t.nicks_changing)do +if a[1]==e.nick then return;end +end +t.send{from=e.nick.."!"..(o[e.nick]and o[e.nick].username or"xmpp"),"PART",n}; +end); +end); +function n.NAMES(a,o) +local i={}; +if type(o)=="table"then o=o[1]end +local t=a.rooms[o]; +local n={ +owner="~", +administrator="&", +moderator="@", +member="+" +} +if not t then return end +for t,e in pairs(t.occupants)do +if e.affiliation=="owner"and e.role=="moderator"then +t=n[e.affiliation]..t; +elseif e.affiliation=="administrator"and e.role=="moderator"then +t=n[e.affiliation]..t; +elseif e.affiliation=="member"and e.role=="moderator"then +t=n[e.role]..t; +elseif e.affiliation=="member"and e.role=="partecipant"then +t=n[e.affiliation]..t; +elseif e.affiliation=="none"and e.role=="moderator"then +t=n[e.role]..t; +end +table.insert(i,t); +end +i=table.concat(i," "); +a.send((":%s 353 %s = %s :%s"):format(e,a.nick,o,i)); +a.send((":%s 366 %s %s :End of /NAMES list."):format(e,a.nick,o)); +a.send(":"..e.." 353 "..a.nick.." = "..o.." :"..i); +end +function n.PART(a,t) +local t,n=unpack(t); +local o=t and w(t:match("^#(%w+)"))or nil; +if not o then return end +t=t:match("^([%S]*)"); +a.rooms[t]:leave(n); +i[a.full_jid].ar_last[o.."@"..e]=nil; +a.send{from=a.nick.."!"..a.username,"PART",t}; +end +function n.PRIVMSG(a,e) +local t,e=unpack(e); +if e and#e>0 then +if e:sub(1,8)=="\1ACTION "then +e="/me "..e:sub(9,-2) +end +e=u(e); +if t:sub(1,1)=="#"then +if a.rooms[t]then +module:log("debug","%s sending PRIVMSG \"%s\" to %s",a.nick,e,t); +a.rooms[t]:send_message(e); +end +else +local t=t; +module:log("debug","PM to %s",t); +for o,a in pairs(a.rooms)do +module:log("debug","looking for %s in %s",t,o); +if a.occupants[t]then +module:log("debug","found %s in %s",t,o); +local o=a.occupants[t]; +local e=p.message({type="chat",to=o.jid},e); +module:log("debug","sending PM to %s: %s",t,tostring(e)); +a:send(e) +break +end +end +end +end +end +function n.PING(a,t) +a.send{from=e,"PONG",t[1]}; +end +function n.TOPIC(a,e) +if not e then return end +local e,t=e[1],e[2]; +e=u(e); +t=u(t); +if not e then return end +local e=a.rooms[e]; +if t then e:set_subject(t);end +end +function n.WHO(t,a) +local a=a[1]; +if t.rooms[a]then +local o=t.rooms[a] +for o in pairs(o.occupants)do +t.send{from=e,352,t.nick,a,o,o,e,o,"H","0 "..o} +end +t.send{from=e,315,t.nick,a,"End of /WHO list"}; +end +end +function n.MODE(e,e) +end +function n.QUIT(e,t) +e.send{"ERROR","Closing Link: "..e.nick}; +for a,e in pairs(e.rooms)do +e:leave(t[1]); +end +i[e.full_jid]=nil; +o[e.nick]=nil; +l[e.username]=nil; +d[e.conn]=nil; +e:close(); +end +function n.RAW(e,e) +end +module:provides("net",{ +name="ircd"; +listener=f; +default_port=7e3; +}); diff --git a/squishy b/squishy new file mode 100644 index 0000000..507b03e --- /dev/null +++ b/squishy @@ -0,0 +1,12 @@ +Output "mod_ircd.lua" + +verse_path = "verse" + +Module "verse" (verse_path.."/verse.lua") + +--Module "verse" (verse_path.."/init.lua") +--Module "verse.component" (verse_path.."/component.lua") +--Module "verse.plugins.groupchat" (verse_path.."/plugins/groupchat.lua") +--Module "verse.plugins.presence" (verse_path.."/plugins/presence.lua") + +Main "mod_ircd.in.lua" diff --git a/verse.orig/verse.lua b/verse.orig/verse.lua new file mode 100644 index 0000000..a968b7c --- /dev/null +++ b/verse.orig/verse.lua @@ -0,0 +1,580 @@ +package.preload['verse.plugins.presence'] = (function (...) +function verse.plugins.presence(stream) + stream.last_presence = nil; + + stream:hook("presence-out", function (presence) + if not presence.attr.to then + stream.last_presence = presence; -- Cache non-directed presence + end + end, 1); + + function stream:resend_presence() + if last_presence then + stream:send(last_presence); + end + end + + function stream:set_status(opts) + local p = verse.presence(); + if type(opts) == "table" then + if opts.show then + p:tag("show"):text(opts.show):up(); + end + if opts.prio then + p:tag("priority"):text(tostring(opts.prio)):up(); + end + if opts.msg then + p:tag("status"):text(opts.msg):up(); + end + end + -- TODO maybe use opts as prio if it's a int, + -- or as show or status if it's a string? + + stream:send(p); + end +end + end) +package.preload['verse.plugins.groupchat'] = (function (...) +local events = require "util.events"; + +local room_mt = {}; +room_mt.__index = room_mt; + +local xmlns_delay = "urn:xmpp:delay"; +local xmlns_muc = "http://jabber.org/protocol/muc"; + +function verse.plugins.groupchat(stream) + stream:add_plugin("presence") + stream.rooms = {}; + + stream:hook("stanza", function (stanza) + local room_jid = jid.bare(stanza.attr.from); + if not room_jid then return end + local room = stream.rooms[room_jid] + if not room and stanza.attr.to and room_jid then + room = stream.rooms[stanza.attr.to.." "..room_jid] + end + if room and room.opts.source and stanza.attr.to ~= room.opts.source then return end + if room then + local nick = select(3, jid.split(stanza.attr.from)); + local body = stanza:get_child_text("body"); + local delay = stanza:get_child("delay", xmlns_delay); + local event = { + room_jid = room_jid; + room = room; + sender = room.occupants[nick]; + nick = nick; + body = body; + stanza = stanza; + delay = (delay and delay.attr.stamp); + }; + local ret = room:event(stanza.name, event); + return ret or (stanza.name == "message") or nil; + end + end, 500); + + function stream:join_room(jid, nick, opts) + if not nick then + return false, "no nickname supplied" + end + opts = opts or {}; + local room = setmetatable({ + stream = stream, jid = jid, nick = nick, + subject = nil, + occupants = {}, + opts = opts, + events = events.new() + }, room_mt); + if opts.source then + self.rooms[opts.source.." "..jid] = room; + else + self.rooms[jid] = room; + end + local occupants = room.occupants; + room:hook("presence", function (presence) + local nick = presence.nick or nick; + if not occupants[nick] and presence.stanza.attr.type ~= "unavailable" then + occupants[nick] = { + nick = nick; + jid = presence.stanza.attr.from; + presence = presence.stanza; + }; + local x = presence.stanza:get_child("x", xmlns_muc .. "#user"); + if x then + local x_item = x:get_child("item"); + if x_item and x_item.attr then + occupants[nick].real_jid = x_item.attr.jid; + occupants[nick].affiliation = x_item.attr.affiliation; + occupants[nick].role = x_item.attr.role; + end + --TODO Check for status 100? + end + if nick == room.nick then + room.stream:event("groupchat/joined", room); + else + room:event("occupant-joined", occupants[nick]); + end + elseif occupants[nick] and presence.stanza.attr.type == "unavailable" then + if nick == room.nick then + room.stream:event("groupchat/left", room); + if room.opts.source then + self.rooms[room.opts.source.." "..jid] = nil; + else + self.rooms[jid] = nil; + end + else + occupants[nick].presence = presence.stanza; + room:event("occupant-left", occupants[nick]); + occupants[nick] = nil; + end + end + end); + room:hook("message", function(event) + local subject = event.stanza:get_child_text("subject"); + if not subject then return end + subject = #subject > 0 and subject or nil; + if subject ~= room.subject then + local old_subject = room.subject; + room.subject = subject; + return room:event("subject-changed", { from = old_subject, to = subject, by = event.sender, event = event }); + end + end, 2000); + local join_st = verse.presence():tag("x",{xmlns = xmlns_muc}):reset(); + self:event("pre-groupchat/joining", join_st); + room:send(join_st) + self:event("groupchat/joining", room); + return room; + end + + stream:hook("presence-out", function(presence) + if not presence.attr.to then + for _, room in pairs(stream.rooms) do + room:send(presence); + end + presence.attr.to = nil; + end + end); +end + +function room_mt:send(stanza) + if stanza.name == "message" and not stanza.attr.type then + stanza.attr.type = "groupchat"; + end + if stanza.name == "presence" then + stanza.attr.to = self.jid .."/"..self.nick; + end + if stanza.attr.type == "groupchat" or not stanza.attr.to then + stanza.attr.to = self.jid; + end + if self.opts.source then + stanza.attr.from = self.opts.source + end + self.stream:send(stanza); +end + +function room_mt:send_message(text) + self:send(verse.message():tag("body"):text(text)); +end + +function room_mt:set_subject(text) + self:send(verse.message():tag("subject"):text(text)); +end + +function room_mt:change_nick(new) + self.nick = new; + self:send(verse.presence()); +end + +function room_mt:leave(message) + self.stream:event("groupchat/leaving", self); + self:send(verse.presence({type="unavailable"})); +end + +function room_mt:admin_set(nick, what, value, reason) + self:send(verse.iq({type="set"}) + :query(xmlns_muc .. "#admin") + :tag("item", {nick = nick, [what] = value}) + :tag("reason"):text(reason or "")); +end + +function room_mt:set_role(nick, role, reason) + self:admin_set(nick, "role", role, reason); +end + +function room_mt:set_affiliation(nick, affiliation, reason) + self:admin_set(nick, "affiliation", affiliation, reason); +end + +function room_mt:kick(nick, reason) + self:set_role(nick, "none", reason); +end + +function room_mt:ban(nick, reason) + self:set_affiliation(nick, "outcast", reason); +end + +function room_mt:event(name, arg) + self.stream:debug("Firing room event: %s", name); + return self.events.fire_event(name, arg); +end + +function room_mt:hook(name, callback, priority) + return self.events.add_handler(name, callback, priority); +end + end) +package.preload['verse.component'] = (function (...) +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local lxp = require "lxp"; +local st = require "util.stanza"; +local sha1 = require "util.sha1".sha1; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_component = "jabber:component:accept"; + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = xmlns_component }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + return stream:event("closed"); +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns or stanza.name == "handshake" then + return stream:event("stream/"..(stanza.attr.xmlns or xmlns_component), stanza); + end + + return stream:event("stanza", stanza); +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_component(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + stream:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + stream:close("xml-not-well-formed"); + end + + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("opened", function (attr) + print(self.jid, self.stream_id, attr.id); + local token = sha1(self.stream_id..pass, true); + + self:send(st.stanza("handshake", { xmlns = xmlns_component }):text(token)); + self:hook("stream/"..xmlns_component, function (stanza) + if stanza.name == "handshake" then + self:event("authentication-success"); + end + end); + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("authentication-success", stream_ready, -1); + + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5347); + self:reopen(); +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.host, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = xmlns_component, version = "1.0" }):top_tag()); +end + +function stream:close(reason) + if not self.notopen then + self:send(""); + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end + end) + +-- Use LuaRocks if available +pcall(require, "luarocks.require"); + +-- Load LuaSec if available +pcall(require, "ssl"); + +local server = require "net.server"; +local events = require "util.events"; +local logger = require "util.logger"; + +module("verse", package.seeall); +local verse = _M; +_M.server = server; + +local stream = {}; +stream.__index = stream; +stream_mt = stream; + +verse.plugins = {}; + +local max_id = 0; + +function verse.new(logger, base) + local t = setmetatable(base or {}, stream); + max_id = max_id + 1; + t.id = tostring(max_id); + t.logger = logger or verse.new_logger("stream"..t.id); + t.events = events.new(); + t.plugins = {}; + t.verse = verse; + return t; +end + +verse.add_task = require "util.timer".add_task; + +verse.logger = logger.init; -- COMPAT: Deprecated +verse.new_logger = logger.init; +verse.log = verse.logger("verse"); + +local function format(format, ...) + local n, arg, maxn = 0, { ... }, select('#', ...); + return (format:gsub("%%(.)", function (c) if n <= maxn then n = n + 1; return tostring(arg[n]); end end)); +end + +function verse.set_log_handler(log_handler, levels) + levels = levels or { "debug", "info", "warn", "error" }; + logger.reset(); + local function _log_handler(name, level, message, ...) + return log_handler(name, level, format(message, ...)); + end + if log_handler then + for i, level in ipairs(levels) do + logger.add_level_sink(level, _log_handler); + end + end +end + +function _default_log_handler(name, level, message) + return io.stderr:write(name, "\t", level, "\t", message, "\n"); +end +verse.set_log_handler(_default_log_handler, { "error" }); + +local function error_handler(err) + verse.log("error", "Error: %s", err); + verse.log("error", "Traceback: %s", debug.traceback()); +end + +function verse.set_error_handler(new_error_handler) + error_handler = new_error_handler; +end + +function verse.loop() + return xpcall(server.loop, error_handler); +end + +function verse.step() + return xpcall(server.step, error_handler); +end + +function verse.quit() + return server.setquitting(true); +end + +function stream:connect(connect_host, connect_port) + connect_host = connect_host or "localhost"; + connect_port = tonumber(connect_port) or 5222; + + -- Create and initiate connection + local conn = socket.tcp() + conn:settimeout(0); + local success, err = conn:connect(connect_host, connect_port); + + if not success and err ~= "timeout" then + self:warn("connect() to %s:%d failed: %s", connect_host, connect_port, err); + return self:event("disconnected", { reason = err }) or false, err; + end + + local conn = server.wrapclient(conn, connect_host, connect_port, new_listener(self), "*a"); + if not conn then + self:warn("connection initialisation failed: %s", err); + return self:event("disconnected", { reason = err }) or false, err; + end + + self.conn = conn; + self.send = function (stream, data) + self:event("outgoing", data); + data = tostring(data); + self:event("outgoing-raw", data); + return conn:write(data); + end; + return true; +end + +function stream:close() + if not self.conn then + verse.log("error", "Attempt to close disconnected connection - possibly a bug"); + return; + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +-- Logging functions +function stream:debug(...) + return self.logger("debug", ...); +end + +function stream:warn(...) + return self.logger("warn", ...); +end + +function stream:error(...) + return self.logger("error", ...); +end + +-- Event handling +function stream:event(name, ...) + self:debug("Firing event: "..tostring(name)); + return self.events.fire_event(name, ...); +end + +function stream:hook(name, ...) + return self.events.add_handler(name, ...); +end + +function stream:unhook(name, handler) + return self.events.remove_handler(name, handler); +end + +function verse.eventable(object) + object.events = events.new(); + object.hook, object.unhook = stream.hook, stream.unhook; + local fire_event = object.events.fire_event; + function object:event(name, ...) + return fire_event(name, ...); + end + return object; +end + +function stream:add_plugin(name) + if self.plugins[name] then return true; end + if require("verse.plugins."..name) then + local ok, err = verse.plugins[name](self); + if ok ~= false then + self:debug("Loaded %s plugin", name); + self.plugins[name] = true; + else + self:warn("Failed to load %s plugin: %s", name, err); + end + end + return self; +end + +-- Listener factory +function new_listener(stream) + local conn_listener = {}; + + function conn_listener.onconnect(conn) + stream.connected = true; + stream:event("connected"); + end + + function conn_listener.onincoming(conn, data) + stream:event("incoming-raw", data); + end + + function conn_listener.ondisconnect(conn, err) + stream.connected = false; + stream:event("disconnected", { reason = err }); + end + + function conn_listener.ondrain(conn) + stream:event("drained"); + end + + function conn_listener.onstatus(conn, new_status) + stream:event("status", new_status); + end + + return conn_listener; +end + +return verse; + diff --git a/verse/.hg/00changelog.i b/verse/.hg/00changelog.i new file mode 100644 index 0000000000000000000000000000000000000000..d3a8311050e54c57c5be7cfe169e60a95768812c GIT binary patch literal 57 zcmWN_K?=Yi3Lg;Khr7K@mZ``U5I>XhHDcK?@!{DC)X>Gkv>lj{83Ieth4Lv{pLq)>3up$%ma6 zryjj=cYj}6a1Qwi$GQL1acaZA=`Xd{l9&e_?UVE8f`ovz`3;1BTwtH-R~tl{cUy>LRoA#kkn3p6H;X0>TAiMsEO9&#KtaF%kC& z0?MC^6O%_8#ghV;dU_j}hGF(fMQ&;r06At5()A@p#K|5IMZNSQ)@$rBU{)6u@d1gk zE~8(8sheObl^(@%jkq@9M8!PBJ{GpYhfTQ4HI>N8>~|m*RlXV$|SeV*7K$`m< z&@>_;ag&Tq#d?FLY0n|N<5ulwdNd?qgA4ajGYB! z(jpPlOi-+bI|I;!g_uG`jBFO2M2s@7VyuRH0wCKKj|)7o_16H{wnS3LEJA_YKY+Ax z-3Ldq^rM)N7f9OAk3sb+K+~qGY=zbml{OQXUs?t5crL08l@v_ihE-O8v^NE|ejQWQ z7$~cRPL~svIeiNNH05$*cySRi&O>0@`@=BfkYo&7IQkn%n6No{j)y*xa(L*$@&%x`qY1@A)9oj-B?t2r(aB+Zz zDeZv0g|b*H-lge2P!&&VA0=7Y0)w5_fb|F+BBcEhkSYy$eo#sbOp`cxA;gFake#MJ zV~Pa%zk%{HP&3#S%w4+4c*We8CalpUVm)41vg#&4jqD_WA{-wU_ckE*JRbP>`J8i- W9u(P4*AF+K@yj44866VZ&yar(`uoZN literal 0 HcmV?d00001 diff --git a/verse/.hg/hgrc b/verse/.hg/hgrc new file mode 100644 index 0000000..9513652 --- /dev/null +++ b/verse/.hg/hgrc @@ -0,0 +1,2 @@ +[paths] +default = http://code.matthewwild.co.uk/verse diff --git a/verse/.hg/requires b/verse/.hg/requires new file mode 100644 index 0000000..f634f66 --- /dev/null +++ b/verse/.hg/requires @@ -0,0 +1,4 @@ +dotencode +fncache +revlogv1 +store diff --git a/verse/.hg/store/00changelog.i b/verse/.hg/store/00changelog.i new file mode 100644 index 0000000000000000000000000000000000000000..edb4179ed4c842cb837cb6c604ed8bdf3b0e9d09 GIT binary patch literal 77591 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

^_FKupde_+chr_AQ0-Znu)q0*o$2fsZd4kIXr zG&B=K^+Jj#0DVbI0OPeZo^;9FY6DB=2*%;LBnR9Ysi4;)5l-X)Yoc`h3X0Al(7p7@ z{^dNJ-GQQe1u4>&*6>7-YzUDn8s plW5gqw$bMUJC6&*%5Y?>x`h&e^uT*4e|_YrWRmJAxQNj3BrU!jGn*XTspn zwTdS7!nNp|;!X9%jJ(3$TaG9BVL!{DpW*4_Z%^}=Aj%T1r1ug{HT<42E1uXBgkImaI4(OZF)y8VjwHs?@UQ&XVP3OpN9g;;R}ok) zflI8=O?;@Ou0fDKvPpWPw&q~(`L4B7twtRE$YtMpGLkX2zTWyFHP47AE4@FUv9<8z z!CvRm;8w4rM>s`uPSLK-T+&D~+{yQ`YaSpBHcZ`yvx+{9K!_y}#1!x+ctAhV7iu_f zo3!j@!TRo$GbV&^@in_tqML`x;6LE=5}f3(VG6cocUf_Ml?9Pw7VG%PZcO6e8~Mf2 z(&a$d-r@*_kyod9zbH^_K3M#&&3gMhp9$`)8+D4yPqAwz*QM`WwCJ!YM`zHk+`_p* z)|BVxm$z%VzEAIK%T43$c3r4G*4svot!y1r*{@c6xY&WctbD<=s=dyq9UME|>vmmS zut95FAlCR&{j-+DQWggbQ2;)zG zQ|GeC6fTuXqhmZSm&fK&=)eqvOXgFUTsoh^V$%5-he4+bsK3B#DIIEgDR@J-B%*Hi z^}E(Mm-LA5c^c_+NGB~F4;=W0mmW%B2;Ft^haqHlZ+qF^-AUtPpPm0#CB@=kwwWobNz75W4q-a-NWxv37m(&yoF5cg}QS!OAX2Jsb z?`bO;yyR(e6?J=7zU>};(K7OVZ)?HZd`!%CZ2s52%NEUkRE{ z=r;57W(Q9R4-R4ZN3ihGM`5#h7>7%O!N;XCm{c~6N+Q$vG%|zCrE@tf9-qe~^BFw6 z2gKX(DlJ90kbxX{h*0IF-9u~@lXEbYgm2w%JT9R+x4VIrnB^moMIRiWO&UTX*> zCYeJbvbbyx5o1y4G>pY%Q7K%E4rKXkI+4u4h#V3P!--4i;WeQqh=M27^|ElwAmVy; zyBQa&t}pfMO`JSqv5JOa>A_3zJCO?~4h#^?0{ucgz5Roy1hazu@Ft*fxGWBfNy8`< zGRC3MF%BE$a4;H?P9ak1Y$_GTA)77W!V|9)ad82j5EpWA%OWmj%YJ&kSIgCF`SIM_ zYVxO3J1)Qef|nL1e{;e1_UGbUu!tOtOk(h8bSeoNgwLgrXgm^&#N^WGd>)6$#Msa~ z6pnz4e7s6)5iU@^9OA;HMkV&-7=O)as`pF`KJ3)!HxAe&WZ>5Wnv4{VpEu86Acn{+ z4xa{HL8J0X96AP_LFcmAJR+G)rc-EqjK!w0SPYh6AV|E!E3y`$KMZo@5q(iE@ET7*7YRfZw@t2ga>a&~Ix7OR%GMuAVccMj++*!(+b{OwQrbS{^LVHl0Z zq*Lh}Du>3Su*h5= zSB56y&rzrZIf{t>wr}!;?LFrDq206@2fL%O!M<&b-${$1RT%x9a3XT}WD=3hC9-Hh zf=glYDHxH?#du6MnL&dw2=ahSgJnoS{4HLc=oA*3F8yzLJC9m zij5um6lHip_i)68{jC0nQRjU&6h(Qt4OkMwrydB~hf4pv!qY8xIwh)3Q?T`H*~;=e z_P|AFLVs2E(Rqq<9;gIH#Q9&R9aZmEJrfWbx&EQfCui=B6rY<4u@&|!V;xV0o6nSOJ6AP~JnCYmx$T@uWA*qCBPx?L?7fQDefm_E`1R(y^9N@)B(E~E-bLiM zE}Oqsm!J@0q?rQ(pJ zfd^8i&cOw&&Xt;Lbe*^t7sO^!>2ErER&ID)+#1>Y?&bt0@z6b^doFLzB+ZO7TpV{Y z&Z!~aymG_G^j8CB)SwEA*h}eh`R5yw3DbC;Z_D;i?rrYtKXYKkhO!$zn==^g-^?D4 zlJJ?nuUN0~Mbd8eo;ICFCL2kpk))mRra}un8Yie>N1&gmac^v#zp1Bd{!NpmBaZux zzZuqkzu*{N`d5O+HXrU^N)8_c7zIQB5dBa(O=9-B_15Q!L_$`g!k z>3F=VpQ9U5SAly3q8@am^kibvERE(LJ0}%a9o1cOD=r!@ElmC%-E1m|FA|kaW>UE@ zs%ShqkIN&|`CJ-_Mxryg3?`3D$GAhpZ{jtH5+4aUDv0=ZHH!gP!*R35h#9|$(e9p| zbAGmokS_l4H*qS31Of%58HLItae1I)u^DuXOyv@(Bo4e&cq|r)#TE=|8MK}V)VrVU zt_trX5%KtWb7iH9jnr3E=h)`Vjas}=E4TzN{VVyq-Dz9~n?$7ZSR^)`338N2W`mRk zifo_>j~ou0%OVm53YLrwUX`^-ucL{oim1;D-@X5c_58KA`s1Iszob8O*gW$1FT%p7 zKcyOlOJ*~8BpMACQ!bssBvL`taCs~~2gB%WnD9(Gmq+0-@F^`L=t)ufqo8IrME@3p zWE%Q<(vQ14?`wLAUp*XAZNn2%{q^uCeTJXM8#kv zg3`$)3075^Yj{PXqh1|yMj`rp_9%0(9D6aBs+Lz->;|JvwgoZ2$l_0bw>}9*6_rb; zktuwjLPaVzht1=&sT4kuM55DK6pTmW@mcuS%L+_D(SfG{^{ON4&t1b5jdL{IJebF9 z57bKr#m`L({hj(h1|E~mp;5^gl}lpMc}y@dNPLj%d@>OxAv6k^&4z`JCa^1H;iKWQ z7U}xY@UDS~tL2Odnk1==*n0HK!#^BIl8R26z z5)%Wff=*`A$TSASq3MCg7o^QBzu^>o+q;yqEjL=1_^K(f zFv~^5jGb9yLyYH6e5pJ=FiPpsxe$j!uWs&+;%3tg{cCSkJw83EisT%@ zSuN>f(3HyF*W~lVbiq_&Xr z-t10MN9&~(R)eJm_G!nZuioO8MjU!QS#G6s)~2a$)C=n*?kGilN6jR60B@|B(7e6y zgql_h{Y1^HAS2^brY!E+kk~Q%zDLGn3zirf^&%HOwm@efSpL+V!+1f#-U0ps`;|my zvUpSwV{{TM&?Fj*%7mc@UCdx{LGChW9E=DnI6lbbUgGsyi!d?HwTL#vSjZWJ==aU$j2PE=lK)OWxZ`C0iIg9bnYlwWhkoDxiffmf!#7w@Ao&k zkBmrli%b zEHV*>Fc}Oa9*4x^GC(WGz)XX_BC!}?C<~;yf&k)!5g}}jaZsy1;v(Oy;fBLEqxl+A z#5H-ByKHu(j{T!KP*dQ4Cmaz})cAmevH1)zMOZWp>|#0-`h-mbWd>{$&|nyR;t+A@ z1-L|;zyRLI;l$s%TU(gC?P=?oLRt9kbkdcW5ijx5!sI`ifXwB9In3qq0VyDX^2p?1 zY$}XSCWpkp5Mo86u#ifD3p#}zc$L;7O)wsE3=kKi4=7KUsVpBodQUvsC5Ph*FL-Y;d1>NHj{XJi3 z$M(294_-QL{06m(eWQyeu0}A9@PY;P-co-pqJmUy3h`H)NORt_6HzjuTogTHoUdBNc*3)c~9wTbf(|GH!_z|-& zK0Vp^BhtFlbg9;&VV?UcNNMKcitH4ZGf8P$#S?ynu7A@#<6y_hwobi{^LkuYsXN2Y z%rLETSaw3LcuikZSK~Xz)0O5gHc*w+W&{Ojyd=vq=g!^qeb$QGX_Rprs&<6FvT67* ziOMF`=I*#r{^*c#qJ&{S;l8x1`^UQA4L1vSU$_;tpgbqK`Esnp9+#|8U*~U>wN(fE z*7z5|O%!e=Un0v^N+SBcJz|h3&SRi?>2n+D|A9EFh zRv?3-3AP1`&fyaQH=tA4L@tBQqcXq>C4umyQ?a48z;z@9s^ek@^@uUt6A>58jEDAT zd1IT$+stzH@6SEA&h5?9-?_jQ5cg3MeWGoEj7nofolJam*6^d@pqq55quG9^DtY3yf#0eBS5JUt0e}vl$l(K= zN&@_l2zm(-i7FNC6Mit?QE7pQewY=3JM$X&( z%*)@o_(vOn(FH~hk;Vlz3@mI6geQn`DuY30gFQrK(ZLp_v3O*B91Jh}i@GV~OhVMP z%-`!Pr>0FNFjh0Q_Aj+rQnR?_|D?`hf!lxw0{~i`#^3?21OpQEQVJ5AWY8+OOb!@j zf>AhJ(ErvV{XZEfm?G*W*JoTg6?#!8c(VKO^WC!oQhnRD{6QVP{;8Bw!PEgEO=f{h zgTbKi`Ao1pSu_@dPNcFy2c(0R%3@Ol<4@TfuhCD0{uIcWjOfo(Qmar_T-M4D7<)|T z!<>++eF|uv3a{|=vGLQ8Z%ooIam}nV%c|ls#w}G*+&q0|@awLQFqZVo(}cl$J63ge zM?OuweZ|6{e(lI3+F3qflZ|(`rOwSc{Y}ALZ1%36VNrPS2kps^w-Nbc$jr|;@xHKKGj1ZS*k>4+S&WKNpT>Yh8t8A;=p zn^)-O+AwRy>vyYPzdAeyRipe0ugqG6KGORzMBj1a@22Lg~pxIJGgdD9pv6}!5qBlNTPzf64E zn7&$0zbt>VxXr_}dBZjGt5cWiq|H&Au=?3Ci!YD6f(o-vPLFEx;0^a5bRTmZ+kLm; zVqjYTo>dvm&E0BAZrZ^suO*bN8SBEDDj&5_EJ@r}>e0x-?K>v$kE&TLPL?b>Dppa? z+g{&cY-2Px+F;779nE3u2m8gTHZxeukCFYx-?t9QFPxEFXc%FC8^m(6i=$Ed zt7PE40IH#|+sKeZLfz(FSadK-v-Dx3*j~TY&l2~2w!VUdkI021k0C)#cUEyZxenkAm|uowcpbR<50 z@wM(JeJWH;LG+z_+mnxcjaj6XztG8bttz=Y;P&31M2CJN+P|-LbQXsQZbAxj)^hnA zDxCsG0iDIC(rNI(0`~#N=K}VK&z_M2%nq$GKdIB8Mk=Dd(O^TK;w8V{6JFK6DXth6 zcWn8s|C2fhSSp=I0*sY{zL8ADxLjxr5{b`*bq?S@3UX+Y1gp%*OuRl(>d1PeA?nAL zjA-HOznJ`CDM9_+DkWCh*XgzYCw2I4j6`IRz&xRY{H-iLJe`=1_A`nfttraS1+C<~kT$#Y+p5zcHA=pn&i- zG^zlD0mFdJqXWuL<$*E818+W{?BGg-0RSi*olN8ck4(XGr)rGX>J5y%fG5NVQb8Dq zk)rzt>iVZ`Nl@78p2T{h;cRn0Utr7Oe}W=S6t7QH+N=vAydrbQy-`M{H50|lug9~P zc`H69UkuiWY8PANPI%p#@#^&oYnMe5GnKNEm}GAL6AKlS*rE!RPnSd2-}TjL@w%A3 zF<_$fVVj+&9k!p;EpS@wbo!{{+XcDQr5kSPYR^OTRFiS4b`W8p1y6`kWUw+3qh*_J zoqlrYf?P%1>lqdCqYiCpI(QN<{VT!e2`bMG;7s9#vsMQB@$l9H#2W$}z*r-b!QD!s z(C9E#VT{831;rIid=8Zj?g@c8soI3s2!lq*1X`}m5EGG!ntAuqthP^dlT0;?mRMY? zuD|COef()9qzB>tr5w@$0p;+SKoLCK;NZsCFnpVFd|(iH9EQLlptb<7Dg?eAJ)sUjQqTfX_m2DYsdbo5skJMV}S zZ^TOvC9v36-&PdYQzq2LR+`$6jgiVu?>l`#j;MM4`UStL=?2OX#z*ww3l3XxX3i8Ej=!ZY+8!4mPjdm;CvRDMCI&emFb>E4?D ziiI28@zTF1_`nfyje~-~q5^_XA%Ou!Cj**CV-P{OfK5eb@cCR0DA!!ZkTN++7OxBq z+MjJ^1vyg@{SCdA;;GaLbLY5ZE*aHmqZ1$*lKMMo@i3T`=sTD{q1F(;V0_R5nhuj2 z1PsQ(m@G0_bv%d^fLYEYk~lmPpU9(9K?Vw7)=|!QwP?`(WWpM9tPm3)^-jM``FPQ( zIrGhv^bob-!(3jbc&*I@r6S{O>OWRh6r+&UOz^nRG$p7yq01y`<6DSzQCiB1#43-SI z=K=Ns*cGF2*ccTIP8N+Vm^q^a;#-u7>A=J^#6&GSVd4oL&u8XcjE!g3$sUJR(FeF}W;oq(i_ANPm3#s;lAEiZWpXInxmn z?HvDwIlSEGTSk7(J6M&v_r>OMPIzfy^1qpYhKEosA|O6Iu;Ad6q5UCDgGYoFmrJ7X z*(|`rIebBPr~?QD7c4x&!C(vTHi(G|`zk`w%ZGH$+vQ*g3vFggC5fyA)rwsY67FPKbl4^S*YwWvSC>#`Oh z4kkCj4iSHP`AF-$enaxq`l$TJM}w+66jG7vP4q%W1`lpg&jZ9nkVzPg14<59dEmtb zXEMYn00;=!2gYL|oeZLS1c01|E?(URpkDz`=;LR=ZI9?r>TEf3ePQ>@%T_i$#1$r9Ux-&L3tU&TBfN#BYmgHuogKCPe+tk-?pS^BPORM|^Hr;S# zq4cm3PNsKw6rFK5KY8wVIdbe>X^Z_sit+LBj~wisPBk2;mS<6Vy{hN$Z18^ivRlLR z;@7bCW+(j|S?Wa8hs%NvmfFNlZT6^b>F@#zhV>1<{%h$GQe^0HRQx}3MyJ9LgPkxFQ%T%WA%U4_+ynfeUu*l+?bFyPv}LrJ`dvzm5g*SfyHGb*@gAyw_J-`eK4t3+Pv4*dnX4JY zeANT5O=*TMI$g~J;T9%Ow3{^h3Th>lm+g(0I7i}VTuOX$W>2U6o{Rx0{dd}lXc%ce z!3PtPVQBWCp$E?d2Q>VY=N{X>Br&mBiNB&Y@`}M0*+m95cxhpR6mId?@G5k$h{`Yk z#b8CFaTp{T1A#Jt(GVf{6;@P?1aSi-J_#Z>=q$mqq9w4OV0{%b;s_PbM2uXLF&TCH zV(~j=6X&lrTlR;aOm}=EIU%D^BIfyx7)0&-fCapyb-Y&jZ@ zOGCkNfUAN(72q>K4u_U`tq{Cc(N;lAh$CWV;y1~#w@#*4j#s2jI)272_34+}J$Pwh z^53ljH24ty#{wS{tY$P6i-mwJK+jkd9>g+0Gy#_n!Fu>Qr&Wg6YAw<#vmnO_F>(m= z9k{K2f%4qq#M!449xG5!-p2*9kPkpl;Q3GC3XmlR0YV%SiA4qd1_DUH%YiYGt%>47 zA%2GfK||D`Ug^hcgVjXXD@X&Hh3I4Q8Uvau{C}8@?ABPY?xfS~^Xn#w&_<7lb_U#k zSqf|_i$ewv3x!RmLQE<|tAei$)IS!V0@@$~m;qDd(*;V`7^Ghi2(ZWrnQ(!3XT$_| zlu5=>iEFzgm*}o{n0a}<(z19@y!22Ko=&YUUkNat_1vOLg$FxRxUo_s1zyq?v1=2b zeNEqJHjQ>mV}$tU4?6~7uRzp}kEN3@wOnej5qr4pnrr&`nJbq(x}K1^+oRyE&vwlu z7lKajJ9Kc z*!4ZA<;fg_Pi7O2%)72IvR!$l{ic#*b2?Mk#$>MO?Oi+9| z4DkMO`BWH*_@En$)=&ar9fW{O!cQa<=DDl?nWMRcBdm;gnF{ zSLdd-&n<307be|mckg1JUK}xIkra_PbCqP#;z{P8Bli&!i=UHSvdbb?_R{A(w{|>k zRx^I4wQ+@cV#=hX-5O)pOiVeZ^L&f}F3QH@#tOk&gn8uMn1h(tE1fq>dfu!}61z>d zc%^LG7HH8hU&y)O;a@X`4>Nf(Esv^~Twxk5!i28OQL_zY-)l&`n2j zJiz~FFf}-eSzM5iWCjE{lK2pYMdd(*I}svzF&>)+?qjfGK(yjrp?v|b4BUg#hOlMVYT_+a;gq=(o%oE;rGylzo;7DA3IV(07Ps)2R%15)Qqu7q8Z8}l;e zurGomL@!`P3@Ok5!48Mc245VwNQiVMm&Zib6AT9q*f3mhT9RmBPQ&^_5d;P5EW&FC zQ&zGQo>1$!!MzZ%^WJ!%ht?n2U?owwEylF%$D4p=TwB2ZLXe;Vf^HH!AlM5Zo@9v6 zfuIr)-83+s>BtMn-~(g}(MnvfoTxw><_mkMF?QqCI6|bqF+3sah#xmZ{o(d(g-Fv} z^|>>bjv>5SmmYsj{xM!!n25*GB!sdXR zk_zT5Oj-zO;PGLz!%)zcuE2{U+6gFU2_k+&+2!4f<)m=K!HhS51BsP%O5%0#KVL(XzUJ^LK?Z2XbxX&cun8MXU8 zpP)22a0@RjOvJXqOhD4@7Y+)GI|2fa9SUWSHzs7>2OP zj)g&>D#V>J0R^Q|z!>0?0C2R|DBJhagQg1E?ZHXYz!I&veBNU#G2;lTpxkMPQ@Mdlt0ay$_A z`yWrqud-I~2|jZonO{aD<`$@f;QsAGvwjcUqR0irW5aG5F3fT0d>)$xbB;^nLcjt9 zB(s?$1|3ifE@x=poxUbspXk@KA%}(Nzv!D$GX9cE=hP+-MftknnvbH1h?dCpkFTdf z3^c4dKn9~gNCNafgs4Dh8>~pM=>_m4Bp4VJfx}8)Apd8;*Q13NNnrFvY(%}SSj|hP zLB+Jbo4xVX!nlyL2`+6Sq~TFQUvLx5e<~_m?_f>?;QB_JN&9G(K$4qcg#k#e^Vw zd@$(0!m9=AN(%Xt5Fe;nxQGvNL+6RtsS$VVvOSmj-3+B=3}#KnOA8Z;eZU9Y1nZy8 zA>d<0FqJK66WAI+M0ggc3TRgX2ar4l8%$#+1mJ<&gpGD(L0AXQ%Q!M#F_@gvy6}W} z;ls^CyiBz@y31Ln{ro8Q9c!0&DGxK;uOEWniCjWntp4DIFW8Vq1+NSUMc4!fF`(c` zgiW##Fab+6gT{nif@E+!f|GH`wKr}(UNcz2LRLH>hmTmfv-qs?%(73WOr7$;=dyhd zHh9Fgi0}cAlA!uZ?tlj&J|OB`Lvb1m3LE~1b%{v9z|)6;3PWat?!ZAoRy;ZjMPmxu zq!O=Mlo2n;@kEUDe&oyDxJIi~@J(H2&m8TnG>`fR;w!Zl`t`ghBchSk;Hl!$I7}YI z6p=xHV{jN433e&Mm;s0tAYXt8FdCD>#ThYB#u)*{R@frmkmH3IanS!VvL+fSqw+>CkuvUp7qsIc@G0XGk1aM)-3S2RC2p^*o>0nb)F|fH{ zVUAm)fiVR9e`BMR0IgC@j>1PlV44?lQ8w=d#jf@09i4BP3(U3(vSPB$}8 z$g^|$ZRHA2U!GDPw3M!3cebIjPEt+)24cBRf7=i9k1Juvon@B@@D7Y107>eJ1OI_i*`Fr z`<(4n_g#F;qs-lBy*R~I?@vkQY?aCyB_&qkxbI5d^Bli;(s`$ZjRoeNp%*`89%vW{ zzr4KJG*l(fVa`6KN5@pc?@c;T#*Ay9DR#MF>x2F>tL0a2wb^FUUMn45F1@dQlf+u* zVUw0k%RX@OSx1k>j={UbN{*iXq>@jPEOgHCiE3FW(WSQ5*Z2UtH$1%LsZOzZi%w%| z-UBJeswMF+=O@+Ny&@LvStMU*y{{sC-0%tC)5_evuBgg2$4ylrBvJI2XOEl0U%t@O ziTu%gUB>O?faZaa(j~+$7Ui=%yF7lx%xu#;`_3ll?XvVFcUdj^e#fP81n=JBP?v*g zc~m1SXa0kQww7;n3myf$Ua|Pg{&8!Us&%xT$-XyBv3ACk+KZp9T}MsSyVfDo<5XG_ zz4YDt7TZ{>u zPx>l#HmQ&BS$QV2rc*g6H@(3<`as3@5bep?K_h>3zxn*F?19wXs83hY6TZET$_joz zx*@yzjmgagAF6k@@cTm^WjrlqZd$n9d$6vK6lJQQ)sr51OHJ*Ft-og3ndZkzZuzEm z8TZMd`5Hk)%Y#kK1ljA324f=jKj=xeI%4YSO9;^p@Q%_8o^(^e_ckwQSs6KH_1C^Y zMV9odQsoC`8C6~T(w=B#+MHqrC>*ft*tS%Ep!-;_8&Rvn(4R+@*P%JyiFh!vBqu5F zfK!cWQS-q1hXKQ%1|K@`Y1-~lS4_Vg%$aRiR6}~8U|Hr9EcVVlqj&4YcdV<0Hz%14 zW|e41o2{9te6(i!oXHkxDuwO-rWXeZDrI#@@)L}#QHp#r!g-3^pnhncUcUXNU~~AH z<5~W9-xG4fXr!#eH&} z(By%tkBcp%t$3ZGzy`84kf=b$)JnugHf2(O#u61-){^V<%69uT_r@xmz)KG$;px2X z)8xg83WSbOolNgX*>8MzbCcudxlJ4OBkI}N)Ut@Z&DEO&zivM8q3C@bBxzX<*~C!w?03unZ07=K@FmTRnlQT+Dny`#upnBaW9A zCerYM_}~4p)-Xu`$TMI!5tj&S4VegtI1y9=P!0L8;fDdc2w8w24DE=X2%agpQ1|_0 zArRgJ5DV+s`S$xq2F@ikv?c{V{4ku6V+hKu=p}IsMDz>D`A7G`;RqOby&)=s1{)_? zYz_wSJj6M{76>L0e9jOY2yqk)!SX*b2Cr6>ja85nh}d}R>%89gOG6zgAeP1s;t$ik z^$D%WBG-Sh0U8k4so?5og0;#((JHW;5L9+(3kC}IhP~p@CNx1r>BKvDt!NZIfhRPI zg5X|-*x)4YJeRk&h$~a+O<0iSQ=WNy{tdkJP=cUQqS7@8@v$E6y450Es`(lAnK(Iy zHcxe~UW`Td2Gh5J9b(HaeUQm{*IGEX|B2(pcQ$&NjN7k+=h;rI&pD>=dpg&kbO}YT zr{*+ana%^^$fs7Ej;@i6`m}MCdG@iyT^!S0;v=NU(@vQ+7EZEStE8umXd4T35Y%=* znGS~RAjGuZ+3Ux)*mmwX(itPZul4ISzeyGX^mmA9=)V8xLg+BqGXql%Hi$t_QISUj zBBDt&h<0HB21JD|=MV@aaF-j$;&p<)FXIYNsO^xOB^a^M)zf-$oMAT6GcY0T-DI9| zerq@lFFlmN&WUO#`Ky?!^}T0Trm4?gk!-kTT6tkx|G7Co<`|#XU;Ro`L0)O{<9n<8 zgF~J^W@UIibY-q6323hI4ShDzP*_lMJk^+Sle2dx<3JuZq60mF{Mfdgj1hQYVsv01R`8uon; z?ejK4-HY1|LRJvDP{hjoXOpt59C*p)dso?hv@7!uTHuCk3z185FDUH@4E>i)#RfnH z17a027l8@sZ%af~~5ZC2LjBAQyx@UUg|h~>eMK;RqIv!0w+U(BDPb zh=BUnAU4*#p5GdsDcjg@81Q3hvbNftF14cH*^mIqDsdF@gaTb;jDoqsLg*%sMg+(K z%s3MG_NZX&!oET#2^@@kA`S3iL2ESP6~l5SQwvXMx`2&Fh(K(-Osvm6+k7T?<%fFP z6yN>tW=IO6!|}g>1cD_i&6T22*hYXudX^98NBNlSXpV}S+=4eOA>CA$AJ(T~2((wR>0$d7O3>_8#n-*w144?r($k5p&u)&=N>kzn9@WwLT z{1^3gkQ0fh*Sp`oYoqZav7`Q~JMXC_o%ZGSZ_3NRse>OLjy-{?3@6|~q!*tANB{)B z0Y(E371+`a$Kn8lCg>{DtA9~P-?9!-54&&2evz>DOQb9%v$c$zl%Y3U0WU2~{_*Vq zQG<2QAYpuPeFHEIXRyG3d_GJV3=XM68wFqsBtsA?iM0F%sC+%5K2Uz2bn<|f zaduo&^06tlgU9a}ei9PKAH;iLyiS2fCT?%~1$cT6dD9?_8z2X02LNMX6A98D;gA?Q z5e`U#uuXu4Anuw6`!ewkm<;6Mf*CBU2tUzije>gvVq#MB$Mv~E)h8{Nyj_`>VS047 zi;+NQf&zjIZvq$s1WQrj9>@#CMLV1XgTWDD3=Rn*Q~{#rvjDQBfGmS}S#axtM~4r7 zwxR9KlN0eOfgM>iA`v4S;f_L#B)$LO+o#2%j`|qey(@e5)lL>0_D{$8=FV`rPgzEHXq+pgk3Wd_4l(gB2mnwqI)luEb8%=8gg}D*<#4bC z_|-r~10)w@80$+)2Q)~6+Dl_K|0#!iBfeie$tQ@=w8=nn`_K7t+Na{?Ygl(hvIw!JW@#voSaz5MU<`5B8O@;kYU~kp|((bOtCJf|VSb zk5?o5>DwS@E22I^ucVMPyG-g%V(*~+3iI;ni6t=3M6bU;9lY7dC4+(Zh2u-8TnIRZ zGfV&$29*kY>KrEY1Ch-UNMlT}ep`!tI*cX4HbgvbQBuUcTJ17F?@z}I(zZ-|ApKQZ zlr%hZ;NAoGZ(a)$8NzVj7yt-irvml{SR3pz1pNWDd$4?1d~lw?=1KwXhN0ye2Oot^ z9|!NTi2m@A+ZuX)I6NO?KH`~{d)N743-A9y|L>1yAgK?BF@kdnW57;)IK7Ashvtzn zuuakNCKM)YIHZsT#ED4yLj04+$H&8a93sB*aD3*BMB7d7P0_ONlqO0fd{HRHOA8aJ zC_p|h!|VS*#v$|wL>}7q57H0LiQ{9SIDtV5;RSFo0-F!NVSsKWa2OB;^B>k>VLNPx zs^bwW%_eACjoM{Bjz=xH$==e*FCOwK8|*Ft@Xnbr4v5lrG?2q<`3-rrI6U*a6<=i z&;c!mv-Ti(9Am;y8Wn^o8|_ME2!=3e6*P ze`v!0pw5Ji6`dE`Jq!8SL|bL+DDWl`66=tUt)nivH`WfqU0R>YMr0-Zx9sN-Zih6$m{cGh&q>#bw~t zT2!qNaeyIzHyrc`XDNVR2YE#yik}4w3=@`8w6g(@e4{|Dp};O6y~ZmByFkdxF38!5 zcwxwkX_D&pZ4CXGIX%3s@XgT!bJ1!ea!CSjl5-&AA9exAPaX}NkO0%csfR4!0_~GV zK0pXofP(?~G(Lt6y_xs+bQtT#Ar1#7hq) z5D%R|M0SClW$oe4GJ7qV7Y1)1Nqm-1Sg=(7PjD}hZ5ku`V0z1x1Q@N#~r@pG}|Qhq5bZxofpTbf5`HfALb#qY|fm47gkYx zajy(QU`xX~_xJ4uroDZo?-eL2S}fI0yL(wEU62zyRo`zHjIfkn-Ls0L8{z)pg*#bA}j)lZ5begM!}*`JIc ztt=HWLQ(bUF}->0HFz;wf_%?q^OvEIy;%AlDgwCvr&*!txCFFW||K=gH!%5m-qO zNC2(R0n{3r1p`%_346=IT4KZTy0Alt0m>o;g1GUXp`OF5MGD$aKK4P*Uc^Uw<@<LTdmQh6Cr|9Az$r zOeDjZAfP|d;6Pge@ov02XlpqlJfTKNgL@w$o*_B^Z1Con-uvg1UcXj1_Mab8{R%HV zlmJG&U7#40ZTzNU=QWco=1DTt-`VW?+GsIUnP3i`PO_{98%YTs7WiHcsm5<1F_3igNu4w@VB(33qqgE#6QOajx*vRNXP@A0IE>f8_X# z35iZg1>r1%VWZ=Z&WzDXU#qto(Wm*KR1zi(-fH{d1>HE~xenbew3g6Iv!a!v4db-D z<(8KIrq+h1uUzOHytGR3^FAv>?P?kR%k)P!x%1XO)OjW@9d&)Pq1xOh1)n+|%-&&` zpYI@(`n8E{oq4f)pl`?J!Jh9rm)8~SJZG^bi#oq&NkshP`<Gp-jIkT1|EJ?K(t8=&0$;o}m@Q#qrpPJ2M$amEaL`R|iwVx~n*o4e=1R0aCUjbXk{(MMkI)tNHbbUq;^sNFxurK-93+T!pc z&FverVh%Ueetf=d!`PbH#RKG3Gxy#;E`HNv;?)<|YJ+>nTsO_NJ2)sa*#6eDIsVx$ zqgUl#7q@SmJL5%~Y03oekgYI5X`D9{3{W!O*(|O+7 ziLY9pd|HT|qDHg--I?fAw*4wc^ebD54Dmk=^My(;97`1P) zx~u!bo1@K4>?Rnj3TLicYnIW~=5MbbrgZA-)KvZ2d+A5=^Oqe`;@_OB7#q0tZTGQ= zg0BYEC$f&pjoa*F@b=I>sif?5qpG)GHd-uixLn>fHSNNYdhuLoe?wh2UmqtKZHE|_ zqq@Q0#+LQHsz24(+Fm+ADdT~WS~t-xq@%EC;%(I@ox^S^#!G9zmuc$0FuALiRqA+} znMJUwUgIyxxqCBDFIGxEYp3~V_14jM*i+-CC1xs)Y2$=JgO7Y(@3$oC=4Fq|*YET` zma)%$5;<&s$RbZI=Kg^3%JWX>?)Lm<^5apM&NjoeS;wbN%=M7${=Q39dDz>@vQ{C^ ztAA7-_1*9B-RHrxM+-{sJd#g8f$e-aCOSXyqethldbjs-5*zm$1lT)FzhPco zy5`a*=Z@whUc;Z&Or%fR;-#V7E>)z~wqRD4`m0OF74r=2rN2w`Mf{lmBrJF2N24WS ztg}jkUzHyz%+g)=y-M?h=lNc-OOb=+@9Y=6%bf1~wnB1+o{wAOk(>Ql3r?(_7HL)2 z7g2JlWjo_Y`SnDv=sjX@p zY2Z`LXCGpkRo$w7yL%cLy{ z-QRw+&X=o+30a|dIXu4U!mJCYFYdiSBRMLb8olakPng9z+M49^rR!Tvwm3^V?YCdh z-4ISGowQ(~s#vTadDENQN5`&RH|i06#@*(VQl_fm=23NKsntyTl)A0V^cj|4KUn4D zFoyb(^>Ir0bq{jB~QfHmN|q@DkY)5pXDD zv)p5b)uIE@m zlcVm@E8~__w~&@pSMA+nJMv2R)W}Lt|E^0#uVmg%a`1m`9AAH3&8F0FdunY7wq~1t z`nqpoXSV8(LM)ggdlxs?g`6LN_jI%xT{ZG}aNx}3w+Hwc44c&ws)k`K0jPuj2})$a zDwMqh55lDooIe-|ZCe7@6Z~@mJamRN1iONj4nAcbY%1bVAfAgPuy4$N;5ETIEF_)* zIR_B&J3FjKpHI7dD{*4oL6fY&YiYNvJ@L}QItPsl$Cm&NGXpPN)6EUDkC~$ru?BYh}9nb;25?~e< z2%yE!%@gu*2;L7OKI})9R6fbSVymZGx+7bf(y4yj=}*H7)JDlTC@(xZ@YlIy6gXZV zbV5Ff3*qem7g504gtiBL2zmDb*k(W{4gC`X7D&${5I`{!a&Z{m4 zQ}N{D&ibXYyw*#1@X|vGY@$lM<19&3un)GKuTU^@@OnGLC2zU+Lovm*hh!ZOsW~1p zw09Wu>oIlFIlcl+qgtF-qj*ucq!nb!v;O z2x)ly2X#0oAN=Zkh|GW!wqPFt7sBP>phh^Y5RNYZ?3hC(LEr{ipo&}e;`M>t5`H@} z$TAW2qrTdPoy+c4&`1Bcv-H9A@!W44`+lb`Z4Opa2;>QmOZe%nAd)C#umjLPD*~b3 zL@u-ioWug*y)gFBzw7|!O(%r zT7-{mc+Wz7y!+~!SAKnK)!9=Yp5Bl3j5r*5%oi0Fx&DQjqCj%k!^nhVOJOr34Gv(Y z^3lEy=nZtBCkK#3i1;N70I#Xn@!DpH&_4z_*@%9um9_cXU2`|BmcPBkN?~=+tqCdu zyHd!26yW4iw(u&%R({1=15S-j!hzY&qqD)K2<~^}kwz{>J~+a`Ck`h;Lu3YSnOF%x z$eAK+9EWO;AvPYAoUfd^V%X+W!;gybm6v~?xugn-YLN@~Vj-ycpI$78rUslCPJoBQ zxM2$rgwnw2U#NfJ00r1_3MYBOmK&Hk0!9{~dXecV92Yr|fv*eqc3NF7pskPyUwTLB zeB5Rif9n;3~+v@PaBC98tS~5b!j_Kc-ac~@uqSh(pv@mJ( zp6O?o9`RrEpt$34%R$e*^Gtp22Nu1M;-@WG`*J~^(#f|}rTpLry{otOzbCz%ciUhV zHMqk-ZHg6f*VNw5&;MZ&O)~{~L z@Na18e$>#ym@oZCb-Sn36rExYY4n{Lg zqp98H4`Pnq;q@e~I3~YKI&AI{v$3iV@^;m7b}~B?zBO%8k#;YWOTa!nw`${7CR={d zU0Zo;VeF)V;j!X-^TK-XDsM4b&fTq=-e@)ba&&a%sbgQlzn@-|W4*;Sx2|nX5@W{5 zvlRc8D&92@4a;BZNc&zSqyC(xg;Ft)OTuB03mIrA94~0ME_6F@RB3=D& zGylN)EO*ULOco3S=h;`|&a@i1XzZS}I8dUkN$Jzf14YN@%bkq6K7t_!Lt)nWEd}B6 zlb&>0UY>M&)9r#EO8pw&Tl+iS%^Wpx-uQOMm6TH@o)uGd_upN(R;Il=*|9vq-|eZT zyTxvkZSw*o?3;Yw@*Eu#8?+LSPCO(ROTVyz(Ye}f|Lo&U=d`%@<(-q_ea3FibmLUd zI@|jBPJzW5oo}PvQoJPk04qGmorV<>T-u;@%Q(7Y9YNDAoVR z+M9q=)wOZsWD1FrB4wt`<1u7vFlH!2nTO*zW)cY*lPRK-F)Ae?L#9MXg%FXkGNqJ~ z$W%!3-Rqn>dEV!FpZEV=|8HN{-tE2H*7}{b*Iw&h_wc*>3#z}`jMBawcxN_u;#xgd zORgxLBJS%=er@#|(ZS!j=S6S4-cgZrXkyAwvG2;tkWg$4Q=7euJf(Mwlv0v|-obb0 zpIjMO3U!pb?z^w5+R#n;)6cMdG#vI=Pmbqw8$JBXZ(Y8{J^fj?%Zs*cuKGF7_B)IB z5SfvH_B#iT>gy~wYzxq@yA_s zjnKX+FlAcyX>Z?1%@2<6EIX8fRSKJy@6abzHWqllO*qYPkZRj*^~%Iox0)0$)sHdW z+Lc3)Jogjj&%JBjG18LWCX6}9MHBiDXjcxr-~6cHx_)o!V0It;-`ee;hF&n*gGH1` z1PPfAq)>d*wa0fo5Mdw2V?CeuH%$N zlvg=0u{l}>wH@MjYDDP8T4#67cYkXdxHoV~Od?M!eMs{Vn;uoIZOcKaw}wuY{9di> zuZwWLA>Smj&3kUXal&t9bw2#HEzfk)(x*?pJU;HrmyOFqjk4Lv`%+!T#`svO*th93 zR||Zke6?3$&cWW^;-;ItB3rY=P~oAh zMvHO%X9BSmc2$l?Cb=@wL)&h&(@Egw=Q^%`k^DY{4Y+US+vIBDyP`XO^MHhkFd6lm z^2}$0#Yc`D)C$eC_C9QF8KFY`ekG#oMb?*&)V^2U?^KusHaZB9*FTBwv@^1eOO*e* zY0lTodYESH#3Q$+xbO98xWOyw68ldIM=Koky|H*&TVuvrH}XSKdh9~iHKPb83aiB2 zc-k1}9;c5b%{yjJ1r0`QCWis{>-ygV?&Q9!@kxc7&1yI|Q%RV8Rk*^v^+u&y!aonV zGILyMJJM6LkGr1HQg&pgOb^i*t|lxi)OFFDMa=M|0rw8vC`7&RgNwx`^LG!=i5|%o zQr<-uzi#gt^zo54!o2%?!2QNoz^bVKmesV>{t?3k+%D;BJEJSDwZrA|l-LEg(nDoe&&E({zOuaE|K!^O%({DI1&rOcUP>$2-!0QqaPqNOFPn7t zmJD2($RU-;;3qc%UJpE5yuA=LxfsYAMIWtxVs_+oWIO)wfl`4+muv-Y3=^4XIn27M zuA5JIR*V~AJu&IdLMdff<}Fl@GVDh2{@(@XMGIbYROAf!_|}Y1w6FRk^%~PUczJ)0 zsj{EsO&ZhFSzLJXcyhC zA3jFpUb^JeP_oFo4@P8#rq~zU=c$HQqcXnF^oVEd@c25YsB<7W;naCuiL-4!8wwQS z*DVcxYW;HE=Fy|klIuI!y`UIw-$A|LMsmESqJ*sG#XO6PUn=Qbj>HQSbi=>90piopL6&QE14i4f#?U$E zuC`ZNE~;5t*@e-2aHmD+!yA5F)j4sB`N}c1`TmsA;lALc$r9DhfPubNFXN~F>ozx0 z=04UGH!{%9(oB1$HX$+{pYulbc^`h+AbfFhGjFQV`=?^b8CYwxCNgI40tZ%=m#=q^ zp!zjq2q+E$?qhInyGFhN@hFKL@FOjn?p$)W5{{{n; z3uI*o1_xGIe)V+@wbO3=%hxmt@9&=B86hDcoctRMV1j|j0PuvuL$nv<5Q5b%7BO8T zH)B{`0tp;Y)_|>t2m|SK2qq^|Yutrv1f^)T&hg_7(fZd%CYpEhZ@a|W@s^7cJ^W`w z<|ee({iEesV{!sLlMK*&z!DeQLIDyo0R0-cEkZ8=N>K0>fh+-7AgLpo&$T`fO4Wh4 zf;t)MoE5O&Ma`kPnX$3-;hhgLDmq@(VJCjbMYs17o+cg8@{X{?BYCDcdL8re#5rM! zF`Ga?YX>espkRZ9j>7C%y0HM>s$O;R6kaxgAN>&a$t|65r z5F3CG0R*!{xG(X2Xrn$!@X#SuUJ2LtP~}~LEI!RYZJZwlW!|!S%$J#bb^IdXY0`nv z4rk!^>Rlj zA4kRJyYCkJjfR%K?^wEx4-guD^?-)bljEK0>deCH;MLK4;#;Nja*A*A;>8DD-d)xZ zF4I1rbgC=0n{lF4`RC;Pp1QU@_%WVA6JNnBneD;qF8chNDrJX`nCZGkV}#;xhZi4- zeeVrv`*LVk2!$-;cK%+D&Q8;FcCN?OV^dGGm6R{CTB@$A(+jBi$gULEX>Nr#bDZRT zb|d>v@3aZy{Hw=g# zb4uIPZA=l&N@ik87SbwtuxNA^Y*~!=mZWKSb@jJW-fXq^92=7Gp4-8PiBV?O*sCg);s$KVMppE!BCedhtn+pLyS@X{MxU zeNeq8)w1ZQ!(!f(HxLNusq{J}Ap2qif6YjM-PPxt{H*S}IOHCn^s-Fmd3}At&!|Jh zXh!*`qs#M+N#e9*8p-wk5jLnjSA*$9}X(A zH`7^^(X!Iq^tQpX;d1ue&hOKK{y|+$Rrz!k&5IA1-qF?BC_X!Um&e#!N1rK%QuKbk z>SCW$vA^S)%K?)&J_pJ0$uOiFmG>8uF@`(}j0!3JAKX;9Htqi? zX&7{~?(x0*1MBCy9-bo?zfS+A;28g=5;_YiUd4&#FNN2BU_W(Sk(6*7Dkv4zjhWtQ zUd(?W?p6_Xm06#pqZcP zSSmfk{8)nPmSE(GOJhM@80GbeRw1)=?rfbi8z1~w6#QB-)^+N1VoExbZd}HT3coj9 zo$A?s>6MC46&HQn?wx+FnvUl5;4<6o%L$Yzpy7gwQd^d5AQmP87DC_*yKlgp?eeHWU zsF=roRF6&4EQ;;BxTHf=4|C~yvpREX?0nJFlx|TRhP_^sO;Yzrvauycp{*9HNQ$Xv z!~Rdhr$fHat@MOFq9ni5vgqP4>35PZuiKV_YYsCxBPQ>*>aXl^UbRl*=2dU~v`m$7 z>0z0VbSKYjx;*+goLxx9+b>Ar>Vy8kwB!CiANbOp(XWxs)l9R_pEuuR1MGnf{_nRr zN6>pTd7Qs=l6EXGh|gA3i|SE6^$QxunuJrHE<-!6=4|y^Z4qO0E_qW*a;lxnEzK7!(NB%*OW^F@nC-nRLvjx3YRY+KQ! zy=MDgQ0$O59}sMbr1?QS@U*k;SK=@HY7Vtyy1pKTIx5( zIJb9AGeh`T5(L7@|7sEma0-$J;%o2^k_Q5AFoMYedx1Q}X949SxRlF4$`GLFMuIHi zCq4gN{9&SUZ*B8Dfd4~HRLi66%#cR8zCtH8g3p?nuNAwp?d$KqBmVpUu6Yi75K26_ z<~1P&)&umK?laLcNHI$-VMiPgu~T5RkHA`+3v0%?RM86Bndrm4N)LGO*+tky$50WgJX>u z6-;IjG6=y9&~1SoN)pjD0ow&6ql1tK#7F|tHiQ5Wmpul605({)5^@cq59l%Mj}VS4 zkROsq-nij9xzgZ`*h4qnSKc9vqz#$i^)^1{V%q!WRYg=Mc_f>bWdLJv(#^};`EK14 zKRzQmJVq`;|KpMS>nB;Y%RZl9e-lb^rCMIFBwVq3OBel+K33+DS9ogr`XdT6O4&u>Z0BoWxj5SjyVh6l@BUaCdx+ra$JUAU$7 zNg|9^e>U)xy|~W{H2)g4yCcRnDw9idwIuVGjBK!clsqYOK_KntGycjg78@yCE96`F ziWdU178Js(#kmKA7`X%YxSgMP=CNcdSN#neAo562TZd~{v8TXxhYpns=KZZR>9?@$ zN=Lh=rNiY^Z@us6eY^ajUdJZ+S+H+1+uOZj7g9G#>G$8@d!!+%*|NRR)8uo0ey01@ z*Vhc350q6NU=}ST^Gn=+nC7Cz$$UN6eVOd{AQM*N3mz=Ae_Cj8YLfq*Lfy&jLj|UdK!#Yel z^6I|)u=~VNS&a8~87eW|8MKwn(<6&H$)AP2OQtqp!<#qQ%fVK9I%XA)^n7H((Mb*6 zx{gk)NwZ{6b2r^K`9wL{SNdats_I;o{SLcLnm!s^+vaq(k1}n^FOazZK4jV((vrLn zyhzJ?!^K7Lte&>M-={Z^#5kB2MJh|~4A8Gn9aL1)_fa`5fB3#YjGeln!PGMijm};h zdxraQ=4kEFQ9uASx};1jz$& zkt{d@fXNfMd?Bj@glvJSQ<6Bt8XqT=w6-0Z;Z75(KkW3o^Ll0+hvJ%XGdg0%FZ1^q z-y=LtI*_JC(!jlDribqc7`)`b6&4HLHsAp(gHm0C_X*@e1~Mr~(1Nl^N)aazWAu#^ z25!>(Xo2fygyT{i&ka!?y3)J&7Kb<&+rEIFK7#B<_zS|}Uvbz2qa7Z?#6S@Np*wgf zNZttfhr#TR1IjJ%%LR{EFaTqTnvo;qgaX&L$5Xh|f^ZmX>u*Z$P5aT%7R2!E&Z7e1 zqfX0zFrc%4#R0}{L_iAqIW&&|pCDL)AoB_ z-Vp5|DClxp&lSfE~n6nzT7xaj~HgW-#TmcT(m zCkQSh&Ok?rbIMvMNHT6mP<$NgUK#3h{qTBGu+GT!{%|Npnkb%7AK2c&eH|hxLBorrfMr49t;$`J1nejLBC`Gd>j ziY8%0M#BNbH4O<09eS5Y=|<`h5)iWn%MOSdhTbnPsUVF3sS5n|LCZ`?xF&<5qJV>r z_)1(eGk&x1j*-?k4bNu-C5}qa<)^ouZY6+L5(Q1vD z37lq-yaf6_WUByS4r1Nm>wvIp7`Gv^9oSYtizx*e1PM4yb`c6)i=zwfbRrx+26ikO zwKYdN>enxv2-monH|JnQc$#$hR~#~U@XJH`LEt?FavNBVfJzyN|6uTj{PjSb0c5Sx z&~b@4i2BR5IJ%*lE`*~RtKw(*%rZ4H_UUr=Kx$YmHS-b)0^x+}3@9AYf`QZ?Yr?T0 zXBmp(1a~0}knAA_ZV3o^l|soYWq}Y7+;kw?6iDcaIEeFt=9>4?1Lbxj9KwBB`_0nV zOI}Qy&+q#zH}5aJ?)`t@p#AT5YiX3h3?vX)1u$7d*em#N$^Z#F4z1Eq{5ec!sLv2R zmrQ;V3SQeBNOj$VV0^io%Gf$j|D<)}Gedj1g&FKkHC~Te#44@^trx-nqD9%6tWarf#hCJU78`y?XQ*aoA+qclTZ*& zsA0sVo`uuDx%B}7q#T64L;MrCEZ~7#Mj8vU2GDju+I7f>gOig3S_FF>QL%2uOek$_ zr+5W-ULp{Jc{$`~-^BCs1T^`QD}}}#%#g4rJWV?MYo~Ak_J1^3VZeC;5&lU^LcG5n z5SxQT9elkI%n9Eva3T`E5i<)yq1tO21G&z?WZ^ zu&&edz!{p?24L?;KwfWC)mdhoUtnB+$66+bf1M*yqyrUvV`%>yd8o4;QTzp_ zP7pC6z+;L7|8JO9fOHu$0fU1oM793vB<7(|3PB7ewZ|}A41*(5!42~U#_|B6Ef3tr&xU4XD7179($QGlZf#9?46K#Znn!Uy8cU%Z*- z#J3J37zqcOdWR8?zFQ^yPFD?t-7mh@ci>O>8orA_&`Cl89W@yCJ8*+E%fqw*A?ZMl z32w-s4nLryNGK63o1fc@TJh(z!~bGBe1_ll^+b(kJb}R z7AuK<@Cm$GTKV7-`We4#?f(M8jAzCsV&iAbiw_e*4ppAyL zAV}hf0||(-WFeOtfwcnUKN!aCK{!Rp zu2QIf7slOl8K{ci2>(ag{4)57`~iCnI^Kxv-MQv-D_`OipgmV7WvTMexskS2}#EAC#*;!Kd%s z5mEl`cwo${%~^RD45p^S1ue?UN2a$28(-w%LX}&>2nQPoB_z!6;Cd8c7K&3M@5-6b z_da5#9QdR!-dI9>jPNw+K(`;>UJzW8{FwjfvVdL%5B$uKoCE_(Ka}893X(g3Ck7;= zgH&j+!U1g-2=;_dWO<2DCUhkd3?%=&LohV&KY3jIzUbSKRt=s)Y^zuvU8?NKih)oeu~K8}~Kr$`8x7OVMAIvedjvA7rQe$Hztk;x7%l_cuueWE9A< z34=a(q9_23Htk(w!<~-^#M5J#t_&AZ zuLNe$O5)?(4I?zTZTOz z`YnvSz~@bziLJg9N>W)<`6swDjwkH5&!AA_ZFO z{b2|~$&jH|klWD$9~3{35Ez9yz#jEGt@07VE=?_ z=bz5-)Zx zC2|ab&Itr5@S#e;glGq&Fdn$H2|d9&j8H6Uhu=uh7(RiJeDqa(@KwNdrPEVTp{159 zDaA49H3xj$sBglc7xDh|F51yFu~Y)0&d)(;8AtgU?# z?tDhI3mNs(Ojrpy9m6guaYqaDg zZD6&CZKZp>cLN#4S7X)s>J!(rA03)WaNBIjw9kd{yUx#>u@n^EY@yE7sbXYI%UN~p z3s)ZT(fA&q)#VdxblKiemSvR0uak6BOQyZv*)Kx1sbgA^$E~CG*gd(IUGjH%Zc!cB zJuI?)yN>8hVb^oFT@Tz^qM@zgvPB4NdF^KD28mqlKurdT$D}ck-4tX<@HNAN+zLud2KIQXQeo*xdbF z?dZjPLA49)xfbhGQ`>Qvug`$yTCb^7NEu8lYqx)>9h5@gB?SK2U}*xcEm&PaFM*{F za5cjB1DZM+dtfh>!4YepBb2uGx6i14&HA?MH|YlZoStd7%pd-b5&a)( zm&3w33c3p7{|9Rguoxij1h9-KpiHL_LkLSP2pJ-F8C%J})sB{IGpP2r((UUHNvR4l z2Q9WQ2P}A8*qTm!n(!y+@DH^k4mo=uPy;#~h#doVRv12@vBA3za)<(1j}+Lk<-uq~ z`0cht{l`mq@2pT9>aeIa2qR`Owo^4rxhyc{Y5C&j^DkD9)(4-2|08W6mc;AnQ-Ecr z@bS8*lJ|3#()p5YA8B8hl%Lc(pm-veGHS$b@JztSR-Hqo+g{Jn6o02ST_Jnut@!k) z>bU#Wd7r+e<9nEUIE)QmHV!LjjXw~MHHx~Sbm7oR;l>Bll~vRBHzYWooOit(F#x{u%M?mn{rNJ8sbnm5r3-bV~#{e|Cs zK25#f$WW$bNc`c7y4~EgU549C>$1P)Hyb8uo3a;%I&TuS6~0~PZ|r9c*wB@o`OdH} z#Q2WJ8T13OAoRcnG6$($=HYq{y_L({o|NB2iywQKm-*9hwR|gyt-%qVCLQQt9->=? z%l|HOfF2zP?_^*u0S;nV-r=#Z1eJqCHF8*3??HcoB@gHch=Uq7iBL2sMM!AA!ku}9 zM(Bw5lm#|x%bmD&@8tO(tP?Ts|I4rfvOH+`sOaJOzsHn9ZWPE@jR)O53~a#Uiw7!g z3=XYxu%IZC1~rL1(D41DX2haJE*Uk*XIYWs(f9t6QRG{_(-VE$Cz!|c#(zJ_S`;(xEV9wjZc#iB{9D3BxOq~T7Hq}{tXkAf_7oXL> ze<4xBr)a#VGOkS+)1BIoaiV_g5j!^`o6Or|WsWb%Hc> zEk!C{ncp3a$xVz%k##IuQJ#){xngGX=B)p@2mPgHK3)m8+>0>Ng*4)9Sa*>e#%bYw zg|p4p%zBo7G8}`YZ`7`^eEIA=mP`LWcyGQ^;oSJ__jMlY4j$b}K_$2A#?|bP8L^m& zQcTWqv(Zs{6$wY_(_Y(cdki7)E6{8}&kGe|xth_~4OwxH`ZJ+d@D z7Cqk`tv-Hdb(JqSj;x+p^_safnUb02C3vBc)u+9Nazzba5s@>Vq)H zExvt8E)7C^)&*H#Rx0`aDS=(3x@U2HuvtpVUCtr~nE>A1Vsc?MX{`-!Dm-sF)D-y>Wl6%mzCZzbCFJMIo_v5X?6WYUWCI13Mbr9zoqFRyd~Hf=?U%h2H1GJc^X{5B_4-7d*q%6IdEksjqP;84 z&q}4%@w!r81qRM1`P1(zZto4J^XPRs+0S$Raf1KucuM_JIlID#W-G%3J9t{|>y=acpWS*`z2@jvl7{Lf6x^QFe_+#K5cG( zmpOdDQvd1EYWB_2ybnFLt$sfd?B5gMKd1Vk-Mo4I{Snq^>6}R)D);7Q!~L{}hgg2L zFCXTXvB=9a)$Mtn;OM|k74qgsgoxwqCArTjeu|f$G+asX^|e=i=|1QvIF@lVGOEU| z-1Ga+VAJZP8m2MwXwH%g11hOG+}>lXg61dYnXf(hzB6Q8(R$?AEz2`qvu1f)gK9U= z?!S22K$2`v%D~sUD%Gbm(?NGQ^3GRgT=3iK!%Vkz$7iLev_~>m`#1A_5MS)9i}^X& z8);hV+$X%>L1){1=3O%eIW~rtCM!O6o07W6MY~wpZc%(_h)Hasygf~Qt{?AC;cu{4 zQf@^2^g`$L<_%f~9nNg*@^1~UM2^M3Nn4NmEKs7Csg+P`daq|6b-P-C3{E2KCgnBX z(rLrY;}aRJit}%(mA;IGE1i$OsOvznM1Fou^r2Lk$FiXCi7hu@Fa-Xbx%Ay4r9aev zdP3uQuk7s;p{I5qG4U|nHlpAxH!a#fFcw@OAh_@(;$jUMUcC9)BwpGw99tv&t)-4b ztm9|-d0AJys2|gY^aaa8ySgEx5EG;w zpz)O1tMSyvoAt@3)At{ntyGE*9ylNpX*Xl)th;qVg)+ABn-Xs$SJkV*+mAU+KF=Me z5wPFY5bxIznJf`0z31F_njg>YdCti1K5r3Tqol&gand<+A&XB*aott#!0>FTqbg<} z*)1vLe^c~YaCd$wAhyn-by9ZM_57YU5s5~8dQOHE&%R78)2+-j>M$M zl-R1jg5^|5@a#>)y9b^)9h}O&xA?Gq?@m9-cUMdHxB7~AM2$LH8ovE8Kdm7t62;FF zEY+aX@yw6T)XbeH zvt*^VvVAIEf9fr5*%o2Cn;Y3*4BkIh#*+Q4dX@ZYQub~%>o{o<=APd|9h#^XVE=}u z%uLtrLiVlpnF5=|mJMRE;+|052qrvDI{brJ1^EOe!B_&4D+;^=`Uyy3fZV3QnG27% zR{*OM&JGA{iFz-mNJ3G7fizVv!kq7Ie-1yt!=+;6i}|+*5`pX z6sb7=NgImvu3$5PU=gs~01XuoU;u+MT7*H?K|lbh6;QfNqHyWdPAC_x%8^))T44$H zMTBF)&3WYXRUxhp+mNQ08=9pIMR>{yPm>Nb&?;zU{tv5i3<$9j(!iB!FCzhrgOad3 z27+fW$J;@if;{9S1EP4)4HINcXJ%9)MaUWuXsNt}K9@QoZkK<8A(&r-daxlGNAu--q)(<~_1i?g8(;fS}>eSBpM&mhnw^ zYw_5M^*H{xmvG3nKa^RgW5(xzFekoFv9QpY+9e@JjjEQZl4WPua|~l=+RL4vD$gv4 zM`c~pPhXib=bCxgS!^_QAT__;@a8uqDaHMIi5Tk?FJasCOH+3ax0#k7m{N0*tYtNH zrI>psImj$~m}{PWe!NnZbDqAgD9~{tZY)&R`GV`#2{vw*3fmBe%VYk5wv!9CZW9yz zdxzWhSr#jtZ0IRHainl8!Qs4ijps7mviys__HLKY!py9~yJb)3LN?XO!Zs0~`|oBk zfxKRgt_ca6mN^RS5kk*h3}PbKN=_9?v&h_w!Y|EKCM){C=hF*_*X6KCibJNKP-LP;^0>TM;Tj-To8>^xZg zamyZ|p~h32(!ZRrI_gmBFv6?Hy|pLdR@5X{1hp2<_<$3eN~YYgANRw!m1NCjX5!O&9-kj%aXR0 z;wneRkn^d5lQTc;O`nl28e`UMR4uS%SPwV;i;~d+QYe5WMFLzu zVcCP?jKG~y0u19|`2${L@LhoLZ(xNcj3jPDgp$`n`2ly95fo#ObC}sv)@Pqi-aeZd z(r%Hg@GCbo3C#b3A`KaiVJQU8T^OL!0Lv_LWC31A;C#VAm=T1>fq@0m91@_o6J{BT zf;FwO0yPolm)1Zfa-2uyI~6NM8#A^eC7g1bi5A0OP*6THtd}chLxgzZ>4m4(4@*T~ zU4FwzkKI$s|JCZUK-#VG4LVqwVshDsPY!Rse8M`IUvA-2VU~Bfi0?wH*FtNli2qR0 z9aZ=EQoo`D?51`u`U*zE>-P%a5?kCIQtt};U3k4xA@X{)TIBWjVDCW7dr{Za>pqWF z>C%S}eo7_Fj^2VzJ}vKk{c3bZpwn#NYJ9v;zGLaATWDXz-EAGOTwLV&InFAwaJsxx&^OCa@*Rf_)x*_7`>V?P_m&wt_t5K{pgt*@ zIf0#OcQP0+W8{gXX2?2Huv=E~^j4W(o`+P&wuJ<~+PN?nYx(5XhGywY2@0k6`ksF@ z7HeX)HN4@pPp9r}U6dWWVp&hZ_ua-hxeFh!3s5+?D?Dgi;x)kuO|vYIta9oVHeHFj zo;I<;M;O;(7S|nG9LwU|Fzy<-bM(fkTRY^^^Oi<#_oY^we;YQR{lq$V&c;1_G`P|t z`A|B3$sy>@SNn>TXWvhb+xHk~d`VMmmSOQq-fcISe*2b+XfJn`<(<+J&9o594Bo?c z^X)n>%D>ohs;@ux7Phdj;2ck8Abz1j^-QWrSc1$v?^Pvk$&~4;OV>Aaq*D9qu2Mfp zXJL=5=DMK7&~>=)V~T~O3v=-yeVOvtpS`5g-9KcTb%%%TaE$C>4v!kpeM+l6rl6-L*t#sYMMMP9}HnV!S3gx1p0b`q^p!@D@F>BWe`yx68LU zT8H5bk9zhaHjRT>Y;l8pJyREs4f~fgeVuj@ciiLp45y~FUjh?;qw)HlbRF#+9{&q0 z^X#?NrPkghU2N$$j;Lf?Eo-UpzHR3|a8CT>Idz(AtCq=}Aq)dQzFL2n<}QaoIq~7j z(DP?QukQ~x-K2EG&s=^N{m_`NU-Ht4SFFQhpYb>Q%-V|2Mv{*P#V2jL`mwjXExIWw zXvp&6mRO-d)@rB3%ZBAUd86aaSt|~ne^|y`Zee|X`Nh{IscKgJR>5I+714QG+5no{ z+SIna+Ey!&ybNrC$->5{~VoZ0XkJ4q$krGPrO~WyiuP%tlt* zo4jW#jP&)orpJ^qXGaoZ4dTE0R4=C2T4u+ah`x1UXGdTPsEVgzFVF>~L)HpvXyec)h9A6_ZX|MDEQFK&%;|I!g#)Qb{dU0 zWdk)o%9ZCH_g-SVovc(PEflM-E0{9nk>zL5GUUotx9o2AVAa>`!Ih}VCI2$Tm9}rr zcU;+y9SEq;&<>Eor zOft`u{j@%B%zpi7`H#GHJjXgOZt%WDitWbi@-u$cl7v(8lq$6^FSOUu z;4ExjM84_W(GYRli1#2yr@NATj_ht~!^H`cmpN4xC$Ap0vq^hsyf=?dElDNcIbXgk zaXg{UW2Jmk)mseh7{@1LNA}|^_EbI%k9CceQqnH8iHc1{fyh;i#I0j@RjN7ts{O6x z@_b75IZdOFTy9#4TW64(z{j;iS_6}D&!FOG+xI4x0ZMwugRIn*BPF-n`$}5cLd^&E z-YL7u{%s^mSi(do3)7y;ntQ;th}LoElya6$|1QCw8y8O&+})>}ZJ2BCyG58|d!mMZ zY;aag+Mz%lD3Jk?%W(hmZ9oJPFAF{csKUN5v+VCVP{O$2MMe?Q|2Ut4p=^u{z9 zijnGxLpG+Wcg|v|%qgulb*M*i>@wpTkO&=c3vqop3z#2jcTiE zS_q(09iEd6-0S}2oN`w?Mt1$}x7XCmJJf2ecHEKTSf${h`G({xvJ;O9pY0kUhaBBN zgU#y4n#_|w3KA`?wG-dfZE3kUkyh?Tc$#$hUqTL$8rXxcFwliT1|o3(!byTH)eZ;F zXUOLZ3x1OjU~P{lS_ga>2<5`^^!J!U4p_(uSX{46x8(gWiDC&=;w!nTqwq-k3~|X# zg7OcGXh^vuBMmD?pmBtB?cgy385Up>3HEGQK|^Xjhyjw9{Y_Et15E(isWr8O#hTa< zoM$IZ^!+yuSty6_8rqJjejdD`OnCa&0l7@X(ms}8B71)3hE`N3Uy6<)Q^tE0J%+@J z+?`#8BJvLd$gCKj50P_~X9pF{wQ~tlp6ygSz@fB<*>L~0v%>5&BEWDpej!Kv;gZ+K zCfP4W7Q^?(?@pzwEZJ}iu+#Hyn-x3%u~hzNi$@KcGEu?v$8@Bjj-c_(EQn-VbrfTZNhbRR@_Quv7 zp*0!uF2}m}XC}DbKcA%2-#Pd$W$MGk&dfL-=80M3xec-{H=gW!!m-o7n1Yv+e0c(| z+O)K=bJJyv%uzS4G%0h@{&kUZw+{1uJ3?Q!gGn_cqi%I46T5DTbsnSS+3nQBhjrR^ zS8@&Mbm&iWu5@GCgQISGc0Sh1j&{a7etw`&<{qJ@EUf=7c*on#DVk?QWI`=0Fb^{~ zI%WJQsXcka&3kBrwiz+8w0Uq*1Z1($J@5ds!F5@4f+gilfs$O?RpgvE_7vHz~Eof>MVLbYGtdxdSt z%w>T)u2o`w@co7d7qE~1XYCGBIe}p9PdLSogpjEVooUTASkNcH? z%n!a5*kCjvjXE??J2k3ZgxnW@$lmZxLcfQQvH01KB9`{?f35vrm1l%Zc|fiKzZJaJ zfaC=Sc4>kTpaKR-c4c@FRYgYjLax95e`r)YqPmr}_Nk8}Tb{Os|6EHn|Js5FenG7#wux-)Qql7uiw#4Jn1LG*uGi-Q($tVcM; zha%sZn;5X3EqEMrpmyhE!a+`!{~H_*$OjqRpB$th>kVXqN4e@DGZA=rLVO7Z*k!>F z7O2LEI0*JXWD zYY9Gt5GgDRp5PGR1Tmn%I0|8*_TaD$?F6afaF87xEY`&M34b0!!E0gA0}MI@qQrHT<0krLI{NkZ%AO^a#d&VTA;PoP=mrtEGyWbKL6$x>e%VV1U#Az9@jNQBp!i z3L_&a3obX{?usUCNyx?pCVd?E+kh)B%#k>ve%HTZP?q6jBDELH*oG0hS{gaZlN zK>ZD>8baUjN6K|FY9}HP@{Irj?FajYk?r!aEpg! z%ch<*P8=RT>4@7%!3w5fCNJTVLI=5L0#q$FU;F|^Wd%)XXxFiQz0HGb30Ya{Q2qIR z`TU^=;UZ27ulV8<0<0{Zzwy+@Wy>@tYYmPsd99uwjSV!%dYc(K=oAadCcmLg>|Y zhBr)Qw-m(jN*ZVwtq2fne+(7UO}mG5~2Z@&9Y%a_wO z4PQFG(Os*MQjza*JpTz7H|#2U*Tvi+`@QEC+;f^m_I=9tx-R8(3LXk{rhXwM5?)y) zwh$kny|j4z(k|s|Vu}{ojpk`HdKeo?y`zTdoqN@d6E&gowK%H&$bU29app(a7qutoS!|cjq<+UJ3YC_I9%u@@5OLw6MerS z&FNTc>v@(A7^yGY=Ck5T^~=et5F&U z3ymhWKh~hynx04|I`_R%{?)5}Gh&-Bb!x`<%AOKlFzxM|?Kf&0EPIy5WeLnJ)?y3C zt5mv8=moYXyS5*$98c&!mDQDU?{Sc1)zm}I6i0;>laudmat!U#P;lrJxzuauJf@(I zTjZ%v@flL)uzA3te{2W2Q0J|$$!SrK*e@g&g^fB%M%UlmHG1NnTrNiukB~Z-{JCRW z^H}D?jOf~~cBkGB+rq?#}7esPVVvZ@m;U4lM^}pSQjkcdmp^^9@!_aQqd6c>9jx6`iE6hK8E-x{B|2qC-)@h+sxOH$x zU1O@_;bQLFbB3*1-yRr`jg&?oZ&Z3x-i8oYI=d&IBobkrvCt9)A8o&h1(@^M>YF>IZD58ZM2IVIfY*Pg!Sn+ugIUdFM6QBBb{6WU;J|2$QU^ zf_avdt=f8cmIgFyh!NP)yhu<=tIcADFm}&WBT!3e6 z>f_$xZ^njA6o*3+NEm~S4 z5Le-;SuRX{Zd>T>t`eEA5lKbv-1d9E{5A{INu9pSjWZT^!&M$*>^a!z+L$)}|Pr1gHfderwDwjDorrmys z=A2L)!p!qamLkkijA*Vx^OW^XDYXIH*W~SmqW9}8vc2omly(!ICLL&b!A%yGKr+m( ziGP#?5fXwtF)&d=03t*mO3OeRGzTDUx08oB4OuYIO2K?eG#`ZK{0#>a;9x{Jg2%k} zG<@On(Baz873GG}{aDrJNP<8(`Bxd&4*Z58ZVHmhLM#(_kU^$uFk4_DNE9M6A(=_A4-74lIEwz!Lo=Vm%u2~YnzAexO| zX$={5-;0kloV~PRgHq#5hB#wxr!9QJkG=C)J(NT1xBVQMwQQ!r`|eu)An>81GE;M7#Q?^ zn$0kl+z0uJ@k@kk6eYBf2>f z{sbMU^k9#H6Xo9o7k>%tfCz2~GY6ky$e<)G6U-#PyvAVH-t9BvJ$un z!Ceu8zzL#Y*nR?zwG9GG67nqw#}`dGi;Ll3TG~x-Y4DRD(~kL?Y)W{Vbolo-V=oKg zlAx`Uh6NO4V+U=KoC9PQlm(Xz5Ldyu7Ctpd)IeM)gvAm{1)T<|LAC-A76ioTraJZ1 zrVfsR>$#biB_yl1-;|;yVIZ8)2SYT#9XKUL1IWvO#A^?}k%;gf0*ZnC0D=J^b+v;6 zg5Ksg@5|^4+4ZeS^?%n6?TxpGWf6=kz``LV4?)H7))ged7Y5=ZA*CZ$ z2CSpR#lRW0zts-L3vyOeyZT9f>S&YaPuA(JuhqX9bS6pL7JBU3?H_)-JaTJ+$O{<- z_{pFzkOLQ42{{yKk9<@iTQV%$rNysl`R%`d`&};mElCg8k7|}hrf7OE z3Hlz&`-}b*_-^C&A--Em(c=@i3!{7|d1lfY;iKvp4q0%(Scia<$jv`=!5Oewq(T@_I70w zZAK;ig$Fs^uo@wH#^VSy(pIxr@{`#JhT3SL8{f=;$ z7GPWZRkOpd*^b)s%5?ANBRVRKJFQgX?O3w?oEg_;6P_j=){ntg%>$RDuX;_W5v;bM zT_K$a1YgMmwYd}oR>G(U!LNAe(6T_j0_$#vU#%KW{7!Y(wCWDP!H#e&e2tzxUJ!y` z5nndbpR<|dOp7cdK_Hw^KZGSOI@|kuyd?(8+x(7s1rK>(aR5eK@QerD1LU{?5(fHz3oFz7+rVxl{AIMJHEc7)jpSau*RwO41G+Uf-+I^!tsE2S+)oud;aPPM3Z z!uIbY48+KzpfAX?0feJ4g+eePXvQHR8i#Q};|&B!!+`TEDRB5Cp~SU7H~`2_1O%fq zf85-$tVSAbH~|O;0&;9WSI_nJ%*g@$beS7nyQ#x-gpkYjnvD?Zh*W<6 zHPjLKie+HHflzD+0|t6v1z_Ei!2vE5g$bd0!2Tu&lL2u8h&V$idu>ptU8mz$!8 z2v3s^{}T%+3-OXD{S0K%Mqw^kFlhiDV5k9(1<<@ua^`FA1-Wb2W9v)$q^p5 z)xghhc{qfY58sR3_9)BuSe?1xbrKB1$^YMBG181s%-X&R(kwX-Y6(V`eP6L&d#svm zqejLjG^F=YzWtB4^6zpA@P6P?U=8BcfY}jZ<6x?U_ah^#0P0QPcSh<~V9gOoMjJj zx$+8-XdDMAvSeg{CFd81bL7O|fbatlJ_Mw!L|W94TR!XMg|EZ1I0l}8m{8)B`u`CK zkV#-6`7vaYwS)K@ND%}~Z4e}lMT#`gfkWIFuo_585GA)LnZE%M03iGbNNFi9UN2fw zD=9_0+(D;?`pn+r)+7vslmFEsI3TWuT=S4x9vnwO@s7uU^e-VN3-R0NI6rtm-WttvZp;erHM2`qht%qNxFX zb|eghlYa*S)MCIy2xbR47{nm<1rPxHg&Z)C1J@YDgh}HeoX}2^sO61j{2LHK0J0n5 z=5)A|OBKZ#hSe)aX^n-tJEh%6HyjdYbr+k5@ znY_;lXE*Ubf!Gk>3(>uhIt$|Jfz=z5IoSjEh63ap1Ea74BmkC!NJq#mLd1a92e4*Z zgFzTD2q6p<=4zuoFO!)mt0lV!+xtDAuDbt+=Kb#iH4YXHKobZ&A3%}=j3mHZ1mV6w z?}i+hf%E}51nnRVF>#8EenqHUV@>5EP`NOwy#B3aO{g2$bA8d6S3$2ck3YC?_J;5@ z=|Blla7ZtMxFvvC4amJv4rDA$H^2Dj&%(3{o6Z^( zqJTmKp%}P+PU^!6cBazv*Sx0eReq!%FDfNGO*&A+7(%TMm;c2X5?G^vmjTuBCNDZh`~OqfmB2%t z{(rM{xKcu?q;e#}%$ONxNGZpH67dUzu(uZZ?nB-pXWW#`8nU8TH!|TA;uDq=a=+;bLSIXPxKC27#NUX zZt!x-Rj9{eqQWtXib34xxqE=I2leNWd<`ExLg2Aa8K^J@eTR>th(aNFt!M*<7C~Vo z-d(;`^w0fc0!-W)h0aUX+UtI^CVswCJv|Xqj-3i#U8JGul3Yp-^ezv`iMlvgp@_kg zr_bOYqTAk4K*9uOh7KUw1jtqsm!yVG23vwtx^q4?(Ib+YmNrn|?tiUGm8>-+V!x2R zRN7USiWSBeg|}L^C(k-O+&)`cytPR#bxU7;^AV|~Z-pdGgibuz`9iN1$( zem}Wo6Rkw`NI^@Nl+P612=WxN6ft9R-%?&aE&J)K?WiWH4af&(s*VItS4O4G#!E=bNcSqp(|A%$3I%~C`V@W zd2YBCq+P*B#Dza8U%Z|D6qg}fIOWj`EGqiA@Nm;0iv$G?LEVg$WiZC!GD7)x+(vw9 zDJqs~RuBdoFjxe}-1J`WjUBUcGR-F~NR?q1tnYYvO%Mbi`~(JwfUa)XcGqQs(KMP{e1qKemQ1))| zRNkt<3YOCh8;g;@sV`UCb?EYx-2_$^tG2?%>2*|DGk@q}nV*p842#h;N5s^J*|d&B@R^a+gSc!$`H zTlT#@xpvz6srTP^cz+nomoC3x*^7^L_@nC{;1?L+&-3rEeu7H|!T`)$$Ogdl^FS?x zfH5AX38Y*+T#k_I|FH$^#37GI@D_yKgl0e>RL9Qz>_t1_b>BZ^tNH7IND27^FR5=2 zynb>ELIA@u0Qm;!UvMQ8wSfU!FgUbf`{TgI?*ZadBz;i>v2zvGq~IA>Epg#O;S%A$Q_djfYpv*xL7(@hM9q54v3aT#h`Z@04 zTVjLzlMmwz)t=xEnFbg}1jeKIESD)hK~44>H>VxCvc~e{=4ZlR!}!--iqi-;F6iY^ za}F$%h?+soAaH)@;e?8dk`u>dk3!M zvtZ%4&_}8Y4oWC-pwPoCk*d55HdHBGGRP!>jmVS3w}(b2P)!mW-akl8fy9I$NpdNX zdRBZRJEGXa{PRMe!jUSLabF`5bHJgo6aO|~!xNwgrif3WYvLv+EG6K#0NbT5&mH`R zpbA2{99Z-QsA!`fe1~H?aF|j!CN?%JNES>Bs#Tr*vD)v`(!3+({{aUbJiNHdQ9KKj zpKLA!4j9asu*`rHjf=_&U=boN0{#+-QKniZhLC^8!wfj46CB63%4DtyzrISN$6N2o z!-anv>UO7pjYAl-`wdeL4hPWSq7X3)BsFlfA)1wo)vAZ`3?OX*IWYM2c*xYG)N9{W~BGmCB@tHI!;Xv>l@mu9Be(r?7~O>&i(?y9UsZTB}f=e*P+Z{T&S;X zkSOTt!5_c^H4kJPE&~;;Kp@QmXD?rFkJ(Q(Cb$}~VQDi+|4pm=RwcD4KA$}Qxsh4P zMAt^Ej&Dp-KV1!61dl}9oCT;R zw3!4)x3A*($Q|CY{^9R8936GjE+i;0fco~p>j$#{N)ufSH)xpV z)9-cyn1(?~PRHyC2^lwS}*Lpg) zw9TbPv*C<+pD~A5H2A`(ChFS*uOIw+2#c`nz#oLt8(97zi-Ii@ehmh0aS(<<&!8j7 zv_F|J&WCDBuy?5SG%M13sux}eB=6e9|t<7KK!6})L+15#DYwZ zfL0PW3F9XW354l|SeHN(=!Q$P-<%P5kZMM7{X>PIS(Dz&9cOKFJZe6p^lI#rEPEsG zkhv3LzV7{}ejPmzaD;*j-3_N6!Vpk;1m;iF6a!Wl>BzqY1u^7$512(EFkqQb^ASIkG&VS|zkkReH1fxu^q=c1?YK56t8Jxa zu(;O5jmA38_3~f3?ysij@2o%U+C4>`ed7-t=*b)yoLkq-6z^7Bo$@^ zht_BrCQg+QIhdsp*V*nblJ1AF03t6GTVD{d$exdtY@{%XHjV9xt z$o}b3#AB#*Z(je5IiD9PdneV)CNJUD)0m2Yw$-`s#1w;fGqY1-5|PWEmAiMo*)YnmzC?T5ueX;q z&v{ktZS9n7?^l!Ea-4axqaveqS?Jrsm(|lXlcn;H#`u`eKAv>=SM4dvGe%Wyc~!pF z=;~STW96T=asNxcjH7G1MAZ1jikkqBq`66YU!us5`tKSyKGwjA0&#?^QnxAL) za-!ZiS4J4{+!jZz3mbC3`Fw?3_8w!GX?1g{ocy-VuzOANBgT`c zC=Y9`yx%W2Im;hCFT3ti;^cT$_LaAymL zL~KiV#^VjfspJ0^KGt)Ien-Mq?Kg3|qv|JlJ8jvL_R^nLpE;#i_j+aX7P}*{p_6Ts z^LnmKS$R~=DL}tjG5AVn{r%iqHBV08O54+xwruFx3ekcRxu=bBlJuhEBm8qV##yc3 z8b*({5{{M^dAurG>r6zd>d52IX?+RCqd9em{5%pB@Vt9_?--8cqjUxDjdSn0Wt`{n z^2B}A&s~r@{vwuF>1&l$ssDT3-4w%!*1QX+J-E`QWuFW`)f$ekjgg5{Hu$hlwPW#v ziTj#9Z8sWMzVyt7kDq!YXHPrJ*uD0-^Y95xOAFMS!vEu5XrK^Rdo!DpGAEh#v2FzP=LD{qExZ&;HrcREuqo>(l8+%XDS7V~N+62F`+poxZ z8Hp5B`A#s?kciZ~HRMh6R^!g4X4_d)2kW%$NLW67*!CUfqNf^JVwWZ@GYPjd2@~xU zDZOXl=%VR7C)=j6q|!fb^{qKaXz|Y%6*l>5C2X2K!#-*m!10xul4BvAu|oinjdm3DX-iGkP|h zmf_?TwI5RvpXYMJw8_WCD(Xhn34>&t+j+g|CMSlEbANsM$ckp)G3tM93%pR8t!;G4 zCbjA6!dGYNI{S*$6Mcsb`R$CNlJK~N1r`g=-{uY;oKdv)<126liK!<`+NqiCqScIi zeZ*|g;CsX8CAuwEbLgztd%5*OsI!4mn^RozqU-MAZm-rZJSIQYc~H_LQT@ik!Eb~M z&g7a`avavnFWED%>7#|CqvnoNh373*$0(FJo6Iru%n zdy+j|cku;HQ3c_TN26(yx9&96>&Ew_kf2ZvtwS%~tPGnNG<(K~>05T>pUm9)G<%Eb zs0hpFzK=DBdt6TMWOXP^S>{;ouVnU7<4V0Q{rGA6L?4Cgb1nU@4C|vW+WLa?VZ*b{ zzc3r_Oumt05cDYNk0AOEuf`8jx5}={l4~o8AFT4vl~1m@Ipn${SKT(Z0`=IZay?d; zFYf+3z1z;yy`&I1MBm@hS(sGM3)WC50xOG9Fls0$hmTZc@xHFNQ%%)z#$IX z9(Cm=^-(PoQx;K+368l03&C;1v7>M3^@d4a3Kt`0=dGR)G%X*Zy5KY94X|k8Uj`gH zf!_YhD2EQ%FcB>Qv!*AgV^NHbIF?8;3KmGz5fzH)1t`aeRiIEL!z6%*n6mysF^?c2 zD9ohh^33-1e4aO~sYc`ee#c|BDwC;i54?V6a0W*#T@OJ;p72G0i<4xYc_3JTNmx~p zd&GpL0H)`D=X`P`)h2Q0`~zb?V9X;hF3=7ebTT)cWM>(10;ZgQyfRQtW&i;`kgDbX z1g6T)P!wJ@cB^Aix5O5`XlsXgvG$4;+H16Ar7s*AQtv`5ml8@%_e*Pe7E--xeM0DB zo!d?0c7zO-i`r5xn?)s~f~AU1EpJ>L{7JWWZPKET)8;5JJhOs4R%|!4czBjGIB8#PdhRVz zXX@JnFVUq~Q|b6m0ppVZ9WQh^CaJswVV=b$4h>X@f)g7~JGc~Jn?=xwDlN^C z^uH_0hEX_F<@peEZRv6iX`Lpk&~H?XpS##l!vdaqx(@ua*h-MJ87M8U!}LJK6&+nr zdb2&?U(sRn_1;vz{Y&t`fp4DXM1a_*U3VINAltt(dvCH=n2DWU;6_-d1fQQCIG&)# zM#&q1fj=L#;kwA3(?O&x63P&dgCt9Y&VmD$KOw2hsdfc%EC!B61cwICTV6~@GW1nB zQ^u7!Zl5jvc%~qV|8el)gkd7C099|$HwpYe)drNAhl>yOfpic6LJBv)@kA|*)MMY_ zz=ohLCOA%qEKsh$S-HkY#ZTX@qI!9!Y$WSH;6V5sDsLjCTn`peSfiMrFozC=N=+C9 zDuN7$F)GmWaeSa!CeHnT&Kwuua3(lFS6j5>^2qg*uY{G~kFe!PH}b)d*3nuoFY^0vhm(GZ0zG_hzTz{KX?UHLif-LZD1jD&G2rG3mq$ znTkaoQhyG*l_RbFHI$#4#~2imw+CFYFcN~l4czkJd_|cNcf=W>I1*MVLe2Zt@-)7J zdstvd-~r zwr#R_(Wc(J=1OWK)W9-gt<316H_#5Mhqs9cUEY^+|3&D7vg?tmOmdKcEhE&qFMXujpF$=o}tIpH?lm3ayE@%!CW)DO%m3H(TtaxjGX zRY)jZpTfRT;Fm7NFD0YCWRw5r#K`D7F574cx7aHWDJ9CMOr*Zu{~EY(OdagxgGO82 zR4q_ZRvlI`Vw~ZW$T{j((UoaaEF{7{NL7p$3jQVh3A41zps=s&omZ6e+tQRV5yPtz zZg6MqpD1xQYvwR>Ve46y2die^aHF-aeR0NNt4V32lmCnBU3Yya9I3tdv{Y(QSKq!j z?e7n`B#g7)d zEct&AA;7(ivm7@L{60D;AH@WdH)3?)3PO=zI7eY%AU&!5UUPs;Co4JMab~tYwl-zWYQ&v27 z$b7LuhgL3a&P*NbFnEjZr;PCxEt2&eLQNY2YIvzFPx6BekC{)m3Q~TvL2R3*waRmc zKkx3eQju9`eR9*VrKdhiy4JWa*I04;IcHj78PApR=KNh1*{eCVqgi7!)@5-6w-;NO zjtL5QsCYtGA(wq8*=G_nM!ouQoQBx^6*rbvKXQ*_OFbMS>1+TWl8{hF4h0uOElET) z@(Xc2(cH){oYbzRQ?lTfigA@C?nfrcjy;~kz1aWb`u~#1#UjHWp=p%0iUi(h2fGJx z{U~V-VMqwUf}e}rB*>3P^a}&=qzJO-pe_Wl8o}=dhH>i1%IN$Khz9{ffY|#0}bYEFvby!8UI4f451pU66o9$o%2ZNwOWw}xpjFr zdNp1z@>t$DXLd*xf2a5NPfjUA+l%z!;oP#B3C(9h|^n2LCJFf3~snI!CLao?KR%e!sM_aunefsm#t>aF3 z#a5lX+?ti~q`NdM|B18ny;b*(3(G>&ONLhG$QboDI+{C9n0~Fw$mz`$xA~@f+9yno@V&#w}CS50G-&as?+V2_!p!yoiW)!I+7 z4DiQAe&`l23AE89zhxg>?|1~)D^nGS;Q)P1H-W!L$i3#}m zQh-m5Wcd(BF!EebY#f>49&Sj#fb34<(NS)Yhin-B-L)@*0s=i|U@3S3$T9+CmqJHf zSxB4CCF|9>`(AzwEf!j(OnrOcC3+9;P9n|^=rIFB%Pa$rJ_ zV4}VryyQ#-^uSk6?Tmd_s3u|FA3)&^6kY^{t)aNJMRKO!(ChmWHE9=xH^|px8w)-^ zFz@Rj*#t#UPR5#X=L{X?kM1Si7>y?c}1uU>gy zV6Z+pQ0On4Npi{06rPE0STEQ&|8S7_*L{harzpMzk1OTXV|&2Qhw2-!tRpf`kByWl z9M32`z*pz?!)lC25Qr~;_z)m#YKIl>x;`OfmB!>KzcCBq&eP`7{tFP;()B!a5D(yn zv?#<7A*cz(F*pz{Ky5)sWlj_-V<3lvnveZ_dD>K9J|KS5d{QTNFWgaHujP-PP_=ctXvLD@+d_#t2EqB1su?@59hzyG^b zV}b+WhyIt7{*$UFO;o*Uul@HdJ^E0_(K|Dn|N2kcgQO;r%G^A*2eL1rd=VuKWkH~c z0S9WY!!5+&_VZU{O`?DZo|OIo;zxiO&}IG@seU)TOH4g5{he@~G)Lk3moXrpzCS7H zs0_pKgsWR0Av?&8Lr(~{CndR1QynbDaK5;+czXPJ<}65Wcmz8SKc-#G#`P Yw;QAq)R-iD1B(JGGGZM)=^kSL2a*4FF8}}l literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/_l_i_c_e_n_s_e.i b/verse/.hg/store/data/_l_i_c_e_n_s_e.i new file mode 100644 index 0000000000000000000000000000000000000000..a0adb675c9d635d64ffd7df00ea624957360db21 GIT binary patch literal 696 zcmV;p0!RG-0RRC20000000MXb00bof00000002e*|NsC0|NsBrpw17_$pYjzUnL22 zr)c{K4EnAB00000000000001ZoLy00Z`v>rfA^<2@zhFL$M&|pnZy)pA&wkV#$!kf z@f2bdo1uRD-38jJlOjdFyWhXN)Sq8RckCiOG>`CoKL7FM`~2G+)^&`Xy~5V@En}q} zPc8)K2XFy8JKFs#9LIWywoTwLS_}RFO;?Xcn?UqX53g{xV?co4$Jz~UI6@5#sxh35 z9nu7Uh}U|wINU-Vf^S@ne9-#laHvzHvskCT5xr- z^Op#t4RLf0aU|e|roXi0;jP`fle;Srd|E+ZNO}pFfP5x!@~u12-%hd4mwoR-mq6=? zdVh&n3{-6FKq$=d)sGNt-!o)z=zGfg;cl8C>t|YMyss!w<=Xkv=Oiw$!(|*$(oUGx z9jUQS^iNa6D47TdM|7)i#w+jzGqSc!PI>7nks`pmt>b{4) zy)O)P-N2~y7H1^+5b^M??&0jmN$>xdPpH2VP^qQaaxFksP-^ueGm*iJSNNVKu$5+| zHU@A)^TO;vErAz1_$7-h0r9)kqN+e?Cf8*y1=eJd=9^3w&#*vTp$z15Ee(dQ$Qfc5&a&2U~$+?Ddqf1o@l+Td1ki}A?idc)nJfSMq zK)m1ssuj<3l45*=?lt*?RFyj|pH~J}D$fKK7XqE}MJ{eq7%I)VTqlt6HGdWpN+FA8 zG2ds0BUM2$QdJm@&jeFy^A6q0N+iIwtY{%it=0*nbs|DdB#0}-jfGYX epDuzU^xjnBoe(m?bL7JF6raxb#Qp(At5JZC!c49J literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/bosh.lua.i b/verse/.hg/store/data/bosh.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..49eca4d6b4798fd80cb8fb684f479364952cd3b7 GIT binary patch literal 3389 zcmXw4byO4H9~~{Jbjn5}2ojqJ2uKY9>5`O&!DP}gQaYqTlpZ17AQE4sM7nV_k_wIy zO2-e--{+k7x%ZuS&pY?MKkf&B1Hb{?6u~k8K$-WK0}L+z6*%sZn!D9IPff4W!>DGWcI7dB zdfu|eNZr5z39yKmoN{GVh%rB#z~t^NpI#?nmxgq0N84+g&jnjQe$I|$D5X|>tl z;|TCqSo3Jk5J(4XsW0#SwG5wFGslC$R4oDA6jV+Gr?3e=7^f=6tMTF-@BO@D$K{AV zeyq~jk+@d!Spy+8U)BzmC|T7|AWq`xJRdq=7-J}6;NVFe!Jpnip)>o}d9594LAjYZ zvSN0`t#Hvh7H+e?<^tRBTg0E(9vA^J4hqXkrW}GH;(l^~@8h^waw-zESP$5HmZ6WP z%peb~@eR8mBqu`uha9QTqiRdjCmS!R@ue<^WwrMd!Tr&!?6PJpb*&(A$=X{>dNo?W zxZJ=o;n;i$=E;0Hdi_obqk%`?^*23gRuFsPiCY2}6G z81uID;{?1Dd6u-v8A(Q=r8HofRbQUZNl<+{ZCf<+c7D=;W-iRL3OK!nTaL@Px1h92 zKSu6sm^$6H+v~Z0Sg;&gAf*x*6@YIYwJePZw5(P)Q+>XdoYTVL(EU}^d0p;X@gb80 zi>zos?%?_vXCJ#e(JyyVpp&d{N(WU?IZb86q0&f535e8iW;2XTE?-Um4si&0U+e*c zwht+5YV&00lot0ZLr;R+E0dK!YQlnsU&srR@TTd8jrA0U?@Kd6QWU8b8Gdx6L+C1w z;q{gQXJ;PnpZt$8zR)d3F{mAno_0hMq#=W|AXWL~Q+zRb@H z=3u6F&Kp9&TizXt_K%*^(VHi6xvW2(h*Qq$vh39<>m+^=CdlLT3A%itx3ELSytZ0j z@o1j15eGJwf5(0GP8-I)O(1RxSdZy2!^3KCog2!;mDgJ9eCKD!drp5d_KdFs z77HqRU2&>Fa$AeE1&X!dgL6pRB|7?SS^9-zW2^J#JtV5w&mD*m_fKYBuyQ=v;7inh zn`|o5f~QuW|J{V7jQeV(bCcy;tMW}xbDmhK=gh+yCdR3;_+Kw$Vb20m+y0BW)|#^s@45A@^x!@4WvEzK=cReQLHOR z?9RreoclT&+};Ga8pha!QfRkEbhCYT{gIL!FuUH$0I2ki$`sAqv$N&A0YAv2k`)WxI*22kSnPf$-B0sx*hbxRLt0CBH%uGbYoAS|P`97Yi zfSRtghLMhurw>1vk@8sepW!XkFw4{DsG9U0B{Q@xHY2G;-4(89M_FD z7JO2s>zKcu4OcaEH6d1S7yw{+jn_kBjk#f-z1m|j_`u(q?*ke3BTDe*?4J(cm5~yw z=Fs_sEL2fG9Zmm3-^)gRnKWbpH0?H-=?wqG$_|Z%7KV(ae=N9A{>olTg7348^uY}j zOG{5yZ`JAON?I#7G^pl45Hy>wDkljzJv@@ofjmTcagE~3D8vb?>WQeA(zi&a!L0(r zfq~tk&;A^d?sRo7q3j!CSxFu?D#`mNz(7uW!66MlQobEU5)}{QSyMgB4A5xa8_=F& z^e^8fuLsI`j$VpDr3k>Sl>?DIznS8@1GX+5Jg;9xSasd; zuNH&aHM=U)tA%s@8@IXA&WFZjn-aQdRqkQu5`Wd+ue(wFYhD@U`hykB5C(`0fo)Wv zK#8t20iRm_+^Fg2b+dWFsE{0OF$|UL>cFGO6h=vORytJg^wuwQt$FA84nC-QnfN)o zGklmc=>;wEPYF@6i?huA^Sj~IV*ZvE)u$ubZzS9;n9 z#fM26b(wJnCKAQARviBJA?~S8PCrD{s9qM?5lD+-2Dt~__{gMw!5g?$BB;#DR=$w; z4u<0HNG_V66D(;MZLhq4pg@!=aNIQ3LYCWtl&O$>t0X7xe_yR>E)=W886!KhJ|wU| zwG%zcxj*gqdht(RqQlk*2!)8!H~hO@G4r)p1o}vxby5F<-3Qk}Fk&VYbstvc#_F zlvF-pcVQ*WC#`4neHz-MQzFCr?GY^>Ryhku`_y|xSng8n$+Be=hfl!C<(!b)|VP>P(Y?k#o=Uk2zOpS<=Qr+*BdG!S+p)9?T3&P^M_sOnP2 zvvj&!m~mPtbd**7o9g2#;v7~tW>;A5Ig}|X{$ErSXBqkC^TKoL|Ekz*Cnu;aYBFp6 zT^ZB^$k|H4>qL~V|NGPpG1!L;o{!KlW9mbSr+0y5>c@xa#+ML-t-CLjs>VcTTuE#k zsj9PfHo6?=d6k+{7E;_?pZWvO0kBi%CHx_e6VD2N4P>yR&Ae_J!L6xvzM6{?-bQwr zI$q3tk8|~lOL|HlHF62e594D~Y{T|F4c!cMo#&PvU=AI%6WB2@F8R>9E2 zIYzSx;sfu#{kJqegOQg@PzP!^+&;+}hk+2+wjq<7~ByCA4K??}Srod8 zk2RguwJlvdstY0XJPHGR0uYE|9&ddkNT%`;EiVP&*at#??SNF^#)syWdH4+j;1FCP z-nIFGCsTzECd{}VadbLG6dl&=vFnU~HGliH`8zX1pFwVKgM!#XM0sSyg_Uv}IfX0v zs-L&(=Ypz5cw~eXD>;Q*(eI;01=^mnQS-u!@^Z*@k`sr!BT=_`Z>K~L#ni9k0-izd z`dr^idi9N0_2kNr;|&Ag5MCkvwV#XLGiLU7etbq)O%PO;si|i39*{O`k-b5)gN-DV& zrTor4?)U%t{a*j`dY$wB-0L~d^L{?#85aNpU;uOxwEzIwrPDD0J0V;tK9=njBrY%f z=Kp#;Jx)HXsc(Q2WFWv`)6ES0-bqlazI&-LX*}A?(KhlpHT_>S>|JFw?7Fj%Ty~8~ z#g(jV-|HuTYUrN%rj{z}@{}A7YU`Qau*>3QrP|11J(Q&&jwkEOlIx(3`a->{N)-D% z<#p)y0u|Ud&E>ao2wyIe^k&L=$va(Yqc9BCWK=co3?sVzcb{Oqac|47@Q#ml_w!zJ zs7do)xRn{2YTL2)eta3jntsP`SEp^9a~l`rZ!7U@j?=NB+w85+IPd6%f!uq16HQ$6 z5gCo<7Z}r5IW=!w~ygDmn4Wo>s-lvLw}e_h9=G}>jrqRDSjE+ZX>qMmQK~FmW(wn zBwBp3Kvyg&VENA3JVJcvekzrP-*Jq?avpogpP@7K!x}EhYSfymj_-)vt+49D{WgV> z99asfCz9zcZ^)G1476Qc(KadLnJ+VC-sq%Ee2kmVmQ&S}D=mmWYkaZ)SH=U|ddcen zKKo8ex7*exHmt4A;0o!O)~vJpGY%}i*k4T8-shULVU5yYs@icaA5@qq7H(GB@%T$? zpK7O2x9m?z8cKd^!AyQYFJfqyqvzoo75Ce&TCe?c&G3JZMm}coOM0c|C@g=T+5diC zNHx-tPJvkHwdiKc_wpv*jh}n#;U%lud#}8(Q72){J`qi~V9D0JcKDr(-|pb`5&+1> zpb-F2;t{{^l_tF2x$nIucZz(tDlLC`_n}u!BS8iN3^w7;McP;idaZ}b3R*L{mqop~ zeYz;v{7G66Gj|ee+HkZtIoVS$GM-_{9>4PE1Il%$C!)d3Hg?Uvym?HFEk4oHZdUU( z- zv8oakN*8LI6!RjQgok$-F)^TU!*Rx$p0y%-D)LIK=d8?`C>u)Q@Vkj*l{WFWv`uir7C?3JKTcxr++WN4C1__uSuSW|OasWn$lIG;iDhfZ21w< z^29KS<#3e&52FDOl-$_y_<}}QX_-t&w7N1Md8Z}Cdu|6Um9tu_-Ly_-U2DZgeNC@d z-9)?^7+R?0Bc2l){&$H_LL5t@<*P+bNq|#4gKui<$sy+w{N4M)e)BpA#`J!#hHv z0J-A@02D;f2mq)-B*Jfj$hQC)IwZo*S5NihaXiVU{qDQJ1Q`f0Sg|YmwCWOw9mC&- zQ&vx$@3yDTdh6r~I`nhf*+epD=Z{Y0?MR_U*=P?HsIUV?Ni9E8g;;d!9`GrXlP4R7 z!Oa<)B<--QZeQ1Bhgnm*9-bU%M>@2+UDfcb-6J}FEsTligq2DQN8-(qKzw1s;2u>b zu%H2v7|+z>xN04tK9!TD=IOhB=g{4W3HOsA0|5pbV$x&SCqYfaK|f|`rCM25m)G9# zR?T1g8l}N-nL9k>Dz|!s#I2Ya(V0)LdNTKRe)*_LhvK?7+i!|qzW%4GkiPA557{?c zhg}k~oc9h78`v)EMMSp29lv)irv_3_M-De7Mg`hSe2CF8?oaLgZD=D=m$>_*D!Irs zof=k1QF7u6N3&E3lIc_j&JPg>$Qu`wVgoW~DK4rS%H^yu80KBxx3bW6e$t(7GC?2u z)}$o$tBVxf@;^&;eFfJonQe*Exab*`w$PNxnv$Z6dwimJA~UH81JFCgPe6{WP>Rk*y!vQTT;g$jo1^%31bsuwCJCEq#g zbx?j^lD(H6Dxeg%Pl(>(y1MbRMLGh_SqwPQs?Z1km_Q`KPqcyZ9RVljH_1#Bs@umv z_#!-V;}by!0u0vG$qC`)c2d?qSxZ_!nU4gg+f)30=BLH9qkreL$ zF)?FACtdv|k6+rXQVjgWIWG)E2{I61uv)28@MsBY%|G5%gTOY?8p4oX=GsNu>n>_` zui#5FdM0Xjo{|4ble|BqSX%eEFk&*w^5TIIf=w`GKAmp7m~3foyhW%Z4jD+_$iiHj zsXy9I#vJtih;&0At$mvwh3YWJSO+F_)ZowsUN(31Qpeb)3=a# z7rw$Mmu-7AX-oH-&5-1s{Y^WSkIY+@i-UCe_g5)Z*vvJ@>y9lB#c$NQuY}JqHL8jf z^03k%STAo+1ZcU4J6EI^|D6(?wcK4~FSez$wOFOi9B}5#GNrmfrC50j_TIEa8}BrQ zB;-06zNvSh5dd(32*=Nt_Oca4DGB2zl{<|YIs@Myx{1t<5@h}Zb#UMb-lC^V|ZX0y?hkxc4_&t{N%Y>*Dvro)4JT8)}+0evzwDq zTpU_f4bk<)=QgGPdd+)NkBnRM&37wTR1~xia`-Qxmt|SsVwIU0ZavQpPECHXEtM#l zaPaBg_>F-cbfR>R2YU$%>Qd^+wA)5?j8PvGMrJq~YkpL<%Ue{Tw|?PMtMQnw6^jaP zgG~zOmuLRT8-Xqta13Uc6IJPWuCL8KqHOY7JuCxJX-fPnwORi!>;*%iAl`MjBQych z06>F3*!W8Tys7ZBR*GX4um9atFkUP#W_;zraU|+(F(g5FKnJ|=6)-^Uf$gtVL|Y^a z3;!Thto)oGc2=)HxnEJQVBRiIN;f}D*ojRX&?jL`#J>s8g5K8$O?nVCiGxUuH)-th zccxMMw$YKtZ?7b#C-6wIhtQ@X$N_khD-521D_P_?_ITqJN5XN}Vb18<#&K8I>;@yL zZNl0tRz{WHl8(E+6g@^dTSe1D9f%bwkWp{GqhaFZ5)j2@%n!&v;XvS3!rw!R)j=xW z&Do1xf^u2FHeU zM)iku#Y8L~n+xfQiH?ctnH%ekqx9x3WnFFBBmx{a5zr)1azd)W0-jC&?*PO|mKs>t`}Gonh1u(nA*HG%;ppnWolf z1xa1S>E$528aeD}jFXdj z{RlD;V6d|3r^UD@m0j6qvVx4*cV`V<(u9?W$ZV$bS<8N?IJ}C3%QgO6dw!y(Zb9sJ z4!{V;ug}qU>R+r+ZU|q{U?gwOPUhDCf!NXFm9zX++>RjcxKdvJGSFwQI84qwVae5M zf2>uE?CT{|`@)1^t9|M0g}+3I48_ZM zc2rQY4S` z9!kz`ZjSywA)aVoA)X-jpdhrLFOTqPXrj`D-oY>PAmMEp5E<}>Rx}si$;U9}Zk{K5 zo7Mv$t-SUAn`KB8TITlt?rv5vlS(`iZK+W(N6ELeD@dxEYr}fQ!2M-7rk{wDrgE-d z%X-ySr^)(As0CTgH~{XyVN?n^mGDAf0P}t171Ab#%S4JuTr^@-0(r)ZF(bh(O_l}V zVp4)~7F8`|PaPzA42=Nb0*H)wl2b~K8yeh#YBbjna?g|Q{+&QBd?m;bfDCL_@HZd> z7ZXF8o+eT^q?LiOG19}`IXEQHJ&5xEpQvD;p#L{XpYf_-VE;!mK~Rccr89W@s;)?% z8aGT?8OBw~m}h0X=qU#YK?VX0)*#bBpIDMUk@t4f*NeMIuB2QI=C;cIcj6yN57u-a zPk(#u!(Jf3Kh%2gZL#oWx1+EFcBk=N4ENKG#jxcr=Na&8$o`X^)J$cTMW` zue_^a2@X9FA(L^~D)E~3>Ea$TLW-<)i|LL$c$lsSGuI`z=iuKwh)o(}o>t|0tp22* zG5)QGY0fU@7I}mdFKbT5ONC}QwP>c{3`r?7R@4|^ad;Avx1h9@&DuDyE@XXpcNgtd zwS6>ArOqllp!8S6tBKuo@Y1kb`z7I|T(DUyRAGOVV|bF7@?rMTKlV9tSwVw)YN&#r zFJMU9EU3ApIgNZlU}*9o%=;A(5%{(FzT3IOQ#md`*7-PdcvXQ`UX#J%|E>)fvO9;t zED@v_w3mG*2r;dP%hH#^;SeeMDW+$F)?+mFDiZ}*1U&=13=7P?ti2qP6B(_{bY}`u zrDahGR6MzvB_$1|W>_0L>e)tHy8dtMT@sajGO~q~S=J4dZ1t&J`Jk=c+%&_-p z!oiqy!r)o%(nr+Z!fVF4?1HNgkymEIZXCpU>Gg|*PPhD{l8_XHat~pejKIhaCs4Wy zA`70j3Kv!9ppJE*+;qE`h=B@mmzxpp|I!A#tAg(C?&gG;n4Y4i?G-|@l9qHD{hn1q z3nF~?auA>0tE_?oJgS5@KHe8n=W8$>F~#Z03C3$EVnIv>(sHoZqrQLArwKf6oP-}zY7cphPt%964d*$ z3?nS{#nV(vBYTV$A|%>ol!i>zYHOwKpA+KRtm|P5oTW?=2E#pq93>9dsD-IkX%b9T zTGm|*>=b5&gDxc{c4|0C{hH4c=Gp303?7n}oR@!MTE!NB__6bgbzQ-QKDlr9k4n-_ z-%`z0?%pD4Q?k@LV-@|EcqO`kC5oa&o(xZk4*KDi2<#&eHUx^vAhO|Iagn#_i23Ab zyuKE3S+;Hd=C4SvD1r(dO3313IWWN5#9cXz3!C&>nps0&NzY6wiow{U!_Cs~{4T(chW#Hd%jv^F;ydvinC zt}jZWA{G<>HPlstrP4r!SGkzFt5qlb$~3(en~k1P6vN&=DfitQJ@MIc9a7fZ3EjP~ z?rA@^>$P>x|A7Kw7 zN~j*5BRHOiZm zLS;KO-*BU;?##>EmrdOst?9CEFwocN$cf!Z#**c_frV!`;Z=3 zYv07%`MBp}>~!ge)>n~{Y31_QJin5v4BF&SDM*h$QzhYD)$@Bad~F(6!s1Wg#^M~_ zl**{hKXZY~zR>x4*1g8;kAVzJC-UY$&K4V$k-Bl{+@UkPQ|Z@z7ps0yp4)p}7?N<}7{#9>Sd? zYsR9_d1w(ieg6GO6nFlQ1|&^*V6gT^`joxu)CMd(Gtd7_&Iwi8_V7?}RiqUm^sCQ# zR(-Noxm`{FJPZA$>?|f@ga11}(8Gzu5O>cvcStQv%08R3zpv=0JJ+tQfB!{uyvcX{ zZ0aRVE%-zgclX*L=CP*WFcD`y&yn~aY1vZxr}N`kyr?&5p+SY;{EIsi4zxaLS2KS^ zZdU9Kr+-Q3>MBWNNqb@^NsIM~&nrehWF4}ZFDfynN7v?BisE47dj_(2;*4?-eXz)8 z(j_>8FY(y%9?v?}E2dJ}cnCX|iF4+$i;CEA%M)ZE0GmNWd<>@k=AFc{+pu37`xCfX zurl4y&hI46@aQY4&zsnk3w(U%mPyLwbcdv^P_qb`5rCl0B1mE`6#guzr(% zA;jjj)9`1whl~`Ddwt4}M80tW*v)S4LbtzlO74`V%em&o-=ud^|J`}$PsPW46-LKn zd8FoZpPzaW{uQ0-T*4k7*8w52cSp)wIL5dx*T|q+@2MYq24bNO++4# zT$0UeuO2I`|GyrIrC-En6)jK8eZ-}T3@1va8$T#J8Ay6MuN9)<6>vWE91df^(z>>( zc0mOLERrJs0Nvp&DQ?P@wQS+zQBI7D`8HB#&@#St4O19Z&de4coG69wt_f8v8wxgrPvwA~EVPUmH{I$Y*N|flzup$2ZqcF;|1(Z(wTD3={wP9xq z<|%8#guy12nmWQW`cQn5o(vdwV;;h*J#(6Wz^0h#=BXLjSrEDKd`Om2QQFcPDa@@X zzb7#MtqXKn|7H49QGk*Z?3=(Fpn=0rf5-@Q4?b-M&YULv$6!SuO;C)_59jcTiK7t( zo|QaX3Cr4PV%zjm&B1bQ1dR}2um8SXG7TwXZwKz z@_)Urj}UE>KY51S*gO)sft?pX_{ssprALEgALIHqGja15GR@j~b-Aa}tC>pWsIAsF`A*uo6bJA<-!Q58w$%v3jKLJdc}!*7o7rT|0?L>&x7%!F1R zhCh->yua{=^vJG(y&lcZZRu#@b}g<8rd^OU;Q=jd6j)arCxLZE9-*#4p=O$jpINqO<`_s0zq693Mg%8kh(T>5Q#|KiFoHyHGs{g&P{0kxG08m-tWgp@4BZ zLU5@b{Ea{+`!GscVwZ~8JUNIcT_Y=rjRc^X7Ki?ogxH7dcYxIoSjFKl7`QJ1Zan47 z*ZJ*Eto~hB)1P=NAu2pw|JW)H5+OV=*xb84;J=@!)qkwOUX(4O`PwnXtF}4`JgMaw zlGH!8j%DSMH-D^^UVr++Qpv}A0bw@BbkNQ4fkj0agn)%>#80D?gc4)S! z&~TB;IF{{+ic^{Yo@8LJK=u** zBmsxJ2L}2D3V}~?U+3%Y;^IQV?wDYtv#%SHM+wOzE)IS43qq`T8Buzi(h9?&YwSTTElz;i_!>GhPd$y4aY4#%H2B^&kpBuDTGXlpuLK<3$ zp0oUNt>9uU2$g6(Ke(XRy{5}X{k)+0&3gU(lt_wm!L}l1>89}q!>7^ShpY3iqF@ZG zs5f5~wo+;MKQmN{P7@gv0sQL2W=bevm``1Ky;k{O`o5}xcxDN}d^0QDq-f#K`qn2cQF-4pS%q0FwF50k@C-Ot;V%IT-iM z)-7vR&9#7KdpQl(@jseVz!9U&t=z`0@;+K>soum8aUGtr}Sh#}SMVk$da1!_hm$8l}XyM^EZX=9S3U0DYPq zbJ^Wk|H`zAO8JQTcT*r=v2ijPFuI_aq(vpfo7#FcA+gxfy||{q`fPP(Zw?+ z5p{kM!kJ!V^JtxK3s4kPx&SKS0^&Rr7m#7#F3^Rv*WEOz_Mw$q6{YaKiZV9ETHelv zrre`LB;WD)n`SvCS6mcKV?D6+n_n*Py+c+qKMXf8+u~ey9l{z)d9rR+CWTeZdCB^u zCWnv$(A5i@(C{6OsHr^GN))EU(|#AYeh^KcrZ5x6S-2U)J!?j9ZdftBBuFL53neZfTyP4us6}AksJjj4%L)NFNa)5c9Bcmm}AiqFH5< z)Chf`X%{{>1Y9O=#yR619gjs9j&(~|LjVKB+>tDqOG4W+OK~IJ z!DknsqRnd0RR^aKQiciKax1X4S6r*Np4e?=N&K7!(xA<3cbeAO*e(2HQB=LQm4C#6 z9wrwNW5gKtaA%fbDlgp-7yU!jy=zg8X|Dg40AnR#?3H8P!xYA5&!D0Z|9K;2kBh_? z7GgDH?wx`ix4EqYnHzm*j}1sTI%*rU(e}r2V?Z-#CEZ2=RJ<#T3F9es9(l1zxj%65 zOns3b!S)2i5#PYzcJ9H^-v8X1;I9JlPHn%sSl>yAvX7UMqC&-cG#I~slxyoVdaux& zLW`f<*z_tHp9-B&|9WO~heZ%%32V$Mk?$8^%8+ZYh~V$5h#;G+;kR}rwim2jtG^{9 z@*CtDu$jATQ<7Fs5)9af?3F8pl6w}BbD;b)g?^Z-Zk@O1KIy!R^EEfGpES|Y@5@Lg z$8**-no``Rn%?TG?OV@0DpH>Y2s&zdtkL=Sdz z>fY6u5)q=)^XD?;{av>Fx$fK%=*G9?;SI;@p7}ELJOfKBBN${jE4tto0cF9EX2EzkM|6uw z>cD=%9DkK1e$k&nW`8%(G_I9nJf+1dMZ}{f1SXBM zH#Z|8{Q6FM-}`v0pfzl<&tloswp>@rFL%ZWxv8#AM=155|7M`HMc!b+8xpQ9^sW`!}(+XJzfP zkZs1@%U=VuG~e3oyyVwwu}c@i6u=-zEqcc~vhig&wTIy6GOU3Wm9g;uP%mL%kGW!b zy&{7duSB*J{Ft|%5OBmm2}W0(RPjlFA5YS8iz81N0FZcsCq=&NHN)fS)Re?GhDNQA zLOwH6a&`Y`P60=JWMahH1Ob7=Q|EZK&1Ch*t$dh@O@S(}?Vt(v(&9Max{zxvsj)}b zx~A2)MpOgCAoVw3irss>U)A{%i?C`4;%0SP(|vBfR-;FBe5sMX-bjv^Fs#}INoB%6 zU%n!_h%@HAtdYh+u40RyHGIAE7$Ouely)OZ<*$S&if(8vPPok1$Q=zHTE9um$b3_A z!`?%)_QV?yeCm*L9LwsI0RWlDNOuxz4Jgpwt4(FjTKe=*{=*}Tp8CtfKblj(5p8a0 zo(fS~MVpor5jIxw|pa{8WNqU)NQ73|m#vXvB6N`z!d_r1JKSSwXW2(y+r zt;#C@@F0W+mSrR3x(-+7+o&*p!AWwAbTO@Lf-ifUupW4E(2*KMCGt2`8q3U8mLskMla6G60>>G18yp<$3z@$HHgoBdZ6-eo{q~`XjYL zq~DtVZx(p~;N$@~SXf-#Q1@NLJkXvhtS8^Vg;SC1@nOeP2B5okj0`7-xrT?-VBkHO z**Dh??KdAUN(KetertYj98s}Dg*6xg0`xwhX%$0q`otyt8VE9cTpvZ{4IRQM%hUh1 zEGcr53IH8E0Ifz|-H*J_Lt803I~!4U26IZ=B!xqSx668+gAFhWnNSJZ1>0Elh(CXq ZXrCM3wX~NWH_~sP(NunD9k_a-=s)Fv=3xK; literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example.lua.i b/verse/.hg/store/data/doc/example.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..f854951c6a5a8c8888bbe36a2be8e3b9c9ffc513 GIT binary patch literal 1096 zcmZQzWME_f1Ex6)46Jn!HiOlFD7YnY%q!R~-6h>9wla3+lZ!pkSE`_Dz*NPY))3$P zD+U6#=KGUAw7T4Kc{(eIMN2`EaTUv|9ii)NPtLOXc1?aaZ^!?0H#{$=tvr%>ee(Bv zXJ&ugvF>5gY2SWQz;MTWRvz!v0@(B2LYkys@jR&_iQVN^r-8%})+YT_}_< zQ~p?Rm0;9DPRT!yPd`=ldCnTkk#(rWq(Wo0)0UW1ujlsdH3|{`ZuKeSK8NZ-7aVK+ToG!8_wIp#;)FJ;jE|w}DMh8ucru zEL`nemVKOGW$H)iiqfhoEAQB490d{=s{N)db2a@#cbC6 znD3JDUTN3Is%29boz#=x{C4`eI;K;iwN-O64m{m=^3|3-Z%R#0PN_UB$8PieS7COH zhfnU?b(vCOqL-B?oqO^^f8zJ+zd#8Tl&XOs8JMl41EwnG=$^Lab8rx7d#D-W_c2vl$ZF~}N0)0SP95_}VlPy0|04YL z_a2u>=P9u+SFW4xI#|=d+7?|Y;m>|Al4n`RBKE!mpPr`p#~Y~Eykm`ibZ5Jxso4LR z#9K#huW4FzCcjBbQ4?ej(|@pSvOp&WfLRO-IY62bYO1zUx()S}{4y}Z&-^1Y_CbZ4~Un_o)B8X+g{?Aq+@A(=xh>mc$;4N;~my^ zj;2o9CW?*qHT(9p{n@{O+wo=kl7r4O_iuS?Rg&tuEa~{tJN;F!rf5!HvZ>A1dcrC0 z-!^Ab^IdmY-`qb{XwTC2S8Mlvy%5DNl)tQRg6zknEh%L#&6DQspWDOngf*3Qaj@Ey zH`xUz*iTFdU&CZ#6q&Z_!nObHvlP#-v@7^Ld;JQ_FCGg2-fOkX`iTE3r)+|GQ> zgu$QY~LA1jwmE-`HDW`em+2hZ>zpL!xWq5Wf*V9wK zx@~hByA1+6`!-I}J93o4c&bYI6U+Z^Kd+dwI@ORn_M^b@c`JIPyyYY>tOyGGqOtv$ zT#?qt8_$m|X7k*^u^@J{%CwWKd%DiUqAas|WT%o%#m9os9b4`x32 zFR<{&liKYktgCXY-#o6mRCuWSY+Ko#Cp-kj`mztn_JebzP23|;z;e{%o2 zD<@OLxhJi8F>!JHy#FTOtDSgU^l!TJY&M>exx-QXdN$vM>%aW2x=#D3VzGYl#fz`Z zm`-bTcE@E#2F?8OBGX+kQSZb4z*0rM=-jtAOn6s!U5?tUy!4N_8WZ~oP&x-?4j|BB zU|@X!W-&0bfM`%Ine*8uWAF5*H-40dx<<>NakINUqXD7@PM&38U{nP16pB&{OEZg7 d6_m{b23x&N|ZFMxd2hCm~#LC literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example__bosh.lua.i b/verse/.hg/store/data/doc/example__bosh.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..fe31057ab98ba2bf558732d705fc14b751086f41 GIT binary patch literal 1041 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example__jingle.lua.i b/verse/.hg/store/data/doc/example__jingle.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..d68c629eadbd4a91cc33d0593aa7686840ffa2ae GIT binary patch literal 1124 zcmZQzWME_f17;Tn26k--n<4K%6eyVH7hGZ7*}FErEP3mLJr2)umuNxNfT@Z(t)agC zx6K4>z4s@b@=iSvw!QtW<4GpnOKh$e#kiMn6h#-DoOou9&nlIz+l0UD{~kCcHrxKf z?4y;RpS?a?v3X&6bf)#P9ZwJV-n3S&{+07(7MtXudzHTCD#s4|bGJKj_d@a7JMXN| zcb!$P-l!%RQQ7y%H!{!c%k^t4uXig9irZRB*=v>T zJDzX*INDa?_QcI!3X6;l3+^+$ayrW%mi)vkCF@pB*|AyQcD(#K@wfDAoo>-fA9$=! z91)3RFrWN($wrTN_q^+CnpTD8-#Etk=Vkxx=u`L8OIBWJ@7c0v{ipT!N@ag%ZPf}@ z-nviv*GA)Qvm|;qYtCHiID5gJ&(9C;KK!x$_4Bs#FI>;B-+O=S$I##%`{q|Q7S&yv z)#Utm`#D>dthwrEzbCU-us{D2#c94FwYPBF;!wep!CMy{l$O^1s8}`4`qYH{BV9LM zY37xM?&bdfc$VV%Sm!Cnfo#hKG`8ZR>xjb@2_?DK^9B>v!*&clA-wS(o=IKFitGF+IBM?r6ckg3GsZ zL*9IE&u0^kax(hvU^%eRT724EanY$qgRTp!ObroBZM?95QM_&6H64>*xlwLj%iElJ zQVSo=6aPKQ#$M5-v3Q34r7225>(?Inb?KC!-(uo!oO=dFQrPsp$7*nb$V| zKk(*t)Ds@@A4hJ)DELndYS5Iq^P>6ZCo7EuGcq@CojBo#!0PoPPn7e5yW>?$n8_flIp9*V^&7SP-S4R`s%Mts!GahYO>fg3_*}C4D=8rz6ez{)>l0yJipc$-tfP4*3&dfB0tjxUh zoK(H!{JfIXyb`^V%7Rn{TU!OCw9K4TC54iV)I2VbUgiq02|5f6Y~R5w21XVj%?Ps7 zgyU9FYSpPnZ+M+IzIbTs87ulV0ip&@UIQuxl?4h#sfDGPMX3r(WvNBQsY-g8d6^{| OO368yK+Baht+@bo%KD-J literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example__jingle__send.lua.i b/verse/.hg/store/data/doc/example__jingle__send.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..f74d8744aaf405e4563f4467c7948c6ed89cbbef GIT binary patch literal 923 zcmZQzWME_f1E%*33~ZGUHbdTjD7e-jq>))}(YWhFitg`B>F+x_88xA5z*NPY))3$P zD+U6#`E^GhaCdI;JiSonQW%q{1>-6fr#oTmY)|g`dh1}>jo3Qd#}?9Nx7Y4aHGY4# zJ^k5K)jMlk-M{md*IdqA({eYa!bC_mn(4EK z3|pCPkStfo{)=-{W#_b9-e|B%mB-XLAmWXe-F$*XU?d-n(4m&%?X8q((c zDcHa)C9OreId_Lq$erfLdG$@JTEa38G5q`a^q<_N|1IZOO6N~t+g|zW(ze`-PhBT= z&hp;*s^q@bWPZV^rW;Q(XM~P2T@+uXaA3c+csv_qR~txRRf%+aKz#nEO4b zY5SKC+1c}LOs+K^Y2T+>;r%4z?98_p7A$#azE`lZt9?(yIZffBZ@Lywq&+5tuVILq zqL{ykEB9~x3eHb+UNM{3|LB?JC>k(UfD4<9A?}5yd-W9XNmBh<;KR2oN~#& zbI+b#`o2}+)=&3eGS@wtjJ`NrSJy8QTK>@MJ70R?g`JvL)ISBUNN6lGc5CTB7a7jH zdR0QLLWneH$u755C-j$1Nv}2$Dp<~xvyw~V+I0yL#-||+vK`Upfuz{pp=#St!x&zbBp-)$WF8U zGEcPhKCMMQgVc%nuj&2sqUYhDx`}v&v!bq8-rBd!scSy83%nY1% zamj`GVs>Hkx?+xo{B+#q`EhbqNTPd}knxg@J%@jmcj>tlUb|LSqPXS+pHW8g(R#*@ zbt;QMX&#hOfItVBU0lH|21XVT4azaUCT^UZD0N!%rs>UWZpW;>pDZk6AZlPFBNGDy pV;GR9P?TC&npu>ppj4JxRGg}$mzkGYqM?+WlbM=VqNHif1ppuYnm+&l literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example__pep.lua.i b/verse/.hg/store/data/doc/example__pep.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..4c4e2ba03e78914f4b97c04f9464fca42ba27dec GIT binary patch literal 1679 zcmX|Bdpy%?9NwDyB+?}v5MRUL3FD2#v!(7JJuvTs(>da+3CXtlX)Q(fA zA(33BP~9peV*?h-xmZ1fkBdj90Gx4+JA9S%Fp#W z6tG>c<!4l^MLRo9 z!p&T-Ybzp+`}CQ%=Xa44RNpp*+3Hqv4M(roK0YH{3LP+rWvj+xuB*!|c(>aHPTu!c zyKE_UmIpg&4ugF2Uxc@J1R!1=kG`&;ZQ{N}W=HH?sp|)4)yPdb45W5UDkWktyAv>E z%R}{YrlU@j`z~=(ugbf&X?)d-VXHs!Sz){|kTx)}I2FE_qOUUKRhq%v!5&<#IzY;N z-*=cgSkx+b#&`4vdSKI`)o$Tk&C!j5r#82}cV=JGU*Xqsxvt$%@;>8L%7eOlBiRe- z?nZeR(`w@O_jY@`XfTxeOd3mKJFBW6O)w~vbtp6s2l?Ec))KvIyHTZA{XYn!QY+58 zk7fC7CD;90-?DGjf5T{SIAfvW@@R5B?cF;B|C&$pG0*s)V~+I7y$-zdGAoFr{wK}yO}VkgF0fe_iaU;cJ0sy z2i;lYEmKt(eI%Gvt9KDLotNbuR$lai#^{@KtP!rnfoIi@H@6avHyJ)3Q%F);Sq^#W zgpVX;jth1T0TtAw`9E(snXJiV_72lX@s=J5Nozh)wKKlFOkoF_&{rr_%;4`D_pu2I zh;+)Rb+sW)x+dEU(LR=?B0O7UlP-89=Dt5XkAeQ!v+aHB&4-;XrOj5q^9v`8Da&rE zEoxyn&9n}o#;MACmw28Pj6_|6^J)`8?J64Pg#!rgJL$R6{8z&@UUPfr8^-a+gS%4G zll|7ln7F-7R?!Pb1l*It4q;EbeFgW7apzS-R%q!(oq@=boI$ z0;J-7Ic7>NaAhl>?W$yHqjcC_nJ-JI>|6cu5A>y&**5rTw}vY>Pc{7bajka6yf`Vy z=441Xw5>Uc2CZY1)_~eaefPBOKwM)b0v?>gnUQ_A);-VfEUn@BXP@PKdh4;DiJ|fi zQXaGL*{Sp!`juuOW449*)}}1AKN^es6{sgFQI=VGLGGguEif6!(BOo?b^ujG-}W;8 z+VyK7z5WrB7X_6 zL?HTb7(nz74#!4?_!9vVQCo|MB}M^SIyz_taeFDW#6(Zz0Fy8fSW1K&#L4(uV;Rnm z6*t7%eYG}ahknjhEU*8i`Tu+&8e+HT1}6acI4l8xYsCYE(|{Hdi^Gb%f}^mapj!WV zg-PDch`iPtfH#T=MC`Sxq};X-^TmQqWl3`@Sz9*?VKVC?36P5OUCrz{s4b3W=f6lH zHGSA{c^m@Qm~K1kG7(c|=V$x+=Q-s~lO_#xlFYrph;472H5GFUInPn%o(1NfC(ccb z%xBEqHJrPkWA>C6@cdDOt{GVF>Bmh;u94U7 z-VC}tw9F%!O8q$2aKG@tFbdMoN3Lcaxy-4@1{}d7Myf4FRtF+Fi(;fTfl(Gsn6MjZ N$}MdI4OS7X{{?fc<;nm6 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/doc/example__pubsub.lua.i b/verse/.hg/store/data/doc/example__pubsub.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..5b89bd02b0fe3eb56b6c607e0b5d350d550d1aec GIT binary patch literal 1225 zcmZQzWME_f1Lkf92KGV-o8j4iD7fQq{Upy{B;n*t4c)&RgbsFhsz8-MsERqQp}zT# z%>-<3?@yXG{Zzx!CE@yF85?u`a|IW;UW|4KR-Vc0y6Vq{$6|lq*Q%tdpJTh>Ezoyw z&rI|9yy#yw6UtJ$*!lKEOD|utaDU1ErIBp~Mf;Zfs{|$dm$g5gX?91`yi>RHz^P&p z`RlK@ExpfgHR0kk%|CNX7$jwHva3yd9dr95W_!9*>q+wsf!q6z ztXX$z#|P2tTt6?Fx@7*JXuj_1mo5eS@`f0@khbjVz~^1OD!q#5yS`4Wew8}qy2Y&| z8-?v(Ke4Q8x_@c=_wV1iuR3p=u%y#@f`pBw%QqEawhL}+Q_j6TRhFIS9K@CPUi?A2 zdEW`Q=#KwC-2O_>bIh7-bN|DJ@8PTFe~=BiII%D}ZHfHe@;diLuNQF(t}wJt5Y1V? z`TsMiPqDq*)a$NI^gGd1CHo*f()HbX$$e(~*1vBL?@-#frmgR+?~C}mnchK1Tg2uT ztJj{{?c1%bQQon0kKF4m=H|cm3DizA%-4%yWm|2}eQ2AbXxFEdyDPm;&Ucoz;qf?_ z%5d&zXhiA0z%6li&*e1MXn1cqHSuGghSVG8>TO-d%T-=i#jStUdg_0&@zr&D4^jnp z7fkS%Nt-s$dU5i;V9RMq#dULn9PS_TmYI^#e>nKtmJ=s;OY{E}aO9Qn-ItQex9NhE z{r|O>^txsRNvw!UnVP;_*?+^;b0PXF5>u{My9Eo{TUdScUhmfDJ!3_CY7n!I(Bv)U z?pr=gdVf;&a{8Yt#kio{35QN)Z9e{-Soc>dM;2?h-{Jis&uUfKvrl&Ohfv#zUke`Gh%aH;$&8A1u6%~)cJJOD zzF)a~Yi=%M@{xbqttTgDMH);=f2>jxa@MfYJM);=CGV-(l|^E!W2Yo5vpxTIsr#(i z?RBwHeeRtFnyH0RZH1ai`|3p_uZ6|r@g;g3d0HTHcIVdDsh{KWR-89Ic(i^mwjH-?YWk^sa1_B*mR<;GR7#LYVG$=RQW-1lhf3Q06XY=|ZSMJQaHx#;~ zA!^{{Sq271MIcY1D7COOvnW+TsVudqI8{k6GcU75Ln%2YGc~V7NzfJbV@{<#D6!P*@QWb0! zlJyEolZs1|G?XB;UU5cZc4~1!YGP5UUQT9-l8%B>K~ZL2a%xUxd~RuRW?p(R$VC85 Cc@`c3 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/init.lua.i b/verse/.hg/store/data/init.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..e83d430a236685e3dc07f31029d42881af05ea77 GIT binary patch literal 10627 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/libs/encodings.lua.i b/verse/.hg/store/data/libs/encodings.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..aa9905ea40e573e9c0dafa43246788932af9843a GIT binary patch literal 507 zcmZQzWME`~03#q}2xT+;hl0J1?F^5m$eP?==ETD=p`viY^S!+=H6Xe)H$SB`Csjcy zH7_|oB{MI*Sc!|PxTFZkDkw@VP_R{~uC?akDoQOWEy`1f_qFB%>jv4yz>o*T8^A0E zhHMZGva~=euzr5PI_a#fa@?NfkDtuR+8_s21EwnGBqua5&!6+>$q|{>EtmE@nRMol zP3xXNlYX4J(Cax3vXCxZnU_1#^qqx!YD=26bG#doW?QkimIZ%9t*F{;$~PiNyip!wWr#f-zO)Es=trLDcSlG5<7O*u&3{R4`4Wmpf;b8vePXSA|0har@!O^% zv1_avpD(1GV0-m+*|{5YpNHKl?QIKeD`;45sO3K)b6V-NCnsumdf)a5nY!kl!tI@Q uvMSrNR~L29dnWpS_tN?wS2VipJEn17X))tF%w}ZRmR%@ literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/libs/hashes.lua.i b/verse/.hg/store/data/libs/hashes.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..20ee870018ff42c8747813fbf2a87ff781169aaa GIT binary patch literal 122 zcmZQzWME`~04pHn0A)A+hk{G@^AtD^2V2&i{uNfnQh&{uk8$va_bal?^c!dUN>7G1uk(pB~vba!m>9i?gPc=ghhcs$zSmLzSRGeYMV-Y_9 DHsLB@ literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/libs/logger.lua.i b/verse/.hg/store/data/libs/logger.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..1b130dfb8b6766ac1f0c8524a2c436a5fc96c7bc GIT binary patch literal 632 zcmZQzWME`~fJ`9O31u_edKbd9e9xd2biTU;O&tZf56Q2Sh7mKR%`}%%8oeEV0rYh#F4Ls;|#6V!} z@n>(?ZFWuZy%F%`#j(1kcm=s6pX`O!v9m&0F3NvcK3A2uyCTTOhD|O1-fVNtu1A|Z zZuCyxtlT8Ow_B=^t59x*`|ri6ESF!mbe}i5ru*cPf#9x8yBiw87c*wLbs80VJWh!3 z6kTs(R+hG~=v(1TPVM^p?N0;4N_C|}Uv6Ge!sX1~-Y*s!d+?Gl$M)Py-RI{OOj3PQ zkh>x4S*fb^-OJ5JA8d{VoZ`@P`cwF|gn#GV9O08GV$z0>-(ETLJOA)sXSsiqq}@{P zq<-Ff_C@@k6P7g`+rC@ISEihuA%9)C=JAGK;+KsURf9au7zy@zJTSb{!7K)bdXO+E zyjC1x^N!g(ExYfWVd4J`8~e1TKUfS^1EwnGBquO13vwJy;6HlF^5@A;ORkweKR$Jx zsk)MDo5M1>8JTB~9yp-FcErpfb=jOb2C7n)D^pimT5)+9s(M**W&U*C*{LeEbLG*a aoiA5faaC1S%~IHDqw1Tj$Dq_HRR#cW{{Gqk literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/libs/xstanza.lua.i b/verse/.hg/store/data/libs/xstanza.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..6b756e872350d35e8a52680f737019213e6298dc GIT binary patch literal 575 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/archive.lua.i b/verse/.hg/store/data/plugins/archive.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..59fb467053175acd1b0f379a02f57753402a42eb GIT binary patch literal 4011 zcmX|E2{@E(7kDSLKevJ6w!vWJpAdnTbQ$-XaR4e>>G*?00G zBoRp{68|jy-?^?i=b3ZebIyIvbDrmY2LJ&efFu+M0HD+&uR&)||B6X5|CQr^J&urN z?haLQP&$q~$EjotS^H-unCd5HNHQsQRN-6L4R21MW4tzl7`Mb?aWKKo1gXv|85%|> z4|=W~ylW8Ir#NUyyLwMaWsh30w*qE7eQ~q*@rY61#uUAJdC)T-xN74&L0khiGlCPsXY*f}ev@q1|I z-Od;J(&dB2v}#{JUI?qMmg*9uExAEa?EzM?maW^q*S`%&9`uBP!I z7L~bq_Qu0nVH?5hFBQf&Ji6ic7It6y8vOE9tyTOMjSQ_bWKREBiu#-qn$f2e@Mb{( zj@TbAHVfs7^h_BPj$ToanuS2ZT{jr&Zi_jNE1Zsejd7U>LBr4=C0*iK^sgyC*^1R4 zR>#&F>Ro-4I&jXcd-UOe;FMC|)?`WvReEsI6Z}Q<`O`kDprS`3q2-|VZ7r6(HB$!k zh<{ua7Sq&cha9JN}|bf^<_kGN)j8PD zLbtqq&=c@vSNSlgsAKru_^+Y4Z;RsEma@}_BTsnUniFl!3d#Fm{e9qMr-!|ly}P%y>n$O9Uk|vD0+bkm z!h;kifRMX~v;shU1PdbOc75dbdVFOoa6xtV-nHi|OX5qz56Kv^_Rn;%3kes#bt@7V zKY)oGm=DXGn4f4#!bC!*fm>h)bq5j3#fjzR&OKAgp4`>=ff`fF#ZD9tdwwi!u|!^e zew&W%Ti(~%DiUwux0LI1`DLr*WR$Bc-}2T2ZV@)uwC^%#Rb0DWs%G#{8!P2`l1< z+wGo%y&I1o*2^2qqsZfh_4IZ2!t(Hv9K7TnfmujD!vX*mH8}uCfMCJIR66}Kxls+? zq0RPHRtM^$=dD{mGLSK3P3)1~i$0)T2YJ{RgM@YoKpUW%C6NVbnk7)30)aGSV3$Bl zfLV;r6q=4MK1~J1UOe~W87%i7Jfgl>nyb95QA45@ZJ z6pAQIr3DnrxPA~*feBkkDBl18=v0U+!X63|)EQDg22-e!-8CtoPXld!b z8wm*bXI7ZD$9|AxqEBW^s$x=_U83LHz;VO3TA9L(R=!tW&>Yh7YIfFAZnhm6jbEKv zz<1FSy_m>MJ!}4y z_8;rp%O6!I3KgTjE&hWMTu@b6T=AXkQ9C{N>7{Nk4jWyGpJ^$1fO|lZ@DwS~T#s6| zgWa~a1^d*m>+aWi6fB=bH$8AMO}U1tVV$P!wXD3GAo-GFbRBOxqI8+tJ^k3Nlpkic z`80?5F+P4#O_Q^wG1;qU>MWlA$Z+N-^2-(IUh_>h_(6Z;gNm{K;Em+Qth!h#&~%y| zfL=ZM)(l28g$p`9x%S4-@d?j`p$=PdL21UKrjZ1zgnz%^*$P)=y?Up0D{&?yG*2G( zQFjfcUyMnLi}Odqw<^>f0&r32mVSxKS;eaT*~fwzF7x43%YkA)5Bqcjk`t<*DzZr` z(v*~0LUQV4L<~2weN3K8p)Abe;^59)Vt#CXnSyZz5 zau;6-x7X{R2unj>d(S(wsFggtTb9JTmx*`-rnG30pNCZKy@kQyn8}F{%_l)FRmkCJ z(8;I!9TrTkr*=k?92CbFXC5vboV-Bk!v-o}PI<|GDhr$WyYXX@`|5>J@M$K)9UT_(DT)A)of2q;T|Hpm?X zq*(j7JPcoIAfCkkgmh&5+kkde@{wU}LI!kwcRtko6r(9bI6XQAngQmlO_Y@8hEgJ6 z0atOydZ7|zObT4Moma(-j&8s^}vB%jP> zO?4Xaiu~}6bNcvD3667?b)i&qymS#rpb|DK7j5l>`xlCU07v@b-G@?Q6spx z_J9~r(_Z&P?XHWbCniwX5a=$gU!wqQS@8M&rNxca~f05IJgb0?JNECU#1+QaP zWIg9_g3NAY+>FwDYDUJ8HHm0EmO!*N^LVwBGcv3*cX{Gha%a*+(!jharFCTP#JjP9 zTvN(|v7`Y`#TOAtT$VYq>|pSF79J%UM0*Mh!c)ldoPUiCkS)qz<2M)PUn|H?D+8sD zkSYY1O`7&f827#(AXEwj3W?Pc6H`l&(vJfG{Tnh75YYo6Qa%j>ph3}B{iM5JY<1j^ zs^7|p;UZ(m+CS45rw@H6$rPDanPd`C&J5?u?z#|7Z5XR@QFdg}C#?AZVl$Ipt!cy_ z+BfiX0reQy7rvKR9vfIi`)CB>q_aeOH^;Y|f}KHMikk{kF3l!jX;C>Jx}TIYaPHE$ z!ll~!`?kjC+Yc~1UrVM!Tiq|X`_8&+IK`qIlG9gc+ht5oZt%_6#~$(X6&pv0^kl&zrW+kdQQ*8nO8Y>_&O#R593Bf&-`|0 zO?||1xaRcZ%(!Sg8?i751*yD!0APfW3k4B(mxkDp6bJ=~m${^G;Jf6Fx!A#X$WvS5 zlm9>Q&%_$4>U63wajN20^Oc>tbtx__P2!a;TLPf=9DadUJBAFB(uOv8|LG0>#Tbma zoQIiF-_L0-D;2s_snkUWYWN^btHfzf$rUx~e+HRUE#O_FbGR*{HsAH3=<|Mh;^IQl zr~ROaUs_4_8o>*D{bdftSe{SXS13iDeU6eIN=@`X)_+;uwOBlc&>mpm{Kxpf76-0h z_dkSjtQ$GE&Ng&!#4n2fTptm6S0G$v`R?%eV9-6{+|250iu|VVg)Cj;--gPF?99hb zTC*6L2Lf+~I&$PfJx4@V)URYnB7Kv?w`auTQ`Lf;%!{CfJNLp_eL9KNLw=HaKzOP# z;K`D%A`V^YS8>g>={}IRHWfB($8#jqT=&D*+w9hVHZIjZt^iEb~x@~5}GAA2u*52DT zVmaMrIz5A<1=DohLcDQ&I06e8Z$1rus&XwFZNqTM_-#^TsqeN@Tw^68;%IcJuA z0l6KnOeD{x_Oz=Zu;KQvbDra43(?X>sH|@p-!f*g@j>?Se+vD}`_l}MYYgWMphi1q z8C~*HQ@^m>{cqQz_J5CZ0Elj7qs+`DBT zl>)`27Qv5jv(+g_YO4Nk#&RVaEn+=l-ppASWY<1`DhdVl`JM80-LE-9Kn@12_;Fnt z;;*wXELLqEZPoe5^3>qt*d^+q*vzHHu%%*$9bW2(+R)!3o?542E6u#v$v#~CP(krWCjHVr(%Dq< z>|;~~Pe$Fnx!d{I^}F28)@QoU0b^ThPA(q9m6Q&QN-_sKu&vLoGF0G{IYeQG!?r=I zQ~^!Z#Ro>N_C67qQl7MwKtaj5z;f>T>l)u#D{$inC*dhghHO;Fuh_m1_1oG#3+eeE Dl5n!w literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/bind.lua.i b/verse/.hg/store/data/plugins/bind.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..a820c8ddd126b85c0cc448e390cf38000b361ca7 GIT binary patch literal 1857 zcmbtTjW?5f9RKZw*{nD+;V4U6wY*f~Wa!{T*3z4?E^((=jET1C<|S9y4W}d~4TY7o zs3f|$5;EDDRNhkb!u8VYJxTW#N8I1@wA(*$zvn!^&*yp0=XpM#eZSuU48Q=iFpmMi zO@HTrxS*}F7W?dQ-=T+nt40;)!{=Lb8us~UH7Fb{j^k)Ve^L!yH9U_uG=H_#a3vt|*cO%l_^AEE`xzs*?->Sp9~6yJTvLmq>&;KtpS7kL z%yKQWe40JHKP2dX0EMZ9ssXo?HeFOvQIpSc{v*9?NAdIX`1%Vg8m=eQr$_nEk8bM- zHcR7tT*hK(W^3=|6a`MV9iS~F>m`(|DBM!&n>3K+kVo5e>MLvhVIt|~;+R^eGk16J zHu(e#xoZt>9Xs8Y_qjH&uv6oDf^f4sB%UkDjoofB)5PzVSP!48ls01Po<|7p5{D@L z`mYzOEXKK=`81dD-Bo)!Q(m2K_PDjxbth}+;G&%V^(F03&95FgV-S?oGyJk|rs(3< z9D~>=Wv?g}eD5TlH#wM)Av~lvkX!^zM%?zKe#u*q(=20p{j)B8i;56ld=e=L9G}eY z9LzX$fH7qrV0XVx+zvQHTT)Li*NwJu8~!TtY}=^X)l={A?vL%7$wU1l8Vc#Un;>nO{$pZ1kWjIUaY#GkUFTf~8tLF?q7UVqT!E_$q#^0?FDnkVcF`vbveB$wCaVmr&bKm{k{L z09YfBk*pVHFE3ckFHjqdlMO%1D|N?RZU$;KD4LGZ|Cka=O`6d}f2vob9fjv!d^avZF1c=j$k)N$Qz+hMX>+iFYQ^l@xmH z${x=4(bh(Xlt$oMWAy;>!4XimL>Lhz@YsnwMk4lHKLh>-)vO}H4VGzwVguE%O7fow zK{Vo9en`kOIQ3Eu5v!tmDKi_mO1?6lZqD8&k7qQh%GGM6O2u|23CfwwsQ@{na3F|G z?wWURB462nFX*Uk?<($5_?31kmJcH1E3Tl9*KUb>gxJW!;c${{VGR)8Z6J(oG8fFYP5ib zn}KbFH%uZ@Vu^^qKiZjpgnvk~#_E9JP?)VaE;N)c7F*d+6RDAcFzRNih%b&4N_fc4 zF@_5X`4a9v`n|)DeLxuiDu_Bri0;*Wgzi_jQ}UgwZEK@X2dB@dcv=k#NdEwiuBZB# zdHnbWB5SEwWKv*UzG@tE3yv3OHlaPVJ!LwCLI-68i$fak#`QBmJ z*?vj`GhTH&#C|zVH*X!=wtJl|d#6A46UUnEK<#+fn(i1^4<_Ja z3Qk%)H`Z<;t?5&Y`oYCe^tJVLDg1Qk)hW4-OPAts)4|kcf-f2D8YZVBui^92mu(Gb zeb_*tF0cM*V0_dau6<>1d|Bmd#9iF3EsKqrRdzB|E&2al?@eLm%K`!gm^E|X?R&rP z_arwn%gGzoE;Z_2n)7LEfan>;S6fbPE4`ieH_3$i@TOqPF1Lf+*V~q_eWSI|=hpHg zY^Ld@LTr~c@7%M!b3dg(*XLeKWB(F)tvBB`8%S{YKic=|QKX3BSl2H{p5ol3nU+-|42#*PU=(Z)R7}s@h*a zSlE^pGR;!HY23lItm~n&+ue^TY)kS~Hl$qIqbyu>d~a=5;PSdtOPDYJ-`4rznwIeY zeZKV<+qxD{KiR#}EF`^7QjMp#ZfB~ovi~+^wc-ZIL`fh&z>r!4XjIe0+(-j zR{2XQUTYK2K2eM9U)fjhD`t38!+fXp`9VO;;8@3So_IU3BHpr8A2{SXDPP OOfnT0XE+qcbr=9@Q|}W1 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/carbons.lua.i b/verse/.hg/store/data/plugins/carbons.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..760d2267badeb6c9bc05f4a9b342d74aa56c8fbc GIT binary patch literal 1829 zcmXw13p|u*7k}sCevJw9F*5bFgEUc5o0LYr5o5}A+toJ1xaQJirZpKMw=H~8XvJjL z7?4m&~O;QTUWm>Ges77Juow4Wld(Qv7@9+Pd^PF@300e*la)WvSfU>0` z21Ylx>~F>&`ml=Y4}vw*SC4Q!Dt=UTd$Gll1B2D$?Qn6}TJzo>LSG7sHJ$bfAzKWvZV}YWSq?QCixLU>>fCUwY!ipR4C# zDZ&nJBa(Ajnok?`(0^## z?bnxW?S~%CAQN0&LQr_A?;)c}lA7V-W?uAjrL~4WB_HDVF~mz6KrFpJEbkwul<}e76!jekKX) zV*ZtiIB9qf;rg7TXK}EN+MgdQ%mu>TUIwly4u+yXcNE8_I~d21+ViLQdX0yw!`!uE zv;4T^P$o6$$$SxkY1b2dFw)H~%B=xojGaD^G_ZfXx_bUK<54sn796t%>#kW(2XzIC zQDzo0NxI4-09XSNROP9qZ0Q*C@I?09QD$NU=JzGuD1M702L_8mPK2ehzDU|kQfvF! zYj$rY49+sQpe zyYBn9JLk9EFwela9f)6G+My+d-VP7Ar4-k%bTQ#)pX#wNg^5e$X9vbJ8{U5i!*}$Y z-tJL9_@$C-U*HCverO|_Oxt7JO)08RV@a=Rd5Is3#IrHEJOg-ScUf#mziEzHw@P2s zg2&G;@9B@j?s{vsyh`~n7SOZ{QH-A<=vUd-7%t!pR(*@=xTv$({l9nm-nl)9c7D1R zKhqOvJy0B!Vx8Qq+Q`H&s=Vn?bqf4{6MGq5mO)U~d^6`O=?E7 zOM;JR-!IZXy0VN9Dmedocs>BY#j-DZHMNL~6#ob_ihp###VAr>ShSgg8IA5A73+T_ zB#2I;goZ@~nn#62SeY>*Bcf;|1(9Z9uMQ@IJ(H)jl0D<9JOUsn@HhC3OVmPB6)BT& zJ1C|-j@0auXM3(s!BKcnJ>+*}GMO(sXCx8{foSFW z9o~^DQH{?_6yrJ}-yKcf+{hK5)Z#=IMBN{0Wirq@6jR?&a!1X9I!z5g1Bg4?8yuS2 znO1({P5G<@sZ&*Us;0%g;uiG9>6i;Fz8A94!&s=EiKn8>}uWE8t+YU`1s}z%WhSxiYll zC|3CPR*GSNWGr>&>t!~FWUO*dzGwIYc^|*YS|8eK9UO@g*t%}F3G1hFrO_GOm5xT` z=^C0k;qF$~a*(x7bkI(+M#Lv;zeBiV+*Ok)YKN#x@SbGBAm=!zKaqF7m+~5Z205}Q zOaOiXsJ-6|ii=Bg>f&`#BD+u5>~i0fOyB@eH+@mw{D%v=%N&}a9p0m9Su?Ll0;_mdusorS2^S*&@y4NvdJ7-)$e%D{WE>C%{ d=S+FTH#VaQKOPRqb*;TDPLiD1v7zrX^bg}B4aNWf literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/compression.lua.i b/verse/.hg/store/data/plugins/compression.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..42b73ee47dfbd43e5d0fbf88bf980a112620d04f GIT binary patch literal 2166 zcmY*a3pkT~8-FmTX$&LkYnH!CtOOt3PpmhLi9RF;bG0_ic-L zJlRqMqp$C;@9*pDpJVE=D0Vm}-h{>Lq_mxWSXF5oM;RgXLs;J@;9>SYI#x$2rLEd1 zNl$1gl;d80qC|qyEL!YADhfzT|AWqGZg1V13$A0S<`ggjO6;_q5NPzu0=&oBktY?} z^XDkRPsX1aUVwFVJC=_;uRg`CK34R{pow07`Cl4+)rY$sPDKX5SF`P0lEPJp3^(H{ zP#NKSkfcX^PYZI)W-+eOH<>w(?y>FJPxfs1nU5xt=9%AV-y zR_9H64MFkYg&meEuQ0h8^joR^vGz<{m{g5w$5XeuMU}*7k^TiwOJqH~E#=XU72&e!K5M`4&jMG8iCvuGkwKd zXVMeCl!aO|;3n7EB15&(uYb>&!Hy0ih_iQW;~UtpAzJYbBW!(`UoH={T((8ddo+}@ z^^!`>#_(e~xax|E&GyW_*h`NSWa;f{Dzs8wU%MabM$hPtzQ>$w6xdd3- zF?`-3znsK7{zN=dw3)x7Z>t;=~eCd$;*F5M&HD+`4CW=VK zX><9CnBsBcO$aibmwy7)V?9=13R}qgCeJwA{cl3wJ)f`LV>f38KZT?kn=NoZd+{)c zCDukPz~Dk8Ol^JhXlv~Uvm|vXMSknDV@nF!M%RWW*Er{+RqD9P?yYY`k2=iN4MQ_k zodciZk!GyP@f_T=S}m@Fwjh<}$;b#}kXDK)Cx8DFLho>lD0G;eet#$<)U%`M`9#~k zEDbP(=nAuHAIy!B%?NO5tx6G3eCV=$U?Y4KP8+-yL2dhNz;^ zL-)T}yU&V9-n>E^uu%KV#LJAsv(^zxF!bV@39(ZuF95WrAla)YaW=`Fl3-x=yf^lq z<;To}d2L$>e!3#ycVrFDRS;bk%5SX7`RybBEreiP5kVDqXMIV1wb})GGSzVTaHeKG z{Hjf@WO1W2oM{?DjTX~N_h5C^wPa>q_>YW*yF!B1QEy4yJ7sd><-RG>Ja%*Qi=4yJ zdik@#Ktt}rwv(mZW`@Ekr>~^CL3_m?A@8gjYo4%RQK8j6cASbQo6)seK^kbZ?@V43PCSmy45T>3q9uKE&hii+57qcUrZu&T9!!uQ73gkCh}Wp zWPjh8r2A7>N3nVttIa9NG;QD0+cGCoa@U1UC$vXF9QhxUbQBUopZqX#WZ{Ceibwg} z#OpEB6sL7v-Y+8+N`QpI0Mg_(hmBx%ve~45Z(q0kg4^eB<`x%M-2+W6l$RlI%B(>YR;F7>HF}Z^gHHd_LcbGz#tbd%TY{MlUFm8ml;q_~b7 z$za}*XIFD5AV3y~x@xGu2K>`m?H`7Ql4>%Vy&mUf{SxluhT>#U^~7S2sUlokQ$2Bo`UktL*2QkD+slm-C>1f@&5L!?VVQV~J< z@9u~1@0|afGxNNA?{jCK_nkZM%0p z@{($iQBiW6{J%hl7bQw2-gApZEq#TZZahh6dFvyduxZY#*BsnkUq5wxv|la%o}BUW z1UCHi^WQ>^_fmb~^UNRGGcD>r&AI4s{R$YE<+H1McK+u{M@s2}ir~#Ae$#vNid?v5 zh*o1{$1u~H!@m`=T_>tOA$FeY3-=5|SBv397DF)O>PLU^zz6w_JPFD=F;MIItTl)e z8y04G@8Q7r{IA!?)bAb%cTHhrn6P~pqH-PIl=?{N%Br3o5m`@^C}C8=!_vu+D17%U z-XQi#gKkizUg(K4wyr@e_03Km$oS^pdtRlrZ705Vfp7zgwUqmMiz&H=8#r=-9SPrZRhnSr2CV=Ot)WaSCE- z)#Cm*ruqas-2vC6k#?k$5dT}cE0LI!zZT8jXZb+NBc*WFi?yc{SwoMxDQH9gVntd% zt#9U0zwE44HyPRjq$7My8ko`I4&y&n9G15@8xDvaCG%nqNU|ArmW24%%r`O7#Uw? z)-N$rN)%iM6>@@%1I3dzBWVl6r@&EgPHFw2R;J`ZU)ra{^@cSG>8Z<~`S&&_A&UuX zm3(GJ7wt*`;|n4?SFwA!m>M~Q)%?oxpNZq#%unF$I&vIY3HG>Ahus?lVUpICq$;hN zN|~XMGo_y^@j()2VcEpd{Xr{xHa8RnMQ;u*=>cPCtRyp=$n%2A{|!K;mUhgeX(*w?;z9tYf4Z)7SvZ zX5ZY6oJ4|STNN`D0__j~Y8WxWcIF&C7&1L^8yrWUKEbHRUbXh{~HnRxRb;*Wj_@kqFF7 zpKa`pb-F$bpzlor*xhvAO&trA0fj7kGdriRdhYx$i zFo4WUVe9DADP5guTKv?wlWon+^nfeEC~;7+>+0qoQpbEZw*^BIY(RGxCZi8sNV9?rU+PkGSmN`%$1?1BShltJ#Ux z#!AECbEmMx(&MBJMC^sc?Bka;>l+b$Wj>Ae1Or~YldrY;ElDPie{wcWMrm#8*ide2 z=RwO_>njE&CM1a*8?n*`#d@58&--cq&j~V$lYGAkHYPfSt2kFt@yJm*Y4S48i6v>9 zC5tem_vAlw2VM5)vWJZPWjHRLF)vw+^pDCRN6b?Zk5)|2f=|83Q>`TQ(@YD@swmDK zgztm#9xv{^@m&9F$-W`=tNzXm=L0;?6`5yaxJK14^$$boO2W&42(#2RG47u!t`ADB z{Y5R~=wBaoUOABdy_e(9{GrXdV=CCZC}HIMqA8*(;M2=)9jD!2!#yFJ1`}uS8aIY< z-_}JYsJ9EFWGze6$6B*O!;&e2R|7KUK-HQ)^vJeU(?x?2Z)1<4iK9?w*<@0a;f#pGoB>Yc%HmQQw75gG0`h_NpPCGZ!I?`!@)g4anU?`3m zw8S78^OHW=&ZWh+m|N9-myuHBS-#`v=4R!pFZO?mTK%8!EYIRFiW{Y;@d3qDeX@@i zgDU8f9;=gbyqj}UoJLG}JMy>@T}9l)pOFrpm=8R8NAhr5GU!kDbC;;^nfOydmXird zl5NDX&vZK+nq24^gU7m{DO9-Jj6O2eD@E(afA}2Ti;lYY%h$+Ut|?hJ_$^^~I6OX=U3oj9enrRZ+tLvZQ1>Pw%SX!_eyX}`<0(3w%ehXYm}!v z5Nd?&-qaQDUZg|!nt?t_MKeLkSofyIc**yPXq|DRk>%c6_T%Nm{8?Iq~%7Zm3_ zN^PYxGL~Ygg*kvBK^nD`kVH_Y{fY?;c)z|X{ z`}Iz{1&+!4ZH&k|&+fAXCHGQ$$_zcK3J8MX#6*7fIU`Ps&*wjZT-C{*wLEjgvAu_3 zk!SZtIw#E@n36fj>^l41eOtDF`CIrIzl@T&+Dy7z z<^_*T2CgZVjcn#D>NZ4+A#Qm1g{1No|r*3?mv4&Qhf0)kF;)4gA z-T^8L98=Ny)P7Mx9IBVt%Vi>DmZ?6X8b0~U>VrN?9a*BK`PVU{Fn#|ogI`mH@-rU? z$u1dAx$Dxx-WLR!6tzWPLK8h2i#vK8lipNjH46AkJM3)-m*P|3_tooy=!`x)b?l4w zCyN6oc`gs`+012j@4t4peFCiodjixI+OPZaGNkLM?vwBxJABG_j^bYJEnN2yBFgLP z9G;5aA--AuX4!H17A_%WzI06%Ae?^08MrcXaa-HHdQ~ANs62>N1f;STH&rHY;$muG2eEG7b@;Om~w`Qu;TW28(x9YW!nl|n%`;TO$ ztS%OHjzk?kktG5EIK0so0JxzLbqbX}F|IP}GXMOllkO-|&Xb&pe)IP&2MzEX8%ym* zYrqCaVl3x+tCv1%qd3~{_3w@3-lF6g3KzbDN*X--DTxM|+NDS16q~AP*g?J#=Qc};6z(&>WW46-$j{T= zDP5{~FD7Z2N5J;N9P6()nfX+#O@VZ&U=hDTz0^lh(*da5`eM%zs8^0eEBQmb59~3l zSFp7wi8r!oTeQz-rsk<7&Dy;FkHHUG)FH1YpI=(ef=W<^9uc)lcw z;nOidX(Mz-wLlF3a3ata0Qevg6Xh4DnKua1bzj*!>W0NCGbY32t%KGr2MtKS66FaZ z1W1(+g?^DY>Lch2rcx-9M6xF2G&GO@Op1zywX2B z-j0<#jp1H3T!C4m6oaYJx}PDP9Y9+E;D#sYre^L~bt5QIbOBvCuF2mLJv z4XAf|&7>3P2e;+qp*~9ypvd56^ds)baZb?|0Qe&j2Q^OQ zJ=v($P2293WoH$chUN6jbH$-s4jNF=K~&mXBHDU7I}xM!SXoX4{+Db7eH9W;0i0QV zLze+VF++DNCQY(98v^_!hg;jPzaHNpoS!3MGrg<09?9g3jmUlh>KF2;QGLAorKZe z8qx5AjD4`c`B&1PTMioFIc^Af;%*43vX_aLpp&_FG0bC=vOQEs(b9RKX##MXq8VSi z+g{;T>Q%@C!Mqv0!L+&VGyY}k>ZSU^z>Ll1cu!InKDm|Ff#Bpl=)!&ZmTWp#Th{;{ zFX_eloGYDGYx}`{eaEuhk|tu+Vtk4K2xbj*_mZEbD0;%7;1KuKekvH=T)Qf_XGWhq zO5~RSuH4wqit|lRT;>{$9Sq($s;6s`&doX9`;c2n%z!y|75YB~GT=e+!rhJ!PPH0N{Oa>uNa?@lfGl#BH2S+pUkHgR4i6@K54i`n*y8 z|HI*{+u^<}Kzc7b<{P1{G`uKQHPDTQ0mIfQOfxru@R5mv-4tZ}Sy++!i*{R!vWF)6 z)h&#g6p4Npe-R2_{!%JN^S`~I4P3yYs}X+vx8C_fcx5;{lO_$ArYaDun(MblC+A44 zS^h)^PbX*_zOA!b3Jv<bq9CiY||PdK5DE)KSKLUMc+Z6)J=lUk)Pd~qDx zs1S7RMeN1DU6ZYqKYp^VOjVrD$T}J&UF6P-cxhQBf~m>S?72V(b&=bYrTW6aY5axP z>?ovC`A6M#7!^s1wf1F!?a~v>u{&Ec-Y!nX%eCUpg{}MZwSHbH{TXsKzF(Njmzt8f zG8{)1gg43q4ANU6i#@4zlg>~A(YWDahc+vsn}$w*qbxq-T-fdzxHCU8-7N>2|C|=a z(S68~bL0F{3da<)3ftC9&)rg#LyO&mN<$=KJJ3%+??c-ff<|LuJ|nL;hz^fYDmZ3d z>s-Uby`MME{%V`}q8d_3*oE16Eku4t)FtcA4K7aLonh3xkOZS=M;wYIWx;Rt-rDTTUW=WdJ61SFr(eJLOXRNnSlheQID01i*@e=4>O z;qg`Q!1ZbY+H-;Dlee5(0NEn|8f!Oe?Ni!005qm+ivTTNQhFfsiJGSd9p$PdgOP+ zOQd%+PP+QjqybmJ1sD($4#DMs}9ImX)NRXtJnvXf16 za+vb7GSUNL^0p(^U07$9S!*nNnoP^(-!=9~WT|D8ElA3QKq+Q2&aK(Z^+;o0 zo1f<~Z>9cxg-SEy5M8#R98m=uEhL3P9OFc|;E{JGpNcoOcB!{0DHCpG6LimA8*b^> z^34zjN7TK5`B#Y~J$?TmpOHAQAyS}odjARWGhAxG!!orBD0MLj; zFe+Myb)&#cRzm%77UkG$oBp6lK*Q>mg9dc8WRY3BdlbMi_X&dwBba-JOw4stvol=` zQcZKRGsrD~vJ^d05!BY!@;VewrJYO9&K{g*+An90C-}rHG)x}XR#Kc@yYN+bAxIk^ z0RN&3L3K{{5IqDBQrG`>t|sIaB9tyZ4D5fes)%^BzAYezRPJDMxRgYo+5gvIApJpA z0D|uW^R}~cleDt6weWQHb#QX$VDa>}^RaWcv138Lx~NCUP{$SkC_;{f+U-!K4_S#( zV?F=vFhT^cDFBNX5uWm-4|T*9VxSqf4LSzDM>{(?dH|dpy->Ief_Y$Q$RyQN*EF?U zJKa=2r_#tgInyZZO<`VjZL#(!w*GNum^`DZu%lPFoMy1M^(zG(XD|8Sr(TueyxFxQ z3cd^oz9=m)IF?-QN%mlXnva?Qj3T{^%0NMc6Ccz-rkx_Bt>^Vf$Jx5pz{~k*EY342 zeU`A6F#U{*yMPx>44+=$aA(1oTFGZ1Y}8@`C6t^JEteYU;li!(86=XRJd}Ae_|A!% z+TxeK>A?#I(ZlJ&6r)?tEpXu>pCd2Ee8$%7r4D%n+vPINKz(vH*)p z`|VD+DtlHI1S*>9E$pM0Ch{}1NC3O^c3B4L~p};3SAY;IDuEh)QHZAJPgUeJld<{{Z&$$Vm>RI z@_bh2+mC-rEbRe0Q!?*#ZRNDiN{`~l3=j+BEjCDvk+aIaSA?k0P>z^>B9{^V`LSCW z%#Yf4icO}t@h-!&DMiyc`$=bTg?Svtc86)Fn!H`pmIUfuN~Wd`H+3)VavN|mAUZU!c*s*KIx}~42+ppZ>*;9vIvLRLiq2BK ztKehhPE@iT3GL`s4+jvc^Ni^tEJ10@Y9q#!Jq}$yzk|$X5-f*vce9Kh2re1Jk{py9 zqy1+Kj7&U~)(&7_TD+MQzY{oSe8W%m>{*V2PBj z%~V+QaSg|p9k#m0`(Ec;4?ZQH4UU%7uxF1fC}_w(hxq*wieJy#y<*SAJ)lhJx-up? zm0S?J>^383X_OuSrbyhO$~Kyyr=a5s_N&p?Gq*Wu=6X!|*(3Pz;2nMtZ@#Pl^}XNi zxMuI4-i!PPhQTMhgF;yKr=6huvTKD~Rrj*L)$<&(WW1ewPp@5{hKN69nXF_MyEpq| zw(0v%nKQy^pXTtlbHB{WIhoQY@;=@XQkq|OS*J0o;q?75OKZNe?igJc5x3fZ9wtz2 z6aAGx0AbY8!?<;=u3%#oqQz?*23qOE>lrY za|uWcVUK+7Z4i2cvFbBQg32&dE9g9f+-d&z=NyO-d4(L6nufoW#KN&@p)Gxk)rJKv z-ehD3LTL7F1J6l#^_ZYrfHZ1Ts812x;-o|1p61@6#K{J|gV!4a+fe9Lhg+vO>yVYi zS^a|_Jy+m4vaarvUlQKm!vGC{X1xYz0v!-VaT(sUVsc%U2C74QZf^A7wWZz`j{4zY z8gJkI@Q2NFaH5m)p3IkDCRZ9Xh$M{=+7JD2)jq*EVwtGyMjlWxHJu()O3?A2wZDJ% VbN%fLw~Xqtc8M(#p=F<%{|A))h8h3> literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/groupchat.lua.i b/verse/.hg/store/data/plugins/groupchat.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..4ca9bdedca4e78e71b4c242ad58006b2c593679a GIT binary patch literal 4314 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 NFJE3T+7;;u`VRp?b~OM1 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/jingle__ft.lua.i b/verse/.hg/store/data/plugins/jingle__ft.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..c6f0c0d4ec645aa30578808496478d126cd10832 GIT binary patch literal 1246 zcmZQzWME_f1LhzG1`aIg;q`q*>&>VziNg_lQh$JB9UFbJP9%cfTg?550D| zy3yvH*x};~udn;J=F86ZU7stO{KLZg4_kxjPo#U#842U!~Su zWwsz&?_U4ahaV#6`6aY&e115i*Sz;7zu!b3b-vZrn*Z`H^@h7`)^E`4*f{^3m-R-M z2#b72zTLZhMOJki^0T^c{CmoKn~eU2B(sUR(c4d%E_>(j>!efRWV1&b8ShSe^P}y^ zZeO2upG_Y+&s?yC$<}c=Usf+%NxeMc*lx;x;tKWwCs)% z&5HfUf0mgooPCPy!pRjzzeO%pYkNp;D1IgVwrcTmM(f29voGfDn8t8&!cirj2TP}` z>pT+f=6q~g{On|X#JtlTWl5zeU%Vf zG9UY$EB8|lZeH=BX?cY1mlEqc^PHPQIKBnFKJaaYr@|e%{km0>eQUa87O8elQFY#P zFWK2U|IPJ`Iazh4YDc}>IF+I%&F?Omel;-Y$le*}Wb7In?tK05V*OKLNs}`X$9_%~ zx%t8CNx=@mNxFMFTNkukiw!Z_U~>Pt4p&TGxshA!Yf-E51fj$$9BzQaYD5aBtaB)W|T;*hg?u z1n(vD=}q-NT8cI6vRfa>$NxW6UfHjIvbF8p-9IH=&o`T^gdAC#a?j7e+$BQz*hd$s z-CzFm-IZS=3d&ZXya)t(z#ORyW-%~ygJ@8ubkPc%r}t*B(}(%4HF96=_RrI}dkLxr zO0kASy2y*wk7?oHSz<7ZVi0jwHMFs1^P_5iu+P|MOv;*<09N>cMmfEHFO zlvEagY*tFk%t=*JsD)a&8LVF$Xr&L-q+dXq31sD~zi!q0xmtKyH@ryydMDLh+y6C6 hkpoo^qU0goD@!dZ2H8=RT3DJ{l&YWv7ErS00sxgd8u0)C literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/jingle__ibb.lua.i b/verse/.hg/store/data/plugins/jingle__ibb.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..3a7843919400f7476c3ff372c40b884397028683 GIT binary patch literal 2179 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 MQ*m*ILvdV(0Zb!z1^@s6 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/legacy.lua.i b/verse/.hg/store/data/plugins/legacy.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..4b196d068c04ccb9ad4b5e7450d9b8f563bf28e2 GIT binary patch literal 1875 zcmZ9Kc{J4f8^>o11{0-hgKUlEiZFIV{cv4M))|pRXp+I$;!<2$XN(3nyO4d$p5@9C zGIm{DGlt6;iA<%CEt2cougpl@L#gFu|m{y&2<55LEN zz7ckn-ouVfMX&Bt$3s<`@~(gU^36iGFl`^UtuveF8ckswBvlQD}JMo)D>xT zaPs^iS%tFW3!4N>Kl;MdjcT(F9%M*Jy>Ba;2&b^(w4bEDHNAs2?&g6u*5Cd5>3j}XdvT2rX4)tW5i+#N$IYgso9LQ)$~7f+0X zOM^>pkQC>}FSOZ!R1ch_sf=MM{s4>aBB;k}?zNnG{*+jTM9EG>)w_&$gGN)C_2ims zp??Y~8H_vPR;b{xO8?{;_c@8rP~$|($@j2KR%RGJDUfzidD=j|`XyEHbh>>KjS%OU zo%#xVEyBDJKO=PiXTk1T3gF1n&i`P^J;yDh>{x+9pi6(&6WpU9=iu0uam8h zofM3Gtip+VNuaKUOL)*Ge&>@EETUTCEW@_C&P86fRm53?TPnO|cG9@ZG9 zp_5OW`fu_|$0vFNlps%l3Kju@pj2QwN)1o|y3Mn<(@$J(;mRBD7Ixoe=U#c(-laLx;7e zGjj9Ru7tX)_d8S8JA$ELQ(4zL7=9s#PfGb|(ZnTt%bFwFNq( zMEmp$m*0=G|2`~|S8jb4o~nsGla|OA|5eMzF1u$M^J2f2-HG9(qVi!vL*W#EF&wJ9 z_nI57HUgAeWq?ZLY%rau9QTYeio~tbA&QZ>Z5P zqA6xEInp zFb!<~2w?5sfVH`HJKYDQf`;TCp)i0eS0{15F5mZL%U`0zi=!n?#QBw!h*!|eNf@AQ z*U0OU!K_pF$H84IydZ{upo0hT!ZOF)K1o(tTQSQO*`(7Hs#D;1-9p_7XBU%`cm1wD zoH^9stA#VuP51jKVI-42%U#{@5O$>PP)43*@4389-OjHj32&zCKRq@1S+77}+Wz`5 z3I(R`f#?ign4A~7u(w0rs`LeVwqF5}|0^EUw|HZ4Ni@5-B#m&%XY8TpfCFYG*74FD zkKMv5Mf`YDTybBkZSt`znogVrMy{i=^-6dB?fv@EN}FT5r0v>u=j{v3$EpIfrgOix zyLH5AB+XOqmTK&1iZ$;}aoQa-)N-QGa+z`|B~8*~Y|)Y!(S!^m#x+ACSsF8BLarm4TxPVK zP_&4H8bWMt2bF7Xxn|cb5xeaCle2qv-*dk2dCq&D?>x`@ofi-RL;zul?gapd%6AUX zws)%G`VhL6tGOU0JIx&71t<6g;VX7DLI~wBiGzs`^$`&<&q{0c2;Xbk9GE=dv* z`-5)fCwr8&ynwpjt}#G}khGYhO|@~9x-S~t-XB?TvESRpyQcTkqpteu>VvG2+NVtN zz?UZ9@}2~hdk8}3>{^?p6N8o%X?Xa@k*+=3)E9-#79}m_irBuBx^+Z$yIV)`5&DG! zL#SyjJjs78D66=V*0q74exaSr@Do-yt%^w1gD$bfW2YC=!TOvkvIe7al7 zIj4)~BFl;ia+7GokA1 z^7qH9`VI6Bj6(?L>D<_!FCo6ioyG>Z%=g$))aMf1sOdIP>Q!P6t_HYIf=Ux?Rh*Ck zKt71zV#%0sPYr(xLfPR=j2~O_$;tX&{yQ2WgmTO~@KO!>2uK&+VYXu*EU*U?|1>?D zu%B`5jF^G8gg!MLSO)MkQJ)HgH29?xPYNU)e zlZNMM`c{9f?Q#s5JG%~S&KfR(!9<@3<23+)Sc{MWK%W3bz?sf|fZL3>zHE%wNCBD zR4K-+wLi>^Mcm?+v6GLn*v=Q8mSWD=@ zI&#a#y&lL}090jj7P3hEc7|xkmGVvlG+r9*lT|N|)WG%eCF@$Els3#<<}Za;;m;XH zqRXpl2!pRQDQ(26=1hOt`p{il&~x&-Dcy?Zx+sMK_@BK!hFbte$-WL8L(Eq=jEjJ^ z3mE{+2v8LK0hEWbcvYIbt6Ab(Dah|QOuoYv{nIW3M?Mlst`hz!JC zo!umH?R$+v$sz&4D5wD%2&`~FQ4m0 z;qO=obpMtx-BYrw>!}7ox~Bi8D{`>OB06PJ>^USNJCpMyIU|!tP0z>?rHDY8e_{l& zdY$STQ&KQ>tRYUVn-w-Uk<&H~OR_~l^hIpBtq{Op8{G_=i60VLVg;$TcFZ@xHW*QZ z!S+^1N?yM*1PnYVXizZCgY0*L ZOrc!8?t8aT^NP=nk9Fxoh&zX4WVia!7V literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/ping.lua.i b/verse/.hg/store/data/plugins/ping.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..24a5a8602843978ddf8648db584740957b7537d5 GIT binary patch literal 1062 zcmZQzWME`~fK3bxjQmix!ha~(bcuC&h3fPNBEpgvyBVf#{onDq7ODnJRm_R)yUW+C zz|*?^qo&k)gG9TA)ANoRyyX#QT&ib$tUUhWUzOboy0*5q@ny4PosU^pJP_H)Q}(W> zJJn#_}y^WYwxyit7gxe-tzSia}&4A{VyJ=?Jj8!k#izzqm_4U zGh1x)=*$kL%Ca}dGdNGwp4%t0t*b=+v&dBKh=-OD;*oc==N{W2%Jt(DzrUZvn@JHZ zKPH^4pCPkP68TdGhQcFsU@)SynN>i=5z|v>Hq&`rE6I8hhkY)s# zYuQ#4~=h4j<#RsB_BZx8(iHTTUF*HhA^)!h+qV zbuTz?i80Mo<$myf$T?Y}POEmbz(~$?8|#Ubdy8Qn`Mw&bepZh1$FCANer1 zc6*BTyA^UGp!HAP=d;6ejkSR$Rhw%oO1q@Iou-QP~FCbCB=KR@DulNEBx{6vuTKq?iek1yg~n zS0Fyn8^J(=6=acSgIbq=!T+B6pA$}aS(h!$d#FDPtOQC_%rWhDyAx)soDKjfLx^7h literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/presence.lua.i b/verse/.hg/store/data/plugins/presence.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..8ab39a69f5a372f85d58ed6358ef8b2c9b1e65c5 GIT binary patch literal 835 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 Th4<*st~l7Gzi`JtvBD_CWHD6x literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/pubsub.lua.i b/verse/.hg/store/data/plugins/pubsub.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..13d97e756c2e7db203fdd750659b836456b0bbd4 GIT binary patch literal 5826 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/register.lua.i b/verse/.hg/store/data/plugins/register.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..8e628d797428dfd8d64e8071cdfed133f6567939 GIT binary patch literal 1044 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/sasl.lua.i b/verse/.hg/store/data/plugins/sasl.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..c5f04cf591f1aaabf3a4bd5c4ba4e95df9f5363b GIT binary patch literal 2454 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/session.lua.i b/verse/.hg/store/data/plugins/session.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..813cae436a68094f12c277126634296d3f0fa7b9 GIT binary patch literal 2122 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 JC-f!Ze*pTzq8R`H literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/tls.lua.i b/verse/.hg/store/data/plugins/tls.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..2a0fedad5e06d8ff4dcde5296fbd440e4a6f52a0 GIT binary patch literal 1424 zcmZQzWME_f1ID8a49uS(YzEu^P_VYcd&)IK)1KY8oJEcwem-TDaijuN4VbE!(;IZU z?~a0i&G{PEM?(E6lS3y>cK_F`(Vp{8Y~yXSC>}nO*~hc~+wxW%+kI}$lxBI8tFECJ zZK1z?& zY6`eq$g#{hmA9JZxucF%N>-L3BIMRv0MGkE6=<4&PCsEDpGt6c()u0qjjU6*J^fyD>TaZ-v$(=K$Lb&7-&7Zf zD7@CK_{=v`mMv-`N0@k|i_z@3?U#ERFAFOlcyOI(dy_#C+qDaG_0E+s*w!nzgmz5d zZyUWZ)#|YSC&q|RK8$Y<3(hrNwofI<=Ipliw(_WpcLVK?v~1hF>*2lE$NA+~rcBqk zZ+_>7ndr~rnNGj-ci#+6eDFN`c(me7)km+i_Pu(uC&QuQ^zZN$ZrNh13zQ=UDW|^x0?i-uktUEV8|2ia_`j_>) zPS^!dOoE~x2o^CguvCFr3=H-l8k7z`G{zMc9GGRa@OjYWnL_9Epc(3KHN?SpPupU8+ek?-7b8H6F1zkpJm_DZURr8{&kQ zZG)--Qx$WP6B1$$95_%hr?K(itjWfv%PmZeeHHchyDZ$XM8niDT;I}O-+%oMm5nQw zs4S0AG}LERnmKFovhX>xVxp$33Yau;*72hU4jkb>lH`#5B%t?T$VnUNCdng~mX;Hr z=8AfCc?GZC$q>0+^A5;9rb}R3Wq?jS17`1x^bV{n3#bNS3j>2QP=py|_8DCh zN$uC7*{Nyfeqvge{CDN-`43hNB@n?d8yF1Jhzf?tO%|p*w`hbeR8cnV?`~}LlBHX~^?&o*z(WFIJj>N2f zbY+i7@1`q@9);|EH0j8mRW7`$3b7$UlY-YQSsbu7VB+k_;cF7O7d~!W#I<$StZOk0 z_EzHH9KpOCY_cZM1(%^#{Q}agAm=3V@4Q%9rd6mD?2=$Ur>snI`R@f#HDC(loCfCB lIe(rUk!b~6&Klji=*po>XBLI9s%~hQWGXJsa43%JFaTSmIr{(r literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/uptime.lua.i b/verse/.hg/store/data/plugins/uptime.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..dfc5cefbdd041c8b13d7f907718b6088372c40a5 GIT binary patch literal 784 zcmZQzWME_f1I8B&3@kGtY=%Ssp}=Unf^zwvXOFdaG^TKE47v09^vWox8ZcEcr!?qx zUb6vDZQJ^e3MS5t<*RDBy$)``Z-V%whU^na-Z|oBO1n z@9o&Zo&J9zi|N#-Y`++fIc-1jRb|Rsp*_Y2^yMb^yl^|QTfH%V*&(_1Dc9z2ty`~M zSi5qU-uY|KI0a^`*6%yeIwSH&$sNnfJ8v_+kq)(JTs_0OIf?B~)|JM}eXfFA3^tsQ z{>H!N<@UomRmc5HOJfh-SvBoMUzD#H_Z+uAr@2|VyFJes_ja1?^*ue0?QE8*#3E0D zr0r|no1K@h)D>FO$yLdm`|M0tl(>X*e&iF?Hl;^1wsqbuc=OQLNB6Mi(~14>>*l2- ze?EI9O+9@@{-O|0X*-9VySNUDGnZXFHXH=KneCHdy`t^T<28UtO}!|M)9gd)0k4uAs;Ur4S&{1g4Xn zU={@EVT=|#c|VPS&#s&iz?toQ836Mnz>wq}!+<^lLXE>QGf1d2Lm#RDV`3 g-RZf~RjPIAO4q4XovNy?RRNK{*?J7G&j}?00KxWCZ2$lO literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/vcard.lua.i b/verse/.hg/store/data/plugins/vcard.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..cd03de75fa51a855b776957ec78e6b9be271c0bf GIT binary patch literal 1565 zcmYjPc|6o<82=699L%H~$!1p0hz&!IB)JB;uVgZeSVy8V#)g zshAnZI4Vcd#^yevm0CwAJHN^9XP-a5&+|U-^Stl#Jl_X^01$vD5NiP7Lw~mc*6yC9 z;F>r({RJ#XJQXppc6$~r>iTXkhNlRm+qsURe8&+TFF570>x}E6E9nR5QWh1rV%475 zv=}c@=U(V@v@Uh5{Q0IT6)AjDNvwdg-5=L8XWriE(Yf-6PZJ+Cm#qkUuoS?IDQd@m zW+c7t(XU8!Lc{Ar)(6e9swL}hjTK8{60r)lK`27yzD(2|ICDo-?ctE33EsuJWz>J% z&%=dfU+<-#ePZ~6_I%~q@@#Wlyv);d%!$Yg`YP~2Ib?d5v*SitQ_%YxXG(=g80yw@ zLQd&M;V(V@!U1jxsXIRMqJK8Xp^kED7(vdFVnr!sR-3fQ;@pTU zib&ivay@wPxk$d)r0iAFh5IpbK9Q=VJiP%wdi)XMTP^gva_`<9prKS)*BnH3#l-I<`%k%_KT3Omv(@32ZQOo*BFki^r^czbSljucEKyXA!x ze*m<_oz)iS5^KGaBu3YY7aCU=$#KDB4!f>IQdWv(s4}^9d3<_f zck)n++#_#+0|DA(dG^G8Ui2P-6<{;{vDo)mEY_=A1bXp# znx(pEgJ_3xT8w*|$0AL6m2>cpp}L$c_Mq|)$?0RH)~U_90HotkF^r-Uq} zi(x?WOM7>)e&|tN7svnCxFOfW4>m5hygh9{L9lnSDXw-PX)}v8XD4~l{{f_(JE%GC zQCS2O{29M10mzd`JdUfi0E`Y>;bDo&`WmKsWfIPPlE< zUFrQAvV;Ygc@9dtJHk`xd(3y_O**!>txdD9tqt4B;hdW!c@>hel}<%slr`H=(;6-? i`?Gq;F$Q^OY%_m2T{6oeSEv7Hme<)(EkGG3`Q#s32#kRM literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/vcard__update.lua.i b/verse/.hg/store/data/plugins/vcard__update.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..b354621bc33df19c48d4fab0f74b162b485f3041 GIT binary patch literal 1582 zcmXYxdpy&79L9f>OPfk*?w4|lS-G^0y;3q>Bq7G3gN%klEtKWnk;|y56Pp~DiKcL3 z?uMk;T#{>AvE0RUR7)e5kYhWZf8Ng@&-e9tJ>Neb00e*lk;K&iKw9j7543IVDCv0` zii=;p)E2j;l58AC=}?b)yOR+ilJ>;S_x^~~d<+u)F0@>e@Nu zrMk-9?r96_gVW{bQU9XS6p*yW2tQVHwCjkMS{l7xs6u1QVll2B*wvkKTVg zS`lH%c;HA?K@m~8CX6!7twp;U?Nlwn-LaPyJq#B~qRG3hkoJ!B3FB7+u1j_PTuL?e z2E4wUk5!CaMpmR}rU{@O1x%P(QP=0W5Qnvmrc&)h_%u%8IIHxm2M;p`XTp?|-(F5-mgF!?-_E>C zZ8D9ZVHaSYjl~tElV517LHs3lH*Vt3t?toO?17nsv^;8@pJA$0t{`BV6d~!BP;*85 zdccq9?+p%Iu`>0vO*)(Hy|PCUK9jHMu#fXCZtal;Q<^>Xt;hO~n;QWfHvl-~j-8vqr%C~cE(p%p7)RUEEPs$G=NJ-H} z_-1qYNsp(GTjwBf^J>TDL1b=^YC~0tj>qkd&`dcCDhl<<> zC%sM+?E8uuin(gCCyOKB;4KXkWLozn_2nh+{qR%+z!0qA6;r6`z7{Fpqru6+pjn@I z$ASS1Dp^~cY)m>nl_v5AFvy z;v?0*hQsreSd>$`IOYd2EqdOg1?&~D5C93BS|JiG-!+yOt-bG?Zwk-UBzy8vAO>&-pvn)?l;jbpvHJ}syG*(0 zIrXm1s^^B|H~BU{H_C>JjqGg?@Ux^NX@YMRf{!R=&3h@|0td`$d&e9T*0TAW^YWmEHMS%qno3zWd#_O9$q+~c&M zDCA$1Yl2Mq`fX4YTBI zR60Bo+o@#i!5T~%=Q-41cUMcxv}7C1Lh6;6PMMwTTT}+*xZs@N0g_>dh1(&I%cdfb zjCcq)1Ys1aDyfeoH5BUAH5@)&ftK?P2?7o9IIGou1XJp}R`xRNkVX6chlKH-!Bku;0{7 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/plugins/version.lua.i b/verse/.hg/store/data/plugins/version.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..81680cf66de720966debe77cfe803b6a3a4e2a17 GIT binary patch literal 1496 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 DeRJUf literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/util/dataforms.lua.i b/verse/.hg/store/data/util/dataforms.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..0c7d6264019589867d188b5689fdf94a12f93f35 GIT binary patch literal 3105 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 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/util/sasl/anonymous.lua.i b/verse/.hg/store/data/util/sasl/anonymous.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..055bc5677885950bdcd366c7e0ce720431f11a0a GIT binary patch literal 391 zcmZQzWME`~fMy`I8p32u`VR$uH}a-l&tB0YmEFu;bFN5tqWdv!sCqC}F(`lQ7l9?JeSHP>xx zxW^?p`=^}D>6r5UKi11tui!eszJ7xFG_X5Bj%Q$K0pflzi-9o>M1y?cX#I8)_y5&* zP9F?xtK)pa`lMIb5TXV`R?JCGNJvOYNQhuN66?BBb*XD5SLV{nld3a+R&q%lRdrq2 zxzmcPvUBIkSut~FW+qH)TD!Js(&G91j}BFoCGzHd++yLpBdgtFSu{M zQFc6RxsX$vBZJ*}9`pJHpBu?en{IeLtgfgmN%To!YEm@MUYxk^_!9+wkDw0KXKVc0 z@BUk{?!w=zS0fbfX>@gOJ+WrN>jTF`yuY>ldie2~RB5F4+WKEx|4mA7DT@xCcxTHr zemg_6uOYleQa@QbO(P1yt_6A#2+D!@8JNYumDFk)7i|ac>00mj;>g4i z!Ycq#10gHsBqt;!q$DJ~V0rXv(UnP;7TL6JxwPlWq%(hPSbHB$T6E<|jBM+kBX>f4 z_0F9!G4MWkEzD8#!10Th{XG>8^<5bkd9B}Kv6VxoS0!vhL`=-Q_$iA5CQY1m{OHNf g!)(5K3%-7D)MK<{U7=&D@9KW=$}Db%8F6}901CaZuK)l5 literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/util/sasl/scram.lua.i b/verse/.hg/store/data/util/sasl/scram.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..2f06ab142eddbe5d275aad94ef1c68bc4f9318fb GIT binary patch literal 2697 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 S3=s?yg!l$P1bqZAgZ~fgf33&> literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/util/sha1.lua.i b/verse/.hg/store/data/util/sha1.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..bf68b61ff705857413afb47199d0e7afbf6ae39d GIT binary patch literal 1480 zcmXYxdpOez7{`B_84DA0nQDg=bFa)LCYAffW@KWiS*Et07-uw8E+?0>3`I+th)nlf zJLPtEP|R()c9Aw`E~U#!B{j+!kMHxm&*y!f_xrxj`_~sx0+fJNDaQhUhN4!rYk5s> zJnv0Rt!GizK;RWFwkRzqr=ir=dJN=@SRY+H(( zkJjBc@QQPfHv)R^0eR+rjWv7Ouhp!(X)md~31A;))kaS+q^HF$PN#B2c^Q-X==OA@ z!{E0TetE(LzkBa4{N);US{sCci?@H>6dAk_z_EtpELvqoi#sr-F0q}s7w!Y@CNIfy ziy@9aIqdBN%!e4II@5u-kSGq?i+Mw(>dp*;)qOoP3Sj(aXv+8oU;vOaP zPLVD|nlDBiN%W)-elRr7F2h|XkXp6P#sxe^$fc)qlB`AQ#&Y?=lO5gB^OV@Lzd_Rny`A{?rqoS zhDu+!ulNz&o1Dk(p0bFdOr4b1xKe6Imd z6KMxyTyb!i6Eq|RIRyx+&u$8is*|aVTy^Ia94~kF-7&?X^o;(24g@A2d0n+G774t% z2cHK-(C#AZ63`4O(oS?#C#urZn*P!rr+u z(T$h^*$Bg0F<}oOF0hZT`3X}J#KZ>2Pg9nhyx?)Bk@qLF%HhqsE+2#5_}sZ`X6~6h zbGmR(wu^rxM#=Q7-0DvqXIbNqKy~bp_QVXc3O}^}ldN;nbl!;kcQ&IzcQ!>1i9g^m zz=b3gN+{sa;_VK%wztF*5sf6_x}PM5%SOWtv<1CGpc3E-#aDa>+kSyoQE3T5%!FaL zj{SrpL|Cz*EWG-8S$(|y`hM3F&T3z_svc+_?WKr|mY!st(EwG*?tU;QjE3?&-n|h9 zC7sIf4!Jq%2%S3jxYkK^)L>hee|E~Fho~^K*v9_2DE(BE1-qOE{?nh;?U!W9dA1{I z68O$W6sIB3Iwl`P>&-R>%Mu&*9k7$fYiA-EW0qDmp0BCt-bb^t(M;Yw!p&`gu(N=b)+Bm{%}dV5%@Nvd?svkEr<2t`VY zq(~De6iPt1h=A`U`dIeiY(%}{AXL8(dKhJ6K1l5Hxr@SDk4{ceDbn*QRR1!@-u_u9^eu`HTS33ZRQBY~+l?$fXl_kHCb~ME9q<;Sb;m!*5 zzpZ)JpzZ|#>MMQ=HMKYzD?TpT2z89cc0T%hJQcOY#%7C^QB-{N5f(j0F`y>0XyzJF X#l{syT~};D)zoOwk>=(u8qog%4OxgA literal 0 HcmV?d00001 diff --git a/verse/.hg/store/data/util/vcard.lua.i b/verse/.hg/store/data/util/vcard.lua.i new file mode 100644 index 0000000000000000000000000000000000000000..cd45d5a08ebcbff01f9e0f9ed40d8a7dfaa88942 GIT binary patch literal 5469 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 p2^Sac24<{tp#I@G1ZR literal 0 HcmV?d00001 diff --git a/verse/.hg/store/fncache b/verse/.hg/store/fncache new file mode 100644 index 0000000..0108cf5 --- /dev/null +++ b/verse/.hg/store/fncache @@ -0,0 +1,58 @@ +data/client.lua.i +data/util/sasl/plain.lua.i +data/plugins/private.lua.i +data/plugins/version.lua.i +data/util/sasl/scram.lua.i +data/plugins/legacy.lua.i +data/plugins/jingle_s5b.lua.i +data/bosh.lua.i +data/doc/example_adhoc.lua.i +data/plugins/roster.lua.i +data/plugins/jingle_ft.lua.i +data/doc/example_bosh.lua.i +data/doc/example_component.lua.i +data/plugins/vcard_update.lua.i +data/plugins/vcard.lua.i +data/plugins/ping.lua.i +data/plugins/session.lua.i +data/plugins/adhoc.lua.i +data/libs/logger.lua.i +data/plugins/uptime.lua.i +data/plugins/pubsub.lua.i +data/libs/xstanza.lua.i +data/doc/example_jingle_send.lua.i +data/plugins/jingle.lua.i +data/plugins/smacks.lua.i +data/doc/example_jingle.lua.i +data/plugins/register.lua.i +data/plugins/carbons.lua.i +data/plugins/jingle_ibb.lua.i +data/init.lua.i +data/plugins/presence.lua.i +data/plugins/blocking.lua.i +data/plugins/pep.lua.i +data/libs/hashes.lua.i +data/squishy.i +data/plugins/bind.lua.i +data/plugins/tls.lua.i +data/doc/example_pep.lua.i +data/util/sha1.lua.i +data/doc/example.lua.i +data/libs/bit.lua.i +data/doc/example_pubsub.lua.i +data/doc/example_carbons.lua.i +data/plugins/receipts.lua.i +data/libs/adhoc.lib.lua.i +data/LICENSE.i +data/plugins/disco.lua.i +data/plugins/archive.lua.i +data/util/sasl/anonymous.lua.i +data/component.lua.i +data/plugins/sasl.lua.i +data/plugins/groupchat.lua.i +data/util/vcard.lua.i +data/plugins/compression.lua.i +data/plugins/proxy65.lua.i +data/util/dataforms.lua.i +data/libs/encodings.lua.i +data/plugins/keepalive.lua.i diff --git a/verse/.hg/store/phaseroots b/verse/.hg/store/phaseroots new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/store/undo b/verse/.hg/store/undo new file mode 100644 index 0000000000000000000000000000000000000000..2583bfc81a8d2279df75b16bf85358618d9cdfa4 GIT binary patch literal 1643 zcmai!K~lpo3`JRU7!nrc0!$aoFl;zLMsd_c)Uk|ZcN$Jlnv$k*?Xs+&fBpUSEmq)aHjeP5vB<_6`iSgnG@%P!yu3T8h6k zDB3--I;lXTxEZeyg)k{PkPcH|>_Cfd?PyXQ#e?~QHL*IhWG?2e+i*cnb##cvJj0se z=q^OF+K=Yb=Ow2*OHY$^C_-$~>V`yd8nupt9lwTX0`p&|8nI^8?yj_=IvQXkmraK6 z5iRI!k{Vl3L;6xT6`aqs*?><0j{V7u%{n@O&q6*9FPn^&MB!&Q4i%F1Q7aePlK8lq z7s`1iAUf?v?zKdEQ2UG-uQMQ6Pkkns!`RyBp_uJYs0wy`YI914ZA?q88<#U*~y-9XuzN*SG!!09ZCG literal 0 HcmV?d00001 diff --git a/verse/.hg/store/undo.phaseroots b/verse/.hg/store/undo.phaseroots new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/undo.bookmarks b/verse/.hg/undo.bookmarks new file mode 100644 index 0000000..e69de29 diff --git a/verse/.hg/undo.branch b/verse/.hg/undo.branch new file mode 100644 index 0000000..331d858 --- /dev/null +++ b/verse/.hg/undo.branch @@ -0,0 +1 @@ +default \ No newline at end of file diff --git a/verse/.hg/undo.desc b/verse/.hg/undo.desc new file mode 100644 index 0000000..421e491 --- /dev/null +++ b/verse/.hg/undo.desc @@ -0,0 +1,3 @@ +0 +pull +http://code.matthewwild.co.uk/verse diff --git a/verse/.hg/undo.dirstate b/verse/.hg/undo.dirstate new file mode 100644 index 0000000..e69de29 diff --git a/verse/LICENSE b/verse/LICENSE new file mode 100644 index 0000000..a60125d --- /dev/null +++ b/verse/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2009-2010 Matthew Wild + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/verse/bosh.lua b/verse/bosh.lua new file mode 100644 index 0000000..ebedfd9 --- /dev/null +++ b/verse/bosh.lua @@ -0,0 +1,209 @@ + +local new_xmpp_stream = require "util.xmppstream".new; +local st = require "util.stanza"; +require "net.httpclient_listener"; -- Required for net.http to work +local http = require "net.http"; + +local stream_mt = setmetatable({}, { __index = verse.stream_mt }); +stream_mt.__index = stream_mt; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_bosh = "http://jabber.org/protocol/httpbind"; + +local reconnect_timeout = 5; + +function verse.new_bosh(logger, url) + local stream = { + bosh_conn_pool = {}; + bosh_waiting_requests = {}; + bosh_rid = math.random(1,999999); + bosh_outgoing_buffer = {}; + bosh_url = url; + conn = {}; + }; + function stream:reopen() + self.bosh_need_restart = true; + self:flush(); + end + local conn = verse.new(logger, stream); + return setmetatable(conn, stream_mt); +end + +function stream_mt:connect() + self:_send_session_request(); +end + +function stream_mt:send(data) + self:debug("Putting into BOSH send buffer: %s", tostring(data)); + self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1] = st.clone(data); + self:flush(); --TODO: Optimize by doing this on next tick (give a chance for data to buffer) +end + +function stream_mt:flush() + if self.connected + and #self.bosh_waiting_requests < self.bosh_max_requests + and (#self.bosh_waiting_requests == 0 + or #self.bosh_outgoing_buffer > 0 + or self.bosh_need_restart) then + self:debug("Flushing..."); + local payload = self:_make_body(); + local buffer = self.bosh_outgoing_buffer; + for i, stanza in ipairs(buffer) do + payload:add_child(stanza); + buffer[i] = nil; + end + self:_make_request(payload); + else + self:debug("Decided not to flush."); + end +end + +function stream_mt:_make_request(payload) + local request, err = http.request(self.bosh_url, { body = tostring(payload) }, function (response, code, request) + if code ~= 0 then + self.inactive_since = nil; + return self:_handle_response(response, code, request); + end + + -- Connection issues, we need to retry this request + local time = os.time(); + if not self.inactive_since then + self.inactive_since = time; -- So we know when it is time to give up + elseif time - self.inactive_since > self.bosh_max_inactivity then + return self:_disconnected(); + else + self:debug("%d seconds left to reconnect, retrying in %d seconds...", + self.bosh_max_inactivity - (time - self.inactive_since), reconnect_timeout); + end + + -- Set up reconnect timer + timer.add_task(reconnect_timeout, function () + self:debug("Retrying request..."); + -- Remove old request + for i, waiting_request in ipairs(self.bosh_waiting_requests) do + if waiting_request == request then + table.remove(self.bosh_waiting_requests, i); + break; + end + end + self:_make_request(payload); + end); + end); + if request then + table.insert(self.bosh_waiting_requests, request); + else + self:warn("Request failed instantly: %s", err); + end +end + +function stream_mt:_disconnected() + self.connected = nil; + self:event("disconnected"); +end + +function stream_mt:_send_session_request() + local body = self:_make_body(); + + -- XEP-0124 + body.attr.hold = "1"; + body.attr.wait = "60"; + body.attr["xml:lang"] = "en"; + body.attr.ver = "1.6"; + + -- XEP-0206 + body.attr.from = self.jid; + body.attr.to = self.host; + body.attr.secure = 'true'; + + http.request(self.bosh_url, { body = tostring(body) }, function (response, code) + if code == 0 then + -- Failed to connect + return self:_disconnected(); + end + -- Handle session creation response + local payload = self:_parse_response(response) + if not payload then + self:warn("Invalid session creation response"); + self:_disconnected(); + return; + end + self.bosh_sid = payload.attr.sid; -- Session id + self.bosh_wait = tonumber(payload.attr.wait); -- How long the server may hold connections for + self.bosh_hold = tonumber(payload.attr.hold); -- How many connections the server may hold + self.bosh_max_inactivity = tonumber(payload.attr.inactivity); -- Max amount of time with no connections + self.bosh_max_requests = tonumber(payload.attr.requests) or self.bosh_hold; -- Max simultaneous requests we can make + self.connected = true; + self:event("connected"); + self:_handle_response_payload(payload); + end); +end + +function stream_mt:_handle_response(response, code, request) + if self.bosh_waiting_requests[1] ~= request then + self:warn("Server replied to request that wasn't the oldest"); + for i, waiting_request in ipairs(self.bosh_waiting_requests) do + if waiting_request == request then + self.bosh_waiting_requests[i] = nil; + break; + end + end + else + table.remove(self.bosh_waiting_requests, 1); + end + local payload = self:_parse_response(response); + if payload then + self:_handle_response_payload(payload); + end + self:flush(); +end + +function stream_mt:_handle_response_payload(payload) + local stanzas = payload.tags; + for i = 1, #stanzas do + local stanza = stanzas[i]; + if stanza.attr.xmlns == xmlns_stream then + self:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns then + self:event("stream/"..stanza.attr.xmlns, stanza); + else + self:event("stanza", stanza); + end + end + if payload.attr.type == "terminate" then + self:_disconnected({reason = payload.attr.condition}); + end +end + +local stream_callbacks = { + stream_ns = "http://jabber.org/protocol/httpbind", stream_tag = "body", + default_ns = "jabber:client", + streamopened = function (session, attr) session.notopen = nil; session.payload = verse.stanza("body", attr); return true; end; + handlestanza = function (session, stanza) session.payload:add_child(stanza); end; +}; +function stream_mt:_parse_response(response) + self:debug("Parsing response: %s", response); + if response == nil then + self:debug("%s", debug.traceback()); + self:_disconnected(); + return; + end + local session = { notopen = true, stream = self }; + local stream = new_xmpp_stream(session, stream_callbacks); + stream:feed(response); + return session.payload; +end + +function stream_mt:_make_body() + self.bosh_rid = self.bosh_rid + 1; + local body = verse.stanza("body", { + xmlns = xmlns_bosh; + content = "text/xml; charset=utf-8"; + sid = self.bosh_sid; + rid = self.bosh_rid; + }); + if self.bosh_need_restart then + self.bosh_need_restart = nil; + body.attr.restart = 'true'; + end + return body; +end diff --git a/verse/client.lua b/verse/client.lua new file mode 100644 index 0000000..fd2eae6 --- /dev/null +++ b/verse/client.lua @@ -0,0 +1,219 @@ +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local adns = require "net.adns"; +local lxp = require "lxp"; +local st = require "util.stanza"; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; + +local function compare_srv_priorities(a,b) + return a.priority < b.priority or (a.priority == b.priority and a.weight > b.weight); +end + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = "jabber:client" }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + stream.notopen = true; + if not stream.closed then + stream:send(""); + stream.closed = true; + end + stream:event("closed"); + return stream:close("stream closed") +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns then + return stream:event("stream/"..stanza.attr.xmlns, stanza); + end + + return stream:event("stanza", stanza); +end + +function stream_callbacks.error(stream, e, stanza) + if stream:event(e, stanza) == nil then + if stanza then + local err = stanza:get_child(nil, "urn:ietf:params:xml:ns:xmpp-streams"); + local text = stanza:get_child_text("text", "urn:ietf:params:xml:ns:xmpp-streams"); + error(err.name..(text and ": "..text or "")); + else + error(stanza and stanza.name or e or "unknown-error"); + end + end +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_client(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + -- Required XMPP features + self:add_plugin("tls"); + self:add_plugin("sasl"); + self:add_plugin("bind"); + self:add_plugin("session"); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + self:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + self:close("xml-not-well-formed"); + end + + self:hook("connected", function () self:reopen(); end); + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + local ret; + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("outgoing", function (data) + if data.name then + self:event("stanza-out", data); + end + end); + + self:hook("stanza-out", function (stanza) + if not stanza.attr.xmlns then + self:event(stanza.name.."-out", stanza); + end + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("session-success", stream_ready, -1) + self:hook("bind-success", stream_ready, -1); + + local _base_close = self.close; + function self:close(reason) + self.close = _base_close; + if not self.closed then + self:send(""); + self.closed = true; + else + return self:close(reason); + end + end + + local function start_connect() + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5222); + end + + if not (self.connect_host or self.connect_port) then + -- Look up SRV records + adns.lookup(function (answer) + if answer then + local srv_hosts = {}; + self.srv_hosts = srv_hosts; + for _, record in ipairs(answer) do + table.insert(srv_hosts, record.srv); + end + table.sort(srv_hosts, compare_srv_priorities); + + local srv_choice = srv_hosts[1]; + self.srv_choice = 1; + if srv_choice then + self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; + self:debug("Best record found, will connect to %s:%d", self.connect_host or self.host, self.connect_port or 5222); + end + + self:hook("disconnected", function () + if self.srv_hosts and self.srv_choice < #self.srv_hosts then + self.srv_choice = self.srv_choice + 1; + local srv_choice = srv_hosts[self.srv_choice]; + self.connect_host, self.connect_port = srv_choice.target, srv_choice.port; + start_connect(); + return true; + end + end, 1000); + + self:hook("connected", function () + self.srv_hosts = nil; + end, 1000); + end + start_connect(); + end, "_xmpp-client._tcp."..(self.host)..".", "SRV"); + else + start_connect(); + end +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.host, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = "jabber:client", version = "1.0" }):top_tag()); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end diff --git a/verse/component.lua b/verse/component.lua new file mode 100644 index 0000000..3a6f3e0 --- /dev/null +++ b/verse/component.lua @@ -0,0 +1,149 @@ +local verse = require "verse"; +local stream = verse.stream_mt; + +local jid_split = require "util.jid".split; +local lxp = require "lxp"; +local st = require "util.stanza"; +local sha1 = require "util.sha1".sha1; + +-- Shortcuts to save having to load util.stanza +verse.message, verse.presence, verse.iq, verse.stanza, verse.reply, verse.error_reply = + st.message, st.presence, st.iq, st.stanza, st.reply, st.error_reply; + +local new_xmpp_stream = require "util.xmppstream".new; + +local xmlns_stream = "http://etherx.jabber.org/streams"; +local xmlns_component = "jabber:component:accept"; + +local stream_callbacks = { + stream_ns = xmlns_stream, + stream_tag = "stream", + default_ns = xmlns_component }; + +function stream_callbacks.streamopened(stream, attr) + stream.stream_id = attr.id; + if not stream:event("opened", attr) then + stream.notopen = nil; + end + return true; +end + +function stream_callbacks.streamclosed(stream) + return stream:event("closed"); +end + +function stream_callbacks.handlestanza(stream, stanza) + if stanza.attr.xmlns == xmlns_stream then + return stream:event("stream-"..stanza.name, stanza); + elseif stanza.attr.xmlns or stanza.name == "handshake" then + return stream:event("stream/"..(stanza.attr.xmlns or xmlns_component), stanza); + end + + return stream:event("stanza", stanza); +end + +function stream:reset() + if self.stream then + self.stream:reset(); + else + self.stream = new_xmpp_stream(self, stream_callbacks); + end + self.notopen = true; + return true; +end + +function stream:connect_component(jid, pass) + self.jid, self.password = jid, pass; + self.username, self.host, self.resource = jid_split(jid); + + function self.data(conn, data) + local ok, err = self.stream:feed(data); + if ok then return; end + stream:debug("debug", "Received invalid XML (%s) %d bytes: %s", tostring(err), #data, data:sub(1, 300):gsub("[\r\n]+", " ")); + stream:close("xml-not-well-formed"); + end + + self:hook("incoming-raw", function (data) return self.data(self.conn, data); end); + + self.curr_id = 0; + + self.tracked_iqs = {}; + self:hook("stanza", function (stanza) + local id, type = stanza.attr.id, stanza.attr.type; + if id and stanza.name == "iq" and (type == "result" or type == "error") and self.tracked_iqs[id] then + self.tracked_iqs[id](stanza); + self.tracked_iqs[id] = nil; + return true; + end + end); + + self:hook("stanza", function (stanza) + local ret; + if stanza.attr.xmlns == nil or stanza.attr.xmlns == "jabber:client" then + if stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then + local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; + if xmlns then + ret = self:event("iq/"..xmlns, stanza); + if not ret then + ret = self:event("iq", stanza); + end + end + if ret == nil then + self:send(verse.error_reply(stanza, "cancel", "service-unavailable")); + return true; + end + else + ret = self:event(stanza.name, stanza); + end + end + return ret; + end, -1); + + self:hook("opened", function (attr) + print(self.jid, self.stream_id, attr.id); + local token = sha1(self.stream_id..pass, true); + + self:send(st.stanza("handshake", { xmlns = xmlns_component }):text(token)); + self:hook("stream/"..xmlns_component, function (stanza) + if stanza.name == "handshake" then + self:event("authentication-success"); + end + end); + end); + + local function stream_ready() + self:event("ready"); + end + self:hook("authentication-success", stream_ready, -1); + + -- Initialise connection + self:connect(self.connect_host or self.host, self.connect_port or 5347); + self:reopen(); +end + +function stream:reopen() + self:reset(); + self:send(st.stanza("stream:stream", { to = self.jid, ["xmlns:stream"]='http://etherx.jabber.org/streams', + xmlns = xmlns_component, version = "1.0" }):top_tag()); +end + +function stream:close(reason) + if not self.notopen then + self:send(""); + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(conn, reason); +end + +function stream:send_iq(iq, callback) + local id = self:new_id(); + self.tracked_iqs[id] = callback; + iq.attr.id = id; + self:send(iq); +end + +function stream:new_id() + self.curr_id = self.curr_id + 1; + return tostring(self.curr_id); +end diff --git a/verse/doc/example.lua b/verse/doc/example.lua new file mode 100644 index 0000000..06be3e2 --- /dev/null +++ b/verse/doc/example.lua @@ -0,0 +1,39 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("version"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + c:query_version(c.jid, function (v) print("I am using "..(v.name or "")); end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_adhoc.lua b/verse/doc/example_adhoc.lua new file mode 100644 index 0000000..1e1b60e --- /dev/null +++ b/verse/doc/example_adhoc.lua @@ -0,0 +1,48 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()); +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("adhoc"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + + local function random_handler() + return { info = tostring(math.random(1,100)), status = "completed" }; + end + + c:add_adhoc_command("Get random number", "random", random_handler); + + c:send(verse.presence():add_child(c:caps())); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_bosh.lua b/verse/doc/example_bosh.lua new file mode 100644 index 0000000..075051c --- /dev/null +++ b/verse/doc/example_bosh.lua @@ -0,0 +1,40 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; +local url = "http://example.com:80/http-bind"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client", "bosh"); + +c = verse.new_bosh(nil, url); +c:add_plugin("version"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "Verse example BOSH client" }; + c:query_version(c.jid, function (v) print("I am using "..(v.name or "")); end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_carbons.lua b/verse/doc/example_carbons.lua new file mode 100644 index 0000000..985f347 --- /dev/null +++ b/verse/doc/example_carbons.lua @@ -0,0 +1,46 @@ +local xmlns_carbons = "urn:xmpp:carbons:1"; +local xmlns_forward = "urn:xmpp:forward:0"; + +local function datetime(t) return os_date("!%Y-%m-%dT%H:%M:%SZ", t); end + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none verse"); + +require "verse".init("client"); + +c = verse.new();--verse.logger()); +c:add_plugin "carbons" + +c:hook("disconnected", verse.quit); +local jid, password = unpack(arg); +assert(jid and password, "You need to supply JID and password as arguments"); +c:connect_client(jid, password); + +-- Print a message after authentication +c:hook("authentication-success", function () c:debug("Logged in!"); end); +c:hook("authentication-failure", function (err) + c:error("Failed to log in! Error: "..tostring(err.condition)); + c:close(); +end); + +c:hook("carbon", function(carbon) + local dir, ts, st = carbon.dir, carbon.timestamp, carbon.stanza; + print("", datetime(ts), dir:upper()); + print(st); +end); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + c:debug("Connected"); + c.carbons:enable(function(ok) + if ok then + c:debug("Carbons enabled") + else + c:error("Could not enable carbons, aborting"); + c:close(); + end + end); +end); + +verse.loop() diff --git a/verse/doc/example_component.lua b/verse/doc/example_component.lua new file mode 100644 index 0000000..398e2fe --- /dev/null +++ b/verse/doc/example_component.lua @@ -0,0 +1,43 @@ +local jid, password = "echo.localhost", "hellohello"; + +--os.execute("squish --minify-level=none verse"); + +require "verse".init("component"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("ping"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c.connect_host = "127.0.0.1" +c:connect_component(jid, password); + +-- Catch binding-success which is (currently) how you know when a stream is ready +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example component" }; +end); + +-- Echo, echo, echo, echo... +c:hook("stanza", function (stanza) + stanza.attr.from, stanza.attr.to = stanza.attr.to, stanza.attr.from; + c:send(stanza); +end) + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_jingle.lua b/verse/doc/example_jingle.lua new file mode 100644 index 0000000..22dadc1 --- /dev/null +++ b/verse/doc/example_jingle.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com/receiver", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("proxy65"); +c:add_plugin("jingle"); +c:add_plugin("jingle_ft"); +c:add_plugin("jingle_s5b"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end, 15); + +-- This one prints all received data +--c:hook("incoming-raw", function (...) print("<<", ...) end, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + -- Handle incoming Jingle requests + c:hook("jingle", function (jingle) + if jingle.content.type == "file" then + print("File offer from "..jingle.peer.."!"); + print("Filename: "..jingle.content.file.name.." Size: "..jingle.content.file.size); + jingle:accept({ save_file = jingle.content.file.name..".received" }); + end + end); + + c:send(verse.presence() + :tag("status"):text("Send me a file!"):up() + :add_child(c:caps()) + ); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_jingle_send.lua b/verse/doc/example_jingle_send.lua new file mode 100644 index 0000000..880009a --- /dev/null +++ b/verse/doc/example_jingle_send.lua @@ -0,0 +1,52 @@ +-- Change these: +local jid, password = "user@example.com/sender", "secret"; +local receiver = "user@example.com/receiver"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(verse.logger()) +c:add_plugin("version"); +c:add_plugin("disco"); +c:add_plugin("proxy65"); +c:add_plugin("jingle"); +c:add_plugin("jingle_ft"); +c:add_plugin("jingle_s5b"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end, 15); + +-- This one prints all received data +--c:hook("incoming-raw", function (...) print("<<", ...) end, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + c:send(verse.presence() + :tag("status"):text("Let me send you a file!"):up() + :add_child(c:caps()) + ); + + c:hook("proxy65/discovered-proxies", function () + print(c:send_file(receiver, "jingle.txt")); + end); +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_pep.lua b/verse/doc/example_pep.lua new file mode 100644 index 0000000..2f6c521 --- /dev/null +++ b/verse/doc/example_pep.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("version"); +c:add_plugin("pep"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + c.version:set{ name = "verse example client" }; + c:hook_pep("http://jabber.org/protocol/mood", function (event) + print(event.from.." is "..event.item.tags[1].name); + end); + + c:hook_pep("http://jabber.org/protocol/tune", function (event) + print(event.from.." is listening to "..event.item:get_child_text("title")); + end); + + c:send(verse.presence()); + + c:publish_pep(verse.stanza("tune", { xmlns = "http://jabber.org/protocol/tune" }) + :tag("title"):text("Beautiful Cedars"):up() + :tag("artist"):text("The Spinners"):up() + :tag("source"):text("Not Quite Folk"):up() + :tag("track"):text("4"):up() + ); + +end); + +print("Starting loop...") +verse.loop() diff --git a/verse/doc/example_pubsub.lua b/verse/doc/example_pubsub.lua new file mode 100644 index 0000000..bafd2b7 --- /dev/null +++ b/verse/doc/example_pubsub.lua @@ -0,0 +1,56 @@ +-- Change these: +local jid, password = "user@example.com", "secret"; + +-- This line squishes verse each time you run, +-- handy if you're hacking on Verse itself +--os.execute("squish --minify-level=none"); + +require "verse".init("client"); + +c = verse.new(); +c:add_plugin("pubsub"); + +-- Add some hooks for debugging +c:hook("opened", function () print("Stream opened!") end); +c:hook("closed", function () print("Stream closed!") end); +c:hook("stanza", function (stanza) print("Stanza:", stanza) end); + +-- This one prints all received data +c:hook("incoming-raw", print, 1000); + +-- Print a message after authentication +c:hook("authentication-success", function () print("Logged in!"); end); +c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); + +-- Print a message and exit when disconnected +c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); + +-- Now, actually start the connection: +c:connect_client(jid, password); + +-- Catch the "ready" event to know when the stream is ready to use +c:hook("ready", function () + print("Stream ready!"); + + -- Create a reference to a node + local node = c.pubsub("pubsub.shakespeare.lit", "princely_musings"); + + -- Callback for when something is published to the node + node:hook(function(event) + print(event.item) + end); + node:subscribe() -- so we actually get the notifications that above callback would get + + node:publish( + nil, -- no id, so the service should give us one + nil, -- no options (not supported at the time of this writing) + verse.stanza("something", { xmlns = "http://example.com/pubsub-thingy" }) -- the actual payload, would turn up in event.item above + :tag("foobar"), + function(success) -- callback + print("publish", success and "success" or "failure") + end) +end); + +print("Starting loop...") +verse.loop() + diff --git a/verse/init.lua b/verse/init.lua new file mode 100644 index 0000000..4166aed --- /dev/null +++ b/verse/init.lua @@ -0,0 +1,250 @@ + +-- Use LuaRocks if available +pcall(require, "luarocks.require"); + +local socket = require"socket"; + +-- Load LuaSec if available +pcall(require, "ssl"); + +local server = require "net.server"; +local events = require "util.events"; +local logger = require "util.logger"; + +module("verse", package.seeall); +local verse = _M; +_M.server = server; + +local stream = {}; +stream.__index = stream; +stream_mt = stream; + +verse.plugins = {}; + +function verse.init(...) + for i=1,select("#", ...) do + local ok = pcall(require, "verse."..select(i,...)); + if not ok then + error("Verse connection module not found: verse."..select(i,...)); + end + end + return verse; +end + + +local max_id = 0; + +function verse.new(logger, base) + local t = setmetatable(base or {}, stream); + max_id = max_id + 1; + t.id = tostring(max_id); + t.logger = logger or verse.new_logger("stream"..t.id); + t.events = events.new(); + t.plugins = {}; + t.verse = verse; + return t; +end + +verse.add_task = require "util.timer".add_task; + +verse.logger = logger.init; -- COMPAT: Deprecated +verse.new_logger = logger.init; +verse.log = verse.logger("verse"); + +local function format(format, ...) + local n, arg, maxn = 0, { ... }, select('#', ...); + return (format:gsub("%%(.)", function (c) if n <= maxn then n = n + 1; return tostring(arg[n]); end end)); +end + +function verse.set_log_handler(log_handler, levels) + levels = levels or { "debug", "info", "warn", "error" }; + logger.reset(); + if io.type(log_handler) == "file" then + local f = log_handler; + function log_handler(name, level, message) + f:write(name, "\t", level, "\t", message, "\n"); + end + end + if log_handler then + local function _log_handler(name, level, message, ...) + return log_handler(name, level, format(message, ...)); + end + for i, level in ipairs(levels) do + logger.add_level_sink(level, _log_handler); + end + end +end + +function _default_log_handler(name, level, message) + return io.stderr:write(name, "\t", level, "\t", message, "\n"); +end +verse.set_log_handler(_default_log_handler, { "error" }); + +local function error_handler(err) + verse.log("error", "Error: %s", err); + verse.log("error", "Traceback: %s", debug.traceback()); +end + +function verse.set_error_handler(new_error_handler) + error_handler = new_error_handler; +end + +function verse.loop() + return xpcall(server.loop, error_handler); +end + +function verse.step() + return xpcall(server.step, error_handler); +end + +function verse.quit() + return server.setquitting(true); +end + +function stream:listen(host, port) + host = host or "localhost"; + port = port or 0; + local conn, err = server.addserver(host, port, new_listener(self, "server"), "*a"); + if conn then + self:debug("Bound to %s:%s", host, port); + self.server = conn; + end + return conn, err; +end + +function stream:connect(connect_host, connect_port) + connect_host = connect_host or "localhost"; + connect_port = tonumber(connect_port) or 5222; + + -- Create and initiate connection + local conn = socket.tcp() + conn:settimeout(0); + local success, err = conn:connect(connect_host, connect_port); + + if not success and err ~= "timeout" then + self:warn("connect() to %s:%d failed: %s", connect_host, connect_port, err); + return self:event("disconnected", { reason = err }) or false, err; + end + + local conn = server.wrapclient(conn, connect_host, connect_port, new_listener(self), "*a"); + if not conn then + self:warn("connection initialisation failed: %s", err); + return self:event("disconnected", { reason = err }) or false, err; + end + self:set_conn(conn); + return true; +end + +function stream:set_conn(conn) + self.conn = conn; + self.send = function (stream, data) + self:event("outgoing", data); + data = tostring(data); + self:event("outgoing-raw", data); + return conn:write(data); + end; +end + +function stream:close(reason) + if not self.conn then + verse.log("error", "Attempt to close disconnected connection - possibly a bug"); + return; + end + local on_disconnect = self.conn.disconnect(); + self.conn:close(); + on_disconnect(self.conn, reason); +end + +-- Logging functions +function stream:debug(...) + return self.logger("debug", ...); +end + +function stream:info(...) + return self.logger("info", ...); +end + +function stream:warn(...) + return self.logger("warn", ...); +end + +function stream:error(...) + return self.logger("error", ...); +end + +-- Event handling +function stream:event(name, ...) + self:debug("Firing event: "..tostring(name)); + return self.events.fire_event(name, ...); +end + +function stream:hook(name, ...) + return self.events.add_handler(name, ...); +end + +function stream:unhook(name, handler) + return self.events.remove_handler(name, handler); +end + +function verse.eventable(object) + object.events = events.new(); + object.hook, object.unhook = stream.hook, stream.unhook; + local fire_event = object.events.fire_event; + function object:event(name, ...) + return fire_event(name, ...); + end + return object; +end + +function stream:add_plugin(name) + if self.plugins[name] then return true; end + if require("verse.plugins."..name) then + local ok, err = verse.plugins[name](self); + if ok ~= false then + self:debug("Loaded %s plugin", name); + self.plugins[name] = true; + else + self:warn("Failed to load %s plugin: %s", name, err); + end + end + return self; +end + +-- Listener factory +function new_listener(stream) + local conn_listener = {}; + + function conn_listener.onconnect(conn) + if stream.server then + local client = verse.new(); + conn:setlistener(new_listener(client)); + client:set_conn(conn); + stream:event("connected", { client = client }); + else + stream.connected = true; + stream:event("connected"); + end + end + + function conn_listener.onincoming(conn, data) + stream:event("incoming-raw", data); + end + + function conn_listener.ondisconnect(conn, err) + if conn ~= stream.conn then return end + stream.connected = false; + stream:event("disconnected", { reason = err }); + end + + function conn_listener.ondrain(conn) + stream:event("drained"); + end + + function conn_listener.onstatus(conn, new_status) + stream:event("status", new_status); + end + + return conn_listener; +end + +return verse; diff --git a/verse/libs/adhoc.lib.lua b/verse/libs/adhoc.lib.lua new file mode 100644 index 0000000..0cb4efe --- /dev/null +++ b/verse/libs/adhoc.lib.lua @@ -0,0 +1,85 @@ +-- Copyright (C) 2009-2010 Florian Zeitz +-- +-- This file is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local st, uuid = require "util.stanza", require "util.uuid"; + +local xmlns_cmd = "http://jabber.org/protocol/commands"; + +local states = {} + +local _M = {}; + +function _cmdtag(desc, status, sessionid, action) + local cmd = st.stanza("command", { xmlns = xmlns_cmd, node = desc.node, status = status }); + if sessionid then cmd.attr.sessionid = sessionid; end + if action then cmd.attr.action = action; end + + return cmd; +end + +function _M.new(name, node, handler, permission) + return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = (permission or "user") }; +end + +function _M.handle_cmd(command, origin, stanza) + local sessionid = stanza.tags[1].attr.sessionid or uuid.generate(); + local dataIn = {}; + dataIn.to = stanza.attr.to; + dataIn.from = stanza.attr.from; + dataIn.action = stanza.tags[1].attr.action or "execute"; + dataIn.form = stanza.tags[1]:child_with_ns("jabber:x:data"); + + local data, state = command:handler(dataIn, states[sessionid]); + states[sessionid] = state; + local stanza = st.reply(stanza); + if data.status == "completed" then + states[sessionid] = nil; + cmdtag = command:cmdtag("completed", sessionid); + elseif data.status == "canceled" then + states[sessionid] = nil; + cmdtag = command:cmdtag("canceled", sessionid); + elseif data.status == "error" then + states[sessionid] = nil; + stanza = st.error_reply(stanza, data.error.type, data.error.condition, data.error.message); + origin.send(stanza); + return true; + else + cmdtag = command:cmdtag("executing", sessionid); + end + + for name, content in pairs(data) do + if name == "info" then + cmdtag:tag("note", {type="info"}):text(content):up(); + elseif name == "warn" then + cmdtag:tag("note", {type="warn"}):text(content):up(); + elseif name == "error" then + cmdtag:tag("note", {type="error"}):text(content.message):up(); + elseif name =="actions" then + local actions = st.stanza("actions"); + for _, action in ipairs(content) do + if (action == "prev") or (action == "next") or (action == "complete") then + actions:tag(action):up(); + else + module:log("error", 'Command "'..command.name.. + '" at node "'..command.node..'" provided an invalid action "'..action..'"'); + end + end + cmdtag:add_child(actions); + elseif name == "form" then + cmdtag:add_child((content.layout or content):form(content.values)); + elseif name == "result" then + cmdtag:add_child((content.layout or content):form(content.values, "result")); + elseif name == "other" then + cmdtag:add_child(content); + end + end + stanza:add_child(cmdtag); + origin.send(stanza); + + return true; +end + +return _M; diff --git a/verse/libs/bit.lua b/verse/libs/bit.lua new file mode 100644 index 0000000..2482c47 --- /dev/null +++ b/verse/libs/bit.lua @@ -0,0 +1,145 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local type = type; +local tonumber = tonumber; +local setmetatable = setmetatable; +local error = error; +local tostring = tostring; +local print = print; + +local xor_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=0;[18]=3;[19]=2;[20]=5;[21]=4;[22]=7;[23]=6;[24]=9;[25]=8;[26]=11;[27]=10;[28]=13;[29]=12;[30]=15;[31]=14;[32]=2;[33]=3;[34]=0;[35]=1;[36]=6;[37]=7;[38]=4;[39]=5;[40]=10;[41]=11;[42]=8;[43]=9;[44]=14;[45]=15;[46]=12;[47]=13;[48]=3;[49]=2;[50]=1;[51]=0;[52]=7;[53]=6;[54]=5;[55]=4;[56]=11;[57]=10;[58]=9;[59]=8;[60]=15;[61]=14;[62]=13;[63]=12;[64]=4;[65]=5;[66]=6;[67]=7;[68]=0;[69]=1;[70]=2;[71]=3;[72]=12;[73]=13;[74]=14;[75]=15;[76]=8;[77]=9;[78]=10;[79]=11;[80]=5;[81]=4;[82]=7;[83]=6;[84]=1;[85]=0;[86]=3;[87]=2;[88]=13;[89]=12;[90]=15;[91]=14;[92]=9;[93]=8;[94]=11;[95]=10;[96]=6;[97]=7;[98]=4;[99]=5;[100]=2;[101]=3;[102]=0;[103]=1;[104]=14;[105]=15;[106]=12;[107]=13;[108]=10;[109]=11;[110]=8;[111]=9;[112]=7;[113]=6;[114]=5;[115]=4;[116]=3;[117]=2;[118]=1;[119]=0;[120]=15;[121]=14;[122]=13;[123]=12;[124]=11;[125]=10;[126]=9;[127]=8;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=0;[137]=1;[138]=2;[139]=3;[140]=4;[141]=5;[142]=6;[143]=7;[144]=9;[145]=8;[146]=11;[147]=10;[148]=13;[149]=12;[150]=15;[151]=14;[152]=1;[153]=0;[154]=3;[155]=2;[156]=5;[157]=4;[158]=7;[159]=6;[160]=10;[161]=11;[162]=8;[163]=9;[164]=14;[165]=15;[166]=12;[167]=13;[168]=2;[169]=3;[170]=0;[171]=1;[172]=6;[173]=7;[174]=4;[175]=5;[176]=11;[177]=10;[178]=9;[179]=8;[180]=15;[181]=14;[182]=13;[183]=12;[184]=3;[185]=2;[186]=1;[187]=0;[188]=7;[189]=6;[190]=5;[191]=4;[192]=12;[193]=13;[194]=14;[195]=15;[196]=8;[197]=9;[198]=10;[199]=11;[200]=4;[201]=5;[202]=6;[203]=7;[204]=0;[205]=1;[206]=2;[207]=3;[208]=13;[209]=12;[210]=15;[211]=14;[212]=9;[213]=8;[214]=11;[215]=10;[216]=5;[217]=4;[218]=7;[219]=6;[220]=1;[221]=0;[222]=3;[223]=2;[224]=14;[225]=15;[226]=12;[227]=13;[228]=10;[229]=11;[230]=8;[231]=9;[232]=6;[233]=7;[234]=4;[235]=5;[236]=2;[237]=3;[238]=0;[239]=1;[240]=15;[241]=14;[242]=13;[243]=12;[244]=11;[245]=10;[246]=9;[247]=8;[248]=7;[249]=6;[250]=5;[251]=4;[252]=3;[253]=2;[254]=1;[255]=0;}; +local or_map = {[0]=0;[1]=1;[2]=2;[3]=3;[4]=4;[5]=5;[6]=6;[7]=7;[8]=8;[9]=9;[10]=10;[11]=11;[12]=12;[13]=13;[14]=14;[15]=15;[16]=1;[17]=1;[18]=3;[19]=3;[20]=5;[21]=5;[22]=7;[23]=7;[24]=9;[25]=9;[26]=11;[27]=11;[28]=13;[29]=13;[30]=15;[31]=15;[32]=2;[33]=3;[34]=2;[35]=3;[36]=6;[37]=7;[38]=6;[39]=7;[40]=10;[41]=11;[42]=10;[43]=11;[44]=14;[45]=15;[46]=14;[47]=15;[48]=3;[49]=3;[50]=3;[51]=3;[52]=7;[53]=7;[54]=7;[55]=7;[56]=11;[57]=11;[58]=11;[59]=11;[60]=15;[61]=15;[62]=15;[63]=15;[64]=4;[65]=5;[66]=6;[67]=7;[68]=4;[69]=5;[70]=6;[71]=7;[72]=12;[73]=13;[74]=14;[75]=15;[76]=12;[77]=13;[78]=14;[79]=15;[80]=5;[81]=5;[82]=7;[83]=7;[84]=5;[85]=5;[86]=7;[87]=7;[88]=13;[89]=13;[90]=15;[91]=15;[92]=13;[93]=13;[94]=15;[95]=15;[96]=6;[97]=7;[98]=6;[99]=7;[100]=6;[101]=7;[102]=6;[103]=7;[104]=14;[105]=15;[106]=14;[107]=15;[108]=14;[109]=15;[110]=14;[111]=15;[112]=7;[113]=7;[114]=7;[115]=7;[116]=7;[117]=7;[118]=7;[119]=7;[120]=15;[121]=15;[122]=15;[123]=15;[124]=15;[125]=15;[126]=15;[127]=15;[128]=8;[129]=9;[130]=10;[131]=11;[132]=12;[133]=13;[134]=14;[135]=15;[136]=8;[137]=9;[138]=10;[139]=11;[140]=12;[141]=13;[142]=14;[143]=15;[144]=9;[145]=9;[146]=11;[147]=11;[148]=13;[149]=13;[150]=15;[151]=15;[152]=9;[153]=9;[154]=11;[155]=11;[156]=13;[157]=13;[158]=15;[159]=15;[160]=10;[161]=11;[162]=10;[163]=11;[164]=14;[165]=15;[166]=14;[167]=15;[168]=10;[169]=11;[170]=10;[171]=11;[172]=14;[173]=15;[174]=14;[175]=15;[176]=11;[177]=11;[178]=11;[179]=11;[180]=15;[181]=15;[182]=15;[183]=15;[184]=11;[185]=11;[186]=11;[187]=11;[188]=15;[189]=15;[190]=15;[191]=15;[192]=12;[193]=13;[194]=14;[195]=15;[196]=12;[197]=13;[198]=14;[199]=15;[200]=12;[201]=13;[202]=14;[203]=15;[204]=12;[205]=13;[206]=14;[207]=15;[208]=13;[209]=13;[210]=15;[211]=15;[212]=13;[213]=13;[214]=15;[215]=15;[216]=13;[217]=13;[218]=15;[219]=15;[220]=13;[221]=13;[222]=15;[223]=15;[224]=14;[225]=15;[226]=14;[227]=15;[228]=14;[229]=15;[230]=14;[231]=15;[232]=14;[233]=15;[234]=14;[235]=15;[236]=14;[237]=15;[238]=14;[239]=15;[240]=15;[241]=15;[242]=15;[243]=15;[244]=15;[245]=15;[246]=15;[247]=15;[248]=15;[249]=15;[250]=15;[251]=15;[252]=15;[253]=15;[254]=15;[255]=15;}; +local and_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=0;[9]=0;[10]=0;[11]=0;[12]=0;[13]=0;[14]=0;[15]=0;[16]=0;[17]=1;[18]=0;[19]=1;[20]=0;[21]=1;[22]=0;[23]=1;[24]=0;[25]=1;[26]=0;[27]=1;[28]=0;[29]=1;[30]=0;[31]=1;[32]=0;[33]=0;[34]=2;[35]=2;[36]=0;[37]=0;[38]=2;[39]=2;[40]=0;[41]=0;[42]=2;[43]=2;[44]=0;[45]=0;[46]=2;[47]=2;[48]=0;[49]=1;[50]=2;[51]=3;[52]=0;[53]=1;[54]=2;[55]=3;[56]=0;[57]=1;[58]=2;[59]=3;[60]=0;[61]=1;[62]=2;[63]=3;[64]=0;[65]=0;[66]=0;[67]=0;[68]=4;[69]=4;[70]=4;[71]=4;[72]=0;[73]=0;[74]=0;[75]=0;[76]=4;[77]=4;[78]=4;[79]=4;[80]=0;[81]=1;[82]=0;[83]=1;[84]=4;[85]=5;[86]=4;[87]=5;[88]=0;[89]=1;[90]=0;[91]=1;[92]=4;[93]=5;[94]=4;[95]=5;[96]=0;[97]=0;[98]=2;[99]=2;[100]=4;[101]=4;[102]=6;[103]=6;[104]=0;[105]=0;[106]=2;[107]=2;[108]=4;[109]=4;[110]=6;[111]=6;[112]=0;[113]=1;[114]=2;[115]=3;[116]=4;[117]=5;[118]=6;[119]=7;[120]=0;[121]=1;[122]=2;[123]=3;[124]=4;[125]=5;[126]=6;[127]=7;[128]=0;[129]=0;[130]=0;[131]=0;[132]=0;[133]=0;[134]=0;[135]=0;[136]=8;[137]=8;[138]=8;[139]=8;[140]=8;[141]=8;[142]=8;[143]=8;[144]=0;[145]=1;[146]=0;[147]=1;[148]=0;[149]=1;[150]=0;[151]=1;[152]=8;[153]=9;[154]=8;[155]=9;[156]=8;[157]=9;[158]=8;[159]=9;[160]=0;[161]=0;[162]=2;[163]=2;[164]=0;[165]=0;[166]=2;[167]=2;[168]=8;[169]=8;[170]=10;[171]=10;[172]=8;[173]=8;[174]=10;[175]=10;[176]=0;[177]=1;[178]=2;[179]=3;[180]=0;[181]=1;[182]=2;[183]=3;[184]=8;[185]=9;[186]=10;[187]=11;[188]=8;[189]=9;[190]=10;[191]=11;[192]=0;[193]=0;[194]=0;[195]=0;[196]=4;[197]=4;[198]=4;[199]=4;[200]=8;[201]=8;[202]=8;[203]=8;[204]=12;[205]=12;[206]=12;[207]=12;[208]=0;[209]=1;[210]=0;[211]=1;[212]=4;[213]=5;[214]=4;[215]=5;[216]=8;[217]=9;[218]=8;[219]=9;[220]=12;[221]=13;[222]=12;[223]=13;[224]=0;[225]=0;[226]=2;[227]=2;[228]=4;[229]=4;[230]=6;[231]=6;[232]=8;[233]=8;[234]=10;[235]=10;[236]=12;[237]=12;[238]=14;[239]=14;[240]=0;[241]=1;[242]=2;[243]=3;[244]=4;[245]=5;[246]=6;[247]=7;[248]=8;[249]=9;[250]=10;[251]=11;[252]=12;[253]=13;[254]=14;[255]=15;} + +local not_map = {[0]=15;[1]=14;[2]=13;[3]=12;[4]=11;[5]=10;[6]=9;[7]=8;[8]=7;[9]=6;[10]=5;[11]=4;[12]=3;[13]=2;[14]=1;[15]=0;}; +local rshift1_map = {[0]=0;[1]=0;[2]=1;[3]=1;[4]=2;[5]=2;[6]=3;[7]=3;[8]=4;[9]=4;[10]=5;[11]=5;[12]=6;[13]=6;[14]=7;[15]=7;}; +local rshift1carry_map = {[0]=0;[1]=8;[2]=0;[3]=8;[4]=0;[5]=8;[6]=0;[7]=8;[8]=0;[9]=8;[10]=0;[11]=8;[12]=0;[13]=8;[14]=0;[15]=8;}; +local lshift1_map = {[0]=0;[1]=2;[2]=4;[3]=6;[4]=8;[5]=10;[6]=12;[7]=14;[8]=0;[9]=2;[10]=4;[11]=6;[12]=8;[13]=10;[14]=12;[15]=14;}; +local lshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=1;[9]=1;[10]=1;[11]=1;[12]=1;[13]=1;[14]=1;[15]=1;}; +local arshift1carry_map = {[0]=0;[1]=0;[2]=0;[3]=0;[4]=0;[5]=0;[6]=0;[7]=0;[8]=8;[9]=8;[10]=8;[11]=8;[12]=8;[13]=8;[14]=8;[15]=8;}; + +module "bit" + +local bit_mt = {__tostring = function(t) return ("%x%x%x%x%x%x%x%x"):format(t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8]); end}; +local function do_bop(a, b, op) + return setmetatable({ + op[a[1]*16+b[1]]; + op[a[2]*16+b[2]]; + op[a[3]*16+b[3]]; + op[a[4]*16+b[4]]; + op[a[5]*16+b[5]]; + op[a[6]*16+b[6]]; + op[a[7]*16+b[7]]; + op[a[8]*16+b[8]]; + }, bit_mt); +end +local function do_uop(a, op) + return setmetatable({ + op[a[1]]; + op[a[2]]; + op[a[3]]; + op[a[4]]; + op[a[5]]; + op[a[6]]; + op[a[7]]; + op[a[8]]; + }, bit_mt); +end + +function bxor(a, b) return do_bop(a, b, xor_map); end +function bor(a, b) return do_bop(a, b, or_map); end +function band(a, b) return do_bop(a, b, and_map); end + +function bnot(a) return do_uop(a, not_map); end +local function _rshift1(t) + local carry = 0; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function rshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _rshift1(t); end + return setmetatable(t, bit_mt); +end +local function _arshift1(t) + local carry = arshift1carry_map[t[1]]; + for i=1,8 do + local t_i = rshift1_map[t[i]] + carry; + carry = rshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function arshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _arshift1(t); end + return setmetatable(t, bit_mt); +end +local function _lshift1(t) + local carry = 0; + for i=8,1,-1 do + local t_i = lshift1_map[t[i]] + carry; + carry = lshift1carry_map[t[i]]; + t[i] = t_i; + end +end +function lshift(a, i) + local t = {a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8]}; + for n = 1,i do _lshift1(t); end + return setmetatable(t, bit_mt); +end + +local function _cast(a) + if type(a) == "number" then a = ("%x"):format(a); + elseif type(a) == "table" then return a; + elseif type(a) ~= "string" then error("string expected, got "..type(a), 2); end + local t = {0,0,0,0,0,0,0,0}; + a = "00000000"..a; + a = a:sub(-8); + for i = 1,8 do + t[i] = tonumber(a:sub(i,i), 16) or error("Number format error", 2); + end + return setmetatable(t, bit_mt); +end + +local function wrap1(f) + return function(a, ...) + if type(a) ~= "table" then a = _cast(a); end + a = f(a, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end +local function wrap2(f) + return function(a, b, ...) + if type(a) ~= "table" then a = _cast(a); end + if type(b) ~= "table" then b = _cast(b); end + a = f(a, b, ...); + a = tonumber(tostring(a), 16); + if a > 0x7fffffff then a = a - 1 - 0xffffffff; end + return a; + end; +end + +bxor = wrap2(bxor); +bor = wrap2(bor); +band = wrap2(band); +bnot = wrap1(bnot); +lshift = wrap1(lshift); +rshift = wrap1(rshift); +arshift = wrap1(arshift); +cast = wrap1(_cast); + +bits = 32; + +return _M; diff --git a/verse/libs/encodings.lua b/verse/libs/encodings.lua new file mode 100644 index 0000000..4364221 --- /dev/null +++ b/verse/libs/encodings.lua @@ -0,0 +1,12 @@ +local function not_impl() + error("Function not implemented"); +end + +local mime = require "mime"; + +module "encodings" + +stringprep = {}; +base64 = { encode = mime.b64, decode = not_impl }; --mime.unb64 is buggy with \0 + +return _M; diff --git a/verse/libs/hashes.lua b/verse/libs/hashes.lua new file mode 100644 index 0000000..6a0e0ef --- /dev/null +++ b/verse/libs/hashes.lua @@ -0,0 +1,3 @@ +local sha1 = require "util.sha1"; + +return { sha1 = sha1.sha1 }; diff --git a/verse/libs/xstanza.lua b/verse/libs/xstanza.lua new file mode 100644 index 0000000..3b12aac --- /dev/null +++ b/verse/libs/xstanza.lua @@ -0,0 +1,27 @@ +local stanza_mt = getmetatable(require "util.stanza".stanza()); + +local xmlns_stanzas = "urn:ietf:params:xml:ns:xmpp-stanzas"; + +function stanza_mt:get_error() + local type, condition, text; + + local error_tag = self:get_child("error"); + if not error_tag then + return nil, nil; + end + type = error_tag.attr.type; + + for child in error_tag:children() do + if child.attr.xmlns == xmlns_stanzas then + if child.name == "text" then + text = child:get_text(); + else + condition = child.name; + end + if condition and text then + break; + end + end + end + return type, condition, text; +end diff --git a/verse/plugins/adhoc.lua b/verse/plugins/adhoc.lua new file mode 100644 index 0000000..372e453 --- /dev/null +++ b/verse/plugins/adhoc.lua @@ -0,0 +1,115 @@ +local verse = require "verse"; +local adhoc = require "lib.adhoc"; + +local xmlns_commands = "http://jabber.org/protocol/commands"; +local xmlns_data = "jabber:x:data"; + +local command_mt = {}; +command_mt.__index = command_mt; + +-- Table of commands we provide +local commands = {}; + +function verse.plugins.adhoc(stream) + stream:add_plugin("disco"); + stream:add_disco_feature(xmlns_commands); + + function stream:query_commands(jid, callback) + stream:disco_items(jid, xmlns_commands, function (items) + stream:debug("adhoc list returned") + local command_list = {}; + for _, item in ipairs(items) do + command_list[item.node] = item.name; + end + stream:debug("adhoc calling callback") + return callback(command_list); + end); + end + + function stream:execute_command(jid, command, callback) + local cmd = setmetatable({ + stream = stream, jid = jid, + command = command, callback = callback + }, command_mt); + return cmd:execute(); + end + + -- ACL checker for commands we provide + local function has_affiliation(jid, aff) + if not(aff) or aff == "user" then return true; end + if type(aff) == "function" then + return aff(jid); + end + -- TODO: Support 'roster', etc. + end + + function stream:add_adhoc_command(name, node, handler, permission) + commands[node] = adhoc.new(name, node, handler, permission); + stream:add_disco_item({ jid = stream.jid, node = node, name = name }, xmlns_commands); + return commands[node]; + end + + local function handle_command(stanza) + local command_tag = stanza.tags[1]; + local node = command_tag.attr.node; + + local handler = commands[node]; + if not handler then return; end + + if not has_affiliation(stanza.attr.from, handler.permission) then + stream:send(verse.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() + :add_child(handler:cmdtag("canceled") + :tag("note", {type="error"}):text("You don't have permission to execute this command"))); + return true + end + + -- User has permission now execute the command + return adhoc.handle_cmd(handler, { send = function (d) return stream:send(d) end }, stanza); + end + + stream:hook("iq/"..xmlns_commands, function (stanza) + local type = stanza.attr.type; + local name = stanza.tags[1].name; + if type == "set" and name == "command" then + return handle_command(stanza); + end + end); +end + +function command_mt:_process_response(result) + if result.attr.type == "error" then + self.status = "canceled"; + self.callback(self, {}); + return; + end + local command = result:get_child("command", xmlns_commands); + self.status = command.attr.status; + self.sessionid = command.attr.sessionid; + self.form = command:get_child("x", xmlns_data); + self.note = command:get_child("note"); --FIXME handle multiple s + self.callback(self); +end + +-- Initial execution of a command +function command_mt:execute() + local iq = verse.iq({ to = self.jid, type = "set" }) + :tag("command", { xmlns = xmlns_commands, node = self.command }); + self.stream:send_iq(iq, function (result) + self:_process_response(result); + end); +end + +function command_mt:next(form) + local iq = verse.iq({ to = self.jid, type = "set" }) + :tag("command", { + xmlns = xmlns_commands, + node = self.command, + sessionid = self.sessionid + }); + + if form then iq:add_child(form); end + + self.stream:send_iq(iq, function (result) + self:_process_response(result); + end); +end diff --git a/verse/plugins/archive.lua b/verse/plugins/archive.lua new file mode 100644 index 0000000..2405e8f --- /dev/null +++ b/verse/plugins/archive.lua @@ -0,0 +1,139 @@ +-- This implements XEP-0313: Message Archive Management +-- http://xmpp.org/extensions/xep-0313.html +-- (ie not XEP-0136) + +local verse = require "verse"; +local st = require "util.stanza"; +local xmlns_mam = "urn:xmpp:mam:0" +local xmlns_forward = "urn:xmpp:forward:0"; +local xmlns_delay = "urn:xmpp:delay"; +local uuid = require "util.uuid".generate; +local parse_datetime = require "util.datetime".parse; +local datetime = require "util.datetime".datetime; +local dataform = require"util.dataforms".new; +local rsm = require "util.rsm"; +local NULL = {}; + +local query_form = dataform { + { name = "FORM_TYPE"; type = "hidden"; value = xmlns_mam; }; + { name = "with"; type = "jid-single"; }; + { name = "start"; type = "text-single" }; + { name = "end"; type = "text-single"; }; +}; + +function verse.plugins.archive(stream) + function stream:query_archive(where, query_params, callback) + local queryid = uuid(); + local query_st = st.iq{ type="set", to = where } + :tag("query", { xmlns = xmlns_mam, queryid = queryid }); + + + local qstart, qend = tonumber(query_params["start"]), tonumber(query_params["end"]); + query_params["start"] = qstart and datetime(qstart); + query_params["end"] = qend and datetime(qend); + + query_st:add_child(query_form:form(query_params, "submit")); + -- query_st:up(); + query_st:add_child(rsm.generate(query_params)); + + local results = {}; + local function handle_archived_message(message) + + local finnished = message:get_child("fin", xmlns_mam) + if finnished and finnished.attr.queryid == queryid then + local rset = rsm.get(finnished); + for k,v in pairs(rset or NULL) do results[k]=v; end + self:unhook("message", handle_archived_message); + callback(results); + return true + end + local result_tag = message:get_child("result", xmlns_mam); + if result_tag and result_tag.attr.queryid == queryid then + local forwarded = result_tag:get_child("forwarded", xmlns_forward); + forwarded = forwarded or message:get_child("forwarded", xmlns_forward); -- COMPAT XEP-0313 pre 2013-05-31 + + local id = result_tag.attr.id; + local delay = forwarded:get_child("delay", xmlns_delay); + local stamp = delay and parse_datetime(delay.attr.stamp) or nil; + + local message = forwarded:get_child("message", "jabber:client") + + results[#results+1] = { id = id, stamp = stamp, message = message }; + return true + end + end + + self:hook("message", handle_archived_message, 1); + self:send_iq(query_st, function(reply) + if reply.attr.type == "error" then + self:warn(table.concat({reply:get_error()}, " ")) + self:unhook("message", handle_archived_message); + callback(false, reply:get_error()) + end + return true + end); + end + + local default_attrs = { + always = true, [true] = "always", + never = false, [false] = "never", + roster = "roster", + } + + local function prefs_decode(stanza) -- from XML + local prefs = {}; + local default = stanza.attr.default; + + if default then + prefs[false] = default_attrs[default]; + end + + local always = stanza:get_child("always"); + if always then + for rule in always:childtags("jid") do + local jid = rule:get_text(); + prefs[jid] = true; + end + end + + local never = stanza:get_child("never"); + if never then + for rule in never:childtags("jid") do + local jid = rule:get_text(); + prefs[jid] = false; + end + end + return prefs; + end + + local function prefs_encode(prefs) -- into XML + local default + default, prefs[false] = prefs[false], nil; + if default ~= nil then + default = default_attrs[default]; + end + local reply = st.stanza("prefs", { xmlns = xmlns_mam, default = default }) + local always = st.stanza("always"); + local never = st.stanza("never"); + for k,v in pairs(prefs) do + (v and always or never):tag("jid"):text(k):up(); + end + return reply:add_child(always):add_child(never); + end + + function stream:archive_prefs_get(callback) + self:send_iq(st.iq{ type="get" }:tag("prefs", { xmlns = xmlns_mam }), + function(result) + if result and result.attr.type == "result" and result.tags[1] then + local prefs = prefs_decode(result.tags[1]); + callback(prefs, result); + else + callback(nil, result); + end + end); + end + + function stream:archive_prefs_set(prefs, callback) + self:send_iq(st.iq{ type="set" }:add_child(prefs_encode(prefs)), callback); + end +end diff --git a/verse/plugins/bind.lua b/verse/plugins/bind.lua new file mode 100644 index 0000000..d53b234 --- /dev/null +++ b/verse/plugins/bind.lua @@ -0,0 +1,28 @@ +local verse = require "verse"; +local jid = require "util.jid"; + +local xmlns_bind = "urn:ietf:params:xml:ns:xmpp-bind"; + +function verse.plugins.bind(stream) + local function handle_features(features) + if stream.bound then return; end + stream:debug("Binding resource..."); + stream:send_iq(verse.iq({ type = "set" }):tag("bind", {xmlns=xmlns_bind}):tag("resource"):text(stream.resource), + function (reply) + if reply.attr.type == "result" then + local result_jid = reply + :get_child("bind", xmlns_bind) + :get_child_text("jid"); + stream.username, stream.host, stream.resource = jid.split(result_jid); + stream.jid, stream.bound = result_jid, true; + stream:event("bind-success", { jid = result_jid }); + elseif reply.attr.type == "error" then + local err = reply:child_with_name("error"); + local type, condition, text = reply:get_error(); + stream:event("bind-failure", { error = condition, text = text, type = type }); + end + end); + end + stream:hook("stream-features", handle_features, 200); + return true; +end diff --git a/verse/plugins/blocking.lua b/verse/plugins/blocking.lua new file mode 100644 index 0000000..017462b --- /dev/null +++ b/verse/plugins/blocking.lua @@ -0,0 +1,46 @@ +local verse = require "verse"; + +local xmlns_blocking = "urn:xmpp:blocking"; + +function verse.plugins.blocking(stream) + -- FIXME: Disco + stream.blocking = {}; + function stream.blocking:block_jid(jid, callback) + stream:send_iq(verse.iq{type="set"} + :tag("block", { xmlns = xmlns_blocking }) + :tag("item", { jid = jid }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:unblock_jid(jid, callback) + stream:send_iq(verse.iq{type="set"} + :tag("unblock", { xmlns = xmlns_blocking }) + :tag("item", { jid = jid }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:unblock_all_jids(callback) + stream:send_iq(verse.iq{type="set"} + :tag("unblock", { xmlns = xmlns_blocking }) + , function () return callback and callback(true); end + , function () return callback and callback(false); end + ); + end + function stream.blocking:get_blocked_jids(callback) + stream:send_iq(verse.iq{type="get"} + :tag("blocklist", { xmlns = xmlns_blocking }) + , function (result) + local list = result:get_child("blocklist", xmlns_blocking); + if not list then return callback and callback(false); end + local jids = {}; + for item in list:childtags() do + jids[#jids+1] = item.attr.jid; + end + return callback and callback(jids); + end + , function (result) return callback and callback(false); end + ); + end +end diff --git a/verse/plugins/carbons.lua b/verse/plugins/carbons.lua new file mode 100644 index 0000000..10d651b --- /dev/null +++ b/verse/plugins/carbons.lua @@ -0,0 +1,67 @@ +local verse = require "verse"; + +local xmlns_carbons = "urn:xmpp:carbons:2"; +local xmlns_forward = "urn:xmpp:forward:0"; +local os_time = os.time; +local parse_datetime = require "util.datetime".parse; +local bare_jid = require "util.jid".bare; + +-- TODO Check disco for support + +function verse.plugins.carbons(stream) + local carbons = {}; + carbons.enabled = false; + stream.carbons = carbons; + + function carbons:enable(callback) + stream:send_iq(verse.iq{type="set"} + :tag("enable", { xmlns = xmlns_carbons }) + , function(result) + local success = result.attr.type == "result"; + if success then + carbons.enabled = true; + end + if callback then + callback(success); + end + end or nil); + end + + function carbons:disable(callback) + stream:send_iq(verse.iq{type="set"} + :tag("disable", { xmlns = xmlns_carbons }) + , function(result) + local success = result.attr.type == "result"; + if success then + carbons.enabled = false; + end + if callback then + callback(success); + end + end or nil); + end + + local my_bare; + stream:hook("bind-success", function() + my_bare = bare_jid(stream.jid); + end); + + stream:hook("message", function(stanza) + local carbon = stanza:get_child(nil, xmlns_carbons); + if stanza.attr.from == my_bare and carbon then + local carbon_dir = carbon.name; + local fwd = carbon:get_child("forwarded", xmlns_forward); + local fwd_stanza = fwd and fwd:get_child("message", "jabber:client"); + local delay = fwd:get_child("delay", "urn:xmpp:delay"); + local stamp = delay and delay.attr.stamp; + stamp = stamp and parse_datetime(stamp); + if fwd_stanza then + return stream:event("carbon", { + dir = carbon_dir, + stanza = fwd_stanza, + timestamp = stamp or os_time(), + }); + end + end + end, 1); +end diff --git a/verse/plugins/compression.lua b/verse/plugins/compression.lua new file mode 100644 index 0000000..e45ae7c --- /dev/null +++ b/verse/plugins/compression.lua @@ -0,0 +1,122 @@ +-- Copyright (C) 2009-2010 Matthew Wild +-- Copyright (C) 2009-2010 Tobias Markmann +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local verse = require "verse"; +local zlib = require "zlib"; + +local xmlns_compression_feature = "http://jabber.org/features/compress" +local xmlns_compression_protocol = "http://jabber.org/protocol/compress" +local xmlns_stream = "http://etherx.jabber.org/streams"; + +local compression_level = 9; + +-- returns either nil or a fully functional ready to use inflate stream +local function get_deflate_stream(session) + local status, deflate_stream = pcall(zlib.deflate, compression_level); + if status == false then + local error_st = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session:send(error_st); + session:error("Failed to create zlib.deflate filter: %s", tostring(deflate_stream)); + return + end + return deflate_stream +end + +-- returns either nil or a fully functional ready to use inflate stream +local function get_inflate_stream(session) + local status, inflate_stream = pcall(zlib.inflate); + if status == false then + local error_st = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("setup-failed"); + session:send(error_st); + session:error("Failed to create zlib.inflate filter: %s", tostring(inflate_stream)); + return + end + return inflate_stream +end + +-- setup compression for a stream +local function setup_compression(session, deflate_stream) + function session:send(t) + --TODO: Better code injection in the sending process + local status, compressed, eof = pcall(deflate_stream, tostring(t), 'sync'); + if status == false then + session:close({ + condition = "undefined-condition"; + text = compressed; + extra = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("processing-failed"); + }); + session:warn("Compressed send failed: %s", tostring(compressed)); + return; + end + session.conn:write(compressed); + end; +end + +-- setup decompression for a stream +local function setup_decompression(session, inflate_stream) + local old_data = session.data + session.data = function(conn, data) + session:debug("Decompressing data..."); + local status, decompressed, eof = pcall(inflate_stream, data); + if status == false then + session:close({ + condition = "undefined-condition"; + text = decompressed; + extra = verse.stanza("failure", {xmlns=xmlns_compression_protocol}):tag("processing-failed"); + }); + stream:warn("%s", tostring(decompressed)); + return; + end + return old_data(conn, decompressed); + end; +end + +function verse.plugins.compression(stream) + local function handle_features(features) + if not stream.compressed then + -- does remote server support compression? + local comp_st = features:child_with_name("compression"); + if comp_st then + -- do we support the mechanism + for a in comp_st:children() do + local algorithm = a[1] + if algorithm == "zlib" then + stream:send(verse.stanza("compress", {xmlns=xmlns_compression_protocol}):tag("method"):text("zlib")) + stream:debug("Enabled compression using zlib.") + return true; + end + end + session:debug("Remote server supports no compression algorithm we support.") + end + end + end + local function handle_compressed(stanza) + if stanza.name == "compressed" then + stream:debug("Activating compression...") + + -- create deflate and inflate streams + local deflate_stream = get_deflate_stream(stream); + if not deflate_stream then return end + + local inflate_stream = get_inflate_stream(stream); + if not inflate_stream then return end + + -- setup compression for stream.w + setup_compression(stream, deflate_stream); + + -- setup decompression for stream.data + setup_decompression(stream, inflate_stream); + + stream.compressed = true; + stream:reopen(); + elseif stanza.name == "failure" then + stream:warn("Failed to establish compression"); + end + end + stream:hook("stream-features", handle_features, 250); + stream:hook("stream/"..xmlns_compression_protocol, handle_compressed); +end diff --git a/verse/plugins/disco.lua b/verse/plugins/disco.lua new file mode 100644 index 0000000..45e3f9a --- /dev/null +++ b/verse/plugins/disco.lua @@ -0,0 +1,369 @@ +-- Verse XMPP Library +-- Copyright (C) 2010 Hubert Chathi +-- Copyright (C) 2010 Matthew Wild +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local verse = require "verse"; +local b64 = require("mime").b64; +local sha1 = require("util.sha1").sha1; + +local xmlns_caps = "http://jabber.org/protocol/caps"; +local xmlns_disco = "http://jabber.org/protocol/disco"; +local xmlns_disco_info = xmlns_disco.."#info"; +local xmlns_disco_items = xmlns_disco.."#items"; + +function verse.plugins.disco(stream) + stream:add_plugin("presence"); + local disco_info_mt = { + __index = function(t, k) + local node = { identities = {}, features = {} }; + if k == "identities" or k == "features" then + return t[false][k] + end + t[k] = node; + return node; + end, + }; + local disco_items_mt = { + __index = function(t, k) + local node = { }; + t[k] = node; + return node; + end, + }; + stream.disco = { + cache = {}, + info = setmetatable({ + [false] = { + identities = { + {category = 'client', type='pc', name='Verse'}, + }, + features = { + [xmlns_caps] = true, + [xmlns_disco_info] = true, + [xmlns_disco_items] = true, + }, + }, + }, disco_info_mt); + items = setmetatable({[false]={}}, disco_items_mt); + }; + + stream.caps = {} + stream.caps.node = 'http://code.matthewwild.co.uk/verse/' + + local function cmp_identity(item1, item2) + if item1.category < item2.category then + return true; + elseif item2.category < item1.category then + return false; + end + if item1.type < item2.type then + return true; + elseif item2.type < item1.type then + return false; + end + if (not item1['xml:lang'] and item2['xml:lang']) or + (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then + return true + end + return false + end + + local function cmp_feature(item1, item2) + return item1.var < item2.var + end + + local function calculate_hash(node) + local identities = stream.disco.info[node or false].identities; + table.sort(identities, cmp_identity) + local features = {}; + for var in pairs(stream.disco.info[node or false].features) do + features[#features+1] = { var = var }; + end + table.sort(features, cmp_feature) + local S = {}; + for key,identity in pairs(identities) do + S[#S+1] = table.concat({ + identity.category, identity.type or '', + identity['xml:lang'] or '', identity.name or '' + }, '/'); + end + for key,feature in pairs(features) do + S[#S+1] = feature.var + end + S[#S+1] = ''; + S = table.concat(S,'<'); + -- FIXME: make sure S is utf8-encoded + --stream:debug("Computed hash string: "..S); + --stream:debug("Computed hash string (sha1): "..sha1(S, true)); + --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S))); + return (b64(sha1(S))) + end + + setmetatable(stream.caps, { + __call = function (...) -- vararg: allow calling as function or member + -- retrieve the c stanza to insert into the + -- presence stanza + local hash = calculate_hash() + stream.caps.hash = hash; + -- TODO proper caching.... some day + return verse.stanza('c', { + xmlns = xmlns_caps, + hash = 'sha-1', + node = stream.caps.node, + ver = hash + }) + end + }) + + function stream:set_identity(identity, node) + self.disco.info[node or false].identities = { identity }; + stream:resend_presence(); + end + + function stream:add_identity(identity, node) + local identities = self.disco.info[node or false].identities; + identities[#identities + 1] = identity; + stream:resend_presence(); + end + + function stream:add_disco_feature(feature, node) + local feature = feature.var or feature; + self.disco.info[node or false].features[feature] = true; + stream:resend_presence(); + end + + function stream:remove_disco_feature(feature, node) + local feature = feature.var or feature; + self.disco.info[node or false].features[feature] = nil; + stream:resend_presence(); + end + + function stream:add_disco_item(item, node) + local items = self.disco.items[node or false]; + items[#items +1] = item; + end + + function stream:remove_disco_item(item, node) + local items = self.disco.items[node or false]; + for i=#items,1,-1 do + if items[i] == item then + table.remove(items, i); + end + end + end + + -- TODO Node? + function stream:jid_has_identity(jid, category, type) + local cached_disco = self.disco.cache[jid]; + if not cached_disco then + return nil, "no-cache"; + end + local identities = self.disco.cache[jid].identities; + if type then + return identities[category.."/"..type] or false; + end + -- Check whether we have any identities with this category instead + for identity in pairs(identities) do + if identity:match("^(.*)/") == category then + return true; + end + end + end + + function stream:jid_supports(jid, feature) + local cached_disco = self.disco.cache[jid]; + if not cached_disco or not cached_disco.features then + return nil, "no-cache"; + end + return cached_disco.features[feature] or false; + end + + function stream:get_local_services(category, type) + local host_disco = self.disco.cache[self.host]; + if not(host_disco) or not(host_disco.items) then + return nil, "no-cache"; + end + + local results = {}; + for _, service in ipairs(host_disco.items) do + if self:jid_has_identity(service.jid, category, type) then + table.insert(results, service.jid); + end + end + return results; + end + + function stream:disco_local_services(callback) + self:disco_items(self.host, nil, function (items) + if not items then + return callback({}); + end + local n_items = 0; + local function item_callback() + n_items = n_items - 1; + if n_items == 0 then + return callback(items); + end + end + + for _, item in ipairs(items) do + if item.jid then + n_items = n_items + 1; + self:disco_info(item.jid, nil, item_callback); + end + end + if n_items == 0 then + return callback(items); + end + end); + end + + function stream:disco_info(jid, node, callback) + local disco_request = verse.iq({ to = jid, type = "get" }) + :tag("query", { xmlns = xmlns_disco_info, node = node }); + self:send_iq(disco_request, function (result) + if result.attr.type == "error" then + return callback(nil, result:get_error()); + end + + local identities, features = {}, {}; + + for tag in result:get_child("query", xmlns_disco_info):childtags() do + if tag.name == "identity" then + identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true; + elseif tag.name == "feature" then + features[tag.attr.var] = true; + end + end + + + if not self.disco.cache[jid] then + self.disco.cache[jid] = { nodes = {} }; + end + + if node then + if not self.disco.cache[jid].nodes[node] then + self.disco.cache[jid].nodes[node] = { nodes = {} }; + end + self.disco.cache[jid].nodes[node].identities = identities; + self.disco.cache[jid].nodes[node].features = features; + else + self.disco.cache[jid].identities = identities; + self.disco.cache[jid].features = features; + end + return callback(self.disco.cache[jid]); + end); + end + + function stream:disco_items(jid, node, callback) + local disco_request = verse.iq({ to = jid, type = "get" }) + :tag("query", { xmlns = xmlns_disco_items, node = node }); + self:send_iq(disco_request, function (result) + if result.attr.type == "error" then + return callback(nil, result:get_error()); + end + local disco_items = { }; + for tag in result:get_child("query", xmlns_disco_items):childtags() do + if tag.name == "item" then + table.insert(disco_items, { + name = tag.attr.name; + jid = tag.attr.jid; + node = tag.attr.node; + }); + end + end + + if not self.disco.cache[jid] then + self.disco.cache[jid] = { nodes = {} }; + end + + if node then + if not self.disco.cache[jid].nodes[node] then + self.disco.cache[jid].nodes[node] = { nodes = {} }; + end + self.disco.cache[jid].nodes[node].items = disco_items; + else + self.disco.cache[jid].items = disco_items; + end + return callback(disco_items); + end); + end + + stream:hook("iq/"..xmlns_disco_info, function (stanza) + local query = stanza.tags[1]; + if stanza.attr.type == 'get' and query.name == "query" then + local query_node = query.attr.node; + local node = stream.disco.info[query_node or false]; + if query_node and query_node == stream.caps.node .. "#" .. stream.caps.hash then + node = stream.disco.info[false]; + end + local identities, features = node.identities, node.features + + -- construct the response + local result = verse.reply(stanza):tag("query", { + xmlns = xmlns_disco_info, + node = query_node, + }); + for _,identity in pairs(identities) do + result:tag('identity', identity):up() + end + for feature in pairs(features) do + result:tag('feature', { var = feature }):up() + end + stream:send(result); + return true + end + end); + + stream:hook("iq/"..xmlns_disco_items, function (stanza) + local query = stanza.tags[1]; + if stanza.attr.type == 'get' and query.name == "query" then + -- figure out what items to send + local items = stream.disco.items[query.attr.node or false]; + + -- construct the response + local result = verse.reply(stanza):tag('query',{ + xmlns = xmlns_disco_items, + node = query.attr.node + }) + for i=1,#items do + result:tag('item', items[i]):up() + end + stream:send(result); + return true + end + end); + + local initial_disco_started; + stream:hook("ready", function () + if initial_disco_started then return; end + initial_disco_started = true; + stream:disco_local_services(function (services) + for _, service in ipairs(services) do + local service_disco_info = stream.disco.cache[service.jid]; + if service_disco_info then + for identity in pairs(service_disco_info.identities) do + local category, type = identity:match("^(.*)/(.*)$"); + stream:event("disco/service-discovered/"..category, { + type = type, jid = service.jid; + }); + end + end + end + stream:event("ready"); + end); + return true; + end, 50); + + stream:hook("presence-out", function (presence) + if not presence:get_child("c", xmlns_caps) then + presence:reset():add_child(stream:caps()):reset(); + end + end, 10); +end + +-- end of disco.lua diff --git a/verse/plugins/groupchat.lua b/verse/plugins/groupchat.lua new file mode 100644 index 0000000..b9e54f9 --- /dev/null +++ b/verse/plugins/groupchat.lua @@ -0,0 +1,177 @@ +local verse = require "verse"; +local events = require "events"; +local jid = require "util.jid"; + +local room_mt = {}; +room_mt.__index = room_mt; + +local xmlns_delay = "urn:xmpp:delay"; +local xmlns_muc = "http://jabber.org/protocol/muc"; + +function verse.plugins.groupchat(stream) + stream:add_plugin("presence") + stream.rooms = {}; + + stream:hook("stanza", function (stanza) + local room_jid = jid.bare(stanza.attr.from); + if not room_jid then return end + local room = stream.rooms[room_jid] + if not room and stanza.attr.to and room_jid then + room = stream.rooms[stanza.attr.to.." "..room_jid] + end + if room and room.opts.source and stanza.attr.to ~= room.opts.source then return end + if room then + local nick = select(3, jid.split(stanza.attr.from)); + local body = stanza:get_child_text("body"); + local delay = stanza:get_child("delay", xmlns_delay); + local event = { + room_jid = room_jid; + room = room; + sender = room.occupants[nick]; + nick = nick; + body = body; + stanza = stanza; + delay = (delay and delay.attr.stamp); + }; + local ret = room:event(stanza.name, event); + return ret or (stanza.name == "message") or nil; + end + end, 500); + + function stream:join_room(jid, nick, opts) + if not nick then + return false, "no nickname supplied" + end + opts = opts or {}; + local room = setmetatable(verse.eventable{ + stream = stream, jid = jid, nick = nick, + subject = nil, + occupants = {}, + opts = opts, + }, room_mt); + if opts.source then + self.rooms[opts.source.." "..jid] = room; + else + self.rooms[jid] = room; + end + local occupants = room.occupants; + room:hook("presence", function (presence) + local nick = presence.nick or nick; + if not occupants[nick] and presence.stanza.attr.type ~= "unavailable" then + occupants[nick] = { + nick = nick; + jid = presence.stanza.attr.from; + presence = presence.stanza; + }; + local x = presence.stanza:get_child("x", xmlns_muc .. "#user"); + if x then + local x_item = x:get_child("item"); + if x_item and x_item.attr then + occupants[nick].real_jid = x_item.attr.jid; + occupants[nick].affiliation = x_item.attr.affiliation; + occupants[nick].role = x_item.attr.role; + end + --TODO Check for status 100? + end + if nick == room.nick then + room.stream:event("groupchat/joined", room); + else + room:event("occupant-joined", occupants[nick]); + end + elseif occupants[nick] and presence.stanza.attr.type == "unavailable" then + if nick == room.nick then + room.stream:event("groupchat/left", room); + if room.opts.source then + self.rooms[room.opts.source.." "..jid] = nil; + else + self.rooms[jid] = nil; + end + else + occupants[nick].presence = presence.stanza; + room:event("occupant-left", occupants[nick]); + occupants[nick] = nil; + end + end + end); + room:hook("message", function(event) + local subject = event.stanza:get_child_text("subject"); + if not subject then return end + subject = #subject > 0 and subject or nil; + if subject ~= room.subject then + local old_subject = room.subject; + room.subject = subject; + return room:event("subject-changed", { from = old_subject, to = subject, by = event.sender, event = event }); + end + end, 2000); + local join_st = verse.presence():tag("x",{xmlns = xmlns_muc}):reset(); + self:event("pre-groupchat/joining", join_st); + room:send(join_st) + self:event("groupchat/joining", room); + return room; + end + + stream:hook("presence-out", function(presence) + if not presence.attr.to then + for _, room in pairs(stream.rooms) do + room:send(presence); + end + presence.attr.to = nil; + end + end); +end + +function room_mt:send(stanza) + if stanza.name == "message" and not stanza.attr.type then + stanza.attr.type = "groupchat"; + end + if stanza.name == "presence" then + stanza.attr.to = self.jid .."/"..self.nick; + end + if stanza.attr.type == "groupchat" or not stanza.attr.to then + stanza.attr.to = self.jid; + end + if self.opts.source then + stanza.attr.from = self.opts.source + end + self.stream:send(stanza); +end + +function room_mt:send_message(text) + self:send(verse.message():tag("body"):text(text)); +end + +function room_mt:set_subject(text) + self:send(verse.message():tag("subject"):text(text)); +end + +function room_mt:leave(message) + self.stream:event("groupchat/leaving", self); + local presence = verse.presence({type="unavailable"}); + if message then + presence:tag("status"):text(message); + end + self:send(presence); +end + +function room_mt:admin_set(nick, what, value, reason) + self:send(verse.iq({type="set"}) + :query(xmlns_muc .. "#admin") + :tag("item", {nick = nick, [what] = value}) + :tag("reason"):text(reason or "")); +end + +function room_mt:set_role(nick, role, reason) + self:admin_set(nick, "role", role, reason); +end + +function room_mt:set_affiliation(nick, affiliation, reason) + self:admin_set(nick, "affiliation", affiliation, reason); +end + +function room_mt:kick(nick, reason) + self:set_role(nick, "none", reason); +end + +function room_mt:ban(nick, reason) + self:set_affiliation(nick, "outcast", reason); +end diff --git a/verse/plugins/jingle.lua b/verse/plugins/jingle.lua new file mode 100644 index 0000000..fedcf10 --- /dev/null +++ b/verse/plugins/jingle.lua @@ -0,0 +1,301 @@ +local verse = require "verse"; +local sha1 = require "util.sha1".sha1; +local timer = require "util.timer"; +local uuid_generate = require "util.uuid".generate; + +local xmlns_jingle = "urn:xmpp:jingle:1"; +local xmlns_jingle_errors = "urn:xmpp:jingle:errors:1"; + +local jingle_mt = {}; +jingle_mt.__index = jingle_mt; + +local registered_transports = {}; +local registered_content_types = {}; + +function verse.plugins.jingle(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle); + end, 10); + + function stream:jingle(to) + return verse.eventable(setmetatable(base or { + role = "initiator"; + peer = to; + sid = uuid_generate(); + stream = stream; + }, jingle_mt)); + end + + function stream:register_jingle_transport(transport) + -- transport is a function that receives a + -- element, and returns a connection + -- We wait for 'connected' on that connection, + -- and use :send() and 'incoming-raw'. + end + + function stream:register_jingle_content_type(content) + -- Call content() for every 'incoming-raw'? + -- I think content() returns the object we return + -- on jingle:accept() + end + + local function handle_incoming_jingle(stanza) + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + local sid = jingle_tag.attr.sid; + local action = jingle_tag.attr.action; + local result = stream:event("jingle/"..sid, stanza); + if result == true then + -- Ack + stream:send(verse.reply(stanza)); + return true; + end + -- No existing Jingle object handled this action, our turn... + if action ~= "session-initiate" then + -- Trying to send a command to a session we don't know + local reply = verse.error_reply(stanza, "cancel", "item-not-found") + :tag("unknown-session", { xmlns = xmlns_jingle_errors }):up(); + stream:send(reply); + return; + end + + -- Ok, session-initiate, new session + + -- Create new Jingle object + local sid = jingle_tag.attr.sid; + + local jingle = verse.eventable{ + role = "receiver"; + peer = stanza.attr.from; + sid = sid; + stream = stream; + }; + + setmetatable(jingle, jingle_mt); + + local content_tag; + local content, transport; + for tag in jingle_tag:childtags() do + if tag.name == "content" and tag.attr.xmlns == xmlns_jingle then + local description_tag = tag:child_with_name("description"); + local description_xmlns = description_tag.attr.xmlns; + if description_xmlns then + local desc_handler = stream:event("jingle/content/"..description_xmlns, jingle, description_tag); + if desc_handler then + content = desc_handler; + end + end + + local transport_tag = tag:child_with_name("transport"); + local transport_xmlns = transport_tag.attr.xmlns; + + transport = stream:event("jingle/transport/"..transport_xmlns, jingle, transport_tag); + if content and transport then + content_tag = tag; + break; + end + end + end + if not content then + -- FIXME: Fail, no content + stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified content is not supported")); + return true; + end + + if not transport then + -- FIXME: Refuse session, no transport + stream:send(verse.error_reply(stanza, "cancel", "feature-not-implemented", "The specified transport is not supported")); + return true; + end + + stream:send(verse.reply(stanza)); + + jingle.content_tag = content_tag; + jingle.creator, jingle.name = content_tag.attr.creator, content_tag.attr.name; + jingle.content, jingle.transport = content, transport; + + function jingle:decline() + -- FIXME: Decline session + end + + stream:hook("jingle/"..sid, function (stanza) + if stanza.attr.from ~= jingle.peer then + return false; + end + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + return jingle:handle_command(jingle_tag); + end); + + stream:event("jingle", jingle); + return true; + end + + function jingle_mt:handle_command(jingle_tag) + local action = jingle_tag.attr.action; + stream:debug("Handling Jingle command: %s", action); + if action == "session-terminate" then + self:destroy(); + elseif action == "session-accept" then + -- Yay! + self:handle_accepted(jingle_tag); + elseif action == "transport-info" then + stream:debug("Handling transport-info"); + self.transport:info_received(jingle_tag); + elseif action == "transport-replace" then + -- FIXME: Used for IBB fallback + stream:error("Peer wanted to swap transport, not implemented"); + else + -- FIXME: Reply unhandled command + stream:warn("Unhandled Jingle command: %s", action); + return nil; + end + return true; + end + + function jingle_mt:send_command(command, element, callback) + local stanza = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { + xmlns = xmlns_jingle, + sid = self.sid, + action = command, + initiator = self.role == "initiator" and self.stream.jid or nil, + responder = self.role == "responder" and self.jid or nil, + }):add_child(element); + if not callback then + self.stream:send(stanza); + else + self.stream:send_iq(stanza, callback); + end + end + + function jingle_mt:accept(options) + local accept_stanza = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { + xmlns = xmlns_jingle, + sid = self.sid, + action = "session-accept", + responder = stream.jid, + }) + :tag("content", { creator = self.creator, name = self.name }); + + local content_accept_tag = self.content:generate_accept(self.content_tag:child_with_name("description"), options); + accept_stanza:add_child(content_accept_tag); + + local transport_accept_tag = self.transport:generate_accept(self.content_tag:child_with_name("transport"), options); + accept_stanza:add_child(transport_accept_tag); + + local jingle = self; + stream:send_iq(accept_stanza, function (result) + if result.attr.type == "error" then + local type, condition, text = result:get_error(); + stream:error("session-accept rejected: %s", condition); -- FIXME: Notify + return false; + end + jingle.transport:connect(function (conn) + stream:warn("CONNECTED (receiver)!!!"); + jingle.state = "active"; + jingle:event("connected", conn); + end); + end); + end + + + stream:hook("iq/"..xmlns_jingle, handle_incoming_jingle); + return true; +end + +function jingle_mt:offer(name, content) + local session_initiate = verse.iq({ to = self.peer, type = "set" }) + :tag("jingle", { xmlns = xmlns_jingle, action = "session-initiate", + initiator = self.stream.jid, sid = self.sid }); + + -- Content tag + session_initiate:tag("content", { creator = self.role, name = name }); + + -- Need description element from someone who can turn 'content' into XML + local description = self.stream:event("jingle/describe/"..name, content); + + if not description then + return false, "Unknown content type"; + end + + session_initiate:add_child(description); + + -- FIXME: Sort transports by 1) recipient caps 2) priority (SOCKS vs IBB, etc.) + -- Fixed to s5b in the meantime + local transport = self.stream:event("jingle/transport/".."urn:xmpp:jingle:transports:s5b:1", self); + self.transport = transport; + + session_initiate:add_child(transport:generate_initiate()); + + self.stream:debug("Hooking %s", "jingle/"..self.sid); + self.stream:hook("jingle/"..self.sid, function (stanza) + if stanza.attr.from ~= self.peer then + return false; + end + local jingle_tag = stanza:get_child("jingle", xmlns_jingle); + return self:handle_command(jingle_tag) + end); + + self.stream:send_iq(session_initiate, function (result) + if result.attr.type == "error" then + self.state = "terminated"; + local type, condition, text = result:get_error(); + return self:event("error", { type = type, condition = condition, text = text }); + end + end); + self.state = "pending"; +end + +function jingle_mt:terminate(reason) + local reason_tag = verse.stanza("reason"):tag(reason or "success"); + self:send_command("session-terminate", reason_tag, function (result) + self.state = "terminated"; + self.transport:disconnect(); + self:destroy(); + end); +end + +function jingle_mt:destroy() + self:event("terminated"); + self.stream:unhook("jingle/"..self.sid, self.handle_command); +end + +function jingle_mt:handle_accepted(jingle_tag) + local transport_tag = jingle_tag:child_with_name("transport"); + self.transport:handle_accepted(transport_tag); + self.transport:connect(function (conn) + self.stream:debug("CONNECTED (initiator)!") + -- Connected, send file + self.state = "active"; + self:event("connected", conn); + end); +end + +function jingle_mt:set_source(source, auto_close) + local function pump() + local chunk, err = source(); + if chunk and chunk ~= "" then + self.transport.conn:send(chunk); + elseif chunk == "" then + return pump(); -- We need some data! + elseif chunk == nil then + if auto_close then + self:terminate(); + end + self.transport.conn:unhook("drained", pump); + source = nil; + end + end + self.transport.conn:hook("drained", pump); + pump(); +end + +function jingle_mt:set_sink(sink) + self.transport.conn:hook("incoming-raw", sink); + self.transport.conn:hook("disconnected", function (event) + self.stream:debug("Closing sink..."); + local reason = event.reason; + if reason == "closed" then reason = nil; end + sink(nil, reason); + end); +end diff --git a/verse/plugins/jingle_ft.lua b/verse/plugins/jingle_ft.lua new file mode 100644 index 0000000..1a52a3a --- /dev/null +++ b/verse/plugins/jingle_ft.lua @@ -0,0 +1,74 @@ +local verse = require "verse"; +local ltn12 = require "ltn12"; + +local dirsep = package.config:sub(1,1); + +local xmlns_jingle_ft = "urn:xmpp:jingle:apps:file-transfer:1"; +local xmlns_si_file_transfer = "http://jabber.org/protocol/si/profile/file-transfer"; + +function verse.plugins.jingle_ft(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle_ft); + end, 10); + + local ft_content = { type = "file" }; + + function ft_content:generate_accept(description, options) + if options and options.save_file then + self.jingle:hook("connected", function () + local sink = ltn12.sink.file(io.open(options.save_file, "w+")); + self.jingle:set_sink(sink); + end); + end + + return description; + end + + local ft_mt = { __index = ft_content }; + stream:hook("jingle/content/"..xmlns_jingle_ft, function (jingle, description_tag) + local file_tag = description_tag:get_child("offer"):get_child("file", xmlns_si_file_transfer); + local file = { + name = file_tag.attr.name; + size = tonumber(file_tag.attr.size); + }; + + return setmetatable({ jingle = jingle, file = file }, ft_mt); + end); + + stream:hook("jingle/describe/file", function (file_info) + -- Return + local date; + if file_info.timestamp then + date = os.date("!%Y-%m-%dT%H:%M:%SZ", file_info.timestamp); + end + return verse.stanza("description", { xmlns = xmlns_jingle_ft }) + :tag("offer") + :tag("file", { xmlns = xmlns_si_file_transfer, + name = file_info.filename, -- Mandatory + size = file_info.size, -- Mandatory + date = date, + hash = file_info.hash, + }) + :tag("desc"):text(file_info.description or ""); + end); + + function stream:send_file(to, filename) + local file, err = io.open(filename); + if not file then return file, err; end + + local file_size = file:seek("end", 0); + file:seek("set", 0); + + local source = ltn12.source.file(file); + + local jingle = self:jingle(to); + jingle:offer("file", { + filename = filename:match("[^"..dirsep.."]+$"); + size = file_size; + }); + jingle:hook("connected", function () + jingle:set_source(source, true); + end); + return jingle; + end +end diff --git a/verse/plugins/jingle_ibb.lua b/verse/plugins/jingle_ibb.lua new file mode 100644 index 0000000..9c96a14 --- /dev/null +++ b/verse/plugins/jingle_ibb.lua @@ -0,0 +1,214 @@ +local verse = require "verse"; +local base64 = require "util.encodings".base64; +local uuid_generate = require "util.uuid".generate; + +local xmlns_jingle_ibb = "urn:xmpp:jingle:transports:ibb:1"; +local xmlns_ibb = "http://jabber.org/protocol/ibb"; +assert(base64.encode("This is a test.") == "VGhpcyBpcyBhIHRlc3Qu", "Base64 encoding failed"); +assert(base64.decode("VGhpcyBpcyBhIHRlc3Qu") == "This is a test.", "Base64 decoding failed"); +local t_concat = table.concat + +local ibb_conn = {}; +local ibb_conn_mt = { __index = ibb_conn }; + +local function new_ibb(stream) + local conn = setmetatable({ stream = stream }, ibb_conn_mt) + conn = verse.eventable(conn); + return conn; +end + +function ibb_conn:initiate(peer, sid, stanza) + self.block = 2048; -- ignored for now + self.stanza = stanza or 'iq'; + self.peer = peer; + self.sid = sid or tostring(self):match("%x+$"); + self.iseq = 0; + self.oseq = 0; + local feeder = function(stanza) + return self:feed(stanza) + end + self.feeder = feeder; + print("Hooking incomming IQs"); + local stream = self.stream; + stream:hook("iq/".. xmlns_ibb, feeder) + if stanza == "message" then + stream:hook("message", feeder) + end +end + +function ibb_conn:open(callback) + self.stream:send_iq(verse.iq{ to = self.peer, type = "set" } + :tag("open", { + xmlns = xmlns_ibb, + ["block-size"] = self.block, + sid = self.sid, + stanza = self.stanza + }) + , function(reply) + if callback then + if reply.attr.type ~= "error" then + callback(true) + else + callback(false, reply:get_error()) + end + end + end); +end + +function ibb_conn:send(data) + local stanza = self.stanza; + local st; + if stanza == "iq" then + st = verse.iq{ type = "set", to = self.peer } + elseif stanza == "message" then + st = verse.message{ to = self.peer } + end + + local seq = self.oseq; + self.oseq = seq + 1; + + st:tag("data", { xmlns = xmlns_ibb, sid = self.sid, seq = seq }) + :text(base64.encode(data)); + + if stanza == "iq" then + self.stream:send_iq(st, function(reply) + self:event(reply.attr.type == "result" and "drained" or "error"); + end) + else + stream:send(st) + self:event("drained"); + end +end + +function ibb_conn:feed(stanza) + if stanza.attr.from ~= self.peer then return end + local child = stanza[1]; + if child.attr.sid ~= self.sid then return end + local ok; + if child.name == "open" then + self:event("connected"); + self.stream:send(verse.reply(stanza)) + return true + elseif child.name == "data" then + local bdata = stanza:get_child_text("data", xmlns_ibb); + local seq = tonumber(child.attr.seq); + local expected_seq = self.iseq; + if bdata and seq then + if seq ~= expected_seq then + self.stream:send(verse.error_reply(stanza, "cancel", "not-acceptable", "Wrong sequence. Packet lost?")) + self:close(); + self:event("error"); + return true; + end + self.iseq = seq + 1; + local data = base64.decode(bdata); + if self.stanza == "iq" then + self.stream:send(verse.reply(stanza)) + end + self:event("incoming-raw", data); + return true; + end + elseif child.name == "close" then + self.stream:send(verse.reply(stanza)) + self:close(); + return true + end +end + +--[[ FIXME some day +function ibb_conn:receive(patt) + -- is this even used? + print("ibb_conn:receive("..tostring(patt)..")"); + assert(patt == "*a" or tonumber(patt)); + local data = t_concat(self.ibuffer):sub(self.pos, tonumber(patt) or nil); + self.pos = self.pos + #data; + return data +end + +function ibb_conn:dirty() + print("ibb_conn:dirty()"); + return false -- ???? +end +function ibb_conn:getfd() + return 0 +end +function ibb_conn:settimeout(n) + -- ignore? +end +-]] + +function ibb_conn:close() + self.stream:unhook("iq/".. xmlns_ibb, self.feeder) + self:event("disconnected"); +end + +function verse.plugins.jingle_ibb(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_jingle_ibb); + end, 10); + + local ibb = {}; + + function ibb:_setup() + local conn = new_ibb(self.stream); + conn.sid = self.sid or conn.sid; + conn.stanza = self.stanza or conn.stanza; + conn.block = self.block or conn.block; + conn:initiate(self.peer, self.sid, self.stanza); + self.conn = conn; + end + function ibb:generate_initiate() + print("ibb:generate_initiate() as ".. self.role); + local sid = uuid_generate(); + self.sid = sid; + self.stanza = 'iq'; + self.block = 2048; + local transport = verse.stanza("transport", { xmlns = xmlns_jingle_ibb, + sid = self.sid, stanza = self.stanza, ["block-size"] = self.block }); + return transport; + end + function ibb:generate_accept(initiate_transport) + print("ibb:generate_accept() as ".. self.role); + local attr = initiate_transport.attr; + self.sid = attr.sid or self.sid; + self.stanza = attr.stanza or self.stanza; + self.block = attr["block-size"] or self.block; + self:_setup(); + return initiate_transport; + end + function ibb:connect(callback) + if not self.conn then + self:_setup(); + end + local conn = self.conn; + print("ibb:connect() as ".. self.role); + if self.role == "initiator" then + conn:open(function(ok, ...) + assert(ok, table.concat({...}, ", ")); + callback(conn); + end); + else + callback(conn); + end + end + function ibb:info_received(jingle_tag) + print("ibb:info_received()"); + -- TODO, what exactly? + end + function ibb:disconnect() + if self.conn then + self.conn:close() + end + end + function ibb:handle_accepted(jingle_tag) end + + local ibb_mt = { __index = ibb }; + stream:hook("jingle/transport/"..xmlns_jingle_ibb, function (jingle) + return setmetatable({ + role = jingle.role, + peer = jingle.peer, + stream = jingle.stream, + jingle = jingle, + }, ibb_mt); + end); +end diff --git a/verse/plugins/jingle_s5b.lua b/verse/plugins/jingle_s5b.lua new file mode 100644 index 0000000..4453272 --- /dev/null +++ b/verse/plugins/jingle_s5b.lua @@ -0,0 +1,220 @@ +local verse = require "verse"; + +local xmlns_s5b = "urn:xmpp:jingle:transports:s5b:1"; +local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; +local sha1 = require "util.sha1".sha1; +local uuid_generate = require "util.uuid".generate; + +local function negotiate_socks5(conn, hash) + local function suppress_connected() + conn:unhook("connected", suppress_connected); + return true; + end + local function receive_connection_response(data) + conn:unhook("incoming-raw", receive_connection_response); + + if data:sub(1, 2) ~= "\005\000" then + return conn:event("error", "connection-failure"); + end + conn:event("connected"); + return true; + end + local function receive_auth_response(data) + conn:unhook("incoming-raw", receive_auth_response); + if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" + -- Server is not SOCKSv5, or does not allow no auth + local err = "version-mismatch"; + if data:sub(1,1) == "\005" then + err = "authentication-failure"; + end + return conn:event("error", err); + end + -- Request SOCKS5 connection + conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? + conn:hook("incoming-raw", receive_connection_response, 100); + return true; + end + conn:hook("connected", suppress_connected, 200); + conn:hook("incoming-raw", receive_auth_response, 100); + conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" +end + +local function connect_to_usable_streamhost(callback, streamhosts, auth_token) + local conn = verse.new(nil, { + streamhosts = streamhosts, + current_host = 0; + }); + --Attempt to connect to the next host + local function attempt_next_streamhost(event) + if event then + return callback(nil, event.reason); + end + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:debug("Attempting to connect to "..conn.streamhosts[conn.current_host].host..":"..conn.streamhosts[conn.current_host].port.."..."); + local ok, err = conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + if not ok then + conn:debug("Error connecting to proxy (%s:%s): %s", + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port, + err + ); + else + conn:debug("Connecting..."); + end + negotiate_socks5(conn, auth_token); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + return callback(nil); + -- Let disconnected event fall through to user handlers... + end + conn:hook("disconnected", attempt_next_streamhost, 100); + -- When this event fires, we're connected to a streamhost + conn:hook("connected", function () + conn:unhook("disconnected", attempt_next_streamhost); + callback(conn.streamhosts[conn.current_host], conn); + end, 100); + attempt_next_streamhost(); -- Set it in motion + return conn; +end + +function verse.plugins.jingle_s5b(stream) + stream:hook("ready", function () + stream:add_disco_feature(xmlns_s5b); + end, 10); + + local s5b = {}; + + function s5b:generate_initiate() + self.s5b_sid = uuid_generate(); + local transport = verse.stanza("transport", { xmlns = xmlns_s5b, + mode = "tcp", sid = self.s5b_sid }); + local p = 0; + for jid, streamhost in pairs(stream.proxy65.available_streamhosts) do + p = p + 1; + transport:tag("candidate", { jid = jid, host = streamhost.host, + port = streamhost.port, cid=jid, priority = p, type = "proxy" }):up(); + end + stream:debug("Have %d proxies", p) + return transport; + end + + function s5b:generate_accept(initiate_transport) + local candidates = {}; + self.s5b_peer_candidates = candidates; + self.s5b_mode = initiate_transport.attr.mode or "tcp"; + self.s5b_sid = initiate_transport.attr.sid or self.jingle.sid; + + -- Import the list of candidates the initiator offered us + for candidate in initiate_transport:childtags() do + --if candidate.attr.jid == "asterix4@jabber.lagaule.org/Gajim" + --and candidate.attr.host == "82.246.25.239" then + candidates[candidate.attr.cid] = { + type = candidate.attr.type; + jid = candidate.attr.jid; + host = candidate.attr.host; + port = tonumber(candidate.attr.port) or 0; + priority = tonumber(candidate.attr.priority) or 0; + cid = candidate.attr.cid; + }; + --end + end + + -- Import our own candidates + -- TODO ^ + local transport = verse.stanza("transport", { xmlns = xmlns_s5b }); + return transport; + end + + function s5b:connect(callback) + stream:warn("Connecting!"); + + local streamhost_array = {}; + for cid, streamhost in pairs(self.s5b_peer_candidates or {}) do + streamhost_array[#streamhost_array+1] = streamhost; + end + + if #streamhost_array > 0 then + self.connecting_peer_candidates = true; + local function onconnect(streamhost, conn) + self.jingle:send_command("transport-info", verse.stanza("content", { creator = self.creator, name = self.name }) + :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("candidate-used", { cid = streamhost.cid })); + self.onconnect_callback = callback; + self.conn = conn; + end + local auth_token = sha1(self.s5b_sid..self.peer..stream.jid, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + else + stream:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); + self.onconnect_callback = callback; + end + end + + function s5b:info_received(jingle_tag) + stream:warn("Info received"); + local content_tag = jingle_tag:child_with_name("content"); + local transport_tag = content_tag:child_with_name("transport"); + if transport_tag:get_child("candidate-used") and not self.connecting_peer_candidates then + local candidate_used = transport_tag:child_with_name("candidate-used"); + if candidate_used then + -- Connect straight away to candidate used, we weren't trying any anyway + local function onconnect(streamhost, conn) + if self.jingle.role == "initiator" then -- More correct would be - "is this a candidate we offered?" + -- Activate the stream + self.jingle.stream:send_iq(verse.iq({ to = streamhost.jid, type = "set" }) + :tag("query", { xmlns = xmlns_bytestreams, sid = self.s5b_sid }) + :tag("activate"):text(self.jingle.peer), function (result) + + if result.attr.type == "result" then + self.jingle:send_command("transport-info", verse.stanza("content", content_tag.attr) + :tag("transport", { xmlns = xmlns_s5b, sid = self.s5b_sid }) + :tag("activated", { cid = candidate_used.attr.cid })); + self.conn = conn; + self.onconnect_callback(conn); + else + self.jingle.stream:error("Failed to activate bytestream"); + end + end); + end + end + + -- FIXME: Another assumption that cid==jid, and that it was our candidate + self.jingle.stream:debug("CID: %s", self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]); + local streamhost_array = { + self.jingle.stream.proxy65.available_streamhosts[candidate_used.attr.cid]; + }; + + local auth_token = sha1(self.s5b_sid..stream.jid..self.peer, true); + connect_to_usable_streamhost(onconnect, streamhost_array, auth_token); + end + elseif transport_tag:get_child("activated") then + self.onconnect_callback(self.conn); + end + end + + function s5b:disconnect() + if self.conn then + self.conn:close(); + end + end + + function s5b:handle_accepted(jingle_tag) + end + + local s5b_mt = { __index = s5b }; + stream:hook("jingle/transport/"..xmlns_s5b, function (jingle) + return setmetatable({ + role = jingle.role, + peer = jingle.peer, + stream = jingle.stream, + jingle = jingle, + }, s5b_mt); + end); +end diff --git a/verse/plugins/keepalive.lua b/verse/plugins/keepalive.lua new file mode 100644 index 0000000..7afd812 --- /dev/null +++ b/verse/plugins/keepalive.lua @@ -0,0 +1,9 @@ +local verse = require "verse"; + +function verse.plugins.keepalive(stream) + stream.keepalive_timeout = stream.keepalive_timeout or 300; + verse.add_task(stream.keepalive_timeout, function () + stream.conn:write(" "); + return stream.keepalive_timeout; + end); +end diff --git a/verse/plugins/legacy.lua b/verse/plugins/legacy.lua new file mode 100644 index 0000000..f12517d --- /dev/null +++ b/verse/plugins/legacy.lua @@ -0,0 +1,65 @@ +local verse = require "verse"; +local uuid = require "util.uuid".generate; + +local xmlns_auth = "jabber:iq:auth"; + +function verse.plugins.legacy(stream) + function handle_auth_form(result) + local query = result:get_child("query", xmlns_auth); + if result.attr.type ~= "result" or not query then + local type, cond, text = result:get_error(); + stream:debug("warn", "%s %s: %s", type, cond, text); + --stream:event("authentication-failure", { condition = cond }); + -- COMPAT continue anyways + end + local auth_data = { + username = stream.username; + password = stream.password; + resource = stream.resource or uuid(); + digest = false, sequence = false, token = false; + }; + local request = verse.iq({ to = stream.host, type = "set" }) + :tag("query", { xmlns = xmlns_auth }); + if #query > 0 then + for tag in query:childtags() do + local field = tag.name; + local value = auth_data[field]; + if value then + request:tag(field):text(auth_data[field]):up(); + elseif value == nil then + local cond = "feature-not-implemented"; + stream:event("authentication-failure", { condition = cond }); + return false; + end + end + else -- COMPAT for servers not following XEP 78 + for field, value in pairs(auth_data) do + if value then + request:tag(field):text(value):up(); + end + end + end + stream:send_iq(request, function (response) + if response.attr.type == "result" then + stream.resource = auth_data.resource; + stream.jid = auth_data.username.."@"..stream.host.."/"..auth_data.resource; + stream:event("authentication-success"); + stream:event("bind-success", stream.jid); + else + local type, cond, text = response:get_error(); + stream:event("authentication-failure", { condition = cond }); + end + end); + end + + function handle_opened(attr) + if not attr.version then + stream:send_iq(verse.iq({type="get"}) + :tag("query", { xmlns = "jabber:iq:auth" }) + :tag("username"):text(stream.username), + handle_auth_form); + + end + end + stream:hook("opened", handle_opened); +end diff --git a/verse/plugins/pep.lua b/verse/plugins/pep.lua new file mode 100644 index 0000000..2a6c7ef --- /dev/null +++ b/verse/plugins/pep.lua @@ -0,0 +1,34 @@ +local verse = require "verse"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = xmlns_pubsub.."#event"; + +function verse.plugins.pep(stream) + stream:add_plugin("disco"); + stream:add_plugin("pubsub"); + stream.pep = {}; + + stream:hook("pubsub/event", function(event) + return stream:event("pep/"..event.node, { from = event.from, item = event.item.tags[1] } ); + end); + + function stream:hook_pep(node, callback, priority) + local handlers = stream.events._handlers["pep/"..node]; + if not(handlers) or #handlers == 0 then + stream:add_disco_feature(node.."+notify"); + end + stream:hook("pep/"..node, callback, priority); + end + + function stream:unhook_pep(node, callback) + stream:unhook("pep/"..node, callback); + local handlers = stream.events._handlers["pep/"..node]; + if not(handlers) or #handlers == 0 then + stream:remove_disco_feature(node.."+notify"); + end + end + + function stream:publish_pep(item, node) + return stream.pubsub:service(nil):node(node or item.attr.xmlns):publish(nil, nil, item) + end +end diff --git a/verse/plugins/ping.lua b/verse/plugins/ping.lua new file mode 100644 index 0000000..b89a671 --- /dev/null +++ b/verse/plugins/ping.lua @@ -0,0 +1,24 @@ +local verse = require "verse"; + +local xmlns_ping = "urn:xmpp:ping"; + +function verse.plugins.ping(stream) + function stream:ping(jid, callback) + local t = socket.gettime(); + stream:send_iq(verse.iq{ to = jid, type = "get" }:tag("ping", { xmlns = xmlns_ping }), + function (reply) + if reply.attr.type == "error" then + local type, condition, text = reply:get_error(); + if condition ~= "service-unavailable" and condition ~= "feature-not-implemented" then + callback(nil, jid, { type = type, condition = condition, text = text }); + return; + end + end + callback(socket.gettime()-t, jid); + end); + end + stream:hook("iq/"..xmlns_ping, function(stanza) + return stream:send(verse.reply(stanza)); + end); + return true; +end diff --git a/verse/plugins/presence.lua b/verse/plugins/presence.lua new file mode 100644 index 0000000..898e27e --- /dev/null +++ b/verse/plugins/presence.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; + +function verse.plugins.presence(stream) + stream.last_presence = nil; + + stream:hook("presence-out", function (presence) + if not presence.attr.to then + stream.last_presence = presence; -- Cache non-directed presence + end + end, 1); + + function stream:resend_presence() + if last_presence then + stream:send(last_presence); + end + end + + function stream:set_status(opts) + local p = verse.presence(); + if type(opts) == "table" then + if opts.show then + p:tag("show"):text(opts.show):up(); + end + if opts.prio then + p:tag("priority"):text(tostring(opts.prio)):up(); + end + if opts.msg then + p:tag("status"):text(opts.msg):up(); + end + end + -- TODO maybe use opts as prio if it's a int, + -- or as show or status if it's a string? + + stream:send(p); + end +end diff --git a/verse/plugins/private.lua b/verse/plugins/private.lua new file mode 100644 index 0000000..473a8f1 --- /dev/null +++ b/verse/plugins/private.lua @@ -0,0 +1,35 @@ +local verse = require "verse"; + +-- Implements XEP-0049: Private XML Storage + +local xmlns_private = "jabber:iq:private"; + +function verse.plugins.private(stream) + function stream:private_set(name, xmlns, data, callback) + local iq = verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_private }); + if data then + if data.name == name and data.attr and data.attr.xmlns == xmlns then + iq:add_child(data); + else + iq:tag(name, { xmlns = xmlns }) + :add_child(data); + end + end + self:send_iq(iq, callback); + end + + function stream:private_get(name, xmlns, callback) + self:send_iq(verse.iq({type="get"}) + :tag("query", { xmlns = xmlns_private }) + :tag(name, { xmlns = xmlns }), + function (reply) + if reply.attr.type == "result" then + local query = reply:get_child("query", xmlns_private); + local result = query:get_child(name, xmlns); + callback(result); + end + end); + end +end + diff --git a/verse/plugins/proxy65.lua b/verse/plugins/proxy65.lua new file mode 100644 index 0000000..377ac2d --- /dev/null +++ b/verse/plugins/proxy65.lua @@ -0,0 +1,186 @@ +local events = require "util.events"; +local uuid = require "util.uuid"; +local sha1 = require "util.sha1"; + +local proxy65_mt = {}; +proxy65_mt.__index = proxy65_mt; + +local xmlns_bytestreams = "http://jabber.org/protocol/bytestreams"; + +local negotiate_socks5; + +function verse.plugins.proxy65(stream) + stream.proxy65 = setmetatable({ stream = stream }, proxy65_mt); + stream.proxy65.available_streamhosts = {}; + local outstanding_proxies = 0; + stream:hook("disco/service-discovered/proxy", function (service) + -- Fill list with available proxies + if service.type == "bytestreams" then + outstanding_proxies = outstanding_proxies + 1; + stream:send_iq(verse.iq({ to = service.jid, type = "get" }) + :tag("query", { xmlns = xmlns_bytestreams }), function (result) + + outstanding_proxies = outstanding_proxies - 1; + if result.attr.type == "result" then + local streamhost = result:get_child("query", xmlns_bytestreams) + :get_child("streamhost").attr; + + stream.proxy65.available_streamhosts[streamhost.jid] = { + jid = streamhost.jid; + host = streamhost.host; + port = tonumber(streamhost.port); + }; + end + if outstanding_proxies == 0 then + stream:event("proxy65/discovered-proxies", stream.proxy65.available_streamhosts); + end + end); + end + end); + stream:hook("iq/"..xmlns_bytestreams, function (request) + local conn = verse.new(nil, { + initiator_jid = request.attr.from, + streamhosts = {}, + current_host = 0; + }); + + -- Parse hosts from request + for tag in request.tags[1]:childtags() do + if tag.name == "streamhost" then + table.insert(conn.streamhosts, tag.attr); + end + end + + --Attempt to connect to the next host + local function attempt_next_streamhost() + -- First connect, or the last connect failed + if conn.current_host < #conn.streamhosts then + conn.current_host = conn.current_host + 1; + conn:connect( + conn.streamhosts[conn.current_host].host, + conn.streamhosts[conn.current_host].port + ); + negotiate_socks5(stream, conn, request.tags[1].attr.sid, request.attr.from, stream.jid); + return true; -- Halt processing of disconnected event + end + -- All streamhosts tried, none successful + conn:unhook("disconnected", attempt_next_streamhost); + stream:send(verse.error_reply(request, "cancel", "item-not-found")); + -- Let disconnected event fall through to user handlers... + end + + function conn:accept() + conn:hook("disconnected", attempt_next_streamhost, 100); + -- When this event fires, we're connected to a streamhost + conn:hook("connected", function () + conn:unhook("disconnected", attempt_next_streamhost); + -- Send XMPP success notification + local reply = verse.reply(request) + :tag("query", request.tags[1].attr) + :tag("streamhost-used", { jid = conn.streamhosts[conn.current_host].jid }); + stream:send(reply); + end, 100); + attempt_next_streamhost(); + end + function conn:refuse() + -- FIXME: XMPP refused reply + end + stream:event("proxy65/request", conn); + end); +end + +function proxy65_mt:new(target_jid, proxies) + local conn = verse.new(nil, { + target_jid = target_jid; + bytestream_sid = uuid.generate(); + }); + + local request = verse.iq{type="set", to = target_jid} + :tag("query", { xmlns = xmlns_bytestreams, mode = "tcp", sid = conn.bytestream_sid }); + for _, proxy in ipairs(proxies or self.proxies) do + request:tag("streamhost", proxy):up(); + end + + + self.stream:send_iq(request, function (reply) + if reply.attr.type == "error" then + local type, condition, text = reply:get_error(); + conn:event("connection-failed", { conn = conn, type = type, condition = condition, text = text }); + else + -- Target connected to streamhost, connect ourselves + local streamhost_used = reply.tags[1]:get_child("streamhost-used"); + if not streamhost_used then + --FIXME: Emit error + end + conn.streamhost_jid = streamhost_used.attr.jid; + local host, port; + for _, proxy in ipairs(proxies or self.proxies) do + if proxy.jid == conn.streamhost_jid then + host, port = proxy.host, proxy.port; + break; + end + end + if not (host and port) then + --FIXME: Emit error + end + + conn:connect(host, port); + + local function handle_proxy_connected() + conn:unhook("connected", handle_proxy_connected); + -- Both of us connected, tell proxy to activate connection + local request = verse.iq{to = conn.streamhost_jid, type="set"} + :tag("query", { xmlns = xmlns_bytestreams, sid = conn.bytestream_sid }) + :tag("activate"):text(target_jid); + self.stream:send_iq(request, function (reply) + if reply.attr.type == "result" then + -- Connection activated, ready to use + conn:event("connected", conn); + else + --FIXME: Emit error + end + end); + return true; + end + conn:hook("connected", handle_proxy_connected, 100); + + negotiate_socks5(self.stream, conn, conn.bytestream_sid, self.stream.jid, target_jid); + end + end); + return conn; +end + +function negotiate_socks5(stream, conn, sid, requester_jid, target_jid) + local hash = sha1.sha1(sid..requester_jid..target_jid); + local function suppress_connected() + conn:unhook("connected", suppress_connected); + return true; + end + local function receive_connection_response(data) + conn:unhook("incoming-raw", receive_connection_response); + + if data:sub(1, 2) ~= "\005\000" then + return conn:event("error", "connection-failure"); + end + conn:event("connected"); + return true; + end + local function receive_auth_response(data) + conn:unhook("incoming-raw", receive_auth_response); + if data ~= "\005\000" then -- SOCKSv5; "NO AUTHENTICATION" + -- Server is not SOCKSv5, or does not allow no auth + local err = "version-mismatch"; + if data:sub(1,1) == "\005" then + err = "authentication-failure"; + end + return conn:event("error", err); + end + -- Request SOCKS5 connection + conn:send(string.char(0x05, 0x01, 0x00, 0x03, #hash)..hash.."\0\0"); --FIXME: Move to "connected"? + conn:hook("incoming-raw", receive_connection_response, 100); + return true; + end + conn:hook("connected", suppress_connected, 200); + conn:hook("incoming-raw", receive_auth_response, 100); + conn:send("\005\001\000"); -- SOCKSv5; 1 mechanism; "NO AUTHENTICATION" +end diff --git a/verse/plugins/pubsub.lua b/verse/plugins/pubsub.lua new file mode 100644 index 0000000..5ff05fd --- /dev/null +++ b/verse/plugins/pubsub.lua @@ -0,0 +1,267 @@ +local verse = require "verse"; +local jid_bare = require "util.jid".bare; + +local t_insert = table.insert; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; + +local pubsub = {}; +local pubsub_mt = { __index = pubsub }; + +function verse.plugins.pubsub(stream) + stream.pubsub = setmetatable({ stream = stream }, pubsub_mt); + stream:hook("message", function (message) + local m_from = message.attr.from; + for pubsub_event in message:childtags("event", xmlns_pubsub_event) do + local items = pubsub_event:get_child("items"); + if items then + local node = items.attr.node; + for item in items:childtags("item") do + stream:event("pubsub/event", { + from = m_from; + node = node; + item = item; + }); + end + end + end + end); + return true; +end + +-- COMPAT +function pubsub:create(server, node, callback) + return self:service(server):node(node):create(nil, callback); +end + +function pubsub:subscribe(server, node, jid, callback) + return self:service(server):node(node):subscribe(jid, nil, callback); +end + +function pubsub:publish(server, node, id, item, callback) + return self:service(server):node(node):publish(id, nil, item, callback); +end + +-------------------------------------------------------------------------- +---------------------New and improved PubSub interface-------------------- +-------------------------------------------------------------------------- + +local pubsub_service = {}; +local pubsub_service_mt = { __index = pubsub_service }; + +-- TODO should the property be named 'jid' instead? +function pubsub:service(service) + return setmetatable({ stream = self.stream, service = service }, pubsub_service_mt) +end + +-- Helper function for iq+pubsub tags + +local function pubsub_iq(iq_type, to, ns, op, node, jid, item_id) + local st = verse.iq{ type = iq_type or "get", to = to } + :tag("pubsub", { xmlns = ns or xmlns_pubsub }) -- ns would be ..#owner + if op then st:tag(op, { node = node, jid = jid }); end + if item_id then st:tag("item", { id = item_id ~= true and item_id or nil }); end + return st; +end + +-- http://xmpp.org/extensions/xep-0060.html#entity-subscriptions +function pubsub_service:subscriptions(callback) + self.stream:send_iq(pubsub_iq(nil, self.service, nil, "subscriptions") + , callback and function (result) + if result.attr.type == "result" then + local ps = result:get_child("pubsub", xmlns_pubsub); + local subs = ps and ps:get_child("subscriptions"); + local nodes = {}; + if subs then + for sub in subs:childtags("subscription") do + local node = self:node(sub.attr.node) + node.subscription = sub; + node.subscribed_jid = sub.attr.jid; + t_insert(nodes, node); + -- FIXME Good enough? + -- Or how about: + -- nodes[node] = sub; + end + end + callback(nodes); + else + callback(false, result:get_error()); + end + end or nil); +end + +-- http://xmpp.org/extensions/xep-0060.html#entity-affiliations +function pubsub_service:affiliations(callback) + self.stream:send_iq(pubsub_iq(nil, self.service, nil, "affiliations") + , callback and function (result) + if result.attr.type == "result" then + local ps = result:get_child("pubsub", xmlns_pubsub); + local affils = ps and ps:get_child("affiliations") or {}; + local nodes = {}; + if affils then + for affil in affils:childtags("affiliation") do + local node = self:node(affil.attr.node) + node.affiliation = affil; + t_insert(nodes, node); + -- nodes[node] = affil; + end + end + callback(nodes); + else + callback(false, result:get_error()); + end + end or nil); +end + +function pubsub_service:nodes(callback) + self.stream:disco_items(self.service, nil, function(items, ...) + if items then + for i=1,#items do + items[i] = self:node(items[i].node); + end + end + callback(items, ...) + end); +end + +local pubsub_node = {}; +local pubsub_node_mt = { __index = pubsub_node }; + +function pubsub_service:node(node) + return setmetatable({ stream = self.stream, service = self.service, node = node }, pubsub_node_mt) +end + +function pubsub_mt:__call(service, node) + local s = self:service(service); + return node and s:node(node) or s; +end + +function pubsub_node:hook(callback, prio) + self._hooks = self._hooks or setmetatable({}, { __mode = 'kv' }); + local function hook(event) + -- FIXME service == nil would mean anyone, + -- publishing would be go to your bare jid. + -- So if you're only interestied in your own + -- events, hook your own bare jid. + if (not event.service or event.from == self.service) and event.node == self.node then + return callback(event) + end + end + self._hooks[callback] = hook; + self.stream:hook("pubsub/event", hook, prio); + return hook; +end + +function pubsub_node:unhook(callback) + if callback then + local hook = self._hooks[callback]; + self.stream:unhook("pubsub/event", hook); + elseif self._hooks then + for hook in pairs(self._hooks) do + self.stream:unhook("pubsub/event", hook); + end + end +end + +function pubsub_node:create(config, callback) + if config ~= nil then + error("Not implemented yet."); + else + self.stream:send_iq(pubsub_iq("set", self.service, nil, "create", self.node), callback); + end +end + +-- and rolled into one +function pubsub_node:configure(config, callback) + if config ~= nil then + error("Not implemented yet."); + --[[ + if config == true then + self.stream:send_iq(pubsub_iq("get", self.service, nil, "configure", self.node) + , function(reply) + local form = reply:get_child("pubsub"):get_child("configure"):get_cild("x"); + local config = callback(require"util.dataforms".something(form)) + self.stream:send_iq(pubsub_iq("set", config, ...)) + end); + end + --]] + -- fetch form and pass it to the callback + -- which would process it and pass it back + -- and then we submit it + -- elseif type(config) == "table" then + -- it's a form or stanza that we submit + -- end + -- this would be done for everything that needs a config + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, config == nil and "default" or "configure", self.node), callback); +end + +function pubsub_node:publish(id, options, node, callback) + if options ~= nil then + error("Node configuration is not implemented yet."); + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, "publish", self.node, nil, id or true) + :add_child(node) + , callback); +end + +function pubsub_node:subscribe(jid, options, callback) + jid = jid or self.stream.jid; + if options ~= nil then + error("Subscription configuration is not implemented yet."); + end + self.stream:send_iq(pubsub_iq("set", self.service, nil, "subscribe", self.node, jid, id) + , callback); +end + +function pubsub_node:subscription(callback) + error("Not implemented yet."); +end + +function pubsub_node:affiliation(callback) + error("Not implemented yet."); +end + +function pubsub_node:unsubscribe(jid, callback) + jid = jid or self.subscribed_jid or self.stream.jid; + self.stream:send_iq(pubsub_iq("set", self.service, nil, "unsubscribe", self.node, jid) + , callback); +end + +function pubsub_node:configure_subscription(options, callback) + error("Not implemented yet."); +end + +function pubsub_node:items(full, callback) + if full then + self.stream:send_iq(pubsub_iq("get", self.service, nil, "items", self.node) + , callback); + else + self.stream:disco_items(self.service, self.node, callback); + end +end + +function pubsub_node:item(id, callback) + self.stream:send_iq(pubsub_iq("get", self.service, nil, "items", self.node, nil, id) + , callback); +end + +function pubsub_node:retract(id, callback) + self.stream:send_iq(pubsub_iq("set", self.service, nil, "retract", self.node, nil, id) + , callback); +end + +function pubsub_node:purge(notify, callback) + assert(not notify, "Not implemented yet."); + self.stream:send_iq(pubsub_iq("set", self.service, xmlns_pubsub_owner, "purge", self.node) + , callback); +end + +function pubsub_node:delete(redirect_uri, callback) + assert(not redirect_uri, "Not implemented yet."); + self.stream:send_iq(pubsub_iq("set", self.service, xmlns_pubsub_owner, "delete", self.node) + , callback); +end diff --git a/verse/plugins/receipts.lua b/verse/plugins/receipts.lua new file mode 100644 index 0000000..3d049fc --- /dev/null +++ b/verse/plugins/receipts.lua @@ -0,0 +1,16 @@ +local verse = require"verse"; +local xmlns_receipts = "urn:xmpp:receipts"; + +function verse.plugins.receipts(stream) + stream:add_plugin("disco"); + local function send_receipt(stanza) + if stanza:get_child("request", xmlns_receipts) then + stream:send(verse.reply(stanza) + :tag("received", { xmlns = xmlns_receipts, id = stanza.attr.id })); + end + end + + stream:add_disco_feature(xmlns_receipts); + stream:hook("message", send_receipt, 1000); +end + diff --git a/verse/plugins/register.lua b/verse/plugins/register.lua new file mode 100644 index 0000000..c4e9e6d --- /dev/null +++ b/verse/plugins/register.lua @@ -0,0 +1,32 @@ +local verse = require "verse"; + +local xmlns_register = "jabber:iq:register"; + +function verse.plugins.register(stream) + local function handle_features(features_stanza) + if features_stanza:get_child("register", "http://jabber.org/features/iq-register") then + local request = verse.iq({ to = stream.host_, type = "set" }) + :tag("query", { xmlns = xmlns_register }) + :tag("username"):text(stream.username):up() + :tag("password"):text(stream.password):up(); + if stream.register_email then + request:tag("email"):text(stream.register_email):up(); + end + stream:send_iq(request, function (result) + if result.attr.type == "result" then + stream:event("registration-success"); + else + local type, condition, text = result:get_error(); + stream:debug("Registration failed: %s", condition); + stream:event("registration-failure", { type = type, condition = condition, text = text }); + end + end); + else + stream:debug("In-band registration not offered by server"); + stream:event("registration-failure", { condition = "service-unavailable" }); + end + stream:unhook("stream-features", handle_features); + return true; + end + stream:hook("stream-features", handle_features, 310); +end diff --git a/verse/plugins/roster.lua b/verse/plugins/roster.lua new file mode 100644 index 0000000..1f7bb53 --- /dev/null +++ b/verse/plugins/roster.lua @@ -0,0 +1,161 @@ +local verse = require "verse"; +local bare_jid = require "util.jid".bare; + +local xmlns_roster = "jabber:iq:roster"; +local xmlns_rosterver = "urn:xmpp:features:rosterver"; +local t_insert = table.insert; + +function verse.plugins.roster(stream) + local ver_supported = false; + local roster = { + items = {}; + ver = ""; + -- TODO: + -- groups = {}; + }; + stream.roster = roster; + + stream:hook("stream-features", function(features_stanza) + if features_stanza:get_child("ver", xmlns_rosterver) then + ver_supported = true; + end + end); + + local function item_lua2xml(item_table) + local xml_item = verse.stanza("item", { xmlns = xmlns_roster }); + for k, v in pairs(item_table) do + if k ~= "groups" then + xml_item.attr[k] = v; + else + for i = 1,#v do + xml_item:tag("group"):text(v[i]):up(); + end + end + end + return xml_item; + end + + local function item_xml2lua(xml_item) + local item_table = { }; + local groups = {}; + item_table.groups = groups; + local jid = xml_item.attr.jid; + + for k, v in pairs(xml_item.attr) do + if k ~= "xmlns" then + item_table[k] = v + end + end + + for group in xml_item:childtags("group") do + t_insert(groups, group:get_text()) + end + return item_table; + end + + function roster:load(r) + roster.ver, roster.items = r.ver, r.items; + end + + function roster:dump() + return { + ver = roster.ver, + items = roster.items, + }; + end + + -- should this be add_contact(item, callback) instead? + function roster:add_contact(jid, name, groups, callback) + local item = { jid = jid, name = name, groups = groups }; + local stanza = verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_roster }) + :add_child(item_lua2xml(item)); + stream:send_iq(stanza, function (reply) + if not callback then return end + if reply.attr.type == "result" then + callback(true); + else + local type, condition, text = reply:get_error(); + callback(nil, { type, condition, text }); + end + end); + end + -- What about subscriptions? + + function roster:delete_contact(jid, callback) + jid = (type(jid) == "table" and jid.jid) or jid; + local item = { jid = jid, subscription = "remove" } + if not roster.items[jid] then return false, "item-not-found"; end + stream:send_iq(verse.iq({ type = "set" }) + :tag("query", { xmlns = xmlns_roster }) + :add_child(item_lua2xml(item)), + function (reply) + if not callback then return end + if reply.attr.type == "result" then + callback(true); + else + local type, condition, text = reply:get_error(); + callback(nil, { type, condition, text }); + end + end); + end + + local function add_item(item) -- Takes one roster + local roster_item = item_xml2lua(item); + roster.items[roster_item.jid] = roster_item; + end + + -- Private low level + local function delete_item(jid) + local deleted_item = roster.items[jid]; + roster.items[jid] = nil; + return deleted_item; + end + + function roster:fetch(callback) + stream:send_iq(verse.iq({type="get"}):tag("query", { xmlns = xmlns_roster, ver = ver_supported and roster.ver or nil }), + function (result) + if result.attr.type == "result" then + local query = result:get_child("query", xmlns_roster); + if query then + roster.items = {}; + for item in query:childtags("item") do + add_item(item) + end + roster.ver = query.attr.ver or ""; + end + callback(roster); + else + local type, condition, text = stanza:get_error(); + callback(nil, { type, condition, text }); --FIXME + end + end); + end + + stream:hook("iq/"..xmlns_roster, function(stanza) + local type, from = stanza.attr.type, stanza.attr.from; + if type == "set" and (not from or from == bare_jid(stream.jid)) then + local query = stanza:get_child("query", xmlns_roster); + local item = query and query:get_child("item"); + if item then + local event, target; + local jid = item.attr.jid; + if item.attr.subscription == "remove" then + event = "removed" + target = delete_item(jid); + else + event = roster.items[jid] and "changed" or "added"; + add_item(item) + target = roster.items[jid]; + end + roster.ver = query.attr.ver; + if target then + stream:event("roster/item-"..event, target); + end + -- TODO else return error? Events? + end + stream:send(verse.reply(stanza)) + return true; + end + end); +end diff --git a/verse/plugins/sasl.lua b/verse/plugins/sasl.lua new file mode 100644 index 0000000..eef8131 --- /dev/null +++ b/verse/plugins/sasl.lua @@ -0,0 +1,80 @@ +-- local verse = require"verse"; +local base64, unbase64 = require "mime".b64, require"mime".unb64; +local xmlns_sasl = "urn:ietf:params:xml:ns:xmpp-sasl"; + +function verse.plugins.sasl(stream) + local function handle_features(features_stanza) + if stream.authenticated then return; end + stream:debug("Authenticating with SASL..."); + local sasl_mechanisms = features_stanza:get_child("mechanisms", xmlns_sasl); + if not sasl_mechanisms then return end + + local mechanisms = {}; + local preference = {}; + + for mech in sasl_mechanisms:childtags("mechanism") do + mech = mech:get_text(); + stream:debug("Server offers %s", mech); + if not mechanisms[mech] then + local name = mech:match("[^-]+"); + local ok, impl = pcall(require, "util.sasl."..name:lower()); + if ok then + stream:debug("Loaded SASL %s module", name); + mechanisms[mech], preference[mech] = impl(stream, mech); + elseif not tostring(impl):match("not found") then + stream:debug("Loading failed: %s", tostring(impl)); + end + end + end + + local supported = {}; -- by the server + for mech in pairs(mechanisms) do + table.insert(supported, mech); + end + if not supported[1] then + stream:event("authentication-failure", { condition = "no-supported-sasl-mechanisms" }); + stream:close(); + return; + end + table.sort(supported, function (a, b) return preference[a] > preference[b]; end); + local mechanism, initial_data = supported[1]; + stream:debug("Selecting %s mechanism...", mechanism); + stream.sasl_mechanism = coroutine.wrap(mechanisms[mechanism]); + initial_data = stream:sasl_mechanism(mechanism); + local auth_stanza = verse.stanza("auth", { xmlns = xmlns_sasl, mechanism = mechanism }); + if initial_data then + auth_stanza:text(base64(initial_data)); + end + stream:send(auth_stanza); + return true; + end + + local function handle_sasl(sasl_stanza) + if sasl_stanza.name == "failure" then + local err = sasl_stanza.tags[1]; + local text = sasl_stanza:get_child_text("text"); + stream:event("authentication-failure", { condition = err.name, text = text }); + stream:close(); + return false; + end + local ok, err = stream.sasl_mechanism(sasl_stanza.name, unbase64(sasl_stanza:get_text())); + if not ok then + stream:event("authentication-failure", { condition = err }); + stream:close(); + return false; + elseif ok == true then + stream:event("authentication-success"); + stream.authenticated = true + stream:reopen(); + else + stream:send(verse.stanza("response", { xmlns = xmlns_sasl }):text(base64(ok))); + end + return true; + end + + stream:hook("stream-features", handle_features, 300); + stream:hook("stream/"..xmlns_sasl, handle_sasl); + + return true; +end + diff --git a/verse/plugins/session.lua b/verse/plugins/session.lua new file mode 100644 index 0000000..0520610 --- /dev/null +++ b/verse/plugins/session.lua @@ -0,0 +1,30 @@ +local verse = require "verse"; + +local xmlns_session = "urn:ietf:params:xml:ns:xmpp-session"; + +function verse.plugins.session(stream) + + local function handle_features(features) + local session_feature = features:get_child("session", xmlns_session); + if session_feature and not session_feature:get_child("optional") then + local function handle_binding(jid) + stream:debug("Establishing Session..."); + stream:send_iq(verse.iq({ type = "set" }):tag("session", {xmlns=xmlns_session}), + function (reply) + if reply.attr.type == "result" then + stream:event("session-success"); + elseif reply.attr.type == "error" then + local err = reply:child_with_name("error"); + local type, condition, text = reply:get_error(); + stream:event("session-failure", { error = condition, text = text, type = type }); + end + end); + return true; + end + stream:hook("bind-success", handle_binding); + end + end + stream:hook("stream-features", handle_features); + + return true; +end diff --git a/verse/plugins/smacks.lua b/verse/plugins/smacks.lua new file mode 100644 index 0000000..81eaef7 --- /dev/null +++ b/verse/plugins/smacks.lua @@ -0,0 +1,144 @@ +local verse = require "verse"; +local now = socket.gettime; + +local xmlns_sm = "urn:xmpp:sm:2"; + +function verse.plugins.smacks(stream) + -- State for outgoing stanzas + local outgoing_queue = {}; + local last_ack = 0; + local last_stanza_time = now(); + local timer_active; + + -- State for incoming stanzas + local handled_stanza_count = 0; + + -- Catch incoming stanzas + local function incoming_stanza(stanza) + if stanza.attr.xmlns == "jabber:client" or not stanza.attr.xmlns then + handled_stanza_count = handled_stanza_count + 1; + stream:debug("Increasing handled stanzas to %d for %s", handled_stanza_count, stanza:top_tag()); + end + end + + -- Catch outgoing stanzas + function outgoing_stanza(stanza) + -- NOTE: This will not behave nice if stanzas are serialized before this point + if stanza.name and not stanza.attr.xmlns then + -- serialize stanzas in order to bypass this on resumption + outgoing_queue[#outgoing_queue+1] = tostring(stanza); + last_stanza_time = now(); + if not timer_active then + timer_active = true; + stream:debug("Waiting to send ack request..."); + verse.add_task(1, function() + if #outgoing_queue == 0 then + timer_active = false; + return; + end + local time_since_last_stanza = now() - last_stanza_time; + if time_since_last_stanza < 1 and #outgoing_queue < 10 then + return 1 - time_since_last_stanza; + end + stream:debug("Time up, sending ..."); + timer_active = false; + stream:send(verse.stanza("r", { xmlns = xmlns_sm })); + end); + end + end + end + + local function on_disconnect() + stream:debug("smacks: connection lost"); + stream.stream_management_supported = nil; + if stream.resumption_token then + stream:debug("smacks: have resumption token, reconnecting in 1s..."); + stream.authenticated = nil; + verse.add_task(1, function () + stream:connect(stream.connect_host or stream.host, stream.connect_port or 5222); + end); + return true; + end + end + + -- Graceful shutdown + local function on_close() + stream.resumption_token = nil; + stream:unhook("disconnected", on_disconnect); + end + + local function handle_sm_command(stanza) + if stanza.name == "r" then -- Request for acks for stanzas we received + stream:debug("Ack requested... acking %d handled stanzas", handled_stanza_count); + stream:send(verse.stanza("a", { xmlns = xmlns_sm, h = tostring(handled_stanza_count) })); + elseif stanza.name == "a" then -- Ack for stanzas we sent + local new_ack = tonumber(stanza.attr.h); + if new_ack > last_ack then + local old_unacked = #outgoing_queue; + for i=last_ack+1,new_ack do + table.remove(outgoing_queue, 1); + end + stream:debug("Received ack: New ack: "..new_ack.." Last ack: "..last_ack.." Unacked stanzas now: "..#outgoing_queue.." (was "..old_unacked..")"); + last_ack = new_ack; + else + stream:warn("Received bad ack for "..new_ack.." when last ack was "..last_ack); + end + elseif stanza.name == "enabled" then + + if stanza.attr.id then + stream.resumption_token = stanza.attr.id; + stream:hook("closed", on_close, 100); + stream:hook("disconnected", on_disconnect, 100); + end + elseif stanza.name == "resumed" then + local new_ack = tonumber(stanza.attr.h); + if new_ack > last_ack then + local old_unacked = #outgoing_queue; + for i=last_ack+1,new_ack do + table.remove(outgoing_queue, 1); + end + stream:debug("Received ack: New ack: "..new_ack.." Last ack: "..last_ack.." Unacked stanzas now: "..#outgoing_queue.." (was "..old_unacked..")"); + last_ack = new_ack; + end + for i=1,#outgoing_queue do + stream:send(outgoing_queue[i]); + end + outgoing_queue = {}; + stream:debug("Resumed successfully"); + stream:event("resumed"); + else + stream:warn("Don't know how to handle "..xmlns_sm.."/"..stanza.name); + end + end + + local function on_bind_success() + if not stream.smacks then + --stream:unhook("bind-success", on_bind_success); + stream:debug("smacks: sending enable"); + stream:send(verse.stanza("enable", { xmlns = xmlns_sm, resume = "true" })); + stream.smacks = true; + + -- Catch stanzas + stream:hook("stanza", incoming_stanza); + stream:hook("outgoing", outgoing_stanza); + end + end + + local function on_features(features) + if features:get_child("sm", xmlns_sm) then + stream.stream_management_supported = true; + if stream.smacks and stream.bound then -- Already enabled in a previous session - resume + stream:debug("Resuming stream with %d handled stanzas", handled_stanza_count); + stream:send(verse.stanza("resume", { xmlns = xmlns_sm, + h = handled_stanza_count, previd = stream.resumption_token })); + return true; + else + stream:hook("bind-success", on_bind_success, 1); + end + end + end + + stream:hook("stream-features", on_features, 250); + stream:hook("stream/"..xmlns_sm, handle_sm_command); + --stream:hook("ready", on_stream_ready, 500); +end diff --git a/verse/plugins/tls.lua b/verse/plugins/tls.lua new file mode 100644 index 0000000..92abd45 --- /dev/null +++ b/verse/plugins/tls.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; + +local xmlns_tls = "urn:ietf:params:xml:ns:xmpp-tls"; + +function verse.plugins.tls(stream) + local function handle_features(features_stanza) + if stream.authenticated then return; end + if features_stanza:get_child("starttls", xmlns_tls) and stream.conn.starttls then + stream:debug("Negotiating TLS..."); + stream:send(verse.stanza("starttls", { xmlns = xmlns_tls })); + return true; + elseif not stream.conn.starttls and not stream.secure then + stream:warn("SSL libary (LuaSec) not loaded, so TLS not available"); + elseif not stream.secure then + stream:debug("Server doesn't offer TLS :("); + end + end + local function handle_tls(tls_status) + if tls_status.name == "proceed" then + stream:debug("Server says proceed, handshake starting..."); + stream.conn:starttls({mode="client", protocol="sslv23", options="no_sslv2"}, true); + end + end + local function handle_status(new_status) + if new_status == "ssl-handshake-complete" then + stream.secure = true; + stream:debug("Re-opening stream..."); + stream:reopen(); + end + end + stream:hook("stream-features", handle_features, 400); + stream:hook("stream/"..xmlns_tls, handle_tls); + stream:hook("status", handle_status, 400); + + return true; +end diff --git a/verse/plugins/uptime.lua b/verse/plugins/uptime.lua new file mode 100644 index 0000000..326af27 --- /dev/null +++ b/verse/plugins/uptime.lua @@ -0,0 +1,42 @@ +local verse = require "verse"; + +local xmlns_last = "jabber:iq:last"; + +local function set_uptime(self, uptime_info) + self.starttime = uptime_info.starttime; +end + +function verse.plugins.uptime(stream) + stream.uptime = { set = set_uptime }; + stream:hook("iq/"..xmlns_last, function (stanza) + if stanza.attr.type ~= "get" then return; end + local reply = verse.reply(stanza) + :tag("query", { seconds = tostring(os.difftime(os.time(), stream.uptime.starttime)), xmlns = xmlns_last }); + stream:send(reply); + return true; + end); + + function stream:query_uptime(target_jid, callback) + callback = callback or function (uptime) return stream:event("uptime/response", uptime); end + stream:send_iq(verse.iq({ type = "get", to = target_jid }) + :tag("query", { xmlns = xmlns_last }), + function (reply) + local query = reply:get_child("query", xmlns_last); + if reply.attr.type == "result" then + local seconds = tonumber(query.attr.seconds); + callback({ + seconds = seconds or nil; + }); + else + local type, condition, text = reply:get_error(); + callback({ + error = true; + condition = condition; + text = text; + type = type; + }); + end + end); + end + return true; +end diff --git a/verse/plugins/vcard.lua b/verse/plugins/vcard.lua new file mode 100644 index 0000000..50f644c --- /dev/null +++ b/verse/plugins/vcard.lua @@ -0,0 +1,36 @@ +local verse = require "verse"; +local vcard = require "util.vcard"; + +local xmlns_vcard = "vcard-temp"; + +function verse.plugins.vcard(stream) + function stream:get_vcard(jid, callback) --jid = nil for self + stream:send_iq(verse.iq({to = jid, type="get"}) + :tag("vCard", {xmlns=xmlns_vcard}), callback and function(stanza) + local lCard, xCard; + vCard = stanza:get_child("vCard", xmlns_vcard); + if stanza.attr.type == "result" and vCard then + vCard = vcard.from_xep54(vCard) + callback(vCard) + else + callback(false) -- FIXME add error + end + end or nil); + end + + function stream:set_vcard(aCard, callback) + local xCard; + if type(aCard) == "table" and aCard.name then + xCard = aCard; + elseif type(aCard) == "string" then + xCard = vcard.to_xep54(vcard.from_text(aCard)[1]); + elseif type(aCard) == "table" then + xCard = vcard.to_xep54(aCard); + error("Converting a table to vCard not implemented") + end + if not xCard then return false end + stream:debug("setting vcard to %s", tostring(xCard)); + stream:send_iq(verse.iq({type="set"}) + :add_child(xCard), callback); + end +end diff --git a/verse/plugins/vcard_update.lua b/verse/plugins/vcard_update.lua new file mode 100644 index 0000000..87f6cc5 --- /dev/null +++ b/verse/plugins/vcard_update.lua @@ -0,0 +1,103 @@ +local verse = require "verse"; + +local xmlns_vcard, xmlns_vcard_update = "vcard-temp", "vcard-temp:x:update"; + +-- MMMmmmm.. hacky +local ok, fun = pcall(function() return require("util.hashes").sha1; end); +if not ok then + ok, fun = pcall(function() return require("util.sha1").sha1; end); + if not ok then + error("Could not find a sha1()") + end +end +local sha1 = fun; + +local ok, fun = pcall(function() + local unb64 = require("util.encodings").base64.decode; + assert(unb64("SGVsbG8=") == "Hello") + return unb64; +end); +if not ok then + ok, fun = pcall(function() return require("mime").unb64; end); + if not ok then + error("Could not find a base64 decoder") + end +end +local unb64 = fun; + +function verse.plugins.vcard_update(stream) + stream:add_plugin("vcard"); + stream:add_plugin("presence"); + + + local x_vcard_update; + + function update_vcard_photo(vCard) + local data; + for i=1,#vCard do + if vCard[i].name == "PHOTO" then + data = vCard[i][1]; + break + end + end + if data then + local hash = sha1(unb64(data), true); + x_vcard_update = verse.stanza("x", { xmlns = xmlns_vcard_update }) + :tag("photo"):text(hash); + + stream:resend_presence() + else + x_vcard_update = nil; + end + end + + local _set_vcard = stream.set_vcard; + + --[[ TODO Complete this, it's probably broken. + -- Maybe better to hook outgoing stanza? + function stream:set_vcard(vCard, callback) + _set_vcard(vCard, function(event, ...) + if event.attr.type == "result" then + local vCard_ = response:get_child("vCard", xmlns_vcard); + if vCard_ then + update_vcard_photo(vCard_); + end -- Or fetch it again? Seems wasteful, but if the server overrides stuff? :/ + end + if callback then + return callback(event, ...); + end + end); + end + --]] + + local initial_vcard_fetch_started; + stream:hook("ready", function(event) + if initial_vcard_fetch_started then return; end + initial_vcard_fetch_started = true; + -- if stream:jid_supports(nil, xmlns_vcard) then TODO this, correctly + stream:get_vcard(nil, function(response) + if response then + update_vcard_photo(response) + end + stream:event("ready"); + end); + return true; + end, 3); + + stream:hook("presence-out", function(presence) + if x_vcard_update and not presence:get_child("x", xmlns_vcard_update) then + presence:add_child(x_vcard_update); + end + end, 10); + + --[[ + stream:hook("presence", function(presence) + local x_vcard_update = presence:get_child("x", xmlns_vcard_update); + local photo_hash = x_vcard_update and x_vcard_update:get_child("photo"); + :get_child_text("photo"); + if x_vcard_update then + -- TODO Cache peoples avatars here + end + end); + --]] +end diff --git a/verse/plugins/version.lua b/verse/plugins/version.lua new file mode 100644 index 0000000..822fd59 --- /dev/null +++ b/verse/plugins/version.lua @@ -0,0 +1,57 @@ +local verse = require "verse"; + +local xmlns_version = "jabber:iq:version"; + +local function set_version(self, version_info) + self.name = version_info.name; + self.version = version_info.version; + self.platform = version_info.platform; +end + +function verse.plugins.version(stream) + stream.version = { set = set_version }; + stream:hook("iq/"..xmlns_version, function (stanza) + if stanza.attr.type ~= "get" then return; end + local reply = verse.reply(stanza) + :tag("query", { xmlns = xmlns_version }); + if stream.version.name then + reply:tag("name"):text(tostring(stream.version.name)):up(); + end + if stream.version.version then + reply:tag("version"):text(tostring(stream.version.version)):up() + end + if stream.version.platform then + reply:tag("os"):text(stream.version.platform); + end + stream:send(reply); + return true; + end); + + function stream:query_version(target_jid, callback) + callback = callback or function (version) return stream:event("version/response", version); end + stream:send_iq(verse.iq({ type = "get", to = target_jid }) + :tag("query", { xmlns = xmlns_version }), + function (reply) + if reply.attr.type == "result" then + local query = reply:get_child("query", xmlns_version); + local name = query and query:get_child_text("name"); + local version = query and query:get_child_text("version"); + local os = query and query:get_child_text("os"); + callback({ + name = name; + version = version; + platform = os; + }); + else + local type, condition, text = reply:get_error(); + callback({ + error = true; + condition = condition; + text = text; + type = type; + }); + end + end); + end + return true; +end diff --git a/verse/squishy b/verse/squishy new file mode 100644 index 0000000..5f849bb --- /dev/null +++ b/verse/squishy @@ -0,0 +1,84 @@ +Output "verse.lua" + +-- Verse-specific versions of libraries +Module "util.encodings" "libs/encodings.lua" +Module "util.hashes" "libs/hashes.lua" +Module "util.sha1" "util/sha1.lua" +Module "lib.adhoc" "libs/adhoc.lib.lua" + +AutoFetchURL("http://hg.prosody.im/prosody-modules/raw-file/tip/mod_mam/?"); +Module "util.rsm" "rsm.lib.lua" +-- Prosody libraries +if not GetOption("prosody") then + AutoFetchURL "http://hg.prosody.im/0.9/raw-file/tip/?" +else + AutoFetchURL(GetOption("prosody").."/?") +end + +Module "util.stanza" "util/stanza.lua" +Module "util.timer" "util/timer.lua" +Module "util.termcolours" "util/termcolours.lua" +Module "util.uuid" "util/uuid.lua" +Module "net.dns" "net/dns.lua" +Module "net.adns" "net/adns.lua" +Module "net.server" "net/server_select.lua" +Module "util.xmppstream" "util/xmppstream.lua" +Module "util.jid" "util/jid.lua" +Module "util.events" "util/events.lua" +Module "util.dataforms" "util/dataforms.lua" +Module "util.caps" "util/caps.lua" +Module "util.vcard" "util/vcard.lua" +Module "util.logger" "util/logger.lua" +Module "util.datetime" "util/datetime.lua" + +Module "util.sasl.scram" "util/sasl/scram.lua" +Module "util.sasl.plain" "util/sasl/plain.lua" +Module "util.sasl.anonymous" "util/sasl/anonymous.lua" + +-- Verse plugins +plugins = { + -- Login + "tls", "sasl", "bind", "session", "legacy", "compression"; + -- Reliability + "smacks", "keepalive"; + -- Queries + "disco", "version", "ping", "uptime"; + -- Privacy control + "blocking"; + -- Jingle / file transfer + "jingle", "jingle_ft", "jingle_s5b", "proxy65", "jingle_ibb"; + -- Pubsub + "pubsub", "pep"; + -- Command and control + "adhoc"; + -- Basics + "presence", "private", "roster", "register"; + -- MUC + "groupchat"; + -- vCard + "vcard", "vcard_update"; + -- Carbons + "carbons"; + + "archive"; +} + +for _, plugin in ipairs(plugins) do + Module("verse.plugins."..plugin)("plugins/"..plugin..".lua") +end + +Module "util.http" "util/http.lua" +Module "net.http.parser" "net/http/parser.lua" +Module "net.http" "net/http.lua" + +Module "verse.bosh" "bosh.lua" + +if GetOption "internal-bit-module" then + Module "bit" "libs/bit.lua" +end + +Module "verse.client" "client.lua" +Module "verse.component" "component.lua" + +-- Main verse file +Main "init.lua" diff --git a/verse/util/dataforms.lua b/verse/util/dataforms.lua new file mode 100644 index 0000000..b2540f4 --- /dev/null +++ b/verse/util/dataforms.lua @@ -0,0 +1,282 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +local setmetatable = setmetatable; +local pairs, ipairs = pairs, ipairs; +local tostring, type, next = tostring, type, next; +local t_concat = table.concat; +local st = require "util.stanza"; +local jid_prep = require "util.jid".prep; + +module "dataforms" + +local xmlns_forms = 'jabber:x:data'; + +local form_t = {}; +local form_mt = { __index = form_t }; + +function new(layout) + return setmetatable(layout, form_mt); +end + +function from_stanza(stanza) + local layout = { + title = stanza:get_child_text("title"); + instructions = stanza:get_child_text("instructions"); + }; + for tag in stanza:childtags("field") do + local field = { + name = tag.attr.var; + label = tag.attr.label; + type = tag.attr.type; + required = tag:get_child("required") and true or nil; + value = tag:get_child_text("value"); + }; + layout[#layout+1] = field; + if field.type then + local value = {}; + if field.type:match"list%-" then + for tag in tag:childtags("option") do + value[#value+1] = { label = tag.attr.label, value = tag:get_child_text("value") }; + end + for tag in tag:childtags("value") do + value[#value+1] = { label = tag.attr.label, value = tag:get_text(), default = true }; + end + elseif field.type:match"%-multi" then + for tag in tag:childtags("value") do + value[#value+1] = tag.attr.label and { label = tag.attr.label, value = tag:get_text() } or tag:get_text(); + end + if field.type == "text-multi" then + field.value = t_concat(value, "\n"); + else + field.value = value; + end + end + end + end + return new(layout); +end + +function form_t.form(layout, data, formtype) + local form = st.stanza("x", { xmlns = xmlns_forms, type = formtype or "form" }); + if layout.title then + form:tag("title"):text(layout.title):up(); + end + if layout.instructions then + form:tag("instructions"):text(layout.instructions):up(); + end + for n, field in ipairs(layout) do + local field_type = field.type or "text-single"; + -- Add field tag + form:tag("field", { type = field_type, var = field.name, label = field.label }); + + local value = (data and data[field.name]) or field.value; + + if value then + -- Add value, depending on type + if field_type == "hidden" then + if type(value) == "table" then + -- Assume an XML snippet + form:tag("value") + :add_child(value) + :up(); + else + form:tag("value"):text(tostring(value)):up(); + end + elseif field_type == "boolean" then + form:tag("value"):text((value and "1") or "0"):up(); + elseif field_type == "fixed" then + + elseif field_type == "jid-multi" then + for _, jid in ipairs(value) do + form:tag("value"):text(jid):up(); + end + elseif field_type == "jid-single" then + form:tag("value"):text(value):up(); + elseif field_type == "text-single" or field_type == "text-private" then + form:tag("value"):text(value):up(); + elseif field_type == "text-multi" then + -- Split into multiple tags, one for each line + for line in value:gmatch("([^\r\n]+)\r?\n*") do + form:tag("value"):text(line):up(); + end + elseif field_type == "list-single" then + local has_default = false; + for _, val in ipairs(value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if val.default and (not has_default) then + form:tag("value"):text(val.value):up(); + has_default = true; + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end + elseif field_type == "list-multi" then + for _, val in ipairs(value) do + if type(val) == "table" then + form:tag("option", { label = val.label }):tag("value"):text(val.value):up():up(); + if val.default then + form:tag("value"):text(val.value):up(); + end + else + form:tag("option", { label= val }):tag("value"):text(tostring(val)):up():up(); + end + end + end + end + + if field.required then + form:tag("required"):up(); + end + + -- Jump back up to list of fields + form:up(); + end + return form; +end + +local field_readers = {}; + +function form_t.data(layout, stanza) + local data = {}; + local errors = {}; + + for _, field in ipairs(layout) do + local tag; + for field_tag in stanza:childtags() do + if field.name == field_tag.attr.var then + tag = field_tag; + break; + end + end + + if not tag then + if field.required then + errors[field.name] = "Required value missing"; + end + else + local reader = field_readers[field.type]; + if reader then + data[field.name], errors[field.name] = reader(tag, field.required); + end + end + end + if next(errors) then + return data, errors; + end + return data; +end + +field_readers["text-single"] = + function (field_tag, required) + local data = field_tag:get_child_text("value"); + if data and #data > 0 then + return data + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["text-private"] = + field_readers["text-single"]; + +field_readers["jid-single"] = + function (field_tag, required) + local raw_data = field_tag:get_child_text("value") + local data = jid_prep(raw_data); + if data and #data > 0 then + return data + elseif raw_data then + return nil, "Invalid JID: " .. raw_data; + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["jid-multi"] = + function (field_tag, required) + local result = {}; + local err = {}; + for value_tag in field_tag:childtags("value") do + local raw_value = value_tag:get_text(); + local value = jid_prep(raw_value); + result[#result+1] = value; + if raw_value and not value then + err[#err+1] = ("Invalid JID: " .. raw_value); + end + end + if #result > 0 then + return result, (#err > 0 and t_concat(err, "\n") or nil); + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["list-multi"] = + function (field_tag, required) + local result = {}; + for value in field_tag:childtags("value") do + result[#result+1] = value:get_text(); + end + return result, (required and #result == 0 and "Required value missing" or nil); + end + +field_readers["text-multi"] = + function (field_tag, required) + local data, err = field_readers["list-multi"](field_tag, required); + if data then + data = t_concat(data, "\n"); + end + return data, err; + end + +field_readers["list-single"] = + field_readers["text-single"]; + +local boolean_values = { + ["1"] = true, ["true"] = true, + ["0"] = false, ["false"] = false, +}; + +field_readers["boolean"] = + function (field_tag, required) + local raw_value = field_tag:get_child_text("value"); + local value = boolean_values[raw_value ~= nil and raw_value]; + if value ~= nil then + return value; + elseif raw_value then + return nil, "Invalid boolean representation"; + elseif required then + return nil, "Required value missing"; + end + end + +field_readers["hidden"] = + function (field_tag) + return field_tag:get_child_text("value"); + end + +return _M; + + +--[=[ + +Layout: +{ + + title = "MUC Configuration", + instructions = [[Use this form to configure options for this MUC room.]], + + { name = "FORM_TYPE", type = "hidden", required = true }; + { name = "field-name", type = "field-type", required = false }; +} + + +--]=] diff --git a/verse/util/sasl/anonymous.lua b/verse/util/sasl/anonymous.lua new file mode 100644 index 0000000..8eca054 --- /dev/null +++ b/verse/util/sasl/anonymous.lua @@ -0,0 +1,8 @@ + +return function (stream, name) + if name == "ANONYMOUS" then + return function () + return coroutine.yield() == "success"; + end, 0; + end +end diff --git a/verse/util/sasl/plain.lua b/verse/util/sasl/plain.lua new file mode 100644 index 0000000..c5aa8ab --- /dev/null +++ b/verse/util/sasl/plain.lua @@ -0,0 +1,9 @@ + +return function (stream, name) + if name == "PLAIN" and stream.username and stream.password then + return function (stream) + return "success" == coroutine.yield("\0"..stream.username.."\0"..stream.password); + end, 5; + end +end + diff --git a/verse/util/sasl/scram.lua b/verse/util/sasl/scram.lua new file mode 100644 index 0000000..ffbe10b --- /dev/null +++ b/verse/util/sasl/scram.lua @@ -0,0 +1,109 @@ + +local base64, unbase64 = require "mime".b64, require"mime".unb64; +local crypto = require"crypto"; +local bit = require"bit"; + +local tonumber = tonumber; +local char, byte = string.char, string.byte; +local gsub = string.gsub; +local xor = bit.bxor; + +local function XOR(a, b) + return (gsub(a, "()(.)", function(i, c) + return char(xor(byte(c), byte(b, i))) + end)); +end + +local function H(str) + return crypto.digest("sha1", str, true); +end + +local _hmac_digest = crypto.hmac.digest; +local function HMAC(key, str) + return _hmac_digest("sha1", str, key, true); +end + +local function Hi(str, salt, i) + local U = HMAC(str, salt .. "\0\0\0\1"); + local ret = U; + for _ = 2, i do + U = HMAC(str, U); + ret = XOR(ret, U); + end + return ret; +end + +local function Normalize(str) + return str; -- TODO +end + +local function value_safe(str) + return (gsub(str, "[,=]", { [","] = "=2C", ["="] = "=3D" })); +end + +local function scram(stream, name) + local username = "n=" .. value_safe(stream.username); + local c_nonce = base64(crypto.rand.bytes(15)); + local our_nonce = "r=" .. c_nonce; + local client_first_message_bare = username .. "," .. our_nonce; + local cbind_data = ""; + local gs2_cbind_flag = stream.conn:ssl() and "y" or "n"; + if name == "SCRAM-SHA-1-PLUS" then + cbind_data = stream.conn:socket():getfinished(); + gs2_cbind_flag = "p=tls-unique"; + end + local gs2_header = gs2_cbind_flag .. ",,"; + local client_first_message = gs2_header .. client_first_message_bare; + local cont, server_first_message = coroutine.yield(client_first_message); + if cont ~= "challenge" then return false end + + local nonce, salt, iteration_count = server_first_message:match("(r=[^,]+),s=([^,]*),i=(%d+)"); + local i = tonumber(iteration_count); + salt = unbase64(salt); + if not nonce or not salt or not i then + return false, "Could not parse server_first_message"; + elseif nonce:find(c_nonce, 3, true) ~= 3 then + return false, "nonce sent by server does not match our nonce"; + elseif nonce == our_nonce then + return false, "server did not append s-nonce to nonce"; + end + + local cbind_input = gs2_header .. cbind_data; + local channel_binding = "c=" .. base64(cbind_input); + local client_final_message_without_proof = channel_binding .. "," .. nonce; + + local SaltedPassword = Hi(Normalize(stream.password), salt, i); + local ClientKey = HMAC(SaltedPassword, "Client Key"); + local StoredKey = H(ClientKey); + local AuthMessage = client_first_message_bare .. "," .. server_first_message .. "," .. client_final_message_without_proof; + local ClientSignature = HMAC(StoredKey, AuthMessage); + local ClientProof = XOR(ClientKey, ClientSignature); + local ServerKey = HMAC(SaltedPassword, "Server Key"); + local ServerSignature = HMAC(ServerKey, AuthMessage); + + local proof = "p=" .. base64(ClientProof); + local client_final_message = client_final_message_without_proof .. "," .. proof; + + local ok, server_final_message = coroutine.yield(client_final_message); + if ok ~= "success" then return false, "success-expected" end + + local verifier = server_final_message:match("v=([^,]+)"); + if unbase64(verifier) ~= ServerSignature then + return false, "server signature did not match"; + end + return true; +end + +return function (stream, name) + if stream.username and (stream.password or (stream.client_key or stream.server_key)) then + if name == "SCRAM-SHA-1" then + return scram, 99; + elseif name == "SCRAM-SHA-1-PLUS" then + local sock = stream.conn:ssl() and stream.conn:socket(); + if sock and sock.getfinished then + return scram, 100; + end + end + end +end + diff --git a/verse/util/sha1.lua b/verse/util/sha1.lua new file mode 100644 index 0000000..1e854c4 --- /dev/null +++ b/verse/util/sha1.lua @@ -0,0 +1,146 @@ +------------------------------------------------- +--- *** SHA-1 algorithm for Lua *** --- +------------------------------------------------- +--- Author: Martin Huesser --- +--- Date: 2008-06-16 --- +--- License: You may use this code in your --- +--- projects as long as this header --- +--- stays intact. --- +------------------------------------------------- + +local strlen = string.len +local strchar = string.char +local strbyte = string.byte +local strsub = string.sub +local floor = math.floor +local bit = require "bit" +local bnot = bit.bnot +local band = bit.band +local bor = bit.bor +local bxor = bit.bxor +local shl = bit.lshift +local shr = bit.rshift +local h0, h1, h2, h3, h4 + +------------------------------------------------- + +local function LeftRotate(val, nr) + return shl(val, nr) + shr(val, 32 - nr) +end + +------------------------------------------------- + +local function ToHex(num) + local i, d + local str = "" + for i = 1, 8 do + d = band(num, 15) + if (d < 10) then + str = strchar(d + 48) .. str + else + str = strchar(d + 87) .. str + end + num = floor(num / 16) + end + return str +end + +------------------------------------------------- + +local function PreProcess(str) + local bitlen, i + local str2 = "" + bitlen = strlen(str) * 8 + str = str .. strchar(128) + i = 56 - band(strlen(str), 63) + if (i < 0) then + i = i + 64 + end + for i = 1, i do + str = str .. strchar(0) + end + for i = 1, 8 do + str2 = strchar(band(bitlen, 255)) .. str2 + bitlen = floor(bitlen / 256) + end + return str .. str2 +end + +------------------------------------------------- + +local function MainLoop(str) + local a, b, c, d, e, f, k, t + local i, j + local w = {} + while (str ~= "") do + for i = 0, 15 do + w[i] = 0 + for j = 1, 4 do + w[i] = w[i] * 256 + strbyte(str, i * 4 + j) + end + end + for i = 16, 79 do + w[i] = LeftRotate(bxor(bxor(w[i - 3], w[i - 8]), bxor(w[i - 14], w[i - 16])), 1) + end + a = h0 + b = h1 + c = h2 + d = h3 + e = h4 + for i = 0, 79 do + if (i < 20) then + f = bor(band(b, c), band(bnot(b), d)) + k = 1518500249 + elseif (i < 40) then + f = bxor(bxor(b, c), d) + k = 1859775393 + elseif (i < 60) then + f = bor(bor(band(b, c), band(b, d)), band(c, d)) + k = 2400959708 + else + f = bxor(bxor(b, c), d) + k = 3395469782 + end + t = LeftRotate(a, 5) + f + e + k + w[i] + e = d + d = c + c = LeftRotate(b, 30) + b = a + a = t + end + h0 = band(h0 + a, 4294967295) + h1 = band(h1 + b, 4294967295) + h2 = band(h2 + c, 4294967295) + h3 = band(h3 + d, 4294967295) + h4 = band(h4 + e, 4294967295) + str = strsub(str, 65) + end +end + +------------------------------------------------- + +local function sha1(str, hexres) + str = PreProcess(str) + h0 = 1732584193 + h1 = 4023233417 + h2 = 2562383102 + h3 = 0271733878 + h4 = 3285377520 + MainLoop(str) + local hex = ToHex(h0)..ToHex(h1)..ToHex(h2) + ..ToHex(h3)..ToHex(h4); + if hexres then + return hex; + else + return (hex:gsub("..", function (byte) + return string.char(tonumber(byte, 16)); + end)); + end +end + +_G.sha1 = {sha1 = sha1}; +return _G.sha1; + +------------------------------------------------- +------------------------------------------------- +------------------------------------------------- diff --git a/verse/util/vcard.lua b/verse/util/vcard.lua new file mode 100644 index 0000000..58b25dc --- /dev/null +++ b/verse/util/vcard.lua @@ -0,0 +1,464 @@ +-- Copyright (C) 2011-2012 Kim Alvefur +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + +-- TODO +-- Fix folding. + +local st = require "util.stanza"; +local t_insert, t_concat = table.insert, table.concat; +local type = type; +local next, pairs, ipairs = next, pairs, ipairs; + +local from_text, to_text, from_xep54, to_xep54; + +local line_sep = "\n"; + +local vCard_dtd; -- See end of file + +local function fold_line() + error "Not implemented" --TODO +end +local function unfold_line() + error "Not implemented" + -- gsub("\r?\n[ \t]([^\r\n])", "%1"); +end + +local function vCard_esc(s) + return s:gsub("[,:;\\]", "\\%1"):gsub("\n","\\n"); +end + +local function vCard_unesc(s) + return s:gsub("\\?[\\nt:;,]", { + ["\\\\"] = "\\", + ["\\n"] = "\n", + ["\\r"] = "\r", + ["\\t"] = "\t", + ["\\:"] = ":", -- FIXME Shouldn't need to espace : in values, just params + ["\\;"] = ";", + ["\\,"] = ",", + [":"] = "\29", + [";"] = "\30", + [","] = "\31", + }); +end + +local function item_to_xep54(item) + local t = st.stanza(item.name, { xmlns = "vcard-temp" }); + + local prop_def = vCard_dtd[item.name]; + if prop_def == "text" then + t:text(item[1]); + elseif type(prop_def) == "table" then + if prop_def.types and item.TYPE then + if type(item.TYPE) == "table" then + for _,v in pairs(prop_def.types) do + for _,typ in pairs(item.TYPE) do + if typ:upper() == v then + t:tag(v):up(); + break; + end + end + end + else + t:tag(item.TYPE:upper()):up(); + end + end + + if prop_def.props then + for _,v in pairs(prop_def.props) do + if item[v] then + t:tag(v):up(); + end + end + end + + if prop_def.value then + t:tag(prop_def.value):text(item[1]):up(); + elseif prop_def.values then + local prop_def_values = prop_def.values; + local repeat_last = prop_def_values.behaviour == "repeat-last" and prop_def_values[#prop_def_values]; + for i=1,#item do + t:tag(prop_def.values[i] or repeat_last):text(item[i]):up(); + end + end + end + + return t; +end + +local function vcard_to_xep54(vCard) + local t = st.stanza("vCard", { xmlns = "vcard-temp" }); + for i=1,#vCard do + t:add_child(item_to_xep54(vCard[i])); + end + return t; +end + +function to_xep54(vCards) + if not vCards[1] or vCards[1].name then + return vcard_to_xep54(vCards) + else + local t = st.stanza("xCard", { xmlns = "vcard-temp" }); + for i=1,#vCards do + t:add_child(vcard_to_xep54(vCards[i])); + end + return t; + end +end + +function from_text(data) + data = data -- unfold and remove empty lines + :gsub("\r\n","\n") + :gsub("\n ", "") + :gsub("\n\n+","\n"); + local vCards = {}; + local c; -- current item + for line in data:gmatch("[^\n]+") do + local line = vCard_unesc(line); + local name, params, value = line:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); + value = value:gsub("\29",":"); + if #params > 0 then + local _params = {}; + for k,isval,v in params:gmatch("\30([^=]+)(=?)([^\30]*)") do + k = k:upper(); + local _vt = {}; + for _p in v:gmatch("[^\31]+") do + _vt[#_vt+1]=_p + _vt[_p]=true; + end + if isval == "=" then + _params[k]=_vt; + else + _params[k]=true; + end + end + params = _params; + end + if name == "BEGIN" and value == "VCARD" then + c = {}; + vCards[#vCards+1] = c; + elseif name == "END" and value == "VCARD" then + c = nil; + elseif c and vCard_dtd[name] then + local dtd = vCard_dtd[name]; + local p = { name = name }; + c[#c+1]=p; + --c[name]=p; + local up = c; + c = p; + if dtd.types then + for _, t in ipairs(dtd.types) do + local t = t:lower(); + if ( params.TYPE and params.TYPE[t] == true) + or params[t] == true then + c.TYPE=t; + end + end + end + if dtd.props then + for _, p in ipairs(dtd.props) do + if params[p] then + if params[p] == true then + c[p]=true; + else + for _, prop in ipairs(params[p]) do + c[p]=prop; + end + end + end + end + end + if dtd == "text" or dtd.value then + t_insert(c, value); + elseif dtd.values then + local value = "\30"..value; + for p in value:gmatch("\30([^\30]*)") do + t_insert(c, p); + end + end + c = up; + end + end + return vCards; +end + +local function item_to_text(item) + local value = {}; + for i=1,#item do + value[i] = vCard_esc(item[i]); + end + value = t_concat(value, ";"); + + local params = ""; + for k,v in pairs(item) do + if type(k) == "string" and k ~= "name" then + params = params .. (";%s=%s"):format(k, type(v) == "table" and t_concat(v,",") or v); + end + end + + return ("%s%s:%s"):format(item.name, params, value) +end + +local function vcard_to_text(vcard) + local t={}; + t_insert(t, "BEGIN:VCARD") + for i=1,#vcard do + t_insert(t, item_to_text(vcard[i])); + end + t_insert(t, "END:VCARD") + return t_concat(t, line_sep); +end + +function to_text(vCards) + if vCards[1] and vCards[1].name then + return vcard_to_text(vCards) + else + local t = {}; + for i=1,#vCards do + t[i]=vcard_to_text(vCards[i]); + end + return t_concat(t, line_sep); + end +end + +local function from_xep54_item(item) + local prop_name = item.name; + local prop_def = vCard_dtd[prop_name]; + + local prop = { name = prop_name }; + + if prop_def == "text" then + prop[1] = item:get_text(); + elseif type(prop_def) == "table" then + if prop_def.value then --single item + prop[1] = item:get_child_text(prop_def.value) or ""; + elseif prop_def.values then --array + local value_names = prop_def.values; + if value_names.behaviour == "repeat-last" then + for i=1,#item.tags do + t_insert(prop, item.tags[i]:get_text() or ""); + end + else + for i=1,#value_names do + t_insert(prop, item:get_child_text(value_names[i]) or ""); + end + end + elseif prop_def.names then + local names = prop_def.names; + for i=1,#names do + if item:get_child(names[i]) then + prop[1] = names[i]; + break; + end + end + end + + if prop_def.props_verbatim then + for k,v in pairs(prop_def.props_verbatim) do + prop[k] = v; + end + end + + if prop_def.types then + local types = prop_def.types; + prop.TYPE = {}; + for i=1,#types do + if item:get_child(types[i]) then + t_insert(prop.TYPE, types[i]:lower()); + end + end + if #prop.TYPE == 0 then + prop.TYPE = nil; + end + end + + -- A key-value pair, within a key-value pair? + if prop_def.props then + local params = prop_def.props; + for i=1,#params do + local name = params[i] + local data = item:get_child_text(name); + if data then + prop[name] = prop[name] or {}; + t_insert(prop[name], data); + end + end + end + else + return nil + end + + return prop; +end + +local function from_xep54_vCard(vCard) + local tags = vCard.tags; + local t = {}; + for i=1,#tags do + t_insert(t, from_xep54_item(tags[i])); + end + return t +end + +function from_xep54(vCard) + if vCard.attr.xmlns ~= "vcard-temp" then + return nil, "wrong-xmlns"; + end + if vCard.name == "xCard" then -- A collection of vCards + local t = {}; + local vCards = vCard.tags; + for i=1,#vCards do + t[i] = from_xep54_vCard(vCards[i]); + end + return t + elseif vCard.name == "vCard" then -- A single vCard + return from_xep54_vCard(vCard) + end +end + +-- This was adapted from http://xmpp.org/extensions/xep-0054.html#dtd +vCard_dtd = { + VERSION = "text", --MUST be 3.0, so parsing is redundant + FN = "text", + N = { + values = { + "FAMILY", + "GIVEN", + "MIDDLE", + "PREFIX", + "SUFFIX", + }, + }, + NICKNAME = "text", + PHOTO = { + props_verbatim = { ENCODING = { "b" } }, + props = { "TYPE" }, + value = "BINVAL", --{ "EXTVAL", }, + }, + BDAY = "text", + ADR = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + values = { + "POBOX", + "EXTADD", + "STREET", + "LOCALITY", + "REGION", + "PCODE", + "CTRY", + } + }, + LABEL = { + types = { + "HOME", + "WORK", + "POSTAL", + "PARCEL", + "DOM", + "INTL", + "PREF", + }, + value = "LINE", + }, + TEL = { + types = { + "HOME", + "WORK", + "VOICE", + "FAX", + "PAGER", + "MSG", + "CELL", + "VIDEO", + "BBS", + "MODEM", + "ISDN", + "PCS", + "PREF", + }, + value = "NUMBER", + }, + EMAIL = { + types = { + "HOME", + "WORK", + "INTERNET", + "PREF", + "X400", + }, + value = "USERID", + }, + JABBERID = "text", + MAILER = "text", + TZ = "text", + GEO = { + values = { + "LAT", + "LON", + }, + }, + TITLE = "text", + ROLE = "text", + LOGO = "copy of PHOTO", + AGENT = "text", + ORG = { + values = { + behaviour = "repeat-last", + "ORGNAME", + "ORGUNIT", + } + }, + CATEGORIES = { + values = "KEYWORD", + }, + NOTE = "text", + PRODID = "text", + REV = "text", + SORTSTRING = "text", + SOUND = "copy of PHOTO", + UID = "text", + URL = "text", + CLASS = { + names = { -- The item.name is the value if it's one of these. + "PUBLIC", + "PRIVATE", + "CONFIDENTIAL", + }, + }, + KEY = { + props = { "TYPE" }, + value = "CRED", + }, + DESC = "text", +}; +vCard_dtd.LOGO = vCard_dtd.PHOTO; +vCard_dtd.SOUND = vCard_dtd.PHOTO; + +return { + from_text = from_text; + to_text = to_text; + + from_xep54 = from_xep54; + to_xep54 = to_xep54; + + -- COMPAT: + lua_to_text = to_text; + lua_to_xep54 = to_xep54; + + text_to_lua = from_text; + text_to_xep54 = function (...) return to_xep54(from_text(...)); end; + + xep54_to_lua = from_xep54; + xep54_to_text = function (...) return to_text(from_xep54(...)) end; +}; diff --git a/verse/verse.lua b/verse/verse.lua new file mode 100644 index 0000000..78fe5d0 --- /dev/null +++ b/verse/verse.lua @@ -0,0 +1,7669 @@ +package.preload['util.encodings']=(function(...) +local function e() +error("Function not implemented"); +end +local t=require"mime"; +module"encodings" +stringprep={}; +base64={encode=t.b64,decode=e}; +return _M; +end) +package.preload['util.hashes']=(function(...) +local e=require"util.sha1"; +return{sha1=e.sha1}; +end) +package.preload['util.sha1']=(function(...) +local m=string.len +local o=string.char +local g=string.byte +local k=string.sub +local c=math.floor +local t=require"bit" +local q=t.bnot +local e=t.band +local y=t.bor +local n=t.bxor +local a=t.lshift +local i=t.rshift +local l,u,d,h,s +local function p(t,e) +return a(t,e)+i(t,32-e) +end +local function r(i) +local t,a +local t="" +for n=1,8 do +a=e(i,15) +if(a<10)then +t=o(a+48)..t +else +t=o(a+87)..t +end +i=c(i/16) +end +return t +end +local function j(t) +local i,a +local n="" +i=m(t)*8 +t=t..o(128) +a=56-e(m(t),63) +if(a<0)then +a=a+64 +end +for e=1,a do +t=t..o(0) +end +for t=1,8 do +n=o(e(i,255))..n +i=c(i/256) +end +return t..n +end +local function b(w) +local r,t,i,a,f,m,c,v +local o,o +local o={} +while(w~="")do +for e=0,15 do +o[e]=0 +for t=1,4 do +o[e]=o[e]*256+g(w,e*4+t) +end +end +for e=16,79 do +o[e]=p(n(n(o[e-3],o[e-8]),n(o[e-14],o[e-16])),1) +end +r=l +t=u +i=d +a=h +f=s +for s=0,79 do +if(s<20)then +m=y(e(t,i),e(q(t),a)) +c=1518500249 +elseif(s<40)then +m=n(n(t,i),a) +c=1859775393 +elseif(s<60)then +m=y(y(e(t,i),e(t,a)),e(i,a)) +c=2400959708 +else +m=n(n(t,i),a) +c=3395469782 +end +v=p(r,5)+m+f+c+o[s] +f=a +a=i +i=p(t,30) +t=r +r=v +end +l=e(l+r,4294967295) +u=e(u+t,4294967295) +d=e(d+i,4294967295) +h=e(h+a,4294967295) +s=e(s+f,4294967295) +w=k(w,65) +end +end +local function a(e,t) +e=j(e) +l=1732584193 +u=4023233417 +d=2562383102 +h=271733878 +s=3285377520 +b(e) +local e=r(l)..r(u)..r(d) +..r(h)..r(s); +if t then +return e; +else +return(e:gsub("..",function(e) +return string.char(tonumber(e,16)); +end)); +end +end +_G.sha1={sha1=a}; +return _G.sha1; +end) +package.preload['lib.adhoc']=(function(...) +local n,h=require"util.stanza",require"util.uuid"; +local e="http://jabber.org/protocol/commands"; +local i={} +local s={}; +function _cmdtag(o,i,t,a) +local e=n.stanza("command",{xmlns=e,node=o.node,status=i}); +if t then e.attr.sessionid=t;end +if a then e.attr.action=a;end +return e; +end +function s.new(t,e,o,a) +return{name=t,node=e,handler=o,cmdtag=_cmdtag,permission=(a or"user")}; +end +function s.handle_cmd(a,s,o) +local e=o.tags[1].attr.sessionid or h.generate(); +local t={}; +t.to=o.attr.to; +t.from=o.attr.from; +t.action=o.tags[1].attr.action or"execute"; +t.form=o.tags[1]:child_with_ns("jabber:x:data"); +local t,h=a:handler(t,i[e]); +i[e]=h; +local o=n.reply(o); +if t.status=="completed"then +i[e]=nil; +cmdtag=a:cmdtag("completed",e); +elseif t.status=="canceled"then +i[e]=nil; +cmdtag=a:cmdtag("canceled",e); +elseif t.status=="error"then +i[e]=nil; +o=n.error_reply(o,t.error.type,t.error.condition,t.error.message); +s.send(o); +return true; +else +cmdtag=a:cmdtag("executing",e); +end +for t,e in pairs(t)do +if t=="info"then +cmdtag:tag("note",{type="info"}):text(e):up(); +elseif t=="warn"then +cmdtag:tag("note",{type="warn"}):text(e):up(); +elseif t=="error"then +cmdtag:tag("note",{type="error"}):text(e.message):up(); +elseif t=="actions"then +local t=n.stanza("actions"); +for o,e in ipairs(e)do +if(e=="prev")or(e=="next")or(e=="complete")then +t:tag(e):up(); +else +module:log("error",'Command "'..a.name.. +'" at node "'..a.node..'" provided an invalid action "'..e..'"'); +end +end +cmdtag:add_child(t); +elseif t=="form"then +cmdtag:add_child((e.layout or e):form(e.values)); +elseif t=="result"then +cmdtag:add_child((e.layout or e):form(e.values,"result")); +elseif t=="other"then +cmdtag:add_child(e); +end +end +o:add_child(cmdtag); +s.send(o); +return true; +end +return s; +end) +package.preload['util.rsm']=(function(...) +local n=require"util.stanza".stanza; +local t,i=tostring,tonumber; +local h=type; +local s=pairs; +local o='http://jabber.org/protocol/rsm'; +local a={}; +do +local e=a; +local function t(e) +return i((e:get_text())); +end +local function a(t) +return t:get_text(); +end +e.after=a; +e.before=function(e) +local e=e:get_text(); +return e==""or e; +end; +e.max=t; +e.index=t; +e.first=function(e) +return{index=i(e.attr.index);e:get_text()}; +end; +e.last=a; +e.count=t; +end +local h=setmetatable({ +first=function(a,e) +if h(e)=="table"then +a:tag("first",{index=e.index}):text(e[1]):up(); +else +a:tag("first"):text(t(e)):up(); +end +end; +before=function(a,e) +if e==true then +a:tag("before"):up(); +else +a:tag("before"):text(t(e)):up(); +end +end +},{ +__index=function(e,a) +return function(e,o) +e:tag(a):text(t(o)):up(); +end +end; +}); +local function t(e) +local t={}; +for o in e:childtags()do +local e=o.name; +local a=e and a[e]; +if a then +t[e]=a(o); +end +end +return t; +end +local function i(e) +local t=n("set",{xmlns=o}); +for e,o in s(e)do +if a[e]then +h[e](t,o); +end +end +return t; +end +local function a(e) +local e=e:get_child("set",o); +if e and#e.tags>0 then +return t(e); +end +end +return{parse=t,generate=i,get=a}; +end) +package.preload['util.stanza']=(function(...) +local t=table.insert; +local d=table.remove; +local p=table.concat; +local h=string.format; +local l=string.match; +local c=tostring; +local m=setmetatable; +local n=pairs; +local o=ipairs; +local i=type; +local v=string.gsub; +local w=string.sub; +local f=string.find; +local e=os; +local u=not e.getenv("WINDIR"); +local r,a; +if u then +local t,e=pcall(require,"util.termcolours"); +if t then +r,a=e.getstyle,e.getstring; +else +u=nil; +end +end +local y="urn:ietf:params:xml:ns:xmpp-stanzas"; +module"stanza" +stanza_mt={__type="stanza"}; +stanza_mt.__index=stanza_mt; +local e=stanza_mt; +function stanza(a,t) +local t={name=a,attr=t or{},tags={}}; +return m(t,e); +end +local s=stanza; +function e:query(e) +return self:tag("query",{xmlns=e}); +end +function e:body(t,e) +return self:tag("body",e):text(t); +end +function e:tag(a,e) +local a=s(a,e); +local e=self.last_add; +if not e then e={};self.last_add=e;end +(e[#e]or self):add_direct_child(a); +t(e,a); +return self; +end +function e:text(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:up() +local e=self.last_add; +if e then d(e);end +return self; +end +function e:reset() +self.last_add=nil; +return self; +end +function e:add_direct_child(e) +if i(e)=="table"then +t(self.tags,e); +end +t(self,e); +end +function e:add_child(t) +local e=self.last_add; +(e and e[#e]or self):add_direct_child(t); +return self; +end +function e:get_child(a,t) +for o,e in o(self.tags)do +if(not a or e.name==a) +and((not t and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==t)then +return e; +end +end +end +function e:get_child_text(e,t) +local e=self:get_child(e,t); +if e then +return e:get_text(); +end +return nil; +end +function e:child_with_name(t) +for a,e in o(self.tags)do +if e.name==t then return e;end +end +end +function e:child_with_ns(t) +for a,e in o(self.tags)do +if e.attr.xmlns==t then return e;end +end +end +function e:children() +local e=0; +return function(t) +e=e+1 +return t[e]; +end,self,e; +end +function e:childtags(i,o) +local e=self.tags; +local t,a=1,#e; +return function() +for a=t,a do +local e=e[a]; +if(not i or e.name==i) +and((not o and self.attr.xmlns==e.attr.xmlns) +or e.attr.xmlns==o)then +t=a+1; +return e; +end +end +end; +end +function e:maptags(i) +local o,e=self.tags,1; +local n,a=#self,#o; +local t=1; +while e<=a and a>0 do +if self[t]==o[e]then +local i=i(self[t]); +if i==nil then +d(self,t); +d(o,e); +n=n-1; +a=a-1; +t=t-1; +e=e-1; +else +self[t]=i; +o[e]=i; +end +e=e+1; +end +t=t+1; +end +return self; +end +function e:find(a) +local e=1; +local s=#a+1; +repeat +local o,t,i; +local n=w(a,e,e); +if n=="@"then +return self.attr[w(a,e+1)]; +elseif n=="{"then +o,e=l(a,"^([^}]+)}()",e+1); +end +t,i,e=l(a,"^([^@/#]*)([/#]?)()",e); +t=t~=""and t or nil; +if e==s then +if i=="#"then +return self:get_child_text(t,o); +end +return self:get_child(t,o); +end +self=self:get_child(t,o); +until not self +end +local d +do +local e={["'"]="'",["\""]=""",["<"]="<",[">"]=">",["&"]="&"}; +function d(t)return(v(t,"['&<>\"]",e));end +_M.xml_escape=d; +end +local function w(o,e,s,a,r) +local i=0; +local h=o.name +t(e,"<"..h); +for o,n in n(o.attr)do +if f(o,"\1",1,true)then +local s,o=l(o,"^([^\1]*)\1?(.*)$"); +i=i+1; +t(e," xmlns:ns"..i.."='"..a(s).."' ".."ns"..i..":"..o.."='"..a(n).."'"); +elseif not(o=="xmlns"and n==r)then +t(e," "..o.."='"..a(n).."'"); +end +end +local i=#o; +if i==0 then +t(e,"/>"); +else +t(e,">"); +for i=1,i do +local i=o[i]; +if i.name then +s(i,e,s,a,o.attr.xmlns); +else +t(e,a(i)); +end +end +t(e,""); +end +end +function e.__tostring(t) +local e={}; +w(t,e,w,d,nil); +return p(e); +end +function e.top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if i(t)=="string"then e=e..h(" %s='%s'",t,d(c(a)));end end +end +return h("<%s%s>",t.name,e); +end +function e.get_text(e) +if#e.tags==0 then +return p(e); +end +end +function e.get_error(t) +local i,e,a; +local t=t:get_child("error"); +if not t then +return nil,nil,nil; +end +i=t.attr.type; +for o,t in o(t.tags)do +if t.attr.xmlns==y then +if not a and t.name=="text"then +a=t:get_text(); +elseif not e then +e=t.name; +end +if e and a then +break; +end +end +end +return i,e or"undefined-condition",a; +end +do +local e=0; +function new_id() +e=e+1; +return"lx"..e; +end +end +function preserialize(e) +local a={name=e.name,attr=e.attr}; +for o,e in o(e)do +if i(e)=="table"then +t(a,preserialize(e)); +else +t(a,e); +end +end +return a; +end +function deserialize(a) +if a then +local s=a.attr; +for e=1,#s do s[e]=nil;end +local h={}; +for e in n(s)do +if f(e,"|",1,true)and not f(e,"\1",1,true)then +local a,t=l(e,"^([^|]+)|(.+)$"); +h[a.."\1"..t]=s[e]; +s[e]=nil; +end +end +for e,t in n(h)do +s[e]=t; +end +m(a,e); +for t,e in o(a)do +if i(e)=="table"then +deserialize(e); +end +end +if not a.tags then +local n={}; +for o,e in o(a)do +if i(e)=="table"then +t(n,e); +end +end +a.tags=n; +end +end +return a; +end +local function l(a) +local o,i={},{}; +for e,t in n(a.attr)do o[e]=t;end +local o={name=a.name,attr=o,tags=i}; +for e=1,#a do +local e=a[e]; +if e.name then +e=l(e); +t(i,e); +end +t(o,e); +end +return m(o,e); +end +clone=l; +function message(e,t) +if not t then +return s("message",e); +else +return s("message",e):tag("body"):text(t):up(); +end +end +function iq(e) +if e and not e.id then e.id=new_id();end +return s("iq",e or{id=new_id()}); +end +function reply(e) +return s(e.name,e.attr and{to=e.attr.from,from=e.attr.to,id=e.attr.id,type=((e.name=="iq"and"result")or e.attr.type)}); +end +do +local t={xmlns=y}; +function error_reply(e,i,o,a) +local e=reply(e); +e.attr.type="error"; +e:tag("error",{type=i}) +:tag(o,t):up(); +if(a)then e:tag("text",t):text(a):up();end +return e; +end +end +function presence(e) +return s("presence",e); +end +if u then +local u=r("yellow"); +local s=r("red"); +local l=r("red"); +local t=r("magenta"); +local s=" "..a(u,"%s")..a(t,"=")..a(s,"'%s'"); +local r=a(t,"<")..a(l,"%s").."%s"..a(t,">"); +local l=r.."%s"..a(t,""); +function e.pretty_print(e) +local t=""; +for a,e in o(e)do +if i(e)=="string"then +t=t..d(e); +else +t=t..e:pretty_print(); +end +end +local a=""; +if e.attr then +for e,t in n(e.attr)do if i(e)=="string"then a=a..h(s,e,c(t));end end +end +return h(l,e.name,a,t,e.name); +end +function e.pretty_top_tag(t) +local e=""; +if t.attr then +for t,a in n(t.attr)do if i(t)=="string"then e=e..h(s,t,c(a));end end +end +return h(r,t.name,e); +end +else +e.pretty_print=e.__tostring; +e.pretty_top_tag=e.top_tag; +end +return _M; +end) +package.preload['util.timer']=(function(...) +local a=require"net.server"; +local h=math.min +local u=math.huge +local i=require"socket".gettime; +local r=table.insert; +local d=pairs; +local l=type; +local s={}; +local t={}; +module"timer" +local e; +if not a.event then +function e(o,n) +local i=i(); +o=o+i; +if o>=i then +r(t,{o,n}); +else +local t=n(i); +if t and l(t)=="number"then +return e(t,n); +end +end +end +a._addtimer(function() +local o=i(); +if#t>0 then +for a,e in d(t)do +r(s,e); +end +t={}; +end +local t=u; +for r,a in d(s)do +local i,n=a[1],a[2]; +if i<=o then +s[r]=nil; +local a=n(o); +if l(a)=="number"then +e(a,n); +t=h(t,a); +end +else +t=h(t,i-o); +end +end +return t; +end); +else +local t=a.event; +local a=a.event_base; +local o=(t.core and t.core.LEAVE)or-1; +function e(n,e) +local t; +t=a:addevent(nil,0,function() +local e=e(i()); +if e then +return 0,e; +elseif t then +return o; +end +end +,n); +end +end +add_task=e; +return _M; +end) +package.preload['util.termcolours']=(function(...) +local i,n=table.concat,table.insert; +local t,a=string.char,string.format; +local r=tonumber; +local h=ipairs; +local s=io.write; +local e; +if os.getenv("WINDIR")then +e=require"util.windows"; +end +local o=e and e.get_consolecolor and e.get_consolecolor(); +module"termcolours" +local d={ +reset=0;bright=1,dim=2,underscore=4,blink=5,reverse=7,hidden=8; +black=30;red=31;green=32;yellow=33;blue=34;magenta=35;cyan=36;white=37; +["black background"]=40;["red background"]=41;["green background"]=42;["yellow background"]=43;["blue background"]=44;["magenta background"]=45;["cyan background"]=46;["white background"]=47; +bold=1,dark=2,underline=4,underlined=4,normal=0; +} +local u={ +["0"]=o, +["1"]=7+8, +["1;33"]=2+4+8, +["1;31"]=4+8 +} +local l={ +[1]="font-weight: bold",[2]="opacity: 0.5",[4]="text-decoration: underline",[8]="visibility: hidden", +[30]="color:black",[31]="color:red",[32]="color:green",[33]="color:#FFD700", +[34]="color:blue",[35]="color: magenta",[36]="color:cyan",[37]="color: white", +[40]="background-color:black",[41]="background-color:red",[42]="background-color:green", +[43]="background-color:yellow",[44]="background-color:blue",[45]="background-color: magenta", +[46]="background-color:cyan",[47]="background-color: white"; +}; +local c=t(27).."[%sm%s"..t(27).."[0m"; +function getstring(e,t) +if e then +return a(c,e,t); +else +return t; +end +end +function getstyle(...) +local e,t={...},{}; +for a,e in h(e)do +e=d[e]; +if e then +n(t,e); +end +end +return i(t,";"); +end +local a="0"; +function setstyle(e) +e=e or"0"; +if e~=a then +s("\27["..e.."m"); +a=e; +end +end +if e then +function setstyle(t) +t=t or"0"; +if t~=a then +e.set_consolecolor(u[t]or o); +a=t; +end +end +if not o then +function setstyle(e)end +end +end +local function a(t) +if t=="0"then return"";end +local e={}; +for t in t:gmatch("[^;]+")do +n(e,l[r(t)]); +end +return""; +end +function tohtml(e) +return e:gsub("\027%[(.-)m",a); +end +return _M; +end) +package.preload['util.uuid']=(function(...) +local e=math.random; +local n=tostring; +local e=os.time; +local i=os.clock; +local o=require"util.hashes".sha1; +module"uuid" +local t=0; +local function a() +local e=e(); +if t>=e then e=t+1;end +t=e; +return e; +end +local function t(e) +return o(e..i()..n({}),true); +end +local e=t(a()); +local function o(a) +e=t(e..a); +end +local function t(t) +if#e"; +end +return e; +end +local w={ +LOC=e.LOC_tostring; +MX=function(e) +return a.format('%2i %s',e.pref,e.mx); +end; +SRV=function(e) +local e=e.srv; +return a.format('%5d %5d %5d %s',e.priority,e.weight,e.port,e.target); +end; +}; +local x={}; +function x.__tostring(e) +local t=(w[e.type]or j)(e); +return a.format('%2s %-5s %6i %-28s %s',e.class,e.type,e.ttl,e.name,t); +end +local j={}; +function j.__tostring(t) +local e={}; +for a,t in m(t)do +n(e,b(t)..'\n'); +end +return i.concat(e); +end +local w={}; +function w.__tostring(e) +local a=s.gettime(); +local t={}; +for i,e in o(e)do +for i,e in o(e)do +for o,e in o(e)do +v(e,a); +n(t,b(e)); +end +end +end +return i.concat(t); +end +function e:new() +local t={active={},cache={},unsorted={}}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +function t.random(...) +p.randomseed(p.floor(1e4*s.gettime())%2147483648); +t.random=p.random; +return t.random(...); +end +local function E(e) +e=e or{}; +e.id=e.id or t.random(0,65535); +e.rd=e.rd or 1; +e.tc=e.tc or 0; +e.aa=e.aa or 0; +e.opcode=e.opcode or 0; +e.qr=e.qr or 0; +e.rcode=e.rcode or 0; +e.z=e.z or 0; +e.ra=e.ra or 0; +e.qdcount=e.qdcount or 1; +e.ancount=e.ancount or 0; +e.nscount=e.nscount or 0; +e.arcount=e.arcount or 0; +local t=a.char( +c(e.id),e.id%256, +e.rd+2*e.tc+4*e.aa+8*e.opcode+128*e.qr, +e.rcode+16*e.z+128*e.ra, +c(e.qdcount),e.qdcount%256, +c(e.ancount),e.ancount%256, +c(e.nscount),e.nscount%256, +c(e.arcount),e.arcount%256 +); +return t,e.id; +end +local function c(t) +local e={}; +for t in a.gmatch(t,'[^.]+')do +n(e,a.char(a.len(t))); +n(e,t); +end +n(e,a.char(0)); +return i.concat(e); +end +local function p(o,a,e) +o=c(o); +a=t.typecode[a or'a']; +e=t.classcode[e or'in']; +return o..a..e; +end +function e:byte(e) +e=e or 1; +local t=self.offset; +local o=t+e-1; +if o>#self.packet then +q(a.format('out of bounds: %i>%i',o,#self.packet)); +end +self.offset=t+e; +return a.byte(self.packet,t,o); +end +function e:word() +local t,e=self:byte(2); +return 256*t+e; +end +function e:dword() +local t,e,o,a=self:byte(4); +return 16777216*t+65536*e+256*o+a; +end +function e:sub(e) +e=e or 1; +local t=a.sub(self.packet,self.offset,self.offset+e-1); +self.offset=self.offset+e; +return t; +end +function e:header(t) +local e=self:word(); +if not self.active[e]and not t then return nil;end +local e={id=e}; +local t,a=self:byte(2); +e.rd=t%2; +e.tc=t/2%2; +e.aa=t/4%2; +e.opcode=t/8%16; +e.qr=t/128; +e.rcode=a%16; +e.z=a/16%8; +e.ra=a/128; +e.qdcount=self:word(); +e.ancount=self:word(); +e.nscount=self:word(); +e.arcount=self:word(); +for a,t in o(e)do e[a]=t-t%1;end +return e; +end +function e:name() +local t,a=nil,0; +local e=self:byte(); +local o={}; +if e==0 then return"."end +while e>0 do +if e>=192 then +a=a+1; +if a>=20 then q('dns error: 20 pointers');end; +local e=((e-192)*256)+self:byte(); +t=t or self.offset; +self.offset=e+1; +else +n(o,self:sub(e)..'.'); +end +e=self:byte(); +end +self.offset=t or self.offset; +return i.concat(o); +end +function e:question() +local e={}; +e.name=self:name(); +e.type=t.type[self:word()]; +e.class=t.class[self:word()]; +return e; +end +function e:A(o) +local e,t,i,n=self:byte(4); +o.a=a.format('%i.%i.%i.%i',e,t,i,n); +end +function e:AAAA(a) +local e={}; +for t=1,a.rdlength,2 do +local a,t=self:byte(2); +i.insert(e,("%02x%02x"):format(a,t)); +end +e=i.concat(e,":"):gsub("%f[%x]0+(%x)","%1"); +local t={}; +for e in e:gmatch(":[0:]+:")do +i.insert(t,e) +end +if#t==0 then +a.aaaa=e; +return +elseif#t>1 then +i.sort(t,function(e,t)return#e>#t end); +end +a.aaaa=e:gsub(t[1],"::",1):gsub("^0::","::"):gsub("::0$","::"); +end +function e:CNAME(e) +e.cname=self:name(); +end +function e:MX(e) +e.pref=self:word(); +e.mx=self:name(); +end +function e:LOC_nibble_power() +local e=self:byte(); +return((e-(e%16))/16)*(10^(e%16)); +end +function e:LOC(e) +e.version=self:byte(); +if e.version==0 then +e.loc=e.loc or{}; +e.loc.size=self:LOC_nibble_power(); +e.loc.horiz_pre=self:LOC_nibble_power(); +e.loc.vert_pre=self:LOC_nibble_power(); +e.loc.latitude=self:dword(); +e.loc.longitude=self:dword(); +e.loc.altitude=self:dword(); +end +end +local function c(e,n,t) +e=e-2147483648; +if e<0 then n=t;e=-e;end +local i,t,o; +o=e%6e4; +e=(e-o)/6e4; +t=e%60; +i=(e-t)/60; +return a.format('%3d %2d %2.3f %s',i,t,o/1e3,n); +end +function e.LOC_tostring(e) +local t={}; +n(t,a.format( +'%s %s %.2fm %.2fm %.2fm %.2fm', +c(e.loc.latitude,'N','S'), +c(e.loc.longitude,'E','W'), +(e.loc.altitude-1e7)/100, +e.loc.size/100, +e.loc.horiz_pre/100, +e.loc.vert_pre/100 +)); +return i.concat(t); +end +function e:NS(e) +e.ns=self:name(); +end +function e:SOA(e) +end +function e:SRV(e) +e.srv={}; +e.srv.priority=self:word(); +e.srv.weight=self:word(); +e.srv.port=self:word(); +e.srv.target=self:name(); +end +function e:PTR(e) +e.ptr=self:name(); +end +function e:TXT(e) +e.txt=self:sub(self:byte()); +end +function e:rr() +local e={}; +r(e,x); +e.name=self:name(self); +e.type=t.type[self:word()]or e.type; +e.class=t.class[self:word()]or e.class; +e.ttl=65536*self:word()+self:word(); +e.rdlength=self:word(); +if e.ttl<=0 then +e.tod=self.time+30; +else +e.tod=self.time+e.ttl; +end +local a=self.offset; +local t=self[t.type[e.type]]; +if t then t(self,e);end +self.offset=a; +e.rdata=self:sub(e.rdlength); +return e; +end +function e:rrs(t) +local e={}; +for t=1,t do n(e,self:rr());end +return e; +end +function e:decode(t,o) +self.packet,self.offset=t,1; +local t=self:header(o); +if not t then return nil;end +local t={header=t}; +t.question={}; +local i=self.offset; +for e=1,t.header.qdcount do +n(t.question,self:question()); +end +t.question.raw=a.sub(self.packet,i,self.offset-1); +if not o then +if not self.active[t.header.id]or not self.active[t.header.id][t.question.raw]then +self.active[t.header.id]=nil; +return nil; +end +end +t.answer=self:rrs(t.header.ancount); +t.authority=self:rrs(t.header.nscount); +t.additional=self:rrs(t.header.arcount); +return t; +end +e.delays={1,3}; +function e:addnameserver(e) +self.server=self.server or{}; +n(self.server,e); +end +function e:setnameserver(e) +self.server={}; +self:addnameserver(e); +end +function e:adddefaultnameservers() +if z then +if y and y.get_nameservers then +for t,e in m(y.get_nameservers())do +self:addnameserver(e); +end +end +if not self.server or#self.server==0 then +self:addnameserver("208.67.222.222"); +self:addnameserver("208.67.220.220"); +end +else +local e=_.open("/etc/resolv.conf"); +if e then +for e in e:lines()do +e=e:gsub("#.*$","") +:match('^%s*nameserver%s+(.*)%s*$'); +if e then +e:gsub("%f[%d.](%d+%.%d+%.%d+%.%d+)%f[^%d.]",function(e) +self:addnameserver(e) +end); +end +end +end +if not self.server or#self.server==0 then +self:addnameserver("127.0.0.1"); +end +end +end +function e:getsocket(a) +self.socket=self.socket or{}; +self.socketset=self.socketset or{}; +local e=self.socket[a]; +if e then return e;end +local o,t; +e,t=s.udp(); +if e and self.socket_wrapper then e,t=self.socket_wrapper(e,self);end +if not e then +return nil,t; +end +e:settimeout(0); +self.socket[a]=e; +self.socketset[e]=a; +o,t=e:setsockname('*',0); +if not o then return self:servfail(e,t);end +o,t=e:setpeername(self.server[a],53); +if not o then return self:servfail(e,t);end +return e; +end +function e:voidsocket(e) +if self.socket[e]then +self.socketset[self.socket[e]]=nil; +self.socket[e]=nil; +elseif self.socketset[e]then +self.socket[self.socketset[e]]=nil; +self.socketset[e]=nil; +end +e:close(); +end +function e:socket_wrapper_set(e) +self.socket_wrapper=e; +end +function e:closeall() +for t,e in m(self.socket)do +self.socket[t]=nil; +self.socketset[e]=nil; +e:close(); +end +end +function e:remember(e,t) +local a,o,i=g(e.name,e.type,e.class); +if t~='*'then +t=o; +local t=d(self.cache,i,'*',a); +if t then n(t,e);end +end +self.cache=self.cache or r({},w); +local a=d(self.cache,i,t,a)or +u(self.cache,i,t,a,r({},j)); +if not a[e[o:lower()]]then +a[e[o:lower()]]=true; +n(a,e); +end +if t=='MX'then self.unsorted[a]=true;end +end +local function c(t,e) +return(t.pref==e.pref)and(t.mx#self.server then +e.server=1; +end +e.retries=(e.retries or 0)+1; +if e.retries>=#self.server then +a[o]=nil; +else +t,n=self:getsocket(e.server); +if t then t:send(e.packet);end +end +end +end +if h(a)==nil then +self.active[s]=nil; +end +end +if i==self.best_server then +self.best_server=self.best_server+1; +if self.best_server>#self.server then +self.best_server=1; +end +end +return t,n; +end +function e:settimeout(e) +self.timeout=e; +end +function e:receive(t) +self.time=s.gettime(); +t=t or self.socket; +local e; +for a,t in o(t)do +if self.socketset[t]then +local t=t:receive(); +if t then +e=self:decode(t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type) +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not h(t)then self.active[e.header.id]=nil;end +if not h(self.active)then self:closeall();end +local e=e.question[1]; +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,e.class,e.type,e.name,nil); +end +end +end +end +end +return e; +end +function e:feed(a,e,t) +self.time=s.gettime(); +local e=self:decode(e,t); +if e and self.active[e.header.id] +and self.active[e.header.id][e.question.raw]then +for a,t in o(e.answer)do +self:remember(t,e.question[1].type); +end +local t=self.active[e.header.id]; +t[e.question.raw]=nil; +if not h(t)then self.active[e.header.id]=nil;end +if not h(self.active)then self:closeall();end +local e=e.question[1]; +if e then +local t=d(self.wanted,e.class,e.type,e.name); +if t then +for e in o(t)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,e.class,e.type,e.name,nil); +end +end +end +return e; +end +function e:cancel(t,e,a) +local i=d(self.wanted,t,e,a); +if i then +for e in o(i)do +if l.status(e)=="suspended"then l.resume(e);end +end +u(self.wanted,t,e,a,nil); +end +end +function e:pulse() +while self:receive()do end +if not h(self.active)then return nil;end +self.time=s.gettime(); +for i,t in o(self.active)do +for a,e in o(t)do +if self.time>=e.retry then +e.server=e.server+1; +if e.server>#self.server then +e.server=1; +e.delay=e.delay+1; +end +if e.delay>#self.delays then +t[a]=nil; +if not h(t)then self.active[i]=nil;end +if not h(self.active)then return nil;end +else +local t=self.socket[e.server]; +if t then t:send(e.packet);end +e.retry=self.time+self.delays[e.delay]; +end +end +end +end +if h(self.active)then return true;end +return nil; +end +function e:lookup(e,t,a) +self:query(e,t,a) +while self:pulse()do +local e={} +for a,t in m(self.socket)do +e[a]=t +end +s.select(e,nil,4) +end +return self:peek(e,t,a); +end +function e:lookupex(o,a,t,e) +return self:peek(a,t,e)or self:query(a,t,e); +end +function e:tohostname(e) +return t.lookup(e:gsub("(%d+)%.(%d+)%.(%d+)%.(%d+)","%4.%3.%2.%1.in-addr.arpa."),"PTR"); +end +local i={ +qr={[0]='query','response'}, +opcode={[0]='query','inverse query','server status request'}, +aa={[0]='non-authoritative','authoritative'}, +tc={[0]='complete','truncated'}, +rd={[0]='recursion not desired','recursion desired'}, +ra={[0]='recursion not available','recursion available'}, +z={[0]='(reserved)'}, +rcode={[0]='no error','format error','server failure','name error','not implemented'}, +type=t.type, +class=t.class +}; +local function s(t,e) +return(i[e]and i[e][t[e]])or''; +end +function e.print(e) +for o,t in o{'id','qr','opcode','aa','tc','rd','ra','z', +'rcode','qdcount','ancount','nscount','arcount'}do +f(a.format('%-30s','header.'..t),e.header[t],s(e.header,t)); +end +for e,t in m(e.question)do +f(a.format('question[%i].name ',e),t.name); +f(a.format('question[%i].type ',e),t.type); +f(a.format('question[%i].class ',e),t.class); +end +local h={name=1,type=1,class=1,ttl=1,rdlength=1,rdata=1}; +local t; +for n,i in o({'answer','authority','additional'})do +for n,e in o(e[i])do +for h,o in o({'name','type','class','ttl','rdlength'})do +t=a.format('%s[%i].%s',i,n,o); +f(a.format('%-30s',t),e[o],s(e,o)); +end +for e,o in o(e)do +if not h[e]then +t=a.format('%s[%i].%s',i,n,e); +f(a.format('%-30s %s',b(t),b(o))); +end +end +end +end +end +function t.resolver() +local t={active={},cache={},unsorted={},wanted={},best_server=1}; +r(t,e); +r(t.cache,w); +r(t.unsorted,{__mode='kv'}); +return t; +end +local e=t.resolver(); +t._resolver=e; +function t.lookup(...) +return e:lookup(...); +end +function t.tohostname(...) +return e:tohostname(...); +end +function t.purge(...) +return e:purge(...); +end +function t.peek(...) +return e:peek(...); +end +function t.query(...) +return e:query(...); +end +function t.feed(...) +return e:feed(...); +end +function t.cancel(...) +return e:cancel(...); +end +function t.settimeout(...) +return e:settimeout(...); +end +function t.cache() +return e.cache; +end +function t.socket_wrapper_set(...) +return e:socket_wrapper_set(...); +end +return t; +end) +package.preload['net.adns']=(function(...) +local c=require"net.server"; +local a=require"net.dns"; +local e=require"util.logger".init("adns"); +local t,t=table.insert,table.remove; +local n,s,l=coroutine,tostring,pcall; +local function u(a,a,e,t)return(t-e)+1;end +module"adns" +function lookup(d,t,h,r) +return n.wrap(function(o) +if o then +e("debug","Records for %s already cached, using those...",t); +d(o); +return; +end +e("debug","Records for %s not in cache, sending query (%s)...",t,s(n.running())); +local i,o=a.query(t,h,r); +if i then +n.yield({r or"IN",h or"A",t,n.running()}); +e("debug","Reply for %s (%s)",t,s(n.running())); +end +if i then +i,o=l(d,a.peek(t,h,r)); +else +e("error","Error sending DNS query: %s",o); +i,o=l(d,nil,o); +end +if not i then +e("error","Error in DNS response handler: %s",s(o)); +end +end)(a.peek(t,h,r)); +end +function cancel(t,o,i) +e("warn","Cancelling DNS lookup for %s",s(t[3])); +a.cancel(t[1],t[2],t[3],t[4],o); +end +function new_async_socket(o,i) +local s=""; +local n={}; +local t={}; +local h; +function n.onincoming(o,e) +if e then +a.feed(t,e); +end +end +function n.ondisconnect(a,o) +if o then +e("warn","DNS socket for %s disconnected: %s",s,o); +local t=i.server; +if i.socketset[a]==i.best_server and i.best_server==#t then +e("error","Exhausted all %d configured DNS servers, next lookup will try %s again",#t,t[1]); +end +i:servfail(a); +end +end +t,h=c.wrapclient(o,"dns",53,n); +if not t then +return nil,h; +end +t.settimeout=function()end +t.setsockname=function(e,...)return o:setsockname(...);end +t.setpeername=function(e,...)s=(...);local o,a=o:setpeername(...);e:set_send(u);return o,a;end +t.connect=function(e,...)return o:connect(...)end +t.send=function(a,t) +e("debug","Sending DNS query to %s",s); +return o:send(t); +end +return t; +end +a.socket_wrapper_set(new_async_socket); +return _M; +end) +package.preload['net.server']=(function(...) +local d=function(e) +return _G[e] +end +local M,e=require("util.logger").init("socket"),table.concat; +local i=function(...)return M("debug",e{...});end +local U=function(...)return M("warn",e{...});end +local ne=1 +local H=d"type" +local D=d"pairs" +local ie=d"ipairs" +local p=d"tonumber" +local h=d"tostring" +local a=d"os" +local t=d"table" +local o=d"string" +local e=d"coroutine" +local V=a.difftime +local Y=math.min +local oe=math.huge +local ve=t.concat +local ye=o.sub +local pe=e.wrap +local we=e.yield +local T=d"ssl" +local v=d"socket"or require"socket" +local P=v.gettime +local fe=(T and T.wrap) +local le=v.bind +local me=v.sleep +local ce=v.select +local K +local G +local ue +local Q +local re +local c +local he +local se +local de +local ae +local ee +local X +local r +local Z +local F +local te +local y +local s +local R +local l +local n +local S +local b +local w +local m +local a +local o +local g +local L +local C +local O +local N +local j +local J +local u +local I +local E +local A +local _ +local z +local W +local q +local k +local x +y={} +s={} +l={} +R={} +n={} +b={} +w={} +S={} +a=0 +o=0 +g=0 +L=0 +C=0 +O=1 +N=0 +j=128 +I=51e3*1024 +E=25e3*1024 +A=12e5 +_=6e4 +z=6*60*60 +local e=package.config:sub(1,1)=="\\" +k=(e and math.huge)or v._SETSIZE or 1024 +q=v._SETSIZE or 1024 +x=30 +ae=function(w,t,f,d,v,u) +if t:getfd()>=k then +U("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +return nil,"fd-too-large" +end +local m=0 +local p,e=w.onconnect,w.ondisconnect +local b=t.accept +local e={} +e.shutdown=function()end +e.ssl=function() +return u~=nil +end +e.sslctx=function() +return u +end +e.remove=function() +m=m-1 +if e then +e.resume() +end +end +e.close=function() +t:close() +o=r(l,t,o) +a=r(s,t,a) +y[f..":"..d]=nil; +n[t]=nil +e=nil +t=nil +i"server.lua: closed server handler and removed sockets from list" +end +e.pause=function(o) +if not e.paused then +a=r(s,t,a) +if o then +n[t]=nil +t:close() +t=nil; +end +e.paused=true; +end +end +e.resume=function() +if e.paused then +if not t then +t=le(f,d,j); +t:settimeout(0) +end +a=c(s,t,a) +n[t]=e +e.paused=false; +end +end +e.ip=function() +return f +end +e.serverport=function() +return d +end +e.socket=function() +return t +end +e.readbuffer=function() +if a>=q or o>=q then +e.pause() +i("server.lua: refused new client connection: server full") +return false +end +local t,o=b(t) +if t then +local a,o=t:getpeername() +local e,n,t=F(e,w,t,a,d,o,v,u) +if t then +return false +end +m=m+1 +i("server.lua: accepted new client connection from ",h(a),":",h(o)," to ",h(d)) +if p and not u then +return p(e); +end +return; +elseif o then +i("server.lua: error with new client connection: ",h(o)) +return false +end +end +return e +end +F=function(D,y,t,H,X,N,O,g) +if t:getfd()>=k then +U("server.lua: Disallowed FD number: "..t:getfd()) +t:close() +if D then +D.pause() +end +return nil,nil,"fd-too-large" +end +t:settimeout(0) +local p +local R +local k +local W +local F=y.onincoming +local U=y.onstatus +local q=y.ondisconnect +local M=y.ondrain +local Y=y.ondetach +local v={} +local d=0 +local G +local J +local P +local f=0 +local j=false +local A=false +local V,B=0,0 +local _=I +local z=E +local e=v +e.dispatch=function() +return F +end +e.disconnect=function() +return q +end +e.setlistener=function(a,t) +if Y then +Y(a) +end +F=t.onincoming +q=t.ondisconnect +U=t.onstatus +M=t.ondrain +Y=t.ondetach +end +e.getstats=function() +return B,V +end +e.ssl=function() +return W +end +e.sslctx=function() +return g +end +e.send=function(n,i,o,a) +return p(t,i,o,a) +end +e.receive=function(o,a) +return R(t,o,a) +end +e.shutdown=function(a) +return k(t,a) +end +e.setoption=function(i,a,o) +if t.setoption then +return t:setoption(a,o); +end +return false,"setoption not implemented"; +end +e.force_close=function(t,a) +if d~=0 then +i("server.lua: discarding unwritten data for ",h(H),":",h(N)) +d=0; +end +return t:close(a); +end +e.close=function(u,h) +if not e then return true;end +a=r(s,t,a) +b[e]=nil +if d~=0 then +e.sendbuffer() +if d~=0 then +if e then +e.write=nil +end +G=true +return false +end +end +if t then +m=k and k(t) +t:close() +o=r(l,t,o) +n[t]=nil +t=nil +else +i"server.lua: socket already closed" +end +if e then +w[e]=nil +S[e]=nil +local t=e; +e=nil +if q then +q(t,h or false); +q=nil +end +end +if D then +D.remove() +end +i"server.lua: closed client handler and removed socket from list" +return true +end +e.ip=function() +return H +end +e.serverport=function() +return X +end +e.clientport=function() +return N +end +e.port=e.clientport +local q=function(i,a) +f=f+#a +if f>_ then +S[e]="send buffer exceeded" +e.write=Q +return false +elseif t and not l[t]then +o=c(l,t,o) +end +d=d+1 +v[d]=a +if e then +w[e]=w[e]or u +end +return true +end +e.write=q +e.bufferqueue=function(t) +return v +end +e.socket=function(a) +return t +end +e.set_mode=function(a,t) +O=t or O +return O +end +e.set_send=function(a,t) +p=t or p +return p +end +e.bufferlen=function(o,a,t) +_=t or _ +z=a or z +return f,z,_ +end +e.lock_read=function(i,o) +if o==true then +local o=a +a=r(s,t,a) +b[e]=nil +if a~=o then +j=true +end +elseif o==false then +if j then +j=false +a=c(s,t,a) +b[e]=u +end +end +return j +end +e.pause=function(t) +return t:lock_read(true); +end +e.resume=function(t) +return t:lock_read(false); +end +e.lock=function(i,a) +e.lock_read(a) +if a==true then +e.write=Q +local a=o +o=r(l,t,o) +w[e]=nil +if o~=a then +A=true +end +elseif a==false then +e.write=q +if A then +A=false +q("") +end +end +return j,A +end +local b=function() +local o,t,a=R(t,O) +if not t or(t=="wantread"or t=="timeout")then +local o=o or a or"" +local a=#o +if a>z then +e:close("receive buffer exceeded") +return false +end +local a=a*ne +B=B+a +C=C+a +b[e]=u +return F(e,o,t) +else +i("server.lua: client ",h(H),":",h(N)," read error: ",h(t)) +J=true +m=e and e:force_close(t) +return false +end +end +local v=function() +local y,a,s,n,c; +if t then +n=ve(v,"",1,d) +y,a,s=p(t,n,1,f) +c=(y or s or 0)*ne +V=V+c +L=L+c +for e=d,1,-1 do +v[e]=nil +end +else +y,a,c=false,"unexpected close",0; +end +if y then +d=0 +f=0 +o=r(l,t,o) +w[e]=nil +if M then +M(e) +end +m=P and e:starttls(nil) +m=G and e:force_close() +return true +elseif s and(a=="timeout"or a=="wantwrite")then +n=ye(n,s+1,f) +v[1]=n +d=1 +f=f-s +w[e]=u +return true +else +i("server.lua: client ",h(H),":",h(N)," write error: ",h(a)) +J=true +m=e and e:force_close(a) +return false +end +end +local u; +function e.set_sslctx(w,t) +g=t; +local f,d +u=pe(function(n) +local t +for h=1,x do +o=(d and r(l,n,o))or o +a=(f and r(s,n,a))or a +f,d=nil,nil +m,t=n:dohandshake() +if not t then +i("server.lua: ssl handshake done") +e.readbuffer=b +e.sendbuffer=v +m=U and U(e,"ssl-handshake-complete") +if w.autostart_ssl and y.onconnect then +y.onconnect(w); +end +a=c(s,n,a) +return true +else +if t=="wantwrite"then +o=c(l,n,o) +d=true +elseif t=="wantread"then +a=c(s,n,a) +f=true +else +break; +end +t=nil; +we() +end +end +i("server.lua: ssl handshake error: ",h(t or"handshake too long")) +m=e and e:force_close("ssl handshake failed") +return false,t +end +) +end +if T then +e.starttls=function(f,m) +if m then +e:set_sslctx(m); +end +if d>0 then +i"server.lua: we need to do tls, but delaying until send buffer empty" +P=true +return +end +i("server.lua: attempting to start tls on "..h(t)) +local d,m=t +t,m=fe(t,g) +if not t then +i("server.lua: error while starting tls on client: ",h(m or"unknown error")) +return nil,m +end +t:settimeout(0) +p=t.send +R=t.receive +k=K +n[t]=e +a=c(s,t,a) +a=r(s,d,a) +o=r(l,d,o) +n[d]=nil +e.starttls=nil +P=nil +W=true +e.readbuffer=u +e.sendbuffer=u +return u(t) +end +end +e.readbuffer=b +e.sendbuffer=v +p=t.send +R=t.receive +k=(W and K)or t.shutdown +n[t]=e +a=c(s,t,a) +if g and T then +i"server.lua: auto-starting ssl negotiation..." +e.autostart_ssl=true; +local e,t=e:starttls(g); +if e==false then +return nil,nil,t +end +end +return e,t +end +K=function() +end +Q=function() +return false +end +c=function(t,a,e) +if not t[a]then +e=e+1 +t[e]=a +t[a]=e +end +return e; +end +r=function(e,i,t) +local a=e[i] +if a then +e[i]=nil +local o=e[t] +e[t]=nil +if o~=i then +e[o]=a +e[a]=o +end +return t-1 +end +return t +end +X=function(e) +o=r(l,e,o) +a=r(s,e,a) +n[e]=nil +e:close() +end +local function f(e,t,o) +local a; +local i=t.sendbuffer; +function t.sendbuffer() +i(); +if a and t.bufferlen()=o then +a=true; +e:lock_read(true); +end +end +e:set_mode("*a"); +end +he=function(t,e,d,l,r) +local o +if H(d)~="table"then +o="invalid listener table" +end +if H(e)~="number"or not(e>=0 and e<=65535)then +o="invalid port" +elseif y[t..":"..e]then +o="listeners on '["..t.."]:"..e.."' already exist" +elseif r and not T then +o="luasec not found" +end +if o then +U("server.lua, [",t,"]:",e,": ",o) +return nil,o +end +t=t or"*" +local o,h=le(t,e,j) +if h then +U("server.lua, [",t,"]:",e,": ",h) +return nil,h +end +local h,d=ae(d,o,t,e,l,r) +if not h then +o:close() +return nil,d +end +o:settimeout(0) +a=c(s,o,a) +y[t..":"..e]=h +n[o]=h +i("server.lua: new "..(r and"ssl "or"").."server listener on '[",t,"]:",e,"'") +return h +end +de=function(e,t) +return y[e..":"..t]; +end +Z=function(e,t) +local a=y[e..":"..t] +if not a then +return nil,"no server found on '["..e.."]:"..h(t).."'" +end +a:close() +y[e..":"..t]=nil +return true +end +re=function() +for e,t in D(n)do +t:close() +n[e]=nil +end +a=0 +o=0 +g=0 +y={} +s={} +l={} +R={} +n={} +end +ee=function() +return{ +select_timeout=O; +select_sleep_time=N; +tcp_backlog=j; +max_send_buffer_size=I; +max_receive_buffer_size=E; +select_idle_check_interval=A; +send_timeout=_; +read_timeout=z; +max_connections=q; +max_ssl_handshake_roundtrips=x; +highest_allowed_fd=k; +} +end +te=function(e) +if H(e)~="table"then +return nil,"invalid settings table" +end +O=p(e.select_timeout)or O +N=p(e.select_sleep_time)or N +I=p(e.max_send_buffer_size)or I +E=p(e.max_receive_buffer_size)or E +A=p(e.select_idle_check_interval)or A +j=p(e.tcp_backlog)or j +_=p(e.send_timeout)or _ +z=p(e.read_timeout)or z +q=e.max_connections or q +x=e.max_ssl_handshake_roundtrips or x +k=e.highest_allowed_fd or k +return true +end +se=function(e) +if H(e)~="function"then +return nil,"invalid listener function" +end +g=g+1 +R[g]=e +return true +end +ue=function() +return C,L,a,o,g +end +local t; +local function y(e) +t=not not e; +end +G=function(a) +if t then return"quitting";end +if a then t="once";end +local e=oe; +repeat +local o,a,s=ce(s,l,Y(O,e)) +for t,e in ie(a)do +local t=n[e] +if t then +t.sendbuffer() +else +X(e) +i"server.lua: found no handler and closed socket (writelist)" +end +end +for e,t in ie(o)do +local e=n[t] +if e then +e.readbuffer() +else +X(t) +i"server.lua: found no handler and closed socket (readlist)" +end +end +for e,t in D(S)do +e.disconnect()(e,t) +e:force_close() +S[e]=nil; +end +u=P() +local a=V(u-J) +if a>A then +J=u +for e,t in D(w)do +if V(u-t)>_ then +e.disconnect()(e,"send timeout") +e:force_close() +end +end +for e,t in D(b)do +if V(u-t)>z then +e.disconnect()(e,"read timeout") +e:close() +end +end +end +if u-W>=Y(e,1)then +e=oe; +for t=1,g do +local t=R[t](u) +if t then e=Y(e,t);end +end +W=u +else +e=e-(u-W); +end +me(N) +until t; +if a and t=="once"then t=nil;return;end +return"quitting" +end +local function h() +return G(true); +end +local function r() +return"select"; +end +local s=function(e,s,a,t,h,i) +local e,a,s=F(nil,t,e,s,a,"clientport",h,i) +if not e then return nil,s end +n[a]=e +if not i then +o=c(l,a,o) +if t.onconnect then +local a=e.sendbuffer; +e.sendbuffer=function() +e.sendbuffer=a; +t.onconnect(e); +return a(); +end +end +end +return e,a +end +local t=function(a,o,i,n,h) +local t,e=v.tcp() +if e then +return nil,e +end +t:settimeout(0) +m,e=t:connect(a,o) +if e then +local e=s(t,a,o,i) +else +F(nil,i,t,a,o,"clientport",n,h) +end +end +d"setmetatable"(n,{__mode="k"}) +d"setmetatable"(b,{__mode="k"}) +d"setmetatable"(w,{__mode="k"}) +W=P() +J=P() +local function a(e) +local t=M; +if e then +M=e; +end +return t; +end +return{ +_addtimer=se, +addclient=t, +wrapclient=s, +loop=G, +link=f, +step=h, +stats=ue, +closeall=re, +addserver=he, +getserver=de, +setlogger=a, +getsettings=ee, +setquitting=y, +removeserver=Z, +get_backend=r, +changesettings=te, +} +end) +package.preload['util.xmppstream']=(function(...) +local e=require"lxp"; +local t=require"util.stanza"; +local b=t.stanza_mt; +local f=error; +local t=tostring; +local d=table.insert; +local p=table.concat; +local _=table.remove; +local v=setmetatable; +local z=pcall(e.new,{StartDoctypeDecl=false}); +local T=pcall(e.new,{XmlDecl=false}); +local a=not not e.new({}).getcurrentbytecount; +local E=1024*1024*10; +module"xmppstream" +local k=e.new; +local x={ +["http://www.w3.org/XML/1998/namespace\1lang"]="xml:lang"; +["http://www.w3.org/XML/1998/namespace\1space"]="xml:space"; +["http://www.w3.org/XML/1998/namespace\1base"]="xml:base"; +["http://www.w3.org/XML/1998/namespace\1id"]="xml:id"; +}; +local s="http://etherx.jabber.org/streams"; +local r="\1"; +local g="^([^"..r.."]*)"..r.."?(.*)$"; +_M.ns_separator=r; +_M.ns_pattern=g; +local function o()end +function new_sax_handlers(n,e,h) +local i={}; +local y=e.streamopened; +local w=e.streamclosed; +local l=e.error or function(o,a,e)f("XML stream error: "..t(a)..(e and": "..t(e)or""),2);end; +local j=e.handlestanza; +h=h or o; +local t=e.stream_ns or s; +local c=e.stream_tag or"stream"; +if t~=""then +c=t..r..c; +end +local q=t..r..(e.error_tag or"error"); +local k=e.default_ns; +local u={}; +local s,e={}; +local t=0; +local r=0; +function i:StartElement(m,o) +if e and#s>0 then +d(e,p(s)); +s={}; +end +local s,i=m:match(g); +if i==""then +s,i="",s; +end +if s~=k or r>0 then +o.xmlns=s; +r=r+1; +end +for t=1,#o do +local e=o[t]; +o[t]=nil; +local t=x[e]; +if t then +o[t]=o[e]; +o[e]=nil; +end +end +if not e then +if a then +t=self:getcurrentbytecount(); +end +if n.notopen then +if m==c then +r=0; +if y then +if a then +h(t); +t=0; +end +y(n,o); +end +else +l(n,"no-stream",m); +end +return; +end +if s=="jabber:client"and i~="iq"and i~="presence"and i~="message"then +l(n,"invalid-top-level-element"); +end +e=v({name=i,attr=o,tags={}},b); +else +if a then +t=t+self:getcurrentbytecount(); +end +d(u,e); +local t=e; +e=v({name=i,attr=o,tags={}},b); +d(t,e); +d(t.tags,e); +end +end +if T then +function i:XmlDecl(e,e,e) +if a then +h(self:getcurrentbytecount()); +end +end +end +function i:StartCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +h(self:getcurrentbytecount()); +end +end +end +function i:EndCdataSection() +if a then +if e then +t=t+self:getcurrentbytecount(); +else +h(self:getcurrentbytecount()); +end +end +end +function i:CharacterData(o) +if e then +if a then +t=t+self:getcurrentbytecount(); +end +d(s,o); +elseif a then +h(self:getcurrentbytecount()); +end +end +function i:EndElement(o) +if a then +t=t+self:getcurrentbytecount() +end +if r>0 then +r=r-1; +end +if e then +if#s>0 then +d(e,p(s)); +s={}; +end +if#u==0 then +if a then +h(t); +end +t=0; +if o~=q then +j(n,e); +else +l(n,"stream-error",e); +end +e=nil; +else +e=_(u); +end +else +if w then +w(n); +end +end +end +local function a(e) +l(n,"parse-error","restricted-xml","Restricted XML, see RFC 6120 section 11.1."); +if not e.stop or not e:stop()then +f("Failed to abort parsing"); +end +end +if z then +i.StartDoctypeDecl=a; +end +i.Comment=a; +i.ProcessingInstruction=a; +local function a() +e,s,t=nil,{},0; +u={}; +end +local function e(t,e) +n=e; +end +return i,{reset=a,set_session=e}; +end +function new(i,n,t) +local e=0; +local o; +if a then +function o(a) +e=e-a; +end +t=t or E; +elseif t then +f("Stanza size limits are not supported on this version of LuaExpat") +end +local n,i=new_sax_handlers(i,n,o); +local o=k(n,r,false); +local s=o.parse; +return{ +reset=function() +o=k(n,r,false); +s=o.parse; +e=0; +i.reset(); +end, +feed=function(n,i) +if a then +e=e+#i; +end +local i,o=s(o,i); +if a and e>t then +return nil,"stanza-too-large"; +end +return i,o; +end, +set_session=i.set_session; +}; +end +return _M; +end) +package.preload['util.jid']=(function(...) +local a,i=string.match,string.sub; +local r=require"util.encodings".stringprep.nodeprep; +local d=require"util.encodings".stringprep.nameprep; +local l=require"util.encodings".stringprep.resourceprep; +local n={ +[" "]="\\20";['"']="\\22"; +["&"]="\\26";["'"]="\\27"; +["/"]="\\2f";[":"]="\\3a"; +["<"]="\\3c";[">"]="\\3e"; +["@"]="\\40";["\\"]="\\5c"; +}; +local s={}; +for e,t in pairs(n)do s[t]=e;end +module"jid" +local function o(e) +if not e then return;end +local o,t=a(e,"^([^@/]+)@()"); +local t,i=a(e,"^([^@/]+)()",t) +if o and not t then return nil,nil,nil;end +local a=a(e,"^/(.+)$",i); +if(not t)or((not a)and#e>=i)then return nil,nil,nil;end +return o,t,a; +end +split=o; +function bare(e) +local t,e=o(e); +if t and e then +return t.."@"..e; +end +return e; +end +local function h(e) +local a,e,t=o(e); +if e then +if i(e,-1,-1)=="."then +e=i(e,1,-2); +end +e=d(e); +if not e then return;end +if a then +a=r(a); +if not a then return;end +end +if t then +t=l(t); +if not t then return;end +end +return a,e,t; +end +end +prepped_split=h; +function prep(e) +local t,e,a=h(e); +if e then +if t then +e=t.."@"..e; +end +if a then +e=e.."/"..a; +end +end +return e; +end +function join(a,e,t) +if a and e and t then +return a.."@"..e.."/"..t; +elseif a and e then +return a.."@"..e; +elseif e and t then +return e.."/"..t; +elseif e then +return e; +end +return nil; +end +function compare(t,e) +local n,i,s=o(t); +local e,t,a=o(e); +if((e~=nil and e==n)or e==nil)and +((t~=nil and t==i)or t==nil)and +((a~=nil and a==s)or a==nil)then +return true +end +return false +end +function escape(e)return e and(e:gsub(".",n));end +function unescape(e)return e and(e:gsub("\\%x%x",s));end +return _M; +end) +package.preload['util.events']=(function(...) +local o=pairs; +local s=table.insert; +local n=table.sort; +local d=setmetatable; +local h=next; +module"events" +function new() +local t={}; +local e={}; +local function r(i,a) +local e=e[a]; +if not e or h(e)==nil then return;end +local t={}; +for e in o(e)do +s(t,e); +end +n(t,function(a,t)return e[a]>e[t];end); +i[a]=t; +return t; +end; +d(t,{__index=r}); +local function s(o,i,n) +local a=e[o]; +if a then +a[i]=n or 0; +else +a={[i]=n or 0}; +e[o]=a; +end +t[o]=nil; +end; +local function i(a,i) +local o=e[a]; +if o then +o[i]=nil; +t[a]=nil; +if h(o)==nil then +e[a]=nil; +end +end +end; +local function a(e) +for e,t in o(e)do +s(e,t); +end +end; +local function n(e) +for t,e in o(e)do +i(t,e); +end +end; +local function o(e,...) +local e=t[e]; +if e then +for t=1,#e do +local e=e[t](...); +if e~=nil then return e;end +end +end +end; +return{ +add_handler=s; +remove_handler=i; +add_handlers=a; +remove_handlers=n; +fire_event=o; +_handlers=t; +_event_map=e; +}; +end +return _M; +end) +package.preload['util.dataforms']=(function(...) +local e=setmetatable; +local t,i=pairs,ipairs; +local r,h,c=tostring,type,next; +local s=table.concat; +local l=require"util.stanza"; +local d=require"util.jid".prep; +module"dataforms" +local u='jabber:x:data'; +local n={}; +local t={__index=n}; +function new(a) +return e(a,t); +end +function from_stanza(e) +local o={ +title=e:get_child_text("title"); +instructions=e:get_child_text("instructions"); +}; +for e in e:childtags("field")do +local a={ +name=e.attr.var; +label=e.attr.label; +type=e.attr.type; +required=e:get_child("required")and true or nil; +value=e:get_child_text("value"); +}; +o[#o+1]=a; +if a.type then +local t={}; +if a.type:match"list%-"then +for e in e:childtags("option")do +t[#t+1]={label=e.attr.label,value=e:get_child_text("value")}; +end +for e in e:childtags("value")do +t[#t+1]={label=e.attr.label,value=e:get_text(),default=true}; +end +elseif a.type:match"%-multi"then +for e in e:childtags("value")do +t[#t+1]=e.attr.label and{label=e.attr.label,value=e:get_text()}or e:get_text(); +end +if a.type=="text-multi"then +a.value=s(t,"\n"); +else +a.value=t; +end +end +end +end +return new(o); +end +function n.form(t,n,e) +local e=l.stanza("x",{xmlns=u,type=e or"form"}); +if t.title then +e:tag("title"):text(t.title):up(); +end +if t.instructions then +e:tag("instructions"):text(t.instructions):up(); +end +for t,o in i(t)do +local a=o.type or"text-single"; +e:tag("field",{type=a,var=o.name,label=o.label}); +local t=(n and n[o.name])or o.value; +if t then +if a=="hidden"then +if h(t)=="table"then +e:tag("value") +:add_child(t) +:up(); +else +e:tag("value"):text(r(t)):up(); +end +elseif a=="boolean"then +e:tag("value"):text((t and"1")or"0"):up(); +elseif a=="fixed"then +elseif a=="jid-multi"then +for a,t in i(t)do +e:tag("value"):text(t):up(); +end +elseif a=="jid-single"then +e:tag("value"):text(t):up(); +elseif a=="text-single"or a=="text-private"then +e:tag("value"):text(t):up(); +elseif a=="text-multi"then +for t in t:gmatch("([^\r\n]+)\r?\n*")do +e:tag("value"):text(t):up(); +end +elseif a=="list-single"then +local a=false; +for o,t in i(t)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default and(not a)then +e:tag("value"):text(t.value):up(); +a=true; +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +elseif a=="list-multi"then +for a,t in i(t)do +if h(t)=="table"then +e:tag("option",{label=t.label}):tag("value"):text(t.value):up():up(); +if t.default then +e:tag("value"):text(t.value):up(); +end +else +e:tag("option",{label=t}):tag("value"):text(r(t)):up():up(); +end +end +end +end +if o.required then +e:tag("required"):up(); +end +e:up(); +end +return e; +end +local e={}; +function n.data(t,s) +local n={}; +local a={}; +for o,t in i(t)do +local o; +for e in s:childtags()do +if t.name==e.attr.var then +o=e; +break; +end +end +if not o then +if t.required then +a[t.name]="Required value missing"; +end +else +local e=e[t.type]; +if e then +n[t.name],a[t.name]=e(o,t.required); +end +end +end +if c(a)then +return n,a; +end +return n; +end +e["text-single"]= +function(t,a) +local t=t:get_child_text("value"); +if t and#t>0 then +return t +elseif a then +return nil,"Required value missing"; +end +end +e["text-private"]= +e["text-single"]; +e["jid-single"]= +function(t,o) +local t=t:get_child_text("value") +local a=d(t); +if a and#a>0 then +return a +elseif t then +return nil,"Invalid JID: "..t; +elseif o then +return nil,"Required value missing"; +end +end +e["jid-multi"]= +function(o,i) +local a={}; +local t={}; +for e in o:childtags("value")do +local e=e:get_text(); +local o=d(e); +a[#a+1]=o; +if e and not o then +t[#t+1]=("Invalid JID: "..e); +end +end +if#a>0 then +return a,(#t>0 and s(t,"\n")or nil); +elseif i then +return nil,"Required value missing"; +end +end +e["list-multi"]= +function(a,o) +local t={}; +for e in a:childtags("value")do +t[#t+1]=e:get_text(); +end +return t,(o and#t==0 and"Required value missing"or nil); +end +e["text-multi"]= +function(t,a) +local t,a=e["list-multi"](t,a); +if t then +t=s(t,"\n"); +end +return t,a; +end +e["list-single"]= +e["text-single"]; +local a={ +["1"]=true,["true"]=true, +["0"]=false,["false"]=false, +}; +e["boolean"]= +function(t,o) +local t=t:get_child_text("value"); +local a=a[t~=nil and t]; +if a~=nil then +return a; +elseif t then +return nil,"Invalid boolean representation"; +elseif o then +return nil,"Required value missing"; +end +end +e["hidden"]= +function(e) +return e:get_child_text("value"); +end +return _M; +end) +package.preload['util.caps']=(function(...) +local d=require"util.encodings".base64.encode; +local l=require"util.hashes".sha1; +local n,h,s=table.insert,table.sort,table.concat; +local r=ipairs; +module"caps" +function calculate_hash(e) +local i,o,a={},{},{}; +for t,e in r(e)do +if e.name=="identity"then +n(i,(e.attr.category or"").."\0"..(e.attr.type or"").."\0"..(e.attr["xml:lang"]or"").."\0"..(e.attr.name or"")); +elseif e.name=="feature"then +n(o,e.attr.var or""); +elseif e.name=="x"and e.attr.xmlns=="jabber:x:data"then +local t={}; +local o; +for a,e in r(e.tags)do +if e.name=="field"and e.attr.var then +local a={}; +for t,e in r(e.tags)do +e=#e.tags==0 and e:get_text(); +if e then n(a,e);end +end +h(a); +if e.attr.var=="FORM_TYPE"then +o=a[1]; +elseif#a>0 then +n(t,e.attr.var.."\0"..s(a,"<")); +else +n(t,e.attr.var); +end +end +end +h(t); +t=s(t,"<"); +if o then t=o.."\0"..t;end +n(a,t); +end +end +h(i); +h(o); +h(a); +if#i>0 then i=s(i,"<"):gsub("%z","/").."<";else i="";end +if#o>0 then o=s(o,"<").."<";else o="";end +if#a>0 then a=s(a,"<"):gsub("%z","<").."<";else a="";end +local e=i..o..a; +local t=d(l(e)); +return t,e; +end +return _M; +end) +package.preload['util.vcard']=(function(...) +local i=require"util.stanza"; +local a,r=table.insert,table.concat; +local s=type; +local e,h,m=next,pairs,ipairs; +local c,l,u,d; +local f="\n"; +local o; +local function e() +error"Not implemented" +end +local function e() +error"Not implemented" +end +local function w(e) +return e:gsub("[,:;\\]","\\%1"):gsub("\n","\\n"); +end +local function y(e) +return e:gsub("\\?[\\nt:;,]",{ +["\\\\"]="\\", +["\\n"]="\n", +["\\r"]="\r", +["\\t"]="\t", +["\\:"]=":", +["\\;"]=";", +["\\,"]=",", +[":"]="\29", +[";"]="\30", +[","]="\31", +}); +end +local function p(t) +local a=i.stanza(t.name,{xmlns="vcard-temp"}); +local e=o[t.name]; +if e=="text"then +a:text(t[1]); +elseif s(e)=="table"then +if e.types and t.TYPE then +if s(t.TYPE)=="table"then +for o,e in h(e.types)do +for o,t in h(t.TYPE)do +if t:upper()==e then +a:tag(e):up(); +break; +end +end +end +else +a:tag(t.TYPE:upper()):up(); +end +end +if e.props then +for o,e in h(e.props)do +if t[e]then +a:tag(e):up(); +end +end +end +if e.value then +a:tag(e.value):text(t[1]):up(); +elseif e.values then +local o=e.values; +local i=o.behaviour=="repeat-last"and o[#o]; +for o=1,#t do +a:tag(e.values[o]or i):text(t[o]):up(); +end +end +end +return a; +end +local function n(t) +local e=i.stanza("vCard",{xmlns="vcard-temp"}); +for a=1,#t do +e:add_child(p(t[a])); +end +return e; +end +function d(e) +if not e[1]or e[1].name then +return n(e) +else +local t=i.stanza("xCard",{xmlns="vcard-temp"}); +for a=1,#e do +t:add_child(n(e[a])); +end +return t; +end +end +function c(t) +t=t +:gsub("\r\n","\n") +:gsub("\n ","") +:gsub("\n\n+","\n"); +local s={}; +local e; +for t in t:gmatch("[^\n]+")do +local t=y(t); +local n,t,i=t:match("^([-%a]+)(\30?[^\29]*)\29(.*)$"); +i=i:gsub("\29",":"); +if#t>0 then +local a={}; +for e,o,i in t:gmatch("\30([^=]+)(=?)([^\30]*)")do +e=e:upper(); +local t={}; +for e in i:gmatch("[^\31]+")do +t[#t+1]=e +t[e]=true; +end +if o=="="then +a[e]=t; +else +a[e]=true; +end +end +t=a; +end +if n=="BEGIN"and i=="VCARD"then +e={}; +s[#s+1]=e; +elseif n=="END"and i=="VCARD"then +e=nil; +elseif e and o[n]then +local o=o[n]; +local n={name=n}; +e[#e+1]=n; +local s=e; +e=n; +if o.types then +for o,a in m(o.types)do +local a=a:lower(); +if(t.TYPE and t.TYPE[a]==true) +or t[a]==true then +e.TYPE=a; +end +end +end +if o.props then +for o,a in m(o.props)do +if t[a]then +if t[a]==true then +e[a]=true; +else +for o,t in m(t[a])do +e[a]=t; +end +end +end +end +end +if o=="text"or o.value then +a(e,i); +elseif o.values then +local t="\30"..i; +for t in t:gmatch("\30([^\30]*)")do +a(e,t); +end +end +e=s; +end +end +return s; +end +local function n(e) +local t={}; +for a=1,#e do +t[a]=w(e[a]); +end +t=r(t,";"); +local a=""; +for e,t in h(e)do +if s(e)=="string"and e~="name"then +a=a..(";%s=%s"):format(e,s(t)=="table"and r(t,",")or t); +end +end +return("%s%s:%s"):format(e.name,a,t) +end +local function i(t) +local e={}; +a(e,"BEGIN:VCARD") +for o=1,#t do +a(e,n(t[o])); +end +a(e,"END:VCARD") +return r(e,f); +end +function l(e) +if e[1]and e[1].name then +return i(e) +else +local t={}; +for a=1,#e do +t[a]=i(e[a]); +end +return r(t,f); +end +end +local function n(i) +local t=i.name; +local e=o[t]; +local t={name=t}; +if e=="text"then +t[1]=i:get_text(); +elseif s(e)=="table"then +if e.value then +t[1]=i:get_child_text(e.value)or""; +elseif e.values then +local e=e.values; +if e.behaviour=="repeat-last"then +for e=1,#i.tags do +a(t,i.tags[e]:get_text()or""); +end +else +for o=1,#e do +a(t,i:get_child_text(e[o])or""); +end +end +elseif e.names then +local e=e.names; +for a=1,#e do +if i:get_child(e[a])then +t[1]=e[a]; +break; +end +end +end +if e.props_verbatim then +for a,e in h(e.props_verbatim)do +t[a]=e; +end +end +if e.types then +local e=e.types; +t.TYPE={}; +for o=1,#e do +if i:get_child(e[o])then +a(t.TYPE,e[o]:lower()); +end +end +if#t.TYPE==0 then +t.TYPE=nil; +end +end +if e.props then +local e=e.props; +for o=1,#e do +local e=e[o] +local o=i:get_child_text(e); +if o then +t[e]=t[e]or{}; +a(t[e],o); +end +end +end +else +return nil +end +return t; +end +local function i(e) +local t=e.tags; +local e={}; +for o=1,#t do +a(e,n(t[o])); +end +return e +end +function u(e) +if e.attr.xmlns~="vcard-temp"then +return nil,"wrong-xmlns"; +end +if e.name=="xCard"then +local t={}; +local a=e.tags; +for e=1,#a do +t[e]=i(a[e]); +end +return t +elseif e.name=="vCard"then +return i(e) +end +end +o={ +VERSION="text", +FN="text", +N={ +values={ +"FAMILY", +"GIVEN", +"MIDDLE", +"PREFIX", +"SUFFIX", +}, +}, +NICKNAME="text", +PHOTO={ +props_verbatim={ENCODING={"b"}}, +props={"TYPE"}, +value="BINVAL", +}, +BDAY="text", +ADR={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +values={ +"POBOX", +"EXTADD", +"STREET", +"LOCALITY", +"REGION", +"PCODE", +"CTRY", +} +}, +LABEL={ +types={ +"HOME", +"WORK", +"POSTAL", +"PARCEL", +"DOM", +"INTL", +"PREF", +}, +value="LINE", +}, +TEL={ +types={ +"HOME", +"WORK", +"VOICE", +"FAX", +"PAGER", +"MSG", +"CELL", +"VIDEO", +"BBS", +"MODEM", +"ISDN", +"PCS", +"PREF", +}, +value="NUMBER", +}, +EMAIL={ +types={ +"HOME", +"WORK", +"INTERNET", +"PREF", +"X400", +}, +value="USERID", +}, +JABBERID="text", +MAILER="text", +TZ="text", +GEO={ +values={ +"LAT", +"LON", +}, +}, +TITLE="text", +ROLE="text", +LOGO="copy of PHOTO", +AGENT="text", +ORG={ +values={ +behaviour="repeat-last", +"ORGNAME", +"ORGUNIT", +} +}, +CATEGORIES={ +values="KEYWORD", +}, +NOTE="text", +PRODID="text", +REV="text", +SORTSTRING="text", +SOUND="copy of PHOTO", +UID="text", +URL="text", +CLASS={ +names={ +"PUBLIC", +"PRIVATE", +"CONFIDENTIAL", +}, +}, +KEY={ +props={"TYPE"}, +value="CRED", +}, +DESC="text", +}; +o.LOGO=o.PHOTO; +o.SOUND=o.PHOTO; +return{ +from_text=c; +to_text=l; +from_xep54=u; +to_xep54=d; +lua_to_text=l; +lua_to_xep54=d; +text_to_lua=c; +text_to_xep54=function(...)return d(c(...));end; +xep54_to_lua=u; +xep54_to_text=function(...)return l(u(...))end; +}; +end) +package.preload['util.logger']=(function(...) +local e=pcall; +local e=string.find; +local e,s,e=ipairs,pairs,setmetatable; +module"logger" +local e={}; +local t; +function init(e) +local n=t(e,"debug"); +local i=t(e,"info"); +local o=t(e,"warn"); +local a=t(e,"error"); +return function(t,e,...) +if t=="debug"then +return n(e,...); +elseif t=="info"then +return i(e,...); +elseif t=="warn"then +return o(e,...); +elseif t=="error"then +return a(e,...); +end +end +end +function t(o,a) +local t=e[a]; +if not t then +t={}; +e[a]=t; +end +local e=function(e,...) +for i=1,#t do +t[i](o,a,e,...); +end +end +return e; +end +function reset() +for t,e in s(e)do +for t=1,#e do +e[t]=nil; +end +end +end +function add_level_sink(t,a) +if not e[t]then +e[t]={a}; +else +e[t][#e[t]+1]=a; +end +end +_M.new=t; +return _M; +end) +package.preload['util.datetime']=(function(...) +local e=os.date; +local n=os.time; +local u=os.difftime; +local t=error; +local r=tonumber; +module"datetime" +function date(t) +return e("!%Y-%m-%d",t); +end +function datetime(t) +return e("!%Y-%m-%dT%H:%M:%SZ",t); +end +function time(t) +return e("!%H:%M:%S",t); +end +function legacy(t) +return e("!%Y%m%dT%H:%M:%S",t); +end +function parse(o) +if o then +local i,s,h,d,l,t,a; +i,s,h,d,l,t,a=o:match("^(%d%d%d%d)%-?(%d%d)%-?(%d%d)T(%d%d):(%d%d):(%d%d)%.?%d*([Z+%-]?.*)$"); +if i then +local u=u(n(e("*t")),n(e("!*t"))); +local o=0; +if a~=""and a~="Z"then +local a,t,e=a:match("([+%-])(%d%d):?(%d*)"); +if not a then return;end +if#e~=2 then e="0";end +t,e=r(t),r(e); +o=t*60*60+e*60; +if a=="-"then o=-o;end +end +t=(t+u)-o; +return n({year=i,month=s,day=h,hour=d,min=l,sec=t,isdst=false}); +end +end +end +return _M; +end) +package.preload['util.sasl.scram']=(function(...) +local i,c=require"mime".b64,require"mime".unb64; +local a=require"crypto"; +local t=require"bit"; +local n=tonumber; +local s,e=string.char,string.byte; +local o=string.gsub; +local h=t.bxor; +local function m(a,t) +return(o(a,"()(.)",function(o,a) +return s(h(e(a),e(t,o))) +end)); +end +local function y(e) +return a.digest("sha1",e,true); +end +local s=a.hmac.digest; +local function e(e,t) +return s("sha1",t,e,true); +end +local function w(o,t,i) +local t=e(o,t.."\0\0\0\1"); +local a=t; +for i=2,i do +t=e(o,t); +a=m(a,t); +end +return a; +end +local function f(e) +return e; +end +local function t(e) +return(o(e,"[,=]",{[","]="=2C",["="]="=3D"})); +end +local function r(o,h) +local t="n="..t(o.username); +local s=i(a.rand.bytes(15)); +local d="r="..s; +local r=t..","..d; +local u=""; +local t=o.conn:ssl()and"y"or"n"; +if h=="SCRAM-SHA-1-PLUS"then +u=o.conn:socket():getfinished(); +t="p=tls-unique"; +end +local l=t..",,"; +local t=l..r; +local t,h=coroutine.yield(t); +if t~="challenge"then return false end +local a,t,p=h:match("(r=[^,]+),s=([^,]*),i=(%d+)"); +local n=n(p); +t=c(t); +if not a or not t or not n then +return false,"Could not parse server_first_message"; +elseif a:find(s,3,true)~=3 then +return false,"nonce sent by server does not match our nonce"; +elseif a==d then +return false,"server did not append s-nonce to nonce"; +end +local s=l..u; +local s="c="..i(s); +local s=s..","..a; +local a=w(f(o.password),t,n); +local t=e(a,"Client Key"); +local n=y(t); +local o=r..","..h..","..s; +local n=e(n,o); +local t=m(t,n); +local a=e(a,"Server Key"); +local e=e(a,o); +local t="p="..i(t); +local t=s..","..t; +local t,a=coroutine.yield(t); +if t~="success"then return false,"success-expected"end +local t=a:match("v=([^,]+)"); +if c(t)~=e then +return false,"server signature did not match"; +end +return true; +end +return function(e,t) +if e.username and(e.password or(e.client_key or e.server_key))then +if t=="SCRAM-SHA-1"then +return r,99; +elseif t=="SCRAM-SHA-1-PLUS"then +local e=e.conn:ssl()and e.conn:socket(); +if e and e.getfinished then +return r,100; +end +end +end +end +end) +package.preload['util.sasl.plain']=(function(...) +return function(e,t) +if t=="PLAIN"and e.username and e.password then +return function(e) +return"success"==coroutine.yield("\0"..e.username.."\0"..e.password); +end,5; +end +end +end) +package.preload['util.sasl.anonymous']=(function(...) +return function(t,e) +if e=="ANONYMOUS"then +return function() +return coroutine.yield()=="success"; +end,0; +end +end +end) +package.preload['verse.plugins.tls']=(function(...) +local a=require"verse"; +local t="urn:ietf:params:xml:ns:xmpp-tls"; +function a.plugins.tls(e) +local function i(o) +if e.authenticated then return;end +if o:get_child("starttls",t)and e.conn.starttls then +e:debug("Negotiating TLS..."); +e:send(a.stanza("starttls",{xmlns=t})); +return true; +elseif not e.conn.starttls and not e.secure then +e:warn("SSL libary (LuaSec) not loaded, so TLS not available"); +elseif not e.secure then +e:debug("Server doesn't offer TLS :("); +end +end +local function o(t) +if t.name=="proceed"then +e:debug("Server says proceed, handshake starting..."); +e.conn:starttls({mode="client",protocol="sslv23",options="no_sslv2"},true); +end +end +local function a(t) +if t=="ssl-handshake-complete"then +e.secure=true; +e:debug("Re-opening stream..."); +e:reopen(); +end +end +e:hook("stream-features",i,400); +e:hook("stream/"..t,o); +e:hook("status",a,400); +return true; +end +end) +package.preload['verse.plugins.sasl']=(function(...) +local n,s=require"mime".b64,require"mime".unb64; +local o="urn:ietf:params:xml:ns:xmpp-sasl"; +function verse.plugins.sasl(e) +local function h(t) +if e.authenticated then return;end +e:debug("Authenticating with SASL..."); +local t=t:get_child("mechanisms",o); +if not t then return end +local a={}; +local i={}; +for t in t:childtags("mechanism")do +t=t:get_text(); +e:debug("Server offers %s",t); +if not a[t]then +local n=t:match("[^-]+"); +local s,o=pcall(require,"util.sasl."..n:lower()); +if s then +e:debug("Loaded SASL %s module",n); +a[t],i[t]=o(e,t); +elseif not tostring(o):match("not found")then +e:debug("Loading failed: %s",tostring(o)); +end +end +end +local t={}; +for e in pairs(a)do +table.insert(t,e); +end +if not t[1]then +e:event("authentication-failure",{condition="no-supported-sasl-mechanisms"}); +e:close(); +return; +end +table.sort(t,function(e,t)return i[e]>i[t];end); +local t,i=t[1]; +e:debug("Selecting %s mechanism...",t); +e.sasl_mechanism=coroutine.wrap(a[t]); +i=e:sasl_mechanism(t); +local t=verse.stanza("auth",{xmlns=o,mechanism=t}); +if i then +t:text(n(i)); +end +e:send(t); +return true; +end +local function i(t) +if t.name=="failure"then +local a=t.tags[1]; +local t=t:get_child_text("text"); +e:event("authentication-failure",{condition=a.name,text=t}); +e:close(); +return false; +end +local t,a=e.sasl_mechanism(t.name,s(t:get_text())); +if not t then +e:event("authentication-failure",{condition=a}); +e:close(); +return false; +elseif t==true then +e:event("authentication-success"); +e.authenticated=true +e:reopen(); +else +e:send(verse.stanza("response",{xmlns=o}):text(n(t))); +end +return true; +end +e:hook("stream-features",h,300); +e:hook("stream/"..o,i); +return true; +end +end) +package.preload['verse.plugins.bind']=(function(...) +local t=require"verse"; +local o=require"util.jid"; +local a="urn:ietf:params:xml:ns:xmpp-bind"; +function t.plugins.bind(e) +local function i(i) +if e.bound then return;end +e:debug("Binding resource..."); +e:send_iq(t.iq({type="set"}):tag("bind",{xmlns=a}):tag("resource"):text(e.resource), +function(t) +if t.attr.type=="result"then +local t=t +:get_child("bind",a) +:get_child_text("jid"); +e.username,e.host,e.resource=o.split(t); +e.jid,e.bound=t,true; +e:event("bind-success",{jid=t}); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local o,t,a=t:get_error(); +e:event("bind-failure",{error=t,text=a,type=o}); +end +end); +end +e:hook("stream-features",i,200); +return true; +end +end) +package.preload['verse.plugins.session']=(function(...) +local a=require"verse"; +local o="urn:ietf:params:xml:ns:xmpp-session"; +function a.plugins.session(e) +local function n(t) +local t=t:get_child("session",o); +if t and not t:get_child("optional")then +local function i(t) +e:debug("Establishing Session..."); +e:send_iq(a.iq({type="set"}):tag("session",{xmlns=o}), +function(t) +if t.attr.type=="result"then +e:event("session-success"); +elseif t.attr.type=="error"then +local a=t:child_with_name("error"); +local t,o,a=t:get_error(); +e:event("session-failure",{error=o,text=a,type=t}); +end +end); +return true; +end +e:hook("bind-success",i); +end +end +e:hook("stream-features",n); +return true; +end +end) +package.preload['verse.plugins.legacy']=(function(...) +local a=require"verse"; +local n=require"util.uuid".generate; +local i="jabber:iq:auth"; +function a.plugins.legacy(e) +function handle_auth_form(t) +local o=t:get_child("query",i); +if t.attr.type~="result"or not o then +local o,a,t=t:get_error(); +e:debug("warn","%s %s: %s",o,a,t); +end +local t={ +username=e.username; +password=e.password; +resource=e.resource or n(); +digest=false,sequence=false,token=false; +}; +local i=a.iq({to=e.host,type="set"}) +:tag("query",{xmlns=i}); +if#o>0 then +for a in o:childtags()do +local a=a.name; +local o=t[a]; +if o then +i:tag(a):text(t[a]):up(); +elseif o==nil then +local t="feature-not-implemented"; +e:event("authentication-failure",{condition=t}); +return false; +end +end +else +for t,e in pairs(t)do +if e then +i:tag(t):text(e):up(); +end +end +end +e:send_iq(i,function(a) +if a.attr.type=="result"then +e.resource=t.resource; +e.jid=t.username.."@"..e.host.."/"..t.resource; +e:event("authentication-success"); +e:event("bind-success",e.jid); +else +local a,t,a=a:get_error(); +e:event("authentication-failure",{condition=t}); +end +end); +end +function handle_opened(t) +if not t.version then +e:send_iq(a.iq({type="get"}) +:tag("query",{xmlns="jabber:iq:auth"}) +:tag("username"):text(e.username), +handle_auth_form); +end +end +e:hook("opened",handle_opened); +end +end) +package.preload['verse.plugins.compression']=(function(...) +local a=require"verse"; +local e=require"zlib"; +local t="http://jabber.org/features/compress" +local t="http://jabber.org/protocol/compress" +local o="http://etherx.jabber.org/streams"; +local i=9; +local function l(o) +local i,e=pcall(e.deflate,i); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +o:send(t); +o:error("Failed to create zlib.deflate filter: %s",tostring(e)); +return +end +return e +end +local function d(o) +local i,e=pcall(e.inflate); +if i==false then +local t=a.stanza("failure",{xmlns=t}):tag("setup-failed"); +o:send(t); +o:error("Failed to create zlib.inflate filter: %s",tostring(e)); +return +end +return e +end +local function h(e,o) +function e:send(i) +local i,o,n=pcall(o,tostring(i),'sync'); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +e:warn("Compressed send failed: %s",tostring(o)); +return; +end +e.conn:write(o); +end; +end +local function r(e,i) +local s=e.data +e.data=function(n,o) +e:debug("Decompressing data..."); +local i,o,h=pcall(i,o); +if i==false then +e:close({ +condition="undefined-condition"; +text=o; +extra=a.stanza("failure",{xmlns=t}):tag("processing-failed"); +}); +stream:warn("%s",tostring(o)); +return; +end +return s(n,o); +end; +end +function a.plugins.compression(e) +local function i(o) +if not e.compressed then +local o=o:child_with_name("compression"); +if o then +for o in o:children()do +local o=o[1] +if o=="zlib"then +e:send(a.stanza("compress",{xmlns=t}):tag("method"):text("zlib")) +e:debug("Enabled compression using zlib.") +return true; +end +end +session:debug("Remote server supports no compression algorithm we support.") +end +end +end +local function o(t) +if t.name=="compressed"then +e:debug("Activating compression...") +local a=l(e); +if not a then return end +local t=d(e); +if not t then return end +h(e,a); +r(e,t); +e.compressed=true; +e:reopen(); +elseif t.name=="failure"then +e:warn("Failed to establish compression"); +end +end +e:hook("stream-features",i,250); +e:hook("stream/"..t,o); +end +end) +package.preload['verse.plugins.smacks']=(function(...) +local s=require"verse"; +local h=socket.gettime; +local n="urn:xmpp:sm:2"; +function s.plugins.smacks(e) +local t={}; +local a=0; +local r=h(); +local o; +local i=0; +local function d(t) +if t.attr.xmlns=="jabber:client"or not t.attr.xmlns then +i=i+1; +e:debug("Increasing handled stanzas to %d for %s",i,t:top_tag()); +end +end +function outgoing_stanza(a) +if a.name and not a.attr.xmlns then +t[#t+1]=tostring(a); +r=h(); +if not o then +o=true; +e:debug("Waiting to send ack request..."); +s.add_task(1,function() +if#t==0 then +o=false; +return; +end +local a=h()-r; +if a<1 and#t<10 then +return 1-a; +end +e:debug("Time up, sending ..."); +o=false; +e:send(s.stanza("r",{xmlns=n})); +end); +end +end +end +local function h() +e:debug("smacks: connection lost"); +e.stream_management_supported=nil; +if e.resumption_token then +e:debug("smacks: have resumption token, reconnecting in 1s..."); +e.authenticated=nil; +s.add_task(1,function() +e:connect(e.connect_host or e.host,e.connect_port or 5222); +end); +return true; +end +end +local function r() +e.resumption_token=nil; +e:unhook("disconnected",h); +end +local function l(o) +if o.name=="r"then +e:debug("Ack requested... acking %d handled stanzas",i); +e:send(s.stanza("a",{xmlns=n,h=tostring(i)})); +elseif o.name=="a"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +else +e:warn("Received bad ack for "..o.." when last ack was "..a); +end +elseif o.name=="enabled"then +if o.attr.id then +e.resumption_token=o.attr.id; +e:hook("closed",r,100); +e:hook("disconnected",h,100); +end +elseif o.name=="resumed"then +local o=tonumber(o.attr.h); +if o>a then +local i=#t; +for a=a+1,o do +table.remove(t,1); +end +e:debug("Received ack: New ack: "..o.." Last ack: "..a.." Unacked stanzas now: "..#t.." (was "..i..")"); +a=o; +end +for a=1,#t do +e:send(t[a]); +end +t={}; +e:debug("Resumed successfully"); +e:event("resumed"); +else +e:warn("Don't know how to handle "..n.."/"..o.name); +end +end +local function t() +if not e.smacks then +e:debug("smacks: sending enable"); +e:send(s.stanza("enable",{xmlns=n,resume="true"})); +e.smacks=true; +e:hook("stanza",d); +e:hook("outgoing",outgoing_stanza); +end +end +local function o(a) +if a:get_child("sm",n)then +e.stream_management_supported=true; +if e.smacks and e.bound then +e:debug("Resuming stream with %d handled stanzas",i); +e:send(s.stanza("resume",{xmlns=n, +h=i,previd=e.resumption_token})); +return true; +else +e:hook("bind-success",t,1); +end +end +end +e:hook("stream-features",o,250); +e:hook("stream/"..n,l); +end +end) +package.preload['verse.plugins.keepalive']=(function(...) +local t=require"verse"; +function t.plugins.keepalive(e) +e.keepalive_timeout=e.keepalive_timeout or 300; +t.add_task(e.keepalive_timeout,function() +e.conn:write(" "); +return e.keepalive_timeout; +end); +end +end) +package.preload['verse.plugins.disco']=(function(...) +local a=require"verse"; +local r=require("mime").b64; +local s=require("util.sha1").sha1; +local n="http://jabber.org/protocol/caps"; +local e="http://jabber.org/protocol/disco"; +local i=e.."#info"; +local o=e.."#items"; +function a.plugins.disco(e) +e:add_plugin("presence"); +local h={ +__index=function(a,e) +local t={identities={},features={}}; +if e=="identities"or e=="features"then +return a[false][e] +end +a[e]=t; +return t; +end, +}; +local t={ +__index=function(t,a) +local e={}; +t[a]=e; +return e; +end, +}; +e.disco={ +cache={}, +info=setmetatable({ +[false]={ +identities={ +{category='client',type='pc',name='Verse'}, +}, +features={ +[n]=true, +[i]=true, +[o]=true, +}, +}, +},h); +items=setmetatable({[false]={}},t); +}; +e.caps={} +e.caps.node='http://code.matthewwild.co.uk/verse/' +local function h(t,e) +if t.category0 then +self.connecting_peer_candidates=true; +local function n(t,e) +self.jingle:send_command("transport-info",a.stanza("content",{creator=self.creator,name=self.name}) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("candidate-used",{cid=t.cid})); +self.onconnect_callback=i; +self.conn=e; +end +local e=h(self.s5b_sid..self.peer..e.jid,true); +s(n,t,e); +else +e:warn("Actually, I'm going to wait for my peer to tell me its streamhost..."); +self.onconnect_callback=i; +end +end +function t:info_received(t) +e:warn("Info received"); +local n=t:child_with_name("content"); +local i=n:child_with_name("transport"); +if i:get_child("candidate-used")and not self.connecting_peer_candidates then +local t=i:child_with_name("candidate-used"); +if t then +local function r(i,e) +if self.jingle.role=="initiator"then +self.jingle.stream:send_iq(a.iq({to=i.jid,type="set"}) +:tag("query",{xmlns=l,sid=self.s5b_sid}) +:tag("activate"):text(self.jingle.peer),function(i) +if i.attr.type=="result"then +self.jingle:send_command("transport-info",a.stanza("content",n.attr) +:tag("transport",{xmlns=o,sid=self.s5b_sid}) +:tag("activated",{cid=t.attr.cid})); +self.conn=e; +self.onconnect_callback(e); +else +self.jingle.stream:error("Failed to activate bytestream"); +end +end); +end +end +self.jingle.stream:debug("CID: %s",self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]); +local t={ +self.jingle.stream.proxy65.available_streamhosts[t.attr.cid]; +}; +local e=h(self.s5b_sid..e.jid..self.peer,true); +s(r,t,e); +end +elseif i:get_child("activated")then +self.onconnect_callback(self.conn); +end +end +function t:disconnect() +if self.conn then +self.conn:close(); +end +end +function t:handle_accepted(e) +end +local t={__index=t}; +e:hook("jingle/transport/"..o,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.proxy65']=(function(...) +local e=require"util.events"; +local r=require"util.uuid"; +local h=require"util.sha1"; +local i={}; +i.__index=i; +local o="http://jabber.org/protocol/bytestreams"; +local n; +function verse.plugins.proxy65(t) +t.proxy65=setmetatable({stream=t},i); +t.proxy65.available_streamhosts={}; +local e=0; +t:hook("disco/service-discovered/proxy",function(a) +if a.type=="bytestreams"then +e=e+1; +t:send_iq(verse.iq({to=a.jid,type="get"}) +:tag("query",{xmlns=o}),function(a) +e=e-1; +if a.attr.type=="result"then +local e=a:get_child("query",o) +:get_child("streamhost").attr; +t.proxy65.available_streamhosts[e.jid]={ +jid=e.jid; +host=e.host; +port=tonumber(e.port); +}; +end +if e==0 then +t:event("proxy65/discovered-proxies",t.proxy65.available_streamhosts); +end +end); +end +end); +t:hook("iq/"..o,function(a) +local e=verse.new(nil,{ +initiator_jid=a.attr.from, +streamhosts={}, +current_host=0; +}); +for t in a.tags[1]:childtags()do +if t.name=="streamhost"then +table.insert(e.streamhosts,t.attr); +end +end +local function o() +if e.current_host<#e.streamhosts then +e.current_host=e.current_host+1; +e:connect( +e.streamhosts[e.current_host].host, +e.streamhosts[e.current_host].port +); +n(t,e,a.tags[1].attr.sid,a.attr.from,t.jid); +return true; +end +e:unhook("disconnected",o); +t:send(verse.error_reply(a,"cancel","item-not-found")); +end +function e:accept() +e:hook("disconnected",o,100); +e:hook("connected",function() +e:unhook("disconnected",o); +local e=verse.reply(a) +:tag("query",a.tags[1].attr) +:tag("streamhost-used",{jid=e.streamhosts[e.current_host].jid}); +t:send(e); +end,100); +o(); +end +function e:refuse() +end +t:event("proxy65/request",e); +end); +end +function i:new(t,s) +local e=verse.new(nil,{ +target_jid=t; +bytestream_sid=r.generate(); +}); +local a=verse.iq{type="set",to=t} +:tag("query",{xmlns=o,mode="tcp",sid=e.bytestream_sid}); +for t,e in ipairs(s or self.proxies)do +a:tag("streamhost",e):up(); +end +self.stream:send_iq(a,function(a) +if a.attr.type=="error"then +local o,t,a=a:get_error(); +e:event("connection-failed",{conn=e,type=o,condition=t,text=a}); +else +local a=a.tags[1]:get_child("streamhost-used"); +if not a then +end +e.streamhost_jid=a.attr.jid; +local a,i; +for o,t in ipairs(s or self.proxies)do +if t.jid==e.streamhost_jid then +a,i=t.host,t.port; +break; +end +end +if not(a and i)then +end +e:connect(a,i); +local function a() +e:unhook("connected",a); +local t=verse.iq{to=e.streamhost_jid,type="set"} +:tag("query",{xmlns=o,sid=e.bytestream_sid}) +:tag("activate"):text(t); +self.stream:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("connected",e); +else +end +end); +return true; +end +e:hook("connected",a,100); +n(self.stream,e,e.bytestream_sid,self.stream.jid,t); +end +end); +return e; +end +function n(i,e,a,o,t) +local t=h.sha1(a..o..t); +local function o() +e:unhook("connected",o); +return true; +end +local function a(t) +e:unhook("incoming-raw",a); +if t:sub(1,2)~="\005\000"then +return e:event("error","connection-failure"); +end +e:event("connected"); +return true; +end +local function i(o) +e:unhook("incoming-raw",i); +if o~="\005\000"then +local t="version-mismatch"; +if o:sub(1,1)=="\005"then +t="authentication-failure"; +end +return e:event("error",t); +end +e:send(string.char(5,1,0,3,#t)..t.."\0\0"); +e:hook("incoming-raw",a,100); +return true; +end +e:hook("connected",o,200); +e:hook("incoming-raw",i,100); +e:send("\005\001\000"); +end +end) +package.preload['verse.plugins.jingle_ibb']=(function(...) +local e=require"verse"; +local i=require"util.encodings".base64; +local s=require"util.uuid".generate; +local n="urn:xmpp:jingle:transports:ibb:1"; +local o="http://jabber.org/protocol/ibb"; +assert(i.encode("This is a test.")=="VGhpcyBpcyBhIHRlc3Qu","Base64 encoding failed"); +assert(i.decode("VGhpcyBpcyBhIHRlc3Qu")=="This is a test.","Base64 decoding failed"); +local t=table.concat +local a={}; +local t={__index=a}; +local function h(a) +local t=setmetatable({stream=a},t) +t=e.eventable(t); +return t; +end +function a:initiate(a,e,t) +self.block=2048; +self.stanza=t or'iq'; +self.peer=a; +self.sid=e or tostring(self):match("%x+$"); +self.iseq=0; +self.oseq=0; +local e=function(e) +return self:feed(e) +end +self.feeder=e; +print("Hooking incomming IQs"); +local a=self.stream; +a:hook("iq/"..o,e) +if t=="message"then +a:hook("message",e) +end +end +function a:open(t) +self.stream:send_iq(e.iq{to=self.peer,type="set"} +:tag("open",{ +xmlns=o, +["block-size"]=self.block, +sid=self.sid, +stanza=self.stanza +}) +,function(e) +if t then +if e.attr.type~="error"then +t(true) +else +t(false,e:get_error()) +end +end +end); +end +function a:send(n) +local a=self.stanza; +local t; +if a=="iq"then +t=e.iq{type="set",to=self.peer} +elseif a=="message"then +t=e.message{to=self.peer} +end +local e=self.oseq; +self.oseq=e+1; +t:tag("data",{xmlns=o,sid=self.sid,seq=e}) +:text(i.encode(n)); +if a=="iq"then +self.stream:send_iq(t,function(e) +self:event(e.attr.type=="result"and"drained"or"error"); +end) +else +stream:send(t) +self:event("drained"); +end +end +function a:feed(t) +if t.attr.from~=self.peer then return end +local a=t[1]; +if a.attr.sid~=self.sid then return end +local n; +if a.name=="open"then +self:event("connected"); +self.stream:send(e.reply(t)) +return true +elseif a.name=="data"then +local o=t:get_child_text("data",o); +local a=tonumber(a.attr.seq); +local n=self.iseq; +if o and a then +if a~=n then +self.stream:send(e.error_reply(t,"cancel","not-acceptable","Wrong sequence. Packet lost?")) +self:close(); +self:event("error"); +return true; +end +self.iseq=a+1; +local a=i.decode(o); +if self.stanza=="iq"then +self.stream:send(e.reply(t)) +end +self:event("incoming-raw",a); +return true; +end +elseif a.name=="close"then +self.stream:send(e.reply(t)) +self:close(); +return true +end +end +function a:close() +self.stream:unhook("iq/"..o,self.feeder) +self:event("disconnected"); +end +function e.plugins.jingle_ibb(a) +a:hook("ready",function() +a:add_disco_feature(n); +end,10); +local t={}; +function t:_setup() +local e=h(self.stream); +e.sid=self.sid or e.sid; +e.stanza=self.stanza or e.stanza; +e.block=self.block or e.block; +e:initiate(self.peer,self.sid,self.stanza); +self.conn=e; +end +function t:generate_initiate() +print("ibb:generate_initiate() as "..self.role); +local t=s(); +self.sid=t; +self.stanza='iq'; +self.block=2048; +local e=e.stanza("transport",{xmlns=n, +sid=self.sid,stanza=self.stanza,["block-size"]=self.block}); +return e; +end +function t:generate_accept(t) +print("ibb:generate_accept() as "..self.role); +local e=t.attr; +self.sid=e.sid or self.sid; +self.stanza=e.stanza or self.stanza; +self.block=e["block-size"]or self.block; +self:_setup(); +return t; +end +function t:connect(t) +if not self.conn then +self:_setup(); +end +local e=self.conn; +print("ibb:connect() as "..self.role); +if self.role=="initiator"then +e:open(function(a,...) +assert(a,table.concat({...},", ")); +t(e); +end); +else +t(e); +end +end +function t:info_received(e) +print("ibb:info_received()"); +end +function t:disconnect() +if self.conn then +self.conn:close() +end +end +function t:handle_accepted(e)end +local t={__index=t}; +a:hook("jingle/transport/"..n,function(e) +return setmetatable({ +role=e.role, +peer=e.peer, +stream=e.stream, +jingle=e, +},t); +end); +end +end) +package.preload['verse.plugins.pubsub']=(function(...) +local o=require"verse"; +local e=require"util.jid".bare; +local n=table.insert; +local i="http://jabber.org/protocol/pubsub"; +local s="http://jabber.org/protocol/pubsub#owner"; +local a="http://jabber.org/protocol/pubsub#event"; +local e="http://jabber.org/protocol/pubsub#errors"; +local e={}; +local h={__index=e}; +function o.plugins.pubsub(e) +e.pubsub=setmetatable({stream=e},h); +e:hook("message",function(t) +local o=t.attr.from; +for t in t:childtags("event",a)do +local t=t:get_child("items"); +if t then +local a=t.attr.node; +for t in t:childtags("item")do +e:event("pubsub/event",{ +from=o; +node=a; +item=t; +}); +end +end +end +end); +return true; +end +function e:create(e,a,t) +return self:service(e):node(a):create(nil,t); +end +function e:subscribe(a,o,t,e) +return self:service(a):node(o):subscribe(t,nil,e); +end +function e:publish(e,t,a,o,i) +return self:service(e):node(t):publish(a,nil,o,i); +end +local a={}; +local t={__index=a}; +function e:service(e) +return setmetatable({stream=self.stream,service=e},t) +end +local function t(e,h,r,a,s,n,t) +local e=o.iq{type=e or"get",to=h} +:tag("pubsub",{xmlns=r or i}) +if a then e:tag(a,{node=s,jid=n});end +if t then e:tag("item",{id=t~=true and t or nil});end +return e; +end +function a:subscriptions(a) +self.stream:send_iq(t(nil,self.service,nil,"subscriptions") +,a and function(o) +if o.attr.type=="result"then +local e=o:get_child("pubsub",i); +local e=e and e:get_child("subscriptions"); +local o={}; +if e then +for t in e:childtags("subscription")do +local e=self:node(t.attr.node) +e.subscription=t; +e.subscribed_jid=t.attr.jid; +n(o,e); +end +end +a(o); +else +a(false,o:get_error()); +end +end or nil); +end +function a:affiliations(a) +self.stream:send_iq(t(nil,self.service,nil,"affiliations") +,a and function(e) +if e.attr.type=="result"then +local e=e:get_child("pubsub",i); +local e=e and e:get_child("affiliations")or{}; +local t={}; +if e then +for e in e:childtags("affiliation")do +local a=self:node(e.attr.node) +a.affiliation=e; +n(t,a); +end +end +a(t); +else +a(false,e:get_error()); +end +end or nil); +end +function a:nodes(a) +self.stream:disco_items(self.service,nil,function(e,...) +if e then +for t=1,#e do +e[t]=self:node(e[t].node); +end +end +a(e,...) +end); +end +local e={}; +local o={__index=e}; +function a:node(e) +return setmetatable({stream=self.stream,service=self.service,node=e},o) +end +function h:__call(e,t) +local e=self:service(e); +return t and e:node(t)or e; +end +function e:hook(a,o) +self._hooks=self._hooks or setmetatable({},{__mode='kv'}); +local function t(e) +if(not e.service or e.from==self.service)and e.node==self.node then +return a(e) +end +end +self._hooks[a]=t; +self.stream:hook("pubsub/event",t,o); +return t; +end +function e:unhook(e) +if e then +local e=self._hooks[e]; +self.stream:unhook("pubsub/event",e); +elseif self._hooks then +for e in pairs(self._hooks)do +self.stream:unhook("pubsub/event",e); +end +end +end +function e:create(a,e) +if a~=nil then +error("Not implemented yet."); +else +self.stream:send_iq(t("set",self.service,nil,"create",self.node),e); +end +end +function e:configure(e,a) +if e~=nil then +error("Not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,e==nil and"default"or"configure",self.node),a); +end +function e:publish(e,o,a,i) +if o~=nil then +error("Node configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"publish",self.node,nil,e or true) +:add_child(a) +,i); +end +function e:subscribe(e,a,o) +e=e or self.stream.jid; +if a~=nil then +error("Subscription configuration is not implemented yet."); +end +self.stream:send_iq(t("set",self.service,nil,"subscribe",self.node,e,id) +,o); +end +function e:subscription(e) +error("Not implemented yet."); +end +function e:affiliation(e) +error("Not implemented yet."); +end +function e:unsubscribe(e,a) +e=e or self.subscribed_jid or self.stream.jid; +self.stream:send_iq(t("set",self.service,nil,"unsubscribe",self.node,e) +,a); +end +function e:configure_subscription(e,e) +error("Not implemented yet."); +end +function e:items(a,e) +if a then +self.stream:send_iq(t("get",self.service,nil,"items",self.node) +,e); +else +self.stream:disco_items(self.service,self.node,e); +end +end +function e:item(e,a) +self.stream:send_iq(t("get",self.service,nil,"items",self.node,nil,e) +,a); +end +function e:retract(a,e) +self.stream:send_iq(t("set",self.service,nil,"retract",self.node,nil,a) +,e); +end +function e:purge(a,e) +assert(not a,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,s,"purge",self.node) +,e); +end +function e:delete(e,a) +assert(not e,"Not implemented yet."); +self.stream:send_iq(t("set",self.service,s,"delete",self.node) +,a); +end +end) +package.preload['verse.plugins.pep']=(function(...) +local e=require"verse"; +local t="http://jabber.org/protocol/pubsub"; +local t=t.."#event"; +function e.plugins.pep(e) +e:add_plugin("disco"); +e:add_plugin("pubsub"); +e.pep={}; +e:hook("pubsub/event",function(t) +return e:event("pep/"..t.node,{from=t.from,item=t.item.tags[1]}); +end); +function e:hook_pep(t,o,i) +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:add_disco_feature(t.."+notify"); +end +e:hook("pep/"..t,o,i); +end +function e:unhook_pep(t,a) +e:unhook("pep/"..t,a); +local a=e.events._handlers["pep/"..t]; +if not(a)or#a==0 then +e:remove_disco_feature(t.."+notify"); +end +end +function e:publish_pep(t,a) +return e.pubsub:service(nil):node(a or t.attr.xmlns):publish(nil,nil,t) +end +end +end) +package.preload['verse.plugins.adhoc']=(function(...) +local o=require"verse"; +local n=require"lib.adhoc"; +local t="http://jabber.org/protocol/commands"; +local r="jabber:x:data"; +local a={}; +a.__index=a; +local i={}; +function o.plugins.adhoc(e) +e:add_plugin("disco"); +e:add_disco_feature(t); +function e:query_commands(a,o) +e:disco_items(a,t,function(t) +e:debug("adhoc list returned") +local a={}; +for o,t in ipairs(t)do +a[t.node]=t.name; +end +e:debug("adhoc calling callback") +return o(a); +end); +end +function e:execute_command(i,t,o) +local e=setmetatable({ +stream=e,jid=i, +command=t,callback=o +},a); +return e:execute(); +end +local function h(t,e) +if not(e)or e=="user"then return true;end +if type(e)=="function"then +return e(t); +end +end +function e:add_adhoc_command(o,a,s,h) +i[a]=n.new(o,a,s,h); +e:add_disco_item({jid=e.jid,node=a,name=o},t); +return i[a]; +end +local function s(a) +local t=a.tags[1]; +local t=t.attr.node; +local t=i[t]; +if not t then return;end +if not h(a.attr.from,t.permission)then +e:send(o.error_reply(a,"auth","forbidden","You don't have permission to execute this command"):up() +:add_child(t:cmdtag("canceled") +:tag("note",{type="error"}):text("You don't have permission to execute this command"))); +return true +end +return n.handle_cmd(t,{send=function(t)return e:send(t)end},a); +end +e:hook("iq/"..t,function(e) +local a=e.attr.type; +local t=e.tags[1].name; +if a=="set"and t=="command"then +return s(e); +end +end); +end +function a:_process_response(e) +if e.attr.type=="error"then +self.status="canceled"; +self.callback(self,{}); +return; +end +local e=e:get_child("command",t); +self.status=e.attr.status; +self.sessionid=e.attr.sessionid; +self.form=e:get_child("x",r); +self.note=e:get_child("note"); +self.callback(self); +end +function a:execute() +local e=o.iq({to=self.jid,type="set"}) +:tag("command",{xmlns=t,node=self.command}); +self.stream:send_iq(e,function(e) +self:_process_response(e); +end); +end +function a:next(e) +local t=o.iq({to=self.jid,type="set"}) +:tag("command",{ +xmlns=t, +node=self.command, +sessionid=self.sessionid +}); +if e then t:add_child(e);end +self.stream:send_iq(t,function(e) +self:_process_response(e); +end); +end +end) +package.preload['verse.plugins.presence']=(function(...) +local a=require"verse"; +function a.plugins.presence(t) +t.last_presence=nil; +t:hook("presence-out",function(e) +if not e.attr.to then +t.last_presence=e; +end +end,1); +function t:resend_presence() +if last_presence then +t:send(last_presence); +end +end +function t:set_status(e) +local a=a.presence(); +if type(e)=="table"then +if e.show then +a:tag("show"):text(e.show):up(); +end +if e.prio then +a:tag("priority"):text(tostring(e.prio)):up(); +end +if e.msg then +a:tag("status"):text(e.msg):up(); +end +end +t:send(a); +end +end +end) +package.preload['verse.plugins.private']=(function(...) +local a=require"verse"; +local o="jabber:iq:private"; +function a.plugins.private(s) +function s:private_set(n,i,e,s) +local t=a.iq({type="set"}) +:tag("query",{xmlns=o}); +if e then +if e.name==n and e.attr and e.attr.xmlns==i then +t:add_child(e); +else +t:tag(n,{xmlns=i}) +:add_child(e); +end +end +self:send_iq(t,s); +end +function s:private_get(i,t,n) +self:send_iq(a.iq({type="get"}) +:tag("query",{xmlns=o}) +:tag(i,{xmlns=t}), +function(e) +if e.attr.type=="result"then +local e=e:get_child("query",o); +local e=e:get_child(i,t); +n(e); +end +end); +end +end +end) +package.preload['verse.plugins.roster']=(function(...) +local i=require"verse"; +local r=require"util.jid".bare; +local a="jabber:iq:roster"; +local o="urn:xmpp:features:rosterver"; +local n=table.insert; +function i.plugins.roster(t) +local s=false; +local e={ +items={}; +ver=""; +}; +t.roster=e; +t:hook("stream-features",function(e) +if e:get_child("ver",o)then +s=true; +end +end); +local function h(t) +local e=i.stanza("item",{xmlns=a}); +for a,t in pairs(t)do +if a~="groups"then +e.attr[a]=t; +else +for a=1,#t do +e:tag("group"):text(t[a]):up(); +end +end +end +return e; +end +local function d(t) +local e={}; +local a={}; +e.groups=a; +local o=t.attr.jid; +for t,a in pairs(t.attr)do +if t~="xmlns"then +e[t]=a +end +end +for e in t:childtags("group")do +n(a,e:get_text()) +end +return e; +end +function e:load(t) +e.ver,e.items=t.ver,t.items; +end +function e:dump() +return{ +ver=e.ver, +items=e.items, +}; +end +function e:add_contact(o,s,n,e) +local o={jid=o,name=s,groups=n}; +local a=i.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(o)); +t:send_iq(a,function(t) +if not e then return end +if t.attr.type=="result"then +e(true); +else +local o,t,a=t:get_error(); +e(nil,{o,t,a}); +end +end); +end +function e:delete_contact(o,n) +o=(type(o)=="table"and o.jid)or o; +local s={jid=o,subscription="remove"} +if not e.items[o]then return false,"item-not-found";end +t:send_iq(i.iq({type="set"}) +:tag("query",{xmlns=a}) +:add_child(h(s)), +function(e) +if not n then return end +if e.attr.type=="result"then +n(true); +else +local a,t,e=e:get_error(); +n(nil,{a,t,e}); +end +end); +end +local function h(t) +local t=d(t); +e.items[t.jid]=t; +end +local function d(t) +local a=e.items[t]; +e.items[t]=nil; +return a; +end +function e:fetch(o) +t:send_iq(i.iq({type="get"}):tag("query",{xmlns=a,ver=s and e.ver or nil}), +function(t) +if t.attr.type=="result"then +local t=t:get_child("query",a); +if t then +e.items={}; +for t in t:childtags("item")do +h(t) +end +e.ver=t.attr.ver or""; +end +o(e); +else +local e,t,a=stanza:get_error(); +o(nil,{e,t,a}); +end +end); +end +t:hook("iq/"..a,function(o) +local s,n=o.attr.type,o.attr.from; +if s=="set"and(not n or n==r(t.jid))then +local s=o:get_child("query",a); +local n=s and s:get_child("item"); +if n then +local o,a; +local i=n.attr.jid; +if n.attr.subscription=="remove"then +o="removed" +a=d(i); +else +o=e.items[i]and"changed"or"added"; +h(n) +a=e.items[i]; +end +e.ver=s.attr.ver; +if a then +t:event("roster/item-"..o,a); +end +end +t:send(i.reply(o)) +return true; +end +end); +end +end) +package.preload['verse.plugins.register']=(function(...) +local t=require"verse"; +local i="jabber:iq:register"; +function t.plugins.register(e) +local function a(o) +if o:get_child("register","http://jabber.org/features/iq-register")then +local t=t.iq({to=e.host_,type="set"}) +:tag("query",{xmlns=i}) +:tag("username"):text(e.username):up() +:tag("password"):text(e.password):up(); +if e.register_email then +t:tag("email"):text(e.register_email):up(); +end +e:send_iq(t,function(t) +if t.attr.type=="result"then +e:event("registration-success"); +else +local a,t,o=t:get_error(); +e:debug("Registration failed: %s",t); +e:event("registration-failure",{type=a,condition=t,text=o}); +end +end); +else +e:debug("In-band registration not offered by server"); +e:event("registration-failure",{condition="service-unavailable"}); +end +e:unhook("stream-features",a); +return true; +end +e:hook("stream-features",a,310); +end +end) +package.preload['verse.plugins.groupchat']=(function(...) +local i=require"verse"; +local e=require"events"; +local n=require"util.jid"; +local a={}; +a.__index=a; +local s="urn:xmpp:delay"; +local h="http://jabber.org/protocol/muc"; +function i.plugins.groupchat(o) +o:add_plugin("presence") +o.rooms={}; +o:hook("stanza",function(e) +local a=n.bare(e.attr.from); +if not a then return end +local t=o.rooms[a] +if not t and e.attr.to and a then +t=o.rooms[e.attr.to.." "..a] +end +if t and t.opts.source and e.attr.to~=t.opts.source then return end +if t then +local o=select(3,n.split(e.attr.from)); +local n=e:get_child_text("body"); +local i=e:get_child("delay",s); +local a={ +room_jid=a; +room=t; +sender=t.occupants[o]; +nick=o; +body=n; +stanza=e; +delay=(i and i.attr.stamp); +}; +local t=t:event(e.name,a); +return t or(e.name=="message")or nil; +end +end,500); +function o:join_room(n,s,t) +if not s then +return false,"no nickname supplied" +end +t=t or{}; +local e=setmetatable(i.eventable{ +stream=o,jid=n,nick=s, +subject=nil, +occupants={}, +opts=t, +},a); +if t.source then +self.rooms[t.source.." "..n]=e; +else +self.rooms[n]=e; +end +local a=e.occupants; +e:hook("presence",function(o) +local t=o.nick or s; +if not a[t]and o.stanza.attr.type~="unavailable"then +a[t]={ +nick=t; +jid=o.stanza.attr.from; +presence=o.stanza; +}; +local o=o.stanza:get_child("x",h.."#user"); +if o then +local e=o:get_child("item"); +if e and e.attr then +a[t].real_jid=e.attr.jid; +a[t].affiliation=e.attr.affiliation; +a[t].role=e.attr.role; +end +end +if t==e.nick then +e.stream:event("groupchat/joined",e); +else +e:event("occupant-joined",a[t]); +end +elseif a[t]and o.stanza.attr.type=="unavailable"then +if t==e.nick then +e.stream:event("groupchat/left",e); +if e.opts.source then +self.rooms[e.opts.source.." "..n]=nil; +else +self.rooms[n]=nil; +end +else +a[t].presence=o.stanza; +e:event("occupant-left",a[t]); +a[t]=nil; +end +end +end); +e:hook("message",function(a) +local t=a.stanza:get_child_text("subject"); +if not t then return end +t=#t>0 and t or nil; +if t~=e.subject then +local o=e.subject; +e.subject=t; +return e:event("subject-changed",{from=o,to=t,by=a.sender,event=a}); +end +end,2e3); +local t=i.presence():tag("x",{xmlns=h}):reset(); +self:event("pre-groupchat/joining",t); +e:send(t) +self:event("groupchat/joining",e); +return e; +end +o:hook("presence-out",function(e) +if not e.attr.to then +for a,t in pairs(o.rooms)do +t:send(e); +end +e.attr.to=nil; +end +end); +end +function a:send(e) +if e.name=="message"and not e.attr.type then +e.attr.type="groupchat"; +end +if e.name=="presence"then +e.attr.to=self.jid.."/"..self.nick; +end +if e.attr.type=="groupchat"or not e.attr.to then +e.attr.to=self.jid; +end +if self.opts.source then +e.attr.from=self.opts.source +end +self.stream:send(e); +end +function a:send_message(e) +self:send(i.message():tag("body"):text(e)); +end +function a:set_subject(e) +self:send(i.message():tag("subject"):text(e)); +end +function a:leave(e) +self.stream:event("groupchat/leaving",self); +local t=i.presence({type="unavailable"}); +if e then +t:tag("status"):text(e); +end +self:send(t); +end +function a:admin_set(e,t,o,a) +self:send(i.iq({type="set"}) +:query(h.."#admin") +:tag("item",{nick=e,[t]=o}) +:tag("reason"):text(a or"")); +end +function a:set_role(e,t,a) +self:admin_set(e,"role",t,a); +end +function a:set_affiliation(t,a,e) +self:admin_set(t,"affiliation",a,e); +end +function a:kick(t,e) +self:set_role(t,"none",e); +end +function a:ban(t,e) +self:set_affiliation(t,"outcast",e); +end +end) +package.preload['verse.plugins.vcard']=(function(...) +local i=require"verse"; +local o=require"util.vcard"; +local n="vcard-temp"; +function i.plugins.vcard(a) +function a:get_vcard(t,e) +a:send_iq(i.iq({to=t,type="get"}) +:tag("vCard",{xmlns=n}),e and function(t) +local a,a; +vCard=t:get_child("vCard",n); +if t.attr.type=="result"and vCard then +vCard=o.from_xep54(vCard) +e(vCard) +else +e(false) +end +end or nil); +end +function a:set_vcard(e,n) +local t; +if type(e)=="table"and e.name then +t=e; +elseif type(e)=="string"then +t=o.to_xep54(o.from_text(e)[1]); +elseif type(e)=="table"then +t=o.to_xep54(e); +error("Converting a table to vCard not implemented") +end +if not t then return false end +a:debug("setting vcard to %s",tostring(t)); +a:send_iq(i.iq({type="set"}) +:add_child(t),n); +end +end +end) +package.preload['verse.plugins.vcard_update']=(function(...) +local n=require"verse"; +local e,i="vcard-temp","vcard-temp:x:update"; +local e,t=pcall(function()return require("util.hashes").sha1;end); +if not e then +e,t=pcall(function()return require("util.sha1").sha1;end); +if not e then +error("Could not find a sha1()") +end +end +local h=t; +local e,t=pcall(function() +local e=require("util.encodings").base64.decode; +assert(e("SGVsbG8=")=="Hello") +return e; +end); +if not e then +e,t=pcall(function()return require("mime").unb64;end); +if not e then +error("Could not find a base64 decoder") +end +end +local s=t; +function n.plugins.vcard_update(e) +e:add_plugin("vcard"); +e:add_plugin("presence"); +local t; +function update_vcard_photo(a) +local o; +for e=1,#a do +if a[e].name=="PHOTO"then +o=a[e][1]; +break +end +end +if o then +local a=h(s(o),true); +t=n.stanza("x",{xmlns=i}) +:tag("photo"):text(a); +e:resend_presence() +else +t=nil; +end +end +local a=e.set_vcard; +local a; +e:hook("ready",function(t) +if a then return;end +a=true; +e:get_vcard(nil,function(t) +if t then +update_vcard_photo(t) +end +e:event("ready"); +end); +return true; +end,3); +e:hook("presence-out",function(e) +if t and not e:get_child("x",i)then +e:add_child(t); +end +end,10); +end +end) +package.preload['verse.plugins.carbons']=(function(...) +local o=require"verse"; +local a="urn:xmpp:carbons:2"; +local n="urn:xmpp:forward:0"; +local s=os.time; +local r=require"util.datetime".parse; +local h=require"util.jid".bare; +function o.plugins.carbons(e) +local t={}; +t.enabled=false; +e.carbons=t; +function t:enable(i) +e:send_iq(o.iq{type="set"} +:tag("enable",{xmlns=a}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=true; +end +if i then +i(e); +end +end or nil); +end +function t:disable(i) +e:send_iq(o.iq{type="set"} +:tag("disable",{xmlns=a}) +,function(e) +local e=e.attr.type=="result"; +if e then +t.enabled=false; +end +if i then +i(e); +end +end or nil); +end +local o; +e:hook("bind-success",function() +o=h(e.jid); +end); +e:hook("message",function(i) +local t=i:get_child(nil,a); +if i.attr.from==o and t then +local o=t.name; +local t=t:get_child("forwarded",n); +local a=t and t:get_child("message","jabber:client"); +local t=t:get_child("delay","urn:xmpp:delay"); +local t=t and t.attr.stamp; +t=t and r(t); +if a then +return e:event("carbon",{ +dir=o, +stanza=a, +timestamp=t or s(), +}); +end +end +end,1); +end +end) +package.preload['verse.plugins.archive']=(function(...) +local e=require"verse"; +local t=require"util.stanza"; +local a="urn:xmpp:mam:0" +local d="urn:xmpp:forward:0"; +local l="urn:xmpp:delay"; +local i=require"util.uuid".generate; +local u=require"util.datetime".parse; +local o=require"util.datetime".datetime; +local n=require"util.dataforms".new; +local h=require"util.rsm"; +local c={}; +local m=n{ +{name="FORM_TYPE";type="hidden";value=a;}; +{name="with";type="jid-single";}; +{name="start";type="text-single"}; +{name="end";type="text-single";}; +}; +function e.plugins.archive(n) +function n:query_archive(n,e,r) +local s=i(); +local i=t.iq{type="set",to=n} +:tag("query",{xmlns=a,queryid=s}); +local n,t=tonumber(e["start"]),tonumber(e["end"]); +e["start"]=n and o(n); +e["end"]=t and o(t); +i:add_child(m:form(e,"submit")); +i:add_child(h.generate(e)); +local o={}; +local function n(i) +local e=i:get_child("fin",a) +if e and e.attr.queryid==s then +local e=h.get(e); +for t,e in pairs(e or c)do o[t]=e;end +self:unhook("message",n); +r(o); +return true +end +local t=i:get_child("result",a); +if t and t.attr.queryid==s then +local e=t:get_child("forwarded",d); +e=e or i:get_child("forwarded",d); +local a=t.attr.id; +local t=e:get_child("delay",l); +local t=t and u(t.attr.stamp)or nil; +local e=e:get_child("message","jabber:client") +o[#o+1]={id=a,stamp=t,message=e}; +return true +end +end +self:hook("message",n,1); +self:send_iq(i,function(e) +if e.attr.type=="error"then +self:warn(table.concat({e:get_error()}," ")) +self:unhook("message",n); +r(false,e:get_error()) +end +return true +end); +end +local i={ +always=true,[true]="always", +never=false,[false]="never", +roster="roster", +} +local function h(t) +local e={}; +local a=t.attr.default; +if a then +e[false]=i[a]; +end +local a=t:get_child("always"); +if a then +for t in a:childtags("jid")do +local t=t:get_text(); +e[t]=true; +end +end +local t=t:get_child("never"); +if t then +for t in t:childtags("jid")do +local t=t:get_text(); +e[t]=false; +end +end +return e; +end +local function s(o) +local e +e,o[false]=o[false],nil; +if e~=nil then +e=i[e]; +end +local i=t.stanza("prefs",{xmlns=a,default=e}) +local a=t.stanza("always"); +local e=t.stanza("never"); +for t,o in pairs(o)do +(o and a or e):tag("jid"):text(t):up(); +end +return i:add_child(a):add_child(e); +end +function n:archive_prefs_get(o) +self:send_iq(t.iq{type="get"}:tag("prefs",{xmlns=a}), +function(e) +if e and e.attr.type=="result"and e.tags[1]then +local t=h(e.tags[1]); +o(t,e); +else +o(nil,e); +end +end); +end +function n:archive_prefs_set(a,e) +self:send_iq(t.iq{type="set"}:add_child(s(a)),e); +end +end +end) +package.preload['util.http']=(function(...) +local t,s=string.format,string.char; +local d,r,n=pairs,ipairs,tonumber; +local i,o=table.insert,table.concat; +local function h(e) +return e and(e:gsub("[^a-zA-Z0-9.~_-]",function(e)return t("%%%02x",e:byte());end)); +end +local function a(e) +return e and(e:gsub("%%(%x%x)",function(e)return s(n(e,16));end)); +end +local function e(e) +return e and(e:gsub("%W",function(e) +if e~=" "then +return t("%%%02x",e:byte()); +else +return"+"; +end +end)); +end +local function s(t) +local a={}; +if t[1]then +for o,t in r(t)do +i(a,e(t.name).."="..e(t.value)); +end +else +for o,t in d(t)do +i(a,e(o).."="..e(t)); +end +end +return o(a,"&"); +end +local function n(e) +if not e:match("=")then return a(e);end +local o={}; +for e,t in e:gmatch("([^=&]*)=([^&]*)")do +e,t=e:gsub("%+","%%20"),t:gsub("%+","%%20"); +e,t=a(e),a(t); +i(o,{name=e,value=t}); +o[e]=t; +end +return o; +end +local function o(e,t) +e=","..e:gsub("[ \t]",""):lower()..","; +return e:find(","..t:lower()..",",1,true)~=nil; +end +return{ +urlencode=h,urldecode=a; +formencode=s,formdecode=n; +contains_token=o; +}; +end) +package.preload['net.http.parser']=(function(...) +local m=tonumber; +local a=assert; +local v=require"socket.url".parse; +local t=require"util.http".urldecode; +local function b(e) +e=t((e:gsub("//+","/"))); +if e:sub(1,1)~="/"then +e="/"..e; +end +local t=0; +for e in e:gmatch("([^/]+)/")do +if e==".."then +t=t-1; +elseif e~="."then +t=t+1; +end +if t<0 then +return nil; +end +end +return e; +end +local y={}; +function y.new(u,r,e,y) +local d=true; +if not e or e=="server"then d=false;else a(e=="client","Invalid parser type");end +local e=""; +local p,o,h; +local s=nil; +local t; +local a; +local c; +local n; +return{ +feed=function(l,i) +if n then return nil,"parse has failed";end +if not i then +if s and d and not a then +t.body=e; +u(t); +elseif e~=""then +n=true;return r(); +end +return; +end +e=e..i; +while#e>0 do +if s==nil then +local f=e:find("\r\n\r\n",nil,true); +if not f then return;end +local w,h,l,i,g; +local u; +local o={}; +for t in e:sub(1,f+1):gmatch("([^\r\n]+)\r\n")do +if u then +local e,t=t:match("^([^%s:]+): *(.*)$"); +if not e then n=true;return r("invalid-header-line");end +e=e:lower(); +o[e]=o[e]and o[e]..","..t or t; +else +u=t; +if d then +l,i,g=t:match("^HTTP/(1%.[01]) (%d%d%d) (.*)$"); +i=m(i); +if not i then n=true;return r("invalid-status-line");end +c=not +((y and y().method=="HEAD") +or(i==204 or i==304 or i==301) +or(i>=100 and i<200)); +else +w,h,l=t:match("^(%w+) (%S+) HTTP/(1%.[01])$"); +if not w then n=true;return r("invalid-status-line");end +end +end +end +if not u then n=true;return r("invalid-status-line");end +p=c and o["transfer-encoding"]=="chunked"; +a=m(o["content-length"]); +if d then +if not c then a=0;end +t={ +code=i; +httpversion=l; +headers=o; +body=c and""or nil; +responseversion=l; +responseheaders=o; +}; +else +local e; +if h:byte()==47 then +local a,t=h:match("([^?]*).?(.*)"); +if t==""then t=nil;end +e={path=a,query=t}; +else +e=v(h); +if not(e and e.path)then n=true;return r("invalid-url");end +end +h=b(e.path); +o.host=e.host or o.host; +a=a or 0; +t={ +method=w; +url=e; +path=h; +httpversion=l; +headers=o; +body=nil; +}; +end +e=e:sub(f+4); +s=true; +end +if s then +if d then +if p then +if not e:find("\r\n",nil,true)then +return; +end +if not o then +o,h=e:match("^(%x+)[^\r\n]*\r\n()"); +o=o and m(o,16); +if not o then n=true;return r("invalid-chunk-size");end +end +if o==0 and e:find("\r\n\r\n",h-2,true)then +s,o=nil,nil; +e=e:gsub("^.-\r\n\r\n",""); +u(t); +elseif#e-h-2>=o then +t.body=t.body..e:sub(h,h+(o-1)); +e=e:sub(h+o+2); +o,h=nil,nil; +else +break; +end +elseif a and#e>=a then +if t.code==101 then +t.body,e=e,""; +else +t.body,e=e:sub(1,a),e:sub(a+1); +end +s=nil;u(t); +else +break; +end +elseif#e>=a then +t.body,e=e:sub(1,a),e:sub(a+1); +s=nil;u(t); +else +break; +end +end +end +end; +}; +end +return y; +end) +package.preload['net.http']=(function(...) +local b=require"socket" +local y=require"util.encodings".base64.encode; +local l=require"socket.url" +local d=require"net.http.parser".new; +local h=require"util.http"; +local j=pcall(require,"ssl"); +local x=require"net.server" +local u,o=table.insert,table.concat; +local m=pairs; +local k,c,q,g,s= +tonumber,tostring,xpcall,select,debug.traceback; +local v,p=assert,error +local r=require"util.logger".init("http"); +module"http" +local i={}; +local n={default_port=80,default_mode="*a"}; +function n.onconnect(t) +local e=i[t]; +local a={e.method or"GET"," ",e.path," HTTP/1.1\r\n"}; +if e.query then +u(a,4,"?"..e.query); +end +t:write(o(a)); +local a={[2]=": ",[4]="\r\n"}; +for e,i in m(e.headers)do +a[1],a[3]=e,i; +t:write(o(a)); +end +t:write("\r\n"); +if e.body then +t:write(e.body); +end +end +function n.onincoming(a,t) +local e=i[a]; +if not e then +r("warn","Received response from connection %s with no request attached!",c(a)); +return; +end +if t and e.reader then +e:reader(t); +end +end +function n.ondisconnect(t,a) +local e=i[t]; +if e and e.conn then +e:reader(nil,a); +end +i[t]=nil; +end +function n.ondetach(e) +i[e]=nil; +end +local function w(e,o,i) +if not e.parser then +local function a(t) +if e.callback then +e.callback(t or"connection-closed",0,e); +e.callback=nil; +end +destroy_request(e); +end +if not o then +a(i); +return; +end +local function o(t) +if e.callback then +e.callback(t.body,t.code,t,e); +e.callback=nil; +end +destroy_request(e); +end +local function t() +return e; +end +e.parser=d(o,a,"client",t); +end +e.parser:feed(o); +end +local function f(e)r("error","Traceback[http]: %s",s(c(e),2));end +function request(e,t,u) +local e=l.parse(e); +if not(e and e.host)then +u(nil,0,e); +return nil,"invalid-url"; +end +if not e.path then +e.path="/"; +end +local d,o,s; +local l,a=e.host,e.port; +local h=l; +if(a=="80"and e.scheme=="http") +or(a=="443"and e.scheme=="https")then +a=nil; +elseif a then +h=h..":"..a; +end +o={ +["Host"]=h; +["User-Agent"]="Prosody XMPP Server"; +}; +if e.userinfo then +o["Authorization"]="Basic "..y(e.userinfo); +end +if t then +e.onlystatus=t.onlystatus; +s=t.body; +if s then +d="POST"; +o["Content-Length"]=c(#s); +o["Content-Type"]="application/x-www-form-urlencoded"; +end +if t.method then d=t.method;end +if t.headers then +for t,e in m(t.headers)do +o[t]=e; +end +end +end +e.method,e.headers,e.body=d,o,s; +local o=e.scheme=="https"; +if o and not j then +p("SSL not available, unable to contact https URL"); +end +local h=a and k(a)or(o and 443 or 80); +local a=b.tcp(); +a:settimeout(10); +local d,s=a:connect(l,h); +if not d and s~="timeout"then +u(nil,0,e); +return nil,s; +end +local s=false; +if o then +s=t and t.sslctx or{mode="client",protocol="sslv23",options={"no_sslv2","no_sslv3"}}; +end +e.handler,e.conn=v(x.wrapclient(a,l,h,n,"*a",s)); +e.write=function(...)return e.handler:write(...);end +e.callback=function(i,t,o,a)r("debug","Calling callback, status %s",t or"---");return g(2,q(function()return u(i,t,o,a)end,f));end +e.reader=w; +e.state="status"; +i[e.handler]=e; +return e; +end +function destroy_request(e) +if e.conn then +e.conn=nil; +e.handler:close() +end +end +local e,t=h.urlencode,h.urldecode; +local o,a=h.formencode,h.formdecode; +_M.urlencode,_M.urldecode=e,t; +_M.formencode,_M.formdecode=o,a; +return _M; +end) +package.preload['verse.bosh']=(function(...) +local n=require"util.xmppstream".new; +local a=require"util.stanza"; +require"net.httpclient_listener"; +local i=require"net.http"; +local e=setmetatable({},{__index=verse.stream_mt}); +e.__index=e; +local s="http://etherx.jabber.org/streams"; +local h="http://jabber.org/protocol/httpbind"; +local o=5; +function verse.new_bosh(a,t) +local t={ +bosh_conn_pool={}; +bosh_waiting_requests={}; +bosh_rid=math.random(1,999999); +bosh_outgoing_buffer={}; +bosh_url=t; +conn={}; +}; +function t:reopen() +self.bosh_need_restart=true; +self:flush(); +end +local t=verse.new(a,t); +return setmetatable(t,e); +end +function e:connect() +self:_send_session_request(); +end +function e:send(e) +self:debug("Putting into BOSH send buffer: %s",tostring(e)); +self.bosh_outgoing_buffer[#self.bosh_outgoing_buffer+1]=a.clone(e); +self:flush(); +end +function e:flush() +if self.connected +and#self.bosh_waiting_requests0 +or self.bosh_need_restart)then +self:debug("Flushing..."); +local t=self:_make_body(); +local e=self.bosh_outgoing_buffer; +for o,a in ipairs(e)do +t:add_child(a); +e[o]=nil; +end +self:_make_request(t); +else +self:debug("Decided not to flush."); +end +end +function e:_make_request(a) +local e,t=i.request(self.bosh_url,{body=tostring(a)},function(i,e,t) +if e~=0 then +self.inactive_since=nil; +return self:_handle_response(i,e,t); +end +local e=os.time(); +if not self.inactive_since then +self.inactive_since=e; +elseif e-self.inactive_since>self.bosh_max_inactivity then +return self:_disconnected(); +else +self:debug("%d seconds left to reconnect, retrying in %d seconds...", +self.bosh_max_inactivity-(e-self.inactive_since),o); +end +timer.add_task(o,function() +self:debug("Retrying request..."); +for a,e in ipairs(self.bosh_waiting_requests)do +if e==t then +table.remove(self.bosh_waiting_requests,a); +break; +end +end +self:_make_request(a); +end); +end); +if e then +table.insert(self.bosh_waiting_requests,e); +else +self:warn("Request failed instantly: %s",t); +end +end +function e:_disconnected() +self.connected=nil; +self:event("disconnected"); +end +function e:_send_session_request() +local e=self:_make_body(); +e.attr.hold="1"; +e.attr.wait="60"; +e.attr["xml:lang"]="en"; +e.attr.ver="1.6"; +e.attr.from=self.jid; +e.attr.to=self.host; +e.attr.secure='true'; +i.request(self.bosh_url,{body=tostring(e)},function(t,e) +if e==0 then +return self:_disconnected(); +end +local e=self:_parse_response(t) +if not e then +self:warn("Invalid session creation response"); +self:_disconnected(); +return; +end +self.bosh_sid=e.attr.sid; +self.bosh_wait=tonumber(e.attr.wait); +self.bosh_hold=tonumber(e.attr.hold); +self.bosh_max_inactivity=tonumber(e.attr.inactivity); +self.bosh_max_requests=tonumber(e.attr.requests)or self.bosh_hold; +self.connected=true; +self:event("connected"); +self:_handle_response_payload(e); +end); +end +function e:_handle_response(t,a,e) +if self.bosh_waiting_requests[1]~=e then +self:warn("Server replied to request that wasn't the oldest"); +for a,t in ipairs(self.bosh_waiting_requests)do +if t==e then +self.bosh_waiting_requests[a]=nil; +break; +end +end +else +table.remove(self.bosh_waiting_requests,1); +end +local e=self:_parse_response(t); +if e then +self:_handle_response_payload(e); +end +self:flush(); +end +function e:_handle_response_payload(t) +local e=t.tags; +for t=1,#e do +local e=e[t]; +if e.attr.xmlns==s then +self:event("stream-"..e.name,e); +elseif e.attr.xmlns then +self:event("stream/"..e.attr.xmlns,e); +else +self:event("stanza",e); +end +end +if t.attr.type=="terminate"then +self:_disconnected({reason=t.attr.condition}); +end +end +local a={ +stream_ns="http://jabber.org/protocol/httpbind",stream_tag="body", +default_ns="jabber:client", +streamopened=function(e,t)e.notopen=nil;e.payload=verse.stanza("body",t);return true;end; +handlestanza=function(t,e)t.payload:add_child(e);end; +}; +function e:_parse_response(e) +self:debug("Parsing response: %s",e); +if e==nil then +self:debug("%s",debug.traceback()); +self:_disconnected(); +return; +end +local t={notopen=true,stream=self}; +local a=n(t,a); +a:feed(e); +return t.payload; +end +function e:_make_body() +self.bosh_rid=self.bosh_rid+1; +local e=verse.stanza("body",{ +xmlns=h; +content="text/xml; charset=utf-8"; +sid=self.bosh_sid; +rid=self.bosh_rid; +}); +if self.bosh_need_restart then +self.bosh_need_restart=nil; +e.attr.restart='true'; +end +return e; +end +end) +package.preload['verse.client']=(function(...) +local t=require"verse"; +local o=t.stream_mt; +local s=require"util.jid".split; +local h=require"net.adns"; +local e=require"lxp"; +local a=require"util.stanza"; +t.message,t.presence,t.iq,t.stanza,t.reply,t.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local d=require"util.xmppstream".new; +local n="http://etherx.jabber.org/streams"; +local function r(e,t) +return e.priorityt.weight); +end +local i={ +stream_ns=n, +stream_tag="stream", +default_ns="jabber:client"}; +function i.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function i.streamclosed(e) +e.notopen=true; +if not e.closed then +e:send(""); +e.closed=true; +end +e:event("closed"); +return e:close("stream closed") +end +function i.handlestanza(t,e) +if e.attr.xmlns==n then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns then +return t:event("stream/"..e.attr.xmlns,e); +end +return t:event("stanza",e); +end +function i.error(a,t,e) +if a:event(t,e)==nil then +if e then +local t=e:get_child(nil,"urn:ietf:params:xml:ns:xmpp-streams"); +local e=e:get_child_text("text","urn:ietf:params:xml:ns:xmpp-streams"); +error(t.name..(e and": "..e or"")); +else +error(e and e.name or t or"unknown-error"); +end +end +end +function o:reset() +if self.stream then +self.stream:reset(); +else +self.stream=d(self,i); +end +self.notopen=true; +return true; +end +function o:connect_client(e,a) +self.jid,self.password=e,a; +self.username,self.host,self.resource=s(e); +self:add_plugin("tls"); +self:add_plugin("sasl"); +self:add_plugin("bind"); +self:add_plugin("session"); +function self.data(t,e) +local a,t=self.stream:feed(e); +if a then return;end +self:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(t),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +self:close("xml-not-well-formed"); +end +self:hook("connected",function()self:reopen();end); +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local a; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local o=e.tags[1]and e.tags[1].attr.xmlns; +if o then +a=self:event("iq/"..o,e); +if not a then +a=self:event("iq",e); +end +end +if a==nil then +self:send(t.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +a=self:event(e.name,e); +end +end +return a; +end,-1); +self:hook("outgoing",function(e) +if e.name then +self:event("stanza-out",e); +end +end); +self:hook("stanza-out",function(e) +if not e.attr.xmlns then +self:event(e.name.."-out",e); +end +end); +local function e() +self:event("ready"); +end +self:hook("session-success",e,-1) +self:hook("bind-success",e,-1); +local e=self.close; +function self:close(t) +self.close=e; +if not self.closed then +self:send(""); +self.closed=true; +else +return self:close(t); +end +end +local function t() +self:connect(self.connect_host or self.host,self.connect_port or 5222); +end +if not(self.connect_host or self.connect_port)then +h.lookup(function(a) +if a then +local e={}; +self.srv_hosts=e; +for a,t in ipairs(a)do +table.insert(e,t.srv); +end +table.sort(e,r); +local a=e[1]; +self.srv_choice=1; +if a then +self.connect_host,self.connect_port=a.target,a.port; +self:debug("Best record found, will connect to %s:%d",self.connect_host or self.host,self.connect_port or 5222); +end +self:hook("disconnected",function() +if self.srv_hosts and self.srv_choice<#self.srv_hosts then +self.srv_choice=self.srv_choice+1; +local e=e[self.srv_choice]; +self.connect_host,self.connect_port=e.target,e.port; +t(); +return true; +end +end,1e3); +self:hook("connected",function() +self.srv_hosts=nil; +end,1e3); +end +t(); +end,"_xmpp-client._tcp."..(self.host)..".","SRV"); +else +t(); +end +end +function o:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.host,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns="jabber:client",version="1.0"}):top_tag()); +end +function o:send_iq(t,a) +local e=self:new_id(); +self.tracked_iqs[e]=a; +t.attr.id=e; +self:send(t); +end +function o:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +package.preload['verse.component']=(function(...) +local o=require"verse"; +local t=o.stream_mt; +local h=require"util.jid".split; +local e=require"lxp"; +local a=require"util.stanza"; +local r=require"util.sha1".sha1; +o.message,o.presence,o.iq,o.stanza,o.reply,o.error_reply= +a.message,a.presence,a.iq,a.stanza,a.reply,a.error_reply; +local d=require"util.xmppstream".new; +local s="http://etherx.jabber.org/streams"; +local i="jabber:component:accept"; +local n={ +stream_ns=s, +stream_tag="stream", +default_ns=i}; +function n.streamopened(e,t) +e.stream_id=t.id; +if not e:event("opened",t)then +e.notopen=nil; +end +return true; +end +function n.streamclosed(e) +return e:event("closed"); +end +function n.handlestanza(t,e) +if e.attr.xmlns==s then +return t:event("stream-"..e.name,e); +elseif e.attr.xmlns or e.name=="handshake"then +return t:event("stream/"..(e.attr.xmlns or i),e); +end +return t:event("stanza",e); +end +function t:reset() +if self.stream then +self.stream:reset(); +else +self.stream=d(self,n); +end +self.notopen=true; +return true; +end +function t:connect_component(e,n) +self.jid,self.password=e,n; +self.username,self.host,self.resource=h(e); +function self.data(a,e) +local o,a=self.stream:feed(e); +if o then return;end +t:debug("debug","Received invalid XML (%s) %d bytes: %s",tostring(a),#e,e:sub(1,300):gsub("[\r\n]+"," ")); +t:close("xml-not-well-formed"); +end +self:hook("incoming-raw",function(e)return self.data(self.conn,e);end); +self.curr_id=0; +self.tracked_iqs={}; +self:hook("stanza",function(t) +local e,a=t.attr.id,t.attr.type; +if e and t.name=="iq"and(a=="result"or a=="error")and self.tracked_iqs[e]then +self.tracked_iqs[e](t); +self.tracked_iqs[e]=nil; +return true; +end +end); +self:hook("stanza",function(e) +local t; +if e.attr.xmlns==nil or e.attr.xmlns=="jabber:client"then +if e.name=="iq"and(e.attr.type=="get"or e.attr.type=="set")then +local a=e.tags[1]and e.tags[1].attr.xmlns; +if a then +t=self:event("iq/"..a,e); +if not t then +t=self:event("iq",e); +end +end +if t==nil then +self:send(o.error_reply(e,"cancel","service-unavailable")); +return true; +end +else +t=self:event(e.name,e); +end +end +return t; +end,-1); +self:hook("opened",function(e) +print(self.jid,self.stream_id,e.id); +local e=r(self.stream_id..n,true); +self:send(a.stanza("handshake",{xmlns=i}):text(e)); +self:hook("stream/"..i,function(e) +if e.name=="handshake"then +self:event("authentication-success"); +end +end); +end); +local function e() +self:event("ready"); +end +self:hook("authentication-success",e,-1); +self:connect(self.connect_host or self.host,self.connect_port or 5347); +self:reopen(); +end +function t:reopen() +self:reset(); +self:send(a.stanza("stream:stream",{to=self.jid,["xmlns:stream"]='http://etherx.jabber.org/streams', +xmlns=i,version="1.0"}):top_tag()); +end +function t:close(t) +if not self.notopen then +self:send(""); +end +local e=self.conn.disconnect(); +self.conn:close(); +e(conn,t); +end +function t:send_iq(e,a) +local t=self:new_id(); +self.tracked_iqs[t]=a; +e.attr.id=t; +self:send(e); +end +function t:new_id() +self.curr_id=self.curr_id+1; +return tostring(self.curr_id); +end +end) +pcall(require,"luarocks.require"); +local h=require"socket"; +pcall(require,"ssl"); +local a=require"net.server"; +local n=require"util.events"; +local o=require"util.logger"; +module("verse",package.seeall); +local e=_M; +_M.server=a; +local t={}; +t.__index=t; +stream_mt=t; +e.plugins={}; +function e.init(...) +for e=1,select("#",...)do +local t=pcall(require,"verse."..select(e,...)); +if not t then +error("Verse connection module not found: verse."..select(e,...)); +end +end +return e; +end +local i=0; +function e.new(o,a) +local t=setmetatable(a or{},t); +i=i+1; +t.id=tostring(i); +t.logger=o or e.new_logger("stream"..t.id); +t.events=n.new(); +t.plugins={}; +t.verse=e; +return t; +end +e.add_task=require"util.timer".add_task; +e.logger=o.init; +e.new_logger=o.init; +e.log=e.logger("verse"); +local function s(o,...) +local e,a,t=0,{...},select('#',...); +return(o:gsub("%%(.)",function(o)if e<=t then e=e+1;return tostring(a[e]);end end)); +end +function e.set_log_handler(e,t) +t=t or{"debug","info","warn","error"}; +o.reset(); +if io.type(e)=="file"then +local t=e; +function e(e,a,o) +t:write(e,"\t",a,"\t",o,"\n"); +end +end +if e then +local function i(a,o,t,...) +return e(a,o,s(t,...)); +end +for t,e in ipairs(t)do +o.add_level_sink(e,i); +end +end +end +function _default_log_handler(a,t,o) +return io.stderr:write(a,"\t",t,"\t",o,"\n"); +end +e.set_log_handler(_default_log_handler,{"error"}); +local function o(t) +e.log("error","Error: %s",t); +e.log("error","Traceback: %s",debug.traceback()); +end +function e.set_error_handler(e) +o=e; +end +function e.loop() +return xpcall(a.loop,o); +end +function e.step() +return xpcall(a.step,o); +end +function e.quit() +return a.setquitting(true); +end +function t:listen(t,e) +t=t or"localhost"; +e=e or 0; +local a,o=a.addserver(t,e,new_listener(self,"server"),"*a"); +if a then +self:debug("Bound to %s:%s",t,e); +self.server=a; +end +return a,o; +end +function t:connect(t,o) +t=t or"localhost"; +o=tonumber(o)or 5222; +local i=h.tcp() +i:settimeout(0); +local n,e=i:connect(t,o); +if not n and e~="timeout"then +self:warn("connect() to %s:%d failed: %s",t,o,e); +return self:event("disconnected",{reason=e})or false,e; +end +local t=a.wrapclient(i,t,o,new_listener(self),"*a"); +if not t then +self:warn("connection initialisation failed: %s",e); +return self:event("disconnected",{reason=e})or false,e; +end +self:set_conn(t); +return true; +end +function t:set_conn(t) +self.conn=t; +self.send=function(a,e) +self:event("outgoing",e); +e=tostring(e); +self:event("outgoing-raw",e); +return t:write(e); +end; +end +function t:close(t) +if not self.conn then +e.log("error","Attempt to close disconnected connection - possibly a bug"); +return; +end +local e=self.conn.disconnect(); +self.conn:close(); +e(self.conn,t); +end +function t:debug(...) +return self.logger("debug",...); +end +function t:info(...) +return self.logger("info",...); +end +function t:warn(...) +return self.logger("warn",...); +end +function t:error(...) +return self.logger("error",...); +end +function t:event(e,...) +self:debug("Firing event: "..tostring(e)); +return self.events.fire_event(e,...); +end +function t:hook(e,...) +return self.events.add_handler(e,...); +end +function t:unhook(t,e) +return self.events.remove_handler(t,e); +end +function e.eventable(e) +e.events=n.new(); +e.hook,e.unhook=t.hook,t.unhook; +local t=e.events.fire_event; +function e:event(e,...) +return t(e,...); +end +return e; +end +function t:add_plugin(t) +if self.plugins[t]then return true;end +if require("verse.plugins."..t)then +local e,a=e.plugins[t](self); +if e~=false then +self:debug("Loaded %s plugin",t); +self.plugins[t]=true; +else +self:warn("Failed to load %s plugin: %s",t,a); +end +end +return self; +end +function new_listener(t) +local a={}; +function a.onconnect(a) +if t.server then +local e=e.new(); +a:setlistener(new_listener(e)); +e:set_conn(a); +t:event("connected",{client=e}); +else +t.connected=true; +t:event("connected"); +end +end +function a.onincoming(a,e) +t:event("incoming-raw",e); +end +function a.ondisconnect(e,a) +if e~=t.conn then return end +t.connected=false; +t:event("disconnected",{reason=a}); +end +function a.ondrain(e) +t:event("drained"); +end +function a.onstatus(a,e) +t:event("status",e); +end +return a; +end +return e;