Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Soundboard #283

Draft
wants to merge 22 commits into
base: patch/voice-effect-event
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions bot/client.go
Expand Up @@ -277,6 +277,15 @@ func (c *clientImpl) RequestMembersWithQuery(ctx context.Context, guildID snowfl
})
}

func (c *clientImpl) RequestSoundboardSounds(ctx context.Context, guildIDs ...snowflake.ID) error {
if !c.HasGateway() {
return discord.ErrNoGateway
}
return c.gateway.Send(ctx, gateway.OpcodeRequestSoundboardSounds, gateway.MessageDataRequestSoundboardSounds{
GuildIDs: guildIDs,
})
}

func (c *clientImpl) SetPresence(ctx context.Context, opts ...gateway.PresenceOpt) error {
if !c.HasGateway() {
return discord.ErrNoGateway
Expand Down
38 changes: 26 additions & 12 deletions cache/cache_config.go
Expand Up @@ -9,18 +9,19 @@ import (
// DefaultConfig returns a Config with sensible defaults.
func DefaultConfig() *Config {
return &Config{
GuildCachePolicy: PolicyAll[discord.Guild],
ChannelCachePolicy: PolicyAll[discord.GuildChannel],
StageInstanceCachePolicy: PolicyAll[discord.StageInstance],
GuildScheduledEventCachePolicy: PolicyAll[discord.GuildScheduledEvent],
RoleCachePolicy: PolicyAll[discord.Role],
MemberCachePolicy: PolicyAll[discord.Member],
ThreadMemberCachePolicy: PolicyAll[discord.ThreadMember],
PresenceCachePolicy: PolicyAll[discord.Presence],
VoiceStateCachePolicy: PolicyAll[discord.VoiceState],
MessageCachePolicy: PolicyAll[discord.Message],
EmojiCachePolicy: PolicyAll[discord.Emoji],
StickerCachePolicy: PolicyAll[discord.Sticker],
GuildCachePolicy: PolicyAll[discord.Guild],
ChannelCachePolicy: PolicyAll[discord.GuildChannel],
StageInstanceCachePolicy: PolicyAll[discord.StageInstance],
GuildScheduledEventCachePolicy: PolicyAll[discord.GuildScheduledEvent],
GuildSoundboardSoundCachePolicy: PolicyAll[discord.SoundboardSound],
RoleCachePolicy: PolicyAll[discord.Role],
MemberCachePolicy: PolicyAll[discord.Member],
ThreadMemberCachePolicy: PolicyAll[discord.ThreadMember],
PresenceCachePolicy: PolicyAll[discord.Presence],
VoiceStateCachePolicy: PolicyAll[discord.VoiceState],
MessageCachePolicy: PolicyAll[discord.Message],
EmojiCachePolicy: PolicyAll[discord.Emoji],
StickerCachePolicy: PolicyAll[discord.Sticker],
}
}

Expand All @@ -42,6 +43,9 @@ type Config struct {
GuildScheduledEventCache GuildScheduledEventCache
GuildScheduledEventCachePolicy Policy[discord.GuildScheduledEvent]

GuildSoundboardSoundCache GuildSoundboardSoundCache
GuildSoundboardSoundCachePolicy Policy[discord.SoundboardSound]

RoleCache RoleCache
RoleCachePolicy Policy[discord.Role]

Expand Down Expand Up @@ -90,6 +94,9 @@ func (c *Config) Apply(opts []ConfigOpt) {
if c.GuildScheduledEventCache == nil {
c.GuildScheduledEventCache = NewGuildScheduledEventCache(NewGroupedCache[discord.GuildScheduledEvent](c.CacheFlags, FlagGuildScheduledEvents, c.GuildScheduledEventCachePolicy))
}
if c.GuildSoundboardSoundCache == nil {
c.GuildSoundboardSoundCache = NewGuildSoundboardSoundCache(NewGroupedCache[discord.SoundboardSound](c.CacheFlags, FlagGuildSoundboardSounds, c.GuildSoundboardSoundCachePolicy))
}
if c.RoleCache == nil {
c.RoleCache = NewRoleCache(NewGroupedCache[discord.Role](c.CacheFlags, FlagRoles, c.RoleCachePolicy))
}
Expand Down Expand Up @@ -179,6 +186,13 @@ func WithGuildScheduledEventCache(guildScheduledEventCache GuildScheduledEventCa
}
}

// WithGuildSoundboardSoundCache sets the GuildSoundboardSoundCache of the Config.
func WithGuildSoundboardSoundCache(guildSoundboardSoundCache GuildSoundboardSoundCache) ConfigOpt {
return func(config *Config) {
config.GuildSoundboardSoundCache = guildSoundboardSoundCache
}
}

// WithRoleCachePolicy sets the Policy[discord.Role] of the Config.
func WithRoleCachePolicy(policy Policy[discord.Role]) ConfigOpt {
return func(config *Config) {
Expand Down
4 changes: 3 additions & 1 deletion cache/cache_flags.go
Expand Up @@ -19,6 +19,7 @@ const (
FlagStickers
FlagVoiceStates
FlagStageInstances
FlagGuildSoundboardSounds

FlagsNone Flags = 0
FlagsAll = FlagGuilds |
Expand All @@ -32,7 +33,8 @@ const (
FlagEmojis |
FlagStickers |
FlagVoiceStates |
FlagStageInstances
FlagStageInstances |
FlagGuildSoundboardSounds
)

// Add allows you to add multiple bits together, producing a new bit
Expand Down
79 changes: 65 additions & 14 deletions cache/caches.go
Expand Up @@ -273,6 +273,54 @@ func (c *guildScheduledEventCacheImpl) RemoveGuildScheduledEventsByGuildID(guild
c.cache.GroupRemove(guildID)
}

type GuildSoundboardSoundCache interface {
GuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool)
GuildSoundboardSoundsForEach(guildID snowflake.ID, fn func(sound discord.SoundboardSound))
GuildSoundboardSoundsAllLen() int
GuildSoundboardSoundsLen(guildID snowflake.ID) int
AddGuildSoundboardSound(sound discord.SoundboardSound)
RemoveGuildSoundboardSound(guildID snowflake.ID, sound snowflake.ID) (discord.SoundboardSound, bool)
RemoveGuildSoundboardSoundsByGuildID(guildID snowflake.ID)
}

func NewGuildSoundboardSoundCache(cache GroupedCache[discord.SoundboardSound]) GuildSoundboardSoundCache {
return &guildSoundboardSoundCacheImpl{
cache: cache,
}
}

type guildSoundboardSoundCacheImpl struct {
cache GroupedCache[discord.SoundboardSound]
}

func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool) {
return c.cache.Get(guildID, soundID)
}

func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsForEach(guildID snowflake.ID, fn func(sound discord.SoundboardSound)) {
c.cache.GroupForEach(guildID, fn)
}

func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsAllLen() int {
return c.cache.Len()
}

func (c *guildSoundboardSoundCacheImpl) GuildSoundboardSoundsLen(guildID snowflake.ID) int {
return c.cache.GroupLen(guildID)
}

func (c *guildSoundboardSoundCacheImpl) AddGuildSoundboardSound(sound discord.SoundboardSound) {
c.cache.Put(*sound.GuildID, sound.SoundID, sound)
}

func (c *guildSoundboardSoundCacheImpl) RemoveGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID) (discord.SoundboardSound, bool) {
return c.cache.Remove(guildID, soundID)
}

func (c *guildSoundboardSoundCacheImpl) RemoveGuildSoundboardSoundsByGuildID(guildID snowflake.ID) {
c.cache.GroupRemove(guildID)
}

type RoleCache interface {
Role(guildID snowflake.ID, roleID snowflake.ID) (discord.Role, bool)
RolesForEach(guildID snowflake.ID, fn func(role discord.Role))
Expand Down Expand Up @@ -678,6 +726,7 @@ type Caches interface {
ChannelCache
StageInstanceCache
GuildScheduledEventCache
GuildSoundboardSoundCache
RoleCache
MemberCache
ThreadMemberCache
Expand Down Expand Up @@ -759,20 +808,21 @@ func New(opts ...ConfigOpt) Caches {
config.Apply(opts)

return &cachesImpl{
config: *config,
SelfUserCache: config.SelfUserCache,
GuildCache: config.GuildCache,
ChannelCache: config.ChannelCache,
StageInstanceCache: config.StageInstanceCache,
GuildScheduledEventCache: config.GuildScheduledEventCache,
RoleCache: config.RoleCache,
MemberCache: config.MemberCache,
ThreadMemberCache: config.ThreadMemberCache,
PresenceCache: config.PresenceCache,
VoiceStateCache: config.VoiceStateCache,
MessageCache: config.MessageCache,
EmojiCache: config.EmojiCache,
StickerCache: config.StickerCache,
config: *config,
SelfUserCache: config.SelfUserCache,
GuildCache: config.GuildCache,
ChannelCache: config.ChannelCache,
StageInstanceCache: config.StageInstanceCache,
GuildScheduledEventCache: config.GuildScheduledEventCache,
GuildSoundboardSoundCache: config.GuildSoundboardSoundCache,
RoleCache: config.RoleCache,
MemberCache: config.MemberCache,
ThreadMemberCache: config.ThreadMemberCache,
PresenceCache: config.PresenceCache,
VoiceStateCache: config.VoiceStateCache,
MessageCache: config.MessageCache,
EmojiCache: config.EmojiCache,
StickerCache: config.StickerCache,
}
}

Expand All @@ -783,6 +833,7 @@ type cachesImpl struct {
ChannelCache
StageInstanceCache
GuildScheduledEventCache
GuildSoundboardSoundCache
RoleCache
MemberCache
ThreadMemberCache
Expand Down
6 changes: 6 additions & 0 deletions discord/audit_log.go
Expand Up @@ -108,6 +108,12 @@ const (
AuditLogApplicationCommandPermissionUpdate AuditLogEvent = 121
)

const (
AuditLogSoundboardSoundCreate AuditLogEvent = iota + 130
AuditLogSoundboardSoundUpdate
AuditLogSoundboardSoundDelete
)

const (
AuditLogAutoModerationRuleCreate AuditLogEvent = iota + 140
AuditLogAutoModerationRuleUpdate
Expand Down
2 changes: 2 additions & 0 deletions discord/cdn_endpoints.go
Expand Up @@ -45,6 +45,8 @@ var (
CustomSticker = NewCDN("/stickers/{sticker.id}", FileFormatPNG, FileFormatLottie, FileFormatGIF)

AttachmentFile = NewCDN("/attachments/{channel.id}/{attachment.id}/{file.name}", FileFormatNone)

SoundboardSoundFile = NewCDN("/soundboard-sounds/{sound.id}", FileFormatNone)
)

// FileFormat is the type of file on Discord's CDN (https://discord.com/developers/docs/reference#image-formatting-image-formats)
Expand Down
2 changes: 2 additions & 0 deletions discord/guild.go
Expand Up @@ -119,6 +119,7 @@ const (
GuildFeatureRoleIcons GuildFeature = "ROLE_ICONS"
GuildFeatureRoleSubscriptionsAvailableForPurchase GuildFeature = "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"
GuildFeatureRoleSubscriptionsEnabled GuildFeature = "ROLE_SUBSCRIPTIONS_ENABLED"
GuildFeatureSoundboard GuildFeature = "SOUNDBOARD"
GuildFeatureTicketedEventsEnabled GuildFeature = "TICKETED_EVENTS_ENABLED"
GuildFeatureVanityURL GuildFeature = "VANITY_URL"
GuildFeatureVerified GuildFeature = "VERIFIED"
Expand Down Expand Up @@ -224,6 +225,7 @@ type GatewayGuild struct {
Presences []Presence `json:"presences"`
StageInstances []StageInstance `json:"stage_instances"`
GuildScheduledEvents []GuildScheduledEvent `json:"guild_scheduled_events"`
SoundboardSounds []SoundboardSound `json:"soundboard_sounds"`
}

func (g *GatewayGuild) UnmarshalJSON(data []byte) error {
Expand Down
59 changes: 59 additions & 0 deletions discord/sound.go
@@ -0,0 +1,59 @@
package discord

import (
"encoding/base64"
"fmt"
"io"

"github.com/disgoorg/json"
)

type SoundType string

const (
SoundTypeMP3 SoundType = "audio/mpeg"
SoundTypeOGG SoundType = "audio/ogg"
SoundTypeWAV SoundType = "audio/wav"
SoundTypeUnknown = SoundTypeMP3
)

func (t SoundType) GetMIME() string {
return string(t)
}

func (t SoundType) GetHeader() string {
return "data:" + string(t) + ";base64"
}

var _ json.Marshaler = (*Sound)(nil)
var _ fmt.Stringer = (*Sound)(nil)

func NewSound(soundType SoundType, reader io.Reader) (*Sound, error) {
data, err := io.ReadAll(reader)
if err != nil {
return nil, err
}
return NewSoundRaw(soundType, data), nil
}

func NewSoundRaw(soundType SoundType, src []byte) *Sound {
data := make([]byte, base64.StdEncoding.EncodedLen(len(src)))
base64.StdEncoding.Encode(data, src)
return &Sound{Type: soundType, Data: data}
}

type Sound struct {
Type SoundType
Data []byte
}

func (s Sound) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}

func (s Sound) String() string {
if len(s.Data) == 0 {
return ""
}
return s.Type.GetHeader() + "," + string(s.Data)
}
34 changes: 34 additions & 0 deletions discord/soundboard.go
@@ -1,8 +1,42 @@
package discord

import (
"github.com/disgoorg/json"
"github.com/disgoorg/snowflake/v2"
)

type SoundboardEffectAnimationType int

const (
SoundboardEffectAnimationTypePremium SoundboardEffectAnimationType = iota
SoundboardEffectAnimationTypeBasic
)

type SoundboardSound struct {
Name string `json:"name"`
SoundID snowflake.ID `json:"sound_id"`
ID *snowflake.ID `json:"id"`
Volume float64 `json:"volume"`
EmojiID *snowflake.ID `json:"emoji_id"`
EmojiName *string `json:"emoji_name"`
OverridePath *string `json:"override_path"`
GuildID *snowflake.ID `json:"guild_id,omitempty"`
UserID snowflake.ID `json:"user_id"`
Available *bool `json:"available,omitempty"`
User *User `json:"user"`
}

type SoundboardSoundCreate struct {
Name string `json:"name"`
Sound Sound `json:"sound"`
Volume *float64 `json:"volume,omitempty"`
EmojiID snowflake.ID `json:"emoji_id,omitempty"`
EmojiName string `json:"emoji_name,omitempty"`
}

type SoundboardSoundUpdate struct {
Name *string `json:"name,omitempty"`
Volume *json.Nullable[float64] `json:"volume,omitempty"`
EmojiID *json.Nullable[snowflake.ID] `json:"emoji_id,omitempty"`
EmojiName *json.Nullable[string] `json:"emoji_name,omitempty"`
}
37 changes: 37 additions & 0 deletions events/guild_soundboard_events.go
@@ -0,0 +1,37 @@
package events

import (
"github.com/disgoorg/disgo/discord"
"github.com/disgoorg/snowflake/v2"
)

// GenericGuildSoundboardSound is called upon receiving GuildSoundboardSoundCreate and GuildSoundboardSoundUpdate
type GenericGuildSoundboardSound struct {
*GenericEvent
discord.SoundboardSound
}

// GuildSoundboardSoundCreate indicates that a discord.SoundboardSound was created in a discord.Guild
type GuildSoundboardSoundCreate struct {
*GenericGuildSoundboardSound
}

// GuildSoundboardSoundUpdate indicates that a discord.SoundboardSound was updated in a discord.Guild
type GuildSoundboardSoundUpdate struct {
*GenericGuildSoundboardSound
OldGuildSoundboardSound discord.SoundboardSound
}

// GuildSoundboardSoundDelete indicates that a discord.SoundboardSound was deleted in a discord.Guild
type GuildSoundboardSoundDelete struct {
*GenericEvent
SoundID snowflake.ID
GuildID snowflake.ID
}

// SoundboardSounds is a response to gateway.OpcodeRequestSoundboardSounds
type SoundboardSounds struct {
*GenericEvent
SoundboardSounds []discord.SoundboardSound
GuildID snowflake.ID
}