plugins/muc/muc.lib.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 select = select;
10
local pairs, ipairs = pairs, ipairs;
11
 
12
local datamanager = require "util.datamanager";
13
local datetime = require "util.datetime";
14
 
15
local dataform = require "util.dataforms";
16
 
17
local jid_split = require "util.jid".split;
18
local jid_bare = require "util.jid".bare;
19
local jid_prep = require "util.jid".prep;
20
local st = require "util.stanza";
21
local log = require "util.logger".init("mod_muc");
22
local multitable_new = require "util.multitable".new;
23
local t_insert, t_remove = table.insert, table.remove;
24
local setmetatable = setmetatable;
25
local base64 = require "util.encodings".base64;
26
local md5 = require "util.hashes".md5;
27
 
28
local muc_domain = nil; --module:get_host();
29
local default_history_length = 20;
30
 
31
------------
32
local function filter_xmlns_from_array(array, filters)
33
	local count = 0;
34
	for i=#array,1,-1 do
35
		local attr = array[i].attr;
36
		if filters[attr and attr.xmlns] then
37
			t_remove(array, i);
38
			count = count + 1;
39
		end
40
	end
41
	return count;
42
end
43
local function filter_xmlns_from_stanza(stanza, filters)
44
	if filters then
45
		if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
46
			return stanza, filter_xmlns_from_array(stanza, filters);
47
		end
48
	end
49
	return stanza, 0;
50
end
51
local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
52
local function get_filtered_presence(stanza)
53
	return filter_xmlns_from_stanza(st.clone(stanza):reset(), presence_filters);
54
end
55
local kickable_error_conditions = {
56
	["gone"] = true;
57
	["internal-server-error"] = true;
58
	["item-not-found"] = true;
59
	["jid-malformed"] = true;
60
	["recipient-unavailable"] = true;
61
	["redirect"] = true;
62
	["remote-server-not-found"] = true;
63
	["remote-server-timeout"] = true;
64
	["service-unavailable"] = true;
65
	["malformed error"] = true;
66
};
67
 
68
local function get_error_condition(stanza)
69
	local _, condition = stanza:get_error();
70
	return condition or "malformed error";
71
end
72
 
73
local function is_kickable_error(stanza)
74
	local cond = get_error_condition(stanza);
75
	return kickable_error_conditions[cond] and cond;
76
end
77
local function getUsingPath(stanza, path, getText)
78
	local tag = stanza;
79
	for _, name in ipairs(path) do
80
		if type(tag) ~= 'table' then return; end
81
		tag = tag:child_with_name(name);
82
	end
83
	if tag and getText then tag = table.concat(tag); end
84
	return tag;
85
end
86
local function getTag(stanza, path) return getUsingPath(stanza, path); end
87
local function getText(stanza, path) return getUsingPath(stanza, path, true); end
88
-----------
89
 
90
local room_mt = {};
91
room_mt.__index = room_mt;
92
 
93
function room_mt:get_default_role(affiliation)
94
	if affiliation == "owner" or affiliation == "admin" then
95
		return "moderator";
96
	elseif affiliation == "member" then
97
		return "participant";
98
	elseif not affiliation then
99
		if not self:is_members_only() then
100
			return self:is_moderated() and "visitor" or "participant";
101
		end
102
	end
103
end
104
 
105
function room_mt:broadcast_presence(stanza, sid, code, nick)
106
	stanza = get_filtered_presence(stanza);
107
	local occupant = self._occupants[stanza.attr.from];
108
	stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
109
		:tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up();
110
	if code then
111
		stanza:tag("status", {code=code}):up();
112
	end
113
	self:broadcast_except_nick(stanza, stanza.attr.from);
114
	local me = self._occupants[stanza.attr.from];
115
	if me then
116
		stanza:tag("status", {code='110'}):up();
117
		stanza.attr.to = sid;
118
		self:_route_stanza(stanza);
119
	end
120
end
121
function room_mt:broadcast_message(stanza, historic)
122
	local to = stanza.attr.to;
123
	for occupant, o_data in pairs(self._occupants) do
124
		for jid in pairs(o_data.sessions) do
125
			stanza.attr.to = jid;
126
			self:_route_stanza(stanza);
127
		end
128
	end
129
	stanza.attr.to = to;
130
	if historic then -- add to history
131
		local history = self._data['history'];
132
		if not history then history = {}; self._data['history'] = history; end
133
		stanza = st.clone(stanza);
134
		stanza.attr.to = "";
135
		local stamp = datetime.datetime();
136
		local chars = #tostring(stanza);
137
		stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203
138
		stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
139
		local entry = { stanza = stanza, stamp = stamp };
140
		t_insert(history, entry);
141
		while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
142
	end
143
end
144
function room_mt:broadcast_except_nick(stanza, nick)
145
	for rnick, occupant in pairs(self._occupants) do
146
		if rnick ~= nick then
147
			for jid in pairs(occupant.sessions) do
148
				stanza.attr.to = jid;
149
				self:_route_stanza(stanza);
150
			end
151
		end
152
	end
153
end
154
 
155
function room_mt:send_occupant_list(to)
156
	local current_nick = self._jid_nick[to];
157
	for occupant, o_data in pairs(self._occupants) do
158
		if occupant ~= current_nick then
159
			local pres = get_filtered_presence(o_data.sessions[o_data.jid]);
160
			pres.attr.to, pres.attr.from = to, occupant;
161
			pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
162
				:tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up();
163
			self:_route_stanza(pres);
164
		end
165
	end
166
end
167
function room_mt:send_history(to, stanza)
168
	local history = self._data['history']; -- send discussion history
169
	if history then
170
		local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc");
171
		local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc");
172
		
173
		local maxchars = history_tag and tonumber(history_tag.attr.maxchars);
174
		if maxchars then maxchars = math.floor(maxchars); end
175
		
176
		local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history);
177
		if not history_tag then maxstanzas = 20; end
178
 
179
		local seconds = history_tag and tonumber(history_tag.attr.seconds);
180
		if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end
181
 
182
		local since = history_tag and history_tag.attr.since;
183
		if since then since = datetime.parse(since); since = since and datetime.datetime(since); end
184
		if seconds and (not since or since < seconds) then since = seconds; end
185
 
186
		local n = 0;
187
		local charcount = 0;
188
		local stanzacount = 0;
189
		
190
		for i=#history,1,-1 do
191
			local entry = history[i];
192
			if maxchars then
193
				if not entry.chars then
194
					entry.stanza.attr.to = "";
195
					entry.chars = #tostring(entry.stanza);
196
				end
197
				charcount = charcount + entry.chars + #to;
198
				if charcount > maxchars then break; end
199
			end
200
			if since and since > entry.stamp then break; end
201
			if n + 1 > maxstanzas then break; end
202
			n = n + 1;
203
		end
204
		for i=#history-n+1,#history do
205
			local msg = history[i].stanza;
206
			msg.attr.to = to;
207
			self:_route_stanza(msg);
208
		end
209
	end
210
	if self._data['subject'] then
211
		self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
212
	end
213
end
214
 
215
function room_mt:get_disco_info(stanza)
216
	return st.reply(stanza):query("http://jabber.org/protocol/disco#info")
217
		:tag("identity", {category="conference", type="text", name=self:get_name()}):up()
218
		:tag("feature", {var="http://jabber.org/protocol/muc"}):up()
219
		:tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up()
220
		:tag("feature", {var=self:is_moderated() and "muc_moderated" or "muc_unmoderated"}):up()
221
		:tag("feature", {var=self:is_members_only() and "muc_membersonly" or "muc_open"}):up()
222
		:tag("feature", {var=self:is_persistent() and "muc_persistent" or "muc_temporary"}):up()
223
		:tag("feature", {var=self:is_hidden() and "muc_hidden" or "muc_public"}):up()
224
		:tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up()
225
		:add_child(dataform.new({
226
			{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" },
227
			{ name = "muc#roominfo_description", label = "Description"}
228
		}):form({["muc#roominfo_description"] = self:get_description()}, 'result'))
229
	;
230
end
231
function room_mt:get_disco_items(stanza)
232
	local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
233
	for room_jid in pairs(self._occupants) do
234
		reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
235
	end
236
	return reply;
237
end
238
function room_mt:set_subject(current_nick, subject)
239
	-- TODO check nick's authority
240
	if subject == "" then subject = nil; end
241
	self._data['subject'] = subject;
242
	self._data['subject_from'] = current_nick;
243
	if self.save then self:save(); end
244
	local msg = st.message({type='groupchat', from=current_nick})
245
		:tag('subject'):text(subject):up();
246
	self:broadcast_message(msg, false);
247
	return true;
248
end
249
 
250
local function build_unavailable_presence_from_error(stanza)
251
	local type, condition, text = stanza:get_error();
252
	local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
253
	if text then
254
		error_message = error_message..": "..text;
255
	end
256
	return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to})
257
		:tag('status'):text(error_message);
258
end
259
 
260
function room_mt:set_name(name)
261
	if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end
262
	if self._data.name ~= name then
263
		self._data.name = name;
264
		if self.save then self:save(true); end
265
	end
266
end
267
function room_mt:get_name()
268
	return self._data.name or jid_split(self.jid);
269
end
270
function room_mt:set_description(description)
271
	if description == "" or type(description) ~= "string" then description = nil; end
272
	if self._data.description ~= description then
273
		self._data.description = description;
274
		if self.save then self:save(true); end
275
	end
276
end
277
function room_mt:get_description()
278
	return self._data.description;
279
end
280
function room_mt:set_password(password)
281
	if password == "" or type(password) ~= "string" then password = nil; end
282
	if self._data.password ~= password then
283
		self._data.password = password;
284
		if self.save then self:save(true); end
285
	end
286
end
287
function room_mt:get_password()
288
	return self._data.password;
289
end
290
function room_mt:set_moderated(moderated)
291
	moderated = moderated and true or nil;
292
	if self._data.moderated ~= moderated then
293
		self._data.moderated = moderated;
294
		if self.save then self:save(true); end
295
	end
296
end
297
function room_mt:is_moderated()
298
	return self._data.moderated;
299
end
300
function room_mt:set_members_only(members_only)
301
	members_only = members_only and true or nil;
302
	if self._data.members_only ~= members_only then
303
		self._data.members_only = members_only;
304
		if self.save then self:save(true); end
305
	end
306
end
307
function room_mt:is_members_only()
308
	return self._data.members_only;
309
end
310
function room_mt:set_persistent(persistent)
311
	persistent = persistent and true or nil;
312
	if self._data.persistent ~= persistent then
313
		self._data.persistent = persistent;
314
		if self.save then self:save(true); end
315
	end
316
end
317
function room_mt:is_persistent()
318
	return self._data.persistent;
319
end
320
function room_mt:set_hidden(hidden)
321
	hidden = hidden and true or nil;
322
	if self._data.hidden ~= hidden then
323
		self._data.hidden = hidden;
324
		if self.save then self:save(true); end
325
	end
326
end
327
function room_mt:is_hidden()
328
	return self._data.hidden;
329
end
330
 
331
function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc
332
	local from, to = stanza.attr.from, stanza.attr.to;
333
	local room = jid_bare(to);
334
	local current_nick = self._jid_nick[from];
335
	local type = stanza.attr.type;
336
	log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag());
337
	if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end
338
	if stanza.name == "presence" then
339
		local pr = get_filtered_presence(stanza);
340
		pr.attr.from = current_nick;
341
		if type == "error" then -- error, kick em out!
342
			if current_nick then
343
				log("debug", "kicking %s from %s", current_nick, room);
344
				self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza));
345
			end
346
		elseif type == "unavailable" then -- unavailable
347
			if current_nick then
348
				log("debug", "%s leaving %s", current_nick, room);
349
				local occupant = self._occupants[current_nick];
350
				local new_jid = next(occupant.sessions);
351
				if new_jid == from then new_jid = next(occupant.sessions, new_jid); end
352
				if new_jid then
353
					local jid = occupant.jid;
354
					occupant.jid = new_jid;
355
					occupant.sessions[from] = nil;
356
					pr.attr.to = from;
357
					pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
358
						:tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up()
359
						:tag("status", {code='110'});
360
					self:_route_stanza(pr);
361
					if jid ~= new_jid then
362
						pr = st.clone(occupant.sessions[new_jid])
363
							:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
364
							:tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"});
365
						pr.attr.from = current_nick;
366
						self:broadcast_except_nick(pr, current_nick);
367
					end
368
				else
369
					occupant.role = 'none';
370
					self:broadcast_presence(pr, from);
371
					self._occupants[current_nick] = nil;
372
				end
373
				self._jid_nick[from] = nil;
374
			end
375
		elseif not type then -- available
376
			if current_nick then
377
				--if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence
378
					if current_nick == to then -- simple presence
379
						log("debug", "%s broadcasted presence", current_nick);
380
						self._occupants[current_nick].sessions[from] = pr;
381
						self:broadcast_presence(pr, from);
382
					else -- change nick
383
						local occupant = self._occupants[current_nick];
384
						local is_multisession = next(occupant.sessions, next(occupant.sessions));
385
						if self._occupants[to] or is_multisession then
386
							log("debug", "%s couldn't change nick", current_nick);
387
							local reply = st.error_reply(stanza, "cancel", "conflict"):up();
388
							reply.tags[1].attr.code = "409";
389
							origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
390
						else
391
							local data = self._occupants[current_nick];
392
							local to_nick = select(3, jid_split(to));
393
							if to_nick then
394
								log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to);
395
								local p = st.presence({type='unavailable', from=current_nick});
396
								self:broadcast_presence(p, from, '303', to_nick);
397
								self._occupants[current_nick] = nil;
398
								self._occupants[to] = data;
399
								self._jid_nick[from] = to;
400
								pr.attr.from = to;
401
								self._occupants[to].sessions[from] = pr;
402
								self:broadcast_presence(pr, from);
403
							else
404
								--TODO malformed-jid
405
							end
406
						end
407
					end
408
				--else -- possible rejoin
409
				--	log("debug", "%s had connection replaced", current_nick);
410
				--	self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to})
411
				--		:tag('status'):text('Replaced by new connection'):up()); -- send unavailable
412
				--	self:handle_to_occupant(origin, stanza); -- resend available
413
				--end
414
			else -- enter room
415
				local new_nick = to;
416
				local is_merge;
417
				if self._occupants[to] then
418
					if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then
419
						new_nick = nil;
420
					end
421
					is_merge = true;
422
				end
423
				local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
424
				password = password and password:get_child("password", "http://jabber.org/protocol/muc");
425
				password = password and password[1] ~= "" and password[1];
426
				if self:get_password() and self:get_password() ~= password then
427
					log("debug", "%s couldn't join due to invalid password: %s", from, to);
428
					local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
429
					reply.tags[1].attr.code = "401";
430
					origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
431
				elseif not new_nick then
432
					log("debug", "%s couldn't join due to nick conflict: %s", from, to);
433
					local reply = st.error_reply(stanza, "cancel", "conflict"):up();
434
					reply.tags[1].attr.code = "409";
435
					origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
436
				else
437
					log("debug", "%s joining as %s", from, to);
438
					if not next(self._affiliations) then -- new room, no owners
439
						self._affiliations[jid_bare(from)] = "owner";
440
					end
441
					local affiliation = self:get_affiliation(from);
442
					local role = self:get_default_role(affiliation)
443
					if role then -- new occupant
444
						if not is_merge then
445
							self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}};
446
						else
447
							self._occupants[to].sessions[from] = get_filtered_presence(stanza);
448
						end
449
						self._jid_nick[from] = to;
450
						self:send_occupant_list(from);
451
						pr.attr.from = to;
452
						pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
453
							:tag("item", {affiliation=affiliation or "none", role=role or "none"}):up();
454
						if not is_merge then
455
							self:broadcast_except_nick(pr, to);
456
						end
457
						pr:tag("status", {code='110'});
458
						if self._data.whois == 'anyone' then
459
							pr:tag("status", {code='100'}):up();
460
						end
461
						pr.attr.to = from;
462
						self:_route_stanza(pr);
463
						self:send_history(from, stanza);
464
					elseif not affiliation then -- registration required for entering members-only room
465
						local reply = st.error_reply(stanza, "auth", "registration-required"):up();
466
						reply.tags[1].attr.code = "407";
467
						origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
468
					else -- banned
469
						local reply = st.error_reply(stanza, "auth", "forbidden"):up();
470
						reply.tags[1].attr.code = "403";
471
						origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
472
					end
473
				end
474
			end
475
		elseif type ~= 'result' then -- bad type
476
			if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
477
				origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
478
			end
479
		end
480
	elseif not current_nick then -- not in room
481
		if type == "error" or type == "result" then
482
			local id = stanza.name == "iq" and stanza.attr.id and base64.decode(stanza.attr.id);
483
			local _nick, _id, _hash = (id or ""):match("^(.+)%z(.*)%z(.+)$");
484
			local occupant = self._occupants[stanza.attr.to];
485
			if occupant and _nick and self._jid_nick[_nick] and _id and _hash then
486
				local id, _to = stanza.attr.id;
487
				for jid in pairs(occupant.sessions) do
488
					if md5(jid) == _hash then
489
						_to = jid;
490
						break;
491
					end
492
				end
493
				if _to then
494
					stanza.attr.to, stanza.attr.from, stanza.attr.id = _to, self._jid_nick[_nick], _id;
495
					self:_route_stanza(stanza);
496
					stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id;
497
				end
498
			end
499
		else
500
			origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
501
		end
502
	elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM
503
		origin.send(st.error_reply(stanza, "modify", "bad-request"));
504
	elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
505
		log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
506
		self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
507
	else -- private stanza
508
		local o_data = self._occupants[to];
509
		if o_data then
510
			log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid);
511
			local jid = o_data.jid;
512
			local bare = jid_bare(jid);
513
			stanza.attr.to, stanza.attr.from = jid, current_nick;
514
			local id = stanza.attr.id;
515
			if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' and bare ~= jid then
516
				stanza.attr.to = bare;
517
				stanza.attr.id = base64.encode(jid.."\0"..id.."\0"..md5(from));
518
			end
519
			self:_route_stanza(stanza);
520
			stanza.attr.to, stanza.attr.from, stanza.attr.id = to, from, id;
521
		elseif type ~= "error" and type ~= "result" then -- recipient not in room
522
			origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
523
		end
524
	end
525
end
526
 
527
function room_mt:send_form(origin, stanza)
528
	origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
529
		:add_child(self:get_form_layout():form())
530
	);
531
end
532
 
533
function room_mt:get_form_layout()
534
	local title = "Configuration for "..self.jid;
535
	return dataform.new({
536
		title = title,
537
		instructions = title,
538
		{
539
			name = 'FORM_TYPE',
540
			type = 'hidden',
541
			value = 'http://jabber.org/protocol/muc#roomconfig'
542
		},
543
		{
544
			name = 'muc#roomconfig_roomname',
545
			type = 'text-single',
546
			label = 'Name',
547
			value = self:get_name() or "",
548
		},
549
		{
550
			name = 'muc#roomconfig_roomdesc',
551
			type = 'text-single',
552
			label = 'Description',
553
			value = self:get_description() or "",
554
		},
555
		{
556
			name = 'muc#roomconfig_persistentroom',
557
			type = 'boolean',
558
			label = 'Make Room Persistent?',
559
			value = self:is_persistent()
560
		},
561
		{
562
			name = 'muc#roomconfig_publicroom',
563
			type = 'boolean',
564
			label = 'Make Room Publicly Searchable?',
565
			value = not self:is_hidden()
566
		},
567
		{
568
			name = 'muc#roomconfig_whois',
569
			type = 'list-single',
570
			label = 'Who May Discover Real JIDs?',
571
			value = {
572
				{ value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' },
573
				{ value = 'anyone',     label = 'Anyone',          default = self._data.whois == 'anyone' }
574
			}
575
		},
576
		{
577
			name = 'muc#roomconfig_roomsecret',
578
			type = 'text-private',
579
			label = 'Password',
580
			value = self:get_password() or "",
581
		},
582
		{
583
			name = 'muc#roomconfig_moderatedroom',
584
			type = 'boolean',
585
			label = 'Make Room Moderated?',
586
			value = self:is_moderated()
587
		},
588
		{
589
			name = 'muc#roomconfig_membersonly',
590
			type = 'boolean',
591
			label = 'Make Room Members-Only?',
592
			value = self:is_members_only()
593
		}
594
	});
595
end
596
 
597
local valid_whois = {
598
	moderators = true,
599
	anyone = true,
600
}
601
 
602
function room_mt:process_form(origin, stanza)
603
	local query = stanza.tags[1];
604
	local form;
605
	for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end
606
	if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end
607
	if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end
608
	if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end
609
 
610
	local fields = self:get_form_layout():data(form);
611
	if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration")); return; end
612
 
613
	local dirty = false
614
 
615
	local name = fields['muc#roomconfig_roomname'];
616
	if name ~= self:get_name() then
617
		self:set_name(name);
618
	end
619
 
620
	local description = fields['muc#roomconfig_roomdesc'];
621
	if description ~= self:get_description() then
622
		self:set_description(description);
623
	end
624
 
625
	local persistent = fields['muc#roomconfig_persistentroom'];
626
	dirty = dirty or (self:is_persistent() ~= persistent)
627
	module:log("debug", "persistent=%s", tostring(persistent));
628
 
629
	local moderated = fields['muc#roomconfig_moderatedroom'];
630
	dirty = dirty or (self:is_moderated() ~= moderated)
631
	module:log("debug", "moderated=%s", tostring(moderated));
632
 
633
	local membersonly = fields['muc#roomconfig_membersonly'];
634
	dirty = dirty or (self:is_members_only() ~= membersonly)
635
	module:log("debug", "membersonly=%s", tostring(membersonly));
636
 
637
	local public = fields['muc#roomconfig_publicroom'];
638
	dirty = dirty or (self:is_hidden() ~= (not public and true or nil))
639
 
640
	local whois = fields['muc#roomconfig_whois'];
641
	if not valid_whois[whois] then
642
	    origin.send(st.error_reply(stanza, 'cancel', 'bad-request', "Invalid value for 'whois'"));
643
	    return;
644
	end
645
	local whois_changed = self._data.whois ~= whois
646
	self._data.whois = whois
647
	module:log('debug', 'whois=%s', whois)
648
 
649
	local password = fields['muc#roomconfig_roomsecret'];
650
	if self:get_password() ~= password then
651
		self:set_password(password);
652
	end
653
	self:set_moderated(moderated);
654
	self:set_members_only(membersonly);
655
	self:set_persistent(persistent);
656
	self:set_hidden(not public);
657
 
658
	if self.save then self:save(true); end
659
	origin.send(st.reply(stanza));
660
 
661
	if dirty or whois_changed then
662
		local msg = st.message({type='groupchat', from=self.jid})
663
			:tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up()
664
 
665
		if dirty then
666
			msg.tags[1]:tag('status', {code = '104'}):up();
667
		end
668
		if whois_changed then
669
			local code = (whois == 'moderators') and "173" or "172";
670
			msg.tags[1]:tag('status', {code = code}):up();
671
		end
672
 
673
		self:broadcast_message(msg, false)
674
	end
675
end
676
 
677
function room_mt:destroy(newjid, reason, password)
678
	local pr = st.presence({type = "unavailable"})
679
		:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
680
			:tag("item", { affiliation='none', role='none' }):up()
681
			:tag("destroy", {jid=newjid})
682
	if reason then pr:tag("reason"):text(reason):up(); end
683
	if password then pr:tag("password"):text(password):up(); end
684
	for nick, occupant in pairs(self._occupants) do
685
		pr.attr.from = nick;
686
		for jid in pairs(occupant.sessions) do
687
			pr.attr.to = jid;
688
			self:_route_stanza(pr);
689
			self._jid_nick[jid] = nil;
690
		end
691
		self._occupants[nick] = nil;
692
	end
693
	self:set_persistent(false);
694
end
695
 
696
function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
697
	local type = stanza.attr.type;
698
	local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns;
699
	if stanza.name == "iq" then
700
		if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" then
701
			origin.send(self:get_disco_info(stanza));
702
		elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" then
703
			origin.send(self:get_disco_items(stanza));
704
		elseif xmlns == "http://jabber.org/protocol/muc#admin" then
705
			local actor = stanza.attr.from;
706
			local affiliation = self:get_affiliation(actor);
707
			local current_nick = self._jid_nick[actor];
708
			local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation);
709
			local item = stanza.tags[1].tags[1];
710
			if item and item.name == "item" then
711
				if type == "set" then
712
					local callback = function() origin.send(st.reply(stanza)); end
713
					if item.attr.jid then -- Validate provided JID
714
						item.attr.jid = jid_prep(item.attr.jid);
715
						if not item.attr.jid then
716
							origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
717
							return;
718
						end
719
					end
720
					if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
721
						local occupant = self._occupants[self.jid.."/"..item.attr.nick];
722
						if occupant then item.attr.jid = occupant.jid; end
723
					elseif not item.attr.nick and item.attr.jid then
724
						local nick = self._jid_nick[item.attr.jid];
725
						if nick then item.attr.nick = select(3, jid_split(nick)); end
726
					end
727
					local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1];
728
					if item.attr.affiliation and item.attr.jid and not item.attr.role then
729
						local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason);
730
						if not success then origin.send(st.error_reply(stanza, errtype, err)); end
731
					elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
732
						local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason);
733
						if not success then origin.send(st.error_reply(stanza, errtype, err)); end
734
					else
735
						origin.send(st.error_reply(stanza, "cancel", "bad-request"));
736
					end
737
				elseif type == "get" then
738
					local _aff = item.attr.affiliation;
739
					local _rol = item.attr.role;
740
					if _aff and not _rol then
741
						if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
742
							local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
743
							for jid, affiliation in pairs(self._affiliations) do
744
								if affiliation == _aff then
745
									reply:tag("item", {affiliation = _aff, jid = jid}):up();
746
								end
747
							end
748
							origin.send(reply);
749
						else
750
							origin.send(st.error_reply(stanza, "auth", "forbidden"));
751
						end
752
					elseif _rol and not _aff then
753
						if role == "moderator" then
754
							-- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
755
							if _rol == "none" then _rol = nil; end
756
							local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
757
							for occupant_jid, occupant in pairs(self._occupants) do
758
								if occupant.role == _rol then
759
									reply:tag("item", {
760
										nick = select(3, jid_split(occupant_jid)),
761
										role = _rol or "none",
762
										affiliation = occupant.affiliation or "none",
763
										jid = occupant.jid
764
										}):up();
765
								end
766
							end
767
							origin.send(reply);
768
						else
769
							origin.send(st.error_reply(stanza, "auth", "forbidden"));
770
						end
771
					else
772
						origin.send(st.error_reply(stanza, "cancel", "bad-request"));
773
					end
774
				end
775
			elseif type == "set" or type == "get" then
776
				origin.send(st.error_reply(stanza, "cancel", "bad-request"));
777
			end
778
		elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
779
			if self:get_affiliation(stanza.attr.from) ~= "owner" then
780
				origin.send(st.error_reply(stanza, "auth", "forbidden"));
781
			elseif stanza.attr.type == "get" then
782
				self:send_form(origin, stanza);
783
			elseif stanza.attr.type == "set" then
784
				local child = stanza.tags[1].tags[1];
785
				if not child then
786
					origin.send(st.error_reply(stanza, "auth", "bad-request"));
787
				elseif child.name == "destroy" then
788
					local newjid = child.attr.jid;
789
					local reason, password;
790
					for _,tag in ipairs(child.tags) do
791
						if tag.name == "reason" then
792
							reason = #tag.tags == 0 and tag[1];
793
						elseif tag.name == "password" then
794
							password = #tag.tags == 0 and tag[1];
795
						end
796
					end
797
					self:destroy(newjid, reason, password);
798
					origin.send(st.reply(stanza));
799
				else
800
					self:process_form(origin, stanza);
801
				end
802
			end
803
		elseif type == "set" or type == "get" then
804
			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
805
		end
806
	elseif stanza.name == "message" and type == "groupchat" then
807
		local from, to = stanza.attr.from, stanza.attr.to;
808
		local room = jid_bare(to);
809
		local current_nick = self._jid_nick[from];
810
		local occupant = self._occupants[current_nick];
811
		if not occupant then -- not in room
812
			origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
813
		elseif occupant.role == "visitor" then
814
			origin.send(st.error_reply(stanza, "cancel", "forbidden"));
815
		else
816
			local from = stanza.attr.from;
817
			stanza.attr.from = current_nick;
818
			local subject = getText(stanza, {"subject"});
819
			if subject then
820
				if occupant.role == "moderator" then
821
					self:set_subject(current_nick, subject); -- TODO use broadcast_message_stanza
822
				else
823
					stanza.attr.from = from;
824
					origin.send(st.error_reply(stanza, "cancel", "forbidden"));
825
				end
826
			else
827
				self:broadcast_message(stanza, true);
828
			end
829
			stanza.attr.from = from;
830
		end
831
	elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
832
		local current_nick = self._jid_nick[stanza.attr.from];
833
		log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
834
		self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
835
	elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
836
		local to = stanza.attr.to;
837
		local current_nick = self._jid_nick[stanza.attr.from];
838
		if current_nick then
839
			stanza.attr.to = current_nick;
840
			self:handle_to_occupant(origin, stanza);
841
			stanza.attr.to = to;
842
		elseif type ~= "error" and type ~= "result" then
843
			origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
844
		end
845
	elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and self._jid_nick[stanza.attr.from]
846
		and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then
847
		local x = stanza.tags[1];
848
		local payload = (#x.tags == 1 and x.tags[1]);
849
		if payload and payload.name == "invite" and payload.attr.to then
850
			local _from, _to = stanza.attr.from, stanza.attr.to;
851
			local _invitee = jid_prep(payload.attr.to);
852
			if _invitee then
853
				local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1];
854
				local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id})
855
					:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
856
						:tag('invite', {from=_from})
857
							:tag('reason'):text(_reason or ""):up()
858
						:up();
859
						if self:get_password() then
860
							invite:tag("password"):text(self:get_password()):up();
861
						end
862
					invite:up()
863
					:tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this
864
						:text(_reason or "")
865
					:up()
866
					:tag('body') -- Add a plain message for clients which don't support invites
867
						:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
868
					:up();
869
				if self:is_members_only() and not self:get_affiliation(_invitee) then
870
					log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
871
					self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
872
				end
873
				self:_route_stanza(invite);
874
			else
875
				origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
876
			end
877
		else
878
			origin.send(st.error_reply(stanza, "cancel", "bad-request"));
879
		end
880
	else
881
		if type == "error" or type == "result" then return; end
882
		origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
883
	end
884
end
885
 
886
function room_mt:handle_stanza(origin, stanza)
887
	local to_node, to_host, to_resource = jid_split(stanza.attr.to);
888
	if to_resource then
889
		self:handle_to_occupant(origin, stanza);
890
	else
891
		self:handle_to_room(origin, stanza);
892
	end
893
end
894
 
895
function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end
896
 
897
function room_mt:get_affiliation(jid)
898
	local node, host, resource = jid_split(jid);
899
	local bare = node and node.."@"..host or host;
900
	local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
901
	if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
902
	return result;
903
end
904
function room_mt:set_affiliation(actor, jid, affiliation, callback, reason)
905
	jid = jid_bare(jid);
906
	if affiliation == "none" then affiliation = nil; end
907
	if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then
908
		return nil, "modify", "not-acceptable";
909
	end
910
	if self:get_affiliation(actor) ~= "owner" then return nil, "cancel", "not-allowed"; end
911
	if jid_bare(actor) == jid then return nil, "cancel", "not-allowed"; end
912
	self._affiliations[jid] = affiliation;
913
	local role = self:get_default_role(affiliation);
914
	local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
915
			:tag("item", {affiliation=affiliation or "none", role=role or "none"})
916
				:tag("reason"):text(reason or ""):up()
917
			:up();
918
	local presence_type = nil;
919
	if not role then -- getting kicked
920
		presence_type = "unavailable";
921
		if affiliation == "outcast" then
922
			x:tag("status", {code="301"}):up(); -- banned
923
		else
924
			x:tag("status", {code="321"}):up(); -- affiliation change
925
		end
926
	end
927
	local modified_nicks = {};
928
	for nick, occupant in pairs(self._occupants) do
929
		if jid_bare(occupant.jid) == jid then
930
			if not role then -- getting kicked
931
				self._occupants[nick] = nil;
932
			else
933
				occupant.affiliation, occupant.role = affiliation, role;
934
			end
935
			for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick
936
				if not role then self._jid_nick[jid] = nil; end
937
				local p = st.clone(pres);
938
				p.attr.from = nick;
939
				p.attr.type = presence_type;
940
				p.attr.to = jid;
941
				p:add_child(x);
942
				self:_route_stanza(p);
943
				if occupant.jid == jid then
944
					modified_nicks[nick] = p;
945
				end
946
			end
947
		end
948
	end
949
	if self.save then self:save(); end
950
	if callback then callback(); end
951
	for nick,p in pairs(modified_nicks) do
952
		p.attr.from = nick;
953
		self:broadcast_except_nick(p, nick);
954
	end
955
	return true;
956
end
957
 
958
function room_mt:get_role(nick)
959
	local session = self._occupants[nick];
960
	return session and session.role or nil;
961
end
962
function room_mt:can_set_role(actor_jid, occupant_jid, role)
963
	local actor = self._occupants[self._jid_nick[actor_jid]];
964
	local occupant = self._occupants[occupant_jid];
965
	
966
	if not occupant or not actor then return nil, "modify", "not-acceptable"; end
967
 
968
	if actor.role == "moderator" then
969
		if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then
970
			if actor.affiliation == "owner" or actor.affiliation == "admin" then
971
				return true;
972
			elseif occupant.role ~= "moderator" and role ~= "moderator" then
973
				return true;
974
			end
975
		end
976
	end
977
	return nil, "cancel", "not-allowed";
978
end
979
function room_mt:set_role(actor, occupant_jid, role, callback, reason)
980
	if role == "none" then role = nil; end
981
	if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end
982
	local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role);
983
	if not allowed then return allowed, err_type, err_condition; end
984
	local occupant = self._occupants[occupant_jid];
985
	local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
986
			:tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"})
987
				:tag("reason"):text(reason or ""):up()
988
			:up();
989
	local presence_type = nil;
990
	if not role then -- kick
991
		presence_type = "unavailable";
992
		self._occupants[occupant_jid] = nil;
993
		for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick
994
			self._jid_nick[jid] = nil;
995
		end
996
		x:tag("status", {code = "307"}):up();
997
	else
998
		occupant.role = role;
999
	end
1000
	local bp;
1001
	for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick
1002
		local p = st.clone(pres);
1003
		p.attr.from = occupant_jid;
1004
		p.attr.type = presence_type;
1005
		p.attr.to = jid;
1006
		p:add_child(x);
1007
		self:_route_stanza(p);
1008
		if occupant.jid == jid then
1009
			bp = p;
1010
		end
1011
	end
1012
	if callback then callback(); end
1013
	if bp then
1014
		self:broadcast_except_nick(bp, occupant_jid);
1015
	end
1016
	return true;
1017
end
1018
 
1019
function room_mt:_route_stanza(stanza)
1020
	local muc_child;
1021
	local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]];
1022
	local from_occupant = self._occupants[stanza.attr.from];
1023
	if stanza.name == "presence" then
1024
		if to_occupant and from_occupant then
1025
			if self._data.whois == 'anyone' then
1026
			    muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
1027
			else
1028
				if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then
1029
					muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
1030
				end
1031
			end
1032
		end
1033
	end
1034
	if muc_child then
1035
		for _, item in pairs(muc_child.tags) do
1036
			if item.name == "item" then
1037
				if from_occupant == to_occupant then
1038
					item.attr.jid = stanza.attr.to;
1039
				else
1040
					item.attr.jid = from_occupant.jid;
1041
				end
1042
			end
1043
		end
1044
	end
1045
	self:route_stanza(stanza);
1046
	if muc_child then
1047
		for _, item in pairs(muc_child.tags) do
1048
			if item.name == "item" then
1049
				item.attr.jid = nil;
1050
			end
1051
		end
1052
	end
1053
end
1054
 
1055
local _M = {}; -- module "muc"
1056
 
1057
function _M.new_room(jid, config)
1058
	return setmetatable({
1059
		jid = jid;
1060
		_jid_nick = {};
1061
		_occupants = {};
1062
		_data = {
1063
		    whois = 'moderators';
1064
		    history_length = (config and config.history_length);
1065
		};
1066
		_affiliations = {};
1067
	}, room_mt);
1068
end
1069
 
1070
return _M;