Skip to main content

twilight_http/request/channel/message/
create_message.rs

1#[cfg(not(target_os = "wasi"))]
2use crate::response::{Response, ResponseFuture};
3use crate::{
4    client::Client,
5    error::Error,
6    request::{
7        Nullable, Request, TryIntoRequest,
8        attachment::{AttachmentManager, PartialAttachment},
9    },
10    routing::Route,
11};
12use serde::Serialize;
13use std::future::IntoFuture;
14use twilight_model::{
15    channel::message::{
16        AllowedMentions, Component, Embed, Message, MessageFlags, MessageReference,
17        MessageReferenceType,
18    },
19    http::attachment::Attachment,
20    id::{
21        Id,
22        marker::{ChannelMarker, MessageMarker, StickerMarker},
23    },
24    poll::Poll,
25};
26use twilight_validate::message::{
27    MessageValidationError, attachment as validate_attachment, components as validate_components,
28    content as validate_content, embeds as validate_embeds, sticker_ids as validate_sticker_ids,
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 const 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 const 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 const 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 const 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 const 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 const 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
393#[cfg(not(target_os = "wasi"))]
394impl IntoFuture for CreateMessage<'_> {
395    type Output = Result<Response<Message>, Error>;
396
397    type IntoFuture = ResponseFuture<Message>;
398
399    fn into_future(self) -> Self::IntoFuture {
400        let http = self.http;
401
402        match self.try_into_request() {
403            Ok(request) => http.request(request),
404            Err(source) => ResponseFuture::error(source),
405        }
406    }
407}
408
409impl TryIntoRequest for CreateMessage<'_> {
410    fn try_into_request(self) -> Result<Request, Error> {
411        let mut fields = self.fields.map_err(Error::validation)?;
412        let mut request = Request::builder(&Route::CreateMessage {
413            channel_id: self.channel_id.get(),
414        });
415
416        // Set the default allowed mentions if required.
417        if fields.allowed_mentions.is_none()
418            && let Some(allowed_mentions) = self.http.default_allowed_mentions()
419        {
420            fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
421        }
422
423        // Determine whether we need to use a multipart/form-data body or a JSON
424        // body.
425        if !self.attachment_manager.is_empty() {
426            let form = if let Some(payload_json) = fields.payload_json {
427                self.attachment_manager.build_form(payload_json)
428            } else {
429                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
430
431                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
432
433                self.attachment_manager.build_form(fields.as_ref())
434            };
435
436            request = request.form(form);
437        } else if let Some(payload_json) = fields.payload_json {
438            request = request.body(payload_json.to_vec());
439        } else {
440            request = request.json(&fields);
441        }
442
443        request.build()
444    }
445}