1033 lines
30 KiB
Go
1033 lines
30 KiB
Go
package discord
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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)),
|
|
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),
|
|
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
|
|
|
|
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
|
const (
|
|
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
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
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"`
|
|
// 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
|
|
|
|
// 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"`
|
|
Embeds []Embed `json:"embeds"`
|
|
|
|
originalMap map[string]interface{}
|
|
Backfilled bool
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
for _, field := range fields {
|
|
_, ok := m.originalMap[field]
|
|
if !ok {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
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 := 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")),
|
|
Flags: MessageFlags(maybeInt(mmap, "flags")),
|
|
|
|
originalMap: mmap,
|
|
}
|
|
|
|
if iattachments, ok := mmap["attachments"]; ok {
|
|
attachments := iattachments.([]interface{})
|
|
for _, iattachment := range attachments {
|
|
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))
|
|
}
|
|
}
|
|
|
|
return &msg
|
|
}
|
|
|
|
// https://discord.com/developers/docs/resources/user#user-object
|
|
type User struct {
|
|
ID string `json:"id"`
|
|
Username string `json:"username"`
|
|
Discriminator string `json:"discriminator"`
|
|
Avatar *string `json:"avatar"`
|
|
IsBot bool `json:"bot"`
|
|
Locale string `json:"locale"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
type GuildMember struct {
|
|
User *User `json:"user"`
|
|
Nick *string `json:"nick"`
|
|
Avatar *string `json:"avatar"`
|
|
// 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"),
|
|
Avatar: maybeStringP(mmap, "avatar"),
|
|
}
|
|
|
|
return gm
|
|
}
|
|
|
|
// https://discord.com/developers/docs/resources/channel#attachment-object
|
|
type Attachment struct {
|
|
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"`
|
|
}
|
|
|
|
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),
|
|
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"),
|
|
}
|
|
|
|
return &a
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
func EmbedFromMap(m interface{}) *Embed {
|
|
mmap := m.(map[string]interface{})
|
|
if mmap == nil {
|
|
return nil
|
|
}
|
|
|
|
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"),
|
|
}
|
|
|
|
return &e
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
TargetID string `json:"target_id"` // id the of user or message targetted by a user or message command
|
|
}
|
|
|
|
// 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"),
|
|
TargetID: maybeString(mmap, "target_id"),
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func maybeStringP(m map[string]interface{}, k string) *string {
|
|
val, ok := m[k]
|
|
if !ok || val == nil {
|
|
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]
|
|
if !ok || val == nil {
|
|
return nil
|
|
}
|
|
intval := int(val.(float64))
|
|
return &intval
|
|
}
|
|
|
|
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]
|
|
if !ok || val == nil {
|
|
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)
|
|
}
|