512 lines
14 KiB
Go
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))
|
|
}
|
|
}
|