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 ID of another message to forward.
334    pub fn forward(mut self, other: Id<MessageMarker>) -> Self {
335        self.fields = self.fields.map(|mut fields| {
336            let channel_id = self.channel_id;
337
338            let reference = if let Some(reference) = fields.message_reference {
339                MessageReference {
340                    channel_id: Some(channel_id),
341                    message_id: Some(other),
342                    ..reference
343                }
344            } else {
345                MessageReference {
346                    kind: MessageReferenceType::Forward,
347                    channel_id: Some(channel_id),
348                    guild_id: None,
349                    message_id: Some(other),
350                    fail_if_not_exists: None,
351                }
352            };
353
354            fields.message_reference = Some(reference);
355
356            fields
357        });
358
359        self
360    }
361
362    /// Set the IDs of up to 3 guild stickers.
363    ///
364    /// # Errors
365    ///
366    /// Returns an error of type [`StickersInvalid`] if the length is invalid.
367    ///
368    /// [`StickersInvalid`]: twilight_validate::message::MessageValidationErrorType::StickersInvalid
369    pub fn sticker_ids(mut self, sticker_ids: &'a [Id<StickerMarker>]) -> Self {
370        self.fields = self.fields.and_then(|mut fields| {
371            validate_sticker_ids(sticker_ids)?;
372            fields.sticker_ids = Some(sticker_ids);
373
374            Ok(fields)
375        });
376
377        self
378    }
379
380    /// Specify true if the message is TTS.
381    pub fn tts(mut self, tts: bool) -> Self {
382        if let Ok(fields) = self.fields.as_mut() {
383            fields.tts = Some(tts);
384        }
385
386        self
387    }
388}
389
390impl IntoFuture for CreateMessage<'_> {
391    type Output = Result<Response<Message>, Error>;
392
393    type IntoFuture = ResponseFuture<Message>;
394
395    fn into_future(self) -> Self::IntoFuture {
396        let http = self.http;
397
398        match self.try_into_request() {
399            Ok(request) => http.request(request),
400            Err(source) => ResponseFuture::error(source),
401        }
402    }
403}
404
405impl TryIntoRequest for CreateMessage<'_> {
406    fn try_into_request(self) -> Result<Request, Error> {
407        let mut fields = self.fields.map_err(Error::validation)?;
408        let mut request = Request::builder(&Route::CreateMessage {
409            channel_id: self.channel_id.get(),
410        });
411
412        // Set the default allowed mentions if required.
413        if fields.allowed_mentions.is_none() {
414            if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
415                fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
416            }
417        }
418
419        // Determine whether we need to use a multipart/form-data body or a JSON
420        // body.
421        if !self.attachment_manager.is_empty() {
422            let form = if let Some(payload_json) = fields.payload_json {
423                self.attachment_manager.build_form(payload_json)
424            } else {
425                fields.attachments = Some(self.attachment_manager.get_partial_attachments());
426
427                let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
428
429                self.attachment_manager.build_form(fields.as_ref())
430            };
431
432            request = request.form(form);
433        } else if let Some(payload_json) = fields.payload_json {
434            request = request.body(payload_json.to_vec());
435        } else {
436            request = request.json(&fields);
437        }
438
439        request.build()
440    }
441}