Changeset

99:0f5a8d530fcd

verse.plugins.disco: Add disco plugin originally developed by Hubert Chathi for Riddim, but here adapted for Verse with new APIs added to allow disco'ing the local server and remote entities
author Matthew Wild <mwild1@gmail.com>
date Sat, 21 Aug 2010 14:51:36 +0100
parents 98:1dccff7df2d5
children 100:e45883a3f39a
files plugins/disco.lua
diffstat 1 files changed, 349 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/disco.lua	Sat Aug 21 14:51:36 2010 +0100
@@ -0,0 +1,349 @@
+-- Verse XMPP Library
+-- Copyright (C) 2010 Hubert Chathi <hubert@uhoreg.ca>
+-- Copyright (C) 2010 Matthew Wild <mwild1@gmail.com>
+-- 
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza"
+local b64 = require("mime").b64
+-- NOTE: The b64 routine in LuaSocket 2.0.2 and below
+-- contains a bug regarding handling \0, it's advisable
+-- that you use another base64 routine, or a patched
+-- version of LuaSocket.
+-- You can borrow Prosody's (binary) util.encodings lib:
+--local b64 = require("util.encodings").base64.encode
+
+local sha1 = require("util.sha1").sha1
+
+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.disco = { cache = {}, info = {} }
+	stream.disco.info.identities = {
+		{category = 'client', type='pc', name='Verse'},
+	}
+	stream.disco.info.features = {
+		{var = 'http://jabber.org/protocol/caps'},
+		{var = 'http://jabber.org/protocol/disco#info'},
+		{var = 'http://jabber.org/protocol/disco#items'},
+	}
+	stream.disco.items = {}
+	stream.disco.nodes = {}
+
+	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()
+		table.sort(stream.disco.info.identities, cmp_identity)
+		table.sort(stream.disco.info.features, cmp_feature)
+		local S = ''
+		for key,identity in pairs(stream.disco.info.identities) do
+			S = S .. string.format(
+				'%s/%s/%s/%s', identity.category, identity.type,
+				identity['xml:lang'] or '', identity.name or ''
+			) .. '<'
+		end
+		for key,feature in pairs(stream.disco.info.features) do
+			S = S .. feature.var .. '<'
+		end
+		-- 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()
+			return st.stanza('c', {
+				xmlns = 'http://jabber.org/protocol/caps',
+				hash = 'sha-1',
+				node = stream.caps.node,
+				ver = hash
+			})
+		end
+	})
+	
+	function stream:add_disco_feature(feature)
+		table.insert(self.disco.info.features, {var=feature});		
+	end
+
+	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)
+			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.nodes[node] then
+					self.disco.cache.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;
+					});
+				end
+			end
+			
+			if not self.disco.cache[jid] then
+				self.disco.cache[jid] = { nodes = {} };
+			end
+			
+			if node then
+				if not self.disco.cache.nodes[node] then
+					self.disco.cache.nodes[node] = { nodes = {} };
+				end
+				self.disco.cache.nodes[node].items = disco_items;
+			else
+				self.disco.cache[jid].items = disco_items;
+			end
+			return callback(disco_items);
+		end);
+	end
+	
+	stream:hook("iq/http://jabber.org/protocol/disco#info", function (stanza)
+		if stanza.attr.type == 'get' then
+			local query = stanza:child_with_name('query')
+			if not query then return; end
+			-- figure out what identities/features to send
+			local identities
+			local features
+			if query.attr.node then
+				local hash = calculate_hash()
+				local node = stream.disco.nodes[query.attr.node]
+				if node and node.info then
+					identities = node.info.identities or {}
+					features = node.info.identities or {}
+				elseif query.attr.node == stream.caps.node..'#'..hash then
+					-- matches caps hash, so use the main info
+					identities = stream.disco.info.identities
+					features = stream.disco.info.features
+				else
+					-- unknown node: give an error
+					local response = st.stanza('iq',{
+						to = stanza.attr.from,
+						from = stanza.attr.to,
+						id = stanza.attr.id,
+						type = 'error'
+					})
+					response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#info'}):reset()
+					response:tag('error',{type = 'cancel'}):tag(
+						'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}
+					)
+					stream:send(response)
+					return true
+				end
+			else
+				identities = stream.disco.info.identities
+				features = stream.disco.info.features
+			end
+			-- construct the response
+			local result = st.stanza('query',{
+				xmlns = 'http://jabber.org/protocol/disco#info',
+				node = query.attr.node
+			})
+			for key,identity in pairs(identities) do
+				result:tag('identity', identity):reset()
+			end
+			for key,feature in pairs(features) do
+				result:tag('feature', feature):reset()
+			end
+			stream:send(st.stanza('iq',{
+				to = stanza.attr.from,
+				from = stanza.attr.to,
+				id = stanza.attr.id,
+				type = 'result'
+			}):add_child(result))
+			return true
+		end
+	end);
+
+	stream:hook("iq/http://jabber.org/protocol/disco#items", function (stanza)
+		if stanza.attr.type == 'get' then
+			local query = stanza:child_with_name('query')
+			if not query then return; end
+			-- figure out what items to send
+			local items
+			if query.attr.node then
+				local node = stream.disco.nodes[query.attr.node]
+				if node then
+					items = node.items or {}
+				else
+					-- unknown node: give an error
+					local response = st.stanza('iq',{
+						to = stanza.attr.from,
+						from = stanza.attr.to,
+						id = stanza.attr.id,
+						type = 'error'
+					})
+					response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#items'}):reset()
+					response:tag('error',{type = 'cancel'}):tag(
+						'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}
+					)
+					stream:send(response)
+					return true
+				end
+			else
+				items = stream.disco.items
+			end
+			-- construct the response
+			local result = st.stanza('query',{
+				xmlns = 'http://jabber.org/protocol/disco#items',
+				node = query.attr.node
+			})
+			for key,item in pairs(items) do
+				result:tag('item', item):reset()
+			end
+			stream:send(st.stanza('iq',{
+				to = stanza.attr.from,
+				from = stanza.attr.to,
+				id = stanza.attr.id,
+				type = 'result'
+			}):add_child(result))
+			return true
+		end
+	end);
+	
+	stream:hook("ready", function ()
+		stream:disco_local_services(function (services)
+			for _, service in ipairs(services) do
+				for identity in pairs(stream.disco.cache[service.jid].identities) do
+					local category, type = identity:match("^(.*)/(.*)$");
+					stream:event("disco/service-discovered/"..category, {
+						type = type, jid = service.jid;
+					});
+				end
+			end
+		end);
+	end, 5);
+end
+
+-- end of disco.lua