diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-27 18:56:34 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-27 18:56:34 +0900 |
| commit | bb2dbeb80ade2a7e128d9eb62fff65358779b075 (patch) | |
| tree | 4090253d55c73bb1079dfc6260d49cfe5b497a93 | |
| parent | c3649ec2b53b21ed5b859cc9410f8db58e661d31 (diff) | |
| parent | 5c3a0d8027eb7be1d5decea71eae4d9bd03accc7 (diff) | |
| download | LunaticChat-bb2dbeb80ade2a7e128d9eb62fff65358779b075.tar.gz LunaticChat-bb2dbeb80ade2a7e128d9eb62fff65358779b075.tar.bz2 LunaticChat-bb2dbeb80ade2a7e128d9eb62fff65358779b075.zip | |
Merge pull request #72 from m1sk9/fix-channel-feature
feat: Implement the remaining channel chat features
44 files changed, 2501 insertions, 60 deletions
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt index cc1f743..568839a 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt @@ -13,6 +13,7 @@ import java.util.UUID * @property isPrivate Indicates whether the channel is private or public. * @property ownerId UUID of the user who owns the channel. * @property createdAt Timestamp of when the channel was created. + * @property bannedPlayers Set of UUIDs of players who are banned from the channel. */ @Serializable data class Channel( @@ -23,6 +24,10 @@ data class Channel( @Serializable(with = UUIDSerializer::class) val ownerId: UUID, val createdAt: Long = System.currentTimeMillis(), + val bannedPlayers: Set< + @Serializable(with = UUIDSerializer::class) + UUID, + > = emptySet(), ) { init { require(id.matches(CHANNEL_ID_PATTERN)) { diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelCannotInviteSelfException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelCannotInviteSelfException.kt new file mode 100644 index 0000000..5d1be73 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelCannotInviteSelfException.kt @@ -0,0 +1,7 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelCannotInviteSelfException( + playerId: UUID, +) : Exception("Player $playerId cannot invite themselves") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelLimitExceededException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelLimitExceededException.kt new file mode 100644 index 0000000..c111ab4 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelLimitExceededException.kt @@ -0,0 +1,7 @@ +package dev.m1sk9.lunaticChat.engine.exception + +class ChannelLimitExceededException( + val limit: Int, +) : Exception( + "Channel creation failed: server has reached the maximum limit of $limit channels", + ) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberLimitExceededException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberLimitExceededException.kt new file mode 100644 index 0000000..7e080dc --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberLimitExceededException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +class ChannelMemberLimitExceededException( + val channelId: String, + val limit: Int, +) : Exception( + "Cannot join channel '$channelId': channel has reached the maximum member limit of $limit", + ) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerAlreadyBannedException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerAlreadyBannedException.kt new file mode 100644 index 0000000..ce905f0 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerAlreadyBannedException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerAlreadyBannedException( + val playerId: UUID, + val channelId: String, +) : Exception("Player $playerId is already banned from channel $channelId") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBannedException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBannedException.kt new file mode 100644 index 0000000..9a70c72 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBannedException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerBannedException( + playerId: UUID, + val channelId: String, +) : Exception("Player $playerId is banned from channel $channelId") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassBanException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassBanException.kt new file mode 100644 index 0000000..529f590 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassBanException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerBypassBanException( + playerId: UUID, + val channelId: String, +) : Exception("Cannot ban player $playerId: player has bypass permission") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassKickException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassKickException.kt new file mode 100644 index 0000000..e818657 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassKickException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerBypassKickException( + playerId: UUID, + val channelId: String, +) : Exception("Cannot kick player $playerId: player has bypass permission") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerMembershipLimitExceededException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerMembershipLimitExceededException.kt new file mode 100644 index 0000000..afad6bf --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerMembershipLimitExceededException.kt @@ -0,0 +1,10 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerMembershipLimitExceededException( + val playerId: UUID, + val limit: Int, +) : Exception( + "Player $playerId has reached the maximum channel membership limit of $limit", + ) diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerNotBannedException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerNotBannedException.kt new file mode 100644 index 0000000..8dcc293 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerNotBannedException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPlayerNotBannedException( + playerId: UUID, + val channelId: String, +) : Exception("Player $playerId is not banned from channel $channelId") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPrivateRequiresInvitationException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPrivateRequiresInvitationException.kt new file mode 100644 index 0000000..737a529 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPrivateRequiresInvitationException.kt @@ -0,0 +1,8 @@ +package dev.m1sk9.lunaticChat.engine.exception + +import java.util.UUID + +class ChannelPrivateRequiresInvitationException( + val playerId: UUID, + val channelId: String, +) : Exception("Player $playerId cannot join private channel $channelId without invitation") diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt index 838a48b..9b6c3c6 100644 --- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt @@ -27,13 +27,29 @@ sealed class LunaticChatPermissionNode( object ChannelStatus : LunaticChatPermissionNode("lunaticchat.command.lc.channel.status") + object ChannelInfo : LunaticChatPermissionNode("lunaticchat.command.lc.channel.info") + object ChannelDelete : LunaticChatPermissionNode("lunaticchat.command.lc.channel.delete") + object ChannelInvite : LunaticChatPermissionNode("lunaticchat.command.lc.channel.invite") + + object ChannelKick : LunaticChatPermissionNode("lunaticchat.command.lc.channel.kick") + + object ChannelBan : LunaticChatPermissionNode("lunaticchat.command.lc.channel.ban") + + object ChannelUnban : LunaticChatPermissionNode("lunaticchat.command.lc.channel.unban") + + object ChannelMod : LunaticChatPermissionNode("lunaticchat.command.lc.channel.mod") + + object ChannelOwnership : LunaticChatPermissionNode("lunaticchat.command.lc.channel.ownership") + object ChatMode : LunaticChatPermissionNode("lunaticchat.command.lc.chatmode") object ChatModeToggle : LunaticChatPermissionNode("lunaticchat.command.lc.chatmode.toggle") object Spy : LunaticChatPermissionNode("lunaticchat.spy") - object NoticeUpdate : LunaticChatPermissionNode("lunaticchat.noticeUpdate") + object NoticeUpdate : LunaticChatPermissionNode("lunaticchat.noticeupdate") + + object ChannelBypass : LunaticChatPermissionNode("lunaticchat.channelbypass") } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt index d238dd6..ba2eca6 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt @@ -4,6 +4,7 @@ import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler +import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.command.core.CommandRegistry import dev.m1sk9.lunaticChat.paper.command.impl.ReplyCommand @@ -36,6 +37,7 @@ class LunaticChat : var channelMembershipManager: ChannelMembershipManager? = null var chatModeManager: ChatModeManager? = null var channelMessageHandler: ChannelMessageHandler? = null + var channelNotificationHandler: ChannelNotificationHandler? = null // Private services private lateinit var services: ServiceContainer @@ -73,6 +75,7 @@ class LunaticChat : channelMembershipManager = services.channelMembershipManager chatModeManager = services.chatModeManager channelMessageHandler = services.channelMessageHandler + channelNotificationHandler = services.channelNotificationHandler // Schedule periodic tasks serviceInitializer.schedulePeriodicTasks() diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt index 3cd9e73..9069bc4 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt @@ -4,6 +4,7 @@ import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler +import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager @@ -23,6 +24,7 @@ import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager * @property channelMembershipManager Optional (only when channel chat feature is enabled) * @property chatModeManager Optional (only when channel chat feature is enabled) * @property channelMessageHandler Optional (only when channel chat feature is enabled) + * @property channelNotificationHandler Optional (only when channel chat feature is enabled) */ data class ServiceContainer( val languageManager: LanguageManager, @@ -33,4 +35,5 @@ data class ServiceContainer( val channelMembershipManager: ChannelMembershipManager? = null, val chatModeManager: ChatModeManager? = null, val channelMessageHandler: ChannelMessageHandler? = null, + val channelNotificationHandler: ChannelNotificationHandler? = null, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt index 35f70d8..cea20bf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt @@ -7,6 +7,7 @@ import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelStorage import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler +import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.chat.handler.DirectMessageHandler import dev.m1sk9.lunaticChat.paper.config.LunaticChatConfiguration import dev.m1sk9.lunaticChat.paper.converter.ConversionCache @@ -27,6 +28,7 @@ private data class ChannelComponents( val channelMembershipManager: ChannelMembershipManager, val chatModeManager: ChatModeManager, val channelMessageHandler: ChannelMessageHandler, + val channelNotificationHandler: ChannelNotificationHandler, ) /** @@ -46,6 +48,7 @@ class ServiceInitializer( private var channelMembershipManager: ChannelMembershipManager? = null private var chatModeManager: ChatModeManager? = null private var channelMessageHandler: ChannelMessageHandler? = null + private var channelNotificationHandler: ChannelNotificationHandler? = null /** * Initializes all services in dependency order. @@ -81,10 +84,10 @@ class ServiceInitializer( null } - // 4. Initialize channel manager, membership manager, chat mode manager, and channel message handler + // 4. Initialize channel manager, membership manager, chat mode manager, channel message handler, and notification handler val channelComponents = if (configuration.features.channelChat.enabled) { - initializeChannelManager(playerSettingsManager) + initializeChannelManager(playerSettingsManager, romajiConverter, languageManager) } else { null } @@ -92,6 +95,7 @@ class ServiceInitializer( val channelMembershipManager = channelComponents?.channelMembershipManager val chatModeManager = channelComponents?.chatModeManager val channelMessageHandler = channelComponents?.channelMessageHandler + val channelNotificationHandler = channelComponents?.channelNotificationHandler // 5. Initialize handlers val directMessageHandler = @@ -109,6 +113,7 @@ class ServiceInitializer( channelMembershipManager = channelMembershipManager, chatModeManager = chatModeManager, channelMessageHandler = channelMessageHandler, + channelNotificationHandler = channelNotificationHandler, ) } @@ -173,9 +178,13 @@ class ServiceInitializer( } /** - * Initializes channel manager, membership manager, chat mode manager, and channel message handler with storage. + * Initializes channel manager, membership manager, chat mode manager, channel message handler, and notification handler with storage. */ - private fun initializeChannelManager(settingsManager: PlayerSettingsManager): ChannelComponents { + private fun initializeChannelManager( + settingsManager: PlayerSettingsManager, + romajiConverter: RomanjiConverter?, + languageManager: LanguageManager, + ): ChannelComponents { val channelsFile = plugin.dataFolder.resolve("channels.json").toPath() val storage = ChannelStorage( @@ -188,6 +197,7 @@ class ServiceInitializer( ChannelManager( storage = storage, logger = logger, + config = configuration.features.channelChat, ) manager.initialize() channelManager = manager @@ -196,6 +206,7 @@ class ServiceInitializer( ChannelMembershipManager( channelManager = manager, logger = logger, + config = configuration.features.channelChat, ) channelMembershipManager = membershipManager @@ -218,18 +229,30 @@ class ServiceInitializer( ChannelMessageHandler( settingsManager = settingsManager, channelManager = manager, + romanjiConverter = romajiConverter, logger = io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), ) channelMessageHandler = messageHandler - logger.info("Channel manager, membership manager, chat mode manager, and channel message handler initialized successfully.") + val notificationHandler = + ChannelNotificationHandler( + channelManager = manager, + languageManager = languageManager, + ) + channelNotificationHandler = notificationHandler + + logger.info( + "Channel manager, membership manager, chat mode manager, " + + "channel message handler, and notification handler initialized successfully.", + ) return ChannelComponents( channelManager = manager, channelMembershipManager = membershipManager, chatModeManager = chatMode, channelMessageHandler = messageHandler, + channelNotificationHandler = notificationHandler, ) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt index fc8d6da..f5e1bf2 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt @@ -5,8 +5,13 @@ import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelContext import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelData import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMember import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelRole +import dev.m1sk9.lunaticChat.engine.exception.ChannelLimitExceededException +import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberLimitExceededException import dev.m1sk9.lunaticChat.engine.exception.ChannelNoOwnerPermissionException import dev.m1sk9.lunaticChat.engine.exception.ChannelNotFoundException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerAlreadyBannedException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPlayerNotBannedException +import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList @@ -16,6 +21,7 @@ import kotlin.collections.forEach class ChannelManager( private val storage: ChannelStorage, private val logger: Logger, + private val config: ChannelChatFeatureConfig, ) { private val channelsCache = ConcurrentHashMap<String, Channel>() private val membersCache = ConcurrentHashMap<String, CopyOnWriteArrayList<ChannelMember>>() @@ -47,12 +53,18 @@ class ChannelManager( * @param channel The channel to create. * @return Result containing the created channel or an error if the channel already exists. * @throws ChannelNotFoundException if a channel with the same ID already exists. + * @throws ChannelLimitExceededException if the server has reached the maximum channel limit. */ fun createChannel(channel: Channel): Result<Channel> { if (channelsCache.containsKey(channel.id)) { return Result.failure(ChannelNotFoundException(channel.id)) } + // Check if max channels limit is reached (0 means unlimited) + if (config.maxChannelsPerServer > 0 && channelsCache.size >= config.maxChannelsPerServer) { + return Result.failure(ChannelLimitExceededException(config.maxChannelsPerServer)) + } + channelsCache[channel.id] = channel val ownerMember = @@ -76,18 +88,20 @@ class ChannelManager( * * @param channelId The ID of the channel to delete. * @param requesterId The ID of the player requesting the deletion. + * @param hasBypassPermission Whether the requester has bypass permission. * @return Result indicating success or failure of the deletion. * @throws ChannelNotFoundException if the channel does not exist. */ fun deleteChannel( channelId: String, requesterId: UUID, + hasBypassPermission: Boolean = false, ): Result<Unit> { val channel = channelsCache[channelId] ?: return Result.failure(ChannelNotFoundException(channelId)) - if (channel.ownerId != requesterId) { + if (channel.ownerId != requesterId && !hasBypassPermission) { return Result.failure(ChannelNoOwnerPermissionException(requesterId)) } @@ -159,6 +173,7 @@ class ChannelManager( * @param role The role of the new member. * @return Result indicating success or failure of the operation. * @throws ChannelNotFoundException if the channel does not exist. + * @throws ChannelMemberLimitExceededException if the channel has reached the maximum member limit. */ fun addMember( channelId: String, @@ -172,6 +187,11 @@ class ChannelManager( membersCache.getOrPut(channelId) { CopyOnWriteArrayList() } + + // Check if max members limit is reached (0 means unlimited) + if (config.maxMembersPerChannel > 0 && members.size >= config.maxMembersPerChannel) { + return Result.failure(ChannelMemberLimitExceededException(channelId, config.maxMembersPerChannel)) + } val newMember = ChannelMember( channelId = channelId, @@ -215,6 +235,162 @@ class ChannelManager( } /** + * Checks if a player is banned from a channel. + * + * @param channelId The ID of the channel. + * @param playerId The UUID of the player to check. + * @return Result containing true if banned, false otherwise. + * @throws ChannelNotFoundException if the channel does not exist. + */ + fun isPlayerBanned( + channelId: String, + playerId: UUID, + ): Result<Boolean> { + val channel = + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + return Result.success(channel.bannedPlayers.contains(playerId)) + } + + /** + * Bans a player from a channel. + * + * @param channelId The ID of the channel. + * @param playerId The UUID of the player to ban. + * @return Result containing the updated channel or an error. + * @throws ChannelNotFoundException if the channel does not exist. + */ + fun banPlayer( + channelId: String, + playerId: UUID, + ): Result<Channel> { + val channel = + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + // Check if player is already banned + if (channel.bannedPlayers.contains(playerId)) { + return Result.failure(ChannelPlayerAlreadyBannedException(playerId, channelId)) + } + + // Add player to banned list + val updatedChannel = channel.copy(bannedPlayers = channel.bannedPlayers + playerId) + channelsCache[channelId] = updatedChannel + + // Remove from members if currently a member + membersCache[channelId]?.removeIf { it.playerId == playerId } + + // Clear active channel if this was the player's active channel + if (activeChannels[playerId] == channelId) { + activeChannels.remove(playerId) + } + + saveToStorage() + logger.info("Player $playerId banned from channel $channelId.") + return Result.success(updatedChannel) + } + + /** + * Unbans a player from a channel. + * + * @param channelId The ID of the channel. + * @param playerId The UUID of the player to unban. + * @return Result containing the updated channel or an error. + * @throws ChannelNotFoundException if the channel does not exist. + * @throws ChannelPlayerNotBannedException if the player is not banned. + */ + fun unbanPlayer( + channelId: String, + playerId: UUID, + ): Result<Channel> { + val channel = + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + // Check if player is actually banned + if (!channel.bannedPlayers.contains(playerId)) { + return Result.failure(ChannelPlayerNotBannedException(playerId, channelId)) + } + + // Remove player from banned list + val updatedChannel = channel.copy(bannedPlayers = channel.bannedPlayers - playerId) + channelsCache[channelId] = updatedChannel + + saveToStorage() + logger.info("Player $playerId unbanned from channel $channelId.") + return Result.success(updatedChannel) + } + + /** + * Updates a member's role in a channel. + * + * @param channelId The ID of the channel. + * @param playerId The UUID of the player whose role to update. + * @param newRole The new role for the member. + * @return Result indicating success or failure of the operation. + * @throws ChannelNotFoundException if the channel does not exist. + */ + fun updateMemberRole( + channelId: String, + playerId: UUID, + newRole: ChannelRole, + ): Result<Unit> { + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + val members = + membersCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + val member = members.find { it.playerId == playerId } ?: return Result.failure(ChannelNotFoundException(channelId)) + + // Update member role + members.remove(member) + members.add(member.copy(role = newRole)) + + saveToStorage() + return Result.success(Unit) + } + + /** + * Updates the channel owner. + * + * @param channelId The ID of the channel. + * @param newOwnerId The UUID of the new owner. + * @return Result containing the updated channel or an error. + * @throws ChannelNotFoundException if the channel does not exist. + */ + fun updateChannelOwner( + channelId: String, + newOwnerId: UUID, + ): Result<Channel> { + val channel = + channelsCache[channelId] + ?: return Result.failure(ChannelNotFoundException(channelId)) + + // Update channel owner + val updatedChannel = channel.copy(ownerId = newOwnerId) + channelsCache[channelId] = updatedChannel + + // Update member roles - new owner becomes OWNER, old owner becomes MODERATOR + val members = membersCache[channelId] + if (members != null) { + members.replaceAll { member -> + when (member.playerId) { + newOwnerId -> member.copy(role = ChannelRole.OWNER) + channel.ownerId -> member.copy(role = ChannelRole.MODERATOR) + else -> member + } + } + } + + saveToStorage() + logger.info("Channel $channelId owner changed from ${channel.ownerId} to $newOwnerId.") + return Result.success(updatedChannel) + } + + /** * Saves the current state of channels and members to storage asynchronously. */ private fun saveToStorage() { 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 6d6ad8d..07ce839 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 @@ -5,13 +5,18 @@ import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException 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.ChannelPlayerMembershipLimitExceededException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.engine.exception.ChannelRuntimeException +import dev.m1sk9.lunaticChat.paper.config.key.ChannelChatFeatureConfig import java.util.UUID import java.util.logging.Logger class ChannelMembershipManager( private val channelManager: ChannelManager, private val logger: Logger, + private val config: ChannelChatFeatureConfig, ) { /** * Checks if a player is a member of a channel. @@ -48,6 +53,23 @@ class ChannelMembershipManager( } /** + * Gets the role of a member in a channel without throwing exceptions. + * + * @param playerId The UUID of the player. + * @param channelId The ID of the channel. + * @return The ChannelRole if the player is a member, null otherwise. + */ + fun getMemberRoleOrNull( + playerId: UUID, + channelId: String, + ): ChannelRole? = + channelManager + .getChannelMembers(channelId) + .getOrNull() + ?.find { it.playerId == playerId } + ?.role + + /** * Checks if a player has a specific role or higher in a channel. * * @param playerId The UUID of the player. @@ -80,14 +102,17 @@ class ChannelMembershipManager( * * @param playerId The UUID of the player. * @param channelId The ID of the channel. + * @param bypassPrivateCheck If true, allows joining private channels without invitation (used for invites). * @return Result indicating success or failure. * @throws ChannelNotFoundException if the channel does not exist. * @throws ChannelMemberAlreadyException if the player is already a member of this channel. + * @throws ChannelPlayerMembershipLimitExceededException if the player has reached the maximum channel membership limit. * @throws ChannelRuntimeException for other runtime errors. */ fun joinChannel( playerId: UUID, channelId: String, + bypassPrivateCheck: Boolean = false, ): Result<Unit> { // Check if channel exists val channel = @@ -105,6 +130,21 @@ class ChannelMembershipManager( ) } + // Check if player is banned + val isBanned = channelManager.isPlayerBanned(channelId, playerId).getOrElse { false } + if (isBanned) { + return Result.failure( + ChannelPlayerBannedException(playerId, channelId), + ) + } + + // Check if this is a private channel (only invited members can join) + if (channel.isPrivate && !bypassPrivateCheck) { + return Result.failure( + ChannelPrivateRequiresInvitationException(playerId, channelId), + ) + } + // Check if player is already a member val isAlreadyMember = isMember(playerId, channelId).getOrElse { @@ -120,6 +160,23 @@ class ChannelMembershipManager( ) } + // Check if player has reached max membership limit (0 means unlimited) + if (config.maxMembershipPerPlayer > 0) { + val playerChannelCount = + getPlayerChannels(playerId) + .getOrElse { + return Result.failure( + ChannelRuntimeException("Failed to get player channels for $playerId", it), + ) + }.size + + if (playerChannelCount >= config.maxMembershipPerPlayer) { + return Result.failure( + ChannelPlayerMembershipLimitExceededException(playerId, config.maxMembershipPerPlayer), + ) + } + } + // Add as member channelManager.addMember(channelId, playerId, ChannelRole.MEMBER).getOrElse { return Result.failure(it) @@ -132,8 +189,8 @@ class ChannelMembershipManager( } /** - * Clears the player's active channel without removing them from channel membership. - * The player remains a member of the channel and can rejoin by using the join command. + * Removes the player from the active channel completely. + * The player will be removed from the channel membership and the active channel will be cleared. * * @param playerId The UUID of the player. * @return Result indicating success or failure. @@ -146,9 +203,14 @@ class ChannelMembershipManager( ChannelNotMemberException(playerId, "no active channel"), ) + // Remove from channel members + channelManager.removeMember(currentChannel, playerId).getOrElse { + return Result.failure(it) + } + // Clear active channel channelManager.setPlayerChannel(playerId, null) - logger.info("Player $playerId left active channel $currentChannel (still a member)") + logger.info("Player $playerId left channel $currentChannel (removed from members)") return Result.success(Unit) } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt index 70756d9..89adb6f 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt @@ -5,6 +5,7 @@ import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification import dev.m1sk9.lunaticChat.paper.config.ConfigManager +import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.ktor.util.logging.Logger import net.kyori.adventure.text.Component @@ -14,6 +15,7 @@ import org.bukkit.entity.Player class ChannelMessageHandler( private val settingsManager: PlayerSettingsManager?, private val channelManager: ChannelManager, + private val romanjiConverter: RomanjiConverter?, private val logger: Logger, ) { private var lunaticChatConfiguration = ConfigManager.getConfiguration() @@ -26,21 +28,39 @@ class ChannelMessageHandler( val context = channelManager.getPlayerChannelContext(playerId) ?: return false - val formattedMessage = formatChannelMessage(player.name, context.channel.name, message) - // Play notification sound to sender if enabled - settingsManager?.let { manager -> - val senderSettings = manager.getSettings(playerId) - if (senderSettings.channelMessageNotificationEnabled) { - player.playMessageSendNotification() + val senderSettings = settingsManager?.getSettings(playerId) + + // Handle romaji conversion if enabled (requires blocking for HTTP call) + val displayMessage = + if (senderSettings?.japaneseConversionEnabled == true && romanjiConverter != null) { + runCatching { + kotlinx.coroutines.runBlocking { + romanjiConverter + ?.convert(message) + ?.let { "$message §e($it)" } + ?: message + } + }.getOrNull() ?: message + } else { + message } + + val formattedMessage = formatChannelMessage(player.name, context.channel.name, displayMessage) + val spyMessage = formatChannelMessage(player.name, context.channel.name, message) + + // Play notification sound to sender if enabled + if (senderSettings?.channelMessageNotificationEnabled == true) { + player.playMessageSendNotification() } + // Send to spy players (exclude sender and channel members) + val memberIds = context.members.map { it.playerId }.toSet() SpyPermissionManager .getDirectMessageSpyPlayers() .values - .filter { it.isOnline && it.uniqueId != playerId } - .forEach { it.sendMessage(formattedMessage) } + .filter { it.isOnline && it.uniqueId != playerId && it.uniqueId !in memberIds } + .forEach { it.sendMessage(spyMessage) } context.members.forEach { member -> Bukkit.getPlayer(member.playerId)?.let { memberPlayer -> if (memberPlayer.isOnline) { diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelNotificationHandler.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelNotificationHandler.kt new file mode 100644 index 0000000..0352780 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelNotificationHandler.kt @@ -0,0 +1,142 @@ +package dev.m1sk9.lunaticChat.paper.chat.handler + +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import org.bukkit.Bukkit + +/** + * Handles broadcasting notifications to channel members. + */ +class ChannelNotificationHandler( + private val channelManager: ChannelManager, + private val languageManager: LanguageManager, +) { + /** + * Broadcasts a join notification to all members of a channel. + * + * @param channelId The ID of the channel. + * @param playerName The name of the player who joined. + */ + fun broadcastJoin( + channelId: String, + playerName: String, + ) { + val channel = channelManager.getChannel(channelId).getOrNull() ?: return + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: return + + val message = + languageManager.getMessage( + "channel.notification.playerJoined", + mapOf("player" to playerName, "channel" to channel.name), + ) + val formattedMessage = MessageFormatter.format(message) + + members.forEach { member -> + Bukkit.getPlayer(member.playerId)?.let { player -> + if (player.isOnline) { + player.sendMessage(formattedMessage) + } + } + } + } + + /** + * Broadcasts a leave notification to all members of a channel. + * + * @param channelId The ID of the channel. + * @param playerName The name of the player who left. + */ + fun broadcastLeave( + channelId: String, + playerName: String, + ) { + val channel = channelManager.getChannel(channelId).getOrNull() ?: return + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: return + + val message = + languageManager.getMessage( + "channel.notification.playerLeft", + mapOf("player" to playerName, "channel" to channel.name), + ) + val formattedMessage = MessageFormatter.format(message) + + members.forEach { member -> + Bukkit.getPlayer(member.playerId)?.let { player -> + if (player.isOnline) { + player.sendMessage(formattedMessage) + } + } + } + } + + /** + * Broadcasts a kick notification to all members of a channel. + * + * @param channelId The ID of the channel. + * @param kickedPlayerName The name of the player who was kicked. + * @param kickerName The name of the player who performed the kick. + */ + fun broadcastKick( + channelId: String, + kickedPlayerName: String, + kickerName: String, + ) { + val channel = channelManager.getChannel(channelId).getOrNull() ?: return + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: return + + val message = + languageManager.getMessage( + "channel.notification.playerKicked", + mapOf( + "player" to kickedPlayerName, + "channel" to channel.name, + "kicker" to kickerName, + ), + ) + val formattedMessage = MessageFormatter.format(message) + + members.forEach { member -> + Bukkit.getPlayer(member.playerId)?.let { player -> + if (player.isOnline) { + player.sendMessage(formattedMessage) + } + } + } + } + + /** + * Broadcasts a ban notification to all members of a channel. + * + * @param channelId The ID of the channel. + * @param bannedPlayerName The name of the player who was banned. + * @param bannerName The name of the player who performed the ban. + */ + fun broadcastBan( + channelId: String, + bannedPlayerName: String, + bannerName: String, + ) { + val channel = channelManager.getChannel(channelId).getOrNull() ?: return + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: return + + val message = + languageManager.getMessage( + "channel.notification.playerBanned", + mapOf( + "player" to bannedPlayerName, + "channel" to channel.name, + "banner" to bannerName, + ), + ) + val formattedMessage = MessageFormatter.format(message) + + members.forEach { member -> + Bukkit.getPlayer(member.playerId)?.let { player -> + if (player.isOnline) { + player.sendMessage(formattedMessage) + } + } + } + } +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt index 3e47c7e..230a813 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt @@ -6,17 +6,25 @@ import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode 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.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelBanCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelCreateCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelDeleteCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelInfoCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelInviteCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelJoinCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelKickCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelLeaveCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelListCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelModCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelOwnershipCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelStatusCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelSwitchCommand +import dev.m1sk9.lunaticChat.paper.command.impl.lc.channel.ChannelUnbanCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack @@ -28,6 +36,7 @@ class ChannelCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, + private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { @@ -58,6 +67,7 @@ class ChannelCommand( plugin, channelManager, membershipManager, + notificationHandler, languageManager, ).buildWithPermissionCheck(), ).then( @@ -65,6 +75,7 @@ class ChannelCommand( plugin, channelManager, membershipManager, + notificationHandler, languageManager, ).buildWithPermissionCheck(), ).then( @@ -82,11 +93,61 @@ class ChannelCommand( languageManager, ).buildWithPermissionCheck(), ).then( + ChannelInfoCommand( + plugin, + channelManager, + languageManager, + ).buildWithPermissionCheck(), + ).then( ChannelDeleteCommand( plugin, channelManager, languageManager, ).buildWithPermissionCheck(), + ).then( + ChannelInviteCommand( + plugin, + channelManager, + membershipManager, + languageManager, + ).buildWithPermissionCheck(), + ).then( + ChannelKickCommand( + plugin, + channelManager, + membershipManager, + notificationHandler, + languageManager, + ).buildWithPermissionCheck(), + ).then( + ChannelBanCommand( + plugin, + channelManager, + membershipManager, + notificationHandler, + languageManager, + ).buildWithPermissionCheck(), + ).then( + ChannelUnbanCommand( + plugin, + channelManager, + membershipManager, + languageManager, + ).buildWithPermissionCheck(), + ).then( + ChannelModCommand( + plugin, + channelManager, + membershipManager, + languageManager, + ).buildWithPermissionCheck(), + ).then( + ChannelOwnershipCommand( + plugin, + channelManager, + membershipManager, + languageManager, + ).buildWithPermissionCheck(), ) // Default help message when no subcommand is provided @@ -168,10 +229,73 @@ class ChannelCommand( .text(" ") .append( MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.info"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( languageManager.getMessage("channel.help.delete"), ), ), ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.invite"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.kick"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.ban"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.unban"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.mod"), + ), + ), + ) + sender.sendMessage( + Component + .text(" ") + .append( + MessageFormatter.formatSuccess( + languageManager.getMessage("channel.help.ownership"), + ), + ), + ) return CommandResult.Success } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt index 73a50e5..d0357f6 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt @@ -11,6 +11,7 @@ import dev.m1sk9.lunaticChat.paper.command.core.CommandContext import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand import dev.m1sk9.lunaticChat.paper.command.impl.lc.chatmode.ChatModeToggleCommand import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import io.papermc.paper.command.brigadier.CommandSourceStack import io.papermc.paper.command.brigadier.Commands import net.kyori.adventure.text.Component @@ -69,9 +70,10 @@ class ChatModeCommand( } sender.sendMessage( - Component - .text(languageManager.getMessage("chatmode.current") + ": ", NamedTextColor.GRAY) - .append(Component.text(languageManager.getMessage(modeKey), modeColor)), + MessageFormatter + .format( + languageManager.getMessage("chatmode.current") + ": ", + ).append(Component.text(languageManager.getMessage(modeKey), modeColor)), ) return CommandResult.Success diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt index af08089..9f608d8 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt @@ -51,14 +51,17 @@ class LunaticChatCommand( // Add channel command if channel manager is available plugin.channelManager?.let { manager -> plugin.channelMembershipManager?.let { membershipManager -> - command.then( - ChannelCommand( - plugin, - manager, - membershipManager, - languageManager, - ).buildWithPermissionCheck(), - ) + plugin.channelNotificationHandler?.let { notificationHandler -> + command.then( + ChannelCommand( + plugin, + manager, + membershipManager, + notificationHandler, + languageManager, + ).buildWithPermissionCheck(), + ) + } } } 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 new file mode 100644 index 0000000..90d4e6d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt @@ -0,0 +1,210 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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 +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.chat.handler.ChannelNotificationHandler +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelBanCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val notificationHandler: ChannelNotificationHandler, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelBan::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("ban") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val channelId = channelManager.getPlayerChannel(player.uniqueId) + if (channelId != null) { + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: emptyList() + members + .filter { it.playerId != player.uniqueId } + .forEach { member -> + Bukkit.getOfflinePlayer(member.playerId).name?.let { name -> + builder.suggest(name) + } + } + } + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ban.noActiveChannel"), + ), + ) + + // Check if sender has permission (OWNER or MODERATOR) + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole == null || senderRole == ChannelRole.MEMBER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ban.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 CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ban.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + } + + val targetPlayerId = targetPlayer.uniqueId + + // Check if banning self + if (targetPlayerId == sender.uniqueId) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ban.cannotBanSelf"), + ), + ) + } + + // Check if target has bypass permission + val onlineTargetPlayer = Bukkit.getPlayer(playerName) + if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ban.cannotBanBypass", + mapOf("player" to onlineTargetPlayer.name), + ), + ), + ) + } + + // Ban player from channel + val banResult = channelManager.banPlayer(channelId, targetPlayerId) + return banResult.fold( + onSuccess = { + val channel = channelManager.getChannel(channelId).getOrNull() + val channelName = channel?.name ?: 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), + ), + ), + ) + } + + // Broadcast ban notification to remaining members + notificationHandler.broadcastBan(channelId, playerName, sender.name) + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.ban.success", + mapOf("player" to playerName, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ban.error"), + ), + ) + } + is ChannelPlayerBypassBanException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ban.cannotBanBypass", + mapOf("player" to playerName), + ), + ), + ) + } + is ChannelPlayerAlreadyBannedException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ban.alreadyBanned", + mapOf("player" to playerName), + ), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ban.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelBanCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt index 29da990..cad2231 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt @@ -5,6 +5,7 @@ import com.mojang.brigadier.arguments.StringArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.chat.channel.Channel import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.exception.ChannelLimitExceededException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager @@ -113,22 +114,42 @@ class ChannelCreateCommand( val result = channelManager.createChannel(channel) return result.fold( onSuccess = { + val successMessage = + languageManager.getMessage( + "channel.create.success", + mapOf("name" to name, "id" to channelId), + ) + + val message = + if (isPrivate) { + val privateNotice = languageManager.getMessage("channel.create.privateNotice") + "$successMessage\n$privateNotice" + } else { + successMessage + } + CommandResult.SuccessWithMessage( - MessageFormatter.format( - languageManager.getMessage( - "channel.create.success", - mapOf("name" to name, "id" to channelId), - ), - ), + MessageFormatter.format(message), ) }, onFailure = { error -> + val messageKey = + when (error) { + is ChannelLimitExceededException -> + "channel.create.limitExceeded" + else -> + "channel.create.alreadyExists" + } + val params = + when (error) { + is ChannelLimitExceededException -> + mapOf("limit" to error.limit.toString()) + else -> + mapOf("id" to channelId) + } CommandResult.Failure( MessageFormatter.formatError( - languageManager.getMessage( - "channel.create.alreadyExists", - mapOf("id" to channelId), - ), + languageManager.getMessage(messageKey, params), ), ) }, diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt index 9d39c70..8ca3a55 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt @@ -35,7 +35,33 @@ class ChannelDeleteCommand( .then( Commands .argument("channelId", StringArgumentType.word()) - .executes { ctx -> + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val hasBypass = + player.hasPermission( + LunaticChatPermissionNode.ChannelBypass.permissionNode, + ) + + val channels = + if (hasBypass) { + // Show all channels if has bypass permission + channelManager.getAllChannels().getOrNull() ?: emptyList() + } else { + // Show only owned channels + channelManager + .getAllChannels() + .getOrNull() + ?.filter { it.ownerId == player.uniqueId } + ?: emptyList() + } + + channels.forEach { channel -> + builder.suggest(channel.id) + } + } + builder.buildFuture() + }.executes { ctx -> val context = wrapContext(ctx) checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } @@ -51,8 +77,9 @@ class ChannelDeleteCommand( channelId: String, ): CommandResult { val sender = ctx.requirePlayer() + val hasBypass = sender.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode) - val result = channelManager.deleteChannel(channelId, sender.uniqueId) + val result = channelManager.deleteChannel(channelId, sender.uniqueId, hasBypass) return result.fold( onSuccess = { CommandResult.SuccessWithMessage( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt new file mode 100644 index 0000000..cb040d6 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt @@ -0,0 +1,179 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +import com.mojang.brigadier.arguments.StringArgumentType +import com.mojang.brigadier.builder.LiteralArgumentBuilder +import dev.m1sk9.lunaticChat.engine.command.CommandResult +import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode +import dev.m1sk9.lunaticChat.paper.LunaticChat +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelInfoCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + companion object { + private const val MAX_MEMBERS_DISPLAY = 10 + } + + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelInfo::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("info") + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val result = execute(context, null) + handleResult(context, result) + }.then( + Commands + .argument("channelId", StringArgumentType.word()) + .suggests { _, builder -> + // Tab completion: suggest all public channel IDs + val channels = channelManager.getAllChannels().getOrNull() ?: emptyList() + channels.forEach { channel -> + builder.suggest(channel.id) + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val channelId = StringArgumentType.getString(ctx, "channelId") + val result = execute(context, channelId) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + channelIdArg: String?, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Determine which channel to show info for + val channelId = + channelIdArg ?: channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.info.noActiveChannel"), + ), + ) + + // Get channel + val channel = + channelManager.getChannel(channelId).getOrElse { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.info.notFound", + mapOf("channelId" to channelId), + ), + ), + ) + } + + // Get members + val members = + channelManager.getChannelMembers(channelId).getOrElse { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.info.error"), + ), + ) + } + + // Get owner name + val ownerName = Bukkit.getOfflinePlayer(channel.ownerId).name ?: channel.ownerId.toString() + + // Display header + sender.sendMessage( + MessageFormatter.format( + languageManager.getMessage("channel.info.header"), + ), + ) + + // Display channel name + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.name"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(channel.name, NamedTextColor.AQUA)), + ) + + // Display channel ID + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.id"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(channel.id, NamedTextColor.YELLOW)), + ) + + // Display owner + sender.sendMessage( + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.owner"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(ownerName, NamedTextColor.GOLD)), + ) + + // Display members + val memberNames = + members.mapNotNull { member -> + Bukkit.getOfflinePlayer(member.playerId).name + } + + val membersText = + if (memberNames.size > MAX_MEMBERS_DISPLAY) { + val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) + val message = + languageManager.getMessage( + "channel.info.membersOmitted", + mapOf("count" to memberNames.size.toString()), + ) + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) + .append(Component.text(" ... ", NamedTextColor.GRAY)) + .append(Component.text("($message)", NamedTextColor.YELLOW)) + } else { + Component + .text(" ") + .append(Component.text(languageManager.getMessage("channel.info.members"), NamedTextColor.GRAY)) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) + } + + sender.sendMessage(membersText) + + return CommandResult.Success + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelInfoCommand should use build() method instead of buildCommand()", + ) +} 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 new file mode 100644 index 0000000..d4ce07a --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt @@ -0,0 +1,192 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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.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 +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelInviteCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelInvite::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("invite") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { _, builder -> + Bukkit + .getOnlinePlayers() + .filter { it.isOnline } + .forEach { player -> + builder.suggest(player.name) + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Get sender's active channel + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.invite.noActiveChannel"), + ), + ) + + // Check if sender has permission (OWNER or MODERATOR) + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole == null || senderRole == ChannelRole.MEMBER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.invite.noPermission"), + ), + ) + } + + // Find target player + val targetPlayer = + Bukkit.getPlayer(playerName) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.invite.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + + // Check if inviting self + if (targetPlayer.uniqueId == sender.uniqueId) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.invite.cannotInviteSelf"), + ), + ) + } + + // Check if player is banned + val isBanned = channelManager.isPlayerBanned(channelId, targetPlayer.uniqueId).getOrElse { false } + if (isBanned) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "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) + return result.fold( + onSuccess = { + // Send success message to sender + val channel = channelManager.getChannel(channelId).getOrNull() + val channelName = channel?.name ?: channelId + + // Send notification to invited player + targetPlayer.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.invite.receivedInvite", + mapOf("channel" to channelName, "inviter" to sender.name), + ), + ), + ) + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.invite.success", + mapOf("player" to targetPlayer.name, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.invite.error"), + ), + ) + } + is ChannelMemberLimitExceededException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.invite.channelFull", + mapOf("limit" to error.limit.toString()), + ), + ), + ) + } + is ChannelPlayerBannedException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.invite.playerBanned", + mapOf("player" to targetPlayer.name), + ), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.invite.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelInviteCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt index 2a73199..83a40b9 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt @@ -5,11 +5,16 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.exception.ChannelAlreadyActiveException import dev.m1sk9.lunaticChat.engine.exception.ChannelMemberAlreadyException +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.exception.ChannelPlayerMembershipLimitExceededException +import dev.m1sk9.lunaticChat.engine.exception.ChannelPrivateRequiresInvitationException import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode 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.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext @@ -25,6 +30,7 @@ class ChannelJoinCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, + private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { @@ -70,6 +76,9 @@ class ChannelJoinCommand( // Play notification sound sender.playChannelJoinNotification() + // Broadcast join notification to all channel members + notificationHandler.broadcastJoin(channelId, sender.name) + CommandResult.SuccessWithMessage( MessageFormatter.format( languageManager.getMessage( @@ -113,6 +122,40 @@ class ChannelJoinCommand( ), ) } + is ChannelMemberLimitExceededException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.join.channelMemberLimitExceeded", + mapOf("limit" to error.limit.toString()), + ), + ), + ) + } + is ChannelPlayerMembershipLimitExceededException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.join.playerChannelLimitExceeded", + mapOf("limit" to error.limit.toString()), + ), + ), + ) + } + is ChannelPlayerBannedException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.join.playerBanned"), + ), + ) + } + is ChannelPrivateRequiresInvitationException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.join.privateChannel"), + ), + ) + } else -> { CommandResult.Failure( MessageFormatter.formatError( 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 new file mode 100644 index 0000000..672cf2d --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt @@ -0,0 +1,207 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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.permission.LunaticChatPermissionNode +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.chat.handler.ChannelNotificationHandler +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelKickCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val notificationHandler: ChannelNotificationHandler, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelKick::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("kick") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val channelId = channelManager.getPlayerChannel(player.uniqueId) + if (channelId != null) { + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: emptyList() + members + .filter { it.playerId != player.uniqueId } + .forEach { member -> + Bukkit.getOfflinePlayer(member.playerId).name?.let { name -> + builder.suggest(name) + } + } + } + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Get sender's active channel + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.kick.noActiveChannel"), + ), + ) + + // Check if sender has permission (OWNER or MODERATOR) + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole == null || senderRole == ChannelRole.MEMBER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("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 CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.kick.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + } + + val targetPlayerId = targetPlayer.uniqueId + + // Check if kicking self + if (targetPlayerId == sender.uniqueId) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.kick.cannotKickSelf"), + ), + ) + } + + // Check if target has bypass permission + val onlineTargetPlayer = Bukkit.getPlayer(playerName) + if (onlineTargetPlayer != null && onlineTargetPlayer.hasPermission(LunaticChatPermissionNode.ChannelBypass.permissionNode)) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.kick.cannotKickBypass", + mapOf("player" to onlineTargetPlayer.name), + ), + ), + ) + } + + // Check if target is a member + val isMember = membershipManager.isMember(targetPlayerId, channelId).getOrElse { false } + if (!isMember) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.kick.notMember", + mapOf("player" to playerName), + ), + ), + ) + } + + // Remove from channel + val removeResult = channelManager.removeMember(channelId, targetPlayerId) + return removeResult.fold( + onSuccess = { + // Clear their active channel if this was it + if (channelManager.getPlayerChannel(targetPlayerId) == channelId) { + channelManager.setPlayerChannel(targetPlayerId, null) + } + + val channel = channelManager.getChannel(channelId).getOrNull() + val channelName = channel?.name ?: 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), + ), + ), + ) + } + + // Broadcast kick notification to remaining members + notificationHandler.broadcastKick(channelId, playerName, sender.name) + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.kick.success", + mapOf("player" to playerName, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.kick.error"), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.kick.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelKickCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt index 299ad47..5e45d27 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt @@ -7,6 +7,7 @@ import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode 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.chat.handler.ChannelNotificationHandler import dev.m1sk9.lunaticChat.paper.command.annotation.Permission import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly import dev.m1sk9.lunaticChat.paper.command.core.CommandContext @@ -21,6 +22,7 @@ class ChannelLeaveCommand( plugin: LunaticChat, private val channelManager: ChannelManager, private val membershipManager: ChannelMembershipManager, + private val notificationHandler: ChannelNotificationHandler, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { @@ -48,6 +50,11 @@ class ChannelLeaveCommand( val result = membershipManager.leaveChannel(sender.uniqueId) return result.fold( onSuccess = { + // Broadcast leave notification to all channel members + if (currentChannelId != null) { + notificationHandler.broadcastLeave(currentChannelId, sender.name) + } + CommandResult.SuccessWithMessage( MessageFormatter.format( languageManager.getMessage( diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt index aaf05af..1004baf 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt @@ -1,5 +1,6 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel +import com.mojang.brigadier.arguments.IntegerArgumentType import com.mojang.brigadier.builder.LiteralArgumentBuilder import dev.m1sk9.lunaticChat.engine.command.CommandResult import dev.m1sk9.lunaticChat.engine.permission.LunaticChatPermissionNode @@ -17,7 +18,7 @@ import net.kyori.adventure.text.Component import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.format.NamedTextColor -import net.kyori.adventure.text.format.TextDecoration +import kotlin.math.ceil @PlayerOnly class ChannelListCommand( @@ -25,6 +26,10 @@ class ChannelListCommand( private val channelManager: ChannelManager, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { + companion object { + private const val CHANNELS_PER_PAGE = 10 + } + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { val builder = build() return applyMethodPermission("build", builder) @@ -32,15 +37,31 @@ class ChannelListCommand( @Permission(LunaticChatPermissionNode.ChannelList::class) fun build(): LiteralArgumentBuilder<CommandSourceStack> = - Commands.literal("list").executes { ctx -> - val context = wrapContext(ctx) - checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + Commands + .literal("list") + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val result = execute(context, 1) + handleResult(context, result) + }.then( + Commands + .argument("page", IntegerArgumentType.integer(1)) + .executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } - val result = execute(context) - handleResult(context, result) - } + val page = IntegerArgumentType.getInteger(ctx, "page") + val result = execute(context, page) + handleResult(context, result) + }, + ) - private fun execute(ctx: CommandContext): CommandResult { + private fun execute( + ctx: CommandContext, + page: Int, + ): CommandResult { val sender = ctx.requirePlayer() val result = channelManager.getPublicChannels() @@ -53,6 +74,12 @@ class ChannelListCommand( ), ) } else { + val totalPages = ceil(channels.size.toDouble() / CHANNELS_PER_PAGE).toInt() + val currentPage = page.coerceIn(1, totalPages) + val startIndex = (currentPage - 1) * CHANNELS_PER_PAGE + val endIndex = (startIndex + CHANNELS_PER_PAGE).coerceAtMost(channels.size) + val pageChannels = channels.subList(startIndex, endIndex) + sender.sendMessage( MessageFormatter.format( languageManager.getMessage( @@ -62,7 +89,7 @@ class ChannelListCommand( ), ) - channels.forEach { channel -> + pageChannels.forEach { channel -> val memberCountResult = channelManager.getChannelMembers(channel.id) val memberCount = memberCountResult.getOrNull()?.size @@ -97,8 +124,7 @@ class ChannelListCommand( .text(" • ", NamedTextColor.GRAY) .append( Component - .text(channel.name, NamedTextColor.AQUA) - .decorate(TextDecoration.BOLD), + .text(channel.name, NamedTextColor.AQUA), ).append(Component.text(" (", NamedTextColor.GRAY)) .append(Component.text(channel.id, NamedTextColor.YELLOW)) .append(Component.text(")", NamedTextColor.GRAY)) @@ -107,6 +133,55 @@ class ChannelListCommand( sender.sendMessage(channelInfo) } + + // Display pagination footer + if (totalPages > 1) { + val paginationComponent = + Component + .text() + .append(Component.text("Page ", NamedTextColor.GRAY)) + .append(Component.text("$currentPage", NamedTextColor.YELLOW)) + .append(Component.text("/", NamedTextColor.GRAY)) + .append(Component.text("$totalPages", NamedTextColor.YELLOW)) + + if (currentPage > 1) { + paginationComponent + .append(Component.text(" ", NamedTextColor.GRAY)) + .append( + Component + .text("[◀]", NamedTextColor.GREEN) + .clickEvent(ClickEvent.runCommand("/lc channel list ${currentPage - 1}")) + .hoverEvent( + HoverEvent.showText( + Component.text( + "Go to page ${currentPage - 1}", + NamedTextColor.WHITE, + ), + ), + ), + ) + } + + if (currentPage < totalPages) { + paginationComponent + .append(Component.text(" ", NamedTextColor.GRAY)) + .append( + Component + .text("[▶]", NamedTextColor.GREEN) + .clickEvent(ClickEvent.runCommand("/lc channel list ${currentPage + 1}")) + .hoverEvent( + HoverEvent.showText( + Component.text( + "Go to page ${currentPage + 1}", + NamedTextColor.WHITE, + ), + ), + ), + ) + } + + sender.sendMessage(paginationComponent.build()) + } } CommandResult.Success }, 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 new file mode 100644 index 0000000..82caa39 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt @@ -0,0 +1,198 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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.permission.LunaticChatPermissionNode +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.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelModCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelMod::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("mod") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val channelId = channelManager.getPlayerChannel(player.uniqueId) + if (channelId != null) { + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: emptyList() + members + .filter { it.playerId != player.uniqueId } + .forEach { member -> + Bukkit.getOfflinePlayer(member.playerId).name?.let { name -> + builder.suggest(name) + } + } + } + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Get sender's active channel + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.mod.noActiveChannel"), + ), + ) + + // Check if sender is OWNER + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole != ChannelRole.OWNER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.mod.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 CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.mod.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + } + + val targetPlayerId = targetPlayer.uniqueId + + // Check if modding self + if (targetPlayerId == sender.uniqueId) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.mod.cannotModSelf"), + ), + ) + } + + // Check if target is a member + val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) + if (targetRole == null) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.mod.notMember", + mapOf("player" to playerName), + ), + ), + ) + } + + // Toggle mod status + val newRole = + if (targetRole == ChannelRole.MODERATOR) { + ChannelRole.MEMBER + } else { + ChannelRole.MODERATOR + } + + val updateResult = channelManager.updateMemberRole(channelId, targetPlayerId, newRole) + return updateResult.fold( + onSuccess = { + val channel = channelManager.getChannel(channelId).getOrNull() + val channelName = channel?.name ?: channelId + + val action = + if (newRole == ChannelRole.MODERATOR) { + languageManager.getMessage("channel.mod.promoted") + } else { + languageManager.getMessage("channel.mod.demoted") + } + + val onlineTargetPlayer = Bukkit.getPlayer(playerName) + onlineTargetPlayer?.let { player -> + player.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.mod.notification", + mapOf("action" to action, "channel" to channelName), + ), + ), + ) + } + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.mod.success", + mapOf("player" to playerName, "action" to action, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.mod.error"), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.mod.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelModCommand should use build() method instead of buildCommand()", + ) +} 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 new file mode 100644 index 0000000..576b072 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt @@ -0,0 +1,184 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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.permission.LunaticChatPermissionNode +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.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelOwnershipCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelOwnership::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("ownership") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val channelId = channelManager.getPlayerChannel(player.uniqueId) + if (channelId != null) { + val members = channelManager.getChannelMembers(channelId).getOrNull() ?: emptyList() + members + .filter { it.playerId != player.uniqueId } + .forEach { member -> + Bukkit.getOfflinePlayer(member.playerId).name?.let { name -> + builder.suggest(name) + } + } + } + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Get sender's active channel + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ownership.noActiveChannel"), + ), + ) + + // Check if sender is OWNER + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole != ChannelRole.OWNER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("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 CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ownership.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + } + + val targetPlayerId = targetPlayer.uniqueId + + // Check if transferring to self + if (targetPlayerId == sender.uniqueId) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ownership.cannotTransferToSelf"), + ), + ) + } + + // Check if target is a member + val targetRole = membershipManager.getMemberRoleOrNull(targetPlayerId, channelId) + if (targetRole == null) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.ownership.notMember", + mapOf("player" to playerName), + ), + ), + ) + } + + // Transfer ownership + val updateResult = channelManager.updateChannelOwner(channelId, targetPlayerId) + return updateResult.fold( + onSuccess = { + val channelName = it.name + + // Notify target player + val onlineTargetPlayer = Bukkit.getPlayer(playerName) + onlineTargetPlayer?.let { player -> + player.sendMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.ownership.receivedOwnership", + mapOf("channel" to channelName, "previousOwner" to sender.name), + ), + ), + ) + } + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.ownership.success", + mapOf("player" to playerName, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ownership.error"), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.ownership.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelOwnershipCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt index fa03965..3b65765 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt @@ -1,6 +1,7 @@ package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel 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.permission.LunaticChatPermissionNode import dev.m1sk9.lunaticChat.paper.LunaticChat @@ -19,6 +20,7 @@ import net.kyori.adventure.text.event.ClickEvent import net.kyori.adventure.text.event.HoverEvent import net.kyori.adventure.text.format.NamedTextColor import net.kyori.adventure.text.format.TextDecoration +import org.bukkit.Bukkit @PlayerOnly class ChannelStatusCommand( @@ -27,6 +29,10 @@ class ChannelStatusCommand( private val membershipManager: ChannelMembershipManager, private val languageManager: LanguageManager, ) : LunaticCommand(plugin) { + companion object { + private const val MAX_MEMBERS_DISPLAY = 10 + } + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { val builder = build() return applyMethodPermission("build", builder) @@ -85,6 +91,59 @@ class ChannelStatusCommand( .append(Component.text(")", NamedTextColor.GRAY)) sender.sendMessage(activeText) + + // Display members of active channel + val activeMembers = + channelManager.getChannelMembers(activeChannelId).getOrElse { + emptyList() + } + + if (activeMembers.isNotEmpty()) { + val memberNames = + activeMembers.mapNotNull { member -> + val playerName = Bukkit.getOfflinePlayer(member.playerId).name ?: return@mapNotNull null + val roleText = + when (member.role) { + ChannelRole.OWNER -> " [OWNER]" + ChannelRole.MODERATOR -> " [MOD]" + ChannelRole.MEMBER -> "" + } + playerName + roleText + } + + val membersText = + if (memberNames.size > MAX_MEMBERS_DISPLAY) { + val displayNames = memberNames.take(MAX_MEMBERS_DISPLAY) + val message = + languageManager.getMessage( + "channel.info.membersOmitted", + mapOf("count" to memberNames.size.toString()), + ) + Component + .text(" ") + .append( + Component.text( + languageManager.getMessage("channel.info.members"), + NamedTextColor.GRAY, + ), + ).append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(displayNames.joinToString(", "), NamedTextColor.WHITE)) + .append(Component.text(" ... ", NamedTextColor.GRAY)) + .append(Component.text("($message)", NamedTextColor.YELLOW)) + } else { + Component + .text(" ") + .append( + Component.text( + languageManager.getMessage("channel.info.members"), + NamedTextColor.GRAY, + ), + ).append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(memberNames.joinToString(", "), NamedTextColor.WHITE)) + } + + sender.sendMessage(membersText) + } } else { sender.sendMessage( Component 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 new file mode 100644 index 0000000..e7a7295 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt @@ -0,0 +1,159 @@ +package dev.m1sk9.lunaticChat.paper.command.impl.lc.channel + +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 +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.command.annotation.Permission +import dev.m1sk9.lunaticChat.paper.command.annotation.PlayerOnly +import dev.m1sk9.lunaticChat.paper.command.core.CommandContext +import dev.m1sk9.lunaticChat.paper.command.core.LunaticCommand +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter +import io.papermc.paper.command.brigadier.CommandSourceStack +import io.papermc.paper.command.brigadier.Commands +import org.bukkit.Bukkit + +@PlayerOnly +class ChannelUnbanCommand( + plugin: LunaticChat, + private val channelManager: ChannelManager, + private val membershipManager: ChannelMembershipManager, + private val languageManager: LanguageManager, +) : LunaticCommand(plugin) { + fun buildWithPermissionCheck(): LiteralArgumentBuilder<CommandSourceStack> { + val builder = build() + return applyMethodPermission("build", builder) + } + + @Permission(LunaticChatPermissionNode.ChannelUnban::class) + fun build(): LiteralArgumentBuilder<CommandSourceStack> = + Commands + .literal("unban") + .then( + Commands + .argument("playerName", StringArgumentType.word()) + .suggests { ctx, builder -> + val player = ctx.source.executor as? org.bukkit.entity.Player + if (player != null) { + val channelId = channelManager.getPlayerChannel(player.uniqueId) + if (channelId != null) { + val channel = channelManager.getChannel(channelId).getOrNull() + channel?.bannedPlayers?.forEach { bannedId -> + Bukkit.getOfflinePlayer(bannedId).name?.let { name -> + builder.suggest(name) + } + } + } + } + builder.buildFuture() + }.executes { ctx -> + val context = wrapContext(ctx) + checkPlayerOnly(context)?.let { return@executes handleResult(context, it) } + + val playerName = StringArgumentType.getString(ctx, "playerName") + val result = execute(context, playerName) + handleResult(context, result) + }, + ) + + private fun execute( + ctx: CommandContext, + playerName: String, + ): CommandResult { + val sender = ctx.requirePlayer() + + // Get sender's active channel + val channelId = + channelManager.getPlayerChannel(sender.uniqueId) + ?: return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.unban.noActiveChannel"), + ), + ) + + // Check if sender has permission (OWNER or MODERATOR) + val senderRole = membershipManager.getMemberRoleOrNull(sender.uniqueId, channelId) + if (senderRole == null || senderRole == ChannelRole.MEMBER) { + return CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.unban.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 CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.unban.playerNotFound", + mapOf("player" to playerName), + ), + ), + ) + } + + val targetPlayerId = targetPlayer.uniqueId + + // Unban player from channel + val unbanResult = channelManager.unbanPlayer(channelId, targetPlayerId) + return unbanResult.fold( + onSuccess = { + val channel = channelManager.getChannel(channelId).getOrNull() + val channelName = channel?.name ?: channelId + + CommandResult.SuccessWithMessage( + MessageFormatter.format( + languageManager.getMessage( + "channel.unban.success", + mapOf("player" to playerName, "channel" to channelName), + ), + ), + ) + }, + onFailure = { error -> + when (error) { + is ChannelNotFoundException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.unban.error"), + ), + ) + } + is ChannelPlayerNotBannedException -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage( + "channel.unban.playerNotBanned", + mapOf("player" to playerName), + ), + ), + ) + } + else -> { + CommandResult.Failure( + MessageFormatter.formatError( + languageManager.getMessage("channel.unban.error"), + ), + ) + } + } + }, + ) + } + + override fun buildCommand(): LiteralArgumentBuilder<CommandSourceStack> = + throw UnsupportedOperationException( + "ChannelUnbanCommand should use build() method instead of buildCommand()", + ) +} diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt index 9aa563a..c53d154 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt @@ -50,6 +50,9 @@ object ConfigManager { channelChat = ChannelChatFeatureConfig( enabled = configFile.getBoolean("features.channelChat.enabled", false), + maxChannelsPerServer = configFile.getInt("features.channelChat.maxChannelsPerServer", 0), + maxMembersPerChannel = configFile.getInt("features.channelChat.maxMembersPerChannel", 0), + maxMembershipPerPlayer = configFile.getInt("features.channelChat.maxMembershipPerPlayer", 0), ), ), messageFormat = diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt index 828bdb3..5b93ac5 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt @@ -2,4 +2,7 @@ package dev.m1sk9.lunaticChat.paper.config.key data class ChannelChatFeatureConfig( val enabled: Boolean, + val maxChannelsPerServer: Int = 0, + val maxMembersPerChannel: Int = 0, + val maxMembershipPerPlayer: Int = 0, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt index f8e8181..1979b6b 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt @@ -42,15 +42,18 @@ object EventListenerRegistry { // Conditionally register chat listener when all required components are available if (services.chatModeManager != null && + services.channelManager != null && services.channelMessageHandler != null && services.romajiConverter != null ) { pluginManager.registerEvents( PlayerChatListener( chatModeManager = services.chatModeManager, + channelManager = services.channelManager, channelMessageHandler = services.channelMessageHandler, romajiConverter = services.romajiConverter, settingsManager = services.playerSettingsManager, + languageManager = services.languageManager, ), plugin, ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt index 47c8cd4..0fa4fa1 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt @@ -2,8 +2,11 @@ package dev.m1sk9.lunaticChat.paper.listener import dev.m1sk9.lunaticChat.engine.chat.ChatMode import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.converter.RomanjiConverter +import dev.m1sk9.lunaticChat.paper.i18n.LanguageManager +import dev.m1sk9.lunaticChat.paper.i18n.MessageFormatter import dev.m1sk9.lunaticChat.paper.settings.PlayerSettingsManager import io.papermc.paper.event.player.AsyncChatEvent import kotlinx.coroutines.runBlocking @@ -14,9 +17,11 @@ import org.bukkit.event.Listener class PlayerChatListener( private val chatModeManager: ChatModeManager, + private val channelManager: ChannelManager, private val channelMessageHandler: ChannelMessageHandler, private val romajiConverter: RomanjiConverter, private val settingsManager: PlayerSettingsManager, + private val languageManager: LanguageManager, ) : Listener { private val plainTextSerializer = PlainTextComponentSerializer.plainText() @@ -70,8 +75,23 @@ class PlayerChatListener( event.message(Component.text(displayMessage)) } ChatMode.CHANNEL -> { - event.isCancelled = true - channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix) + val hasActiveChannel = channelManager.getPlayerChannel(player.uniqueId) != null + + if (hasActiveChannel) { + // Send to channel as normal + event.isCancelled = true + channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix) + } else { + // Auto-fallback to global chat + event.message(Component.text(displayMessage)) + + // Send warning to player + player.sendMessage( + MessageFormatter.format( + languageManager.getMessage("channel.autoFallback"), + ), + ) + } } } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt index fe4239d..86ae129 100644 --- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt @@ -63,11 +63,13 @@ class PlayerPresenceListener( val context = manager.getPlayerChannelContext(player.uniqueId) context?.let { val notification = - languageManager.getMessage( - "channel.notification.login", - mapOf("channelName" to it.channel.name), + MessageFormatter.format( + languageManager.getMessage( + "channel.notification.login", + mapOf("channelName" to it.channel.name), + ), ) - player.sendMessage(Component.text(notification)) + player.sendMessage(notification) } } } diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml index 839e0e3..8efa1fa 100644 --- a/platform-paper/src/main/resources/config.yml +++ b/platform-paper/src/main/resources/config.yml @@ -51,6 +51,12 @@ features: channelChat: # If enabled, channel-based chat functionality will be activated. enabled: false + # Maximum number of channels that can be created per server. Set to 0 for unlimited. + maxChannelsPerServer: 0 + # Maximum number of members allowed in a single channel. Set to 0 for unlimited. + maxMembersPerChannel: 0 + # Maximum number of channels a single player can join. Set to 0 for unlimited. + maxMembershipPerPlayer: 0 # ---------------------------------------------- # --------- Message Format Settings -------- diff --git a/platform-paper/src/main/resources/languages/en.yml b/platform-paper/src/main/resources/languages/en.yml index 925d7e7..2cbff48 100644 --- a/platform-paper/src/main/resources/languages/en.yml +++ b/platform-paper/src/main/resources/languages/en.yml @@ -50,11 +50,20 @@ channel: leave: "/lc channel leave - Leave the active channel" switch: "/lc channel switch <channelId> - Switch to a channel you're already a member of" status: "/lc channel status - Show your channel status and memberships" + info: "/lc channel info [channelId] - Show detailed information about a channel" delete: "/lc channel delete <channelId> - Delete a channel" + invite: "/lc channel invite <player> - Invite a player to your active channel" + kick: "/lc channel kick <player> - Kick a player from your active channel" + ban: "/lc channel ban <player> - Ban a player from your active channel" + unban: "/lc channel unban <player> - Unban a player from your active channel" + mod: "/lc channel mod <player> - Promote or demote a player to/from moderator" + ownership: "/lc channel ownership <player> - Transfer channel ownership to a player" create: success: "Created channel '{name}' (ID: {id})" + privateNotice: "This is a private channel. Players can only join through invitations." alreadyExists: "Channel ID '{id}' is already in use" invalidId: "Invalid channel ID '{id}'. Must be 3-30 characters using only letters, numbers, underscores, and hyphens" + limitExceeded: "Cannot create channel: server has reached the maximum limit of {limit} channels" list: header: "=== Public Channels ({count}) ===" empty: "No public channels available" @@ -69,8 +78,11 @@ channel: success: "Joined channel '{channelName}' (ID: {channelId})" alreadyActive: "Channel '{channelName}' is already your active channel" alreadyMember: "You are already a member of channel '{channelName}'" + playerBanned: "You are banned from this channel" notFound: "Channel '{channelId}' not found" privateChannel: "This is a private channel and requires an invitation" + channelMemberLimitExceeded: "Cannot join channel: channel has reached the maximum member limit of {limit}" + playerChannelLimitExceeded: "Cannot join channel: you have reached the maximum channel membership limit of {limit}" error: "Failed to join channel" leave: success: "Left channel '{channelName}'" @@ -90,8 +102,80 @@ channel: noChannels: "You are not a member of any channels" clickToSwitch: "Click to switch" error: "Failed to retrieve channel status" + info: + header: "=== Channel Information ===" + name: "Name" + id: "ID" + owner: "Owner" + members: "Members" + membersOmitted: "{count} members (showing first 10)" + notFound: "Channel '{channelId}' not found" + noActiveChannel: "You don't have an active channel. Please specify a channel ID" + error: "Failed to retrieve channel information" notification: login: "You are in channel '{channelName}'" + playerJoined: "{player} joined {channel}" + playerLeft: "{player} left {channel}" + playerKicked: "{player} was kicked from {channel} by {kicker}" + playerBanned: "{player} was banned from {channel} by {banner}" + autoFallback: "No active channel - message sent to global chat" + invite: + success: "Invited {player} to {channel}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners and moderators can invite players" + playerNotFound: "Player '{player}' not found" + cannotInviteSelf: "You cannot invite yourself" + playerBanned: "{player} is banned from this channel" + channelFull: "Cannot invite: channel has reached the maximum member limit of {limit}" + receivedInvite: "You have been invited to {channel} by {inviter}" + error: "Failed to invite player" + kick: + success: "Kicked {player} from {channel}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners and moderators can kick players" + playerNotFound: "Player '{player}' not found" + cannotKickSelf: "You cannot kick yourself" + cannotKickBypass: "Cannot kick {player}: player has bypass permission" + notMember: "{player} is not a member of this channel" + wasKicked: "You were kicked from {channel} by {kicker}" + error: "Failed to kick player" + ban: + success: "Banned {player} from {channel}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners and moderators can ban players" + playerNotFound: "Player '{player}' not found" + cannotBanSelf: "You cannot ban yourself" + cannotBanBypass: "Cannot ban {player}: player has bypass permission" + alreadyBanned: "{player} is already banned from this channel" + wasBanned: "You were banned from {channel} by {banner}" + error: "Failed to ban player" + unban: + success: "Unbanned {player} from {channel}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners and moderators can unban players" + playerNotFound: "Player '{player}' not found" + playerNotBanned: "{player} is not banned from this channel" + error: "Failed to unban player" + mod: + success: "{player} has been {action} in {channel}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners can manage moderators" + playerNotFound: "Player '{player}' not found" + notMember: "{player} is not a member of this channel" + cannotModSelf: "You cannot modify your own moderator status" + promoted: "promoted to moderator" + demoted: "demoted from moderator" + notification: "You have been {action} in {channel}" + error: "Failed to manage moderator status" + ownership: + success: "Ownership of {channel} transferred to {player}" + noActiveChannel: "You don't have an active channel" + noPermission: "Only channel owners can transfer ownership" + playerNotFound: "Player '{player}' not found" + notMember: "{player} is not a member of this channel" + cannotTransferToSelf: "You cannot transfer ownership to yourself" + receivedOwnership: "You are now the owner of {channel} (previously owned by {previousOwner})" + error: "Failed to transfer ownership" chatmode: current: "Current chat mode" diff --git a/platform-paper/src/main/resources/languages/ja.yml b/platform-paper/src/main/resources/languages/ja.yml index 17b0bf6..b51d605 100644 --- a/platform-paper/src/main/resources/languages/ja.yml +++ b/platform-paper/src/main/resources/languages/ja.yml @@ -50,11 +50,20 @@ channel: leave: "/lc channel leave - アクティブチャンネルから退出します" switch: "/lc channel switch <チャンネルID> - 既に参加しているチャンネルに切り替えます" status: "/lc channel status - チャンネルのステータスとメンバーシップを表示します" + info: "/lc channel info [チャンネルID] - チャンネルの詳細情報を表示します" delete: "/lc channel delete <チャンネルID> - チャンネルを削除します" + invite: "/lc channel invite <プレイヤー名> - プレイヤーをアクティブチャンネルに招待します" + kick: "/lc channel kick <プレイヤー名> - プレイヤーをアクティブチャンネルからキックします" + ban: "/lc channel ban <プレイヤー名> - プレイヤーをアクティブチャンネルからバンします" + unban: "/lc channel unban <プレイヤー名> - プレイヤーをアクティブチャンネルからアンバンします" + mod: "/lc channel mod <プレイヤー名> - プレイヤーをモデレーターに任命 / 任命解除します" + ownership: "/lc channel ownership <プレイヤー名> - チャンネルのオーナー権限を譲渡します" create: success: "チャンネル '{name}' (ID: {id}) を作成しました" + privateNotice: "これはプライベートチャンネルです。招待されたプレイヤーのみが参加できます。" alreadyExists: "チャンネルID '{id}' は既に使用されています" invalidId: "無効なチャンネルID '{id}' です。3-30文字の英数字、アンダースコア、ハイフンのみ使用できます" + limitExceeded: "チャンネルを作成できません: サーバーはチャンネル数の上限 ({limit}件) に達しています" list: header: "=== 公開チャンネル ({count}件) ===" empty: "公開チャンネルはありません" @@ -69,13 +78,16 @@ channel: success: "チャンネル '{channelName}' (ID: {channelId}) に参加しました" alreadyActive: "チャンネル '{channelName}' は既にアクティブです" alreadyMember: "既にチャンネル '{channelName}' のメンバーです" + playerBanned: "このチャンネルから永久追放されているため参加できません" notFound: "チャンネル '{channelId}' が見つかりません" privateChannel: "プライベートチャンネルには招待が必要です" + channelMemberLimitExceeded: "チャンネルに参加できません: チャンネルのメンバー数が上限 ({limit}人) に達しています" + playerChannelLimitExceeded: "チャンネルに参加できません: 参加できるチャンネル数の上限 ({limit}件) に達しています" error: "チャンネルへの参加に失敗しました" leave: success: "チャンネル '{channelName}' から退出しました" noActiveChannel: "アクティブなチャンネルがありません" - error: "チャンネルからの退出に失敗しました" + error: "チャンネルの退出に失敗しました" switch: success: "チャンネル '{channelName}' (ID: {channelId}) に切り替えました" alreadyActive: "チャンネル '{channelName}' は既にアクティブです" @@ -90,8 +102,80 @@ channel: noChannels: "どのチャンネルにも参加していません" clickToSwitch: "クリックして切り替え" error: "チャンネルステータスの取得に失敗しました" + info: + header: "=== チャンネル情報 ===" + name: "名前" + id: "ID" + owner: "オーナー" + members: "メンバー" + membersOmitted: "{count}人のメンバー (最初の10人を表示)" + notFound: "チャンネル '{channelId}' が見つかりません" + noActiveChannel: "アクティブなチャンネルがありません。チャンネルIDを指定してください" + error: "チャンネル情報の取得に失敗しました" notification: login: "チャンネル '{channelName}' に入室中です" + playerJoined: "{player}がチャンネル {channel} に参加しました" + playerLeft: "{player}がチャンネル {channel} から退出しました" + playerKicked: "{player}は{kicker}によってチャンネル {channel} から追放されました" + playerBanned: "{player}は{banner}によってチャンネル {channel} から永久追放されました" + autoFallback: "アクティブなチャンネルがないため、メッセージはグローバルチャットに送信されました" + invite: + success: "{player}をチャンネル {channel} に招待しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーとモデレーターのみがプレイヤーを招待できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + cannotInviteSelf: "自分自身を招待することはできません" + playerBanned: "{player}はこのチャンネルから永久追放されています" + channelFull: "招待できません: チャンネルはメンバー数の上限 ({limit}人) に達しています" + receivedInvite: "チャンネル {channel} に{inviter}から招待されました" + error: "プレイヤーの招待に失敗しました" + kick: + success: "チャンネル {channel} から{player}を追放しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーとモデレーターのみがプレイヤーを追放できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + cannotKickSelf: "自分自身を追放することはできません" + cannotKickBypass: "{player}は追放できません" + notMember: "{player}はこのチャンネルのメンバーではありません" + wasKicked: "{kicker}によってチャンネル {channel} から追放されました" + error: "プレイヤーの追放に失敗しました" + ban: + success: "チャンネル {channel} から{player}を永久追放しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーとモデレーターのみがプレイヤーを永久追放できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + cannotBanSelf: "自分自身を永久追放することはできません" + cannotBanBypass: "{player}は追放できません" + alreadyBanned: "{player}は既にこのチャンネルから永久追放されています" + wasBanned: "{banner}によってチャンネル {channel} から永久追放されました" + error: "プレイヤーの永久追放に失敗しました" + unban: + success: "チャンネル {channel} から{player}の追放を解除しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーとモデレーターのみがプレイヤーの追放を解除できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + playerNotBanned: "{player}はこのチャンネルから永久追放されていません" + error: "プレイヤーの追放の解除に失敗しました" + mod: + success: "チャンネル {channel} で{player}を{action}しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーのみがモデレーターを管理できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + notMember: "{player}はこのチャンネルのメンバーではありません" + cannotModSelf: "自分自身のモデレーター状態を変更することはできません" + promoted: "モデレーターに昇格" + demoted: "モデレーターから降格" + notification: "チャンネル {channel} で{action}されました" + error: "モデレーター状態の変更に失敗しました" + ownership: + success: "チャンネル {channel} のオーナー権限を{player}に譲渡しました" + noActiveChannel: "アクティブなチャンネルがありません" + noPermission: "チャンネルのオーナーのみがオーナー権限を譲渡できます" + playerNotFound: "プレイヤー '{player}' が見つかりません" + notMember: "{player}はこのチャンネルのメンバーではありません" + cannotTransferToSelf: "自分自身にオーナー権限を譲渡することはできません" + receivedOwnership: "チャンネル {channel} のオーナーになりました (前のオーナー: {previousOwner})" + error: "オーナー権限の譲渡に失敗しました" chatmode: current: "現在のチャットモード" diff --git a/platform-paper/src/main/resources/paper-plugin.yml b/platform-paper/src/main/resources/paper-plugin.yml index b3a9d8f..38d2026 100644 --- a/platform-paper/src/main/resources/paper-plugin.yml +++ b/platform-paper/src/main/resources/paper-plugin.yml @@ -33,8 +33,22 @@ permissions: default: true lunaticchat.command.lc.channel.status: default: true + lunaticchat.command.lc.channel.info: + default: true lunaticchat.command.lc.channel.delete: default: true + lunaticchat.command.lc.channel.invite: + default: true + lunaticchat.command.lc.channel.kick: + default: true + lunaticchat.command.lc.channel.ban: + default: true + lunaticchat.command.lc.channel.unban: + default: true + lunaticchat.command.lc.channel.mod: + default: true + lunaticchat.command.lc.channel.ownership: + default: true lunaticchat.command.lc.chatmode: default: true lunaticchat.command.lc.chatmode.toggle: @@ -42,5 +56,7 @@ permissions: lunaticchat.spy: default: op - lunaticchat.noticeUpdate: + lunaticchat.noticeupdate: + default: op + lunaticchat.channelbypass: default: op |
