Conversation
📝 WalkthroughWalkthroughDiscordボットに参加・離脱イベントハンドラーとサーバーごとの設定管理機能を追加し、ダッシュボードに対応するAPI ルートとUI コンポーネントを実装しました。埋め込みテンプレートとウェルカム・グッドバイメッセージの設定、作成、削除が可能になります。 Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
| { $unset: { [`embeds.${body.title}`]: "" } }, | ||
| ); | ||
|
|
||
| console.log(`Deleting embed for guild ${guildId}:`, body.title); |
Check warning
Code scanning / CodeQL
Log injection
Show autofix suggestion
Hide autofix suggestion
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 thetryblock wherebodyis read and the databaseupdateOneis called, replace the directconsole.logcall with:- creation of
sanitizedTitle = String(body.title).replace(/[\r\n]/g, "") - logging
sanitizedTitleinstead ofbody.title.
- creation of
| @@ -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) { |
There was a problem hiding this comment.
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]);または、親コンポーネント側で
onChangeをuseCallbackでメモ化することを推奨します。🤖 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): returnAlso 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タイポ:
settung→setting変数名にタイポがあります。
✏️ 修正案
- 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:colorが0(黒)の場合、フォールバック値が使用されます。
embed.color?.toString(16) || "202225"では、colorが0の場合は 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/tinycolor2はdevDependenciesに配置すべきです。型定義パッケージはビルド時のみ必要であり、本番環境では不要です。
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モジュールにはFileTextやLayoutなど、埋め込み作成に適した別のアイコンを使用することを検討してください。♻️ 提案する修正
-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
⛔ Files ignored due to path filters (1)
src/dashboard/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (20)
src/bot/cogs/welcome.pysrc/bot/lib/embed.pysrc/bot/main.pysrc/dashboard/package.jsonsrc/dashboard/src/app/api/guilds/[guildId]/channels/route.tsxsrc/dashboard/src/app/api/guilds/[guildId]/modules/embed/route.tssrc/dashboard/src/app/api/guilds/[guildId]/modules/route.tssrc/dashboard/src/app/api/guilds/[guildId]/modules/welcome/route.tssrc/dashboard/src/app/components/CollapsibleSection.tsxsrc/dashboard/src/app/components/ColorPicker.tsxsrc/dashboard/src/app/components/EmbedBuilder.tsxsrc/dashboard/src/app/components/EmbedSelecter.tsxsrc/dashboard/src/app/components/channel-selecter.tsxsrc/dashboard/src/app/components/toggleSwitch.tsxsrc/dashboard/src/app/dashboard/[guildId]/embed/page.tsxsrc/dashboard/src/app/dashboard/[guildId]/layout.tsxsrc/dashboard/src/app/dashboard/[guildId]/page.tsxsrc/dashboard/src/app/dashboard/[guildId]/welcome/page.tsxsrc/dashboard/src/lib/discord.tssrc/dashboard/src/lib/modules.ts
| 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) |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| try { | ||
| const channels = await getGuildChannels(guildId); | ||
|
|
||
| return NextResponse.json(channels); | ||
| } catch { | ||
| return NextResponse.json({ error: "DB接続エラー" }, { status: 500 }); | ||
| } |
There was a problem hiding this comment.
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.ts で getGuildChannels がエラー時に例外をスローするよう修正し、このルートで適切にハンドリングします。
📝 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.
| 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.
| export async function POST( | ||
| request: Request, | ||
| { params }: { params: { guildId: string } } | ||
| ) { | ||
| try { | ||
| const { guildId } = await params; |
There was a problem hiding this comment.
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.
| 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.
| export async function DELETE( | ||
| request: Request, | ||
| { params }: { params: { guildId: string } } | ||
| ) { | ||
| try { | ||
| const { guildId } = await params; |
There was a problem hiding this comment.
DELETE ハンドラーでも params の型が不整合です。
POST と同様に、DELETE ハンドラーの params も Promise 型に修正してください。
🐛 修正案
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) { |
There was a problem hiding this comment.
🛠️ 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.
| 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} |
There was a problem hiding this comment.
オブジェクトに対する.lengthチェックが常にundefinedを返します。
embedsはオブジェクト{}として初期化されていますが、embeds.lengthをチェックしています。オブジェクトには.lengthプロパティがないため、常にundefinedとなり、undefined === 0はfalseになります。これにより、埋め込みが空でも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.
| 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.
| 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%に固定 | ||
| `} |
There was a problem hiding this comment.
テンプレート文字列内のコメントが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.
| 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.
| 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, | ||
| }, | ||
| }), | ||
| }); | ||
| }; |
There was a problem hiding this comment.
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.
| 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 []; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
リリースノート
新機能
改善