initial commit

This commit is contained in:
basil 2025-06-02 13:18:10 -04:00
commit 313bd35b23
Signed by: basil
SSH key fingerprint: SHA256:y04xIFL/yqNaG9ae9Vl95vELtHfApGAIoOGLeVLP/fE
8 changed files with 591 additions and 0 deletions

9
channel.go Normal file
View file

@ -0,0 +1,9 @@
package discord
type Channel struct {
ID string `json:"id"`
}
func (u *Channel) BucketID() string {
return u.ID
}

205
component.go Normal file
View file

@ -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,
}
}

30
emoji.go Normal file
View file

@ -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,
}
}

29
guild.go Normal file
View file

@ -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
}

34
http.go Normal file
View file

@ -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)
}

133
interaction.go Normal file
View file

@ -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"`
}

27
message.go Normal file
View file

@ -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,
}
}

124
user.go Normal file
View file

@ -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"`
}