diff --git a/chatcommands.lua b/chatcommands.lua index 2731338..1b877e1 100644 --- a/chatcommands.lua +++ b/chatcommands.lua @@ -1,51 +1,39 @@ -local channel_created_string = "|#${channel_name}| Channel created" -local channel_updated_string = "|#${channel_name}| Channel updated" -local channel_invitation_string = "|#${channel_name}| Channel invite from (${from_player}), " - .. "to join the channel, do /jc ${channel_name},${channel_password} after " - .. "which you can send messages to the channel via #${channel_name}: message" -local channel_invited_string = "|#${channel_name}| Invite sent to ${to_player}" -local channel_deleted_string = "|#${channel_name}| Channel deleted" -local channel_left_string = "|#${channel_name}| Left channel" -local channel_already_deleted_string = "|#${channel_name}| Channel seems to have already " - .. "been deleted, will unregister channel from your list of channels" +local channel_created_string = "|#${channel}| Channel created" +local channel_updated_string = "|#${channel}| Channel updated" +local channel_deleted_string = "|#${channel}| Channel deleted" +local channel_left_string = "|#${channel}| Left channel" +local nochannel_string = "|#${channel}| Channel does not exist, will unregister channel from your list of channels" -local leave_channel_sound = "beerchat_chirp" -- Sound when you leave a channel -local channel_invite_sound = "beerchat_chirp" -- Sound when sending/ receiving an invite to a channel +local leave_channel_sound = "beerchat_chirp" -- Sound when you leave a channel local create_channel = { - params = ",,", - description = "Create or edit a channel named with optional and " - .. "hexadecimal starting with # (e.g. #00ff00 for green). Use comma's " - .. "to separate the arguments, e.g. " - .. "/cc my secret channel,#0000ff for a blue colored my secret channel without password", + params = "[[,],]", + description = "Create or edit a channel named with optional and hexadecimal " + .. "starting with # (e.g. #00ff00 for green, defaults to #ffffff). Use commas to separate the arguments, e.g. " + .. "/cc my-secret-channel,#0000ff for a blue colored my-secret-channel without password", func = function(lname, param) - local lowner = lname - if not param or param == "" then return false, "ERROR: Invalid number of arguments. Please supply the channel name as a minimum." end - local str = string.split(param:gsub("^#",""), ",") + local str = param:gsub("^#",""):split(",") if #str > 3 then return false, "ERROR: Invalid number of arguments. 4 parameters passed, " .. "maximum of 3 allowed: ,," end - local lchannel_name = string.trim(str[1] or ""):gsub("%s", "-") + local lchannel_name = (str[1] or ""):trim():gsub("%s", "-") if lchannel_name == "" then return false, "ERROR: You must supply a channel name" - end - - if lchannel_name == beerchat.main_channel_name then + elseif lchannel_name == beerchat.main_channel_name then return false, "ERROR: You cannot use channel name \"" .. beerchat.main_channel_name .. "\"" end local msg = channel_created_string if beerchat.channels[lchannel_name] then local cowner = beerchat.channels[lchannel_name].owner - if not cowner or cowner == "" or cowner ~= lowner then - return false, "ERROR: Channel " .. lchannel_name - .. " already exists, owned by player " .. beerchat.channels[lchannel_name].owner + if not cowner or cowner == "" or cowner ~= lname then + return false, "ERROR: Channel " .. lchannel_name .. " already exists, owned by player " .. cowner end msg = channel_updated_string end @@ -66,13 +54,12 @@ local create_channel = { lcolor = string.lower(str[3]) end - beerchat.channels[lchannel_name] = { owner = lowner, name = lchannel_name, - password = lpassword, color = lcolor } + beerchat.channels[lchannel_name] = { owner = lname, name = lchannel_name, password = lpassword, color = lcolor } beerchat.mod_storage:set_string("channels", minetest.write_json(beerchat.channels)) - beerchat.add_player_channel(lowner, lchannel_name, "owner") - beerchat.sound_play(lowner, beerchat.channel_management_sound) - minetest.chat_send_player(lowner, beerchat.format_message(msg, { channel_name = lchannel_name })) + beerchat.add_player_channel(lname, lchannel_name, "owner") + beerchat.sound_play(lname, beerchat.channel_management_sound) + minetest.chat_send_player(lname, beerchat.format_message(msg, { channel = lchannel_name })) return true end } @@ -106,7 +93,7 @@ local delete_channel = { beerchat.sound_play(name, beerchat.channel_management_sound) minetest.chat_send_player(name, beerchat.format_message( - channel_deleted_string, { channel_name = delete.channel, color = color } + channel_deleted_string, { channel = delete.channel, color = color } )) return true end @@ -127,8 +114,7 @@ local my_channels = { beerchat.sound_play(name, beerchat.channel_management_sound) minetest.chat_send_player(name, dump2(beerchat.channels[param])) else - minetest.chat_send_player(name, "ERROR: Channel not in your channel list") - return false + return false, "ERROR: Channel is not in your channel list." end end return true @@ -136,38 +122,22 @@ local my_channels = { } local join_channel = { - params = ",", + params = "", description = "Join channel named . After joining you will see messages " - .. "sent to that channel (in addition to the other channels you have joined)", - func = function(name, param) - if not param or param == "" then - return false, "ERROR: Invalid number of arguments. Please supply the channel " - .. "name as a minimum." - end - - local str = string.split(param:gsub("^#",""), ",") - local channel_name = str[1] or "" - - if not beerchat.channels[channel_name] then - return false, "ERROR: Channel " .. channel_name .. " does not exist." - end - - if beerchat.playersChannels[name] and beerchat.playersChannels[name][channel_name] then - return false, "ERROR: You already joined "..channel_name..", no need to rejoin" + .. "sent to that channel in addition to the other channels you have joined.", + func = function(name, channel) + if not channel or channel == "" then + return false, "ERROR: Invalid arguments. Please supply the channel name." end - if beerchat.channels[channel_name].password and beerchat.channels[channel_name].password ~= "" then - if #str == 1 then - return false, "ERROR: This channel requires that you supply a password. " - .. "Supply it in the following format: /jc my channel,password01" - end - if str[2] ~= beerchat.channels[channel_name].password then - return false, "ERROR: Invalid password." - end + channel = channel:match("^#?(%S+)") + if not channel or not beerchat.channels[channel] then + return false, "ERROR: Channel " .. (channel or "") .. " does not exist." + elseif beerchat.playersChannels[name] and beerchat.playersChannels[name][channel] then + return false, "ERROR: You already joined " .. channel .. ", no need to rejoin" end - return beerchat.join_channel(name, channel_name) + beerchat.join_channel(name, channel) end } @@ -178,14 +148,13 @@ local leave_channel = { .. "NOTE: You can also leave the main channel", func = function(name, channel) if not channel or channel == "" then - return false, "ERROR: Invalid number of arguments. Please supply the channel name." + return false, "ERROR: Invalid arguments. Please supply the channel name." end + channel = channel:match("^#?(%S+)") if not beerchat.playersChannels[name][channel] then return false, "ERROR: You are not member of " .. channel .. ", no need to leave." - end - - if not beerchat.execute_callbacks('before_leave', name, channel) then + elseif not beerchat.execute_callbacks('before_leave', name, channel) then return false end @@ -193,69 +162,9 @@ local leave_channel = { beerchat.sound_play(name, leave_channel_sound) if not beerchat.channels[channel] then - minetest.chat_send_player(name, - beerchat.format_message(channel_already_deleted_string, - { channel_name = channel }) - ) - else - minetest.chat_send_player(name, - beerchat.format_message(channel_left_string, - { channel_name = channel }) - ) - end - return true - end -} - -local invite_channel = { - params = ",", - description = "Invite player named to channel named . " - .. "You must be the owner of the channel in order to invite others.", - func = function(name, param) - if not param or param == "" then - return false, "ERROR: Invalid number of arguments. Please supply the channel " - .. "name and the player name." - end - - local channel_name, player_name = string.match(param, "#?(.*),(.*)") - - if not channel_name or channel_name == "" then - return false, "ERROR: Channel name is empty." - end - - if not player_name or player_name == "" then - return false, "ERROR: Player name not supplied or empty." - end - - if not beerchat.channels[channel_name] then - return false, "ERROR: Channel " .. channel_name .. " does not exist." - end - - if name ~= beerchat.channels[channel_name].owner then - return false, "ERROR: You are not the owner of channel " .. param .. "." - end - - if not minetest.get_player_by_name(player_name) then - return false, "ERROR: " .. player_name .. " does not exist or is not online." + minetest.chat_send_player(name, beerchat.format_message(nochannel_string, { channel = channel })) else - if not beerchat.execute_callbacks('before_invite', name, player_name, channel_name) then - return false - end - if not beerchat.has_player_muted_player(player_name, name) then - beerchat.sound_play(player_name, channel_invite_sound) - -- Sending the message - minetest.chat_send_player( - player_name, - beerchat.format_message(channel_invitation_string, - { channel_name = channel_name, from_player = name }) - ) - end - beerchat.sound_play(name, channel_invite_sound) - minetest.chat_send_player( - name, - beerchat.format_message(channel_invited_string, - { channel_name = channel_name, to_player = player_name }) - ) + minetest.chat_send_player(name, beerchat.format_message(channel_left_string, { channel = channel })) end return true end @@ -273,5 +182,3 @@ minetest.register_chatcommand("jc", join_channel) minetest.register_chatcommand("join_channel", join_channel) minetest.register_chatcommand("lc", leave_channel) minetest.register_chatcommand("leave_channel", leave_channel) -minetest.register_chatcommand("ic", invite_channel) -minetest.register_chatcommand("invite_channel", invite_channel) diff --git a/common.lua b/common.lua index 9a012d4..7d5d673 100644 --- a/common.lua +++ b/common.lua @@ -47,14 +47,15 @@ beerchat.fix_player_channel = function(name, notify) beerchat.set_player_channel(name, beerchat.main_channel_name) end -beerchat.join_channel = function(name, channel, set_default) - if not beerchat.execute_callbacks('before_join', name, channel) then +beerchat.join_channel = function(name, channel, data) + data = type(data) == "table" and data or { set_default = data } + data.channel = channel + if not beerchat.execute_callbacks('before_join', name, channel, data) then return false end - (set_default and beerchat.set_player_channel or beerchat.add_player_channel)(name, channel) + ;(data.set_default and beerchat.set_player_channel or beerchat.add_player_channel)(name, data.channel) beerchat.sound_play(name, "beerchat_chirp") - local msg = beerchat.format_message("|#${channel_name}| Joined channel", { channel_name = channel }) - minetest.chat_send_player(name, msg) + minetest.chat_send_player(name, beerchat.format_message("|#${channel}| Joined channel", { channel = data.channel })) return true end diff --git a/format_message.lua b/format_message.lua index 6b72b38..322ee9a 100644 --- a/format_message.lua +++ b/format_message.lua @@ -17,7 +17,7 @@ beerchat.format_message = function(s, tab) local owner local password local color = beerchat.default_channel_color - local channel_name = tab.channel_name or "" + local channel_name = tab.channel or "" if beerchat.channels[channel_name] then owner = beerchat.channels[channel_name].owner @@ -36,9 +36,9 @@ beerchat.format_message = function(s, tab) end local params = { - channel_name = channel_name, + channel = channel_name, channel_owner = owner, - channel_password = password, + password = password, from_player = tab.from_player, to_player = tab.to_player, message = colorize_target_name(tab.message, tab.to_player), diff --git a/init.lua b/init.lua index 2ad6e6c..003b258 100644 --- a/init.lua +++ b/init.lua @@ -27,7 +27,7 @@ beerchat = { -- Sound when a message is sent to a channel channel_message_sound = "beerchat_chime", - main_channel_message_string = "|#${channel_name}| <${from_player}> ${message}", + main_channel_message_string = "|#${channel}| <${from_player}> ${message}", moderator_channel_name = minetest.settings:get("beerchat.moderator_channel_name"), diff --git a/message.lua b/message.lua index ac45111..7723dc2 100644 --- a/message.lua +++ b/message.lua @@ -5,9 +5,9 @@ -- player names at the end of the chat message, etc. -- -- The following parameters are available and can be specified : --- ${channel_name} name of the channel +-- ${channel} name of the channel -- ${channel_owner} owner of the channel --- ${channel_password} password to use when joining the channel, used e.g. for invites +-- ${password} password to use when joining the channel, used e.g. for invites -- ${from_player} the player that is sending the message -- ${to_player} player to which the message is sent, will contain multiple player names -- e.g. when sending a PM to multiple players diff --git a/plugin/acl.lua b/plugin/acl.lua new file mode 100644 index 0000000..e4575bf --- /dev/null +++ b/plugin/acl.lua @@ -0,0 +1,89 @@ +--luacheck: no_unused_args + +-- Event handler priority for ACL related hooks, could be made configurable if needed. +-- ACLs should not modify request data but should have lower priority than handlers +-- which might affect request data, for example alias plugin which can rewrite channels. +-- Important predefined priority levels are: high=0, medium=250, default=500 +local PRIORITY = 50 + +-- +-- Load ACL core functionality and ACL modules +-- + +local srcdir = minetest.get_modpath("beerchat").."/plugin/acl" + +local acls = dofile(srcdir .. "/acls.lua")( + minetest.deserialize(beerchat.mod_storage:get("acl.acls")), + function (data) beerchat.mod_storage:set_string("acl.acls", minetest.serialize(data)) end +) + +local password_protected_join = dofile(srcdir .. "/password.lua") + +loadfile(srcdir .. "/chatcommands.lua")(acls) + +-- +-- Load player identity provider, also used as a default provider +-- + +acls:register_provider("*", dofile(srcdir .. "/players.lua")) + +-- +-- Load privilege identity provider +-- + +acls:register_provider("$", dofile(srcdir .. "/privileges.lua")) + +-- +-- Access level / authorization checks for player actions +-- + +beerchat.register_callback('before_join', function(name, _, data) + return password_protected_join(name, data) +end, PRIORITY) + +beerchat.register_callback('before_join', function(name, _, data) + return acls:check_access(data.channel, name) +end, PRIORITY) + +beerchat.register_callback('after_joinplayer', function(player) + local name = player:get_player_name() + if name and beerchat.playersChannels[name] then + for channel in pairs(beerchat.playersChannels[name]) do + local success, message = acls:check_access(channel, name) + if success == false then + beerchat.remove_player_channel(name, channel) + minetest.chat_send_player(name, message) + end + end + end +end, PRIORITY) + +beerchat.register_callback('before_invite', function(name, data) + -- Check if name is allowed to invite others to target channel + if data.role == "owner" or data.role == "manager" then + return acls:check_access(data.channel, name, "owner") + end + return acls:check_access(data.channel, name, "manager") +end, PRIORITY) + +beerchat.register_callback("before_send", function(name, msg, data) + -- Check if name is allowed to receive messages on target channel. + -- Result is used to strip out second return value which is error message that + -- would be delivered to each player who joided the channel without read access. + local result = acls:check_access(data.channel, name, "read", "read") + return result +end, PRIORITY) + +beerchat.register_callback("before_send_on_channel", function(name, msg) + -- Check if name is allowed to send messages on target channel + return acls:check_access(msg.channel, name, "write", "write") +end, PRIORITY) + +beerchat.register_callback('before_switch_chan', function(name, switch) + return acls:check_access(switch.to, name) +end, PRIORITY) + +beerchat.register_callback('on_forced_join', function(name, target, channel, from_channel) + -- INJECT EVERYTHING THAT IS REQUIRED TO HAVE FULL ACCESS TO CHANNEL SO THAT + -- PLAYERS WITH THE FORCE CAN MOVE ANYONE TO ANY CHANNEL, ALSO TO LOCKED CHANNELS. +end) diff --git a/plugin/acl/acls.lua b/plugin/acl/acls.lua new file mode 100644 index 0000000..32fa059 --- /dev/null +++ b/plugin/acl/acls.lua @@ -0,0 +1,167 @@ +-- ACL storage + +local acls = { + rolenum = { deny = 0, owner = 1, manager = 2, write = 3, read = 4 }, + data = {}, + idp = {}, +} + +acls.maxrolenum = (function() + local count = 0 + for _ in pairs(acls.rolenum) do count = count + 1 end + return count +end)() + +local function is_valid_player_name(name) + return name:find("[^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%-_]") == nil +end + +local function is_valid_provider_name(name) + -- Valid provider name should have characters that wont appear in player name + return name:find("^[^abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%-_]") ~= nil +end + +-- rtdef_* are identity provider methods and defined here to avoid shadowing or renaming `self` +local function idp_get_role_default(self, channel, name) + local cache = self.cache + if cache[channel] and cache[channel][name] ~= nil then + return cache[channel][name] + elseif not acls.data[channel] then + return + end + + local num = acls.maxrolenum + 1 + cache[channel] = { [name] = false } + for identity in self:identities(name) do + local current = acls.data[channel][self:get_id(identity)] + if current and acls.rolenum[current] < num then + cache[channel][name] = current + num = acls.rolenum[current] + if num <= 0 then + break + end + end + end + return cache[channel][name] +end + +local function idp_get_id_default(self, name) + return self.roletype .. name +end + +function acls:register_provider(roletype, def) + -- Raise error if trying to register invalid provider + assert(is_valid_provider_name(roletype), "acls:register_provider: invalid roletype '"..tostring(roletype).."'.") + assert(type(def.validate) == "function", "acls:register_provider: provider must have :validate(identity) method.") + def.cache = {} + def.roletype = roletype + def.get_role = def.get_role or idp_get_role_default + def.get_id = def.get_id or idp_get_id_default + self.idp[roletype] = def +end + +function acls:get_provider(name) + if is_valid_provider_name(name) then + -- Return nil if named provider does not exist + return self.idp[name:sub(1,1)] + end + -- Default provider handles player names + return self.idp["*"] +end + +function acls:check_access(channel, name, minimum, default) + local role = self:get_role(channel, name, default) + if role == "deny" or (not role and minimum) then + return false, "ERROR: You do not have " .. (minimum and minimum or "any") .. " access to #" .. channel .. "." + elseif minimum then + if (self.rolenum[minimum] or 0) < self.rolenum[role] then + return false, "ERROR: You do not have " .. minimum .. " access to #" .. channel .. "." + end + end + -- No role and no minimum will fall through intentionally. +end + +-- Get channel role for actual permissions +function acls:get_role(channel, name, default) + if beerchat.channels[channel] and beerchat.channels[channel].owner == name then + return "owner" + end + local acl = self.data[channel] + if acl then + -- First check for name based roles and fallback role if explicitly asking for it + if name == "*" or acl[name] then + return acl[name] + end + -- Check for roles defined by identity providers if name is valid player name + if is_valid_player_name(name) then + for _, provider in pairs(self.idp) do + local role = provider:get_role(channel, name) + if role then + return role + end + end + end + -- Check for and return wildcard role if configured + if acl["*"] then + return acl["*"] + end + end + -- Return default value if any given, nil otherwise + return default +end + +-- Get exact channel role without fallback or secondary roles +function acls:get_idp_role(channel, name) + if beerchat.channels[channel] and beerchat.channels[channel].owner == name then + return "owner" + elseif self.data[channel] then + return self.data[channel][name] + end +end + +function acls:set_role(channel, name, role) + if role == nil or self.rolenum[role] then + self.data[channel] = self.data[channel] or {} + if self.data[channel][name] ~= role then + -- Clear caches for role type + local provider = self:get_provider(name) + if provider then + provider.cache[channel] = nil + end + -- Set role + self.data[channel][name] = role + -- Trigger storage + self:write_storage() + end + return true + end + return false +end + +function acls:get_roles(channel) + local acl = self.data[channel] + if acl then + local results = {} + for identity, role in pairs(acl) do + table.insert(results, { + identity = identity, + role = role, + }) + end + return results + end +end + +return function(data, write_fn) + acls.data = data or acls.data + function acls:write_storage() + if self.write_pending then + self.write_pending:cancel() + end + self.write_pending = minetest.after(60, function() + self.write_pending = nil + write_fn(acls.data) + end) + end + return acls +end diff --git a/plugin/acl/chatcommands.lua b/plugin/acl/chatcommands.lua new file mode 100644 index 0000000..bba62d4 --- /dev/null +++ b/plugin/acl/chatcommands.lua @@ -0,0 +1,132 @@ +-- ACL chat commands +-- + +local acls = ... + +local fmt_prefix = "" +local fmt_exists = "Role `${role}` already exists for `${target}` on #${channel}." +local fmt_noneed = "Identity `${target}` had no roles on #${channel}." +local invite_sound = "beerchat_chirp" -- Sound when sending / receiving an invite to a channel + +local function align(s, w, r) + s = tostring(s) + return s .. string.rep(r, w - #s) +end + +local function list_acls(channel) + local roles = acls:get_roles(channel) + if roles then + local result = {} + local c1_length = 8 -- "Identity" header + local c2_length = 4 -- "Role" header + if #roles == 1 then + c1_length = math.max(#roles[1].identity, c1_length) + c2_length = math.max(#roles[1].role, c2_length) + else + table.sort(roles, function(a, b) + c1_length = math.max(#a.identity, #b.identity, c1_length) + c2_length = math.max(#a.role, #b.role, c2_length) + return a.identity ~= "*" and a.identity:lower() < b.identity:lower() + end) + end + c1_length = c1_length + 1 -- Gap between identity and role + table.insert(result, align("Identity", c1_length, " ") .. "Role") + table.insert(result, align("", c1_length + c2_length, "-")) + for _, aclrole in ipairs(roles) do + table.insert(result, align(aclrole.identity, c1_length, " ") .. aclrole.role) + end + return table.concat(result, "\n") + end + return "Channel #"..channel.." is not using any access control lists." +end + +local channel_acl = { + params = " [[-d] [Access Role]]", + description = "Invite player to channel or manage permissions on channel . Use -d to remove identity" + .. " from channel. You must be at least channel manager in order to invite others or manage permissions.\n" + .. "Identity can be a player name or privilege. Privileges must be prefixed with $, for example: $interact.\n" + .. "Possible access roles are: deny, read, write (default), manager or owner.", + func = function(name, param) + if not param or param == "" then + return false, "ERROR: Invalid arguments. Please supply at least the channel name and the player name.\n" + .. "Identity can be player name or privilege if prefixed with $, for example: $interact." + .. "Basic access roles are: deny, read, write (default), manager or owner." + end + + local data, delete = {} + do -- parse arguments + local args, role + data.channel, args = param:match("#?(%S+)%s*(.*)") + delete, param = args:match("^(%-d)%s*(.*)$") + if param then + data.target, role = param:match("(%S+)%s*(%S*)") + else + data.target, role = args:match("(%S+)%s*(%S*)") + end + if not delete and role then + data.role = role ~= "" and role or "write" + end + end + + if not data.channel or data.channel == "" then + return false, "ERROR: Channel name is empty." + elseif not beerchat.channels[data.channel] then + return false, "ERROR: Channel #" .. data.channel .. " does not exist." + elseif not delete and not data.target and not data.role then + return true, list_acls(data.channel) + elseif not data.target or data.target == "" then + return false, "ERROR: Identity not supplied or empty." + end + + local provider = acls:get_provider(data.target) + if not provider or not provider:validate(data.target) then + return false, "ERROR: Invalid identity." + end + + -- Callbacks to check access and possibly modify original request + if not beerchat.execute_callbacks('before_invite', name, data) then + return true -- Assume that callback handler already handled error messages + end + + -- Exact identity provider role, here we want exact match instead of access control match + data.old_role = acls:get_idp_role(data.channel, data.target) + if data.old_role == data.role then + -- Nothing to do, target already has or does not have requested role + return true, beerchat.format_string(delete and fmt_noneed or fmt_exists, data) + end + + if not acls:set_role(data.channel, data.target, data.role) then + -- Failed setting role for target + return false, "ERROR: Could not set role `" .. data.role .. "` for " .. data.target + end + + -- Notifications for invitations / new roles, identity provider decides if message should be sent + if data.role and not data.old_role then + local message = provider:get_invite_message(name, data) + if message and beerchat.allow_private_message(name, data.target) then + -- To player receiving new role, not for deletions / updates + beerchat.sound_play(data.target, invite_sound) + minetest.chat_send_player(data.target, beerchat.format_message(fmt_prefix .. message, { + channel = data.channel, + from_player = name, + to_player = data.target, + })) + end + end + + -- Feedback to the operator, identity provider decides if message should be sent + local message = provider:get_feedback_message(name, data) + if message then + minetest.chat_send_player(name, beerchat.format_message(fmt_prefix .. message, { + channel = data.channel, + from_player = name, + to_player = data.target, + })) + end + end +} + +minetest.register_chatcommand("channel_acl", channel_acl) +minetest.register_chatcommand("ca", channel_acl) +minetest.register_chatcommand("invite_channel", channel_acl) +minetest.register_chatcommand("ic", channel_acl) diff --git a/plugin/acl/password.lua b/plugin/acl/password.lua new file mode 100644 index 0000000..35336de --- /dev/null +++ b/plugin/acl/password.lua @@ -0,0 +1,40 @@ +-- Channel password handler +-- + +local function protected_join(name, password, data) + if type(data) ~= "table" or type(data.channel) ~= "string" or data.channel == "" then + minetest.log("warning", "Invalid password_requests data for player '" .. name .. "'") + minetest.chat_send_player(name, "ERROR: Something went wrong with authorization, please try joining again.") + return + end + local channel = beerchat.channels[data.channel] + if type(channel) ~= "table" then + minetest.chat_send_player(name, "ERROR: Channel #"..data.channel.." disappeared while joining.") + return + end + if password == channel.password then + minetest.chat_send_player(name, "OK: Channel password accepted for #"..data.channel..".") + ;(data.set_default and beerchat.set_player_channel or beerchat.add_player_channel)(name, data.channel) + else + minetest.chat_send_player(name, "ERROR: Invalid password, please verify password and try joining again.") + end +end + +return function(name, data) + local channel = beerchat.channels[data.channel] + if channel and channel.password and channel.password ~= "" then + if not data or not data.password or data.password == "" then + -- Channel has password but nothing has provided any password so far, ask player to provide password + beerchat.capture_message(name, function(playername, password) + protected_join(playername, password, { channel = data.channel, set_default = data.set_default }) + end) + return false, minetest.colorize("#f00d00", "ATTENTION:") .. "This channel requires that you supply" + .. " a password. Your next message will be used as a password and hidden from other players.\n" + .. minetest.colorize("#f00d00", "Please enter channel password for #"..data.channel..":") + end + -- External password handling mechanism has already provided password for this channel, verify it + if data.password ~= channel.password then + return false, "ERROR: Invalid password." + end + end +end diff --git a/plugin/acl/players.lua b/plugin/acl/players.lua new file mode 100644 index 0000000..b4b0c52 --- /dev/null +++ b/plugin/acl/players.lua @@ -0,0 +1,54 @@ +--luacheck: no_unused_args +-- ACL extension for player based roles +-- + +local fmt_invited = "${from_player} invited you to join the channel #${channel}." +local fmt_invite = "Sent invite for ${target} to join #${channel} with `${role}` role." +local fmt_denied = "Denied channel access for ${target} at #${channel}." +local fmt_create = "Added default ACL role `${role}` for #${channel}." +local fmt_update = "Updated ACL entry for ${target} from `${old_role}` to `${role}` at #${channel}." +local fmt_delete = "Removed ACL entry for ${target} from channel #${channel}." + +local def = {} + +-- Sequential iterator without index +local function values(t) + local index = 0 + return function() + index = index + 1 + return t[index] + end +end + +function def:identities(name) + return values({self:get_id(name)}) +end + +function def:validate(identity) + return identity == "*" or minetest.get_auth_handler().get_auth(identity) +end + +function def:get_invite_message(from, data) + if data.target ~= "*" and data.role ~= "deny" then + return beerchat.format_string(fmt_invited, data) + end +end + +function def:get_feedback_message(from, data) + if data.role and data.old_role then + -- Updating role + return beerchat.format_string(fmt_update, data) + elseif data.role and data.target == "*" then + -- Adding default role + return beerchat.format_string(fmt_create, data) + elseif data.role then + -- Adding role + return beerchat.format_string(data.role == "deny" and fmt_denied or fmt_invite, data) + elseif data.old_role then + -- Removing role + return beerchat.format_string(fmt_delete, data) + end + -- No meaningful action +end + +return def diff --git a/plugin/acl/privileges.lua b/plugin/acl/privileges.lua new file mode 100644 index 0000000..f5e9bef --- /dev/null +++ b/plugin/acl/privileges.lua @@ -0,0 +1,40 @@ +--luacheck: no_unused_args +-- ACL extension for privilege based roles +-- + +local fmt_delete = "ACL entry removed for `${target}` privilege at #${channel}." +local fmt_update = "ACL entry updated for `${target}` privilege from `${old_role}` to `${role}` at #${channel}." +local fmt_create = "ACL entry added for `${target}` privilege with `${role}` access at #${channel}." + +local def = {} + +function def:identities(name) + return pairs(minetest.get_player_privs(name)) +end + +function def:validate(identity) + if #identity < 2 or identity:sub(1,1) ~= "$" then + return false + end + return minetest.registered_privileges[identity:sub(2)] ~= nil +end + +function def:get_invite_message() + -- noop: do not send invite messages when data.target is privilege instead of player +end + +function def:get_feedback_message(from, data) + if data.role and data.old_role then + -- Updating role + return beerchat.format_string(fmt_update, data) + elseif data.role then + -- Adding role + return beerchat.format_string(fmt_create, data) + elseif data.old_role then + -- Removing role + return beerchat.format_string(fmt_delete, data) + end + -- No meaningful action +end + +return def diff --git a/plugin/hash.lua b/plugin/hash.lua index f0fcab1..51d0b6e 100644 --- a/plugin/hash.lua +++ b/plugin/hash.lua @@ -46,7 +46,7 @@ beerchat.register_callback("before_send", function(target, message, data) -- Apply formatting for channel messages. data.message = beerchat.format_message( beerchat.main_channel_message_string, { - channel_name = data.channel, + channel = data.channel, to_player = target, from_player = data.name, message = message diff --git a/plugin/init.lua b/plugin/init.lua index d25a401..48d0c20 100644 --- a/plugin/init.lua +++ b/plugin/init.lua @@ -20,6 +20,9 @@ load_plugin("me", true) -- Allows switching channels with "#channelname" and sending to channel with "#channelname message here" load_plugin("hash", true) +-- Adds "/channel_acl" command and optional password protection for channels +load_plugin("acl", true) + -- Allows "@player message here" to send private messages to players load_plugin("pm", true) diff --git a/plugin/jail.lua b/plugin/jail.lua index 157af2f..4051bd5 100644 --- a/plugin/jail.lua +++ b/plugin/jail.lua @@ -147,7 +147,14 @@ end) beerchat.register_callback("before_send_on_channel", function(name, msg) if msg.channel ~= beerchat.jail.channel_name and beerchat.is_player_jailed(name) then -- redirect #channel messages sent by jailed players toward jail channel and reconstruct full command. - msg.message = "#"..msg.channel.." "..msg.message + -- preformat message, no fancy stuff like nick colors in jail, generic formatting should be skipped. + msg.message = beerchat.format_string(beerchat.jail.format_string, { + channel = minetest.colorize(color, beerchat.jail.channel_name), + channel_owner = owner, + from_player = name, + message = "#"..msg.channel.." "..msg.message, + time = os.date("%X") + }) msg.channel = beerchat.jail.channel_name end end) diff --git a/plugin/me.lua b/plugin/me.lua index 5eea442..8ae5ab2 100644 --- a/plugin/me.lua +++ b/plugin/me.lua @@ -1,5 +1,5 @@ -local me_message_string = "|#${channel_name}| * ${from_player} ${message}" +local me_message_string = "|#${channel}| * ${from_player} ${message}" minetest.register_chatcommand("me", { params = "", @@ -32,7 +32,7 @@ minetest.register_chatcommand("me", { name = name, message = beerchat.format_message(me_message_string, { to_player = target, - channel_name = channel, + channel = channel, from_player = name, message = msg.message }) diff --git a/router.lua b/router.lua index ff2dee6..63471a5 100644 --- a/router.lua +++ b/router.lua @@ -3,11 +3,20 @@ -- message before allowing any plugin or default handler to actually handle message. local on_chat_message_handlers = {} +local capture_message = {} function beerchat.register_on_chat_message(func) table.insert(on_chat_message_handlers, func) end +minetest.register_on_leaveplayer(function(player) + capture_message[player:get_player_name() or ""] = nil +end) + +function beerchat.capture_message(name, callback) + capture_message[name] = callback +end + local function default_message_handler(msg) -- Do not allow players without shout priv to chat in channels if not minetest.check_player_privs(msg.name, "shout") then @@ -37,6 +46,14 @@ end -- All messages are handled either by sending to channel or through special plugin function. minetest.register_on_chat_message(function(name, message) + -- Execute one shot overrides + local capture_fn = capture_message[name] + if capture_fn then + capture_message[name] = nil + capture_fn(name, message) + return true + end + -- Execute on_receive callbacks allowing modifications to sender and message local msg = beerchat.default_on_receive(name, message) if not msg then diff --git a/spec/fixtures/minetest.conf b/spec/fixtures/minetest.conf index aa94edb..5d593a3 100644 --- a/spec/fixtures/minetest.conf +++ b/spec/fixtures/minetest.conf @@ -1,23 +1,34 @@ # Basic configuration -beerchat.colorize_channels = * -beerchat.moderator_channel_name = mod -# Plugin configuration - -beerchat.enable_alias = true +beerchat.moderator_channel_name = mod -beerchat.enable_announce = true +# Enable plugins +beerchat.enable_mute = true +beerchat.enable_me = true +beerchat.enable_hash = true +beerchat.enable_acl = true +beerchat.enable_pm = true +beerchat.enable_whisper = true +beerchat.enable_jail = true beerchat.enable_ban = true - +beerchat.enable_remote_mute = true beerchat.enable_cleaner = true +beerchat.enable_override = true +beerchat.enable_colorize = true +beerchat.enable_announce = true +beerchat.enable_force2channel = true +beerchat.enable_password = true +beerchat.enable_event-logging = true +beerchat.enable_alias = true -beerchat.enable_jail = true -beerchat.jail.channel_name = jailchannel +# Plugin configuration -beerchat.enable_remote_mute = true +beerchat.jail.channel_name = jailchannel +beerchat.colorize_channels = * # Web relay + secure.http_mods = beerchat beerchat.matterbridge_url = http://matterbridge:4242 beerchat.matterbridge_token = mytoken diff --git a/spec/init_spec.lua b/spec/init_spec.lua index 1dca135..4c5a027 100644 --- a/spec/init_spec.lua +++ b/spec/init_spec.lua @@ -4,20 +4,17 @@ mineunit("core") mineunit("player") mineunit("server") -describe("Mod initialization", function() +describe("Core functionality", function() - it("Wont crash", function() - sourcefile("init") - end) - -end) - -describe("Chatting", function() + sourcefile("init") + mineunit:mods_loaded() local M = function(s) return require("luassert.match").matches(s) end local SX = Player("SX", { shout = 1 }) setup(function() + beerchat.channels["testchannel"] = { owner = "beerholder", color = beerchat.default_channel_color } + beerchat.channels["testchannel2"] = { owner = "SX", color = beerchat.default_channel_color } mineunit:execute_on_joinplayer(SX) end) @@ -36,10 +33,10 @@ describe("Chatting", function() assert.not_nil(beerchat.channels["foo"]) end) - it("switches channels", function() - SX:send_chat_message("#foo") - assert.equals("foo", SX:get_meta():get_string("beerchat:current_channel")) - SX:send_chat_message("Everyone ignore me, this is just a test") + it("joins channel", function() + assert.is_nil(beerchat.playersChannels["SX"]["testchannel"]) + SX:send_chat_message("/jc testchannel") + assert.not_nil(beerchat.playersChannels["SX"]["testchannel"]) end) it("deletes channel", function() @@ -47,4 +44,19 @@ describe("Chatting", function() assert.is_nil(beerchat.channels["foo"]) end) + it("lists channels", function() + SX:send_chat_message("/jc testchannel") + SX:send_chat_message("/jc testchannel2") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/mc") + assert.spy(minetest.chat_send_player).called_with("SX", M("testchannel.+testchannel")) + end) + + it("lists channel information", function() + SX:send_chat_message("/jc testchannel") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/mc testchannel") + assert.spy(minetest.chat_send_player).called_with("SX", M("beerholder")) + end) + end) diff --git a/spec/mineunit.conf b/spec/mineunit.conf new file mode 100644 index 0000000..3a5f465 --- /dev/null +++ b/spec/mineunit.conf @@ -0,0 +1 @@ +time_step = 100 diff --git a/spec/plugin_acl_acls_spec.lua b/spec/plugin_acl_acls_spec.lua new file mode 100644 index 0000000..990f7d5 --- /dev/null +++ b/spec/plugin_acl_acls_spec.lua @@ -0,0 +1,206 @@ +require("mineunit") + +mineunit("core") +mineunit("player") +mineunit("server") + +describe("ACL/acls", function() + + local M = function(s) return require("luassert.match").matches(s) end + + -- Test bare acls.lua with privilege extension, assuming no dependencies other than channels table. + _G.beerchat = { + channels = { + TEST = {} + } + } + + local acls + local SX = Player("SX", { shout = 1, fast = 1 }) + setup(function() mineunit:execute_on_joinplayer(SX) end) + teardown(function() mineunit:execute_on_leaveplayer(SX) end) + before_each(function() + acls = sourcefile("plugin/acl/acls")(nil, function() end) + acls:register_provider("*", dofile("plugin/acl/players.lua")) + acls:register_provider("$", dofile("plugin/acl/privileges.lua")) + end) + + it("acls:set_role", function() + acls:set_role("TEST", "SX", "read") + assert.same(acls.data.TEST, { SX = "read" }) + end) + + it("acls:set_role privilege", function() + acls:set_role("TEST", "$fast", "write") + assert.same(acls.data.TEST, { ["$fast"] = "write" }) + end) + + it("acls:get_role", function() + assert.is_nil(acls:get_role("TEST", "SX")) + end) + + it("acls:get_role non player", function() + assert.is_nil(acls:get_role("TEST", "?")) + end) + + it("acls:get_role (privilege extension, empty roles)", function() + local provider = acls:get_provider("$name") + assert.not_nil(provider) + assert.not_equals(acls:get_provider("name"), provider) + assert.is_nil(provider:get_role("TEST", "SX")) + end) + + it("acls:get_role (privilege extension, set/get missing privilege)", function() + local provider = acls:get_provider("$name") + assert.not_nil(provider) + acls:set_role("TEST", "$interact", "read") + assert.not_equals(acls:get_provider("name"), provider) + assert.is_false(provider:get_role("TEST", "SX")) + end) + + it("acls:get_role (privilege extension, set/get valid privilege)", function() + local provider = acls:get_provider("$name") + assert.not_nil(provider) + acls:set_role("TEST", "$shout", "read") + assert.not_equals(acls:get_provider("name"), provider) + assert.equals("read", provider:get_role("TEST", "SX")) + end) + + it("acls:get_role (privilege extension, non player)", function() + local provider = acls:get_provider("$name") + assert.not_nil(provider) + assert.not_equals(acls:get_provider("name"), provider) + assert.is_nil(provider:get_role("TEST", "?")) + end) + + it("acls:get_role (unknown/missing extension)", function() + local provider = acls:get_provider("%name") + assert.is_nil(provider) + end) + + it("acls:write_storage", function() + acls:write_storage() + end) + + it("acls:check_access", function() + assert.is_nil(acls:check_access("TEST", "SX")) + end) + + it("returns correct player role", function() + acls:set_role("TEST", "SX", "deny") + acls:set_role("TEST", "SX", "manager") + assert.equals("manager", acls:get_role("TEST", "SX")) + acls:set_role("TEST", "SX", "deny") + assert.equals("deny", acls:get_role("TEST", "SX")) + end) + + it("returns correct privilege role", function() + acls:set_role("TEST", "$fast", "deny") + acls:set_role("TEST", "$fast", "manager") + assert.equals(acls:get_role("TEST", "SX"), "manager") + acls:set_role("TEST", "$fast", "deny") + assert.equals(acls:get_role("TEST", "SX"), "deny") + end) + + it("acls:check_access deny", function() + acls:set_role("TEST", "$fast", "deny") + assert.is_false(acls:check_access("TEST", "SX")) + assert.is_false(acls:check_access("TEST", "SX", "read")) + assert.is_false(acls:check_access("TEST", "SX", "write")) + assert.is_false(acls:check_access("TEST", "SX", "manager")) + assert.is_false(acls:check_access("TEST", "SX", "owner")) + end) + + it("acls:check_access write", function() + acls:set_role("TEST", "$fast", "write") + assert.is_nil(acls:check_access("TEST", "SX")) + assert.is_nil(acls:check_access("TEST", "SX", "read")) + assert.is_nil(acls:check_access("TEST", "SX", "write")) + assert.is_false(acls:check_access("TEST", "SX", "manager")) + assert.is_false(acls:check_access("TEST", "SX", "owner")) + end) + +end) + +describe("ACL/acls caching", function() + + local M = function(s) return require("luassert.match").matches(s) end + + local acls, SX + + before_each(function() + _G.beerchat = { channels = { TEST = {} } } + acls = sourcefile("plugin/acl/acls")(nil, function() end) + acls:register_provider("*", dofile("plugin/acl/players.lua")) + acls:register_provider("$", dofile("plugin/acl/privileges.lua")) + SX = Player("SX", { shout = 1, fast = 1 }) + mineunit:execute_on_joinplayer(SX) + end) + + after_each(function() + mineunit:execute_on_leaveplayer(SX) + SX = nil + end) + + local function spy_idp_identities(name) + local idp = assert(acls:get_provider(name)) + spy.on(idp, "identities") + return idp + end + + it("acls:get_role caching not needed for names", function() + local idp = spy_idp_identities("SX") + acls:set_role("TEST", "SX", "write") + acls:get_role("TEST", "SX", "read") + local role = acls:get_role("TEST", "SX", "read") + assert.spy(idp.identities).not_called() + assert.equals("write", role) + end) + + it("acls:get_role caching is used for fallback roles", function() + local idp = spy_idp_identities("*") + acls:set_role("TEST", "*", "write") + -- First call populates cache, second call uses cache + local role1 = acls:get_role("TEST", "SX", "read") + local role2 = acls:get_role("TEST", "SX", "read") + assert.spy(idp.identities).called(1) + assert.equals("write", role1) + assert.equals(role1, role2) + end) + + it("acls:get_role caching is used for privilege roles", function() + local idp = spy_idp_identities("$shout") + acls:set_role("TEST", "$shout", "write") + -- First call populates cache, second call uses cache + local role1 = acls:get_role("TEST", "SX", "read") + local role2 = acls:get_role("TEST", "SX", "read") + assert.spy(idp.identities).called(1) + assert.equals("write", role1) + assert.equals(role1, role2) + end) + + it("acls:set_role invalidates caching for fallback roles", function() + local idp = spy_idp_identities("*") + acls:set_role("TEST", "*", "manager") + -- acls:set_role clears caches + local role1 = acls:get_role("TEST", "SX", "read") + acls:set_role("TEST", "*", "write") + local role2 = acls:get_role("TEST", "SX", "read") + assert.spy(idp.identities).called(2) + assert.equals("manager", role1) + assert.equals("write", role2) + end) + + it("acls:set_role invalidates caching for privilege roles", function() + local idp = spy_idp_identities("$shout") + acls:set_role("TEST", "$shout", "manager") + -- acls:set_role clears caches + local role1 = acls:get_role("TEST", "SX", "read") + acls:set_role("TEST", "$shout", "write") + local role2 = acls:get_role("TEST", "SX", "read") + assert.spy(idp.identities).called(2) + assert.equals("manager", role1) + assert.equals("write", role2) + end) + +end) \ No newline at end of file diff --git a/spec/plugin_acl_spec.lua b/spec/plugin_acl_spec.lua new file mode 100644 index 0000000..a58dabd --- /dev/null +++ b/spec/plugin_acl_spec.lua @@ -0,0 +1,758 @@ +require("mineunit") + +mineunit("core") +mineunit("player") +mineunit("server") +mineunit("auth") + +sourcefile("init") + +-- Utility functions to safely clear tables / beerchat data +local function clear_table(t) for k in pairs(t) do t[k] = nil end end +local function reset_beerchat() + clear_table(beerchat.channels) + clear_table(beerchat.playersChannels) + clear_table(beerchat.currentPlayerChannel) + beerchat.channels[beerchat.main_channel_name] = { owner = "SX", color = beerchat.default_channel_color } +end +local TEST_CHANNEL_INDEX = {} +local function NEXT_TEST_CHANNEL_NAME(prefix) + TEST_CHANNEL_INDEX[prefix] = TEST_CHANNEL_INDEX[prefix] and (TEST_CHANNEL_INDEX[prefix] + 1) or 1 + return prefix .. tostring(TEST_CHANNEL_INDEX[prefix]) +end + +describe("ACL basic behavior", function() + + local M = function(s) return require("luassert.match").matches(s) end + local ANY = require("luassert.match")._ + + -- Matcher: operator invited other player to join chat channel + local function INVITED(name, role) return M("[Ii]nvite .+"..name..".+"..role) end + -- Matcher: someone inivites you to join chat channel + local function INVITES(name, channel) return M(name..".+invited .+#"..channel:gsub("(%-)", "%%%1")) end + + -- Test players, reinitialized for each test + local SX, Sam + + before_each(function() + -- Reset all test channels + beerchat.channels["acl-password"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-password-fail"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-invalid-name"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-invalid-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-fallback-read-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-fallback-default-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-fallback-deny-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-default-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-deny-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-owner-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-manager-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-write-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-read-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-update-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-update-privilege-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-delete-role"] = { owner = "SX", color = beerchat.default_channel_color } + beerchat.channels["acl-chat"] = { owner = "SX", color = beerchat.default_channel_color } + -- Recreate players for fresh data + SX = Player("SX", { shout = 1 }) + Sam = Player("Sam", { shout = 1 }) + -- Join players + mineunit:execute_on_joinplayer(SX) + mineunit:execute_on_joinplayer(Sam) + -- Set player channels + beerchat.set_player_channel("SX", "main") + beerchat.set_player_channel("Sam", "main") + end) + + after_each(function() + -- Remove players and reset test channels + mineunit:execute_on_leaveplayer(Sam) + mineunit:execute_on_leaveplayer(SX) + Sam, SX = nil, nil + reset_beerchat() + end) + + it("checks password", function() + SX:send_chat_message("/cc #acl-password,qwerty") + spy.on(minetest, "chat_send_player") + -- Initiate password protected join, it should ask for password and should not join the channel + Sam:send_chat_message("/jc #acl-password") + assert.spy(minetest.chat_send_player).called_with("Sam", M(".+assword.+lease.+assword")) + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.is_nil(beerchat.playersChannels["Sam"]["acl-password"]) + -- Joining succeeds and password is not visible to other players + Sam:send_chat_message("qwerty") + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.not_nil(beerchat.playersChannels["Sam"]["acl-password"]) + -- Next messages will be visible to other players + Sam:send_chat_message("qwerty") + assert.spy(minetest.chat_send_player).called_with("SX", ANY) + end) + + it("checks wrong password", function() + SX:send_chat_message("/cc #acl-password-fail,qwerty") + spy.on(minetest, "chat_send_player") + -- Initiate password protected join, it should ask for password and should not join the channel + Sam:send_chat_message("/jc #acl-password-fail") + assert.spy(minetest.chat_send_player).called_with("Sam", M(".+assword.+lease.+assword")) + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.is_nil(beerchat.playersChannels["Sam"]["acl-password-fail"]) + -- Joining failed and password is not visible to other players + Sam:send_chat_message("foobar") + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.is_nil(beerchat.playersChannels["Sam"]["acl-password-fail"]) + -- Next messages will be visible to other players + Sam:send_chat_message("foobar") + assert.spy(minetest.chat_send_player).called_with("SX", ANY) + end) + + it("/ca handles invalid name", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-invalid-name ?") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR.+ident")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + end) + + it("/ca handles invalid role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-invalid-role Sam ?") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR.+role")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + end) + + it("/ca sets fallback role to write as default role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-fallback-default-role *") + assert.spy(minetest.chat_send_player).called_with("SX", M("write.+#acl%-fallback%-default%-role")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + end) + + it("/ca sets fallback role to read", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-fallback-read-role * read") + assert.spy(minetest.chat_send_player).called_with("SX", M("read.+#acl%-fallback%-read%-role")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + end) + + it("/ca uses write role as default role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-default-role Sam") + assert.spy(minetest.chat_send_player).called_with("SX", INVITED("Sam", "write")) + assert.spy(minetest.chat_send_player).called_with("Sam", INVITES("SX", "acl-default-role")) + end) + + it("/ca sets deny role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-deny-role Sam deny") + assert.spy(minetest.chat_send_player).not_called_with("SX", INVITED("Sam", "deny")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", INVITES("SX", "acl-deny-role")) + assert.spy(minetest.chat_send_player).called_with("SX", M("[Dd]enied.+Sam.+acl%-deny%-role")) + end) + + it("/ca sets owner role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-owner-role Sam owner") + assert.spy(minetest.chat_send_player).called_with("SX", INVITED("Sam", "owner")) + assert.spy(minetest.chat_send_player).called_with("Sam", INVITES("SX", "acl-owner-role")) + end) + + it("/ca sets manager role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-manager-role Sam manager") + assert.spy(minetest.chat_send_player).called_with("SX", INVITED("Sam", "manager")) + assert.spy(minetest.chat_send_player).called_with("Sam", INVITES("SX", "acl-manager-role")) + end) + + it("/ca sets write role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-write-role Sam write") + assert.spy(minetest.chat_send_player).called_with("SX", INVITED("Sam", "write")) + assert.spy(minetest.chat_send_player).called_with("Sam", INVITES("SX", "acl-write-role")) + end) + + it("/ca sets read role", function() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-read-role Sam read") + assert.spy(minetest.chat_send_player).called_with("SX", INVITED("Sam", "read")) + assert.spy(minetest.chat_send_player).called_with("Sam", INVITES("SX", "acl-read-role")) + end) + + it("/ca updates role", function() + SX:send_chat_message("/ca #acl-update-role Sam read") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-update-role Sam manager") + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).called_with("SX", M("pdate.+manager")) + end) + + it("/ca updates privilege role", function() + SX:send_chat_message("/ca #acl-update-privilege-role $shout read") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-update-privilege-role $shout manager") + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).called_with("SX", M("pdate.+manager")) + end) + + it("/ca removes role", function() + SX:send_chat_message("/ca #acl-delete-role Sam manager") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca #acl-delete-role -d Sam") + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).called_with("SX", M("emoved.+elete")) + end) + + it("read role allows reading messages", function() + beerchat.set_player_channel("SX", "acl-chat") + beerchat.set_player_channel("Sam", "acl-chat") + SX:send_chat_message("/ca #acl-chat Sam read") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("Test message") + -- Channel message allowed and delivered + assert.spy(minetest.chat_send_player).called_with("Sam", M("Test message")) + assert.spy(minetest.chat_send_player).called_with("SX", M("Test message")) + end) + + it("read role disallows sending messages", function() + beerchat.set_player_channel("SX", "acl-chat") + beerchat.set_player_channel("Sam", "acl-chat") + SX:send_chat_message("/ca #acl-chat Sam read") + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("Test message") + -- Channel message disallowed and player informed + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("Test message")) + assert.spy(minetest.chat_send_player).called_with("Sam", ANY) + end) + + it("default fallback role allows sending messages", function() + SX:send_chat_message("/ca #acl-fallback-default-role *") + beerchat.set_player_channel("SX", "acl-fallback-default-role") + beerchat.set_player_channel("Sam", "acl-fallback-default-role") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("Test message") + assert.spy(minetest.chat_send_player).called_with("SX", M("Test message")) + assert.spy(minetest.chat_send_player).called_with("Sam", M("Test message")) + end) + + it("read fallback disallows sending messages", function() + SX:send_chat_message("/ca #acl-fallback-read-role * read") + beerchat.set_player_channel("SX", "acl-fallback-read-role") + beerchat.set_player_channel("Sam", "acl-fallback-read-role") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("Test message") + assert.spy(minetest.chat_send_player).not_called_with("SX", ANY) + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("Test message")) + assert.spy(minetest.chat_send_player).called_with("Sam", M("ERROR.+write.+acl%-fallback%-read%-role")) + end) + + it("read fallback role allows joining channel", function() + SX:send_chat_message("/ca #acl-fallback-read-role * read") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("/jc #acl-fallback-read-role") + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("ERROR")) + assert.not_nil(beerchat.playersChannels["Sam"]["acl-fallback-read-role"]) + end) + + it("deny fallback role disallows joining", function() + SX:send_chat_message("/ca #acl-fallback-deny-role * deny") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("/jc #acl-fallback-deny-role") + assert.spy(minetest.chat_send_player).called_with("Sam", M("ERROR.+access.+acl%-fallback%-deny%-role")) + assert.not_nil(beerchat.playersChannels["Sam"]["main"]) + assert.is_nil(beerchat.playersChannels["Sam"]["acl-fallback-deny-role"]) + end) + + it("deny fallback role disallows reading messages", function() + beerchat.set_player_channel("SX", "acl-fallback-deny-role") + beerchat.set_player_channel("Sam", "acl-fallback-deny-role") + SX:send_chat_message("/ca #acl-fallback-deny-role * deny") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + SX:send_chat_message("Test message") + assert.spy(minetest.chat_send_player).called_with("SX", M("Test message")) + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("Test message")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("ERROR")) + end) + + it("deny fallback role disallows sending messages", function() + beerchat.set_player_channel("SX", "acl-fallback-deny-role") + beerchat.set_player_channel("Sam", "acl-fallback-deny-role") + SX:send_chat_message("/ca #acl-fallback-deny-role * deny") + -- Channel message disallowed and player informed + spy.on(minetest, "chat_send_player") + Sam:send_chat_message("Test message") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("Test message")) + assert.spy(minetest.chat_send_player).not_called_with("Sam", M("Test message")) + assert.spy(minetest.chat_send_player).called_with("Sam", M("ERROR.+access.+acl%-fallback%-deny%-role")) + end) + +end) + +describe("ACL chatcommand", function() + + local M = function(s) return require("luassert.match").matches(s) end + local SX + local function SendMessage(msg) + spy.on(minetest, "chat_send_player") + SX:send_chat_message(msg) + end + + local TEST_CHANNEL + before_each(function() + -- Recreate and join players for fresh data + SX = Player("SX", { shout = 1 }) + mineunit:execute_on_joinplayer(SX) + -- Create fresh test channel + TEST_CHANNEL = NEXT_TEST_CHANNEL_NAME("#acl_chatcommand") + SX:send_chat_message("/cc "..TEST_CHANNEL) + end) + + after_each(function() + -- Remove players and reset test channels + mineunit:execute_on_leaveplayer(SX) + SX = nil + reset_beerchat() + end) + + -- Usage: /ca [[-d] [Access Role]] + + it("rejects /ca", function() + SendMessage("/ca") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test", function() + SendMessage("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("rejects /ca #notchannel", function() + SendMessage("/ca #notchannel") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test -d", function() + SendMessage("/ca "..TEST_CHANNEL.." -d") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test @", function() + SendMessage("/ca "..TEST_CHANNEL.." @") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test $", function() + SendMessage("/ca "..TEST_CHANNEL.." $") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test *", function() + SendMessage("/ca "..TEST_CHANNEL.." *") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test * manager", function() + SendMessage("/ca "..TEST_CHANNEL.." * manager") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test SX", function() + SendMessage("/ca "..TEST_CHANNEL.." *") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test SX manager", function() + SendMessage("/ca "..TEST_CHANNEL.." * manager") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test SX notrole", function() + SendMessage("/ca "..TEST_CHANNEL.." * notrole") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test $fly", function() + SendMessage("/ca "..TEST_CHANNEL.." $fly") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test $fly manager", function() + SendMessage("/ca "..TEST_CHANNEL.." $fly manager") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test $banana", function() + SendMessage("/ca "..TEST_CHANNEL.." $banana") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("rejects /ca #test $banana manager", function() + SendMessage("/ca "..TEST_CHANNEL.." $banana manager") + assert.spy(minetest.chat_send_player).called_with("SX", M("ERROR")) + end) + + it("accepts /ca #test -d *", function() + SendMessage("/ca "..TEST_CHANNEL.." -d *") + assert.spy(minetest.chat_send_player).not_called_with("SX", M("ERROR")) + end) + +end) + +describe("ACL integration", function() + + -- Test players, reinitialized for each test + local SX, Sam, Joe + + -- Matchers / helpers + local M = function(s) return require("luassert.match").matches(s) end + local ANY = require("luassert.match")._ + local FROM_SX = M("From .*SX") + local FROM_SAM = M("From .*Sam") + local FROM_JOE = M("From .*Joe") + local DENIED = M("ERROR") + local function SendMessages() + spy.on(minetest, "chat_send_player") + SX:send_chat_message("From SX") + Sam:send_chat_message("From Sam") + Joe:send_chat_message("From Joe") + end + local function Esc(s) + return s:gsub("([^a-zA-Z0-9_])", "%%%1") + end + + setup(function() + minetest.register_privilege("privilegename", "Test privilege") + end) + + local TEST_CHANNEL + before_each(function() + -- Recreate and join players for fresh data + SX = Player("SX", { shout = 1 }) + Sam = Player("Sam", { shout = 1, fly = 1 }) + Joe = Player("Joe", { shout = 1, fast = 1 }) + mineunit:execute_on_joinplayer(SX) + mineunit:execute_on_joinplayer(Sam) + mineunit:execute_on_joinplayer(Joe) + -- Create fresh test channel and switch to it + TEST_CHANNEL = NEXT_TEST_CHANNEL_NAME("#acl_integration") + SX:send_chat_message("/cc "..TEST_CHANNEL) + SX:send_chat_message(TEST_CHANNEL) + Sam:send_chat_message(TEST_CHANNEL) + Joe:send_chat_message(TEST_CHANNEL) + end) + + after_each(function() + -- Remove players and reset test channels + mineunit:execute_on_leaveplayer(Joe) + mineunit:execute_on_leaveplayer(Sam) + mineunit:execute_on_leaveplayer(SX) + SX, Sam, Joe = nil, nil, nil + reset_beerchat() + end) + + it("passess test 1", function() + -- ACL: SX=channel-owner (no ACL) + SendMessages() + -- Everyone can send and receive + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_JOE) + end) + + it("passess test 2", function() + -- ACL: SX=channel-owner SX=deny + SX:send_chat_message("/ca "..TEST_CHANNEL.." SX deny") + SendMessages() + -- Everyone can send and receive, main channel owner (not ACL owner) cannot override their own ownership + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_JOE) + end) + + it("passess test 3", function() + -- ACL: SX=channel-owner Sam=read Joe=write + SX:send_chat_message("/ca "..TEST_CHANNEL.." Sam read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe write") + SendMessages() + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", DENIED) + assert.spy(minetest.chat_send_player).not_called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_JOE) + end) + + it("passess test 4", function() + -- ACL: SX=channel-owner *=read Joe=write + SX:send_chat_message("/ca "..TEST_CHANNEL.." * read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe write") + SendMessages() + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", DENIED) + assert.spy(minetest.chat_send_player).not_called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_JOE) + end) + + it("passess test 5", function() + -- ACL: SX=channel-owner *=deny Joe=read + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + SendMessages() + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", DENIED) + assert.spy(minetest.chat_send_player).not_called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", DENIED) + end) + + it("passess test 6", function() + -- ACL: SX=channel-owner *=deny Joe=read + SX:send_chat_message("/ca "..TEST_CHANNEL.." SX deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Sam read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fly owner") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe write") + SX:send_chat_message("/ca "..TEST_CHANNEL.." * read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast manager") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe write") + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." -d $fly") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." -d Sam read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." -d $fast") + SendMessages() + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", DENIED) + assert.spy(minetest.chat_send_player).not_called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", DENIED) + end) + + it("passess test 7", function() + -- ACL: SX=channel-owner *=deny $fast=read + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + SendMessages() + assert.spy(minetest.chat_send_player).called_with("SX", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_SX) + assert.spy(minetest.chat_send_player).called_with("Joe", FROM_SX) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_SAM) + assert.spy(minetest.chat_send_player).called_with("Sam", DENIED) + assert.spy(minetest.chat_send_player).not_called_with("Joe", FROM_SAM) + assert.spy(minetest.chat_send_player).not_called_with("SX", FROM_JOE) + assert.spy(minetest.chat_send_player).not_called_with("Sam", FROM_JOE) + assert.spy(minetest.chat_send_player).called_with("Joe", DENIED) + end) + + it("passess test 8", function() + -- ACL: SX=channel-owner *=deny + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + -- Check output sorting and formatting: privileges, player names, fallback + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "* deny") + )) + -- Other players shouldn't get any feedback from management actions + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).not_called_with("Joe", ANY) + end) + + it("passess test 9", function() + -- ACL: SX=channel-owner *=deny + SX:send_chat_message("/ca "..TEST_CHANNEL.." $privilegename deny") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + -- Check output sorting and formatting: privileges, player names, fallback + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$privilegename deny") + )) + -- Other players shouldn't get any feedback from management actions + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).not_called_with("Joe", ANY) + end) + + it("passess test 10", function() + -- ACL: SX=channel-owner *=deny $fast=read Sam=manager + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Sam manager") + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + -- Check output sorting and formatting: privileges, player names, fallback + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "Sam manager\n" .. + "* deny") + )) + -- Other players shouldn't get any feedback from management actions + assert.spy(minetest.chat_send_player).not_called_with("Sam", ANY) + assert.spy(minetest.chat_send_player).not_called_with("Joe", ANY) + end) + + it("passess test 11", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through privilege role. + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "Joe read\n" .. + "* deny") + )) + end) + + it("passess test 12", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through privilege role. + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "Joe read\n" .. + "* deny") + )) + end) + + it("passess test 13", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through cached privilege role. + Joe:send_chat_message("/lc "..TEST_CHANNEL) + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + -- Initialize caches by reading ACLs once + Joe:send_chat_message(TEST_CHANNEL) + -- Add role that is already provided to Joe through $fast privilege + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "Joe read\n" .. + "* deny") + )) + end) + + it("passess test 14", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through cached privilege role. + Joe:send_chat_message("/lc "..TEST_CHANNEL) + SX:send_chat_message("/ca "..TEST_CHANNEL.." Joe read") + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + -- Initialize caches by reading ACLs once + Joe:send_chat_message(TEST_CHANNEL) + -- Add role that is already provided to Joe through player name + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "Joe read\n" .. + "* deny") + )) + end) + + it("passess test 15", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through privilege role. + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + -- Add role that is already provided to Joe through another privilege role + SX:send_chat_message("/ca "..TEST_CHANNEL.." $shout write") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "$shout write\n" .. + "* deny") + )) + end) + + it("passess test 16", function() + -- ACL: SX=channel-owner *=deny $fast=read Joe=read + -- Purpose of this test is to make sure that player name can be added + -- even if player already has access through cached privilege role. + Joe:send_chat_message("/lc "..TEST_CHANNEL) + SX:send_chat_message("/ca "..TEST_CHANNEL.." * deny") + SX:send_chat_message("/ca "..TEST_CHANNEL.." $fast read") + -- Initialize caches by reading ACLs once + Joe:send_chat_message(TEST_CHANNEL) + -- Add role that is already provided to Joe through another privilege role + SX:send_chat_message("/ca "..TEST_CHANNEL.." $shout write") + -- Validate channel ACL: sorting, formatting, privileges, player names, fallback + spy.on(minetest, "chat_send_player") + SX:send_chat_message("/ca "..TEST_CHANNEL) + assert.spy(minetest.chat_send_player).called_with("SX", M( + "Identity Role\n" .. + "%-+\n" .. Esc( + "$fast read\n" .. + "$shout write\n" .. + "* deny") + )) + end) + +end) \ No newline at end of file diff --git a/spec/plugin_ban_spec.lua b/spec/plugin_ban_spec.lua index 3939908..76952d6 100644 --- a/spec/plugin_ban_spec.lua +++ b/spec/plugin_ban_spec.lua @@ -62,7 +62,6 @@ describe("channel_ban command", function() end) it("handles invalid player name", function() - pending("Mineunit auth handler raises exception for invalid names, this test wont work") SX:send_chat_message("/channel_ban ***") end) @@ -110,7 +109,7 @@ describe("channel ban", function() SX:send_chat_message("/channel_ban "..XX_name) spy.on(beerchat, "send_on_channel") spy.on(beerchat, "execute_callbacks") - spy.on(minetest, "chat_send_player") beerchat.register_callback("on_send_on_channel", print) + spy.on(minetest, "chat_send_player") SX:send_chat_message("test") assert.spy(beerchat.send_on_channel).was.called() assert.spy(beerchat.execute_callbacks).was.called_with("on_send_on_channel", "SX", ANY, ANY)