diff --git a/src/builder/create_stage_instance.rs b/src/builder/create_stage_instance.rs new file mode 100644 index 00000000000..bf517510181 --- /dev/null +++ b/src/builder/create_stage_instance.rs @@ -0,0 +1,26 @@ +use std::collections::HashMap; + +use serde_json::Value; + +use crate::internal::prelude::*; + +/// Creates a [`StageInstance`]. +/// +/// [`StageInstance`]: crate::model::channel::StageInstance +#[derive(Clone, Debug, Default)] +pub struct CreateStageInstance(pub HashMap<&'static str, Value>); + +impl CreateStageInstance { + // Sets the stage channel id of the stage channel instance. + pub fn channel_id(&mut self, id: u64) -> &mut Self { + self.0.insert("channel_id", Value::Number(Number::from(id))); + self + } + + /// Sets the topic of the stage channel instance. + pub fn topic(&mut self, topic: D) -> &mut Self { + self.0.insert("topic", Value::String(topic.to_string())); + + self + } +} diff --git a/src/builder/edit_stage_instance.rs b/src/builder/edit_stage_instance.rs new file mode 100644 index 00000000000..78bc4f7c10b --- /dev/null +++ b/src/builder/edit_stage_instance.rs @@ -0,0 +1,18 @@ +use std::collections::HashMap; + +use serde_json::Value; + +/// Edits a [`StageInstance`]. +/// +/// [`StageInstance`]: crate::model::channel::StageInstance +#[derive(Clone, Debug, Default)] +pub struct EditStageInstance(pub HashMap<&'static str, Value>); + +impl EditStageInstance { + /// Sets the topic of the stage channel instance. + pub fn topic(&mut self, topic: D) -> &mut Self { + self.0.insert("topic", Value::String(topic.to_string())); + + self + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 21252683258..45e283388ce 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -27,6 +27,7 @@ mod create_interaction_response; mod create_interaction_response_followup; mod create_invite; mod create_message; +mod create_stage_instance; mod edit_channel; mod edit_guild; mod edit_guild_welcome_screen; @@ -38,6 +39,7 @@ mod edit_member; mod edit_message; mod edit_profile; mod edit_role; +mod edit_stage_instance; mod edit_voice_state; mod edit_webhook_message; mod execute_webhook; @@ -50,6 +52,7 @@ pub use self::{ create_embed::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp}, create_invite::CreateInvite, create_message::CreateMessage, + create_stage_instance::CreateStageInstance, edit_channel::EditChannel, edit_guild::EditGuild, edit_guild_welcome_screen::EditGuildWelcomeScreen, @@ -58,6 +61,7 @@ pub use self::{ edit_message::EditMessage, edit_profile::EditProfile, edit_role::EditRole, + edit_stage_instance::EditStageInstance, edit_voice_state::EditVoiceState, edit_webhook_message::EditWebhookMessage, execute_webhook::ExecuteWebhook, diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs index 4247a6a9344..30eab7e3cda 100644 --- a/src/client/dispatch.rs +++ b/src/client/dispatch.rs @@ -810,5 +810,26 @@ async fn handle_event( event_handler.application_command_delete(context, event.application_command).await; }); }, + DispatchEvent::Model(Event::StageInstanceCreate(event)) => { + let event_handler = Arc::clone(event_handler); + + tokio::spawn(async move { + event_handler.stage_instance_create(context, event.stage_instance).await; + }); + }, + DispatchEvent::Model(Event::StageInstanceUpdate(event)) => { + let event_handler = Arc::clone(event_handler); + + tokio::spawn(async move { + event_handler.stage_instance_update(context, event.stage_instance).await; + }); + }, + DispatchEvent::Model(Event::StageInstanceDelete(event)) => { + let event_handler = Arc::clone(event_handler); + + tokio::spawn(async move { + event_handler.stage_instance_delete(context, event.stage_instance).await; + }); + }, } } diff --git a/src/client/event_handler.rs b/src/client/event_handler.rs index cceae0a34e8..4a0c70c01ba 100644 --- a/src/client/event_handler.rs +++ b/src/client/event_handler.rs @@ -493,6 +493,21 @@ pub trait EventHandler: Send + Sync { _application_command: ApplicationCommand, ) { } + + /// Dispatched when a stage instance is created. + /// + /// Provides the created stage instance. + async fn stage_instance_create(&self, _ctx: Context, _stage_instance: StageInstance) {} + + /// Dispatched when a stage instance is updated. + /// + /// Provides the created stage instance. + async fn stage_instance_update(&self, _ctx: Context, _stage_instance: StageInstance) {} + + /// Dispatched when a stage instance is deleted. + /// + /// Provides the created stage instance. + async fn stage_instance_delete(&self, _ctx: Context, _stage_instance: StageInstance) {} } /// This core trait for handling raw events diff --git a/src/http/client.rs b/src/http/client.rs index 72601d7c4da..62315097b33 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -370,6 +370,16 @@ impl Http { .await } + /// Creates a stage instance. + pub async fn create_stage_instance(&self, map: &Value) -> Result { + self.fire(Request { + body: Some(map.to_string().as_bytes()), + headers: None, + route: RouteInfo::CreateStageInstance, + }) + .await + } + /// Creates an emoji in the given [`Guild`] with the given data. /// /// View the source code for [`Guild::create_emoji`] method to see what @@ -742,6 +752,18 @@ impl Http { .await } + /// Deletes a stage instance. + pub async fn delete_stage_instance(&self, channel_id: u64) -> Result<()> { + self.wind(204, Request { + body: None, + headers: None, + route: RouteInfo::DeleteStageInstance { + channel_id, + }, + }) + .await + } + /// Deletes an emoji from a server. pub async fn delete_emoji(&self, guild_id: u64, emoji_id: u64) -> Result<()> { self.wind(204, Request { @@ -1069,6 +1091,18 @@ impl Http { .await } + /// Edits a stage instance. + pub async fn edit_stage_instance(&self, channel_id: u64, map: &Value) -> Result { + self.fire(Request { + body: Some(map.to_string().as_bytes()), + headers: None, + route: RouteInfo::EditStageInstance { + channel_id, + }, + }) + .await + } + /// Changes emoji information. pub async fn edit_emoji(&self, guild_id: u64, emoji_id: u64, map: &Value) -> Result { let body = serde_json::to_vec(map)?; @@ -2004,6 +2038,18 @@ impl Http { .await } + /// Gets a stage instance. + pub async fn get_stage_instance(&self, channel_id: u64) -> Result { + self.fire(Request { + body: None, + headers: None, + route: RouteInfo::GetStageInstance { + channel_id, + }, + }) + .await + } + /// Gets information about the current application. /// /// **Note**: Only applications may use this endpoint. diff --git a/src/http/routing.rs b/src/http/routing.rs index 5e4fdb1f36f..843e4ac46bd 100644 --- a/src/http/routing.rs +++ b/src/http/routing.rs @@ -372,6 +372,18 @@ pub enum Route { #[cfg(feature = "unstable_discord_api")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))] ApplicationsIdGuildsIdCommandsId(u64), + /// Route for the `/stage-instances` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: crate::model::id::ChannelId + StageInstances, + /// Route for the `/stage-instances/:channel_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: crate::model::id::ChannelId + StageInstancesChannelId(u64), /// Route where no ratelimit headers are in place (i.e. user account-only /// routes). /// @@ -849,6 +861,14 @@ impl Route { pub fn application_guild_commands_permissions(application_id: u64, guild_id: u64) -> String { format!(api!("/applications/{}/guilds/{}/commands/permissions"), application_id, guild_id) } + + pub fn stage_instances() -> String { + api!("/stage-instances").to_string() + } + + pub fn stage_instance(channel_id: u64) -> String { + format!(api!("/stage-instances/{}"), channel_id) + } } #[derive(Clone, Debug)] @@ -871,6 +891,7 @@ pub enum RouteInfo<'a> { CreateChannel { guild_id: u64, }, + CreateStageInstance, CreateEmoji { guild_id: u64, }, @@ -938,6 +959,9 @@ pub enum RouteInfo<'a> { DeleteChannel { channel_id: u64, }, + DeleteStageInstance { + channel_id: u64, + }, DeleteEmoji { guild_id: u64, emoji_id: u64, @@ -1023,6 +1047,9 @@ pub enum RouteInfo<'a> { EditChannel { channel_id: u64, }, + EditStageInstance { + channel_id: u64, + }, EditEmoji { guild_id: u64, emoji_id: u64, @@ -1158,6 +1185,9 @@ pub enum RouteInfo<'a> { GetChannels { guild_id: u64, }, + GetStageInstance { + channel_id: u64, + }, GetCurrentApplicationInfo, GetCurrentUser, GetEmojis { @@ -1381,6 +1411,9 @@ impl<'a> RouteInfo<'a> { Route::GuildsIdChannels(guild_id), Cow::from(Route::guild_channels(guild_id)), ), + RouteInfo::CreateStageInstance => { + (LightMethod::Post, Route::StageInstances, Cow::from(Route::stage_instances())) + }, RouteInfo::CreateEmoji { guild_id, } => ( @@ -1516,6 +1549,13 @@ impl<'a> RouteInfo<'a> { Route::ChannelsId(channel_id), Cow::from(Route::channel(channel_id)), ), + RouteInfo::DeleteStageInstance { + channel_id, + } => ( + LightMethod::Delete, + Route::StageInstancesChannelId(channel_id), + Cow::from(Route::stage_instance(channel_id)), + ), RouteInfo::DeleteEmoji { emoji_id, guild_id, @@ -1674,6 +1714,13 @@ impl<'a> RouteInfo<'a> { Route::ChannelsId(channel_id), Cow::from(Route::channel(channel_id)), ), + RouteInfo::EditStageInstance { + channel_id, + } => ( + LightMethod::Patch, + Route::StageInstancesChannelId(channel_id), + Cow::from(Route::stage_instance(channel_id)), + ), RouteInfo::EditEmoji { emoji_id, guild_id, @@ -1913,6 +1960,13 @@ impl<'a> RouteInfo<'a> { Route::ChannelsId(channel_id), Cow::from(Route::channel(channel_id)), ), + RouteInfo::GetStageInstance { + channel_id, + } => ( + LightMethod::Get, + Route::StageInstancesChannelId(channel_id), + Cow::from(Route::stage_instance(channel_id)), + ), RouteInfo::GetChannelInvites { channel_id, } => ( diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 7a8192b53b9..655567c11bf 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -15,6 +15,7 @@ use tokio::{fs::File, io::AsyncReadExt}; #[cfg(feature = "model")] use crate::builder::{CreateInvite, CreateMessage, EditChannel, EditMessage, GetMessages}; +use crate::builder::{CreateStageInstance, EditStageInstance}; #[cfg(all(feature = "cache", feature = "model"))] use crate::cache::Cache; #[cfg(feature = "collector")] @@ -970,6 +971,70 @@ impl ChannelId { ) -> ReactionCollectorBuilder<'a> { ReactionCollectorBuilder::new(shard_messenger).channel_id(self.0) } + + /// Gets a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, + /// or if there is no stage instance currently. + pub async fn get_stage_instance(&self, http: impl AsRef) -> Result { + http.as_ref().get_stage_instance(self.0).await + } + + /// Creates a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, + /// or if there is already a stage instance currently. + pub async fn create_stage_instance( + &self, + http: impl AsRef, + f: F, + ) -> Result + where + F: FnOnce(&mut CreateStageInstance) -> &mut CreateStageInstance, + { + let mut instance = CreateStageInstance::default(); + f(&mut instance); + + let map = utils::hashmap_to_json_map(instance.0); + + http.as_ref().create_stage_instance(&Value::Object(map)).await + } + + /// Edits a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, + /// or if there is not stage instance currently. + pub async fn edit_stage_instance( + &self, + http: impl AsRef, + f: F, + ) -> Result + where + F: FnOnce(&mut EditStageInstance) -> &mut EditStageInstance, + { + let mut instance = EditStageInstance::default(); + f(&mut instance); + + let map = utils::hashmap_to_json_map(instance.0); + + http.as_ref().edit_stage_instance(self.0, &Value::Object(map)).await + } + + /// Deletes a stage instance. + /// + /// # Errors + /// + /// Returns [`Error::Http`] if the channel is not a stage channel, + /// or if there is no stage instance currently. + pub async fn delete_stage_instance(&self, http: impl AsRef) -> Result<()> { + http.as_ref().delete_stage_instance(self.0).await + } } impl From for ChannelId { diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 2a7d2a64657..6fe00fce238 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -11,6 +11,7 @@ use futures::stream::StreamExt; use crate::builder::EditChannel; #[cfg(feature = "model")] use crate::builder::{CreateInvite, CreateMessage, EditMessage, EditVoiceState, GetMessages}; +use crate::builder::{CreateStageInstance, EditStageInstance}; #[cfg(feature = "cache")] use crate::cache::Cache; #[cfg(feature = "collector")] @@ -1169,6 +1170,76 @@ impl GuildChannel { self.id.create_webhook_with_avatar(&http, name, avatar).await } + + /// Gets a stage instance. + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// Returns [`Error::Http`] if there is no stage instance currently. + pub async fn get_stage_instance(&self, http: impl AsRef) -> Result { + if self.kind.num() != 13 { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + + self.id.get_stage_instance(http).await + } + + /// Creates a stage instance. + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// Returns [`Error::Http`] if there is already a stage instance currently. + pub async fn create_stage_instance( + &self, + http: impl AsRef, + f: F, + ) -> Result + where + F: FnOnce(&mut CreateStageInstance) -> &mut CreateStageInstance, + { + if self.kind.num() != 13 { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + + self.id.create_stage_instance(http, f).await + } + + /// Edits a stage instance. + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// Returns [`Error::Http`] if there is no stage instance currently. + pub async fn edit_stage_instance( + &self, + http: impl AsRef, + f: F, + ) -> Result + where + F: FnOnce(&mut EditStageInstance) -> &mut EditStageInstance, + { + if self.kind.num() != 13 { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + + self.id.edit_stage_instance(http, f).await + } + + /// Deletes a stage instance. + /// + /// # Errors + /// + /// Returns [`ModelError::InvalidChannelType`] if the channel is not a stage channel. + /// Returns [`Error::Http`] if there is no stage instance currently. + pub async fn delete_stage_instance(&self, http: impl AsRef) -> Result<()> { + if self.kind.num() != 13 { + return Err(Error::Model(ModelError::InvalidChannelType)); + } + + self.id.delete_stage_instance(http).await + } } impl Display for GuildChannel { diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 55c5b093b9f..8489cfb1d13 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -429,6 +429,19 @@ enum_number!(VideoQualityMode { Full }); +#[derive(Clone, Debug, Deserialize, Serialize)] +#[non_exhaustive] +pub struct StageInstance { + /// The Id of the stage instance. + pub id: StageInstanceId, + /// The guild Id of the associated stage channel. + pub guild_id: GuildId, + /// The Id of the associated stage channel. + pub channel_id: ChannelId, + /// The topic of the stage instance. + pub topic: String, +} + #[cfg(test)] mod test { #[cfg(all(feature = "model", feature = "utils"))] diff --git a/src/model/event.rs b/src/model/event.rs index 0dfc2d2c479..b1fcaf41620 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -1512,6 +1512,54 @@ impl<'de> Deserialize<'de> for ApplicationCommandDeleteEvent { } } +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +pub struct StageInstanceCreateEvent { + pub stage_instance: StageInstance, +} + +impl<'de> Deserialize<'de> for StageInstanceCreateEvent { + fn deserialize>(deserializer: D) -> StdResult { + let stage_instance = StageInstance::deserialize(deserializer)?; + + Ok(Self { + stage_instance, + }) + } +} + +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +pub struct StageInstanceUpdateEvent { + pub stage_instance: StageInstance, +} + +impl<'de> Deserialize<'de> for StageInstanceUpdateEvent { + fn deserialize>(deserializer: D) -> StdResult { + let stage_instance = StageInstance::deserialize(deserializer)?; + + Ok(Self { + stage_instance, + }) + } +} + +#[derive(Clone, Debug, Serialize)] +#[non_exhaustive] +pub struct StageInstanceDeleteEvent { + pub stage_instance: StageInstance, +} + +impl<'de> Deserialize<'de> for StageInstanceDeleteEvent { + fn deserialize>(deserializer: D) -> StdResult { + let stage_instance = StageInstance::deserialize(deserializer)?; + + Ok(Self { + stage_instance, + }) + } +} + #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, Serialize)] #[non_exhaustive] @@ -1726,6 +1774,12 @@ pub enum Event { #[cfg(feature = "unstable_discord_api")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))] ApplicationCommandDelete(ApplicationCommandDeleteEvent), + /// A stage instance was created. + StageInstanceCreate(StageInstanceCreateEvent), + /// A stage instance was updated. + StageInstanceUpdate(StageInstanceUpdateEvent), + /// A stage instance was deleted. + StageInstanceDelete(StageInstanceDeleteEvent), /// An event type not covered by the above Unknown(UnknownEvent), } @@ -1785,6 +1839,9 @@ impl Event { Self::ApplicationCommandUpdate(_) => EventType::ApplicationCommandUpdate, #[cfg(feature = "unstable_discord_api")] Self::ApplicationCommandDelete(_) => EventType::ApplicationCommandDelete, + Self::StageInstanceCreate(_) => EventType::StageInstanceCreate, + Self::StageInstanceUpdate(_) => EventType::StageInstanceUpdate, + Self::StageInstanceDelete(_) => EventType::StageInstanceDelete, Self::Unknown(unknown) => EventType::Other(unknown.kind.clone()), } } @@ -1888,6 +1945,9 @@ pub fn deserialize_event_with_type(kind: EventType, v: Value) -> Result { EventType::ApplicationCommandDelete => { Event::ApplicationCommandDelete(serde_json::from_value(v)?) }, + EventType::StageInstanceCreate => Event::StageInstanceCreate(serde_json::from_value(v)?), + EventType::StageInstanceUpdate => Event::StageInstanceUpdate(serde_json::from_value(v)?), + EventType::StageInstanceDelete => Event::StageInstanceDelete(serde_json::from_value(v)?), EventType::Other(kind) => Event::Unknown(UnknownEvent { kind, value: v, @@ -2089,6 +2149,15 @@ pub enum EventType { #[cfg(feature = "unstable_discord_api")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))] ApplicationCommandDelete, + /// Indicator that a stage instance was created. + /// This maps to [`StageInstanceCreateEvent`]. + StageInstanceCreate, + /// Indicator that a stage instance was updated. + /// This maps to [`StageInstanceUpdateEvent`]. + StageInstanceUpdate, + /// Indicator that a stage instance was deleted. + /// This maps to [`StageInstanceDeleteEvent`]. + StageInstanceDelete, /// An unknown event was received over the gateway. /// /// This should be logged so that support for it can be added in the @@ -2160,6 +2229,9 @@ impl EventType { #[cfg(feature = "unstable_discord_api")] #[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))] const APPLICATION_COMMAND_DELETE: &'static str = "APPLICATION_COMMAND_DELETE"; + const STAGE_INSTANCE_CREATE: &'static str = "STAGE_INSTANCE_CREATE"; + const STAGE_INSTANCE_UPDATE: &'static str = "STAGE_INSTANCE_UPDATE"; + const STAGE_INSTANCE_DELETE: &'static str = "STAGE_INSTANCE_DELETE"; /// Return the event name of this event. Some events are synthetic, and we lack /// the information to recover the original event name for these events, in which @@ -2216,6 +2288,9 @@ impl EventType { Self::ApplicationCommandUpdate => Some(Self::APPLICATION_COMMAND_UPDATE), #[cfg(feature = "unstable_discord_api")] Self::ApplicationCommandDelete => Some(Self::APPLICATION_COMMAND_DELETE), + Self::StageInstanceCreate => Some(Self::STAGE_INSTANCE_CREATE), + Self::StageInstanceUpdate => Some(Self::STAGE_INSTANCE_UPDATE), + Self::StageInstanceDelete => Some(Self::STAGE_INSTANCE_DELETE), // GuildUnavailable is a synthetic event type, corresponding to either // `GUILD_CREATE` or `GUILD_DELETE`, but we don't have enough information // to recover the name here, so we return `None` instead. @@ -2294,6 +2369,9 @@ impl<'de> Deserialize<'de> for EventType { EventType::APPLICATION_COMMAND_UPDATE => EventType::ApplicationCommandUpdate, #[cfg(feature = "unstable_discord_api")] EventType::APPLICATION_COMMAND_DELETE => EventType::ApplicationCommandDelete, + EventType::STAGE_INSTANCE_CREATE => EventType::StageInstanceCreate, + EventType::STAGE_INSTANCE_UPDATE => EventType::StageInstanceUpdate, + EventType::STAGE_INSTANCE_DELETE => EventType::StageInstanceDelete, other => EventType::Other(other.to_owned()), }) } diff --git a/src/model/id.rs b/src/model/id.rs index 414ae67dd6f..b8bcc7dc049 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -154,6 +154,10 @@ pub struct CommandId(pub u64); #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)] pub struct CommandPermissionId(pub u64); +/// An identifier for a stage channel instance. +#[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)] +pub struct StageInstanceId(pub u64); + id_u64! { AttachmentId; ApplicationId; @@ -171,4 +175,5 @@ id_u64! { InteractionId; CommandId; CommandPermissionId; + StageInstanceId; }