twilight_validate/
message.rs

1//! Constants, error types, and functions for validating [`Message`] fields.
2//!
3//! [`Message`]: twilight_model::channel::Message
4
5use crate::{
6    component::{ComponentValidationErrorType, COMPONENT_COUNT},
7    embed::{chars as embed_chars, EmbedValidationErrorType, EMBED_TOTAL_LENGTH},
8    request::ValidationError,
9};
10use std::{
11    error::Error,
12    fmt::{Display, Formatter, Result as FmtResult},
13};
14use twilight_model::{
15    channel::message::{Component, Embed},
16    http::attachment::Attachment,
17    id::{marker::StickerMarker, Id},
18};
19
20/// Maximum length of an attachment's description.
21pub const ATTACHMENT_DESCIPTION_LENGTH_MAX: usize = 1024;
22
23/// Maximum number of embeds that a message may have.
24pub const EMBED_COUNT_LIMIT: usize = 10;
25
26/// Maximum length of message content.
27pub const MESSAGE_CONTENT_LENGTH_MAX: usize = 2000;
28
29/// Maximum amount of stickers.
30pub const STICKER_MAX: usize = 3;
31
32/// ASCII dash.
33const DASH: char = '-';
34
35/// ASCII dot.
36const DOT: char = '.';
37
38/// ASCII underscore.
39const UNDERSCORE: char = '_';
40
41/// A message is not valid.
42#[derive(Debug)]
43pub struct MessageValidationError {
44    /// Type of error that occurred.
45    kind: MessageValidationErrorType,
46    /// Source of the error, if any.
47    source: Option<Box<dyn Error + Send + Sync>>,
48}
49
50impl MessageValidationError {
51    /// Immutable reference to the type of error that occurred.
52    #[must_use = "retrieving the type has no effect if left unused"]
53    pub const fn kind(&self) -> &MessageValidationErrorType {
54        &self.kind
55    }
56
57    /// Consume the error, returning the source error if there is any.
58    #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
59    pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
60        self.source
61    }
62
63    /// Consume the error, returning the owned error type and the source error.
64    #[must_use = "consuming the error into its parts has no effect if left unused"]
65    pub fn into_parts(
66        self,
67    ) -> (
68        MessageValidationErrorType,
69        Option<Box<dyn Error + Send + Sync>>,
70    ) {
71        (self.kind, self.source)
72    }
73
74    /// Create a [`MessageValidationError`] from a [`ValidationError`].
75    #[must_use = "has no effect if unused"]
76    pub fn from_validation_error(
77        kind: MessageValidationErrorType,
78        source: ValidationError,
79    ) -> Self {
80        Self {
81            kind,
82            source: Some(Box::new(source)),
83        }
84    }
85}
86
87impl Display for MessageValidationError {
88    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
89        match &self.kind {
90            MessageValidationErrorType::AttachmentDescriptionTooLarge { chars } => {
91                f.write_str("the attachment description is ")?;
92                Display::fmt(chars, f)?;
93                f.write_str(" characters long, but the max is ")?;
94
95                Display::fmt(&ATTACHMENT_DESCIPTION_LENGTH_MAX, f)
96            }
97            MessageValidationErrorType::AttachmentFilename { filename } => {
98                f.write_str("attachment filename `")?;
99                Display::fmt(filename, f)?;
100
101                f.write_str("`is invalid")
102            }
103            MessageValidationErrorType::ComponentCount { count } => {
104                Display::fmt(count, f)?;
105                f.write_str(" components were provided, but only ")?;
106                Display::fmt(&COMPONENT_COUNT, f)?;
107
108                f.write_str(" root components are allowed")
109            }
110            MessageValidationErrorType::ComponentInvalid { .. } => {
111                f.write_str("a provided component is invalid")
112            }
113            MessageValidationErrorType::ContentInvalid => f.write_str("message content is invalid"),
114            MessageValidationErrorType::EmbedInvalid { idx, .. } => {
115                f.write_str("embed at index ")?;
116                Display::fmt(idx, f)?;
117
118                f.write_str(" is invalid")
119            }
120            MessageValidationErrorType::StickersInvalid { len } => {
121                f.write_str("amount of stickers provided is ")?;
122                Display::fmt(len, f)?;
123                f.write_str(" but it must be at most ")?;
124
125                Display::fmt(&STICKER_MAX, f)
126            }
127            MessageValidationErrorType::TooManyEmbeds { .. } => {
128                f.write_str("message has too many embeds")
129            }
130            MessageValidationErrorType::WebhookUsername { .. } => {
131                if let Some(source) = self.source() {
132                    Display::fmt(&source, f)
133                } else {
134                    f.write_str("webhook username is invalid")
135                }
136            }
137        }
138    }
139}
140
141impl Error for MessageValidationError {}
142
143/// Type of [`MessageValidationError`] that occurred.
144#[derive(Debug)]
145pub enum MessageValidationErrorType {
146    /// Attachment filename is not valid.
147    AttachmentFilename {
148        /// Invalid filename.
149        filename: String,
150    },
151    /// Attachment description is too large.
152    AttachmentDescriptionTooLarge {
153        /// Provided number of codepoints.
154        chars: usize,
155    },
156    /// Too many message components were provided.
157    ComponentCount {
158        /// Number of components that were provided.
159        count: usize,
160    },
161    /// An invalid message component was provided.
162    ComponentInvalid {
163        /// Index of the component.
164        idx: usize,
165        /// Additional details about the validation failure type.
166        kind: ComponentValidationErrorType,
167    },
168    /// Returned when the content is over 2000 UTF-16 characters.
169    ContentInvalid,
170    /// Returned when the embed is invalid.
171    EmbedInvalid {
172        /// Index of the embed.
173        idx: usize,
174        /// Additional details about the validation failure type.
175        kind: EmbedValidationErrorType,
176    },
177    /// Amount of stickers provided is invalid.
178    StickersInvalid {
179        /// Invalid length.
180        len: usize,
181    },
182    /// Too many embeds were provided.
183    ///
184    /// A followup message can have up to 10 embeds.
185    TooManyEmbeds,
186    /// Provided webhook username was invalid.
187    WebhookUsername,
188}
189
190/// Ensure an attachment is correct.
191///
192/// # Errors
193///
194/// Returns an error of type [`AttachmentDescriptionTooLarge`] if
195/// the attachments's description is too large.
196///
197/// Returns an error of type [`AttachmentFilename`] if the
198/// filename is invalid.
199///
200/// [`AttachmentDescriptionTooLarge`]: MessageValidationErrorType::AttachmentDescriptionTooLarge
201/// [`AttachmentFilename`]: MessageValidationErrorType::AttachmentFilename
202pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
203    attachment_filename(&attachment.filename)?;
204
205    if let Some(description) = &attachment.description {
206        attachment_description(description)?;
207    }
208
209    Ok(())
210}
211
212/// Ensure an attachment's description is correct.
213///
214/// # Errors
215///
216/// Returns an error of type [`AttachmentDescriptionTooLarge`] if
217/// the attachment's description is too large.
218///
219/// [`AttachmentDescriptionTooLarge`]: MessageValidationErrorType::AttachmentDescriptionTooLarge
220pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
221    let chars = description.as_ref().chars().count();
222    if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
223        Ok(())
224    } else {
225        Err(MessageValidationError {
226            kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
227            source: None,
228        })
229    }
230}
231
232/// Ensure an attachment's description is correct.
233///
234/// The filename can contain ASCII alphanumeric characters, dots, dashes, and
235/// underscores.
236///
237/// # Errors
238///
239/// Returns an error of type [`AttachmentFilename`] if the filename is invalid.
240///
241/// [`AttachmentFilename`]: MessageValidationErrorType::AttachmentFilename
242pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
243    if filename
244        .as_ref()
245        .chars()
246        .all(|c| (c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE))
247    {
248        Ok(())
249    } else {
250        Err(MessageValidationError {
251            kind: MessageValidationErrorType::AttachmentFilename {
252                filename: filename.as_ref().to_string(),
253            },
254            source: None,
255        })
256    }
257}
258
259/// Ensure a list of components is correct.
260///
261/// # Errors
262///
263/// Returns a [`ComponentValidationErrorType::ComponentCount`] if there are
264/// too many components in the provided list.
265///
266/// Refer to the errors section of [`component`] for a list of errors that may
267/// be returned as a result of validating each provided component.
268///
269/// [`component`]: crate::component::component
270pub fn components(components: &[Component]) -> Result<(), MessageValidationError> {
271    let count = components.len();
272
273    if count > COMPONENT_COUNT {
274        Err(MessageValidationError {
275            kind: MessageValidationErrorType::ComponentCount { count },
276            source: None,
277        })
278    } else {
279        for (idx, component) in components.iter().enumerate() {
280            crate::component::component(component).map_err(|source| {
281                let (kind, source) = source.into_parts();
282
283                MessageValidationError {
284                    kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
285                    source,
286                }
287            })?;
288        }
289
290        Ok(())
291    }
292}
293
294/// Ensure a message's content is correct.
295///
296/// # Errors
297///
298/// Returns an error of type [`ContentInvalid`] if the message's content is
299/// invalid.
300///
301/// [`ContentInvalid`]: MessageValidationErrorType::ContentInvalid
302pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
303    // <https://discordapp.com/developers/docs/resources/channel#create-message-params>
304    if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
305        Ok(())
306    } else {
307        Err(MessageValidationError {
308            kind: MessageValidationErrorType::ContentInvalid,
309            source: None,
310        })
311    }
312}
313
314/// Ensure a list of embeds is correct.
315///
316/// # Errors
317///
318/// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
319///
320/// Otherwise, refer to the errors section of [`embed`] for a list of errors
321/// that may occur.
322///
323/// [`TooManyEmbeds`]: MessageValidationErrorType::TooManyEmbeds
324/// [`embed`]: crate::embed::embed
325pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
326    if embeds.len() > EMBED_COUNT_LIMIT {
327        Err(MessageValidationError {
328            kind: MessageValidationErrorType::TooManyEmbeds,
329            source: None,
330        })
331    } else {
332        let mut chars = 0;
333        for (idx, embed) in embeds.iter().enumerate() {
334            chars += embed_chars(embed);
335
336            if chars > EMBED_TOTAL_LENGTH {
337                return Err(MessageValidationError {
338                    kind: MessageValidationErrorType::EmbedInvalid {
339                        idx,
340                        kind: EmbedValidationErrorType::EmbedTooLarge { chars },
341                    },
342                    source: None,
343                });
344            }
345
346            crate::embed::embed(embed).map_err(|source| {
347                let (kind, source) = source.into_parts();
348
349                MessageValidationError {
350                    kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
351                    source,
352                }
353            })?;
354        }
355
356        Ok(())
357    }
358}
359
360/// Ensure that the amount of stickers in a message is correct.
361///
362/// There must be at most [`STICKER_MAX`] stickers. This is based on [this
363/// documentation entry].
364///
365/// # Errors
366///
367/// Returns an error of type [`StickersInvalid`] if the length is invalid.
368///
369/// [`StickersInvalid`]: MessageValidationErrorType::StickersInvalid
370/// [this documentation entry]: https://discord.com/developers/docs/resources/channel#create-message-jsonform-params
371pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
372    let len = sticker_ids.len();
373
374    if len <= STICKER_MAX {
375        Ok(())
376    } else {
377        Err(MessageValidationError {
378            kind: MessageValidationErrorType::StickersInvalid { len },
379            source: None,
380        })
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn attachment_description_limit() {
390        assert!(attachment_description("").is_ok());
391        assert!(attachment_description(str::repeat("a", 1024)).is_ok());
392
393        assert!(matches!(
394            attachment_description(str::repeat("a", 1025))
395                .unwrap_err()
396                .kind(),
397            MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
398        ));
399    }
400
401    #[test]
402    fn attachment_allowed_filename() {
403        assert!(attachment_filename("one.jpg").is_ok());
404        assert!(attachment_filename("two.png").is_ok());
405        assert!(attachment_filename("three.gif").is_ok());
406        assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
407
408        assert!(attachment_filename("????????").is_err());
409    }
410
411    #[test]
412    fn content_length() {
413        assert!(content("").is_ok());
414        assert!(content("a".repeat(2000)).is_ok());
415
416        assert!(content("a".repeat(2001)).is_err());
417    }
418}