plugins/pubsub.lua

1
local verse = require "verse";
2
 
3
local t_insert = table.insert;
4
 
5
local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
6
local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
7
local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
8
-- local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors";
9
 
10
local pubsub = {};
11
local pubsub_mt = { __index = pubsub };
12
 
13
function verse.plugins.pubsub(stream)
14
	stream.pubsub = setmetatable({ stream = stream }, pubsub_mt);
15
	stream:hook("message", function (message)
16
		local m_from = message.attr.from;
17
		for pubsub_event in message:childtags("event", xmlns_pubsub_event) do
18
			local items = pubsub_event:get_child("items");
19
			if items then
20
				local node = items.attr.node;
21
				for item in items:childtags("item") do
22
					stream:event("pubsub/event", {
23
						from = m_from;
24
						node = node;
25
						item = item;
26
					});
27
				end
28
				for retract in items:childtags("retract") do
29
					stream:event("pubsub/retraction", {
30
						from = m_from;
31
						node = node;
32
						item = retract;
33
					});
34
				end
35
			end
36
		end
37
	end);
38
	return true;
39
end
40
 
41
-- COMPAT
42
function pubsub:create(server, node, callback)
43
	return self:service(server):node(node):create(nil, callback);
44
end
45
 
46
function pubsub:subscribe(server, node, jid, callback)
47
	return self:service(server):node(node):subscribe(jid, nil, callback);
48
end
49
 
50
function pubsub:publish(server, node, id, item, callback)
51
	return self:service(server):node(node):publish(id, nil, item, callback);
52
end
53
 
54
--------------------------------------------------------------------------
55
---------------------New and improved PubSub interface--------------------
56
--------------------------------------------------------------------------
57
 
58
local pubsub_service = {};
59
local pubsub_service_mt = { __index = pubsub_service };
60
 
61
-- TODO should the property be named 'jid' instead?
62
function pubsub:service(service)
63
	return setmetatable({ stream = self.stream, service = service }, pubsub_service_mt)
64
end
65
 
66
-- Helper function for iq+pubsub tags
67
 
68
local function pubsub_iq(iq_type, to, ns, op, node, jid, item_id, op_attr_extra)
69
	local st = verse.iq{ type = iq_type or "get", to = to }
70
		:tag("pubsub", { xmlns = ns or xmlns_pubsub }) -- ns would be ..#owner
71
			local op_attr = { node = node, jid = jid };
72
			if op_attr_extra then
73
				for k, v in pairs(op_attr_extra) do
74
					op_attr[k] = v;
75
				end
76
			end
77
			if op then st:tag(op, op_attr); end
78
			if item_id then
79
				st:tag("item", { id = item_id ~= true and item_id or nil });
80
			end
81
	return st;
82
end
83
 
84
-- http://xmpp.org/extensions/xep-0060.html#entity-subscriptions
85
function pubsub_service:subscriptions(callback)
86
	self.stream:send_iq(pubsub_iq(nil, self.service, nil, "subscriptions")
87
	, callback and function (result)
88
		if result.attr.type == "result" then
89
			local ps = result:get_child("pubsub", xmlns_pubsub);
90
			local subs = ps and ps:get_child("subscriptions");
91
			local nodes = {};
92
			if subs then
93
				for sub in subs:childtags("subscription") do
94
					local node = self:node(sub.attr.node)
95
					node.subscription = sub;
96
					node.subscribed_jid = sub.attr.jid;
97
					t_insert(nodes, node);
98
					-- FIXME Good enough?
99
					-- Or how about:
100
					-- nodes[node] = sub;
101
				end
102
			end
103
			callback(nodes);
104
		else
105
			callback(false, result:get_error());
106
		end
107
	end or nil);
108
end
109
 
110
-- http://xmpp.org/extensions/xep-0060.html#entity-affiliations
111
function pubsub_service:affiliations(callback)
112
	self.stream:send_iq(pubsub_iq(nil, self.service, nil, "affiliations")
113
	, callback and function (result)
114
		if result.attr.type == "result" then
115
			local ps = result:get_child("pubsub", xmlns_pubsub);
116
			local affils = ps and ps:get_child("affiliations") or {};
117
			local nodes = {};
118
			if affils then
119
				for affil in affils:childtags("affiliation") do
120
					local node = self:node(affil.attr.node)
121
					node.affiliation = affil;
122
					t_insert(nodes, node);
123
					-- nodes[node] = affil;
124
				end
125
			end
126
			callback(nodes);
127
		else
128
			callback(false, result:get_error());
129
		end
130
	end or nil);
131
end
132
 
133
function pubsub_service:nodes(callback)
134
	self.stream:disco_items(self.service, nil, function(items, ...)
135
		if items then
136
			for i=1,#items do
137
				items[i] = self:node(items[i].node);
138
			end
139
		end
140
		callback(items, ...)
141
	end);
142
end
143
 
144
local pubsub_node = {};
145
local pubsub_node_mt = { __index = pubsub_node };
146
 
147
function pubsub_service:node(node)
148
	return setmetatable({ stream = self.stream, service = self.service, node = node }, pubsub_node_mt)
149
end
150
 
151
function pubsub_mt:__call(service, node)
152
	local s = self:service(service);
153
	return node and s:node(node) or s;
154
end
155
 
156
function pubsub_node:hook(callback, prio)
157
	self._hooks = self._hooks or setmetatable({}, { __mode = 'kv' });
158
	local function hook(event)
159
		-- FIXME service == nil would mean anyone,
160
		-- publishing would be go to your bare jid.
161
		-- So if you're only interestied in your own
162
		-- events, hook your own bare jid.
163
		if (not event.service or event.from == self.service) and event.node == self.node then
164
			return callback(event)
165
		end
166
	end
167
	self._hooks[callback] = hook;
168
	self.stream:hook("pubsub/event", hook, prio);
169
	return hook;
170
end
171
 
172
function pubsub_node:unhook(callback)
173
	if callback then
174
		local hook = self._hooks[callback];
175
		self.stream:unhook("pubsub/event", hook);
176
	elseif self._hooks then
177
		for hook in pairs(self._hooks) do
178
			self.stream:unhook("pubsub/event", hook);
179
		end
180
	end
181
end
182
 
183
function pubsub_node:create(config, callback)
184
	if config ~= nil then
185
		error("Not implemented yet.");
186
	else
187
		self.stream:send_iq(pubsub_iq("set", self.service, nil, "create", self.node), callback);
188
	end
189
end
190
 
191
-- <configure/> and <default/> rolled into one
192
function pubsub_node:configure(config, callback)
193
	if config ~= nil then
194
		error("Not implemented yet.");
195
		--[[
196
		if config == true then
197
			self.stream:send_iq(pubsub_iq("get", self.service, nil, "configure", self.node)
198
			, function(reply)
199
				local form = reply:get_child("pubsub"):get_child("configure"):get_cild("x");
200
				local config = callback(require"prosody.util.dataforms".something(form))
201
				self.stream:send_iq(pubsub_iq("set", config, ...))
202
			end);
203
		end
204
		--]]
205
		-- fetch form and pass it to the callback
206
		-- which would process it and pass it back
207
		-- and then we submit it
208
		-- elseif type(config) == "table" then
209
		-- it's a form or stanza that we submit
210
		-- end
211
		-- this would be done for everything that needs a config
212
	end
213
	self.stream:send_iq(pubsub_iq("set", self.service, nil, config == nil and "default" or "configure", self.node), callback);
214
end
215
 
216
function pubsub_node:publish(id, options, node, callback)
217
	if options ~= nil then
218
		error("Node configuration is not implemented yet.");
219
	end
220
	self.stream:send_iq(pubsub_iq("set", self.service, nil, "publish", self.node, nil, id or true)
221
	:add_child(node)
222
	, callback);
223
end
224
 
225
function pubsub_node:subscribe(jid, options, callback)
226
	jid = jid or self.stream.jid;
227
	if options ~= nil then
228
		error("Subscription configuration is not implemented yet.");
229
	end
230
	self.stream:send_iq(pubsub_iq("set", self.service, nil, "subscribe", self.node, jid)
231
	, callback);
232
end
233
 
234
function pubsub_node:subscription(callback)
235
	error("Not implemented yet.");
236
end
237
 
238
function pubsub_node:affiliation(callback)
239
	error("Not implemented yet.");
240
end
241
 
242
function pubsub_node:unsubscribe(jid, callback)
243
	jid = jid or self.subscribed_jid or self.stream.jid;
244
	self.stream:send_iq(pubsub_iq("set", self.service, nil, "unsubscribe", self.node, jid)
245
	, callback);
246
end
247
 
248
function pubsub_node:configure_subscription(options, callback)
249
	error("Not implemented yet.");
250
end
251
 
252
function pubsub_node:items(full, callback)
253
	if full then
254
		return self:item(nil, callback);
255
	else
256
		self.stream:disco_items(self.service, self.node, callback);
257
	end
258
end
259
 
260
function pubsub_node:item(id, callback)
261
	self.stream:send_iq(pubsub_iq("get", self.service, nil, "items", self.node, nil, id)
262
	, callback);
263
end
264
 
265
function pubsub_node:retract(id, notify, callback)
266
	if type(notify) == "function" then -- COMPAT w/ older versions before 'notify' was added
267
		notify, callback = false, notify;
268
	end
269
	self.stream:send_iq(
270
		pubsub_iq(
271
			"set",
272
			self.service,
273
			nil,
274
			"retract",
275
			self.node,
276
			nil,
277
			id,
278
			{ notify = notify and "1" or nil }
279
		),
280
		callback
281
	);
282
end
283
 
284
function pubsub_node:purge(notify, callback)
285
	self.stream:send_iq(
286
		pubsub_iq(
287
			"set",
288
			self.service,
289
			xmlns_pubsub_owner,
290
			"purge",
291
			self.node,
292
			nil,
293
			nil,
294
			{ notify = notify and "1" or nil }
295
		),
296
		callback
297
	);
298
end
299
 
300
function pubsub_node:delete(redirect_uri, callback)
301
	assert(not redirect_uri, "Not implemented yet.");
302
	self.stream:send_iq(pubsub_iq("set", self.service, xmlns_pubsub_owner, "delete", self.node)
303
	, callback);
304
end