summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-02 19:36:39 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-02 19:36:39 +0900
commit288d1e4a4babc136e8b3837d19970cc37023eb8e (patch)
tree79eba159ba5f04789384895d30c45039f6053fd6
parentbf62ebe35653492c123c729ae03cb32d8e20e2e6 (diff)
downloadLunaticChat-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>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt69
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt99
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt75
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt98
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt50
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt48
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelSubCommand.kt61
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt59
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManagerTest.kt93
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommandTest.kt15
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommandTest.kt20
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommandTest.kt13
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommandTest.kt10
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommandTest.kt10
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommandTest.kt7
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