diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:36:39 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-02 19:36:39 +0900 |
| commit | 288d1e4a4babc136e8b3837d19970cc37023eb8e (patch) | |
| tree | 79eba159ba5f04789384895d30c45039f6053fd6 | |
| parent | bf62ebe35653492c123c729ae03cb32d8e20e2e6 (diff) | |
| download | LunaticChat-288d1e4a4babc136e8b3837d19970cc37023eb8e.tar.gz LunaticChat-288d1e4a4babc136e8b3837d19970cc37023eb8e.tar.bz2 LunaticChat-288d1e4a4babc136e8b3837d19970cc37023eb8e.zip | |
refactor: move channel moderation rules out of the commands
The rule "owners and moderators may kick and ban" was written out six
times, as `senderRole == null || senderRole == ChannelRole.MEMBER`, while
ChannelMembershipManager.hasRole - which encodes the OWNER > MODERATOR >
MEMBER hierarchy in one place and has tests - had no production caller at
all. Letting moderators ban meant editing six files and hoping none was
missed; a miss is a silent privilege change.
The bypass and self-invite rules had drifted further still: the engine
defines ChannelPlayerBypassBanException, ChannelPlayerBypassKickException
and ChannelCannotInviteSelfException, but nothing threw them. The checks
lived inline in the commands, and ChannelBanCommand carried a catch arm
for an exception that could never arrive. Those three rules now live on
ChannelMembershipManager and throw what the engine already declared, so
every caller is held to them rather than only the command path.
ChannelInviteCommand also pre-checked for a banned target, which
joinChannel checks again a moment later and reports through the same
message; the pre-check is gone.
What stays in the commands is what belongs there: parsing an argument and
choosing which message to show. ChannelSubCommand names the steps they
share and derives message keys from the subcommand's own literal.
The tests followed the rules: bypass and self-invite are now asserted
against ChannelMembershipManager, with a bypass predicate injected so the
manager stays testable without a running server.
Co-Authored-By: Claude <noreply@anthropic.com>
15 files changed, 354 insertions, 373 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt index 07ce839..3833376 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt @@ -2,21 +2,38 @@ package dev.m1sk9.lunaticChat.paper.chat.channel import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerMembershipLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.engine.exception.ChannelRuntimeException +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig +import org.bukkit.Bukkit import java.util.UUID import java.util.logging.Logger +/** + * Whether the player is currently online and holds the moderation bypass permission. + * Offline players cannot be checked, matching how the permission is evaluated elsewhere. + */ +private fun hasChannelBypassPermission(playerId: UUID): Boolean = + Bukkit.getPlayer(playerId)?.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode) == true + +/** + * @param hasModerationBypass Whether a player is exempt from being kicked or banned. Injected + * rather than read from Bukkit inline so this manager stays testable without a running server. + */ class ChannelMembershipManager( private val channelManager: ChannelManager, private val logger: Logger, private val config: ChannelChatFeatureConfig, + private val hasModerationBypass: (UUID) -> Boolean = ::hasChannelBypassPermission, ) { /** * Checks if a player is a member of a channel. @@ -264,6 +281,58 @@ class ChannelMembershipManager( } /** + * Bans [targetId] from [channelId]. + * + * The bypass rule lives here rather than in the ban command so that every path to a ban - + * commands today, anything else later - is held to it. + * + * @return Result indicating success or failure. + * @throws ChannelPlayerBypassBanException if the target is exempt from moderation. + */ + fun banPlayer( + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (hasModerationBypass(targetId)) { + return Result.failure(ChannelPlayerBypassBanException(targetId, channelId)) + } + return channelManager.banPlayer(channelId, targetId).map { } + } + + /** + * Removes [targetId] from [channelId] as a moderation action. + * + * @return Result indicating success or failure. + * @throws ChannelPlayerBypassKickException if the target is exempt from moderation. + */ + fun kickPlayer( + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (hasModerationBypass(targetId)) { + return Result.failure(ChannelPlayerBypassKickException(targetId, channelId)) + } + return channelManager.removeMember(channelId, targetId) + } + + /** + * Adds [targetId] to [channelId] on [actorId]'s invitation, bypassing the private-channel gate. + * + * @return Result indicating success or failure. + * @throws ChannelCannotInviteSelfException if the actor invited themselves. + */ + fun inviteToChannel( + actorId: UUID, + targetId: UUID, + channelId: String, + ): Result<Unit> { + if (actorId == targetId) { + return Result.failure(ChannelCannotInviteSelfException(actorId)) + } + return joinChannel(targetId, channelId, bypassPrivateCheck = true) + } + + /** * Gets all channels where the player is a member. * * @param playerId The UUID of the player. diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt index a372b26..a57c92d 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt @@ -4,7 +4,6 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode @@ -14,7 +13,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -24,11 +22,11 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelBanCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "ban" override val permissionNode = LunaticChatPermissionNode.ChannelBan @@ -70,90 +68,35 @@ class ChannelBanCommand( ): CommandResult { val sender = ctx.requirePlayer() - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.ban.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return fail("channel.ban.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotBanSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) + val targetName = target.name ?: playerName - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.ban.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if banning self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.ban.cannotBanSelf") - } - - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return fail( - "channel.ban.cannotBanBypass", - mapOf("player" to onlineTargetPlayer.name), - ) - } - - // Ban player from channel - val banResult = channelManager.banPlayer(channelId, targetPlayerId) - return banResult.fold( + return membershipManager.banPlayer(target.uniqueId, channelId).fold( onSuccess = { - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId + val channelName = channelNameOf(channelId) - // Send notification to banned player if online - onlineTargetPlayer?.let { player -> - player.sendMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.ban.wasBanned", - mapOf("channel" to channelName, "banner" to sender.name), - ), + Bukkit.getPlayer(playerName)?.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.ban.wasBanned", + mapOf("channel" to channelName, "banner" to sender.name), ), - ) - } - - // Broadcast ban notification to remaining members + ), + ) notificationHandler.broadcastBan(channelId, playerName, sender.name) - ok( - "channel.ban.success", - mapOf("player" to playerName, "channel" to channelName), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - fail("channel.ban.error") - } - is ChannelPlayerBypassBanException -> { - fail( - "channel.ban.cannotBanBypass", - mapOf("player" to playerName), - ) - } - is ChannelPlayerAlreadyBannedException -> { - fail( - "channel.ban.alreadyBanned", - mapOf("player" to playerName), - ) - } - else -> { - fail("channel.ban.error") - } + is ChannelPlayerBypassBanException -> failHere("cannotBanBypass", mapOf("player" to targetName)) + is ChannelPlayerAlreadyBannedException -> failHere("alreadyBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt index e645967..03fb738 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt @@ -4,8 +4,8 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberLimitExceededException -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -13,7 +13,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -23,10 +22,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelInviteCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "invite" override val permissionNode = LunaticChatPermissionNode.ChannelInvite override val aliases = listOf("inv") @@ -61,46 +60,17 @@ class ChannelInviteCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.invite.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return fail("channel.invite.noPermission") - } - - // Find target player val targetPlayer = Bukkit.getPlayer(playerName) - ?: return fail( - "channel.invite.playerNotFound", - mapOf("player" to playerName), - ) - - // Check if inviting self - if (targetPlayer.uniqueId == sender.uniqueId) { - return fail("channel.invite.cannotInviteSelf") - } + ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Check if player is banned - val isBanned = channelManager.isPlayerBanned(channelId, targetPlayer.uniqueId).getOrElse { false } - if (isBanned) { - return fail( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ) - } - - // Attempt to join the target player to the channel (bypass private check for invites) - val result = membershipManager.joinChannel(targetPlayer.uniqueId, channelId, bypassPrivateCheck = true) + val result = membershipManager.inviteToChannel(sender.uniqueId, targetPlayer.uniqueId, channelId) return result.fold( onSuccess = { - // Send success message to sender - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId + val channelName = channelNameOf(channelId) // Send notification to invited player targetPlayer.sendMessage( @@ -112,31 +82,14 @@ class ChannelInviteCommand( ), ) - ok( - "channel.invite.success", - mapOf("player" to targetPlayer.name, "channel" to channelName), - ) + okHere("success", mapOf("player" to targetPlayer.name, "channel" to channelName)) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - fail("channel.invite.error") - } - is ChannelMemberLimitExceededException -> { - fail( - "channel.invite.channelFull", - mapOf("limit" to error.limit.toString()), - ) - } - is ChannelPlayerBannedException -> { - fail( - "channel.invite.playerBanned", - mapOf("player" to targetPlayer.name), - ) - } - else -> { - fail("channel.invite.error") - } + is ChannelCannotInviteSelfException -> failHere("cannotInviteSelf") + is ChannelMemberLimitExceededException -> failHere("channelFull", mapOf("limit" to error.limit.toString())) + is ChannelPlayerBannedException -> failHere("playerBanned", mapOf("player" to targetPlayer.name)) + else -> failHere("error") } }, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt index 7aae219..fa1c4d7 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt @@ -4,6 +4,7 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -11,7 +12,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -21,11 +21,11 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelKickCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, private val notificationHandler: ChannelNotificationHandler, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "kick" override val permissionNode = LunaticChatPermissionNode.ChannelKick override val aliases = listOf("k") @@ -68,87 +68,45 @@ class ChannelKickCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.kick.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return fail("channel.kick.noPermission") - } - - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.kick.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if kicking self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.kick.cannotKickSelf") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotKickSelf") - // Check if target has bypass permission - val onlineTargetPlayer = Bukkit.getPlayer(playerName) - if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { - return fail( - "channel.kick.cannotKickBypass", - mapOf("player" to onlineTargetPlayer.name), - ) - } + val targetId = target.uniqueId + val targetName = target.name ?: playerName - // Check if target is a member - val isMember = membershipManager.isMember(targetPlayerId, channelId).getOrElse { false } - if (!isMember) { - return fail( - "channel.kick.notMember", - mapOf("player" to playerName), - ) + if (!membershipManager.isMember(targetId, channelId).getOrElse { false }) { + return failHere("notMember", mapOf("player" to playerName)) } - // Remove from channel - val removeResult = channelManager.removeMember(channelId, targetPlayerId) - return removeResult.fold( + return membershipManager.kickPlayer(targetId, channelId).fold( onSuccess = { // Clear their active channel if this was it - if (channelManager.getPlayerChannel(targetPlayerId) == channelId) { - channelManager.setPlayerChannel(targetPlayerId, null) + if (channelManager.getPlayerChannel(targetId) == channelId) { + channelManager.setPlayerChannel(targetId, null) } - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId + val channelName = channelNameOf(channelId) - // Send notification to kicked player if online - onlineTargetPlayer?.let { player -> - player.sendMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.kick.wasKicked", - mapOf("channel" to channelName, "kicker" to sender.name), - ), + Bukkit.getPlayer(playerName)?.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.kick.wasKicked", + mapOf("channel" to channelName, "kicker" to sender.name), ), - ) - } - - // Broadcast kick notification to remaining members + ), + ) notificationHandler.broadcastKick(channelId, playerName, sender.name) - ok( - "channel.kick.success", - mapOf("player" to playerName, "channel" to channelName), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelName)) }, onFailure = { error -> - fail("channel.kick.error") + when (error) { + is ChannelPlayerBypassKickException -> failHere("cannotKickBypass", mapOf("player" to targetName)) + else -> failHere("error") + } }, ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt index 5a6ada8..baf9d5c 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt @@ -10,7 +10,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -20,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelModCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "mod" override val permissionNode = LunaticChatPermissionNode.ChannelMod @@ -65,43 +64,16 @@ class ChannelModCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.mod.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.OWNER)?.let { return it } - // Check if sender is OWNER - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole != ChannelRole.OWNER) { - return fail("channel.mod.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotModSelf") - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.mod.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Check if modding self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.mod.cannotModSelf") - } - - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return fail( - "channel.mod.notMember", - mapOf("player" to playerName), - ) - } + val targetPlayerId = target.uniqueId + val targetRole = + membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) + ?: return failHere("notMember", mapOf("player" to playerName)) // Toggle mod status val newRole = diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt index 3236db4..374684b 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt @@ -10,7 +10,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -20,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelOwnershipCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "ownership" override val permissionNode = LunaticChatPermissionNode.ChannelOwnership override val aliases = listOf("own") @@ -66,42 +65,15 @@ class ChannelOwnershipCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.ownership.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.OWNER)?.let { return it } - // Check if sender is OWNER - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole != ChannelRole.OWNER) { - return fail("channel.ownership.noPermission") - } - - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.ownership.playerNotFound", - mapOf("player" to playerName), - ) - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) + if (target.uniqueId == sender.uniqueId) return failHere("cannotTransferToSelf") - val targetPlayerId = targetPlayer.uniqueId - - // Check if transferring to self - if (targetPlayerId == sender.uniqueId) { - return fail("channel.ownership.cannotTransferToSelf") - } - - // Check if target is a member - val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) - if (targetRole == null) { - return fail( - "channel.ownership.notMember", - mapOf("player" to playerName), - ) + val targetPlayerId = target.uniqueId + if (membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) == null) { + return failHere("notMember", mapOf("player" to playerName)) } // Transfer ownership diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt new file mode 100644 index 0000000..0075970 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt @@ -0,0 +1,61 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole +import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.entity.Player +import java.util.UUID + +/** + * A subcommand of `/lc channel`. + * + * The moderation subcommands all open with the same sequence - resolve the sender's active + * channel, check their role, resolve the named target - once written out in full in each of them. + * These helpers name each step, and derive their message keys from [literal] so a subcommand + * cannot accidentally report another one's text. + */ +abstract class ChannelSubCommand( + plugin: LunaticChat, + protected val channelManager: ChannelManager, + protected val membershipManager: ChannelMembershipManager, +) : LunaticSubCommand(plugin) { + /** A failure carrying `channel.<literal>.<suffix>`. */ + protected fun failHere( + suffix: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = fail("channel.$literal.$suffix", args) + + /** A success carrying `channel.<literal>.<suffix>`. */ + protected fun okHere( + suffix: String, + args: Map<String, String> = emptyMap(), + ): CommandResult = ok("channel.$literal.$suffix", args) + + /** The channel [sender] is currently talking in, or null if they have none. */ + protected fun activeChannelOf(sender: Player): String? = channelManager.getPlayerChannel(sender.uniqueId) + + /** The display name of [channelId], falling back to the id when the channel is gone. */ + protected fun channelNameOf(channelId: String): String = channelManager.getChannel(channelId).getOrNull()?.name ?: channelId + + /** + * Null when [playerId] holds [role] or higher in [channelId]; otherwise the "no permission" + * failure for this subcommand. + */ + protected fun denyUnlessRole( + playerId: UUID, + channelId: String, + role: ChannelRole, + ): CommandResult? = if (membershipManager.hasRole(playerId, channelId, role).getOrDefault(false)) null else failHere("noPermission") + + /** + * The named player if the server has ever seen them, or null. Offline players are resolvable + * because bans and role changes must work while the target is away. + */ + protected fun knownPlayer(playerName: String): OfflinePlayer? = + Bukkit.getOfflinePlayer(playerName).takeIf { it.hasPlayedBefore() || it.isOnline } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt index 43aeaf2..c361d62 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt @@ -4,7 +4,6 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult -import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -12,7 +11,6 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext -import dev.m1sk9.lunaticChat.paper.command.core.LunaticSubCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands @@ -21,10 +19,10 @@ import org.bukkit.Bukkit @PlayerOnly class ChannelUnbanCommand( plugin: LunaticChat, - private val channelManager: ChannelManager, - private val membershipManager: ChannelMembershipManager, + channelManager: ChannelManager, + membershipManager: ChannelMembershipManager, override val languageManager: LanguageManager, -) : LunaticSubCommand(plugin) { +) : ChannelSubCommand(plugin, channelManager, membershipManager) { override val literal = "unban" override val permissionNode = LunaticChatPermissionNode.ChannelUnban @@ -64,56 +62,19 @@ class ChannelUnbanCommand( ): CommandResult { val sender = ctx.requirePlayer() - // Get sender's active channel - val channelId = - channelManager.getPlayerChannel(sender.uniqueId) - ?: return fail("channel.unban.noActiveChannel") + val channelId = activeChannelOf(sender) ?: return failHere("noActiveChannel") + denyUnlessRole(sender.uniqueId, channelId, ChannelRole.MODERATOR)?.let { return it } - // Check if sender has permission (OWNER or MODERATOR) - val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) - if (senderRole == null || senderRole == ChannelRole.MEMBER) { - return fail("channel.unban.noPermission") - } + val target = knownPlayer(playerName) ?: return failHere("playerNotFound", mapOf("player" to playerName)) - // Find target player - val targetPlayer = Bukkit.getOfflinePlayer(playerName) - - // Check if player exists (has played before or is online) - if (!targetPlayer.hasPlayedBefore() && !targetPlayer.isOnline) { - return fail( - "channel.unban.playerNotFound", - mapOf("player" to playerName), - ) - } - - val targetPlayerId = targetPlayer.uniqueId - - // Unban player from channel - val unbanResult = channelManager.unbanPlayer(channelId, targetPlayerId) - return unbanResult.fold( + return channelManager.unbanPlayer(channelId, target.uniqueId).fold( onSuccess = { - val channel = channelManager.getChannel(channelId).getOrNull() - val channelName = channel?.name ?: channelId - - ok( - "channel.unban.success", - mapOf("player" to playerName, "channel" to channelName), - ) + okHere("success", mapOf("player" to playerName, "channel" to channelNameOf(channelId))) }, onFailure = { error -> when (error) { - is ChannelNotFoundException -> { - fail("channel.unban.error") - } - is ChannelPlayerNotBannedException -> { - fail( - "channel.unban.playerNotBanned", - mapOf("player" to playerName), - ) - } - else -> { - fail("channel.unban.error") - } + is ChannelPlayerNotBannedException -> failHere("playerNotBanned", mapOf("player" to playerName)) + else -> failHere("error") } }, ) diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt index 1fcc39b..28e1c82 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt @@ -3,10 +3,13 @@ package dev.m1sk9.lunaticChat.paper.chat.channel import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotMemberException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassBanException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBypassKickException import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerMembershipLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.paper.TestUtils @@ -15,6 +18,7 @@ import dev.m1sk9.lunaticChat.paper.TestUtils.createTestUUID import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import io.mockk.every import io.mockk.mockk +import java.util.UUID import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -28,6 +32,7 @@ class ChannelMembershipManagerTest { maxChannelsPerServer: Int = 10, maxMembersPerChannel: Int = 50, maxMembershipPerPlayer: Int = 5, + playersWithBypass: Set<UUID> = emptySet(), ): Triple<ChannelMembershipManager, ChannelManager, TestUtils.TestLogger> { val logger = TestUtils.TestLogger() val storage = mockk<ChannelStorage>(relaxed = true) @@ -44,7 +49,8 @@ class ChannelMembershipManagerTest { val channelManager = ChannelManager(storage, logger, config) channelManager.initialize() - val membershipManager = ChannelMembershipManager(channelManager, logger, config) + val membershipManager = + ChannelMembershipManager(channelManager, logger, config, hasModerationBypass = { it in playersWithBypass }) return Triple(membershipManager, channelManager, logger) } @@ -381,4 +387,89 @@ class ChannelMembershipManagerTest { assertTrue(channels.contains("ch1")) assertTrue(channels.contains("ch2")) } + + @Test + fun `banPlayer refuses a target holding the moderation bypass`() { + val ownerId = createTestUUID(1) + val protectedPlayer = createTestUUID(2) + val (membership, channelManager, _) = createManagers(playersWithBypass = setOf(protectedPlayer)) + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + val result = membership.banPlayer(protectedPlayer, "mod-ch") + + assertIs<ChannelPlayerBypassBanException>(result.exceptionOrNull()) + assertFalse(channelManager.isPlayerBanned("mod-ch", protectedPlayer).getOrThrow()) + } + + @Test + fun `banPlayer bans a target without the bypass`() { + val ownerId = createTestUUID(1) + val playerId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + assertTrue(membership.banPlayer(playerId, "mod-ch").isSuccess) + assertTrue(channelManager.isPlayerBanned("mod-ch", playerId).getOrThrow()) + } + + @Test + fun `kickPlayer refuses a target holding the moderation bypass`() { + val ownerId = createTestUUID(1) + val protectedPlayer = createTestUUID(2) + val (membership, channelManager, _) = createManagers(playersWithBypass = setOf(protectedPlayer)) + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.joinChannel(protectedPlayer, "mod-ch") + + val result = membership.kickPlayer(protectedPlayer, "mod-ch") + + assertIs<ChannelPlayerBypassKickException>(result.exceptionOrNull()) + assertTrue(membership.isMember(protectedPlayer, "mod-ch").getOrThrow()) + } + + @Test + fun `kickPlayer removes a target without the bypass`() { + val ownerId = createTestUUID(1) + val playerId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.joinChannel(playerId, "mod-ch") + + assertTrue(membership.kickPlayer(playerId, "mod-ch").isSuccess) + assertFalse(membership.isMember(playerId, "mod-ch").getOrThrow()) + } + + @Test + fun `inviteToChannel refuses an actor inviting themselves`() { + val ownerId = createTestUUID(1) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + + val result = membership.inviteToChannel(ownerId, ownerId, "mod-ch") + + assertIs<ChannelCannotInviteSelfException>(result.exceptionOrNull()) + } + + @Test + fun `inviteToChannel admits a guest to a private channel`() { + val ownerId = createTestUUID(1) + val guestId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId).copy(isPrivate = true)) + + assertTrue(membership.inviteToChannel(ownerId, guestId, "mod-ch").isSuccess) + assertTrue(membership.isMember(guestId, "mod-ch").getOrThrow()) + } + + @Test + fun `inviteToChannel still refuses a banned guest`() { + val ownerId = createTestUUID(1) + val bannedId = createTestUUID(2) + val (membership, channelManager, _) = createManagers() + channelManager.createChannel(createTestChannel(id = "mod-ch", name = "Moderated", ownerId = ownerId)) + membership.banPlayer(bannedId, "mod-ch") + + val result = membership.inviteToChannel(ownerId, bannedId, "mod-ch") + + assertIs<ChannelPlayerBannedException>(result.exceptionOrNull()) + } } diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt index 59fc98a..732ee52 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -72,18 +71,18 @@ class ChannelBanCommandTest { fun `execute should return SuccessWithMessage on ban`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) - every { channelManager.banPlayer(channelId, targetUUID) } returns Result.success(channel) + every { membershipManager.banPlayer(targetUUID, channelId) } returns Result.success(Unit) every { channelManager.getChannel(channelId) } returns Result.success(channel) val result = command.execute(ctx, "TargetPlayer") assertIs<CommandResult.SuccessWithMessage>(result) - verify { channelManager.banPlayer(channelId, targetUUID) } + verify { membershipManager.banPlayer(targetUUID, channelId) } } @Test @@ -100,7 +99,7 @@ class ChannelBanCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -111,7 +110,7 @@ class ChannelBanCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") @@ -123,11 +122,11 @@ class ChannelBanCommandTest { fun `execute should return Failure when already banned`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null - every { channelManager.banPlayer(channelId, targetUUID) } returns + every { membershipManager.banPlayer(targetUUID, channelId) } returns Result.failure(ChannelPlayerAlreadyBannedException(targetUUID, channelId)) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt index c51f460..65db127 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt @@ -1,7 +1,8 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelCannotInviteSelfException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.TestUtils import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -53,9 +54,9 @@ class ChannelInviteCommandTest { val channel = TestUtils.createTestChannel(id = channelId, ownerId = testUUID) every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(false) - every { deps.membershipManager.joinChannel(targetUUID, channelId, bypassPrivateCheck = true) } returns Result.success(Unit) + every { deps.membershipManager.inviteToChannel(testUUID, targetUUID, channelId) } returns Result.success(Unit) every { deps.channelManager.getChannel(channelId) } returns Result.success(channel) mockkStatic(Bukkit::class) @@ -76,11 +77,13 @@ class ChannelInviteCommandTest { val selfTarget = TestUtils.createMockPlayer(uuid = testUUID, name = "Player1") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) mockkStatic(Bukkit::class) try { every { Bukkit.getPlayer(any<String>()) } returns selfTarget + every { deps.membershipManager.inviteToChannel(testUUID, testUUID, channelId) } returns + Result.failure(ChannelCannotInviteSelfException(testUUID)) val result = deps.command.execute(deps.ctx, "Player1") @@ -96,8 +99,9 @@ class ChannelInviteCommandTest { val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER - every { deps.channelManager.isPlayerBanned(channelId, targetUUID) } returns Result.success(true) + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) + every { deps.membershipManager.inviteToChannel(testUUID, targetUUID, channelId) } returns + Result.failure(ChannelPlayerBannedException(targetUUID, channelId)) mockkStatic(Bukkit::class) try { @@ -116,7 +120,7 @@ class ChannelInviteCommandTest { val deps = createDependencies() every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) mockkStatic(Bukkit::class) try { @@ -136,7 +140,7 @@ class ChannelInviteCommandTest { val targetPlayer = TestUtils.createMockPlayer(uuid = targetUUID, name = "TargetPlayer") every { deps.channelManager.getPlayerChannel(testUUID) } returns channelId - every { deps.membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { deps.membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) mockkStatic(Bukkit::class) try { diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt index cab6ef0..23032c1 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.TestUtils @@ -71,20 +70,20 @@ class ChannelKickCommandTest { fun `execute should return SuccessWithMessage on kick`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(true) val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) - every { channelManager.removeMember(channelId, targetUUID) } returns Result.success(Unit) + every { membershipManager.kickPlayer(targetUUID, channelId) } returns Result.success(Unit) every { channelManager.getPlayerChannel(targetUUID) } returns channelId every { channelManager.getChannel(channelId) } returns Result.success(channel) val result = command.execute(ctx, "TargetPlayer") assertIs<CommandResult.SuccessWithMessage>(result) - verify { channelManager.removeMember(channelId, targetUUID) } + verify { membershipManager.kickPlayer(targetUUID, channelId) } } @Test @@ -101,7 +100,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -112,7 +111,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { Bukkit.getPlayer(any<String>()) } returns null every { membershipManager.isMember(targetUUID, channelId) } returns Result.success(false) @@ -126,7 +125,7 @@ class ChannelKickCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt index da7c7e2..bd9802c 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt @@ -69,7 +69,7 @@ class ChannelModCommandTest { fun `execute should promote to moderator`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER @@ -88,7 +88,7 @@ class ChannelModCommandTest { fun `execute should demote from moderator`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MODERATOR @@ -107,7 +107,7 @@ class ChannelModCommandTest { fun `execute should return Failure when modding self`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) val offlinePlayer = mockk<OfflinePlayer>(relaxed = true) every { offlinePlayer.uniqueId } returns testUUID @@ -125,7 +125,7 @@ class ChannelModCommandTest { fun `execute should return Failure when not owner`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -136,7 +136,7 @@ class ChannelModCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt index 9f25264..8f0d7d1 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt @@ -69,7 +69,7 @@ class ChannelOwnershipCommandTest { fun `execute should transfer ownership successfully`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns ChannelRole.MEMBER @@ -87,7 +87,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when transferring to self`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) val offlinePlayer = mockk<OfflinePlayer>(relaxed = true) every { offlinePlayer.uniqueId } returns testUUID @@ -105,7 +105,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when target not member`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { membershipManager.getMemberRoleOrNull(targetUUID, channelId) } returns null @@ -118,7 +118,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when not owner`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MODERATOR + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -129,7 +129,7 @@ class ChannelOwnershipCommandTest { fun `execute should return Failure when player not found`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer(hasPlayedBefore = false, isOnline = false) val result = command.execute(ctx, "TargetPlayer") diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt index 16dd383..93d5749 100644 --- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt +++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt @@ -1,7 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel import dev.m1sk9.lunaticChat.engine.chat.channel.Channel -import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -70,7 +69,7 @@ class ChannelUnbanCommandTest { fun `execute should return SuccessWithMessage on unban`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() val channel = Channel(id = channelId, name = "Test Channel", ownerId = testUUID, createdAt = 1000L) @@ -97,7 +96,7 @@ class ChannelUnbanCommandTest { fun `execute should return Failure when no permission`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.MEMBER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(false) val result = command.execute(ctx, "TargetPlayer") @@ -108,7 +107,7 @@ class ChannelUnbanCommandTest { fun `execute should return Failure when player not banned`() { val ctx = createContext() every { channelManager.getPlayerChannel(testUUID) } returns channelId - every { membershipManager.getMemberRoleOrNull(testUUID, channelId) } returns ChannelRole.OWNER + every { membershipManager.hasRole(testUUID, channelId, any()) } returns Result.success(true) setupOfflinePlayer() every { channelManager.unbanPlayer(channelId, targetUUID) } returns |
