diff --git a/src/builder/edit_onboarding/mod.rs b/src/builder/edit_onboarding/mod.rs index d4e955ed354..7d768a2ad9a 100644 --- a/src/builder/edit_onboarding/mod.rs +++ b/src/builder/edit_onboarding/mod.rs @@ -1,5 +1,20 @@ +#[cfg(feature = "http")] +use super::Builder; + use crate::model::guild::OnboardingMode; use crate::model::id::ChannelId; + +#[cfg(feature = "http")] +use crate::model::id::GuildId; +#[cfg(feature = "http")] +use crate::model::Permissions; +#[cfg(feature = "http")] +use crate::http::CacheHttp; +#[cfg(feature = "http")] +use crate::model::guild::Onboarding; +#[cfg(feature = "http")] +use crate::internal::prelude::*; + mod prompt_option_structure; mod prompt_structure; @@ -32,17 +47,20 @@ use sealed::*; #[derive(serde::Serialize, Clone, Debug)] #[must_use = "Builders do nothing unless built"] -pub struct EditOnboarding { +pub struct EditOnboarding<'a, Stage: Sealed> { prompts: Vec>, default_channel_ids: Vec, enabled: bool, mode: OnboardingMode, + #[serde(skip)] + audit_log_reason: Option<&'a str>, + #[serde(skip)] _stage: Stage, } -impl Default for EditOnboarding { +impl<'a> Default for EditOnboarding<'a, NeedsPrompts> { /// See the documentation of [`Self::new`]. fn default() -> Self { // Producing dummy values is okay as we must transition through all `Stage`s before firing, @@ -52,13 +70,14 @@ impl Default for EditOnboarding { default_channel_ids: Vec::new(), enabled: true, mode: OnboardingMode::default(), + audit_log_reason: None, _stage: NeedsPrompts, } } } -impl EditOnboarding { +impl<'a> EditOnboarding<'a, NeedsPrompts> { pub fn new() -> Self { Self::default() } @@ -66,56 +85,86 @@ impl EditOnboarding { pub fn prompts( self, prompts: Vec>, - ) -> EditOnboarding { + ) -> EditOnboarding<'a, NeedsChannels> { EditOnboarding { prompts, default_channel_ids: self.default_channel_ids, enabled: self.enabled, mode: self.mode, + audit_log_reason: self.audit_log_reason, _stage: NeedsChannels, } } } -impl EditOnboarding { +impl<'a> EditOnboarding<'a, NeedsChannels> { pub fn default_channels( self, default_channel_ids: Vec, - ) -> EditOnboarding { + ) -> EditOnboarding<'a, NeedsEnabled> { EditOnboarding { prompts: self.prompts, default_channel_ids, enabled: self.enabled, mode: self.mode, + audit_log_reason: self.audit_log_reason, _stage: NeedsEnabled, } } } -impl EditOnboarding { - pub fn enabled(self, enabled: bool) -> EditOnboarding { +impl<'a> EditOnboarding<'a, NeedsEnabled> { + pub fn enabled(self, enabled: bool) -> EditOnboarding<'a, NeedsMode> { EditOnboarding { prompts: self.prompts, default_channel_ids: self.default_channel_ids, enabled, mode: self.mode, + audit_log_reason: self.audit_log_reason, _stage: NeedsMode, } } } -impl EditOnboarding { - pub fn mode(self, mode: OnboardingMode) -> EditOnboarding { +impl<'a> EditOnboarding<'a, NeedsMode> { + pub fn mode(self, mode: OnboardingMode) -> EditOnboarding<'a, Ready> { EditOnboarding { prompts: self.prompts, default_channel_ids: self.default_channel_ids, enabled: self.enabled, mode, + audit_log_reason: self.audit_log_reason, _stage: Ready, } } } + +impl<'a, Stage: Sealed> EditOnboarding<'a, Stage> { + pub fn emoji(mut self, audit_log_reason: &'a str) -> Self { + self.audit_log_reason = Some(audit_log_reason); + self + } +} + + +#[cfg(feature = "http")] +#[async_trait::async_trait] +impl<'a> Builder for EditOnboarding<'a, Ready> { + type Context<'ctx> = GuildId; + type Built = Onboarding; + + async fn execute( + mut self, + cache_http: impl CacheHttp, + ctx: Self::Context<'_>, + ) -> Result { + #[cfg(feature = "cache")] + crate::utils::user_has_guild_perms(&cache_http, ctx, Permissions::MANAGE_GUILD | Permissions::MANAGE_ROLES)?; + + cache_http.http().set_guild_onboarding(ctx, &self, self.audit_log_reason).await + } +} diff --git a/src/builder/edit_onboarding/prompt_option_structure.rs b/src/builder/edit_onboarding/prompt_option_structure.rs index 4da6fc03065..7bd0cc1c1a2 100644 --- a/src/builder/edit_onboarding/prompt_option_structure.rs +++ b/src/builder/edit_onboarding/prompt_option_structure.rs @@ -22,19 +22,18 @@ mod sealed { use sealed::*; -#[derive(serde::Serialize, Clone, Debug)] +#[derive(Clone, Debug)] #[must_use = "Builders do nothing unless built"] pub struct CreatePromptOption { channel_ids: Vec, role_ids: Vec, - emoji: Option, title: String, description: Option, - - #[serde(skip)] + emoji: Option, _stage: Stage, } + impl Default for CreatePromptOption { /// See the documentation of [`Self::new`]. fn default() -> Self { @@ -109,3 +108,38 @@ impl CreatePromptOption { self } } + +use serde::ser::{Serialize, Serializer, SerializeStruct}; + +impl Serialize for CreatePromptOption { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("CreatePromptOption", 4)?; + + state.serialize_field("channel_ids", &self.channel_ids)?; + state.serialize_field("role_ids", &self.role_ids)?; + state.serialize_field("title", &self.title)?; + state.serialize_field("description", &self.description)?; + + if let Some(ref emoji) = self.emoji { + match emoji { + ReactionType::Custom { + animated, + id, + name, + } => { + state.serialize_field("emoji_animated", animated)?; + state.serialize_field("emoji_id", id)?; + state.serialize_field("emoji_name", name)?; + } + ReactionType::Unicode(name) => { + state.serialize_field("emoji_name", name)?; + } + } + } + + state.end() + } +} diff --git a/src/builder/edit_onboarding/prompt_structure.rs b/src/builder/edit_onboarding/prompt_structure.rs index 832f26d8575..2c1a14afdde 100644 --- a/src/builder/edit_onboarding/prompt_structure.rs +++ b/src/builder/edit_onboarding/prompt_structure.rs @@ -35,6 +35,7 @@ use crate::all::OnboardingPromptType; #[derive(serde::Serialize, Clone, Debug)] #[must_use = "Builders do nothing unless built"] pub struct CreateOnboardingPrompt { + id: u64, prompt_type: OnboardingPromptType, options: Vec>, title: String, @@ -52,6 +53,7 @@ impl Default for CreateOnboardingPrompt { // Producing dummy values is okay as we must transition through all `Stage`s before firing, // which fills in the values with real values. Self { + id: 0, prompt_type: OnboardingPromptType::Dropdown, options: Vec::new(), title: String::new(), @@ -74,6 +76,7 @@ impl CreateOnboardingPrompt { prompt_type: OnboardingPromptType, ) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type, options: self.options, title: self.title, @@ -92,6 +95,7 @@ impl CreateOnboardingPrompt { options: Vec>, ) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type: self.prompt_type, options, title: self.title, @@ -107,6 +111,7 @@ impl CreateOnboardingPrompt { impl CreateOnboardingPrompt { pub fn title(self, title: impl Into) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type: self.prompt_type, options: self.options, title: title.into(), @@ -122,6 +127,7 @@ impl CreateOnboardingPrompt { impl CreateOnboardingPrompt { pub fn single_select(self, single_select: bool) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type: self.prompt_type, options: self.options, title: self.title, @@ -137,6 +143,7 @@ impl CreateOnboardingPrompt { impl CreateOnboardingPrompt { pub fn required(self, required: bool) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type: self.prompt_type, options: self.options, title: self.title, @@ -152,6 +159,7 @@ impl CreateOnboardingPrompt { impl CreateOnboardingPrompt { pub fn in_onboarding(self, in_onboarding: bool) -> CreateOnboardingPrompt { CreateOnboardingPrompt { + id: self.id, prompt_type: self.prompt_type, options: self.options, title: self.title, diff --git a/src/http/client.rs b/src/http/client.rs index 56981949505..350d36d5a99 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -4686,6 +4686,23 @@ impl Http { .await } + /// Sets the onboarding configuration for the guild. + pub async fn set_guild_onboarding(&self, guild_id: GuildId, map: &impl serde::Serialize, audit_log_reason: Option<&str>) -> Result { + let body = to_vec(map)?; + + self.fire(Request { + body: Some(body), + multipart: None, + headers: audit_log_reason.map(reason_into_header), + method: LightMethod::Put, + route: Route::GuildOnboarding { + guild_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/onboarding.rs b/src/model/guild/onboarding.rs index ef7f693a105..de01d0aa7fa 100644 --- a/src/model/guild/onboarding.rs +++ b/src/model/guild/onboarding.rs @@ -1,5 +1,6 @@ use crate::all::ReactionType; -use crate::model::id::{ChannelId, GenericId, GuildId, RoleId}; +use serde::{Deserialize, Deserializer}; +use crate::model::id::{ChannelId, GenericId, GuildId, RoleId, EmojiId}; #[derive(Debug, Clone, Deserialize, Serialize)] #[non_exhaustive] @@ -41,6 +42,7 @@ pub struct OnboardingPromptOption { pub id: GenericId, pub channel_ids: Vec, pub role_ids: Vec, + #[serde(default, deserialize_with = "onboarding_reaction")] pub emoji: Option, pub title: String, pub description: Option, @@ -57,3 +59,45 @@ enum_number! { _ => Unknown(u8), } } + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum TestType { + /// A reaction with a [`Guild`]s custom [`Emoji`], which is unique to the guild. + Custom { + /// Whether the emoji is animated. + animated: bool, + /// The Id of the custom [`Emoji`]. + id: EmojiId, + /// The name of the custom emoji. This is primarily used for decoration and distinguishing + /// the emoji client-side. + name: Option, + }, + /// A reaction with a twemoji. + Unicode(String), +} + +/// This exists to handle the weird case where discord decides to send every field as null +/// instead of sending the emoji as null itself. +fn onboarding_reaction<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + struct PartialEmoji { + #[serde(default)] + animated: bool, + id: Option, + name: Option, + } + let emoji = PartialEmoji::deserialize(deserializer)?; + Ok(match (emoji.id, emoji.name) { + (Some(id), name) => Some(ReactionType::Custom { + animated: emoji.animated, + id, + name, + }), + (None, Some(name)) => Some(ReactionType::Unicode(name)), + (None, None) => return Ok(None), + }) +} \ No newline at end of file