comparison plugins/muc/muc.lib.lua @ 6609:d2faaaca695d

Merge 0.10->trunk
author Matthew Wild <mwild1@gmail.com>
date Fri, 27 Mar 2015 22:24:57 +0000
parents 3bbd59c03aeb
children 4caef6d53304
comparison
equal deleted inserted replaced
6608:b6e558febb7a 6609:d2faaaca695d
1 -- Prosody IM 1 -- Prosody IM
2 -- Copyright (C) 2008-2010 Matthew Wild 2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain 3 -- Copyright (C) 2008-2010 Waqas Hussain
4 -- Copyright (C) 2014 Daurnimator
4 -- 5 --
5 -- This project is MIT/X11 licensed. Please see the 6 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information. 7 -- COPYING file in the source package for more information.
7 -- 8 --
8 9
9 local select = select; 10 local select = select;
10 local pairs, ipairs = pairs, ipairs; 11 local pairs, ipairs = pairs, ipairs;
11 12 local next = next;
12 local datetime = require "util.datetime"; 13 local setmetatable = setmetatable;
13 14
14 local dataform = require "util.dataforms"; 15 local dataform = require "util.dataforms";
15 16 local iterators = require "util.iterators";
16 local jid_split = require "util.jid".split; 17 local jid_split = require "util.jid".split;
17 local jid_bare = require "util.jid".bare; 18 local jid_bare = require "util.jid".bare;
18 local jid_prep = require "util.jid".prep; 19 local jid_prep = require "util.jid".prep;
20 local jid_join = require "util.jid".join;
19 local st = require "util.stanza"; 21 local st = require "util.stanza";
20 local log = require "util.logger".init("mod_muc"); 22 local log = require "util.logger".init("mod_muc");
21 local t_insert, t_remove = table.insert, table.remove;
22 local setmetatable = setmetatable;
23 local base64 = require "util.encodings".base64; 23 local base64 = require "util.encodings".base64;
24 local md5 = require "util.hashes".md5; 24 local md5 = require "util.hashes".md5;
25 25
26 local muc_domain = nil; --module:get_host(); 26 local occupant_lib = module:require "muc/occupant"
27 local default_history_length, max_history_length = 20, math.huge; 27 local muc_util = module:require "muc/util";
28 28 local is_kickable_error = muc_util.is_kickable_error;
29 ------------ 29 local valid_roles, valid_affiliations = muc_util.valid_roles, muc_util.valid_affiliations;
30 local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
31 local function presence_filter(tag)
32 if presence_filters[tag.attr.xmlns] then
33 return nil;
34 end
35 return tag;
36 end
37
38 local function get_filtered_presence(stanza)
39 return st.clone(stanza):maptags(presence_filter);
40 end
41 local kickable_error_conditions = {
42 ["gone"] = true;
43 ["internal-server-error"] = true;
44 ["item-not-found"] = true;
45 ["jid-malformed"] = true;
46 ["recipient-unavailable"] = true;
47 ["redirect"] = true;
48 ["remote-server-not-found"] = true;
49 ["remote-server-timeout"] = true;
50 ["service-unavailable"] = true;
51 ["malformed error"] = true;
52 };
53
54 local function get_error_condition(stanza)
55 local _, condition = stanza:get_error();
56 return condition or "malformed error";
57 end
58
59 local function is_kickable_error(stanza)
60 local cond = get_error_condition(stanza);
61 return kickable_error_conditions[cond] and cond;
62 end
63 -----------
64 30
65 local room_mt = {}; 31 local room_mt = {};
66 room_mt.__index = room_mt; 32 room_mt.__index = room_mt;
67 33
68 function room_mt:__tostring() 34 function room_mt:__tostring()
69 return "MUC room ("..self.jid..")"; 35 return "MUC room ("..self.jid..")";
70 end 36 end
71 37
38 function room_mt:get_occupant_jid(real_jid)
39 return self._jid_nick[real_jid]
40 end
41
72 function room_mt:get_default_role(affiliation) 42 function room_mt:get_default_role(affiliation)
73 if affiliation == "owner" or affiliation == "admin" then 43 local role = module:fire_event("muc-get-default-role", {
44 room = self;
45 affiliation = affiliation;
46 affiliation_rank = valid_affiliations[affiliation or "none"];
47 });
48 return role, valid_roles[role or "none"];
49 end
50 module:hook("muc-get-default-role", function(event)
51 if event.affiliation_rank >= valid_affiliations.admin then
74 return "moderator"; 52 return "moderator";
75 elseif affiliation == "member" then 53 elseif event.affiliation_rank >= valid_affiliations.none then
76 return "participant"; 54 return "participant";
77 elseif not affiliation then 55 end
78 if not self:get_members_only() then 56 end);
79 return self:get_moderated() and "visitor" or "participant"; 57
80 end 58 --- Occupant functions
81 end 59 function room_mt:new_occupant(bare_real_jid, nick)
82 end 60 local occupant = occupant_lib.new(bare_real_jid, nick);
83 61 local affiliation = self:get_affiliation(bare_real_jid);
84 function room_mt:broadcast_presence(stanza, sid, code, nick) 62 occupant.role = self:get_default_role(affiliation);
85 stanza = get_filtered_presence(stanza); 63 return occupant;
86 local occupant = self._occupants[stanza.attr.from]; 64 end
87 stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) 65
88 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none", nick=nick}):up(); 66 function room_mt:get_occupant_by_nick(nick)
89 if code then 67 local occupant = self._occupants[nick];
90 stanza:tag("status", {code=code}):up(); 68 if occupant == nil then return nil end
91 end 69 return occupant_lib.copy(occupant);
92 self:broadcast_except_nick(stanza, stanza.attr.from); 70 end
93 local me = self._occupants[stanza.attr.from]; 71
94 if me then 72 do
95 stanza:tag("status", {code='110'}):up(); 73 local function next_copied_occupant(occupants, occupant_jid)
96 stanza.attr.to = sid; 74 local next_occupant_jid, raw_occupant = next(occupants, occupant_jid);
97 self:_route_stanza(stanza); 75 if next_occupant_jid == nil then return nil end
98 end 76 return next_occupant_jid, occupant_lib.copy(raw_occupant);
99 end 77 end
100 function room_mt:broadcast_message(stanza, historic) 78 function room_mt:each_occupant(read_only)
79 return next_copied_occupant, self._occupants, nil;
80 end
81 end
82
83 function room_mt:has_occupant()
84 return next(self._occupants, nil) ~= nil
85 end
86
87 function room_mt:get_occupant_by_real_jid(real_jid)
88 local occupant_jid = self:get_occupant_jid(real_jid);
89 if occupant_jid == nil then return nil end
90 return self:get_occupant_by_nick(occupant_jid);
91 end
92
93 function room_mt:save_occupant(occupant)
94 occupant = occupant_lib.copy(occupant); -- So that occupant can be modified more
95 local id = occupant.nick
96
97 -- Need to maintain _jid_nick secondary index
98 local old_occupant = self._occupants[id];
99 if old_occupant then
100 for real_jid in old_occupant:each_session() do
101 self._jid_nick[real_jid] = nil;
102 end
103 end
104
105 local has_live_session = false
106 if occupant.role ~= nil then
107 for real_jid, presence in occupant:each_session() do
108 if presence.attr.type == nil then
109 has_live_session = true
110 self._jid_nick[real_jid] = occupant.nick;
111 end
112 end
113 if not has_live_session then
114 -- Has no live sessions left; they have left the room.
115 occupant.role = nil
116 end
117 end
118 if not has_live_session then
119 occupant = nil
120 end
121 self._occupants[id] = occupant
122 end
123
124 function room_mt:route_to_occupant(occupant, stanza)
101 local to = stanza.attr.to; 125 local to = stanza.attr.to;
102 for occupant, o_data in pairs(self._occupants) do 126 for jid, pr in occupant:each_session() do
103 for jid in pairs(o_data.sessions) do 127 stanza.attr.to = jid;
104 stanza.attr.to = jid; 128 self:route_stanza(stanza);
105 self:_route_stanza(stanza);
106 end
107 end 129 end
108 stanza.attr.to = to; 130 stanza.attr.to = to;
109 if historic then -- add to history 131 end
110 return self:save_to_history(stanza) 132
111 end 133 -- actor is the attribute table
112 end 134 local function add_item(x, affiliation, role, jid, nick, actor_nick, actor_jid, reason)
113 function room_mt:save_to_history(stanza) 135 x:tag("item", {affiliation = affiliation; role = role; jid = jid; nick = nick;})
114 local history = self._data['history']; 136 if actor_nick or actor_jid then
115 if not history then history = {}; self._data['history'] = history; end 137 x:tag("actor", {nick = actor_nick; jid = actor_jid;}):up()
116 stanza = st.clone(stanza); 138 end
117 stanza.attr.to = ""; 139 if reason then
118 local stamp = datetime.datetime(); 140 x:tag("reason"):text(reason):up()
119 stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = stamp}):up(); -- XEP-0203 141 end
120 stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated) 142 x:up();
121 local entry = { stanza = stanza, stamp = stamp }; 143 return x
122 t_insert(history, entry); 144 end
123 while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end 145
124 end 146 -- actor is (real) jid
125 function room_mt:broadcast_except_nick(stanza, nick) 147 function room_mt:build_item_list(occupant, x, is_anonymous, nick, actor_nick, actor_jid, reason)
126 for rnick, occupant in pairs(self._occupants) do 148 local affiliation = self:get_affiliation(occupant.bare_jid) or "none";
127 if rnick ~= nick then 149 local role = occupant.role or "none";
128 for jid in pairs(occupant.sessions) do 150 if is_anonymous then
129 stanza.attr.to = jid; 151 add_item(x, affiliation, role, nil, nick, actor_nick, actor_jid, reason);
130 self:_route_stanza(stanza); 152 else
131 end 153 for real_jid, session in occupant:each_session() do
132 end 154 add_item(x, affiliation, role, real_jid, nick, actor_nick, actor_jid, reason);
133 end 155 end
134 end 156 end
135 157 return x
136 function room_mt:send_occupant_list(to) 158 end
137 local current_nick = self._jid_nick[to]; 159
138 for occupant, o_data in pairs(self._occupants) do 160 function room_mt:broadcast_message(stanza)
139 if occupant ~= current_nick then 161 if module:fire_event("muc-broadcast-message", {room = self, stanza = stanza}) then
140 local pres = get_filtered_presence(o_data.sessions[o_data.jid]); 162 return true;
141 pres.attr.to, pres.attr.from = to, occupant; 163 end
142 pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'}) 164 self:broadcast(stanza);
143 :tag("item", {affiliation=o_data.affiliation or "none", role=o_data.role or "none"}):up(); 165 return true;
144 self:_route_stanza(pres); 166 end
145 end 167
146 end 168 -- Broadcast a stanza to all occupants in the room.
147 end 169 -- optionally checks conditional called with (nick, occupant)
148 function room_mt:send_history(to, stanza) 170 function room_mt:broadcast(stanza, cond_func)
149 local history = self._data['history']; -- send discussion history 171 for nick, occupant in self:each_occupant() do
150 if history then 172 if cond_func == nil or cond_func(nick, occupant) then
151 local x_tag = stanza and stanza:get_child("x", "http://jabber.org/protocol/muc"); 173 self:route_to_occupant(occupant, stanza)
152 local history_tag = x_tag and x_tag:get_child("history", "http://jabber.org/protocol/muc"); 174 end
153 175 end
154 local maxchars = history_tag and tonumber(history_tag.attr.maxchars); 176 end
155 if maxchars then maxchars = math.floor(maxchars); end 177
156 178 local function can_see_real_jids(whois, occupant)
157 local maxstanzas = math.floor(history_tag and tonumber(history_tag.attr.maxstanzas) or #history); 179 if whois == "anyone" then
158 if not history_tag then maxstanzas = 20; end 180 return true;
159 181 elseif whois == "moderators" then
160 local seconds = history_tag and tonumber(history_tag.attr.seconds); 182 return valid_roles[occupant.role or "none"] >= valid_roles.moderator;
161 if seconds then seconds = datetime.datetime(os.time() - math.floor(seconds)); end 183 end
162 184 end
163 local since = history_tag and history_tag.attr.since; 185
164 if since then since = datetime.parse(since); since = since and datetime.datetime(since); end 186 -- Broadcasts an occupant's presence to the whole room
165 if seconds and (not since or since < seconds) then since = seconds; end 187 -- Takes the x element that goes into the stanzas
166 188 function room_mt:publicise_occupant_status(occupant, base_x, nick, actor, reason)
167 local n = 0; 189 -- Build real jid and (optionally) occupant jid template presences
168 local charcount = 0; 190 local base_presence;
169 191 if occupant.role ~= nil then
170 for i=#history,1,-1 do 192 -- Try to use main jid's presence
171 local entry = history[i]; 193 local pr = occupant:get_presence();
172 if maxchars then 194 if pr ~= nil then
173 if not entry.chars then 195 base_presence = st.clone(pr);
174 entry.stanza.attr.to = ""; 196 end
175 entry.chars = #tostring(entry.stanza); 197 end
176 end 198 base_presence = base_presence or st.presence {from = occupant.nick; type = "unavailable";};
177 charcount = charcount + entry.chars + #to; 199
178 if charcount > maxchars then break; end 200 -- Fire event (before full_p and anon_p are created)
179 end 201 local event = {
180 if since and since > entry.stamp then break; end 202 room = self; stanza = base_presence; x = base_x;
181 if n + 1 > maxstanzas then break; end 203 occupant = occupant; nick = nick; actor = actor;
182 n = n + 1; 204 reason = reason;
183 end 205 }
184 for i=#history-n+1,#history do 206 module:fire_event("muc-broadcast-presence", event);
185 local msg = history[i].stanza; 207
186 msg.attr.to = to; 208 -- Allow muc-broadcast-presence listeners to change things
187 self:_route_stanza(msg); 209 nick = event.nick;
188 end 210 actor = event.actor;
189 end 211 reason = event.reason;
190 end 212
191 function room_mt:send_subject(to) 213 local whois = self:get_whois();
192 if self._data['subject'] then 214
193 self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject'])); 215 local actor_nick;
216 if actor then
217 actor_nick = select(3, jid_split(self:get_occupant_jid(actor)));
218 end
219
220 local full_p, full_x;
221 local function get_full_p()
222 if full_p == nil then
223 full_x = st.clone(base_x);
224 self:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason);
225 full_p = st.clone(base_presence):add_child(full_x);
226 end
227 return full_p, full_x;
228 end
229
230 local anon_p, anon_x;
231 local function get_anon_p()
232 if anon_p == nil then
233 anon_x = st.clone(base_x);
234 self:build_item_list(occupant, anon_x, true, nick, actor_nick, nil, reason);
235 anon_p = st.clone(base_presence):add_child(anon_x);
236 end
237 return anon_p, anon_x;
238 end
239
240 local self_p, self_x;
241 if can_see_real_jids(whois, occupant) then
242 self_p, self_x = get_full_p();
243 else
244 -- Can always see your own full jids
245 -- But not allowed to see actor's
246 self_x = st.clone(base_x);
247 self:build_item_list(occupant, self_x, false, nick, actor_nick, nil, reason);
248 self_p = st.clone(base_presence):add_child(self_x);
249 end
250
251 -- General populance
252 for nick, n_occupant in self:each_occupant() do
253 if nick ~= occupant.nick then
254 local pr;
255 if can_see_real_jids(whois, n_occupant) then
256 pr = get_full_p();
257 elseif occupant.bare_jid == n_occupant.bare_jid then
258 pr = self_p;
259 else
260 pr = get_anon_p();
261 end
262 self:route_to_occupant(n_occupant, pr);
263 end
264 end
265
266 -- Presences for occupant itself
267 self_x:tag("status", {code = "110";}):up();
268 if occupant.role == nil then
269 -- They get an unavailable
270 self:route_to_occupant(occupant, self_p);
271 else
272 -- use their own presences as templates
273 for full_jid, pr in occupant:each_session() do
274 pr = st.clone(pr);
275 pr.attr.to = full_jid;
276 pr:add_child(self_x);
277 self:route_stanza(pr);
278 end
279 end
280 end
281
282 function room_mt:send_occupant_list(to, filter)
283 local to_bare = jid_bare(to);
284 local is_anonymous = false;
285 local whois = self:get_whois();
286 if whois ~= "anyone" then
287 local affiliation = self:get_affiliation(to);
288 if affiliation ~= "admin" and affiliation ~= "owner" then
289 local occupant = self:get_occupant_by_real_jid(to);
290 if not (occupant and can_see_real_jids(whois, occupant)) then
291 is_anonymous = true;
292 end
293 end
294 end
295 for occupant_jid, occupant in self:each_occupant() do
296 if filter == nil or filter(occupant_jid, occupant) then
297 local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
298 self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids
299 local pres = st.clone(occupant:get_presence());
300 pres.attr.to = to;
301 pres:add_child(x);
302 self:route_stanza(pres);
303 end
194 end 304 end
195 end 305 end
196 306
197 function room_mt:get_disco_info(stanza) 307 function room_mt:get_disco_info(stanza)
198 local count = 0; for _ in pairs(self._occupants) do count = count + 1; end 308 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#info");
199 return st.reply(stanza):query("http://jabber.org/protocol/disco#info") 309 local form = dataform.new {
200 :tag("identity", {category="conference", type="text", name=self:get_name()}):up() 310 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" };
201 :tag("feature", {var="http://jabber.org/protocol/muc"}):up() 311 };
202 :tag("feature", {var=self:get_password() and "muc_passwordprotected" or "muc_unsecured"}):up() 312 module:fire_event("muc-disco#info", {room = self; reply = reply; form = form;});
203 :tag("feature", {var=self:get_moderated() and "muc_moderated" or "muc_unmoderated"}):up() 313 reply:add_child(form:form(nil, "result"));
204 :tag("feature", {var=self:get_members_only() and "muc_membersonly" or "muc_open"}):up() 314 return reply;
205 :tag("feature", {var=self:get_persistent() and "muc_persistent" or "muc_temporary"}):up() 315 end
206 :tag("feature", {var=self:get_hidden() and "muc_hidden" or "muc_public"}):up() 316 module:hook("muc-disco#info", function(event)
207 :tag("feature", {var=self._data.whois ~= "anyone" and "muc_semianonymous" or "muc_nonanonymous"}):up() 317 event.reply:tag("feature", {var = "http://jabber.org/protocol/muc"}):up();
208 :add_child(dataform.new({ 318 end);
209 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#roominfo" }, 319 module:hook("muc-disco#info", function(event)
210 { name = "muc#roominfo_description", label = "Description", value = "" }, 320 local count = iterators.count(event.room:each_occupant());
211 { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) } 321 table.insert(event.form, { name = "muc#roominfo_occupants", label = "Number of occupants", value = tostring(count) });
212 }):form({["muc#roominfo_description"] = self:get_description()}, 'result')) 322 end);
213 ; 323
214 end
215 function room_mt:get_disco_items(stanza) 324 function room_mt:get_disco_items(stanza)
216 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items"); 325 local reply = st.reply(stanza):query("http://jabber.org/protocol/disco#items");
217 for room_jid in pairs(self._occupants) do 326 for room_jid in self:each_occupant() do
218 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up(); 327 reply:tag("item", {jid = room_jid, name = room_jid:match("/(.*)")}):up();
219 end 328 end
220 return reply; 329 return reply;
221 end 330 end
222 function room_mt:set_subject(current_nick, subject) 331
223 if subject == "" then subject = nil; end 332 function room_mt:handle_kickable(origin, stanza)
224 self._data['subject'] = subject; 333 local real_jid = stanza.attr.from;
225 self._data['subject_from'] = current_nick; 334 local occupant = self:get_occupant_by_real_jid(real_jid);
226 if self.save then self:save(); end 335 if occupant == nil then return nil; end
227 local msg = st.message({type='groupchat', from=current_nick})
228 :tag('subject'):text(subject):up();
229 self:broadcast_message(msg, false);
230 return true;
231 end
232
233 local function build_unavailable_presence_from_error(stanza)
234 local type, condition, text = stanza:get_error(); 336 local type, condition, text = stanza:get_error();
235 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error"); 337 local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
236 if text then 338 if text then
237 error_message = error_message..": "..text; 339 error_message = error_message..": "..text;
238 end 340 end
239 return st.presence({type='unavailable', from=stanza.attr.from, to=stanza.attr.to}) 341 occupant:set_session(real_jid, st.presence({type="unavailable"})
240 :tag('status'):text(error_message); 342 :tag('status'):text(error_message));
241 end 343 self:save_occupant(occupant);
242 344 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";})
243 function room_mt:set_name(name) 345 :tag("status", {code = "307"})
244 if name == "" or type(name) ~= "string" or name == (jid_split(self.jid)) then name = nil; end 346 self:publicise_occupant_status(occupant, x);
245 if self._data.name ~= name then 347 if occupant.jid == real_jid then -- Was last session
246 self._data.name = name; 348 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
247 if self.save then self:save(true); end 349 end
248 end 350 return true;
249 end 351 end
250 function room_mt:get_name() 352
251 return self._data.name or jid_split(self.jid); 353 -- Give the room creator owner affiliation
252 end 354 module:hook("muc-room-pre-create", function(event)
253 function room_mt:set_description(description) 355 event.room:set_affiliation(true, jid_bare(event.stanza.attr.from), "owner");
254 if description == "" or type(description) ~= "string" then description = nil; end 356 end, -1);
255 if self._data.description ~= description then 357
256 self._data.description = description; 358 -- check if user is banned
257 if self.save then self:save(true); end 359 module:hook("muc-occupant-pre-join", function(event)
258 end 360 local room, stanza = event.room, event.stanza;
259 end 361 local affiliation = room:get_affiliation(stanza.attr.from);
260 function room_mt:get_description() 362 if affiliation == "outcast" then
261 return self._data.description; 363 local reply = st.error_reply(stanza, "auth", "forbidden"):up();
262 end 364 reply.tags[1].attr.code = "403";
263 function room_mt:set_password(password) 365 event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
264 if password == "" or type(password) ~= "string" then password = nil; end 366 return true;
265 if self._data.password ~= password then 367 end
266 self._data.password = password; 368 end, -10);
267 if self.save then self:save(true); end 369
268 end 370 function room_mt:handle_presence_to_occupant(origin, stanza)
269 end 371 local type = stanza.attr.type;
270 function room_mt:get_password() 372 if type == "error" then -- error, kick em out!
271 return self._data.password; 373 return self:handle_kickable(origin, stanza)
272 end 374 elseif type == nil or type == "unavailable" then
273 function room_mt:set_moderated(moderated) 375 local real_jid = stanza.attr.from;
274 moderated = moderated and true or nil; 376 local bare_jid = jid_bare(real_jid);
275 if self._data.moderated ~= moderated then 377 local orig_occupant, dest_occupant;
276 self._data.moderated = moderated; 378 local is_new_room = next(self._affiliations) == nil;
277 if self.save then self:save(true); end 379 if is_new_room then
278 end 380 if type == "unavailable" then return true; end -- Unavailable from someone not in the room
279 end 381 if module:fire_event("muc-room-pre-create", {
280 function room_mt:get_moderated() 382 room = self;
281 return self._data.moderated; 383 origin = origin;
282 end 384 stanza = stanza;
283 function room_mt:set_members_only(members_only) 385 }) then return true; end
284 members_only = members_only and true or nil; 386 else
285 if self._data.members_only ~= members_only then 387 orig_occupant = self:get_occupant_by_real_jid(real_jid);
286 self._data.members_only = members_only; 388 if type == "unavailable" and orig_occupant == nil then return true; end -- Unavailable from someone not in the room
287 if self.save then self:save(true); end 389 end
288 end 390 local is_first_dest_session;
289 end 391 if type == "unavailable" then
290 function room_mt:get_members_only() 392 -- dest_occupant = nil
291 return self._data.members_only; 393 elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update
292 end 394 log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid);
293 function room_mt:set_persistent(persistent) 395 dest_occupant = orig_occupant;
294 persistent = persistent and true or nil; 396 else
295 if self._data.persistent ~= persistent then 397 local dest_jid = stanza.attr.to;
296 self._data.persistent = persistent; 398 dest_occupant = self:get_occupant_by_nick(dest_jid);
297 if self.save then self:save(true); end 399 if dest_occupant == nil then
298 end 400 log("debug", "no occupant found for %s; creating new occupant object for %s", dest_jid, real_jid);
299 end 401 is_first_dest_session = true;
300 function room_mt:get_persistent() 402 dest_occupant = self:new_occupant(bare_jid, dest_jid);
301 return self._data.persistent; 403 else
302 end 404 is_first_dest_session = false;
303 function room_mt:set_hidden(hidden) 405 end
304 hidden = hidden and true or nil; 406 end
305 if self._data.hidden ~= hidden then 407 local is_last_orig_session;
306 self._data.hidden = hidden; 408 if orig_occupant ~= nil then
307 if self.save then self:save(true); end 409 -- Is there are least 2 sessions?
308 end 410 local iter, ob, last = orig_occupant:each_session();
309 end 411 is_last_orig_session = iter(ob, iter(ob, last)) == nil;
310 function room_mt:get_hidden() 412 end
311 return self._data.hidden; 413
312 end 414 local event, event_name = {
313 function room_mt:get_public() 415 room = self;
314 return not self:get_hidden(); 416 origin = origin;
315 end 417 stanza = stanza;
316 function room_mt:set_public(public) 418 is_first_session = is_first_dest_session;
317 return self:set_hidden(not public); 419 is_last_session = is_last_orig_session;
318 end 420 };
319 function room_mt:set_changesubject(changesubject) 421 if orig_occupant == nil then
320 changesubject = changesubject and true or nil; 422 event_name = "muc-occupant-pre-join";
321 if self._data.changesubject ~= changesubject then 423 event.is_new_room = is_new_room;
322 self._data.changesubject = changesubject; 424 event.occupant = dest_occupant;
323 if self.save then self:save(true); end 425 elseif dest_occupant == nil then
324 end 426 event_name = "muc-occupant-pre-leave";
325 end 427 event.occupant = orig_occupant;
326 function room_mt:get_changesubject() 428 else
327 return self._data.changesubject; 429 event_name = "muc-occupant-pre-change";
328 end 430 event.orig_occupant = orig_occupant;
329 function room_mt:get_historylength() 431 event.dest_occupant = dest_occupant;
330 return self._data.history_length or default_history_length; 432 end
331 end 433 if module:fire_event(event_name, event) then return true; end
332 function room_mt:set_historylength(length) 434
333 length = math.min(tonumber(length) or default_history_length, max_history_length or math.huge); 435 -- Check for nick conflicts
334 if length == default_history_length then 436 if dest_occupant ~= nil and not is_first_dest_session and bare_jid ~= jid_bare(dest_occupant.bare_jid) then -- new nick or has different bare real jid
335 length = nil; 437 log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick);
336 end 438 local reply = st.error_reply(stanza, "cancel", "conflict"):up();
337 self._data.history_length = length; 439 reply.tags[1].attr.code = "409";
338 end 440 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
339 441 return true;
340 442 end
341 local valid_whois = { moderators = true, anyone = true }; 443
342 444 -- Send presence stanza about original occupant
343 function room_mt:set_whois(whois) 445 if orig_occupant ~= nil and orig_occupant ~= dest_occupant then
344 if valid_whois[whois] and self._data.whois ~= whois then 446 local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
345 self._data.whois = whois; 447 local dest_nick;
346 if self.save then self:save(true); end 448 if dest_occupant == nil then -- Session is leaving
347 end 449 log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick);
348 end 450 if is_last_orig_session then
349 451 orig_occupant.role = nil;
350 function room_mt:get_whois() 452 end
351 return self._data.whois; 453 orig_occupant:set_session(real_jid, stanza);
352 end 454 else
353 455 log("debug", "session %s is changing from occupant %s to %s", real_jid, orig_occupant.nick, dest_occupant.nick);
354 local function construct_stanza_id(room, stanza) 456 local generated_unavail = st.presence {from = orig_occupant.nick, to = real_jid, type = "unavailable"};
355 local from_jid, to_nick = stanza.attr.from, stanza.attr.to; 457 orig_occupant:set_session(real_jid, generated_unavail);
356 local from_nick = room._jid_nick[from_jid]; 458 dest_nick = select(3, jid_split(dest_occupant.nick));
357 local occupant = room._occupants[to_nick]; 459 if not is_first_dest_session then -- User is swapping into another pre-existing session
358 local to_jid = occupant.jid; 460 log("debug", "session %s is swapping into multisession %s, showing it leave.", real_jid, dest_occupant.nick);
359 461 -- Show the other session leaving
360 return from_nick, to_jid, base64.encode(to_jid.."\0"..stanza.attr.id.."\0"..md5(from_jid)); 462 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";})
361 end 463 :tag("status"):text("you are joining pre-existing session " .. dest_nick):up();
362 local function deconstruct_stanza_id(room, stanza) 464 add_item(x, self:get_affiliation(bare_jid), "none");
363 local from_jid_possiblybare, to_nick = stanza.attr.from, stanza.attr.to; 465 local pr = st.presence{from = dest_occupant.nick, to = real_jid, type = "unavailable"}
364 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$"); 466 :add_child(x);
365 local from_nick = room._jid_nick[from_jid]; 467 self:route_stanza(pr);
366 468 end
367 if not(from_nick) then return; end 469 if is_first_dest_session and is_last_orig_session then -- Normal nick change
368 if not(from_jid_possiblybare == from_jid or from_jid_possiblybare == jid_bare(from_jid)) then return; end 470 log("debug", "no sessions in %s left; publically marking as nick change", orig_occupant.nick);
369 471 orig_x:tag("status", {code = "303";}):up();
370 local occupant = room._occupants[to_nick]; 472 else -- The session itself always needs to see a nick change
371 for to_jid in pairs(occupant and occupant.sessions or {}) do 473 -- don't want to get our old nick's available presence,
372 if md5(to_jid) == to_jid_hash then 474 -- so remove our session from there, and manually generate an unavailable
373 return from_nick, to_jid, id; 475 orig_occupant:remove_session(real_jid);
374 end 476 log("debug", "generating nick change for %s", real_jid);
375 end 477 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
376 end 478 -- self:build_item_list(orig_occupant, x, false, dest_nick); -- COMPAT: clients get confused if they see other items besides their own
377 479 add_item(x, self:get_affiliation(bare_jid), orig_occupant.role, real_jid, dest_nick);
378 480 x:tag("status", {code = "303";}):up();
379 function room_mt:handle_to_occupant(origin, stanza) -- PM, vCards, etc 481 x:tag("status", {code = "110";}):up();
482 self:route_stanza(generated_unavail:add_child(x));
483 dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant
484 end
485 end
486 self:save_occupant(orig_occupant);
487 self:publicise_occupant_status(orig_occupant, orig_x, dest_nick);
488
489 if is_last_orig_session then
490 module:fire_event("muc-occupant-left", {room = self; nick = orig_occupant.nick; occupant = orig_occupant;});
491 end
492 end
493
494 if dest_occupant ~= nil then
495 dest_occupant:set_session(real_jid, stanza);
496 local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
497 if is_new_room then
498 dest_x:tag("status", {code = "201"}):up();
499 end
500 if orig_occupant == nil and self:get_whois() == "anyone" then
501 dest_x:tag("status", {code = "100"}):up();
502 end
503 self:save_occupant(dest_occupant);
504
505 if orig_occupant == nil then
506 -- Send occupant list to newly joined user
507 self:send_occupant_list(real_jid, function(nick, occupant)
508 -- Don't include self
509 return occupant:get_presence(real_jid) == nil;
510 end)
511 end
512 self:publicise_occupant_status(dest_occupant, dest_x);
513
514 if orig_occupant ~= nil and orig_occupant ~= dest_occupant and not is_last_orig_session then -- If user is swapping and wasn't last original session
515 log("debug", "session %s split nicks; showing %s rejoining", real_jid, orig_occupant.nick);
516 -- Show the original nick joining again
517 local pr = st.clone(orig_occupant:get_presence());
518 pr.attr.to = real_jid;
519 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
520 self:build_item_list(orig_occupant, x, false);
521 -- TODO: new status code to inform client this was the multi-session it left?
522 pr:add_child(x);
523 self:route_stanza(pr);
524 end
525
526 if orig_occupant == nil then
527 if is_first_dest_session then
528 module:fire_event("muc-occupant-joined", {room = self; nick = dest_occupant.nick; occupant = dest_occupant;});
529 end
530 module:fire_event("muc-occupant-session-new", {room = self; nick = dest_occupant.nick; occupant = dest_occupant; stanza = stanza; jid = real_jid;});
531 end
532 end
533 elseif type ~= 'result' then -- bad type
534 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
535 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
536 end
537 end
538 return true;
539 end
540
541 function room_mt:handle_iq_to_occupant(origin, stanza)
380 local from, to = stanza.attr.from, stanza.attr.to; 542 local from, to = stanza.attr.from, stanza.attr.to;
381 local room = jid_bare(to);
382 local current_nick = self._jid_nick[from];
383 local type = stanza.attr.type; 543 local type = stanza.attr.type;
384 log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag()); 544 local id = stanza.attr.id;
385 if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end 545 local occupant = self:get_occupant_by_nick(to);
386 if stanza.name == "presence" then 546 if (type == "error" or type == "result") then
387 local pr = get_filtered_presence(stanza); 547 do -- deconstruct_stanza_id
388 pr.attr.from = current_nick; 548 if not occupant then return nil; end
389 if type == "error" then -- error, kick em out! 549 local from_jid, id, to_jid_hash = (base64.decode(stanza.attr.id) or ""):match("^(.+)%z(.*)%z(.+)$");
390 if current_nick then 550 if not(from == from_jid or from == jid_bare(from_jid)) then return nil; end
391 log("debug", "kicking %s from %s", current_nick, room); 551 local from_occupant_jid = self:get_occupant_jid(from_jid);
392 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); 552 if from_occupant_jid == nil then return nil; end
393 end 553 local session_jid
394 elseif type == "unavailable" then -- unavailable 554 for to_jid in occupant:each_session() do
395 if current_nick then 555 if md5(to_jid) == to_jid_hash then
396 log("debug", "%s leaving %s", current_nick, room); 556 session_jid = to_jid;
397 self._jid_nick[from] = nil; 557 break;
398 local occupant = self._occupants[current_nick];
399 local new_jid = next(occupant.sessions);
400 if new_jid == from then new_jid = next(occupant.sessions, new_jid); end
401 if new_jid then
402 local jid = occupant.jid;
403 occupant.jid = new_jid;
404 occupant.sessions[from] = nil;
405 pr.attr.to = from;
406 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
407 :tag("item", {affiliation=occupant.affiliation or "none", role='none'}):up()
408 :tag("status", {code='110'}):up();
409 self:_route_stanza(pr);
410 if jid ~= new_jid then
411 pr = st.clone(occupant.sessions[new_jid])
412 :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
413 :tag("item", {affiliation=occupant.affiliation or "none", role=occupant.role or "none"});
414 pr.attr.from = current_nick;
415 self:broadcast_except_nick(pr, current_nick);
416 end
417 else
418 occupant.role = 'none';
419 self:broadcast_presence(pr, from);
420 self._occupants[current_nick] = nil;
421 end 558 end
422 end 559 end
423 elseif not type then -- available 560 if session_jid == nil then return nil; end
424 if current_nick then 561 stanza.attr.from, stanza.attr.to, stanza.attr.id = from_occupant_jid, session_jid, id;
425 --if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence 562 end
426 if current_nick == to then -- simple presence 563 log("debug", "%s sent private iq stanza to %s (%s)", from, to, stanza.attr.to);
427 log("debug", "%s broadcasted presence", current_nick); 564 self:route_stanza(stanza);
428 self._occupants[current_nick].sessions[from] = pr; 565 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
429 self:broadcast_presence(pr, from); 566 return true;
430 else -- change nick 567 else -- Type is "get" or "set"
431 local occupant = self._occupants[current_nick]; 568 local current_nick = self:get_occupant_jid(from);
432 local is_multisession = next(occupant.sessions, next(occupant.sessions)); 569 if not current_nick then
433 if self._occupants[to] or is_multisession then
434 log("debug", "%s couldn't change nick", current_nick);
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 local data = self._occupants[current_nick];
440 local to_nick = select(3, jid_split(to));
441 if to_nick then
442 log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to);
443 local p = st.presence({type='unavailable', from=current_nick});
444 self:broadcast_presence(p, from, '303', to_nick);
445 self._occupants[current_nick] = nil;
446 self._occupants[to] = data;
447 self._jid_nick[from] = to;
448 pr.attr.from = to;
449 self._occupants[to].sessions[from] = pr;
450 self:broadcast_presence(pr, from);
451 else
452 --TODO malformed-jid
453 end
454 end
455 end
456 --else -- possible rejoin
457 -- log("debug", "%s had connection replaced", current_nick);
458 -- self:handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to})
459 -- :tag('status'):text('Replaced by new connection'):up()); -- send unavailable
460 -- self:handle_to_occupant(origin, stanza); -- resend available
461 --end
462 else -- enter room
463 local new_nick = to;
464 local is_merge;
465 if self._occupants[to] then
466 if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then
467 new_nick = nil;
468 end
469 is_merge = true;
470 end
471 local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
472 password = password and password:get_child("password", "http://jabber.org/protocol/muc");
473 password = password and password[1] ~= "" and password[1];
474 if self:get_password() and self:get_password() ~= password then
475 log("debug", "%s couldn't join due to invalid password: %s", from, to);
476 local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
477 reply.tags[1].attr.code = "401";
478 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
479 elseif not new_nick then
480 log("debug", "%s couldn't join due to nick conflict: %s", from, to);
481 local reply = st.error_reply(stanza, "cancel", "conflict"):up();
482 reply.tags[1].attr.code = "409";
483 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
484 else
485 log("debug", "%s joining as %s", from, to);
486 if not next(self._affiliations) then -- new room, no owners
487 self._affiliations[jid_bare(from)] = "owner";
488 if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
489 self.locked = nil; -- Older groupchat protocol doesn't lock
490 end
491 elseif self.locked then -- Deny entry
492 origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
493 return;
494 end
495 local affiliation = self:get_affiliation(from);
496 local role = self:get_default_role(affiliation)
497 if role then -- new occupant
498 if not is_merge then
499 self._occupants[to] = {affiliation=affiliation, role=role, jid=from, sessions={[from]=get_filtered_presence(stanza)}};
500 else
501 self._occupants[to].sessions[from] = get_filtered_presence(stanza);
502 end
503 self._jid_nick[from] = to;
504 self:send_occupant_list(from);
505 pr.attr.from = to;
506 pr:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
507 :tag("item", {affiliation=affiliation or "none", role=role or "none"}):up();
508 if not is_merge then
509 self:broadcast_except_nick(pr, to);
510 end
511 pr:tag("status", {code='110'}):up();
512 if self._data.whois == 'anyone' then
513 pr:tag("status", {code='100'}):up();
514 end
515 if self.locked then
516 pr:tag("status", {code='201'}):up();
517 end
518 pr.attr.to = from;
519 self:_route_stanza(pr);
520 self:send_history(from, stanza);
521 self:send_subject(from);
522 elseif not affiliation then -- registration required for entering members-only room
523 local reply = st.error_reply(stanza, "auth", "registration-required"):up();
524 reply.tags[1].attr.code = "407";
525 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
526 else -- banned
527 local reply = st.error_reply(stanza, "auth", "forbidden"):up();
528 reply.tags[1].attr.code = "403";
529 origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
530 end
531 end
532 end
533 elseif type ~= 'result' then -- bad type
534 if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
535 origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
536 end
537 end
538 elseif not current_nick then -- not in room
539 if (type == "error" or type == "result") and stanza.name == "iq" then
540 local id = stanza.attr.id;
541 stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza);
542 if stanza.attr.id then
543 self:_route_stanza(stanza);
544 end
545 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
546 elseif type ~= "error" then
547 origin.send(st.error_reply(stanza, "cancel", "not-acceptable")); 570 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
548 end 571 return true;
549 elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM 572 end
573 if not occupant then -- recipient not in room
574 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
575 return true;
576 end
577 do -- construct_stanza_id
578 stanza.attr.id = base64.encode(occupant.jid.."\0"..stanza.attr.id.."\0"..md5(from));
579 end
580 stanza.attr.from, stanza.attr.to = current_nick, occupant.jid;
581 log("debug", "%s sent private iq stanza to %s (%s)", from, to, occupant.jid);
582 if stanza.tags[1].attr.xmlns == 'vcard-temp' then
583 stanza.attr.to = jid_bare(stanza.attr.to);
584 end
585 self:route_stanza(stanza);
586 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
587 return true;
588 end
589 end
590
591 function room_mt:handle_message_to_occupant(origin, stanza)
592 local from, to = stanza.attr.from, stanza.attr.to;
593 local current_nick = self:get_occupant_jid(from);
594 local type = stanza.attr.type;
595 if not current_nick then -- not in room
596 if type ~= "error" then
597 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
598 end
599 return true;
600 end
601 if type == "groupchat" then -- groupchat messages not allowed in PM
550 origin.send(st.error_reply(stanza, "modify", "bad-request")); 602 origin.send(st.error_reply(stanza, "modify", "bad-request"));
551 elseif current_nick and stanza.name == "message" and type == "error" and is_kickable_error(stanza) then 603 return true;
604 elseif type == "error" and is_kickable_error(stanza) then
552 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid); 605 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
553 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable 606 return self:handle_kickable(origin, stanza); -- send unavailable
554 else -- private stanza 607 end
555 local o_data = self._occupants[to]; 608
556 if o_data then 609 local o_data = self:get_occupant_by_nick(to);
557 log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid); 610 if not o_data then
558 if stanza.name == "iq" then 611 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
559 local id = stanza.attr.id; 612 return true;
560 if stanza.attr.type == "get" or stanza.attr.type == "set" then 613 end
561 stanza.attr.from, stanza.attr.to, stanza.attr.id = construct_stanza_id(self, stanza); 614 log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid);
562 else 615 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
563 stanza.attr.from, stanza.attr.to, stanza.attr.id = deconstruct_stanza_id(self, stanza); 616 stanza.attr.from = current_nick;
564 end 617 self:route_to_occupant(o_data, stanza)
565 if type == 'get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then 618 -- TODO: Remove x tag?
566 stanza.attr.to = jid_bare(stanza.attr.to); 619 stanza.attr.from = from;
567 end 620 return true;
568 if stanza.attr.id then
569 self:_route_stanza(stanza);
570 end
571 stanza.attr.from, stanza.attr.to, stanza.attr.id = from, to, id;
572 else -- message
573 stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
574 stanza.attr.from = current_nick;
575 for jid in pairs(o_data.sessions) do
576 stanza.attr.to = jid;
577 self:_route_stanza(stanza);
578 end
579 stanza.attr.from, stanza.attr.to = from, to;
580 end
581 elseif type ~= "error" and type ~= "result" then -- recipient not in room
582 origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
583 end
584 end
585 end 621 end
586 622
587 function room_mt:send_form(origin, stanza) 623 function room_mt:send_form(origin, stanza)
588 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner") 624 origin.send(st.reply(stanza):query("http://jabber.org/protocol/muc#owner")
589 :add_child(self:get_form_layout(stanza.attr.from):form()) 625 :add_child(self:get_form_layout(stanza.attr.from):form())
596 instructions = "Complete and submit this form to configure the room.", 632 instructions = "Complete and submit this form to configure the room.",
597 { 633 {
598 name = 'FORM_TYPE', 634 name = 'FORM_TYPE',
599 type = 'hidden', 635 type = 'hidden',
600 value = 'http://jabber.org/protocol/muc#roomconfig' 636 value = 'http://jabber.org/protocol/muc#roomconfig'
601 },
602 {
603 name = 'muc#roomconfig_roomname',
604 type = 'text-single',
605 label = 'Name',
606 value = self:get_name() or "",
607 },
608 {
609 name = 'muc#roomconfig_roomdesc',
610 type = 'text-single',
611 label = 'Description',
612 value = self:get_description() or "",
613 },
614 {
615 name = 'muc#roomconfig_persistentroom',
616 type = 'boolean',
617 label = 'Make Room Persistent?',
618 value = self:get_persistent()
619 },
620 {
621 name = 'muc#roomconfig_publicroom',
622 type = 'boolean',
623 label = 'Make Room Publicly Searchable?',
624 value = not self:get_hidden()
625 },
626 {
627 name = 'muc#roomconfig_changesubject',
628 type = 'boolean',
629 label = 'Allow Occupants to Change Subject?',
630 value = self:get_changesubject()
631 },
632 {
633 name = 'muc#roomconfig_whois',
634 type = 'list-single',
635 label = 'Who May Discover Real JIDs?',
636 value = {
637 { value = 'moderators', label = 'Moderators Only', default = self._data.whois == 'moderators' },
638 { value = 'anyone', label = 'Anyone', default = self._data.whois == 'anyone' }
639 }
640 },
641 {
642 name = 'muc#roomconfig_roomsecret',
643 type = 'text-private',
644 label = 'Password',
645 value = self:get_password() or "",
646 },
647 {
648 name = 'muc#roomconfig_moderatedroom',
649 type = 'boolean',
650 label = 'Make Room Moderated?',
651 value = self:get_moderated()
652 },
653 {
654 name = 'muc#roomconfig_membersonly',
655 type = 'boolean',
656 label = 'Make Room Members-Only?',
657 value = self:get_members_only()
658 },
659 {
660 name = 'muc#roomconfig_historylength',
661 type = 'text-single',
662 label = 'Maximum Number of History Messages Returned by Room',
663 value = tostring(self:get_historylength())
664 } 637 }
665 }); 638 });
666 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form; 639 return module:fire_event("muc-config-form", { room = self, actor = actor, form = form }) or form;
667 end 640 end
668 641
669 function room_mt:process_form(origin, stanza) 642 function room_mt:process_form(origin, stanza)
670 local query = stanza.tags[1]; 643 local form = stanza.tags[1]:get_child("x", "jabber:x:data");
671 local form; 644 if form.attr.type == "cancel" then
672 for _, tag in ipairs(query.tags) do if tag.name == "x" and tag.attr.xmlns == "jabber:x:data" then form = tag; break; end end 645 origin.send(st.reply(stanza));
673 if not form then origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); return; end 646 elseif form.attr.type == "submit" then
674 if form.attr.type == "cancel" then origin.send(st.reply(stanza)); return; end 647 local fields;
675 if form.attr.type ~= "submit" then origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form")); return; end 648 if form.tags[1] == nil then -- Instant room
676 649 fields = {};
677 local fields = self:get_form_layout(stanza.attr.from):data(form); 650 else
678 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 651 fields = self:get_form_layout(stanza.attr.from):data(form);
679 652 if fields.FORM_TYPE ~= "http://jabber.org/protocol/muc#roomconfig" then
680 653 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Form is not of type room configuration"));
681 local changed = {}; 654 return true;
682 655 end
683 local function handle_option(name, field, allowed) 656 end
684 local new = fields[field]; 657
685 if new == nil then return; end 658 local event = {room = self; origin = origin; stanza = stanza; fields = fields; status_codes = {};};
686 if allowed and not allowed[new] then return; end 659 function event.update_option(name, field, allowed)
687 if new == self["get_"..name](self) then return; end 660 local new = fields[field];
688 changed[name] = true; 661 if new == nil then return; end
689 self["set_"..name](self, new); 662 if allowed and not allowed[new] then return; end
690 end 663 if new == self["get_"..name](self) then return; end
691 664 event.status_codes["104"] = true;
692 local event = { room = self, fields = fields, changed = changed, stanza = stanza, origin = origin, update_option = handle_option }; 665 self["set_"..name](self, new);
693 module:fire_event("muc-config-submitted", event); 666 return true;
694 667 end
695 handle_option("name", "muc#roomconfig_roomname"); 668 module:fire_event("muc-config-submitted", event);
696 handle_option("description", "muc#roomconfig_roomdesc"); 669
697 handle_option("persistent", "muc#roomconfig_persistentroom"); 670 if self.save then self:save(true); end
698 handle_option("moderated", "muc#roomconfig_moderatedroom"); 671 origin.send(st.reply(stanza));
699 handle_option("members_only", "muc#roomconfig_membersonly"); 672
700 handle_option("public", "muc#roomconfig_publicroom"); 673 if next(event.status_codes) then
701 handle_option("changesubject", "muc#roomconfig_changesubject"); 674 local msg = st.message({type='groupchat', from=self.jid})
702 handle_option("historylength", "muc#roomconfig_historylength"); 675 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
703 handle_option("whois", "muc#roomconfig_whois", valid_whois); 676 for code in pairs(event.status_codes) do
704 handle_option("password", "muc#roomconfig_roomsecret"); 677 msg:tag("status", {code = code;}):up();
705 678 end
706 if self.save then self:save(true); end 679 msg:up();
707 if self.locked then 680 self:broadcast_message(msg);
708 module:fire_event("muc-room-unlocked", { room = self }); 681 end
709 self.locked = nil; 682 else
710 end 683 origin.send(st.error_reply(stanza, "cancel", "bad-request", "Not a submitted form"));
684 end
685 return true;
686 end
687
688 -- Removes everyone from the room
689 function room_mt:clear(x)
690 x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
691 local occupants_updated = {};
692 for nick, occupant in self:each_occupant() do
693 occupant.role = nil;
694 self:save_occupant(occupant);
695 occupants_updated[occupant] = true;
696 end
697 for occupant in pairs(occupants_updated) do
698 self:publicise_occupant_status(occupant, x);
699 module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant;});
700 end
701 end
702
703 function room_mt:destroy(newjid, reason, password)
704 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
705 :tag("item", { affiliation='none', role='none' }):up()
706 :tag("destroy", {jid=newjid});
707 if reason then x:tag("reason"):text(reason):up(); end
708 if password then x:tag("password"):text(password):up(); end
709 x:up();
710 self:clear(x);
711 module:fire_event("muc-room-destroyed", { room = self });
712 end
713
714 function room_mt:handle_disco_info_get_query(origin, stanza)
715 origin.send(self:get_disco_info(stanza));
716 return true;
717 end
718
719 function room_mt:handle_disco_items_get_query(origin, stanza)
720 origin.send(self:get_disco_items(stanza));
721 return true;
722 end
723
724 function room_mt:handle_admin_query_set_command(origin, stanza)
725 local item = stanza.tags[1].tags[1];
726 if item.attr.jid then -- Validate provided JID
727 item.attr.jid = jid_prep(item.attr.jid);
728 if not item.attr.jid then
729 origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
730 return true;
731 end
732 end
733 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
734 local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick);
735 if occupant then item.attr.jid = occupant.jid; end
736 elseif not item.attr.nick and item.attr.jid then
737 local nick = self:get_occupant_jid(item.attr.jid);
738 if nick then item.attr.nick = select(3, jid_split(nick)); end
739 end
740 local actor = stanza.attr.from;
741 local reason = item:get_child_text("reason");
742 local success, errtype, err
743 if item.attr.affiliation and item.attr.jid and not item.attr.role then
744 success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason);
745 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
746 success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason);
747 else
748 success, errtype, err = nil, "cancel", "bad-request";
749 end
750 if not success then origin.send(st.error_reply(stanza, errtype, err)); end
711 origin.send(st.reply(stanza)); 751 origin.send(st.reply(stanza));
712 752 return true;
713 if next(changed) then 753 end
714 local msg = st.message({type='groupchat', from=self.jid}) 754
715 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'}):up() 755 function room_mt:handle_admin_query_get_command(origin, stanza)
716 :tag('status', {code = '104'}):up(); 756 local actor = stanza.attr.from;
717 if changed.whois then 757 local affiliation = self:get_affiliation(actor);
718 local code = (self:get_whois() == 'moderators') and "173" or "172"; 758 local item = stanza.tags[1].tags[1];
719 msg.tags[1]:tag('status', {code = code}):up(); 759 local _aff = item.attr.affiliation;
720 end 760 local _aff_rank = valid_affiliations[_aff or "none"];
721 self:broadcast_message(msg, false) 761 local _rol = item.attr.role;
722 end 762 if _aff and _aff_rank and not _rol then
723 end 763 -- You need to be at least an admin, and be requesting info about your affifiliation or lower
724 764 -- e.g. an admin can't ask for a list of owners
725 function room_mt:destroy(newjid, reason, password) 765 local affiliation_rank = valid_affiliations[affiliation];
726 local pr = st.presence({type = "unavailable"}) 766 if affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank then
727 :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) 767 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
728 :tag("item", { affiliation='none', role='none' }):up() 768 for jid in self:each_affiliation(_aff or "none") do
729 :tag("destroy", {jid=newjid}) 769 reply:tag("item", {affiliation = _aff, jid = jid}):up();
730 if reason then pr:tag("reason"):text(reason):up(); end 770 end
731 if password then pr:tag("password"):text(password):up(); end 771 origin.send(reply:up());
732 for nick, occupant in pairs(self._occupants) do 772 return true;
733 pr.attr.from = nick; 773 else
734 for jid in pairs(occupant.sessions) do 774 origin.send(st.error_reply(stanza, "auth", "forbidden"));
735 pr.attr.to = jid; 775 return true;
736 self:_route_stanza(pr); 776 end
737 self._jid_nick[jid] = nil; 777 elseif _rol and valid_roles[_rol or "none"] and not _aff then
738 end 778 local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation);
739 self._occupants[nick] = nil; 779 if valid_roles[role or "none"] >= valid_roles.moderator then
740 end 780 if _rol == "none" then _rol = nil; end
741 self:set_persistent(false); 781 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
742 module:fire_event("muc-room-destroyed", { room = self }); 782 -- TODO: whois check here? (though fully anonymous rooms are not supported)
743 end 783 for occupant_jid, occupant in self:each_occupant() do
744 784 if occupant.role == _rol then
745 function room_mt:handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc 785 local nick = select(3,jid_split(occupant_jid));
786 self:build_item_list(occupant, reply, false, nick);
787 end
788 end
789 origin.send(reply:up());
790 return true;
791 else
792 origin.send(st.error_reply(stanza, "auth", "forbidden"));
793 return true;
794 end
795 else
796 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
797 return true;
798 end
799 end
800
801 function room_mt:handle_owner_query_get_to_room(origin, stanza)
802 if self:get_affiliation(stanza.attr.from) ~= "owner" then
803 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
804 return true;
805 end
806
807 self:send_form(origin, stanza);
808 return true;
809 end
810 function room_mt:handle_owner_query_set_to_room(origin, stanza)
811 if self:get_affiliation(stanza.attr.from) ~= "owner" then
812 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
813 return true;
814 end
815
816 local child = stanza.tags[1].tags[1];
817 if not child then
818 origin.send(st.error_reply(stanza, "modify", "bad-request"));
819 return true;
820 elseif child.name == "destroy" then
821 local newjid = child.attr.jid;
822 local reason = child:get_child_text("reason");
823 local password = child:get_child_text("password");
824 self:destroy(newjid, reason, password);
825 origin.send(st.reply(stanza));
826 return true;
827 elseif child.name == "x" and child.attr.xmlns == "jabber:x:data" then
828 return self:process_form(origin, stanza);
829 else
830 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
831 return true;
832 end
833 end
834
835 function room_mt:handle_groupchat_to_room(origin, stanza)
836 local from = stanza.attr.from;
837 local occupant = self:get_occupant_by_real_jid(from);
838 if module:fire_event("muc-occupant-groupchat", {
839 room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
840 }) then return true; end
841 stanza.attr.from = occupant.nick;
842 self:broadcast_message(stanza);
843 stanza.attr.from = from;
844 return true;
845 end
846
847 -- Role check
848 module:hook("muc-occupant-groupchat", function(event)
849 local role_rank = valid_roles[event.occupant and event.occupant.role or "none"];
850 if role_rank <= valid_roles.none then
851 event.origin.send(st.error_reply(event.stanza, "cancel", "not-acceptable"));
852 return true;
853 elseif role_rank <= valid_roles.visitor then
854 event.origin.send(st.error_reply(event.stanza, "auth", "forbidden"));
855 return true;
856 end
857 end, 50);
858
859 -- hack - some buggy clients send presence updates to the room rather than their nick
860 function room_mt:handle_presence_to_room(origin, stanza)
861 local current_nick = self:get_occupant_jid(stanza.attr.from);
862 local handled
863 if current_nick then
864 local to = stanza.attr.to;
865 stanza.attr.to = current_nick;
866 handled = self:handle_presence_to_occupant(origin, stanza);
867 stanza.attr.to = to;
868 end
869 return handled;
870 end
871
872 -- Need visitor role or higher to invite
873 module:hook("muc-pre-invite", function(event)
874 local room, stanza = event.room, event.stanza;
875 local _from, _to = stanza.attr.from, stanza.attr.to;
876 local inviter = room:get_occupant_by_real_jid(_from);
877 local role = inviter and inviter.role or room:get_default_role(room:get_affiliation(_from));
878 if valid_roles[role or "none"] <= valid_roles.visitor then
879 event.origin.send(st.error_reply(stanza, "auth", "forbidden"));
880 return true;
881 end
882 end);
883
884 function room_mt:handle_mediated_invite(origin, stanza)
885 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
886 local invitee = jid_prep(payload.attr.to);
887 if not invitee then
888 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
889 return true;
890 elseif module:fire_event("muc-pre-invite", {room = self, origin = origin, stanza = stanza}) then
891 return true;
892 end
893 local invite = muc_util.filter_muc_x(st.clone(stanza));
894 invite.attr.from = self.jid;
895 invite.attr.to = invitee;
896 invite:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
897 :tag('invite', {from = stanza.attr.from;})
898 :tag('reason'):text(payload:get_child_text("reason")):up()
899 :up()
900 :up();
901 if not module:fire_event("muc-invite", {room = self, stanza = invite, origin = origin, incoming = stanza}) then
902 self:route_stanza(invite);
903 end
904 return true;
905 end
906
907 -- COMPAT: Some older clients expect this
908 module:hook("muc-invite", function(event)
909 local room, stanza = event.room, event.stanza;
910 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
911 local reason = invite:get_child_text("reason");
912 stanza:tag('x', {xmlns = "jabber:x:conference"; jid = room.jid;})
913 :text(reason or "")
914 :up();
915 end);
916
917 -- Add a plain message for clients which don't support invites
918 module:hook("muc-invite", function(event)
919 local room, stanza = event.room, event.stanza;
920 if not stanza:get_child("body") then
921 local invite = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("invite");
922 local reason = invite:get_child_text("reason") or "";
923 stanza:tag("body")
924 :text(invite.attr.from.." invited you to the room "..room.jid..(reason == "" and (" ("..reason..")") or ""))
925 :up();
926 end
927 end);
928
929 function room_mt:handle_mediated_decline(origin, stanza)
930 local payload = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
931 local declinee = jid_prep(payload.attr.to);
932 if not declinee then
933 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
934 return true;
935 elseif module:fire_event("muc-pre-decline", {room = self, origin = origin, stanza = stanza}) then
936 return true;
937 end
938 local decline = muc_util.filter_muc_x(st.clone(stanza));
939 decline.attr.from = self.jid;
940 decline.attr.to = declinee;
941 decline:tag("x", {xmlns = "http://jabber.org/protocol/muc#user"})
942 :tag("decline", {from = stanza.attr.from})
943 :tag("reason"):text(payload:get_child_text("reason")):up()
944 :up()
945 :up();
946 if not module:fire_event("muc-decline", {room = self, stanza = decline, origin = origin, incoming = stanza}) then
947 local declinee = decline.attr.to; -- re-fetch, in case event modified it
948 local occupant
949 if jid_bare(declinee) == self.jid then -- declinee jid is already an in-room jid
950 occupant = self:get_occupant_by_nick(declinee);
951 end
952 if occupant then
953 self:route_to_occupant(occupant, decline);
954 else
955 self:route_stanza(decline);
956 end
957 end
958 return true;
959 end
960
961 -- Add a plain message for clients which don't support declines
962 module:hook("muc-decline", function(event)
963 local room, stanza = event.room, event.stanza;
964 if not stanza:get_child("body") then
965 local decline = stanza:get_child("x", "http://jabber.org/protocol/muc#user"):get_child("decline");
966 local reason = decline:get_child_text("reason") or "";
967 stanza:tag("body")
968 :text(decline.attr.from.." declined your invite to the room "..room.jid..(reason == "" and (" ("..reason..")") or ""))
969 :up();
970 end
971 end);
972
973 function room_mt:handle_message_to_room(origin, stanza)
746 local type = stanza.attr.type; 974 local type = stanza.attr.type;
747 local xmlns = stanza.tags[1] and stanza.tags[1].attr.xmlns; 975 if type == "groupchat" then
748 if stanza.name == "iq" then 976 return self:handle_groupchat_to_room(origin, stanza)
749 if xmlns == "http://jabber.org/protocol/disco#info" and type == "get" and not stanza.tags[1].attr.node then 977 elseif type == "error" and is_kickable_error(stanza) then
750 origin.send(self:get_disco_info(stanza)); 978 return self:handle_kickable(origin, stanza)
751 elseif xmlns == "http://jabber.org/protocol/disco#items" and type == "get" and not stanza.tags[1].attr.node then 979 elseif type == nil then
752 origin.send(self:get_disco_items(stanza)); 980 local x = stanza:get_child("x", "http://jabber.org/protocol/muc#user");
753 elseif xmlns == "http://jabber.org/protocol/muc#admin" then 981 if x then
754 local actor = stanza.attr.from; 982 local payload = x.tags[1];
755 local affiliation = self:get_affiliation(actor); 983 if payload == nil then
756 local current_nick = self._jid_nick[actor]; 984 -- fallthrough
757 local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation); 985 elseif payload.name == "invite" and payload.attr.to then
758 local item = stanza.tags[1].tags[1]; 986 return self:handle_mediated_invite(origin, stanza)
759 if item and item.name == "item" then 987 elseif payload.name == "decline" and payload.attr.to then
760 if type == "set" then 988 return self:handle_mediated_decline(origin, stanza)
761 local callback = function() origin.send(st.reply(stanza)); end 989 end
762 if item.attr.jid then -- Validate provided JID
763 item.attr.jid = jid_prep(item.attr.jid);
764 if not item.attr.jid then
765 origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
766 return;
767 end
768 end
769 if not item.attr.jid and item.attr.nick then -- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
770 local occupant = self._occupants[self.jid.."/"..item.attr.nick];
771 if occupant then item.attr.jid = occupant.jid; end
772 elseif not item.attr.nick and item.attr.jid then
773 local nick = self._jid_nick[item.attr.jid];
774 if nick then item.attr.nick = select(3, jid_split(nick)); end
775 end
776 local reason = item.tags[1] and item.tags[1].name == "reason" and #item.tags[1] == 1 and item.tags[1][1];
777 if item.attr.affiliation and item.attr.jid and not item.attr.role then
778 local success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, callback, reason);
779 if not success then origin.send(st.error_reply(stanza, errtype, err)); end
780 elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
781 local success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, callback, reason);
782 if not success then origin.send(st.error_reply(stanza, errtype, err)); end
783 else
784 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
785 end
786 elseif type == "get" then
787 local _aff = item.attr.affiliation;
788 local _rol = item.attr.role;
789 if _aff and not _rol then
790 if affiliation == "owner" or (affiliation == "admin" and _aff ~= "owner" and _aff ~= "admin") then
791 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
792 for jid, affiliation in pairs(self._affiliations) do
793 if affiliation == _aff then
794 reply:tag("item", {affiliation = _aff, jid = jid}):up();
795 end
796 end
797 origin.send(reply);
798 else
799 origin.send(st.error_reply(stanza, "auth", "forbidden"));
800 end
801 elseif _rol and not _aff then
802 if role == "moderator" then
803 -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
804 if _rol == "none" then _rol = nil; end
805 local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
806 for occupant_jid, occupant in pairs(self._occupants) do
807 if occupant.role == _rol then
808 reply:tag("item", {
809 nick = select(3, jid_split(occupant_jid)),
810 role = _rol or "none",
811 affiliation = occupant.affiliation or "none",
812 jid = occupant.jid
813 }):up();
814 end
815 end
816 origin.send(reply);
817 else
818 origin.send(st.error_reply(stanza, "auth", "forbidden"));
819 end
820 else
821 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
822 end
823 end
824 elseif type == "set" or type == "get" then
825 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
826 end
827 elseif xmlns == "http://jabber.org/protocol/muc#owner" and (type == "get" or type == "set") and stanza.tags[1].name == "query" then
828 if self:get_affiliation(stanza.attr.from) ~= "owner" then
829 origin.send(st.error_reply(stanza, "auth", "forbidden", "Only owners can configure rooms"));
830 elseif stanza.attr.type == "get" then
831 self:send_form(origin, stanza);
832 elseif stanza.attr.type == "set" then
833 local child = stanza.tags[1].tags[1];
834 if not child then
835 origin.send(st.error_reply(stanza, "modify", "bad-request"));
836 elseif child.name == "destroy" then
837 local newjid = child.attr.jid;
838 local reason, password;
839 for _,tag in ipairs(child.tags) do
840 if tag.name == "reason" then
841 reason = #tag.tags == 0 and tag[1];
842 elseif tag.name == "password" then
843 password = #tag.tags == 0 and tag[1];
844 end
845 end
846 self:destroy(newjid, reason, password);
847 origin.send(st.reply(stanza));
848 else
849 self:process_form(origin, stanza);
850 end
851 end
852 elseif type == "set" or type == "get" then
853 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
854 end
855 elseif stanza.name == "message" and type == "groupchat" then
856 local from = stanza.attr.from;
857 local current_nick = self._jid_nick[from];
858 local occupant = self._occupants[current_nick];
859 if not occupant then -- not in room
860 origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
861 elseif occupant.role == "visitor" then
862 origin.send(st.error_reply(stanza, "auth", "forbidden"));
863 else
864 local from = stanza.attr.from;
865 stanza.attr.from = current_nick;
866 local subject = stanza:get_child_text("subject");
867 if subject then
868 if occupant.role == "moderator" or
869 ( self._data.changesubject and occupant.role == "participant" ) then -- and participant
870 self:set_subject(current_nick, subject);
871 else
872 stanza.attr.from = from;
873 origin.send(st.error_reply(stanza, "auth", "forbidden"));
874 end
875 else
876 self:broadcast_message(stanza, self:get_historylength() > 0 and stanza:get_child("body"));
877 end
878 stanza.attr.from = from;
879 end
880 elseif stanza.name == "message" and type == "error" and is_kickable_error(stanza) then
881 local current_nick = self._jid_nick[stanza.attr.from];
882 log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
883 self:handle_to_occupant(origin, build_unavailable_presence_from_error(stanza)); -- send unavailable
884 elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
885 local to = stanza.attr.to;
886 local current_nick = self._jid_nick[stanza.attr.from];
887 if current_nick then
888 stanza.attr.to = current_nick;
889 self:handle_to_occupant(origin, stanza);
890 stanza.attr.to = to;
891 elseif type ~= "error" and type ~= "result" then
892 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
893 end
894 elseif stanza.name == "message" and not(type == "chat" or type == "error" or type == "groupchat" or type == "headline") and #stanza.tags == 1
895 and self._jid_nick[stanza.attr.from] and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" then
896 local x = stanza.tags[1];
897 local payload = (#x.tags == 1 and x.tags[1]);
898 if payload and payload.name == "invite" and payload.attr.to then
899 local _from, _to = stanza.attr.from, stanza.attr.to;
900 local _invitee = jid_prep(payload.attr.to);
901 if _invitee then
902 local _reason = payload.tags[1] and payload.tags[1].name == 'reason' and #payload.tags[1].tags == 0 and payload.tags[1][1];
903 local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id})
904 :tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
905 :tag('invite', {from=_from})
906 :tag('reason'):text(_reason or ""):up()
907 :up();
908 if self:get_password() then
909 invite:tag("password"):text(self:get_password()):up();
910 end
911 invite:up()
912 :tag('x', {xmlns="jabber:x:conference", jid=_to}) -- COMPAT: Some older clients expect this
913 :text(_reason or "")
914 :up()
915 :tag('body') -- Add a plain message for clients which don't support invites
916 :text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
917 :up();
918 if self:get_members_only() and not self:get_affiliation(_invitee) then
919 log("debug", "%s invited %s into members only room %s, granting membership", _from, _invitee, _to);
920 self:set_affiliation(_from, _invitee, "member", nil, "Invited by " .. self._jid_nick[_from])
921 end
922 self:_route_stanza(invite);
923 else
924 origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
925 end
926 else
927 origin.send(st.error_reply(stanza, "cancel", "bad-request")); 990 origin.send(st.error_reply(stanza, "cancel", "bad-request"));
928 end 991 return true;
929 else 992 end
930 if type == "error" or type == "result" then return; end 993 end
931 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); 994 end
932 end 995
933 end 996 function room_mt:route_stanza(stanza)
934 997 module:send(stanza);
935 function room_mt:handle_stanza(origin, stanza) 998 end
936 local to_node, to_host, to_resource = jid_split(stanza.attr.to);
937 if to_resource then
938 self:handle_to_occupant(origin, stanza);
939 else
940 self:handle_to_room(origin, stanza);
941 end
942 end
943
944 function room_mt:route_stanza(stanza) end -- Replace with a routing function, e.g., function(room, stanza) core_route_stanza(origin, stanza); end
945 999
946 function room_mt:get_affiliation(jid) 1000 function room_mt:get_affiliation(jid)
947 local node, host, resource = jid_split(jid); 1001 local node, host, resource = jid_split(jid);
948 local bare = node and node.."@"..host or host; 1002 local bare = node and node.."@"..host or host;
949 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID. 1003 local result = self._affiliations[bare]; -- Affiliations are granted, revoked, and maintained based on the user's bare JID.
950 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned 1004 if not result and self._affiliations[host] == "outcast" then result = "outcast"; end -- host banned
951 return result; 1005 return result;
952 end 1006 end
953 function room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 1007
954 jid = jid_bare(jid); 1008 -- Iterates over jid, affiliation pairs
955 if affiliation == "none" then affiliation = nil; end 1009 function room_mt:each_affiliation(with_affiliation)
956 if affiliation and affiliation ~= "outcast" and affiliation ~= "owner" and affiliation ~= "admin" and affiliation ~= "member" then 1010 if not with_affiliation then
1011 return pairs(self._affiliations);
1012 else
1013 return function(_affiliations, jid)
1014 local affiliation;
1015 repeat -- Iterate until we get a match
1016 jid, affiliation = next(_affiliations, jid);
1017 until jid == nil or affiliation == with_affiliation
1018 return jid, affiliation;
1019 end, self._affiliations, nil
1020 end
1021 end
1022
1023 function room_mt:set_affiliation(actor, jid, affiliation, reason)
1024 if not actor then return nil, "modify", "not-acceptable"; end;
1025
1026 local node, host, resource = jid_split(jid);
1027 if not host then return nil, "modify", "not-acceptable"; end
1028 jid = jid_join(node, host); -- Bare
1029 local is_host_only = node == nil;
1030
1031 if valid_affiliations[affiliation or "none"] == nil then
957 return nil, "modify", "not-acceptable"; 1032 return nil, "modify", "not-acceptable";
958 end 1033 end
959 if actor ~= true then 1034 affiliation = affiliation ~= "none" and affiliation or nil; -- coerces `affiliation == false` to `nil`
1035
1036 local target_affiliation = self._affiliations[jid]; -- Raw; don't want to check against host
1037 local is_downgrade = valid_affiliations[target_affiliation or "none"] > valid_affiliations[affiliation or "none"];
1038
1039 if actor == true then
1040 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
1041 else
960 local actor_affiliation = self:get_affiliation(actor); 1042 local actor_affiliation = self:get_affiliation(actor);
961 local target_affiliation = self:get_affiliation(jid); 1043 if actor_affiliation == "owner" then
962 if target_affiliation == affiliation then -- no change, shortcut 1044 if jid_bare(actor) == jid then -- self change
963 if callback then callback(); end 1045 -- need at least one owner
964 return true; 1046 local is_last = true;
965 end 1047 for j in self:each_affiliation("owner") do
966 if actor_affiliation ~= "owner" then 1048 if j ~= jid then is_last = false; break; end
967 if affiliation == "owner" or affiliation == "admin" or actor_affiliation ~= "admin" or target_affiliation == "owner" or target_affiliation == "admin" then 1049 end
968 return nil, "cancel", "not-allowed"; 1050 if is_last then
969 end 1051 return nil, "cancel", "conflict";
970 elseif target_affiliation == "owner" and jid_bare(actor) == jid then -- self change 1052 end
971 local is_last = true; 1053 end
972 for j, aff in pairs(self._affiliations) do if j ~= jid and aff == "owner" then is_last = false; break; end end 1054 -- owners can do anything else
973 if is_last then 1055 elseif affiliation == "owner" or affiliation == "admin"
974 return nil, "cancel", "conflict"; 1056 or actor_affiliation ~= "admin"
975 end 1057 or target_affiliation == "owner" or target_affiliation == "admin" then
976 end 1058 -- Can't demote owners or other admins
977 end 1059 return nil, "cancel", "not-allowed";
1060 end
1061 end
1062
1063 -- Set in 'database'
978 self._affiliations[jid] = affiliation; 1064 self._affiliations[jid] = affiliation;
1065
1066 -- Update roles
979 local role = self:get_default_role(affiliation); 1067 local role = self:get_default_role(affiliation);
980 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) 1068 local role_rank = valid_roles[role or "none"];
981 :tag("item", {affiliation=affiliation or "none", role=role or "none"}) 1069 local occupants_updated = {}; -- Filled with old roles
982 :tag("reason"):text(reason or ""):up() 1070 for nick, occupant in self:each_occupant() do
983 :up(); 1071 if occupant.bare_jid == jid or (
984 local presence_type = nil; 1072 -- Outcast can be by host.
1073 is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host
1074 ) then
1075 -- need to publcize in all cases; as affiliation in <item/> has changed.
1076 occupants_updated[occupant] = occupant.role;
1077 if occupant.role ~= role and (
1078 is_downgrade or
1079 valid_roles[occupant.role or "none"] < role_rank -- upgrade
1080 ) then
1081 occupant.role = role;
1082 self:save_occupant(occupant);
1083 end
1084 end
1085 end
1086
1087 -- Tell the room of the new occupant affiliations+roles
1088 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
985 if not role then -- getting kicked 1089 if not role then -- getting kicked
986 presence_type = "unavailable";
987 if affiliation == "outcast" then 1090 if affiliation == "outcast" then
988 x:tag("status", {code="301"}):up(); -- banned 1091 x:tag("status", {code="301"}):up(); -- banned
989 else 1092 else
990 x:tag("status", {code="321"}):up(); -- affiliation change 1093 x:tag("status", {code="321"}):up(); -- affiliation change
991 end 1094 end
992 end 1095 end
993 local modified_nicks = {}; 1096 local is_semi_anonymous = self:get_whois() == "moderators";
994 for nick, occupant in pairs(self._occupants) do 1097 for occupant, old_role in pairs(occupants_updated) do
995 if jid_bare(occupant.jid) == jid then 1098 self:publicise_occupant_status(occupant, x, nil, actor, reason);
996 if not role then -- getting kicked 1099 if occupant.role == nil then
997 self._occupants[nick] = nil; 1100 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
998 else 1101 elseif is_semi_anonymous and
999 occupant.affiliation, occupant.role = affiliation, role; 1102 (old_role == "moderator" and occupant.role ~= "moderator") or
1000 end 1103 (old_role ~= "moderator" and occupant.role == "moderator") then -- Has gained or lost moderator status
1001 for jid,pres in pairs(occupant.sessions) do -- remove for all sessions of the nick 1104 -- Send everyone else's presences (as jid visibility has changed)
1002 if not role then self._jid_nick[jid] = nil; end 1105 for real_jid in occupant:each_session() do
1003 local p = st.clone(pres); 1106 self:send_occupant_list(real_jid, function(occupant_jid, occupant)
1004 p.attr.from = nick; 1107 return occupant.bare_jid ~= jid;
1005 p.attr.type = presence_type; 1108 end);
1006 p.attr.to = jid; 1109 end
1007 p:add_child(x); 1110 end
1008 self:_route_stanza(p); 1111 end
1009 if occupant.jid == jid then 1112
1010 modified_nicks[nick] = p;
1011 end
1012 end
1013 end
1014 end
1015 if self.save then self:save(); end 1113 if self.save then self:save(); end
1016 if callback then callback(); end 1114
1017 for nick,p in pairs(modified_nicks) do 1115 module:fire_event("muc-set-affiliation", {
1018 p.attr.from = nick; 1116 room = self;
1019 self:broadcast_except_nick(p, nick); 1117 actor = actor;
1020 end 1118 jid = jid;
1119 affiliation = affiliation or "none";
1120 reason = reason;
1121 previous_affiliation = target_affiliation;
1122 in_room = next(occupants_updated) ~= nil;
1123 });
1124
1021 return true; 1125 return true;
1022 end 1126 end
1023 1127
1024 function room_mt:get_role(nick) 1128 function room_mt:get_role(nick)
1025 local session = self._occupants[nick]; 1129 local occupant = self:get_occupant_by_nick(nick);
1026 return session and session.role or nil; 1130 return occupant and occupant.role or nil;
1027 end 1131 end
1028 function room_mt:can_set_role(actor_jid, occupant_jid, role) 1132
1029 local occupant = self._occupants[occupant_jid]; 1133 function room_mt:set_role(actor, occupant_jid, role, reason)
1030 if not occupant or not actor_jid then return nil, "modify", "not-acceptable"; end 1134 if not actor then return nil, "modify", "not-acceptable"; end
1031 1135
1032 if actor_jid == true then return true; end 1136 local occupant = self:get_occupant_by_nick(occupant_jid);
1033 1137 if not occupant then return nil, "modify", "not-acceptable"; end
1034 local actor = self._occupants[self._jid_nick[actor_jid]]; 1138
1035 if actor and actor.role == "moderator" then 1139 if valid_roles[role or "none"] == nil then
1036 if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then 1140 return nil, "modify", "not-acceptable";
1037 if actor.affiliation == "owner" or actor.affiliation == "admin" then 1141 end
1038 return true; 1142 role = role ~= "none" and role or nil; -- coerces `role == false` to `nil`
1039 elseif occupant.role ~= "moderator" and role ~= "moderator" then 1143
1040 return true; 1144 if actor == true then
1041 end 1145 actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
1042 end 1146 else
1043 end 1147 -- Can't do anything to other owners or admins
1044 return nil, "cancel", "not-allowed"; 1148 local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
1045 end 1149 if occupant_affiliation == "owner" and occupant_affiliation == "admin" then
1046 function room_mt:set_role(actor, occupant_jid, role, callback, reason) 1150 return nil, "cancel", "not-allowed";
1047 if role == "none" then role = nil; end 1151 end
1048 if role and role ~= "moderator" and role ~= "participant" and role ~= "visitor" then return nil, "modify", "not-acceptable"; end 1152
1049 local allowed, err_type, err_condition = self:can_set_role(actor, occupant_jid, role); 1153 -- If you are trying to give or take moderator role you need to be an owner or admin
1050 if not allowed then return allowed, err_type, err_condition; end 1154 if occupant.role == "moderator" or role == "moderator" then
1051 local occupant = self._occupants[occupant_jid]; 1155 local actor_affiliation = self:get_affiliation(actor);
1052 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"}) 1156 if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
1053 :tag("item", {affiliation=occupant.affiliation or "none", nick=select(3, jid_split(occupant_jid)), role=role or "none"}) 1157 return nil, "cancel", "not-allowed";
1054 :tag("reason"):text(reason or ""):up() 1158 end
1055 :up(); 1159 end
1056 local presence_type = nil; 1160
1057 if not role then -- kick 1161 -- Need to be in the room and a moderator
1058 presence_type = "unavailable"; 1162 local actor_occupant = self:get_occupant_by_real_jid(actor);
1059 self._occupants[occupant_jid] = nil; 1163 if not actor_occupant or actor_occupant.role ~= "moderator" then
1060 for jid in pairs(occupant.sessions) do -- remove for all sessions of the nick 1164 return nil, "cancel", "not-allowed";
1061 self._jid_nick[jid] = nil; 1165 end
1062 end 1166 end
1167
1168 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
1169 if not role then
1063 x:tag("status", {code = "307"}):up(); 1170 x:tag("status", {code = "307"}):up();
1064 else 1171 end
1065 occupant.role = role; 1172 occupant.role = role;
1066 end 1173 self:save_occupant(occupant);
1067 local bp; 1174 self:publicise_occupant_status(occupant, x, nil, actor, reason);
1068 for jid,pres in pairs(occupant.sessions) do -- send to all sessions of the nick 1175 if role == nil then
1069 local p = st.clone(pres); 1176 module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
1070 p.attr.from = occupant_jid; 1177 end
1071 p.attr.type = presence_type; 1178 return true;
1072 p.attr.to = jid; 1179 end
1073 p:add_child(x); 1180
1074 self:_route_stanza(p); 1181 local affiliation_notify = module:require "muc/affiliation_notify";
1075 if occupant.jid == jid then 1182
1076 bp = p; 1183 local name = module:require "muc/name";
1077 end 1184 room_mt.get_name = name.get;
1078 end 1185 room_mt.set_name = name.set;
1079 if callback then callback(); end 1186
1080 if bp then 1187 local description = module:require "muc/description";
1081 self:broadcast_except_nick(bp, occupant_jid); 1188 room_mt.get_description = description.get;
1082 end 1189 room_mt.set_description = description.set;
1083 return true; 1190
1084 end 1191 local hidden = module:require "muc/hidden";
1085 1192 room_mt.get_hidden = hidden.get;
1086 function room_mt:_route_stanza(stanza) 1193 room_mt.set_hidden = hidden.set;
1087 local muc_child; 1194 function room_mt:get_public()
1088 local to_occupant = self._occupants[self._jid_nick[stanza.attr.to]]; 1195 return not self:get_hidden();
1089 local from_occupant = self._occupants[stanza.attr.from]; 1196 end
1090 if stanza.name == "presence" then 1197 function room_mt:set_public(public)
1091 if to_occupant and from_occupant then 1198 return self:set_hidden(not public);
1092 if self._data.whois == 'anyone' then 1199 end
1093 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); 1200
1094 else 1201 local password = module:require "muc/password";
1095 if to_occupant.role == "moderator" or jid_bare(to_occupant.jid) == jid_bare(from_occupant.jid) then 1202 room_mt.get_password = password.get;
1096 muc_child = stanza:get_child("x", "http://jabber.org/protocol/muc#user"); 1203 room_mt.set_password = password.set;
1097 end 1204
1098 end 1205 local whois = module:require "muc/whois";
1099 end 1206 room_mt.get_whois = whois.get;
1100 end 1207 room_mt.set_whois = whois.set;
1101 if muc_child then 1208
1102 for _, item in pairs(muc_child.tags) do 1209 local members_only = module:require "muc/members_only";
1103 if item.name == "item" then 1210 room_mt.get_members_only = members_only.get;
1104 if from_occupant == to_occupant then 1211 room_mt.set_members_only = members_only.set;
1105 item.attr.jid = stanza.attr.to; 1212
1106 else 1213 local moderated = module:require "muc/moderated";
1107 item.attr.jid = from_occupant.jid; 1214 room_mt.get_moderated = moderated.get;
1108 end 1215 room_mt.set_moderated = moderated.set;
1109 end 1216
1110 end 1217 local persistent = module:require "muc/persistent";
1111 end 1218 room_mt.get_persistent = persistent.get;
1112 self:route_stanza(stanza); 1219 room_mt.set_persistent = persistent.set;
1113 if muc_child then 1220
1114 for _, item in pairs(muc_child.tags) do 1221 local subject = module:require "muc/subject";
1115 if item.name == "item" then 1222 room_mt.get_changesubject = subject.get_changesubject;
1116 item.attr.jid = nil; 1223 room_mt.set_changesubject = subject.set_changesubject;
1117 end 1224 room_mt.get_subject = subject.get;
1118 end 1225 room_mt.set_subject = subject.set;
1119 end 1226 room_mt.send_subject = subject.send;
1120 end 1227
1228 local history = module:require "muc/history";
1229 room_mt.send_history = history.send;
1230 room_mt.get_historylength = history.get_length;
1231 room_mt.set_historylength = history.set_length;
1121 1232
1122 local _M = {}; -- module "muc" 1233 local _M = {}; -- module "muc"
1123 1234
1124 function _M.new_room(jid, config) 1235 function _M.new_room(jid, config)
1125 return setmetatable({ 1236 return setmetatable({
1126 jid = jid; 1237 jid = jid;
1127 _jid_nick = {}; 1238 _jid_nick = {};
1128 _occupants = {}; 1239 _occupants = {};
1129 _data = { 1240 _data = {
1130 whois = 'moderators';
1131 history_length = math.min((config and config.history_length)
1132 or default_history_length, max_history_length);
1133 }; 1241 };
1134 _affiliations = {}; 1242 _affiliations = {};
1135 }, room_mt); 1243 }, room_mt);
1136 end 1244 end
1137 1245
1138 function _M.set_max_history_length(_max_history_length)
1139 max_history_length = _max_history_length or math.huge;
1140 end
1141
1142 _M.room_mt = room_mt; 1246 _M.room_mt = room_mt;
1143 1247
1144 return _M; 1248 return _M;