Comparison

plugins/disco.lua @ 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
child 109:60a03b2cabec
comparison
equal deleted inserted replaced
98:1dccff7df2d5 99:0f5a8d530fcd
1 -- Verse XMPP Library
2 -- Copyright (C) 2010 Hubert Chathi <hubert@uhoreg.ca>
3 -- Copyright (C) 2010 Matthew Wild <mwild1@gmail.com>
4 --
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
7 --
8
9 local st = require "util.stanza"
10 local b64 = require("mime").b64
11 -- NOTE: The b64 routine in LuaSocket 2.0.2 and below
12 -- contains a bug regarding handling \0, it's advisable
13 -- that you use another base64 routine, or a patched
14 -- version of LuaSocket.
15 -- You can borrow Prosody's (binary) util.encodings lib:
16 --local b64 = require("util.encodings").base64.encode
17
18 local sha1 = require("util.sha1").sha1
19
20 local xmlns_disco = "http://jabber.org/protocol/disco";
21 local xmlns_disco_info = xmlns_disco.."#info";
22 local xmlns_disco_items = xmlns_disco.."#items";
23
24 function verse.plugins.disco(stream)
25 stream.disco = { cache = {}, info = {} }
26 stream.disco.info.identities = {
27 {category = 'client', type='pc', name='Verse'},
28 }
29 stream.disco.info.features = {
30 {var = 'http://jabber.org/protocol/caps'},
31 {var = 'http://jabber.org/protocol/disco#info'},
32 {var = 'http://jabber.org/protocol/disco#items'},
33 }
34 stream.disco.items = {}
35 stream.disco.nodes = {}
36
37 stream.caps = {}
38 stream.caps.node = 'http://code.matthewwild.co.uk/verse/'
39
40 local function cmp_identity(item1, item2)
41 if item1.category < item2.category then
42 return true;
43 elseif item2.category < item1.category then
44 return false;
45 end
46 if item1.type < item2.type then
47 return true;
48 elseif item2.type < item1.type then
49 return false;
50 end
51 if (not item1['xml:lang'] and item2['xml:lang']) or
52 (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then
53 return true
54 end
55 return false
56 end
57
58 local function cmp_feature(item1, item2)
59 return item1.var < item2.var
60 end
61
62 local function calculate_hash()
63 table.sort(stream.disco.info.identities, cmp_identity)
64 table.sort(stream.disco.info.features, cmp_feature)
65 local S = ''
66 for key,identity in pairs(stream.disco.info.identities) do
67 S = S .. string.format(
68 '%s/%s/%s/%s', identity.category, identity.type,
69 identity['xml:lang'] or '', identity.name or ''
70 ) .. '<'
71 end
72 for key,feature in pairs(stream.disco.info.features) do
73 S = S .. feature.var .. '<'
74 end
75 -- FIXME: make sure S is utf8-encoded
76 --stream:debug("Computed hash string: "..S);
77 --stream:debug("Computed hash string (sha1): "..sha1(S, true));
78 --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S)));
79 return (b64(sha1(S)))
80 end
81
82 setmetatable(stream.caps, {
83 __call = function (...) -- vararg: allow calling as function or member
84 -- retrieve the c stanza to insert into the
85 -- presence stanza
86 local hash = calculate_hash()
87 return st.stanza('c', {
88 xmlns = 'http://jabber.org/protocol/caps',
89 hash = 'sha-1',
90 node = stream.caps.node,
91 ver = hash
92 })
93 end
94 })
95
96 function stream:add_disco_feature(feature)
97 table.insert(self.disco.info.features, {var=feature});
98 end
99
100 function stream:jid_has_identity(jid, category, type)
101 local cached_disco = self.disco.cache[jid];
102 if not cached_disco then
103 return nil, "no-cache";
104 end
105 local identities = self.disco.cache[jid].identities;
106 if type then
107 return identities[category.."/"..type] or false;
108 end
109 -- Check whether we have any identities with this category instead
110 for identity in pairs(identities) do
111 if identity:match("^(.*)/") == category then
112 return true;
113 end
114 end
115 end
116
117 function stream:jid_supports(jid, feature)
118 local cached_disco = self.disco.cache[jid];
119 if not cached_disco or not cached_disco.features then
120 return nil, "no-cache";
121 end
122 return cached_disco.features[feature] or false;
123 end
124
125 function stream:get_local_services(category, type)
126 local host_disco = self.disco.cache[self.host];
127 if not(host_disco) or not(host_disco.items) then
128 return nil, "no-cache";
129 end
130
131 local results = {};
132 for _, service in ipairs(host_disco.items) do
133 if self:jid_has_identity(service.jid, category, type) then
134 table.insert(results, service.jid);
135 end
136 end
137 return results;
138 end
139
140 function stream:disco_local_services(callback)
141 self:disco_items(self.host, nil, function (items)
142 local n_items = 0;
143 local function item_callback()
144 n_items = n_items - 1;
145 if n_items == 0 then
146 return callback(items);
147 end
148 end
149
150 for _, item in ipairs(items) do
151 if item.jid then
152 n_items = n_items + 1;
153 self:disco_info(item.jid, nil, item_callback);
154 end
155 end
156 if n_items == 0 then
157 return callback(items);
158 end
159 end);
160 end
161
162 function stream:disco_info(jid, node, callback)
163 local disco_request = verse.iq({ to = jid, type = "get" })
164 :tag("query", { xmlns = xmlns_disco_info, node = node });
165 self:send_iq(disco_request, function (result)
166 if result.attr.type == "error" then
167 return callback(nil, result:get_error());
168 end
169
170 local identities, features = {}, {};
171
172 for tag in result:get_child("query", xmlns_disco_info):childtags() do
173 if tag.name == "identity" then
174 identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true;
175 elseif tag.name == "feature" then
176 features[tag.attr.var] = true;
177 end
178 end
179
180
181 if not self.disco.cache[jid] then
182 self.disco.cache[jid] = { nodes = {} };
183 end
184
185 if node then
186 if not self.disco.cache.nodes[node] then
187 self.disco.cache.nodes[node] = { nodes = {} };
188 end
189 self.disco.cache[jid].nodes[node].identities = identities;
190 self.disco.cache[jid].nodes[node].features = features;
191 else
192 self.disco.cache[jid].identities = identities;
193 self.disco.cache[jid].features = features;
194 end
195 return callback(self.disco.cache[jid]);
196 end);
197 end
198
199 function stream:disco_items(jid, node, callback)
200 local disco_request = verse.iq({ to = jid, type = "get" })
201 :tag("query", { xmlns = xmlns_disco_items, node = node });
202 self:send_iq(disco_request, function (result)
203 if result.attr.type == "error" then
204 return callback(nil, result:get_error());
205 end
206 local disco_items = { };
207 for tag in result:get_child("query", xmlns_disco_items):childtags() do
208 if tag.name == "item" then
209 table.insert(disco_items, {
210 name = tag.attr.name;
211 jid = tag.attr.jid;
212 });
213 end
214 end
215
216 if not self.disco.cache[jid] then
217 self.disco.cache[jid] = { nodes = {} };
218 end
219
220 if node then
221 if not self.disco.cache.nodes[node] then
222 self.disco.cache.nodes[node] = { nodes = {} };
223 end
224 self.disco.cache.nodes[node].items = disco_items;
225 else
226 self.disco.cache[jid].items = disco_items;
227 end
228 return callback(disco_items);
229 end);
230 end
231
232 stream:hook("iq/http://jabber.org/protocol/disco#info", function (stanza)
233 if stanza.attr.type == 'get' then
234 local query = stanza:child_with_name('query')
235 if not query then return; end
236 -- figure out what identities/features to send
237 local identities
238 local features
239 if query.attr.node then
240 local hash = calculate_hash()
241 local node = stream.disco.nodes[query.attr.node]
242 if node and node.info then
243 identities = node.info.identities or {}
244 features = node.info.identities or {}
245 elseif query.attr.node == stream.caps.node..'#'..hash then
246 -- matches caps hash, so use the main info
247 identities = stream.disco.info.identities
248 features = stream.disco.info.features
249 else
250 -- unknown node: give an error
251 local response = st.stanza('iq',{
252 to = stanza.attr.from,
253 from = stanza.attr.to,
254 id = stanza.attr.id,
255 type = 'error'
256 })
257 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#info'}):reset()
258 response:tag('error',{type = 'cancel'}):tag(
259 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}
260 )
261 stream:send(response)
262 return true
263 end
264 else
265 identities = stream.disco.info.identities
266 features = stream.disco.info.features
267 end
268 -- construct the response
269 local result = st.stanza('query',{
270 xmlns = 'http://jabber.org/protocol/disco#info',
271 node = query.attr.node
272 })
273 for key,identity in pairs(identities) do
274 result:tag('identity', identity):reset()
275 end
276 for key,feature in pairs(features) do
277 result:tag('feature', feature):reset()
278 end
279 stream:send(st.stanza('iq',{
280 to = stanza.attr.from,
281 from = stanza.attr.to,
282 id = stanza.attr.id,
283 type = 'result'
284 }):add_child(result))
285 return true
286 end
287 end);
288
289 stream:hook("iq/http://jabber.org/protocol/disco#items", function (stanza)
290 if stanza.attr.type == 'get' then
291 local query = stanza:child_with_name('query')
292 if not query then return; end
293 -- figure out what items to send
294 local items
295 if query.attr.node then
296 local node = stream.disco.nodes[query.attr.node]
297 if node then
298 items = node.items or {}
299 else
300 -- unknown node: give an error
301 local response = st.stanza('iq',{
302 to = stanza.attr.from,
303 from = stanza.attr.to,
304 id = stanza.attr.id,
305 type = 'error'
306 })
307 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#items'}):reset()
308 response:tag('error',{type = 'cancel'}):tag(
309 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}
310 )
311 stream:send(response)
312 return true
313 end
314 else
315 items = stream.disco.items
316 end
317 -- construct the response
318 local result = st.stanza('query',{
319 xmlns = 'http://jabber.org/protocol/disco#items',
320 node = query.attr.node
321 })
322 for key,item in pairs(items) do
323 result:tag('item', item):reset()
324 end
325 stream:send(st.stanza('iq',{
326 to = stanza.attr.from,
327 from = stanza.attr.to,
328 id = stanza.attr.id,
329 type = 'result'
330 }):add_child(result))
331 return true
332 end
333 end);
334
335 stream:hook("ready", function ()
336 stream:disco_local_services(function (services)
337 for _, service in ipairs(services) do
338 for identity in pairs(stream.disco.cache[service.jid].identities) do
339 local category, type = identity:match("^(.*)/(.*)$");
340 stream:event("disco/service-discovered/"..category, {
341 type = type, jid = service.jid;
342 });
343 end
344 end
345 end);
346 end, 5);
347 end
348
349 -- end of disco.lua