Skip to content

[v9] Projectsにあるロールパネル機能の実装#223

Open
ysmreg wants to merge 7 commits intofeat/gofrom
ysmreg
Open

[v9] Projectsにあるロールパネル機能の実装#223
ysmreg wants to merge 7 commits intofeat/gofrom
ysmreg

Conversation

@ysmreg
Copy link

@ysmreg ysmreg commented Jan 24, 2026

This pull request introduces a new "role panel" feature for server management in the Discord bot, allowing admins to create, list, add roles to, remove roles from, and delete role panels via the /rolepanel command. It adds the necessary command registration, handler logic, database models, and subcommand implementations to support interactive role management through Discord's UI.

The most important changes are:

Role Panel Feature Implementation:

  • Added a new rolepanel command under server management, with subcommands for creating, deleting, adding, removing, and listing role panels. This includes command registration in commands.go and handler mapping in registry.go. [1] [2] [3] [4] [5]
  • Implemented subcommand logic in separate files: create.go for panel creation, delete.go for deletion, add.go for adding roles, remove.go for removing roles, and list.go for listing all panels in a server. Each subcommand provides user feedback and interactive selection via Discord embeds and select menus. [1] [2] [3] [4] [5]

Database Model and Migration:

  • Added new GORM models RolePanel and RolePanelOption to represent panels and their selectable roles, and updated the database setup to auto-migrate these models. [1] [2]

These changes enable server admins to manage self-assignable roles through a user-friendly panel system, improving the bot's role management capabilities.

Summary by CodeRabbit

  • New Features

    • ロールパネルコマンドを追加しました。サーバーでロール割り当てを管理するための新しいUIが利用できるようになります。ロールパネルの作成、削除、ロールの追加・削除、一覧表示が可能です。
  • Chores

    • 開発環境設定の更新

@ysmreg ysmreg requested a review from Copilot January 24, 2026 09:23
@ysmreg ysmreg requested a review from a team as a code owner January 24, 2026 09:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements a role panel feature for Discord server management, allowing admins to create interactive role selection panels using Discord's select menu UI. Users can self-assign roles through these panels, improving the bot's role management capabilities.

Changes:

  • Added database models (RolePanel and RolePanelOption) with GORM auto-migration support for storing panel configurations
  • Implemented a RolePanelRepository with full CRUD operations for panels and their role options
  • Created a /rolepanel command with five subcommands (create, delete, add, remove, list) for panel administration
  • Developed message component handlers for interactive panel functionality, including role selection and administrative actions

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/internal/model/rolepanel.go Defines database models for role panels and their selectable role options
src/internal/db/db.go Adds new models to database auto-migration
src/internal/repository/rolepanel.go Implements repository pattern with CRUD operations for role panels
src/internal/bot/command/server_management/rolepanel.go Main command handler with subcommand routing
src/internal/bot/command/server_management/rolepanel/create.go Subcommand to create new role panels
src/internal/bot/command/server_management/rolepanel/delete.go Subcommand to delete existing panels
src/internal/bot/command/server_management/rolepanel/add.go Subcommand to add roles to panels
src/internal/bot/command/server_management/rolepanel/remove.go Subcommand to remove roles from panels
src/internal/bot/command/server_management/rolepanel/list.go Subcommand to list all panels in a server
src/internal/bot/messageComponent/rolepanel.go Handles interactive select menu interactions for role assignment and panel management
src/internal/bot/command/commands.go Registers the rolepanel command
src/internal/bot/command/registry.go Maps the rolepanel command to its handler
.gitignore Adds docker-compose.yaml to ignored files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

ysmreg and others added 4 commits January 24, 2026 18:39
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com>
@yuito-it
Copy link
Member

@ysmreg コパイロットに生成させているのだろうけど、

  • Overview
  • DB構成に変更はないか
  • 関連するIssue
    についてセクションを分けて記述してください。

Copy link
Member

@yuito-it yuito-it left a comment

Choose a reason for hiding this comment

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

ロールパネルの情報をデータとして保存することは、可能な限りデータを保持しないという方針から容認しがたく、セレクトメニューのIDにrp-hogehogeのようにして処理するようにしてください。
TSの既存実装を参考にすることをお勧めします。

@yuito-it yuito-it added kind/feature 新機能のリクエスト priority/mid 優先度: 中 scope/rp ロールパネルに関するIssue labels Jan 24, 2026
@yuito-it yuito-it linked an issue Jan 27, 2026 that may be closed by this pull request
1 task
@yuito-it yuito-it changed the title Projectsにあるロールパネル機能の実装 [v9] Projectsにあるロールパネル機能の実装 Jan 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

新しいロールパネル機能をDiscordボットに実装しました。ユーザーが作成・削除・追加・削除・リストのサブコマンドを使用してロールパネルを管理でき、対応するデータモデル、リポジトリ、メッセージコンポーネントハンドラーが含まれます。

Changes

Cohort / File(s) Summary
Configuration
.gitignore
docker-compose.yaml を無視ファイルリストに追加。
Command Registration
src/internal/bot/command/commands.go, src/internal/bot/command/registry.go
rolepanel コマンドをコマンドコンテキストに登録し、ハンドラーレジストリで"rolepanel"キーにマッピング。
RolePanel Command Implementation
src/internal/bot/command/server_management/rolepanel.go, src/internal/bot/command/server_management/rolepanel/create.go, src/internal/bot/command/server_management/rolepanel/delete.go, src/internal/bot/command/server_management/rolepanel/list.go, src/internal/bot/command/server_management/rolepanel/add.go, src/internal/bot/command/server_management/rolepanel/remove.go
ロールパネルのメインコマンドと5つのサブコマンド(create、delete、list、add、remove)を実装。各サブコマンドは対応するパネル操作(作成、削除、リスト表示、ロール追加、ロール削除)を処理。
Message Component Handler
src/internal/bot/messageComponent/rolepanel.go
ロールパネル選択、追加、削除、削除のメッセージコンポーネントハンドラーを実装。ロール選択ロジック、保留中の追加フロー(TTLベース)、パネルユーティリティ、パネルメッセージ更新機能を含む。
Data Model & Repository
src/internal/model/rolepanel.go, src/internal/repository/rolepanel.go
RolePanelおよびRolePanelOption データモデルを定義し、対応するGORM管理の構造体とリレーションシップを確立。CRUD操作、オプション管理、マップされたクエリ、エラーハンドリングを備えたリポジトリパターンを実装。
Database Migration
src/internal/db/db.go
RolepanelおよびRolePanelOptionモデルを自動マイグレーションセットに追加し、RolePanelOptionから既存のrole_id列を削除するマイグレーションステップを実施。

Sequence Diagram(s)

sequenceDiagram
    actor User as ユーザー
    participant Discord as Discord API
    participant Bot as Bot Command Handler
    participant DB as Database/Repository
    participant MC as Message Component Handler

    User->>Discord: /rolepanel create コマンド実行
    Discord->>Bot: InteractionCreate イベント送信
    Bot->>Bot: オプション抽出 (title, description)
    Bot->>Discord: パネル埋め込みメッセージ送信
    Discord->>User: チャネルにパネル表示
    Bot->>DB: RolePanel レコード作成・保存
    DB-->>Bot: 成功/失敗応答
    Bot->>Discord: エフェメラルレスポンス送信
    Discord->>User: 成功/エラーメッセージ表示

    User->>Discord: パネルのロール選択ボタン/メニュー操作
    Discord->>MC: MessageComponentInteractionCreate イベント送信
    MC->>DB: パネルメタデータ取得
    DB-->>MC: RolePanel 情報返却
    MC->>MC: 現在のロール vs 希望ロール比較
    MC->>Discord: ロール追加/削除実行
    MC->>Discord: パネルメッセージ更新
    Discord->>User: ロール変更反映・パネル更新
    MC->>Discord: エフェメラル確認メッセージ送信
    Discord->>User: 操作結果表示
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.14% 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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ysmreg
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Copy link

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/internal/db/db.go (1)

18-45: ⚠️ Potential issue | 🟠 Major

マイグレーション失敗を成功扱いにしないでください。

AutoMigrate が失敗しても後続の cleanup を続けていますし、各 DropColumn の error も返していません。ここで失敗を握りつぶすと、起動だけ成功して schema だけ半端な状態が残ります。AutoMigrate は即 return、DropColumn も失敗時に return する形にそろえたいです。

🔧 修正例
 func SetupDB(db *gorm.DB) error {
-	err := db.AutoMigrate(
+	if err := db.AutoMigrate(
 		&model.Guild{},
 		&model.Member{},
 		&model.AuditLogSetting{},
 		&model.BotSystemSetting{},
 		&model.PinSetting{},
 		&model.RSSSetting{},
 		&model.ScheduleSetting{},
 		&model.TTSConnection{},
 		&model.TTSPersonalSetting{},
 		&model.TTSDictionary{},
 		&model.RolePanel{},
 		&model.RolePanelOption{},
-	)
+	); err != nil {
+		return err
+	}
 	migrator := db.Migrator()
 	if migrator.HasColumn(&model.TTSPersonalSetting{}, "speaker_seed") {
-		migrator.DropColumn(&model.TTSPersonalSetting{}, "speaker_seed")
+		if err := migrator.DropColumn(&model.TTSPersonalSetting{}, "speaker_seed"); err != nil {
+			return err
+		}
 	}
 	if migrator.HasColumn(&model.TTSPersonalSetting{}, "speaker_pitch") {
-		migrator.DropColumn(&model.TTSPersonalSetting{}, "speaker_pitch")
+		if err := migrator.DropColumn(&model.TTSPersonalSetting{}, "speaker_pitch"); err != nil {
+			return err
+		}
 	}
 	if migrator.HasColumn(&model.TTSPersonalSetting{}, "speed_scale") {
-		migrator.DropColumn(&model.TTSPersonalSetting{}, "speed_scale")
+		if err := migrator.DropColumn(&model.TTSPersonalSetting{}, "speed_scale"); err != nil {
+			return err
+		}
 	}
 	if migrator.HasColumn(&model.RolePanelOption{}, "role_id") {
-		migrator.DropColumn(&model.RolePanelOption{}, "role_id")
+		if err := migrator.DropColumn(&model.RolePanelOption{}, "role_id"); err != nil {
+			return err
+		}
 	}
-	return err
+	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/db/db.go` around lines 18 - 45, The migration currently swallows
errors: call to db.AutoMigrate should immediately return the error if non-nil
(i.e., if err := db.AutoMigrate(...) != nil { return err }) instead of
continuing to cleanup, and each migration cleanup using
migrator.HasColumn/migrator.DropColumn must check the error returned by
DropColumn and return it when non-nil; update the logic around
migrator.HasColumn, migrator.DropColumn and the final return so every DropColumn
call's error is propagated (reuse a local err variable or assign/return the
DropColumn result) rather than always returning the original err.
🧹 Nitpick comments (3)
src/internal/bot/command/server_management/rolepanel/create.go (1)

37-49: Options[0] へのアクセスでパニックの可能性

Line 39で i.ApplicationCommandData().Options[0].Options にアクセスしていますが、Options が空の場合にパニックが発生します。サブコマンド経由で呼ばれる想定なので通常は問題ありませんが、防御的にチェックを追加することを推奨します。

🛡️ 防御的なnilチェックの追加
 func Create(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) {
 	config := ctx.Config
-	options := i.ApplicationCommandData().Options[0].Options
+	cmdOptions := i.ApplicationCommandData().Options
+	if len(cmdOptions) == 0 || cmdOptions[0].Options == nil {
+		return
+	}
+	options := cmdOptions[0].Options
 
 	var title, description string
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/command/server_management/rolepanel/create.go` around lines
37 - 49, The code in Create accesses
i.ApplicationCommandData().Options[0].Options without checking for empty or nil,
which can panic; update Create to defensively validate
i.ApplicationCommandData() and its Options slice length before indexing (e.g.,
check that ApplicationCommandData() != nil and len(Options) > 0 and Options[0]
!= nil) and handle the error path by returning early or sending a user-facing
error/ephemeral response rather than indexing; refer to Create, the local
variable options, and the switch over opt.Name when adding the guard.
src/internal/bot/messageComponent/rolepanel.go (2)

188-203: ロール操作失敗時のログ出力を改善できます

fmt.Printf でエラーをログ出力していますが、構造化ロギングを使用することでより良い運用監視が可能になります。また、ユーザーに一部のロール操作が失敗したことを通知することも検討してください。

📝 エラーハンドリングの改善案
+var failedRoles []string
 for _, roleID := range panelRoleIDs {
 	hasRole := currentRoles[roleID]
 	shouldHaveRole := selectedRoleMap[roleID]

 	if shouldHaveRole && !hasRole {
 		if err := s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID); err != nil {
-			fmt.Printf("failed to add role %s to user %s in guild %s: %v\n", roleID, i.Member.User.ID, i.GuildID, err)
+			// TODO: 構造化ロギングの使用を検討
+			failedRoles = append(failedRoles, roleID)
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/messageComponent/rolepanel.go` around lines 188 - 203,
Replace the ad-hoc fmt.Printf error prints inside the role add/remove handling
with structured logging and surface failures to the user; specifically, in the
block using s.GuildMemberRoleAdd and s.GuildMemberRoleRemove (where addedRoles
and removedRoles are appended) remove fmt.Printf and call the project's logger
with contextual fields (guild ID, user ID, roleID, and the error) e.g.,
logger.WithFields(...).Errorf(...) or the equivalent logging helper used in this
package, and optionally queue a concise ephemeral/user-facing notification
indicating that some role operations failed so the caller sees partial failures.

36-41: インメモリストレージはボット再起動時にデータが失われます

rolePanelPendingAdds はインメモリのマップで管理されているため、ボット再起動時に保留中の追加操作がすべて失われます。TTLが10分と短いため許容範囲かもしれませんが、この動作を認識しておく必要があります。

将来的に永続化が必要な場合は、Redisやデータベースへの移行を検討してください。

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

In `@src/internal/bot/messageComponent/rolepanel.go` around lines 36 - 41,
rolePanelPendingAdds(型 RolePanelPendingAdd を保持するインメモリ
map)はボット再起動でデータを失うため、永続化ストレージに移す必要があります。修正方法:rolePanelPendingAdds
を直接使っている箇所(参照する関数やメソッド)を抽象化してインターフェース(例: GetPendingAdd, SetPendingAdd,
DeletePendingAdd, ListPendingAdds)を導入し、既存の sync.Mutex + map
実装をそのままの振る舞いでラップしたローカル実装と、Redis(または既存のDB)を使った永続実装の2つを用意して切り替え可能にすること。Redis
実装ではキーに TTL(10分)を設定し、RolePanelPendingAdd をシリアライズして保存・復元するように実装してください。最終的に
rolePanelPendingAdds の直接参照を削除して新しいインターフェース経由でアクセスするように変更してください。
🤖 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/internal/bot/command/registry.go`:
- Around line 37-39: The registry entry for "rolepanel" currently lacks the
Ephemeral flag, which causes deferred responses to be public; update the
registry map entry for "rolepanel" (the map key "rolepanel" with Handler:
server_management.Rolepanel) to include Ephemeral: true so it matches the
handlers (rolepanel.go, create.go, add.go, remove.go, list.go, delete.go) that
set MessageFlagsEphemeral and ensures deferred responses are private.

In `@src/internal/bot/command/server_management/rolepanel.go`:
- Around line 38-65: The handlers are calling InteractionRespond although the
dispatcher already ACKed the interaction, causing double-response errors;
replace all calls to InteractionRespond in Rolepanel and the rolepanel/*
handlers (rolepanel.go and rolepanel/{add,delete,remove,create,list}.go) with a
post-ACK approach: either edit the original response using
s.InteractionResponseEdit(i.Interaction, ...) when you intend to update the
deferred response, or create a follow-up using
s.FollowupMessageCreate(i.Interaction, ...) for additional messages; ensure you
preserve the same InteractionResponseData content (embeds, flags, etc.) when
converting each InteractionRespond call.

In `@src/internal/bot/command/server_management/rolepanel/list.go`:
- Around line 72-102: The embed fields are built from panels into the local
variable fields and may exceed Discord's 25-field limit; update the loop in
list.go (where panels are iterated and fields appended) to enforce a maximum of
25 fields: stop appending after 25 and either add a final MessageEmbedField
noting "表示は最初の25件までです" (or similar) or implement pagination by returning only
the first page and providing controls to fetch subsequent pages; ensure the code
references the existing variables panels, fields, and the embed construction so
the truncation/notice logic happens before the embed is sent.

In `@src/internal/bot/messageComponent/rolepanel.go`:
- Around line 846-847: Handle the ignored error from repo.GetByMessageID in the
same way as HandleRolePanelAdd: capture the error returned when calling
repo.GetByMessageID(messageID), check if err != nil (or if panel == nil) and
return/log the error before calling UpdatePanelMessage(s, panel, config, nil) to
avoid a panic; update the code around the call to use the returned error value
(instead of discarding it with _), and ensure you exit early (or handle
recovery) when repo.GetByMessageID fails.
- Around line 501-504: repo.GetByMessageID's error is currently ignored which
can lead to a panic when panel is nil; update the call site to capture and
handle the returned error and ensure panel is non-nil before calling
UpdatePanelMessage (e.g., check err from repo.GetByMessageID and
return/log/handle it, and if panel == nil return an appropriate error or log and
skip UpdatePanelMessage), referencing the repo.GetByMessageID call, the panel
variable, messageID, and the subsequent UpdatePanelMessage invocation so the
code path safely aborts instead of dereferencing a nil panel.
- Around line 622-641: Deletion order bug: deleting the Discord message via
s.ChannelMessageDelete before removing the DB record with repo.DeleteByID can
leave an orphaned DB record if the DB delete fails. Change the sequence in the
handler around panelTitle/panel to call repo.DeleteByID(panel.ID) first and only
if that succeeds call s.ChannelMessageDelete(panel.ChannelID, panel.MessageID);
on DB failure respond via s.InteractionRespond with the existing error embed and
do not delete the message; preserve existing error handling and return early on
repo.DeleteByID error.

---

Outside diff comments:
In `@src/internal/db/db.go`:
- Around line 18-45: The migration currently swallows errors: call to
db.AutoMigrate should immediately return the error if non-nil (i.e., if err :=
db.AutoMigrate(...) != nil { return err }) instead of continuing to cleanup, and
each migration cleanup using migrator.HasColumn/migrator.DropColumn must check
the error returned by DropColumn and return it when non-nil; update the logic
around migrator.HasColumn, migrator.DropColumn and the final return so every
DropColumn call's error is propagated (reuse a local err variable or
assign/return the DropColumn result) rather than always returning the original
err.

---

Nitpick comments:
In `@src/internal/bot/command/server_management/rolepanel/create.go`:
- Around line 37-49: The code in Create accesses
i.ApplicationCommandData().Options[0].Options without checking for empty or nil,
which can panic; update Create to defensively validate
i.ApplicationCommandData() and its Options slice length before indexing (e.g.,
check that ApplicationCommandData() != nil and len(Options) > 0 and Options[0]
!= nil) and handle the error path by returning early or sending a user-facing
error/ephemeral response rather than indexing; refer to Create, the local
variable options, and the switch over opt.Name when adding the guard.

In `@src/internal/bot/messageComponent/rolepanel.go`:
- Around line 188-203: Replace the ad-hoc fmt.Printf error prints inside the
role add/remove handling with structured logging and surface failures to the
user; specifically, in the block using s.GuildMemberRoleAdd and
s.GuildMemberRoleRemove (where addedRoles and removedRoles are appended) remove
fmt.Printf and call the project's logger with contextual fields (guild ID, user
ID, roleID, and the error) e.g., logger.WithFields(...).Errorf(...) or the
equivalent logging helper used in this package, and optionally queue a concise
ephemeral/user-facing notification indicating that some role operations failed
so the caller sees partial failures.
- Around line 36-41: rolePanelPendingAdds(型 RolePanelPendingAdd を保持するインメモリ
map)はボット再起動でデータを失うため、永続化ストレージに移す必要があります。修正方法:rolePanelPendingAdds
を直接使っている箇所(参照する関数やメソッド)を抽象化してインターフェース(例: GetPendingAdd, SetPendingAdd,
DeletePendingAdd, ListPendingAdds)を導入し、既存の sync.Mutex + map
実装をそのままの振る舞いでラップしたローカル実装と、Redis(または既存のDB)を使った永続実装の2つを用意して切り替え可能にすること。Redis
実装ではキーに TTL(10分)を設定し、RolePanelPendingAdd をシリアライズして保存・復元するように実装してください。最終的に
rolePanelPendingAdds の直接参照を削除して新しいインターフェース経由でアクセスするように変更してください。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 810d1c7b-a41a-4682-ae3b-ac3cd1f8a5ea

📥 Commits

Reviewing files that changed from the base of the PR and between 39e43cb and 1accc8c.

📒 Files selected for processing (13)
  • .gitignore
  • src/internal/bot/command/commands.go
  • src/internal/bot/command/registry.go
  • src/internal/bot/command/server_management/rolepanel.go
  • src/internal/bot/command/server_management/rolepanel/add.go
  • src/internal/bot/command/server_management/rolepanel/create.go
  • src/internal/bot/command/server_management/rolepanel/delete.go
  • src/internal/bot/command/server_management/rolepanel/list.go
  • src/internal/bot/command/server_management/rolepanel/remove.go
  • src/internal/bot/messageComponent/rolepanel.go
  • src/internal/db/db.go
  • src/internal/model/rolepanel.go
  • src/internal/repository/rolepanel.go

Comment on lines +37 to +39
"rolepanel": {
Handler: server_management.Rolepanel,
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n[レジストリ]\n'
rg -n -C2 '"rolepanel":|Ephemeral' src/internal/bot/command/registry.go

printf '\n[rolepanel 側のエフェメラル指定]\n'
rg -n -C1 'MessageFlagsEphemeral' \
  src/internal/bot/command/server_management/rolepanel.go \
  src/internal/bot/command/server_management/rolepanel

Repository: UniPro-tech/UniBot

Length of output: 4773


🏁 Script executed:

cat -n src/internal/bot/handler/interaction.go | head -60

Repository: UniPro-tech/UniBot

Length of output: 2271


rolepanelEphemeral: true にそろえた方がよいです。

src/internal/bot/handler/interaction.go:29-32 で deferred response にレジストリの Ephemeral フラグが反映されるため、現在の rolepanel 登録ではデファード応答が公開扱いになります。一方、rolepanel 配下のすべてのハンドラ (rolepanel.gocreate.goadd.goremove.golist.godelete.go) は応答時に MessageFlagsEphemeral を指定しており、プライベート前提の運用です。登録側と実装側のエフェメラル指定を統一するため、レジストリ側に Ephemeral: true を追加してください。

🔧 修正例
 	"rolepanel": {
-		Handler: server_management.Rolepanel,
+		Handler:   server_management.Rolepanel,
+		Ephemeral: true,
 	},
📝 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
"rolepanel": {
Handler: server_management.Rolepanel,
},
"rolepanel": {
Handler: server_management.Rolepanel,
Ephemeral: true,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/command/registry.go` around lines 37 - 39, The registry
entry for "rolepanel" currently lacks the Ephemeral flag, which causes deferred
responses to be public; update the registry map entry for "rolepanel" (the map
key "rolepanel" with Handler: server_management.Rolepanel) to include Ephemeral:
true so it matches the handlers (rolepanel.go, create.go, add.go, remove.go,
list.go, delete.go) that set MessageFlagsEphemeral and ensures deferred
responses are private.

Comment on lines +38 to +65
func Rolepanel(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) {
config := ctx.Config
subCommand := i.ApplicationCommandData().Options[0]

if handler, exists := rolepanelHandler[subCommand.Name]; exists {
handler(ctx, s, i)
return
}

_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "エラー",
Description: "不明なサブコマンドです。",
Color: config.Colors.Error,
Footer: &discordgo.MessageEmbedFooter{
Text: "Requested by " + i.Member.DisplayName(),
IconURL: i.Member.AvatarURL(""),
},
Timestamp: time.Now().Format(time.RFC3339),
},
},
Flags: discordgo.MessageFlagsEphemeral,
},
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n[dispatcher の ACK 順序]\n'
rg -n -C3 'InteractionRespond|entry\.Handler\(ctx, s, i\)' \
  src/internal/bot/handler/interaction.go

printf '\n[rolepanel 配下の InteractionRespond 呼び出し]\n'
rg -n -C1 'InteractionRespond\s*\(' \
  src/internal/bot/command/server_management/rolepanel.go \
  src/internal/bot/command/server_management/rolepanel

Repository: UniPro-tech/UniBot

Length of output: 6411


rolepanel コマンド群は dispatcher による ACK 後に InteractionRespond を呼び出しており、双重応答エラーが発生します。

src/internal/bot/handler/interaction.go:38 で既に ACK を返してから entry.Handler(ctx, s, i) で handler を呼んでいるため、rolepanel.go:47 および rolepanel/{add,delete,remove,create,list}.go 内の InteractionRespond 呼び出しはすべて双重応答になります。Discord API は同一 interaction に対する応答は 1 回のみ許可しているため、このままでは成功系・エラー系ともに応答失敗となります。original response の edit または followup message へ統一してください。

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

In `@src/internal/bot/command/server_management/rolepanel.go` around lines 38 -
65, The handlers are calling InteractionRespond although the dispatcher already
ACKed the interaction, causing double-response errors; replace all calls to
InteractionRespond in Rolepanel and the rolepanel/* handlers (rolepanel.go and
rolepanel/{add,delete,remove,create,list}.go) with a post-ACK approach: either
edit the original response using s.InteractionResponseEdit(i.Interaction, ...)
when you intend to update the deferred response, or create a follow-up using
s.FollowupMessageCreate(i.Interaction, ...) for additional messages; ensure you
preserve the same InteractionResponseData content (embeds, flags, etc.) when
converting each InteractionRespond call.

Comment on lines +72 to +102
var fields []*discordgo.MessageEmbedField
for _, panel := range panels {
var roles []string
roleIDsByKey, err := loadPanelRoleIDs(s, panel)
if err != nil {
fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("%s (ID: %s)", panel.Title, panel.MessageID),
Value: fmt.Sprintf("チャンネル: <#%s>\nロール: 取得失敗", panel.ChannelID),
Inline: false,
})
continue
}

for _, opt := range panel.Options {
roleID, ok := roleIDsByKey[opt.OptionKey]
if ok {
roles = append(roles, fmt.Sprintf("<@&%s>", roleID))
}
}

roleList := "なし"
if len(roles) > 0 {
roleList = strings.Join(roles, ", ")
}

fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("%s (ID: %s)", panel.Title, panel.MessageID),
Value: fmt.Sprintf("チャンネル: <#%s>\nロール: %s", panel.ChannelID, roleList),
Inline: false,
})
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Discord埋め込みのフィールド数上限に注意

Discordの埋め込みは最大25フィールドまでという制限があります。サーバーに25個以上のロールパネルがある場合、一部のパネルが表示されない可能性があります。ページネーションの実装、または最初の25件のみ表示する旨のメッセージを追加することを検討してください。

🔧 フィールド数の上限チェック追加案
 	var fields []*discordgo.MessageEmbedField
+	const maxFields = 25
 	for _, panel := range panels {
+		if len(fields) >= maxFields {
+			break
+		}
 		var roles []string
 		roleIDsByKey, err := loadPanelRoleIDs(s, panel)

Description部分に超過した場合のメッセージを追加:

-	Description: fmt.Sprintf("このサーバーには %d 個のロールパネルがあります。", len(panels)),
+	description := fmt.Sprintf("このサーバーには %d 個のロールパネルがあります。", len(panels))
+	if len(panels) > maxFields {
+		description += fmt.Sprintf("\n(最初の %d 件を表示しています)", maxFields)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/command/server_management/rolepanel/list.go` around lines 72
- 102, The embed fields are built from panels into the local variable fields and
may exceed Discord's 25-field limit; update the loop in list.go (where panels
are iterated and fields appended) to enforce a maximum of 25 fields: stop
appending after 25 and either add a final MessageEmbedField noting
"表示は最初の25件までです" (or similar) or implement pagination by returning only the first
page and providing controls to fetch subsequent pages; ensure the code
references the existing variables panels, fields, and the embed construction so
the truncation/notice logic happens before the embed is sent.

Comment on lines +501 to +504
panel, _ = repo.GetByMessageID(messageID)
if err := UpdatePanelMessage(s, panel, config, map[string]string{
optionKey: pendingAdd.RoleID,
}); err != nil {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

エラーが無視されています

repo.GetByMessageID のエラーが無視されており、panelnil の場合、続く UpdatePanelMessage でパニックが発生する可能性があります。

🐛 エラーハンドリングの追加
-	panel, _ = repo.GetByMessageID(messageID)
+	panel, err = repo.GetByMessageID(messageID)
+	if err != nil || panel == nil {
+		_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+			Type: discordgo.InteractionResponseUpdateMessage,
+			Data: &discordgo.InteractionResponseData{
+				Embeds: []*discordgo.MessageEmbed{
+					{
+						Title:       "エラー",
+						Description: "パネル情報の再取得に失敗しました。",
+						Color:       config.Colors.Error,
+						Timestamp:   time.Now().Format(time.RFC3339),
+					},
+				},
+				Components: []discordgo.MessageComponent{},
+			},
+		})
+		return
+	}
 	if err := UpdatePanelMessage(s, panel, config, map[string]string{
📝 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
panel, _ = repo.GetByMessageID(messageID)
if err := UpdatePanelMessage(s, panel, config, map[string]string{
optionKey: pendingAdd.RoleID,
}); err != nil {
panel, err = repo.GetByMessageID(messageID)
if err != nil || panel == nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "エラー",
Description: "パネル情報の再取得に失敗しました。",
Color: config.Colors.Error,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Components: []discordgo.MessageComponent{},
},
})
return
}
if err := UpdatePanelMessage(s, panel, config, map[string]string{
optionKey: pendingAdd.RoleID,
}); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/messageComponent/rolepanel.go` around lines 501 - 504,
repo.GetByMessageID's error is currently ignored which can lead to a panic when
panel is nil; update the call site to capture and handle the returned error and
ensure panel is non-nil before calling UpdatePanelMessage (e.g., check err from
repo.GetByMessageID and return/log/handle it, and if panel == nil return an
appropriate error or log and skip UpdatePanelMessage), referencing the
repo.GetByMessageID call, the panel variable, messageID, and the subsequent
UpdatePanelMessage invocation so the code path safely aborts instead of
dereferencing a nil panel.

Comment on lines +622 to +641
panelTitle := panel.Title
_ = s.ChannelMessageDelete(panel.ChannelID, panel.MessageID)

if err := repo.DeleteByID(panel.ID); err != nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "エラー",
Description: "パネルの削除中にエラーが発生しました。",
Color: config.Colors.Error,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Components: []discordgo.MessageComponent{},
},
})
return
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

削除順序により不整合が発生する可能性

Line 623でメッセージを削除した後、Line 625でDBレコードを削除しています。DB削除が失敗した場合、メッセージは既に削除されているため、孤立したDBレコードが残ります。

順序を逆にする(先にDBを削除、その後メッセージを削除)ことで、DBレコードが残る方向の一貫性を保つことができます。

💡 削除順序の変更案
 	panelTitle := panel.Title
-	_ = s.ChannelMessageDelete(panel.ChannelID, panel.MessageID)

 	if err := repo.DeleteByID(panel.ID); err != nil {
 		_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
 			// ... error response
 		})
 		return
 	}

+	// DBレコード削除成功後にメッセージを削除
+	_ = s.ChannelMessageDelete(panel.ChannelID, panel.MessageID)

 	_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/messageComponent/rolepanel.go` around lines 622 - 641,
Deletion order bug: deleting the Discord message via s.ChannelMessageDelete
before removing the DB record with repo.DeleteByID can leave an orphaned DB
record if the DB delete fails. Change the sequence in the handler around
panelTitle/panel to call repo.DeleteByID(panel.ID) first and only if that
succeeds call s.ChannelMessageDelete(panel.ChannelID, panel.MessageID); on DB
failure respond via s.InteractionRespond with the existing error embed and do
not delete the message; preserve existing error handling and return early on
repo.DeleteByID error.

Comment on lines +846 to +847
panel, _ = repo.GetByMessageID(messageID)
if err := UpdatePanelMessage(s, panel, config, nil); err != nil {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

同様のエラー無視の問題

HandleRolePanelAdd と同様に、repo.GetByMessageID のエラーが無視されています。panelnil の場合に UpdatePanelMessage でパニックが発生します。

🐛 エラーハンドリングの追加
-	panel, _ = repo.GetByMessageID(messageID)
+	panel, err = repo.GetByMessageID(messageID)
+	if err != nil || panel == nil {
+		_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
+			Type: discordgo.InteractionResponseUpdateMessage,
+			Data: &discordgo.InteractionResponseData{
+				Embeds: []*discordgo.MessageEmbed{
+					{
+						Title:       "エラー",
+						Description: "パネル情報の再取得に失敗しました。",
+						Color:       config.Colors.Error,
+						Timestamp:   time.Now().Format(time.RFC3339),
+					},
+				},
+				Components: []discordgo.MessageComponent{},
+			},
+		})
+		return
+	}
 	if err := UpdatePanelMessage(s, panel, config, nil); err != nil {
📝 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
panel, _ = repo.GetByMessageID(messageID)
if err := UpdatePanelMessage(s, panel, config, nil); err != nil {
panel, err = repo.GetByMessageID(messageID)
if err != nil || panel == nil {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "エラー",
Description: "パネル情報の再取得に失敗しました。",
Color: config.Colors.Error,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Components: []discordgo.MessageComponent{},
},
})
return
}
if err := UpdatePanelMessage(s, panel, config, nil); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/internal/bot/messageComponent/rolepanel.go` around lines 846 - 847,
Handle the ignored error from repo.GetByMessageID in the same way as
HandleRolePanelAdd: capture the error returned when calling
repo.GetByMessageID(messageID), check if err != nil (or if panel == nil) and
return/log the error before calling UpdatePanelMessage(s, panel, config, nil) to
avoid a panic; update the code around the call to use the returned error value
(instead of discarding it with _), and ensure you exit early (or handle
recovery) when repo.GetByMessageID fails.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature 新機能のリクエスト priority/mid 優先度: 中 scope/rp ロールパネルに関するIssue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v9] ロールパネル機能

3 participants