hmn/src/discord/payloads.go

1033 lines
30 KiB
Go
Raw Permalink Normal View History

package discord
import (
"encoding/json"
2021-08-21 16:15:27 +00:00
"fmt"
"time"
)
type Opcode int
// https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes
// NOTE(ben): I'm not using iota because 5 is missing
const (
OpcodeDispatch Opcode = 0
OpcodeHeartbeat Opcode = 1
OpcodeIdentify Opcode = 2
OpcodePresenceUpdate Opcode = 3
OpcodeVoiceStateUpdate Opcode = 4
OpcodeResume Opcode = 6
OpcodeReconnect Opcode = 7
OpcodeRequestGuildMembers Opcode = 8
OpcodeInvalidSession Opcode = 9
OpcodeHello Opcode = 10
OpcodeHeartbeatACK Opcode = 11
)
type Intent int
// https://discord.com/developers/docs/topics/gateway#list-of-intents
// NOTE(ben): I'm not using iota because the opcode thing made me paranoid
const (
IntentGuilds Intent = 1 << 0
IntentGuildMembers Intent = 1 << 1
IntentGuildBans Intent = 1 << 2
IntentGuildEmojisAndStickers Intent = 1 << 3
IntentGuildIntegrations Intent = 1 << 4
IntentGuildWebhooks Intent = 1 << 5
IntentGuildInvites Intent = 1 << 6
IntentGuildVoiceStates Intent = 1 << 7
IntentGuildPresences Intent = 1 << 8
IntentGuildMessages Intent = 1 << 9
IntentGuildMessageReactions Intent = 1 << 10
IntentGuildMessageTyping Intent = 1 << 11
IntentDirectMessages Intent = 1 << 12
IntentDirectMessageReactions Intent = 1 << 13
IntentDirectMessageTyping Intent = 1 << 14
)
type GatewayMessage struct {
Opcode Opcode `json:"op"`
Data interface{} `json:"d"`
SequenceNumber *int `json:"s,omitempty"`
EventName *string `json:"t,omitempty"`
}
func (m *GatewayMessage) ToJSON() []byte {
mBytes, err := json.Marshal(m)
if err != nil {
panic(err)
}
return mBytes
}
type Hello struct {
HeartbeatIntervalMs int `json:"heartbeat_interval"`
}
func HelloFromMap(m interface{}) Hello {
return Hello{
HeartbeatIntervalMs: int(m.(map[string]interface{})["heartbeat_interval"].(float64)),
}
}
type Identify struct {
Token string `json:"token"`
Properties IdentifyConnectionProperties `json:"properties"`
Intents Intent `json:"intents"`
}
type IdentifyConnectionProperties struct {
OS string `json:"$os"`
Browser string `json:"$browser"`
Device string `json:"$device"`
}
type Ready struct {
GatewayVersion int `json:"v"`
User User `json:"user"`
SessionID string `json:"session_id"`
}
func ReadyFromMap(m interface{}) Ready {
mmap := m.(map[string]interface{})
return Ready{
GatewayVersion: int(mmap["v"].(float64)),
2021-09-26 22:34:38 +00:00
User: *UserFromMap(mmap["user"], ""),
SessionID: mmap["session_id"].(string),
}
}
type Resume struct {
Token string `json:"token"`
SessionID string `json:"session_id"`
SequenceNumber int `json:"seq"`
}
// https://discord.com/developers/docs/topics/gateway#message-delete
type MessageDelete struct {
ID string `json:"id"`
ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
}
func MessageDeleteFromMap(m interface{}) MessageDelete {
mmap := m.(map[string]interface{})
return MessageDelete{
ID: mmap["id"].(string),
ChannelID: mmap["channel_id"].(string),
2021-08-27 00:58:41 +00:00
GuildID: maybeString(mmap, "guild_id"),
}
}
// https://discord.com/developers/docs/topics/gateway#message-delete
type MessageBulkDelete struct {
IDs []string `json:"ids"`
ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
}
func MessageBulkDeleteFromMap(m interface{}) MessageBulkDelete {
mmap := m.(map[string]interface{})
iids := mmap["ids"].([]interface{})
ids := make([]string, len(iids))
for i, iid := range iids {
ids[i] = iid.(string)
}
return MessageBulkDelete{
IDs: ids,
ChannelID: mmap["channel_id"].(string),
GuildID: maybeString(mmap, "guild_id"),
}
}
type ChannelType int
2021-08-23 21:52:57 +00:00
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
const (
2021-08-23 21:52:57 +00:00
ChannelTypeGuildText ChannelType = 0
ChannelTypeDM ChannelType = 1
ChannelTypeGuildVoice ChannelType = 2
ChannelTypeGroupDM ChannelType = 3
ChannelTypeGuildCategory ChannelType = 4
ChannelTypeGuildNews ChannelType = 5
ChannelTypeGuildStore ChannelType = 6
ChannelTypeGuildNewsThread ChannelType = 10
ChannelTypeGuildPublicThread ChannelType = 11
ChannelTypeGuildPrivateThread ChannelType = 12
ChannelTypeGuildStageVoice ChannelType = 13
)
2021-08-24 03:26:27 +00:00
// https://discord.com/developers/docs/topics/permissions#role-object
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
// more fields not yet present
}
2021-09-26 22:34:38 +00:00
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
}
2021-08-23 21:52:57 +00:00
// https://discord.com/developers/docs/resources/channel#channel-object
type Channel struct {
2021-09-26 22:34:38 +00:00
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
2021-08-23 21:52:57 +00:00
// https://discord.com/developers/docs/resources/channel#message-object-message-types
const (
MessageTypeDefault MessageType = 0
MessageTypeRecipientAdd MessageType = 1
MessageTypeRecipientRemove MessageType = 2
MessageTypeCall MessageType = 3
MessageTypeChannelNameChange MessageType = 4
MessageTypeChannelIconChange MessageType = 5
MessageTypeChannelPinnedMessage MessageType = 6
MessageTypeGuildMemberJoin MessageType = 7
MessageTypeUserPremiumGuildSubscription MessageType = 8
MessageTypeUserPremiumGuildSubscriptionTier1 MessageType = 9
MessageTypeUserPremiumGuildSubscriptionTier2 MessageType = 10
MessageTypeUserPremiumGuildSubscriptionTier3 MessageType = 11
MessageTypeChannelFollowAdd MessageType = 12
MessageTypeGuildDiscoveryDisqualified MessageType = 14
MessageTypeGuildDiscoveryRequalified MessageType = 15
MessageTypeGuildDiscoveryGracePeriodInitialWarning MessageType = 16
MessageTypeGuildDiscoveryGracePeriodFinalWarning MessageType = 17
MessageTypeThreadCreated MessageType = 18
MessageTypeReply MessageType = 19
MessageTypeApplicationCommand MessageType = 20
MessageTypeThreadStarterMessage MessageType = 21
MessageTypeGuildInviteReminder MessageType = 22
)
type MessageFlags int
const (
MessageFlagCrossposted MessageFlags = 1 << iota
MessageFlagIsCrosspost
MessageFlagSuppressEmbeds
MessageFlagSourceMessageDeleted
MessageFlagUrgent
MessageFlagHasThread
MessageFlagEphemeral
MessageFlagLoading
MessageFlagFailedToMentionSomeRolesInThread
MessageFlagSuppressNotifications
MessageFlagIsVoiceMessage
)
// https://discord.com/developers/docs/resources/channel#message-object
type Message struct {
ID string `json:"id"`
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)
Timestamp string `json:"timestamp"`
Type MessageType `json:"type"`
Flags MessageFlags `json:"flags"`
Attachments []Attachment `json:"attachments"`
2021-08-23 21:52:57 +00:00
Embeds []Embed `json:"embeds"`
originalMap map[string]interface{}
2024-03-28 19:24:46 +00:00
Backfilled bool
}
2021-08-21 16:15:27 +00:00
func (m *Message) JumpURL() string {
guildStr := "@me"
if m.GuildID != nil {
guildStr = *m.GuildID
}
return fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildStr, m.ChannelID, m.ID)
}
func (m *Message) Time() time.Time {
t, err := time.Parse(time.RFC3339Nano, m.Timestamp)
if err != nil {
panic(err)
}
return t
}
func (m *Message) ShortString() string {
return fmt.Sprintf("%s / %s: \"%s\" (%d attachments, %d embeds)", m.Timestamp, m.Author.Username, m.Content, len(m.Attachments), len(m.Embeds))
}
2021-08-21 16:15:27 +00:00
func (m *Message) OriginalHasFields(fields ...string) bool {
if m.originalMap == nil {
// If we don't know, we assume the fields are there.
// Usually this is because it came from their API, where we
// always have all fields.
return true
2021-08-21 16:15:27 +00:00
}
for _, field := range fields {
_, ok := m.originalMap[field]
if !ok {
return false
}
}
return true
}
2021-09-26 22:34:38 +00:00
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.
*/
2021-09-26 22:34:38 +00:00
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
msg := Message{
ID: mmap["id"].(string),
ChannelID: mmap["channel_id"].(string),
2021-08-21 16:15:27 +00:00
GuildID: maybeStringP(mmap, "guild_id"),
Content: maybeString(mmap, "content"),
2021-09-26 22:34:38 +00:00
Author: UserFromMap(m, "author"),
2021-08-21 16:15:27 +00:00
Timestamp: maybeString(mmap, "timestamp"),
Type: MessageType(maybeInt(mmap, "type")),
Flags: MessageFlags(maybeInt(mmap, "flags")),
originalMap: mmap,
}
if iattachments, ok := mmap["attachments"]; ok {
attachments := iattachments.([]interface{})
for _, iattachment := range attachments {
2021-09-26 22:34:38 +00:00
msg.Attachments = append(msg.Attachments, *AttachmentFromMap(iattachment, ""))
}
}
2021-08-23 21:52:57 +00:00
if iembeds, ok := mmap["embeds"]; ok {
embeds := iembeds.([]interface{})
for _, iembed := range embeds {
2021-09-26 22:34:38 +00:00
msg.Embeds = append(msg.Embeds, *EmbedFromMap(iembed))
2021-08-23 21:52:57 +00:00
}
}
2021-08-21 16:15:27 +00:00
2021-09-26 22:34:38 +00:00
return &msg
}
// https://discord.com/developers/docs/resources/user#user-object
type User struct {
2021-08-16 04:40:56 +00:00
ID string `json:"id"`
Username string `json:"username"`
Discriminator string `json:"discriminator"`
Avatar *string `json:"avatar"`
IsBot bool `json:"bot"`
Locale string `json:"locale"`
Add Discord login (#106) This leverages our existing Discord OAuth implementation. Any users with a linked Discord account will be able to log in immediately. When logging in, we request the `email` scope in addition to `identity`, so existing users will be prompted one time to accept the new permissions. On subsequent logins, Discord will skip the prompt. When linking your Discord account to an existing HMN account, we continue to only request the `identity` scope, so we do not receive the user's Discord email. Both login and linking go through the same Discord OAuth callback. All flows through the callback try to achieve the same end goal: a logged-in HMN user with a linked Discord account. Linking works the same as it ever has. Login, however, is different because we do not have a session ID to use as the OAuth state. To account for this, I have added a `pending_login` table that stores a secure unique ID and the eventual destination URL. These pending logins expire after 10 minutes. When we receive the OAuth callback, we look up the pending login by the OAuth `state` and immediately delete it. The destination URL will be used to redirect the user to the right place. If we have a `discord_user` entry for the OAuth'd Discord user, we immediately log the user into the associated HMN account. This is the typical login case. If we do not have a `discord_user`, but there is exactly one HMN user with the same email address as the Discord user, we will link the two accounts and log into the HMN account. (It is possible for multiple HMN accounts to have the same email, because we don't have a uniqueness constraint there. We fail the login in this case rather than link to the wrong account.) Finally, if no associated HMN user exists, a new one will be created. It will use the Discord user's username, email, and avatar. This user will have no password, but they can set or reset a password through the usual flows. Co-authored-by: Ben Visness <bvisness@gmail.com> Reviewed-on: https://git.handmade.network/hmn/hmn/pulls/106
2023-05-06 19:38:50 +00:00
Email string `json:"email"`
}
2021-09-26 22:34:38 +00:00
func UserFromMap(m interface{}, k string) *User {
mmap := maybeGetKey(m, k)
if mmap == nil {
return nil
}
u := User{
ID: mmap["id"].(string),
Username: mmap["username"].(string),
Discriminator: mmap["discriminator"].(string),
}
if isBot, ok := mmap["bot"]; ok {
u.IsBot = isBot.(bool)
}
2021-09-26 22:34:38 +00:00
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
}
2021-08-24 03:26:27 +00:00
// https://discord.com/developers/docs/resources/guild#guild-member-object
type GuildMember struct {
Add Discord login (#106) This leverages our existing Discord OAuth implementation. Any users with a linked Discord account will be able to log in immediately. When logging in, we request the `email` scope in addition to `identity`, so existing users will be prompted one time to accept the new permissions. On subsequent logins, Discord will skip the prompt. When linking your Discord account to an existing HMN account, we continue to only request the `identity` scope, so we do not receive the user's Discord email. Both login and linking go through the same Discord OAuth callback. All flows through the callback try to achieve the same end goal: a logged-in HMN user with a linked Discord account. Linking works the same as it ever has. Login, however, is different because we do not have a session ID to use as the OAuth state. To account for this, I have added a `pending_login` table that stores a secure unique ID and the eventual destination URL. These pending logins expire after 10 minutes. When we receive the OAuth callback, we look up the pending login by the OAuth `state` and immediately delete it. The destination URL will be used to redirect the user to the right place. If we have a `discord_user` entry for the OAuth'd Discord user, we immediately log the user into the associated HMN account. This is the typical login case. If we do not have a `discord_user`, but there is exactly one HMN user with the same email address as the Discord user, we will link the two accounts and log into the HMN account. (It is possible for multiple HMN accounts to have the same email, because we don't have a uniqueness constraint there. We fail the login in this case rather than link to the wrong account.) Finally, if no associated HMN user exists, a new one will be created. It will use the Discord user's username, email, and avatar. This user will have no password, but they can set or reset a password through the usual flows. Co-authored-by: Ben Visness <bvisness@gmail.com> Reviewed-on: https://git.handmade.network/hmn/hmn/pulls/106
2023-05-06 19:38:50 +00:00
User *User `json:"user"`
Nick *string `json:"nick"`
Avatar *string `json:"avatar"`
2021-08-24 03:26:27 +00:00
// more fields not yet handled here
}
2021-09-26 22:34:38 +00:00
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{
Add Discord login (#106) This leverages our existing Discord OAuth implementation. Any users with a linked Discord account will be able to log in immediately. When logging in, we request the `email` scope in addition to `identity`, so existing users will be prompted one time to accept the new permissions. On subsequent logins, Discord will skip the prompt. When linking your Discord account to an existing HMN account, we continue to only request the `identity` scope, so we do not receive the user's Discord email. Both login and linking go through the same Discord OAuth callback. All flows through the callback try to achieve the same end goal: a logged-in HMN user with a linked Discord account. Linking works the same as it ever has. Login, however, is different because we do not have a session ID to use as the OAuth state. To account for this, I have added a `pending_login` table that stores a secure unique ID and the eventual destination URL. These pending logins expire after 10 minutes. When we receive the OAuth callback, we look up the pending login by the OAuth `state` and immediately delete it. The destination URL will be used to redirect the user to the right place. If we have a `discord_user` entry for the OAuth'd Discord user, we immediately log the user into the associated HMN account. This is the typical login case. If we do not have a `discord_user`, but there is exactly one HMN user with the same email address as the Discord user, we will link the two accounts and log into the HMN account. (It is possible for multiple HMN accounts to have the same email, because we don't have a uniqueness constraint there. We fail the login in this case rather than link to the wrong account.) Finally, if no associated HMN user exists, a new one will be created. It will use the Discord user's username, email, and avatar. This user will have no password, but they can set or reset a password through the usual flows. Co-authored-by: Ben Visness <bvisness@gmail.com> Reviewed-on: https://git.handmade.network/hmn/hmn/pulls/106
2023-05-06 19:38:50 +00:00
User: UserFromMap(m, "user"),
Nick: maybeStringP(mmap, "nick"),
Avatar: maybeStringP(mmap, "avatar"),
2021-09-26 22:34:38 +00:00
}
return gm
}
2021-08-23 21:52:57 +00:00
// https://discord.com/developers/docs/resources/channel#attachment-object
type Attachment struct {
2021-08-23 21:52:57 +00:00
ID string `json:"id"`
Filename string `json:"filename"`
ContentType *string `json:"content_type"`
Size int `json:"size"`
Url string `json:"url"`
ProxyUrl string `json:"proxy_url"`
Height *int `json:"height"`
Width *int `json:"width"`
}
2021-09-26 22:34:38 +00:00
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),
2021-08-23 21:52:57 +00:00
ContentType: maybeStringP(mmap, "content_type"),
Size: int(mmap["size"].(float64)),
Url: mmap["url"].(string),
ProxyUrl: mmap["proxy_url"].(string),
Height: maybeIntP(mmap, "height"),
Width: maybeIntP(mmap, "width"),
}
2021-09-26 22:34:38 +00:00
return &a
}
2021-08-23 21:52:57 +00:00
// https://discord.com/developers/docs/resources/channel#embed-object
type Embed struct {
Title *string `json:"title"`
Type *string `json:"type"`
Description *string `json:"description"`
Url *string `json:"url"`
Timestamp *string `json:"timestamp"`
Color *int `json:"color"`
Footer *EmbedFooter `json:"footer"`
Image *EmbedImage `json:"image"`
Thumbnail *EmbedThumbnail `json:"thumbnail"`
Video *EmbedVideo `json:"video"`
Provider *EmbedProvider `json:"provider"`
Author *EmbedAuthor `json:"author"`
Fields []EmbedField `json:"fields"`
}
type EmbedFooter struct {
Text string `json:"text"`
IconUrl *string `json:"icon_url"`
ProxyIconUrl *string `json:"proxy_icon_url"`
}
type EmbedImageish struct {
Url *string `json:"url"`
ProxyUrl *string `json:"proxy_url"`
Height *int `json:"height"`
Width *int `json:"width"`
}
type EmbedImage struct {
EmbedImageish
}
type EmbedThumbnail struct {
EmbedImageish
}
type EmbedVideo struct {
EmbedImageish
}
type EmbedProvider struct {
Name *string `json:"name"`
Url *string `json:"url"`
}
type EmbedAuthor struct {
Name *string `json:"name"`
Url *string `json:"url"`
IconUrl *string `json:"icon_url"`
ProxyIconUrl *string `json:"proxy_icon_url"`
}
type EmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline *bool `json:"inline"`
}
2021-09-26 22:34:38 +00:00
func EmbedFromMap(m interface{}) *Embed {
2021-08-23 21:52:57 +00:00
mmap := m.(map[string]interface{})
2021-09-26 22:34:38 +00:00
if mmap == nil {
return nil
}
2021-08-23 21:52:57 +00:00
e := Embed{
Title: maybeStringP(mmap, "title"),
Type: maybeStringP(mmap, "type"),
Description: maybeStringP(mmap, "description"),
Url: maybeStringP(mmap, "url"),
Timestamp: maybeStringP(mmap, "timestamp"),
Color: maybeIntP(mmap, "color"),
Footer: EmbedFooterFromMap(mmap, "footer"),
Image: EmbedImageFromMap(mmap, "image"),
Thumbnail: EmbedThumbnailFromMap(mmap, "thumbnail"),
Video: EmbedVideoFromMap(mmap, "video"),
Provider: EmbedProviderFromMap(mmap, "provider"),
Author: EmbedAuthorFromMap(mmap, "author"),
Fields: EmbedFieldsFromMap(mmap, "fields"),
}
2021-09-26 22:34:38 +00:00
return &e
2021-08-23 21:52:57 +00:00
}
func EmbedFooterFromMap(m map[string]interface{}, k string) *EmbedFooter {
f, ok := m[k]
if !ok {
return nil
}
fMap, ok := f.(map[string]interface{})
if !ok {
return nil
}
return &EmbedFooter{
Text: maybeString(fMap, "text"),
IconUrl: maybeStringP(fMap, "icon_url"),
ProxyIconUrl: maybeStringP(fMap, "proxy_icon_url"),
}
}
func EmbedImageFromMap(m map[string]interface{}, k string) *EmbedImage {
val, ok := m[k]
if !ok {
return nil
}
valMap, ok := val.(map[string]interface{})
if !ok {
return nil
}
return &EmbedImage{
EmbedImageish: EmbedImageish{
Url: maybeStringP(valMap, "url"),
ProxyUrl: maybeStringP(valMap, "proxy_url"),
Height: maybeIntP(valMap, "height"),
Width: maybeIntP(valMap, "width"),
},
}
}
func EmbedThumbnailFromMap(m map[string]interface{}, k string) *EmbedThumbnail {
val, ok := m[k]
if !ok {
return nil
}
valMap, ok := val.(map[string]interface{})
if !ok {
return nil
}
return &EmbedThumbnail{
EmbedImageish: EmbedImageish{
Url: maybeStringP(valMap, "url"),
ProxyUrl: maybeStringP(valMap, "proxy_url"),
Height: maybeIntP(valMap, "height"),
Width: maybeIntP(valMap, "width"),
},
}
}
func EmbedVideoFromMap(m map[string]interface{}, k string) *EmbedVideo {
val, ok := m[k]
if !ok {
return nil
}
valMap, ok := val.(map[string]interface{})
if !ok {
return nil
}
return &EmbedVideo{
EmbedImageish: EmbedImageish{
Url: maybeStringP(valMap, "url"),
ProxyUrl: maybeStringP(valMap, "proxy_url"),
Height: maybeIntP(valMap, "height"),
Width: maybeIntP(valMap, "width"),
},
}
}
func EmbedProviderFromMap(m map[string]interface{}, k string) *EmbedProvider {
val, ok := m[k]
if !ok {
return nil
}
valMap, ok := val.(map[string]interface{})
if !ok {
return nil
}
return &EmbedProvider{
Name: maybeStringP(valMap, "name"),
Url: maybeStringP(valMap, "url"),
}
}
func EmbedAuthorFromMap(m map[string]interface{}, k string) *EmbedAuthor {
val, ok := m[k]
if !ok {
return nil
}
valMap, ok := val.(map[string]interface{})
if !ok {
return nil
}
return &EmbedAuthor{
Name: maybeStringP(valMap, "name"),
Url: maybeStringP(valMap, "url"),
}
}
func EmbedFieldsFromMap(m map[string]interface{}, k string) []EmbedField {
val, ok := m[k]
if !ok {
return nil
}
valSlice, ok := val.([]interface{})
if !ok {
return nil
}
var result []EmbedField
for _, innerVal := range valSlice {
valMap, ok := innerVal.(map[string]interface{})
if !ok {
continue
}
result = append(result, EmbedField{
Name: maybeString(valMap, "name"),
Value: maybeString(valMap, "value"),
Inline: maybeBoolP(valMap, "inline"),
})
}
return result
}
2021-09-26 22:34:38 +00:00
// 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
2021-09-27 01:30:09 +00:00
TargetID string `json:"target_id"` // id the of user or message targetted by a user or message command
2021-09-26 22:34:38 +00:00
}
// 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"),
2021-09-27 01:30:09 +00:00
TargetID: maybeString(mmap, "target_id"),
2021-09-26 22:34:38 +00:00
}
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 {
return ""
}
return val.(string)
}
2021-08-21 16:15:27 +00:00
func maybeStringP(m map[string]interface{}, k string) *string {
val, ok := m[k]
2021-09-26 22:34:38 +00:00
if !ok || val == nil {
2021-08-21 16:15:27 +00:00
return nil
}
strval := val.(string)
return &strval
}
func maybeInt(m map[string]interface{}, k string) int {
val, ok := m[k]
if !ok {
return 0
}
return int(val.(float64))
}
func maybeIntP(m map[string]interface{}, k string) *int {
val, ok := m[k]
2021-09-26 22:34:38 +00:00
if !ok || val == nil {
return nil
}
intval := int(val.(float64))
return &intval
}
2021-08-23 21:52:57 +00:00
func maybeBool(m map[string]interface{}, k string) bool {
val, ok := m[k]
if !ok {
return false
}
return val.(bool)
}
func maybeBoolP(m map[string]interface{}, k string) *bool {
val, ok := m[k]
2021-09-26 22:34:38 +00:00
if !ok || val == nil {
2021-08-23 21:52:57 +00:00
return nil
}
boolval := val.(bool)
return &boolval
}
func maybeArray(m map[string]any, k string) []any {
val, ok := m[k]
if !ok || val == nil {
return nil
}
return val.([]any)
}