Skip to content

Feat/logging#19

Merged
sharkbot-neko merged 7 commits intomainfrom
feat/logging
Mar 28, 2026
Merged

Feat/logging#19
sharkbot-neko merged 7 commits intomainfrom
feat/logging

Conversation

@sharkbot-neko
Copy link
Copy Markdown
Contributor

@sharkbot-neko sharkbot-neko commented Mar 28, 2026

  • 新機能
  • メッセージ削除、メッセージ編集、モデレート系、チャンネル・ロール関連のログを追加

Summary by CodeRabbit

  • 新機能

    • サーバーログ機能を追加。メッセージ編集・削除や主要なモデレーション操作をイベント単位で記録します。
    • ダッシュボードにログ設定ページとAPI(ギルド単位の設定管理・イベント単位のCRUD)を追加。管理者認証を行い、イベント一覧からWebhook URLは表示されません。
  • 改善

    • 設定保存時に既存のWebhookを可能な限り保持する挙動を追加。配信失敗時は該当イベント設定を自動で削除します。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6119a239-0f4e-4fd5-8af4-9f7b1903418b

📥 Commits

Reviewing files that changed from the base of the PR and between d58017a and 63d9f9f.

📒 Files selected for processing (1)
  • src/bot/cogs/logging.py

📝 Walkthrough

Walkthrough

ギルド単位のログ設定永続化モデルと API ルーター、ダッシュボードの UI/API、ボットのログ送信コグおよびクライアント API を追加し、イベント単位でチャンネル・Webhook・無視チャネルを保存・操作できるようにします。

Changes

Cohort / File(s) Summary
Backend — 初期化 / モデル
src/api/src/cmd/main.go, src/api/src/internal/model/LoggingSetting.go
LoggingSetting/LoggingEvent とカスタム JSON 型 StringSlice を追加。GORM 用の Value/Scan 実装と main.go に AutoMigrate 登録を追加。
Backend — ルーター
src/api/src/internal/router/logging.go
/guilds/logging/:id/guilds/logging/:id/event/:name の GET/POST/DELETE ハンドラを追加(設定の取得/保存/削除、イベント単位の取得/更新/削除、FirstOrCreate/OnConflict ロジック含む)。
Bot — コグ
src/bot/cogs/logging.py
LoggingCog を追加。イベントごとの Webhook 解決/再利用・作成ロジック、送信エラーハンドリング、on_message_edit/delete と監査ログのイベント処理を実装。
Bot — API クライアント
src/bot/lib/api.py
LoggingEvent 型と ResourceAPIClient にログ設定取得/保存/削除・イベント操作の非同期メソッドを追加。非200時の None/bool 戻りを導入。
Dashboard — サーバー設定ページ / クライアント
src/dashboard/src/app/dashboard/[guildId]/logging/page.tsx, src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx
ログ管理ページとクライアントコンポーネントを追加。イベント追加、チャンネル選択、無視チャネル管理、保存処理を実装。
Dashboard — API ルート / フロントAPI
src/dashboard/src/app/api/guilds/[guildId]/modules/logging/route.ts, src/dashboard/src/lib/api/requests.ts
管理者検証付きの Next.js API ルート(GET/POST/DELETE)を追加。フロント向け fetch/save/delete 関数と型定義を追加。POST で既存 webhook を保持する差分マージロジックを実装。
Dashboard — モジュール登録 & Dockerfile
src/dashboard/src/lib/modules.ts, src/dashboard/Dockerfile
ダッシュボードの modules マップに logging を追加。Dockerfile から非 root ユーザー切替を削除(root 実行へ変更)。

Sequence Diagram(s)

sequenceDiagram
    participant Dashboard as Dashboard(UI)
    participant DashAPI as Dashboard API(Route)
    participant BackendAPI as Backend API(Go)
    participant DB as Database(GORM)
    participant Bot as Bot(Python)
    participant Discord as Discord(Webhook)

    Dashboard->>DashAPI: POST /api/guilds/{id}/modules/logging (保存)
    DashAPI->>DashAPI: validateAdmin(guildId)
    DashAPI->>BackendAPI: POST /guilds/logging/{id} (LoggingSetting JSON)
    BackendAPI->>DB: Upsert LoggingSetting (OnConflict/Save)
    DB-->>BackendAPI: 保存完了
    BackendAPI-->>DashAPI: 200 OK
    DashAPI-->>Dashboard: 成功応答

    Bot->>BackendAPI: GET /guilds/logging/{id}
    BackendAPI->>DB: Query LoggingSetting
    DB-->>BackendAPI: setting JSON
    BackendAPI-->>Bot: LoggingSetting
    Bot->>Bot: Resolve or create webhook (共有再利用チェック)
    Bot->>Discord: POST webhook (Embed)
    Discord-->>Bot: 2xx / NotFound/Forbidden
    alt NotFound or Forbidden
        Bot->>BackendAPI: DELETE event config (on failure)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Feat/logging #19 — 同様のログ機能(モデル・API・ダッシュボード・ボット側)が修正されており、実装が重複または競合する可能性あり。
  • Feat/level レベル機能を追加 #7src/api/src/cmd/main.go のマイグレーション/ルーター登録変更に関連するワイヤリング差分があるため密接に関連。

Suggested labels

difficulity/normal, kind/enhancement, priority/mid

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive PRのタイトル「Feat/logging」は曖昧で説明不足です。ログ機能の追加という大まかなカテゴリのみで、具体的な変更内容を示していません。 具体的な変更内容を含むより詳細なタイトルに変更してください。例えば「Add comprehensive logging system with webhooks and event filtering」のように、主要な機能を明示することをお勧めします。
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/logging

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (1)
src/dashboard/src/lib/api/requests.ts (1)

272-323: logging 系API関数にも Guild ID バリデーションを揃えることを推奨します。

同ファイル内の他APIヘルパーと同様に、invalid ID をリクエスト前に弾く方がデバッグしやすくなります。

リファクタ案(共通ガード)
+function assertValidGuildId(guildId: string) {
+    if (!isValidDiscordId(guildId)) {
+        throw new Error("Invalid Guild ID");
+    }
+}
+
 export async function fetchLoggingSetting(guildId: string): Promise<LoggingSetting> {
+    assertValidGuildId(guildId);
     const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/logging/${guildId}`);
@@
 export async function saveLoggingSetting(guildId: string, data: Partial<LoggingSetting>) {
+    assertValidGuildId(guildId);
     const response = await fetch(`${RESOURCE_API_BASE_URL}/guilds/logging/${guildId}`, {
@@
 export async function saveOneLoggingEvent(guildId: string, eventName: string, data: LoggingEvent) {
+    assertValidGuildId(guildId);
@@
 export async function deleteOneLoggingEvent(guildId: string, eventName: string) {
+    assertValidGuildId(guildId);
@@
 export async function deleteLoggingSetting(guildId: string) {
+    assertValidGuildId(guildId);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/lib/api/requests.ts` around lines 272 - 323, Add the same
pre-request guild ID validation used elsewhere to all logging API helpers
(fetchLoggingSetting, saveLoggingSetting, saveOneLoggingEvent,
deleteOneLoggingEvent, deleteLoggingSetting): call the shared validation guard
(e.g., validateGuildId or request_verification utility) at the top of each
function and throw/return early for invalid IDs before making fetch calls; if no
shared guard exists, extract a small helper (validateGuildId) and use it from
each of these functions to keep behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/src/internal/model/LoggingSetting.go`:
- Around line 13-19: StringSlice.Scan currently only asserts value as []byte
which fails for NULL (nil) or when the DB returns a string; update the
StringSlice.Scan implementation to handle three cases: if value == nil return
nil, if value is a string convert it to []byte (e.g., []byte(str)) then
json.Unmarshal, and if value is []byte behave as today; keep using
json.Unmarshal into s and mirror the nil/string handling used in
ModeratorSetting.go to avoid type assertion errors.

In `@src/api/src/internal/router/logging.go`:
- Around line 13-23: The RegisterLoggingSetting routes are currently exposed
without middleware; require authentication/authorization by attaching the
appropriate middleware to the group or enforcing checks in each handler: update
RegisterLoggingSetting to accept/attach the auth middleware (or call
router.Group("/guilds/logging", <AuthMiddleware>) ) and ensure handlers
getLoggingSetting, saveLoggingSetting, deleteLoggingSetting,
getOneLoggingEventConfig, setOneLoggingEvent, and deleteOneLoggingEvent perform
authorization verifying the requester is the guild admin or a valid service
token before returning or mutating webhook/logging settings. Ensure failure
paths return proper unauthorized/forbidden responses.
- Around line 98-113: The handlers setOneLoggingEvent and deleteOneLoggingEvent
currently ignore errors from db.FirstOrCreate and db.Save so they always return
HTTP 200 even when DB writes fail; update both functions to check the returned
gorm.Error after db.FirstOrCreate(&setting, model.LoggingSetting{GuildID: id})
and after db.Save(&setting) (and after delete operations), log the error, and
return an appropriate non-200 response (e.g., 500 with a JSON error) when
persistence fails so callers can detect webhook save/delete failures; reference
the variables and symbols setting, input, eventName, and the DB calls in each
handler when adding these checks.

In `@src/bot/cogs/logging.py`:
- Around line 56-72: _process_log currently only checks
event_config.ignored_channels and never consults the global ignored list; call
the API helper get_logging_setting(str(guild.id), event_name) (from
src/bot/lib/api.py) to obtain the merged logging setting (which includes
global_ignored_channels and per-event ignored channels) and use that combined
ignored list to decide early-return; also ensure callers like channel_create and
channel_delete pass the trigger_channel_id through to _process_log so per-event
ignores are respected (reference symbols: _process_log, get_logging_setting,
event_config, global_ignored_channels, trigger_channel_id, channel_create,
channel_delete).
- Around line 83-98: The current early returns in on_message_delete and
on_message_edit skip logging for messages without text; update on_message_delete
(listener on_message_delete) to only return for bot authors or missing guild,
then build the embed using message.content if present else include
attachment/sticker summary and always include message ID and any attachment
filenames/URLs before calling _process_log; for on_message_edit (listener
on_message_edit) change the guard to return only when author is a bot or when
before and after are identical in both content and attachments/stickers (i.e.,
compare before.content==after.content AND attachments/stickers equality), and
when creating the edit embed include both old/new text if present and list
attachments/stickers (or indicate "(添付のみ)" when text is empty) so edits with no
text are still logged.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/logging/route.ts:
- Around line 41-52: The current error handling in the route conflates auth
errors and internal errors; update the catch blocks around
validateAdmin()/fetchLoggingSetting()/createLoggingSetting()/deleteLoggingSetting()
so that you only map errors thrown by validateAdmin() to 401 (Unauthorized) or
403 (Forbidden) based on the error message, and treat all other errors as 500
(Internal Server Error); specifically, in the GET/POST/DELETE handlers inspect
the caught error to see if it originated from validateAdmin (e.g., check
error.message === "Unauthorized" or "Forbidden") and return NextResponse.json({
error: error.message }, { status: 401/403 }) for those cases, otherwise return
NextResponse.json({ error: error.message }, { status: 500 }).

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx:
- Around line 144-148: ChannelSelecter の onChange ハンドラは log_channel_id
のみを更新して既存の webhook_url を残しているため、チャンネル変更後も古い webhook が使われ続けます;修正は ChannelSelecter
の onChange 内で evs をコピーして evs[index].log_channel_id を新しい val にセットするのに加え、同じイベントの
webhook_url(おそらく event.webhook_url または evs[index].webhook_url)を明示的に null
か空文字にクリアしてから setSetting({...setting, events: evs}) を呼ぶようにしてください(関係するシンボル:
ChannelSelecter, setting.events, evs[index].log_channel_id,
evs[index].webhook_url, setSetting)。
- Around line 131-132: The map over setting.events in LoggingClient.tsx is using
index as the React key which causes state reuse issues (e.g., ChannelSelecter)
when items are added/removed; change the key passed to the mapped element from
index to a stable unique identifier such as event.event_name (or another unique
event id on the event object) in the JSX returned by setting.events.map to
ensure each event's component instance and state stay tied to that specific
event.
- Around line 47-55: The current handleSave incorrectly uses
setting.events.filter(...) in the validation which always evaluates truthy and
also leaves loading stuck true; change the validation to check for any empty
channel id (e.g., use Array.prototype.some on setting.events to detect
event.log_channel_id === "") and, if validation fails, call setLoading(false)
before returning so the component isn't stuck in loading state; update
references in the handleSave function to use the corrected predicate and ensure
setLoading is properly reset on all early returns or error paths.

In `@src/dashboard/src/lib/api/requests.ts`:
- Around line 293-307: The URL construction in saveOneLoggingEvent and
deleteOneLoggingEvent risks breaking when eventName contains slashes or spaces;
update both functions to wrap eventName with encodeURIComponent(eventName) in
the fetch URL (same approach used for moduleName elsewhere) so the path segments
are safely encoded and requests resolve correctly.
- Around line 21-26: The TypeScript LoggingEvent interface's webhook_url is
required but the backend Go model LoggingSetting uses WebhookUrl *string
(nullable), causing a type mismatch when fetchLoggingSetting returns raw JSON;
change the LoggingEvent definition (and any usages) to accept null by making
webhook_url type string | null (or optional) and update code paths that read or
pass LoggingEvent.webhook_url to handle null safely (e.g., guard before using
the URL) so the TS types match LoggingSetting.go and fetchLoggingSetting's raw
response.

---

Nitpick comments:
In `@src/dashboard/src/lib/api/requests.ts`:
- Around line 272-323: Add the same pre-request guild ID validation used
elsewhere to all logging API helpers (fetchLoggingSetting, saveLoggingSetting,
saveOneLoggingEvent, deleteOneLoggingEvent, deleteLoggingSetting): call the
shared validation guard (e.g., validateGuildId or request_verification utility)
at the top of each function and throw/return early for invalid IDs before making
fetch calls; if no shared guard exists, extract a small helper (validateGuildId)
and use it from each of these functions to keep behavior consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b10d6be-1ab0-4ba7-8f93-b78a91220fc0

📥 Commits

Reviewing files that changed from the base of the PR and between 15769c5 and 62c880d.

📒 Files selected for processing (11)
  • src/api/src/cmd/main.go
  • src/api/src/internal/model/LoggingSetting.go
  • src/api/src/internal/router/logging.go
  • src/bot/cogs/logging.py
  • src/bot/lib/api.py
  • src/dashboard/Dockerfile
  • src/dashboard/src/app/api/guilds/[guildId]/modules/logging/route.ts
  • src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx
  • src/dashboard/src/app/dashboard/[guildId]/logging/page.tsx
  • src/dashboard/src/lib/api/requests.ts
  • src/dashboard/src/lib/modules.ts
💤 Files with no reviewable changes (1)
  • src/dashboard/Dockerfile

Comment thread src/api/src/internal/model/LoggingSetting.go
Comment thread src/api/src/internal/router/logging.go
Comment thread src/api/src/internal/router/logging.go Outdated
Comment thread src/bot/cogs/logging.py
Comment on lines +56 to +72
async def _process_log(self, guild: discord.Guild, event_name: str, embed: discord.Embed, trigger_channel_id: str = None):
if not guild: return

is_enabled = await self.bot.api.is_module_enabled(str(guild.id), "logging")
if not is_enabled or not is_enabled.get('enabled'):
return

event_config = await self.bot.api.get_event_config(str(guild.id), event_name)
if not event_config or not self.bot.api._is_valid_discord_id(event_config.log_channel_id):
return

if trigger_channel_id:
ignored = event_config.ignored_channels or []
if trigger_channel_id in ignored:
return

webhook_url = await self._get_or_create_webhook(guild, event_config, event_name)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

無視チャンネル設定が bot 側で反映しきれていません。

_process_log()event_config.ignored_channels しか見ておらず、global_ignored_channels を一度も参照していません。さらに channel_create / channel_delete では trigger_channel_id を渡していないので、そのイベント個別の無視設定も効きません。src/bot/lib/api.py には get_logging_setting() が追加済みなので、ここで統合判定するのが安全です。

Also applies to: 130-156

🧰 Tools
🪛 Ruff (0.15.7)

[warning] 56-56: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


[error] 57-57: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 56 - 72, _process_log currently only
checks event_config.ignored_channels and never consults the global ignored list;
call the API helper get_logging_setting(str(guild.id), event_name) (from
src/bot/lib/api.py) to obtain the merged logging setting (which includes
global_ignored_channels and per-event ignored channels) and use that combined
ignored list to decide early-return; also ensure callers like channel_create and
channel_delete pass the trigger_channel_id through to _process_log so per-event
ignores are respected (reference symbols: _process_log, get_logging_setting,
event_config, global_ignored_channels, trigger_channel_id, channel_create,
channel_delete).

Comment thread src/bot/cogs/logging.py
Comment on lines +47 to +55
const handleSave = async () => {
if (loading) return;

setLoading(true);

if (setting.events.filter((value) => value.log_channel_id === "")) {
alert("ログには必ずチャンネルidを指定してください。");
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

保存前バリデーションが常に失敗し、以後ずっと保存不能になります。

filter() は空配列でもオブジェクトとして真扱いされるため、Line 52 の条件は常に成立します。さらに Line 50 で loadingtrue にしたまま return しているので、最初のクリック以降は永続的に保存できません。

修正案
   const handleSave = async () => {
     if (loading) return;
 
-    setLoading(true);
-
-    if (setting.events.filter((value) => value.log_channel_id === "")) {
+    if (setting.events.some((value) => value.log_channel_id === "")) {
       alert("ログには必ずチャンネルidを指定してください。");
-      return
+      return;
     }
+
+    setLoading(true);
 
     try {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx around
lines 47 - 55, The current handleSave incorrectly uses
setting.events.filter(...) in the validation which always evaluates truthy and
also leaves loading stuck true; change the validation to check for any empty
channel id (e.g., use Array.prototype.some on setting.events to detect
event.log_channel_id === "") and, if validation fails, call setLoading(false)
before returning so the component isn't stuck in loading state; update
references in the handleSave function to use the corrected predicate and ensure
setLoading is properly reset on all early returns or error paths.

Comment thread src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx Outdated
Comment thread src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx
Comment thread src/dashboard/src/lib/api/requests.ts
Comment thread src/dashboard/src/lib/api/requests.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/bot/cogs/logging.py (2)

139-148: ⚠️ Potential issue | 🟠 Major

channel_create / channel_delete にも対象 channel ID を渡してください。

_process_log() の ignore 判定は trigger_channel_id 前提ですが、監査ログ経由のチャンネルイベントでは未設定のまま呼んでいます。これだと channel 単位の ignore 設定が効かないので、entry.target.id を渡して同じ判定に乗せたいです。

Also applies to: 161-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 139 - 148, The channel_create and
channel_delete audit-log branches don't pass the affected channel's ID into
_process_log, so per-channel ignore rules aren't applied; update the handling
for discord.AuditLogAction.channel_create and channel_delete to call
_process_log with trigger_channel_id=entry.target.id (use entry.target.id as the
unique channel identifier) so the same ignore logic applies as for other events;
ensure you reference the branches that set event_key/embed
(channel_create/channel_delete) and pass entry.target.id into the _process_log
invocation used for audit log events.

94-95: ⚠️ Potential issue | 🟠 Major

本文が空の削除/編集もログ対象にしてください。

not message.content / not after.content で早期 return しているため、画像・ファイル・スタンプだけのメッセージが欠落します。編集側も before.content == after.content だけだと添付差分を見落とすので、本文がなくても添付情報やメッセージ ID を残した方がよいです。

Also applies to: 106-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 94 - 95, Remove the early-return that
skips messages with empty content in the message handlers so
image/file/sticker-only messages aren’t dropped: in the on_message handler (the
function checking "if message.author.bot or not message.guild or not
message.content: return"), stop checking "not message.content" and only return
for bot/guild cases; in the on_message_edit handler (the block currently
returning when "before.content == after.content"), extend the equality check to
also compare attachments/stickers/embeds (e.g., compare before.attachments vs
after.attachments, before.stickers vs after.stickers, and before.embeds vs
after.embeds) and log when any of those differ, and always include message.id
and attachment metadata in the logged payload so content-less messages are still
recorded.
src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx (1)

47-55: ⚠️ Potential issue | 🔴 Critical

保存前バリデーションが常に失敗し、loading も戻りません。

filter() は空配列でも truthy なのでこの条件は毎回成立します。さらに setLoading(true) の後で return しているため、1 回目以降ずっと保存不能になります。

💡 修正例
   const handleSave = async () => {
     if (loading) return;
-
-    setLoading(true);
-
-    if (setting.events.filter((value) => value.log_channel_id === "")) {
+    if (setting.events.some((value) => value.log_channel_id === "")) {
       alert("ログには必ずチャンネルidを指定してください。");
-      return
+      return;
     }
+
+    setLoading(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx around
lines 47 - 55, The save handler handleSave incorrectly uses
setting.events.filter(...) as a boolean which always evaluates truthy and also
calls setLoading(true) before returning, causing the save to always abort and
loading to remain true; change the validation to check whether any event has an
empty log_channel_id (e.g., use setting.events.some(ev => ev.log_channel_id ===
"") or check the filtered array's length > 0) and, if validation fails, call
setLoading(false) before returning so loading is reset and the alert is shown;
ensure you reference handleSave, setting.events, and setLoading when updating
the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/src/internal/router/logging.go`:
- Around line 70-72: The handlers getOneLoggingEventConfig and
deleteOneLoggingEvent currently treat any db.Where(...).First(&setting).Error as
a 404; change their error handling to match getLoggingSetting: if err ==
gorm.ErrRecordNotFound return 404 with "Settings not found", otherwise log the
error and return HTTP 500 (internal server error) so transient DB/SQL errors
aren't misclassified; adjust the error branch around db.Where("guild_id = ?",
id).First(&setting).Error in both functions to check for gorm.ErrRecordNotFound
first and handle other errors as server errors.

In `@src/bot/cogs/logging.py`:
- Around line 12-13: 現在の _get_or_create_webhook()/event_config.webhook_url
ロジックはキャッシュされた webhook_url を無条件に返しており、webhook.send()
が失敗してもキャッシュを消さないため復旧できません。対処として _process_log() 呼び出し時に webhook.send() を
try/except でラップし、送信失敗時には event_config.webhook_url をクリアして永続化(保存)し、続けて
_get_or_create_webhook() を再呼び出して新しい webhook を取得して再試行するか、事前に webhook
の有効性チェックを行うよう修正してください。対象シンボル: event_config.webhook_url、_process_log(),
_get_or_create_webhook(), webhook.send().
- Around line 97-100: The embed fields currently use message.content,
before.content, and after.content directly which can exceed Discord's
1024-character limit and cause webhook.send() to 400; update the code that
builds the embed (where embed is created in the message deletion and message
edit handlers) to ensure each field value is at most 1024 chars—truncate long
strings (e.g., value[:1021] + '...') before embed.add_field or, for very long
messages, place the full text into embed.description or attach it as a file
instead; apply this truncation/relocation logic for the fields that use
message.content, before.content, and after.content.

---

Duplicate comments:
In `@src/bot/cogs/logging.py`:
- Around line 139-148: The channel_create and channel_delete audit-log branches
don't pass the affected channel's ID into _process_log, so per-channel ignore
rules aren't applied; update the handling for
discord.AuditLogAction.channel_create and channel_delete to call _process_log
with trigger_channel_id=entry.target.id (use entry.target.id as the unique
channel identifier) so the same ignore logic applies as for other events; ensure
you reference the branches that set event_key/embed
(channel_create/channel_delete) and pass entry.target.id into the _process_log
invocation used for audit log events.
- Around line 94-95: Remove the early-return that skips messages with empty
content in the message handlers so image/file/sticker-only messages aren’t
dropped: in the on_message handler (the function checking "if message.author.bot
or not message.guild or not message.content: return"), stop checking "not
message.content" and only return for bot/guild cases; in the on_message_edit
handler (the block currently returning when "before.content == after.content"),
extend the equality check to also compare attachments/stickers/embeds (e.g.,
compare before.attachments vs after.attachments, before.stickers vs
after.stickers, and before.embeds vs after.embeds) and log when any of those
differ, and always include message.id and attachment metadata in the logged
payload so content-less messages are still recorded.

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx:
- Around line 47-55: The save handler handleSave incorrectly uses
setting.events.filter(...) as a boolean which always evaluates truthy and also
calls setLoading(true) before returning, causing the save to always abort and
loading to remain true; change the validation to check whether any event has an
empty log_channel_id (e.g., use setting.events.some(ev => ev.log_channel_id ===
"") or check the filtered array's length > 0) and, if validation fails, call
setLoading(false) before returning so loading is reset and the alert is shown;
ensure you reference handleSave, setting.events, and setLoading when updating
the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d7789d20-917c-4e18-8872-920983efc846

📥 Commits

Reviewing files that changed from the base of the PR and between 62c880d and 100670d.

📒 Files selected for processing (5)
  • src/api/src/internal/model/LoggingSetting.go
  • src/api/src/internal/router/logging.go
  • src/bot/cogs/logging.py
  • src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx
  • src/dashboard/src/lib/api/requests.ts
✅ Files skipped from review due to trivial changes (2)
  • src/api/src/internal/model/LoggingSetting.go
  • src/dashboard/src/lib/api/requests.ts

Comment on lines +70 to +72
if err := db.Where("guild_id = ?", id).First(&setting).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"})
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/api/src/internal/router/logging.go | head -150

Repository: SharkBot-Dev/NewSharkBot

Length of output: 4886


DB エラーをすべて 404 として返さないでください。

getLoggingSetting()(行31-38)と異なり、getOneLoggingEventConfig()(行70-72)と deleteOneLoggingEvent()(行128-130)は First() の失敗をすべて 404 に割り当てています。接続障害や SQL エラーまで 404 になると、呼び出し側が「未設定」と「一時障害」を区別できず、誤った再保存や誤表示につながります。

💡 修正例
  var setting model.LoggingSetting
  if err := db.Where("guild_id = ?", id).First(&setting).Error; err != nil {
-     c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"})
-     return
+     if errors.Is(err, gorm.ErrRecordNotFound) {
+         c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"})
+         return
+     }
+     c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load settings"})
+     return
  }

同じパターンを両関数に適用してください。getLoggingSetting() はすでに正しく処理されています。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := db.Where("guild_id = ?", id).First(&setting).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"})
return
if err := db.Where("guild_id = ?", id).First(&setting).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load settings"})
return
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/internal/router/logging.go` around lines 70 - 72, The handlers
getOneLoggingEventConfig and deleteOneLoggingEvent currently treat any
db.Where(...).First(&setting).Error as a 404; change their error handling to
match getLoggingSetting: if err == gorm.ErrRecordNotFound return 404 with
"Settings not found", otherwise log the error and return HTTP 500 (internal
server error) so transient DB/SQL errors aren't misclassified; adjust the error
branch around db.Where("guild_id = ?", id).First(&setting).Error in both
functions to check for gorm.ErrRecordNotFound first and handle other errors as
server errors.

Comment thread src/bot/cogs/logging.py
Comment on lines +12 to +13
if event_config.webhook_url:
return event_config.webhook_url
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/bot/cogs/logging.py

Repository: SharkBot-Dev/NewSharkBot

Length of output: 95


🏁 Script executed:

cat -n src/bot/cogs/logging.py

Repository: SharkBot-Dev/NewSharkBot

Length of output: 8286


キャッシュされた webhook URL が無効になった場合、復旧できません

保存済み webhook_url をここで無条件に返しているため、Discord 側で webhook が手動削除・再生成されると、以降の webhook.send() 呼び出しが例外を発生させます。しかし _process_log() の呼び出し側で例外処理がないため、ログ送信は失敗したまま、キャッシュされた無効な URL は保存されたままになります。その後の呼び出しでも同じ無効な URL が返され続けるため、恒久的にログが送信されません。

webhook 送信時のエラーハンドリングを追加し、失敗時は保存済み URL をクリアして _get_or_create_webhook() を再試行する、または webhook の有効性を事前に確認する必要があります。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 12 - 13, 現在の
_get_or_create_webhook()/event_config.webhook_url ロジックはキャッシュされた webhook_url
を無条件に返しており、webhook.send() が失敗してもキャッシュを消さないため復旧できません。対処として _process_log() 呼び出し時に
webhook.send() を try/except でラップし、送信失敗時には event_config.webhook_url
をクリアして永続化(保存)し、続けて _get_or_create_webhook() を再呼び出して新しい webhook を取得して再試行するか、事前に
webhook の有効性チェックを行うよう修正してください。対象シンボル: event_config.webhook_url、_process_log(),
_get_or_create_webhook(), webhook.send().

Comment thread src/bot/cogs/logging.py
Comment on lines +97 to +100
embed = discord.Embed(title="🗑️ メッセージ削除", color=discord.Color.red(), timestamp=message.created_at)
embed.add_field(name="送信者", value=f"{message.author.mention} (`{message.author.id}`)")
embed.add_field(name="チャンネル", value=message.channel.mention)
embed.add_field(name="内容", value=message.content, inline=False)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/bot/cogs/logging.py | sed -n '85,125p'

Repository: SharkBot-Dev/NewSharkBot

Length of output: 2345


🏁 Script executed:

cat -n src/bot/cogs/logging.py | sed -n '1,95p' | tail -30

Repository: SharkBot-Dev/NewSharkBot

Length of output: 1410


🏁 Script executed:

cat -n src/bot/cogs/logging.py | sed -n '55,90p'

Repository: SharkBot-Dev/NewSharkBot

Length of output: 1679


🏁 Script executed:

# Check if there's any truncation logic elsewhere in the file
rg -n "truncate|1024|2000|\[:1024\]|\[:2000\]" src/bot/cogs/logging.py

Repository: SharkBot-Dev/NewSharkBot

Length of output: 50


🏁 Script executed:

# Check the full _process_log method and see if there's any error handling
cat -n src/bot/cogs/logging.py | sed -n '56,91p'

Repository: SharkBot-Dev/NewSharkBot

Length of output: 1679


メッセージ内容が 1024 文字を超える場合、embed 送信が失敗します。

Discord の embed field 値は 1024 文字までですが、メッセージ本文は 2000 文字まで入ります。現在のコードはmessage.contentbefore.contentafter.content を検証なしに embed field に直接割り当てているため、長文メッセージの場合 webhook.send() が 400 エラーで失敗します。各 field を 1024 文字以下に切り詰めるか、embed.description に逃がす必要があります。

該当箇所

行 97-100(メッセージ削除):

embed.add_field(name="内容", value=message.content, inline=False)

行 109-113(メッセージ編集):

embed.add_field(name="変更前", value=before.content, inline=False)
embed.add_field(name="変更後", value=after.content, inline=False)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 97 - 100, The embed fields currently
use message.content, before.content, and after.content directly which can exceed
Discord's 1024-character limit and cause webhook.send() to 400; update the code
that builds the embed (where embed is created in the message deletion and
message edit handlers) to ensure each field value is at most 1024 chars—truncate
long strings (e.g., value[:1021] + '...') before embed.add_field or, for very
long messages, place the full text into embed.description or attach it as a file
instead; apply this truncation/relocation logic for the fields that use
message.content, before.content, and after.content.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (5)
src/bot/cogs/logging.py (4)

100-100: ⚠️ Potential issue | 🟠 Major

Embed field の文字数上限未考慮でWebhook送信が失敗します。

message.content / before.content / after.content をそのまま field に入れており、長文時に Discord API 側で 400 になります。field 値は 1024 文字以内へ切り詰める処理を追加してください。

✂️ 例: field用の共通トリムを追加
+    def _trim_field(self, value: str) -> str:
+        if value is None:
+            return "(空)"
+        return value if len(value) <= 1024 else value[:1021] + "..."

-        embed.add_field(name="内容", value=message.content, inline=False)
+        embed.add_field(name="内容", value=self._trim_field(message.content), inline=False)

-        embed.add_field(name="変更前", value=before.content, inline=False)
-        embed.add_field(name="変更後", value=after.content, inline=False)
+        embed.add_field(name="変更前", value=self._trim_field(before.content), inline=False)
+        embed.add_field(name="変更後", value=self._trim_field(after.content), inline=False)

Also applies to: 112-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` at line 100, The embed field is using
message.content (and similarly before.content / after.content) raw which can
exceed Discord's 1024-character limit and cause webhook 400 errors; add a small
helper (e.g., trim_embed_field or truncate_field) that truncates strings to 1024
chars (optionally appending "…" when cut) and use it when calling
embed.add_field for the message.content, before.content and after.content fields
in the logging cog so all field values are guaranteed <=1024 characters.

12-13: ⚠️ Potential issue | 🟠 Major

無効なWebhook URLを自己回復できない実装です。

Line 12 で webhook_url を無条件再利用し、Line 86 の webhook.send() 失敗時の再取得処理がないため、URL失効後に恒久的に送信不能になります。送信失敗時に webhook_url をクリアして再作成・再送するフローを入れてください。

Also applies to: 85-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 12 - 13, The code unconditionally
reuses event_config.webhook_url (event_config.webhook_url) and never recovers if
the webhook becomes invalid, so wrap the webhook.send() call in a try/except and
on failure clear event_config.webhook_url, persist that change, recreate the
webhook via the existing helper used to obtain webhooks (e.g., the module's
get_or_create_webhook / create_webhook_for_channel function), update
event_config.webhook_url with the new URL, and retry the send once; ensure you
catch specific send errors, persist the cleared/updated event_config state, and
avoid infinite retry loops.

94-95: ⚠️ Potential issue | 🟠 Major

本文なしメッセージを除外しており、削除/編集ログが欠損します。

Line 94 と Line 106 の早期 return により、添付・スタンプのみのメッセージ削除/編集がログされません。本文空でも (添付のみ) と添付情報を含めて記録するべきです。

Also applies to: 106-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 94 - 95, 早期 return が message.content
のみを条件にしているため、本文なし(添付・スタンプのみ)のメッセージが削除/編集ログから除外されています; message.author.bot と not
message.guild のチェックは保持しつつ、message.content が空でも message.attachments /
message.stickers / message.embeds 等が存在する場合は処理を継続するよう、該当の早期 return 条件(現在の
message.author.bot or not message.guild or not
message.content)を修正してください。さらに、ログ作成箇所(削除/編集のハンドラで使われている message
内容を組み立てるロジック)に「(添付のみ)」というプレースホルダを導入し、添付情報(attachment.filename/URL
など)やスタンプ情報をログ本文に含めるよう更新してください。参照シンボル: message.author.bot, message.guild,
message.content, message.attachments, message.stickers, 該当の削除/編集ログハンドラ関数。

139-140: ⚠️ Potential issue | 🟠 Major

チャンネル系監査イベントでignore判定が効いていません。

channel_create / channel_delete でも _process_logtrigger_channel_id を渡していないため、グローバル/イベント別の無視チャンネル設定が適用されません。

🎯 最小修正例
-        if event_key:
+        if event_key:
             embed.add_field(name="実行者", value=f"{entry.user.mention}")
             if entry.reason:
                 embed.add_field(name="理由", value=entry.reason, inline=False)
-            await self._process_log(guild, event_key, embed)
+            trigger_channel_id = None
+            if event_key in {"channel_create", "channel_delete"} and getattr(entry.target, "id", None):
+                trigger_channel_id = str(entry.target.id)
+            await self._process_log(guild, event_key, embed, trigger_channel_id)

Also applies to: 145-146, 165-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/logging.py` around lines 139 - 140, The channel audit handlers
for discord.AuditLogAction.channel_create and channel_delete are not passing the
trigger_channel_id into _process_log, so per-channel/global ignore settings
never apply; update the calls to _process_log in the blocks handling
channel_create (event_key "channel_create"), channel_delete (event_key
"channel_delete") and the related channel event block referenced around the same
area to pass trigger_channel_id (use the channel target's id, e.g., target.id)
as the corresponding argument to _process_log so the ignore filtering logic
receives the channel id.
src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx (1)

49-54: ⚠️ Potential issue | 🔴 Critical

バリデーション失敗時に loading が固定化し、以後保存できません。

Line 49 で setLoading(true) した後、Line 53 で return しており loading が戻りません。次回以降は Line 47 のガードで常時中断されます。

✅ 修正例
   const handleSave = async () => {
     if (loading) return;

-    setLoading(true);
-    
     if (setting.events.some((event) => event.log_channel_id === "")) {
       alert("ログには必ずチャンネルidを指定してください。");
       return;
     }
+
+    setLoading(true);

     try {
       const response = await fetch(`/api/guilds/${guildId}/modules/logging`, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx around
lines 49 - 54, The code sets setLoading(true) before validating events and
returns early when an event has an empty log_channel_id, leaving loading stuck;
either perform the validation on setting.events before calling setLoading(true)
or ensure you call setLoading(false) immediately before the early return in the
save handler so loading is reset; locate the save handler where setLoading(true)
is called and the guard that checks setting.events.some(event =>
event.log_channel_id === "") and implement one of these fixes (move validation
above setLoading(true) or add setLoading(false) just before the return).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/bot/cogs/logging.py`:
- Line 100: The embed field is using message.content (and similarly
before.content / after.content) raw which can exceed Discord's 1024-character
limit and cause webhook 400 errors; add a small helper (e.g., trim_embed_field
or truncate_field) that truncates strings to 1024 chars (optionally appending
"…" when cut) and use it when calling embed.add_field for the message.content,
before.content and after.content fields in the logging cog so all field values
are guaranteed <=1024 characters.
- Around line 12-13: The code unconditionally reuses event_config.webhook_url
(event_config.webhook_url) and never recovers if the webhook becomes invalid, so
wrap the webhook.send() call in a try/except and on failure clear
event_config.webhook_url, persist that change, recreate the webhook via the
existing helper used to obtain webhooks (e.g., the module's
get_or_create_webhook / create_webhook_for_channel function), update
event_config.webhook_url with the new URL, and retry the send once; ensure you
catch specific send errors, persist the cleared/updated event_config state, and
avoid infinite retry loops.
- Around line 94-95: 早期 return が message.content
のみを条件にしているため、本文なし(添付・スタンプのみ)のメッセージが削除/編集ログから除外されています; message.author.bot と not
message.guild のチェックは保持しつつ、message.content が空でも message.attachments /
message.stickers / message.embeds 等が存在する場合は処理を継続するよう、該当の早期 return 条件(現在の
message.author.bot or not message.guild or not
message.content)を修正してください。さらに、ログ作成箇所(削除/編集のハンドラで使われている message
内容を組み立てるロジック)に「(添付のみ)」というプレースホルダを導入し、添付情報(attachment.filename/URL
など)やスタンプ情報をログ本文に含めるよう更新してください。参照シンボル: message.author.bot, message.guild,
message.content, message.attachments, message.stickers, 該当の削除/編集ログハンドラ関数。
- Around line 139-140: The channel audit handlers for
discord.AuditLogAction.channel_create and channel_delete are not passing the
trigger_channel_id into _process_log, so per-channel/global ignore settings
never apply; update the calls to _process_log in the blocks handling
channel_create (event_key "channel_create"), channel_delete (event_key
"channel_delete") and the related channel event block referenced around the same
area to pass trigger_channel_id (use the channel target's id, e.g., target.id)
as the corresponding argument to _process_log so the ignore filtering logic
receives the channel id.

In `@src/dashboard/src/app/dashboard/`[guildId]/logging/LoggingClient.tsx:
- Around line 49-54: The code sets setLoading(true) before validating events and
returns early when an event has an empty log_channel_id, leaving loading stuck;
either perform the validation on setting.events before calling setLoading(true)
or ensure you call setLoading(false) immediately before the early return in the
save handler so loading is reset; locate the save handler where setLoading(true)
is called and the guard that checks setting.events.some(event =>
event.log_channel_id === "") and implement one of these fixes (move validation
above setLoading(true) or add setLoading(false) just before the return).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a972b55-3a89-4238-b568-3b9877df1f0f

📥 Commits

Reviewing files that changed from the base of the PR and between 100670d and 7f3f822.

📒 Files selected for processing (3)
  • src/bot/cogs/logging.py
  • src/dashboard/src/app/api/guilds/[guildId]/modules/logging/route.ts
  • src/dashboard/src/app/dashboard/[guildId]/logging/LoggingClient.tsx
✅ Files skipped from review due to trivial changes (1)
  • src/dashboard/src/app/api/guilds/[guildId]/modules/logging/route.ts

@sharkbot-neko sharkbot-neko merged commit a4a9163 into main Mar 28, 2026
4 checks passed
@sharkbot-neko sharkbot-neko deleted the feat/logging branch March 28, 2026 08:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant