plugins/disco.lua

changeset 99
0f5a8d530fcd
child 109
60a03b2cabec
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

mercurial