Files
self-bot/Bot/Commands/reactionrole.go
T
2025-04-08 12:10:48 -04:00

548 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package commands
import (
"encoding/json"
"fmt"
"os"
"strings"
helpers "ion606_bot/Bot/Helpers"
"github.com/bwmarrin/discordgo"
)
// reactionRoleTarget maps an interactions ID to its target message ID
var reactionRoleTarget = make(map[string]string)
// SaveReactionRoleTarget saves the reactionRoleTarget map to a JSON file.
func SaveReactionRoleTarget(filename string) error {
data, err := json.Marshal(reactionRoleTarget)
if err != nil {
return fmt.Errorf("failed to marshal reactionRoleTarget: %w", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write reactionRoleTarget file: %w", err)
}
return nil
}
// LoadReactionRoleTarget loads the reactionRoleTarget map from a JSON file.
func LoadReactionRoleTarget(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read reactionRoleTarget file: %w", err)
}
if err := json.Unmarshal(data, &reactionRoleTarget); err != nil {
return fmt.Errorf("failed to unmarshal reactionRoleTarget: %w", err)
}
return nil
}
// 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
// Store the target message ID keyed by the user's ID.
reactionRoleTarget[i.Member.User.ID] = targetMsgID
}
}
// 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))
}
}