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(components)?;
170            fields.components = Some(components);
171
172            Ok(fields)
173        });
174
175        self
176    }
177
178    /// Set the message's content.
179    ///
180    /// The maximum length is 2000 UTF-16 characters.
181    ///
182    /// # Errors
183    ///
184    /// Returns an error of type [`ContentInvalid`] if the content length is too
185    /// long.
186    ///
187    /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
188    pub fn content(mut self, content: &'a str) -> Self {
189        self.fields = self.fields.and_then(|mut fields| {
190            validate_content(content)?;
191            fields.content.replace(content);
192
193            Ok(fields)
194        });
195
196        self
197    }
198
199    /// Set the message's list of embeds.
200    ///
201    /// Calling this method will clear previous calls.
202    ///
203    /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
204    /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
205    /// characters. Additionally, the internal fields also have character
206    /// limits. See [Discord Docs/Embed Limits].
207    ///
208    /// # Errors
209    ///
210    /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
211    ///
212    /// Otherwise, refer to the errors section of
213    /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
214    ///
215    /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
216    /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
217    /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
218    /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
219    pub fn embeds(mut self, embeds: &'a [Embed]) -> Self {
220        self.fields = self.fields.and_then(|mut fields| {
221            validate_embeds(embeds)?;
222            fields.embeds = Some(embeds);
223
224            Ok(fields)
225        });
226
227        self
228    }
229
230    /// Specify if this message is a poll.
231    pub fn poll(mut self, poll: &'a Poll) -> Self {
232        if let Ok(fields) = self.fields.as_mut() {
233            fields.poll = Some(poll);
234        }
235
236        self
237    }
238
239    /// Whether to fail sending if the reply no longer exists.
240    ///
241    /// Defaults to [`true`].
242    pub fn fail_if_not_exists(mut self, fail_if_not_exists: bool) -> Self {
243        if let Ok(fields) = self.fields.as_mut() {
244            if let Some(reference) = fields.message_reference.as_mut() {
245                reference.fail_if_not_exists = Some(fail_if_not_exists);
246            } else {
247                fields.message_reference = Some(MessageReference {
248                    kind: MessageReferenceType::default(),
249                    channel_id: None,
250                    guild_id: None,
251                    message_id: None,
252                    fail_if_not_exists: Some(fail_if_not_exists),
253                });
254            }
255        }
256
257        self
258    }
259
260    /// Set the message's flags.
261    ///
262    /// The only supported flags are [`SUPPRESS_EMBEDS`] and
263    /// [`SUPPRESS_NOTIFICATIONS`].
264    ///
265    /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
266    /// [`SUPPRESS_NOTIFICATIONS`]: MessageFlags::SUPPRESS_NOTIFICATIONS
267    pub fn flags(mut self, flags: MessageFlags) -> Self {
268        if let Ok(fields) = self.fields.as_mut() {
269            fields.flags = Some(flags);
270        }
271
272        self
273    }
274
275    /// Attach a nonce to the message, for optimistic message sending.
276    pub fn nonce(mut self, nonce: u64) -> Self {
277        if let Ok(fields) = self.fields.as_mut() {
278            fields.nonce = Some(nonce);
279        }
280
281        self
282    }
283
284    /// JSON encoded body of any additional request fields.
285    ///
286    /// If this method is called, all other fields are ignored, except for
287    /// [`attachments`]. See [Discord Docs/Uploading Files].
288    ///
289    /// # Examples
290    ///
291    /// See [`ExecuteWebhook::payload_json`] for examples.
292    ///
293    /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
294    /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
295    /// [`attachments`]: Self::attachments
296    pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
297        if let Ok(fields) = self.fields.as_mut() {
298            fields.payload_json = Some(payload_json);
299        }
300
301        self
302    }
303
304    /// Specify the ID of another message to create a reply to.
305    pub fn reply(mut self, other: Id<MessageMarker>) -> Self {
306        self.fields = self.fields.map(|mut fields| {
307            let channel_id = self.channel_id;
308
309            let reference = if let Some(reference) = fields.message_reference {
310                MessageReference {
311                    channel_id: Some(channel_id),
312                    message_id: Some(other),
313                    ..reference
314                }
315            } else {
316                MessageReference {
317                    kind: MessageReferenceType::Default,
318                    channel_id: Some(channel_id),
319                    guild_id: None,
320                    message_id: Some(other),
321                    fail_if_not_exists: None,
322                }
323            };
324
325            fields.message_reference = Some(reference);
326
327            fields
328        });
329
330        self
331    }
332
333    /// Specify the channel and message IDs of another message to forward.
334    pub fn forward(mut self, channel_id: Id<ChannelMarker>, message_id: Id<MessageMarker>) -> Self {
335        self.fields = self.fields.map(|mut fields| {
336            let reference = if let Some(reference) = fields.message_reference {
337                MessageReference {
338                    channel_id: Some(channel_id),
339                    message_id: Some(message_id),
340                    ..reference
341                }
342            } else {
343                MessageReference {
344                    kind: MessageReferenceType::Forward,
345                    channel_id: Some(channel_id),
346                    guild_id: None,
347                    message_id: Some(message_id),
348                    fail_if_not_exists: None,
349                }
350            };
351
352            fields.message_reference = Some(reference);
353
354            fields
355        });
356
357        self
358    }
359
360    /// Set the IDs of up to 3 guild stickers.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error of type [`StickersInvalid`] if the length is invalid.
365    ///
366    /// [`StickersInvalid`]: twilight_validate::message::MessageValidationErrorType::StickersInvalid
367    pub fn sticker_ids(mut self, sticker_ids: &'a [Id<StickerMarker>]) -> Self {
368        self.fields = self.fields.and_then(|mut fields| {
369            validate_sticker_ids(sticker_ids)?;
370            fields.sticker_ids = Some(sticker_ids);
371
372            Ok(fields)
373        });
374
375        self
376    }
377
378    /// Specify true if the message is TTS.
379    pub fn tts(mut self, tts: bool) -> Self {
380        if let Ok(fields) = self.fields.as_mut() {
381            fields.tts = Some(tts);
382        }
383
384        self
385    }
386}
387
388impl IntoFuture for CreateMessage<'_> {
389    type Output = Result<Response<Message>, Error>;
390
391    type IntoFuture = ResponseFuture<Message>;
392
393    fn into_future(self) -> Self::IntoFuture {
394        let http = self.http;
395
396        match self.try_into_request() {
397            Ok(request) => http.request(request),
398            Err(source) => ResponseFuture::error(source),
399        }
400    }
401}
402
403impl TryIntoRequest for CreateMessage<'_> {
404    fn try_into_request(self) -> Result<Request, Error> {
405        let mut fields = self.fields.map_err(Error::validation)?;
406        let mut request = Request::builder(&Route::CreateMessage {
407            channel_id: self.channel_id.get(),
408        });
409
410        // Set the default allowed mentions if required.
411        if fields.allowed_mentions.is_none() {
412            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
413                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
414            }
415        }
416
417        // Determine whether we need to use a multipart/form-data body or a JSON
418        // body.
419        if !self.attachment_manager.is_empty() {
420            let form = if let Some(payload_json) = fields.payload_json {
421                self.attachment_manager.build_form(payload_json)
422            } else {
423                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
424
425                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
426
427                self.attachment_manager.build_form(fields.as_ref())
428            };
429
430            request = request.form(form);
431        } else if let Some(payload_json) = fields.payload_json {
432            request = request.body(payload_json.to_vec());
433        } else {
434            request = request.json(&fields);
435        }
436
437        request.build()
438    }
439}