15 local xmlns_disco_info = xmlns_disco.."#info"; |
15 local xmlns_disco_info = xmlns_disco.."#info"; |
16 local xmlns_disco_items = xmlns_disco.."#items"; |
16 local xmlns_disco_items = xmlns_disco.."#items"; |
17 |
17 |
18 function verse.plugins.disco(stream) |
18 function verse.plugins.disco(stream) |
19 stream:add_plugin("presence"); |
19 stream:add_plugin("presence"); |
20 stream.disco = { cache = {}, info = {} } |
20 local disco_info_mt = { |
21 stream.disco.info.identities = { |
21 __index = function(t, k) |
22 {category = 'client', type='pc', name='Verse'}, |
22 local node = { identities = {}, features = {} }; |
23 } |
23 if k == "identities" or k == "features" then |
24 stream.disco.info.features = { |
24 return t[false][k] |
25 {var = xmlns_caps}, |
25 end |
26 {var = xmlns_disco_info}, |
26 t[k] = node; |
27 {var = xmlns_disco_items}, |
27 return node; |
28 } |
28 end, |
29 stream.disco.items = {} |
29 }; |
30 stream.disco.nodes = {} |
30 local disco_items_mt = { |
|
31 __index = function(t, k) |
|
32 local node = { }; |
|
33 t[k] = node; |
|
34 return node; |
|
35 end, |
|
36 }; |
|
37 stream.disco = { |
|
38 cache = {}, |
|
39 info = setmetatable({ |
|
40 [false] = { |
|
41 identities = { |
|
42 {category = 'client', type='pc', name='Verse'}, |
|
43 }, |
|
44 features = { |
|
45 [xmlns_caps] = true, |
|
46 [xmlns_disco_info] = true, |
|
47 [xmlns_disco_items] = true, |
|
48 }, |
|
49 }, |
|
50 }, disco_info_mt); |
|
51 items = setmetatable({[false]={}}, disco_items_mt); |
|
52 }; |
31 |
53 |
32 stream.caps = {} |
54 stream.caps = {} |
33 stream.caps.node = 'http://code.matthewwild.co.uk/verse/' |
55 stream.caps.node = 'http://code.matthewwild.co.uk/verse/' |
34 |
56 |
35 local function cmp_identity(item1, item2) |
57 local function cmp_identity(item1, item2) |
52 |
74 |
53 local function cmp_feature(item1, item2) |
75 local function cmp_feature(item1, item2) |
54 return item1.var < item2.var |
76 return item1.var < item2.var |
55 end |
77 end |
56 |
78 |
57 local function calculate_hash() |
79 local function calculate_hash(node) |
58 table.sort(stream.disco.info.identities, cmp_identity) |
80 local identities = stream.disco.info[node or false].identities; |
59 table.sort(stream.disco.info.features, cmp_feature) |
81 table.sort(identities, cmp_identity) |
60 local S = '' |
82 local features = {}; |
61 for key,identity in pairs(stream.disco.info.identities) do |
83 for var in pairs(stream.disco.info[node or false].features) do |
62 S = S .. string.format( |
84 features[#features+1] = { var = var }; |
63 '%s/%s/%s/%s', identity.category, identity.type, |
85 end |
|
86 table.sort(features, cmp_feature) |
|
87 local S = {}; |
|
88 for key,identity in pairs(identities) do |
|
89 S[#S+1] = table.concat({ |
|
90 identity.category, identity.type or '', |
64 identity['xml:lang'] or '', identity.name or '' |
91 identity['xml:lang'] or '', identity.name or '' |
65 ) .. '<' |
92 }, '/'); |
66 end |
93 end |
67 for key,feature in pairs(stream.disco.info.features) do |
94 for key,feature in pairs(features) do |
68 S = S .. feature.var .. '<' |
95 S[#S+1] = feature.var |
69 end |
96 end |
|
97 S[#S+1] = ''; |
|
98 S = table.concat(S,'<'); |
70 -- FIXME: make sure S is utf8-encoded |
99 -- FIXME: make sure S is utf8-encoded |
71 --stream:debug("Computed hash string: "..S); |
100 --stream:debug("Computed hash string: "..S); |
72 --stream:debug("Computed hash string (sha1): "..sha1(S, true)); |
101 --stream:debug("Computed hash string (sha1): "..sha1(S, true)); |
73 --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S))); |
102 --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S))); |
74 return (b64(sha1(S))) |
103 return (b64(sha1(S))) |
77 setmetatable(stream.caps, { |
106 setmetatable(stream.caps, { |
78 __call = function (...) -- vararg: allow calling as function or member |
107 __call = function (...) -- vararg: allow calling as function or member |
79 -- retrieve the c stanza to insert into the |
108 -- retrieve the c stanza to insert into the |
80 -- presence stanza |
109 -- presence stanza |
81 local hash = calculate_hash() |
110 local hash = calculate_hash() |
|
111 stream.caps.hash = hash; |
|
112 -- TODO proper caching.... some day |
82 return verse.stanza('c', { |
113 return verse.stanza('c', { |
83 xmlns = xmlns_caps, |
114 xmlns = xmlns_caps, |
84 hash = 'sha-1', |
115 hash = 'sha-1', |
85 node = stream.caps.node, |
116 node = stream.caps.node, |
86 ver = hash |
117 ver = hash |
87 }) |
118 }) |
88 end |
119 end |
89 }) |
120 }) |
90 |
121 |
91 function stream:add_disco_feature(feature) |
122 function stream:add_disco_feature(feature, node) |
92 table.insert(self.disco.info.features, {var=feature}); |
123 local feature = feature.var or feature; |
|
124 self.disco.info[node or false].features[feature] = true; |
93 stream:resend_presence(); |
125 stream:resend_presence(); |
94 end |
126 end |
95 |
127 |
96 function stream:remove_disco_feature(feature) |
128 function stream:remove_disco_feature(feature, node) |
97 for idx, disco_feature in ipairs(self.disco.info.features) do |
129 local feature = feature.var or feature; |
98 if disco_feature.var == feature then |
130 self.disco.info[node or false].features[feature] = nil; |
99 table.remove(self.disco.info.features, idx); |
131 stream:resend_presence(); |
100 stream:resend_presence(); |
|
101 return true; |
|
102 end |
|
103 end |
|
104 end |
132 end |
105 |
133 |
106 function stream:add_disco_item(item, node) |
134 function stream:add_disco_item(item, node) |
107 local disco_items = self.disco.items; |
135 local items = self.disco.items[node or false]; |
108 if node then |
136 items[#items +1] = item; |
109 disco_items = self.disco.nodes[node]; |
137 end |
110 if not disco_items then |
138 |
111 disco_items = { features = {}, items = {} }; |
139 function stream:remove_disco_item(item, node) |
112 self.disco.nodes[node] = disco_items; |
140 local items = self.disco.items[node or false]; |
113 disco_items = disco_items.items; |
141 for i=#items,1,-1 do |
114 else |
142 if items[i] == item then |
115 disco_items = disco_items.items; |
143 table.remove(items, i); |
116 end |
144 end |
117 end |
145 end |
118 table.insert(disco_items, item); |
146 end |
119 end |
147 |
120 |
148 -- TODO Node? |
121 function stream:jid_has_identity(jid, category, type) |
149 function stream:jid_has_identity(jid, category, type) |
122 local cached_disco = self.disco.cache[jid]; |
150 local cached_disco = self.disco.cache[jid]; |
123 if not cached_disco then |
151 if not cached_disco then |
124 return nil, "no-cache"; |
152 return nil, "no-cache"; |
125 end |
153 end |
253 return callback(disco_items); |
281 return callback(disco_items); |
254 end); |
282 end); |
255 end |
283 end |
256 |
284 |
257 stream:hook("iq/"..xmlns_disco_info, function (stanza) |
285 stream:hook("iq/"..xmlns_disco_info, function (stanza) |
258 if stanza.attr.type == 'get' then |
286 local query = stanza.tags[1]; |
259 local query = stanza:child_with_name('query') |
287 if stanza.attr.type == 'get' and query.name == "query" then |
260 if not query then return; end |
288 local query_node = query.attr.node; |
261 -- figure out what identities/features to send |
289 local node = stream.disco.info[query_node or false]; |
262 local identities |
290 if query_node and query_node == stream.caps.node .. "#" .. stream.caps.hash then |
263 local features |
291 node = stream.disco.info[false]; |
264 if query.attr.node then |
292 end |
265 local hash = calculate_hash() |
293 local identities, features = node.identities, node.features |
266 local node = stream.disco.nodes[query.attr.node] |
294 |
267 if node and node.info then |
|
268 identities = node.info.identities or {} |
|
269 features = node.info.identities or {} |
|
270 elseif query.attr.node == stream.caps.node..'#'..hash then |
|
271 -- matches caps hash, so use the main info |
|
272 identities = stream.disco.info.identities |
|
273 features = stream.disco.info.features |
|
274 else |
|
275 -- unknown node: give an error |
|
276 local response = verse.stanza('iq',{ |
|
277 to = stanza.attr.from, |
|
278 from = stanza.attr.to, |
|
279 id = stanza.attr.id, |
|
280 type = 'error' |
|
281 }) |
|
282 response:tag('query',{xmlns = xmlns_disco_info}):reset() |
|
283 response:tag('error',{type = 'cancel'}):tag( |
|
284 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} |
|
285 ) |
|
286 stream:send(response) |
|
287 return true |
|
288 end |
|
289 else |
|
290 identities = stream.disco.info.identities |
|
291 features = stream.disco.info.features |
|
292 end |
|
293 -- construct the response |
295 -- construct the response |
294 local result = verse.stanza('query',{ |
296 local result = verse.reply(stanza):tag("query", { |
295 xmlns = xmlns_disco_info, |
297 xmlns = xmlns_disco_info, |
296 node = query.attr.node |
298 node = query_node, |
297 }) |
299 }); |
298 for key,identity in pairs(identities) do |
300 for _,identity in pairs(identities) do |
299 result:tag('identity', identity):reset() |
301 result:tag('identity', identity):up() |
300 end |
302 end |
301 for key,feature in pairs(features) do |
303 for feature in pairs(features) do |
302 result:tag('feature', feature):reset() |
304 result:tag('feature', { var = feature }):up() |
303 end |
305 end |
304 stream:send(verse.stanza('iq',{ |
306 stream:send(result); |
305 to = stanza.attr.from, |
|
306 from = stanza.attr.to, |
|
307 id = stanza.attr.id, |
|
308 type = 'result' |
|
309 }):add_child(result)) |
|
310 return true |
307 return true |
311 end |
308 end |
312 end); |
309 end); |
313 |
310 |
314 stream:hook("iq/"..xmlns_disco_items, function (stanza) |
311 stream:hook("iq/"..xmlns_disco_items, function (stanza) |
315 if stanza.attr.type == 'get' then |
312 local query = stanza.tags[1]; |
316 local query = stanza:child_with_name('query') |
313 if stanza.attr.type == 'get' and query.name == "query" then |
317 if not query then return; end |
|
318 -- figure out what items to send |
314 -- figure out what items to send |
319 local items |
315 local items = stream.disco.items[query.attr.node or false]; |
320 if query.attr.node then |
316 |
321 local node = stream.disco.nodes[query.attr.node] |
|
322 if node then |
|
323 items = node.items or {} |
|
324 else |
|
325 -- unknown node: give an error |
|
326 local response = verse.stanza('iq',{ |
|
327 to = stanza.attr.from, |
|
328 from = stanza.attr.to, |
|
329 id = stanza.attr.id, |
|
330 type = 'error' |
|
331 }) |
|
332 response:tag('query',{xmlns = xmlns_disco_items}):reset() |
|
333 response:tag('error',{type = 'cancel'}):tag( |
|
334 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} |
|
335 ) |
|
336 stream:send(response) |
|
337 return true |
|
338 end |
|
339 else |
|
340 items = stream.disco.items |
|
341 end |
|
342 -- construct the response |
317 -- construct the response |
343 local result = verse.stanza('query',{ |
318 local result = verse.reply(stanza):tag('query',{ |
344 xmlns = xmlns_disco_items, |
319 xmlns = xmlns_disco_items, |
345 node = query.attr.node |
320 node = query.attr.node |
346 }) |
321 }) |
347 for key,item in pairs(items) do |
322 for i=1,#items do |
348 result:tag('item', item):reset() |
323 result:tag('item', items[i]):up() |
349 end |
324 end |
350 stream:send(verse.stanza('iq',{ |
325 stream:send(result); |
351 to = stanza.attr.from, |
|
352 from = stanza.attr.to, |
|
353 id = stanza.attr.id, |
|
354 type = 'result' |
|
355 }):add_child(result)) |
|
356 return true |
326 return true |
357 end |
327 end |
358 end); |
328 end); |
359 |
329 |
360 local initial_disco_started; |
330 local initial_disco_started; |