From b3eeab3ec75c2e284c84b44cd25ac3ffeb991e16 Mon Sep 17 00:00:00 2001 From: ysmreg <33682244+ysmreg@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:19:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20RolePanel=E3=82=92=E4=BD=9C?= =?UTF-8?q?=E6=88=90=E3=81=97=E3=81=A6=E3=81=BF=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/internal/bot/command/commands.go | 2 + src/internal/bot/command/registry.go | 2 + .../command/server_management/rolepanel.go | 65 ++ .../server_management/rolepanel/add.go | 154 +++++ .../server_management/rolepanel/create.go | 143 +++++ .../server_management/rolepanel/delete.go | 109 ++++ .../server_management/rolepanel/list.go | 108 ++++ .../server_management/rolepanel/remove.go | 151 +++++ .../bot/messageComponent/rolepanel.go | 580 ++++++++++++++++++ src/internal/db/db.go | 2 + src/internal/model/rolepanel.go | 28 + src/internal/repository/rolepanel.go | 128 ++++ 13 files changed, 1473 insertions(+) create mode 100644 src/internal/bot/command/server_management/rolepanel.go create mode 100644 src/internal/bot/command/server_management/rolepanel/add.go create mode 100644 src/internal/bot/command/server_management/rolepanel/create.go create mode 100644 src/internal/bot/command/server_management/rolepanel/delete.go create mode 100644 src/internal/bot/command/server_management/rolepanel/list.go create mode 100644 src/internal/bot/command/server_management/rolepanel/remove.go create mode 100644 src/internal/bot/messageComponent/rolepanel.go create mode 100644 src/internal/model/rolepanel.go create mode 100644 src/internal/repository/rolepanel.go diff --git a/.gitignore b/.gitignore index 420532b3..fc402d02 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ go.work.sum # Scripts included sensitive information scripts/build.sh docker-compose.prod.yaml +docker-compose.yaml diff --git a/src/internal/bot/command/commands.go b/src/internal/bot/command/commands.go index 4e365267..ae1c3293 100644 --- a/src/internal/bot/command/commands.go +++ b/src/internal/bot/command/commands.go @@ -3,6 +3,7 @@ package command import ( "unibot/internal/bot/command/admin" "unibot/internal/bot/command/general" + "unibot/internal/bot/command/server_management" "github.com/bwmarrin/discordgo" ) @@ -12,4 +13,5 @@ var Commands = []*discordgo.ApplicationCommand{ general.LoadAboutCommandContext(), general.LoadTtsCommandContext(), admin.LoadMaintenanceCommandContext(), + server_management.LoadRolepanelCommandContext(), } diff --git a/src/internal/bot/command/registry.go b/src/internal/bot/command/registry.go index 38b03119..7fea5331 100644 --- a/src/internal/bot/command/registry.go +++ b/src/internal/bot/command/registry.go @@ -4,6 +4,7 @@ import ( "unibot/internal" "unibot/internal/bot/command/admin" "unibot/internal/bot/command/general" + "unibot/internal/bot/command/server_management" "github.com/bwmarrin/discordgo" ) @@ -13,4 +14,5 @@ var Handlers = map[string]func(*internal.BotContext, *discordgo.Session, *discor "about": general.About, "maintenance": admin.Maintenance, "tts": general.Tts, + "rolepanel": server_management.Rolepanel, } diff --git a/src/internal/bot/command/server_management/rolepanel.go b/src/internal/bot/command/server_management/rolepanel.go new file mode 100644 index 00000000..906f6054 --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel.go @@ -0,0 +1,65 @@ +package server_management + +import ( + "time" + "unibot/internal" + "unibot/internal/bot/command/server_management/rolepanel" + + "github.com/bwmarrin/discordgo" +) + +func LoadRolepanelCommandContext() *discordgo.ApplicationCommand { + return &discordgo.ApplicationCommand{ + Name: "rolepanel", + Description: "ロールパネルを管理します", + DefaultMemberPermissions: ptrInt64(discordgo.PermissionManageRoles), + Options: []*discordgo.ApplicationCommandOption{ + rolepanel.LoadCreateCommandContext(), + rolepanel.LoadDeleteCommandContext(), + rolepanel.LoadAddCommandContext(), + rolepanel.LoadRemoveCommandContext(), + rolepanel.LoadListCommandContext(), + }, + } +} + +func ptrInt64(i int64) *int64 { + return &i +} + +var rolepanelHandler = map[string]func(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate){ + "create": rolepanel.Create, + "delete": rolepanel.Delete, + "add": rolepanel.Add, + "remove": rolepanel.Remove, + "list": rolepanel.List, +} + +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, + }, + }) +} diff --git a/src/internal/bot/command/server_management/rolepanel/add.go b/src/internal/bot/command/server_management/rolepanel/add.go new file mode 100644 index 00000000..94488798 --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel/add.go @@ -0,0 +1,154 @@ +package rolepanel + +import ( + "fmt" + "time" + "unibot/internal" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func LoadAddCommandContext() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "add", + Description: "ロールパネルにロールを追加します", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionRole, + Name: "role", + Description: "追加するロール", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "label", + Description: "セレクトメニューに表示するラベル", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "description", + Description: "ロールの説明", + Required: false, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "emoji", + Description: "絵文字 (例: 🎮)", + Required: false, + }, + }, + } +} + +func Add(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + options := i.ApplicationCommandData().Options[0].Options + + var roleID, label, description, emoji string + for _, opt := range options { + switch opt.Name { + case "role": + roleID = opt.RoleValue(s, i.GuildID).ID + case "label": + label = opt.StringValue() + case "description": + description = opt.StringValue() + case "emoji": + emoji = opt.StringValue() + } + } + + repo := repository.NewRolePanelRepository(ctx.DB) + + // このギルドのパネル一覧を取得 + panels, err := repo.ListByGuild(i.GuildID) + if err != nil { + _ = 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, + }, + }) + return + } + + if len(panels) == 0 { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "このサーバーにはロールパネルがありません。\n先に `/rolepanel create` でパネルを作成してください。", + 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, + }, + }) + return + } + + // パネル選択用セレクトメニューを作成 + var selectOptions []discordgo.SelectMenuOption + for _, panel := range panels { + selectOptions = append(selectOptions, discordgo.SelectMenuOption{ + Label: panel.Title, + Value: panel.MessageID, + Description: fmt.Sprintf("%d個のロール", len(panel.Options)), + }) + } + + // CustomIDにロール情報をエンコード (roleID|label|description|emoji) + customID := fmt.Sprintf("rolepanel_add_%s|%s|%s|%s", roleID, label, description, emoji) + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "パネルを選択", + Description: fmt.Sprintf("ロール <@&%s> を追加するパネルを選択してください。", roleID), + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: customID, + Placeholder: "パネルを選択...", + Options: selectOptions, + }, + }, + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/internal/bot/command/server_management/rolepanel/create.go b/src/internal/bot/command/server_management/rolepanel/create.go new file mode 100644 index 00000000..9fe853c4 --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel/create.go @@ -0,0 +1,143 @@ +package rolepanel + +import ( + "fmt" + "time" + "unibot/internal" + "unibot/internal/model" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func LoadCreateCommandContext() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "create", + Description: "ロールパネルを作成します", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "title", + Description: "パネルのタイトル", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "description", + Description: "パネルの説明", + Required: false, + }, + }, + } +} + +func Create(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + options := i.ApplicationCommandData().Options[0].Options + + var title, description string + for _, opt := range options { + switch opt.Name { + case "title": + title = opt.StringValue() + case "description": + description = opt.StringValue() + } + } + + // パネルメッセージを送信 + panelEmbed := &discordgo.MessageEmbed{ + Title: title, + Description: description, + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "ロールを選択してください", + }, + Timestamp: time.Now().Format(time.RFC3339), + } + + msg, err := s.ChannelMessageSendEmbed(i.ChannelID, panelEmbed) + if err != nil { + _ = 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, + }, + }) + return + } + + // データベースに保存 + repo := repository.NewRolePanelRepository(ctx.DB) + panel := &model.RolePanel{ + GuildID: i.GuildID, + ChannelID: i.ChannelID, + MessageID: msg.ID, + Title: title, + Description: description, + } + + err = repo.Create(panel) + if err != nil { + // メッセージを削除 + _ = s.ChannelMessageDelete(i.ChannelID, msg.ID) + _ = 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, + }, + }) + return + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールパネルを作成しました", + Description: fmt.Sprintf("メッセージID: `%s`\n\n`/rolepanel add` コマンドでロールを追加してください。", msg.ID), + Color: config.Colors.Success, + Fields: []*discordgo.MessageEmbedField{ + { + Name: "タイトル", + Value: title, + Inline: true, + }, + }, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/internal/bot/command/server_management/rolepanel/delete.go b/src/internal/bot/command/server_management/rolepanel/delete.go new file mode 100644 index 00000000..cb130ca4 --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel/delete.go @@ -0,0 +1,109 @@ +package rolepanel + +import ( + "fmt" + "time" + "unibot/internal" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func LoadDeleteCommandContext() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "delete", + Description: "ロールパネルを削除します", + } +} + +func Delete(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + repo := repository.NewRolePanelRepository(ctx.DB) + + // このギルドのパネル一覧を取得 + panels, err := repo.ListByGuild(i.GuildID) + if err != nil { + _ = 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, + }, + }) + return + } + + if len(panels) == 0 { + _ = 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, + }, + }) + return + } + + // パネル選択用セレクトメニューを作成 + var selectOptions []discordgo.SelectMenuOption + for _, panel := range panels { + selectOptions = append(selectOptions, discordgo.SelectMenuOption{ + Label: panel.Title, + Value: panel.MessageID, + Description: fmt.Sprintf("%d個のロール", len(panel.Options)), + }) + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "パネルを選択", + Description: "削除するパネルを選択してください。", + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "rolepanel_delete", + Placeholder: "パネルを選択...", + Options: selectOptions, + }, + }, + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/internal/bot/command/server_management/rolepanel/list.go b/src/internal/bot/command/server_management/rolepanel/list.go new file mode 100644 index 00000000..98fd388f --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel/list.go @@ -0,0 +1,108 @@ +package rolepanel + +import ( + "fmt" + "strings" + "time" + "unibot/internal" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func LoadListCommandContext() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "list", + Description: "このサーバーのロールパネル一覧を表示します", + } +} + +func List(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + repo := repository.NewRolePanelRepository(ctx.DB) + + panels, err := repo.ListByGuild(i.GuildID) + if err != nil { + _ = 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, + }, + }) + return + } + + if len(panels) == 0 { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールパネル一覧", + Description: "このサーバーにはロールパネルがありません。\n\n`/rolepanel create` でパネルを作成してください。", + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + var fields []*discordgo.MessageEmbedField + for _, panel := range panels { + var roles []string + for _, opt := range panel.Options { + roles = append(roles, fmt.Sprintf("<@&%s>", opt.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, + }) + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールパネル一覧", + Description: fmt.Sprintf("このサーバーには %d 個のロールパネルがあります。", len(panels)), + Color: config.Colors.Primary, + Fields: fields, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/internal/bot/command/server_management/rolepanel/remove.go b/src/internal/bot/command/server_management/rolepanel/remove.go new file mode 100644 index 00000000..45acf922 --- /dev/null +++ b/src/internal/bot/command/server_management/rolepanel/remove.go @@ -0,0 +1,151 @@ +package rolepanel + +import ( + "fmt" + "time" + "unibot/internal" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func LoadRemoveCommandContext() *discordgo.ApplicationCommandOption { + return &discordgo.ApplicationCommandOption{ + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "remove", + Description: "ロールパネルからロールを削除します", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionRole, + Name: "role", + Description: "削除するロール", + Required: true, + }, + }, + } +} + +func Remove(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + options := i.ApplicationCommandData().Options[0].Options + + var roleID string + for _, opt := range options { + if opt.Name == "role" { + roleID = opt.RoleValue(s, i.GuildID).ID + } + } + + repo := repository.NewRolePanelRepository(ctx.DB) + + // このギルドのパネル一覧を取得 + panels, err := repo.ListByGuild(i.GuildID) + if err != nil { + _ = 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, + }, + }) + return + } + + // このロールを含むパネルをフィルタ + var matchingPanels []*struct { + Title string + MessageID string + RoleCount int + } + for _, panel := range panels { + for _, opt := range panel.Options { + if opt.RoleID == roleID { + matchingPanels = append(matchingPanels, &struct { + Title string + MessageID string + RoleCount int + }{ + Title: panel.Title, + MessageID: panel.MessageID, + RoleCount: len(panel.Options), + }) + break + } + } + } + + if len(matchingPanels) == 0 { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: fmt.Sprintf("ロール <@&%s> を含むパネルが見つかりません。", roleID), + 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, + }, + }) + return + } + + // パネル選択用セレクトメニューを作成 + var selectOptions []discordgo.SelectMenuOption + for _, panel := range matchingPanels { + selectOptions = append(selectOptions, discordgo.SelectMenuOption{ + Label: panel.Title, + Value: panel.MessageID, + Description: fmt.Sprintf("%d個のロール", panel.RoleCount), + }) + } + + customID := fmt.Sprintf("rolepanel_remove_%s", roleID) + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "パネルを選択", + Description: fmt.Sprintf("ロール <@&%s> を削除するパネルを選択してください。", roleID), + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "Requested by " + i.Member.DisplayName(), + IconURL: i.Member.AvatarURL(""), + }, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: customID, + Placeholder: "パネルを選択...", + Options: selectOptions, + }, + }, + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} diff --git a/src/internal/bot/messageComponent/rolepanel.go b/src/internal/bot/messageComponent/rolepanel.go new file mode 100644 index 00000000..feb2e5f6 --- /dev/null +++ b/src/internal/bot/messageComponent/rolepanel.go @@ -0,0 +1,580 @@ +package messageComponent + +import ( + "fmt" + "strings" + "time" + "unibot/internal" + "unibot/internal/model" + "unibot/internal/repository" + + "github.com/bwmarrin/discordgo" +) + +func init() { + RegisterHandler("rolepanel_select_", HandleRolePanelSelect) + RegisterHandler("rolepanel_add_", HandleRolePanelAdd) + RegisterHandler("rolepanel_delete", HandleRolePanelDelete) + RegisterHandler("rolepanel_remove_", HandleRolePanelRemove) +} + +// HandleRolePanelSelect はロールパネルのセレクトメニューを処理します +func HandleRolePanelSelect(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + customID := i.MessageComponentData().CustomID + selectedRoleIDs := i.MessageComponentData().Values + + // CustomIDからメッセージIDを取得 (rolepanel_select_messageID) + messageID := strings.TrimPrefix(customID, "rolepanel_select_") + + // パネルを取得 + repo := repository.NewRolePanelRepository(ctx.DB) + panel, err := repo.GetByMessageID(messageID) + if err != nil || panel == nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "パネルが見つかりませんでした。", + Color: config.Colors.Error, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + // パネルに登録されている全ロールIDを取得 + panelRoleIDs := make(map[string]bool) + for _, opt := range panel.Options { + panelRoleIDs[opt.RoleID] = true + } + + // 選択されたロールをマップに変換 + selectedMap := make(map[string]bool) + for _, roleID := range selectedRoleIDs { + selectedMap[roleID] = true + } + + var addedRoles []string + var removedRoles []string + + // ユーザーの現在のロールを確認 + member, err := s.GuildMember(i.GuildID, i.Member.User.ID) + if err != nil { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "メンバー情報の取得に失敗しました。", + Color: config.Colors.Error, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + + currentRoles := make(map[string]bool) + for _, roleID := range member.Roles { + currentRoles[roleID] = true + } + + // ロールの追加・削除を処理 + for roleID := range panelRoleIDs { + hasRole := currentRoles[roleID] + shouldHaveRole := selectedMap[roleID] + + if shouldHaveRole && !hasRole { + // ロールを追加 + err := s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID) + if err == nil { + addedRoles = append(addedRoles, roleID) + } + } else if !shouldHaveRole && hasRole { + // ロールを削除 + err := s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID) + if err == nil { + removedRoles = append(removedRoles, roleID) + } + } + } + + // 更新後のユーザーのロール状態を計算 + newRoles := make(map[string]bool) + for roleID := range currentRoles { + newRoles[roleID] = true + } + for _, roleID := range addedRoles { + newRoles[roleID] = true + } + for _, roleID := range removedRoles { + delete(newRoles, roleID) + } + + // 結果メッセージを作成 + var description string + if len(addedRoles) == 0 && len(removedRoles) == 0 { + description = "ロールに変更はありませんでした。" + } else { + if len(addedRoles) > 0 { + description += "**追加されたロール:**\n" + for _, roleID := range addedRoles { + description += fmt.Sprintf("- <@&%s>\n", roleID) + } + } + if len(removedRoles) > 0 { + if len(addedRoles) > 0 { + description += "\n" + } + description += "**削除されたロール:**\n" + for _, roleID := range removedRoles { + description += fmt.Sprintf("- <@&%s>\n", roleID) + } + } + } + + // 現在のロール状態を表示 + description += "\n**現在のロール:**\n" + hasAnyRole := false + for _, opt := range panel.Options { + if newRoles[opt.RoleID] { + description += fmt.Sprintf("- <@&%s>\n", opt.RoleID) + hasAnyRole = true + } + } + if !hasAnyRole { + description += "なし\n" + } + + // ユーザー専用のセレクトメニューを作成(現在のロールがデフォルト選択) + var selectOptions []discordgo.SelectMenuOption + for _, opt := range panel.Options { + option := discordgo.SelectMenuOption{ + Label: opt.Label, + Value: opt.RoleID, + Description: opt.Description, + Default: newRoles[opt.RoleID], + } + if opt.Emoji != "" { + option.Emoji = &discordgo.ComponentEmoji{ + Name: opt.Emoji, + } + } + selectOptions = append(selectOptions, option) + } + + var components []discordgo.MessageComponent + if len(selectOptions) > 0 { + components = []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "rolepanel_select_" + panel.MessageID, + Placeholder: "ロールを選択...", + MinValues: intPtr(0), + MaxValues: len(selectOptions), + Options: selectOptions, + }, + }, + }, + } + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールを更新しました", + Description: description, + Color: config.Colors.Success, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: components, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} + +func intPtr(i int) *int { + return &i +} + +// HandleRolePanelAdd はロール追加用のパネル選択を処理します +func HandleRolePanelAdd(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + customID := i.MessageComponentData().CustomID + values := i.MessageComponentData().Values + + if len(values) == 0 { + return + } + + messageID := values[0] + + // CustomIDからロール情報をデコード (rolepanel_add_roleID|label|description|emoji) + parts := strings.TrimPrefix(customID, "rolepanel_add_") + data := strings.Split(parts, "|") + if len(data) < 2 { + return + } + + roleID := data[0] + label := data[1] + description := "" + emoji := "" + if len(data) > 2 { + description = data[2] + } + if len(data) > 3 { + emoji = data[3] + } + + repo := repository.NewRolePanelRepository(ctx.DB) + + // パネルを取得 + 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 + } + + // オプション数の上限チェック (Discord制限: 25) + if len(panel.Options) >= 25 { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "このパネルには最大25個までのロールしか追加できません。", + Color: config.Colors.Error, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{}, + }, + }) + return + } + + // 重複チェック + for _, opt := range panel.Options { + if opt.RoleID == roleID { + _ = 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 + } + } + + // オプションを追加 + option := &model.RolePanelOption{ + RolePanelID: panel.ID, + RoleID: roleID, + Label: label, + Description: description, + Emoji: emoji, + } + + err = repo.AddOption(option) + if 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 + } + + // パネルを再取得してメッセージを更新 + panel, _ = repo.GetByMessageID(messageID) + UpdatePanelMessage(s, panel, config) + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールを追加しました", + Description: fmt.Sprintf("ロール <@&%s> を **%s** に追加しました。", roleID, panel.Title), + Color: config.Colors.Success, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{}, + }, + }) +} + +// HandleRolePanelDelete はパネル削除用のセレクトを処理します +func HandleRolePanelDelete(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + values := i.MessageComponentData().Values + + if len(values) == 0 { + return + } + + messageID := values[0] + repo := repository.NewRolePanelRepository(ctx.DB) + + // パネルを取得 + 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 + } + + panelTitle := panel.Title + + // メッセージを削除 + _ = s.ChannelMessageDelete(panel.ChannelID, panel.MessageID) + + // データベースから削除 + err = repo.DeleteByID(panel.ID) + if 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 + } + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールパネルを削除しました", + Description: fmt.Sprintf("**%s** を削除しました。", panelTitle), + Color: config.Colors.Success, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{}, + }, + }) +} + +// HandleRolePanelRemove はロール削除用のパネル選択を処理します +func HandleRolePanelRemove(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { + config := ctx.Config + customID := i.MessageComponentData().CustomID + values := i.MessageComponentData().Values + + if len(values) == 0 { + return + } + + messageID := values[0] + roleID := strings.TrimPrefix(customID, "rolepanel_remove_") + + repo := repository.NewRolePanelRepository(ctx.DB) + + // パネルを取得 + 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 + } + + // ロールを探す + var optionID uint + found := false + for _, opt := range panel.Options { + if opt.RoleID == roleID { + optionID = opt.ID + found = true + break + } + } + + if !found { + _ = 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 + } + + // オプションを削除 + err = repo.DeleteOptionByID(optionID) + if 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 + } + + // パネルを再取得してメッセージを更新 + panel, _ = repo.GetByMessageID(messageID) + UpdatePanelMessage(s, panel, config) + + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseUpdateMessage, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "ロールを削除しました", + Description: fmt.Sprintf("ロール <@&%s> を **%s** から削除しました。", roleID, panel.Title), + Color: config.Colors.Success, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Components: []discordgo.MessageComponent{}, + }, + }) +} + +// UpdatePanelMessage はパネルのメッセージを更新します +func UpdatePanelMessage(s *discordgo.Session, panel *model.RolePanel, config *internal.Config) { + embed := &discordgo.MessageEmbed{ + Title: panel.Title, + Description: panel.Description, + Color: config.Colors.Primary, + Footer: &discordgo.MessageEmbedFooter{ + Text: "ロールを選択してください", + }, + Timestamp: time.Now().Format(time.RFC3339), + } + + var selectOptions []discordgo.SelectMenuOption + for _, opt := range panel.Options { + option := discordgo.SelectMenuOption{ + Label: opt.Label, + Value: opt.RoleID, + Description: opt.Description, + } + if opt.Emoji != "" { + option.Emoji = &discordgo.ComponentEmoji{ + Name: opt.Emoji, + } + } + selectOptions = append(selectOptions, option) + } + + var components []discordgo.MessageComponent + if len(selectOptions) > 0 { + components = []discordgo.MessageComponent{ + discordgo.ActionsRow{ + Components: []discordgo.MessageComponent{ + discordgo.SelectMenu{ + CustomID: "rolepanel_select_" + panel.MessageID, + Placeholder: "ロールを選択...", + MinValues: intPtr(0), + MaxValues: len(selectOptions), + Options: selectOptions, + }, + }, + }, + } + } + + _, _ = s.ChannelMessageEditComplex(&discordgo.MessageEdit{ + Channel: panel.ChannelID, + ID: panel.MessageID, + Embeds: &[]*discordgo.MessageEmbed{embed}, + Components: &components, + }) +} diff --git a/src/internal/db/db.go b/src/internal/db/db.go index f2ffc657..dcb40e70 100644 --- a/src/internal/db/db.go +++ b/src/internal/db/db.go @@ -26,6 +26,8 @@ func SetupDB(db *gorm.DB) error { &model.TTSConnection{}, &model.TTSPersonalSetting{}, &model.TTSDictionary{}, + &model.RolePanel{}, + &model.RolePanelOption{}, ) return err } diff --git a/src/internal/model/rolepanel.go b/src/internal/model/rolepanel.go new file mode 100644 index 00000000..6cf38d94 --- /dev/null +++ b/src/internal/model/rolepanel.go @@ -0,0 +1,28 @@ +package model + +import "time" + +// RolePanel セレクトメニュー式ロールパネルのモデル +type RolePanel struct { + ID uint `gorm:"primaryKey;autoIncrement"` + GuildID string `gorm:"size:255;not null;index"` + ChannelID string `gorm:"size:255;not null"` + MessageID string `gorm:"size:255;not null;uniqueIndex"` + Title string `gorm:"size:255;not null"` + Description string `gorm:"size:1000"` + Options []*RolePanelOption `gorm:"foreignKey:RolePanelID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` + CreatedAt time.Time + UpdatedAt time.Time +} + +// RolePanelOption ロールパネルの選択肢 +type RolePanelOption struct { + ID uint `gorm:"primaryKey;autoIncrement"` + RolePanelID uint `gorm:"not null;index"` + RoleID string `gorm:"size:255;not null"` + Label string `gorm:"size:100;not null"` + Description string `gorm:"size:100"` + Emoji string `gorm:"size:255"` + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/src/internal/repository/rolepanel.go b/src/internal/repository/rolepanel.go new file mode 100644 index 00000000..7fb09c33 --- /dev/null +++ b/src/internal/repository/rolepanel.go @@ -0,0 +1,128 @@ +package repository + +import ( + "errors" + + "gorm.io/gorm" + + "unibot/internal/model" +) + +type RolePanelRepository struct { + db *gorm.DB +} + +// 新しいRolePanelリポジトリを作成する +func NewRolePanelRepository(db *gorm.DB) *RolePanelRepository { + return &RolePanelRepository{db: db} +} + +/* ---------------------- CRUD Methods ------------------ */ + +// ロールパネルを作成する関数 +func (r *RolePanelRepository) Create(panel *model.RolePanel) error { + return r.db.Create(panel).Error +} + +// ロールパネルをIDで取得する関数 +func (r *RolePanelRepository) GetByID(id uint) (*model.RolePanel, error) { + var panel model.RolePanel + err := r.db.Preload("Options").First(&panel, id).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return &panel, err +} + +// ロールパネルをメッセージIDで取得する関数 +func (r *RolePanelRepository) GetByMessageID(messageID string) (*model.RolePanel, error) { + var panel model.RolePanel + err := r.db.Preload("Options").First(&panel, "message_id = ?", messageID).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return &panel, err +} + +// ロールパネルをギルドIDで全て取得する関数 +func (r *RolePanelRepository) ListByGuild(guildID string) ([]*model.RolePanel, error) { + var panels []*model.RolePanel + err := r.db.Preload("Options").Where("guild_id = ?", guildID).Order("created_at ASC").Find(&panels).Error + return panels, err +} + +// ロールパネルをチャンネルIDで全て取得する関数 +func (r *RolePanelRepository) ListByChannel(channelID string) ([]*model.RolePanel, error) { + var panels []*model.RolePanel + err := r.db.Preload("Options").Where("channel_id = ?", channelID).Order("created_at ASC").Find(&panels).Error + return panels, err +} + +// 全てのロールパネルを取得する関数 +func (r *RolePanelRepository) List() ([]*model.RolePanel, error) { + var panels []*model.RolePanel + err := r.db.Preload("Options").Find(&panels).Error + return panels, err +} + +// ロールパネルを更新する関数 +func (r *RolePanelRepository) Update(panel *model.RolePanel) error { + return r.db.Save(panel).Error +} + +// ロールパネルをIDで削除する関数 +func (r *RolePanelRepository) DeleteByID(id uint) error { + return r.db.Delete(&model.RolePanel{}, id).Error +} + +// ロールパネルをメッセージIDで削除する関数 +func (r *RolePanelRepository) DeleteByMessageID(messageID string) error { + return r.db.Delete(&model.RolePanel{}, "message_id = ?", messageID).Error +} + +// ロールパネルをギルドIDで削除する関数 +func (r *RolePanelRepository) DeleteByGuild(guildID string) error { + return r.db.Delete(&model.RolePanel{}, "guild_id = ?", guildID).Error +} + +/* ---------------------- Option Methods ------------------ */ + +// ロールパネルにオプションを追加する関数 +func (r *RolePanelRepository) AddOption(option *model.RolePanelOption) error { + return r.db.Create(option).Error +} + +// オプションをIDで取得する関数 +func (r *RolePanelRepository) GetOptionByID(id uint) (*model.RolePanelOption, error) { + var option model.RolePanelOption + err := r.db.First(&option, id).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return &option, err +} + +// オプションをロールパネルIDで取得する関数 +func (r *RolePanelRepository) ListOptionsByPanelID(panelID uint) ([]*model.RolePanelOption, error) { + var options []*model.RolePanelOption + err := r.db.Where("role_panel_id = ?", panelID).Order("created_at ASC").Find(&options).Error + return options, err +} + +// オプションを更新する関数 +func (r *RolePanelRepository) UpdateOption(option *model.RolePanelOption) error { + return r.db.Save(option).Error +} + +// オプションをIDで削除する関数 +func (r *RolePanelRepository) DeleteOptionByID(id uint) error { + return r.db.Delete(&model.RolePanelOption{}, id).Error +} + +// オプションをロールパネルIDで全て削除する関数 +func (r *RolePanelRepository) DeleteOptionsByPanelID(panelID uint) error { + return r.db.Delete(&model.RolePanelOption{}, "role_panel_id = ?", panelID).Error +} From d7a65418686e0b22056d82e850887b4754f93059 Mon Sep 17 00:00:00 2001 From: ysmreg <33682244+ysmreg@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:39:54 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=83=8F?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com> --- src/internal/bot/messageComponent/rolepanel.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/internal/bot/messageComponent/rolepanel.go b/src/internal/bot/messageComponent/rolepanel.go index feb2e5f6..c1fde80d 100644 --- a/src/internal/bot/messageComponent/rolepanel.go +++ b/src/internal/bot/messageComponent/rolepanel.go @@ -96,15 +96,19 @@ func HandleRolePanelSelect(ctx *internal.BotContext, s *discordgo.Session, i *di if shouldHaveRole && !hasRole { // ロールを追加 err := s.GuildMemberRoleAdd(i.GuildID, i.Member.User.ID, roleID) - if err == nil { - addedRoles = append(addedRoles, roleID) + if 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) + continue } + addedRoles = append(addedRoles, roleID) } else if !shouldHaveRole && hasRole { // ロールを削除 err := s.GuildMemberRoleRemove(i.GuildID, i.Member.User.ID, roleID) - if err == nil { - removedRoles = append(removedRoles, roleID) + if err != nil { + fmt.Printf("failed to remove role %s from user %s in guild %s: %v\n", roleID, i.Member.User.ID, i.GuildID, err) + continue } + removedRoles = append(removedRoles, roleID) } } From 8bef316657c21cf7e9c458179765b7f9e36e2dd6 Mon Sep 17 00:00:00 2001 From: ysmreg <33682244+ysmreg@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:40:32 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Length=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com> --- src/internal/bot/command/server_management/rolepanel/create.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/internal/bot/command/server_management/rolepanel/create.go b/src/internal/bot/command/server_management/rolepanel/create.go index 9fe853c4..9a3c8cd9 100644 --- a/src/internal/bot/command/server_management/rolepanel/create.go +++ b/src/internal/bot/command/server_management/rolepanel/create.go @@ -21,12 +21,14 @@ func LoadCreateCommandContext() *discordgo.ApplicationCommandOption { Name: "title", Description: "パネルのタイトル", Required: true, + MaxLength: 256, }, { Type: discordgo.ApplicationCommandOptionString, Name: "description", Description: "パネルの説明", Required: false, + MaxLength: 4096, }, }, } From 4ec078cc1701599e2926263929498a0760b8dba6 Mon Sep 17 00:00:00 2001 From: ysmreg <33682244+ysmreg@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:40:54 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Length=E3=82=92=E8=A8=AD=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: ysmreg <33682244+ysmreg@users.noreply.github.com> --- src/internal/bot/command/server_management/rolepanel/add.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/internal/bot/command/server_management/rolepanel/add.go b/src/internal/bot/command/server_management/rolepanel/add.go index 94488798..f3429694 100644 --- a/src/internal/bot/command/server_management/rolepanel/add.go +++ b/src/internal/bot/command/server_management/rolepanel/add.go @@ -26,18 +26,21 @@ func LoadAddCommandContext() *discordgo.ApplicationCommandOption { Name: "label", Description: "セレクトメニューに表示するラベル", Required: true, + MaxLength: 100, }, { Type: discordgo.ApplicationCommandOptionString, Name: "description", Description: "ロールの説明", Required: false, + MaxLength: 100, }, { Type: discordgo.ApplicationCommandOptionString, Name: "emoji", Description: "絵文字 (例: 🎮)", Required: false, + MaxLength: 100, }, }, } From 4c69e2d80f04f283c06719c57fdb61ceb85c6e41 Mon Sep 17 00:00:00 2001 From: ysmreg <33682244+ysmreg@users.noreply.github.com> Date: Sat, 24 Jan 2026 18:47:26 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E6=A8=A9=E9=99=90=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=92=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bot/messageComponent/rolepanel.go | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) diff --git a/src/internal/bot/messageComponent/rolepanel.go b/src/internal/bot/messageComponent/rolepanel.go index c1fde80d..a10bb0fb 100644 --- a/src/internal/bot/messageComponent/rolepanel.go +++ b/src/internal/bot/messageComponent/rolepanel.go @@ -21,6 +21,26 @@ func init() { // HandleRolePanelSelect はロールパネルのセレクトメニューを処理します func HandleRolePanelSelect(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { config := ctx.Config + + // Guildチェック + if i.GuildID == "" { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "このコマンドはサーバー内でのみ使用できます。", + Color: config.Colors.Error, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + customID := i.MessageComponentData().CustomID selectedRoleIDs := i.MessageComponentData().Values @@ -48,6 +68,25 @@ func HandleRolePanelSelect(ctx *internal.BotContext, s *discordgo.Session, i *di return } + // GuildID一致チェック + if panel.GuildID != i.GuildID { + _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{ + { + Title: "エラー", + Description: "このパネルは別のサーバーのものです。", + Color: config.Colors.Error, + Timestamp: time.Now().Format(time.RFC3339), + }, + }, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) + return + } + // パネルに登録されている全ロールIDを取得 panelRoleIDs := make(map[string]bool) for _, opt := range panel.Options { @@ -217,6 +256,45 @@ func intPtr(i int) *int { // HandleRolePanelAdd はロール追加用のパネル選択を処理します func HandleRolePanelAdd(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { config := ctx.Config + + // Guildチェック + if i.GuildID == "" { + _ = 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 i.Member.Permissions&discordgo.PermissionManageRoles == 0 { + _ = 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 + } + customID := i.MessageComponentData().CustomID values := i.MessageComponentData().Values @@ -266,6 +344,25 @@ func HandleRolePanelAdd(ctx *internal.BotContext, s *discordgo.Session, i *disco return } + // GuildID一致チェック + if panel.GuildID != i.GuildID { + _ = 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 + } + // オプション数の上限チェック (Discord制限: 25) if len(panel.Options) >= 25 { _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ @@ -357,6 +454,45 @@ func HandleRolePanelAdd(ctx *internal.BotContext, s *discordgo.Session, i *disco // HandleRolePanelDelete はパネル削除用のセレクトを処理します func HandleRolePanelDelete(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { config := ctx.Config + + // Guildチェック + if i.GuildID == "" { + _ = 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 i.Member.Permissions&discordgo.PermissionManageRoles == 0 { + _ = 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 + } + values := i.MessageComponentData().Values if len(values) == 0 { @@ -386,6 +522,25 @@ func HandleRolePanelDelete(ctx *internal.BotContext, s *discordgo.Session, i *di return } + // GuildID一致チェック + if panel.GuildID != i.GuildID { + _ = 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 + } + panelTitle := panel.Title // メッセージを削除 @@ -430,6 +585,45 @@ func HandleRolePanelDelete(ctx *internal.BotContext, s *discordgo.Session, i *di // HandleRolePanelRemove はロール削除用のパネル選択を処理します func HandleRolePanelRemove(ctx *internal.BotContext, s *discordgo.Session, i *discordgo.InteractionCreate) { config := ctx.Config + + // Guildチェック + if i.GuildID == "" { + _ = 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 i.Member.Permissions&discordgo.PermissionManageRoles == 0 { + _ = 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 + } + customID := i.MessageComponentData().CustomID values := i.MessageComponentData().Values @@ -462,6 +656,25 @@ func HandleRolePanelRemove(ctx *internal.BotContext, s *discordgo.Session, i *di return } + // GuildID一致チェック + if panel.GuildID != i.GuildID { + _ = 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 + } + // ロールを探す var optionID uint found := false