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
import (
"encoding/json"
"fmt"
"log"
"os"
"strings"
helpers "ion606_bot/Bot/Helpers"
@@ -9,17 +12,50 @@ import (
"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 {
p := int64(discordgo.PermissionManageRoles)
fmt.Println("checking perms")
p := int64(discordgo.PermissionManageRoles | discordgo.PermissionAll)
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.
var ReactionRoleCommand = &discordgo.ApplicationCommand{
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(),
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 {
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 {
@@ -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) {
// 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 {
helpers.HandleError(s, i, fmt.Errorf("command must be used in a guild"))
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))
return
}
if !hasPerm {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
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).
modalComponents := []discordgo.MessageComponent{}
for idx := 1; idx <= count; idx++ {
@@ -137,7 +198,6 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
Placeholder: "Enter Role ID",
Required: true,
}
// Input for Button Label.
labelInput := discordgo.TextInput{
CustomID: fmt.Sprintf("role_label_%d", idx),
@@ -149,17 +209,26 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
modalComponents = append(modalComponents, discordgo.ActionsRow{
Components: []discordgo.MessageComponent{&roleInput},
})
modalComponents = append(modalComponents, discordgo.ActionsRow{
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{
Title: "Reaction Role Setup",
// Append the style value after a colon so it is available on submit.
CustomID: fmt.Sprintf("reactionrole_modal:%s", style),
CustomID: customID,
Components: modalComponents,
}
@@ -167,22 +236,37 @@ func HandleReactionRole(s *discordgo.Session, i *discordgo.InteractionCreate) {
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 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) {
// Extract the style from the modal's CustomID.
customID := i.ModalSubmitData().CustomID
parts := strings.Split(customID, ":")
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)
log.Default().Println(targetMsgID, targetChannelID)
data := i.ModalSubmitData()
// There will be two action rows per pair.
count := len(data.Components) / 2
@@ -194,7 +278,6 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
var err error
guildRoles, err = s.GuildRoles(i.GuildID)
if err != nil {
// Send an ephemeral error message.
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
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 idx := 1; idx <= count; idx++ {
var roleID, label string
// Iterate over the action rows.
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 {
@@ -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" {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
@@ -243,7 +322,7 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
return
}
// Verify the given role exists in the guild.
// Verify that the given role exists.
roleExists := false
if i.GuildID != "" {
for _, r := range guildRoles {
@@ -253,7 +332,6 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
}
}
} else {
// If not in a guild, we cannot verify role existence.
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
for j := 0; j < len(buttons); j += 5 {
end := j + 5
@@ -292,15 +370,70 @@ func HandleReactionRoleModalSubmit(s *discordgo.Session, i *discordgo.Interactio
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.",
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))
}
}
}
+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
import (
"fmt"
"log"
"os"
"os/signal"
@@ -36,6 +35,12 @@ func Run() {
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.
discord.AddHandler(newMessage)
discord.AddHandler(handleInteractionCreate)
@@ -47,11 +52,19 @@ func Run() {
RegisterCommands(discord, "")
fmt.Println("Bot running....")
log.Println("Bot running....")
// Wait for interrupt signal.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-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()
if err != nil {
log.Println("Error closing Discord session: ", err)