# HG changeset patch # User Hubert Chathi # Date 1270861272 14400 # Node ID b0fec41e695b349828220df886403f4c7920d5c2 # Parent d9ed6e7d9936468f5283ed653d552b19830b22cb initial implementation of disco responses (XEP-0030) and entity caps sending (XEP-0115) diff -r d9ed6e7d9936 -r b0fec41e695b init.lua --- 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); diff -r d9ed6e7d9936 -r b0fec41e695b plugins/disco.lua --- /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 + +-- 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