Skip to content

Commit

Permalink
feat(model): Implement Premium Button Style (#2363)
Browse files Browse the repository at this point in the history
This PR implements the new premium button style.

Refs:
- discord/discord-api-docs#6875
  • Loading branch information
rickmartensnl authored Aug 31, 2024
1 parent c4d8327 commit 5ae72bc
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ jobs:
- name: Cache dependencies
uses: Swatinem/rust-cache@v2

- name: Lower Tokio version to keep MSRV at 1.67
run: cargo add "tokio@>=1.19.0,<1.39.0" -p twilight-gateway

- name: Output processor info
run: cat /proc/cpuinfo

Expand Down
19 changes: 18 additions & 1 deletion twilight-model/src/channel/message/component/button.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::channel::message::EmojiReactionType;
use crate::{
channel::message::EmojiReactionType,
id::{marker::SkuMarker, Id},
};
use serde::{Deserialize, Serialize};

/// Clickable [`Component`] below messages.
Expand Down Expand Up @@ -27,6 +30,10 @@ pub struct Button {
pub style: ButtonStyle,
/// URL for buttons of a [`ButtonStyle::Link`] style.
pub url: Option<String>,
/// The ID of the SKU that is attached to the button.
///
/// This field is required when using the [`ButtonStyle::Premium`] style.
pub sku_id: Option<Id<SkuMarker>>,
}

/// Style of a [`Button`].
Expand Down Expand Up @@ -60,6 +67,13 @@ pub enum ButtonStyle {
/// Selecting this button style requires specifying the [`Button::url`]
/// field.
Link,
/// Button indicates a premium upgrade action.
///
/// Selecting this button style requires specifying the [`Button::sku_id`]
/// field.
/// The following fields are not available for this button style: [`Button::custom_id`], [`Button::label`], [`Button::url`] & [`Button::emoji`].
/// Premium button styles do not fire an interaction event.
Premium,
/// Variant value is unknown to the library.
Unknown(u8),
}
Expand All @@ -72,6 +86,7 @@ impl From<u8> for ButtonStyle {
3 => ButtonStyle::Success,
4 => ButtonStyle::Danger,
5 => ButtonStyle::Link,
6 => ButtonStyle::Premium,
unknown => ButtonStyle::Unknown(unknown),
}
}
Expand All @@ -85,6 +100,7 @@ impl From<ButtonStyle> for u8 {
ButtonStyle::Success => 3,
ButtonStyle::Danger => 4,
ButtonStyle::Link => 5,
ButtonStyle::Premium => 6,
ButtonStyle::Unknown(unknown) => unknown,
}
}
Expand Down Expand Up @@ -120,6 +136,7 @@ mod tests {
serde_test::assert_tokens(&ButtonStyle::Success, &[Token::U8(3)]);
serde_test::assert_tokens(&ButtonStyle::Danger, &[Token::U8(4)]);
serde_test::assert_tokens(&ButtonStyle::Link, &[Token::U8(5)]);
serde_test::assert_tokens(&ButtonStyle::Premium, &[Token::U8(6)]);
serde_test::assert_tokens(&ButtonStyle::Unknown(99), &[Token::U8(99)]);
}
}
59 changes: 58 additions & 1 deletion twilight-model/src/channel/message/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ pub use self::{
};

use super::EmojiReactionType;
use crate::channel::ChannelType;
use crate::{
channel::ChannelType,
id::{marker::SkuMarker, Id},
};
use serde::{
de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
ser::{Error as SerError, SerializeStruct},
Expand Down Expand Up @@ -48,6 +51,7 @@ use std::fmt::{Formatter, Result as FmtResult};
/// label: Some("Click me!".to_owned()),
/// style: ButtonStyle::Primary,
/// url: None,
/// sku_id: None,
/// })]),
/// });
/// ```
Expand Down Expand Up @@ -140,6 +144,7 @@ impl Component {
/// label: Some("ping".to_owned()),
/// style: ButtonStyle::Primary,
/// url: None,
/// sku_id: None,
/// });
///
/// assert_eq!(ComponentType::Button, component.kind());
Expand Down Expand Up @@ -211,6 +216,7 @@ enum Field {
Style,
Type,
Url,
SkuId,
Value,
}

Expand Down Expand Up @@ -247,6 +253,7 @@ impl<'de> Visitor<'de> for ComponentVisitor {
let mut placeholder: Option<Option<String>> = None;
let mut required: Option<Option<bool>> = None;
let mut url: Option<Option<String>> = None;
let mut sku_id: Option<Id<SkuMarker>> = None;
let mut value: Option<Option<String>> = None;

loop {
Expand Down Expand Up @@ -380,6 +387,13 @@ impl<'de> Visitor<'de> for ComponentVisitor {

url = Some(map.next_value()?);
}
Field::SkuId => {
if sku_id.is_some() {
return Err(DeError::duplicate_field("sku_id"));
}

sku_id = map.next_value()?;
}
Field::Value => {
if value.is_some() {
return Err(DeError::duplicate_field("value"));
Expand Down Expand Up @@ -409,6 +423,7 @@ impl<'de> Visitor<'de> for ComponentVisitor {
// - emoji
// - label
// - url
// - sku_id
ComponentType::Button => {
let style = style
.ok_or_else(|| DeError::missing_field("style"))?
Expand All @@ -428,6 +443,7 @@ impl<'de> Visitor<'de> for ComponentVisitor {
label: label.flatten(),
style,
url: url.unwrap_or_default(),
sku_id,
})
}
// Required fields:
Expand Down Expand Up @@ -543,12 +559,14 @@ impl Serialize for Component {
// - emoji
// - label
// - url
// - sku_id
Component::Button(button) => {
2 + usize::from(button.custom_id.is_some())
+ usize::from(button.disabled)
+ usize::from(button.emoji.is_some())
+ usize::from(button.label.is_some())
+ usize::from(button.url.is_some())
+ usize::from(button.sku_id.is_some())
}
// Required fields:
// - custom_id
Expand Down Expand Up @@ -629,6 +647,10 @@ impl Serialize for Component {
if button.url.is_some() {
state.serialize_field("url", &button.url)?;
}

if button.sku_id.is_some() {
state.serialize_field("sku_id", &button.sku_id)?;
}
}
Component::SelectMenu(select_menu) => {
match &select_menu.kind {
Expand Down Expand Up @@ -751,6 +773,7 @@ mod tests {
label: Some("test label".into()),
style: ButtonStyle::Primary,
url: None,
sku_id: None,
}),
Component::SelectMenu(SelectMenu {
channel_types: None,
Expand Down Expand Up @@ -854,6 +877,7 @@ mod tests {
style: ButtonStyle::Primary,
label: Some("Button".to_owned()),
url: None,
sku_id: None,
})]),
});

Expand Down Expand Up @@ -905,6 +929,7 @@ mod tests {
label: Some("Test".to_owned()),
style: ButtonStyle::Link,
url: Some("https://twilight.rs".to_owned()),
sku_id: None,
});

serde_test::assert_tokens(
Expand Down Expand Up @@ -1063,4 +1088,36 @@ mod tests {
],
);
}

#[test]
fn premium_button() {
let value = Component::Button(Button {
custom_id: None,
disabled: false,
emoji: None,
label: None,
style: ButtonStyle::Premium,
url: None,
sku_id: Some(Id::new(114_941_315_417_899_012)),
});

serde_test::assert_tokens(
&value,
&[
Token::Struct {
name: "Component",
len: 3,
},
Token::String("type"),
Token::U8(ComponentType::Button.into()),
Token::String("style"),
Token::U8(ButtonStyle::Premium.into()),
Token::String("sku_id"),
Token::Some,
Token::NewtypeStruct { name: "Id" },
Token::Str("114941315417899012"),
Token::StructEnd,
],
);
}
}
6 changes: 6 additions & 0 deletions twilight-model/src/http/interaction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! Models used when responding to interactions over HTTP.
#![allow(deprecated)]

use super::attachment::Attachment;
use crate::{
Expand Down Expand Up @@ -102,6 +103,11 @@ pub enum InteractionResponseType {
Modal = 9,
/// Respond to an interaction with an upgrade button, only available
/// for apps with monetization enabled
///
/// Deprecated: Please send a [`InteractionResponseType::ChannelMessageWithSource`]
/// with an [`Button`](crate::channel::message::component::Button) with the style [`ButtonStyle::Premium`](crate::channel::message::component::ButtonStyle)
/// instead.
#[deprecated(note = "Deprecated by Discord in favor of Premium Buttons")]
PremiumRequired = 10,
}

Expand Down
2 changes: 2 additions & 0 deletions twilight-util/src/builder/interaction_response_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use twilight_model::{
/// custom_id: Some("button_id".to_string()),
/// url: None,
/// disabled: false,
/// sku_id: None,
/// })]),
/// });
///
Expand Down Expand Up @@ -202,6 +203,7 @@ mod tests {
custom_id: Some("test custom id".into()),
url: None,
disabled: false,
sku_id: None,
});

let embed = Embed {
Expand Down
4 changes: 4 additions & 0 deletions twilight-validate/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ mod tests {
ButtonStyle::Success,
ButtonStyle::Danger,
ButtonStyle::Link,
ButtonStyle::Premium,
];

#[test]
Expand All @@ -1227,6 +1228,7 @@ mod tests {
label: Some("Read".into()),
style: ButtonStyle::Link,
url: Some("https://abebooks.com".into()),
sku_id: None,
};

let select_menu = SelectMenu {
Expand Down Expand Up @@ -1285,6 +1287,7 @@ mod tests {
label: None,
style: ButtonStyle::Primary,
url: Some("https://twilight.rs".to_owned()),
sku_id: None,
};

assert!(matches!(
Expand All @@ -1307,6 +1310,7 @@ mod tests {
label: Some("some label".to_owned()),
style: *style,
url: None,
sku_id: None,
};

assert!(matches!(
Expand Down

0 comments on commit 5ae72bc

Please sign in to comment.