plugins/disco.lua

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 verse = require "verse";
10
local b64 = require("mime").b64;
11
local sha1 = require("prosody.util.hashes").sha1;
12
local calculate_hash = require "prosody.util.caps".calculate_hash;
13
 
14
local xmlns_caps = "http://jabber.org/protocol/caps";
15
local xmlns_disco = "http://jabber.org/protocol/disco";
16
local xmlns_disco_info = xmlns_disco.."#info";
17
local xmlns_disco_items = xmlns_disco.."#items";
18
 
19
function verse.plugins.disco(stream)
20
	stream:add_plugin("presence");
21
	local disco_info_mt = {
22
		__index = function(t, k)
23
			local node = { identities = {}, features = {} };
24
			if k == "identities" or k == "features" then
25
				return t[false][k]
26
			end
27
			t[k] = node;
28
			return node;
29
		end,
30
	};
31
	local disco_items_mt = {
32
		__index = function(t, k)
33
			local node = { };
34
			t[k] = node;
35
			return node;
36
		end,
37
	};
38
	stream.disco = {
39
		cache = {},
40
		info = setmetatable({
41
			[false] = {
42
				identities = {
43
					{category = 'client', type='pc', name='Verse'},
44
				},
45
				features = {
46
					[xmlns_caps] = true,
47
					[xmlns_disco_info] = true,
48
					[xmlns_disco_items] = true,
49
				},
50
			},
51
		}, disco_info_mt);
52
		items = setmetatable({[false]={}}, disco_items_mt);
53
	};
54
 
55
	stream.caps = {}
56
	stream.caps.node = 'http://code.matthewwild.co.uk/verse/'
57
 
58
	local function build_self_disco_info_stanza(query_node)
59
		local node = stream.disco.info[query_node or false];
60
		if query_node and query_node == stream.caps.node .. "#" .. stream.caps.hash then
61
			node = stream.disco.info[false];
62
		end
63
		local identities, features = node.identities, node.features
64
 
65
		-- construct the response
66
		local result = verse.stanza("query", {
67
			xmlns = xmlns_disco_info,
68
			node = query_node,
69
		});
70
		for _,identity in pairs(identities) do
71
			result:tag('identity', identity):up()
72
		end
73
		for feature in pairs(features) do
74
			result:tag('feature', { var = feature }):up()
75
		end
76
		return result;
77
	end
78
 
79
	setmetatable(stream.caps, {
80
		__call = function (...) -- vararg: allow calling as function or member
81
			-- retrieve the c stanza to insert into the
82
			-- presence stanza
83
			local hash = calculate_hash(build_self_disco_info_stanza())
84
			stream.caps.hash = hash;
85
			-- TODO proper caching.... some day
86
			return verse.stanza('c', {
87
				xmlns = xmlns_caps,
88
				hash = 'sha-1',
89
				node = stream.caps.node,
90
				ver = hash
91
			})
92
		end
93
	})
94
 
95
	function stream:set_identity(identity, node)
96
		self.disco.info[node or false].identities = { identity };
97
		stream:event("disco-info-changed");
98
	end
99
 
100
	function stream:add_identity(identity, node)
101
		local identities = self.disco.info[node or false].identities;
102
		identities[#identities + 1] = identity;
103
		stream:event("disco-info-changed");
104
	end
105
 
106
	function stream:add_disco_feature(feature, node)
107
		local feature = feature.var or feature;
108
		self.disco.info[node or false].features[feature] = true;
109
		stream:event("disco-info-changed");
110
	end
111
 
112
	function stream:remove_disco_feature(feature, node)
113
		local feature = feature.var or feature;
114
		self.disco.info[node or false].features[feature] = nil;
115
		stream:event("disco-info-changed");
116
	end
117
 
118
	function stream:add_disco_item(item, node)
119
		local items = self.disco.items[node or false];
120
		items[#items +1] = item;
121
	end
122
 
123
	function stream:remove_disco_item(item, node)
124
		local items = self.disco.items[node or false];
125
		for i=#items,1,-1 do
126
			if items[i] == item then
127
				table.remove(items, i);
128
			end
129
		end
130
	end
131
 
132
	-- TODO Node?
133
	function stream:jid_has_identity(jid, category, type)
134
		local cached_disco = self.disco.cache[jid];
135
		if not cached_disco then
136
			return nil, "no-cache";
137
		end
138
		local identities = self.disco.cache[jid].identities;
139
		if type then
140
			return identities[category.."/"..type] or false;
141
		end
142
		-- Check whether we have any identities with this category instead
143
		for identity in pairs(identities) do
144
			if identity:match("^(.*)/") == category then
145
				return true;
146
			end
147
		end
148
	end
149
 
150
	function stream:jid_supports(jid, feature)
151
		local cached_disco = self.disco.cache[jid];
152
		if not cached_disco or not cached_disco.features then
153
			return nil, "no-cache";
154
		end
155
		return cached_disco.features[feature] or false;
156
	end
157
 
158
	function stream:get_local_services(category, type)
159
		local host_disco = self.disco.cache[self.host];
160
		if not(host_disco) or not(host_disco.items) then
161
			return nil, "no-cache";
162
		end
163
 
164
		local results = {};
165
		for _, service in ipairs(host_disco.items) do
166
			if self:jid_has_identity(service.jid, category, type) then
167
				table.insert(results, service.jid);
168
			end
169
		end
170
		return results;
171
	end
172
 
173
	function stream:disco_local_services(callback)
174
		self:disco_items(self.host, nil, function (items)
175
			if not items then
176
				return callback({});
177
			end
178
			local n_items = 0;
179
			local function item_callback()
180
				n_items = n_items - 1;
181
				if n_items == 0 then
182
					return callback(items);
183
				end
184
			end
185
 
186
			for _, item in ipairs(items) do
187
				if item.jid then
188
					n_items = n_items + 1;
189
					self:disco_info(item.jid, nil, item_callback);
190
				end
191
			end
192
			if n_items == 0 then
193
				return callback(items);
194
			end
195
		end);
196
	end
197
 
198
	function stream:disco_info(jid, node, callback)
199
		local disco_request = verse.iq({ to = jid, type = "get" })
200
			:tag("query", { xmlns = xmlns_disco_info, node = node });
201
		self:send_iq(disco_request, function (result)
202
			if result.attr.type == "error" then
203
				return callback(nil, result:get_error());
204
			end
205
 
206
			local identities, features, extended = {}, {}, {};
207
 
208
			for tag in result:get_child("query", xmlns_disco_info):childtags() do
209
				if tag.name == "identity" then
210
					identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true;
211
				elseif tag.name == "feature" then
212
					features[tag.attr.var] = true;
213
				end
214
			end
215
 
216
			for tag in result:get_child("query", xmlns_disco_info):childtags("x", "jabber:x:data") do
217
				local form_type_field = tag:get_child_with_attr("field", nil, "var", "FORM_TYPE");
218
				local form_type = form_type_field and form_type_field:get_child_text("value");
219
				if form_type then
220
					extended[form_type] = tag;
221
				end
222
			end
223
 
224
			if not self.disco.cache[jid] then
225
				self.disco.cache[jid] = { nodes = {} };
226
			end
227
 
228
			if node then
229
				if not self.disco.cache[jid].nodes[node] then
230
					self.disco.cache[jid].nodes[node] = { nodes = {} };
231
				end
232
				self.disco.cache[jid].nodes[node].identities = identities;
233
				self.disco.cache[jid].nodes[node].features = features;
234
				self.disco.cache[jid].nodes[node].extended = extended;
235
			else
236
				self.disco.cache[jid].identities = identities;
237
				self.disco.cache[jid].features = features;
238
				self.disco.cache[jid].extended = extended;
239
			end
240
			return callback(self.disco.cache[jid]);
241
		end);
242
	end
243
 
244
	function stream:disco_items(jid, node, callback)
245
		local disco_request = verse.iq({ to = jid, type = "get" })
246
			:tag("query", { xmlns = xmlns_disco_items, node = node });
247
		self:send_iq(disco_request, function (result)
248
			if result.attr.type == "error" then
249
				return callback(nil, result:get_error());
250
			end
251
			local disco_items = { };
252
			for tag in result:get_child("query", xmlns_disco_items):childtags() do
253
				if tag.name == "item" then
254
					table.insert(disco_items, {
255
						name = tag.attr.name;
256
						jid = tag.attr.jid;
257
						node = tag.attr.node;
258
					});
259
				end
260
			end
261
 
262
			if not self.disco.cache[jid] then
263
				self.disco.cache[jid] = { nodes = {} };
264
			end
265
 
266
			if node then
267
				if not self.disco.cache[jid].nodes[node] then
268
					self.disco.cache[jid].nodes[node] = { nodes = {} };
269
				end
270
				self.disco.cache[jid].nodes[node].items = disco_items;
271
			else
272
				self.disco.cache[jid].items = disco_items;
273
			end
274
			return callback(disco_items);
275
		end);
276
	end
277
 
278
	stream:hook("iq/"..xmlns_disco_info, function (stanza)
279
		local query = stanza.tags[1];
280
		if stanza.attr.type == 'get' and query.name == "query" then
281
			local query_tag = build_self_disco_info_stanza(query.attr.node);
282
			local result = verse.reply(stanza):add_child(query_tag);
283
			stream:send(result);
284
			return true
285
		end
286
	end);
287
 
288
	stream:hook("iq/"..xmlns_disco_items, function (stanza)
289
		local query = stanza.tags[1];
290
		if stanza.attr.type == 'get' and query.name == "query" then
291
			-- figure out what items to send
292
			local items = stream.disco.items[query.attr.node or false];
293
 
294
			-- construct the response
295
			local result = verse.reply(stanza):tag('query',{
296
				xmlns = xmlns_disco_items,
297
				node = query.attr.node
298
			})
299
			for i=1,#items do
300
				result:tag('item', items[i]):up()
301
			end
302
			stream:send(result);
303
			return true
304
		end
305
	end);
306
 
307
	local initial_disco_started;
308
	stream:hook("ready", function ()
309
		if initial_disco_started then return; end
310
		initial_disco_started = true;
311
 
312
		-- Using the disco cache, fires events for each identity of a given JID
313
		local function scan_identities_for_service(service_jid)
314
			local service_disco_info = stream.disco.cache[service_jid];
315
			if service_disco_info then
316
				for identity in pairs(service_disco_info.identities) do
317
					local category, type = identity:match("^(.*)/(.*)$");
318
					stream:event("disco/service-discovered/"..category, {
319
						type = type, jid = service_jid;
320
					});
321
				end
322
			end
323
		end
324
 
325
		stream:disco_info(stream.host, nil, function ()
326
			scan_identities_for_service(stream.host);
327
		end);
328
 
329
		stream:disco_local_services(function (services)
330
			for _, service in ipairs(services) do
331
				scan_identities_for_service(service.jid);
332
			end
333
			stream:event("ready");
334
		end);
335
		return true;
336
	end, 50);
337
 
338
	stream:hook("presence-out", function (presence)
339
		presence:remove_children("c", xmlns_caps);
340
		presence:reset():add_child(stream:caps()):reset();
341
	end, 10);
342
 
343
	stream:hook("disco-info-changed", function ()
344
		stream:resend_presence();
345
	end);
346
end
347
 
348
-- end of disco.lua