From 313bd35b239ea745da06502dc85d856254a22836 Mon Sep 17 00:00:00 2001 From: red Date: Mon, 2 Jun 2025 13:18:10 -0400 Subject: [PATCH] initial commit --- channel.go | 9 +++ component.go | 205 +++++++++++++++++++++++++++++++++++++++++++++++++ emoji.go | 30 ++++++++ guild.go | 29 +++++++ http.go | 34 ++++++++ interaction.go | 133 ++++++++++++++++++++++++++++++++ message.go | 27 +++++++ user.go | 124 ++++++++++++++++++++++++++++++ 8 files changed, 591 insertions(+) create mode 100644 channel.go create mode 100644 component.go create mode 100644 emoji.go create mode 100644 guild.go create mode 100644 http.go create mode 100644 interaction.go create mode 100644 message.go create mode 100644 user.go diff --git a/channel.go b/channel.go new file mode 100644 index 0000000..b6f5fe3 --- /dev/null +++ b/channel.go @@ -0,0 +1,9 @@ +package discord + +type Channel struct { + ID string `json:"id"` +} + +func (u *Channel) BucketID() string { + return u.ID +} diff --git a/component.go b/component.go new file mode 100644 index 0000000..6969930 --- /dev/null +++ b/component.go @@ -0,0 +1,205 @@ +package discord + +type ComponentType int + +const ( + ComponentTypeActionRow ComponentType = 1 + ComponentTypeButton ComponentType = 2 + ComponentTypeSection ComponentType = 9 + ComponentTypeText ComponentType = 10 + ComponentTypeThumbnail ComponentType = 11 + ComponentTypeSep ComponentType = 14 + ComponentTypeContainer ComponentType = 17 +) + +type BaseComponent struct { + ID *int `json:"id,omitempty"` + Type ComponentType `json:"type"` +} + +type ComponentLike interface { + Base() BaseComponent +} + +type AccessoryLike interface { + AccessoryLike() bool +} + +type TextDisplayLike interface { + TextDisplayLike() bool +} + +type ActionRowComponent struct { + BaseComponent + Components []ComponentLike `json:"components"` +} + +func (t ActionRowComponent) Base() BaseComponent { + return t.BaseComponent +} + +func ActionRow(components ...ComponentLike) ActionRowComponent { + return ActionRowComponent{ + BaseComponent: BaseComponent{ + Type: ComponentTypeActionRow, + }, + Components: components, + } +} + +type TextComponent struct { + BaseComponent + Content string `json:"content"` +} + +func (t TextComponent) Base() BaseComponent { + return t.BaseComponent +} + +func (t TextComponent) TextDisplayLike() bool { + return true +} + +type ContainerComponent struct { + BaseComponent + Components any `json:"components"` + Color *int `json:"accent_color,omitempty"` +} + +func (t ContainerComponent) Base() BaseComponent { + return t.BaseComponent +} + +type UnfurledMedia struct { + URL string `json:"url"` +} + +type ThumbnailComponent struct { + BaseComponent + Media UnfurledMedia `json:"unfurledMedia"` + Description string `json:"description"` +} + +func (t ThumbnailComponent) Base() BaseComponent { + return t.BaseComponent +} + +func (t ThumbnailComponent) AccessoryLike() bool { + return true +} + +type SectionComponent struct { + BaseComponent + Components []TextDisplayLike `json:"components"` + Accessory AccessoryLike `json:"accessory"` +} + +func (t SectionComponent) Base() BaseComponent { + return t.BaseComponent +} + +func Section(text []TextDisplayLike, accessory AccessoryLike) SectionComponent { + return SectionComponent{ + BaseComponent: BaseComponent{ + Type: ComponentTypeSection, + }, + Components: text, + Accessory: accessory, + } +} + +type SeparatorComponent struct { + BaseComponent + Divider bool `json:"divider"` + Spacing int `json:"spacing"` +} + +func (t SeparatorComponent) Base() BaseComponent { + return t.BaseComponent +} + +type ButtonStyle int + +const ( + ButtonStylePrimary ButtonStyle = iota + 1 + ButtonStyleSecondary + ButtonStyleSuccess + ButtonStyleDanger + ButtonStyleLink + ButtonStylePremium +) + +type ButtonComponent struct { + BaseComponent + + Style ButtonStyle `json:"style"` + Label string `json:"label"` + + Emoji *PartialEmoji `json:"emoji,omitempty"` + CustomID string `json:"custom_id,omitempty"` + URL string `json:"url,omitempty"` +} + +func (t ButtonComponent) Base() BaseComponent { + return t.BaseComponent +} + +func (t ButtonComponent) AccessoryLike() bool { + return true +} + +func Container(id int, color *int, components ...any) ContainerComponent { + return ContainerComponent{ + BaseComponent: BaseComponent{ + ID: &id, + Type: ComponentTypeContainer, + }, + Color: color, + Components: components, + } +} + +func Text(id int, content string) TextComponent { + return TextComponent{ + BaseComponent: BaseComponent{ + ID: &id, + Type: ComponentTypeText, + }, + Content: content, + } +} + +func Separator(id int, divider bool, spacing int) SeparatorComponent { + return SeparatorComponent{ + BaseComponent: BaseComponent{ + ID: &id, + Type: ComponentTypeSep, + }, + Divider: divider, + Spacing: spacing, + } +} + +func Button(style ButtonStyle, customID, label string, emoji *PartialEmoji) ButtonComponent { + return ButtonComponent{ + BaseComponent: BaseComponent{ + Type: ComponentTypeButton, + }, + Style: style, + CustomID: customID, + Label: label, + Emoji: emoji, + } +} + +func LinkButton(label, url string, emoji *PartialEmoji) ButtonComponent { + return ButtonComponent{ + BaseComponent: BaseComponent{ + Type: ComponentTypeButton, + }, + Style: ButtonStyleLink, + URL: url, + Label: label, + Emoji: emoji, + } +} diff --git a/emoji.go b/emoji.go new file mode 100644 index 0000000..c59ac9a --- /dev/null +++ b/emoji.go @@ -0,0 +1,30 @@ +package discord + +type PartialEmoji struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +func (u *PartialEmoji) BucketID() string { + return u.ID +} + +type Emoji struct { + PartialEmoji + Roles []string `json:"roles"` + User any `json:"user,omitempty"` // TODO: User object + RequireColons bool `json:"requires_colons"` + Managed bool `json:"managed"` + Animated bool `json:"animated"` + Available bool `json:"available"` +} + +func (u *Emoji) BucketID() string { + return u.ID +} + +func StandardEmoji(emoji string) *PartialEmoji { + return &PartialEmoji{ + Name: emoji, + } +} diff --git a/guild.go b/guild.go new file mode 100644 index 0000000..9aff0e2 --- /dev/null +++ b/guild.go @@ -0,0 +1,29 @@ +package discord + +import "time" + +type Guild struct { + ID string `json:"id"` +} + +type GuildMemberFlags int + +const ( + GuildMemberFlagsDidRejoin GuildMemberFlags = iota +) + +type GuildMember struct { + User User `json:"user"` + Nick string `json:"nick"` + Avatar string `json:"avatar"` + Banner string `json:"banner"` + Roles []string `json:"roles"` + JoinedAt time.Time `json:"joined_at"` + PremiumSince time.Time `json:"premium_since"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` +} + +func (u *Guild) BucketID() string { + return u.ID +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..4c787e7 --- /dev/null +++ b/http.go @@ -0,0 +1,34 @@ +package discord + +import ( + "bytes" + "encoding/json" + "net/http" +) + +type Identifiable interface { + BucketID() string +} + +func requestWithToken( + token string, + client *http.Client, + method, url string, + body any, +) (*http.Response, error) { + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(body) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(method, url, buf) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bot "+token) + req.Header.Set("Content-Type", "application/json") + + return http.DefaultClient.Do(req) +} diff --git a/interaction.go b/interaction.go new file mode 100644 index 0000000..06481c4 --- /dev/null +++ b/interaction.go @@ -0,0 +1,133 @@ +package discord + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type InteractionContextType int + +const ( + InteractionContextTypeGuild InteractionContextType = iota + InteractionContextTypeBotDM + InteractionContextTypePrivateChannel +) + +type InteractionType int + +const ( + InteractionTypePing InteractionType = iota + 1 + InteractionTypeApplicationCommand + InteractionTypeMessageComponent + InteractionTypeApplicationCommandAutocomplete + InteractionTypeModalSubmit +) + +type Interaction[T any] struct { + ID string `json:"id"` + ApplicationID string `json:"application_id"` + Type InteractionType `json:"type"` + Data T `json:"data"` + Guild any `json:"guild"` // TODO: Partial guilds + GuildID string `json:"guild_id"` + Channel any `json:"channel"` // TODO: Partial channels + ChannelID string `json:"channel_id"` + Member GuildMember `json:"member"` // TODO: Guild member object + User User `json:"user"` + Token string `json:"token"` + Version int `json:"version"` + Message any `json:"message"` // TODO: Message object + AppPermissions string `json:"app_permissions"` + GuildLocale string `json:"guild_locale"` + Entitlements []any `json:"entitlements"` // TODO: Entitlements + + Context InteractionContextType `json:"context"` + AttachmentSizeLimit int `json:"attachment_size_limit"` + + AuthorizingIntegrationOwners map[string]any `json:"authorizing_integration_owners"` // TODO: Yeah maybe not? + + followupCount int +} + +func (i *Interaction[T]) CallbackURL() string { + return fmt.Sprintf( + "https://discord.com/api/v10/interactions/%s/%s/callback", + i.ID, i.Token, + ) +} + +func (i *Interaction[T]) FollowupURL() string { + return fmt.Sprintf( + "https://discord.com/api/v10/webhooks/%s/%s", i.ApplicationID, i.Token, + ) +} + +func (i *Interaction[T]) Reply(token string, message ComponentsV2Message) (*http.Response, error) { + return requestWithToken( + token, http.DefaultClient, "POST", i.CallbackURL(), + InteractionResponse[ComponentsV2Message]{ + Type: InteractionResponseTypeChannelMessageWithSource, + Data: message, + }, + ) +} + +func (i *Interaction[T]) Defer(token string) (*http.Response, error) { + return requestWithToken( + token, http.DefaultClient, "POST", i.CallbackURL(), + InteractionResponse[any]{ + Type: InteractionResponseTypeDeferredChannelMessageWithSource, + }, + ) +} + +func (i *Interaction[T]) Followup(msg ComponentsV2Message) (*http.Response, error) { + if i.followupCount >= 5 { + return nil, errors.New("too many followups") + } + + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(msg) + if err != nil { + return nil, err + } + + url := "https://discord.com/api/v10/webhooks/" + i.ApplicationID + "/" + i.Token + + req, err := http.NewRequest("POST", url, buf) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + i.followupCount++ + + return resp, err +} + +type InteractionResponseType int + +const ( + InteractionResponseTypePong InteractionResponseType = 1 + + InteractionResponseTypeChannelMessageWithSource InteractionResponseType = iota + 3 + InteractionResponseTypeDeferredChannelMessageWithSource + InteractionResponseTypeDeferredUpdateMessage + InteractionResponseTypeUpdateMessage + InteractionResponseTypeApplicationCommandAutocompleteResult + InteractionResponseTypeModal + + InteractionResponseTypePremiumRequired // deprecated + + InteractionResponseTypeLaunchActivity InteractionResponseType = 12 +) + +type InteractionResponse[T any] struct { + Type InteractionResponseType `json:"type"` + Data T `json:"data,omitempty"` +} diff --git a/message.go b/message.go new file mode 100644 index 0000000..8ae22fd --- /dev/null +++ b/message.go @@ -0,0 +1,27 @@ +package discord + +type MessageFlags int + +const ( + MessageFlagsEphemeral MessageFlags = 1 << 6 + MessageFlagsComponentsV2 MessageFlags = 1 << 15 +) + +type ComponentsV2Message struct { + Components []ComponentLike `json:"components"` + Flags MessageFlags `json:"flags"` +} + +func ComponentsMessage(components ...ComponentLike) ComponentsV2Message { + return ComponentsV2Message{ + Components: components, + Flags: MessageFlagsComponentsV2, + } +} + +func EphemeralComponentsMessage(components ...ComponentLike) ComponentsV2Message { + return ComponentsV2Message{ + Components: components, + Flags: MessageFlagsComponentsV2 | MessageFlagsEphemeral, + } +} diff --git a/user.go b/user.go new file mode 100644 index 0000000..7abb093 --- /dev/null +++ b/user.go @@ -0,0 +1,124 @@ +package discord + +import "time" + +type CacheStatus int + +const ( + CacheStatusNotFound CacheStatus = iota + CacheStatusFound + CacheStatusRefresh + CacheStatusRemove +) + +type cacheUser struct { + lastUpdated time.Time + user *User + remove bool // if true, references to this should be removed +} + +type UserCache struct { + guildID string + lastUpdated time.Time + frequency time.Duration + users map[string]*cacheUser +} + +func NewUserCache(guildID string, updateFrequency time.Duration) *UserCache { + return &UserCache{ + guildID: guildID, + lastUpdated: time.Unix(0, 0), + frequency: updateFrequency, + users: make(map[string]*cacheUser), + } +} + +func (u *UserCache) Insert(user *User) { + cu, ok := u.users[user.ID] + if ok { + cu.user = user + cu.lastUpdated = time.Now() + cu.remove = false + } + u.users[user.ID] = &cacheUser{ + user: user, + lastUpdated: time.Now(), + remove: false, + } +} + +func (u *UserCache) Find(userID string) (*User, CacheStatus) { + cu, ok := u.users[userID] + if !ok { + return nil, CacheStatusNotFound + } + + if cu.remove { + return nil, CacheStatusRemove + } + + if time.Since(cu.lastUpdated) > u.frequency { + return cu.user, CacheStatusRefresh + } + + return cu.user, CacheStatusFound +} + +type UserFlags int + +const ( + UserFlagsStaff UserFlags = 1 << 1 + UserFlagsPartner UserFlags = 1 << 2 + UserFlagsHypesquad UserFlags = 1 << 3 + UserFlagsBugHunterLevel1 UserFlags = 1 << 4 + UserFlagsHypesquadOnlineHouse1 UserFlags = 1 << 6 + UserFlagsHypesquadOnlineHouse2 UserFlags = 1 << 7 + UserFlagsHypesquadOnlineHouse3 UserFlags = 1 << 8 + UserFlagsPremiumEarlySupporter UserFlags = 1 << 9 + UserFlagsTeamPseudoUser UserFlags = 1 << 10 + UserFlagsBugHunterLevel2 UserFlags = 1 << 14 + UserFlagsVerifiedBot UserFlags = 1 << 16 + UserFlagsVerifiedDeveloper UserFlags = 1 << 17 + UserFlagsCertifiedModerator UserFlags = 1 << 18 + UserFlagsBotHTTPInteractions UserFlags = 1 << 19 + UserFlagsActiveDeveloper UserFlags = 1 << 22 +) + +type PremiumType int + +const ( + PremiumTypeNone PremiumType = iota + PremiumTypeNitroClassic + PremiumTypeNitro + PremiumTypeNitroBasic +) + +// TODO: more fields +type User struct { + ID string `json:"id"` + Username string `json:"username"` + GlobalName string `json:"global_name"` + Avatar string `json:"avatar"` + Bot bool `json:"bot"` + System bool `json:"system"` + MFAEnabled bool `json:"mfa_enabled"` + Banner string `json:"banner"` + AccentColor int `json:"accent_color"` + Locale string `json:"locale"` + Verified bool `json:"verified"` + Email string `json:"email"` + Flags UserFlags `json:"flags"` + PublicFlags UserFlags `json:"public_flags"` + PremiumType PremiumType `json:"premium_type"` + + AvatarDecorationData AvatarDecorationData `json:"avatar_decoration_data"` +} + +func (u *User) BucketID() string { + return u.ID +} + +type AvatarDecorationData struct { + Asset string `json:"asset"` + SkuID string `json:"sku_id"` +}