diff mod_ircd/verse/verse.lua @ 491:5b3db688213d

mod_ircd: Fixed nick change logic (thanks mva), so that the self nick-change "flag" is removed properly, improved the logic to use verse's room_mt:change_nick (thanks Zash) yet to be pushed into main, added squished verse with the meta method included.
author Marco Cirillo <maranda@lightwitch.org>
date Fri, 02 Dec 2011 20:53:09 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_ircd/verse/verse.lua	Fri Dec 02 20:53:09 2011 +0000
@@ -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 "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;
+