serenity/utils/
quick_modal.rs

1use crate::builder::{
2    Builder as _,
3    CreateActionRow,
4    CreateInputText,
5    CreateInteractionResponse,
6    CreateModal,
7};
8use crate::client::Context;
9use crate::collector::ModalInteractionCollector;
10use crate::model::prelude::*;
11
12#[cfg(feature = "collector")]
13pub struct QuickModalResponse {
14    pub interaction: ModalInteraction,
15    pub inputs: Vec<String>,
16}
17
18/// Convenience builder to create a modal, wait for the user to submit and parse the response.
19///
20/// ```rust
21/// # use serenity::{builder::*, model::prelude::*, prelude::*, utils::CreateQuickModal, Result};
22/// # async fn foo_(ctx: &Context, interaction: &CommandInteraction) -> Result<()> {
23/// let modal = CreateQuickModal::new("About you")
24///     .timeout(std::time::Duration::from_secs(600))
25///     .short_field("First name")
26///     .short_field("Last name")
27///     .paragraph_field("Hobbies and interests");
28/// let response = interaction.quick_modal(ctx, modal).await?;
29/// let inputs = response.unwrap().inputs;
30/// let (first_name, last_name, hobbies) = (&inputs[0], &inputs[1], &inputs[2]);
31/// # Ok(())
32/// # }
33/// ```
34#[cfg(feature = "collector")]
35#[must_use]
36pub struct CreateQuickModal {
37    title: String,
38    timeout: Option<std::time::Duration>,
39    input_texts: Vec<CreateInputText>,
40}
41
42#[cfg(feature = "collector")]
43impl CreateQuickModal {
44    pub fn new(title: impl Into<String>) -> Self {
45        Self {
46            title: title.into(),
47            timeout: None,
48            input_texts: Vec::new(),
49        }
50    }
51
52    /// Sets a timeout when waiting for the modal response.
53    ///
54    /// You should almost always set a timeout here. Otherwise, if the user exits the modal, you
55    /// will wait forever.
56    pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
57        self.timeout = Some(timeout);
58        self
59    }
60
61    /// Adds an input text field.
62    ///
63    /// As the `custom_id` field of [`CreateInputText`], just supply an empty string. All custom
64    /// IDs are overwritten by [`CreateQuickModal`] when sending the modal.
65    pub fn field(mut self, input_text: CreateInputText) -> Self {
66        self.input_texts.push(input_text);
67        self
68    }
69
70    /// Convenience method to add a single-line input text field.
71    ///
72    /// Wraps [`Self::field`].
73    pub fn short_field(self, label: impl Into<String>) -> Self {
74        self.field(CreateInputText::new(InputTextStyle::Short, label, ""))
75    }
76
77    /// Convenience method to add a multi-line input text field.
78    ///
79    /// Wraps [`Self::field`].
80    pub fn paragraph_field(self, label: impl Into<String>) -> Self {
81        self.field(CreateInputText::new(InputTextStyle::Paragraph, label, ""))
82    }
83
84    /// # Errors
85    ///
86    /// See [`CreateInteractionResponse::execute()`].
87    pub async fn execute(
88        self,
89        ctx: &Context,
90        interaction_id: InteractionId,
91        token: &str,
92    ) -> Result<Option<QuickModalResponse>, crate::Error> {
93        let modal_custom_id = interaction_id.get().to_string();
94        let builder = CreateInteractionResponse::Modal(
95            CreateModal::new(&modal_custom_id, self.title).components(
96                self.input_texts
97                    .into_iter()
98                    .enumerate()
99                    .map(|(i, input_text)| {
100                        CreateActionRow::InputText(input_text.custom_id(i.to_string()))
101                    })
102                    .collect(),
103            ),
104        );
105        builder.execute(ctx, (interaction_id, token)).await?;
106
107        let collector =
108            ModalInteractionCollector::new(&ctx.shard).custom_ids(vec![modal_custom_id]);
109
110        let collector = match self.timeout {
111            Some(timeout) => collector.timeout(timeout),
112            None => collector,
113        };
114
115        let modal_interaction = collector.next().await;
116
117        let Some(modal_interaction) = modal_interaction else { return Ok(None) };
118
119        let inputs = modal_interaction
120            .data
121            .components
122            .iter()
123            .filter_map(|row| match row.components.first() {
124                Some(ActionRowComponent::InputText(text)) => {
125                    if let Some(value) = &text.value {
126                        Some(value.clone())
127                    } else {
128                        tracing::warn!("input text value was empty in modal response");
129                        None
130                    }
131                },
132                Some(other) => {
133                    tracing::warn!("expected input text in modal response, got {:?}", other);
134                    None
135                },
136                None => {
137                    tracing::warn!("empty action row");
138                    None
139                },
140            })
141            .collect();
142
143        Ok(Some(QuickModalResponse {
144            inputs,
145            interaction: modal_interaction,
146        }))
147    }
148}