From f806dbc5632ad8a30154b7ea2c8ef7a61b8ae46b Mon Sep 17 00:00:00 2001 From: Rootspring Date: Sun, 17 Nov 2024 13:11:19 -0500 Subject: [PATCH 01/18] Replace delete_message_days with delete_message_seconds in ban_user --- src/http/client.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index 15ee7f4b382..8787280fcd5 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -332,17 +332,15 @@ impl Http { /// Bans a [`User`] from a [`Guild`], removing their messages sent in the last X number of /// days. /// - /// Passing a `delete_message_days` of `0` is equivalent to not removing any messages. Up to - /// `7` days' worth of messages may be deleted. + /// Passing a `delete_message_seconds` of `0` is equivalent to not removing any messages. Up to + /// `7` days' (or 604800 seconds) worth of messages may be deleted. pub async fn ban_user( &self, guild_id: GuildId, user_id: UserId, - delete_message_days: u8, + delete_message_seconds: u32, reason: Option<&str>, ) -> Result<()> { - let delete_message_seconds = u32::from(delete_message_days) * 86400; - self.wind(204, Request { body: None, multipart: None, From d0dcbf14c1c2616a6a510ad2466c2f744083df05 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sun, 17 Nov 2024 18:24:47 +0000 Subject: [PATCH 02/18] replace ban's delete_message_days with delete_message_seconds --- src/http/client.rs | 2 +- src/model/error.rs | 5 ++++- src/model/guild/guild_id.rs | 26 ++++++++++++++++++++------ src/model/guild/member.rs | 9 +++++++-- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index 8787280fcd5..cd7e03815a8 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -333,7 +333,7 @@ impl Http { /// days. /// /// Passing a `delete_message_seconds` of `0` is equivalent to not removing any messages. Up to - /// `7` days' (or 604800 seconds) worth of messages may be deleted. + /// `604800 seconds` (or 7 days) worth of messages may be deleted. pub async fn ban_user( &self, guild_id: GuildId, diff --git a/src/model/error.rs b/src/model/error.rs index 29192d178db..e3798d03fed 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -13,6 +13,7 @@ pub enum Maximum { WebhookName, AuditLogReason, DeleteMessageDays, + DeleteMessageSeconds, BulkDeleteAmount, } @@ -39,6 +40,7 @@ impl Maximum { Self::WebhookName | Self::BulkDeleteAmount => 100, Self::AuditLogReason => 512, Self::DeleteMessageDays => 7, + Self::DeleteMessageSeconds => 604800, } } } @@ -53,6 +55,7 @@ impl fmt::Display for Maximum { Self::WebhookName => f.write_str("Webhook name"), Self::AuditLogReason => f.write_str("Audit log reason"), Self::DeleteMessageDays => f.write_str("Delete message days"), + Self::DeleteMessageSeconds => f.write_str("Delete message seconds"), Self::BulkDeleteAmount => f.write_str("Message bulk delete count"), } } @@ -117,7 +120,7 @@ impl fmt::Display for Minimum { /// #[serenity::async_trait] /// impl EventHandler for Handler { /// async fn guild_ban_removal(&self, ctx: Context, guild_id: GuildId, user: User) { -/// match guild_id.ban(&ctx.http, user.id, 8, Some("No unbanning people!")).await { +/// match guild_id.ban(&ctx.http, user.id, 691200, Some("No unbanning people!")).await { /// Ok(()) => { /// // Ban successful. /// }, diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 6d096589732..2eecf1f9cde 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -165,8 +165,8 @@ impl GuildId { builder.execute(http, self, user_id).await } - /// Ban a [`User`] from the guild, deleting a number of days' worth of messages (`dmd`) between - /// the range 0 and 7. + /// Ban a [`User`] from the guild, deleting a number of seconds' worth of messages + /// (`delete_message_seconds`) between the range 0 and 604800. /// /// **Note**: Requires the [Ban Members] permission. /// @@ -182,7 +182,7 @@ impl GuildId { /// # let http: Http = unimplemented!(); /// # let user = UserId::new(1); /// // assuming a `user` has already been bound - /// let _ = GuildId::new(81384788765712384).ban(&http, user, 4, None).await; + /// let _ = GuildId::new(81384788765712384).ban(&http, user, 345600, None).await; /// # Ok(()) /// # } /// ``` @@ -195,13 +195,27 @@ impl GuildId { /// Also can return [`Error::Http`] if the current user lacks permission. /// /// [Ban Members]: Permissions::BAN_MEMBERS - pub async fn ban(self, http: &Http, user: UserId, dmd: u8, reason: Option<&str>) -> Result<()> { - Maximum::DeleteMessageDays.check_overflow(dmd.into())?; + pub async fn ban( + self, + http: &Http, + user: UserId, + delete_message_seconds: u32, + reason: Option<&str>, + ) -> Result<()> { + // Convert to usize for check overflow + let delete_message_seconds_usize = + usize::try_from(delete_message_seconds).map_err(|_| { + Error::Model(ModelError::TooLarge { + maximum: Maximum::DeleteMessageSeconds, + value: usize::MAX, + }) + })?; + Maximum::DeleteMessageSeconds.check_overflow(delete_message_seconds_usize)?; if let Some(reason) = reason { Maximum::AuditLogReason.check_overflow(reason.len())?; } - http.ban_user(self, user, dmd, reason).await + http.ban_user(self, user, delete_message_seconds, reason).await } /// Bans multiple users from the guild, returning the users that were and weren't banned, and diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index 7ce2ca004f4..6d06ffa5060 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -144,8 +144,13 @@ impl Member { /// return [`Error::Http`] if the current user lacks permission to ban this member. /// /// [Ban Members]: Permissions::BAN_MEMBERS - pub async fn ban(&self, http: &Http, dmd: u8, audit_log_reason: Option<&str>) -> Result<()> { - self.guild_id.ban(http, self.user.id, dmd, audit_log_reason).await + pub async fn ban( + &self, + http: &Http, + delete_message_seconds: u32, + audit_log_reason: Option<&str>, + ) -> Result<()> { + self.guild_id.ban(http, self.user.id, delete_message_seconds, audit_log_reason).await } /// Determines the member's colour. From 426201a693858efe8f657ad1a7c0be29e4103637 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sun, 17 Nov 2024 23:20:19 +0000 Subject: [PATCH 03/18] begin implementing soundboard --- src/builder/mod.rs | 2 ++ src/builder/send_soundboard_sound.rs | 50 ++++++++++++++++++++++++++ src/http/client.rs | 22 ++++++++++++ src/http/routing.rs | 4 +++ src/model/channel/channel_id.rs | 14 ++++++++ src/model/channel/guild_channel.rs | 14 ++++++++ src/model/guild/mod.rs | 2 ++ src/model/guild/soundboard.rs | 54 ++++++++++++++++++++++++++++ src/model/id.rs | 1 + 9 files changed, 163 insertions(+) create mode 100644 src/builder/send_soundboard_sound.rs create mode 100644 src/model/guild/soundboard.rs diff --git a/src/builder/mod.rs b/src/builder/mod.rs index d923cbcae63..60e15121ec1 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -77,6 +77,7 @@ mod edit_webhook_message; mod execute_webhook; mod get_entitlements; mod get_messages; +mod send_soundboard_sound; pub use add_member::*; pub use bot_auth_parameters::*; @@ -120,6 +121,7 @@ pub use edit_webhook_message::*; pub use execute_webhook::*; pub use get_entitlements::*; pub use get_messages::*; +pub use send_soundboard_sound::*; macro_rules! button_and_select_menu_convenience_methods { ($self:ident $(. $components_path:tt)+) => { diff --git a/src/builder/send_soundboard_sound.rs b/src/builder/send_soundboard_sound.rs new file mode 100644 index 00000000000..da77b63847e --- /dev/null +++ b/src/builder/send_soundboard_sound.rs @@ -0,0 +1,50 @@ +#[cfg(feature = "http")] +use crate::http::Http; +use crate::model::prelude::*; + +/// A builder which to send a soundboard sound, to be used in conjunction with +/// [`GuildChannel::send_soundboard_sound`]. +/// +/// Discord docs: +/// - [Send Soundboard Sound](https://discord.com/developers/docs/resources/soundboard#send-soundboard-sound) +#[derive(Clone, Debug, Default, Serialize)] +#[must_use] +pub struct SendSoundboardSound { + sound_id: SoundboardSoundId, + source_guild_id: Option, +} + +impl SendSoundboardSound { + /// Create a new builder with the given soundboard sound id + pub fn new(sound_id: SoundboardSoundId) -> Self { + Self::default().sound_id(sound_id) + } + + pub fn sound_id(mut self, sound_id: SoundboardSoundId) -> Self { + self.sound_id = sound_id; + self + } + + pub fn source_guild_id(mut self, source_guild_id: GuildId) -> Self { + self.source_guild_id = Some(source_guild_id); + self + } + + /// Edits the given user's voice state in a stage channel. Providing a [`UserId`] will edit + /// that user's voice state, otherwise the current user's voice state will be edited. + /// + /// **Note**: Requires the [Request to Speak] permission. Also requires the [Mute Members] + /// permission to suppress another user or unsuppress the current user. This is not required if + /// suppressing the current user. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the user lacks permission, or if invalid data is given. + /// + /// [Request to Speak]: Permissions::REQUEST_TO_SPEAK + /// [Mute Members]: Permissions::MUTE_MEMBERS + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result<()> { + http.send_soundboard_sound(channel_id, &self).await + } +} diff --git a/src/http/client.rs b/src/http/client.rs index cd7e03815a8..c93bdec9da5 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4210,6 +4210,28 @@ impl Http { self.fire(request).await } + /// Send a soundboard sound to a voice channel the user is connected to. Fires a Voice Channel + /// Effect Send Gateway event. + pub async fn send_soundboard_sound( + &self, + channel_id: ChannelId, + map: &impl serde::Serialize, + ) -> Result<()> { + let body = to_vec(map)?; + + self.wind(204, Request { + body: Some(body), + multipart: None, + headers: None, + method: LightMethod::Post, + route: Route::SendSoundboardSound { + channel_id, + }, + params: None, + }) + .await + } + /// Pins a message in a channel. pub async fn pin_message( &self, diff --git a/src/http/routing.rs b/src/http/routing.rs index 7602244f385..245e3644f8c 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -405,6 +405,10 @@ routes! ('a, { api!("/sticker-packs/{}", sticker_pack_id), Some(RatelimitingKind::Path); + SendSoundboardSound { channel_id: ChannelId }, + api!("/channels/{}/send-soundboard-sound", channel_id), + Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); + User { user_id: UserId }, api!("/users/{}", user_id), Some(RatelimitingKind::Path); diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 332a1a999ff..07c484932da 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -6,6 +6,7 @@ use std::sync::Arc; #[cfg(feature = "model")] use futures::stream::Stream; +use crate::all::SendSoundboardSound; #[cfg(feature = "model")] use crate::builder::{ CreateAttachment, @@ -1055,6 +1056,19 @@ impl ChannelId { pub async fn end_poll(self, http: &Http, message_id: MessageId) -> Result { http.expire_poll(self, message_id).await } + + /// Sends a soundboard sound. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if there was an error sending the sound. + pub async fn send_soundboard_sound( + self, + http: &Http, + builder: SendSoundboardSound, + ) -> Result<()> { + builder.execute(http, self).await + } } #[cfg(feature = "model")] diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 6071ed59ea9..34704e21231 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -2,6 +2,7 @@ use std::fmt; use nonmax::{NonMaxU16, NonMaxU32, NonMaxU8}; +use crate::all::SendSoundboardSound; #[cfg(feature = "model")] use crate::builder::{ CreateMessage, @@ -505,6 +506,19 @@ impl GuildChannel { self.id.delete_stage_instance(http, reason).await } + + /// Sends a soundboard sound. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if there was an error sending the sound. + pub async fn send_soundboard_sound( + &self, + http: &Http, + builder: SendSoundboardSound, + ) -> Result<()> { + self.id.send_soundboard_sound(http, builder).await + } } impl fmt::Display for GuildChannel { diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index c1d061a51d9..0e2699aef80 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -11,6 +11,7 @@ mod partial_guild; mod premium_tier; mod role; mod scheduled_event; +mod soundboard; mod system_channel; mod welcome_screen; @@ -30,6 +31,7 @@ pub use self::partial_guild::*; pub use self::premium_tier::*; pub use self::role::*; pub use self::scheduled_event::*; +pub use self::soundboard::*; pub use self::system_channel::*; pub use self::welcome_screen::*; #[cfg(feature = "model")] diff --git a/src/model/guild/soundboard.rs b/src/model/guild/soundboard.rs new file mode 100644 index 00000000000..b64a5d2ed43 --- /dev/null +++ b/src/model/guild/soundboard.rs @@ -0,0 +1,54 @@ +//! Models relating to soundboard which are sounds that can be played in voice channels. +//! +//! See [Soundboard](https://discord.com/developers/docs/resources/soundboard) for more information + +use crate::model::prelude::*; + +/// Represents a custom guild emoji, which can either be created using the API, or via an +/// integration. Emojis created using the API only work within the guild it was created in. +/// +/// [Discord docs](https://discord.com/developers/docs/resources/emoji#emoji-object). +#[bool_to_bitflags::bool_to_bitflags] +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[non_exhaustive] +pub struct SoundboardSound { + /// The name of this sound + pub name: FixedString, + /// The id of this sound + pub id: SoundboardSoundId, + /// The volume of this sound, from 0 to 1 + pub volume: f64, + /// the id of this sound's custom emoji + pub emoji_id: Option, + /// the unicode character of this sound's standard emoji + pub emoji_unicode: Option>, + /// the id of the guild this sound is in + pub guild_id: Option, + /// whether this sound can be used, may be false due to loss of Server Boosts + pub available: bool, + /// the user who created this sound + pub user: Option, +} + +#[cfg(feature = "model")] +impl SoundboardSoundId { + /// Generates a URL to the soundboard sound's file. + /// + /// # Examples + /// + /// Print the direct link to the given soundboard sound: + /// + /// ```rust,no_run + /// # use serenity::model::soundboard::SoundboardSound; + /// # + /// # fn run(sound: SoundboardSound) { + /// // assuming sound has been set already + /// println!("Direct link to sound file: {}", sound.url()); + /// # } + /// ``` + #[must_use] + pub fn url(self) -> String { + cdn!("/soundboard-sounds/{}", self.get()) + } +} diff --git a/src/model/id.rs b/src/model/id.rs index 2bcca2d8818..407b77ca60c 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -190,6 +190,7 @@ id_u64! { StickerPackId: "An identifier for a sticker pack."; StickerPackBannerId: "An identifier for a sticker pack banner."; SkuId: "An identifier for a SKU."; + SoundboardSoundId: "An identifier for a soundboard sound."; UserId: "An identifier for a User"; WebhookId: "An identifier for a [`Webhook`]"; AuditLogEntryId: "An identifier for an audit log entry."; From 668946c0ad59808bced95b1dbd3b6beb807f15db Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sun, 17 Nov 2024 23:35:54 +0000 Subject: [PATCH 04/18] add list_guild_soundboard_sounds --- src/http/client.rs | 91 ++++++++++++++++++++++++++++--------- src/http/routing.rs | 8 ++++ src/model/guild/guild_id.rs | 13 ++++++ 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/src/http/client.rs b/src/http/client.rs index c93bdec9da5..12e62549096 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4210,28 +4210,6 @@ impl Http { self.fire(request).await } - /// Send a soundboard sound to a voice channel the user is connected to. Fires a Voice Channel - /// Effect Send Gateway event. - pub async fn send_soundboard_sound( - &self, - channel_id: ChannelId, - map: &impl serde::Serialize, - ) -> Result<()> { - let body = to_vec(map)?; - - self.wind(204, Request { - body: Some(body), - multipart: None, - headers: None, - method: LightMethod::Post, - route: Route::SendSoundboardSound { - channel_id, - }, - params: None, - }) - .await - } - /// Pins a message in a channel. pub async fn pin_message( &self, @@ -4392,6 +4370,75 @@ impl Http { .await } + /// Send a soundboard sound to a voice channel the user is connected to. + /// Fires a Voice Channel Effect Send Gateway event. + pub async fn send_soundboard_sound( + &self, + channel_id: ChannelId, + map: &impl serde::Serialize, + ) -> Result<()> { + let body = to_vec(map)?; + + self.wind(204, Request { + body: Some(body), + multipart: None, + headers: None, + method: LightMethod::Post, + route: Route::SendSoundboardSound { + channel_id, + }, + params: None, + }) + .await + } + + /// Returns an array of soundboard sound objects that can be used by all users. + pub async fn list_default_soundboard_sounds(&self) -> Result> { + self.fire(Request { + body: None, + multipart: None, + headers: None, + method: LightMethod::Get, + route: Route::DefaultSoundboardSounds, + params: None, + }) + .await + } + + /// Returns a list of the guild's soundboard sounds. + /// + /// Includes user fields if the bot has the `CREATE_GUILD_EXPRESSIONS` or + /// `MANAGE_GUILD_EXPRESSIONS` permission. + pub async fn list_guild_soundboard_sounds( + &self, + guild_id: GuildId, + ) -> Result> { + // Why, discord... + #[derive(Deserialize)] + struct ListGuildSoundboardSounds { + items: Vec, + } + + let mut value: ListGuildSoundboardSounds = self + .fire(Request { + body: None, + multipart: None, + headers: None, + method: LightMethod::Get, + route: Route::ListGuildSoundboardSounds { + guild_id, + }, + params: None, + }) + .await?; + + for sound in &mut value.items { + sound.guild_id = Some(guild_id); + } + + Ok(value.items) + } + /// Fires off a request, deserializing the response reader via the given type bound. /// /// If you don't need to deserialize the response and want the response instance itself, use diff --git a/src/http/routing.rs b/src/http/routing.rs index 245e3644f8c..4c38dbf22cd 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -409,6 +409,14 @@ routes! ('a, { api!("/channels/{}/send-soundboard-sound", channel_id), Some(RatelimitingKind::PathAndId(GenericId::new(channel_id.get()))); + DefaultSoundboardSounds, + api!("/soundboard-default-sounds"), + Some(RatelimitingKind::Path); + + ListGuildSoundboardSounds { guild_id: GuildId }, + api!("/guilds/{}/soundboard-sounds", guild_id), + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); + User { user_id: UserId }, api!("/users/{}", user_id), Some(RatelimitingKind::Path); diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 2eecf1f9cde..d434bc8abc4 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -1598,6 +1598,19 @@ impl GuildId { pub async fn get_active_threads(self, http: &Http) -> Result { http.get_guild_active_threads(self).await } + + /// Returns a list of the guild's soundboard sounds. + /// + /// Includes user fields if the bot has the `CREATE_GUILD_EXPRESSIONS` or + /// `MANAGE_GUILD_EXPRESSIONS` permission. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if there is an error in the deserialization, or if the bot + /// issuing the request is not in the guild. + pub async fn list_guild_soundboard_sounds(self, http: &Http) -> Result> { + http.list_guild_soundboard_sounds(self).await + } } impl From for GuildId { From 28ebaad295bb78a6f33daf98a3f6952e687f677e Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Sun, 17 Nov 2024 23:40:43 +0000 Subject: [PATCH 05/18] add get_guild_soundboard_sound --- src/http/client.rs | 28 ++++++++++++++++++++++++++++ src/http/routing.rs | 4 ++++ src/model/guild/guild_id.rs | 17 +++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/http/client.rs b/src/http/client.rs index 12e62549096..b0377f091fa 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4432,6 +4432,7 @@ impl Http { }) .await?; + // Ensure that the guild_id is set on all sounds for sound in &mut value.items { sound.guild_id = Some(guild_id); } @@ -4439,6 +4440,33 @@ impl Http { Ok(value.items) } + /// Returns a soundboard sound object for the given sound id. + /// + /// Includes the user field if the bot has the `CREATE_GUILD_EXPRESSIONS` or + /// `MANAGE_GUILD_EXPRESSIONS` permission. + pub async fn get_guild_soundboard_sound( + &self, + guild_id: GuildId, + sound_id: SoundboardSoundId, + ) -> Result { + let mut value: SoundboardSound = self + .fire(Request { + body: None, + multipart: None, + headers: None, + method: LightMethod::Get, + route: Route::GuildSoundboardSound { + guild_id, + sound_id, + }, + params: None, + }) + .await?; + + value.guild_id = Some(guild_id); + Ok(value) + } + /// Fires off a request, deserializing the response reader via the given type bound. /// /// If you don't need to deserialize the response and want the response instance itself, use diff --git a/src/http/routing.rs b/src/http/routing.rs index 4c38dbf22cd..adf7544a96e 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -417,6 +417,10 @@ routes! ('a, { api!("/guilds/{}/soundboard-sounds", guild_id), Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); + GuildSoundboardSound { guild_id: GuildId, sound_id: SoundboardSoundId }, + api!("/guilds/{}/soundboard-sounds/{}", guild_id, sound_id), + Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); + User { user_id: UserId }, api!("/users/{}", user_id), Some(RatelimitingKind::Path); diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index d434bc8abc4..6e7ff57d726 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -1611,6 +1611,23 @@ impl GuildId { pub async fn list_guild_soundboard_sounds(self, http: &Http) -> Result> { http.list_guild_soundboard_sounds(self).await } + + /// Returns a soundboard sound object for the given sound id. + /// + /// Includes the user field if the bot has the `CREATE_GUILD_EXPRESSIONS` or + /// `MANAGE_GUILD_EXPRESSIONS` permission. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if there is an error in the deserialization, or if the bot + /// issuing the request is not in the guild. + pub async fn get_guild_soundboard_sound( + self, + http: &Http, + sound_id: SoundboardSoundId, + ) -> Result { + http.get_guild_soundboard_sound(self, sound_id).await + } } impl From for GuildId { From 7b332a305a026c2e6055ac0898f77497b8c5656a Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:03:08 +0000 Subject: [PATCH 06/18] add create_guild_soundboard_sound --- src/builder/create_guild_soundboard_sound.rs | 91 ++++++++++++++++++++ src/builder/mod.rs | 2 + src/http/client.rs | 27 +++++- src/http/routing.rs | 2 +- src/model/guild/guild_id.rs | 15 ++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 src/builder/create_guild_soundboard_sound.rs diff --git a/src/builder/create_guild_soundboard_sound.rs b/src/builder/create_guild_soundboard_sound.rs new file mode 100644 index 00000000000..fa02da83b58 --- /dev/null +++ b/src/builder/create_guild_soundboard_sound.rs @@ -0,0 +1,91 @@ +use std::borrow::Cow; + +use super::CreateAttachment; +#[cfg(feature = "http")] +use crate::all::Http; +use crate::model::prelude::*; + +/// A builder to create a guild soundboard sound +/// +/// [Discord docs](https://discord.com/developers/docs/resources/soundboard#get-guild-soundboard-sound) +#[derive(serde::Serialize, Clone, Debug)] +#[must_use] +pub struct CreateGuildSoundboardSound<'a> { + name: Cow<'static, str>, + sound: String, + volume: Option, + emoji_id: Option, + emoji_name: Option>, + audit_log_reason: Option<&'a str>, +} + +impl<'a> CreateGuildSoundboardSound<'a> { + /// Creates a new builder with the given data. + pub fn new(name: impl Into>, sound: &CreateAttachment<'a>) -> Self { + Self { + name: name.into(), + sound: sound.to_base64(), + volume: None, + emoji_id: None, + emoji_name: None, + audit_log_reason: None, + } + } + + /// Set the name of the guild soundboard sound, replacing the current value as set in + /// [`Self::new`]. + /// + /// **Note**: Must be between 2 and 32 characters long. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = name.into(); + self + } + + /// Set the soundboard file. Replaces the current value as set in [`Self::new`]. + /// + /// **Note**: Must be a MPG or OGG, max 512 KB and max duration of 5.2 secons. + pub fn sound(mut self, sound: &CreateAttachment<'a>) -> Self { + self.sound = sound.to_base64(); + self + } + + /// Set the volume of the soundboard sound. + /// + /// **Note**: Must be between 0.0 and 1.0. + pub fn volume(mut self, volume: f64) -> Self { + self.volume = Some(volume); + self + } + + /// Set the emoji id of the soundboard sound. + pub fn emoji_id(mut self, emoji_id: EmojiId) -> Self { + self.emoji_id = Some(emoji_id); + self + } + + /// Set the unicode character (emoji name) of a standard emoji for the soundboard sound + pub fn emoji_name(mut self, emoji_name: impl Into>) -> Self { + self.emoji_name = Some(emoji_name.into()); + self + } + + /// Sets the request's audit log reason. + pub fn audit_log_reason(mut self, reason: &'a str) -> Self { + self.audit_log_reason = Some(reason); + self + } + + /// Creates a new guild soundboard sound in the guild with the data set, if any. + /// + /// **Note**: Requires the [Create Guild Expressions] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. + /// + /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS + #[cfg(feature = "http")] + pub async fn execute(self, http: &Http, guild_id: GuildId) -> Result { + http.create_guild_soundboard_sound(guild_id, &self, self.audit_log_reason).await + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 60e15121ec1..40daf214395 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -46,6 +46,7 @@ mod create_components; mod create_embed; mod create_forum_post; mod create_forum_tag; +mod create_guild_soundboard_sound; mod create_interaction_response; mod create_interaction_response_followup; mod create_invite; @@ -90,6 +91,7 @@ pub use create_components::*; pub use create_embed::*; pub use create_forum_post::*; pub use create_forum_tag::*; +pub use create_guild_soundboard_sound::*; pub use create_interaction_response::*; pub use create_interaction_response_followup::*; pub use create_invite::*; diff --git a/src/http/client.rs b/src/http/client.rs index b0377f091fa..7acf752e274 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4425,7 +4425,7 @@ impl Http { multipart: None, headers: None, method: LightMethod::Get, - route: Route::ListGuildSoundboardSounds { + route: Route::GuildSoundboardSounds { guild_id, }, params: None, @@ -4467,6 +4467,31 @@ impl Http { Ok(value) } + /// Creates a guild soundboard sound. + pub async fn create_guild_soundboard_sound( + &self, + guild_id: GuildId, + map: &impl serde::Serialize, + audit_log_reason: Option<&str>, + ) -> Result { + let body = to_vec(map)?; + let mut value: SoundboardSound = self + .fire(Request { + body: Some(body), + multipart: None, + headers: audit_log_reason.map(reason_into_header), + method: LightMethod::Post, + route: Route::GuildSoundboardSounds { + guild_id, + }, + params: None, + }) + .await?; + + value.guild_id = Some(guild_id); + Ok(value) + } + /// Fires off a request, deserializing the response reader via the given type bound. /// /// If you don't need to deserialize the response and want the response instance itself, use diff --git a/src/http/routing.rs b/src/http/routing.rs index adf7544a96e..41c9981cae5 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -413,7 +413,7 @@ routes! ('a, { api!("/soundboard-default-sounds"), Some(RatelimitingKind::Path); - ListGuildSoundboardSounds { guild_id: GuildId }, + GuildSoundboardSounds { guild_id: GuildId }, api!("/guilds/{}/soundboard-sounds", guild_id), Some(RatelimitingKind::PathAndId(GenericId::new(guild_id.get()))); diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 6e7ff57d726..7c1069e96c6 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -10,6 +10,7 @@ use crate::builder::{ AddMember, CreateChannel, CreateCommand, + CreateGuildSoundboardSound, CreateScheduledEvent, CreateSticker, EditAutoModRule, @@ -1628,6 +1629,20 @@ impl GuildId { ) -> Result { http.get_guild_soundboard_sound(self, sound_id).await } + + /// Creates a soundboard sound object on the guild. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if there is an error in the deserialization, or if the bot + /// issuing the request is not in the guild. + pub async fn create_guild_soundboard_sound( + self, + http: &Http, + builder: CreateGuildSoundboardSound<'_>, + ) -> Result { + builder.execute(http, self).await + } } impl From for GuildId { From 7fee139bb0d05475a9b3b32c944a947865c57621 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:10:31 +0000 Subject: [PATCH 07/18] add edit_guild_soundboard_sound --- src/builder/edit_guild_soundboard_sound.rs | 79 ++++++++++++++++++++++ src/builder/mod.rs | 2 + src/http/client.rs | 27 ++++++++ src/model/guild/guild_id.rs | 16 +++++ 4 files changed, 124 insertions(+) create mode 100644 src/builder/edit_guild_soundboard_sound.rs diff --git a/src/builder/edit_guild_soundboard_sound.rs b/src/builder/edit_guild_soundboard_sound.rs new file mode 100644 index 00000000000..227f4fb1e8b --- /dev/null +++ b/src/builder/edit_guild_soundboard_sound.rs @@ -0,0 +1,79 @@ +use std::borrow::Cow; + +#[cfg(feature = "http")] +use crate::all::Http; +use crate::model::prelude::*; + +/// A builder to modify a guild soundboard sound +/// +/// [Discord docs](https://discord.com/developers/docs/resources/soundboard#get-guild-soundboard-sound) +#[derive(Default, serde::Serialize, Clone, Debug)] +#[must_use] +pub struct EditGuildSoundboardSound<'a> { + name: Option>, + volume: Option, + emoji_id: Option, + emoji_name: Option>, + audit_log_reason: Option<&'a str>, +} + +impl<'a> EditGuildSoundboardSound<'a> { + /// Equivalent to [`Self::default`]. + pub fn new() -> Self { + Self::default() + } + + /// Set the name of the guild soundboard sound, replacing the current value as set in + /// [`Self::new`]. + /// + /// **Note**: Must be between 2 and 32 characters long. + pub fn name(mut self, name: impl Into>) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the volume of the soundboard sound. + /// + /// **Note**: Must be between 0.0 and 1.0. + pub fn volume(mut self, volume: f64) -> Self { + self.volume = Some(volume); + self + } + + /// Set the emoji id of the soundboard sound. + pub fn emoji_id(mut self, emoji_id: EmojiId) -> Self { + self.emoji_id = Some(emoji_id); + self + } + + /// Set the unicode character (emoji name) of a standard emoji for the soundboard sound + pub fn emoji_name(mut self, emoji_name: impl Into>) -> Self { + self.emoji_name = Some(emoji_name.into()); + self + } + + /// Sets the request's audit log reason. + pub fn audit_log_reason(mut self, reason: &'a str) -> Self { + self.audit_log_reason = Some(reason); + self + } + + /// Modifies a new guild soundboard sound in the guild with the data set, if any. + /// + /// **Note**: Requires the [Create Guild Expressions] permission. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. + /// + /// [Create Guild Expressions]: Permissions::CREATE_GUILD_EXPRESSIONS + #[cfg(feature = "http")] + pub async fn execute( + self, + http: &Http, + guild_id: GuildId, + sound_id: SoundboardSoundId, + ) -> Result { + http.edit_guild_soundboard_sound(guild_id, sound_id, &self, self.audit_log_reason).await + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 40daf214395..6ffb84e7ab9 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -61,6 +61,7 @@ mod edit_automod_rule; mod edit_channel; mod edit_command; mod edit_guild; +mod edit_guild_soundboard_sound; mod edit_guild_welcome_screen; mod edit_guild_widget; mod edit_interaction_response; @@ -106,6 +107,7 @@ pub use edit_automod_rule::*; pub use edit_channel::*; pub use edit_command::*; pub use edit_guild::*; +pub use edit_guild_soundboard_sound::*; pub use edit_guild_welcome_screen::*; pub use edit_guild_widget::*; pub use edit_interaction_response::*; diff --git a/src/http/client.rs b/src/http/client.rs index 7acf752e274..07d1669eda7 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4492,6 +4492,33 @@ impl Http { Ok(value) } + /// Edits a guild soundboard sound. + pub async fn edit_guild_soundboard_sound( + &self, + guild_id: GuildId, + sound_id: SoundboardSoundId, + map: &impl serde::Serialize, + audit_log_reason: Option<&str>, + ) -> Result { + let body = to_vec(map)?; + let mut value: SoundboardSound = self + .fire(Request { + body: Some(body), + multipart: None, + headers: audit_log_reason.map(reason_into_header), + method: LightMethod::Patch, + route: Route::GuildSoundboardSound { + guild_id, + sound_id, + }, + params: None, + }) + .await?; + + value.guild_id = Some(guild_id); + Ok(value) + } + /// Fires off a request, deserializing the response reader via the given type bound. /// /// If you don't need to deserialize the response and want the response instance itself, use diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 7c1069e96c6..c7c0e848706 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -17,6 +17,7 @@ use crate::builder::{ EditCommand, EditCommandPermissions, EditGuild, + EditGuildSoundboardSound, EditGuildWelcomeScreen, EditGuildWidget, EditMember, @@ -1643,6 +1644,21 @@ impl GuildId { ) -> Result { builder.execute(http, self).await } + + /// Edits a soundboard sound object on the guild. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if there is an error in the deserialization, or if the bot + /// issuing the request is not in the guild. + pub async fn edit_guild_soundboard_sound( + self, + sound_id: SoundboardSoundId, + http: &Http, + builder: EditGuildSoundboardSound<'_>, + ) -> Result { + builder.execute(http, self, sound_id).await + } } impl From for GuildId { From 6c434b3a955b1efb2ac4efe0971073628e21aa99 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:13:43 +0000 Subject: [PATCH 08/18] add delete_guild_soundboard_sound --- src/http/client.rs | 21 +++++++++++++++++++++ src/model/guild/guild_id.rs | 15 +++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/http/client.rs b/src/http/client.rs index 07d1669eda7..f5e7e8a2f21 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4519,6 +4519,27 @@ impl Http { Ok(value) } + /// Deletes a guild soundboard sound. + pub async fn delete_guild_soundboard_sound( + &self, + guild_id: GuildId, + sound_id: SoundboardSoundId, + audit_log_reason: Option<&str>, + ) -> Result<()> { + self.wind(204, Request { + body: None, + multipart: None, + headers: audit_log_reason.map(reason_into_header), + method: LightMethod::Delete, + route: Route::GuildSoundboardSound { + guild_id, + sound_id, + }, + params: None, + }) + .await + } + /// Fires off a request, deserializing the response reader via the given type bound. /// /// If you don't need to deserialize the response and want the response instance itself, use diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index c7c0e848706..4aaab1f260f 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -1659,6 +1659,21 @@ impl GuildId { ) -> Result { builder.execute(http, self, sound_id).await } + + /// Deletes a soundboard sound object on the guild. + /// + /// # Errors + /// + /// Returns an [`Error::Http`] if there is an error in the deserialization, or if the bot + /// issuing the request is not in the guild. + pub async fn delete_guild_soundboard_sound( + self, + sound_id: SoundboardSoundId, + http: &Http, + audit_log_reason: Option<&str>, + ) -> Result<()> { + http.delete_guild_soundboard_sound(self, sound_id, audit_log_reason).await + } } impl From for GuildId { From 555c89d0ebe01e44f3f60960019e87bde2878702 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:36:15 +0000 Subject: [PATCH 09/18] begin adding gateway events --- src/gateway/client/dispatch.rs | 19 +++++++++ src/gateway/client/event_handler.rs | 15 +++++++ src/model/event.rs | 61 +++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/src/gateway/client/dispatch.rs b/src/gateway/client/dispatch.rs index fba9e54b907..ac61a602a2f 100644 --- a/src/gateway/client/dispatch.rs +++ b/src/gateway/client/dispatch.rs @@ -476,6 +476,25 @@ fn update_cache_with_event( Event::MessagePollVoteRemove(event) => FullEvent::MessagePollVoteRemove { event, }, + Event::GuildSoundboardSoundCreate(event) => FullEvent::GuildSoundboardSoundCreate { + sound: event.soundboard_sound, + guild_id: event.guild_id, + }, + Event::GuildSoundboardSoundUpdate(event) => FullEvent::GuildSoundboardSoundUpdate { + sound: event.soundboard_sound, + guild_id: event.guild_id, + }, + Event::GuildSoundboardSoundDelete(event) => FullEvent::GuildSoundboardSoundDelete { + sound_id: event.sound_id, + guild_id: event.guild_id, + }, + Event::GuildSoundboardSoundsUpdate(event) => FullEvent::GuildSoundboardSoundsUpdate { + sounds: event.soundboard_sounds, + guild_id: event.guild_id, + }, + Event::VoiceChannelEffectSend(event) => FullEvent::VoiceChannelEffectSend { + event, + }, }; (event, extra_event) diff --git a/src/gateway/client/event_handler.rs b/src/gateway/client/event_handler.rs index b6b2da75bdc..bbdb43acaa1 100644 --- a/src/gateway/client/event_handler.rs +++ b/src/gateway/client/event_handler.rs @@ -501,6 +501,21 @@ event_handler! { /// Dispatched when an HTTP rate limit is hit Ratelimit { data: RatelimitInfo } => async fn ratelimit(&self); + + /// Sent when a guild soundboard sound is created. + GuildSoundboardSoundCreate { sound: SoundboardSound, guild_id: GuildId } => async fn guild_soundboard_sound_create(&self, ctx: Context); + + /// Sent when a guild soundboard sound is updated. + GuildSoundboardSoundUpdate { sound: SoundboardSound, guild_id: GuildId } => async fn guild_soundboard_sound_update(&self, ctx: Context); + + /// Sent when a guild soundboard sound is deleted. + GuildSoundboardSoundDelete { sound_id: SoundboardSoundId, guild_id: GuildId } => async fn guild_soundboard_sound_delete(&self, ctx: Context); + + /// Sent when multiple guild soundboard sounds are updated. + GuildSoundboardSoundsUpdate { sounds: Vec, guild_id: GuildId } => async fn guild_soundboard_sounds_update(&self, ctx: Context); + + /// Sent when someone sends an effect, such as an emoji reaction or a soundboard sound, in a voice channel the current user is connected to. + VoiceChannelEffectSend { event: VoiceChannelEffectSendEvent } => async fn voice_channel_effect_send(&self, ctx: Context); } /// This core trait for handling raw events diff --git a/src/model/event.rs b/src/model/event.rs index 9d6157031cc..f53358c79ec 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1067,6 +1067,56 @@ pub struct MessagePollVoteRemoveEvent { pub answer_id: AnswerId, } +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. +/// +/// [Discord docs](https://discord.com/developers/docs/events/gateway-events#guild-soundboard-sound-create). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct GuildSoundboardSoundCreateUpdateEvent { + pub soundboard_sound: SoundboardSound, + pub guild_id: GuildId, +} + +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. +/// +/// [Discord docs](https://discord.com/developers/docs/events/gateway-events#guild-soundboard-sounds-update). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct GuildSoundboardSoundsUpdateEvent { + pub soundboard_sounds: Vec, + pub guild_id: GuildId, +} + +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. +/// +/// [Discord docs](https://discord.com/developers/docs/events/gateway-events#guild-soundboard-sounds-update). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct GuildSoundboardSoundDeleteEvent { + pub sound_id: SoundboardSoundId, + pub guild_id: GuildId, +} + +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. +/// +/// [Discord docs](https://discord.com/developers/docs/events/gateway-events#guild-soundboard-sounds-update). +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct VoiceChannelEffectSendEvent { + pub channel_id: ChannelId, + pub guild_id: GuildId, + pub user_id: UserId, + pub emoji: Option, + pub animation_type: Option, // TODO + pub animation_id: Option, + pub sound_id: Option, + pub sound_volume: Option, +} + /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#payload-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Debug, Clone, Serialize)] @@ -1325,6 +1375,17 @@ pub enum Event { MessagePollVoteAdd(MessagePollVoteAddEvent), /// A user has removed a previous vote on a Message Poll. MessagePollVoteRemove(MessagePollVoteRemoveEvent), + /// Sent when a guild soundboard sound is created + GuildSoundboardSoundCreate(GuildSoundboardSoundCreateUpdateEvent), + /// Sent when a guild soundboard sound is updated + GuildSoundboardSoundUpdate(GuildSoundboardSoundCreateUpdateEvent), + /// Sent when multiple guild soundboard sounds are updated. + GuildSoundboardSoundsUpdate(GuildSoundboardSoundsUpdateEvent), + /// Sent when a guild soundboard sound is deleted + GuildSoundboardSoundDelete(GuildSoundboardSoundDeleteEvent), + /// Sent when someone sends an effect, such as an emoji reaction or a soundboard sound, in a + /// voice channel the current user is connected to. + VoiceChannelEffectSend(VoiceChannelEffectSendEvent), } impl Event { From 2daaf23829cebc7f029e97c28cc84c455dcf6d4f Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:46:47 +0000 Subject: [PATCH 10/18] add gateway bits --- src/constants.rs | 2 ++ src/gateway/client/dispatch.rs | 4 ++++ src/gateway/client/event_handler.rs | 5 +++++ src/gateway/sharding/mod.rs | 14 ++++++++++++++ src/gateway/ws.rs | 20 ++++++++++++++++++++ src/model/event.rs | 15 +++++++++++++++ 6 files changed, 60 insertions(+) diff --git a/src/constants.rs b/src/constants.rs index 86a9d925cf6..4318b183c6e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -66,6 +66,8 @@ enum_number! { Hello = 10, /// Sent immediately following a client heartbeat that was received. HeartbeatAck = 11, + /// Request information about soundboard sounds in a set of guilds. + RequestSoundboardSounds = 31, _ => Unknown(u8), } } diff --git a/src/gateway/client/dispatch.rs b/src/gateway/client/dispatch.rs index ac61a602a2f..f7980bfb51f 100644 --- a/src/gateway/client/dispatch.rs +++ b/src/gateway/client/dispatch.rs @@ -495,6 +495,10 @@ fn update_cache_with_event( Event::VoiceChannelEffectSend(event) => FullEvent::VoiceChannelEffectSend { event, }, + Event::SoundboardSounds(event) => FullEvent::SoundboardSounds { + sounds: event.soundboard_sounds, + guild_id: event.guild_id, + }, }; (event, extra_event) diff --git a/src/gateway/client/event_handler.rs b/src/gateway/client/event_handler.rs index bbdb43acaa1..d4cf290f6d8 100644 --- a/src/gateway/client/event_handler.rs +++ b/src/gateway/client/event_handler.rs @@ -516,6 +516,11 @@ event_handler! { /// Sent when someone sends an effect, such as an emoji reaction or a soundboard sound, in a voice channel the current user is connected to. VoiceChannelEffectSend { event: VoiceChannelEffectSendEvent } => async fn voice_channel_effect_send(&self, ctx: Context); + + /// Includes a guild's list of soundboard sounds. + /// + /// Sent in response to Request Soundboard Sounds. + SoundboardSounds { guild_id: GuildId, sounds: Vec } => async fn soundboard_sounds(&self, ctx: Context); } /// This core trait for handling raw events diff --git a/src/gateway/sharding/mod.rs b/src/gateway/sharding/mod.rs index 9ce61ce27a5..0cedcc92106 100644 --- a/src/gateway/sharding/mod.rs +++ b/src/gateway/sharding/mod.rs @@ -702,6 +702,20 @@ impl Shard { .await } + /// Requests that soundboard sounds for a list of GuildId's. The server will send Soundboard + /// Sounds events for each guild in response. + /// + /// + /// # Errors + /// Errors if there is a problem with the WS connection. + /// + /// [`Event::GuildMembersChunk`]: crate::model::event::Event::GuildMembersChunk + /// [`GuildId`]: crate::model::guild::GuildId + #[cfg_attr(feature = "tracing_instrument", instrument(skip(self)))] + pub async fn request_soundboard_sounds(&mut self, guild_ids: Vec) -> Result<()> { + self.client.send_request_soundboard_sounds(&self.shard_info, guild_ids).await + } + /// Sets the shard as going into identifying stage, which sets: /// - the time that the last heartbeat sent as being now /// - the `stage` to [`ConnectionStage::Identifying`] diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index 9fd28e1a17f..0d6e25e1f8d 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -67,6 +67,9 @@ enum WebSocketMessageData<'a> { token: &'a str, seq: u64, }, + RequestSoundboardSounds { + guild_ids: Vec, + }, } #[derive(Serialize)] @@ -194,6 +197,23 @@ impl WsClient { .await } + #[expect(clippy::missing_errors_doc)] + pub async fn send_request_soundboard_sounds( + &mut self, + shard_info: &ShardInfo, + guild_ids: Vec, + ) -> Result<()> { + debug!("[{:?}] Requesting soundboard sounds for guild id: {:?}", shard_info, guild_ids); + + self.send_json(&WebSocketMessage { + op: Opcode::RequestSoundboardSounds, + d: WebSocketMessageData::RequestSoundboardSounds { + guild_ids, + }, + }) + .await + } + /// # Errors /// /// Errors if there is a problem with the WS connection. diff --git a/src/model/event.rs b/src/model/event.rs index f53358c79ec..7a66452a37b 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1117,6 +1117,17 @@ pub struct VoiceChannelEffectSendEvent { pub sound_volume: Option, } +/// Includes a guild's list of soundboard sounds +/// +/// Sent in response to the Request Soundboard Sounds. +#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct SoundboardSoundsEvent { + pub guild_id: GuildId, + pub soundboard_sounds: Vec, +} + /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#payload-structure). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] #[derive(Debug, Clone, Serialize)] @@ -1386,6 +1397,10 @@ pub enum Event { /// Sent when someone sends an effect, such as an emoji reaction or a soundboard sound, in a /// voice channel the current user is connected to. VoiceChannelEffectSend(VoiceChannelEffectSendEvent), + /// Includes a guild's list of soundboard sounds. + /// + /// Sent in response to Request Soundboard Sounds. + SoundboardSounds(SoundboardSoundsEvent), } impl Event { From 9d3223d32777ca1267adf02d6818ec8dc91bf3db Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:51:30 +0000 Subject: [PATCH 11/18] fix gateway intents --- src/model/event.rs | 4 ++-- src/model/gateway.rs | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/model/event.rs b/src/model/event.rs index 7a66452a37b..b087c097368 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -193,7 +193,7 @@ pub struct GuildDeleteEvent { pub guild: UnavailableGuild, } -/// Requires [`GatewayIntents::GUILD_EMOJIS_AND_STICKERS`]. +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#guild-emojis-update). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] @@ -364,7 +364,7 @@ impl<'de> Deserialize<'de> for GuildRoleUpdateEvent { } } -/// Requires [`GatewayIntents::GUILD_EMOJIS_AND_STICKERS`]. +/// Requires [`GatewayIntents::GUILD_EXPRESSIONS`]. /// /// [Discord docs](https://discord.com/developers/docs/topics/gateway-events#guild-stickers-update). #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] diff --git a/src/model/gateway.rs b/src/model/gateway.rs index 3bff63ebbb0..f68048bfdef 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -487,7 +487,11 @@ bitflags! { /// Enables the following gateway events: /// - GUILD_EMOJIS_UPDATE /// - GUILD_STICKERS_UPDATE - const GUILD_EMOJIS_AND_STICKERS = 1 << 3; + /// - GUILD_SOUNDBOARD_SOUND_CREATE + /// - GUILD_SOUNDBOARD_SOUND_UPDATE + /// - GUILD_SOUNDBOARD_SOUND_DELETE + /// - GUILD_SOUNDBOARD_SOUNDS_UPDATE + const GUILD_EXPRESSIONS = 1 << 3; /// Enables the following gateway events: /// - GUILD_INTEGRATIONS_UPDATE /// - INTEGRATION_CREATE @@ -632,13 +636,23 @@ impl GatewayIntents { self.contains(Self::GUILD_MODERATION) } - /// Shorthand for checking that the set of intents contains the [GUILD_EMOJIS_AND_STICKERS] + /// Shorthand for checking that the set of intents contains the [GUILD_EXPRESSIONS ] /// intent. /// - /// [GUILD_EMOJIS_AND_STICKERS]: Self::GUILD_EMOJIS_AND_STICKERS + /// [GUILD_EXPRESSIONS ]: Self::GUILD_EXPRESSIONS #[must_use] + #[deprecated = "Use `guild_expressions` instead"] pub const fn guild_emojis_and_stickers(self) -> bool { - self.contains(Self::GUILD_EMOJIS_AND_STICKERS) + self.contains(Self::GUILD_EXPRESSIONS) + } + + /// Shorthand for checking that the set of intents contains the [GUILD_EXPRESSIONS ] + /// intent. + /// + /// [GUILD_EXPRESSIONS ]: Self::GUILD_EXPRESSIONS + #[must_use] + pub const fn guild_expressions(self) -> bool { + self.contains(Self::GUILD_EXPRESSIONS) } /// Shorthand for checking that the set of intents contains the [GUILD_INTEGRATIONS] intent. From 697b6e551dc20ead863c2b195aa3568e19fa465d Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:53:58 +0000 Subject: [PATCH 12/18] fix builder error for SendSoundboardSound --- src/model/channel/channel_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 07c484932da..a9b10c0b822 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -6,7 +6,6 @@ use std::sync::Arc; #[cfg(feature = "model")] use futures::stream::Stream; -use crate::all::SendSoundboardSound; #[cfg(feature = "model")] use crate::builder::{ CreateAttachment, @@ -21,6 +20,7 @@ use crate::builder::{ EditStageInstance, EditThread, GetMessages, + SendSoundboardSound, }; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::Cache; From 649f05cff2ef7c0b58fba4ab7b6b83b9b828405d Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:55:14 +0000 Subject: [PATCH 13/18] fix docs build error in guild --- src/model/guild/soundboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/guild/soundboard.rs b/src/model/guild/soundboard.rs index b64a5d2ed43..df757dcae83 100644 --- a/src/model/guild/soundboard.rs +++ b/src/model/guild/soundboard.rs @@ -40,7 +40,7 @@ impl SoundboardSoundId { /// Print the direct link to the given soundboard sound: /// /// ```rust,no_run - /// # use serenity::model::soundboard::SoundboardSound; + /// # use serenity::model::guild::soundboard::SoundboardSound; /// # /// # fn run(sound: SoundboardSound) { /// // assuming sound has been set already From bd6e493bebd6e42ec8f69102bc2e642d8871de9c Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 00:58:05 +0000 Subject: [PATCH 14/18] add SendSoundboardSound to guild channel as well --- src/model/channel/guild_channel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 34704e21231..8d4e877992e 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -2,7 +2,6 @@ use std::fmt; use nonmax::{NonMaxU16, NonMaxU32, NonMaxU8}; -use crate::all::SendSoundboardSound; #[cfg(feature = "model")] use crate::builder::{ CreateMessage, @@ -12,6 +11,7 @@ use crate::builder::{ EditStageInstance, EditThread, EditVoiceState, + SendSoundboardSound }; #[cfg(feature = "cache")] use crate::cache::{self, Cache}; From 005b28a0cba63acd6881d42535f8c7bcba958817 Mon Sep 17 00:00:00 2001 From: Rootspring Date: Sun, 17 Nov 2024 20:01:09 -0500 Subject: [PATCH 15/18] Update mod.rs --- src/model/guild/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index 0e2699aef80..8a96c5d27e0 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -11,7 +11,7 @@ mod partial_guild; mod premium_tier; mod role; mod scheduled_event; -mod soundboard; +pub mod soundboard; mod system_channel; mod welcome_screen; From 44ce9fa230476e4a3bb9646d1f6d907128a8b02f Mon Sep 17 00:00:00 2001 From: Rootspring Date: Sun, 17 Nov 2024 20:04:14 -0500 Subject: [PATCH 16/18] Update soundboard.rs --- src/model/guild/soundboard.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/guild/soundboard.rs b/src/model/guild/soundboard.rs index df757dcae83..c84d40ebea6 100644 --- a/src/model/guild/soundboard.rs +++ b/src/model/guild/soundboard.rs @@ -44,7 +44,7 @@ impl SoundboardSoundId { /// # /// # fn run(sound: SoundboardSound) { /// // assuming sound has been set already - /// println!("Direct link to sound file: {}", sound.url()); + /// println!("Direct link to sound file: {}", sound.id.url()); /// # } /// ``` #[must_use] From 49631293bb4c81a825d7f3a000b373d436271b97 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 02:04:19 +0000 Subject: [PATCH 17/18] format --- src/model/channel/guild_channel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 8d4e877992e..7e7b1074b0b 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -11,7 +11,7 @@ use crate::builder::{ EditStageInstance, EditThread, EditVoiceState, - SendSoundboardSound + SendSoundboardSound, }; #[cfg(feature = "cache")] use crate::cache::{self, Cache}; From cf1861cc0211ae8006c3f86be3d86951283c6f99 Mon Sep 17 00:00:00 2001 From: Sanandan Sashikumar Date: Mon, 18 Nov 2024 18:38:55 +0000 Subject: [PATCH 18/18] update builders based on given feedback --- src/builder/create_guild_soundboard_sound.rs | 10 +++++++--- src/builder/edit_guild_soundboard_sound.rs | 5 +++++ src/builder/send_soundboard_sound.rs | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/builder/create_guild_soundboard_sound.rs b/src/builder/create_guild_soundboard_sound.rs index fa02da83b58..74e44f31cfc 100644 --- a/src/builder/create_guild_soundboard_sound.rs +++ b/src/builder/create_guild_soundboard_sound.rs @@ -12,10 +12,14 @@ use crate::model::prelude::*; #[must_use] pub struct CreateGuildSoundboardSound<'a> { name: Cow<'static, str>, - sound: String, + sound: Cow<'static, str>, + #[serde(skip_serializing_if = "Option::is_none")] volume: Option, + #[serde(skip_serializing_if = "Option::is_none")] emoji_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] emoji_name: Option>, + #[serde(skip)] audit_log_reason: Option<&'a str>, } @@ -24,7 +28,7 @@ impl<'a> CreateGuildSoundboardSound<'a> { pub fn new(name: impl Into>, sound: &CreateAttachment<'a>) -> Self { Self { name: name.into(), - sound: sound.to_base64(), + sound: sound.to_base64().into(), volume: None, emoji_id: None, emoji_name: None, @@ -45,7 +49,7 @@ impl<'a> CreateGuildSoundboardSound<'a> { /// /// **Note**: Must be a MPG or OGG, max 512 KB and max duration of 5.2 secons. pub fn sound(mut self, sound: &CreateAttachment<'a>) -> Self { - self.sound = sound.to_base64(); + self.sound = sound.to_base64().into(); self } diff --git a/src/builder/edit_guild_soundboard_sound.rs b/src/builder/edit_guild_soundboard_sound.rs index 227f4fb1e8b..4b0f6eb2e77 100644 --- a/src/builder/edit_guild_soundboard_sound.rs +++ b/src/builder/edit_guild_soundboard_sound.rs @@ -10,10 +10,15 @@ use crate::model::prelude::*; #[derive(Default, serde::Serialize, Clone, Debug)] #[must_use] pub struct EditGuildSoundboardSound<'a> { + #[serde(skip_serializing_if = "Option::is_none")] name: Option>, + #[serde(skip_serializing_if = "Option::is_none")] volume: Option, + #[serde(skip_serializing_if = "Option::is_none")] emoji_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] emoji_name: Option>, + #[serde(skip)] audit_log_reason: Option<&'a str>, } diff --git a/src/builder/send_soundboard_sound.rs b/src/builder/send_soundboard_sound.rs index da77b63847e..4fd322126ba 100644 --- a/src/builder/send_soundboard_sound.rs +++ b/src/builder/send_soundboard_sound.rs @@ -11,6 +11,7 @@ use crate::model::prelude::*; #[must_use] pub struct SendSoundboardSound { sound_id: SoundboardSoundId, + #[serde(skip_serializing_if = "Option::is_none")] source_guild_id: Option, }