Skip to content

よろしくメッセージと軽微な修正#5

Merged
sharkbot-neko merged 6 commits intomainfrom
feat/welcome-message
Mar 26, 2026
Merged

よろしくメッセージと軽微な修正#5
sharkbot-neko merged 6 commits intomainfrom
feat/welcome-message

Conversation

@yuito-it
Copy link
Copy Markdown
Member

@yuito-it yuito-it commented Mar 26, 2026

Summary by CodeRabbit

リリースノート

  • 新機能

    • ウェルカムおよびグッドバイメッセージ機能を追加。サーバーメンバーの入退出時にカスタムメッセージを自動送信。
    • 埋め込みビルダーで Discord 埋め込みの作成・管理が可能に。
    • ダッシュボードのモジュール表示をグループ別に整理。
    • カラーピッカーとチャネル選択ツールなど UI コンポーネントを追加。
  • 改善

    • ダッシュボードレイアウトを最適化し、ナビゲーションを改良。
    • 関連する依存関係を更新。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Discordボットに参加・離脱イベントハンドラーとサーバーごとの設定管理機能を追加し、ダッシュボードに対応するAPI ルートとUI コンポーネントを実装しました。埋め込みテンプレートとウェルカム・グッドバイメッセージの設定、作成、削除が可能になります。

Changes

Cohort / File(s) Summary
Bot Core
src/bot/main.py, src/bot/lib/embed.py, src/bot/cogs/welcome.py
WelcomeCogでメンバー参加・離脱イベントを監視し、ギルド別設定をMongoDBから取得して、テンプレート置換後のメッセージ/埋め込みをDiscordに送信。EmbedクラスがギルドIDと埋め込みタイトルから埋め込みテンプレートを取得。
Dashboard API Routes (埋め込み)
src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts
埋め込み設定のCRUD操作を実装。認証・認可チェック後、MongoDB embed_setting から埋め込みを取得・追加・削除。
Dashboard API Routes (ウェルカム)
src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts
ウェルカム・グッドバイ設定の取得・更新を実装。認証・認可チェック後、MongoDB welcome_setting を読み書き。
Dashboard API Routes (その他)
src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx, src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts
チャネル一覧取得とモジュール設定変更。modulesのデフォルトがtest:falseからhelp:trueに変更。
Dashboard React Components
src/dashboard/src/app/components/CollapsibleSection.tsx, src/dashboard/src/app/components/ColorPicker.tsx, src/dashboard/src/app/components/EmbedBuilder.tsx, src/dashboard/src/app/components/EmbedSelecter.tsx, src/dashboard/src/app/components/channel-selecter.tsx, src/dashboard/src/app/components/toggleSwitch.tsx
埋め込み作成・色選択・チャネル選択・切り替えスイッチなどのUI コンポーネント。EmbedBuilderが埋め込みのライブプレビュー機能を提供。
Dashboard Pages & Layout
src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx, src/dashboard/src/app/dashboard/[guildId]/welcome/page.tsx, src/dashboard/src/app/dashboard/[guildId]/layout.tsx, src/dashboard/src/app/dashboard/[guildId]/page.tsx
埋め込み管理ページとウェルカム・グッドバイ管理ページを追加。レイアウトにグループ化されたモジュールナビゲーション、ページに動的グループ分けを実装。
Configuration & Dependencies
src/dashboard/package.json, src/dashboard/src/lib/modules.ts, src/dashboard/src/lib/discord.ts
tinycolor2と型定義を追加。新モジュール(welcomeembed)をサーバー管理・ユーティリティグループに追加。チャネル取得関数を追加。

Sequence Diagram(s)

sequenceDiagram
    participant User as ユーザー
    participant Bot as Discordボット
    participant DB as MongoDB
    participant Discord as Discord API

    User->>Discord: ギルドに参加
    Discord->>Bot: on_member_join イベント
    Bot->>DB: guild_id でウェルカム設定を取得
    DB-->>Bot: welcome_setting ドキュメント
    alt 設定が有効
        Bot->>Bot: プレースホルダーを置換
        Bot->>DB: embed_setting からテンプレートを取得
        DB-->>Bot: embed オブジェクト
        Bot->>Discord: メッセージ/埋め込みを送信
        Discord-->>User: ウェルカムメッセージを表示
    end

    User->>Discord: ギルドから退出
    Discord->>Bot: on_member_remove イベント
    Bot->>DB: guild_id でグッドバイ設定を取得
    DB-->>Bot: welcome_setting ドキュメント
    alt 設定が有効
        Bot->>Bot: プレースホルダーを置換
        Bot->>Discord: グッドバイメッセージを送信
        Discord-->>Bot: 送信完了
    end
Loading
sequenceDiagram
    participant Admin as 管理者
    participant Dashboard as ダッシュボード UI
    participant API as API ルート
    participant Auth as 認証サービス
    participant DB as MongoDB
    participant Discord as Discord API

    Admin->>Dashboard: 埋め込み設定ページにアクセス
    Dashboard->>API: GET /api/guilds/[guildId]/modules/embed
    API->>Auth: 連携 Discord アカウント・トークンを取得
    Auth-->>API: アクセストークン
    API->>API: 管理者権限を確認
    alt 権限あり
        API->>DB: embed_setting を取得
        DB-->>API: 埋め込み設定
        API-->>Dashboard: 設定一覧を返す
        Dashboard->>Dashboard: EmbedBuilder で編集
        Admin->>Dashboard: 埋め込みを保存
        Dashboard->>API: POST /api/guilds/[guildId]/modules/embed
        API->>DB: embed_setting を更新
        DB-->>API: 完了
        API-->>Dashboard: 成功レスポンス
        Dashboard-->>Admin: 成功アラート
    else 権限なし
        API-->>Dashboard: 403 Forbidden
        Dashboard-->>Admin: エラーアラート
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • NewSharkBot#2: ダッシュボードのAPI ハンドラーがauth.api.listUserAccounts/getAccessTokenによる認証フロー、およびPrisma/better-auth の設定に依存しており、これらはこのPRで導入されたものと関連しています。

Suggested reviewers

  • sharkbot-neko
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトル「よろしくメッセージと軽微な修正」は、ウェルカムメッセージ機能の実装と複数の関連機能を追加するこの変更を、日本語で簡潔に説明しており、主要な変更内容を適切に表現しています。

✏️ 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/welcome-message

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

{ $unset: { [`embeds.${body.title}`]: "" } },
);

console.log(`Deleting embed for guild ${guildId}:`, body.title);

Check warning

Code scanning / CodeQL

Log injection

Log entry depends on a [user-provided value](1).

Copilot Autofix

AI 21 days ago

To fix this, ensure that any user-controlled value is sanitized before being written to logs. For plain-text logs, the key step is to strip newline (\n) and carriage-return (\r) characters (and optionally other control characters) from the logged value. This preserves functionality (we still log the title) while preventing an attacker from breaking the log format.

In this specific file, the best minimal change is to create a sanitized version of body.title just before logging: convert it to a string (to avoid logging objects) and remove \r and \n characters, then log that sanitized value instead of the raw body.title. This does not change any database operations or route behavior; it only affects the logging statement on line 200. No new imports are required because we can use built-in string methods.

Concretely:

  • In src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts, in the try block where body is read and the database updateOne is called, replace the direct console.log call with:
    • creation of sanitizedTitle = String(body.title).replace(/[\r\n]/g, "")
    • logging sanitizedTitle instead of body.title.
Suggested changeset 1
src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts b/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts
--- a/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts
+++ b/src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts
@@ -197,7 +197,8 @@
         { $unset: { [`embeds.${body.title}`]: "" } },
       );
 
-    console.log(`Deleting embed for guild ${guildId}:`, body.title);
+    const sanitizedTitle = String(body.title).replace(/[\r\n]/g, "");
+    console.log(`Deleting embed for guild ${guildId}:`, sanitizedTitle);
 
     return NextResponse.json({ success: true, message: "Embed deleted!" });
   } catch (error) {
EOF
@@ -197,7 +197,8 @@
{ $unset: { [`embeds.${body.title}`]: "" } },
);

console.log(`Deleting embed for guild ${guildId}:`, body.title);
const sanitizedTitle = String(body.title).replace(/[\r\n]/g, "");
console.log(`Deleting embed for guild ${guildId}:`, sanitizedTitle);

return NextResponse.json({ success: true, message: "Embed deleted!" });
} catch (error) {
Copilot is powered by AI and may make mistakes. Always verify output.
@yuito-it yuito-it assigned yuito-it and sharkbot-neko and unassigned yuito-it Mar 26, 2026
@yuito-it yuito-it added difficulity/normal 時間は少しかかるけど困難なく解決可能 kind/enhancement 機能改善のリクエスト priority/mid 優先度: 中 labels Mar 26, 2026
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: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (6)
src/dashboard/src/app/components/EmbedBuilder.tsx-89-93 (1)

89-93: ⚠️ Potential issue | 🟡 Minor

onChange が依存配列に含まれており、無限ループの原因になる可能性があります。

親コンポーネントが onChange をインラインで定義している場合、毎回新しい関数参照が渡され、useEffect が無限に実行される可能性があります。

♻️ 修正案
   useEffect(() => {
     if (onChange) {
       onChange(embed);
     }
-  }, [embed, onChange]);
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [embed]);

または、親コンポーネント側で onChangeuseCallback でメモ化することを推奨します。

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

In `@src/dashboard/src/app/components/EmbedBuilder.tsx` around lines 89 - 93, The
useEffect inside the EmbedBuilder component currently lists onChange in the
dependency array and can trigger an infinite loop if the parent passes an inline
callback; remove onChange from the dependency array and instead call a stable
reference to it (e.g., store the latest onChange in a ref inside EmbedBuilder
and invoke ref.current(embed) inside the effect) so the effect only depends on
embed, or alternatively require the parent to memoize onChange with useCallback
and document that requirement; update the useEffect (and add the ref-management
logic) so embed is the sole reactive dependency while still invoking the most
recent onChange.
src/bot/cogs/welcome.py-24-25 (1)

24-25: ⚠️ Potential issue | 🟡 Minor

Falseとの等価比較を避けてください。

静的解析ツール(Ruff E712)が指摘しているように、== Falseではなくnotを使用してください。

🧹 提案する修正
-        if data["welcome"].get("enabled", False) == False:
+        if not data["welcome"].get("enabled", False):
             return
-        if data["goodbye"].get("enabled", False) == False:
+        if not data["goodbye"].get("enabled", False):
             return

Also applies to: 64-65

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

In `@src/bot/cogs/welcome.py` around lines 24 - 25, Replace the explicit equality
check against False with a negation of the retrieved flag: instead of comparing
data["welcome"].get("enabled", False) == False, use Python's not operator on
data["welcome"].get("enabled", False); apply the same change to the second
occurrence around the later check (the one noted at lines 64-65) so both
early-return branches use negation instead of equality.
src/dashboard/src/app/dashboard/[guildId]/welcome/page.tsx-6-6 (1)

6-6: ⚠️ Potential issue | 🟡 Minor

未使用のインポートがあります。

CommandsControlがインポートされていますが、コンポーネント内で使用されていません。

🧹 提案する修正
-import CommandsControl from "@/app/components/commands";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx at line 6, The
import CommandsControl in page.tsx is unused; either remove the import statement
for CommandsControl or actually render/use the CommandsControl component inside
the component exported from src/app/dashboard/[guildId]/welcome/page.tsx
(reference the CommandsControl import at the top of the file) — if you choose to
remove it, delete the import line; if you meant to use it, add the
CommandsControl JSX where the welcome page comp (the default export component)
returns its layout.
src/bot/cogs/welcome.py-93-93 (1)

93-93: ⚠️ Potential issue | 🟡 Minor

on_member_removeでメッセージ処理のロジックに不整合があります。

Line 93でmessageが空でもコンテンツ付きで送信しています。on_member_join(Lines 51-54)ではmessage == ""の場合はembed のみを送信していますが、on_member_removeではその分岐がありません。

🐛 提案する修正
         embed = discord.Embed.from_dict(embed_data)
-        await channel.send(content=self.welcome_parse(data["goodbye"].get("message", "{ユーザー名}が退出しました。"), member), embed=embed)
+        if message == "":
+            await channel.send(embed=embed)
+        else:
+            await channel.send(content=message, embed=embed)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bot/cogs/welcome.py` at line 93, In on_member_remove ensure message-empty
behavior matches on_member_join: extract the goodbye message into a variable
(using data["goodbye"].get("message", "{ユーザー名}が退出しました。") and pass through
self.welcome_parse(member)), then if the parsed message is an empty string call
channel.send(embed=embed) only, otherwise call
channel.send(content=parsed_message, embed=embed); update references to
welcome_parse, data["goodbye"].get(...), member, embed and channel.send
accordingly.
src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts-148-148 (1)

148-148: ⚠️ Potential issue | 🟡 Minor

タイポ: settungsetting

変数名にタイポがあります。

✏️ 修正案
-    const settung: WelcomeSetting = {
+    const setting: WelcomeSetting = {
         guildId,
         ...body
     };

Line 157 も併せて修正してください:

-        { $set: { ...settung } },
+        { $set: { ...setting } },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/welcome/route.ts at line
148, Rename the misspelled variable settung to setting where it's declared as a
WelcomeSetting and update all usages (including the later occurrence noted
around line 157) to match the new name; ensure the variable name is consistently
changed in the route handler so references to settung are replaced with setting
and types/signatures using WelcomeSetting remain correct.
src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx-26-33 (1)

26-33: ⚠️ Potential issue | 🟡 Minor

モジュール有効化チェックのレスポンスエラー処理が不足しています。

statusRes.ok のチェックなしで statusData を使用しています。APIエラー時に予期しない動作を引き起こす可能性があります。

🛡️ 修正案
         const statusRes = await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=embed`);
+        if (!statusRes.ok) {
+          throw new Error("Failed to check module status");
+        }
         const statusData = await statusRes.json();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/embed/page.tsx around lines 26 -
33, The code uses statusData without checking the HTTP response; update the
fetch handling around statusRes/statusData to first verify statusRes.ok and
handle non-OK responses (e.g., show an alert, log the error, and redirect via
router.push(`/dashboard/${guildId}`) or return) before accessing
statusData.enabled; modify the block that calls
fetch(`/api/guilds/${guildId}/modules/isEnabled?module=embed`) so that failures
and malformed JSON are caught and handled gracefully (use try/catch or check
statusRes.ok, then parse JSON) to avoid using statusData when the API returned
an error.
🧹 Nitpick comments (10)
src/dashboard/src/app/components/ColorPicker.tsx (1)

1-1: "use client" をこのファイルに明示する運用を推奨します。

このコンポーネントはイベントハンドラを持つため、呼び出し元依存を減らす目的で client 境界をファイル先頭で明示しておくと安全です。

As per coding guidelines, "Before writing Next.js code, read the relevant guide in node_modules/next/dist/docs/ to account for breaking changes in APIs, conventions, and file structure".

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

In `@src/dashboard/src/app/components/ColorPicker.tsx` at line 1, Add the React
client boundary directive to this component file by inserting the literal "use
client" as the very first line of
src/dashboard/src/app/components/ColorPicker.tsx so the ColorPicker component
and its event handlers (e.g., any ChangeEvent handlers imported from "react")
run on the client; ensure the directive appears before any imports and save to
keep the file explicitly client-side as requested for verification.
src/dashboard/src/app/components/channel-selecter.tsx (1)

68-72: チャンネルタイプの表示ロジックが不完全です。

Discord のチャンネルタイプは多岐にわたります(0: テキスト、2: ボイス、4: カテゴリ、5: アナウンス、13: ステージ、15: フォーラム等)。現在の実装では type === 0 以外はすべて「🔊」と表示されますが、カテゴリやフォーラムなども含まれてしまいます。

♻️ 修正案
 {channels.map((channel) => (
   <option key={channel.id} value={channel.id}>
-    {channel.type === 0 ? "# " : "🔊 "}{channel.name}
+    {channel.type === 0 ? "# " : channel.type === 2 ? "🔊 " : ""}{channel.name}
   </option>
 ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/components/channel-selecter.tsx` around lines 68 - 72,
The current rendering inside the channels.map in channel-selecter.tsx uses a
binary test (channel.type === 0) and otherwise shows "🔊", which mislabels many
Discord channel types; update the display logic (in the channels.map callback)
to map channel.type to the correct icon string (e.g., 0 -> "# ", 2 -> "🔊 ", 4
-> "📁 " or "📂 " for categories, 5 -> "📢 " for announcements, 13 -> "🎤 " for
stage, 15 -> "🧵 " for forum) using a small switch or lookup object and fall
back to a generic icon for unknown types, ensuring the option content uses the
mapped icon + channel.name.
src/dashboard/src/app/components/EmbedBuilder.tsx (1)

177-177: color0(黒)の場合、フォールバック値が使用されます。

embed.color?.toString(16) || "202225" では、color0 の場合は falsy となり、デフォルト色 "202225" が適用されます。黒色(0x000000)を正しく表示するには、undefined のみをチェックする必要があります。

♻️ 修正案
-style={{ borderColor: tinycolor(embed.color?.toString(16) || "202225").toHexString() }}
+style={{ borderColor: tinycolor(embed.color !== undefined ? embed.color.toString(16).padStart(6, '0') : "202225").toHexString() }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/components/EmbedBuilder.tsx` at line 177, The current
fallback uses a falsy check so a color value of 0 (black) falls back to
"202225"; update the expression in EmbedBuilder.tsx where
tinycolor(embed.color?.toString(16) || "202225").toHexString() is used to only
treat undefined/null as missing (e.g. use the nullish coalescing operator:
tinycolor(embed.color?.toString(16) ?? "202225").toHexString() or explicitly
check embed.color === undefined and use the default), so 0x000000 is preserved
correctly.
src/dashboard/package.json (1)

16-16: @types/tinycolor2devDependencies に配置すべきです。

型定義パッケージはビルド時のみ必要であり、本番環境では不要です。devDependencies に移動することを推奨します。

♻️ 修正案
  "dependencies": {
    "@better-auth/prisma-adapter": "^1.5.6",
    "@prisma/adapter-pg": "^7.5.0",
    "@prisma/client": "^7.5.0",
-   "@types/tinycolor2": "^1.4.6",
    "better-auth": "^1.5.6",

devDependencies に追加:

  "devDependencies": {
    "@tailwindcss/postcss": "^4.2.2",
    "@types/node": "^20.19.37",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
+   "@types/tinycolor2": "^1.4.6",
    "@biomejs/biome": "^2.4.8",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/package.json` at line 16, The package "@types/tinycolor2" is
listed under dependencies but should be a dev-only type package; open
package.json and move the "@types/tinycolor2" entry from "dependencies" into
"devDependencies" (remove the key from "dependencies" and add it under
"devDependencies" with the same version), then update the lockfile by running
your package manager (npm/yarn/pnpm install) so the lockfile reflects the
change; reference the dependency name "@types/tinycolor2" and the package.json
top-level "dependencies"/"devDependencies" sections when making the change.
src/dashboard/src/lib/modules.ts (1)

46-56: embedモジュールに別のアイコンを検討してください。

embedモジュールとwelcomeモジュールの両方がHandアイコンを使用しています。ユーザーが視覚的に区別しやすくするため、embedモジュールにはFileTextLayoutなど、埋め込み作成に適した別のアイコンを使用することを検討してください。

♻️ 提案する修正
-import { HelpCircle, Hand } from "lucide-react";
+import { HelpCircle, Hand, FileText } from "lucide-react";
   [
     "embed",
     {
       id: "embed",
       name: "埋め込み作成モジュール",
       description: "サーバー内の埋め込みを作成&管理できます",
       enabled: true,
-      icon: Hand,
+      icon: FileText,
       group: "ユーティリティ",
     },
   ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/lib/modules.ts` around lines 46 - 56, The embed module
configuration currently uses the Hand icon (see the module array entry with id
"embed" and icon: Hand); change its icon to a more appropriate one (e.g.,
FileText or Layout) so it’s visually distinct from the welcome module that also
uses Hand—update the icon property for the object with id "embed" to the chosen
icon symbol and ensure the icon import/usage matches existing icon naming
conventions.
src/dashboard/src/app/components/EmbedSelecter.tsx (1)

3-7: Propsのパラメータ名が実際の用途と一致していません。

onChangeコールバックのパラメータ名がchannelIdになっていますが、実際には埋め込みのタイトルを渡しています。可読性向上のため、パラメータ名を修正することを検討してください。

♻️ 提案する修正
 interface Props {
   guildId: string;
   value?: string;
-  onChange?: (channelId: string) => void; 
+  onChange?: (embedTitle: string) => void; 
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/components/EmbedSelecter.tsx` around lines 3 - 7,
PropsのonChangeのコールバック型とその呼び出しで使われているパラメータ名が実際の用途(埋め込みのタイトル)と一致していません: update the
interface Props (and any local type annotations) to change onChange?:
(channelId: string) => void to a clearer name such as onChange?: (title: string)
=> void or onChange?: (embedTitle: string) => void, then update the
EmbedSelecter component where it invokes onChange (and any parents/callers) to
pass and accept the new parameter name (title or embedTitle) instead of
channelId so names reflect the actual value passed.
src/dashboard/src/app/dashboard/[guildId]/welcome/page.tsx (1)

32-59: 非同期処理のパターンが混在しています。

useEffect内で.then()チェーンとawaitが混在しており、また最初のfetchの完了を待たずに2番目のfetchが実行されています。モジュールが無効の場合でも2番目のfetch(設定取得)が実行されてしまいます。

♻️ 提案する修正
 useEffect(() => {
   async function init() {
-    (await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=welcome`))
-      .json()
-      .then((data) => {
-        if (data.enabled) {
-          setLoading(false);
-        } else {
-          alert("このサーバーではモジュールが有効になっていません。");
-          router.push(`/dashboard/${guildId}`);
-        }
-      });
+    const enabledRes = await fetch(`/api/guilds/${guildId}/modules/isEnabled?module=welcome`);
+    const enabledData = await enabledRes.json();
+    
+    if (!enabledData.enabled) {
+      alert("このサーバーではモジュールが有効になっていません。");
+      router.push(`/dashboard/${guildId}`);
+      return;
+    }

     const res = await fetch(`/api/guilds/${guildId}/modules/welcome`);
     const data = await res.json();
     if (res.ok) {
-      setWelcomeEnabled(data.settings?.welcome?.enabled || false);
-      setWelcomeSelectedChannel(data.settings?.welcome?.channelId || "");
-      setWelcomeContent(data.settings?.welcome?.message || "");
-      setWelcomeEmbed(data.settings?.welcome?.embed || null);
-      setGoodbyeEnabled(data.settings?.goodbye?.enabled || false);
-      setGoodbyeSelectedChannel(data.settings?.goodbye?.channelId || "");
-      setGoodbyeContent(data.settings?.goodbye?.message || "");
-      setGoodbyeEmbed(data.settings?.goodbye?.embed || null);
+      setWelcomeEnabled(data.settings?.welcome?.enabled || false);
+      setWelcomeSelectedChannel(data.settings?.welcome?.channelId || "");
+      setWelcomeContent(data.settings?.welcome?.message || "");
+      setWelcomeEmbed(data.settings?.welcome?.embed || "");
+      setGoodbyeEnabled(data.settings?.goodbye?.enabled || false);
+      setGoodbyeSelectedChannel(data.settings?.goodbye?.channelId || "");
+      setGoodbyeContent(data.settings?.goodbye?.message || "");
+      setGoodbyeEmbed(data.settings?.goodbye?.embed || "");
     }
+    
+    setLoading(false);
   }
   init();
 }, [guildId, router]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx around lines 32 -
59, In useEffect's async init function, avoid mixing .then() and await: await
the first fetch to `/api/guilds/${guildId}/modules/isEnabled?module=welcome`,
check res.ok and parsed JSON, and if data.enabled is false call
router.push(`/dashboard/${guildId`) and return early so the second fetch to
`/api/guilds/${guildId}/modules/welcome` is not executed; also wrap the whole
init in try/catch to handle errors and ensure setLoading(false) is only called
after confirming enabled (or in finally if you want to hide spinner on error).
Update the init function (and any uses of setLoading, setWelcomeEnabled,
setWelcomeSelectedChannel, etc.) accordingly to use only await-based control
flow.
src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx (2)

101-104: 未使用の関数 handleEditClick が定義されています。

handleEditClick 関数が定義されていますが、JSX内で使用されていません。保存済み埋め込みリストに「編集」ボタンを追加するか、不要であれば削除を検討してください。

💡 編集ボタンを追加する例
                     <div className="flex gap-4 ml-4">
+                      <button 
+                        onClick={() => handleEditClick(embed)}
+                        className="text-sm font-semibold text-indigo-500 hover:text-indigo-700"
+                      >
+                        編集
+                      </button>
                       <button 
                         onClick={() => handleEmbedDelete(embed.title)}
                         className="text-sm font-semibold text-red-500 hover:text-red-700"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/embed/page.tsx around lines 101 -
104, The function handleEditClick (which calls setCurrentEmbed and
window.scrollTo) is defined but never used; either remove it or wire it up by
adding an "Edit" button next to each saved embed item that calls
handleEditClick(embed) on click. Locate the saved embeds render (the list/map
that outputs saved embed rows/cards) and add a button or clickable element that
invokes handleEditClick with the embed object, or delete the handleEditClick
function and any related state updates (setCurrentEmbed) if editing is not
needed.

153-154: embed.title を React の key として使用するのはリスクがあります。

タイトルが重複した場合や空の場合、Reactのリコンシリエーションに問題が発生する可能性があります。UUIDやインデックスと組み合わせた一意のキーの使用を検討してください。

♻️ 修正案
-                 savedEmbeds.map((embed: any) => (
-                   <div key={embed.title} className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border border-slate-200 hover:border-indigo-200 transition-colors">
+                 savedEmbeds.map((embed: any, index: number) => (
+                   <div key={`${embed.title}-${index}`} className="flex items-center justify-between p-4 bg-slate-50 rounded-lg border border-slate-200 hover:border-indigo-200 transition-colors">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/embed/page.tsx around lines 153 -
154, Replace the fragile key usage embed.title inside the savedEmbeds.map
callback with a stable unique identifier: prefer an existing unique field (e.g.,
embed.id) and, if not present, fall back to a deterministic combination such as
`${embed.id ?? index}` or generate a UUID when the item is created; update the
key prop in the JSX returned by savedEmbeds.map accordingly (change the key on
the <div> currently using embed.title to use the stable id/fallback) to prevent
reconciliation issues when titles are duplicated or empty.
src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts (1)

19-56: 認証・認可ロジックが各ハンドラーで重複しています。

GET、POST、DELETE の各ハンドラーで約50行の認証ロジックが重複しています。共通のミドルウェアまたはヘルパー関数に抽出することで、保守性とテスト容易性が向上します。

♻️ リファクタリング例
// 認証ヘルパー関数を作成
async function getAuthenticatedDiscordToken() {
  const allLinkedAccounts = await auth.api.listUserAccounts({
    headers: await headers(),
  });
  const discordAccountData = allLinkedAccounts.find(
    (account) => account.providerId === "discord",
  );
  if (!discordAccountData) {
    throw new UnauthorizedError();
  }
  const discordToken = await auth.api.getAccessToken({
    headers: await headers(),
    body: {
      providerId: "discord",
      accountId: discordAccountData.accountId,
      userId: discordAccountData.userId,
    },
  });
  if (!discordToken.accessTokenExpiresAt ||
      Date.now() >= new Date(discordToken.accessTokenExpiresAt).getTime()) {
    throw new UnauthorizedError();
  }
  return discordToken;
}

async function requireGuildAdmin(guildId: string) {
  const token = await getAuthenticatedDiscordToken();
  const hasPermission = await checkAdminPermission(guildId, token.accessToken);
  if (!hasPermission) {
    throw new ForbiddenError();
  }
  return token;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/embed/route.ts around
lines 19 - 56, The authentication/authorization block duplicated across handlers
should be extracted into reusable helpers: implement
getAuthenticatedDiscordToken() that uses auth.api.listUserAccounts and
auth.api.getAccessToken to find the discord account, validate
accessTokenExpiresAt and throw a clear Unauthorized error on failure, and
implement requireGuildAdmin(guildId) which calls getAuthenticatedDiscordToken()
and then checkAdminPermission(guildId, token.accessToken) throwing Forbidden
when permission is missing; replace the repeated code in each GET/POST/DELETE
handler with calls to requireGuildAdmin (or getAuthenticatedDiscordToken when
only the token is needed) and propagate errors as appropriate.
🤖 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/bot/cogs/welcome.py`:
- Around line 47-50: The code mutates the dict returned from getEmbed by writing
into embed_data["description"] and ["title"], which can alter cached/shared
data; fix by creating a shallow copy of the returned dict (e.g., new_embed =
embed_data.copy() or dict(embed_data)) and run welcome_parse on the copy before
passing it to discord.Embed.from_dict; apply the same defensive copy change to
the analogous code in on_member_remove where embed_data is modified.

In `@src/dashboard/src/app/api/guilds/`[guildId]/channels/route.tsx:
- Line 5: Remove the unused MongoDB import by deleting the `clientPromise`
import statement from this route handler (the `import clientPromise from
"@/lib/mongodb";` line) and ensure there are no remaining references to
`clientPromise` in the file; if any code later requires DB access, replace with
the correct data-access helper instead, otherwise simply remove the import to
avoid dead code.
- Around line 56-62: The current try/catch in the route is dead code because
getGuildChannels (in discord.ts) catches errors and returns [] instead of
throwing; remove the surrounding try/catch in the handler and simply return
NextResponse.json(channels) so the actual [] result is returned with 200 OK, or
alternatively update getGuildChannels to rethrow on failure and keep the route's
error handling—reference getGuildChannels and the route handler to implement one
of these two consistent behaviors.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/embed/route.ts:
- Line 200: The console.log in route.ts prints raw user input (body.title)
causing a log injection risk; update the delete handler that uses
console.log(`Deleting embed for guild ${guildId}:`, body.title) to either remove
the log in production or sanitize/escape body.title before logging (e.g., strip
control/newline characters and dangerous sequences) and switch to a structured
logger that treats message and fields separately; ensure you reference the same
symbols (guildId, body.title, console.log in the delete embed handler) when
making the change.
- Around line 137-142: DELETE ハンドラーの params
型が同期オブジェクトになっているため不整合が起きています。関数シグネチャの { params }: { params: { guildId: string }
} を POST と同様に Promise 型の { params: Promise<{ guildId: string }> } に変更し、既に書かれている
await params のままで await した結果から guildId を取り出すようにしてください(参照シンボル: DELETE 関数,
params)。
- Around line 68-73: The POST route handler's params are typed as a plain object
but Next.js 15 provides params as a Promise; update the POST signature to accept
params: Promise<{ guildId: string }> (i.e. change the second parameter type to {
params: Promise<{ guildId: string }> }) and ensure you await params inside POST
(e.g. const { guildId } = await params) just like the GET handler does so the
code matches Next.js 15 expectations for the POST function.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/welcome/route.ts:
- Around line 97-102: The POST handler's params type is incorrect: make it
consistent with the GET handler by changing the function signature to accept
params as a Promise (i.e., { params }: { params: Promise<{ guildId: string }>
}), then await params and destructure guildId as you already do (const { guildId
} = await params); ensure the POST export uses the same Promise<{ guildId:
string }> typing as the GET handler to match Next.js 15 conventions.

In `@src/dashboard/src/app/components/channel-selecter.tsx`:
- Around line 1-2: This component uses React hooks (useState, useEffect,
useCallback) so it must be a client component; add the React Server Components
directive "use client" as the very first line of channel-selecter.tsx (before
any imports) to enable client-side behavior for the component that defines
ChannelSelecter (and any exported functions/components in that file).

In `@src/dashboard/src/app/components/CollapsibleSection.tsx`:
- Around line 1-2: このコンポーネントは useState を使っているので Next.js App Router
でクライアントコンポーネントとして宣言する必要があります。ファイルの先頭(最初の行)に "use client"
ディレクティブを追加して、CollapsibleSection コンポーネント(およびファイル内の useState/ReactNode を使用するロジックや
ChevronDown/ChevronUp のインポート)をクライアントサイドで実行されるようにしてください。

In `@src/dashboard/src/app/components/ColorPicker.tsx`:
- Around line 9-13: Normalize color values to 24-bit before formatting and
emitting: mask the incoming/value used in hexValue with 0xFFFFFF (e.g., use
(value & 0xFFFFFF) before calling toString(16).padStart(6,"0")) and likewise
mask the parsed newColor in handleChange before calling onChange (e.g.,
onChange(parsedColor & 0xFFFFFF)) so negative or >24-bit ints produce a valid
`#rrggbb` string and a safe 24-bit value.

In `@src/dashboard/src/app/components/EmbedBuilder.tsx`:
- Around line 1-4: This module uses React hooks (useState, useEffect) so it must
be a client component: add the "use client" directive as the very first line of
the file (before any imports) to opt into client-side rendering; update the top
of EmbedBuilder.tsx so the string "use client" appears alone on the first line
to ensure hooks like useState and useEffect work correctly in the component
(e.g., affecting functions/components such as EmbedBuilder, any local handlers
using ChangeEvent, and ColorPicker usage).

In `@src/dashboard/src/app/components/EmbedSelecter.tsx`:
- Line 54: The disabled check in EmbedSelecter.tsx uses embeds.length but embeds
is initialized as an object, so embeds.length is always undefined; update the
condition used for the disabled prop to correctly detect an empty embeds object
(e.g., replace embeds.length === 0 with Object.keys(embeds).length === 0 or
Object.values(embeds).length === 0), or alternatively change the embeds state to
an array so length is valid—update the disabled prop and any related logic in
the EmbedSelecter component accordingly.
- Line 9: The component is misnamed: the default export function is
ChannelSelecter but the file is EmbedSelecter.tsx; rename the component function
and its default export to EmbedSelecter to match the file and intent, update any
internal references (e.g., usages/imports of ChannelSelecter) to the new name,
and run/adjust tests or TypeScript imports that reference ChannelSelecter so
everything compiles and imports correctly; keep the existing Props signature
(guildId, value, onChange) unchanged.

In `@src/dashboard/src/app/dashboard/`[guildId]/layout.tsx:
- Around line 106-111: The template literal for the sidebar's className contains
a JavaScript-style comment string ("// 高さを100%に固定") which is being rendered as
an invalid CSS class; remove that comment from the className value and instead
place it as a proper JSX comment (e.g. {/* 高さを100%に固定 */}) adjacent to the
element or above the prop so only valid classes remain; update the component
that uses isSidebarOpen and className (the layout component's className
assignment) accordingly.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx:
- Around line 61-82: handleWelcomeSubmit currently neither updates the saving
state nor handles API errors; wrap the fetch call by calling setSaving(true) at
the start and setSaving(false) in a finally block, check response.ok and parse
the body for errors, and on failure call the existing UI error handler (e.g.
setError or showToast) to display a user-facing message and keep the submit
button disabled while saving; update success handling to show confirmation and
update any local state as needed.

In `@src/dashboard/src/lib/discord.ts`:
- Around line 42-51: The getGuildChannels function is missing a res.ok check and
may return an error object as channel data; after the fetch in getGuildChannels,
verify if res.ok is true and if not return an empty array (matching the pattern
used in getGuildRequest) or throw a handled error, then only call res.json()
when res.ok; reference getGuildChannels, res.ok, fetch, headers, and
DISCORD_API_BASE_URL to locate and update the code.

---

Minor comments:
In `@src/bot/cogs/welcome.py`:
- Around line 24-25: Replace the explicit equality check against False with a
negation of the retrieved flag: instead of comparing
data["welcome"].get("enabled", False) == False, use Python's not operator on
data["welcome"].get("enabled", False); apply the same change to the second
occurrence around the later check (the one noted at lines 64-65) so both
early-return branches use negation instead of equality.
- Line 93: In on_member_remove ensure message-empty behavior matches
on_member_join: extract the goodbye message into a variable (using
data["goodbye"].get("message", "{ユーザー名}が退出しました。") and pass through
self.welcome_parse(member)), then if the parsed message is an empty string call
channel.send(embed=embed) only, otherwise call
channel.send(content=parsed_message, embed=embed); update references to
welcome_parse, data["goodbye"].get(...), member, embed and channel.send
accordingly.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/welcome/route.ts:
- Line 148: Rename the misspelled variable settung to setting where it's
declared as a WelcomeSetting and update all usages (including the later
occurrence noted around line 157) to match the new name; ensure the variable
name is consistently changed in the route handler so references to settung are
replaced with setting and types/signatures using WelcomeSetting remain correct.

In `@src/dashboard/src/app/components/EmbedBuilder.tsx`:
- Around line 89-93: The useEffect inside the EmbedBuilder component currently
lists onChange in the dependency array and can trigger an infinite loop if the
parent passes an inline callback; remove onChange from the dependency array and
instead call a stable reference to it (e.g., store the latest onChange in a ref
inside EmbedBuilder and invoke ref.current(embed) inside the effect) so the
effect only depends on embed, or alternatively require the parent to memoize
onChange with useCallback and document that requirement; update the useEffect
(and add the ref-management logic) so embed is the sole reactive dependency
while still invoking the most recent onChange.

In `@src/dashboard/src/app/dashboard/`[guildId]/embed/page.tsx:
- Around line 26-33: The code uses statusData without checking the HTTP
response; update the fetch handling around statusRes/statusData to first verify
statusRes.ok and handle non-OK responses (e.g., show an alert, log the error,
and redirect via router.push(`/dashboard/${guildId}`) or return) before
accessing statusData.enabled; modify the block that calls
fetch(`/api/guilds/${guildId}/modules/isEnabled?module=embed`) so that failures
and malformed JSON are caught and handled gracefully (use try/catch or check
statusRes.ok, then parse JSON) to avoid using statusData when the API returned
an error.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx:
- Line 6: The import CommandsControl in page.tsx is unused; either remove the
import statement for CommandsControl or actually render/use the CommandsControl
component inside the component exported from
src/app/dashboard/[guildId]/welcome/page.tsx (reference the CommandsControl
import at the top of the file) — if you choose to remove it, delete the import
line; if you meant to use it, add the CommandsControl JSX where the welcome page
comp (the default export component) returns its layout.

---

Nitpick comments:
In `@src/dashboard/package.json`:
- Line 16: The package "@types/tinycolor2" is listed under dependencies but
should be a dev-only type package; open package.json and move the
"@types/tinycolor2" entry from "dependencies" into "devDependencies" (remove the
key from "dependencies" and add it under "devDependencies" with the same
version), then update the lockfile by running your package manager
(npm/yarn/pnpm install) so the lockfile reflects the change; reference the
dependency name "@types/tinycolor2" and the package.json top-level
"dependencies"/"devDependencies" sections when making the change.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/embed/route.ts:
- Around line 19-56: The authentication/authorization block duplicated across
handlers should be extracted into reusable helpers: implement
getAuthenticatedDiscordToken() that uses auth.api.listUserAccounts and
auth.api.getAccessToken to find the discord account, validate
accessTokenExpiresAt and throw a clear Unauthorized error on failure, and
implement requireGuildAdmin(guildId) which calls getAuthenticatedDiscordToken()
and then checkAdminPermission(guildId, token.accessToken) throwing Forbidden
when permission is missing; replace the repeated code in each GET/POST/DELETE
handler with calls to requireGuildAdmin (or getAuthenticatedDiscordToken when
only the token is needed) and propagate errors as appropriate.

In `@src/dashboard/src/app/components/channel-selecter.tsx`:
- Around line 68-72: The current rendering inside the channels.map in
channel-selecter.tsx uses a binary test (channel.type === 0) and otherwise shows
"🔊", which mislabels many Discord channel types; update the display logic (in
the channels.map callback) to map channel.type to the correct icon string (e.g.,
0 -> "# ", 2 -> "🔊 ", 4 -> "📁 " or "📂 " for categories, 5 -> "📢 " for
announcements, 13 -> "🎤 " for stage, 15 -> "🧵 " for forum) using a small
switch or lookup object and fall back to a generic icon for unknown types,
ensuring the option content uses the mapped icon + channel.name.

In `@src/dashboard/src/app/components/ColorPicker.tsx`:
- Line 1: Add the React client boundary directive to this component file by
inserting the literal "use client" as the very first line of
src/dashboard/src/app/components/ColorPicker.tsx so the ColorPicker component
and its event handlers (e.g., any ChangeEvent handlers imported from "react")
run on the client; ensure the directive appears before any imports and save to
keep the file explicitly client-side as requested for verification.

In `@src/dashboard/src/app/components/EmbedBuilder.tsx`:
- Line 177: The current fallback uses a falsy check so a color value of 0
(black) falls back to "202225"; update the expression in EmbedBuilder.tsx where
tinycolor(embed.color?.toString(16) || "202225").toHexString() is used to only
treat undefined/null as missing (e.g. use the nullish coalescing operator:
tinycolor(embed.color?.toString(16) ?? "202225").toHexString() or explicitly
check embed.color === undefined and use the default), so 0x000000 is preserved
correctly.

In `@src/dashboard/src/app/components/EmbedSelecter.tsx`:
- Around line 3-7:
PropsのonChangeのコールバック型とその呼び出しで使われているパラメータ名が実際の用途(埋め込みのタイトル)と一致していません: update the
interface Props (and any local type annotations) to change onChange?:
(channelId: string) => void to a clearer name such as onChange?: (title: string)
=> void or onChange?: (embedTitle: string) => void, then update the
EmbedSelecter component where it invokes onChange (and any parents/callers) to
pass and accept the new parameter name (title or embedTitle) instead of
channelId so names reflect the actual value passed.

In `@src/dashboard/src/app/dashboard/`[guildId]/embed/page.tsx:
- Around line 101-104: The function handleEditClick (which calls setCurrentEmbed
and window.scrollTo) is defined but never used; either remove it or wire it up
by adding an "Edit" button next to each saved embed item that calls
handleEditClick(embed) on click. Locate the saved embeds render (the list/map
that outputs saved embed rows/cards) and add a button or clickable element that
invokes handleEditClick with the embed object, or delete the handleEditClick
function and any related state updates (setCurrentEmbed) if editing is not
needed.
- Around line 153-154: Replace the fragile key usage embed.title inside the
savedEmbeds.map callback with a stable unique identifier: prefer an existing
unique field (e.g., embed.id) and, if not present, fall back to a deterministic
combination such as `${embed.id ?? index}` or generate a UUID when the item is
created; update the key prop in the JSX returned by savedEmbeds.map accordingly
(change the key on the <div> currently using embed.title to use the stable
id/fallback) to prevent reconciliation issues when titles are duplicated or
empty.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx:
- Around line 32-59: In useEffect's async init function, avoid mixing .then()
and await: await the first fetch to
`/api/guilds/${guildId}/modules/isEnabled?module=welcome`, check res.ok and
parsed JSON, and if data.enabled is false call
router.push(`/dashboard/${guildId`) and return early so the second fetch to
`/api/guilds/${guildId}/modules/welcome` is not executed; also wrap the whole
init in try/catch to handle errors and ensure setLoading(false) is only called
after confirming enabled (or in finally if you want to hide spinner on error).
Update the init function (and any uses of setLoading, setWelcomeEnabled,
setWelcomeSelectedChannel, etc.) accordingly to use only await-based control
flow.

In `@src/dashboard/src/lib/modules.ts`:
- Around line 46-56: The embed module configuration currently uses the Hand icon
(see the module array entry with id "embed" and icon: Hand); change its icon to
a more appropriate one (e.g., FileText or Layout) so it’s visually distinct from
the welcome module that also uses Hand—update the icon property for the object
with id "embed" to the chosen icon symbol and ensure the icon import/usage
matches existing icon naming conventions.
🪄 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: 22cd3121-7a92-4de0-ac21-d90b25f5f803

📥 Commits

Reviewing files that changed from the base of the PR and between 1ee57b8 and f7de186.

⛔ Files ignored due to path filters (1)
  • src/dashboard/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • src/bot/cogs/welcome.py
  • src/bot/lib/embed.py
  • src/bot/main.py
  • src/dashboard/package.json
  • src/dashboard/src/app/api/guilds/[guildId]/channels/route.tsx
  • src/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.ts
  • src/dashboard/src/app/api/guilds/[guildId]/modules/route.ts
  • src/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.ts
  • src/dashboard/src/app/components/CollapsibleSection.tsx
  • src/dashboard/src/app/components/ColorPicker.tsx
  • src/dashboard/src/app/components/EmbedBuilder.tsx
  • src/dashboard/src/app/components/EmbedSelecter.tsx
  • src/dashboard/src/app/components/channel-selecter.tsx
  • src/dashboard/src/app/components/toggleSwitch.tsx
  • src/dashboard/src/app/dashboard/[guildId]/embed/page.tsx
  • src/dashboard/src/app/dashboard/[guildId]/layout.tsx
  • src/dashboard/src/app/dashboard/[guildId]/page.tsx
  • src/dashboard/src/app/dashboard/[guildId]/welcome/page.tsx
  • src/dashboard/src/lib/discord.ts
  • src/dashboard/src/lib/modules.ts

Comment thread src/bot/cogs/welcome.py
Comment on lines +47 to +50
embed_data["description"] = self.welcome_parse(embed_data.get("description", ""), member)
embed_data["title"] = self.welcome_parse(embed_data.get("title", ""), member)

embed = discord.Embed.from_dict(embed_data)
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

embed_dataを直接変更すると、キャッシュされたデータに影響する可能性があります。

getEmbedから返されたディクショナリを直接変更しています。MongoDBドライバやアプリケーションがこのデータをキャッシュしている場合、他のリクエストに影響を与える可能性があります。コピーを作成してから変更することを検討してください。

🛡️ 提案する修正
+import copy
+
 # on_member_join内
         embed_data = await self.bot.embed.getEmbed(guild_id, embed_name)
         if not embed_data:
             if message == "":
                 return
             await channel.send(content=message)
             return
         
+        embed_data = copy.deepcopy(embed_data)
         embed_data["description"] = self.welcome_parse(embed_data.get("description", ""), member)
         embed_data["title"] = self.welcome_parse(embed_data.get("title", ""), member)

同様の修正をon_member_remove内(Lines 82-90)にも適用してください。

Also applies to: 89-92

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

In `@src/bot/cogs/welcome.py` around lines 47 - 50, The code mutates the dict
returned from getEmbed by writing into embed_data["description"] and ["title"],
which can alter cached/shared data; fix by creating a shallow copy of the
returned dict (e.g., new_embed = embed_data.copy() or dict(embed_data)) and run
welcome_parse on the copy before passing it to discord.Embed.from_dict; apply
the same defensive copy change to the analogous code in on_member_remove where
embed_data is modified.

import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { checkAdminPermission, getGuildChannels } from "@/lib/discord";
import clientPromise from "@/lib/mongodb";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

未使用のインポート clientPromise を削除してください。

このルートハンドラーでは MongoDB への接続は行われていません。

♻️ 修正案
 import { auth } from "@/lib/auth";
 import { checkAdminPermission, getGuildChannels } from "@/lib/discord";
-import clientPromise from "@/lib/mongodb";
📝 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
import clientPromise from "@/lib/mongodb";
import { auth } from "@/lib/auth";
import { checkAdminPermission, getGuildChannels } from "@/lib/discord";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/channels/route.tsx at line 5,
Remove the unused MongoDB import by deleting the `clientPromise` import
statement from this route handler (the `import clientPromise from
"@/lib/mongodb";` line) and ensure there are no remaining references to
`clientPromise` in the file; if any code later requires DB access, replace with
the correct data-access helper instead, otherwise simply remove the import to
avoid dead code.

Comment on lines +56 to +62
try {
const channels = await getGuildChannels(guildId);

return NextResponse.json(channels);
} catch {
return NextResponse.json({ error: "DB接続エラー" }, { status: 500 });
}
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

getGuildChannels は例外をスローしないため、catch ブロックは到達不能コードです。

getGuildChannels 関数(src/dashboard/src/lib/discord.ts Lines 42-52)は内部で例外をキャッチし、エラー時は空配列 [] を返します。そのため、この try-catch は意味がなく、エラー時も空配列がそのまま 200 OK で返されます。

また、エラーメッセージ「DB接続エラー」は、このルートが DB を使用しないため不適切です。

🐛 修正案(オプションA: シンプルに try-catch を削除)
-  try {
-    const channels = await getGuildChannels(guildId);
-
-    return NextResponse.json(channels);
-  } catch {
-    return NextResponse.json({ error: "DB接続エラー" }, { status: 500 });
-  }
+  const channels = await getGuildChannels(guildId);
+  return NextResponse.json(channels);
🐛 修正案(オプションB: getGuildChannels を修正してエラーをスローさせる)

src/dashboard/src/lib/discord.tsgetGuildChannels がエラー時に例外をスローするよう修正し、このルートで適切にハンドリングします。

📝 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
try {
const channels = await getGuildChannels(guildId);
return NextResponse.json(channels);
} catch {
return NextResponse.json({ error: "DB接続エラー" }, { status: 500 });
}
const channels = await getGuildChannels(guildId);
return NextResponse.json(channels);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/channels/route.tsx around lines
56 - 62, The current try/catch in the route is dead code because
getGuildChannels (in discord.ts) catches errors and returns [] instead of
throwing; remove the surrounding try/catch in the handler and simply return
NextResponse.json(channels) so the actual [] result is returned with 200 OK, or
alternatively update getGuildChannels to rethrow on failure and keep the route's
error handling—reference getGuildChannels and the route handler to implement one
of these two consistent behaviors.

Comment on lines +68 to +73
export async function POST(
request: Request,
{ params }: { params: { guildId: string } }
) {
try {
const { guildId } = await params;
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

Next.js 15 では Route Handler の params は Promise 型です。

GET ハンドラーでは params: Promise<{ guildId: string }> を使用していますが、POST ハンドラーでは params: { guildId: string } となっています。Next.js 15 の仕様に合わせて修正してください。

🐛 修正案
 export async function POST(
   request: Request,
-  { params }: { params: { guildId: string } }
+  { params }: { params: Promise<{ guildId: string }> }
 ) {

Based on learnings: Before writing Next.js code, read the relevant guide in node_modules/next/dist/docs/ to account for breaking changes in APIs, conventions, and file structure.

📝 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
export async function POST(
request: Request,
{ params }: { params: { guildId: string } }
) {
try {
const { guildId } = await params;
export async function POST(
request: Request,
{ params }: { params: Promise<{ guildId: string }> }
) {
try {
const { guildId } = await params;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/embed/route.ts around
lines 68 - 73, The POST route handler's params are typed as a plain object but
Next.js 15 provides params as a Promise; update the POST signature to accept
params: Promise<{ guildId: string }> (i.e. change the second parameter type to {
params: Promise<{ guildId: string }> }) and ensure you await params inside POST
(e.g. const { guildId } = await params) just like the GET handler does so the
code matches Next.js 15 expectations for the POST function.

Comment on lines +137 to +142
export async function DELETE(
request: Request,
{ params }: { params: { guildId: string } }
) {
try {
const { guildId } = await params;
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

DELETE ハンドラーでも params の型が不整合です。

POST と同様に、DELETE ハンドラーの paramsPromise 型に修正してください。

🐛 修正案
 export async function DELETE(
   request: Request,
-  { params }: { params: { guildId: string } }
+  { params }: { params: Promise<{ guildId: string }> }
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/api/guilds/`[guildId]/modules/embed/route.ts around
lines 137 - 142, DELETE ハンドラーの params 型が同期オブジェクトになっているため不整合が起きています。関数シグネチャの {
params }: { params: { guildId: string } } を POST と同様に Promise 型の { params:
Promise<{ guildId: string }> } に変更し、既に書かれている await params のままで await した結果から
guildId を取り出すようにしてください(参照シンボル: DELETE 関数, params)。

onChange?: (channelId: string) => void;
}

export default function ChannelSelecter({ guildId, value, onChange }: Props) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

コンポーネント名がファイル名と一致していません。

コンポーネント名がChannelSelecterになっていますが、ファイル名はEmbedSelecter.tsxです。埋め込み選択コンポーネントとして機能することを明確にするため、名前を修正してください。

♻️ 提案する修正
-export default function ChannelSelecter({ guildId, value, onChange }: Props) {
+export default function EmbedSelecter({ guildId, value, onChange }: Props) {
📝 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
export default function ChannelSelecter({ guildId, value, onChange }: Props) {
export default function EmbedSelecter({ guildId, value, onChange }: Props) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/components/EmbedSelecter.tsx` at line 9, The component
is misnamed: the default export function is ChannelSelecter but the file is
EmbedSelecter.tsx; rename the component function and its default export to
EmbedSelecter to match the file and intent, update any internal references
(e.g., usages/imports of ChannelSelecter) to the new name, and run/adjust tests
or TypeScript imports that reference ChannelSelecter so everything compiles and
imports correctly; keep the existing Props signature (guildId, value, onChange)
unchanged.

<select
value={value || ""}
onChange={handleChange}
disabled={isLoading || embeds.length === 0}
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

オブジェクトに対する.lengthチェックが常にundefinedを返します。

embedsはオブジェクト{}として初期化されていますが、embeds.lengthをチェックしています。オブジェクトには.lengthプロパティがないため、常にundefinedとなり、undefined === 0falseになります。これにより、埋め込みが空でもselectが無効化されません。

🐛 提案する修正
-        disabled={isLoading || embeds.length === 0}
+        disabled={isLoading || Object.keys(embeds).length === 0}
📝 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
disabled={isLoading || embeds.length === 0}
disabled={isLoading || Object.keys(embeds).length === 0}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/components/EmbedSelecter.tsx` at line 54, The disabled
check in EmbedSelecter.tsx uses embeds.length but embeds is initialized as an
object, so embeds.length is always undefined; update the condition used for the
disabled prop to correctly detect an empty embeds object (e.g., replace
embeds.length === 0 with Object.keys(embeds).length === 0 or
Object.values(embeds).length === 0), or alternatively change the embeds state to
an array so length is valid—update the disabled prop and any related logic in
the EmbedSelecter component accordingly.

Comment on lines 106 to +111
className={`
fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
`}
fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200
transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
flex flex-col h-full // 高さを100%に固定
`}
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

テンプレート文字列内のコメントがCSSクラスとして出力されます。

Line 110の// 高さを100%に固定はJavaScriptコメントではなく、テンプレートリテラル内の文字列として扱われます。これにより//高さを100%に固定が無効なCSSクラスとして要素に追加されてしまいます。

🐛 提案する修正
       <aside
         className={`
           fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200 
           transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
           ${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
-          flex flex-col h-full // 高さを100%に固定
+          flex flex-col h-full
         `}
       >

コメントを残したい場合は、JSXコメントとして別の場所に記載してください。

📝 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
className={`
fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200 transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
`}
fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200
transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
flex flex-col h-full // 高さを100%に固定
`}
className={`
fixed inset-y-0 left-0 z-50 w-72 bg-white border-r border-slate-200
transform transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0
${isSidebarOpen ? "translate-x-0" : "-translate-x-full"}
flex flex-col h-full
`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/layout.tsx around lines 106 - 111,
The template literal for the sidebar's className contains a JavaScript-style
comment string ("// 高さを100%に固定") which is being rendered as an invalid CSS
class; remove that comment from the className value and instead place it as a
proper JSX comment (e.g. {/* 高さを100%に固定 */}) adjacent to the element or above
the prop so only valid classes remain; update the component that uses
isSidebarOpen and className (the layout component's className assignment)
accordingly.

Comment on lines +61 to +82
const handleWelcomeSubmit = async () => {
const response = await fetch(`/api/guilds/${guildId}/modules/welcome`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
welcome: {
channelId: welcomeSelectedChannel,
message: welcomeContent,
embed: welcomeEmbed,
enabled: welcomeEnabled,
},
goodbye: {
channelId: goodbyeSelectedChannel,
message: goodbyeContent,
embed: goodbyeEmbed,
enabled: goodbyeEnabled,
},
}),
});
};
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

saving状態が更新されず、エラーハンドリングがありません。

handleWelcomeSubmit関数内でsetSavingが呼び出されていないため、「保存中...」の表示やボタンの無効化が機能しません。また、APIエラー時のユーザーフィードバックがありません。

🐛 提案する修正
 const handleWelcomeSubmit = async () => {
+  setSaving(true);
+  try {
     const response = await fetch(`/api/guilds/${guildId}/modules/welcome`, {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
       },
       body: JSON.stringify({
         welcome: {
           channelId: welcomeSelectedChannel,
           message: welcomeContent,
           embed: welcomeEmbed,
           enabled: welcomeEnabled,
         },
         goodbye: {
           channelId: goodbyeSelectedChannel,
           message: goodbyeContent,
           embed: goodbyeEmbed,
           enabled: goodbyeEnabled,
         },
       }),
     });
+
+    if (!response.ok) {
+      throw new Error("保存に失敗しました");
+    }
+
+    alert("設定を保存しました");
+  } catch (error) {
+    console.error("Save error:", error);
+    alert("設定の保存に失敗しました");
+  } finally {
+    setSaving(false);
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/app/dashboard/`[guildId]/welcome/page.tsx around lines 61 -
82, handleWelcomeSubmit currently neither updates the saving state nor handles
API errors; wrap the fetch call by calling setSaving(true) at the start and
setSaving(false) in a finally block, check response.ok and parse the body for
errors, and on failure call the existing UI error handler (e.g. setError or
showToast) to display a user-facing message and keep the submit button disabled
while saving; update success handling to show confirmation and update any local
state as needed.

Comment on lines +42 to +51
export async function getGuildChannels(guildId: string) {
try {
const res = await fetch(`${DISCORD_API_BASE_URL}/guilds/${guildId}/channels`, {
headers: headers,
next: { revalidate: 30 },
});
return res.json();
} catch {
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

res.ok のチェックが欠落しており、APIエラー時に不正なデータが返される可能性があります。

Discord APIがエラーレスポンス(4xx/5xx)を返した場合、res.json() はエラーオブジェクトを返し、それがそのままチャンネルリストとして扱われます。同ファイルの getGuildRequest (Lines 19-21) と同様に、res.ok をチェックすべきです。

🐛 修正案
 export async function getGuildChannels(guildId: string) {
   try {
     const res = await fetch(`${DISCORD_API_BASE_URL}/guilds/${guildId}/channels`, {
       headers: headers,
       next: { revalidate: 30 },
     });
+    if (!res.ok) {
+      return [];
+    }
     return res.json();
   } catch {
     return [];
   }
-
 }
📝 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
export async function getGuildChannels(guildId: string) {
try {
const res = await fetch(`${DISCORD_API_BASE_URL}/guilds/${guildId}/channels`, {
headers: headers,
next: { revalidate: 30 },
});
return res.json();
} catch {
return [];
}
export async function getGuildChannels(guildId: string) {
try {
const res = await fetch(`${DISCORD_API_BASE_URL}/guilds/${guildId}/channels`, {
headers: headers,
next: { revalidate: 30 },
});
if (!res.ok) {
return [];
}
return res.json();
} catch {
return [];
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/dashboard/src/lib/discord.ts` around lines 42 - 51, The getGuildChannels
function is missing a res.ok check and may return an error object as channel
data; after the fetch in getGuildChannels, verify if res.ok is true and if not
return an empty array (matching the pattern used in getGuildRequest) or throw a
handled error, then only call res.json() when res.ok; reference
getGuildChannels, res.ok, fetch, headers, and DISCORD_API_BASE_URL to locate and
update the code.

@sharkbot-neko sharkbot-neko merged commit f7de186 into main Mar 26, 2026
6 checks passed
@sharkbot-neko sharkbot-neko deleted the feat/welcome-message branch March 26, 2026 01:27
@yuito-it yuito-it restored the feat/welcome-message branch March 26, 2026 01:28
@yuito-it yuito-it deleted the feat/welcome-message branch March 26, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

difficulity/normal 時間は少しかかるけど困難なく解決可能 kind/enhancement 機能改善のリクエスト priority/mid 優先度: 中

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants