581 lines
15 KiB
Lua
581 lines
15 KiB
Lua
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("</stream:stream>");
|
|
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;
|
|
|