sparkle_convenience/
reply.rs

1use twilight_http::{response::marker::EmptyBody, Response};
2use twilight_model::{
3    channel::{
4        message::{AllowedMentions, Component, Embed, MessageFlags},
5        Message,
6    },
7    http::{attachment::Attachment, interaction::InteractionResponseData},
8    id::{
9        marker::{ChannelMarker, MessageMarker, StickerMarker, UserMarker, WebhookMarker},
10        Id,
11    },
12};
13
14use crate::error::Error;
15
16/// Defines what to do when the reference message doesn't exist
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum MissingMessageReferenceHandleMethod {
19    /// Return an error
20    Fail,
21    /// Ignore and don't set a reference
22    Ignore,
23}
24
25/// The response of an executed webhook
26#[derive(Debug)]
27pub enum ExecuteWebhookResponse {
28    /// The response returns nothing
29    EmptyBody(Response<EmptyBody>),
30    /// The response returns a message
31    Message(Response<Message>),
32}
33
34impl ExecuteWebhookResponse {
35    /// Return the wrapped response if this is a
36    /// [`ExecuteWebhookResponse::Message`], `None` otherwise
37    #[allow(clippy::missing_const_for_fn)]
38    pub fn message(self) -> Option<Response<Message>> {
39        if let Self::Message(response) = self {
40            Some(response)
41        } else {
42            None
43        }
44    }
45}
46
47/// The message to reply with, combining similar data in messages, interactions
48/// and webhooks
49///
50/// - Used in interactions with [`InteractionHandle::reply`]
51/// - Used to create or edit messages with [`Reply::create_message`] and
52///   [`Reply::update_message`]
53/// - Used to send or edit DM messages with [`Reply::create_private_message`]
54///   and [`Reply::update_private_message`]
55/// - Used to execute webhooks with [`Reply::execute_webhook`]
56///
57/// [`InteractionHandle::reply`]: crate::interaction::InteractionHandle::reply
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct Reply {
60    /// The content of the reply
61    pub content: String,
62    /// The embeds of the reply
63    pub embeds: Vec<Embed>,
64    /// The components of the reply
65    pub components: Vec<Component>,
66    /// The attachments of the reply
67    pub attachments: Vec<Attachment>,
68    /// The flags of the reply
69    pub flags: MessageFlags,
70    /// The allowed mentions of the reply
71    ///
72    /// Use `None` to use the bot's default allowed mentions and `Some(None)` to
73    /// override this default
74    #[allow(clippy::option_option)]
75    pub allowed_mentions: Option<Option<AllowedMentions>>,
76    /// Whether the reply should be TTS
77    pub tts: bool,
78    /// See [`Reply::update_last`]
79    pub update_last: bool,
80    /// See [`Reply::sticker`]
81    pub sticker_ids: Vec<Id<StickerMarker>>,
82    /// See [`Reply::message_reference`]
83    pub message_reference: Option<Id<MessageMarker>>,
84    /// See [`Reply::message_reference`]
85    pub missing_message_reference_handle_method: MissingMessageReferenceHandleMethod,
86    /// See [`Reply::nonce`]
87    pub nonce: Option<u64>,
88    /// See [`Reply::username`]
89    pub username: Option<String>,
90    /// See [`Reply::avatar_url`]
91    pub avatar_url: Option<String>,
92    /// See [`Reply::thread_id`]
93    pub thread_id: Option<Id<ChannelMarker>>,
94    /// See [`Reply::thread_name`]
95    pub thread_name: Option<String>,
96    /// See [`Reply::wait`]
97    pub wait: bool,
98}
99
100impl Default for Reply {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl From<Reply> for InteractionResponseData {
107    fn from(reply: Reply) -> Self {
108        Self {
109            content: Some(reply.content),
110            embeds: Some(reply.embeds),
111            components: Some(reply.components),
112            attachments: Some(reply.attachments),
113            flags: Some(reply.flags),
114            tts: Some(reply.tts),
115            allowed_mentions: reply.allowed_mentions.flatten(),
116            choices: None,
117            custom_id: None,
118            title: None,
119        }
120    }
121}
122
123impl Reply {
124    /// Create a new, empty [`Reply`]
125    ///
126    /// At least one of [`Reply::content`], [`Reply::embed`],
127    /// [`Reply::component`], [`Reply::attachment`] must be called
128    ///
129    /// By default, the message is not ephemeral or TTS and its allowed mentions
130    /// use the bot's default allowed mentions
131    #[must_use]
132    pub const fn new() -> Self {
133        Self {
134            content: String::new(),
135            embeds: vec![],
136            components: vec![],
137            attachments: vec![],
138            flags: MessageFlags::empty(),
139            allowed_mentions: None,
140            tts: false,
141            update_last: false,
142            sticker_ids: vec![],
143            message_reference: None,
144            nonce: None,
145            missing_message_reference_handle_method: MissingMessageReferenceHandleMethod::Fail,
146            username: None,
147            avatar_url: None,
148            thread_id: None,
149            thread_name: None,
150            wait: false,
151        }
152    }
153
154    /// Set the content of the reply
155    ///
156    /// This overwrites the previous content
157    #[must_use]
158    pub fn content(mut self, content: impl Into<String>) -> Self {
159        self.content = content.into();
160        self
161    }
162
163    /// Add an embed to the reply
164    #[must_use]
165    pub fn embed(mut self, embed: Embed) -> Self {
166        self.embeds.push(embed);
167        self
168    }
169
170    /// Add a component to the reply
171    #[must_use]
172    pub fn component(mut self, component: Component) -> Self {
173        self.components.push(component);
174        self
175    }
176
177    /// Add an attachment to the reply
178    #[must_use]
179    pub fn attachment(mut self, attachment: Attachment) -> Self {
180        self.attachments.push(attachment);
181        self
182    }
183
184    /// Set the flags of the reply
185    ///
186    /// # Warning
187    ///
188    /// Overwrites [`Reply::ephemeral`]
189    #[must_use]
190    pub const fn flags(mut self, flags: MessageFlags) -> Self {
191        self.flags = flags;
192        self
193    }
194
195    /// Set the allowed mentions of the reply
196    ///
197    /// Pass `None` to ignore the bot's default allowed mentions
198    #[must_use]
199    #[allow(clippy::missing_const_for_fn)]
200    pub fn allowed_mentions(mut self, allowed_mentions: Option<AllowedMentions>) -> Self {
201        self.allowed_mentions = Some(allowed_mentions);
202        self
203    }
204
205    /// Make the reply TTS
206    #[must_use]
207    pub const fn tts(mut self) -> Self {
208        self.tts = true;
209        self
210    }
211
212    /// Make the reply update the last reply if one exists
213    ///
214    /// Currently only available in [`InteractionHandle`]
215    ///
216    /// [`InteractionHandle`]: crate::interaction::InteractionHandle
217    #[must_use]
218    pub const fn update_last(mut self) -> Self {
219        self.update_last = true;
220        self
221    }
222
223    /// Make the reply ephemeral
224    ///
225    /// Only used in interactions
226    #[must_use]
227    pub const fn ephemeral(mut self) -> Self {
228        self.flags = self.flags.union(MessageFlags::EPHEMERAL);
229        self
230    }
231
232    /// Add a sticker to the reply
233    ///
234    /// Only used when creating messages
235    #[must_use]
236    pub fn sticker(mut self, sticker_id: Id<StickerMarker>) -> Self {
237        self.sticker_ids.push(sticker_id);
238        self
239    }
240
241    /// Set the message reference of the reply, this is what's done in the
242    /// Discord client using the `Reply` button
243    ///
244    /// Only used when creating messages
245    #[must_use]
246    pub const fn message_reference(
247        mut self,
248        message_id: Id<MessageMarker>,
249        missing_handle_method: MissingMessageReferenceHandleMethod,
250    ) -> Self {
251        self.message_reference = Some(message_id);
252        self.missing_message_reference_handle_method = missing_handle_method;
253        self
254    }
255
256    /// Attach a nonce to the reply
257    ///
258    /// Only used when creating messages
259    #[must_use]
260    pub const fn nonce(mut self, nonce: u64) -> Self {
261        self.nonce = Some(nonce);
262        self
263    }
264
265    /// Set the username of the reply
266    ///
267    /// Only used when executing webhooks
268    #[must_use]
269    #[allow(clippy::missing_const_for_fn)]
270    pub fn username(mut self, username: impl Into<String>) -> Self {
271        self.username = Some(username.into());
272        self
273    }
274
275    /// Set the avatar URL of the reply
276    ///
277    /// Only used when executing webhooks
278    #[must_use]
279    #[allow(clippy::missing_const_for_fn)]
280    pub fn avatar_url(mut self, avatar_url: impl Into<String>) -> Self {
281        self.avatar_url = Some(avatar_url.into());
282        self
283    }
284
285    /// Set the thread ID of the reply
286    ///
287    /// Only used when executing webhooks and updating webhook messages
288    #[must_use]
289    pub const fn thread_id(mut self, thread_id: Id<ChannelMarker>) -> Self {
290        self.thread_id = Some(thread_id);
291        self
292    }
293
294    /// Set the name of the thread created when using the reply in a forum
295    /// channel
296    ///
297    /// Only used when executing webhooks
298    #[must_use]
299    #[allow(clippy::missing_const_for_fn)]
300    pub fn thread_name(mut self, thread_name: impl Into<String>) -> Self {
301        self.thread_name = Some(thread_name.into());
302        self
303    }
304
305    /// Wait for the message to be sent
306    ///
307    /// Only used when executing webhooks
308    #[must_use]
309    pub const fn wait(mut self) -> Self {
310        self.wait = true;
311        self
312    }
313
314    /// Create a message using this reply
315    ///
316    /// # Errors
317    ///
318    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
319    /// [`twilight_http::request::channel::message::create_message::CreateMessage`])
320    ///
321    /// Returns [`Error::Http`] if creating the message fails
322    pub async fn create_message(
323        &self,
324        http: &twilight_http::Client,
325        channel_id: Id<ChannelMarker>,
326    ) -> Result<Response<Message>, Error> {
327        let mut create_message = http.create_message(channel_id);
328
329        if let Some(message_reference) = self.message_reference {
330            create_message = create_message.reply(message_reference);
331        }
332        if let Some(allowed_mentions) = self.allowed_mentions.as_ref() {
333            create_message = create_message.allowed_mentions(allowed_mentions.as_ref());
334        }
335        if let Some(nonce) = self.nonce {
336            create_message = create_message.nonce(nonce);
337        }
338
339        Ok(create_message
340            .content(&self.content)?
341            .embeds(&self.embeds)?
342            .components(&self.components)?
343            .attachments(&self.attachments)?
344            .sticker_ids(&self.sticker_ids)?
345            .flags(self.flags)
346            .tts(self.tts)
347            .fail_if_not_exists(
348                self.missing_message_reference_handle_method
349                    == MissingMessageReferenceHandleMethod::Fail,
350            )
351            .await?)
352    }
353
354    /// Edit a message using this reply
355    ///
356    /// Overwrites all of the older message
357    ///
358    /// # Errors
359    ///
360    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
361    /// [`twilight_http::request::channel::message::update_message::UpdateMessage`])
362    ///
363    /// Returns [`Error::Http`] if updating the message fails
364    pub async fn update_message(
365        &self,
366        http: &twilight_http::Client,
367        channel_id: Id<ChannelMarker>,
368        message_id: Id<MessageMarker>,
369    ) -> Result<Response<Message>, Error> {
370        let mut update_message = http.update_message(channel_id, message_id);
371
372        if let Some(allowed_mentions) = self.allowed_mentions.as_ref() {
373            update_message = update_message.allowed_mentions(allowed_mentions.as_ref());
374        }
375
376        Ok(update_message
377            .content(Some(&self.content))?
378            .embeds(Some(&self.embeds))?
379            .components(Some(&self.components))?
380            .attachments(&self.attachments)?
381            .flags(self.flags)
382            .await?)
383    }
384
385    /// Send a DM message to a user using this reply
386    ///
387    /// # Errors
388    ///
389    /// Returns [`Error::Http`] if creating or getting the private channel, or
390    /// creating the message fails
391    ///
392    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
393    /// [`twilight_http::request::channel::message::create_message::CreateMessage`])
394    pub async fn create_private_message(
395        &self,
396        http: &twilight_http::Client,
397        user_id: Id<UserMarker>,
398    ) -> Result<Response<Message>, Error> {
399        let channel_id = http
400            .create_private_channel(user_id)
401            .await?
402            .model()
403            .await?
404            .id;
405
406        self.create_message(http, channel_id).await
407    }
408
409    /// Edit a DM message using this reply
410    ///
411    /// Overwrites all of the older message
412    ///
413    /// # Errors
414    ///
415    /// Returns [`Error::Http`] if creating or getting the private channel, or
416    /// updating the message fails
417    ///
418    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
419    /// [`twilight_http::request::channel::message::update_message::UpdateMessage`])
420    pub async fn update_private_message(
421        &self,
422        http: &twilight_http::Client,
423        user_id: Id<UserMarker>,
424        message_id: Id<MessageMarker>,
425    ) -> Result<Response<Message>, Error> {
426        let channel_id = http
427            .create_private_channel(user_id)
428            .await?
429            .model()
430            .await?
431            .id;
432
433        self.update_message(http, channel_id, message_id).await
434    }
435
436    /// Execute a webhook using this reply
437    ///
438    /// If [`Reply::wait`] was called, returns
439    /// [`ExecuteWebhookResponse::Message`], otherwise returns
440    /// [`ExecuteWebhookResponse::EmptyBody`]
441    ///
442    /// # Errors
443    ///
444    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
445    /// [`twilight_http::request::channel::webhook::execute_webhook::ExecuteWebhook`])
446    ///
447    /// Returns [`Error::Http`] if executing the webhook fails
448    pub async fn execute_webhook(
449        &self,
450        http: &twilight_http::Client,
451        webhook_id: Id<WebhookMarker>,
452        token: &str,
453    ) -> Result<ExecuteWebhookResponse, Error> {
454        let mut execute_webhook = http.execute_webhook(webhook_id, token);
455
456        if let Some(username) = self.username.as_ref() {
457            execute_webhook = execute_webhook.username(username)?;
458        }
459        if let Some(avatar_url) = self.avatar_url.as_ref() {
460            execute_webhook = execute_webhook.avatar_url(avatar_url);
461        }
462        if let Some(thread_id) = self.thread_id {
463            execute_webhook = execute_webhook.thread_id(thread_id);
464        }
465        if let Some(thread_name) = self.thread_name.as_ref() {
466            execute_webhook = execute_webhook.thread_name(thread_name);
467        }
468        if let Some(allowed_mentions) = self.allowed_mentions.as_ref() {
469            execute_webhook = execute_webhook.allowed_mentions(allowed_mentions.as_ref());
470        }
471
472        execute_webhook = execute_webhook
473            .content(&self.content)?
474            .embeds(&self.embeds)?
475            .components(&self.components)?
476            .attachments(&self.attachments)?
477            .flags(self.flags)
478            .tts(self.tts);
479
480        if self.wait {
481            Ok(ExecuteWebhookResponse::Message(
482                execute_webhook.wait().await?,
483            ))
484        } else {
485            Ok(ExecuteWebhookResponse::EmptyBody(execute_webhook.await?))
486        }
487    }
488
489    /// Update a webhook message using this reply
490    ///
491    /// Overwrites all of the older message
492    ///
493    /// # Errors
494    ///
495    /// Returns [`Error::MessageValidation`] if the reply is invalid (Refer to
496    /// [`twilight_http::request::channel::webhook::update_webhook_message::UpdateWebhookMessage`])
497    ///
498    /// Returns [`Error::Http`] if updating the webhook message fails
499    pub async fn update_webhook_message(
500        &self,
501        http: &twilight_http::Client,
502        webhook_id: Id<WebhookMarker>,
503        token: &str,
504        message_id: Id<MessageMarker>,
505    ) -> Result<Response<Message>, Error> {
506        let mut update_webhook_message = http.update_webhook_message(webhook_id, token, message_id);
507
508        if let Some(thread_id) = self.thread_id {
509            update_webhook_message = update_webhook_message.thread_id(thread_id);
510        }
511        if let Some(allowed_mentions) = self.allowed_mentions.as_ref() {
512            update_webhook_message =
513                update_webhook_message.allowed_mentions(allowed_mentions.as_ref());
514        }
515
516        Ok(update_webhook_message
517            .content(Some(&self.content))?
518            .embeds(Some(&self.embeds))?
519            .components(Some(&self.components))?
520            .attachments(&self.attachments)?
521            .await?)
522    }
523}