diff --git a/bot/client.go b/bot/client.go index 82f90f42..eb593651 100644 --- a/bot/client.go +++ b/bot/client.go @@ -92,6 +92,9 @@ type Client interface { // limit : The number of discord.Member(s) to return. RequestMembersWithQuery(ctx context.Context, guildID snowflake.ID, presence bool, nonce string, query string, limit int) error + // RequestSoundboardSounds a gateway.MessageDataRequestSoundboardSounds to the specific gateway.Gateway and requests the SoundboardSounds of the specified guilds. + RequestSoundboardSounds(ctx context.Context, guildIDs ...snowflake.ID) error + // SetPresence sends new presence data to the gateway.Gateway. SetPresence(ctx context.Context, opts ...gateway.PresenceOpt) error @@ -277,6 +280,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 diff --git a/cache/cache_config.go b/cache/cache_config.go index dc039545..6ed76a16 100644 --- a/cache/cache_config.go +++ b/cache/cache_config.go @@ -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], } } @@ -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] @@ -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)) } @@ -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) { diff --git a/cache/cache_flags.go b/cache/cache_flags.go index 2be7d5f5..dc3b80fb 100644 --- a/cache/cache_flags.go +++ b/cache/cache_flags.go @@ -19,6 +19,7 @@ const ( FlagStickers FlagVoiceStates FlagStageInstances + FlagGuildSoundboardSounds FlagsNone Flags = 0 FlagsAll = FlagGuilds | @@ -32,7 +33,8 @@ const ( FlagEmojis | FlagStickers | FlagVoiceStates | - FlagStageInstances + FlagStageInstances | + FlagGuildSoundboardSounds ) // Add allows you to add multiple bits together, producing a new bit diff --git a/cache/caches.go b/cache/caches.go index 3c2edb5f..dbc9cdfa 100644 --- a/cache/caches.go +++ b/cache/caches.go @@ -296,6 +296,59 @@ func (c *guildScheduledEventCacheImpl) RemoveGuildScheduledEventsByGuildID(guild c.cache.GroupRemove(guildID) } +type GuildSoundboardSoundCache interface { + GuildSoundboardSoundCache() GroupedCache[discord.SoundboardSound] + 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) GuildSoundboardSoundCache() GroupedCache[discord.SoundboardSound] { + return c.cache +} + +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 { RoleCache() GroupedCache[discord.Role] @@ -749,6 +802,7 @@ type Caches interface { ChannelCache StageInstanceCache GuildScheduledEventCache + GuildSoundboardSoundCache RoleCache MemberCache ThreadMemberCache @@ -830,38 +884,40 @@ 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, } } // these type aliases are needed to allow having the GuildCache, ChannelCache, etc. as methods on the cachesImpl struct type ( - guildCache = GuildCache - channelCache = ChannelCache - stageInstanceCache = StageInstanceCache - guildScheduledEventCache = GuildScheduledEventCache - roleCache = RoleCache - memberCache = MemberCache - threadMemberCache = ThreadMemberCache - presenceCache = PresenceCache - voiceStateCache = VoiceStateCache - messageCache = MessageCache - emojiCache = EmojiCache - stickerCache = StickerCache - selfUserCache = SelfUserCache + guildCache = GuildCache + channelCache = ChannelCache + stageInstanceCache = StageInstanceCache + guildScheduledEventCache = GuildScheduledEventCache + guildSoundboardSoundCache = GuildSoundboardSoundCache + roleCache = RoleCache + memberCache = MemberCache + threadMemberCache = ThreadMemberCache + presenceCache = PresenceCache + voiceStateCache = VoiceStateCache + messageCache = MessageCache + emojiCache = EmojiCache + stickerCache = StickerCache + selfUserCache = SelfUserCache ) type cachesImpl struct { @@ -871,6 +927,7 @@ type cachesImpl struct { channelCache stageInstanceCache guildScheduledEventCache + guildSoundboardSoundCache roleCache memberCache threadMemberCache diff --git a/discord/audit_log.go b/discord/audit_log.go index f859b454..50b5b4de 100644 --- a/discord/audit_log.go +++ b/discord/audit_log.go @@ -95,6 +95,12 @@ const ( AuditLogApplicationCommandPermissionUpdate AuditLogEvent = 121 ) +const ( + AuditLogSoundboardSoundCreate AuditLogEvent = iota + 130 + AuditLogSoundboardSoundUpdate + AuditLogSoundboardSoundDelete +) + const ( AuditLogAutoModerationRuleCreate AuditLogEvent = iota + 140 AuditLogAutoModerationRuleUpdate diff --git a/discord/cdn_endpoints.go b/discord/cdn_endpoints.go index 88ca24aa..a36598ac 100644 --- a/discord/cdn_endpoints.go +++ b/discord/cdn_endpoints.go @@ -46,6 +46,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) diff --git a/discord/guild.go b/discord/guild.go index 3da1f8a7..03a383de 100644 --- a/discord/guild.go +++ b/discord/guild.go @@ -111,6 +111,7 @@ const ( GuildFeatureInvitesDisabled GuildFeature = "INVITES_DISABLED" GuildFeatureInviteSplash GuildFeature = "INVITE_SPLASH" GuildFeatureMemberVerificationGateEnabled GuildFeature = "MEMBER_VERIFICATION_GATE_ENABLED" + GuildFeatureMoreSoundboard GuildFeature = "MORE_SOUNDBOARD" GuildFeatureMoreStickers GuildFeature = "MORE_STICKERS" GuildFeatureNews GuildFeature = "NEWS" GuildFeaturePartnered GuildFeature = "PARTNERED" @@ -119,6 +120,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" @@ -224,6 +226,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 { diff --git a/discord/sound.go b/discord/sound.go new file mode 100644 index 00000000..0bd2908f --- /dev/null +++ b/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) MIME() string { + return string(t) +} + +func (t SoundType) Header() 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.Header() + "," + string(s.Data) +} diff --git a/discord/soundboard.go b/discord/soundboard.go index c8df7d90..0dc4b72a 100644 --- a/discord/soundboard.go +++ b/discord/soundboard.go @@ -1,8 +1,48 @@ package discord +import ( + "github.com/disgoorg/json" + "github.com/disgoorg/snowflake/v2" +) + type VoiceChannelEffectAnimationType int const ( VoiceChannelEffectAnimationTypePremium VoiceChannelEffectAnimationType = iota VoiceChannelEffectAnimationTypeBasic ) + +type SoundboardSound struct { + Name string `json:"name"` + SoundID snowflake.ID `json:"sound_id"` + Volume float64 `json:"volume"` + EmojiID *snowflake.ID `json:"emoji_id"` + EmojiName *string `json:"emoji_name"` + GuildID *snowflake.ID `json:"guild_id,omitempty"` + Available *bool `json:"available,omitempty"` + User *User `json:"user,omitempty"` +} + +func (s SoundboardSound) URL(opts ...CDNOpt) string { + return formatAssetURL(SoundboardSoundFile, opts, s.SoundID) +} + +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"` +} + +type SendSoundboardSound struct { + SoundID snowflake.ID `json:"sound_id"` + SourceGuildID *snowflake.ID `json:"source_guild_id,omitempty"` +} diff --git a/events/guild_emoji_events.go b/events/guild_emoji_events.go index 8ffa30d2..80d97235 100644 --- a/events/guild_emoji_events.go +++ b/events/guild_emoji_events.go @@ -14,25 +14,25 @@ type EmojisUpdate struct { gateway.EventGuildEmojisUpdate } -// GenericEmoji is called upon receiving EmojiCreate , EmojiUpdate or EmojiDelete (requires gateway.IntentGuildEmojisAndStickers) +// GenericEmoji is called upon receiving EmojiCreate , EmojiUpdate or EmojiDelete (requires gateway.IntentGuildExpressions) type GenericEmoji struct { *GenericEvent GuildID snowflake.ID Emoji discord.Emoji } -// EmojiCreate indicates that a new discord.Emoji got created in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiCreate indicates that a new discord.Emoji got created in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiCreate struct { *GenericEmoji } -// EmojiUpdate indicates that a discord.Emoji got updated in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiUpdate indicates that a discord.Emoji got updated in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiUpdate struct { *GenericEmoji OldEmoji discord.Emoji } -// EmojiDelete indicates that a discord.Emoji got deleted in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// EmojiDelete indicates that a discord.Emoji got deleted in a discord.Guild (requires gateway.IntentGuildExpressions) type EmojiDelete struct { *GenericEmoji } diff --git a/events/guild_soundboard_events.go b/events/guild_soundboard_events.go new file mode 100644 index 00000000..0bce60aa --- /dev/null +++ b/events/guild_soundboard_events.go @@ -0,0 +1,43 @@ +package events + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +// GenericGuildSoundboardSound is called upon receiving GuildSoundboardSoundCreate and GuildSoundboardSoundUpdate (requires gateway.IntentGuildExpressions) +type GenericGuildSoundboardSound struct { + *GenericEvent + discord.SoundboardSound +} + +// GuildSoundboardSoundCreate indicates that a discord.SoundboardSound was created in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundCreate struct { + *GenericGuildSoundboardSound +} + +// GuildSoundboardSoundUpdate indicates that a discord.SoundboardSound was updated in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundUpdate struct { + *GenericGuildSoundboardSound + OldGuildSoundboardSound discord.SoundboardSound +} + +// GuildSoundboardSoundDelete indicates that a discord.SoundboardSound was deleted in a discord.Guild (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundDelete struct { + *GenericEvent + SoundID snowflake.ID + GuildID snowflake.ID +} + +// GuildSoundboardSoundsUpdate indicates when multiple discord.Guild soundboard sounds were updated (requires gateway.IntentGuildExpressions) +type GuildSoundboardSoundsUpdate struct { + *GenericEvent + SoundboardSounds []discord.SoundboardSound +} + +// SoundboardSounds is a response to gateway.OpcodeRequestSoundboardSounds +type SoundboardSounds struct { + *GenericEvent + SoundboardSounds []discord.SoundboardSound + GuildID snowflake.ID +} diff --git a/events/guild_sticker_events.go b/events/guild_sticker_events.go index 39f40ad3..9984f531 100644 --- a/events/guild_sticker_events.go +++ b/events/guild_sticker_events.go @@ -14,25 +14,25 @@ type StickersUpdate struct { gateway.EventGuildStickersUpdate } -// GenericSticker is called upon receiving StickerCreate , StickerUpdate or StickerDelete (requires gateway.IntentGuildEmojisAndStickers) +// GenericSticker is called upon receiving StickerCreate , StickerUpdate or StickerDelete (requires gateway.IntentGuildExpressions) type GenericSticker struct { *GenericEvent GuildID snowflake.ID Sticker discord.Sticker } -// StickerCreate indicates that a new discord.Sticker got created in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerCreate indicates that a new discord.Sticker got created in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerCreate struct { *GenericSticker } -// StickerUpdate indicates that a discord.Sticker got updated in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerUpdate indicates that a discord.Sticker got updated in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerUpdate struct { *GenericSticker OldSticker discord.Sticker } -// StickerDelete indicates that a discord.Sticker got deleted in a discord.Guild (requires gateway.IntentGuildEmojisAndStickers) +// StickerDelete indicates that a discord.Sticker got deleted in a discord.Guild (requires gateway.IntentGuildExpressions) type StickerDelete struct { *GenericSticker } diff --git a/events/listener_adapter.go b/events/listener_adapter.go index 1da36850..8f68fb77 100644 --- a/events/listener_adapter.go +++ b/events/listener_adapter.go @@ -116,6 +116,13 @@ type ListenerAdapter struct { OnGuildMessageReactionRemoveEmoji func(event *GuildMessageReactionRemoveEmoji) OnGuildMessageReactionRemoveAll func(event *GuildMessageReactionRemoveAll) + // Guild Soundboard Events + OnGuildSoundboardSoundCreate func(event *GuildSoundboardSoundCreate) + OnGuildSoundboardSoundUpdate func(event *GuildSoundboardSoundUpdate) + OnGuildSoundboardSoundDelete func(event *GuildSoundboardSoundDelete) + OnGuildSoundboardSoundsUpdate func(event *GuildSoundboardSoundsUpdate) + OnSoundboardSounds func(event *SoundboardSounds) + // Guild Voice Events OnVoiceServerUpdate func(event *VoiceServerUpdate) OnGuildVoiceChannelEffectSend func(event *GuildVoiceChannelEffectSend) @@ -498,6 +505,28 @@ func (l *ListenerAdapter) OnEvent(event bot.Event) { listener(e) } + // Guild Soundboard Sound Events + case *GuildSoundboardSoundCreate: + if listener := l.OnGuildSoundboardSoundCreate; listener != nil { + listener(e) + } + case *GuildSoundboardSoundUpdate: + if listener := l.OnGuildSoundboardSoundUpdate; listener != nil { + listener(e) + } + case *GuildSoundboardSoundDelete: + if listener := l.OnGuildSoundboardSoundDelete; listener != nil { + listener(e) + } + case *GuildSoundboardSoundsUpdate: + if listener := l.OnGuildSoundboardSoundsUpdate; listener != nil { + listener(e) + } + case *SoundboardSounds: + if listener := l.OnSoundboardSounds; listener != nil { + listener(e) + } + // Guild Voice Events case *VoiceServerUpdate: if listener := l.OnVoiceServerUpdate; listener != nil { diff --git a/gateway/gateway_event_type.go b/gateway/gateway_event_type.go index 63c145df..ac4843a1 100644 --- a/gateway/gateway_event_type.go +++ b/gateway/gateway_event_type.go @@ -49,6 +49,10 @@ const ( EventTypeGuildScheduledEventDelete EventType = "GUILD_SCHEDULED_EVENT_DELETE" EventTypeGuildScheduledEventUserAdd EventType = "GUILD_SCHEDULED_EVENT_USER_ADD" EventTypeGuildScheduledEventUserRemove EventType = "GUILD_SCHEDULED_EVENT_USER_REMOVE" + EventTypeGuildSoundboardSoundCreate EventType = "GUILD_SOUNDBOARD_SOUND_CREATE" + EventTypeGuildSoundboardSoundUpdate EventType = "GUILD_SOUNDBOARD_SOUND_UPDATE" + EventTypeGuildSoundboardSoundDelete EventType = "GUILD_SOUNDBOARD_SOUND_DELETE" + EventTypeGuildSoundboardSoundsUpdate EventType = "GUILD_SOUNDBOARD_SOUNDS_UPDATE" EventTypeIntegrationCreate EventType = "INTEGRATION_CREATE" EventTypeIntegrationUpdate EventType = "INTEGRATION_UPDATE" EventTypeIntegrationDelete EventType = "INTEGRATION_DELETE" @@ -66,6 +70,7 @@ const ( EventTypeMessageReactionRemoveAll EventType = "MESSAGE_REACTION_REMOVE_ALL" EventTypeMessageReactionRemoveEmoji EventType = "MESSAGE_REACTION_REMOVE_EMOJI" EventTypePresenceUpdate EventType = "PRESENCE_UPDATE" + EventTypeSoundboardSounds EventType = "SOUNDBOARD_SOUNDS" EventTypeStageInstanceCreate EventType = "STAGE_INSTANCE_CREATE" EventTypeStageInstanceDelete EventType = "STAGE_INSTANCE_DELETE" EventTypeStageInstanceUpdate EventType = "STAGE_INSTANCE_UPDATE" diff --git a/gateway/gateway_events.go b/gateway/gateway_events.go index e3ba2263..fdfa6acc 100644 --- a/gateway/gateway_events.go +++ b/gateway/gateway_events.go @@ -441,6 +441,33 @@ type EventGuildScheduledEventUserRemove struct { func (EventGuildScheduledEventUserRemove) messageData() {} func (EventGuildScheduledEventUserRemove) eventData() {} +type EventGuildSoundboardSoundCreate struct { + discord.SoundboardSound +} + +func (EventGuildSoundboardSoundCreate) messageData() {} +func (EventGuildSoundboardSoundCreate) eventData() {} + +type EventGuildSoundboardSoundUpdate struct { + discord.SoundboardSound +} + +func (EventGuildSoundboardSoundUpdate) messageData() {} +func (EventGuildSoundboardSoundUpdate) eventData() {} + +type EventGuildSoundboardSoundDelete struct { + SoundID snowflake.ID `json:"sound_id"` + GuildID snowflake.ID `json:"guild_id"` +} + +func (EventGuildSoundboardSoundDelete) messageData() {} +func (EventGuildSoundboardSoundDelete) eventData() {} + +type EventGuildSoundboardSoundsUpdate []discord.SoundboardSound + +func (EventGuildSoundboardSoundsUpdate) messageData() {} +func (EventGuildSoundboardSoundsUpdate) eventData() {} + type EventInteractionCreate struct { discord.Interaction } @@ -549,6 +576,14 @@ type EventPresenceUpdate struct { func (EventPresenceUpdate) messageData() {} func (EventPresenceUpdate) eventData() {} +type EventSoundboardSounds struct { + SoundboardSounds []discord.SoundboardSound `json:"soundboard_sounds"` + GuildID snowflake.ID `json:"guild_id"` +} + +func (EventSoundboardSounds) messageData() {} +func (EventSoundboardSounds) eventData() {} + type EventStageInstanceCreate struct { discord.StageInstance } diff --git a/gateway/gateway_intents.go b/gateway/gateway_intents.go index 68f6c4ec..2945dd69 100644 --- a/gateway/gateway_intents.go +++ b/gateway/gateway_intents.go @@ -10,6 +10,7 @@ const ( IntentGuilds Intents = 1 << iota IntentGuildMembers IntentGuildModeration + // Deprecated: Use IntentGuildExpressions instead IntentGuildEmojisAndStickers IntentGuildIntegrations IntentGuildWebhooks @@ -34,10 +35,12 @@ const ( IntentGuildMessagePolls IntentDirectMessagePolls + IntentGuildExpressions = IntentGuildEmojisAndStickers + IntentsGuild = IntentGuilds | IntentGuildMembers | IntentGuildModeration | - IntentGuildEmojisAndStickers | + IntentGuildExpressions | IntentGuildIntegrations | IntentGuildWebhooks | IntentGuildInvites | @@ -59,7 +62,7 @@ const ( IntentsNonPrivileged = IntentGuilds | IntentGuildModeration | - IntentGuildEmojisAndStickers | + IntentGuildExpressions | IntentGuildIntegrations | IntentGuildWebhooks | IntentGuildInvites | diff --git a/gateway/gateway_messages.go b/gateway/gateway_messages.go index 35e23e1e..26dba4a5 100644 --- a/gateway/gateway_messages.go +++ b/gateway/gateway_messages.go @@ -82,6 +82,11 @@ func (e *Message) UnmarshalJSON(data []byte) error { case OpcodeHeartbeatACK: + case OpcodeRequestSoundboardSounds: + var d MessageDataRequestSoundboardSounds + err = json.Unmarshal(v.D, &d) + messageData = d + default: var d MessageDataUnknown err = json.Unmarshal(v.D, &d) @@ -311,6 +316,26 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeGuildSoundboardSoundCreate: + var d EventGuildSoundboardSoundCreate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundUpdate: + var d EventGuildSoundboardSoundUpdate + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundDelete: + var d EventGuildSoundboardSoundDelete + err = json.Unmarshal(data, &d) + eventData = d + + case EventTypeGuildSoundboardSoundsUpdate: + var d EventGuildSoundboardSoundsUpdate + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeIntegrationCreate: var d EventIntegrationCreate err = json.Unmarshal(data, &d) @@ -386,6 +411,11 @@ func UnmarshalEventData(data []byte, eventType EventType) (EventData, error) { err = json.Unmarshal(data, &d) eventData = d + case EventTypeSoundboardSounds: + var d EventSoundboardSounds + err = json.Unmarshal(data, &d) + eventData = d + case EventTypeStageInstanceCreate: var d EventStageInstanceCreate err = json.Unmarshal(data, &d) @@ -636,3 +666,9 @@ type MessageDataHello struct { } func (MessageDataHello) messageData() {} + +type MessageDataRequestSoundboardSounds struct { + GuildIDs []snowflake.ID `json:"guild_ids"` +} + +func (MessageDataRequestSoundboardSounds) messageData() {} diff --git a/gateway/gateway_opcodes.go b/gateway/gateway_opcodes.go index 85413d55..d6c8e045 100644 --- a/gateway/gateway_opcodes.go +++ b/gateway/gateway_opcodes.go @@ -17,6 +17,7 @@ const ( OpcodeInvalidSession OpcodeHello OpcodeHeartbeatACK + OpcodeRequestSoundboardSounds Opcode = 31 ) type CloseEventCode struct { diff --git a/handlers/all_handlers.go b/handlers/all_handlers.go index b130093c..d3872cd2 100644 --- a/handlers/all_handlers.go +++ b/handlers/all_handlers.go @@ -87,6 +87,12 @@ var allEventHandlers = []bot.GatewayEventHandler{ bot.NewGatewayEventHandler(gateway.EventTypeGuildScheduledEventUserAdd, gatewayHandlerGuildScheduledEventUserAdd), bot.NewGatewayEventHandler(gateway.EventTypeGuildScheduledEventUserRemove, gatewayHandlerGuildScheduledEventUserRemove), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundCreate, gatewayHandlerGuildSoundboardSoundCreate), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundUpdate, gatewayHandlerGuildSoundboardSoundUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundDelete, gatewayHandlerGuildSoundboardSoundDelete), + bot.NewGatewayEventHandler(gateway.EventTypeGuildSoundboardSoundsUpdate, gatewayHandlerGuildSoundboardSoundsUpdate), + bot.NewGatewayEventHandler(gateway.EventTypeSoundboardSounds, gatewayHandlerSoundboardSounds), + bot.NewGatewayEventHandler(gateway.EventTypeIntegrationCreate, gatewayHandlerIntegrationCreate), bot.NewGatewayEventHandler(gateway.EventTypeIntegrationUpdate, gatewayHandlerIntegrationUpdate), bot.NewGatewayEventHandler(gateway.EventTypeIntegrationDelete, gatewayHandlerIntegrationDelete), diff --git a/handlers/guild_handlers.go b/handlers/guild_handlers.go index 03cd70c6..66df917f 100644 --- a/handlers/guild_handlers.go +++ b/handlers/guild_handlers.go @@ -58,6 +58,10 @@ func gatewayHandlerGuildCreate(client bot.Client, sequenceNumber int, shardID in client.Caches().AddGuildScheduledEvent(guildScheduledEvent) } + for _, soundboardSound := range event.SoundboardSounds { + client.Caches().AddGuildSoundboardSound(soundboardSound) + } + for _, presence := range event.Presences { presence.GuildID = event.ID // populate unset field client.Caches().AddPresence(presence) @@ -130,6 +134,7 @@ func gatewayHandlerGuildDelete(client bot.Client, sequenceNumber int, shardID in client.Caches().RemoveMembersByGuildID(event.ID) client.Caches().RemoveStageInstancesByGuildID(event.ID) client.Caches().RemoveGuildScheduledEventsByGuildID(event.ID) + client.Caches().RemoveGuildSoundboardSoundsByGuildID(event.ID) client.Caches().RemoveMessagesByGuildID(event.ID) if event.Unavailable { diff --git a/handlers/guild_soundboard_handlers.go b/handlers/guild_soundboard_handlers.go new file mode 100644 index 00000000..6ee340c2 --- /dev/null +++ b/handlers/guild_soundboard_handlers.go @@ -0,0 +1,60 @@ +package handlers + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + "github.com/disgoorg/disgo/gateway" +) + +func gatewayHandlerGuildSoundboardSoundCreate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundCreate) { + client.Caches().AddGuildSoundboardSound(event.SoundboardSound) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundCreate{ + GenericGuildSoundboardSound: &events.GenericGuildSoundboardSound{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSound: event.SoundboardSound, + }, + }) +} + +func gatewayHandlerGuildSoundboardSoundUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundUpdate) { + oldSound, _ := client.Caches().GuildSoundboardSound(*event.GuildID, event.SoundID) + client.Caches().AddGuildSoundboardSound(event.SoundboardSound) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundUpdate{ + GenericGuildSoundboardSound: &events.GenericGuildSoundboardSound{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSound: event.SoundboardSound, + }, + OldGuildSoundboardSound: oldSound, + }) +} + +func gatewayHandlerGuildSoundboardSoundDelete(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundDelete) { + client.Caches().RemoveGuildSoundboardSound(event.GuildID, event.SoundID) + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundDelete{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundID: event.SoundID, + GuildID: event.GuildID, + }) +} + +func gatewayHandlerGuildSoundboardSoundsUpdate(client bot.Client, sequenceNumber int, shardID int, event gateway.EventGuildSoundboardSoundsUpdate) { + for _, sound := range event { + client.Caches().AddGuildSoundboardSound(sound) + } + + client.EventManager().DispatchEvent(&events.GuildSoundboardSoundsUpdate{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSounds: event, + }) +} + +func gatewayHandlerSoundboardSounds(client bot.Client, sequenceNumber int, shardID int, event gateway.EventSoundboardSounds) { + client.EventManager().DispatchEvent(&events.SoundboardSounds{ + GenericEvent: events.NewGenericEvent(client, sequenceNumber, shardID), + SoundboardSounds: event.SoundboardSounds, + GuildID: event.GuildID, + }) +} diff --git a/rest/rest.go b/rest/rest.go index d89e42a4..aeb09a01 100644 --- a/rest/rest.go +++ b/rest/rest.go @@ -18,6 +18,7 @@ type Rest interface { Users Voice Webhooks + SoundboardSounds StageInstances Emojis Stickers @@ -45,6 +46,7 @@ func New(client Client) Rest { Users: NewUsers(client), Voice: NewVoice(client), Webhooks: NewWebhooks(client), + SoundboardSounds: NewSoundboardSounds(client), StageInstances: NewStageInstances(client), Emojis: NewEmojis(client), Stickers: NewStickers(client), @@ -70,6 +72,7 @@ type restImpl struct { Users Voice Webhooks + SoundboardSounds StageInstances Emojis Stickers diff --git a/rest/rest_endpoints.go b/rest/rest_endpoints.go index 1e63f152..5015e6b9 100644 --- a/rest/rest_endpoints.go +++ b/rest/rest_endpoints.go @@ -139,6 +139,16 @@ var ( GetGuildScheduledEventUsers = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/scheduled-events/{guild_scheduled_event.id}/users") ) +// Sounds +var ( + GetSoundboardDefaultSounds = NewEndpoint(http.MethodGet, "/soundboard-default-sounds") + GetGuildSoundboardSounds = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/soundboard-sounds") + CreateGuildSoundboardSound = NewEndpoint(http.MethodPost, "/guilds/{guild.id}/soundboard-sounds") + GetGuildSoundboardSound = NewEndpoint(http.MethodGet, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") + UpdateGuildSoundboardSound = NewEndpoint(http.MethodPatch, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") + DeleteGuildSoundboardSound = NewEndpoint(http.MethodDelete, "/guilds/{guild.id}/soundboard-sounds/{sound.id}") +) + // StageInstance var ( GetStageInstance = NewEndpoint(http.MethodGet, "/stage-instances/{channel.id}") @@ -176,6 +186,8 @@ var ( GetPollAnswerVotes = NewEndpoint(http.MethodGet, "/channels/{channel.id}/polls/{message.id}/answers/{answer.id}") ExpirePoll = NewEndpoint(http.MethodPost, "/channels/{channel.id}/polls/{message.id}/expire") + + SendSoundboardSound = NewEndpoint(http.MethodPost, "/channels/{channel.id}/send-soundboard-sound") ) // Threads diff --git a/rest/soundboard_sounds.go b/rest/soundboard_sounds.go new file mode 100644 index 00000000..7886e6b6 --- /dev/null +++ b/rest/soundboard_sounds.go @@ -0,0 +1,67 @@ +package rest + +import ( + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/snowflake/v2" +) + +var _ SoundboardSounds = (*soundsImpl)(nil) + +func NewSoundboardSounds(client Client) SoundboardSounds { + return &soundsImpl{client: client} +} + +type SoundboardSounds interface { + GetSoundboardDefaultSounds(opts ...RequestOpt) ([]discord.SoundboardSound, error) + GetGuildSoundboardSounds(guildID snowflake.ID, opts ...RequestOpt) ([]discord.SoundboardSound, error) + CreateGuildSoundboardSound(guildID snowflake.ID, soundCreate discord.SoundboardSoundCreate, opts ...RequestOpt) (*discord.SoundboardSound, error) + GetGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) (*discord.SoundboardSound, error) + UpdateGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, soundUpdate discord.SoundboardSoundUpdate, opts ...RequestOpt) (*discord.SoundboardSound, error) + DeleteGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) error + SendSoundboardSound(channelID snowflake.ID, sendSound discord.SendSoundboardSound, opts ...RequestOpt) error +} + +type soundsImpl struct { + client Client +} + +func (s *soundsImpl) GetSoundboardDefaultSounds(opts ...RequestOpt) (sounds []discord.SoundboardSound, err error) { + err = s.client.Do(GetSoundboardDefaultSounds.Compile(nil), nil, &sounds, opts...) + return +} + +func (s *soundsImpl) GetGuildSoundboardSounds(guildID snowflake.ID, opts ...RequestOpt) (sounds []discord.SoundboardSound, err error) { + var rs soundsResponse + err = s.client.Do(GetGuildSoundboardSounds.Compile(nil, guildID), nil, &rs, opts...) + if err == nil { + sounds = rs.Items + } + return +} + +func (s *soundsImpl) CreateGuildSoundboardSound(guildID snowflake.ID, soundCreate discord.SoundboardSoundCreate, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(CreateGuildSoundboardSound.Compile(nil, guildID), soundCreate, &sound, opts...) + return +} + +func (s *soundsImpl) GetGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(GetGuildSoundboardSound.Compile(nil, guildID, soundID), nil, &sound, opts...) + return +} + +func (s *soundsImpl) UpdateGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, soundUpdate discord.SoundboardSoundUpdate, opts ...RequestOpt) (sound *discord.SoundboardSound, err error) { + err = s.client.Do(UpdateGuildSoundboardSound.Compile(nil, guildID, soundID), soundUpdate, &sound, opts...) + return +} + +func (s *soundsImpl) DeleteGuildSoundboardSound(guildID snowflake.ID, soundID snowflake.ID, opts ...RequestOpt) error { + return s.client.Do(DeleteGuildSoundboardSound.Compile(nil, guildID, soundID), nil, nil, opts...) +} + +func (s *soundsImpl) SendSoundboardSound(channelID snowflake.ID, sendSound discord.SendSoundboardSound, opts ...RequestOpt) error { + return s.client.Do(SendSoundboardSound.Compile(nil, channelID), sendSound, nil, opts...) +} + +type soundsResponse struct { + Items []discord.SoundboardSound `json:"items"` +}