sparkle_convenience/
interaction.rs

1#![allow(deprecated)]
2
3use std::{
4    fmt::{Debug, Display},
5    sync::{
6        atomic::{AtomicBool, AtomicU64, Ordering},
7        Arc,
8    },
9};
10
11use twilight_http::client::InteractionClient;
12use twilight_model::{
13    application::{
14        command::CommandOptionChoice,
15        interaction::{Interaction, InteractionType},
16    },
17    channel::{
18        message::{
19            component::{ActionRow, TextInput},
20            Component, MessageFlags,
21        },
22        Message,
23    },
24    guild::Permissions,
25    http::interaction::{InteractionResponse, InteractionResponseData, InteractionResponseType},
26    id::{
27        marker::{InteractionMarker, MessageMarker},
28        Id,
29    },
30};
31
32use crate::{
33    error::{CombinedUserError, Error, ErrorExt, NoCustomError, UserError},
34    reply::Reply,
35    Bot,
36};
37
38/// Extracting data from interactions
39pub mod extract;
40
41/// Defines whether a defer request should be ephemeral
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum DeferVisibility {
44    /// The defer request is only shown to the user that created the interaction
45    Ephemeral,
46    /// The defer request is shown to everyone in the channel
47    Visible,
48}
49
50/// Defines whether a defer request should update the message or create a new
51/// message on the next response
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum DeferBehavior {
54    /// The next response creates a new message
55    Followup,
56    /// The next response updates the last message
57    Update,
58}
59
60/// Allows convenient interaction-related methods
61///
62/// Created from [`Bot::interaction_handle`]
63#[derive(Clone, Debug)]
64#[allow(clippy::module_name_repetitions)]
65pub struct InteractionHandle<'bot> {
66    /// The bot data to make requests with
67    bot: &'bot Bot,
68    /// The interaction's ID
69    id: Id<InteractionMarker>,
70    /// The interaction's token
71    token: String,
72    /// The interaction's type
73    kind: InteractionType,
74    /// The bot's permissions
75    app_permissions: Permissions,
76    /// Whether the interaction was already responded to
77    responded: Arc<AtomicBool>,
78    /// ID of the last message sent as response to the interaction
79    ///
80    /// 0 if `None`
81    last_message_id: Arc<AtomicU64>,
82}
83
84impl Bot {
85    /// Return an interaction's handle
86    #[must_use]
87    pub fn interaction_handle(&self, interaction: &Interaction) -> InteractionHandle<'_> {
88        InteractionHandle {
89            bot: self,
90            id: interaction.id,
91            token: interaction.token.clone(),
92            kind: interaction.kind,
93            app_permissions: interaction.app_permissions.unwrap_or(Permissions::all()),
94            responded: Arc::new(AtomicBool::new(false)),
95            last_message_id: Arc::new(AtomicU64::new(0)),
96        }
97    }
98
99    /// Return the interaction client for this bot
100    #[must_use]
101    pub const fn interaction_client(&self) -> InteractionClient<'_> {
102        self.http.interaction(self.application.id)
103    }
104}
105
106impl InteractionHandle<'_> {
107    /// Check that the bot has the required permissions
108    ///
109    /// Always returns `Ok` in DM channels, make sure the command can actually
110    /// run in DMs
111    ///
112    /// # Errors
113    ///
114    /// Returns [`CombinedUserError::MissingPermissions`] if the bot doesn't
115    /// have the required permissions, the wrapped permissions are the
116    /// permissions the bot is missing
117    pub fn combined_check_permissions<C>(
118        &self,
119        required_permissions: Permissions,
120    ) -> Result<(), CombinedUserError<C>> {
121        let missing_permissions = required_permissions - self.app_permissions;
122        if !missing_permissions.is_empty() {
123            return Err(CombinedUserError::MissingPermissions(Some(
124                missing_permissions,
125            )));
126        }
127
128        Ok(())
129    }
130
131    /// Check that the bot has the required permissions
132    ///
133    /// Always returns `Ok` in DM channels, make sure the command can actually
134    /// run in DMs
135    ///
136    /// # Errors
137    ///
138    /// Returns [`UserError::MissingPermissions`] if the bot doesn't have the
139    /// required permissions, the wrapped permissions are the permissions
140    /// the bot is missing
141    #[deprecated(note = "use `combined_check_permissions` instead")]
142    pub fn check_permissions(&self, required_permissions: Permissions) -> Result<(), UserError> {
143        let missing_permissions = required_permissions - self.app_permissions;
144        if !missing_permissions.is_empty() {
145            return Err(UserError::MissingPermissions(Some(missing_permissions)));
146        }
147
148        Ok(())
149    }
150
151    /// Handle an error returned in an interaction
152    ///
153    /// The passed reply should be the reply that should be shown to the user
154    /// based on the error
155    ///
156    /// The type parameter `Custom` is used to determine if the error is
157    /// internal, if you don't have a custom error type, you can use
158    /// [`InteractionHandle::handle_error_no_custom`]
159    ///
160    /// - If the given error should be ignored, simply returns early
161    /// - If the given error is internal, logs the error
162    /// - Tries to reply to the interaction with the given reply, if it fails
163    ///   and the error is internal, logs the error, if it succeeds, returns
164    ///   what [`InteractionHandle::reply`] would return
165    #[deprecated(note = "Use `report_error` instead and do the internal error handling yourself")]
166    pub async fn handle_error<Custom: Display + Debug + Send + Sync + 'static>(
167        &self,
168        reply: Reply,
169        error: anyhow::Error,
170    ) -> Option<Message> {
171        if error.ignore() {
172            return None;
173        }
174
175        if let Some(internal_err) = error.internal::<Custom>() {
176            self.bot.log(internal_err).await;
177        }
178
179        match self
180            .reply(reply)
181            .await
182            .map_err(|err| anyhow::Error::new(err).internal::<Custom>())
183        {
184            Ok(message) => message,
185            Err(reply_err) => {
186                if let Some(reply_internal_err) = reply_err {
187                    self.bot.log(reply_internal_err).await;
188                }
189                None
190            }
191        }
192    }
193
194    /// Handle an error without checking for a custom error type
195    ///
196    /// See [`InteractionHandle::handle_error`] for more information
197    #[deprecated(note = "Use `report_error` instead and do the internal error handling yourself")]
198    pub async fn handle_error_no_custom(
199        &self,
200        reply: Reply,
201        error: anyhow::Error,
202    ) -> Option<Message> {
203        self.handle_error::<NoCustomError>(reply, error).await
204    }
205
206    /// Report an error returned in an interaction to the user
207    ///
208    /// The passed reply should be the reply that should be shown to the user
209    /// based on the error
210    ///
211    /// See [`CombinedUserError`] for creating the error parameter
212    ///
213    /// - If the given error should be ignored, simply returns `Ok(None)` early
214    /// - Tries to reply to the interaction with the given reply
215    ///     - If that fails and the error is internal, returns the error
216    ///     - If it succeeds, returns what [`InteractionHandle::reply`] returns
217    #[allow(clippy::missing_errors_doc)]
218    pub async fn report_error<C: Send>(
219        &self,
220        reply: Reply,
221        error: CombinedUserError<C>,
222    ) -> Result<Option<Message>, Error> {
223        if let CombinedUserError::Ignore = error {
224            return Ok(None);
225        }
226
227        match self.reply(reply).await {
228            Ok(message) => Ok(message),
229            Err(Error::Http(err))
230                if matches!(
231                    CombinedUserError::<C>::from_http_err(&err),
232                    CombinedUserError::Internal
233                ) =>
234            {
235                Err(Error::Http(err))
236            }
237            Err(err) => Err(err),
238        }
239    }
240
241    /// Defer the interaction
242    ///
243    /// The `visibility` parameter only affects the first
244    /// [`InteractionHandle::reply`]
245    ///
246    /// # Warning
247    ///
248    /// If responding to a component interaction, use
249    /// [`InteractionHandle::defer_component`] instead
250    ///
251    /// # Errors
252    ///
253    /// Returns [`Error::AlreadyResponded`] if this is not the first
254    /// response to the interaction
255    ///
256    /// Returns [`Error::Http`] if deferring the interaction fails
257    pub async fn defer(&self, visibility: DeferVisibility) -> Result<(), Error> {
258        self.defer_with_behavior(visibility, DeferBehavior::Followup)
259            .await
260    }
261
262    /// Defer a component interaction
263    ///
264    /// The `visibility` parameter only affects the first
265    /// [`InteractionHandle::reply`]
266    ///
267    /// # Errors
268    ///
269    /// Returns [`Error::AlreadyResponded`] if this is not the first
270    /// response to the interaction
271    ///
272    /// Returns [`Error::Http`] if deferring the interaction fails
273    pub async fn defer_component(
274        &self,
275        visibility: DeferVisibility,
276        behavior: DeferBehavior,
277    ) -> Result<(), Error> {
278        self.defer_with_behavior(visibility, behavior).await
279    }
280
281    /// Simply calls `self.defer_with_behavior(DeferVisibility::Visible,
282    /// DeferBehavior::Update)`
283    #[deprecated(note = "use `defer_component` instead")]
284    #[allow(clippy::missing_errors_doc)]
285    pub async fn defer_update_message(&self) -> Result<(), Error> {
286        self.defer_with_behavior(DeferVisibility::Visible, DeferBehavior::Update)
287            .await
288    }
289
290    /// Defer the interaction
291    ///
292    /// The `visibility` parameter only affects the first
293    /// [`InteractionHandle::reply`]
294    ///
295    /// `behavior` parameter only has an effect on component interactions
296    ///
297    /// # Errors
298    ///
299    /// Returns [`Error::AlreadyResponded`] if this is not the first
300    /// response to the interaction
301    ///
302    /// Returns [`Error::Http`] if deferring the interaction fails
303    #[deprecated(note = "use `defer` or `defer_component` instead ")]
304    pub async fn defer_with_behavior(
305        &self,
306        visibility: DeferVisibility,
307        behavior: DeferBehavior,
308    ) -> Result<(), Error> {
309        if self.responded() {
310            return Err(Error::AlreadyResponded);
311        }
312
313        let kind = if self.kind == InteractionType::MessageComponent {
314            match behavior {
315                DeferBehavior::Followup => {
316                    InteractionResponseType::DeferredChannelMessageWithSource
317                }
318                DeferBehavior::Update => InteractionResponseType::DeferredUpdateMessage,
319            }
320        } else {
321            InteractionResponseType::DeferredChannelMessageWithSource
322        };
323
324        let defer_response = InteractionResponse {
325            kind,
326            data: Some(InteractionResponseData {
327                flags: (visibility == DeferVisibility::Ephemeral)
328                    .then_some(MessageFlags::EPHEMERAL),
329                ..Default::default()
330            }),
331        };
332
333        self.bot
334            .interaction_client()
335            .create_response(self.id, &self.token, &defer_response)
336            .await?;
337
338        self.set_responded(true);
339
340        Ok(())
341    }
342
343    /// Reply to this command
344    ///
345    /// If the interaction was already responded to, makes a followup response,
346    /// otherwise responds to the interaction with a message
347    ///
348    /// Discord gives 3 seconds of deadline to respond to an interaction, if the
349    /// reply might take longer, consider using [`InteractionHandle::defer`] or
350    /// [`InteractionHandle::defer_component`] before this method
351    ///
352    /// - If this is the first response sent, returns `None`
353    /// - Unless [`Reply::update_last`] was called, returns `Some`
354    /// - If [`Reply::update_last`] was called and this is the first response,
355    ///   returns `Some`
356    /// - If [`Reply::update_last`] was called but this isn't the first
357    ///   response, returns `None`
358    ///
359    /// # Updating Last Response
360    ///
361    /// You can use [`Reply::update_last`] to update the last response, the
362    /// update overwrites all of the older response, if one doesn't exist, it
363    /// makes a new response
364    ///
365    /// On component interactions, if there is no later response, updates the
366    /// message the component is attached to
367    ///
368    /// # Errors
369    ///
370    /// Returns [`Error::RequestValidation`] if the reply is invalid (Refer to
371    /// [`twilight_http::request::application::interaction::CreateFollowup`])
372    ///
373    /// Returns [`Error::Http`] if creating the followup
374    /// response fails
375    ///
376    /// Returns [`Error::DeserializeBody`] if deserializing the response fails
377    pub async fn reply(&self, reply: Reply) -> Result<Option<Message>, Error> {
378        if self.responded() {
379            let client = self.bot.interaction_client();
380
381            if reply.update_last {
382                if let Some(last_message_id) = self.last_message_id() {
383                    let mut update_followup = client.update_followup(&self.token, last_message_id);
384
385                    if let Some(allowed_mentions) = &reply.allowed_mentions {
386                        update_followup =
387                            update_followup.allowed_mentions(allowed_mentions.as_ref());
388                    }
389                    update_followup
390                        .content((!reply.content.is_empty()).then_some(&reply.content))?
391                        .embeds(Some(&reply.embeds))?
392                        .components(Some(&reply.components))?
393                        .attachments(&reply.attachments)?
394                        .await?;
395
396                    Ok(None)
397                } else {
398                    let mut update_response = client.update_response(&self.token);
399
400                    if let Some(allowed_mentions) = &reply.allowed_mentions {
401                        update_response =
402                            update_response.allowed_mentions(allowed_mentions.as_ref());
403                    }
404
405                    let message = update_response
406                        .content((!reply.content.is_empty()).then_some(&reply.content))?
407                        .embeds(Some(&reply.embeds))?
408                        .components(Some(&reply.components))?
409                        .attachments(&reply.attachments)?
410                        .await?
411                        .model()
412                        .await?;
413
414                    self.set_last_message_id(message.id);
415
416                    Ok(Some(message))
417                }
418            } else {
419                let mut followup = client.create_followup(&self.token);
420
421                if !reply.content.is_empty() {
422                    followup = followup.content(&reply.content)?;
423                }
424                if let Some(allowed_mentions) = &reply.allowed_mentions {
425                    followup = followup.allowed_mentions(allowed_mentions.as_ref());
426                }
427
428                let message = followup
429                    .embeds(&reply.embeds)?
430                    .components(&reply.components)?
431                    .attachments(&reply.attachments)?
432                    .flags(reply.flags)
433                    .tts(reply.tts)
434                    .await?
435                    .model()
436                    .await?;
437
438                self.set_last_message_id(message.id);
439
440                Ok(Some(message))
441            }
442        } else {
443            let kind = if reply.update_last && self.kind == InteractionType::MessageComponent {
444                InteractionResponseType::UpdateMessage
445            } else {
446                InteractionResponseType::ChannelMessageWithSource
447            };
448
449            self.bot
450                .interaction_client()
451                .create_response(
452                    self.id,
453                    &self.token,
454                    &InteractionResponse {
455                        kind,
456                        data: Some(reply.into()),
457                    },
458                )
459                .await?;
460
461            self.set_responded(true);
462
463            Ok(None)
464        }
465    }
466
467    /// Simply calls `self.reply(reply.update_last())`
468    #[deprecated(note = "Use `self.reply(reply.update_last())` instead")]
469    #[allow(clippy::missing_errors_doc)]
470    pub async fn update_message(&self, reply: Reply) -> Result<Option<Message>, Error> {
471        self.reply(reply.update_last()).await
472    }
473
474    /// Respond to this command with autocomplete suggestions
475    ///
476    /// # Errors
477    ///
478    /// Returns [`Error::AlreadyResponded`] if this is not the first
479    /// response to the interaction
480    ///
481    /// Returns [`Error::Http`] if creating the response fails
482    pub async fn autocomplete(&self, choices: Vec<CommandOptionChoice>) -> Result<(), Error> {
483        if self.responded() {
484            return Err(Error::AlreadyResponded);
485        }
486
487        self.bot
488            .interaction_client()
489            .create_response(
490                self.id,
491                &self.token,
492                &InteractionResponse {
493                    kind: InteractionResponseType::ApplicationCommandAutocompleteResult,
494                    data: Some(InteractionResponseData {
495                        choices: Some(choices),
496                        ..Default::default()
497                    }),
498                },
499            )
500            .await?;
501
502        self.set_responded(true);
503
504        Ok(())
505    }
506
507    /// Respond to this command with a modal
508    ///
509    /// # Errors
510    ///
511    /// Returns [`Error::AlreadyResponded`] if this is not the first
512    /// response to the interaction
513    ///
514    /// Returns [`Error::Http`] if creating the response fails
515    pub async fn modal(
516        &self,
517        custom_id: String,
518        title: String,
519        text_inputs: Vec<TextInput>,
520    ) -> Result<(), Error> {
521        let responded = self.responded();
522
523        if responded {
524            return Err(Error::AlreadyResponded);
525        }
526
527        self.bot
528            .interaction_client()
529            .create_response(
530                self.id,
531                &self.token,
532                &InteractionResponse {
533                    kind: InteractionResponseType::Modal,
534                    data: Some(InteractionResponseData {
535                        custom_id: Some(custom_id),
536                        title: Some(title),
537                        components: Some(
538                            text_inputs
539                                .into_iter()
540                                .map(|text_input| {
541                                    Component::ActionRow(ActionRow {
542                                        components: vec![Component::TextInput(text_input)],
543                                    })
544                                })
545                                .collect(),
546                        ),
547                        ..Default::default()
548                    }),
549                },
550            )
551            .await?;
552
553        self.set_responded(true);
554
555        Ok(())
556    }
557
558    fn responded(&self) -> bool {
559        self.responded.load(Ordering::Acquire)
560    }
561
562    fn set_responded(&self, val: bool) {
563        self.responded.store(val, Ordering::Release);
564    }
565
566    fn last_message_id(&self) -> Option<Id<MessageMarker>> {
567        let id = self.last_message_id.load(Ordering::Acquire);
568        if id == 0 {
569            None
570        } else {
571            Some(Id::new(id))
572        }
573    }
574
575    fn set_last_message_id(&self, val: Id<MessageMarker>) {
576        self.last_message_id.store(val.get(), Ordering::Release);
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use std::sync::{
583        atomic::{AtomicBool, Ordering},
584        Arc,
585    };
586
587    #[test]
588    fn atomic_preserved() {
589        let responded = Arc::new(AtomicBool::new(false));
590        let responded_clone = responded.clone();
591
592        responded.store(true, Ordering::Release);
593
594        assert!(responded_clone.load(Ordering::Acquire));
595    }
596}