Files
self-bot/Bot/Commands/reactionrole.go
T
2025-04-08 15:21:23 -04:00

512 lines
14 KiB
Go

package commands
import (
"fmt"
"github.com/bwmarrin/discordgo"
"strings"
helpers "ion606_bot/Bot/Helpers"
)
// Convert the manage roles permission into an int64 pointer for command registration.
func manageRolesPerm() *int64 {
fmt.Println("checking perms")
p := int64(discordgo.PermissionManageRoles | discordgo.PermissionAll)
return &p
}
// ReactionRoleCommand defines the reaction role slash command with options for count, style, and an optional message ID.
// Only members with the "Manage Roles" permission may use this command.
var ReactionRoleCommand = &discordgo.ApplicationCommand{
Name: "reactionrole",
Description: "Setup a reaction role message using buttons. Provide up to 5 roles.",
DefaultMemberPermissions: manageRolesPerm(),
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionRole, // type 8
Name: "role1",
Description: "The first role",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role2",
Description: "The second role",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role3",
Description: "The third role",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role4",
Description: "The fourth role",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role5",
Description: "The fifth role",
Required: false,
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "style",
Description: "Button style (primary, secondary, success, danger)",
Required: false,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Blue",
Value: "primary",
},
{
Name: "Gray",
Value: "secondary",
},
{
Name: "Green",
Value: "success",
},
{
Name: "Red",
Value: "danger",
},
},
},
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message_id",
Description: "Optional: Message ID of the message to edit and add buttons to.",
Required: false,
},
},
}
// 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
}
}
// memberHasManageRoles checks if the given member has the Manage Roles permission.
func memberHasManageRoles(s *discordgo.Session, guildID string, member *discordgo.Member) (bool, error) {
roles, err := s.GuildRoles(guildID)
if err != nil {
return false, err
}
guild, err := s.Guild(guildID)
if err != nil {
return false, err
}
if member.User.ID == guild.OwnerID {
return true, nil
}
var perms int64 = 0
for _, roleID := range member.Roles {
for _, role := range roles {
if role.ID == roleID {
perms |= int64(role.Permissions)
}
}
}
return (perms&discordgo.PermissionManageRoles != 0 || perms&discordgo.PermissionAdministrator != 0), nil
}
func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Standard guild and permission checks.
if i.GuildID == "" || i.Member == nil {
helpers.HandleError(s, i, fmt.Errorf("command must be used in a guild"))
return
}
hasPerm, err := memberHasManageRoles(s, i.GuildID, i.Member)
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to check user permissions: %w", err))
return
}
if !hasPerm {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error: You need the Manage Roles permission to use this command.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
options := i.ApplicationCommandData().Options
// Collect role IDs from the role options.
var roles []*discordgo.Role
for _, optName := range []string{"role1", "role2", "role3", "role4", "role5"} {
if opt := helpers.GetOption(options, optName); opt != nil {
roles = append(roles, opt.RoleValue(s, i.GuildID))
}
}
if len(roles) == 0 {
helpers.HandleError(s, i, fmt.Errorf("at least one role must be provided"))
return
}
// Determine style; default to primary.
style := "primary"
if opt := helpers.GetOption(options, "style"); opt != nil {
if val := opt.StringValue(); val != "" {
style = val
}
}
// Extract target message ID, if provided.
targetMsgID := ""
if opt := helpers.GetOption(options, "message_id"); opt != nil {
if val := opt.StringValue(); val != "" {
targetMsgID = val
}
}
// Build the modal components: one text input per role for its button label.
modalComponents := []discordgo.MessageComponent{}
for _, role := range roles {
labelInput := discordgo.TextInput{
CustomID: fmt.Sprintf("label_%s", role.ID),
Label: fmt.Sprintf("Button label for role %s", role.Name),
Style: discordgo.TextInputShort,
Placeholder: "Enter Button Label",
Required: true,
}
modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&labelInput},
})
}
// Build a custom ID that contains the style and, if provided, the target message ID.
customID := "reactionrole_modal"
if targetMsgID != "" {
customID += fmt.Sprintf(":%s-%s", i.ChannelID, targetMsgID)
} else {
customID += ":NA"
}
if style != "primary" {
customID += fmt.Sprintf(":%s", style)
}
modalData := &discordgo.InteractionResponseData{
Title: "Reaction Role Setup",
CustomID: customID,
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, uses the style from the modal custom ID,
// and if a target message ID is provided, edits that message to add the buttons.
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"
targetMsgID := ""
targetChannelID := ""
if len(parts) >= 3 {
styleStr = parts[2]
}
if len(parts) >= 2 && parts[1] != "NA" {
spl := strings.Split(parts[1], "-")
targetChannelID = spl[0]
targetMsgID = spl[1]
}
btnStyle := mapStyle(styleStr)
data := i.ModalSubmitData()
// There will be two action rows per pair.
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 {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error fetching guild roles.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
}
// FLAT
var rolearr [][]string
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
}
parts := strings.Split(textInput.CustomID, "_")
if len(parts) < 2 {
continue
}
roleID := parts[1]
rolearr = append(rolearr, []string{textInput.Value, roleID})
}
}
for idx := range len(rolearr) {
var roleID, label string
label = rolearr[idx][0]
roleID = rolearr[idx][1]
// Allow a cancellation action.
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 that the given role exists.
roleExists := false
if i.GuildID != "" {
for _, r := range guildRoles {
if r.ID == roleID {
roleExists = true
break
}
}
} else {
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)
}
}
// Arrange buttons into rows (up to 5 per row).
var components []discordgo.MessageComponent
for j := 0; j < len(buttons); j += 5 {
end := min(j+5, len(buttons))
row := discordgo.ActionsRow{
Components: buttons[j:end],
}
components = append(components, row)
}
// If a target message ID was provided, edit that message. Otherwise, send a new message.
if targetMsgID != "" {
// First, get the existing message to preserve its content.
originalMsg, err := s.ChannelMessage(targetChannelID, targetMsgID)
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to fetch target message: %w", err))
return
}
// Edit the target message with the original content and new buttons.
_, err = s.ChannelMessageEditComplex(&discordgo.MessageEdit{
Channel: targetChannelID,
ID: targetMsgID,
Content: &originalMsg.Content, // Preserve original content.
Components: &components,
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to edit target message: %w", err))
return
}
// Acknowledge the update with an ephemeral confirmation.
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Successfully added reaction roles to [this message](%s).",
helpers.CreateMessageLink(i.GuildID, i.ChannelID, targetMsgID)),
Flags: discordgo.MessageFlagsEphemeral,
},
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send confirmation: %w", err))
}
} else {
// For new messages, first respond to the interaction.
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))
return
}
// Get the message we just sent to provide a reference.
msg, err := s.InteractionResponse(i.Interaction)
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to get sent message: %w", err))
return
}
// Send a follow-up with the message link.
_, err = s.FollowupMessageCreate(i.Interaction, true, &discordgo.WebhookParams{
Content: fmt.Sprintf("Reaction roles setup complete! [Jump to message](%s)",
helpers.CreateMessageLink(i.GuildID, i.ChannelID, msg.ID)),
Flags: discordgo.MessageFlagsEphemeral,
})
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send follow-up: %w", err))
}
}
}
// botHasManageRoles checks if the bot has the Manage Roles permission in the guild.
func botHasManageRoles(s *discordgo.Session, guildID string) (bool, error) {
botMember, err := s.GuildMember(guildID, s.State.User.ID)
if err != nil {
return false, err
}
roles, err := s.GuildRoles(guildID)
if err != nil {
return false, err
}
// Combine permissions from all roles the bot has.
var perms int64 = 0
for _, botRoleID := range botMember.Roles {
for _, role := range roles {
if role.ID == botRoleID {
perms |= int64(role.Permissions)
}
}
}
// discordgo.PermissionManageRoles is 0x10000000 (268435456)
if perms&discordgo.PermissionManageRoles != 0 {
return true, nil
}
return false, nil
}
// HandleReactionRoleButton processes button clicks for reaction roles.
func HandleReactionRoleButton(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Check if the bot has Manage Roles permission.
if i.GuildID == "" {
helpers.HandleError(s, i, fmt.Errorf("cannot assign roles in DMs"))
return
}
hasPerm, err := botHasManageRoles(s, i.GuildID)
if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to check bot permissions: %w", err))
return
}
if !hasPerm {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "*Bot does not have the Manage Roles permission!*",
Flags: discordgo.MessageFlagsEphemeral,
},
})
return
}
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
}
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))
}
}