# HG changeset patch # User Matthew Wild # Date 1282398696 -3600 # Node ID 0f5a8d530fcd4912ff43378480d9e463bf5c33a5 # Parent 1dccff7df2d53e57065c1fe2a3dda8603c8df58b 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 diff -r 1dccff7df2d5 -r 0f5a8d530fcd plugins/disco.lua --- /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 +-- Copyright (C) 2010 Matthew Wild +-- +-- 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