serenity/model/application/
component.rs

1use serde::de::Error as DeError;
2use serde::ser::{Serialize, Serializer};
3
4use crate::internal::prelude::*;
5use crate::json::from_value;
6use crate::model::prelude::*;
7use crate::model::utils::{default_true, deserialize_val};
8
9enum_number! {
10    /// The type of a component
11    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
12    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
13    #[serde(from = "u8", into = "u8")]
14    #[non_exhaustive]
15    pub enum ComponentType {
16        ActionRow = 1,
17        Button = 2,
18        StringSelect = 3,
19        InputText = 4,
20        UserSelect = 5,
21        RoleSelect = 6,
22        MentionableSelect = 7,
23        ChannelSelect = 8,
24        _ => Unknown(u8),
25    }
26}
27
28/// An action row.
29///
30/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#action-rows).
31#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
32#[derive(Clone, Debug, Deserialize, Serialize)]
33#[non_exhaustive]
34pub struct ActionRow {
35    /// Always [`ComponentType::ActionRow`]
36    #[serde(rename = "type")]
37    pub kind: ComponentType,
38    /// The components of this ActionRow.
39    #[serde(default)]
40    pub components: Vec<ActionRowComponent>,
41}
42
43/// A component which can be inside of an [`ActionRow`].
44///
45/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#component-object-component-types).
46#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
47#[derive(Clone, Debug)]
48#[non_exhaustive]
49pub enum ActionRowComponent {
50    Button(Button),
51    SelectMenu(SelectMenu),
52    InputText(InputText),
53}
54
55impl<'de> Deserialize<'de> for ActionRowComponent {
56    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
57        let map = JsonMap::deserialize(deserializer)?;
58
59        let raw_kind = map.get("type").ok_or_else(|| DeError::missing_field("type"))?.clone();
60        let value = Value::from(map);
61
62        match deserialize_val(raw_kind)? {
63            ComponentType::Button => from_value(value).map(ActionRowComponent::Button),
64            ComponentType::InputText => from_value(value).map(ActionRowComponent::InputText),
65            ComponentType::StringSelect
66            | ComponentType::UserSelect
67            | ComponentType::RoleSelect
68            | ComponentType::MentionableSelect
69            | ComponentType::ChannelSelect => from_value(value).map(ActionRowComponent::SelectMenu),
70            ComponentType::ActionRow => {
71                return Err(DeError::custom("Invalid component type ActionRow"))
72            },
73            ComponentType::Unknown(i) => {
74                return Err(DeError::custom(format_args!("Unknown component type {i}")))
75            },
76        }
77        .map_err(DeError::custom)
78    }
79}
80
81impl Serialize for ActionRowComponent {
82    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
83        match self {
84            Self::Button(c) => c.serialize(serializer),
85            Self::InputText(c) => c.serialize(serializer),
86            Self::SelectMenu(c) => c.serialize(serializer),
87        }
88    }
89}
90
91impl From<Button> for ActionRowComponent {
92    fn from(component: Button) -> Self {
93        ActionRowComponent::Button(component)
94    }
95}
96
97impl From<SelectMenu> for ActionRowComponent {
98    fn from(component: SelectMenu) -> Self {
99        ActionRowComponent::SelectMenu(component)
100    }
101}
102
103#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
104#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
105#[serde(untagged)]
106pub enum ButtonKind {
107    Link { url: String },
108    Premium { sku_id: SkuId },
109    NonLink { custom_id: String, style: ButtonStyle },
110}
111
112impl Serialize for ButtonKind {
113    fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
114    where
115        S: Serializer,
116    {
117        #[derive(Serialize)]
118        struct Helper<'a> {
119            style: u8,
120            #[serde(skip_serializing_if = "Option::is_none")]
121            url: Option<&'a str>,
122            #[serde(skip_serializing_if = "Option::is_none")]
123            custom_id: Option<&'a str>,
124            #[serde(skip_serializing_if = "Option::is_none")]
125            sku_id: Option<SkuId>,
126        }
127
128        let helper = match self {
129            ButtonKind::Link {
130                url,
131            } => Helper {
132                style: 5,
133                url: Some(url),
134                custom_id: None,
135                sku_id: None,
136            },
137            ButtonKind::Premium {
138                sku_id,
139            } => Helper {
140                style: 6,
141                url: None,
142                custom_id: None,
143                sku_id: Some(*sku_id),
144            },
145            ButtonKind::NonLink {
146                custom_id,
147                style,
148            } => Helper {
149                style: (*style).into(),
150                url: None,
151                custom_id: Some(custom_id),
152                sku_id: None,
153            },
154        };
155        helper.serialize(serializer)
156    }
157}
158
159/// A button component.
160///
161/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#button-object-button-structure).
162#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
163#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
164#[non_exhaustive]
165pub struct Button {
166    /// The component type, it will always be [`ComponentType::Button`].
167    #[serde(rename = "type")]
168    pub kind: ComponentType,
169    /// The button kind and style.
170    #[serde(flatten)]
171    pub data: ButtonKind,
172    /// The text which appears on the button.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub label: Option<String>,
175    /// The emoji of this button, if there is one.
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub emoji: Option<ReactionType>,
178    /// Whether the button is disabled.
179    #[serde(default)]
180    pub disabled: bool,
181}
182
183enum_number! {
184    /// The style of a button.
185    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
186    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
187    #[serde(from = "u8", into = "u8")]
188    #[non_exhaustive]
189    pub enum ButtonStyle {
190        Primary = 1,
191        Secondary = 2,
192        Success = 3,
193        Danger = 4,
194        // No Link, because we represent Link using enum variants
195        _ => Unknown(u8),
196    }
197}
198
199/// A select menu component.
200///
201/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure).
202#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
203#[derive(Clone, Debug, Deserialize, Serialize)]
204#[non_exhaustive]
205pub struct SelectMenu {
206    /// The component type, which may either be [`ComponentType::StringSelect`],
207    /// [`ComponentType::UserSelect`], [`ComponentType::RoleSelect`],
208    /// [`ComponentType::MentionableSelect`], or [`ComponentType::ChannelSelect`].
209    #[serde(rename = "type")]
210    pub kind: ComponentType,
211    /// An identifier defined by the developer for the select menu.
212    pub custom_id: Option<String>,
213    /// The options of this select menu.
214    ///
215    /// Required for [`ComponentType::StringSelect`] and unavailable for all others.
216    #[serde(default)]
217    pub options: Vec<SelectMenuOption>,
218    /// List of channel types to include in the [`ComponentType::ChannelSelect`].
219    #[serde(default)]
220    pub channel_types: Vec<ChannelType>,
221    /// The placeholder shown when nothing is selected.
222    pub placeholder: Option<String>,
223    /// The minimum number of selections allowed.
224    pub min_values: Option<u8>,
225    /// The maximum number of selections allowed.
226    pub max_values: Option<u8>,
227    /// Whether select menu is disabled.
228    #[serde(default)]
229    pub disabled: bool,
230}
231
232/// A select menu component options.
233///
234/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure).
235#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
236#[derive(Clone, Debug, Deserialize, Serialize)]
237#[non_exhaustive]
238pub struct SelectMenuOption {
239    /// The text displayed on this option.
240    pub label: String,
241    /// The value to be sent for this option.
242    pub value: String,
243    /// The description shown for this option.
244    pub description: Option<String>,
245    /// The emoji displayed on this option.
246    pub emoji: Option<ReactionType>,
247    /// Render this option as the default selection.
248    #[serde(default)]
249    pub default: bool,
250}
251
252/// An input text component for modal interactions
253///
254/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure).
255#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
256#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
257#[non_exhaustive]
258pub struct InputText {
259    /// The component type, it will always be [`ComponentType::InputText`].
260    #[serde(rename = "type")]
261    pub kind: ComponentType,
262    /// Developer-defined identifier for the input; max 100 characters
263    pub custom_id: String,
264    /// The [`InputTextStyle`]. Required when sending modal data.
265    ///
266    /// Discord docs are wrong here; it says the field is always sent in modal submit interactions
267    /// but it's not. It's only required when _sending_ modal data to Discord.
268    /// <https://github.com/discord/discord-api-docs/issues/6141>
269    pub style: Option<InputTextStyle>,
270    /// Label for this component; max 45 characters. Required when sending modal data.
271    ///
272    /// Discord docs are wrong here; it says the field is always sent in modal submit interactions
273    /// but it's not. It's only required when _sending_ modal data to Discord.
274    /// <https://github.com/discord/discord-api-docs/issues/6141>
275    pub label: Option<String>,
276    /// Minimum input length for a text input; min 0, max 4000
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub min_length: Option<u16>,
279    /// Maximum input length for a text input; min 1, max 4000
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub max_length: Option<u16>,
282    /// Whether this component is required to be filled (defaults to true)
283    #[serde(default = "default_true")]
284    pub required: bool,
285    /// When sending: Pre-filled value for this component; max 4000 characters (may be None).
286    ///
287    /// When receiving: The input from the user (always Some)
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub value: Option<String>,
290    /// Custom placeholder text if the input is empty; max 100 characters
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub placeholder: Option<String>,
293}
294
295enum_number! {
296    /// The style of the input text
297    ///
298    /// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-styles).
299    #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
300    #[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
301    #[serde(from = "u8", into = "u8")]
302    #[non_exhaustive]
303    pub enum InputTextStyle {
304        Short = 1,
305        Paragraph = 2,
306        _ => Unknown(u8),
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use crate::json::{assert_json, json};
314
315    #[test]
316    fn test_button_serde() {
317        let mut button = Button {
318            kind: ComponentType::Button,
319            data: ButtonKind::NonLink {
320                custom_id: "hello".into(),
321                style: ButtonStyle::Danger,
322            },
323            label: Some("a".into()),
324            emoji: None,
325            disabled: false,
326        };
327        assert_json(
328            &button,
329            json!({"type": 2, "style": 4, "custom_id": "hello", "label": "a", "disabled": false}),
330        );
331
332        button.data = ButtonKind::Link {
333            url: "https://google.com".into(),
334        };
335        assert_json(
336            &button,
337            json!({"type": 2, "style": 5, "url": "https://google.com", "label": "a", "disabled": false}),
338        );
339
340        button.data = ButtonKind::Premium {
341            sku_id: 1234965026943668316.into(),
342        };
343        assert_json(
344            &button,
345            json!({"type": 2, "style": 6, "sku_id": "1234965026943668316", "label": "a", "disabled": false}),
346        );
347    }
348}