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, components as validate_components,
23 content as validate_content, embeds as validate_embeds, 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 /// # Errors
169 ///
170 /// Refer to the errors section of
171 /// [`twilight_validate::component::component`] for a list of errors that
172 /// may be returned as a result of validating each provided component.
173 pub fn components(mut self, components: Option<&'a [Component]>) -> Self {
174 self.fields = self.fields.and_then(|mut fields| {
175 if let Some(components) = components {
176 validate_components(components)?;
177 }
178
179 fields.components = Some(Nullable(components));
180
181 Ok(fields)
182 });
183
184 self
185 }
186
187 /// Set the message's content.
188 ///
189 /// The maximum length is 2000 UTF-16 characters.
190 ///
191 /// # Editing
192 ///
193 /// Pass [`None`] to remove the message content. This is impossible if it
194 /// would leave the message empty of `attachments`, `content`, `embeds`, or
195 /// `sticker_ids`.
196 ///
197 /// # Errors
198 ///
199 /// Returns an error of type [`ContentInvalid`] if the content length is too
200 /// long.
201 ///
202 /// [`ContentInvalid`]: twilight_validate::message::MessageValidationErrorType::ContentInvalid
203 pub fn content(mut self, content: Option<&'a str>) -> Self {
204 self.fields = self.fields.and_then(|mut fields| {
205 if let Some(content) = content {
206 validate_content(content)?;
207 }
208
209 fields.content = Some(Nullable(content));
210
211 Ok(fields)
212 });
213
214 self
215 }
216
217 /// Set the message's list of embeds.
218 ///
219 /// Calling this method will clear previous calls.
220 ///
221 /// The amount of embeds must not exceed [`EMBED_COUNT_LIMIT`]. The total
222 /// character length of each embed must not exceed [`EMBED_TOTAL_LENGTH`]
223 /// characters. Additionally, the internal fields also have character
224 /// limits. Refer to [Discord Docs/Embed Limits] for more information.
225 ///
226 /// # Editing
227 ///
228 /// To keep all embeds, do not call this method. To modify one or more
229 /// embeds in the message, acquire them from the previous message, mutate
230 /// them in place, then pass that list to this method. To remove all embeds,
231 /// pass [`None`]. This is impossible if it would leave the message empty of
232 /// `attachments`, `content`, `embeds`, or `sticker_ids`.
233 ///
234 /// # Errors
235 ///
236 /// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
237 ///
238 /// Otherwise, refer to the errors section of
239 /// [`twilight_validate::embed::embed`] for a list of errors that may occur.
240 ///
241 /// [Discord Docs/Embed Limits]: https://discord.com/developers/docs/resources/channel#embed-limits
242 /// [`EMBED_COUNT_LIMIT`]: twilight_validate::message::EMBED_COUNT_LIMIT
243 /// [`EMBED_TOTAL_LENGTH`]: twilight_validate::embed::EMBED_TOTAL_LENGTH
244 /// [`TooManyEmbeds`]: twilight_validate::message::MessageValidationErrorType::TooManyEmbeds
245 pub fn embeds(mut self, embeds: Option<&'a [Embed]>) -> Self {
246 self.fields = self.fields.and_then(|mut fields| {
247 if let Some(embeds) = embeds {
248 validate_embeds(embeds)?;
249 }
250
251 fields.embeds = Some(Nullable(embeds));
252
253 Ok(fields)
254 });
255 self
256 }
257
258 /// Set the message's flags.
259 ///
260 /// The only supported flag is [`SUPPRESS_EMBEDS`].
261 ///
262 /// [`SUPPRESS_EMBEDS`]: MessageFlags::SUPPRESS_EMBEDS
263 pub fn flags(mut self, flags: MessageFlags) -> Self {
264 if let Ok(fields) = self.fields.as_mut() {
265 fields.flags = Some(flags);
266 }
267
268 self
269 }
270
271 /// Specify multiple [`Id<AttachmentMarker>`]s already present in the target
272 /// message to keep.
273 ///
274 /// If called, all unspecified attachments (except ones added with
275 /// [`attachments`]) will be removed from the message. This is impossible if
276 /// it would leave the message empty of `attachments`, `content`, `embeds`,
277 /// or `sticker_ids`. If not called, all attachments will be kept.
278 ///
279 /// [`attachments`]: Self::attachments
280 pub fn keep_attachment_ids(mut self, attachment_ids: &'a [Id<AttachmentMarker>]) -> Self {
281 if let Ok(fields) = self.fields.as_mut() {
282 self.attachment_manager = self.attachment_manager.set_ids(attachment_ids.to_vec());
283
284 // Set an empty list. This will be overwritten in `TryIntoRequest` if
285 // the actual list is not empty.
286 fields.attachments = Some(Nullable(Some(Vec::new())));
287 }
288
289 self
290 }
291
292 /// JSON encoded body of any additional request fields.
293 ///
294 /// If this method is called, all other fields are ignored, except for
295 /// [`attachments`]. See [Discord Docs/Uploading Files].
296 ///
297 /// # Examples
298 ///
299 /// See [`ExecuteWebhook::payload_json`] for examples.
300 ///
301 /// [Discord Docs/Uploading Files]: https://discord.com/developers/docs/reference#uploading-files
302 /// [`ExecuteWebhook::payload_json`]: crate::request::channel::webhook::ExecuteWebhook::payload_json
303 /// [`attachments`]: Self::attachments
304 pub fn payload_json(mut self, payload_json: &'a [u8]) -> Self {
305 if let Ok(fields) = self.fields.as_mut() {
306 fields.payload_json = Some(payload_json);
307 }
308
309 self
310 }
311}
312
313impl IntoFuture for UpdateMessage<'_> {
314 type Output = Result<Response<Message>, Error>;
315
316 type IntoFuture = ResponseFuture<Message>;
317
318 fn into_future(self) -> Self::IntoFuture {
319 let http = self.http;
320
321 match self.try_into_request() {
322 Ok(request) => http.request(request),
323 Err(source) => ResponseFuture::error(source),
324 }
325 }
326}
327
328impl TryIntoRequest for UpdateMessage<'_> {
329 fn try_into_request(self) -> Result<Request, Error> {
330 let mut fields = self.fields.map_err(Error::validation)?;
331 let mut request = Request::builder(&Route::UpdateMessage {
332 channel_id: self.channel_id.get(),
333 message_id: self.message_id.get(),
334 });
335
336 // Set the default allowed mentions if required.
337 if fields.allowed_mentions.is_none() {
338 if let Some(allowed_mentions) = self.http.default_allowed_mentions() {
339 fields.allowed_mentions = Some(Nullable(Some(allowed_mentions)));
340 }
341 }
342
343 // Determine whether we need to use a multipart/form-data body or a JSON
344 // body.
345 if !self.attachment_manager.is_empty() {
346 let form = if let Some(payload_json) = fields.payload_json {
347 self.attachment_manager.build_form(payload_json)
348 } else {
349 fields.attachments = Some(Nullable(Some(
350 self.attachment_manager.get_partial_attachments(),
351 )));
352
353 let fields = crate::json::to_vec(&fields).map_err(Error::json)?;
354
355 self.attachment_manager.build_form(fields.as_ref())
356 };
357
358 request = request.form(form);
359 } else if let Some(payload_json) = fields.payload_json {
360 request = request.body(payload_json.to_vec());
361 } else {
362 request = request.json(&fields);
363 }
364
365 request.build()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use std::error::Error;
373
374 #[test]
375 fn clear_attachment() -> Result<(), Box<dyn Error>> {
376 const CHANNEL_ID: Id<ChannelMarker> = Id::new(1);
377 const MESSAGE_ID: Id<MessageMarker> = Id::new(2);
378
379 let client = Client::new("token".into());
380
381 let expected = r#"{"attachments":[]}"#;
382 let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID)
383 .keep_attachment_ids(&[])
384 .try_into_request()?;
385
386 assert_eq!(Some(expected.as_bytes()), actual.body());
387
388 let expected = r"{}";
389 let actual = UpdateMessage::new(&client, CHANNEL_ID, MESSAGE_ID).try_into_request()?;
390
391 assert_eq!(Some(expected.as_bytes()), actual.body());
392
393 Ok(())
394 }
395}