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