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, COMPONENT_V2_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, is_v2 } => {
104                Display::fmt(count, f)?;
105                f.write_str(" components were provided, but only ")?;
106                if *is_v2 {
107                    Display::fmt(&COMPONENT_V2_COUNT, f)?;
108                } else {
109                    Display::fmt(&COMPONENT_COUNT, f)?;
110                }
111
112                f.write_str(" root components are allowed")
113            }
114            MessageValidationErrorType::ComponentInvalid { .. } => {
115                f.write_str("a provided component is invalid")
116            }
117            MessageValidationErrorType::ContentInvalid => f.write_str("message content is invalid"),
118            MessageValidationErrorType::EmbedInvalid { idx, .. } => {
119                f.write_str("embed at index ")?;
120                Display::fmt(idx, f)?;
121
122                f.write_str(" is invalid")
123            }
124            MessageValidationErrorType::StickersInvalid { len } => {
125                f.write_str("amount of stickers provided is ")?;
126                Display::fmt(len, f)?;
127                f.write_str(" but it must be at most ")?;
128
129                Display::fmt(&STICKER_MAX, f)
130            }
131            MessageValidationErrorType::TooManyEmbeds => f.write_str("message has too many embeds"),
132            MessageValidationErrorType::WebhookUsername => {
133                if let Some(source) = self.source() {
134                    Display::fmt(&source, f)
135                } else {
136                    f.write_str("webhook username is invalid")
137                }
138            }
139        }
140    }
141}
142
143impl Error for MessageValidationError {}
144
145/// Type of [`MessageValidationError`] that occurred.
146#[derive(Debug)]
147pub enum MessageValidationErrorType {
148    /// Attachment filename is not valid.
149    AttachmentFilename {
150        /// Invalid filename.
151        filename: String,
152    },
153    /// Attachment description is too large.
154    AttachmentDescriptionTooLarge {
155        /// Provided number of codepoints.
156        chars: usize,
157    },
158    /// Too many message components were provided.
159    ComponentCount {
160        /// Number of components that were provided.
161        count: usize,
162        /// If it was a components V2 check.
163        is_v2: bool,
164    },
165    /// An invalid message component was provided.
166    ComponentInvalid {
167        /// Index of the component.
168        idx: usize,
169        /// Additional details about the validation failure type.
170        kind: ComponentValidationErrorType,
171    },
172    /// Returned when the content is over 2000 UTF-16 characters.
173    ContentInvalid,
174    /// Returned when the embed is invalid.
175    EmbedInvalid {
176        /// Index of the embed.
177        idx: usize,
178        /// Additional details about the validation failure type.
179        kind: EmbedValidationErrorType,
180    },
181    /// Amount of stickers provided is invalid.
182    StickersInvalid {
183        /// Invalid length.
184        len: usize,
185    },
186    /// Too many embeds were provided.
187    ///
188    /// A followup message can have up to 10 embeds.
189    TooManyEmbeds,
190    /// Provided webhook username was invalid.
191    WebhookUsername,
192}
193
194/// Ensure an attachment is correct.
195///
196/// # Errors
197///
198/// Returns an error of type [`AttachmentDescriptionTooLarge`] if
199/// the attachments's description is too large.
200///
201/// Returns an error of type [`AttachmentFilename`] if the
202/// filename is invalid.
203///
204/// [`AttachmentDescriptionTooLarge`]: MessageValidationErrorType::AttachmentDescriptionTooLarge
205/// [`AttachmentFilename`]: MessageValidationErrorType::AttachmentFilename
206pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
207    attachment_filename(&attachment.filename)?;
208
209    if let Some(description) = &attachment.description {
210        attachment_description(description)?;
211    }
212
213    Ok(())
214}
215
216/// Ensure an attachment's description is correct.
217///
218/// # Errors
219///
220/// Returns an error of type [`AttachmentDescriptionTooLarge`] if
221/// the attachment's description is too large.
222///
223/// [`AttachmentDescriptionTooLarge`]: MessageValidationErrorType::AttachmentDescriptionTooLarge
224pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
225    let chars = description.as_ref().chars().count();
226    if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
227        Ok(())
228    } else {
229        Err(MessageValidationError {
230            kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
231            source: None,
232        })
233    }
234}
235
236/// Ensure an attachment's description is correct.
237///
238/// The filename can contain ASCII alphanumeric characters, dots, dashes, and
239/// underscores.
240///
241/// # Errors
242///
243/// Returns an error of type [`AttachmentFilename`] if the filename is invalid.
244///
245/// [`AttachmentFilename`]: MessageValidationErrorType::AttachmentFilename
246pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
247    if filename
248        .as_ref()
249        .chars()
250        .all(|c| c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE)
251    {
252        Ok(())
253    } else {
254        Err(MessageValidationError {
255            kind: MessageValidationErrorType::AttachmentFilename {
256                filename: filename.as_ref().to_string(),
257            },
258            source: None,
259        })
260    }
261}
262
263/// Ensure a list of components is correct.
264///
265/// # Errors
266///
267/// Returns a [`ComponentValidationErrorType::ComponentCount`] if there are
268/// too many components in the provided list.
269///
270/// Refer to the errors section of [`component`] for a list of errors that may
271/// be returned as a result of validating each provided component.
272///
273/// [`component`]: crate::component::component
274pub fn components(components: &[Component], is_v2: bool) -> Result<(), MessageValidationError> {
275    if is_v2 {
276        let count = components
277            .iter()
278            .map(Component::component_count)
279            .sum::<usize>();
280        if count > COMPONENT_V2_COUNT {
281            return Err(MessageValidationError {
282                kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
283                source: None,
284            });
285        }
286    } else {
287        let count = components.len();
288
289        if count > COMPONENT_COUNT {
290            return Err(MessageValidationError {
291                kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
292                source: None,
293            });
294        }
295    }
296
297    let function = if is_v2 {
298        crate::component::component_v2
299    } else {
300        crate::component::component_v1
301    };
302    for (idx, component) in components.iter().enumerate() {
303        function(component).map_err(|source| {
304            let (kind, source) = source.into_parts();
305
306            MessageValidationError {
307                kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
308                source,
309            }
310        })?;
311    }
312
313    Ok(())
314}
315
316/// Ensure a message's content is correct.
317///
318/// # Errors
319///
320/// Returns an error of type [`ContentInvalid`] if the message's content is
321/// invalid.
322///
323/// [`ContentInvalid`]: MessageValidationErrorType::ContentInvalid
324pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
325    // <https://discordapp.com/developers/docs/resources/channel#create-message-params>
326    if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
327        Ok(())
328    } else {
329        Err(MessageValidationError {
330            kind: MessageValidationErrorType::ContentInvalid,
331            source: None,
332        })
333    }
334}
335
336/// Ensure a list of embeds is correct.
337///
338/// # Errors
339///
340/// Returns an error of type [`TooManyEmbeds`] if there are too many embeds.
341///
342/// Otherwise, refer to the errors section of [`embed`] for a list of errors
343/// that may occur.
344///
345/// [`TooManyEmbeds`]: MessageValidationErrorType::TooManyEmbeds
346/// [`embed`]: crate::embed::embed
347pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
348    if embeds.len() > EMBED_COUNT_LIMIT {
349        Err(MessageValidationError {
350            kind: MessageValidationErrorType::TooManyEmbeds,
351            source: None,
352        })
353    } else {
354        let mut chars = 0;
355        for (idx, embed) in embeds.iter().enumerate() {
356            chars += embed_chars(embed);
357
358            if chars > EMBED_TOTAL_LENGTH {
359                return Err(MessageValidationError {
360                    kind: MessageValidationErrorType::EmbedInvalid {
361                        idx,
362                        kind: EmbedValidationErrorType::EmbedTooLarge { chars },
363                    },
364                    source: None,
365                });
366            }
367
368            crate::embed::embed(embed).map_err(|source| {
369                let (kind, source) = source.into_parts();
370
371                MessageValidationError {
372                    kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
373                    source,
374                }
375            })?;
376        }
377
378        Ok(())
379    }
380}
381
382/// Ensure that the amount of stickers in a message is correct.
383///
384/// There must be at most [`STICKER_MAX`] stickers. This is based on [this
385/// documentation entry].
386///
387/// # Errors
388///
389/// Returns an error of type [`StickersInvalid`] if the length is invalid.
390///
391/// [`StickersInvalid`]: MessageValidationErrorType::StickersInvalid
392/// [this documentation entry]: https://discord.com/developers/docs/resources/channel#create-message-jsonform-params
393pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
394    let len = sticker_ids.len();
395
396    if len <= STICKER_MAX {
397        Ok(())
398    } else {
399        Err(MessageValidationError {
400            kind: MessageValidationErrorType::StickersInvalid { len },
401            source: None,
402        })
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn attachment_description_limit() {
412        assert!(attachment_description("").is_ok());
413        assert!(attachment_description(str::repeat("a", 1024)).is_ok());
414
415        assert!(matches!(
416            attachment_description(str::repeat("a", 1025))
417                .unwrap_err()
418                .kind(),
419            MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
420        ));
421    }
422
423    #[test]
424    fn attachment_allowed_filename() {
425        assert!(attachment_filename("one.jpg").is_ok());
426        assert!(attachment_filename("two.png").is_ok());
427        assert!(attachment_filename("three.gif").is_ok());
428        assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
429
430        assert!(attachment_filename("????????").is_err());
431    }
432
433    #[test]
434    fn content_length() {
435        assert!(content("").is_ok());
436        assert!(content("a".repeat(2000)).is_ok());
437
438        assert!(content("a".repeat(2001)).is_err());
439    }
440}