525 lines
15 KiB
Go
525 lines
15 KiB
Go
package commands
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
|
||
helpers "ion606_bot/Bot/Helpers"
|
||
|
||
"github.com/bwmarrin/discordgo"
|
||
)
|
||
|
||
// reactionRoleTarget maps an interaction’s 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 a number (max 10) for role-button pairs",
|
||
DefaultMemberPermissions: manageRolesPerm(),
|
||
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",
|
||
},
|
||
},
|
||
},
|
||
{
|
||
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) {
|
||
// Ensure the command is used in a guild and member data is present.
|
||
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
|
||
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
|
||
}
|
||
}
|
||
|
||
targetMsgID := ""
|
||
if len(options) > 2 {
|
||
opt := options[2]
|
||
if val := opt.StringValue(); val != "" {
|
||
targetMsgID = val
|
||
reactionRoleTarget[i.Member.User.ID] = targetMsgID
|
||
}
|
||
}
|
||
|
||
// 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},
|
||
})
|
||
}
|
||
|
||
// 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)
|
||
|
||
log.Default().Println(targetMsgID, targetChannelID)
|
||
|
||
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 {
|
||
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
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 := j + 5
|
||
if end > len(buttons) {
|
||
end = 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))
|
||
}
|
||
}
|