serenity/builder/
create_components.rs

1use serde::Serialize;
2
3use crate::model::prelude::*;
4
5/// A builder for creating a components action row in a message.
6///
7/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#component-object).
8#[derive(Clone, Debug, PartialEq)]
9#[must_use]
10pub enum CreateActionRow {
11    Buttons(Vec<CreateButton>),
12    SelectMenu(CreateSelectMenu),
13    /// Only valid in modals!
14    InputText(CreateInputText),
15}
16
17impl serde::Serialize for CreateActionRow {
18    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
19        use serde::ser::SerializeMap as _;
20
21        let mut map = serializer.serialize_map(Some(2))?;
22        map.serialize_entry("type", &1_u8)?;
23
24        match self {
25            CreateActionRow::Buttons(buttons) => map.serialize_entry("components", &buttons)?,
26            CreateActionRow::SelectMenu(select) => map.serialize_entry("components", &[select])?,
27            CreateActionRow::InputText(input) => map.serialize_entry("components", &[input])?,
28        }
29
30        map.end()
31    }
32}
33
34/// A builder for creating a button component in a message
35#[derive(Clone, Debug, Serialize, PartialEq)]
36#[must_use]
37pub struct CreateButton(Button);
38
39impl CreateButton {
40    /// Creates a link button to the given URL. You must also set [`Self::label`] and/or
41    /// [`Self::emoji`] after this.
42    ///
43    /// Clicking this button _will not_ trigger an interaction event in your bot.
44    pub fn new_link(url: impl Into<String>) -> Self {
45        Self(Button {
46            kind: ComponentType::Button,
47            data: ButtonKind::Link {
48                url: url.into(),
49            },
50            label: None,
51            emoji: None,
52            disabled: false,
53        })
54    }
55
56    /// Creates a new premium button associated with the given SKU.
57    ///
58    /// Clicking this button _will not_ trigger an interaction event in your bot.
59    pub fn new_premium(sku_id: impl Into<SkuId>) -> Self {
60        Self(Button {
61            kind: ComponentType::Button,
62            data: ButtonKind::Premium {
63                sku_id: sku_id.into(),
64            },
65            label: None,
66            emoji: None,
67            disabled: false,
68        })
69    }
70
71    /// Creates a normal button with the given custom ID. You must also set [`Self::label`] and/or
72    /// [`Self::emoji`] after this.
73    pub fn new(custom_id: impl Into<String>) -> Self {
74        Self(Button {
75            kind: ComponentType::Button,
76            data: ButtonKind::NonLink {
77                style: ButtonStyle::Primary,
78                custom_id: custom_id.into(),
79            },
80            label: None,
81            emoji: None,
82            disabled: false,
83        })
84    }
85
86    /// Sets the custom id of the button, a developer-defined identifier. Replaces the current
87    /// value as set in [`Self::new`].
88    ///
89    /// Has no effect on link buttons and premium buttons.
90    pub fn custom_id(mut self, id: impl Into<String>) -> Self {
91        if let ButtonKind::NonLink {
92            custom_id, ..
93        } = &mut self.0.data
94        {
95            *custom_id = id.into();
96        }
97        self
98    }
99
100    /// Sets the style of this button.
101    ///
102    /// Has no effect on link buttons and premium buttons.
103    pub fn style(mut self, new_style: ButtonStyle) -> Self {
104        if let ButtonKind::NonLink {
105            style, ..
106        } = &mut self.0.data
107        {
108            *style = new_style;
109        }
110        self
111    }
112
113    /// Sets label of the button.
114    pub fn label(mut self, label: impl Into<String>) -> Self {
115        self.0.label = Some(label.into());
116        self
117    }
118
119    /// Sets emoji of the button.
120    pub fn emoji(mut self, emoji: impl Into<ReactionType>) -> Self {
121        self.0.emoji = Some(emoji.into());
122        self
123    }
124
125    /// Sets the disabled state for the button.
126    pub fn disabled(mut self, disabled: bool) -> Self {
127        self.0.disabled = disabled;
128        self
129    }
130}
131
132impl From<Button> for CreateButton {
133    fn from(button: Button) -> Self {
134        Self(button)
135    }
136}
137
138struct CreateSelectMenuDefault(Mention);
139
140impl Serialize for CreateSelectMenuDefault {
141    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
142        use serde::ser::SerializeMap as _;
143
144        let (id, kind) = match self.0 {
145            Mention::Channel(c) => (c.get(), "channel"),
146            Mention::Role(r) => (r.get(), "role"),
147            Mention::User(u) => (u.get(), "user"),
148        };
149
150        let mut map = serializer.serialize_map(Some(2))?;
151        map.serialize_entry("id", &id)?;
152        map.serialize_entry("type", kind)?;
153        map.end()
154    }
155}
156
157/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure).
158#[derive(Clone, Debug, PartialEq)]
159pub enum CreateSelectMenuKind {
160    String { options: Vec<CreateSelectMenuOption> },
161    User { default_users: Option<Vec<UserId>> },
162    Role { default_roles: Option<Vec<RoleId>> },
163    Mentionable { default_users: Option<Vec<UserId>>, default_roles: Option<Vec<RoleId>> },
164    Channel { channel_types: Option<Vec<ChannelType>>, default_channels: Option<Vec<ChannelId>> },
165}
166
167impl Serialize for CreateSelectMenuKind {
168    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169        #[derive(Serialize)]
170        struct Json<'a> {
171            #[serde(rename = "type")]
172            kind: u8,
173            #[serde(skip_serializing_if = "Option::is_none")]
174            options: Option<&'a [CreateSelectMenuOption]>,
175            #[serde(skip_serializing_if = "Option::is_none")]
176            channel_types: Option<&'a [ChannelType]>,
177            #[serde(skip_serializing_if = "Vec::is_empty")]
178            default_values: Vec<CreateSelectMenuDefault>,
179        }
180
181        #[allow(clippy::ref_option)]
182        fn map<I: Into<Mention> + Copy>(
183            values: &Option<Vec<I>>,
184        ) -> impl Iterator<Item = CreateSelectMenuDefault> + '_ {
185            // Calling `.iter().flatten()` on the `Option` treats `None` like an empty vec
186            values.iter().flatten().map(|&i| CreateSelectMenuDefault(i.into()))
187        }
188
189        #[rustfmt::skip]
190        let default_values = match self {
191            Self::String { .. } => vec![],
192            Self::User { default_users: default_values } => map(default_values).collect(),
193            Self::Role { default_roles: default_values } => map(default_values).collect(),
194            Self::Mentionable { default_users, default_roles } => {
195                let users = map(default_users);
196                let roles = map(default_roles);
197                users.chain(roles).collect()
198            },
199            Self::Channel { channel_types: _, default_channels: default_values } => map(default_values).collect(),
200        };
201
202        #[rustfmt::skip]
203        let json = Json {
204            kind: match self {
205                Self::String { .. } => 3,
206                Self::User { .. } => 5,
207                Self::Role { .. } => 6,
208                Self::Mentionable { .. } => 7,
209                Self::Channel { .. } => 8,
210            },
211            options: match self {
212                Self::String { options } => Some(options),
213                _ => None,
214            },
215            channel_types: match self {
216                Self::Channel { channel_types, default_channels: _ } => channel_types.as_deref(),
217                _ => None,
218            },
219            default_values,
220        };
221
222        json.serialize(serializer)
223    }
224}
225
226/// A builder for creating a select menu component in a message
227///
228/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-menu-structure).
229#[derive(Clone, Debug, Serialize, PartialEq)]
230#[must_use]
231pub struct CreateSelectMenu {
232    custom_id: String,
233    #[serde(skip_serializing_if = "Option::is_none")]
234    placeholder: Option<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    min_values: Option<u8>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    max_values: Option<u8>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    disabled: Option<bool>,
241
242    #[serde(flatten)]
243    kind: CreateSelectMenuKind,
244}
245
246impl CreateSelectMenu {
247    /// Creates a builder with given custom id (a developer-defined identifier), and a list of
248    /// options, leaving all other fields empty.
249    pub fn new(custom_id: impl Into<String>, kind: CreateSelectMenuKind) -> Self {
250        Self {
251            custom_id: custom_id.into(),
252            placeholder: None,
253            min_values: None,
254            max_values: None,
255            disabled: None,
256            kind,
257        }
258    }
259
260    /// The placeholder of the select menu.
261    pub fn placeholder(mut self, label: impl Into<String>) -> Self {
262        self.placeholder = Some(label.into());
263        self
264    }
265
266    /// Sets the custom id of the select menu, a developer-defined identifier. Replaces the current
267    /// value as set in [`Self::new`].
268    pub fn custom_id(mut self, id: impl Into<String>) -> Self {
269        self.custom_id = id.into();
270        self
271    }
272
273    /// Sets the minimum values for the user to select.
274    pub fn min_values(mut self, min: u8) -> Self {
275        self.min_values = Some(min);
276        self
277    }
278
279    /// Sets the maximum values for the user to select.
280    pub fn max_values(mut self, max: u8) -> Self {
281        self.max_values = Some(max);
282        self
283    }
284
285    /// Sets the disabled state for the button.
286    pub fn disabled(mut self, disabled: bool) -> Self {
287        self.disabled = Some(disabled);
288        self
289    }
290}
291
292/// A builder for creating an option of a select menu component in a message
293///
294/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure)
295#[derive(Clone, Debug, Serialize, PartialEq)]
296#[must_use]
297pub struct CreateSelectMenuOption {
298    label: String,
299    value: String,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    description: Option<String>,
302    #[serde(skip_serializing_if = "Option::is_none")]
303    emoji: Option<ReactionType>,
304    #[serde(skip_serializing_if = "Option::is_none")]
305    default: Option<bool>,
306}
307
308impl CreateSelectMenuOption {
309    /// Creates a select menu option with the given label and value, leaving all other fields
310    /// empty.
311    pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
312        Self {
313            label: label.into(),
314            value: value.into(),
315            description: None,
316            emoji: None,
317            default: None,
318        }
319    }
320
321    /// Sets the label of this option, replacing the current value as set in [`Self::new`].
322    pub fn label(mut self, label: impl Into<String>) -> Self {
323        self.label = label.into();
324        self
325    }
326
327    /// Sets the value of this option, replacing the current value as set in [`Self::new`].
328    pub fn value(mut self, value: impl Into<String>) -> Self {
329        self.value = value.into();
330        self
331    }
332
333    /// Sets the description shown on this option.
334    pub fn description(mut self, description: impl Into<String>) -> Self {
335        self.description = Some(description.into());
336        self
337    }
338
339    /// Sets emoji of the option.
340    pub fn emoji(mut self, emoji: impl Into<ReactionType>) -> Self {
341        self.emoji = Some(emoji.into());
342        self
343    }
344
345    /// Sets this option as selected by default.
346    pub fn default_selection(mut self, default: bool) -> Self {
347        self.default = Some(default);
348        self
349    }
350}
351
352/// A builder for creating an input text component in a modal
353///
354/// [Discord docs](https://discord.com/developers/docs/interactions/message-components#text-inputs-text-input-structure).
355#[derive(Clone, Debug, Serialize, PartialEq)]
356#[must_use]
357pub struct CreateInputText(InputText);
358
359impl CreateInputText {
360    /// Creates a text input with the given style, label, and custom id (a developer-defined
361    /// identifier), leaving all other fields empty.
362    pub fn new(
363        style: InputTextStyle,
364        label: impl Into<String>,
365        custom_id: impl Into<String>,
366    ) -> Self {
367        Self(InputText {
368            style: Some(style),
369            label: Some(label.into()),
370            custom_id: custom_id.into(),
371
372            placeholder: None,
373            min_length: None,
374            max_length: None,
375            value: None,
376            required: true,
377
378            kind: ComponentType::InputText,
379        })
380    }
381
382    /// Sets the style of this input text. Replaces the current value as set in [`Self::new`].
383    pub fn style(mut self, kind: InputTextStyle) -> Self {
384        self.0.style = Some(kind);
385        self
386    }
387
388    /// Sets the label of this input text. Replaces the current value as set in [`Self::new`].
389    pub fn label(mut self, label: impl Into<String>) -> Self {
390        self.0.label = Some(label.into());
391        self
392    }
393
394    /// Sets the custom id of the input text, a developer-defined identifier. Replaces the current
395    /// value as set in [`Self::new`].
396    pub fn custom_id(mut self, id: impl Into<String>) -> Self {
397        self.0.custom_id = id.into();
398        self
399    }
400
401    /// Sets the placeholder of this input text.
402    pub fn placeholder(mut self, label: impl Into<String>) -> Self {
403        self.0.placeholder = Some(label.into());
404        self
405    }
406
407    /// Sets the minimum length required for the input text
408    pub fn min_length(mut self, min: u16) -> Self {
409        self.0.min_length = Some(min);
410        self
411    }
412
413    /// Sets the maximum length required for the input text
414    pub fn max_length(mut self, max: u16) -> Self {
415        self.0.max_length = Some(max);
416        self
417    }
418
419    /// Sets the value of this input text.
420    pub fn value(mut self, value: impl Into<String>) -> Self {
421        self.0.value = Some(value.into());
422        self
423    }
424
425    /// Sets if the input text is required
426    pub fn required(mut self, required: bool) -> Self {
427        self.0.required = required;
428        self
429    }
430}