end
end
+function room_mt:lock()
+ self.locked = true
+end
+function room_mt:unlock()
+ module:fire_event("muc-room-unlocked", { room = self });
+ self.locked = nil
+end
+function room_mt:is_locked()
+ return not not self.locked
+end
+
+function room_mt:route_to_occupant(o_data, stanza)
+ local to = stanza.attr.to;
+ for jid in pairs(o_data.sessions) do
+ stanza.attr.to = jid;
+ self:_route_stanza(stanza);
+ end
+ stanza.attr.to = to;
+end
+
function room_mt:broadcast_presence(stanza, sid, code, nick)
stanza = get_filtered_presence(stanza);
local occupant = self._occupants[stanza.attr.from];
stanza:tag("status", {code=code}):up();
end
self:broadcast_except_nick(stanza, stanza.attr.from);
- local me = self._occupants[stanza.attr.from];
- if me then
- stanza:tag("status", {code='110'}):up();
- stanza.attr.to = sid;
- self:_route_stanza(stanza);
- end
+ stanza:tag("status", {code='110'}):up();
+ stanza.attr.to = sid;
+ self:_route_stanza(stanza);
end
function room_mt:broadcast_message(stanza, historic)
- local to = stanza.attr.to;
- for occupant, o_data in pairs(self._occupants) do
- for jid in pairs(o_data.sessions) do
- stanza.attr.to = jid;
- self:_route_stanza(stanza);
- end
+ module:fire_event("muc-broadcast-message", {room = self, stanza = stanza, historic = historic});
+ self:broadcast(stanza);
+end
+
+-- add to history
+module:hook("muc-broadcast-message", function(event)
+ if event.historic then
+ local room = event.room
+ local history = room._data['history'];
+ if not history then history = {}; room._data['history'] = history; end
+ local stanza = st.clone(event.stanza);
+ stanza.attr.to = "";
+ local ts = gettime();
+ local stamp = datetime.datetime(ts);
+ stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = module.host, stamp = stamp}):up(); -- XEP-0203
+ stanza:tag("x", {xmlns = "jabber:x:delay", from = module.host, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
+ local entry = { stanza = stanza, timestamp = ts };
+ t_insert(history, entry);
+ while #history > room:get_historylength() do t_remove(history, 1) end
end
- stanza.attr.to = to;
- if historic then -- add to history
- return self:save_to_history(stanza)
- end
-end
-function room_mt:save_to_history(stanza)
- local history = self._data['history'];
- if not history then history = {}; self._data['history'] = history; end
- stanza = st.clone(stanza);
- stanza.attr.to = "";
- local stamp = datetime.datetime();
- stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = module.host, stamp = stamp}):up(); -- XEP-0203
- stanza:tag("x", {xmlns = "jabber:x:delay", from = module.host, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
- local entry = { stanza = stanza, stamp = stamp };
- t_insert(history, entry);
- while #history > (self._data.history_length or default_history_length) do t_remove(history, 1) end
-end
+end)
+
function room_mt:broadcast_except_nick(stanza, nick)
- for rnick, occupant in pairs(self._occupants) do
- if rnick ~= nick then
- for jid in pairs(occupant.sessions) do
- stanza.attr.to = jid;
- self:_route_stanza(stanza);
- end
+ return self:broadcast(stanza, function(rnick, occupant) return rnick ~= nick end)
+end
+
+-- Broadcast a stanza to all occupants in the room.
+-- optionally checks conditional called with nicl
+function room_mt:broadcast(stanza, cond_func)
+ for nick, occupant in pairs(self._occupants) do
+ if cond_func == nil or cond_func(nick, occupant) then
+ self:route_to_occupant(occupant, stanza)
end
end
end
return maxchars, maxstanzas, since
end
--- Get history for 'to'
-function room_mt:get_history(to, maxchars, maxstanzas, since)
- local history = self._data['history']; -- send discussion history
- if not history then return function() end end
+
+module:hook("muc-get-history", function(event)
+ local room = event.room
+ local history = room._data['history']; -- send discussion history
+ if not history then return nil end
local history_len = #history
- maxstanzas = maxstanzas or history_len
+ local to = event.to
+ local maxchars = event.maxchars
+ local maxstanzas = event.maxstanzas or history_len
+ local since = event.since
local n = 0;
local charcount = 0;
for i=history_len,1,-1 do
charcount = charcount + entry.chars + #to;
if charcount > maxchars then break; end
end
- if since and since > entry.stamp then break; end
+ if since and since > entry.timestamp then break; end
if n + 1 > maxstanzas then break; end
n = n + 1;
end
local i = history_len-n+1
- return function()
+ function event:next_stanza()
if i > history_len then return nil end
local entry = history[i]
local msg = entry.stanza
i = i + 1
return msg
end
-end
-function room_mt:send_history(to, stanza)
+ return true;
+end)
+
+function room_mt:send_history(stanza)
local maxchars, maxstanzas, since = parse_history(stanza)
- for msg in self:get_history(to, maxchars, maxstanzas, since) do
+ local event = {
+ room = self;
+ to = stanza.attr.from; -- `to` is required to calculate the character count for `maxchars`
+ maxchars = maxchars, maxstanzas = maxstanzas, since = since;
+ next_stanza = function() end; -- events should define this iterator
+ }
+ module:fire_event("muc-get-history", event)
+ for msg in event.next_stanza , event do
self:_route_stanza(msg);
end
end
-function room_mt:send_subject(to)
- if self._data['subject'] then
- self:_route_stanza(st.message({type='groupchat', from=self._data['subject_from'] or self.jid, to=to}):tag("subject"):text(self._data['subject']));
- end
-end
function room_mt:get_disco_info(stanza)
local count = 0; for _ in pairs(self._occupants) do count = count + 1; end
end
return reply;
end
+
+function room_mt:get_subject()
+ return self._data['subject'], self._data['subject_from']
+end
+local function create_subject_message(subject)
+ return st.message({type='groupchat'})
+ :tag('subject'):text(subject):up();
+end
+function room_mt:send_subject(to)
+ local from, subject = self:get_subject()
+ if subject then
+ local msg = create_subject_message(subject)
+ msg.attr.from = from
+ msg.attr.to = to
+ self:_route_stanza(msg);
+ end
+end
function room_mt:set_subject(current_nick, subject)
if subject == "" then subject = nil; end
self._data['subject'] = subject;
self._data['subject_from'] = current_nick;
if self.save then self:save(); end
- local msg = st.message({type='groupchat', from=current_nick})
- :tag('subject'):text(subject):up();
+ local msg = create_subject_message(subject)
+ msg.attr.from = current_nick
self:broadcast_message(msg, false);
return true;
end
end
end
-function room_mt:handle_join(origin, stanza)
+module:hook("muc-occupant-pre-join", function(event)
+ return module:fire_event("muc-occupant-pre-join/affiliation", event)
+ or module:fire_event("muc-occupant-pre-join/password", event)
+ or module:fire_event("muc-occupant-pre-join/locked", event)
+ or module:fire_event("muc-occupant-pre-join/nick-conflict", event)
+end, -1)
+
+module:hook("muc-occupant-pre-join/password", function(event)
+ local room, stanza = event.room, event.stanza;
local from, to = stanza.attr.from, stanza.attr.to;
- log("debug", "%s joining as %s", from, to);
- if not next(self._affiliations) then -- new room, no owners
- self._affiliations[jid_bare(from)] = "owner";
- if self.locked and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
- self.locked = nil; -- Older groupchat protocol doesn't lock
- end
- elseif self.locked then -- Deny entry
- origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
+ password = password and password:get_child_text("password", "http://jabber.org/protocol/muc");
+ if not password or password == "" then password = nil; end
+ if room:get_password() ~= password then
+ local from, to = stanza.attr.from, stanza.attr.to;
+ log("debug", "%s couldn't join due to invalid password: %s", from, to);
+ local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
+ reply.tags[1].attr.code = "401";
+ event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ return true;
+ end
+end, -1)
+
+module:hook("muc-occupant-pre-join/nick-conflict", function(event)
+ local room, stanza = event.room, event.stanza;
+ local from, to = stanza.attr.from, stanza.attr.to;
+ local occupant = room._occupants[to]
+ if occupant -- occupant already exists
+ and jid_bare(from) ~= jid_bare(occupant.jid) then -- and has different bare real jid
+ log("debug", "%s couldn't join due to nick conflict: %s", from, to);
+ local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ reply.tags[1].attr.code = "409";
+ event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ return true;
+ end
+end, -1)
+
+module:hook("muc-occupant-pre-join/locked", function(event)
+ if event.room:is_locked() then -- Deny entry
+ event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found"));
return true;
end
+end, -1)
+
+function room_mt:handle_join(origin, stanza)
+ local from, to = stanza.attr.from, stanza.attr.to;
local affiliation = self:get_affiliation(from);
+ if affiliation == nil and next(self._affiliations) == nil then -- new room, no owners
+ affiliation = "owner";
+ self._affiliations[jid_bare(from)] = affiliation;
+ if self:is_locked() and not stanza:get_child("x", "http://jabber.org/protocol/muc") then
+ self:unlock(); -- Older groupchat protocol doesn't lock
+ end
+ end
+ if module:fire_event("muc-occupant-pre-join", {
+ room = self;
+ origin = origin;
+ stanza = stanza;
+ affiliation = affiliation;
+ }) then return true; end
+ log("debug", "%s joining as %s", from, to);
+
local role = self:get_default_role(affiliation)
if role then -- new occupant
local is_merge = not not self._occupants[to]
if self:get_whois() == 'anyone' then
pr:tag("status", {code='100'}):up();
end
- if self.locked then
+ if self:is_locked() then
pr:tag("status", {code='201'}):up();
end
pr.attr.to = from;
self:send_history(from, stanza);
self:send_subject(from);
return true;
- elseif not affiliation then -- registration required for entering members-only room
- local reply = st.error_reply(stanza, "auth", "registration-required"):up();
+ end
+end
+
+-- registration required for entering members-only room
+module:hook("muc-occupant-pre-join/affiliation", function(event)
+ if event.affiliation == nil and event.room:get_members_only() then
+ local reply = st.error_reply(event.stanza, "auth", "registration-required"):up();
reply.tags[1].attr.code = "407";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
- else -- banned
- local reply = st.error_reply(stanza, "auth", "forbidden"):up();
+ end
+end, -1)
+
+-- banned
+module:hook("muc-occupant-pre-join/affiliation", function(event)
+ if event.affiliation == "outcast" then
+ local reply = st.error_reply(event.stanza, "auth", "forbidden"):up();
reply.tags[1].attr.code = "403";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
+ event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
end
-end
+end, -1)
function room_mt:handle_available_to_occupant(origin, stanza)
local from, to = stanza.attr.from, stanza.attr.to;
-- self:handle_to_occupant(origin, stanza); -- resend available
--end
else -- enter room
- local new_nick = to;
- if self._occupants[to] then
- if jid_bare(from) ~= jid_bare(self._occupants[to].jid) then
- new_nick = nil;
- end
- end
- local password = stanza:get_child("x", "http://jabber.org/protocol/muc");
- password = password and password:get_child("password", "http://jabber.org/protocol/muc");
- password = password and password[1] ~= "" and password[1];
- if self:get_password() and self:get_password() ~= password then
- log("debug", "%s couldn't join due to invalid password: %s", from, to);
- local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
- reply.tags[1].attr.code = "401";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
- return true;
- elseif not new_nick then
- log("debug", "%s couldn't join due to nick conflict: %s", from, to);
- local reply = st.error_reply(stanza, "cancel", "conflict"):up();
- reply.tags[1].attr.code = "409";
- origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
- return true;
- else
- return self:handle_join(origin, stanza)
- end
+ return self:handle_join(origin, stanza)
end
end
log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid);
stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
stanza.attr.from = current_nick;
- for jid in pairs(o_data.sessions) do
- stanza.attr.to = jid;
- self:_route_stanza(stanza);
- end
- stanza.attr.from, stanza.attr.to = from, to;
+ self:route_to_occupant(o_data, stanza)
+ stanza.attr.from = from;
return true;
end
handle_option("password", "muc#roomconfig_roomsecret");
if self.save then self:save(true); end
- if self.locked then
- module:fire_event("muc-room-unlocked", { room = self });
- self.locked = nil;
+ if self:is_locked() then
+ self:unlock();
end
origin.send(st.reply(stanza));
return true;
end
-function room_mt:handle_admin_item_set_command(origin, stanza)
+function room_mt:handle_admin_query_set_command(origin, stanza)
local item = stanza.tags[1].tags[1];
if item.attr.jid then -- Validate provided JID
item.attr.jid = jid_prep(item.attr.jid);
end
end
-function room_mt:handle_admin_item_get_command(origin, stanza)
+function room_mt:handle_admin_query_get_command(origin, stanza)
local actor = stanza.attr.from;
local affiliation = self:get_affiliation(actor);
- local current_nick = self:get_occupant_jid(actor);
- local role = current_nick and self._occupants[current_nick].role or self:get_default_role(affiliation);
local item = stanza.tags[1].tags[1];
local _aff = item.attr.affiliation;
local _rol = item.attr.role;
return true;
end
elseif _rol and not _aff then
+ local role = self:get_role(self:get_occupant_jid(actor)) or self:get_default_role(affiliation);
if role == "moderator" then
- -- TODO allow admins and owners not in room? Provide read-only access to everyone who can see the participants anyway?
if _rol == "none" then _rol = nil; end
local reply = st.reply(stanza):query("http://jabber.org/protocol/muc#admin");
for occupant_jid, occupant in pairs(self._occupants) do
local _from, _to = stanza.attr.from, stanza.attr.to;
local current_nick = self:get_occupant_jid(_from)
-- Need visitor role or higher to invite
- if not self._occupants[current_nick].role then
+ if not self:get_role(current_nick) or not self:get_default_role(self:get_affiliation(_from)) then
origin.send(st.error_reply(stanza, "auth", "forbidden"));
return true;
end
local _invitee = jid_prep(payload.attr.to);
if _invitee then
+ if self:get_whois() == "moderators" then
+ _from = current_nick;
+ end
local _reason = payload:get_child_text("reason")
local invite = st.message({from = _to, to = _invitee, id = stanza.attr.id})
:tag('x', {xmlns='http://jabber.org/protocol/muc#user'})
:tag('body') -- Add a plain message for clients which don't support invites
:text(_from..' invited you to the room '.._to..(_reason and (' ('.._reason..')') or ""))
:up();
- module:fire_event("muc-invite-prepared", { room = self, stanza = invite })
- self:_route_stanza(invite);
+ module:fire_event("muc-invite", { room = self, stanza = invite, origin = origin, incoming = stanza });
return true;
else
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
end
end
+module:hook("muc-invite", function(event)
+ event.room:_route_stanza(event.stanza);
+ return true;
+end, -1)
+
-- When an invite is sent; add an affiliation for the invitee
-module:hook("muc-invite-prepared", function(event)
+module:hook("muc-invite", function(event)
local room, stanza = event.room, event.stanza
local invitee = stanza.attr.to
if room:get_members_only() and not room:get_affiliation(invitee) then
:tag('body') -- Add a plain message for clients which don't support declines
:text(from..' declined your invite to the room '..to..(reason and (' ('..reason..')') or ""))
:up();
- self:_route_stanza(decline);
+ module:fire_event("muc-decline", { room = self, stanza = decline, origin = origin, incoming = stanza });
return true;
else
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
end
end
+module:hook("muc-decline", function(event)
+ local room, stanza = event.room, event.stanza
+ local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
+ if occupant then
+ room:route_to_occupant(occupant, stanza)
+ else
+ room:route_stanza(stanza);
+ end
+ return true;
+end, -1)
+
function room_mt:handle_message_to_room(origin, stanza)
local type = stanza.attr.type;
if type == "groupchat" then
if actor_jid == true then return true; end
local actor = self._occupants[self:get_occupant_jid(actor_jid)];
- if actor.role == "moderator" then
+ if actor and actor.role == "moderator" then
if occupant.affiliation ~= "owner" and occupant.affiliation ~= "admin" then
if actor.affiliation == "owner" or actor.affiliation == "admin" then
return true;
function _M.new_room(jid, config)
return setmetatable({
jid = jid;
+ locked = nil;
_jid_nick = {};
_occupants = {};
_data = {