what
This commit is contained in:
+158
-25
@@ -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 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 {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user