plugins/muc_ping.lua

1
-- XEP-0410: MUC Self-Ping
2
--
3
-- Detects silent MUC disconnects (e.g. a remote MUC service restarting
4
-- without us receiving an unavailable presence) by pinging our own
5
-- occupant JID in rooms we have received no traffic from for a while,
6
-- and rejoining when the ping shows we are no longer joined.
7
local verse = require "verse";
8
local jid = require "prosody.util.jid";
9
 
10
local xmlns_ping = "urn:xmpp:ping";
11
local xmlns_muc = "http://jabber.org/protocol/muc";
12
 
13
-- Ping error conditions that still mean we are joined (XEP-0410 ยง3.2)
14
local still_joined = {
15
	["service-unavailable"] = true;
16
	["feature-not-implemented"] = true;
17
};
18
 
19
-- Conditions meaning the MUC was unreachable, so joined-ness is unknown
20
local unreachable = {
21
	["remote-server-not-found"] = true;
22
	["remote-server-timeout"] = true;
23
	["timeout"] = true;
24
};
25
 
26
function riddim.plugins.muc_ping(bot)
27
	local stream = bot.stream;
28
	local interval = bot.config.muc_ping_interval or 300;
29
	local timeout = bot.config.muc_ping_timeout or 60;
30
	local serial = 0;
31
 
32
	-- Any stanza from a room is proof that we are still connected to it
33
	stream:hook("stanza", function (stanza)
34
		local room_jid = jid.bare(stanza.attr.from);
35
		local room = room_jid and stream.rooms[room_jid];
36
		if room then
37
			room.last_activity = os.time();
38
		end
39
	end, 1000);
40
 
41
	stream:hook("groupchat/joining", function (room)
42
		room.last_activity = os.time();
43
	end, 1000);
44
 
45
	local function rejoin(room, forget_occupants)
46
		if forget_occupants then
47
			-- The room's presence handler keeps a reference to this table,
48
			-- so it must be cleared in place, not replaced
49
			for nick in pairs(room.occupants) do
50
				room.occupants[nick] = nil;
51
			end
52
		end
53
		local join_st = verse.presence():tag("x", { xmlns = xmlns_muc }):reset();
54
		stream:event("pre-groupchat/joining", join_st);
55
		room:send(join_st);
56
	end
57
 
58
	local function ping_room(room)
59
		serial = serial + 1;
60
		local this_ping = serial;
61
		room.pending_ping = this_ping;
62
		local function result(ok, condition)
63
			if room.pending_ping ~= this_ping then return; end
64
			room.pending_ping = nil;
65
			room.last_activity = os.time();
66
			if stream.rooms[room.jid] ~= room then return; end -- room was left
67
			if ok or still_joined[condition] then
68
				return;
69
			elseif unreachable[condition] then
70
				-- Try joining in case we were dropped (harmless if we are
71
				-- still an occupant), and keep pinging
72
				stream:warn("muc_ping: no response from %s (%s), rejoining", room.jid, condition);
73
				rejoin(room, false);
74
			else -- e.g. not-acceptable, item-not-found: we are not joined
75
				stream:warn("muc_ping: not in %s (%s), rejoining", room.jid, condition);
76
				rejoin(room, true);
77
			end
78
		end
79
		stream:debug("muc_ping: pinging %s", room.jid);
80
		stream:send_iq(verse.iq({ to = room.jid.."/"..room.nick, type = "get" })
81
			:tag("ping", { xmlns = xmlns_ping }), function (reply)
82
				if reply.attr.type == "error" then
83
					local _, condition = reply:get_error();
84
					result(false, condition or "undefined-condition");
85
				else
86
					result(true);
87
				end
88
			end);
89
		verse.add_task(timeout, function ()
90
			result(false, "timeout");
91
		end);
92
	end
93
 
94
	verse.add_task(interval, function ()
95
		local now = os.time();
96
		for _, room in pairs(stream.rooms) do
97
			if not room.pending_ping and now - (room.last_activity or now) >= interval then
98
				ping_room(room);
99
			end
100
			if not room.last_activity then
101
				room.last_activity = now;
102
			end
103
		end
104
		return interval;
105
	end);
106
end