From 14f12cd5e4771490e279dfedf02772f23eddda9a Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Thu, 23 Jul 2026 22:18:24 +0200 Subject: [PATCH] some soundboard endpoints --- src/grom.gleam | 273 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 263 insertions(+), 10 deletions(-) diff --git a/src/grom.gleam b/src/grom.gleam index 3f80ed8..a1f4373 100644 --- a/src/grom.gleam +++ b/src/grom.gleam @@ -1780,7 +1780,7 @@ pub type VoiceChannel { id: Snowflake(VoiceChannel), channel_id: Snowflake(Channel), guild_channel_id: Snowflake(GuildChannel), - movable_channel_id: Snowflake(MovableChannel), + voice_like_channel_id: Snowflake(VoiceLikeChannel), permission_overwrites: List(PermissionOverwrite), guild_id: Option(Snowflake(Guild)), position: Int, @@ -1810,7 +1810,7 @@ fn voice_channel_decoder() -> Decoder(VoiceChannel) { use id <- decode.field("id", snowflake_decoder()) use channel_id <- decode.field("id", snowflake_decoder()) use guild_channel_id <- decode.field("id", snowflake_decoder()) - use movable_channel_id <- decode.field("id", snowflake_decoder()) + use voice_like_channel_id <- decode.field("id", snowflake_decoder()) use permission_overwrites <- decode.optional_field( "permission_overwrites", [], @@ -1858,7 +1858,7 @@ fn voice_channel_decoder() -> Decoder(VoiceChannel) { id:, channel_id:, guild_channel_id:, - movable_channel_id:, + voice_like_channel_id:, permission_overwrites:, guild_id:, position:, @@ -2055,7 +2055,7 @@ pub type StageChannel { id: Snowflake(StageChannel), channel_id: Snowflake(Channel), guild_channel_id: Snowflake(GuildChannel), - movable_channel_id: Snowflake(MovableChannel), + voice_like_channel_id: Snowflake(VoiceLikeChannel), permission_overwrites: List(PermissionOverwrite), guild_id: Option(Snowflake(Guild)), position: Int, @@ -2085,7 +2085,7 @@ fn stage_channel_decoder() -> Decoder(StageChannel) { use id <- decode.field("id", snowflake_decoder()) use channel_id <- decode.field("id", snowflake_decoder()) use guild_channel_id <- decode.field("id", snowflake_decoder()) - use movable_channel_id <- decode.field("id", snowflake_decoder()) + use voice_like_channel_id <- decode.field("id", snowflake_decoder()) use permission_overwrites <- decode.optional_field( "permission_overwrites", [], @@ -2132,7 +2132,7 @@ fn stage_channel_decoder() -> Decoder(StageChannel) { id:, channel_id:, guild_channel_id:, - movable_channel_id:, + voice_like_channel_id:, permission_overwrites:, guild_id:, position:, @@ -8108,14 +8108,13 @@ pub opaque type ModifyGuildMember { roles: Modification(List(Snowflake(Role))), is_mute: Modification(Bool), is_deaf: Modification(Bool), - channel_id: Modification(Snowflake(MovableChannel)), + channel_id: Modification(Snowflake(VoiceLikeChannel)), communication_disabled_until: Modification(Timestamp), flags: Modification(List(GuildMemberFlag)), ) } -/// A movable channel is either a voice channel or a stage channel. (because you can move members to and from it) -pub type MovableChannel +pub type VoiceLikeChannel pub fn new_modify_guild_member() -> ModifyGuildMember { ModifyGuildMember( @@ -8204,7 +8203,7 @@ pub fn undeafen_guild_member(modify: ModifyGuildMember) -> ModifyGuildMember { /// Will return a bad request if the user is not in a voice channel. pub fn move_guild_member( modify: ModifyGuildMember, - to channel_id: Snowflake(MovableChannel), + to channel_id: Snowflake(VoiceLikeChannel), ) -> ModifyGuildMember { ModifyGuildMember(..modify, channel_id: Modify(channel_id)) } @@ -17058,3 +17057,257 @@ pub fn modify_current_application_response( ) -> Result(Application, RestError) { handle_response(response, decode_with: application_decoder()) } + +pub type SoundboardSound { + SoundboardSound( + name: String, + id: Snowflake(SoundboardSound), + /// Between 0.0 and 1.0, represents the volume percentage. + volume: Float, + emoji: Option(SoundboardSoundEmoji), + guild_id: Option(Snowflake(Guild)), + is_available: Bool, + creator: Option(User), + ) +} + +fn soundboard_sound_decoder() -> Decoder(SoundboardSound) { + use name <- decode.field("name", decode.string) + use id <- decode.field("sound_id", snowflake_decoder()) + use volume <- decode.field("volume", decode.float) + use guild_id <- decode.optional_field( + "guild_id", + None, + decode.map(snowflake_decoder(), Some), + ) + use emoji <- decode.then(soundboard_sound_emoji_decoder()) + use is_available <- decode.field("available", decode.bool) + use creator <- decode.optional_field( + "user", + None, + decode.map(user_decoder(), Some), + ) + decode.success(SoundboardSound( + name:, + id:, + volume:, + emoji:, + guild_id:, + is_available:, + creator:, + )) +} + +pub type SoundboardSoundEmoji { + UnicodeSoundboardSoundEmoji(character: String) + CustomSoundboardSoundEmoji(id: Snowflake(Emoji)) +} + +fn soundboard_sound_emoji_decoder() -> Decoder(Option(SoundboardSoundEmoji)) { + use id <- decode.optional_field( + "emoji_id", + None, + decode.optional(snowflake_decoder()), + ) + use character <- decode.optional_field( + "emoji_name", + None, + decode.optional(decode.string), + ) + + case id, character { + Some(id), _ -> decode.success(Some(CustomSoundboardSoundEmoji(id:))) + _, Some(character) -> + decode.success(Some(UnicodeSoundboardSoundEmoji(character:))) + None, None -> decode.success(None) + } +} + +fn soundboard_sound_emoji_to_json( + emoji: Option(SoundboardSoundEmoji), +) -> Result(#(String, Json), Nil) { + case emoji { + None -> Error(Nil) + Some(UnicodeSoundboardSoundEmoji(character:)) -> + Ok(#("emoji_name", json.string(character))) + Some(CustomSoundboardSoundEmoji(id:)) -> + Ok(#("emoji_id", snowflake_to_json(id))) + } +} + +/// Requirements: +/// - must have the `AllowSpeakingInVoiceChannels` and `AllowUsingSoundboard` permissions +/// - must be connected to the channel +/// - must not be muted, deafened or suppressed +/// - if using a sound from another guild: +/// - must supply the source guild ID +/// - must additionally have the `AllowUsingExternalSoundboardSounds` permission +/// +/// Do not supply the source guild ID if the sound is from a default pack/the channel's guild. +pub fn send_soundboard_sound_request( + token token: Token, + in_channel_with_id channel_id: Snowflake(VoiceLikeChannel), + sound_with_id sound_id: Snowflake(SoundboardSound), + sound_from_guild_with_id source_guild_id: Option(Snowflake(Guild)), +) -> Request(String) { + let body = + [ + Ok(#("sound_id", snowflake_to_json(sound_id))), + optional_to_json(source_guild_id, "source_guild_id", snowflake_to_json), + ] + |> list_to_json_object + |> json.to_string + + new_request( + token:, + to: "/channels/" + <> snowflake_to_string(channel_id) + <> "/send-soundboard-sound", + method: http.Post, + ) + |> request.set_body(body) +} + +pub fn send_soundboard_sound_response( + response: Response(String), +) -> Result(Nil, RestError) { + handle_no_content_response(response) +} + +pub fn get_default_soundboard_sounds_request( + token token: Token, +) -> Request(String) { + new_request(token:, to: "/soundboard-default-sounds", method: http.Get) +} + +pub fn get_default_soundboard_sounds_response( + response: Response(String), +) -> Result(List(SoundboardSound), RestError) { + handle_response( + response, + decode_with: decode.list(of: soundboard_sound_decoder()), + ) +} + +/// Includes the `creator` field if the current user has the +/// `AllowCreatingGuildExpressions` or `AllowManagingGuildExpressions` permission. +pub fn get_guild_soundboard_sounds_request( + token token: Token, + for_guild_with_id guild_id: Snowflake(Guild), +) -> Request(String) { + new_request( + token:, + to: "/guilds/" <> snowflake_to_string(guild_id) <> "/soundboard-sounds", + method: http.Get, + ) +} + +pub fn get_guild_soundboard_sounds_response( + response: Response(String), +) -> Result(List(SoundboardSound), RestError) { + handle_response( + response, + decode_with: decode.at( + ["items"], + decode.list(of: soundboard_sound_decoder()), + ), + ) +} + +/// Includes the `creator` field if the current user has the +/// `AllowCreatingGuildExpressions` or `AllowManagingGuildExpressions` permission. +pub fn get_guild_soundboard_sound_request( + token token: Token, + for_guild_with_id guild_id: Snowflake(Guild), + sound_with_id sound_id: Snowflake(SoundboardSound), +) -> Request(String) { + new_request( + token:, + to: "/guilds/" + <> snowflake_to_string(guild_id) + <> "/soundboard-sounds/" + <> snowflake_to_string(sound_id), + method: http.Get, + ) +} + +pub fn get_guild_soundboard_sound_response( + response: Response(String), +) -> Result(SoundboardSound, RestError) { + handle_response(response, decode_with: soundboard_sound_decoder()) +} + +pub type SoundboardSoundContentType { + Mp3SoundboardSound + OggSoundboardSound +} + +pub opaque type CreateSoundboardSound { + CreateSoundboardSound( + name: String, + data: BitArray, + content_type: SoundboardSoundContentType, + volume: Option(Float), + emoji: Option(SoundboardSoundEmoji), + ) +} + +/// The data must be maximum 512 kB and maximum 5.2 seconds. +pub fn new_soundboard_sound( + named name: String, + data data: BitArray, + content_type content_type: SoundboardSoundContentType, +) -> CreateSoundboardSound { + CreateSoundboardSound(name:, data:, content_type:, volume: None, emoji: None) +} + +/// The volume will be clamped between 0.0 and 1.0, so make sure to provide a value within that range. +pub fn new_soundboard_sound_with_volume( + create: CreateSoundboardSound, + volume: Float, +) -> CreateSoundboardSound { + CreateSoundboardSound(..create, volume: Some(float.clamp(volume, 0.0, 1.0))) +} + +pub fn new_soundboard_sound_with_emoji( + create: CreateSoundboardSound, + emoji: SoundboardSoundEmoji, +) -> CreateSoundboardSound { + CreateSoundboardSound(..create, emoji: Some(emoji)) +} + +fn create_soundboard_sound_to_json(create: CreateSoundboardSound) -> Json { + let content_type = case create.content_type { + Mp3SoundboardSound -> "audio/mpeg" + OggSoundboardSound -> "audio/ogg" + } + + let data = bit_array.base64_encode(create.data, True) + + let sound = "data:" <> content_type <> ";base64," <> data + [ + Ok(#("name", json.string(create.name))), + Ok(#("sound", json.string(sound))), + optional_to_json(create.volume, "volume", json.float), + soundboard_sound_emoji_to_json(create.emoji), + ] + |> list_to_json_object +} + +/// Requires the `AllowCreatingGuildExpressions` permission +pub fn new_guild_soundboard_sound_request( + token token: Token, + in_guild_with_id guild_id: Snowflake(Guild), + using create: CreateSoundboardSound, + reason reason: Option(String), +) -> Request(String) { + let body = create |> create_soundboard_sound_to_json |> json.to_string + + new_request( + token:, + to: "/guilds/" <> snowflake_to_string(guild_id) <> "/soundboard-sounds", + method: http.Post, + ) + |> request.set_body(body) + |> request_with_reason(reason) +} -- 2.51.2