This commit is contained in:
2025-04-07 16:16:27 -04:00
parent e34ea79572
commit fc3588b1d0
3 changed files with 191 additions and 36 deletions
+158 -25
View File
@@ -1,7 +1,10 @@
package commands package commands
import ( import (
"encoding/json"
"fmt" "fmt"
"log"
"os"
"strings" "strings"
helpers "ion606_bot/Bot/Helpers" helpers "ion606_bot/Bot/Helpers"
@@ -9,17 +12,50 @@ import (
"github.com/bwmarrin/discordgo" "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. // Convert the manage roles permission into an int64 pointer for command registration.
func manageRolesPerm() *int64 { func manageRolesPerm() *int64 {
p := int64(discordgo.PermissionManageRoles) fmt.Println("checking perms")
p := int64(discordgo.PermissionManageRoles | discordgo.PermissionAll)
return &p return &p
} }
// ReactionRoleCommand defines the reaction role slash command with options for count and style. // 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. // Only members with the "Manage Roles" permission may use this command.
var ReactionRoleCommand = &discordgo.ApplicationCommand{ var ReactionRoleCommand = &discordgo.ApplicationCommand{
Name: "reactionrole", Name: "reactionrole",
Description: "Setup a reaction role message using buttons. Provide a number (max 10) for role-button pairs.", Description: "Setup a reaction role message using buttons. Provide a number (max 10) for role-button pairs",
DefaultMemberPermissions: manageRolesPerm(), DefaultMemberPermissions: manageRolesPerm(),
Options: []*discordgo.ApplicationCommandOption{ Options: []*discordgo.ApplicationCommandOption{
{ {
@@ -54,6 +90,12 @@ var ReactionRoleCommand = &discordgo.ApplicationCommand{
}, },
}, },
}, },
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message_id",
Description: "Optional: Message ID of the message to edit and add buttons to.",
Required: false,
},
}, },
} }
@@ -79,6 +121,16 @@ func memberHasManageRoles(s *discordgo.Session, guildID string, member *discordg
if err != nil { if err != nil {
return false, err 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 var perms int64 = 0
for _, roleID := range member.Roles { for _, roleID := range member.Roles {
for _, role := range roles { for _, role := range roles {
@@ -87,13 +139,12 @@ func memberHasManageRoles(s *discordgo.Session, guildID string, member *discordg
} }
} }
} }
return perms&discordgo.PermissionManageRoles != 0, nil
return (perms&discordgo.PermissionManageRoles != 0 || perms&discordgo.PermissionAdministrator != 0), nil
} }
// HandleReactionRole shows a modal with inputs for each role/button pair.
// It verifies that the invoking member has the Manage Roles permission.
func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) { func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Check that the invoking member has the Manage Roles permission. // Ensure the command is used in a guild and member data is present.
if i.GuildID == "" || i.Member == nil { if i.GuildID == "" || i.Member == nil {
helpers.HandleError(s, i, fmt.Errorf("command must be used in a guild")) helpers.HandleError(s, i, fmt.Errorf("command must be used in a guild"))
return return
@@ -103,6 +154,7 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
helpers.HandleError(s, i, fmt.Errorf("failed to check user permissions: %w", err)) helpers.HandleError(s, i, fmt.Errorf("failed to check user permissions: %w", err))
return return
} }
if !hasPerm { if !hasPerm {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
@@ -126,6 +178,15 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
} }
} }
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). // Build modal components: for each pair, add two text inputs (each in its own action row).
modalComponents := []discordgo.MessageComponent{} modalComponents := []discordgo.MessageComponent{}
for idx := 1; idx <= count; idx++ { for idx := 1; idx <= count; idx++ {
@@ -137,7 +198,6 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
Placeholder: "Enter Role ID", Placeholder: "Enter Role ID",
Required: true, Required: true,
} }
// Input for Button Label. // Input for Button Label.
labelInput := discordgo.TextInput{ labelInput := discordgo.TextInput{
CustomID: fmt.Sprintf("role_label_%d", idx), CustomID: fmt.Sprintf("role_label_%d", idx),
@@ -149,17 +209,26 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
modalComponents = append(modalComponents, discordgo.ActionsRow{ modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&roleInput}, Components: []discordgo.MessageComponent{&roleInput},
}) })
modalComponents = append(modalComponents, discordgo.ActionsRow{ modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&labelInput}, Components: []discordgo.MessageComponent{&labelInput},
}) })
} }
// Append the style to the modal's custom ID. // 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{ modalData := &discordgo.InteractionResponseData{
Title: "Reaction Role Setup", Title: "Reaction Role Setup",
// Append the style value after a colon so it is available on submit. CustomID: customID,
CustomID: fmt.Sprintf("reactionrole_modal:%s", style),
Components: modalComponents, Components: modalComponents,
} }
@@ -167,22 +236,37 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
Type: discordgo.InteractionResponseModal, Type: discordgo.InteractionResponseModal,
Data: modalData, Data: modalData,
}) })
if err != nil { if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send modal: %w", err)) 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. // 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) { func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.InteractionCreate) {
// Extract the style from the modal's CustomID. // Extract the style from the modal's CustomID.
customID := i.ModalSubmitData().CustomID customID := i.ModalSubmitData().CustomID
parts := strings.Split(customID, ":") parts := strings.Split(customID, ":")
styleStr := "primary" styleStr := "primary"
if len(parts) == 2 {
styleStr = parts[1] 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) btnStyle := mapStyle(styleStr)
log.Default().Println(targetMsgID, targetChannelID)
data := i.ModalSubmitData() data := i.ModalSubmitData()
// There will be two action rows per pair. // There will be two action rows per pair.
count := len(data.Components) / 2 count := len(data.Components) / 2
@@ -194,7 +278,6 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
var err error var err error
guildRoles, err = s.GuildRoles(i.GuildID) guildRoles, err = s.GuildRoles(i.GuildID)
if err != nil { if err != nil {
// Send an ephemeral error message.
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
@@ -209,15 +292,11 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
// For each pair, extract the Role ID and label. // For each pair, extract the Role ID and label.
for idx := 1; idx <= count; idx++ { for idx := 1; idx <= count; idx++ {
var roleID, label string var roleID, label string
// Iterate over the action rows.
for _, comp := range data.Components { for _, comp := range data.Components {
row, ok := comp.(*discordgo.ActionsRow) row, ok := comp.(*discordgo.ActionsRow)
if !ok { if !ok {
continue continue
} }
for _, innerComp := range row.Components { for _, innerComp := range row.Components {
textInput, ok := innerComp.(*discordgo.TextInput) textInput, ok := innerComp.(*discordgo.TextInput)
if !ok { if !ok {
@@ -231,7 +310,7 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
} }
} }
// Check for cancellation request. // Allow a cancellation action.
if strings.ToLower(roleID) == "cancel" { if strings.ToLower(roleID) == "cancel" {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
@@ -243,7 +322,7 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
return return
} }
// Verify the given role exists in the guild. // Verify that the given role exists.
roleExists := false roleExists := false
if i.GuildID != "" { if i.GuildID != "" {
for _, r := range guildRoles { for _, r := range guildRoles {
@@ -253,7 +332,6 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
} }
} }
} else { } else {
// If not in a guild, we cannot verify role existence.
roleExists = true roleExists = true
} }
@@ -279,7 +357,7 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
} }
} }
// Discord supports up to 5 buttons per row. // Arrange buttons into rows (up to 5 per row).
var components []discordgo.MessageComponent var components []discordgo.MessageComponent
for j := 0; j < len(buttons); j += 5 { for j := 0; j < len(buttons); j += 5 {
end := j + 5 end := j + 5
@@ -292,15 +370,70 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
components = append(components, row) 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{ err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: "Click a button to receive the corresponding role.", Content: "Click a button to receive the corresponding role:",
Components: components, Components: components,
}, },
}) })
if err != nil { if err != nil {
helpers.HandleError(s, i, fmt.Errorf("failed to send reaction role message: %w", err)) 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))
}
} }
} }
+9
View File
@@ -0,0 +1,9 @@
package helpers
import (
"fmt"
)
func CreateMessageLink(guildID, channelID, messageID string) string {
return fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildID, channelID, messageID)
}
+15 -2
View File
@@ -1,7 +1,6 @@
package bot package bot
import ( import (
"fmt"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@@ -36,6 +35,12 @@ func Run() {
log.Fatal("Error creating Discord session: ", err) log.Fatal("Error creating Discord session: ", err)
} }
// Load reaction role target map from file.
err = commands.LoadReactionRoleTarget("reactionRoleTarget.json")
if err != nil {
log.Printf("No reaction role target config loaded: %v", err)
}
// add event handlers for messages and interactions. // add event handlers for messages and interactions.
discord.AddHandler(newMessage) discord.AddHandler(newMessage)
discord.AddHandler(handleInteractionCreate) discord.AddHandler(handleInteractionCreate)
@@ -47,11 +52,19 @@ func Run() {
RegisterCommands(discord, "") RegisterCommands(discord, "")
fmt.Println("Bot running....") log.Println("Bot running....")
// Wait for interrupt signal.
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Notify(c, os.Interrupt)
<-c <-c
// Save reaction role target map to file on shutdown.
err = commands.SaveReactionRoleTarget("reactionRoleTarget.json")
if err != nil {
log.Println("Error saving reaction role target:", err)
}
err = discord.Close() err = discord.Close()
if err != nil { if err != nil {
log.Println("Error closing Discord session: ", err) log.Println("Error closing Discord session: ", err)