summaryrefslogtreecommitdiff
path: root/engine/src
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-01-26 02:58:23 +0900
committerGitHub <noreply@github.com>2026-01-26 02:58:23 +0900
commit18a4bbc4ba1a6766af920fb0849215f058ff260e (patch)
treec570633f29f2398cb263e2eb90a7d56f0ea41e78 /engine/src
parent0fb8f4c92ad5dd7d20778de862b4d12f67f71d5e (diff)
parentc7714add1934baf55a5e6268b1a70780eaa84fea (diff)
downloadLunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.gz
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.tar.bz2
LunaticChat-18a4bbc4ba1a6766af920fb0849215f058ff260e.zip
Merge pull request #69 from m1sk9/feat-core-channel-chat
feat: Core Channel Features (Phase 1)
Diffstat (limited to 'engine/src')
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt43
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt14
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt40
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt11
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt22
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt17
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt5
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt7
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt5
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt8
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt6
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt12
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt12
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt6
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/permission/LunaticChatPermissionNode.kt20
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt2
-rw-r--r--engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt9
20 files changed, 262 insertions, 0 deletions
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt
new file mode 100644
index 0000000..98bb79d
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatMode.kt
@@ -0,0 +1,43 @@
+package dev.m1sk9.lunaticChat.engine.chat
+
+/**
+ * Represents the chat mode for a player.
+ *
+ * Chat mode determines where messages are sent by default:
+ * - GLOBAL: All messages go to global chat (visible to everyone)
+ * - CHANNEL: All messages go to the active channel (visible to channel members only)
+ *
+ * Players can temporarily override their mode by prefixing messages with '!'.
+ * Chat mode is persisted and maintains its state across server restarts.
+ * Default mode is GLOBAL.
+ */
+enum class ChatMode {
+ /**
+ * Global chat mode - messages visible to all players.
+ */
+ GLOBAL,
+
+ /**
+ * Channel chat mode - messages visible only to channel members.
+ * Requires player to be in an active channel.
+ */
+ CHANNEL,
+ ;
+
+ /**
+ * Returns the opposite chat mode.
+ * Used when player prefixes message with '!'.
+ */
+ fun toggle(): ChatMode =
+ when (this) {
+ GLOBAL -> CHANNEL
+ CHANNEL -> GLOBAL
+ }
+
+ companion object {
+ /**
+ * Default chat mode for all players.
+ */
+ val DEFAULT = GLOBAL
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt
new file mode 100644
index 0000000..b462ede
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeData.kt
@@ -0,0 +1,14 @@
+package dev.m1sk9.lunaticChat.engine.chat
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+@Serializable
+data class ChatModeData(
+ val modes: Map<
+ @Serializable(with = UUIDSerializer::class)
+ UUID,
+ ChatMode,
+ > = emptyMap(),
+)
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
new file mode 100644
index 0000000..cc1f743
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/Channel.kt
@@ -0,0 +1,40 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+/**
+ * Represents a communication channel within the LunaticChat application.
+ *
+ * @property id Unique identifier for the channel.
+ * @property name Name of the channel.
+ * @property description Optional description of the channel.
+ * @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.
+ */
+@Serializable
+data class Channel(
+ val id: String,
+ val name: String,
+ val description: String? = null,
+ val isPrivate: Boolean = false,
+ @Serializable(with = UUIDSerializer::class)
+ val ownerId: UUID,
+ val createdAt: Long = System.currentTimeMillis(),
+) {
+ init {
+ require(id.matches(CHANNEL_ID_PATTERN)) {
+ "Channel ID must be 3-30 characters long and can only contain letters, numbers, underscores, and hyphens."
+ }
+
+ require(name.isNotBlank()) {
+ "Channel name cannot be blank."
+ }
+ }
+
+ companion object {
+ val CHANNEL_ID_PATTERN = Regex("^[a-zA-Z0-9_-]{3,30}$")
+ }
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt
new file mode 100644
index 0000000..02dc1d3
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelContext.kt
@@ -0,0 +1,7 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+data class ChannelContext(
+ val channelId: String,
+ val channel: Channel,
+ val members: List<ChannelMember>,
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt
new file mode 100644
index 0000000..de942f7
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelData.kt
@@ -0,0 +1,11 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ChannelData(
+ val version: Int = 1,
+ val channels: Map<String, Channel> = emptyMap(),
+ val members: Map<String, List<ChannelMember>> = emptyMap(),
+ val activeChannels: Map<String, String> = emptyMap(),
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt
new file mode 100644
index 0000000..1eb49f4
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMember.kt
@@ -0,0 +1,22 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import dev.m1sk9.lunaticChat.engine.settings.UUIDSerializer
+import kotlinx.serialization.Serializable
+import java.util.UUID
+
+/**
+ * Represents a member of a channel with their role and join timestamp.
+ *
+ * @property channelId The ID of the channel.
+ * @property playerId The UUID of the player.
+ * @property role The role of the member in the channel.
+ * @property joinedAt The timestamp when the member joined the channel.
+ */
+@Serializable
+data class ChannelMember(
+ val channelId: String,
+ @Serializable(with = UUIDSerializer::class)
+ val playerId: UUID,
+ val role: ChannelRole,
+ val joinedAt: Long = System.currentTimeMillis(),
+)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt
new file mode 100644
index 0000000..be7bf58
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelRole.kt
@@ -0,0 +1,17 @@
+package dev.m1sk9.lunaticChat.engine.chat.channel
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the role of a user within a channel.
+ *
+ * - OWNER: The creator and primary administrator of the channel.
+ * - MODERATOR: A user with elevated permissions to manage channel content and users.
+ * - MEMBER: A regular user with standard access to the channel.
+ */
+@Serializable
+enum class ChannelRole {
+ OWNER,
+ MODERATOR,
+ MEMBER,
+}
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt
new file mode 100644
index 0000000..6d88a73
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelAlreadyActiveException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelAlreadyActiveException(
+ val playerId: UUID,
+ val channelId: String,
+) : Exception("Channel $channelId is already active for player $playerId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt
new file mode 100644
index 0000000..deaacb1
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberAlreadyException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelMemberAlreadyException(
+ playerId: UUID,
+ val channelId: String,
+) : Exception("Player $playerId is already a member of channel $channelId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt
new file mode 100644
index 0000000..8dfd6f2
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelMemberNotFoundException.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelMemberNotFoundException(
+ channelId: String,
+) : Exception("Channel member not found in channel with ID $channelId.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt
new file mode 100644
index 0000000..2cc6625
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNoOwnerPermissionException.kt
@@ -0,0 +1,7 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelNoOwnerPermissionException(
+ ownerId: UUID,
+) : Exception("No permission for channel owned by player with ID $ownerId.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt
new file mode 100644
index 0000000..0f60766
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotFoundException.kt
@@ -0,0 +1,5 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelNotFoundException(
+ channelId: String,
+) : Exception("Channel with ID $channelId not found.")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt
new file mode 100644
index 0000000..b66c791
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelNotMemberException.kt
@@ -0,0 +1,8 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+import java.util.UUID
+
+class ChannelNotMemberException(
+ playerId: UUID,
+ channelId: String,
+) : Exception("Player $playerId is not a member of channel $channelId")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt
new file mode 100644
index 0000000..4805489
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelRuntimeException.kt
@@ -0,0 +1,6 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChannelRuntimeException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt
new file mode 100644
index 0000000..21bd654
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageLoadException.kt
@@ -0,0 +1,12 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+/**
+ * Exception thrown when there is an error loading channel storage.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception, if any.
+ */
+class ChannelStorageLoadException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt
new file mode 100644
index 0000000..0e9990b
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChannelStorageSaveException.kt
@@ -0,0 +1,12 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+/**
+ * Exception thrown when there is an error saving channel storage.
+ *
+ * @param message The detail message for the exception.
+ * @param cause The cause of the exception, if any.
+ */
+class ChannelStorageSaveException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt
new file mode 100644
index 0000000..85c1e69
--- /dev/null
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/exception/ChatModeStorageException.kt
@@ -0,0 +1,6 @@
+package dev.m1sk9.lunaticChat.engine.exception
+
+class ChatModeStorageException(
+ message: String,
+ cause: Throwable? = null,
+) : Exception(message, cause)
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 9328423..838a48b 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
@@ -13,6 +13,26 @@ sealed class LunaticChatPermissionNode(
object Status : LunaticChatPermissionNode("lunaticchat.command.lc.status")
+ object Channel : LunaticChatPermissionNode("lunaticchat.command.lc.channel")
+
+ object ChannelCreate : LunaticChatPermissionNode("lunaticchat.command.lc.channel.create")
+
+ object ChannelList : LunaticChatPermissionNode("lunaticchat.command.lc.channel.list")
+
+ object ChannelJoin : LunaticChatPermissionNode("lunaticchat.command.lc.channel.join")
+
+ object ChannelLeave : LunaticChatPermissionNode("lunaticchat.command.lc.channel.leave")
+
+ object ChannelSwitch : LunaticChatPermissionNode("lunaticchat.command.lc.channel.switch")
+
+ object ChannelStatus : LunaticChatPermissionNode("lunaticchat.command.lc.channel.status")
+
+ object ChannelDelete : LunaticChatPermissionNode("lunaticchat.command.lc.channel.delete")
+
+ 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")
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
index e970ae9..4cb44b4 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerChatSettings.kt
@@ -9,6 +9,7 @@ import java.util.UUID
* @property uuid The unique identifier of the player
* @property japaneseConversionEnabled Whether romaji-to-Japanese conversion is enabled for this player
* @property directMessageNotificationEnabled Whether direct message notifications are enabled for this player
+ * @property channelMessageNotificationEnabled Whether channel message notifications are enabled for this player
*/
@Serializable
data class PlayerChatSettings(
@@ -16,4 +17,5 @@ data class PlayerChatSettings(
val uuid: UUID,
val japaneseConversionEnabled: Boolean = true,
val directMessageNotificationEnabled: Boolean = true,
+ val channelMessageNotificationEnabled: Boolean = true,
)
diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
index 3916bbf..0e3cd24 100644
--- a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
+++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/settings/PlayerSettingsData.kt
@@ -16,11 +16,15 @@ import java.util.UUID
* directMessageNotification:
* ceaea267-39dd-3bac-931c-761ada671ebe: true
* another-uuid-here: false
+ * channelMessageNotification:
+ * ceaea267-39dd-3bac-931c-761ada671ebe: true
+ * another-uuid-here: false
* ```
*
* @property version The schema version for future compatibility
* @property japaneseConversion Map of player UUIDs to their Japanese conversion enabled status
* @property directMessageNotification Map of player UUIDs to their direct message notification enabled status
+ * @property channelMessageNotification Map of player UUIDs to their channel message notification enabled status
*/
@Serializable
data class PlayerSettingsData(
@@ -35,4 +39,9 @@ data class PlayerSettingsData(
UUID,
Boolean,
> = emptyMap(),
+ val channelMessageNotification: Map<
+ @Serializable(with = UUIDASStringSerializer::class)
+ UUID,
+ Boolean,
+ > = emptyMap(),
)