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