From 6a931006596c9061190370834ff44a18813d02ff Mon Sep 17 00:00:00 2001 From: Meditation Mind <115960723+meditationmind@users.noreply.github.com> Date: Mon, 11 May 2026 05:57:32 +0900 Subject: [PATCH] Update invite models --- examples/testing/src/model_type_sizes.rs | 4 +- src/builder/create_invite.rs | 54 ++--- src/http/client.rs | 16 +- src/model/channel/channel_id.rs | 4 +- src/model/guild/guild_id.rs | 6 +- src/model/invite.rs | 259 ++++++++++------------- src/model/permissions.rs | 4 +- src/utils/mod.rs | 4 +- 8 files changed, 156 insertions(+), 195 deletions(-) diff --git a/examples/testing/src/model_type_sizes.rs b/examples/testing/src/model_type_sizes.rs index ef5c77c5a1f..d09e0a32147 100644 --- a/examples/testing/src/model_type_sizes.rs +++ b/examples/testing/src/model_type_sizes.rs @@ -111,7 +111,8 @@ pub fn print_ranking() { ("InviteCreateEvent", std::mem::size_of::()), ("InviteDeleteEvent", std::mem::size_of::()), ("InviteGuild", std::mem::size_of::()), - ("InviteStageInstance", std::mem::size_of::()), + ("InviteMetadata", std::mem::size_of::()), + ("InviteRole", std::mem::size_of::()), ("Maintenance", std::mem::size_of::()), ("Member", std::mem::size_of::()), ("Message", std::mem::size_of::()), @@ -154,7 +155,6 @@ pub fn print_ranking() { ("ReadyEvent", std::mem::size_of::()), ("ResolvedOption", std::mem::size_of::()), ("ResumedEvent", std::mem::size_of::()), - ("RichInvite", std::mem::size_of::()), ("Role", std::mem::size_of::()), ("RoleId", std::mem::size_of::()), ("RoleTags", std::mem::size_of::()), diff --git a/src/builder/create_invite.rs b/src/builder/create_invite.rs index 26b1e180e59..c9b7f6670d7 100644 --- a/src/builder/create_invite.rs +++ b/src/builder/create_invite.rs @@ -1,10 +1,12 @@ +use std::borrow::Cow; + #[cfg(feature = "http")] use crate::http::Http; #[cfg(feature = "http")] use crate::internal::prelude::*; use crate::model::prelude::*; -/// A builder to create a [`RichInvite`] for use via [`ChannelId::create_invite`]. +/// A builder to create an [`Invite`] for use via [`ChannelId::create_invite`]. /// /// This is a structured and cleaner way of creating an invite, as all parameters are optional. /// @@ -41,6 +43,8 @@ pub struct CreateInvite<'a> { target_user_id: Option, #[serde(skip_serializing_if = "Option::is_none")] target_application_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + role_ids: Option>, #[serde(skip)] audit_log_reason: Option<&'a str>, @@ -52,27 +56,23 @@ impl<'a> CreateInvite<'a> { Self::default() } - /// The duration that the invite will be valid for. - /// - /// Set to `0` for an invite which does not expire after an amount of time. + /// The duration of the invite in seconds before expiry. /// - /// Defaults to `86400`, or 24 hours. + /// Between `0` (never) and `604800` (7 days). Defaults to `86400` (24 hours). pub fn max_age(mut self, max_age: u32) -> Self { self.max_age = Some(max_age); self } - /// The number of uses that the invite will be valid for. + /// The maximum number of uses for the invite. /// - /// Set to `0` for an invite which does not expire after a number of uses. - /// - /// Defaults to `0`. + /// Between `0` (unlimited) and `100`. Defaults to `0`. pub fn max_uses(mut self, max_uses: u8) -> Self { self.max_uses = Some(max_uses); self } - /// Whether an invite grants a temporary membership. + /// Whether the invite only grants temporary membership. /// /// Defaults to `false`. pub fn temporary(mut self, temporary: bool) -> Self { @@ -94,34 +94,36 @@ impl<'a> CreateInvite<'a> { self } - /// The ID of the user whose stream to display for this invite, required if `target_type` is - /// `Stream` - /// The user must be streaming in the channel. + /// The [`UserId`] of the user whose stream to display for this invite. + /// + /// Required if `target_type` is `Stream`. The user must be streaming in the channel. pub fn target_user_id(mut self, target_user_id: UserId) -> Self { self.target_user_id = Some(target_user_id); self } - /// The ID of the embedded application to open for this invite, required if `target_type` is - /// `EmmbeddedApplication`. + /// The ID of the embedded application to open for this invite. /// - /// The application must have the `EMBEDDED` flag. + /// Required if `target_type` is `EmmbeddedApplication`. The application must have the + /// [`ApplicationFlags::EMBEDDED`] flag. /// - /// When sending an invite with this value, the first user to use the invite will have to click - /// on the URL, that will enable the buttons in the embed. + /// Some examples of popular embedded applications: /// - /// These are some of the known applications which have the flag: - /// - /// betrayal: `773336526917861400` - /// youtube: `755600276941176913` - /// fishing: `814288819477020702` - /// poker: `755827207812677713` - /// chess: `832012774040141894` + /// Watch Together: `880218394199220334` + /// Wordle: `1211781489931452447` + /// Poker Night: `755827207812677713` + /// Chess in the Park: `832012774040141894` pub fn target_application_id(mut self, target_application_id: ApplicationId) -> Self { self.target_application_id = Some(target_application_id); self } + /// The [`RoleId`]s for roles in the guild given to the users that accept this invite. + pub fn role_ids(mut self, role_ids: impl Into>) -> Self { + self.role_ids = Some(role_ids.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); @@ -139,7 +141,7 @@ impl<'a> CreateInvite<'a> { /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE #[cfg(feature = "http")] - pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { + pub async fn execute(self, http: &Http, channel_id: ChannelId) -> Result { http.create_invite(channel_id, &self, self.audit_log_reason).await } } diff --git a/src/http/client.rs b/src/http/client.rs index 5e12fcd4656..9c4f8dae1a3 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -722,13 +722,13 @@ impl Http { self.wind(request).await } - /// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. + /// Creates an [`Invite`] for the given [channel][`GuildChannel`]. pub async fn create_invite( &self, channel_id: ChannelId, map: &impl serde::Serialize, audit_log_reason: Option<&str>, - ) -> Result { + ) -> Result { let body = to_vec(map)?; self.fire(Request { @@ -2621,7 +2621,7 @@ impl Http { } /// Gets all invites for a channel. - pub async fn get_channel_invites(&self, channel_id: ChannelId) -> Result> { + pub async fn get_channel_invites(&self, channel_id: ChannelId) -> Result> { self.fire(Request { body: None, multipart: None, @@ -3401,7 +3401,7 @@ impl Http { } /// Gets all invites to a guild. - pub async fn get_guild_invites(&self, guild_id: GuildId) -> Result> { + pub async fn get_guild_invites(&self, guild_id: GuildId) -> Result> { self.fire(Request { body: None, multipart: None, @@ -3801,18 +3801,14 @@ impl Http { &self, code: &str, member_counts: bool, - expiration: bool, event_id: Option, ) -> Result { - let (member_counts_str, expiration_str, event_id_str); - let mut params = ArrayVec::<_, 3>::new(); + let (member_counts_str, event_id_str); + let mut params = ArrayVec::<_, 2>::new(); member_counts_str = member_counts.to_arraystring(); params.push(("with_counts", member_counts_str.as_str())); - expiration_str = expiration.to_arraystring(); - params.push(("with_expiration", &expiration_str)); - if let Some(event_id) = event_id { event_id_str = event_id.to_arraystring(); params.push(("guild_scheduled_event_id", &event_id_str)); diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index ec4df209e62..227afb8cb53 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -98,7 +98,7 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Create Instant Invite]: Permissions::CREATE_INSTANT_INVITE - pub async fn create_invite(self, http: &Http, builder: CreateInvite<'_>) -> Result { + pub async fn create_invite(self, http: &Http, builder: CreateInvite<'_>) -> Result { builder.execute(http, self).await } @@ -215,7 +215,7 @@ impl ChannelId { /// Returns [`Error::Http`] if the current user lacks permission. /// /// [Manage Channels]: Permissions::MANAGE_CHANNELS - pub async fn invites(self, http: &Http) -> Result> { + pub async fn invites(self, http: &Http) -> Result> { http.get_channel_invites(self).await } diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 2076f26cc1a..2936712fbfd 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -1016,7 +1016,8 @@ impl GuildId { /// Gets all of the guild's invites. /// - /// Requires the [Manage Guild] permission. + /// Requires the [Manage Guild] or [View Audit Log] permission. [`InviteMetadata`] is only + /// included with the [Manage Guild] permission. /// /// # Errors /// @@ -1024,7 +1025,8 @@ impl GuildId { /// [`Error::Json`] if there is an error in deserializing the API response. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - pub async fn invites(self, http: &Http) -> Result> { + /// [View Audit Log]: Permissions::VIEW_AUDIT_LOG + pub async fn invites(self, http: &Http) -> Result> { http.get_guild_invites(self).await } diff --git a/src/model/invite.rs b/src/model/invite.rs index 3098fc0323c..d4d7c2fc11d 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -10,53 +10,79 @@ use crate::http::Http; /// Information about an invite code. /// -/// Information can not be accessed for guilds the current user is banned from. -/// /// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-object). #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] pub struct Invite { - /// The approximate number of [`Member`]s in the related [`Guild`]. - pub approximate_member_count: Option, - /// The approximate number of [`Member`]s with an active session in the related [`Guild`]. - /// - /// An active session is defined as an open, heartbeating WebSocket connection. - /// These include [invisible][`OnlineStatus::Invisible`] members. - pub approximate_presence_count: Option, + /// The type of invite. + #[serde(rename = "type")] + pub kind: InviteType, /// The unique code for the invite. pub code: FixedString, - /// A representation of the minimal amount of information needed about the [`GuildChannel`] - /// being invited to. - pub channel: InviteChannel, /// A representation of the minimal amount of information needed about the [`Guild`] being /// invited to. pub guild: Option, - /// A representation of the minimal amount of information needed about the [`User`] that - /// created the invite. + /// A representation of the minimal amount of information needed about the [`GuildChannel`] + /// being invited to. + pub channel: InviteChannel, + /// The [`User`] who created the invite. /// - /// This can be [`None`] for invites created by Discord such as invite-widgets or vanity invite + /// Can be [`None`] for invites created by Discord, e.g., server widgets or vanity invite /// links. pub inviter: Option, /// The type of target for this voice channel invite. pub target_type: Option, - /// The user whose stream to display for this voice channel stream invite. + /// The [`User`] whose stream to display for this voice channel stream invite. /// /// Only shows up if `target_type` is `Stream`. - pub target_user: Option, + pub target_user: Option, /// The embedded application to open for this voice channel embedded application invite. /// /// Only shows up if `target_type` is `EmmbeddedApplication`. - pub target_application: Option, - /// The expiration date of this invite, returned from `Http::get_invite` when `with_expiration` - /// is true. + pub target_application: Option, + /// The approximate number of [`Member`]s with an active session in the related [`Guild`]. + /// + /// An active session is defined as an open, heartbeating WebSocket connection. + /// These include [invisible][`OnlineStatus::Invisible`] members. + /// + /// Only included when retrieving a single invite with `member_counts` set to `true`. + pub approximate_presence_count: Option, + /// The approximate number of [`Member`]s in the related [`Guild`]. + /// + /// Only included when retrieving a single invite with `member_counts` set to `true`. + pub approximate_member_count: Option, + /// The expiration date of this invite. pub expires_at: Option, - /// The Stage instance data if there is a public Stage instance in the Stage channel this - /// invite is for. - pub stage_instance: Option, - /// Guild scheduled event data, only included if guild_scheduled_event_id contains a valid - /// guild scheduled event id (according to Discord docs, whatever that means). + /// Guild scheduled event data. + /// + /// Only included when retrieving a single invite with a valid [`ScheduledEventId`] provided + /// for `event_id`. #[serde(rename = "guild_scheduled_event")] pub scheduled_event: Option, + /// Guild invite flags for guild invites. + pub flags: Option, + /// A representation of the minimal amount of information needed about the [`Role`]s assigned + /// to a user upon accepting the invite. + pub roles: Option>, + /// Extra information about an invite. + /// + /// Only included when retrieving guild invites with the + /// [`MANAGE_GUILD`][Permissions::MANAGE_GUILD] permission or channel invites with the + /// [`MANAGE_CHANNELS`][Permissions::MANAGE_CHANNELS] permission. + #[serde(flatten)] + pub metadata: Option, +} + +bitflags! { + /// Flags for a guild invite. + /// + /// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-object-guild-invite-flags). + #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] + #[derive(Copy, Clone, Default, Debug, Eq, Hash, PartialEq)] + pub struct GuildInviteFlags: u32 { + /// This invite is a guest invite for a voice channel. + const IS_GUEST_INVITE = 1 << 0; + } } #[cfg(feature = "model")] @@ -74,21 +100,21 @@ impl Invite { http: &Http, channel_id: ChannelId, builder: CreateInvite<'_>, - ) -> Result { + ) -> Result { channel_id.create_invite(http, builder).await } /// Deletes the invite. /// - /// **Note**: Requires the [Manage Guild] permission. + /// **Note**: Requires the [Manage Channels] permission on the channel this invite belongs to, + /// or [Manage Guild] to remove any invite across the guild. /// /// # Errors /// - /// Returns [`Error::Http`] if the current user lacks permission or if the invite is - /// invalid. + /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. /// /// [Manage Guild]: Permissions::MANAGE_GUILD - /// [permission]: super::permissions + /// [Manage Channels]: Permissions::MANAGE_CHANNELS pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { http.delete_invite(&self.code, reason).await } @@ -99,7 +125,6 @@ impl Invite { /// * `code` - The invite code. /// * `member_counts` - Whether to include information about the current number of members in /// the server that the invite belongs to. - /// * `expiration` - Whether to include information about when the invite expires. /// * `event_id` - An optional server event ID to include with the invite. /// /// More information about these arguments can be found on Discord's @@ -113,10 +138,9 @@ impl Invite { http: &Http, code: &str, member_counts: bool, - expiration: bool, event_id: Option, ) -> Result { - http.get_invite(code, member_counts, expiration, event_id).await + http.get_invite(code, member_counts, event_id).await } /// Returns a URL to use for the invite. @@ -131,14 +155,8 @@ impl Invite { /// # /// # fn main() { /// # let invite = from_value::(json!({ - /// # "approximate_member_count": Some(1812), - /// # "approximate_presence_count": Some(717), + /// # "type": 0, /// # "code": "WxZumR", - /// # "channel": { - /// # "id": ChannelId::new(1), - /// # "name": "foo", - /// # "type": ChannelType::Text, - /// # }, /// # "guild": { /// # "id": GuildId::new(2), /// # "icon": None::, @@ -150,12 +168,19 @@ impl Invite { /// # "verification_level": 2, /// # "nsfw_level": 0, /// # }, + /// # "channel": { + /// # "id": ChannelId::new(1), + /// # "name": "foo", + /// # "type": ChannelType::Text, + /// # }, /// # "inviter": { /// # "id": UserId::new(3), /// # "username": "foo", /// # "discriminator": "1234", /// # "avatar": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" /// # }, + /// # "approximate_presence_count": Some(717), + /// # "approximate_member_count": Some(1812), /// # })).unwrap(); /// # /// assert_eq!(invite.url(), "https://discord.gg/WxZumR"); @@ -198,130 +223,66 @@ pub struct InviteGuild { pub premium_subscription_count: Option, } -/// Detailed information about an invite. +/// A minimal amount of information about a role assigned to a user upon accepting an invite. +/// +/// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-object-invite-structure). +#[non_exhaustive] +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct InviteRole { + pub id: RoleId, + pub name: FixedString, + pub position: i16, + #[serde(rename = "color")] + pub colour: Colour, + #[serde(rename = "colors")] + pub colours: RoleColours, + pub icon: Option, + pub unicode_emoji: Option, +} + +/// Extra information about an invite. Extends [`Invite`]. /// -/// This information can only be retrieved by anyone with the [Manage Guild] permission. Otherwise, -/// a minimal amount of information can be retrieved via the [`Invite`] struct. +/// Retrieving this information requires the [Manage Guild] permission when retrieving guild +/// invites or the [Manage Channels] permission when retrieving channel invites. /// /// [Manage Guild]: Permissions::MANAGE_GUILD +/// [Manage Channels]: Permissions::MANAGE_CHANNELS /// -/// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-metadata-object) (extends [`Invite`] fields). +/// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-metadata-object). #[derive(Clone, Debug, Deserialize, Serialize)] #[non_exhaustive] -pub struct RichInvite { - /// A representation of the minimal amount of information needed about the channel being - /// invited to. - pub channel: InviteChannel, - /// The unique code for the invite. - pub code: FixedString, - /// When the invite was created. - pub created_at: Timestamp, - /// A representation of the minimal amount of information needed about the [`Guild`] being - /// invited to. - pub guild: Option, - /// The user that created the invite. - pub inviter: Option, - /// The maximum age of the invite in seconds, from when it was created. - pub max_age: u32, - /// The maximum number of times that an invite may be used before it expires. - /// - /// Note that this does not supersede the [`Self::max_age`] value, if the value of - /// [`Self::temporary`] is `true`. If the value of `temporary` is `false`, then the invite - /// _will_ self-expire after the given number of max uses. +pub struct InviteMetadata { + /// The number of times the invite has been used. + pub uses: u64, + /// The maximum number of times the invite can be used. /// - /// If the value is `0`, then the invite is permanent. + /// If the value is `0`, then the invite has unlimited uses. pub max_uses: u8, - /// Indicator of whether the invite self-expires after a certain amount of time or uses. + /// The duration (in seconds) after which the invite expires. + /// + /// If the value is `0`, then the invite never expires. + pub max_age: u32, + /// Whether the invite only grants temporary membership. pub temporary: bool, - /// The amount of times that an invite has been used. - pub uses: u64, + /// When the invite was created. + pub created_at: Timestamp, } -#[cfg(feature = "model")] -impl RichInvite { - /// Deletes the invite. - /// - /// Refer to [`Http::delete_invite`] for more information. - /// - /// **Note**: Requires the [Manage Guild] permission. - /// - /// # Errors - /// - /// Returns [`Error::Http`] if the current user lacks permission or if invalid data is given. - /// - /// [Manage Guild]: Permissions::MANAGE_GUILD - /// [permission]: super::permissions - pub async fn delete(&self, http: &Http, reason: Option<&str>) -> Result { - http.delete_invite(&self.code, reason).await - } - - /// Returns a URL to use for the invite. - /// - /// # Examples - /// - /// Retrieve the URL for an invite with the code `WxZumR`: +enum_number! { + /// Type of invite. /// - /// ```rust - /// # use serde_json::{json, from_value}; - /// # use serenity::model::prelude::*; - /// # - /// # fn main() { - /// # let invite = from_value::(json!({ - /// # "code": "WxZumR", - /// # "channel": { - /// # "id": ChannelId::new(1), - /// # "name": "foo", - /// # "type": ChannelType::Text, - /// # }, - /// # "created_at": "2017-01-29T15:35:17.136000+00:00", - /// # "guild": { - /// # "id": GuildId::new(2), - /// # "icon": None::, - /// # "name": "baz", - /// # "splash_hash": None::, - /// # "text_channel_count": None::, - /// # "voice_channel_count": None::, - /// # "features": ["NEWS", "DISCOVERABLE"], - /// # "verification_level": 2, - /// # "nsfw_level": 0, - /// # }, - /// # "inviter": { - /// # "avatar": None::, - /// # "bot": false, - /// # "discriminator": "1234", - /// # "id": UserId::new(4), - /// # "username": "qux", - /// # "public_flags": None::, - /// # }, - /// # "max_age": 5, - /// # "max_uses": 6, - /// # "temporary": true, - /// # "uses": 7, - /// # })).unwrap(); - /// # - /// assert_eq!(invite.url(), "https://discord.gg/WxZumR"); - /// # } - /// ``` - #[must_use] - pub fn url(&self) -> String { - format!("https://discord.gg/{}", self.code) + /// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-object-invite-types). + #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] + #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))] + #[non_exhaustive] + pub enum InviteType { + Guild = 0, + GroupDm = 1, + Friend = 2, + _ => Unknown(u8), } } -/// [Discord docs](https://docs.discord.com/developers/resources/invite#invite-stage-instance-object). -#[derive(Clone, Debug, Deserialize, Serialize)] -#[non_exhaustive] -pub struct InviteStageInstance { - /// The members speaking in the Stage - pub members: FixedArray, - /// The number of users in the Stage - pub participant_count: u64, - /// The number of users speaking in the Stage - pub speaker_count: u64, - /// The topic of the Stage instance (1-120 characters) - pub topic: FixedString, -} - enum_number! { /// Type of target for a voice channel invite. /// diff --git a/src/model/permissions.rs b/src/model/permissions.rs index fa7ad4cb651..7b253f21eaa 100644 --- a/src/model/permissions.rs +++ b/src/model/permissions.rs @@ -267,9 +267,9 @@ pub const PRESET_VOICE: Permissions = Permissions::from_bits_truncate( pub struct Permissions(u64); generate_permissions! { - /// Allows for the creation of [`RichInvite`]s. + /// Allows for the creation of [`Invite`]s. /// - /// [`RichInvite`]: super::invite::RichInvite + /// [`Invite`]: super::invite::Invite CREATE_INSTANT_INVITE, create_instant_invite, "Create Invites" = 1 << 0; /// Allows for the kicking of guild [member]s. /// diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 564fdfd74e4..6e053d50249 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -22,7 +22,7 @@ use crate::model::prelude::*; /// /// # Examples /// -/// Two formats of [invite][`RichInvite`] codes are supported, both regardless of protocol prefix. +/// Two formats of [invite][`Invite`] codes are supported, both regardless of protocol prefix. /// Some examples: /// /// 1. Retrieving the code from the URL `"https://discord.gg/0cDvIgU2voY8RSYL"`: @@ -45,7 +45,7 @@ use crate::model::prelude::*; /// assert_eq!(utils::parse_invite(url), "0cDvIgU2voY8RSYL"); /// ``` /// -/// [`RichInvite`]: crate::model::invite::RichInvite +/// [`Invite`]: crate::model::invite::Invite #[must_use] pub fn parse_invite(code: &str) -> &str { let code = code.trim_start_matches("http://").trim_start_matches("https://");