Skip to main content

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