Skip to content

Commit

Permalink
feat(http, model, util): add role subscriptions
Browse files Browse the repository at this point in the history
Add support for guild role subscriptions. This involves:

- a new `RoleSubscriptionPurchase` variant on `MessageType`
- four new guild features:
  - `CreatorMonetizableProvisional`
  - `CreatorStorePage`
  - `RoleSubscriptionsAvailableForPurchase`
  - `RoleSubscriptionsEnabled`
- a new `GuildSubscription` variant on `GuildIntegrationType`
- two new role tag fields:
  - `available_for_purchase` (bool)
  - `subscription_listing_id` (optional snowflake)

Depends on PR #2030 since it makes `GuildIntegrationType` into an enum
which this adds a variant to, is related to #2032 since it abstracts the
optional null boolean, and relates to #2033 since it reworks tests this
adds to.

API docs:
discord/discord-api-docs@e003051

Closes #2027.
  • Loading branch information
zeylahellyer committed Jan 5, 2023
1 parent 34bd1bd commit cf09f4a
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ use twilight_model::{
};

/// Get the guild's integrations.
///
/// This endpoint returns a maximum of 50 integrations. If a guild has more
/// integrations then they can't be accessed.
#[must_use = "requests must be configured and executed"]
pub struct GetGuildIntegrations<'a> {
guild_id: Id<GuildMarker>,
Expand Down
4 changes: 4 additions & 0 deletions twilight-model/src/channel/message/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub enum MessageType {
ContextMenuCommand,
/// Message is an auto moderation action.
AutoModerationAction,
/// System message denoting a user subscribed to a role.
RoleSubscriptionPurchase,
/// Variant value is unknown to the library.
Unknown(u8),
}
Expand Down Expand Up @@ -83,6 +85,7 @@ impl From<u8> for MessageType {
22 => Self::GuildInviteReminder,
23 => Self::ContextMenuCommand,
24 => Self::AutoModerationAction,
25 => Self::RoleSubscriptionPurchase,
unknown => Self::Unknown(unknown),
}
}
Expand Down Expand Up @@ -115,6 +118,7 @@ impl From<MessageType> for u8 {
MessageType::GuildInviteReminder => 22,
MessageType::ContextMenuCommand => 23,
MessageType::AutoModerationAction => 24,
MessageType::RoleSubscriptionPurchase => 25,
MessageType::Unknown(unknown) => unknown,
}
}
Expand Down
37 changes: 37 additions & 0 deletions twilight-model/src/guild/feature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ pub enum GuildFeature {
/// Can enable welcome screen, membership screening, stage channels,
/// discovery, and receives community updates.
Community,
/// Guild has enabled monetization.
CreatorMonetizableProvisional,
/// Guild has enabled the role subscription promotional page.
CreatorStorePage,
/// Is able to be discovered in the directory.
Discoverable,
/// Is able to be featured in the directory.
Expand All @@ -37,6 +41,7 @@ pub enum GuildFeature {
/// Has enabled membership screening.
MemberVerificationGateEnabled,
/// Has enabled monetization.
#[deprecated(since = "0.14.1", note = "not in active use by discord")]
MonetizationEnabled,
/// Has increased custom sticker slots.
MoreStickers,
Expand All @@ -50,6 +55,10 @@ pub enum GuildFeature {
PrivateThreads,
/// Is able to set role icons.
RoleIcons,
/// Guild has role subscriptions that can be purchased.
RoleSubscriptionsAvailableForPurchase,
/// Guild has enabled role subscriptions.
RoleSubscriptionsEnabled,
/// Has enabled ticketed events.
TicketedEventsEnabled,
/// Has access to set a vanity URL.
Expand All @@ -73,6 +82,8 @@ impl From<GuildFeature> for Cow<'static, str> {
GuildFeature::Banner => "BANNER".into(),
GuildFeature::Commerce => "COMMERCE".into(),
GuildFeature::Community => "COMMUNITY".into(),
GuildFeature::CreatorMonetizableProvisional => "CREATOR_MONETIZABLE_PROVISIONAL".into(),
GuildFeature::CreatorStorePage => "CREATOR_STORE_PAGE".into(),
GuildFeature::Discoverable => "DISCOVERABLE".into(),
GuildFeature::Featurable => "FEATURABLE".into(),
GuildFeature::InvitesDisabled => "INVITES_DISABLED".into(),
Expand All @@ -87,6 +98,10 @@ impl From<GuildFeature> for Cow<'static, str> {
GuildFeature::PreviewEnabled => "PREVIEW_ENABLED".into(),
GuildFeature::PrivateThreads => "PRIVATE_THREADS".into(),
GuildFeature::RoleIcons => "ROLE_ICONS".into(),
GuildFeature::RoleSubscriptionsAvailableForPurchase => {
"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE".into()
}
GuildFeature::RoleSubscriptionsEnabled => "ROLE_SUBSCRIPTIONS_ENABLED".into(),
GuildFeature::TicketedEventsEnabled => "TICKETED_EVENTS_ENABLED".into(),
GuildFeature::VanityUrl => "VANITY_URL".into(),
GuildFeature::Verified => "VERIFIED".into(),
Expand All @@ -106,6 +121,8 @@ impl From<String> for GuildFeature {
"BANNER" => Self::Banner,
"COMMERCE" => Self::Commerce,
"COMMUNITY" => Self::Community,
"CREATOR_MONETIZABLE_PROVISIONAL" => GuildFeature::CreatorMonetizableProvisional,
"CREATOR_STORE_PAGE" => GuildFeature::CreatorStorePage,
"DISCOVERABLE" => Self::Discoverable,
"FEATURABLE" => Self::Featurable,
"INVITES_DISABLED" => Self::InvitesDisabled,
Expand All @@ -118,6 +135,10 @@ impl From<String> for GuildFeature {
"PREVIEW_ENABLED" => Self::PreviewEnabled,
"PRIVATE_THREADS" => Self::PrivateThreads,
"ROLE_ICONS" => Self::RoleIcons,
"ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" => {
GuildFeature::RoleSubscriptionsAvailableForPurchase
}
"ROLE_SUBSCRIPTIONS_ENABLED" => GuildFeature::RoleSubscriptionsEnabled,
"TICKETED_EVENTS_ENABLED" => Self::TicketedEventsEnabled,
"VANITY_URL" => Self::VanityUrl,
"VERIFIED" => Self::Verified,
Expand Down Expand Up @@ -147,6 +168,14 @@ mod tests {
serde_test::assert_tokens(&GuildFeature::Banner, &[Token::Str("BANNER")]);
serde_test::assert_tokens(&GuildFeature::Commerce, &[Token::Str("COMMERCE")]);
serde_test::assert_tokens(&GuildFeature::Community, &[Token::Str("COMMUNITY")]);
serde_test::assert_tokens(
&GuildFeature::CreatorMonetizableProvisional,
&[Token::Str("CREATOR_MONETIZABLE_PROVISIONAL")],
);
serde_test::assert_tokens(
&GuildFeature::CreatorStorePage,
&[Token::Str("CREATOR_STORE_PAGE")],
);
serde_test::assert_tokens(&GuildFeature::Discoverable, &[Token::Str("DISCOVERABLE")]);
serde_test::assert_tokens(&GuildFeature::Featurable, &[Token::Str("FEATURABLE")]);
serde_test::assert_tokens(
Expand Down Expand Up @@ -174,6 +203,14 @@ mod tests {
&[Token::Str("PRIVATE_THREADS")],
);
serde_test::assert_tokens(&GuildFeature::RoleIcons, &[Token::Str("ROLE_ICONS")]);
serde_test::assert_tokens(
&GuildFeature::RoleSubscriptionsAvailableForPurchase,
&[Token::Str("ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE")],
);
serde_test::assert_tokens(
&GuildFeature::RoleSubscriptionsEnabled,
&[Token::Str("ROLE_SUBSCRIPTIONS_ENABLED")],
);
serde_test::assert_tokens(
&GuildFeature::TicketedEventsEnabled,
&[Token::Str("TICKETED_EVENTS_ENABLED")],
Expand Down
8 changes: 8 additions & 0 deletions twilight-model/src/guild/integration_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use serde::{Deserialize, Serialize};
pub enum GuildIntegrationType {
/// Integration is a Discord application.
Discord,
/// Integration is a Discord guild subscription.
GuildSubscription,
/// Integration is a Twitch connection.
Twitch,
/// Integration is a YouTube connection.
Expand All @@ -26,6 +28,7 @@ impl From<GuildIntegrationType> for Cow<'static, str> {
fn from(value: GuildIntegrationType) -> Self {
match value {
GuildIntegrationType::Discord => "discord".into(),
GuildIntegrationType::GuildSubscription => "guild_subscription".into(),
GuildIntegrationType::Twitch => "twitch".into(),
GuildIntegrationType::YouTube => "youtube".into(),
GuildIntegrationType::Unknown(unknown) => unknown.into(),
Expand All @@ -37,6 +40,7 @@ impl From<String> for GuildIntegrationType {
fn from(value: String) -> Self {
match value.as_str() {
"discord" => Self::Discord,
"guild_subscription" => Self::GuildSubscription,
"twitch" => Self::Twitch,
"youtube" => Self::YouTube,
_ => Self::Unknown(value),
Expand All @@ -53,6 +57,10 @@ mod tests {
fn variants() {
const MAP: &[(GuildIntegrationType, &str)] = &[
(GuildIntegrationType::Discord, "discord"),
(
GuildIntegrationType::GuildSubscription,
"guild_subscription",
),
(GuildIntegrationType::Twitch, "twitch"),
(GuildIntegrationType::YouTube, "youtube"),
];
Expand Down
80 changes: 72 additions & 8 deletions twilight-model/src/guild/role_tags.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
id::{
marker::{IntegrationMarker, UserMarker},
marker::{IntegrationMarker, RoleSubscriptionSkuMarker, UserMarker},
Id,
},
util::is_false,
Expand Down Expand Up @@ -57,13 +57,19 @@ mod premium_subscriber {
/// [`Role`]: super::Role
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct RoleTags {
/// Whether this role is available for purchase.
#[serde(default, skip_serializing_if = "is_false", with = "premium_subscriber")]
pub available_for_purchase: bool,
/// ID of the bot the role belongs to.
#[serde(skip_serializing_if = "Option::is_none")]
pub bot_id: Option<Id<UserMarker>>,
/// ID of the integration the role belongs to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integration_id: Option<Id<IntegrationMarker>>,
/// Whether this is the guild's premium subscriber role.
/// ID of the role's subscription SKU and listing.
#[serde(skip_serializing_if = "Option::is_none")]
pub subscription_listing_id: Option<Id<RoleSubscriptionSkuMarker>>,
/// Whether this is the guild's Booster role.
#[serde(default, skip_serializing_if = "is_false", with = "premium_subscriber")]
pub premium_subscriber: bool,
}
Expand All @@ -75,19 +81,21 @@ mod tests {
use serde_test::Token;

#[test]
fn role_tags_all() {
fn bot() {
let tags = RoleTags {
available_for_purchase: false,
bot_id: Some(Id::new(1)),
integration_id: Some(Id::new(2)),
premium_subscriber: true,
premium_subscriber: false,
subscription_listing_id: None,
};

serde_test::assert_tokens(
&tags,
&[
Token::Struct {
name: "RoleTags",
len: 3,
len: 2,
},
Token::Str("bot_id"),
Token::Some,
Expand All @@ -97,22 +105,78 @@ mod tests {
Token::Some,
Token::NewtypeStruct { name: "Id" },
Token::Str("2"),
Token::StructEnd,
],
);
}

#[test]
fn premium_subscriber() {
let tags = RoleTags {
available_for_purchase: false,
bot_id: None,
integration_id: None,
premium_subscriber: true,
subscription_listing_id: None,
};

serde_test::assert_tokens(
&tags,
&[
Token::Struct {
name: "RoleTags",
len: 1,
},
Token::Str("premium_subscriber"),
Token::None,
Token::StructEnd,
],
);
}

/// Test that if all fields are None and `premium_subscriber` is false, then
/// serialize back into the source payload (where all fields are not
#[test]
fn subscription() {
let tags = RoleTags {
available_for_purchase: true,
bot_id: None,
integration_id: Some(Id::new(1)),
premium_subscriber: false,
subscription_listing_id: Some(Id::new(2)),
};

serde_test::assert_tokens(
&tags,
&[
Token::Struct {
name: "RoleTags",
len: 3,
},
Token::Str("available_for_purchase"),
Token::None,
Token::Str("integration_id"),
Token::Some,
Token::NewtypeStruct { name: "Id" },
Token::Str("1"),
Token::Str("subscription_listing_id"),
Token::Some,
Token::NewtypeStruct { name: "Id" },
Token::Str("2"),
Token::StructEnd,
],
);
}

/// Test that if all fields are None and the optional null fields are false,
/// then serialize back into the source payload (where all fields are not
/// present).
#[test]
fn role_tags_none() {
fn none() {
let tags = RoleTags {
available_for_purchase: false,
bot_id: None,
integration_id: None,
premium_subscriber: false,
subscription_listing_id: None,
};

serde_test::assert_tokens(
Expand Down
9 changes: 9 additions & 0 deletions twilight-model/src/id/marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@ pub struct StickerPackMarker;
#[non_exhaustive]
pub struct StickerPackSkuMarker;

/// Marker for SKU IDs.
///
/// Types such as [`RoleTags`] use this ID marker.
///
/// [`RoleTags`]: crate::guild::RoleTags
#[derive(Debug)]
#[non_exhaustive]
pub struct RoleSubscriptionSkuMarker;

/// Marker for forum tag IDs.
///
/// Types such as [`ForumTag`] use this ID marker.
Expand Down
11 changes: 10 additions & 1 deletion twilight-model/src/id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,8 @@ mod tests {
marker::{
ApplicationMarker, AttachmentMarker, AuditLogEntryMarker, ChannelMarker, CommandMarker,
CommandVersionMarker, EmojiMarker, GenericMarker, GuildMarker, IntegrationMarker,
InteractionMarker, MessageMarker, RoleMarker, StageMarker, UserMarker, WebhookMarker,
InteractionMarker, MessageMarker, RoleMarker, RoleSubscriptionSkuMarker, StageMarker,
UserMarker, WebhookMarker,
},
Id,
};
Expand Down Expand Up @@ -436,6 +437,7 @@ mod tests {
assert_impl_all!(InteractionMarker: Debug, Send, Sync);
assert_impl_all!(MessageMarker: Debug, Send, Sync);
assert_impl_all!(RoleMarker: Debug, Send, Sync);
assert_impl_all!(RoleSubscriptionSkuMarker: Debug, Send, Sync);
assert_impl_all!(StageMarker: Debug, Send, Sync);
assert_impl_all!(UserMarker: Debug, Send, Sync);
assert_impl_all!(WebhookMarker: Debug, Send, Sync);
Expand Down Expand Up @@ -729,6 +731,13 @@ mod tests {
Token::U64(114_941_315_417_899_012),
],
);
serde_test::assert_tokens(
&Id::<RoleSubscriptionSkuMarker>::new(114_941_315_417_899_012),
&[
Token::NewtypeStruct { name: "Id" },
Token::Str("114941315417899012"),
],
);
serde_test::assert_tokens(
&Id::<StageMarker>::new(114_941_315_417_899_012),
&[
Expand Down
11 changes: 9 additions & 2 deletions twilight-util/src/snowflake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use twilight_model::id::{
ApplicationMarker, AttachmentMarker, AuditLogEntryMarker, ChannelMarker, CommandMarker,
CommandVersionMarker, EmojiMarker, GenericMarker, GuildMarker, IntegrationMarker,
InteractionMarker, MessageMarker, OauthSkuMarker, OauthTeamMarker, RoleMarker,
ScheduledEventEntityMarker, ScheduledEventMarker, StageMarker, StickerMarker,
StickerPackMarker, StickerPackSkuMarker, UserMarker, WebhookMarker,
RoleSubscriptionSkuMarker, ScheduledEventEntityMarker, ScheduledEventMarker, StageMarker,
StickerMarker, StickerPackMarker, StickerPackSkuMarker, UserMarker, WebhookMarker,
},
Id,
};
Expand Down Expand Up @@ -181,6 +181,12 @@ impl Snowflake for Id<RoleMarker> {
}
}

impl Snowflake for Id<RoleSubscriptionSkuMarker> {
fn id(&self) -> u64 {
self.get()
}
}

impl Snowflake for Id<ScheduledEventMarker> {
fn id(&self) -> u64 {
self.get()
Expand Down Expand Up @@ -249,6 +255,7 @@ mod tests {
assert_impl_all!(Id<OauthSkuMarker>: Snowflake);
assert_impl_all!(Id<OauthTeamMarker>: Snowflake);
assert_impl_all!(Id<RoleMarker>: Snowflake);
assert_impl_all!(Id<RoleSubscriptionSkuMarker>: Snowflake);
assert_impl_all!(Id<ScheduledEventMarker>: Snowflake);
assert_impl_all!(Id<ScheduledEventEntityMarker>: Snowflake);
assert_impl_all!(Id<StageMarker>: Snowflake);
Expand Down

0 comments on commit cf09f4a

Please sign in to comment.