Add /profile slash command

This commit is contained in:
Ben Visness 2021-09-26 17:34:38 -05:00
parent 0cebe90268
commit 85a7a37162
4 changed files with 613 additions and 31 deletions

142
src/discord/commands.go Normal file
View File

@ -0,0 +1,142 @@
package discord
import (
"context"
"errors"
"fmt"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
)
const CommandNameProfile = "profile"
const ProfileCommandOptionUser = "user"
func (bot *botInstance) createApplicationCommands(ctx context.Context) {
doOrWarn := func(err error) {
if err == nil {
logging.ExtractLogger(ctx).Info().Msg("Created Discord application command")
} else {
logging.ExtractLogger(ctx).Warn().Err(err).Msg("Failed to create Discord application command")
}
}
doOrWarn(CreateGuildApplicationCommand(ctx, CreateGuildApplicationCommandRequest{
Name: CommandNameProfile,
Description: "Get a link to a user's Handmade Network profile",
Options: []ApplicationCommandOption{
{
Type: ApplicationCommandOptionTypeUser,
Name: ProfileCommandOptionUser,
Description: "The Discord user to look up on Handmade Network",
Required: true,
},
},
Type: ApplicationCommandTypeChatInput,
}))
}
func (bot *botInstance) doInteraction(ctx context.Context, i *Interaction) {
defer func() {
if recovered := recover(); recovered != nil {
logger := logging.ExtractLogger(ctx).Error()
if err, ok := recovered.(error); ok {
logger = logger.Err(err)
} else {
logger = logger.Interface("recovered", recovered)
}
logger.Msg("panic when handling Discord interaction")
}
}()
switch i.Data.Name {
case CommandNameProfile:
bot.handleProfileCommand(ctx, i)
default:
logging.ExtractLogger(ctx).Warn().Str("name", i.Data.Name).Msg("didn't recognize Discord interaction name")
}
}
func (bot *botInstance) handleProfileCommand(ctx context.Context, i *Interaction) {
userOpt := mustGetInteractionOption(i.Data.Options, ProfileCommandOptionUser)
userID := userOpt.Value.(string)
member := i.Data.Resolved.Members[userID]
if userID == config.Config.Discord.BotUserID {
err := CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
Type: InteractionCallbackTypeChannelMessageWithSource,
Data: &InteractionCallbackData{
Content: "<a:confusedparrot:891814826484064336>",
},
})
if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
}
return
}
type profileResult struct {
HMNUser models.User `db:"auth_user"`
}
ires, err := db.QueryOne(ctx, bot.dbConn, profileResult{},
`
SELECT $columns
FROM
handmade_discorduser AS duser
JOIN auth_user ON duser.hmn_user_id = auth_user.id
WHERE
duser.userid = $1
`,
userID,
)
if err != nil {
if errors.Is(err, db.NotFound) {
err = CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
Type: InteractionCallbackTypeChannelMessageWithSource,
Data: &InteractionCallbackData{
Content: fmt.Sprintf("%s hasn't linked a Handmade Network profile.", member.DisplayName()),
},
})
if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
}
} else {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to look up user profile")
}
return
}
res := ires.(*profileResult)
url := hmnurl.BuildUserProfile(res.HMNUser.Username)
err = CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
Type: InteractionCallbackTypeChannelMessageWithSource,
Data: &InteractionCallbackData{
Content: fmt.Sprintf("%s's profile can be viewed at %s.", member.DisplayName(), url),
},
})
if err != nil {
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
}
}
func getInteractionOption(opts []ApplicationCommandInteractionDataOption, name string) (ApplicationCommandInteractionDataOption, bool) {
for _, opt := range opts {
if opt.Name == name {
return opt, true
}
}
return ApplicationCommandInteractionDataOption{}, false
}
func mustGetInteractionOption(opts []ApplicationCommandInteractionDataOption, name string) ApplicationCommandInteractionDataOption {
opt, ok := getInteractionOption(opts, name)
if !ok {
panic(fmt.Errorf("failed to get interaction option with name '%s'", name))
}
return opt
}

View File

@ -544,15 +544,17 @@ func (bot *botInstance) processEventMsg(ctx context.Context, msg *GatewayMessage
case "RESUMED":
// Nothing to do, but at least we can log something
logging.ExtractLogger(ctx).Info().Msg("Finished resuming gateway session")
bot.createApplicationCommands(ctx)
case "MESSAGE_CREATE":
newMessage := MessageFromMap(msg.Data)
newMessage := *MessageFromMap(msg.Data, "")
err := bot.messageCreateOrUpdate(ctx, &newMessage)
if err != nil {
return oops.New(err, "error on new message")
}
case "MESSAGE_UPDATE":
newMessage := MessageFromMap(msg.Data)
newMessage := *MessageFromMap(msg.Data, "")
err := bot.messageCreateOrUpdate(ctx, &newMessage)
if err != nil {
@ -569,6 +571,15 @@ func (bot *botInstance) processEventMsg(ctx context.Context, msg *GatewayMessage
GuildID: bulkDelete.GuildID,
})
}
case "GUILD_CREATE":
guild := *GuildFromMap(msg.Data, "")
if guild.ID != config.Config.Discord.GuildID {
break
}
bot.createApplicationCommands(ctx)
case "INTERACTION_CREATE":
go bot.doInteraction(ctx, InteractionFromMap(msg.Data, ""))
}
return nil

View File

@ -95,7 +95,7 @@ func ReadyFromMap(m interface{}) Ready {
return Ready{
GatewayVersion: int(mmap["v"].(float64)),
User: UserFromMap(mmap["user"]),
User: *UserFromMap(mmap["user"], ""),
SessionID: mmap["session_id"].(string),
}
}
@ -170,15 +170,43 @@ type Role struct {
// more fields not yet present
}
func RoleFromMap(m interface{}, k string) *Role {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
r := &Role{
ID: mmap["id"].(string),
Name: mmap["name"].(string),
}
return r
}
// https://discord.com/developers/docs/resources/channel#channel-object
type Channel struct {
ID string `json:"id"`
Type ChannelType `json:"type"`
GuildID string `json:"guild_id"`
Name string `json:"name"`
Receipients []User `json:"recipients"`
OwnerID User `json:"owner_id"`
ParentID *string `json:"parent_id"`
ID string `json:"id"`
Type ChannelType `json:"type"`
GuildID string `json:"guild_id"`
Name string `json:"name"`
// More fields not yet present
}
func ChannelFromMap(m interface{}, k string) *Channel {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
c := &Channel{
ID: mmap["id"].(string),
Type: ChannelType(mmap["type"].(float64)),
GuildID: maybeString(mmap, "guild_id"),
Name: maybeString(mmap, "name"),
}
return c
}
type MessageType int
@ -222,7 +250,7 @@ type Message struct {
ChannelID string `json:"channel_id"`
GuildID *string `json:"guild_id"`
Content string `json:"content"`
Author User `json:"author"` // note that this may not be an actual valid user (see the docs)
Author *User `json:"author"` // note that this may not be an actual valid user (see the docs)
Timestamp string `json:"timestamp"`
Type MessageType `json:"type"`
@ -269,44 +297,45 @@ func (m *Message) OriginalHasFields(fields ...string) bool {
return true
}
func MessageFromMap(m interface{}) Message {
func MessageFromMap(m interface{}, k string) *Message {
/*
Some gateway events, like MESSAGE_UPDATE, do not contain the
entire message body. So we need to be defensive on all fields here,
except the most basic identifying information.
*/
mmap := m.(map[string]interface{})
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
msg := Message{
ID: mmap["id"].(string),
ChannelID: mmap["channel_id"].(string),
GuildID: maybeStringP(mmap, "guild_id"),
Content: maybeString(mmap, "content"),
Author: UserFromMap(m, "author"),
Timestamp: maybeString(mmap, "timestamp"),
Type: MessageType(maybeInt(mmap, "type")),
originalMap: mmap,
}
if author, ok := mmap["author"]; ok {
msg.Author = UserFromMap(author)
}
if iattachments, ok := mmap["attachments"]; ok {
attachments := iattachments.([]interface{})
for _, iattachment := range attachments {
msg.Attachments = append(msg.Attachments, AttachmentFromMap(iattachment))
msg.Attachments = append(msg.Attachments, *AttachmentFromMap(iattachment, ""))
}
}
if iembeds, ok := mmap["embeds"]; ok {
embeds := iembeds.([]interface{})
for _, iembed := range embeds {
msg.Embeds = append(msg.Embeds, EmbedFromMap(iembed))
msg.Embeds = append(msg.Embeds, *EmbedFromMap(iembed))
}
}
return msg
return &msg
}
// https://discord.com/developers/docs/resources/user#user-object
@ -319,8 +348,11 @@ type User struct {
Locale string `json:"locale"`
}
func UserFromMap(m interface{}) User {
mmap := m.(map[string]interface{})
func UserFromMap(m interface{}, k string) *User {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
u := User{
ID: mmap["id"].(string),
@ -332,7 +364,25 @@ func UserFromMap(m interface{}) User {
u.IsBot = isBot.(bool)
}
return u
return &u
}
type Guild struct {
ID string `json:"id"`
// Who cares about the rest tbh
}
func GuildFromMap(m interface{}, k string) *Guild {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
g := Guild{
ID: mmap["id"].(string),
}
return &g
}
// https://discord.com/developers/docs/resources/guild#guild-member-object
@ -342,6 +392,30 @@ type GuildMember struct {
// more fields not yet handled here
}
func (gm *GuildMember) DisplayName() string {
if gm.Nick != nil {
return *gm.Nick
}
if gm.User != nil {
return gm.User.Username
}
return "<UNKNOWN USER>"
}
func GuildMemberFromMap(m interface{}, k string) *GuildMember {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
gm := &GuildMember{
User: UserFromMap(m, "user"),
Nick: maybeStringP(mmap, "nick"),
}
return gm
}
// https://discord.com/developers/docs/resources/channel#attachment-object
type Attachment struct {
ID string `json:"id"`
@ -354,8 +428,12 @@ type Attachment struct {
Width *int `json:"width"`
}
func AttachmentFromMap(m interface{}) Attachment {
mmap := m.(map[string]interface{})
func AttachmentFromMap(m interface{}, k string) *Attachment {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
a := Attachment{
ID: mmap["id"].(string),
Filename: mmap["filename"].(string),
@ -367,7 +445,7 @@ func AttachmentFromMap(m interface{}) Attachment {
Width: maybeIntP(mmap, "width"),
}
return a
return &a
}
// https://discord.com/developers/docs/resources/channel#embed-object
@ -430,8 +508,11 @@ type EmbedField struct {
Inline *bool `json:"inline"`
}
func EmbedFromMap(m interface{}) Embed {
func EmbedFromMap(m interface{}) *Embed {
mmap := m.(map[string]interface{})
if mmap == nil {
return nil
}
e := Embed{
Title: maybeStringP(mmap, "title"),
@ -449,7 +530,7 @@ func EmbedFromMap(m interface{}) Embed {
Fields: EmbedFieldsFromMap(mmap, "fields"),
}
return e
return &e
}
func EmbedFooterFromMap(m map[string]interface{}, k string) *EmbedFooter {
@ -588,6 +669,286 @@ func EmbedFieldsFromMap(m map[string]interface{}, k string) []EmbedField {
return result
}
// Data is always present on application command and message component interaction types. It is optional for future-proofing against new interaction types.
//
// Member is sent when the interaction is invoked in a guild, and User is sent when invoked in a DM.
//
// See https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-structure
type Interaction struct {
ID string `json:"id"` // id of the interaction
ApplicationID string `json:"application_id"` // id of the application this interaction is for
Type InteractionType `json:"type"` // the type of interaction
Data *InteractionData `json:"data"` // the command data payload
GuildID string `json:"guild_id"` // the guild it was sent from
ChannelID string `json:"channel_id"` // the channel it was sent from
Member *GuildMember `json:"member"` // guild member data for the invoking user, including permissions
User *User `json:"user"` // user object for the invoking user, if invoked in a DM
Token string `json:"token"` // a continuation token for responding to the interaction
Version int `json:"version"` // read-only property, always 1
Message *Message `json:"message"` // for components, the message they were attached to
}
// https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-type
type InteractionType int
const (
InteractionTypePing InteractionType = 1
InteractionTypeApplicationCommand InteractionType = 2
InteractionTypeMessageComponent InteractionType = 3
)
// See https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-data-structure
type InteractionData struct {
// Fields for Application Commands
// See https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
ID string `json:"id"` // the ID of the invoked command
Name string `json:"name"` // the name of the invoked command
Type ApplicationCommandType `json:"type"` // the type of the invoked command
Resolved *ResolvedData `json:"resolved"` // converted users + roles + channels
Options []ApplicationCommandInteractionDataOption `json:"options"` // the params + values from the user
// Fields for Components
// TODO
// Fields for User Command and Message Command
// TODO
}
// See https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure
type ResolvedData struct {
Users map[string]User `json:"users"`
Members map[string]GuildMember `json:"members"` // Partial Member objects are missing user, deaf and mute fields. If data for a Member is included, data for its corresponding User will also be included.
Roles map[string]Role `json:"roles"`
Channels map[string]Channel `json:"channels"` // Partial Channel objects only have id, name, type and permissions fields. Threads will also have thread_metadata and parent_id fields.
Messages map[string]Message `json:"messages"`
}
// See https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-response-structure
type InteractionResponse struct {
Type InteractionCallbackType `json:"type"` // the type of response
Data *InteractionCallbackData `json:"data"` // an optional response message
}
// See https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-response-object-interaction-callback-type
type InteractionCallbackType int
const (
InteractionCallbackTypePong InteractionCallbackType = 1 // ACK a Ping
InteractionCallbackTypeChannelMessageWithSource InteractionCallbackType = 4 // respond to an interaction with a message
InteractionCallbackTypeDeferredChannelMessageWithSource InteractionCallbackType = 5 // ACK an interaction and edit a response later, the user sees a loading state
InteractionCallbackTypeDeferredUpdateMessage InteractionCallbackType = 6 // for components, ACK an interaction and edit the original message later; the user does not see a loading state
InteractionCallbackTypeUpdateMessage InteractionCallbackType = 7 // for components, edit the message the component was attached to
)
type InteractionCallbackData struct {
TTS bool `json:"bool,omitempty"`
Content string `json:"content,omitempty"`
Embeds []Embed `json:"embeds,omitempty"`
// TODO: Allowed mentions
Flags InteractionCallbackDataFlags `json:"flags,omitempty"`
// TODO: Components
}
type InteractionCallbackDataFlags int
const (
FlagEphemeral InteractionCallbackDataFlags = 1 << 6
)
type ApplicationCommandType int
const (
ApplicationCommandTypeChatInput ApplicationCommandType = 1 // Slash commands; a text-based command that shows up when a user types `/`
ApplicationCommandTypeUser ApplicationCommandType = 2 // A UI-based command that shows up when you right click or tap on a user
ApplicationCommandTypeMessage ApplicationCommandOptionType = 3 // A UI-based command that shows up when you right click or tap on a message
)
// Required `options` must be listed before optional options
type ApplicationCommandOption struct {
Type ApplicationCommandOptionType `json:"type"` // the type of option
Name string `json:"name"` // 1-32 character name
Description string `json:"description"` // 1-100 character description
Required bool `json:"required"` // if the parameter is required or optional--default false
Choices []ApplicationCommandOptionChoice `json:"choices"` // choices for STRING, INTEGER, and NUMBER types for the user to pick from, max 25
Options []ApplicationCommandOption `json:"options"` // if the option is a subcommand or subcommand group type, this nested options will be the parameters
}
type ApplicationCommandOptionType int
const (
ApplicationCommandOptionTypeSubCommand ApplicationCommandOptionType = 1
ApplicationCommandOptionTypeSubCommandGroup ApplicationCommandOptionType = 2
ApplicationCommandOptionTypeString ApplicationCommandOptionType = 3
ApplicationCommandOptionTypeInteger ApplicationCommandOptionType = 4 // Any integer between -2^53 and 2^53
ApplicationCommandOptionTypeBoolean ApplicationCommandOptionType = 5
ApplicationCommandOptionTypeUser ApplicationCommandOptionType = 6
ApplicationCommandOptionTypeChannel ApplicationCommandOptionType = 7 // Includes all channel types + categories
ApplicationCommandOptionTypeRole ApplicationCommandOptionType = 8
ApplicationCommandOptionTypeMentionable ApplicationCommandOptionType = 9 // Includes users and roles
ApplicationCommandOptionTypeNumber ApplicationCommandOptionType = 10 // Any double between -2^53 and 2^53
)
// If you specify `choices` for an option, they are the only valid values for a user to pick
type ApplicationCommandOptionChoice struct {
Name string `json:"name"` // 1-100 character choice name
Value interface{} `json:"value"` // value of the choice, up to 100 characters if string
}
// All options have names, and an option can either be a parameter and input
// value--in which case Value will be set--or it can denote a subcommand or
// group--in which case it will contain a top-level key and another array
// of Options.
//
// Value and Options are mutually exclusive.
type ApplicationCommandInteractionDataOption struct {
Name string `json:"name"`
Type ApplicationCommandOptionType `json:"type"`
Value interface{} `json:"value"` // the value of the pair
Options []ApplicationCommandInteractionDataOption `json:"options"` // present if this option is a group or subcommand
}
func InteractionFromMap(m interface{}, k string) *Interaction {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
i := &Interaction{
ID: mmap["id"].(string),
ApplicationID: mmap["application_id"].(string),
Type: InteractionType(mmap["type"].(float64)),
Data: InteractionDataFromMap(m, "data"),
GuildID: maybeString(mmap, "guild_id"),
ChannelID: maybeString(mmap, "channel_id"),
Member: GuildMemberFromMap(mmap, "member"),
User: UserFromMap(mmap, "user"),
Token: mmap["token"].(string),
Version: int(mmap["version"].(float64)),
Message: MessageFromMap(mmap, "message"),
}
return i
}
func InteractionDataFromMap(m interface{}, k string) *InteractionData {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
d := &InteractionData{
ID: mmap["id"].(string),
Name: mmap["name"].(string),
Type: ApplicationCommandType(mmap["type"].(float64)),
Resolved: ResolvedDataFromMap(mmap, "resolved"),
}
if ioptions, ok := mmap["options"]; ok {
options := ioptions.([]interface{})
for _, ioption := range options {
d.Options = append(d.Options, *ApplicationCommandInteractionDataOptionFromMap(ioption, ""))
}
}
return d
}
func ResolvedDataFromMap(m interface{}, k string) *ResolvedData {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
d := &ResolvedData{}
if iusers, ok := mmap["users"]; ok {
users := iusers.(map[string]interface{})
d.Users = make(map[string]User)
for id, iuser := range users {
d.Users[id] = *UserFromMap(iuser, "")
}
}
if imembers, ok := mmap["members"]; ok {
members := imembers.(map[string]interface{})
d.Members = make(map[string]GuildMember)
for id, imember := range members {
member := *GuildMemberFromMap(imember, "")
user := d.Users[id]
member.User = &user
d.Members[id] = member
}
}
if iroles, ok := mmap["roles"]; ok {
roles := iroles.(map[string]interface{})
d.Roles = make(map[string]Role)
for id, irole := range roles {
d.Roles[id] = *RoleFromMap(irole, "")
}
}
if ichannels, ok := mmap["channels"]; ok {
channels := ichannels.(map[string]interface{})
d.Channels = make(map[string]Channel)
for id, ichannel := range channels {
d.Channels[id] = *ChannelFromMap(ichannel, "")
}
}
if imessages, ok := mmap["messages"]; ok {
messages := imessages.(map[string]interface{})
d.Messages = make(map[string]Message)
for id, imessage := range messages {
d.Messages[id] = *MessageFromMap(imessage, "")
}
}
return d
}
func ApplicationCommandInteractionDataOptionFromMap(m interface{}, k string) *ApplicationCommandInteractionDataOption {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
o := &ApplicationCommandInteractionDataOption{
Name: mmap["name"].(string),
Type: ApplicationCommandOptionType(mmap["type"].(float64)),
Value: mmap["value"],
}
if ioptions, ok := mmap["options"]; ok {
options := ioptions.([]interface{})
for _, ioption := range options {
o.Options = append(o.Options, *ApplicationCommandInteractionDataOptionFromMap(ioption, ""))
}
}
return o
}
// If called without a key, returns m. Otherwise, returns m[k].
// If m[k] does not exist, returns nil.
//
// The intent is to allow the ThingFromMap functions to be flexibly called,
// either with the data in question as the root (no key) or as a child of
// another object (with a key).
func maybeGetKey(m interface{}, k string) map[string]interface{} {
if k == "" {
return m.(map[string]interface{})
} else {
mmap := m.(map[string]interface{})
if mk, ok := mmap[k]; ok {
return mk.(map[string]interface{})
} else {
return nil
}
}
}
func maybeString(m map[string]interface{}, k string) string {
val, ok := m[k]
if !ok {
@ -598,7 +959,7 @@ func maybeString(m map[string]interface{}, k string) string {
func maybeStringP(m map[string]interface{}, k string) *string {
val, ok := m[k]
if !ok {
if !ok || val == nil {
return nil
}
strval := val.(string)
@ -615,7 +976,7 @@ func maybeInt(m map[string]interface{}, k string) int {
func maybeIntP(m map[string]interface{}, k string) *int {
val, ok := m[k]
if !ok {
if !ok || val == nil {
return nil
}
intval := int(val.(float64))
@ -632,7 +993,7 @@ func maybeBool(m map[string]interface{}, k string) bool {
func maybeBoolP(m map[string]interface{}, k string) *bool {
val, ok := m[k]
if !ok {
if !ok || val == nil {
return nil
}
boolval := val.(bool)

View File

@ -500,6 +500,74 @@ func GetChannelMessages(ctx context.Context, channelID string, in GetChannelMess
return msgs, nil
}
// See https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command-json-params
type CreateGuildApplicationCommandRequest struct {
Name string `json:"name"` // 1-32 character name
Description string `json:"description"` // 1-100 character description
Options []ApplicationCommandOption `json:"options"` // the parameters for the command
DefaultPermission *bool `json:"default_permission"` // whether the command is enabled by default when the app is added to a guild
Type ApplicationCommandType `json:"type"` // the type of command, defaults 1 if not set
}
// See https://discord.com/developers/docs/interactions/application-commands#create-guild-application-command
func CreateGuildApplicationCommand(ctx context.Context, in CreateGuildApplicationCommandRequest) error {
const name = "Create Guild Application Command"
if in.Type == 0 {
in.Type = ApplicationCommandTypeChatInput
}
payloadJSON, err := json.Marshal(in)
if err != nil {
return oops.New(nil, "failed to marshal request body")
}
path := fmt.Sprintf("/applications/%s/guilds/%s/commands", config.Config.Discord.BotUserID, config.Config.Discord.GuildID)
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
req := makeRequest(ctx, http.MethodPost, path, []byte(payloadJSON))
req.Header.Add("Content-Type", "application/json")
return req
})
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
logErrorResponse(ctx, name, res, "")
return oops.New(nil, "received error from Discord")
}
return nil
}
func CreateInteractionResponse(ctx context.Context, interactionID, interactionToken string, in InteractionResponse) error {
const name = "Create Interaction Response"
payloadJSON, err := json.Marshal(in)
if err != nil {
return oops.New(nil, "failed to marshal request body")
}
path := fmt.Sprintf("/interactions/%s/%s/callback", interactionID, interactionToken)
res, err := doWithRateLimiting(ctx, name, func(ctx context.Context) *http.Request {
req := makeRequest(ctx, http.MethodPost, path, []byte(payloadJSON))
req.Header.Add("Content-Type", "application/json")
return req
})
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
logErrorResponse(ctx, name, res, "")
return oops.New(nil, "received error from Discord")
}
return nil
}
func GetAuthorizeUrl(state string) string {
params := make(url.Values)
params.Set("response_type", "code")