diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-01-31 20:19:05 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-31 20:19:05 +0900 |
| commit | 28436a3dea7aaed50f06be9a905e9a461d145966 (patch) | |
| tree | 39dadbf4a26405a9de492108b0dbccb776180a7c | |
| parent | ec963f344264edfca80c8e1487206997fc1033ff (diff) | |
| parent | 2b8f53067b7c497bb8c4a90d9dcc41e2d5ca78e6 (diff) | |
| download | LunaticChat-28436a3dea7aaed50f06be9a905e9a461d145966.tar.gz LunaticChat-28436a3dea7aaed50f06be9a905e9a461d145966.tar.bz2 LunaticChat-28436a3dea7aaed50f06be9a905e9a461d145966.zip | |
Merge pull request #81 from m1sk9/fix/78/channel-chat
fix: Prevent other plugins from intercepting channel chat
24 files changed, 833 insertions, 80 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index df917cd..6563e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## v0 +### v0.7.0 + +#### Breaking Changes + +- Other plugins such as CoreProtect can no longer intercept channel chat. + - This has broken the integration functionality with CoreProtect. + - Channel Chat logs are now uniformly accessible via a text-based viewing method. + +---- + +- Channel chat logging functionality has been implemented. + - Logs are now recorded daily in `plugins/LunaticChat/logs/channelchat/`. + ### v0.6.0 - Added experimental feature to Channel Chat support. @@ -36,10 +36,9 @@ See the [Documentation](https://lc.m1sk9.dev/guide/getting-started). - 1on1 Direct Messaging System (`/tell`, `/msg`) - Quick Reply Functionality (`/reply`) - Romaji to Japanese Conversion -- CoreProtect-compatible chat logging +- Channel Chat System - Multi-platform support (Paper, Velocity) (coming soon) - Spigot? No problem, just use Paper! -- Channel Chat System (coming soon) ## License diff --git a/build.gradle.kts b/build.gradle.kts index c275e7d..039b977 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,7 +14,7 @@ plugins { allprojects { group = "dev.m1sk9" - version = "0.6.0" + version = "0.7.0" repositories { mavenCentral() diff --git a/docs/.vitepress/config/en.ts b/docs/.vitepress/config/en.ts index 5718a4e..24a3691 100644 --- a/docs/.vitepress/config/en.ts +++ b/docs/.vitepress/config/en.ts @@ -19,6 +19,19 @@ export const en: DefaultTheme.Config = { text: 'For Server Administrators', items: [ { + text: 'Channel Chat', + items: [ + { + text: 'Deployment Guide', + link: '/en/guide/admin/channel-chat/introduction', + }, + { + text: 'Logs', + link: '/en/guide/admin/channel-chat/logs', + }, + ], + }, + { text: 'Cache System', link: '/en/guide/admin/cache', }, @@ -31,12 +44,8 @@ export const en: DefaultTheme.Config = { link: '/en/guide/admin/velocity', }, { - text: 'Channel Chat Deployment Guide', - link: '/en/guide/admin/introduction-channel-chat', - }, - { - text: 'Data and Logs', - link: '/en/guide/admin/data-and-logs', + text: 'Data Management', + link: '/en/guide/admin/management-data', }, ], }, @@ -59,7 +68,7 @@ export const en: DefaultTheme.Config = { text: 'Moderation', link: '/en/guide/player/channel-chat/moderation', }, - ] + ], }, { text: 'Direct Messages', diff --git a/docs/.vitepress/config/ja.ts b/docs/.vitepress/config/ja.ts index 9882687..4beec42 100644 --- a/docs/.vitepress/config/ja.ts +++ b/docs/.vitepress/config/ja.ts @@ -19,6 +19,19 @@ export const ja: DefaultTheme.Config = { text: 'サーバー管理者向け', items: [ { + text: 'チャンネルチャット', + items: [ + { + text: '展開ガイド', + link: '/guide/admin/channel-chat/introduction', + }, + { + text: 'ログ', + link: '/guide/admin/channel-chat/logs', + }, + ], + }, + { text: 'キャッシュシステム', link: '/guide/admin/cache', }, @@ -30,14 +43,10 @@ export const ja: DefaultTheme.Config = { text: 'Velocity 連携', link: '/guide/admin/velocity', }, - { - text: "チャンネルチャット展開ガイド", - link: "/guide/admin/introduction-channel-chat", - }, - { - text: "データ・ログ", - link: "/guide/admin/data-and-logs", - } + { + text: 'データの管理', + link: '/guide/admin/management-data', + }, ], }, { @@ -45,21 +54,21 @@ export const ja: DefaultTheme.Config = { items: [ { text: 'チャンネルチャット', - link: "/guide/player/channel-chat/about", + link: '/guide/player/channel-chat/about', items: [ - { - text: 'チャットモード', - link: '/guide/player/channel-chat/chatmode', - }, - { - text: 'プライベートチャンネル', - link: '/guide/player/channel-chat/private-channel', - }, - { - text: 'モデレーション', - link: '/guide/player/channel-chat/moderation', - }, - ] + { + text: 'チャットモード', + link: '/guide/player/channel-chat/chatmode', + }, + { + text: 'プライベートチャンネル', + link: '/guide/player/channel-chat/private-channel', + }, + { + text: 'モデレーション', + link: '/guide/player/channel-chat/moderation', + }, + ], }, { text: 'ダイレクトメッセージ', @@ -120,14 +129,14 @@ export const ja: DefaultTheme.Config = { text: '/lc status', link: '/reference/commands/lc/status', }, - { - text: '/lc channel', - link: '/reference/commands/lc/channel', - }, - { - text: '/lc chatmode', - link: '/reference/commands/lc/chatmode', - }, + { + text: '/lc channel', + link: '/reference/commands/lc/channel', + }, + { + text: '/lc chatmode', + link: '/reference/commands/lc/chatmode', + }, ], }, ], diff --git a/docs/biome.jsonc b/docs/biome.jsonc index 5d91e18..7b0325f 100644 --- a/docs/biome.jsonc +++ b/docs/biome.jsonc @@ -2,7 +2,7 @@ "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "files": { "ignoreUnknown": true, - "includes": [".vitepress/config.mts", "biome.jsonc"] + "includes": [".vitepress/config.mts", "biome.jsonc", ".vitepress/config/**"] }, "formatter": { "enabled": true, diff --git a/docs/src/en/guide/admin/introduction-channel-chat.md b/docs/src/en/guide/admin/channel-chat/introduction.md index 2a81d63..e755139 100644 --- a/docs/src/en/guide/admin/introduction-channel-chat.md +++ b/docs/src/en/guide/admin/channel-chat/introduction.md @@ -1,4 +1,4 @@ -# Channel Chat Deployment Guide +# Channel Chat: Deployment Guide This guide explains how to deploy channel chat. @@ -6,7 +6,7 @@ This guide explains how to deploy channel chat. Channel chat is a feature that allows players to create channels and share chat among specific players. -For detailed features, please refer to the [Player Guide](../player/channel-chat/about.md). +For detailed features, please refer to the [Player Guide](../../player/channel-chat/about.md). ## Preparing to Deploy Channel Chat @@ -43,9 +43,21 @@ Recommended settings are as follows: ::: +## Channel Chat Logging + +::: warning Compatibility with Plugins like CoreProtect + +As of v0.7.0, LunaticChat's channel chat feature is not compatible with logging plugins like CoreProtect. + +::: + +Channel chat logging is enabled by default. + +For more details, see [Channel Chat: Logs](logs.md). + ## Channel Management -Basically, [players manage channels themselves](../player/channel-chat/moderation.md). +Basically, [players manage channels themselves](../../player/channel-chat/moderation.md). However, as a server administrator, please note the following: @@ -54,3 +66,9 @@ However, as a server administrator, please note the following: - If inappropriate channels or member behavior occurs, take appropriate action. If you want to avoid troubles related to channel management, consider restricting moderate commands such as `/lc channel ban`. + +## Plugins that Intercept Channel Chat + +Logging plugins like CoreProtect do not intercept LunaticChat's channel chat messages by default. + +However, plugins that use Paper API's `originalMessage()` to retrieve messages may intercept LunaticChat's channel chat messages. diff --git a/docs/src/en/guide/admin/channel-chat/logs.md b/docs/src/en/guide/admin/channel-chat/logs.md new file mode 100644 index 0000000..b65750b --- /dev/null +++ b/docs/src/en/guide/admin/channel-chat/logs.md @@ -0,0 +1,119 @@ +# Channel Chat: Logs <Badge type="tip" text="v0.7.0" /> + +Channel chat logs are a record of all messages and activities that occur within the chat. + +::: warning Compatibility with Plugins like CoreProtect + +As of v0.7.0, LunaticChat's channel chat feature is not compatible with logging plugins like CoreProtect. + +::: + +## Checking Channel Chat Logs + +Channel chat logs are stored in the `plugins/LunaticChat/logs/channelchat/` directory. + +Channel chat log files are saved in the following format: + +``` +{"timestamp":"2026-01-31T08:54:18.504343071Z","playerId":"ceaea267-39dd-3bac-931c-761ada671ebe","playerName":"m1sk9","channelId":"test","message":"Hello"} +``` + +## About File Size + +Each line is a complete JSON object, separated by line breaks, and the file as a whole is not a JSON array. + +```text +plugins/LunaticChat/logs/ +├── channel-messages-2026-01-17.json (5.2 MB) +├── channel-messages-2026-01-18.json (4.8 MB) +├── channel-messages-2026-01-19.json (6.1 MB) +├── channel-messages-2026-01-20.json (5.5 MB) +├── channel-messages-2026-01-21.json (7.2 MB) <- Weekend, active +├── channel-messages-2026-01-22.json (6.9 MB) +├── channel-messages-2026-01-23.json (4.3 MB) +├── channel-messages-2026-01-24.json (5.0 MB) +├── channel-messages-2026-01-25.json (5.4 MB) +├── channel-messages-2026-01-26.json (4.9 MB) +├── channel-messages-2026-01-27.json (6.2 MB) +├── channel-messages-2026-01-28.json (7.5 MB) +├── channel-messages-2026-01-29.json (5.8 MB) +├── channel-messages-2026-01-30.json (6.0 MB) +└── channel-messages-2026-01-31.json (2.1 MB) <- Today (in progress) +``` + +Total: approximately 83 MB + +With a 30-day retention setting, the January 17 file will be automatically deleted tomorrow. + +### Size per Message + +Channel chat log entries are approximately 200 bytes per line. + +Assuming 1000 messages per hour: + +```text +1000 msg/h × 24h × 220 bytes = 5,280,000 bytes ≈ 5.3 MB/day +``` + +With the default 30-day retention setting, this amounts to approximately **159 MB**. + +```text +5.3 MB × 30 days = 159 MB +``` + +## Visualization with Grafana Loki + +Due to the JSON format, using Promtail to ingest channel chat logs into Grafana Loki will parse them for easier reading. + +```text +2026-01-31 10:23:45.123 {job="lunatichat", player="Steve", channel="Global"} +Hello everyone! + +2026-01-31 10:24:12.456 {job="lunatichat", player="Alex", channel="Global"} +Hi Steve! + +2026-01-31 10:25:03.789 {job="lunatichat", player="Notch", channel="Development Team"} +Working on new features +``` + +::: tip Filtering Examples + +Examples of querying logs in Grafana Loki: + +```text +{job="lunatichat"} |= "new features" +{job="lunatichat", channel="Global"} +{job="lunatichat", player="Steve"} +``` + +::: + +## Command Line Checking Examples + +### View the latest 10 entries + +```bash +tail -n 10 plugins/LunaticChat/logs/channel-messages-2026-01-31.json | jq +``` + +### Extract messages from a specific player + +```bash +cat plugins/LunaticChat/logs/channel-messages-*.json | \ +jq 'select(.playerName=="Steve")' +``` + +### Count messages from a specific channel + +```bash +cat plugins/LunaticChat/logs/channel-messages-*.json | \ +jq 'select(.channelId=="global")' | wc -l +``` + +### Message count by date + +```bash +for file in plugins/LunaticChat/logs/channel-messages-*.json; do +echo "$file: $(wc -l < $file) messages" +done +``` diff --git a/docs/src/en/guide/admin/configuration.md b/docs/src/en/guide/admin/configuration.md index 6ce63b4..ef33ddc 100644 --- a/docs/src/en/guide/admin/configuration.md +++ b/docs/src/en/guide/admin/configuration.md @@ -54,6 +54,20 @@ 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 + # Channel message logging configuration + messageLogging: + # If enabled, all channel messages will be logged to NDJSON files for analysis and archival. + enabled: true + # Number of days to retain log files. Set to 0 to keep logs indefinitely. + retentionDays: 30 + # Maximum size of a single log file in megabytes. Files exceeding this size will stop accepting new entries. + maxFileSizeMB: 100 # ---------------------------------------------- # --------- Message Format Settings -------- @@ -171,13 +185,67 @@ Specifies the timeout duration (in milliseconds) for API requests to the romaniz Specifies the number of retry attempts for failed API requests to the romanization conversion service. -### `features.channelChat.enabled` +### `features.channelChat` + +#### `enabled` - Type: `boolean` - Default: `false` Enables channel-based chat functionality. +#### `maxChannelsPerServer` + +- Type: `integer` +- Default: `0` + +Specifies the maximum number of channels that can be created per server. + +Set to `0` for unlimited. + +#### `maxMembersPerChannel` + +- Type: `integer` +- Default: `0` + +Specifies the maximum number of members that can join a single channel. + +Set to `0` for unlimited. + +#### `maxMembershipPerPlayer` + +- Type: `integer` +- Default: `0` + +Specifies the maximum number of channels a single player can join. + +Set to `0` for unlimited. + +#### `messageLogging.enabled` + +- Type: `boolean` +- Default: `true` + +Specifies whether to log channel chat messages in NDJSON format. + +#### `messageLogging.retentionDays` + +- Type: `integer` +- Default: `30` + +Specifies the number of days to retain channel chat log files. + +Set to `0` to never delete log files. + +#### `messageLogging.maxFileSizeMB` + +- Type: `integer` +- Default: `100` + +Specifies the maximum size (in megabytes) of channel chat log files. + +Log files exceeding `maxFileSizeMB` will stop accepting new entries. + ## Message Format Settings Available placeholders: diff --git a/docs/src/en/guide/admin/data-and-logs.md b/docs/src/en/guide/admin/management-data.md index e869552..ffed59d 100644 --- a/docs/src/en/guide/admin/data-and-logs.md +++ b/docs/src/en/guide/admin/management-data.md @@ -105,15 +105,3 @@ If the version does not match, LunaticChat recognizes the cache file as **old fo ```json {"version":"1","entries":{}} ``` - -## About CoreProtect - -Various chat logs in LunaticChat can also be recorded in CoreProtect without requiring an API. - -Logs for each feature can be checked with the following actions. When using the `/co lookup` command, specify the following actions: - -- Direct messages: `command` -- Global chat and channel chat: `chat` - - Japanese and romanization conversion is saved according to the player's settings. - - diff --git a/docs/src/en/index.md b/docs/src/en/index.md index c88e080..0fe8a53 100644 --- a/docs/src/en/index.md +++ b/docs/src/en/index.md @@ -32,9 +32,6 @@ features: - title: Japanese Romanization Conversion details: Automatically converts messages input in romaji to Japanese icon: 🌍 - - title: CoreProtect Support - details: LunaticChat's chat logs are compatible with CoreProtect - icon: 🗒️ - title: Channel Chat Feature (Planned) details: Create and manage chat channels, send private messages between specific players icon: ☎️ diff --git a/docs/src/guide/admin/introduction-channel-chat.md b/docs/src/guide/admin/channel-chat/introduction.md index 6993435..cf9fe52 100644 --- a/docs/src/guide/admin/introduction-channel-chat.md +++ b/docs/src/guide/admin/channel-chat/introduction.md @@ -1,4 +1,4 @@ -# チャンネルチャット展開ガイド +# チャンネルチャット: 展開ガイド このガイドでは、チャンネルチャットの展開方法について説明します. @@ -6,7 +6,7 @@ プレイヤー間でチャンネルを作成し,特定のプレイヤー間で,チャットを共有できる機能です. -詳しい機能については [プレイヤー向けガイド版](../player/channel-chat/about.md) を参照してください. +詳しい機能については [プレイヤー向けガイド版](../../player/channel-chat/about.md) を参照してください. ## チャンネルチャットの展開準備 @@ -43,9 +43,21 @@ ::: +## チャンネルチャットのログ + +::: warning CoreProtect などのプラグインとの互換性 + +LunaticChat では,チャンネルチャット機能に限り v0.7.0 以降 CoreProtect などのログ記録プラグインと互換性はありません. + +::: + +チャンネルチャットのログ機能はデフォルトで有効になっています. + +詳しくは [チャンネルチャット: ログ](logs.md) を参照してください. + ## チャンネルの管理 -基本的に,[チャンネルの管理はプレイヤー自身が行います](../player/channel-chat/moderation.md). +基本的に,[チャンネルの管理はプレイヤー自身が行います](../../player/channel-chat/moderation.md). ただし,サーバー管理者として,以下の点に注意してください: @@ -54,3 +66,9 @@ - 不適切なチャンネルやメンバー行動が発生した場合は,適切な措置を講じてください. また,それらのチャンネルの管理によるトラブルを回避したい場合は `/lc channel ban` などのモデレートコマンドの制限を検討してください. + +## チャンネルチャットを捕捉してしまうプラグイン + +基本的に CoreProtect などのログ記録プラグインは,LunaticChat のチャンネルチャットメッセージを捕捉しません. + +ただし,Paper API の `originalMessage()` を使用してメッセージを取得しているプラグインは,LunaticChat のチャンネルチャットメッセージを捕捉してしまう可能性があります. diff --git a/docs/src/guide/admin/channel-chat/logs.md b/docs/src/guide/admin/channel-chat/logs.md new file mode 100644 index 0000000..9b88bdd --- /dev/null +++ b/docs/src/guide/admin/channel-chat/logs.md @@ -0,0 +1,119 @@ +# チャンネルチャット: ログ <Badge type="tip" text="v0.7.0" /> + +チャンネルチャットのログは,チャット内で行われたすべてのメッセージとアクティビティの記録です. + +::: warning CoreProtect などのプラグインとの互換性 + +LunaticChat では,チャンネルチャット機能に限り v0.7.0 以降 CoreProtect などのログ記録プラグインと互換性はありません. + +::: + +## チャンネルチャットのログを確認する + +チャンネルチャットのログは `plugins/LunaticChat/logs/channelchat/` ディレクトリに保存されます. + +チャンネルチャットのログファイルは,以下のフォーマットで保存されます: + +``` +{"timestamp":"2026-01-31T08:54:18.504343071Z","playerId":"ceaea267-39dd-3bac-931c-761ada671ebe","playerName":"m1sk9","channelId":"test","message":"こんにちは"} +``` + +## ファイルサイズについて + +各行が1つの完全な JSON オブジェクトであり,各行で区切られているだけで,ファイル全体としては JSON 配列ではありません. + +```text +plugins/LunaticChat/logs/ +├── channel-messages-2026-01-17.json (5.2 MB) +├── channel-messages-2026-01-18.json (4.8 MB) +├── channel-messages-2026-01-19.json (6.1 MB) +├── channel-messages-2026-01-20.json (5.5 MB) +├── channel-messages-2026-01-21.json (7.2 MB) <- 週末、アクティブ +├── channel-messages-2026-01-22.json (6.9 MB) +├── channel-messages-2026-01-23.json (4.3 MB) +├── channel-messages-2026-01-24.json (5.0 MB) +├── channel-messages-2026-01-25.json (5.4 MB) +├── channel-messages-2026-01-26.json (4.9 MB) +├── channel-messages-2026-01-27.json (6.2 MB) +├── channel-messages-2026-01-28.json (7.5 MB) +├── channel-messages-2026-01-29.json (5.8 MB) +├── channel-messages-2026-01-30.json (6.0 MB) +└── channel-messages-2026-01-31.json (2.1 MB) <- 今日(進行中) +``` + +合計: 約 83 MB + +30日保持設定 の場合,1月17日のファイルは明日自動削除されます. + +### 1メッセージあたりのサイズ + +チャンネルチャットのログエントリは1行につき,約200バイトです. + +1時間に1000メッセージの計算として + +```text +1000 msg/h × 24h × 220 bytes = 5,280,000 bytes ≈ 5.3 MB/日 +``` + +デフォルト設定の30日間保持の場合は **159 MB** 程度になります. + +```text +5.3 MB × 30日 = 159 MB +``` + +## Grafana Loki での可視化 + +JSON 形式のため,Promtail を使用し,Grafana Loki にチャンネルチャットのログを取り込むとパースされ読みやすくなります. + +```text +2026-01-31 10:23:45.123 {job="lunatichat", player="Steve", channel="Global"} +Hello everyone! + +2026-01-31 10:24:12.456 {job="lunatichat", player="Alex", channel="Global"} +Hi Steve! + +2026-01-31 10:25:03.789 {job="lunatichat", player="Notch", channel="Development Team"} +Working on new features +``` + +::: tip フィルタリング例 + +Grafana Loki でログをクエリ化する例: + +```text +{job="lunatichat"} |= "new features" +{job="lunatichat", channel="Global"} +{job="lunatichat", player="Steve"} +``` + +::: + +## コマンドラインでの確認例 + +### 最新10件を見る + +```bash +tail -n 10 plugins/LunaticChat/logs/channel-messages-2026-01-31.json | jq +``` + +### 特定プレイヤーのメッセージを抽出 + +```bash +cat plugins/LunaticChat/logs/channel-messages-*.json | \ +jq 'select(.playerName=="Steve")' +``` + +### 特定チャンネルのメッセージ数をカウント + +```bash +cat plugins/LunaticChat/logs/channel-messages-*.json | \ +jq 'select(.channelId=="global")' | wc -l +``` + +### 日付別メッセージ数 + +```bash +for file in plugins/LunaticChat/logs/channel-messages-*.json; do +echo "$file: $(wc -l < $file) messages" +done +``` diff --git a/docs/src/guide/admin/configuration.md b/docs/src/guide/admin/configuration.md index dfa41f9..5eb5fc6 100644 --- a/docs/src/guide/admin/configuration.md +++ b/docs/src/guide/admin/configuration.md @@ -60,6 +60,14 @@ features: maxMembersPerChannel: 0 # Maximum number of channels a single player can join. Set to 0 for unlimited. maxMembershipPerPlayer: 0 + # Channel message logging configuration + messageLogging: + # If enabled, all channel messages will be logged to NDJSON files for analysis and archival. + enabled: true + # Number of days to retain log files. Set to 0 to keep logs indefinitely. + retentionDays: 30 + # Maximum size of a single log file in megabytes. Files exceeding this size will stop accepting new entries. + maxFileSizeMB: 100 # ---------------------------------------------- # --------- Message Format Settings -------- @@ -213,6 +221,31 @@ LunaticChat の [`/reply`](../../reference/commands/reply.md) コマンドによ `0` に設定すると無制限になります. +#### `messageLogging.enabled` + +- Type: `boolean` +- Default: `true` + +チャンネルチャットのメッセージを NDJSON 形式でログに記録するかどうかを指定します. + +#### `messageLogging.retentionDays` + +- Type: `integer` +- Default: `30` + +チャンネルチャットのログファイルを保存する日数を指定します. + +`0` に設定すると,ログファイルは削除されません. + +#### `messageLogging.maxFileSizeMB` + +- Type: `integer` +- Default: `100` + +チャンネルチャットのログファイルの最大サイズ(メガバイト)を指定します. + +`maxFileSizeMB` を超えたログファイルは新しいエントリを受け付けなくなります. + ## Message Format Settings 使用できるプレースホルダー: diff --git a/docs/src/guide/admin/data-and-logs.md b/docs/src/guide/admin/management-data.md index 6f48857..5527ffa 100644 --- a/docs/src/guide/admin/data-and-logs.md +++ b/docs/src/guide/admin/management-data.md @@ -105,15 +105,3 @@ channelMessageNotification: ```json {"version":"1","entries":{}} ``` - -## CoreProtect について - -LunaticChat の各種チャットログは API 不要で CoreProtect でも記録することができます. - -各機能のログは次のアクションで確認できます. `/co lookup` コマンド使用時に以下のアクションを指定してください: - -- ダイレクトメッセージ: `command` -- 全体チャット・チャンネルチャット: `chat` - - かな・ローマ字変換はそのプレイヤーの設定状況により保存されます. - - diff --git a/docs/src/index.md b/docs/src/index.md index 430077f..c3410cb 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -32,9 +32,6 @@ features: - title: かな・ローマ字変換 details: ローマ字で入力したメッセージを,自動的に日本語に変換 icon: 🌍 - - title: CoreProtect への対応 - details: LunaticChat のチャットログは CoreProtect と互換性があります - icon: 🗒️ - title: チャンネルチャット機能 details: チャットチャンネルを作成・管理し,特定のプレイヤー間でのプライベートメッセージを送信可能 icon: ☎️ diff --git a/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMessageLogEntry.kt b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMessageLogEntry.kt new file mode 100644 index 0000000..7cd4eb6 --- /dev/null +++ b/engine/src/main/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelMessageLogEntry.kt @@ -0,0 +1,51 @@ +package dev.m1sk9.lunaticChat.engine.chat.channel + +import kotlinx.serialization.Serializable +import java.time.Instant +import java.util.UUID + +/** + * Represents a single channel message log entry in NDJSON format. + * + * This data class is designed for append-only logging to daily rotated files, + * compatible with Grafana Loki and other log aggregation systems. + * + * @property timestamp ISO-8601 formatted UTC timestamp of when the message was sent + * @property playerId UUID of the player who sent the message + * @property playerName Display name of the player + * @property channelId Unique identifier of the channel + * @property message The chat message content + */ +@Serializable +data class ChannelMessageLogEntry( + val timestamp: String, + val playerId: String, + val playerName: String, + val channelId: String, + val message: String, +) { + companion object { + /** + * Creates a new log entry with the current timestamp. + * + * @param playerId UUID of the player + * @param playerName Display name of the player + * @param channelId Channel identifier + * @param message Message content + * @return New ChannelMessageLogEntry instance + */ + fun create( + playerId: UUID, + playerName: String, + channelId: String, + message: String, + ): ChannelMessageLogEntry = + ChannelMessageLogEntry( + timestamp = Instant.now().toString(), + playerId = playerId.toString(), + playerName = playerName, + channelId = channelId, + message = message, + ) + } +} 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 daba3dd..70495a4 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 @@ -5,6 +5,7 @@ import dev.m1sk9.lunaticChat.paper.chat.ChatModeManager import dev.m1sk9.lunaticChat.paper.chat.ChatModeStorage import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMembershipManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMessageLogger import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelStorage import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelMessageHandler import dev.m1sk9.lunaticChat.paper.chat.handler.ChannelNotificationHandler @@ -49,6 +50,7 @@ class ServiceInitializer( private var chatModeManager: ChatModeManager? = null private var channelMessageHandler: ChannelMessageHandler? = null private var channelNotificationHandler: ChannelNotificationHandler? = null + private var channelMessageLogger: ChannelMessageLogger? = null /** * Initializes all services in dependency order. @@ -227,6 +229,28 @@ class ServiceInitializer( chatMode.initialize() chatModeManager = chatMode + // Initialize channel message logger if enabled + val messageLogger = + if (configuration.features.channelChat.messageLogging.enabled) { + val logsDir = plugin.dataFolder.resolve("logs/channelchat").toPath() + ChannelMessageLogger( + logsDirectory = logsDir, + plugin = plugin, + logger = + io.ktor.util.logging + .KtorSimpleLogger("ChannelMessageLogger"), + maxFileSizeBytes = configuration.features.channelChat.messageLogging.maxFileSizeMB * 1024L * 1024L, + retentionDays = configuration.features.channelChat.messageLogging.retentionDays, + ).also { + channelMessageLogger = it + logger.info( + "Channel message logging enabled (retention: ${configuration.features.channelChat.messageLogging.retentionDays} days)", + ) + } + } else { + null + } + val messageHandler = ChannelMessageHandler( configuration = configuration, @@ -234,6 +258,7 @@ class ServiceInitializer( channelManager = manager, romanjiConverter = romajiConverter, languageManager = languageManager, + messageLogger = messageLogger, logger = io.ktor.util.logging .KtorSimpleLogger("ChannelMessageHandler"), @@ -285,5 +310,6 @@ class ServiceInitializer( conversionCache?.saveToDisk() services.channelManager?.saveToDisk() services.chatModeManager?.shutdown() + channelMessageLogger?.shutdown() } } diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMessageLogger.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMessageLogger.kt new file mode 100644 index 0000000..206968c --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/chat/channel/ChannelMessageLogger.kt @@ -0,0 +1,247 @@ +package dev.m1sk9.lunaticChat.paper.chat.channel + +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMessageLogEntry +import io.ktor.util.logging.Logger +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.bukkit.plugin.Plugin +import java.io.BufferedWriter +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.time.LocalDate +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.io.path.deleteIfExists +import kotlin.io.path.fileSize +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name + +/** + * Asynchronous logger for channel messages. + * + * Writes messages in NDJSON format to daily rotated log files. + * Uses a concurrent queue and scheduled flushing to minimize performance impact. + * + * @property logsDirectory Directory where log files are stored + * @property plugin Bukkit plugin instance for scheduling tasks + * @property logger Logger for diagnostic messages + * @property maxFileSizeBytes Maximum size of a single log file + * @property retentionDays Number of days to retain log files (0 = keep forever) + */ +class ChannelMessageLogger( + private val logsDirectory: Path, + private val plugin: Plugin, + private val logger: Logger, + private val maxFileSizeBytes: Long, + private val retentionDays: Int, +) { + private val pendingEntries = ConcurrentLinkedQueue<ChannelMessageLogEntry>() + private val json = Json { encodeDefaults = true } + private var flushTaskId: Int? = null + private var cleanupTaskId: Int? = null + + companion object { + private const val LOG_FILE_PREFIX = "channel-messages-" + private const val LOG_FILE_EXTENSION = ".json" + private val DATE_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + private const val FLUSH_INTERVAL_TICKS = 20L // 1 second + private const val CLEANUP_INTERVAL_TICKS = 24 * 60 * 60 * 20L // 24 hours + private const val CLEANUP_INITIAL_DELAY_TICKS = 5 * 60 * 20L // 5 minutes + } + + init { + // Ensure logs directory exists + try { + Files.createDirectories(logsDirectory) + logger.info("Channel message logger initialized at: $logsDirectory") + schedulePeriodicFlush() + schedulePeriodicCleanup() + } catch (e: Exception) { + logger.error("Failed to initialize channel message logger", e) + } + } + + /** + * Queues a message for asynchronous logging. + * + * @param entry The log entry to write + */ + fun logMessage(entry: ChannelMessageLogEntry) { + pendingEntries.offer(entry) + } + + /** + * Schedules periodic flushing of pending log entries. + */ + private fun schedulePeriodicFlush() { + flushTaskId = + plugin.server.scheduler + .runTaskTimerAsynchronously( + plugin, + Runnable { flushPendingEntries() }, + FLUSH_INTERVAL_TICKS, + FLUSH_INTERVAL_TICKS, + ).taskId + } + + /** + * Schedules periodic cleanup of old log files. + */ + private fun schedulePeriodicCleanup() { + if (retentionDays <= 0) { + logger.info("Log retention disabled (retentionDays = $retentionDays)") + return + } + + cleanupTaskId = + plugin.server.scheduler + .runTaskTimerAsynchronously( + plugin, + Runnable { cleanupOldLogs(retentionDays) }, + CLEANUP_INITIAL_DELAY_TICKS, + CLEANUP_INTERVAL_TICKS, + ).taskId + + logger.info("Scheduled log cleanup task (retention: $retentionDays days)") + } + + /** + * Flushes all pending entries to the current day's log file. + * Automatically creates new files with suffixes when size limit is exceeded. + * + * This method is synchronized to prevent race conditions between periodic + * flush operations and shutdown flush. + */ + @Synchronized + private fun flushPendingEntries() { + if (pendingEntries.isEmpty()) { + return + } + + val entries = mutableListOf<ChannelMessageLogEntry>() + while (true) { + val entry = pendingEntries.poll() ?: break + entries.add(entry) + } + + if (entries.isEmpty()) { + return + } + + try { + val logFile = getCurrentLogFile() + + BufferedWriter( + Files.newBufferedWriter( + logFile, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND, + ), + ).use { writer -> + for (entry in entries) { + val jsonLine = json.encodeToString(entry) + writer.write(jsonLine) + writer.newLine() + } + } + } catch (e: Exception) { + logger.error("Failed to flush log entries", e) + // Re-queue entries for retry + entries.forEach { pendingEntries.offer(it) } + } + } + + /** + * Shuts down the logger by cancelling scheduled tasks and flushing pending entries. + * Should be called during plugin shutdown. + */ + fun shutdown() { + // Cancel scheduled tasks + flushTaskId?.let { plugin.server.scheduler.cancelTask(it) } + cleanupTaskId?.let { plugin.server.scheduler.cancelTask(it) } + + // Flush remaining entries + flushPendingEntries() + logger.info("Channel message logger shut down (flushed all pending entries)") + } + + /** + * Deletes log files older than the specified retention period. + * Handles both base files (YYYY-MM-DD.json) and suffixed files (YYYY-MM-DD-N.json). + * + * @param retentionDays Number of days to retain log files + */ + fun cleanupOldLogs(retentionDays: Int) { + if (retentionDays <= 0) { + return + } + + try { + val cutoffDate = LocalDate.now(ZoneOffset.UTC).minusDays(retentionDays.toLong()) + val logFiles = logsDirectory.listDirectoryEntries("$LOG_FILE_PREFIX*$LOG_FILE_EXTENSION") + + // Pattern: channel-messages-YYYY-MM-DD(-N)?.json + val datePattern = Regex("""${Regex.escape(LOG_FILE_PREFIX)}(\d{4}-\d{2}-\d{2})(?:-\d+)?${Regex.escape(LOG_FILE_EXTENSION)}""") + + var deletedCount = 0 + for (logFile in logFiles) { + val fileName = logFile.name + val matchResult = datePattern.matchEntire(fileName) + + if (matchResult != null) { + val dateStr = matchResult.groupValues[1] + try { + val fileDate = LocalDate.parse(dateStr, DATE_FORMATTER) + if (fileDate.isBefore(cutoffDate)) { + logFile.deleteIfExists() + deletedCount++ + logger.info("Deleted old log file: $fileName") + } + } catch (e: Exception) { + logger.warn("Failed to parse date from log file: $fileName", e) + } + } else { + logger.warn("Log file name does not match expected pattern: $fileName") + } + } + + if (deletedCount > 0) { + logger.info("Cleaned up $deletedCount old log file(s)") + } + } catch (e: Exception) { + logger.error("Failed to cleanup old logs", e) + } + } + + /** + * Gets the log file path for the current UTC date. + * If the current file exceeds the size limit, returns a new file with a suffix. + * Filenames follow the pattern: channel-messages-YYYY-MM-DD(-N).json + */ + private fun getCurrentLogFile(): Path { + val currentDate = LocalDate.now(ZoneOffset.UTC) + val dateStr = currentDate.format(DATE_FORMATTER) + + // Try base filename first + var fileName = "$LOG_FILE_PREFIX$dateStr$LOG_FILE_EXTENSION" + var logFile = logsDirectory.resolve(fileName) + + // If file exists and exceeds size limit, find next available suffix + var suffix = 1 + while (Files.exists(logFile) && logFile.fileSize() >= maxFileSizeBytes) { + fileName = "$LOG_FILE_PREFIX$dateStr-$suffix$LOG_FILE_EXTENSION" + logFile = logsDirectory.resolve(fileName) + suffix++ + + // Safety limit to prevent infinite loop + if (suffix > 1000) { + logger.error("Too many log files for date $dateStr (limit: 1000), using latest") + break + } + } + + return logFile + } +} 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 12426cf..810d1ca 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 @@ -1,6 +1,8 @@ package dev.m1sk9.lunaticChat.paper.chat.handler +import dev.m1sk9.lunaticChat.engine.chat.channel.ChannelMessageLogEntry import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelManager +import dev.m1sk9.lunaticChat.paper.chat.channel.ChannelMessageLogger import dev.m1sk9.lunaticChat.paper.common.SpyPermissionManager import dev.m1sk9.lunaticChat.paper.common.playChannelReceiveNotification import dev.m1sk9.lunaticChat.paper.common.playMessageSendNotification @@ -20,6 +22,7 @@ class ChannelMessageHandler( private val channelManager: ChannelManager, private val romanjiConverter: RomanjiConverter?, private val languageManager: LanguageManager, + private val messageLogger: ChannelMessageLogger?, private val logger: Logger, ) { fun sendChannelMessage( @@ -92,6 +95,19 @@ class ChannelMessageHandler( } logger.info("Channel Message from ${player.name} in ${context.channel.name}: $message") + + // Log message to file if logging is enabled + messageLogger?.let { + val logEntry = + ChannelMessageLogEntry.create( + playerId = player.uniqueId, + playerName = player.name, + channelId = context.channelId, + message = message, + ) + it.logMessage(logEntry) + } + return true } 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 5b93ac5..9a64428 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 @@ -1,8 +1,12 @@ package dev.m1sk9.lunaticChat.paper.config.key +import kotlinx.serialization.Serializable + +@Serializable data class ChannelChatFeatureConfig( val enabled: Boolean, val maxChannelsPerServer: Int = 0, val maxMembersPerChannel: Int = 0, val maxMembershipPerPlayer: Int = 0, + val messageLogging: ChannelMessageLoggingConfig = ChannelMessageLoggingConfig(), ) diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelMessageLoggingConfig.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelMessageLoggingConfig.kt new file mode 100644 index 0000000..f036620 --- /dev/null +++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/config/key/ChannelMessageLoggingConfig.kt @@ -0,0 +1,20 @@ +package dev.m1sk9.lunaticChat.paper.config.key + +import kotlinx.serialization.Serializable + +/** + * Configuration for channel message logging feature. + * + * Enables file-based logging of all channel messages in NDJSON format, + * compatible with log aggregation systems like Grafana Loki. + * + * @property enabled Whether message logging is enabled + * @property retentionDays Number of days to retain log files (0 = keep forever) + * @property maxFileSizeMB Maximum size of a single log file in megabytes + */ +@Serializable +data class ChannelMessageLoggingConfig( + val enabled: Boolean = true, + val retentionDays: Int = 30, + val maxFileSizeMB: Int = 100, +) 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 ec8804d..03423e5 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 @@ -13,6 +13,7 @@ import kotlinx.coroutines.runBlocking import net.kyori.adventure.text.Component import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer import org.bukkit.event.EventHandler +import org.bukkit.event.EventPriority import org.bukkit.event.Listener class PlayerChatListener( @@ -25,7 +26,7 @@ class PlayerChatListener( ) : Listener { private val plainTextSerializer = PlainTextComponentSerializer.plainText() - @EventHandler(ignoreCancelled = true) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) fun onChat(event: AsyncChatEvent) { val player = event.player val settings = settingsManager.getSettings(player.uniqueId) @@ -81,8 +82,13 @@ class PlayerChatListener( val hasActiveChannel = channelManager.getPlayerChannel(player.uniqueId) != null if (hasActiveChannel) { - // Send to channel as normal + // Cancel event and clear all data to prevent other plugins from capturing it + // Even MONITOR priority listeners with ignoreCancelled=false won't get useful data + // Message will be delivered by ChannelMessageHandler instead + // Logging is handled by our own ChannelMessageLogger event.isCancelled = true + event.viewers().clear() + event.message(Component.empty()) channelMessageHandler.sendChannelMessage(player, messageWithoutPrefix) } else { // Auto-fallback to global chat diff --git a/platform-paper/src/main/resources/config.yml b/platform-paper/src/main/resources/config.yml index 8efa1fa..0889070 100644 --- a/platform-paper/src/main/resources/config.yml +++ b/platform-paper/src/main/resources/config.yml @@ -57,6 +57,14 @@ features: maxMembersPerChannel: 0 # Maximum number of channels a single player can join. Set to 0 for unlimited. maxMembershipPerPlayer: 0 + # Channel message logging configuration + messageLogging: + # If enabled, all channel messages will be logged to NDJSON files for analysis and archival. + enabled: true + # Number of days to retain log files. Set to 0 to keep logs indefinitely. + retentionDays: 30 + # Maximum size of a single log file in megabytes. Files exceeding this size will stop accepting new entries. + maxFileSizeMB: 100 # ---------------------------------------------- # --------- Message Format Settings -------- |
