summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-27 18:56:34 +0900
committerGitHub <noreply@github.com>2026-01-27 18:56:34 +0900
commitbb2dbeb80ade2a7e128d9eb62fff65358779b075 (patch)
tree4090253d55c73bb1079dfc6260d49cfe5b497a93
parentc3649ec2b53b21ed5b859cc9410f8db58e661d31 (diff)
parent5c3a0d8027eb7be1d5decea71eae4d9bd03accc7 (diff)
downloadLunaticChat-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
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt5
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelCannotInviteSelfException.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelLimitExceededException.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberLimitExceededException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerAlreadyBannedException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBannedException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassBanException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerBypassKickException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerMembershipLimitExceededException.kt10
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPlayerNotBannedException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelPrivateRequiresInvitationException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt18
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/LunaticChat.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceContainer.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/ServiceInitializer.kt33
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelManager.kt178
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMembershipManager.kt68
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelMessageHandler.kt36
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/handler/ChannelNotificationHandler.kt142
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChannelCommand.kt124
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/ChatModeCommand.kt8
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/LunaticChatCommand.kt19
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelBanCommand.kt210
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelCreateCommand.kt41
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelDeleteCommand.kt31
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInfoCommand.kt179
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelInviteCommand.kt192
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelJoinCommand.kt43
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelKickCommand.kt207
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelLeaveCommand.kt7
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelListCommand.kt97
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelModCommand.kt198
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelOwnershipCommand.kt184
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelStatusCommand.kt59
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/command/impl/lc/channel/ChannelUnbanCommand.kt159
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/ConfigManager.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelChatFeatureConfig.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/EventListenerRegistry.kt3
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerChatListener.kt24
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/listener/PlayerPresenceListener.kt10
-rw-r--r--platform-paper/src/main/resources/config.yml6
-rw-r--r--platform-paper/src/main/resources/languages/en.yml84
-rw-r--r--platform-paper/src/main/resources/languages/ja.yml86
-rw-r--r--platform-paper/src/main/resources/paper-plugin.yml18
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