framework/response.go

447 lines
14 KiB
Go

package framework
import (
"fmt"
"time"
"github.com/bwmarrin/discordgo"
)
// response.go
// This file contains structures and functions that make it easier to create and send response embeds
// ResponseComponents
// Stores the components for response
// allows for functions to add data
type ResponseComponents struct {
Components []discordgo.MessageComponent
SelectMenuOptions []discordgo.SelectMenuOption
}
// Response
// The Response type, can be build and sent to a given guild
type Response struct {
Ctx *Context
Success bool
Loading bool
Ephemeral bool
Embed *discordgo.MessageEmbed
ResponseComponents *ResponseComponents
}
// CreateField
// Create message field to use for an embed
func CreateField(name string, value string, inline bool) *discordgo.MessageEmbedField {
return &discordgo.MessageEmbedField{
Name: name,
Value: value,
Inline: inline,
}
}
// CreateEmbed
// Create an embed
func CreateEmbed(color int, title string, description string, fields []*discordgo.MessageEmbedField) *discordgo.MessageEmbed {
return &discordgo.MessageEmbed{
Title: title,
Description: description,
Color: color,
Fields: fields,
}
}
// CreateComponentFields
// Returns a slice of a Message Component, containing a singular ActionsRow
func CreateComponentFields() []discordgo.MessageComponent {
return []discordgo.MessageComponent{
discordgo.ActionsRow{},
}
}
// NewResponse
// Create a response object for a guild, which starts off as an empty Embed which will have fields added to it
// The response starts with some "auditing" information
// The embed will be finalized in .Send()
func NewResponse(ctx *Context, messageComponents bool, ephemeral bool) *Response {
r := &Response{
Ctx: ctx,
Embed: CreateEmbed(0, "", "", nil),
ResponseComponents: &ResponseComponents{
Components: nil,
SelectMenuOptions: nil,
},
Loading: ctx.Cmd.IsTyping,
Ephemeral: ephemeral,
}
if messageComponents {
r.ResponseComponents.Components = CreateComponentFields()
r.ResponseComponents.SelectMenuOptions = []discordgo.SelectMenuOption{}
}
if r.Loading && ctx.Interaction != nil {
if ephemeral {
_ = Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
// Ephemeral is type 64 don't ask why
Flags: 1 << 6,
},
})
} else {
_ = Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseDeferredChannelMessageWithSource,
})
}
}
// If the command context is not empty, append the command
if ctx.Cmd.Trigger != "" {
// Get the command used as a string, and all interpreted arguments, so it can be a part of the output
commandUsed := ""
if r.Ctx.Cmd.IsChild {
commandUsed = fmt.Sprintf("%s%s %s", r.Ctx.Guild.Info.Prefix, r.Ctx.Cmd.ParentID, r.Ctx.Cmd.Trigger)
} else {
commandUsed = r.Ctx.Guild.Info.Prefix + r.Ctx.Cmd.Trigger
}
// Just makes the thing prettier
if ctx.Interaction != nil {
commandUsed = "/" + r.Ctx.Cmd.Trigger
}
for _, k := range r.Ctx.Cmd.Arguments.Keys() {
arg := ctx.Args[k]
if arg.StringValue() == "" {
continue
}
vv, ok := r.Ctx.Cmd.Arguments.Get(k)
if ok {
argInfo := vv.(*ArgInfo)
switch argInfo.TypeGuard {
case Int:
fallthrough
case Boolean:
fallthrough
case String:
commandUsed += " " + arg.StringValue()
break
case User:
user, err := arg.UserValue(Session)
if err != nil {
commandUsed += " " + arg.StringValue()
} else {
commandUsed += " " + user.Mention()
}
case Role:
role, err := arg.RoleValue(Session, r.Ctx.Guild.ID)
if err != nil {
commandUsed += " " + arg.StringValue()
} else {
commandUsed += " " + role.Mention()
}
case Channel:
channel, err := arg.ChannelValue(Session)
if err != nil {
commandUsed += " " + arg.StringValue()
} else {
commandUsed += " " + channel.Mention()
}
}
} else {
commandUsed += " " + arg.StringValue()
}
}
commandUsed = "```\n" + commandUsed + "\n```"
r.AppendField("Command used:", commandUsed, false)
}
// If the message is not nil, append an invoker
if ctx.Message != nil {
r.AppendField("Invoked by:", r.Ctx.Message.Author.Mention(), false)
}
return r
}
// -- Fields --
// AppendField
// Create a new basic field and append it to an existing Response
func (r *Response) AppendField(name string, value string, inline bool) {
r.Embed.Fields = append(r.Embed.Fields, CreateField(name, value, inline))
}
// PrependField
// Create a new basic field and prepend it to an existing Response
func (r *Response) PrependField(name string, value string, inline bool) {
fields := []*discordgo.MessageEmbedField{CreateField(name, value, inline)}
r.Embed.Fields = append(fields, r.Embed.Fields...)
}
// AppendUsage
// Add the command usage to the response. Intended for syntax error responses
func (r *Response) AppendUsage() {
if r.Ctx.Cmd.Description == "" {
r.AppendField("Command description:", "no description", false)
return
}
r.AppendField("Command description:", r.Ctx.Cmd.Description, false)
//r.AppendField("Command usage:", r.Ctx.Guild.GetCommandUsage(r.Ctx.Cmd), false)
}
// -- Message Components --
func CreateButton(label string, style discordgo.ButtonStyle, customID string, url string, disabled bool) *discordgo.Button {
button := &discordgo.Button{
Label: label,
Style: style,
Disabled: disabled,
Emoji: discordgo.ComponentEmoji{},
URL: url,
CustomID: customID,
}
return button
}
func CreateDropDown(customID string, placeholder string, options []discordgo.SelectMenuOption) discordgo.SelectMenu {
dropDown := discordgo.SelectMenu{
CustomID: customID,
Placeholder: placeholder,
Options: options,
}
return dropDown
}
// AppendButton
// Appends a button
func (r *Response) AppendButton(label string, style discordgo.ButtonStyle, url string, customID string, rowID int) {
row := r.ResponseComponents.Components[rowID].(discordgo.ActionsRow)
row.Components = append(row.Components, CreateButton(label, style, customID, url, false))
r.ResponseComponents.Components[rowID] = row
}
//AppendDropDown
// Adds a DropDown component
func (r *Response) AppendDropDown(customID string, placeholder string, noNewRow bool) {
if noNewRow {
row := r.ResponseComponents.Components[0].(discordgo.ActionsRow)
row.Components = append(row.Components, CreateDropDown(customID, placeholder, r.ResponseComponents.SelectMenuOptions))
r.ResponseComponents.Components[0] = row
} else {
actionRow := discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
discordgo.SelectMenu{
CustomID: customID,
Placeholder: placeholder,
Options: r.ResponseComponents.SelectMenuOptions,
},
},
}
r.ResponseComponents.Components = append(r.ResponseComponents.Components, actionRow)
}
}
// Send
// Send a compiled response
func (r *Response) Send(success bool, title string, description string) {
// Determine what color to use based on the success state
var color int
if success {
color = ColorSuccess
} else {
// On failure, also append the command usage
r.AppendUsage()
color = ColorFailure
}
// Fill out the main embed
r.Embed.Title = title
r.Embed.Description = description
r.Embed.Color = color
// If guild is nil, this is intended to be sent to Bot Admins
if r.Ctx.Guild == nil {
for admin := range botAdmins {
dmChannel, dmCreateErr := Session.UserChannelCreate(admin)
if dmCreateErr != nil {
// Since error reports also use DMs, sending this as an error report would be redundant
// Just log the error
log.Errorf("Failed sending Response DM to admin: %s; Response title: %s", admin, r.Embed.Title)
return
}
_, dmSendErr := Session.ChannelMessageSendComplex(dmChannel.ID, &discordgo.MessageSend{
Embed: r.Embed,
Components: r.ResponseComponents.Components,
})
if dmSendErr != nil {
// Since error reports also use DMs, sending this as an error report would be redundant
// Just log the error
log.Errorf("Failed sending Response DM to admin: %s; Response title: %s", admin, r.Embed.Title)
return
}
return
}
}
// If this is a interaction (slash command)
// Run it as a interaction response and then return early
if r.Ctx.Interaction != nil {
// Some commands take a while to load
// Slash commands expect a response in 3 seconds or the interaction gets invalidated
if r.Loading {
// Check to see if the command is ephemeral (only shown to the user)
if r.Ephemeral {
_, err := Session.InteractionResponseEdit(Session.State.User.ID, r.Ctx.Interaction, &discordgo.WebhookEdit{
Components: r.ResponseComponents.Components,
Embeds: []*discordgo.MessageEmbed{
r.Embed,
},
})
// Just in case the interaction gets removed.
if err != nil {
if err != nil {
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err)
}
if r.Ctx.Guild.Info.ResponseChannelId != "" {
_, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed)
} else {
_, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed)
}
if err != nil {
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send message", err)
}
}
} else {
_, err := Session.InteractionResponseEdit(Session.State.User.ID, r.Ctx.Interaction, &discordgo.WebhookEdit{
Content: "",
Embeds: []*discordgo.MessageEmbed{
r.Embed,
},
Components: r.ResponseComponents.Components,
})
// Just in case the interaction gets removed.
if err != nil {
_, err := Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed)
if err != nil {
_, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed)
if err != nil {
}
}
}
}
r.Loading = false
return
}
// Check to see if the command is ephemeral (only shown to the user)
if r.Ephemeral {
Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{
// Ephemeral is type 64 don't ask why
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: 1 << 6,
Embeds: []*discordgo.MessageEmbed{
r.Embed,
},
Components: r.ResponseComponents.Components,
},
})
return
}
// Default response for interaction
err := Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
r.Embed,
},
Components: r.ResponseComponents.Components,
},
})
if err != nil {
if err != nil {
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send interaction messages", err)
}
if r.Ctx.Guild.Info.ResponseChannelId != "" {
_, err = Session.ChannelMessageSendEmbed(r.Ctx.Guild.Info.ResponseChannelId, r.Embed)
} else {
_, err = Session.ChannelMessageSendEmbed(r.Ctx.Message.ChannelID, r.Embed)
}
if err != nil {
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Interaction.ChannelID, r.Ctx.Message.Author.ID, "Unable to send message", err)
}
}
return
}
// Try sending the response in the configured output channel
// If that fails, try sending the response in the current channel
// If THAT fails, send an error report
_, err := Session.ChannelMessageSendComplex(r.Ctx.Guild.Info.ResponseChannelId, &discordgo.MessageSend{
Embed: r.Embed,
Components: r.ResponseComponents.Components,
})
if err != nil {
// Reply to user if no output channel
_, err = ReplyToUser(r.Ctx.Message.ChannelID, &discordgo.MessageSend{
Embed: r.Embed,
Components: r.ResponseComponents.Components,
Reference: &discordgo.MessageReference{
MessageID: r.Ctx.Message.ID,
ChannelID: r.Ctx.Message.ChannelID,
GuildID: r.Ctx.Guild.ID,
},
AllowedMentions: &discordgo.MessageAllowedMentions{
RepliedUser: false,
},
})
if err != nil {
SendErrorReport(r.Ctx.Guild.ID, r.Ctx.Message.ChannelID, r.Ctx.Message.Author.ID, "Ultimately failed to send bot response", err)
}
}
}
func ErrorResponse(i *discordgo.Interaction, errorMsg string, trigger string) {
var errorEmbed = CreateEmbed(0xff3232, "Error", errorMsg, []*discordgo.MessageEmbedField{
{
Name: "Command Used",
Value: "/" + trigger,
},
{
Name: "Invoked by:",
Value: i.Member.User.Mention(),
},
})
Session.InteractionRespond(i, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
errorEmbed,
},
},
})
time.AfterFunc(time.Second*5, func() {
time.Sleep(time.Second * 4)
Session.InteractionResponseDelete(Session.State.User.ID, i)
})
}
func (r *Response) AcknowledgeInteraction() {
Session.InteractionRespond(r.Ctx.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "<:loadingdots:759625992166965288>",
},
})
r.Loading = true
}
func ReplyToUser(channelID string, messageSend *discordgo.MessageSend) (*discordgo.Message, error) {
return Session.ChannelMessageSendComplex(channelID, messageSend)
}