Add /profile slash command
This commit is contained in:
parent
0cebe90268
commit
85a7a37162
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
// 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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue