Software /
code /
verse
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 |