From 36f8377682c849a030804a782bc50de7c5272fd8 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Tue, 4 Jul 2023 11:17:51 -0400 Subject: [PATCH] feat(model, http): Add support for guild onboarding (#2130) Implements guild onboarding ref: discord/discord-api-docs#5915 This adds the following structures: Onboarding OnboardingPrompt OnboardingPromptOption OnboardingPromptType As well as supporting the http GET for retrieving guild onboarding information. Closes #2134 --- twilight-http-ratelimiting/src/request.rs | 3 + twilight-http/src/client/mod.rs | 7 +- .../src/request/guild/get_guild_onboarding.rs | 69 ++++++++ twilight-http/src/request/guild/mod.rs | 15 +- twilight-http/src/request/try_into_request.rs | 11 +- twilight-http/src/routing.rs | 19 +++ twilight-model/src/guild/mod.rs | 1 + twilight-model/src/guild/onboarding/mod.rs | 67 ++++++++ twilight-model/src/guild/onboarding/option.rs | 156 ++++++++++++++++++ twilight-model/src/guild/onboarding/prompt.rs | 71 ++++++++ .../src/guild/onboarding/prompt_type.rs | 66 ++++++++ twilight-model/src/id/marker.rs | 18 ++ 12 files changed, 490 insertions(+), 13 deletions(-) create mode 100644 twilight-http/src/request/guild/get_guild_onboarding.rs create mode 100644 twilight-model/src/guild/onboarding/mod.rs create mode 100644 twilight-model/src/guild/onboarding/option.rs create mode 100644 twilight-model/src/guild/onboarding/prompt.rs create mode 100644 twilight-model/src/guild/onboarding/prompt_type.rs diff --git a/twilight-http-ratelimiting/src/request.rs b/twilight-http-ratelimiting/src/request.rs index 3cde31aaa50..f5351a9d18c 100644 --- a/twilight-http-ratelimiting/src/request.rs +++ b/twilight-http-ratelimiting/src/request.rs @@ -208,6 +208,8 @@ pub enum Path { GuildsIdMembersSearch(u64), /// Operating on one of the user's guilds' MFA level. GuildsIdMfa(u64), + /// Operating on one of the user's guilds' onboarding. + GuildsIdOnboarding(u64), /// Operating on one of the user's guilds' by previewing it. GuildsIdPreview(u64), /// Operating on one of the user's guilds' by pruning members. @@ -400,6 +402,7 @@ impl FromStr for Path { ["guilds", id, "members", _] => GuildsIdMembersId(parse_id(id)?), ["guilds", id, "members", _, "roles", _] => GuildsIdMembersIdRolesId(parse_id(id)?), ["guilds", id, "members", "@me", "nick"] => GuildsIdMembersMeNick(parse_id(id)?), + ["guilds", id, "onboarding"] => GuildsIdOnboarding(parse_id(id)?), ["guilds", id, "preview"] => GuildsIdPreview(parse_id(id)?), ["guilds", id, "prune"] => GuildsIdPrune(parse_id(id)?), ["guilds", id, "regions"] => GuildsIdRegions(parse_id(id)?), diff --git a/twilight-http/src/client/mod.rs b/twilight-http/src/client/mod.rs index f07d07462cd..89cc10a4f97 100644 --- a/twilight-http/src/client/mod.rs +++ b/twilight-http/src/client/mod.rs @@ -4,7 +4,7 @@ mod interaction; pub use self::{builder::ClientBuilder, interaction::InteractionClient}; -use crate::request::GetCurrentAuthorizationInformation; +use crate::request::{guild::GetGuildOnboarding, GetCurrentAuthorizationInformation}; #[allow(deprecated)] use crate::{ client::connector::Connector, @@ -1231,6 +1231,11 @@ impl Client { RemoveRoleFromMember::new(self, guild_id, user_id, role_id) } + /// Retrieves the onboarding data for a guild. + pub const fn guild_onboarding(&self, guild_id: Id) -> GetGuildOnboarding<'_> { + GetGuildOnboarding::new(self, guild_id) + } + /// For public guilds, get the guild preview. /// /// This works even if the user is not in the guild. diff --git a/twilight-http/src/request/guild/get_guild_onboarding.rs b/twilight-http/src/request/guild/get_guild_onboarding.rs new file mode 100644 index 00000000000..fed0be56eac --- /dev/null +++ b/twilight-http/src/request/guild/get_guild_onboarding.rs @@ -0,0 +1,69 @@ +use std::future::IntoFuture; + +use twilight_model::{ + guild::onboarding::Onboarding, + id::{marker::GuildMarker, Id}, +}; + +use crate::{ + request::{Request, TryIntoRequest}, + response::ResponseFuture, + routing::Route, + Client, Error, Response, +}; + +/// Get the onboarding information for a guild. +/// +/// # Examples +/// +/// ```no_run +/// use twilight_http::Client; +/// use twilight_model::id::Id; +/// +/// # #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let client = Client::new("token".to_owned()); +/// +/// let guild_id = Id::new(101); +/// let onboarding = client.guild_onboarding(guild_id).await?.model().await?; +/// +/// for prompt in onboarding.prompts { +/// println!("Prompt: {}", prompt.title); +/// } +/// # Ok(()) } +/// ``` +pub struct GetGuildOnboarding<'a> { + guild_id: Id, + http: &'a Client, +} + +impl<'a> GetGuildOnboarding<'a> { + pub(crate) const fn new(http: &'a Client, guild_id: Id) -> Self { + Self { guild_id, http } + } +} + +impl IntoFuture for GetGuildOnboarding<'_> { + type Output = Result, Error>; + + type IntoFuture = ResponseFuture; + + fn into_future(self) -> Self::IntoFuture { + let http = self.http; + + match self.try_into_request() { + Ok(request) => http.request(request), + Err(source) => ResponseFuture::error(source), + } + } +} + +impl TryIntoRequest for GetGuildOnboarding<'_> { + fn try_into_request(self) -> Result { + let request = Request::from_route(&Route::GetGuildOnboarding { + guild_id: self.guild_id.get(), + }); + + Ok(request) + } +} diff --git a/twilight-http/src/request/guild/mod.rs b/twilight-http/src/request/guild/mod.rs index 7f6a24c031a..8df6f4acdb6 100644 --- a/twilight-http/src/request/guild/mod.rs +++ b/twilight-http/src/request/guild/mod.rs @@ -17,6 +17,7 @@ mod get_audit_log; mod get_guild; mod get_guild_channels; mod get_guild_invites; +mod get_guild_onboarding; mod get_guild_preview; mod get_guild_prune_count; mod get_guild_vanity_url; @@ -36,12 +37,12 @@ pub use self::{ create_guild_prune::CreateGuildPrune, delete_guild::DeleteGuild, get_active_threads::GetActiveThreads, get_audit_log::GetAuditLog, get_guild::GetGuild, get_guild_channels::GetGuildChannels, get_guild_invites::GetGuildInvites, - get_guild_preview::GetGuildPreview, get_guild_prune_count::GetGuildPruneCount, - get_guild_vanity_url::GetGuildVanityUrl, get_guild_voice_regions::GetGuildVoiceRegions, - get_guild_webhooks::GetGuildWebhooks, get_guild_welcome_screen::GetGuildWelcomeScreen, - get_guild_widget::GetGuildWidget, get_guild_widget_settings::GetGuildWidgetSettings, - update_current_member::UpdateCurrentMember, update_guild::UpdateGuild, - update_guild_channel_positions::UpdateGuildChannelPositions, update_guild_mfa::UpdateGuildMfa, - update_guild_welcome_screen::UpdateGuildWelcomeScreen, + get_guild_onboarding::GetGuildOnboarding, get_guild_preview::GetGuildPreview, + get_guild_prune_count::GetGuildPruneCount, get_guild_vanity_url::GetGuildVanityUrl, + get_guild_voice_regions::GetGuildVoiceRegions, get_guild_webhooks::GetGuildWebhooks, + get_guild_welcome_screen::GetGuildWelcomeScreen, get_guild_widget::GetGuildWidget, + get_guild_widget_settings::GetGuildWidgetSettings, update_current_member::UpdateCurrentMember, + update_guild::UpdateGuild, update_guild_channel_positions::UpdateGuildChannelPositions, + update_guild_mfa::UpdateGuildMfa, update_guild_welcome_screen::UpdateGuildWelcomeScreen, update_guild_widget_settings::UpdateGuildWidgetSettings, }; diff --git a/twilight-http/src/request/try_into_request.rs b/twilight-http/src/request/try_into_request.rs index 72a8b006528..c2df5f0faec 100644 --- a/twilight-http/src/request/try_into_request.rs +++ b/twilight-http/src/request/try_into_request.rs @@ -65,11 +65,11 @@ mod private { }, user::{UpdateCurrentUserVoiceState, UpdateUserVoiceState}, CreateGuild, CreateGuildChannel, CreateGuildPrune, DeleteGuild, GetActiveThreads, - GetAuditLog, GetGuild, GetGuildChannels, GetGuildInvites, GetGuildPreview, - GetGuildPruneCount, GetGuildVanityUrl, GetGuildVoiceRegions, GetGuildWebhooks, - GetGuildWelcomeScreen, GetGuildWidget, GetGuildWidgetSettings, UpdateCurrentMember, - UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, UpdateGuildWelcomeScreen, - UpdateGuildWidgetSettings, + GetAuditLog, GetGuild, GetGuildChannels, GetGuildInvites, GetGuildOnboarding, + GetGuildPreview, GetGuildPruneCount, GetGuildVanityUrl, GetGuildVoiceRegions, + GetGuildWebhooks, GetGuildWelcomeScreen, GetGuildWidget, GetGuildWidgetSettings, + UpdateCurrentMember, UpdateGuild, UpdateGuildChannelPositions, UpdateGuildMfa, + UpdateGuildWelcomeScreen, UpdateGuildWidgetSettings, }, scheduled_event::{ CreateGuildExternalScheduledEvent, CreateGuildStageInstanceScheduledEvent, @@ -191,6 +191,7 @@ mod private { impl Sealed for GetGuildIntegrations<'_> {} impl Sealed for GetGuildInvites<'_> {} impl Sealed for GetGuildMembers<'_> {} + impl Sealed for GetGuildOnboarding<'_> {} impl Sealed for GetGuildPreview<'_> {} impl Sealed for GetGuildPruneCount<'_> {} impl Sealed for GetGuildRoles<'_> {} diff --git a/twilight-http/src/routing.rs b/twilight-http/src/routing.rs index 014cae7844f..4bcbc1dae05 100644 --- a/twilight-http/src/routing.rs +++ b/twilight-http/src/routing.rs @@ -542,6 +542,11 @@ pub enum Route<'a> { /// The maximum number of members to get. limit: Option, }, + /// Route information to get a guild's onboarding information. + GetGuildOnboarding { + /// The ID of the guild to get onboarding information for. + guild_id: u64, + }, /// Route information to get a guild's preview. GetGuildPreview { /// The ID of the guild. @@ -1176,6 +1181,7 @@ impl<'a> Route<'a> { | Self::GetGuildIntegrations { .. } | Self::GetGuildInvites { .. } | Self::GetGuildMembers { .. } + | Self::GetGuildOnboarding { .. } | Self::GetGuildPreview { .. } | Self::GetGuildPruneCount { .. } | Self::GetGuildRoles { .. } @@ -1547,6 +1553,7 @@ impl<'a> Route<'a> { Self::GetGuildMembers { guild_id, .. } | Self::UpdateCurrentMember { guild_id, .. } => { Path::GuildsIdMembers(guild_id) } + Self::GetGuildOnboarding { guild_id } => Path::GuildsIdOnboarding(guild_id), Self::CreateGuildScheduledEvent { guild_id, .. } | Self::GetGuildScheduledEvents { guild_id, .. } => { Path::GuildsIdScheduledEvents(guild_id) @@ -2406,6 +2413,12 @@ impl Display for Route<'_> { Ok(()) } + Route::GetGuildOnboarding { guild_id } => { + f.write_str("guilds/")?; + Display::fmt(guild_id, f)?; + + f.write_str("/onboarding") + } Route::GetGuildPreview { guild_id } => { f.write_str("guilds/")?; Display::fmt(guild_id, f)?; @@ -4672,4 +4685,10 @@ mod tests { format!("guilds/{GUILD_ID}/auto-moderation/rules/{AUTO_MODERATION_RULE_ID}") ); } + + #[test] + fn get_guild_onboarding() { + let route = Route::GetGuildOnboarding { guild_id: GUILD_ID }; + assert_eq!(route.to_string(), format!("guilds/{GUILD_ID}/onboarding")); + } } diff --git a/twilight-model/src/guild/mod.rs b/twilight-model/src/guild/mod.rs index 742d544eb55..4ef9047a872 100644 --- a/twilight-model/src/guild/mod.rs +++ b/twilight-model/src/guild/mod.rs @@ -8,6 +8,7 @@ pub mod audit_log; pub mod auto_moderation; pub mod invite; +pub mod onboarding; pub mod scheduled_event; pub mod template; pub mod widget; diff --git a/twilight-model/src/guild/onboarding/mod.rs b/twilight-model/src/guild/onboarding/mod.rs new file mode 100644 index 00000000000..a03f1b2f713 --- /dev/null +++ b/twilight-model/src/guild/onboarding/mod.rs @@ -0,0 +1,67 @@ +//! Types for guild onboarding. + +mod option; +mod prompt; +mod prompt_type; + +use crate::id::{ + marker::{ChannelMarker, GuildMarker}, + Id, +}; +use serde::{Deserialize, Serialize}; + +pub use self::{ + option::OnboardingPromptOption, prompt::OnboardingPrompt, prompt_type::OnboardingPromptType, +}; + +/// The onboarding data for a guild. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct Onboarding { + /// Channel IDs that new members get opted into automatically + pub default_channel_ids: Vec>, + /// Whether the guild has enabled onboarding. + pub enabled: bool, + /// ID of the guild this onboarding is a part of. + pub guild_id: Id, + /// Array of [`OnboardingPrompt`]s for the guild onboarding flow. + pub prompts: Vec, +} + +#[cfg(test)] +mod tests { + use super::Onboarding; + use crate::id::Id; + use serde_test::Token; + + #[test] + fn onboarding() { + let onboarding = Onboarding { + default_channel_ids: Vec::new(), + enabled: true, + guild_id: Id::new(123_456_789), + prompts: Vec::new(), + }; + + serde_test::assert_tokens( + &onboarding, + &[ + Token::Struct { + name: "Onboarding", + len: 4, + }, + Token::Str("default_channel_ids"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("enabled"), + Token::Bool(true), + Token::Str("guild_id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("123456789"), + Token::Str("prompts"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::StructEnd, + ], + ) + } +} diff --git a/twilight-model/src/guild/onboarding/option.rs b/twilight-model/src/guild/onboarding/option.rs new file mode 100644 index 00000000000..0b2f2bc8bdd --- /dev/null +++ b/twilight-model/src/guild/onboarding/option.rs @@ -0,0 +1,156 @@ +use crate::{ + guild::Emoji, + id::{ + marker::{ChannelMarker, EmojiMarker, OnboardingPromptOptionMarker, RoleMarker}, + Id, + }, +}; +use serde::{Deserialize, Serialize}; + +/// An emoji for a guild onboarding prompt. +/// This is used instead [`Emoji`] as both it's id and name can be `null` in prompt options. +/// +/// [`Emoji`]: crate::guild::Emoji +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct OnboardingPromptEmoji { + name: Option, + id: Option>, + #[serde(default)] + animated: bool, +} + +impl From for OnboardingPromptEmoji { + fn from(value: Emoji) -> Self { + Self { + animated: value.animated, + id: Some(value.id), + name: Some(value.name), + } + } +} + +/// A prompt option for a guild onboarding screen. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct OnboardingPromptOption { + /// Channels opted into when this option is selected. + pub channel_ids: Vec>, + /// Description of the option. + pub description: Option, + /// Emoji of the option. + pub emoji: OnboardingPromptEmoji, + /// ID of the option. + pub id: Id, + /// Roles assigned when this option is selected. + pub role_ids: Vec>, + /// Title of the option. + pub title: String, +} + +#[cfg(test)] +mod tests { + use super::{OnboardingPromptEmoji, OnboardingPromptOption}; + use crate::{ + guild::Emoji, + id::{ + marker::{ChannelMarker, EmojiMarker, OnboardingPromptOptionMarker, RoleMarker}, + Id, + }, + }; + use serde_test::Token; + + #[test] + fn prompt_option() { + let option = OnboardingPromptOption { + channel_ids: Vec::from([ + Id::::new(1), + Id::::new(2), + Id::::new(3), + ]), + description: Some(String::from("an option description")), + emoji: OnboardingPromptEmoji { + animated: false, + id: Some(Id::::new(7)), + name: Some(String::from("test")), + }, + id: Id::::new(123_456_789), + role_ids: Vec::from([ + Id::::new(4), + Id::::new(5), + Id::::new(6), + ]), + title: String::from("an option"), + }; + + serde_test::assert_tokens( + &option, + &[ + Token::Struct { + name: "OnboardingPromptOption", + len: 6, + }, + Token::Str("channel_ids"), + Token::Seq { len: Some(3) }, + Token::NewtypeStruct { name: "Id" }, + Token::Str("1"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("2"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("3"), + Token::SeqEnd, + Token::Str("description"), + Token::Some, + Token::Str("an option description"), + Token::Str("emoji"), + Token::Struct { + name: "OnboardingPromptEmoji", + len: 3, + }, + Token::Str("name"), + Token::Some, + Token::Str("test"), + Token::Str("id"), + Token::Some, + Token::NewtypeStruct { name: "Id" }, + Token::Str("7"), + Token::Str("animated"), + Token::Bool(false), + Token::StructEnd, + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("123456789"), + Token::Str("role_ids"), + Token::Seq { len: Some(3) }, + Token::NewtypeStruct { name: "Id" }, + Token::Str("4"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("5"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("6"), + Token::SeqEnd, + Token::Str("title"), + Token::Str("an option"), + Token::StructEnd, + ], + ) + } + + #[test] + fn conversion() { + let emoji = Emoji { + animated: false, + available: false, + id: Id::::new(7), + managed: false, + name: String::from("test"), + require_colons: false, + roles: Vec::new(), + user: None, + }; + + let emoji: OnboardingPromptEmoji = emoji.into(); + + assert!(!emoji.animated); + assert_eq!(emoji.id, Some(Id::::new(7))); + assert_eq!(emoji.name, Some(String::from("test"))); + } +} diff --git a/twilight-model/src/guild/onboarding/prompt.rs b/twilight-model/src/guild/onboarding/prompt.rs new file mode 100644 index 00000000000..d4a4eeed580 --- /dev/null +++ b/twilight-model/src/guild/onboarding/prompt.rs @@ -0,0 +1,71 @@ +use crate::id::{marker::OnboardingPromptMarker, Id}; +use serde::{Deserialize, Serialize}; + +use super::{OnboardingPromptOption, OnboardingPromptType}; + +/// A prompt in the onboarding flow. +#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +pub struct OnboardingPrompt { + /// ID of the prompt. + pub id: Id, + /// Whether this prompt is in the onboarding flow. + pub in_onboarding: bool, + /// [`OnboardingPromptType`] of the prompt. + #[serde(rename = "type")] + pub kind: OnboardingPromptType, + /// Array of [`OnboardingPromptOption`]s available to the prompt. + pub options: Vec, + /// Whether this prompt is required in the onboarding flow. + pub required: bool, + /// Whether this prompt allows selecting only one option. + pub single_select: bool, + /// Title of the prompt. + pub title: String, +} + +#[cfg(test)] +mod tests { + use super::OnboardingPrompt; + use crate::{guild::onboarding::OnboardingPromptType, id::Id}; + use serde_test::Token; + + #[test] + fn onboarding_prompt() { + let prompt = OnboardingPrompt { + id: Id::new(123_456_789), + in_onboarding: true, + kind: OnboardingPromptType::MultipleChoice, + options: Vec::new(), + required: true, + single_select: true, + title: String::from("a prompt"), + }; + + serde_test::assert_tokens( + &prompt, + &[ + Token::Struct { + name: "OnboardingPrompt", + len: 7, + }, + Token::Str("id"), + Token::NewtypeStruct { name: "Id" }, + Token::Str("123456789"), + Token::Str("in_onboarding"), + Token::Bool(true), + Token::Str("type"), + Token::U8(1), + Token::Str("options"), + Token::Seq { len: Some(0) }, + Token::SeqEnd, + Token::Str("required"), + Token::Bool(true), + Token::Str("single_select"), + Token::Bool(true), + Token::Str("title"), + Token::Str("a prompt"), + Token::StructEnd, + ], + ); + } +} diff --git a/twilight-model/src/guild/onboarding/prompt_type.rs b/twilight-model/src/guild/onboarding/prompt_type.rs new file mode 100644 index 00000000000..8f5c9e0b331 --- /dev/null +++ b/twilight-model/src/guild/onboarding/prompt_type.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; + +/// The type of an onboarding prompt. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(from = "u8", into = "u8")] +pub enum OnboardingPromptType { + /// A prompt that allows the user to select multiple options. + MultipleChoice, + /// A prompt that allows the user to use a dropdown to select an option. + Dropdown, + /// An unknown prompt type. + Unknown(u8), +} + +impl OnboardingPromptType { + pub const fn kind(self) -> &'static str { + match self { + Self::MultipleChoice => "MultipleChoice", + Self::Dropdown => "DropDown", + Self::Unknown(_) => "Unknown", + } + } +} + +impl From for OnboardingPromptType { + fn from(value: u8) -> Self { + match value { + 1 => Self::MultipleChoice, + 2 => Self::Dropdown, + unknown => Self::Unknown(unknown), + } + } +} + +impl From for u8 { + fn from(value: OnboardingPromptType) -> Self { + match value { + OnboardingPromptType::MultipleChoice => 1, + OnboardingPromptType::Dropdown => 2, + OnboardingPromptType::Unknown(unknown) => unknown, + } + } +} + +#[cfg(test)] +mod tests { + use super::OnboardingPromptType; + use serde_test::{assert_tokens, Token}; + + #[test] + fn onboarding_prompt_type() { + const MAP: &[(OnboardingPromptType, u8, &str)] = &[ + (OnboardingPromptType::MultipleChoice, 1, "MultipleChoice"), + (OnboardingPromptType::Dropdown, 2, "DropDown"), + (OnboardingPromptType::Unknown(3), 3, "Unknown"), + ]; + + for (prompt_type, number, name) in MAP { + assert_eq!(prompt_type.kind(), *name); + assert_eq!(u8::from(*prompt_type), *number); + assert_eq!(OnboardingPromptType::from(*number), *prompt_type); + assert_tokens(number, &[Token::U8(*number)]) + } + } +} diff --git a/twilight-model/src/id/marker.rs b/twilight-model/src/id/marker.rs index dcb40be8212..9b333908394 100644 --- a/twilight-model/src/id/marker.rs +++ b/twilight-model/src/id/marker.rs @@ -156,6 +156,24 @@ pub struct OauthSkuMarker; #[non_exhaustive] pub struct OauthTeamMarker; +/// Marker for onboarding prompt IDs. +/// +/// Types such as [`OnboardingPrompt`] use this ID marker. +/// +/// [`OnboardingPrompt`]: crate::guild::onboarding::OnboardingPrompt +#[derive(Debug)] +#[non_exhaustive] +pub struct OnboardingPromptMarker; + +/// Marker for onboarding prompt option IDs. +/// +/// Types such as [`OnboardingPromptOption`] use this ID marker. +/// +/// [`OnboardingPromptOption`]: crate::guild::onboarding::OnboardingPromptOption +#[derive(Debug)] +#[non_exhaustive] +pub struct OnboardingPromptOptionMarker; + /// Marker for role IDs. /// /// Types such as [`Member`] or [`Role`] use this ID marker.