aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/muc
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/muc')
-rw-r--r--plugins/muc/hats.lib.lua26
-rw-r--r--plugins/muc/history.lib.lua35
-rw-r--r--plugins/muc/language.lib.lua1
-rw-r--r--plugins/muc/lock.lib.lua2
-rw-r--r--plugins/muc/members_only.lib.lua4
-rw-r--r--plugins/muc/mod_muc.lua96
-rw-r--r--plugins/muc/muc.lib.lua345
-rw-r--r--plugins/muc/name.lib.lua4
-rw-r--r--plugins/muc/occupant_id.lib.lua70
-rw-r--r--plugins/muc/password.lib.lua2
-rw-r--r--plugins/muc/presence_broadcast.lib.lua83
-rw-r--r--plugins/muc/register.lib.lua50
-rw-r--r--plugins/muc/subject.lib.lua6
-rw-r--r--plugins/muc/util.lib.lua18
14 files changed, 618 insertions, 124 deletions
diff --git a/plugins/muc/hats.lib.lua b/plugins/muc/hats.lib.lua
new file mode 100644
index 00000000..358e5100
--- /dev/null
+++ b/plugins/muc/hats.lib.lua
@@ -0,0 +1,26 @@
+local st = require "util.stanza";
+local muc_util = module:require "muc/util";
+
+local xmlns_hats = "xmpp:prosody.im/protocol/hats:1";
+
+-- Strip any hats claimed by the client (to prevent spoofing)
+muc_util.add_filtered_namespace(xmlns_hats);
+
+
+module:hook("muc-build-occupant-presence", function (event)
+ local bare_jid = event.occupant and event.occupant.bare_jid or event.bare_jid;
+ local aff_data = event.room:get_affiliation_data(bare_jid);
+ local hats = aff_data and aff_data.hats;
+ if not hats then return; end
+ local hats_el;
+ for hat_id, hat_data in pairs(hats) do
+ if hat_data.active then
+ if not hats_el then
+ hats_el = st.stanza("hats", { xmlns = xmlns_hats });
+ end
+ hats_el:tag("hat", { uri = hat_id, title = hat_data.title }):up();
+ end
+ end
+ if not hats_el then return; end
+ event.stanza:add_direct_child(hats_el);
+end);
diff --git a/plugins/muc/history.lib.lua b/plugins/muc/history.lib.lua
index 0d69c97d..075b1890 100644
--- a/plugins/muc/history.lib.lua
+++ b/plugins/muc/history.lib.lua
@@ -48,16 +48,18 @@ module:hook("muc-config-form", function(event)
table.insert(event.form, {
name = "muc#roomconfig_historylength";
type = "text-single";
+ datatype = "xs:integer";
label = "Maximum number of history messages returned by room";
desc = "Specify the maximum number of previous messages that should be sent to users when they join the room";
- value = tostring(get_historylength(event.room));
+ value = get_historylength(event.room);
});
table.insert(event.form, {
name = 'muc#roomconfig_defaulthistorymessages',
type = 'text-single',
+ datatype = "xs:integer";
label = 'Default number of history messages returned by room',
desc = "Specify the number of previous messages sent to new users when they join the room";
- value = tostring(get_defaulthistorymessages(event.room))
+ value = get_defaulthistorymessages(event.room);
});
end, 70-5);
@@ -171,6 +173,10 @@ end, 50); -- Before subject(20)
-- add to history
module:hook("muc-add-history", function(event)
local room = event.room
+ if get_historylength(room) == 0 then
+ room._history = nil;
+ return;
+ end
local history = room._history;
if not history then history = {}; room._history = history; end
local stanza = st.clone(event.stanza);
@@ -180,9 +186,6 @@ module:hook("muc-add-history", function(event)
stanza:tag("delay", { -- XEP-0203
xmlns = "urn:xmpp:delay", from = room.jid, stamp = stamp
}):up();
- stanza:tag("x", { -- XEP-0091 (deprecated)
- xmlns = "jabber:x:delay", from = room.jid, stamp = datetime.legacy()
- }):up();
local entry = { stanza = stanza, timestamp = ts };
table.insert(history, entry);
while #history > get_historylength(room) do table.remove(history, 1) end
@@ -198,7 +201,27 @@ module:hook("muc-broadcast-message", function(event)
end);
module:hook("muc-message-is-historic", function (event)
- return event.stanza:get_child("body");
+ local stanza = event.stanza;
+ if stanza:get_child("no-store", "urn:xmpp:hints")
+ or stanza:get_child("no-permanent-store", "urn:xmpp:hints") then
+ -- XXX Experimental XEP
+ return false, "hint";
+ end
+ if stanza:get_child("store", "urn:xmpp:hints") then
+ return true, "hint";
+ end
+ if stanza:get_child("body") then
+ return true;
+ end
+ if stanza:get_child("encryption", "urn:xmpp:eme:0") then
+ -- Since we can't know what an encrypted message contains, we assume it's important
+ -- XXX Experimental XEP
+ return true, "encrypted";
+ end
+ if stanza:get_child(nil, "urn:xmpp:chat-markers:0") then
+ -- XXX Experimental XEP
+ return true, "marker";
+ end
end, -1);
return {
diff --git a/plugins/muc/language.lib.lua b/plugins/muc/language.lib.lua
index ee80806b..2ee2ba0f 100644
--- a/plugins/muc/language.lib.lua
+++ b/plugins/muc/language.lib.lua
@@ -32,6 +32,7 @@ local function add_form_option(event)
label = "Language tag for room (e.g. 'en', 'de', 'fr' etc.)";
type = "text-single";
desc = "Indicate the primary language spoken in this room";
+ datatype = "xs:language";
value = get_language(event.room) or "";
});
end
diff --git a/plugins/muc/lock.lib.lua b/plugins/muc/lock.lib.lua
index 062ab615..32f2647b 100644
--- a/plugins/muc/lock.lib.lua
+++ b/plugins/muc/lock.lib.lua
@@ -43,7 +43,7 @@ end
module:hook("muc-occupant-pre-join", function(event)
if not event.is_new_room and is_locked(event.room) then -- Deny entry
module:log("debug", "Room is locked, denying entry");
- event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found"));
+ event.origin.send(st.error_reply(event.stanza, "cancel", "item-not-found", nil, module.host));
return true;
end
end, -30);
diff --git a/plugins/muc/members_only.lib.lua b/plugins/muc/members_only.lib.lua
index 3220225f..6a2543e1 100644
--- a/plugins/muc/members_only.lib.lua
+++ b/plugins/muc/members_only.lib.lua
@@ -121,7 +121,7 @@ module:hook("muc-occupant-pre-join", function(event)
local stanza = event.stanza;
local affiliation = room:get_affiliation(stanza.attr.from);
if valid_affiliations[affiliation or "none"] <= valid_affiliations.none then
- local reply = st.error_reply(stanza, "auth", "registration-required"):up();
+ local reply = st.error_reply(stanza, "auth", "registration-required", nil, room.jid):up();
reply.tags[1].attr.code = "407";
event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
@@ -139,7 +139,7 @@ module:hook("muc-pre-invite", function(event)
local inviter_affiliation = room:get_affiliation(stanza.attr.from) or "none";
local required_affiliation = room._data.allow_member_invites and "member" or "admin";
if valid_affiliations[inviter_affiliation] < valid_affiliations[required_affiliation] then
- event.origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ event.origin.send(st.error_reply(stanza, "auth", "forbidden", nil, room.jid));
return true;
end
end
diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua
index b9c7c859..04846359 100644
--- a/plugins/muc/mod_muc.lua
+++ b/plugins/muc/mod_muc.lua
@@ -86,7 +86,16 @@ room_mt.get_registered_nick = register.get_registered_nick;
room_mt.get_registered_jid = register.get_registered_jid;
room_mt.handle_register_iq = register.handle_register_iq;
+local presence_broadcast = module:require "muc/presence_broadcast";
+room_mt.get_presence_broadcast = presence_broadcast.get;
+room_mt.set_presence_broadcast = presence_broadcast.set;
+room_mt.get_valid_broadcast_roles = presence_broadcast.get_valid_broadcast_roles;
+
+local occupant_id = module:require "muc/occupant_id";
+room_mt.get_occupant_id = occupant_id.get_occupant_id;
+
local jid_split = require "util.jid".split;
+local jid_prep = require "util.jid".prep;
local jid_bare = require "util.jid".bare;
local st = require "util.stanza";
local cache = require "util.cache";
@@ -98,6 +107,7 @@ module:depends("disco");
module:add_identity("conference", "text", module:get_option_string("name", "Prosody Chatrooms"));
module:add_feature("http://jabber.org/protocol/muc");
module:depends "muc_unique"
+module:require "muc/hats";
module:require "muc/lock";
local function is_admin(jid)
@@ -129,7 +139,12 @@ local room_items_cache = {};
local function room_save(room, forced, savestate)
local node = jid_split(room.jid);
local is_persistent = persistent.get(room);
- room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
+ if room:get_public() then
+ room_items_cache[room.jid] = room:get_name() or "";
+ else
+ room_items_cache[room.jid] = nil;
+ end
+
if is_persistent or savestate then
persistent_rooms:set(nil, room.jid, true);
local data, state = room:freeze(savestate);
@@ -155,7 +170,11 @@ local rooms = cache.new(max_rooms or max_live_rooms, function (jid, room)
end
module:log("debug", "Evicting room %s", jid);
room_eviction();
- room_items_cache[room.jid] = room:get_public() and room:get_name() or nil;
+ if room:get_public() then
+ room_items_cache[room.jid] = room:get_name() or "";
+ else
+ room_items_cache[room.jid] = nil;
+ end
local ok, err = room_save(room, nil, true); -- Force to disk
if not ok then
module:log("error", "Failed to swap inactive room %s to disk: %s", jid, err);
@@ -163,6 +182,11 @@ local rooms = cache.new(max_rooms or max_live_rooms, function (jid, room)
end
end);
+local measure_rooms_size = module:measure("live_room", "amount");
+module:hook_global("stats-update", function ()
+ measure_rooms_size(rooms:count());
+end);
+
-- Automatically destroy empty non-persistent rooms
module:hook("muc-occupant-left",function(event)
local room = event.room
@@ -185,7 +209,7 @@ end
local function handle_broken_room(room, origin, stanza)
module:log("debug", "Returning error from broken room %s", room.jid);
- origin.send(st.error_reply(stanza, "wait", "internal-server-error"));
+ origin.send(st.error_reply(stanza, "wait", "internal-server-error", nil, room.jid));
return true;
end
@@ -264,9 +288,13 @@ local function set_room_defaults(room, lang)
room:set_changesubject(module:get_option_boolean("muc_room_default_change_subject", room:get_changesubject()));
room:set_historylength(module:get_option_number("muc_room_default_history_length", room:get_historylength()));
room:set_language(lang or module:get_option_string("muc_room_default_language"));
+ room:set_presence_broadcast(module:get_option("muc_room_default_presence_broadcast", room:get_presence_broadcast()));
end
function create_room(room_jid, config)
+ if jid_bare(room_jid) ~= room_jid or not jid_prep(room_jid, true) then
+ return nil, "invalid-jid";
+ end
local exists = get_room_from_jid(room_jid);
if exists then
return nil, "room-exists";
@@ -325,13 +353,14 @@ module:hook("host-disco-items", function(event)
module:log("debug", "host-disco-items called");
if next(room_items_cache) ~= nil then
for jid, room_name in pairs(room_items_cache) do
+ if room_name == "" then room_name = nil; end
reply:tag("item", { jid = jid, name = room_name }):up();
end
else
for room in all_rooms() do
if not room:get_hidden() then
local jid, room_name = room.jid, room:get_name();
- room_items_cache[jid] = room_name;
+ room_items_cache[jid] = room_name or "";
reply:tag("item", { jid = jid, name = room_name }):up();
end
end
@@ -345,7 +374,7 @@ end, 1);
module:hook("muc-room-pre-create", function(event)
local origin, stanza = event.origin, event.stanza;
if not track_room(event.room) then
- origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
+ origin.send(st.error_reply(stanza, "wait", "resource-constraint", nil, module.host));
return true;
end
end, -1000);
@@ -396,7 +425,7 @@ do
restrict_room_creation == "local" and
select(2, jid_split(user_jid)) == host_suffix
) then
- origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted"));
+ origin.send(st.error_reply(stanza, "cancel", "not-allowed", "Room creation is restricted", module.host));
return true;
end
end);
@@ -441,7 +470,7 @@ for event_name, method in pairs {
room = nil;
else
if stanza.attr.type ~= "error" then
- local reply = st.error_reply(stanza, "cancel", "gone", room._data.reason)
+ local reply = st.error_reply(stanza, "cancel", "gone", room._data.reason, module.host)
if room._data.newjid then
local uri = "xmpp:"..room._data.newjid.."?join";
reply:get_child("error"):child_with_name("gone"):text(uri);
@@ -454,17 +483,21 @@ for event_name, method in pairs {
if room == nil then
-- Watch presence to create rooms
- if stanza.attr.type == nil and stanza.name == "presence" then
+ if not jid_prep(room_jid, true) then
+ origin.send(st.error_reply(stanza, "modify", "jid-malformed", nil, module.host));
+ return true;
+ end
+ if stanza.attr.type == nil and stanza.name == "presence" and stanza:get_child("x", "http://jabber.org/protocol/muc") then
room = muclib.new_room(room_jid);
return room:handle_first_presence(origin, stanza);
elseif stanza.attr.type ~= "error" then
- origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", nil, module.host));
return true;
else
return;
end
elseif room == false then -- Error loading room
- origin.send(st.error_reply(stanza, "wait", "resource-constraint"));
+ origin.send(st.error_reply(stanza, "wait", "resource-constraint", nil, module.host));
return true;
end
return room[method](room, origin, stanza);
@@ -483,6 +516,7 @@ do -- Ad-hoc commands
local t_concat = table.concat;
local adhoc_new = module:require "adhoc".new;
local adhoc_initial = require "util.adhoc".new_initial_data_form;
+ local adhoc_simple = require "util.adhoc".new_simple_form;
local array = require "util.array";
local dataforms_new = require "util.dataforms".new;
@@ -513,4 +547,46 @@ do -- Ad-hoc commands
"http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
module:provides("adhoc", destroy_rooms_desc);
+
+
+ local set_affiliation_layout = dataforms_new {
+ -- FIXME wordsmith title, instructions, labels etc
+ title = "Set affiliation";
+
+ { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#set-affiliation" };
+ { name = "room", type = "jid-single", required = true, label = "Room"};
+ { name = "jid", type = "jid-single", required = true, label = "JID"};
+ { name = "affiliation", type = "list-single", required = true, label = "Affiliation",
+ options = { "owner"; "admin"; "member"; "none"; "outcast"; },
+ };
+ { name = "reason", type = "text-single", "Reason", }
+ };
+
+ local set_affiliation_handler = adhoc_simple(set_affiliation_layout, function (fields, errors)
+ if errors then
+ local errmsg = {};
+ for field, err in pairs(errors) do
+ errmsg[#errmsg + 1] = field .. ": " .. err;
+ end
+ return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
+ end
+
+ local room = get_room_from_jid(fields.room);
+ if not room then
+ return { status = "canceled", error = { message = "No such room"; }; };
+ end
+ local ok, err, condition = room:set_affiliation(true, fields.jid, fields.affiliation, fields.reason);
+
+ if not ok then
+ return { status = "canceled", error = { message = "Affiliation change failed: "..err..":"..condition; }; };
+ end
+
+ return { status = "completed", info = "Affiliation updated",
+ };
+ end);
+
+ local set_affiliation_desc = adhoc_new("Set affiliation in room",
+ "http://prosody.im/protocol/muc#set-affiliation", set_affiliation_handler, "admin");
+
+ module:provides("adhoc", set_affiliation_desc);
end
diff --git a/plugins/muc/muc.lib.lua b/plugins/muc/muc.lib.lua
index f037c4f6..f4d06529 100644
--- a/plugins/muc/muc.lib.lua
+++ b/plugins/muc/muc.lib.lua
@@ -23,6 +23,7 @@ local resourceprep = require "util.encodings".stringprep.resourceprep;
local st = require "util.stanza";
local base64 = require "util.encodings".base64;
local md5 = require "util.hashes".md5;
+local new_id = require "util.id".medium;
local log = module._log;
@@ -39,7 +40,7 @@ function room_mt:__tostring()
end
function room_mt.save()
- -- overriden by mod_muc.lua
+ -- overridden by mod_muc.lua
end
function room_mt:get_occupant_jid(real_jid)
@@ -215,15 +216,16 @@ local function can_see_real_jids(whois, occupant)
end
end
+
-- Broadcasts an occupant's presence to the whole room
-- Takes the x element that goes into the stanzas
-function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
+function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason, prev_role, force_unavailable, recipient)
local base_x = x.base or x;
-- Build real jid and (optionally) occupant jid template presences
local base_presence do
-- Try to use main jid's presence
local pr = occupant:get_presence();
- if pr and (occupant.role ~= nil or pr.attr.type == "unavailable") then
+ if pr and (occupant.role ~= nil or pr.attr.type == "unavailable") and not force_unavailable then
base_presence = st.clone(pr);
else -- user is leaving but didn't send a leave presence. make one for them
base_presence = st.presence {from = occupant.nick; type = "unavailable";};
@@ -236,7 +238,10 @@ function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
occupant = occupant; nick = nick; actor = actor;
reason = reason;
}
- module:fire_event("muc-broadcast-presence", event);
+ module:fire_event("muc-build-occupant-presence", event);
+ if not recipient then
+ module:fire_event("muc-broadcast-presence", event);
+ end
-- Allow muc-broadcast-presence listeners to change things
nick = event.nick;
@@ -279,18 +284,34 @@ function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
self_p = st.clone(base_presence):add_child(self_x);
end
- -- General populance
+ local function get_p(rec_occupant)
+ local pr;
+ if can_see_real_jids(whois, rec_occupant) then
+ pr = get_full_p();
+ elseif occupant.bare_jid == rec_occupant.bare_jid then
+ pr = self_p;
+ else
+ pr = get_anon_p();
+ end
+ return pr
+ end
+
+ if recipient then
+ return self:route_to_occupant(recipient, get_p(recipient));
+ end
+
+ local broadcast_roles = self:get_presence_broadcast();
+ -- General populace
for occupant_nick, n_occupant in self:each_occupant() do
if occupant_nick ~= occupant.nick then
- local pr;
- if can_see_real_jids(whois, n_occupant) then
- pr = get_full_p();
- elseif occupant.bare_jid == n_occupant.bare_jid then
- pr = self_p;
- else
- pr = get_anon_p();
+ local pr = get_p(n_occupant);
+ if broadcast_roles[occupant.role or "none"] or force_unavailable then
+ self:route_to_occupant(n_occupant, pr);
+ elseif prev_role and broadcast_roles[prev_role] then
+ pr.attr.type = 'unavailable';
+ self:route_to_occupant(n_occupant, pr);
end
- self:route_to_occupant(n_occupant, pr);
+
end
end
@@ -303,6 +324,7 @@ function room_mt:publicise_occupant_status(occupant, x, nick, actor, reason)
-- use their own presences as templates
for full_jid, pr in occupant:each_session() do
pr = st.clone(pr);
+ module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pr });
pr.attr.to = full_jid;
pr:add_child(self_x);
self:route_stanza(pr);
@@ -312,25 +334,40 @@ end
function room_mt:send_occupant_list(to, filter)
local to_bare = jid_bare(to);
- local is_anonymous = false;
- local whois = self:get_whois();
- if whois ~= "anyone" then
- local affiliation = self:get_affiliation(to);
- if affiliation ~= "admin" and affiliation ~= "owner" then
- local occupant = self:get_occupant_by_real_jid(to);
- if not (occupant and can_see_real_jids(whois, occupant)) then
- is_anonymous = true;
- end
- end
- end
+ local broadcast_roles = self:get_presence_broadcast();
+ local is_anonymous = self:is_anonymous_for(to);
+ local broadcast_bare_jids = {}; -- Track which bare JIDs we have sent presence for
for occupant_jid, occupant in self:each_occupant() do
+ broadcast_bare_jids[occupant.bare_jid] = true;
if filter == nil or filter(occupant_jid, occupant) then
local x = st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
self:build_item_list(occupant, x, is_anonymous and to_bare ~= occupant.bare_jid); -- can always see your own jids
local pres = st.clone(occupant:get_presence());
pres.attr.to = to;
pres:add_child(x);
- self:route_stanza(pres);
+ module:fire_event("muc-build-occupant-presence", { room = self, occupant = occupant, stanza = pres });
+ if to_bare == occupant.bare_jid or broadcast_roles[occupant.role or "none"] then
+ self:route_stanza(pres);
+ end
+ end
+ end
+ if broadcast_roles.none then
+ -- Broadcast stanzas for affiliated users not currently in the MUC
+ for affiliated_jid, affiliation, affiliation_data in self:each_affiliation() do
+ local nick = affiliation_data and affiliation_data.reserved_nickname;
+ if (nick or not is_anonymous) and not broadcast_bare_jids[affiliated_jid]
+ and (filter == nil or filter(affiliated_jid, nil)) then
+ local from = nick and (self.jid.."/"..nick) or self.jid;
+ local pres = st.presence({ to = to, from = from, type = "unavailable" })
+ :tag("x", { xmlns = 'http://jabber.org/protocol/muc#user' })
+ :tag("item", {
+ affiliation = affiliation;
+ role = "none";
+ nick = nick;
+ jid = not is_anonymous and affiliated_jid or nil }):up()
+ :up();
+ self:route_stanza(pres);
+ end
end
end
end
@@ -373,13 +410,14 @@ function room_mt:handle_kickable(origin, stanza) -- luacheck: ignore 212
local real_jid = stanza.attr.from;
local occupant = self:get_occupant_by_real_jid(real_jid);
if occupant == nil then return nil; end
- local type, condition, text = stanza:get_error();
+ local _, condition, text = stanza:get_error();
local error_message = "Kicked: "..(condition and condition:gsub("%-", " ") or "presence error");
if text and self:get_whois() == "anyone" then
error_message = error_message..": "..text;
end
occupant:set_session(real_jid, st.presence({type="unavailable"})
:tag('status'):text(error_message));
+ local orig_role = occupant.role;
local is_last_session = occupant.jid == real_jid;
if is_last_session then
occupant.role = nil;
@@ -389,9 +427,13 @@ function room_mt:handle_kickable(origin, stanza) -- luacheck: ignore 212
if is_last_session then
x:tag("status", {code = "333"});
end
- self:publicise_occupant_status(new_occupant or occupant, x);
+ self:publicise_occupant_status(new_occupant or occupant, x, nil, nil, nil, orig_role);
if is_last_session then
- module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
+ module:fire_event("muc-occupant-left", {
+ room = self;
+ nick = occupant.nick;
+ occupant = occupant;
+ });
end
return true;
end
@@ -406,7 +448,7 @@ module:hook("muc-occupant-pre-join", function(event)
local room, stanza = event.room, event.stanza;
local affiliation = room:get_affiliation(stanza.attr.from);
if affiliation == "outcast" then
- local reply = st.error_reply(stanza, "auth", "forbidden"):up();
+ local reply = st.error_reply(stanza, "auth", "forbidden", nil, room.jid):up();
reply.tags[1].attr.code = "403";
event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
@@ -414,28 +456,41 @@ module:hook("muc-occupant-pre-join", function(event)
end, -10);
module:hook("muc-occupant-pre-join", function(event)
+ local room = event.room;
local nick = jid_resource(event.occupant.nick);
if not nick:find("%S") then
- event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden"));
+ event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden", room.jid));
return true;
end
end, 1);
module:hook("muc-occupant-pre-change", function(event)
+ local room = event.room;
if not jid_resource(event.dest_occupant.nick):find("%S") then
- event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden"));
+ event.origin.send(st.error_reply(event.stanza, "modify", "not-allowed", "Invisible Nicknames are forbidden", room.jid));
return true;
end
end, 1);
-function room_mt:handle_first_presence(origin, stanza)
- if not stanza:get_child("x", "http://jabber.org/protocol/muc") then
- module:log("debug", "Room creation without <x>, possibly desynced");
+module:hook("muc-occupant-pre-join", function(event)
+ local room = event.room;
+ local nick = jid_resource(event.occupant.nick);
+ if not resourceprep(nick, true) then -- strict
+ event.origin.send(st.error_reply(event.stanza, "modify", "jid-malformed", "Nickname must pass strict validation", room.jid));
+ return true;
+ end
+end, 2);
- origin.send(st.error_reply(stanza, "cancel", "item-not-found"));
+module:hook("muc-occupant-pre-change", function(event)
+ local room = event.room;
+ local nick = jid_resource(event.dest_occupant.nick);
+ if not resourceprep(nick, true) then -- strict
+ event.origin.send(st.error_reply(event.stanza, "modify", "jid-malformed", "Nickname must pass strict validation", room.jid));
return true;
end
+end, 2);
+function room_mt:handle_first_presence(origin, stanza)
local real_jid = stanza.attr.from;
local dest_jid = stanza.attr.to;
local bare_jid = jid_bare(real_jid);
@@ -495,6 +550,72 @@ function room_mt:handle_first_presence(origin, stanza)
return true;
end
+
+function room_mt:is_anonymous_for(jid)
+ local is_anonymous = false;
+ local whois = self:get_whois();
+ if whois ~= "anyone" then
+ local affiliation = self:get_affiliation(jid);
+ if affiliation ~= "admin" and affiliation ~= "owner" then
+ local occupant = self:get_occupant_by_real_jid(jid);
+ if not (occupant and can_see_real_jids(whois, occupant)) then
+ is_anonymous = true;
+ end
+ end
+ end
+ return is_anonymous;
+end
+
+
+function room_mt:build_unavailable_presence(from_muc_jid, to_jid)
+ local nick = jid_resource(from_muc_jid);
+ local from_jid = self:get_registered_jid(nick);
+ if (not from_jid) then
+ module:log("debug", "Received presence probe for unavailable nickname that's not registered");
+ return;
+ end
+ local is_anonymous = self:is_anonymous_for(to_jid);
+ local affiliation = self:get_affiliation(from_jid) or "none";
+ local pr = st.presence({ to = to_jid, from = from_muc_jid, type = "unavailable" })
+ :tag("x", { xmlns = 'http://jabber.org/protocol/muc#user' })
+ :tag("item", {
+ affiliation = affiliation;
+ role = "none";
+ nick = nick;
+ jid = not is_anonymous and from_jid or nil }):up()
+ :up();
+
+ local x = pr:get_child("x", "http://jabber.org/protocol/muc");
+ local event = {
+ room = self; stanza = pr; x = x;
+ bare_jid = from_jid;
+ nick = nick;
+ }
+ module:fire_event("muc-build-occupant-presence", event);
+ return event.stanza;
+end
+
+function room_mt:respond_to_probe(origin, stanza, probing_occupant)
+ if probing_occupant == nil then
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid));
+ return;
+ end
+
+ local from_muc_jid = stanza.attr.to;
+ local probed_occupant = self:get_occupant_by_nick(from_muc_jid);
+ if probed_occupant == nil then
+ local to_jid = stanza.attr.from;
+ local pr = self:build_unavailable_presence(from_muc_jid, to_jid);
+ if pr then
+ self:route_stanza(pr);
+ end
+ return;
+ end
+ local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"});
+ self:publicise_occupant_status(probed_occupant, x, nil, nil, nil, nil, false, probing_occupant);
+end
+
+
function room_mt:handle_normal_presence(origin, stanza)
local type = stanza.attr.type;
local real_jid = stanza.attr.from;
@@ -505,7 +626,7 @@ function room_mt:handle_normal_presence(origin, stanza)
if orig_occupant == nil and not muc_x and stanza.attr.type == nil then
module:log("debug", "Attempted join without <x>, possibly desynced");
origin.send(st.error_reply(stanza, "cancel", "item-not-found",
- "You must join the room before sending presence updates"));
+ "You are not currently connected to this chat", self.jid));
return true;
end
@@ -514,6 +635,9 @@ function room_mt:handle_normal_presence(origin, stanza)
if type == "unavailable" then
if orig_occupant == nil then return true; end -- Unavailable from someone not in the room
-- dest_occupant = nil
+ elseif type == "probe" then
+ self:respond_to_probe(origin, stanza, orig_occupant)
+ return true;
elseif orig_occupant and orig_occupant.nick == stanza.attr.to then -- Just a presence update
log("debug", "presence update for %s from session %s", orig_occupant.nick, real_jid);
dest_occupant = orig_occupant;
@@ -567,7 +691,7 @@ function room_mt:handle_normal_presence(origin, stanza)
and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
-- new nick or has different bare real jid
log("debug", "%s couldn't join due to nick conflict: %s", real_jid, dest_occupant.nick);
- local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ local reply = st.error_reply(stanza, "cancel", "conflict", nil, self.jid):up();
reply.tags[1].attr.code = "409";
origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
@@ -576,6 +700,7 @@ function room_mt:handle_normal_presence(origin, stanza)
-- Send presence stanza about original occupant
if orig_occupant ~= nil and orig_occupant ~= dest_occupant then
local orig_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
+ local orig_role = orig_occupant.role;
local dest_nick;
if dest_occupant == nil then -- Session is leaving
log("debug", "session %s is leaving occupant %s", real_jid, orig_occupant.nick);
@@ -613,12 +738,12 @@ function room_mt:handle_normal_presence(origin, stanza)
x:tag("status", {code = "303";}):up();
x:tag("status", {code = "110";}):up();
self:route_stanza(generated_unavail:add_child(x));
- dest_nick = nil; -- set dest_nick to nil; so general populance doesn't see it for whole orig_occupant
+ dest_nick = nil; -- set dest_nick to nil; so general populace doesn't see it for whole orig_occupant
end
end
self:save_occupant(orig_occupant);
- self:publicise_occupant_status(orig_occupant, orig_x, dest_nick);
+ self:publicise_occupant_status(orig_occupant, orig_x, dest_nick, nil, nil, orig_role);
if is_last_orig_session then
module:fire_event("muc-occupant-left", {
@@ -639,7 +764,7 @@ function room_mt:handle_normal_presence(origin, stanza)
-- Send occupant list to newly joined or desynced user
self:send_occupant_list(real_jid, function(nick, occupant) -- luacheck: ignore 212
-- Don't include self
- return occupant:get_presence(real_jid) == nil;
+ return (not occupant) or occupant:get_presence(real_jid) == nil;
end)
end
local dest_x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
@@ -650,7 +775,7 @@ function room_mt:handle_normal_presence(origin, stanza)
if nick_changed then
self_x:tag("status", {code="210"}):up();
end
- self:publicise_occupant_status(dest_occupant, {base=dest_x,self=self_x});
+ self:publicise_occupant_status(dest_occupant, {base=dest_x,self=self_x}, nil, nil, nil, orig_occupant and orig_occupant.role or nil);
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
@@ -692,11 +817,11 @@ function room_mt:handle_presence_to_occupant(origin, stanza)
local type = stanza.attr.type;
if type == "error" then -- error, kick em out!
return self:handle_kickable(origin, stanza)
- elseif type == nil or type == "unavailable" then
+ elseif type == nil or type == "unavailable" or type == "probe" then
return self:handle_normal_presence(origin, stanza);
elseif type ~= 'result' then -- bad type
if type ~= 'visible' and type ~= 'invisible' then -- COMPAT ejabberd can broadcast or forward XEP-0018 presences
- origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
+ origin.send(st.error_reply(stanza, "modify", "bad-request", nil, self.jid)); -- FIXME correct error?
end
end
return true;
@@ -731,11 +856,11 @@ function room_mt:handle_iq_to_occupant(origin, stanza)
else -- Type is "get" or "set"
local current_nick = self:get_occupant_jid(from);
if not current_nick then
- origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat"));
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid));
return true;
end
if not occupant then -- recipient not in room
- origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room", self.jid));
return true;
end
-- XEP-0410 MUC Self-Ping #1220
@@ -764,12 +889,12 @@ function room_mt:handle_message_to_occupant(origin, stanza)
local type = stanza.attr.type;
if not current_nick then -- not in room
if type ~= "error" then
- origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat"));
+ origin.send(st.error_reply(stanza, "cancel", "not-acceptable", "You are not currently connected to this chat", self.jid));
end
return true;
end
if type == "groupchat" then -- groupchat messages not allowed in PM
- origin.send(st.error_reply(stanza, "modify", "bad-request"));
+ origin.send(st.error_reply(stanza, "modify", "bad-request", nil, self.jid));
return true;
elseif type == "error" and is_kickable_error(stanza) then
log("debug", "%s kicked from %s for sending an error message", current_nick, self.jid);
@@ -778,14 +903,16 @@ function room_mt:handle_message_to_occupant(origin, stanza)
local o_data = self:get_occupant_by_nick(to);
if not o_data then
- origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
+ origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room", self.jid));
return true;
end
log("debug", "%s sent private message stanza to %s (%s)", from, to, o_data.jid);
stanza = muc_util.filter_muc_x(st.clone(stanza));
stanza:tag("x", { xmlns = "http://jabber.org/protocol/muc#user" }):up();
stanza.attr.from = current_nick;
- self:route_to_occupant(o_data, stanza)
+ if module:fire_event("muc-private-message", { room = self, origin = origin, stanza = stanza }) ~= false then
+ self:route_to_occupant(o_data, stanza)
+ end
-- TODO: Remove x tag?
stanza.attr.from = from;
return true;
@@ -815,10 +942,12 @@ function room_mt:process_form(origin, stanza)
if form.attr.type == "cancel" then
origin.send(st.reply(stanza));
elseif form.attr.type == "submit" then
+ -- luacheck: ignore 231/errors
local fields, errors, present;
if form.tags[1] == nil then -- Instant room
fields, present = {}, {};
else
+ -- FIXME handle form errors
fields, errors, present = self:get_form_layout(stanza.attr.from):data(form);
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"));
@@ -873,13 +1002,18 @@ function room_mt:clear(x)
x = x or st.stanza("x", {xmlns='http://jabber.org/protocol/muc#user'});
local occupants_updated = {};
for nick, occupant in self:each_occupant() do -- luacheck: ignore 213
+ local prev_role = occupant.role;
occupant.role = nil;
self:save_occupant(occupant);
- occupants_updated[occupant] = true;
+ occupants_updated[occupant] = prev_role;
end
- for occupant in pairs(occupants_updated) do
- self:publicise_occupant_status(occupant, x);
- module:fire_event("muc-occupant-left", { room = self; nick = occupant.nick; occupant = occupant;});
+ for occupant, prev_role in pairs(occupants_updated) do
+ self:publicise_occupant_status(occupant, x, nil, nil, nil, prev_role);
+ module:fire_event("muc-occupant-left", {
+ room = self;
+ nick = occupant.nick;
+ occupant = occupant;
+ });
end
end
@@ -972,7 +1106,7 @@ function room_mt:handle_admin_query_get_command(origin, stanza)
local _aff_rank = valid_affiliations[_aff or "none"];
local _rol = item.attr.role;
if _aff and _aff_rank and not _rol then
- -- You need to be at least an admin, and be requesting info about your affifiliation or lower
+ -- You need to be at least an admin, and be requesting info about your affiliation or lower
-- e.g. an admin can't ask for a list of owners
local affiliation_rank = valid_affiliations[affiliation or "none"];
if (affiliation_rank >= valid_affiliations.admin and affiliation_rank >= _aff_rank)
@@ -1049,10 +1183,18 @@ end
function room_mt:handle_groupchat_to_room(origin, stanza)
local from = stanza.attr.from;
local occupant = self:get_occupant_by_real_jid(from);
- if module:fire_event("muc-occupant-groupchat", {
- room = self; origin = origin; stanza = stanza; from = from; occupant = occupant;
- }) then return true; end
- stanza.attr.from = occupant.nick;
+ if not stanza.attr.id then
+ stanza.attr.id = new_id()
+ end
+ local event_data = {room = self; origin = origin; stanza = stanza; from = from; occupant = occupant};
+ if module:fire_event("muc-occupant-groupchat", event_data) then
+ return true;
+ end
+ if event_data.occupant then
+ stanza.attr.from = event_data.occupant.nick;
+ else
+ stanza.attr.from = self.jid;
+ end
self:broadcast_message(stanza);
stanza.attr.from = from;
return true;
@@ -1218,7 +1360,7 @@ function room_mt:route_stanza(stanza) -- luacheck: ignore 212
end
function room_mt:get_affiliation(jid)
- local node, host, resource = jid_split(jid);
+ local node, host = jid_split(jid);
-- Affiliations are granted, revoked, and maintained based on the user's bare JID.
local bare = node and node.."@"..host or host;
local result = self._affiliations[bare];
@@ -1241,7 +1383,7 @@ end
function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
if not actor then return nil, "modify", "not-acceptable"; end;
- local node, host, resource = jid_split(jid);
+ local node, host = jid_split(jid);
if not host then return nil, "modify", "not-acceptable"; end
jid = jid_join(node, host); -- Bare
local is_host_only = node == nil;
@@ -1297,7 +1439,7 @@ function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
-- Outcast can be by host.
is_host_only and affiliation == "outcast" and select(2, jid_split(occupant.bare_jid)) == host
) then
- -- need to publcize in all cases; as affiliation in <item/> has changed.
+ -- need to publicize in all cases; as affiliation in <item/> has changed.
occupants_updated[occupant] = occupant.role;
if occupant.role ~= role and (
is_downgrade or
@@ -1322,16 +1464,20 @@ function room_mt:set_affiliation(actor, jid, affiliation, reason, data)
if next(occupants_updated) ~= nil then
for occupant, old_role in pairs(occupants_updated) do
- self:publicise_occupant_status(occupant, x, nil, actor, reason);
+ self:publicise_occupant_status(occupant, x, nil, actor, reason, old_role);
if occupant.role == nil then
- module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
+ module:fire_event("muc-occupant-left", {
+ room = self;
+ nick = occupant.nick;
+ occupant = occupant;
+ });
elseif is_semi_anonymous and
((old_role == "moderator" and occupant.role ~= "moderator") or
(old_role ~= "moderator" and occupant.role == "moderator")) then -- Has gained or lost moderator status
-- Send everyone else's presences (as jid visibility has changed)
for real_jid in occupant:each_session() do
self:send_occupant_list(real_jid, function(occupant_jid, occupant) --luacheck: ignore 212 433
- return occupant.bare_jid ~= jid;
+ return (not occupant) or occupant.bare_jid ~= jid;
end);
end
end
@@ -1376,6 +1522,42 @@ function room_mt:get_role(nick)
return occupant and occupant.role or nil;
end
+function room_mt:may_set_role(actor, occupant, role)
+ local event = {
+ room = self,
+ actor = actor,
+ occupant = occupant,
+ role = role,
+ };
+
+ module:fire_event("muc-pre-set-role", event);
+ if event.allowed ~= nil then
+ return event.allowed, event.error, event.condition;
+ end
+
+ -- Can't do anything to other owners or admins
+ local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
+ if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
+ return nil, "cancel", "not-allowed";
+ end
+
+ -- If you are trying to give or take moderator role you need to be an owner or admin
+ if occupant.role == "moderator" or role == "moderator" then
+ local actor_affiliation = self:get_affiliation(actor);
+ if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
+ return nil, "cancel", "not-allowed";
+ end
+ end
+
+ -- Need to be in the room and a moderator
+ local actor_occupant = self:get_occupant_by_real_jid(actor);
+ if not actor_occupant or actor_occupant.role ~= "moderator" then
+ return nil, "cancel", "not-allowed";
+ end
+
+ return true;
+end
+
function room_mt:set_role(actor, occupant_jid, role, reason)
if not actor then return nil, "modify", "not-acceptable"; end
@@ -1390,24 +1572,9 @@ function room_mt:set_role(actor, occupant_jid, role, reason)
if actor == true then
actor = nil -- So we can pass it safely to 'publicise_occupant_status' below
else
- -- Can't do anything to other owners or admins
- local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
- return nil, "cancel", "not-allowed";
- end
-
- -- If you are trying to give or take moderator role you need to be an owner or admin
- if occupant.role == "moderator" or role == "moderator" then
- local actor_affiliation = self:get_affiliation(actor);
- if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
- return nil, "cancel", "not-allowed";
- end
- end
-
- -- Need to be in the room and a moderator
- local actor_occupant = self:get_occupant_by_real_jid(actor);
- if not actor_occupant or actor_occupant.role ~= "moderator" then
- return nil, "cancel", "not-allowed";
+ local allowed, err, condition = self:may_set_role(actor, occupant, role)
+ if not allowed then
+ return allowed, err, condition;
end
end
@@ -1415,11 +1582,17 @@ function room_mt:set_role(actor, occupant_jid, role, reason)
if not role then
x:tag("status", {code = "307"}):up();
end
+
+ local prev_role = occupant.role;
occupant.role = role;
self:save_occupant(occupant);
- self:publicise_occupant_status(occupant, x, nil, actor, reason);
+ self:publicise_occupant_status(occupant, x, nil, actor, reason, prev_role);
if role == nil then
- module:fire_event("muc-occupant-left", {room = self; nick = occupant.nick; occupant = occupant;});
+ module:fire_event("muc-occupant-left", {
+ room = self;
+ nick = occupant.nick;
+ occupant = occupant;
+ });
end
return true;
end
@@ -1441,7 +1614,7 @@ function _M.new_room(jid, config)
}, room_mt);
end
-local new_format = module:get_option_boolean("new_muc_storage_format", false);
+local new_format = module:get_option_boolean("new_muc_storage_format", true);
function room_mt:freeze(live)
local frozen, state;
@@ -1505,7 +1678,7 @@ function _M.restore_room(frozen, state)
else
-- New storage format
for jid, data in pairs(frozen) do
- local node, host, resource = jid_split(jid);
+ local _, host, resource = jid_split(jid);
if host:sub(1,1) ~= "_" and not resource and type(data) == "string" then
-- bare jid: affiliation
room._affiliations[jid] = data;
diff --git a/plugins/muc/name.lib.lua b/plugins/muc/name.lib.lua
index 37fe1259..5d73e74d 100644
--- a/plugins/muc/name.lib.lua
+++ b/plugins/muc/name.lib.lua
@@ -7,10 +7,8 @@
-- COPYING file in the source package for more information.
--
-local jid_split = require "util.jid".split;
-
local function get_name(room)
- return room._data.name or jid_split(room.jid);
+ return room._data.name;
end
local function set_name(room, name)
diff --git a/plugins/muc/occupant_id.lib.lua b/plugins/muc/occupant_id.lib.lua
new file mode 100644
index 00000000..1a44462c
--- /dev/null
+++ b/plugins/muc/occupant_id.lib.lua
@@ -0,0 +1,70 @@
+-- Implementation of https://xmpp.org/extensions/inbox/occupant-id.html
+-- XEP-0421: Anonymous unique occupant identifiers for MUCs
+
+-- (C) 2020 Maxime “pep” Buquet <pep@bouah.net>
+-- (C) 2020 Matthew Wild <mwild1@gmail.com>
+
+local uuid = require "util.uuid";
+local hmac_sha256 = require "util.hashes".hmac_sha256;
+local b64encode = require "util.encodings".base64.encode;
+
+local xmlns_occupant_id = "urn:xmpp:occupant-id:0";
+
+local function get_occupant_id(room, occupant)
+ if occupant.stable_id then
+ return occupant.stable_id;
+ end
+
+ local salt = room._data.occupant_id_salt;
+ if not salt then
+ salt = uuid.generate();
+ room._data.occupant_id_salt = salt;
+ end
+
+ occupant.stable_id = b64encode(hmac_sha256(occupant.bare_jid, salt));
+
+ return occupant.stable_id;
+end
+
+local function update_occupant(event)
+ local stanza, room, occupant, dest_occupant = event.stanza, event.room, event.occupant, event.dest_occupant;
+
+ -- "muc-occupant-pre-change" provides "dest_occupant" but not "occupant".
+ if dest_occupant ~= nil then
+ occupant = dest_occupant;
+ end
+
+ -- strip any existing <occupant-id/> tags to avoid forgery
+ stanza:remove_children("occupant-id", xmlns_occupant_id);
+
+ local unique_id = get_occupant_id(room, occupant);
+ stanza:tag("occupant-id", { xmlns = xmlns_occupant_id, id = unique_id }):up();
+end
+
+local function muc_private(event)
+ local stanza, room = event.stanza, event.room;
+ local occupant = room._occupants[stanza.attr.from];
+
+ update_occupant({
+ stanza = stanza,
+ room = room,
+ occupant = occupant,
+ });
+end
+
+if module:get_option_boolean("muc_occupant_id", true) then
+ module:add_feature(xmlns_occupant_id);
+ module:hook("muc-disco#info", function (event)
+ event.reply:tag("feature", { var = xmlns_occupant_id }):up();
+ end);
+
+ module:hook("muc-broadcast-presence", update_occupant);
+ module:hook("muc-occupant-pre-join", update_occupant);
+ module:hook("muc-occupant-pre-change", update_occupant);
+ module:hook("muc-occupant-groupchat", update_occupant);
+ module:hook("muc-private-message", muc_private);
+end
+
+return {
+ get_occupant_id = get_occupant_id;
+};
diff --git a/plugins/muc/password.lib.lua b/plugins/muc/password.lib.lua
index 1f4b2add..6695c0cf 100644
--- a/plugins/muc/password.lib.lua
+++ b/plugins/muc/password.lib.lua
@@ -50,7 +50,7 @@ module:hook("muc-occupant-pre-join", function(event)
if get_password(room) ~= password then
local from, to = stanza.attr.from, stanza.attr.to;
module:log("debug", "%s couldn't join due to invalid password: %s", from, to);
- local reply = st.error_reply(stanza, "auth", "not-authorized"):up();
+ local reply = st.error_reply(stanza, "auth", "not-authorized", nil, room.jid):up();
reply.tags[1].attr.code = "401";
event.origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
diff --git a/plugins/muc/presence_broadcast.lib.lua b/plugins/muc/presence_broadcast.lib.lua
new file mode 100644
index 00000000..82a89fee
--- /dev/null
+++ b/plugins/muc/presence_broadcast.lib.lua
@@ -0,0 +1,83 @@
+-- Prosody IM
+-- Copyright (C) 2008-2010 Matthew Wild
+-- Copyright (C) 2008-2010 Waqas Hussain
+-- Copyright (C) 2014 Daurnimator
+--
+-- This project is MIT/X11 licensed. Please see the
+-- COPYING file in the source package for more information.
+--
+
+local st = require "util.stanza";
+
+local valid_roles = { "none", "visitor", "participant", "moderator" };
+local default_broadcast = {
+ visitor = true;
+ participant = true;
+ moderator = true;
+};
+
+local function get_presence_broadcast(room)
+ return room._data.presence_broadcast or default_broadcast;
+end
+
+local function set_presence_broadcast(room, broadcast_roles)
+ broadcast_roles = broadcast_roles or default_broadcast;
+
+ local changed = false;
+ local old_broadcast_roles = get_presence_broadcast(room);
+ for _, role in ipairs(valid_roles) do
+ if old_broadcast_roles[role] ~= broadcast_roles[role] then
+ changed = true;
+ end
+ end
+
+ if not changed then return false; end
+
+ room._data.presence_broadcast = broadcast_roles;
+
+ for _, occupant in room:each_occupant() do
+ local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";});
+ local role = occupant.role or "none";
+ if broadcast_roles[role] and not old_broadcast_roles[role] then
+ -- Presence broadcast is now enabled, so announce existing user
+ room:publicise_occupant_status(occupant, x);
+ elseif old_broadcast_roles[role] and not broadcast_roles[role] then
+ -- Presence broadcast is now disabled, so mark existing user as unavailable
+ room:publicise_occupant_status(occupant, x, nil, nil, nil, nil, true);
+ end
+ end
+
+ return true;
+end
+
+module:hook("muc-config-form", function(event)
+ local values = {};
+ for role, value in pairs(get_presence_broadcast(event.room)) do
+ if value then
+ values[#values + 1] = role;
+ end
+ end
+
+ table.insert(event.form, {
+ name = "muc#roomconfig_presencebroadcast";
+ type = "list-multi";
+ label = "Only show participants with roles:";
+ value = values;
+ options = valid_roles;
+ });
+end, 70-7);
+
+module:hook("muc-config-submitted/muc#roomconfig_presencebroadcast", function(event)
+ local broadcast_roles = {};
+ for _, role in ipairs(event.value) do
+ broadcast_roles[role] = true;
+ end
+ if set_presence_broadcast(event.room, broadcast_roles) then
+ event.status_codes["104"] = true;
+ end
+end);
+
+return {
+ get = get_presence_broadcast;
+ set = set_presence_broadcast;
+};
diff --git a/plugins/muc/register.lib.lua b/plugins/muc/register.lib.lua
index 95ed1a84..b6559410 100644
--- a/plugins/muc/register.lib.lua
+++ b/plugins/muc/register.lib.lua
@@ -15,8 +15,7 @@ local function get_reserved_nicks(room)
end
module:log("debug", "Refreshing reserved nicks...");
local reserved_nicks = {};
- for jid in room:each_affiliation() do
- local data = room._affiliation_data[jid];
+ for jid, _, data in room:each_affiliation() do
local nick = data and data.reserved_nickname;
module:log("debug", "Refreshed for %s: %s", jid, nick);
if nick then
@@ -54,9 +53,23 @@ end);
local registration_form = dataforms.new {
{ name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/protocol/muc#register" },
- { name = "muc#register_roomnick", type = "text-single", label = "Nickname"},
+ { name = "muc#register_roomnick", type = "text-single", required = true, label = "Nickname"},
};
+module:handle_items("muc-registration-field", function (event)
+ module:log("debug", "Adding MUC registration form field: %s", event.item.name);
+ table.insert(registration_form, event.item);
+end, function (event)
+ module:log("debug", "Removing MUC registration form field: %s", event.item.name);
+ local removed_field_name = event.item.name;
+ for i, field in ipairs(registration_form) do
+ if field.name == removed_field_name then
+ table.remove(registration_form, i);
+ break;
+ end
+ end
+end);
+
local function enforce_nick_policy(event)
local origin, stanza = event.origin, event.stanza;
local room = assert(event.room); -- FIXME
@@ -67,7 +80,7 @@ local function enforce_nick_policy(event)
local reserved_by = get_registered_jid(room, requested_nick);
if reserved_by and reserved_by ~= jid_bare(stanza.attr.from) then
module:log("debug", "%s attempted to use nick %s reserved by %s", stanza.attr.from, requested_nick, reserved_by);
- local reply = st.error_reply(stanza, "cancel", "conflict"):up();
+ local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
end
@@ -80,7 +93,7 @@ local function enforce_nick_policy(event)
event.occupant.nick = jid_bare(event.occupant.nick) .. "/" .. nick;
elseif event.dest_occupant.nick ~= jid_bare(event.dest_occupant.nick) .. "/" .. nick then
module:log("debug", "Attempt by %s to join as %s, but their reserved nick is %s", stanza.attr.from, requested_nick, nick);
- local reply = st.error_reply(stanza, "cancel", "not-acceptable"):up();
+ local reply = st.error_reply(stanza, "cancel", "not-acceptable", nil, room.jid):up();
origin.send(reply:tag("x", {xmlns = "http://jabber.org/protocol/muc"}));
return true;
end
@@ -104,7 +117,7 @@ local function handle_register_iq(room, origin, stanza)
local user_jid = jid_bare(stanza.attr.from)
local affiliation = room:get_affiliation(user_jid);
if affiliation == "outcast" then
- origin.send(st.error_reply(stanza, "auth", "forbidden"));
+ origin.send(st.error_reply(stanza, "auth", "forbidden", room.jid));
return true;
elseif not (affiliation or allow_unaffiliated) then
origin.send(st.error_reply(stanza, "auth", "registration-required"));
@@ -115,6 +128,8 @@ local function handle_register_iq(room, origin, stanza)
if stanza.attr.type == "get" then
reply:query("jabber:iq:register");
if registered_nick then
+ -- I find it strange, but XEP-0045 says not to include
+ -- the current registration data (only the registered name)
reply:tag("registered"):up();
reply:tag("username"):text(registered_nick);
origin.send(reply);
@@ -135,13 +150,25 @@ local function handle_register_iq(room, origin, stanza)
return true;
end
local form_tag = query:get_child("x", "jabber:x:data");
- local reg_data = form_tag and registration_form:data(form_tag);
+ if not form_tag then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform"));
+ return true;
+ end
+ local form_type, err = dataforms.get_type(form_tag);
+ if not form_type then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Error with form: "..err));
+ return true;
+ elseif form_type ~= "http://jabber.org/protocol/muc#register" then
+ origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form"));
+ return true;
+ end
+ local reg_data = registration_form:data(form_tag);
if not reg_data then
origin.send(st.error_reply(stanza, "modify", "bad-request", "Error in form"));
return true;
end
-- Is the nickname valid?
- local desired_nick = resourceprep(reg_data["muc#register_roomnick"]);
+ local desired_nick = resourceprep(reg_data["muc#register_roomnick"], true);
if not desired_nick then
origin.send(st.error_reply(stanza, "modify", "bad-request", "Invalid Nickname"));
return true;
@@ -172,6 +199,13 @@ local function handle_register_iq(room, origin, stanza)
-- Checks passed, save the registration
if registered_nick ~= desired_nick then
local registration_data = { reserved_nickname = desired_nick };
+ module:fire_event("muc-registration-submitted", {
+ room = room;
+ origin = origin;
+ stanza = stanza;
+ submitted_data = reg_data;
+ affiliation_data = registration_data;
+ });
local ok, err_type, err_condition = room:set_affiliation(true, user_jid, affiliation or "member", nil, registration_data);
if not ok then
origin.send(st.error_reply(stanza, err_type, err_condition));
diff --git a/plugins/muc/subject.lib.lua b/plugins/muc/subject.lib.lua
index 14256bbc..3230817c 100644
--- a/plugins/muc/subject.lib.lua
+++ b/plugins/muc/subject.lib.lua
@@ -94,6 +94,12 @@ module:hook("muc-occupant-groupchat", function(event)
local stanza = event.stanza;
local subject = stanza:get_child("subject");
if subject then
+ if stanza:get_child("body") or stanza:get_child("thread") then
+ -- Note: A message with a <subject/> and a <body/> or a <subject/> and
+ -- a <thread/> is a legitimate message, but it SHALL NOT be interpreted
+ -- as a subject change.
+ return;
+ end
local room = event.room;
local occupant = event.occupant;
-- Role check for subject changes
diff --git a/plugins/muc/util.lib.lua b/plugins/muc/util.lib.lua
index 53a83fae..0877150f 100644
--- a/plugins/muc/util.lib.lua
+++ b/plugins/muc/util.lib.lua
@@ -41,18 +41,22 @@ function _M.is_kickable_error(stanza)
return kickable_error_conditions[cond];
end
-local muc_x_filters = {
- ["http://jabber.org/protocol/muc"] = true;
- ["http://jabber.org/protocol/muc#user"] = true;
-}
-local function muc_x_filter(tag)
- if muc_x_filters[tag.attr.xmlns] then
+local filtered_namespaces = module:shared("filtered-namespaces");
+filtered_namespaces["http://jabber.org/protocol/muc"] = true;
+filtered_namespaces["http://jabber.org/protocol/muc#user"] = true;
+
+local function muc_ns_filter(tag)
+ if filtered_namespaces[tag.attr.xmlns] then
return nil;
end
return tag;
end
function _M.filter_muc_x(stanza)
- return stanza:maptags(muc_x_filter);
+ return stanza:maptags(muc_ns_filter);
+end
+
+function _M.add_filtered_namespace(xmlns)
+ filtered_namespaces[xmlns] = true;
end
function _M.only_with_min_role(role)