added reactionrole command

This commit is contained in:
2025-04-07 12:27:19 -04:00
parent 6006c78e5b
commit e6a6d0a5f5
4 changed files with 335 additions and 16 deletions
+298
View File
@@ -0,0 +1,298 @@
package commands
import (
"fmt"
"strings"
helpers "ion606_bot/Bot/Helpers"
"github.com/bwmarrin/discordgo"
)
// ReactionRoleCommand defines the reaction role slash command with options for count and style.
var ReactionRoleCommand = &discordgo.ApplicationCommand{
Name: "reactionrole",
Description: "Setup a reaction role message using buttons. Provide a number (max 10) for role-button pairs.",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "count",
Description: "Number of role-button pairs to setup (max 10)",
Required: true,
MinValue: func(v float64) *float64 { return &v }(1),
MaxValue: func(v float64) float64 { return v }(10),
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "style",
Description: "Button style",
Required: false,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Blue",
Value: "primary",
},
{
Name: "Gray",
Value: "secondary",
},
{
Name: "Green",
Value: "success",
},
{
Name: "Red",
Value: "danger",
},
},
},
},
}
// mapStyle converts a style string to a discordgo.ButtonStyle.
func mapStyle(style string) discordgo.ButtonStyle {
switch strings.ToLower(style) {
case "primary", "blue":
return discordgo.PrimaryButton
case "secondary", "gray":
return discordgo.SecondaryButton
case "success", "green":
return discordgo.SuccessButton
case "danger", "red":
return discordgo.DangerButton
default:
return discordgo.PrimaryButton
}
}
// HandleReactionRole shows a modal with inputs for each role/button pair.
// It appends the chosen style to the modal's custom ID so that it is available during modal submit.
func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
options := i.ApplicationCommandData().Options
count := int(options[0].IntValue())
// Determine style based on the additional option; default to primary.
style := "primary"
if len(options) > 1 {
styleOpt := options[1]
if val := styleOpt.StringValue(); val != "" {
style = val
}
}
// Build modal components: for each pair, add two text inputs (each in its own action row).
modalComponents := []discordgo.MessageComponent{}
for idx := 1; idx <= count; idx++ {
// Input for Role ID.
roleInput := discordgo.TextInput{
CustomID: fmt.Sprintf("role_id_%d", idx),
Label: fmt.Sprintf("Role ID for pair %d", idx),
Style: discordgo.TextInputShort,
Placeholder: "Enter Role ID",
Required: true,
}
// Input for Button Label.
labelInput := discordgo.TextInput{
CustomID: fmt.Sprintf("role_label_%d", idx),
Label: fmt.Sprintf("Button label for pair %d", idx),
Style: discordgo.TextInputShort,
Placeholder: "Button Text",
Required: true,
}
modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&roleInput},
})
modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&labelInput},
})
}
// Append the style to the modal's custom ID.
modalData := &discordgo.InteractionResponseData{
Title: "Reaction Role Setup",
// Append the style value after a colon so it is available on submit.
CustomID: fmt.Sprintf("reactionrole_modal:%s", style),
Components: modalComponents,
}
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseModal,
Data: modalData,
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send modal: %w", err))
}
}
// HandleReactionRoleModalSubmit processes the modal submission and uses the style passed via the modal custom ID.
func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Extract the style from the modal's CustomID.
customID := i.ModalSubmitData().CustomID
parts := strings.Split(customID, ":")
styleStr := "primary"
if len(parts) == 2 {
styleStr = parts[1]
}
btnStyle := mapStyle(styleStr)
data := i.ModalSubmitData()
// There will be two action rows per pair.
count := len(data.Components) / 2
var buttons []discordgo.MessageComponent
// Retrieve guild roles to verify existence.
var guildRoles []*discordgo.Role
if i.GuildID != "" {
var err error
guildRoles, err = s.GuildRoles(i.GuildID)
if err != nil {
// Send an ephemeral error message.
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error fetching guild roles.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
}
// For each pair, extract the Role ID and label.
for idx := 1; idx <= count; idx++ {
var roleID, label string
// Iterate over the action rows.
for _, comp := range data.Components {
row, ok := comp.(*discordgo.ActionsRow)
if !ok {
continue
}
for _, innerComp := range row.Components {
textInput, ok := innerComp.(*discordgo.TextInput)
if !ok {
continue
}
if textInput.CustomID == fmt.Sprintf("role_id_%d", idx) {
roleID = textInput.Value
} else if textInput.CustomID == fmt.Sprintf("role_label_%d", idx) {
label = textInput.Value
}
}
}
// Check for cancellation request.
if strings.ToLower(roleID) == "cancel" {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Reaction role setup has been cancelled.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Verify the given role exists in the guild.
roleExists := false
if i.GuildID != "" {
for _, r := range guildRoles {
if r.ID == roleID {
roleExists = true
break
}
}
} else {
// If not in a guild, we cannot verify role existence.
roleExists = true
}
if !roleExists {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Error: Role with ID `%s` does not exist in this server.", roleID),
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
// Create the button if valid.
if roleID != "" && label != "" {
btn := discordgo.Button{
Label: label,
Style: btnStyle,
CustomID: "rr:" + roleID,
}
buttons = append(buttons, btn)
}
}
// Discord supports up to 5 buttons per row.
var components []discordgo.MessageComponent
for j := 0; j < len(buttons); j += 5 {
end := j + 5
if end > len(buttons) {
end = len(buttons)
}
row := discordgo.ActionsRow{
Components: buttons[j:end],
}
components = append(components, row)
}
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Click a button to receive the corresponding role.",
Components: components,
},
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send reaction role message: %w", err))
}
}
// HandleReactionRoleButton processes button clicks for reaction roles.
func HandleReactionRoleButton(s *discordgo.Session, i *discordgo.InteractionCreate) {
customID := i.MessageComponentData().CustomID
// Expect customID in the format "rr:<roleID>"
roleID := strings.TrimPrefix(customID, "rr:")
if roleID == "" {
helpers.HandleError(s, i, fmt.Errorf("invalid role ID"))
return
}
if i.GuildID == "" {
helpers.HandleError(s, i, fmt.Errorf("cannot assign roles in DMs"))
return
}
member := i.Member
if member == nil {
helpers.HandleError(s, i, fmt.Errorf("member not found"))
return
}
// Assign the role.
if err := s.GuildMemberRoleAdd(i.GuildID, member.User.ID, roleID); err != nil {
helpers.HandleError(s, i, fmt.Errorf("error assigning role: %w", err))
return
}
// Respond to acknowledge the action.
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Role successfully assigned!",
Flags: discordgo.MessageFlagsEphemeral,
},
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to respond to button interaction: %w", err))
}
}
+35 -15
View File
@@ -5,6 +5,7 @@ import (
"log"
"os"
"os/signal"
"strings"
commands "ion606_bot/Bot/Commands"
@@ -15,15 +16,16 @@ var BotToken string
// commandHandlers maps command names to their interaction handler functions.
var commandHandlers = map[string]func(*discordgo.Session, *discordgo.InteractionCreate){
"meow": commands.HandleMeow,
"purr": commands.HandlePurr,
"boop": commands.HandleBoop,
"hug": commands.HandleHug,
"cuddle": commands.HandleCuddle,
"snuggle": commands.HandleSnuggle,
"catfact": commands.HandleCatfact,
"animalgif": commands.HandleAnimalGif,
"action": commands.HandleAction,
"meow": commands.HandleMeow,
"purr": commands.HandlePurr,
"boop": commands.HandleBoop,
"hug": commands.HandleHug,
"cuddle": commands.HandleCuddle,
"snuggle": commands.HandleSnuggle,
"catfact": commands.HandleCatfact,
"animalgif": commands.HandleAnimalGif,
"action": commands.HandleAction,
"reactionrole": commands.HandleReactionRole,
}
// Run starts the Discord bot session and listens for both message and slash command events.
@@ -61,14 +63,32 @@ func newMessage(s *discordgo.Session, m *discordgo.MessageCreate) {
if m.Author.ID == s.State.User.ID {
return
}
}
func handleInteractionCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
name := i.ApplicationCommandData().Name
if handler, found := commandHandlers[name]; found {
handler(s, i)
} else {
log.Printf("No handler found for command: %v", name)
// Check for modal submissions first.
if i.Type == discordgo.InteractionModalSubmit {
if strings.HasPrefix(i.ModalSubmitData().CustomID, "reactionrole_modal:") {
commands.HandleReactionRoleModalSubmit(s, i)
return
}
}
// Then check for message components (buttons).
if i.Type == discordgo.InteractionMessageComponent {
if strings.HasPrefix(i.MessageComponentData().CustomID, "rr:") {
commands.HandleReactionRoleButton(s, i)
return
}
}
// Finally, process application commands.
if i.Type == discordgo.InteractionApplicationCommand {
name := i.ApplicationCommandData().Name
if handler, found := commandHandlers[name]; found {
handler(s, i)
} else {
log.Printf("No handler found for command: %v", name)
}
}
}
+1
View File
@@ -19,6 +19,7 @@ var commandsList = []*discordgo.ApplicationCommand{
Commands.CatfactCommand,
Commands.AnimalGifCommand,
Commands.ActionCommand,
Commands.ReactionRoleCommand,
}
func RegisterCommands(s *discordgo.Session, guildID string) {
+1 -1
View File
@@ -1,7 +1,7 @@
build:
docker build -t ion606-bot .
run:
run: build
docker run --rm -d --env-file .env ion606-bot
stop: