plugins/mod_presence.lua

1
-- Prosody IM
2
-- Copyright (C) 2008-2010 Matthew Wild
3
-- Copyright (C) 2008-2010 Waqas Hussain
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 log = module._log;
10
 
11
local require = require;
12
local pairs, ipairs = pairs, ipairs;
13
local t_insert = table.insert;
14
local s_find = string.find;
15
local tonumber = tonumber;
16
 
17
local st = require "util.stanza";
18
local jid_split = require "util.jid".split;
19
local jid_bare = require "util.jid".bare;
20
local hosts = hosts;
21
local NULL = {};
22
 
23
local rostermanager = require "core.rostermanager";
24
local sessionmanager = require "core.sessionmanager";
25
 
26
local function select_top_resources(user)
27
	local priority = 0;
28
	local recipients = {};
29
	for _, session in pairs(user.sessions) do -- find resource with greatest priority
30
		if session.presence then
31
			-- TODO check active privacy list for session
32
			local p = session.priority;
33
			if p > priority then
34
				priority = p;
35
				recipients = {session};
36
			elseif p == priority then
37
				t_insert(recipients, session);
38
			end
39
		end
40
	end
41
	return recipients;
42
end
43
local function recalc_resource_map(user)
44
	if user then
45
		user.top_resources = select_top_resources(user);
46
		if #user.top_resources == 0 then user.top_resources = nil; end
47
	end
48
end
49
 
50
local ignore_presence_priority = module:get_option("ignore_presence_priority");
51
 
52
function handle_normal_presence(origin, stanza)
53
	if ignore_presence_priority then
54
		local priority = stanza:child_with_name("priority");
55
		if priority and priority[1] ~= "0" then
56
			for i=#priority.tags,1,-1 do priority.tags[i] = nil; end
57
			for i=#priority,1,-1 do priority[i] = nil; end
58
			priority[1] = "0";
59
		end
60
	end
61
	local priority = stanza:child_with_name("priority");
62
	if priority and #priority > 0 then
63
		priority = priority:get_text();
64
		if s_find(priority, "^[+-]?[0-9]+$") then
65
			priority = tonumber(priority);
66
			if priority < -128 then priority = -128 end
67
			if priority > 127 then priority = 127 end
68
		else priority = 0; end
69
	else priority = 0; end
70
	if full_sessions[origin.full_jid] then -- if user is still connected
71
		origin.send(stanza); -- reflect their presence back to them
72
	end
73
	local roster = origin.roster;
74
	local node, host = origin.username, origin.host;
75
	local user = bare_sessions[node.."@"..host];
76
	for _, res in pairs(user and user.sessions or NULL) do -- broadcast to all resources
77
		if res ~= origin and res.presence then -- to resource
78
			stanza.attr.to = res.full_jid;
79
			core_post_stanza(origin, stanza, true);
80
		end
81
	end
82
	for jid, item in pairs(roster) do -- broadcast to all interested contacts
83
		if item.subscription == "both" or item.subscription == "from" then
84
			stanza.attr.to = jid;
85
			core_post_stanza(origin, stanza, true);
86
		end
87
	end
88
	if stanza.attr.type == nil and not origin.presence then -- initial presence
89
		origin.presence = stanza; -- FIXME repeated later
90
		local probe = st.presence({from = origin.full_jid, type = "probe"});
91
		for jid, item in pairs(roster) do -- probe all contacts we are subscribed to
92
			if item.subscription == "both" or item.subscription == "to" then
93
				probe.attr.to = jid;
94
				core_post_stanza(origin, probe, true);
95
			end
96
		end
97
		for _, res in pairs(user and user.sessions or NULL) do -- broadcast from all available resources
98
			if res ~= origin and res.presence then
99
				res.presence.attr.to = origin.full_jid;
100
				core_post_stanza(res, res.presence, true);
101
				res.presence.attr.to = nil;
102
			end
103
		end
104
		if roster.pending then -- resend incoming subscription requests
105
			for jid in pairs(roster.pending) do
106
				origin.send(st.presence({type="subscribe", from=jid})); -- TODO add to attribute? Use original?
107
			end
108
		end
109
		local request = st.presence({type="subscribe", from=origin.username.."@"..origin.host});
110
		for jid, item in pairs(roster) do -- resend outgoing subscription requests
111
			if item.ask then
112
				request.attr.to = jid;
113
				core_post_stanza(origin, request, true);
114
			end
115
		end
116
 
117
		if priority >= 0 then
118
                        local event = { origin = origin }
119
                        module:fire_event('message/offline/broadcast', event);
120
		end
121
	end
122
	if stanza.attr.type == "unavailable" then
123
		origin.presence = nil;
124
		if origin.priority then
125
			origin.priority = nil;
126
			recalc_resource_map(user);
127
		end
128
		if origin.directed then
129
			for jid in pairs(origin.directed) do
130
				stanza.attr.to = jid;
131
				core_post_stanza(origin, stanza, true);
132
			end
133
			origin.directed = nil;
134
		end
135
	else
136
		origin.presence = stanza;
137
		if origin.priority ~= priority then
138
			origin.priority = priority;
139
			recalc_resource_map(user);
140
		end
141
	end
142
	stanza.attr.to = nil; -- reset it
143
end
144
 
145
function send_presence_of_available_resources(user, host, jid, recipient_session, stanza)
146
	local h = hosts[host];
147
	local count = 0;
148
	if h and h.type == "local" then
149
		local u = h.sessions[user];
150
		if u then
151
			for k, session in pairs(u.sessions) do
152
				local pres = session.presence;
153
				if pres then
154
					if stanza then pres = stanza; pres.attr.from = session.full_jid; end
155
					pres.attr.to = jid;
156
					core_post_stanza(session, pres, true);
157
					pres.attr.to = nil;
158
					count = count + 1;
159
				end
160
			end
161
		end
162
	end
163
	log("debug", "broadcasted presence of "..count.." resources from "..user.."@"..host.." to "..jid);
164
	return count;
165
end
166
 
167
function handle_outbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
168
	local node, host = jid_split(from_bare);
169
	if to_bare == from_bare then return; end -- No self contacts
170
	local st_from, st_to = stanza.attr.from, stanza.attr.to;
171
	stanza.attr.from, stanza.attr.to = from_bare, to_bare;
172
	log("debug", "outbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
173
	if stanza.attr.type == "probe" then
174
		stanza.attr.from, stanza.attr.to = st_from, st_to;
175
		return;
176
	elseif stanza.attr.type == "subscribe" then
177
		-- 1. route stanza
178
		-- 2. roster push (subscription = none, ask = subscribe)
179
		if rostermanager.set_contact_pending_out(node, host, to_bare) then
180
			rostermanager.roster_push(node, host, to_bare);
181
		end -- else file error
182
		core_post_stanza(origin, stanza);
183
	elseif stanza.attr.type == "unsubscribe" then
184
		-- 1. route stanza
185
		-- 2. roster push (subscription = none or from)
186
		if rostermanager.unsubscribe(node, host, to_bare) then
187
			rostermanager.roster_push(node, host, to_bare); -- FIXME do roster push when roster has in fact not changed?
188
		end -- else file error
189
		core_post_stanza(origin, stanza);
190
	elseif stanza.attr.type == "subscribed" then
191
		-- 1. route stanza
192
		-- 2. roster_push ()
193
		-- 3. send_presence_of_available_resources
194
		if rostermanager.subscribed(node, host, to_bare) then
195
			rostermanager.roster_push(node, host, to_bare);
196
		end
197
		core_post_stanza(origin, stanza);
198
		send_presence_of_available_resources(node, host, to_bare, origin);
199
	elseif stanza.attr.type == "unsubscribed" then
200
		-- 1. route stanza
201
		-- 2. roster push (subscription = none or to)
202
		if rostermanager.unsubscribed(node, host, to_bare) then
203
			rostermanager.roster_push(node, host, to_bare);
204
		end
205
		core_post_stanza(origin, stanza);
206
	end
207
	stanza.attr.from, stanza.attr.to = st_from, st_to;
208
	return true;
209
end
210
 
211
function handle_inbound_presence_subscriptions_and_probes(origin, stanza, from_bare, to_bare)
212
	local node, host = jid_split(to_bare);
213
	local st_from, st_to = stanza.attr.from, stanza.attr.to;
214
	stanza.attr.from, stanza.attr.to = from_bare, to_bare;
215
	log("debug", "inbound presence "..stanza.attr.type.." from "..from_bare.." for "..to_bare);
216
	
217
	if stanza.attr.type == "probe" then
218
		local result, err = rostermanager.is_contact_subscribed(node, host, from_bare);
219
		if result then
220
			if 0 == send_presence_of_available_resources(node, host, st_from, origin) then
221
				core_post_stanza(hosts[host], st.presence({from=to_bare, to=st_from, type="unavailable"}), true); -- TODO send last activity
222
			end
223
		elseif not err then
224
			core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unsubscribed"}), true);
225
		end
226
	elseif stanza.attr.type == "subscribe" then
227
		if rostermanager.is_contact_subscribed(node, host, from_bare) then
228
			core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="subscribed"}), true); -- already subscribed
229
			-- Sending presence is not clearly stated in the RFC, but it seems appropriate
230
			if 0 == send_presence_of_available_resources(node, host, from_bare, origin) then
231
				core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- TODO send last activity
232
			end
233
		else
234
			core_post_stanza(hosts[host], st.presence({from=to_bare, to=from_bare, type="unavailable"}), true); -- acknowledging receipt
235
			if not rostermanager.is_contact_pending_in(node, host, from_bare) then
236
				if rostermanager.set_contact_pending_in(node, host, from_bare) then
237
					sessionmanager.send_to_available_resources(node, host, stanza);
238
				end -- TODO else return error, unable to save
239
			end
240
		end
241
	elseif stanza.attr.type == "unsubscribe" then
242
		if rostermanager.process_inbound_unsubscribe(node, host, from_bare) then
243
			sessionmanager.send_to_interested_resources(node, host, stanza);
244
			rostermanager.roster_push(node, host, from_bare);
245
		end
246
	elseif stanza.attr.type == "subscribed" then
247
		if rostermanager.process_inbound_subscription_approval(node, host, from_bare) then
248
			sessionmanager.send_to_interested_resources(node, host, stanza);
249
			rostermanager.roster_push(node, host, from_bare);
250
		end
251
	elseif stanza.attr.type == "unsubscribed" then
252
		if rostermanager.process_inbound_subscription_cancellation(node, host, from_bare) then
253
			sessionmanager.send_to_interested_resources(node, host, stanza);
254
			rostermanager.roster_push(node, host, from_bare);
255
		end
256
	end -- discard any other type
257
	stanza.attr.from, stanza.attr.to = st_from, st_to;
258
	return true;
259
end
260
 
261
local outbound_presence_handler = function(data)
262
	-- outbound presence recieved
263
	local origin, stanza = data.origin, data.stanza;
264
 
265
	local to = stanza.attr.to;
266
	if to then
267
		local t = stanza.attr.type;
268
		if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes
269
			return handle_outbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
270
		end
271
 
272
		local to_bare = jid_bare(to);
273
		local roster = origin.roster;
274
		if roster and not(roster[to_bare] and (roster[to_bare].subscription == "both" or roster[to_bare].subscription == "from")) then -- directed presence
275
			origin.directed = origin.directed or {};
276
			if t then -- removing from directed presence list on sending an error or unavailable
277
				origin.directed[to] = nil; -- FIXME does it make more sense to add to_bare rather than to?
278
			else
279
				origin.directed[to] = true; -- FIXME does it make more sense to add to_bare rather than to?
280
			end
281
		end
282
	end -- TODO maybe handle normal presence here, instead of letting it pass to incoming handlers?
283
end
284
 
285
module:hook("pre-presence/full", outbound_presence_handler);
286
module:hook("pre-presence/bare", outbound_presence_handler);
287
module:hook("pre-presence/host", outbound_presence_handler);
288
 
289
module:hook("presence/bare", function(data)
290
	-- inbound presence to bare JID recieved
291
	local origin, stanza = data.origin, data.stanza;
292
 
293
	local to = stanza.attr.to;
294
	local t = stanza.attr.type;
295
	if to then
296
		if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to bare JID
297
			return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
298
		end
299
	
300
		local user = bare_sessions[to];
301
		if user then
302
			for _, session in pairs(user.sessions) do
303
				if session.presence then -- only send to available resources
304
					session.send(stanza);
305
				end
306
			end
307
		end -- no resources not online, discard
308
	elseif not t or t == "unavailable" then
309
		handle_normal_presence(origin, stanza);
310
	end
311
	return true;
312
end);
313
module:hook("presence/full", function(data)
314
	-- inbound presence to full JID recieved
315
	local origin, stanza = data.origin, data.stanza;
316
 
317
	local t = stanza.attr.type;
318
	if t ~= nil and t ~= "unavailable" and t ~= "error" then -- check for subscriptions and probes sent to full JID
319
		return handle_inbound_presence_subscriptions_and_probes(origin, stanza, jid_bare(stanza.attr.from), jid_bare(stanza.attr.to));
320
	end
321
 
322
	local session = full_sessions[stanza.attr.to];
323
	if session then
324
		-- TODO fire post processing event
325
		session.send(stanza);
326
	end -- resource not online, discard
327
	return true;
328
end);
329
module:hook("presence/host", function(data)
330
	-- inbound presence to the host
331
	local origin, stanza = data.origin, data.stanza;
332
	
333
	local from_bare = jid_bare(stanza.attr.from);
334
	local t = stanza.attr.type;
335
	if t == "probe" then
336
		core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
337
	elseif t == "subscribe" then
338
		core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id, type = "subscribed" }));
339
		core_post_stanza(hosts[module.host], st.presence({ from = module.host, to = from_bare, id = stanza.attr.id }));
340
	end
341
	return true;
342
end);
343
 
344
module:hook("resource-unbind", function(event)
345
	local session, err = event.session, event.error;
346
	-- Send unavailable presence
347
	if session.presence then
348
		local pres = st.presence{ type = "unavailable" };
349
		if not(err) or err == "closed" then err = "connection closed"; end
350
		pres:tag("status"):text("Disconnected: "..err):up();
351
		session:dispatch_stanza(pres);
352
	elseif session.directed then
353
		local pres = st.presence{ type = "unavailable", from = session.full_jid };
354
		if not(err) or err == "closed" then err = "connection closed"; end
355
		pres:tag("status"):text("Disconnected: "..err):up();
356
		for jid in pairs(session.directed) do
357
			pres.attr.to = jid;
358
			core_post_stanza(session, pres, true);
359
		end
360
		session.directed = nil;
361
	end
362
end);