twilight_http/request/channel/message/
create_message.rs

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