hmn/src/discord/payloads.go

645 lines
16 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)
}
// TODO: check if the payload is too big, either here or where we actually send
// https://discord.com/developers/docs/topics/gateway#sending-payloads
return mBytes
}
type Hello struct {
HeartbeatIntervalMs int `json:"heartbeat_interval"`
}
func HelloFromMap(m interface{}) Hello {
// TODO: This should probably have some error handling, right?
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
}
// 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"`
}
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
)
// 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"`
Attachments []Attachment `json:"attachments"`
Embeds []Embed `json:"embeds"`
originalMap map[string]interface{}
}
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{}) 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{})
msg := Message{
ID: mmap["id"].(string),
ChannelID: mmap["channel_id"].(string),
GuildID: maybeStringP(mmap, "guild_id"),
Content: maybeString(mmap, "content"),
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))
}
}
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"`
}
func UserFromMap(m interface{}) User {
mmap := m.(map[string]interface{})
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
}
// https://discord.com/developers/docs/resources/guild#guild-member-object
type GuildMember struct {
User *User `json:"user"`
Nick *string `json:"nick"`
// more fields not yet handled here
}
// 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{}) Attachment {
mmap := m.(map[string]interface{})
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{})
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
}
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 {
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 {
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 {
return nil
}
boolval := val.(bool)
return &boolval
}