twilight_http/request/channel/message/update_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::{AllowedMentions, Component, Embed, Message, MessageFlags},
15 http::attachment::Attachment,
16 id::{
17 marker::{AttachmentMarker, ChannelMarker, MessageMarker},
18 Id,
19 },
20};
21use twilight_validate::message::{
22 attachment as validate_attachment, content as validate_content, embeds as validate_embeds,
23 MessageValidationError,
24};
25
26#[derive(Serialize)]
27struct UpdateMessageFields<'a> {
28 #[serde(skip_serializing_if = "Option::is_none")]
29 allowed_mentions: Option<Nullable<&'a AllowedMentions>>,
30 /// List of attachments to keep, and new attachments to add.
31 #[serde(skip_serializing_if = "Option::is_none")]
32 attachments: Option<Nullable<Vec<PartialAttachment<'a>>>>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 components: Option<Nullable<&'a [Component]>>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 content: Option<Nullable<&'a str>>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 embeds: Option<Nullable<&'a [Embed]>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 flags: Option<MessageFlags>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 payload_json: Option<&'a [u8]>,
43}
44
45/// Update a message by [`Id<ChannelMarker>`] and [`Id<MessageMarker>`].
46///
47/// You can pass [`None`] to any of the methods to remove the associated field.
48/// Pass [`None`] to [`content`] to remove the content. You must ensure that the
49/// message still contains at least one of [`attachments`], [`content`],
50/// [`embeds`], or stickers.
51///
52/// # Examples
53///
54/// Replace the content with `"test update"`:
55///
56/// ```no_run
57/// # #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {
58/// use twilight_http::Client;
59/// use twilight_model::id::Id;
60///
61/// let client = Client::new("my token".to_owned());
62/// client
63/// .update_message(Id::new(1), Id::new(2))
64/// .content(Some("test update"))
65/// .await?;
66/// # Ok(()) }
67/// ```
68///
69/// Remove the message's content:
70///
71/// ```no_run
72/// # use twilight_http::Client;
73/// # use twilight_model::id::Id;
74/// #
75/// # #[tokio::main]
76/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
77/// # let client = Client::new("my token".to_owned());
78/// client
79/// .update_message(Id::new(1), Id::new(2))
80/// .content(None)
81/// .await?;
82/// # Ok(()) }
83/// ```
84///
85/// [`attachments`]: Self::attachments
86/// [`content`]: Self::content
87/// [`embeds`]: Self::embeds
88#[must_use = "requests must be configured and executed"]
89pub struct UpdateMessage<'a> {
90 attachment_manager: AttachmentManager<'a>,
91 channel_id: Id<ChannelMarker>,
92 fields: Result<UpdateMessageFields<'a>, MessageValidationError>,
93 http: &'a Client,
94 message_id: Id<MessageMarker>,
95}
96
97impl<'a> UpdateMessage<'a> {
98 pub(crate) const fn new(
99 http: &'a Client,
100 channel_id: Id<ChannelMarker>,
101 message_id: Id<MessageMarker>,
102 ) -> Self {
103 Self {
104 attachment_manager: AttachmentManager::new(),
105 channel_id,
106 fields: Ok(UpdateMessageFields {
107 allowed_mentions: None,
108 attachments: None,
109 components: None,
110 content: None,
111 embeds: None,
112 flags: None,
113 payload_json: None,
114 }),
115 http,
116 message_id,
117 }
118 }
119
120 /// Specify the [`AllowedMentions`] for the message.
121 ///
122 /// If not called, the request will use the client's default allowed
123 /// mentions.
124 pub fn allowed_mentions(mut self, allowed_mentions: Option<&'a AllowedMentions>) -> Self {
125 if let Ok(fields) = self.fields.as_mut() {
126 fields.allowed_mentions = Some(Nullable(allowed_mentions));
127 }
128
129 self
130 }
131
132 /// Attach multiple new files to the message.
133 ///
134 /// This method clears previous calls.
135 ///
136 /// # Errors
137 ///
138 /// Returns an error of type [`AttachmentDescriptionTooLarge`] if
139 /// the attachments's description is too large.
140 ///
141 /// Returns an error of type [`AttachmentFilename`] if any filename is
142 /// invalid.
143 ///
144 /// [`AttachmentDescriptionTooLarge`]: twilight_validate::message::MessageValidationErrorType::AttachmentDescriptionTooLarge
145 /// [`AttachmentFilename`]: twilight_validate::message::MessageValidationErrorType::AttachmentFilename
146 pub fn attachments(mut self, attachments: &'a [Attachment]) -> Self {
147 if self.fields.is_ok() {
148 if let Err(source) = attachments.iter().try_for_each(validate_attachment) {
149 self.fields = Err(source);
150 } else {
151 self.attachment_manager = self
152 .attachment_manager
153 .set_files(attachments.iter().collect());
154 }
155 }
156
157 self
158 }
159
160 /// Set the message's list of [`Component`]s.
161 ///
162 /// Calling this method will clear previous calls.
163 ///
164 /// # Editing
165 ///
166 /// Pass [`None`] to clear existing components.
167 ///
168 /// # Manual Validation
169 ///
170 /// Validation of components is not done automatically here, as we don't know which component
171 /// version is in use, you can validate them manually using the [`twilight_validate::component::component_v1`]
172 /// or [`twilight_validate::component::component_v2`] functions.
173 pub fn components(mut self, components: Option<&'a [Component]>) -> Self {
174 self.fields = self.fields.map(|mut fields| {
175 fields.components = Some(Nullable(components));
176 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 /// # Editing
187 ///
188 /// Pass [`None`] to remove the message content. This is impossible if it
189 /// would leave the message empty of `attachments`, `content`, `embeds`, or
190 /// `sticker_ids`.
191 ///
192 /// # Errors
193 ///
194 /// Returns an error of type [`ContentInvalid`] if the content length is too
195 /// long.
196 ///
197 /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
198 pub fn content(mut self, content: Option<&'a str>) -> Self {
199 self.fields = self.fields.and_then(|mut fields| {
200 if let Some(content) = content {
201 validate_content(content)?;
202 }
203
204 fields.content = Some(Nullable(content));
205
206 Ok(fields)
207 });
208
209 self
210 }
211
212 /// Set the message's list of embeds.
213 ///
214 /// Calling this method will clear previous calls.
215 ///
216 /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
217 /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
218 /// characters. Additionally, the internal fields also have character
219 /// limits. Refer to [Discord Docs/Embed Limits] for more information.
220 ///
221 /// # Editing
222 ///
223 /// To keep all embeds, do not call this method. To modify one or more
224 /// embeds in the message, acquire them from the previous message, mutate
225 /// them in place, then pass that list to this method. To remove all embeds,
226 /// pass [`None`]. This is impossible if it would leave the message empty of
227 /// `attachments`, `content`, `embeds`, or `sticker_ids`.
228 ///
229 /// # Errors
230 ///
231 /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
232 ///
233 /// Otherwise, refer to the errors section of
234 /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
235 ///
236 /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
237 /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
238 /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
239 /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
240 pub fn embeds(mut self, embeds: Option<&'a [Embed]>) -> Self {
241 self.fields = self.fields.and_then(|mut fields| {
242 if let Some(embeds) = embeds {
243 validate_embeds(embeds)?;
244 }
245
246 fields.embeds = Some(Nullable(embeds));
247
248 Ok(fields)
249 });
250 self
251 }
252
253 /// Set the message's flags.
254 ///
255 /// The only supported flag is [`SUPPRESS_EMBEDS`].
256 ///
257 /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
258 pub fn flags(mut self, flags: MessageFlags) -> Self {
259 if let Ok(fields) = self.fields.as_mut() {
260 fields.flags = Some(flags);
261 }
262
263 self
264 }
265
266 /// Specify multiple [`Id<AttachmentMarker>`]s already present in the target
267 /// message to keep.
268 ///
269 /// If called, all unspecified attachments (except ones added with
270 /// [`attachments`]) will be removed from the message. This is impossible if
271 /// it would leave the message empty of `attachments`, `content`, `embeds`,
272 /// or `sticker_ids`. If not called, all attachments will be kept.
273 ///
274 /// [`attachments`]: Self::attachments
275 pub fn keep_attachment_ids(mut self, attachment_ids: &'a [Id<AttachmentMarker>]) -> Self {
276 if let Ok(fields) = self.fields.as_mut() {
277 self.attachment_manager = self.attachment_manager.set_ids(attachment_ids.to_vec());
278
279 // Set an empty list. This will be overwritten in `TryIntoRequest` if
280 // the actual list is not empty.
281 fields.attachments = Some(Nullable(Some(Vec::new())));
282 }
283
284 self
285 }
286
287 /// JSON encoded body of any additional request fields.
288 ///
289 /// If this method is called, all other fields are ignored, except for
290 /// [`attachments`]. See [Discord Docs/Uploading Files].
291 ///
292 /// # Examples
293 ///
294 /// See [`ExecuteWebhook::payload_json`] for examples.
295 ///
296 /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
297 /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
298 /// [`attachments`]: Self::attachments
299 pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
300 if let Ok(fields) = self.fields.as_mut() {
301 fields.payload_json = Some(payload_json);
302 }
303
304 self
305 }
306}
307
308impl IntoFuture for UpdateMessage<'_> {
309 type Output = Result<Response<Message>, Error>;
310
311 type IntoFuture = ResponseFuture<Message>;
312
313 fn into_future(self) -> Self::IntoFuture {
314 let http = self.http;
315
316 match self.try_into_request() {
317 Ok(request) => http.request(request),
318 Err(source) => ResponseFuture::error(source),
319 }
320 }
321}
322
323impl TryIntoRequest for UpdateMessage<'_> {
324 fn try_into_request(self) -> Result<Request, Error> {
325 let mut fields = self.fields.map_err(Error::validation)?;
326 let mut request = Request::builder(&Route::UpdateMessage {
327 channel_id: self.channel_id.get(),
328 message_id: self.message_id.get(),
329 });
330
331 // Set the default allowed mentions if required.
332 if fields.allowed_mentions.is_none() {
333 if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
334 fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
335 }
336 }
337
338 // Determine whether we need to use a multipart/form-data body or a JSON
339 // body.
340 if !self.attachment_manager.is_empty() {
341 let form = if let Some(payload_json) = fields.payload_json {
342 self.attachment_manager.build_form(payload_json)
343 } else {
344 fields.attachments = Some(Nullable(Some(
345 self.attachment_manager.get_partial_attachments(),
346 )));
347
348 let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
349
350 self.attachment_manager.build_form(fields.as_ref())
351 };
352
353 request = request.form(form);
354 } else if let Some(payload_json) = fields.payload_json {
355 request = request.body(payload_json.to_vec());
356 } else {
357 request = request.json(&fields);
358 }
359
360 request.build()
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use std::error::Error;
368
369 #[test]
370 fn clear_attachment() -> Result<(), Box<dyn Error>> {
371 const CHANNEL_ID: Id<ChannelMarker> = Id::new(1);
372 const MESSAGE_ID: Id<MessageMarker> = Id::new(2);
373
374 let client = Client::new("token".into());
375
376 let expected = r#"{"attachments":[]}"#;
377 let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID)
378 .keep_attachment_ids(&[])
379 .try_into_request()?;
380
381 assert_eq!(Some(expected.as_bytes()), actual.body());
382
383 let expected = r"{}";
384 let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID).try_into_request()?;
385
386 assert_eq!(Some(expected.as_bytes()), actual.body());
387
388 Ok(())
389 }
390}