1use 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
20pub const ATTACHMENT_TITLE_LENGTH_MAX: usize = 1024;
22
23pub const ATTACHMENT_DESCIPTION_LENGTH_MAX: usize = 1024;
25
26pub const ATTACHMENT_WAVEFORM_LENGTH_MAX: usize = 400;
28
29pub const EMBED_COUNT_LIMIT: usize = 10;
31
32pub const MESSAGE_CONTENT_LENGTH_MAX: usize = 2000;
34
35pub const STICKER_MAX: usize = 3;
37
38const DASH: char = '-';
40
41const DOT: char = '.';
43
44const UNDERSCORE: char = '_';
46
47#[derive(Debug)]
49pub struct MessageValidationError {
50 kind: MessageValidationErrorType,
52 source: Option<Box<dyn Error + Send + Sync>>,
54}
55
56impl MessageValidationError {
57 #[must_use = "retrieving the type has no effect if left unused"]
59 pub const fn kind(&self) -> &MessageValidationErrorType {
60 &self.kind
61 }
62
63 #[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 #[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 #[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#[derive(Debug)]
167pub enum MessageValidationErrorType {
168 AttachmentFilename {
170 filename: String,
172 },
173 AttachmentTitleTooLarge {
175 chars: usize,
177 },
178 AttachmentDescriptionTooLarge {
180 chars: usize,
182 },
183 AttachmentWaveformTooLarge {
185 chars: usize,
187 },
188 ComponentCount {
190 count: usize,
192 is_v2: bool,
194 },
195 ComponentInvalid {
197 idx: usize,
199 kind: ComponentValidationErrorType,
201 },
202 ContentInvalid,
204 EmbedInvalid {
206 idx: usize,
208 kind: EmbedValidationErrorType,
210 },
211 StickersInvalid {
213 len: usize,
215 },
216 TooManyEmbeds,
220 WebhookUsername,
222}
223
224pub 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
262pub 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
282pub 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
302pub 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
329pub 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
349pub 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
402pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
411 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
422pub 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
468pub 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}