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 := 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)) } }