diff --git a/src/builder/create_guild_soundboard_sound.rs b/src/builder/create_guild_soundboard_sound.rs new file mode 100644 index 00000000000..74e44f31cfc --- /dev/null +++ b/src/builder/create_guild_soundboard_sound.rs @@ -0,0 +1,95 @@ +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: 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>, +} + +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().into(), + 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().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 + } + + /// 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/edit_guild_soundboard_sound.rs b/src/builder/edit_guild_soundboard_sound.rs new file mode 100644 index 00000000000..4b0f6eb2e77 --- /dev/null +++ b/src/builder/edit_guild_soundboard_sound.rs @@ -0,0 +1,84 @@ +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> { + #[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>, +} + +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 d923cbcae63..6ffb84e7ab9 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; @@ -60,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; @@ -77,6 +79,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::*; @@ -89,6 +92,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::*; @@ -103,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::*; @@ -120,6 +125,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..4fd322126ba --- /dev/null +++ b/src/builder/send_soundboard_sound.rs @@ -0,0 +1,51 @@ +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + 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/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 fba9e54b907..f7980bfb51f 100644 --- a/src/gateway/client/dispatch.rs +++ b/src/gateway/client/dispatch.rs @@ -476,6 +476,29 @@ 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::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 b6b2da75bdc..d4cf290f6d8 100644 --- a/src/gateway/client/event_handler.rs +++ b/src/gateway/client/event_handler.rs @@ -501,6 +501,26 @@ 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); + + /// 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/http/client.rs b/src/http/client.rs index 15ee7f4b382..f5e7e8a2f21 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 + /// `604800 seconds` (or 7 days) 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, @@ -4372,6 +4370,176 @@ 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::GuildSoundboardSounds { + guild_id, + }, + params: None, + }) + .await?; + + // Ensure that the guild_id is set on all sounds + for sound in &mut value.items { + sound.guild_id = Some(guild_id); + } + + 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) + } + + /// 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) + } + + /// 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) + } + + /// 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/http/routing.rs b/src/http/routing.rs index 7602244f385..41c9981cae5 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -405,6 +405,22 @@ 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()))); + + DefaultSoundboardSounds, + api!("/soundboard-default-sounds"), + Some(RatelimitingKind::Path); + + GuildSoundboardSounds { guild_id: GuildId }, + 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/channel/channel_id.rs b/src/model/channel/channel_id.rs index 332a1a999ff..a9b10c0b822 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -20,6 +20,7 @@ use crate::builder::{ EditStageInstance, EditThread, GetMessages, + SendSoundboardSound, }; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::Cache; @@ -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..7e7b1074b0b 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -11,6 +11,7 @@ use crate::builder::{ EditStageInstance, EditThread, EditVoiceState, + SendSoundboardSound, }; #[cfg(feature = "cache")] use crate::cache::{self, Cache}; @@ -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/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/event.rs b/src/model/event.rs index 9d6157031cc..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))] @@ -1067,6 +1067,67 @@ 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, +} + +/// 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)] @@ -1325,6 +1386,21 @@ 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), + /// Includes a guild's list of soundboard sounds. + /// + /// Sent in response to Request Soundboard Sounds. + SoundboardSounds(SoundboardSoundsEvent), } impl Event { 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. diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 6d096589732..4aaab1f260f 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -10,12 +10,14 @@ use crate::builder::{ AddMember, CreateChannel, CreateCommand, + CreateGuildSoundboardSound, CreateScheduledEvent, CreateSticker, EditAutoModRule, EditCommand, EditCommandPermissions, EditGuild, + EditGuildSoundboardSound, EditGuildWelcomeScreen, EditGuildWidget, EditMember, @@ -165,8 +167,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 +184,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 +197,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 @@ -1584,6 +1600,80 @@ 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 { 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. diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index c1d061a51d9..8a96c5d27e0 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; +pub 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..c84d40ebea6 --- /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::guild::soundboard::SoundboardSound; + /// # + /// # fn run(sound: SoundboardSound) { + /// // assuming sound has been set already + /// println!("Direct link to sound file: {}", sound.id.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.";