plugins/mod_pubsub.lua

1
local pubsub = require "util.pubsub";
2
local st = require "util.stanza";
3
local jid_bare = require "util.jid".bare;
4
local uuid_generate = require "util.uuid".generate;
5
 
6
require "core.modulemanager".load(module.host, "iq");
7
 
8
local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
9
local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
10
local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
11
 
12
local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false);
13
local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false);
14
 
15
local service;
16
 
17
local handlers = {};
18
 
19
function handle_pubsub_iq(event)
20
	local origin, stanza = event.origin, event.stanza;
21
	local pubsub = stanza.tags[1];
22
	local action = pubsub.tags[1];
23
	local handler = handlers[stanza.attr.type.."_"..action.name];
24
	if handler then
25
		handler(origin, stanza, action);
26
		return true;
27
	end
28
end
29
 
30
local pubsub_errors = {
31
	["conflict"] = { "cancel", "conflict" };
32
	["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" };
33
	["item-not-found"] = { "cancel", "item-not-found" };
34
	["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" };
35
	["forbidden"] = { "cancel", "forbidden" };
36
};
37
function pubsub_error_reply(stanza, error)
38
	local e = pubsub_errors[error];
39
	local reply = st.error_reply(stanza, unpack(e, 1, 3));
40
	if e[4] then
41
		reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up();
42
	end
43
	return reply;
44
end
45
 
46
function handlers.get_items(origin, stanza, items)
47
	local node = items.attr.node;
48
	local item = items:get_child("item");
49
	local id = item and item.attr.id;
50
	
51
	local ok, results = service:get_items(node, stanza.attr.from, id);
52
	if not ok then
53
		return origin.send(pubsub_error_reply(stanza, results));
54
	end
55
	
56
	local data = st.stanza("items", { node = node });
57
	for _, entry in pairs(results) do
58
		data:add_child(entry);
59
	end
60
	if data then
61
		reply = st.reply(stanza)
62
			:tag("pubsub", { xmlns = xmlns_pubsub })
63
				:add_child(data);
64
	else
65
		reply = pubsub_error_reply(stanza, "item-not-found");
66
	end
67
	return origin.send(reply);
68
end
69
 
70
function handlers.get_subscriptions(origin, stanza, subscriptions)
71
	local node = subscriptions.attr.node;
72
	local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from);
73
	if not ok then
74
		return origin.send(pubsub_error_reply(stanza, ret));
75
	end
76
	local reply = st.reply(stanza)
77
		:tag("pubsub", { xmlns = xmlns_pubsub })
78
			:tag("subscriptions");
79
	for _, sub in ipairs(ret) do
80
		reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up();
81
	end
82
	return origin.send(reply);
83
end
84
 
85
function handlers.set_create(origin, stanza, create)
86
	local node = create.attr.node;
87
	local ok, ret, reply;
88
	if node then
89
		ok, ret = service:create(node, stanza.attr.from);
90
		if ok then
91
			reply = st.reply(stanza);
92
		else
93
			reply = pubsub_error_reply(stanza, ret);
94
		end
95
	else
96
		repeat
97
			node = uuid_generate();
98
			ok, ret = service:create(node, stanza.attr.from);
99
		until ok or ret ~= "conflict";
100
		if ok then
101
			reply = st.reply(stanza)
102
				:tag("pubsub", { xmlns = xmlns_pubsub })
103
					:tag("create", { node = node });
104
		else
105
			reply = pubsub_error_reply(stanza, ret);
106
		end
107
	end
108
	return origin.send(reply);
109
end
110
 
111
function handlers.set_subscribe(origin, stanza, subscribe)
112
	local node, jid = subscribe.attr.node, subscribe.attr.jid;
113
	if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
114
		return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
115
	end
116
	local ok, ret = service:add_subscription(node, stanza.attr.from, jid);
117
	local reply;
118
	if ok then
119
		reply = st.reply(stanza)
120
			:tag("pubsub", { xmlns = xmlns_pubsub })
121
				:tag("subscription", {
122
					node = node,
123
					jid = jid,
124
					subscription = "subscribed"
125
				});
126
	else
127
		reply = pubsub_error_reply(stanza, ret);
128
	end
129
	return origin.send(reply);
130
end
131
 
132
function handlers.set_unsubscribe(origin, stanza, unsubscribe)
133
	local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid;
134
	if jid_bare(jid) ~= jid_bare(stanza.attr.from) then
135
		return origin.send(pubsub_error_reply(stanza, "invalid-jid"));
136
	end
137
	local ok, ret = service:remove_subscription(node, stanza.attr.from, jid);
138
	local reply;
139
	if ok then
140
		reply = st.reply(stanza);
141
	else
142
		reply = pubsub_error_reply(stanza, ret);
143
	end
144
	return origin.send(reply);
145
end
146
 
147
function handlers.set_publish(origin, stanza, publish)
148
	local node = publish.attr.node;
149
	local item = publish:get_child("item");
150
	local id = (item and item.attr.id) or uuid_generate();
151
	local ok, ret = service:publish(node, stanza.attr.from, id, item);
152
	local reply;
153
	if ok then
154
		reply = st.reply(stanza)
155
			:tag("pubsub", { xmlns = xmlns_pubsub })
156
				:tag("publish", { node = node })
157
					:tag("item", { id = id });
158
	else
159
		reply = pubsub_error_reply(stanza, ret);
160
	end
161
	return origin.send(reply);
162
end
163
 
164
function handlers.set_retract(origin, stanza, retract)
165
	local node, notify = retract.attr.node, retract.attr.notify;
166
	notify = (notify == "1") or (notify == "true");
167
	local item = retract:get_child("item");
168
	local id = item and item.attr.id
169
	local reply, notifier;
170
	if notify then
171
		notifier = st.stanza("retract", { id = id });
172
	end
173
	local ok, ret = service:retract(node, stanza.attr.from, id, notifier);
174
	if ok then
175
		reply = st.reply(stanza);
176
	else
177
		reply = pubsub_error_reply(stanza, ret);
178
	end
179
	return origin.send(reply);
180
end
181
 
182
function simple_broadcast(node, jids, item)
183
	item = st.clone(item);
184
	item.attr.xmlns = nil; -- Clear the pubsub namespace
185
	local message = st.message({ from = module.host, type = "headline" })
186
		:tag("event", { xmlns = xmlns_pubsub_event })
187
			:tag("items", { node = node })
188
				:add_child(item);
189
	for jid in pairs(jids) do
190
		module:log("debug", "Sending notification to %s", jid);
191
		message.attr.to = jid;
192
		core_post_stanza(hosts[module.host], message);
193
	end
194
end
195
 
196
module:hook("iq/host/http://jabber.org/protocol/pubsub:pubsub", handle_pubsub_iq);
197
 
198
local disco_info;
199
 
200
local feature_map = {
201
	create = { "create-nodes", autocreate_on_publish and "instant-nodes", "item-ids" };
202
	retract = { "delete-items", "retract-items" };
203
	publish = { "publish" };
204
	get_items = { "retrieve-items" };
205
	add_subscription = { "subscribe" };
206
	get_subscriptions = { "retrieve-subscriptions" };
207
};
208
 
209
local function add_disco_features_from_service(disco, service)
210
	for method, features in pairs(feature_map) do
211
		if service[method] then
212
			for _, feature in ipairs(features) do
213
				if feature then
214
					disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up();
215
				end
216
			end
217
		end
218
	end
219
	for affiliation in pairs(service.config.capabilities) do
220
		if affiliation ~= "none" and affiliation ~= "owner" then
221
			disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up();
222
		end
223
	end
224
end
225
 
226
local function build_disco_info(service)
227
	local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" })
228
		:tag("identity", { category = "pubsub", type = "service" }):up()
229
		:tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up();
230
	add_disco_features_from_service(disco_info, service);
231
	return disco_info;
232
end
233
 
234
module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event)
235
	local origin, stanza = event.origin, event.stanza;
236
	local node = stanza.tags[1].attr.node;
237
	if not node then
238
		return origin.send(st.reply(stanza):add_child(disco_info));
239
	else
240
		local ok, ret = service:get_nodes(stanza.attr.from);
241
		if ok and not ret[node] then
242
			ok, ret = false, "item-not-found";
243
		end
244
		if not ok then
245
			return origin.send(pubsub_error_reply(stanza, ret));
246
		end
247
		local reply = st.reply(stanza)
248
			:tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node })
249
				:tag("identity", { category = "pubsub", type = "leaf" });
250
		return origin.send(reply);
251
	end
252
end);
253
 
254
local function handle_disco_items_on_node(event)
255
	local stanza, origin = event.stanza, event.origin;
256
	local query = stanza.tags[1];
257
	local node = query.attr.node;
258
	local ok, ret = service:get_items(node, stanza.attr.from);
259
	if not ok then
260
		return origin.send(pubsub_error_reply(stanza, ret));
261
	end
262
	
263
	local reply = st.reply(stanza)
264
		:tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node });
265
	
266
	for id, item in pairs(ret) do
267
		reply:tag("item", { jid = module.host, name = id }):up();
268
	end
269
	
270
	return origin.send(reply);
271
end
272
 
273
 
274
module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event)
275
	if event.stanza.tags[1].attr.node then
276
		return handle_disco_items_on_node(event);
277
	end
278
	local ok, ret = service:get_nodes(event.stanza.attr.from);
279
	if not ok then
280
		event.origin.send(pubsub_error_reply(stanza, ret));
281
	else
282
		local reply = st.reply(event.stanza)
283
			:tag("query", { xmlns = "http://jabber.org/protocol/disco#items" });
284
		for node, node_obj in pairs(ret) do
285
			reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up();
286
		end
287
		event.origin.send(reply);
288
	end
289
	return true;
290
end);
291
 
292
local admin_aff = module:get_option_string("default_admin_affiliation", "owner");
293
local function get_affiliation(jid)
294
	local bare_jid = jid_bare(jid);
295
	if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then
296
		return admin_aff;
297
	end
298
end
299
 
300
function set_service(new_service)
301
	service = new_service;
302
	module.environment.service = service;
303
	disco_info = build_disco_info(service);
304
end
305
 
306
function module.save()
307
	return { service = service };
308
end
309
 
310
function module.restore(data)
311
	set_service(data.service);
312
end
313
 
314
set_service(pubsub.new({
315
	capabilities = {
316
		none = {
317
			create = false;
318
			publish = false;
319
			retract = false;
320
			get_nodes = true;
321
			
322
			subscribe = true;
323
			unsubscribe = true;
324
			get_subscription = true;
325
			get_subscriptions = true;
326
			get_items = true;
327
			
328
			subscribe_other = false;
329
			unsubscribe_other = false;
330
			get_subscription_other = false;
331
			get_subscriptions_other = false;
332
			
333
			be_subscribed = true;
334
			be_unsubscribed = true;
335
			
336
			set_affiliation = false;
337
		};
338
		owner = {
339
			create = true;
340
			publish = true;
341
			retract = true;
342
			get_nodes = true;
343
			
344
			subscribe = true;
345
			unsubscribe = true;
346
			get_subscription = true;
347
			get_subscriptions = true;
348
			get_items = true;
349
			
350
			
351
			subscribe_other = true;
352
			unsubscribe_other = true;
353
			get_subscription_other = true;
354
			get_subscriptions_other = true;
355
			
356
			be_subscribed = true;
357
			be_unsubscribed = true;
358
			
359
			set_affiliation = true;
360
		};
361
	};
362
	
363
	autocreate_on_publish = autocreate_on_publish;
364
	autocreate_on_subscribe = autocreate_on_subscribe;
365
	
366
	broadcaster = simple_broadcast;
367
	get_affiliation = get_affiliation;
368
	
369
	normalize_jid = jid_bare;
370
}));