twilight_http/request/channel/message/
create_message.rs

1use crate::{
2    client::Client,
3    error::Error,
4    request::{
5        attachment::{AttachmentManager, PartialAttachment},
6        Nullable, Request, TryIntoRequest,
7    },
8    response::{Response, ResponseFuture},
9    routing::Route,
10};
11use serde::Serialize;
12use std::future::IntoFuture;
13use twilight_model::{
14    channel::message::{
15        AllowedMentions, Component, Embed, Message, MessageFlags, MessageReference,
16        MessageReferenceType,
17    },
18    http::attachment::Attachment,
19    id::{
20        marker::{ChannelMarker, MessageMarker, StickerMarker},
21        Id,
22    },
23    poll::Poll,
24};
25use twilight_validate::message::{
26    attachment as validate_attachment, components as validate_components,
27    content as validate_content, embeds as validate_embeds, sticker_ids as validate_sticker_ids,
28    MessageValidationError,
29};
30
31#[derive(Serialize)]
32pub(crate) struct CreateMessageFields<'a> {
33    #[serde(skip_serializing_if = "Option::is_none")]
34    allowed_mentions: Option<Nullable<&'a AllowedMentions>>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    attachments: Option<Vec<PartialAttachment<'a>>>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    components: Option<&'a [Component]>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    content: Option<&'a str>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    embeds: Option<&'a [Embed]>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    flags: Option<MessageFlags>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    message_reference: Option<MessageReference>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    nonce: Option<u64>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    payload_json: Option<&'a [u8]>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    poll: Option<&'a Poll>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    sticker_ids: Option<&'a [Id<StickerMarker>]>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    tts: Option<bool>,
57}
58
59/// Send a message to a channel.
60///
61/// The message must include at least one of [`attachments`], [`content`],
62/// [`components`], [`embeds`], or [`sticker_ids`].
63///
64/// # Example
65///
66/// ```no_run
67/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
68/// use twilight_http::Client;
69/// use twilight_model::id::Id;
70///
71/// let client = Client::new("my token".to_owned());
72///
73/// let channel_id = Id::new(123);
74/// let message = client
75///     .create_message(channel_id)
76///     .content("Twilight is best pony")
77///     .tts(true)
78///     .await?;
79/// # Ok(()) }
80/// ```
81///
82/// [`attachments`]: Self::attachments
83/// [`content`]: Self::content
84/// [`components`]: Self::components
85/// [`embeds`]: Self::embeds
86/// [`sticker_ids`]: Self::sticker_ids
87#[must_use = "requests must be configured and executed"]
88pub struct CreateMessage<'a> {
89    attachment_manager: AttachmentManager<'a>,
90    channel_id: Id<ChannelMarker>,
91    fields: Result<CreateMessageFields<'a>, MessageValidationError>,
92    http: &'a Client,
93}
94
95impl<'a> CreateMessage<'a> {
96    pub(crate) const fn new(http: &'a Client, channel_id: Id<ChannelMarker>) -> Self {
97        Self {
98            attachment_manager: AttachmentManager::new(),
99            channel_id,
100            fields: Ok(CreateMessageFields {
101                attachments: None,
102                components: None,
103                content: None,
104                embeds: None,
105                flags: None,
106                message_reference: None,
107                nonce: None,
108                payload_json: None,
109                poll: None,
110                allowed_mentions: None,
111                sticker_ids: None,
112                tts: None,
113            }),
114            http,
115        }
116    }
117
118    /// Specify the [`AllowedMentions`] for the message.
119    ///
120    /// Unless otherwise called, the request will use the client's default
121    /// allowed mentions. Set to `None` to ignore this default.
122    pub fn allowed_mentions(mut self, allowed_mentions: Option<&'a AllowedMentions>) -> Self {
123        if let Ok(fields) = self.fields.as_mut() {
124            fields.allowed_mentions = Some(Nullable(allowed_mentions));
125        }
126
127        self
128    }
129
130    /// Attach multiple files to the message.
131    ///
132    /// Calling this method will clear previous calls.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error of type [`AttachmentDescriptionTooLarge`] if
137    /// the attachments's description is too large.
138    ///
139    /// Returns an error of type [`AttachmentFilename`] if any filename is
140    /// invalid.
141    ///
142    /// [`AttachmentDescriptionTooLarge`]: twilight_validate::message::MessageValidationErrorType::AttachmentDescriptionTooLarge
143    /// [`AttachmentFilename`]: twilight_validate::message::MessageValidationErrorType::AttachmentFilename
144    pub fn attachments(mut self, attachments: &'a [Attachment]) -> Self {
145        if self.fields.is_ok() {
146            if let Err(source) = attachments.iter().try_for_each(validate_attachment) {
147                self.fields = Err(source);
148            } else {
149                self.attachment_manager = self
150                    .attachment_manager
151                    .set_files(attachments.iter().collect());
152            }
153        }
154
155        self
156    }
157
158    /// Set the message's list of [`Component`]s.
159    ///
160    /// Calling this method will clear previous calls.
161    ///
162    /// # Errors
163    ///
164    /// Refer to the errors section of
165    /// [`twilight_validate::component::component`] for a list of errors that
166    /// may be returned as a result of validating each provided component.
167    pub fn components(mut self, components: &'a [Component]) -> Self {
168        self.fields = self.fields.and_then(|mut fields| {
169            validate_components(
170                components,
171                fields
172                    .flags
173                    .is_some_and(|flags| flags.contains(MessageFlags::IS_COMPONENTS_V2)),
174            )?;
175            fields.components = Some(components);
176
177            Ok(fields)
178        });
179
180        self
181    }
182
183    /// Set the message's content.
184    ///
185    /// The maximum length is 2000 UTF-16 characters.
186    ///
187    /// # Errors
188    ///
189    /// Returns an error of type [`ContentInvalid`] if the content length is too
190    /// long.
191    ///
192    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
193    pub fn content(mut self, content: &'a str) -> Self {
194        self.fields = self.fields.and_then(|mut fields| {
195            validate_content(content)?;
196            fields.content.replace(content);
197
198            Ok(fields)
199        });
200
201        self
202    }
203
204    /// Set the message's list of embeds.
205    ///
206    /// Calling this method will clear previous calls.
207    ///
208    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
209    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
210    /// characters. Additionally, the internal fields also have character
211    /// limits. See [Discord Docs/Embed Limits].
212    ///
213    /// # Errors
214    ///
215    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
216    ///
217    /// Otherwise, refer to the errors section of
218    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
219    ///
220    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
221    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
222    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
223    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
224    pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
225        self.fields = self.fields.and_then(|mut fields| {
226            validate_embeds(embeds)?;
227            fields.embeds = Some(embeds);
228
229            Ok(fields)
230        });
231
232        self
233    }
234
235    /// Specify if this message is a poll.
236    pub fn poll(mut self, poll: &'a Poll) -> Self {
237        if let Ok(fields) = self.fields.as_mut() {
238            fields.poll = Some(poll);
239        }
240
241        self
242    }
243
244    /// Whether to fail sending if the reply no longer exists.
245    ///
246    /// Defaults to [`true`].
247    pub fn fail_if_not_exists(mut self, fail_if_not_exists: bool) -> Self {
248        if let Ok(fields) = self.fields.as_mut() {
249            if let Some(reference) = fields.message_reference.as_mut() {
250                reference.fail_if_not_exists = Some(fail_if_not_exists);
251            } else {
252                fields.message_reference = Some(MessageReference {
253                    kind: MessageReferenceType::default(),
254                    channel_id: None,
255                    guild_id: None,
256                    message_id: None,
257                    fail_if_not_exists: Some(fail_if_not_exists),
258                });
259            }
260        }
261
262        self
263    }
264
265    /// Set the message's flags.
266    ///
267    /// The only supported flags are [`SUPPRESS_EMBEDS`] and
268    /// [`SUPPRESS_NOTIFICATIONS`].
269    ///
270    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
271    /// [`SUPPRESS_NOTIFICATIONS`]: MessageFlags::SUPPRESS_NOTIFICATIONS
272    pub fn flags(mut self, flags: MessageFlags) -> Self {
273        if let Ok(fields) = self.fields.as_mut() {
274            fields.flags = Some(flags);
275        }
276
277        self
278    }
279
280    /// Attach a nonce to the message, for optimistic message sending.
281    pub fn nonce(mut self, nonce: u64) -> Self {
282        if let Ok(fields) = self.fields.as_mut() {
283            fields.nonce = Some(nonce);
284        }
285
286        self
287    }
288
289    /// JSON encoded body of any additional request fields.
290    ///
291    /// If this method is called, all other fields are ignored, except for
292    /// [`attachments`]. See [Discord Docs/Uploading Files].
293    ///
294    /// # Examples
295    ///
296    /// See [`ExecuteWebhook::payload_json`] for examples.
297    ///
298    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
299    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
300    /// [`attachments`]: Self::attachments
301    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
302        if let Ok(fields) = self.fields.as_mut() {
303            fields.payload_json = Some(payload_json);
304        }
305
306        self
307    }
308
309    /// Specify the ID of another message to create a reply to.
310    pub fn reply(mut self, other: Id<MessageMarker>) -> Self {
311        self.fields = self.fields.map(|mut fields| {
312            let channel_id = self.channel_id;
313
314            let reference = if let Some(reference) = fields.message_reference {
315                MessageReference {
316                    channel_id: Some(channel_id),
317                    message_id: Some(other),
318                    ..reference
319                }
320            } else {
321                MessageReference {
322                    kind: MessageReferenceType::Default,
323                    channel_id: Some(channel_id),
324                    guild_id: None,
325                    message_id: Some(other),
326                    fail_if_not_exists: None,
327                }
328            };
329
330            fields.message_reference = Some(reference);
331
332            fields
333        });
334
335        self
336    }
337
338    /// Specify the channel and message IDs of another message to forward.
339    pub fn forward(mut self, channel_id: Id<ChannelMarker>, message_id: Id<MessageMarker>) -> Self {
340        self.fields = self.fields.map(|mut fields| {
341            let reference = if let Some(reference) = fields.message_reference {
342                MessageReference {
343                    channel_id: Some(channel_id),
344                    message_id: Some(message_id),
345                    ..reference
346                }
347            } else {
348                MessageReference {
349                    kind: MessageReferenceType::Forward,
350                    channel_id: Some(channel_id),
351                    guild_id: None,
352                    message_id: Some(message_id),
353                    fail_if_not_exists: None,
354                }
355            };
356
357            fields.message_reference = Some(reference);
358
359            fields
360        });
361
362        self
363    }
364
365    /// Set the IDs of up to 3 guild stickers.
366    ///
367    /// # Errors
368    ///
369    /// Returns an error of type [`StickersInvalid`] if the length is invalid.
370    ///
371    /// [`StickersInvalid`]: twilight_validate::message::MessageValidationErrorType::StickersInvalid
372    pub fn sticker_ids(mut self, sticker_ids: &'a [Id<StickerMarker>]) -> Self {
373        self.fields = self.fields.and_then(|mut fields| {
374            validate_sticker_ids(sticker_ids)?;
375            fields.sticker_ids = Some(sticker_ids);
376
377            Ok(fields)
378        });
379
380        self
381    }
382
383    /// Specify true if the message is TTS.
384    pub fn tts(mut self, tts: bool) -> Self {
385        if let Ok(fields) = self.fields.as_mut() {
386            fields.tts = Some(tts);
387        }
388
389        self
390    }
391}
392
393impl IntoFuture for CreateMessage<'_> {
394    type Output = Result<Response<Message>, Error>;
395
396    type IntoFuture = ResponseFuture<Message>;
397
398    fn into_future(self) -> Self::IntoFuture {
399        let http = self.http;
400
401        match self.try_into_request() {
402            Ok(request) => http.request(request),
403            Err(source) => ResponseFuture::error(source),
404        }
405    }
406}
407
408impl TryIntoRequest for CreateMessage<'_> {
409    fn try_into_request(self) -> Result<Request, Error> {
410        let mut fields = self.fields.map_err(Error::validation)?;
411        let mut request = Request::builder(&Route::CreateMessage {
412            channel_id: self.channel_id.get(),
413        });
414
415        // Set the default allowed mentions if required.
416        if fields.allowed_mentions.is_none() {
417            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
418                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
419            }
420        }
421
422        // Determine whether we need to use a multipart/form-data body or a JSON
423        // body.
424        if !self.attachment_manager.is_empty() {
425            let form = if let Some(payload_json) = fields.payload_json {
426                self.attachment_manager.build_form(payload_json)
427            } else {
428                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
429
430                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
431
432                self.attachment_manager.build_form(fields.as_ref())
433            };
434
435            request = request.form(form);
436        } else if let Some(payload_json) = fields.payload_json {
437            request = request.body(payload_json.to_vec());
438        } else {
439            request = request.json(&fields);
440        }
441
442        request.build()
443    }
444}