initial implementation of disco responses (XEP-0030) and entity caps sending (XEP-0115)

Fri, 09 Apr 2010 21:01:12 -0400

author
Hubert Chathi <hubert@uhoreg.ca>
date
Fri, 09 Apr 2010 21:01:12 -0400
changeset 6
b0fec41e695b
parent 5
d9ed6e7d9936
child 7
59129c1e4e07

initial implementation of disco responses (XEP-0030) and entity caps sending (XEP-0115)

init.lua file | annotate | diff | comparison | revisions
plugins/disco.lua file | annotate | diff | comparison | revisions
--- a/init.lua	Mon Mar 22 10:50:55 2010 -0400
+++ b/init.lua	Fri Apr 09 21:01:12 2010 -0400
@@ -124,7 +124,10 @@
 	end
 	
 	b:hook("started", function ()
-		b:send(verse.presence());
+		local presence = verse.presence()
+		if b.caps then
+			presence:add_child(b:caps())
+		end
 		for k, v in pairs(config.autojoin or {}) do
 			if type(k) == "number" then
 				b:join_room(v);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/disco.lua	Fri Apr 09 21:01:12 2010 -0400
@@ -0,0 +1,197 @@
+-- disco.lua
+
+-- Responds to service discovery queries (XEP-0030), and calculates the entity
+-- capabilities hash (XEP-0115).
+
+-- Fill the bot.disco.info.identities, bot.disco.info.features, and
+-- bot.disco.items tables with the relevant disco data.  It comes pre-populated
+-- to advertise support for disco#info, disco#items, and entity capabilities,
+-- and to identify itself as Riddim.
+
+-- If you want to advertise a node, add entries to the bot.disco.nodes table
+-- with the relevant data.  The bot.disco.nodes table should have the same
+-- format as bot.disco (without the nodes element).  The nodes are NOT
+-- automatically added to the base disco items, so you will need to add them
+-- yourself.
+
+-- To property implement Entity Capabilities, you should make sure that you
+-- send a "c" element within presence stanzas that are sent.  The correct "c"
+-- element can be obtained by calling bot.caps() (or bot:caps()).
+
+-- Hubert Chathi <hubert@uhoreg.ca>
+
+-- This file is hereby placed in the public domain.  Feel free to modify and
+-- redistribute it at will
+
+local st = require "util.stanza"
+local b64 = require("mime").b64
+local sha1 = require("util.hashes").sha1
+
+function riddim.plugins.disco(bot)
+   bot.disco = {}
+   bot.disco.info = {}
+   bot.disco.info.identities = {
+      {category = 'client', type='bot', name='Riddim'},
+   }
+   bot.disco.info.features = {
+      {var = 'http://jabber.org/protocol/caps'},
+      {var = 'http://jabber.org/protocol/disco#info'},
+      {var = 'http://jabber.org/protocol/disco#items'},
+   }
+   bot.disco.items = {}
+   bot.disco.nodes = {}
+
+   bot.caps = {}
+   bot.caps.node = 'http://code.matthewwild.co.uk/riddim/'
+
+   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(bot.disco.info.identities, cmp_identity)
+      table.sort(bot.disco.info.features, cmp_feature)
+      local S = ''
+      for key,identity in pairs(bot.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(bot.disco.info.features) do
+	 S = S .. feature.var
+	       .. '<'
+      end
+      -- FIXME: make sure S is utf8-encoded
+      return (b64(sha1(S)))
+   end
+
+   setmetatable(bot.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 = bot.caps.node,
+						 ver = hash})
+			    end})
+
+   bot:hook("iq/http://jabber.org/protocol/disco#info",
+	    function (event)
+	       local stanza = event.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 = bot.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 == bot.caps.node..'#'..hash then
+			-- matches caps hash, so use the main info
+			identities = bot.disco.info.identities
+			features = bot.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'})
+			bot:send(response)
+			return true
+		     end
+		  else
+		     identities = bot.disco.info.identities
+		     features = bot.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
+		  bot: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);
+
+   bot:hook("iq/http://jabber.org/protocol/disco#items",
+	    function (event)
+	       local stanza = event.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 = bot.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'})
+			bot:send(response)
+			return true
+		     end
+		  else
+		     items = bot.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
+		  bot: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);
+end
+
+-- end of disco.lua

mercurial