commit 313bd35b239ea745da06502dc85d856254a22836 Author: red Date: Mon Jun 2 13:18:10 2025 -0400 initial commit 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"` +}