Changeset

118:76ac96c53ee5

Merge roster & presence from waqas
author Matthew Wild <mwild1@gmail.com>
date Wed, 22 Oct 2008 23:12:26 +0100
parents 99:ba08b8a4eeef (current diff) 117:8e5c5e6a3240 (diff)
children 119:b48a573608e8
files core/sessionmanager.lua
diffstat 9 files changed, 333 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/core/rostermanager.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/core/rostermanager.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -7,14 +7,18 @@
 local setmetatable = setmetatable;
 local format = string.format;
 local loadfile, setfenv, pcall = loadfile, setfenv, pcall;
+local pairs, ipairs = pairs, ipairs;
+
+local hosts = hosts;
 
 require "util.datamanager"
 
 local datamanager = datamanager;
+local st = require "util.stanza";
 
 module "rostermanager"
 
-function getroster(username, host)
+--[[function getroster(username, host)
 	return { 
 			["mattj@localhost"] = true,
 			["tobias@getjabber.ath.cx"] = true,
@@ -22,7 +26,81 @@
 			["thorns@getjabber.ath.cx"] = true, 
 			["idw@getjabber.ath.cx"] = true, 
 		}
---	return datamanager.load(username, host, "roster") or {};
+	--return datamanager.load(username, host, "roster") or {};
+end]]
+
+function add_to_roster(session, jid, item)
+	if session.roster then
+		local old_item = session.roster[jid];
+		session.roster[jid] = item;
+		if save_roster(session.username, session.host) then
+			return true;
+		else
+			session.roster[jid] = old_item;
+			return nil, "wait", "internal-server-error", "Unable to save roster";
+		end
+	else
+		return nil, "auth", "not-authorized", "Session's roster not loaded";
+	end
+end
+
+function remove_from_roster(session, jid)
+	if session.roster then
+		local old_item = session.roster[jid];
+		session.roster[jid] = nil;
+		if save_roster(session.username, session.host) then
+			return true;
+		else
+			session.roster[jid] = old_item;
+			return nil, "wait", "internal-server-error", "Unable to save roster";
+		end
+	else
+		return nil, "auth", "not-authorized", "Session's roster not loaded";
+	end
+end
+
+function roster_push(username, host, jid)
+	if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then
+		local item = hosts[host].sessions[username].roster[jid];
+		local stanza = st.iq({type="set"});
+		stanza:tag("query", {xmlns = "jabber:iq:roster"});
+		if item then
+			stanza:tag("item", {jid = jid, subscription = item.subscription, name = item.name});
+			for group in pairs(item.groups) do
+				stanza:tag("group"):text(group):up();
+			end
+		else
+			stanza:tag("item", {jid = jid, subscription = "remove"});
+		end
+		stanza:up();
+		stanza:up();
+		-- stanza ready
+		for _, session in pairs(hosts[host].sessions[username].sessions) do
+			if session.interested then
+				-- FIXME do we need to set stanza.attr.to?
+				session.send(stanza);
+			end
+		end
+	end
+end
+
+function load_roster(username, host)
+	if hosts[host] and hosts[host].sessions[username] then
+		local roster = hosts[host].sessions[username].roster;
+		if not roster then
+			roster = datamanager.load(username, host, "roster") or {};
+			hosts[host].sessions[username].roster = roster;
+		end
+		return roster;
+	end
+	-- Attempt to load roster for non-loaded user
+end
+
+function save_roster(username, host)
+	if hosts[host] and hosts[host].sessions[username] and hosts[host].sessions[username].roster then
+		return datamanager.store(username, host, "roster", hosts[host].sessions[username].roster);
+	end
+	return nil;
 end
 
 return _M;
\ No newline at end of file
--- a/core/servermanager.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/core/servermanager.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -10,11 +10,13 @@
 	-- Use plugins
 	if not modulemanager.handle_stanza(origin, stanza) then
 		if stanza.name == "iq" then
-			local reply = st.reply(stanza);
-			reply.attr.type = "error";
-			reply:tag("error", { type = "cancel" })
-				:tag("service-unavailable", { xmlns = xmlns_stanzas });
-			send(origin, reply);
+			if stanza.attr.type ~= "result" and stanza.attr.type ~= "error" then
+				send(origin, st.error_reply(stanza, "cancel", "service-unavailable"));
+			end
+		elseif stanza.name == "message" then
+			send(origin, st.error_reply(stanza, "cancel", "service-unavailable"));
+		elseif stanza.name ~= "presence" then
+			error("Unknown stanza");
 		end
 	end
 end
--- a/core/sessionmanager.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/core/sessionmanager.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -12,7 +12,7 @@
 local log = require "util.logger".init("sessionmanager");
 local error = error;
 local uuid_generate = require "util.uuid".uuid_generate;
-local rm_getroster = require "core.rostermanager".getroster
+local rm_load_roster = require "core.rostermanager".load_roster;
 
 local newproxy = newproxy;
 local getmetatable = getmetatable;
@@ -84,7 +84,7 @@
 	else
 		if hosts[session.host].sessions[session.username].sessions[resource] then
 			-- Resource conflict
-			return false, "conflict";
+			return false, "conflict"; -- TODO kick old resource
 		end
 	end
 	
@@ -92,7 +92,7 @@
 	session.full_jid = session.username .. '@' .. session.host .. '/' .. resource;
 	hosts[session.host].sessions[session.username].sessions[resource] = session;
 	
-	session.roster = rm_getroster(session.username, session.host);
+	session.roster = rm_load_roster(session.username, session.host);
 	
 	return true;
 end
--- a/core/stanza_router.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/core/stanza_router.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -9,15 +9,20 @@
 
 local st = require "util.stanza";
 local send = require "core.sessionmanager".send_to_session;
+local user_exists = require "core.usermanager".user_exists;
 
-require "util.jid"
-local jid_split = jid.split;
+local jid_split = require "util.jid".split;
+local print = print;
 
 function core_process_stanza(origin, stanza)
 	log("debug", "Received: "..tostring(stanza))
 	-- TODO verify validity of stanza (as well as JID validity)
 	if stanza.name == "iq" and not(#stanza.tags == 1 and stanza.tags[1].attr.xmlns) then
-		error("Invalid IQ");
+		if stanza.attr.type == "set" or stanza.attr.type == "get" then
+			error("Invalid IQ");
+		elseif #stanza.tags > 1 or not(stanza.attr.type == "error" or stanza.attr.type == "result") then
+			error("Invalid IQ");
+		end
 	end
 
 	if origin.type == "c2s" and not origin.full_jid
@@ -26,13 +31,50 @@
 		error("Client MUST bind resource after auth");
 	end
 
-	
 	local to = stanza.attr.to;
-	stanza.attr.from = origin.full_jid -- quick fix to prevent impersonation
+	stanza.attr.from = origin.full_jid; -- quick fix to prevent impersonation (FIXME this would be incorrect when the origin is not c2s)
+	-- TODO also, stazas should be returned to their original state before the function ends
 	
-	if not to or (hosts[to] and hosts[to].type == "local") then
+	-- TODO presence subscriptions
+	if not to then
+		if stanza.name == "presence" and origin.roster then
+			if stanza.attr.type == nil or stanza.attr.type == "available" or stanza.attr.type == "unavailable" then
+				--stanza.attr.from = origin.full_jid;
+				for jid in pairs(origin.roster) do -- broadcast to all interested contacts
+					local subscription = origin.roster[jid].subscription;
+					if subscription == "both" or subscription == "from" then
+						stanza.attr.to = jid;
+						core_route_stanza(origin, stanza);
+					end
+				end
+				--[[local node, host = jid_split(stanza.attr.from);
+				for _, res in pairs(hosts[host].sessions[node].sessions) do -- broadcast to all resources
+					if res.full_jid then
+						res = user.sessions[k];
+						break;
+					end
+				end]]
+				if not origin.presence then -- presence probes on initial presence
+					local probe = st.presence({from = origin.full_jid, type = "probe"});
+					for jid in pairs(origin.roster) do
+						local subscription = origin.roster[jid].subscription;
+						if subscription == "both" or subscription == "to" then
+							probe.attr.to = jid;
+							core_route_stanza(origin, probe);
+						end
+					end
+				end
+				origin.presence = stanza;
+				stanza.attr.to = nil; -- reset it
+			else
+				-- TODO error, bad type
+			end
+		else
+			core_handle_stanza(origin, stanza);
+		end
+	elseif hosts[to] and hosts[to].type == "local" then
 		core_handle_stanza(origin, stanza);
-	elseif to and stanza.name == "iq" and not select(3, jid_split(to)) then
+	elseif stanza.name == "iq" and not select(3, jid_split(to)) then
 		core_handle_stanza(origin, stanza);
 	elseif origin.type == "c2s" then
 		core_route_stanza(origin, stanza);
@@ -43,7 +85,6 @@
 	-- Handlers
 	if origin.type == "c2s" or origin.type == "c2s_unauthed" then
 		local session = origin;
-		stanza.attr.from = session.full_jid;
 		
 		log("debug", "Routing stanza");
 		-- Stanza has no to attribute
@@ -56,32 +97,95 @@
 	end
 end
 
+function is_authorized_to_see_presence(origin, username, host)
+	local roster = datamanager.load(username, host, "roster") or {};
+	local item = roster[origin.username.."@"..origin.host];
+	return item and (item.subscription == "both" or item.subscription == "from");
+end
+
 function core_route_stanza(origin, stanza)
 	-- Hooks
 	--- ...later
 	
 	-- Deliver
-	local node, host, resource = jid_split(stanza.attr.to);
+	local to = stanza.attr.to;
+	local node, host, resource = jid_split(to);
+
+	if stanza.name == "presence" and stanza.attr.type == "probe" then resource = nil; end
+
 	local host_session = hosts[host]
 	if host_session and host_session.type == "local" then
 		-- Local host
 		local user = host_session.sessions[node];
 		if user then
 			local res = user.sessions[resource];
-			-- TODO do something about presence broadcast
 			if not res then
 				-- if we get here, resource was not specified or was unavailable
-				for k in pairs(user.sessions) do
-					res = user.sessions[k];
-					break;
+				if stanza.name == "presence" then
+					if stanza.attr.type == "probe" then
+						if is_authorized_to_see_presence(origin, node, host) then
+							for k in pairs(user.sessions) do -- return presence for all resources
+								if user.sessions[k].presence then
+									local pres = user.sessions[k].presence;
+									pres.attr.to = origin.full_jid;
+									pres.attr.from = user.sessions[k].full_jid;
+									send(origin, pres);
+									pres.attr.to = nil;
+									pres.attr.from = nil;
+								end
+							end
+						else
+							send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"}));
+						end
+					else
+						for k in pairs(user.sessions) do -- presence broadcast to all user resources
+							if user.sessions[k].full_jid then
+								stanza.attr.to = user.sessions[k].full_jid;
+								send(user.sessions[k], stanza);
+							end
+						end
+					end
+				elseif stanza.name == "message" then -- select a resource to recieve message
+					for k in pairs(user.sessions) do
+						if user.sessions[k].full_jid then
+							res = user.sessions[k];
+							break;
+						end
+					end
+					-- TODO find resource with greatest priority
+					send(res, stanza);
+				else
+					-- TODO send IQ error
 				end
-				-- TODO find resource with greatest priority
+			else
+				stanza.attr.to = res.full_jid;
+				send(res, stanza); -- Yay \o/
 			end
-			stanza.attr.to = res.full_jid;
-			send(res, stanza); -- Yay \o/
 		else
-			-- user not found
-			send(origin, st.error_reply(stanza, "cancel", "service-unavailable"));
+			-- user not online
+			if user_exists(node, host) then
+				if stanza.name == "presence" then
+					if stanza.attr.type == "probe" and is_authorized_to_see_presence(origin, node, host) then -- FIXME what to do for not c2s?
+						-- TODO send last recieved unavailable presence
+					else
+						-- TODO send unavailable presence
+					end
+				elseif stanza.name == "message" then
+					-- TODO send message error, or store offline messages
+				elseif stanza.name == "iq" then
+					-- TODO send IQ error
+				end
+			else -- user does not exist
+				-- TODO we would get here for nodeless JIDs too. Do something fun maybe? Echo service? Let plugins use xmpp:server/resource addresses?
+				if stanza.name == "presence" then
+					if stanza.attr.type == "probe" then
+						send(origin, st.presence({from = user.."@"..host, to = origin.username.."@"..origin.host, type = "unsubscribed"}));
+					end
+					-- else ignore
+				else
+					send(origin, st.error_reply(stanza, "cancel", "service-unavailable"));
+				end
+			end
 		end
 	else
 		-- Remote host
@@ -91,6 +195,7 @@
 			-- Need to establish the connection
 		end
 	end
+	stanza.attr.to = to; -- reset
 end
 
 function handle_stanza_nodest(stanza)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/roster_format.txt	Wed Oct 22 23:12:26 2008 +0100
@@ -0,0 +1,18 @@
+
+This file documents the structure of the roster object.
+
+table roster {
+  [string bare_jid] = roster_item
+}
+
+table roster_item {
+  string subscription = "none" | "to" | "from" | "both"
+  string name = Opaque string set by client. (optional)
+  set groups = a set of opaque strings set by the client
+}
+
+The roster is available as
+ hosts[host].sessions[username].roster
+and a copy is made to session.roster for all sessions.
+
+All modifications to a roster should be done through the rostermanager.
--- a/doc/session.txt	Wed Oct 22 17:36:21 2008 +0100
+++ b/doc/session.txt	Wed Oct 22 23:12:26 2008 +0100
@@ -14,8 +14,15 @@
 	resource -- the resource part of the client's full jid (not defined before resource binding)
 	full_jid -- convenience for the above 3 as string in username@host/resource form (not defined before resource binding)
 	priority -- the resource priority, default: 0 (not defined before initial presence)
+	presence -- the last non-directed presence. initially nil.
+	interested -- true if the resource requested the roster. Interested resources recieve roster updates. Initially nil.
+	roster -- the user's roster. Loaded as soon as the resource is bound (session becomes a connected resource).
 	
 	-- methods --
 	send(x) -- converts x to a string, and writes it to the connection
 	disconnect(x) -- Disconnect the user and clean up the session, best call sessionmanager.destroy_session() instead of this in most cases
 }
+
+if session.full_jid (also session.roster and session.resource) then this is a "connected resource"
+if session.presence then this is an "available resource"
+if session.interested then this is an "interested resource"
--- a/plugins/mod_roster.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/plugins/mod_roster.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -2,15 +2,87 @@
 local st = require "util.stanza"
 local send = require "core.sessionmanager".send_to_session
 
+local jid_split = require "util.jid".split;
+local t_concat = table.concat;
+
+local rm_remove_from_roster = require "core.rostermanager".remove_from_roster;
+local rm_add_to_roster = require "core.rostermanager".add_to_roster;
+local rm_roster_push = require "core.rostermanager".roster_push;
+
 add_iq_handler("c2s", "jabber:iq:roster", 
 		function (session, stanza)
-			if stanza.attr.type == "get" then
-				local roster = st.reply(stanza)
-							:query("jabber:iq:roster");
-				for jid in pairs(session.roster) do
-					roster:tag("item", { jid = jid, subscription = "none" }):up();
+			if stanza.tags[1].name == "query" then
+				if stanza.attr.type == "get" then
+					local roster = st.reply(stanza)
+								:query("jabber:iq:roster");
+					for jid in pairs(session.roster) do
+						roster:tag("item", {
+							jid = jid,
+							subscription = session.roster[jid].subscription,
+							name = session.roster[jid].name,
+						});
+						for group in pairs(session.roster[jid].groups) do
+							roster:tag("group"):text(group):up();
+						end
+					end
+					send(session, roster);
+					session.interested = true; -- resource is interested in roster updates
+					return true;
+				elseif stanza.attr.type == "set" then
+					local query = stanza.tags[1];
+					if #query.tags == 1 and query.tags[1].name == "item"
+							and query.tags[1].attr.xmlns == "jabber:iq:roster" and query.tags[1].attr.jid then
+						local item = query.tags[1];
+						local from_node, from_host = jid_split(stanza.attr.from);
+						local node, host, resource = jid_split(item.attr.jid);
+						if not resource then
+							if item.attr.jid ~= from_node.."@"..from_host then
+								if item.attr.subscription == "remove" then
+									if session.roster[item.attr.jid] then
+										local success, err_type, err_cond, err_msg = rm_remove_from_roster(session, item.attr.jid);
+										if success then
+											send(session, st.reply(stanza));
+											rm_roster_push(from_node, from_host, item.attr.jid);
+										else
+											send(session, st.error_reply(stanza, err_type, err_cond, err_msg));
+										end
+									else
+										send(session, st.error_reply(stanza, "modify", "item-not-found"));
+									end
+								else
+									local r_item = {name = item.attr.name, groups = {}};
+									if r_item.name == "" then r_item.name = nil; end
+									if session.roster[item.attr.jid] then
+										r_item.subscription = session.roster[item.attr.jid].subscription;
+									else
+										r_item.subscription = "none";
+									end
+									for _, child in ipairs(item) do	
+										if child.name == "group" then
+											local text = t_concat(child);
+											if text and text ~= "" then
+												r_item.groups[text] = true;
+											end
+										end
+									end
+									local success, err_type, err_cond, err_msg = rm_add_to_roster(session, item.attr.jid, r_item);
+									if success then
+										send(session, st.reply(stanza));
+										rm_roster_push(from_node, from_host, item.attr.jid);
+									else
+										send(session, st.error_reply(stanza, err_type, err_cond, err_msg));
+									end
+								end
+							else
+								send(session, st.error_reply(stanza, "cancel", "not-allowed"));
+							end
+						else
+							send(session, st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error?
+						end
+					else
+						send(session, st.error_reply(stanza, "modify", "bad-request"));
+					end
+					return true;
 				end
-				send(session, roster);
-				return true;
 			end
 		end);
\ No newline at end of file
--- a/util/datamanager.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/util/datamanager.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -68,16 +68,25 @@
 
 function load(username, host, datastore)
 	local data, ret = loadfile(getpath(username, host, datastore));
-	if not data then log("warn", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end
+	if not data then
+		log("warn", "Failed to load "..datastore.." storage ('"..ret.."') for user: "..(username or nil).."@"..(host or nil));
+		return nil;
+	end
 	setfenv(data, {});
 	local success, ret = pcall(data);
-	if not success then log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..username.."@"..host); return nil; end
+	if not success then
+		log("error", "Unable to load "..datastore.." storage ('"..ret.."') for user: "..(username or nil).."@"..(host or nil));
+		return nil;
+	end
 	return ret;
 end
 
 function store(username, host, datastore, data)
 	local f, msg = io_open(getpath(username, host, datastore), "w+");
-	if not f then log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..username.."@"..host); return nil; end
+	if not f then
+		log("error", "Unable to write to "..datastore.." storage ('"..msg.."') for user: "..(username or nil).."@"..(host or nil));
+		return nil;
+	end
 	f:write("return ");
 	simplesave(f, data);
 	f:close();
--- a/util/jid.lua	Wed Oct 22 17:36:21 2008 +0100
+++ b/util/jid.lua	Wed Oct 22 23:12:26 2008 +0100
@@ -4,9 +4,12 @@
 module "jid"
 
 function split(jid)
-	if not jid then return nil; end
+	if not jid then return; end
+	-- TODO verify JID, and return; if invalid
 	local node = match(jid, "^([^@]+)@");
 	local server = (node and match(jid, ".-@([^@/]+)")) or match(jid, "^([^@/]+)");
 	local resource = match(jid, "/(.+)$");
 	return node, server, resource;
 end
+
+return _M;
\ No newline at end of file