plugins/disco.lua

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
child 16
ae69cea97598
permissions
-rw-r--r--

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

-- 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