Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ go.work.sum
# Scripts included sensitive information
scripts/build.sh
docker-compose.prod.yaml
docker-compose.yaml
2 changes: 2 additions & 0 deletions src/internal/bot/command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -12,4 +13,5 @@ var Commands = []*discordgo.ApplicationCommand{
general.LoadAboutCommandContext(),
general.LoadTtsCommandContext(),
admin.LoadMaintenanceCommandContext(),
server_management.LoadRolepanelCommandContext(),
}
2 changes: 2 additions & 0 deletions src/internal/bot/command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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,
}
65 changes: 65 additions & 0 deletions src/internal/bot/command/server_management/rolepanel.go
Original file line number Diff line number Diff line change
@@ -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,
},
})
}
157 changes: 157 additions & 0 deletions src/internal/bot/command/server_management/rolepanel/add.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
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,
MaxLength: 100,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "description",
Description: "ロールの説明",
Required: false,
MaxLength: 100,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "emoji",
Description: "絵文字 (例: 🎮)",
Required: false,
MaxLength: 100,
},
},
}
}

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)
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

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

Unsafe delimiter usage in CustomID encoding: Using pipe character (|) as delimiter is problematic if user-provided fields (label, description, emoji) contain pipe characters. This could cause parsing errors in HandleRolePanelAdd when the customID is decoded. Consider using URL encoding or a safer encoding scheme for the customID to handle special characters properly.

Copilot uses AI. Check for mistakes.

_ = 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,
},
})
}
Loading