1use std::{
4 error::Error,
5 fmt::{Display, Formatter, Result as FmtResult},
6};
7use twilight_model::channel::message::Embed;
8
9pub const AUTHOR_NAME_LENGTH: usize = 256;
11
12pub const COLOR_MAXIMUM: u32 = 0xff_ff_ff;
14
15pub const DESCRIPTION_LENGTH: usize = 4096;
17
18pub const EMBED_TOTAL_LENGTH: usize = 6000;
20
21pub const FIELD_COUNT: usize = 25;
23
24pub const FIELD_NAME_LENGTH: usize = 256;
26
27pub const FIELD_VALUE_LENGTH: usize = 1024;
29
30pub const FOOTER_TEXT_LENGTH: usize = 2048;
32
33pub const TITLE_LENGTH: usize = 256;
35
36#[derive(Debug)]
42pub struct EmbedValidationError {
43 kind: EmbedValidationErrorType,
45}
46
47impl EmbedValidationError {
48 #[must_use = "retrieving the type has no effect if left unused"]
50 pub const fn kind(&self) -> &EmbedValidationErrorType {
51 &self.kind
52 }
53
54 #[allow(clippy::unused_self)]
56 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
57 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
58 None
59 }
60
61 #[must_use = "consuming the error into its parts has no effect if left unused"]
63 pub fn into_parts(
64 self,
65 ) -> (
66 EmbedValidationErrorType,
67 Option<Box<dyn Error + Send + Sync>>,
68 ) {
69 (self.kind, None)
70 }
71}
72
73impl Display for EmbedValidationError {
74 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
75 match &self.kind {
76 EmbedValidationErrorType::AuthorNameTooLarge { chars } => {
77 f.write_str("the author name is ")?;
78 Display::fmt(chars, f)?;
79 f.write_str(" characters long, but the max is ")?;
80
81 Display::fmt(&AUTHOR_NAME_LENGTH, f)
82 }
83 EmbedValidationErrorType::ColorNotRgb { color } => {
84 f.write_str("the color is ")?;
85 Display::fmt(color, f)?;
86 f.write_str(", but it must be less than ")?;
87
88 Display::fmt(&COLOR_MAXIMUM, f)
89 }
90 EmbedValidationErrorType::DescriptionTooLarge { chars } => {
91 f.write_str("the description is ")?;
92 Display::fmt(chars, f)?;
93 f.write_str(" characters long, but the max is ")?;
94
95 Display::fmt(&DESCRIPTION_LENGTH, f)
96 }
97 EmbedValidationErrorType::EmbedTooLarge { chars } => {
98 f.write_str("the combined total length of the embed is ")?;
99 Display::fmt(chars, f)?;
100 f.write_str(" characters long, but the max is ")?;
101
102 Display::fmt(&EMBED_TOTAL_LENGTH, f)
103 }
104 EmbedValidationErrorType::FieldNameTooLarge { chars } => {
105 f.write_str("a field name is ")?;
106 Display::fmt(chars, f)?;
107 f.write_str(" characters long, but the max is ")?;
108
109 Display::fmt(&FIELD_NAME_LENGTH, f)
110 }
111 EmbedValidationErrorType::FieldValueTooLarge { chars } => {
112 f.write_str("a field value is ")?;
113 Display::fmt(chars, f)?;
114 f.write_str(" characters long, but the max is ")?;
115
116 Display::fmt(&FIELD_VALUE_LENGTH, f)
117 }
118 EmbedValidationErrorType::FooterTextTooLarge { chars } => {
119 f.write_str("the footer's text is ")?;
120 Display::fmt(chars, f)?;
121 f.write_str(" characters long, but the max is ")?;
122
123 Display::fmt(&FOOTER_TEXT_LENGTH, f)
124 }
125 EmbedValidationErrorType::TitleTooLarge { chars } => {
126 f.write_str("the title's length is ")?;
127 Display::fmt(chars, f)?;
128 f.write_str(" characters long, but the max is ")?;
129
130 Display::fmt(&TITLE_LENGTH, f)
131 }
132 EmbedValidationErrorType::TooManyFields { amount } => {
133 f.write_str("there are ")?;
134 Display::fmt(amount, f)?;
135 f.write_str(" fields, but the maximum amount is ")?;
136
137 Display::fmt(&FIELD_COUNT, f)
138 }
139 }
140 }
141}
142
143impl Error for EmbedValidationError {}
144
145#[derive(Debug)]
147#[non_exhaustive]
148pub enum EmbedValidationErrorType {
149 AuthorNameTooLarge {
151 chars: usize,
153 },
154 ColorNotRgb {
156 color: u32,
158 },
159 DescriptionTooLarge {
161 chars: usize,
163 },
164 EmbedTooLarge {
170 chars: usize,
172 },
173 FieldNameTooLarge {
175 chars: usize,
177 },
178 FieldValueTooLarge {
180 chars: usize,
182 },
183 FooterTextTooLarge {
185 chars: usize,
187 },
188 TitleTooLarge {
190 chars: usize,
192 },
193 TooManyFields {
195 amount: usize,
197 },
198}
199
200pub fn embed(embed: &Embed) -> Result<(), EmbedValidationError> {
241 let chars = self::chars(embed);
242
243 if chars > EMBED_TOTAL_LENGTH {
244 return Err(EmbedValidationError {
245 kind: EmbedValidationErrorType::EmbedTooLarge { chars },
246 });
247 }
248
249 if let Some(color) = embed.color
250 && color > COLOR_MAXIMUM
251 {
252 return Err(EmbedValidationError {
253 kind: EmbedValidationErrorType::ColorNotRgb { color },
254 });
255 }
256
257 if let Some(description) = embed.description.as_ref() {
258 let chars = description.chars().count();
259
260 if chars > DESCRIPTION_LENGTH {
261 return Err(EmbedValidationError {
262 kind: EmbedValidationErrorType::DescriptionTooLarge { chars },
263 });
264 }
265 }
266
267 if embed.fields.len() > FIELD_COUNT {
268 return Err(EmbedValidationError {
269 kind: EmbedValidationErrorType::TooManyFields {
270 amount: embed.fields.len(),
271 },
272 });
273 }
274
275 for field in &embed.fields {
276 let name_chars = field.name.chars().count();
277
278 if name_chars > FIELD_NAME_LENGTH {
279 return Err(EmbedValidationError {
280 kind: EmbedValidationErrorType::FieldNameTooLarge { chars: name_chars },
281 });
282 }
283
284 let value_chars = field.value.chars().count();
285
286 if value_chars > FIELD_VALUE_LENGTH {
287 return Err(EmbedValidationError {
288 kind: EmbedValidationErrorType::FieldValueTooLarge { chars: value_chars },
289 });
290 }
291 }
292
293 if let Some(footer) = embed.footer.as_ref() {
294 let chars = footer.text.chars().count();
295
296 if chars > FOOTER_TEXT_LENGTH {
297 return Err(EmbedValidationError {
298 kind: EmbedValidationErrorType::FooterTextTooLarge { chars },
299 });
300 }
301 }
302
303 if let Some(name) = embed.author.as_ref().map(|author| &author.name) {
304 let chars = name.chars().count();
305
306 if chars > AUTHOR_NAME_LENGTH {
307 return Err(EmbedValidationError {
308 kind: EmbedValidationErrorType::AuthorNameTooLarge { chars },
309 });
310 }
311 }
312
313 if let Some(title) = embed.title.as_ref() {
314 let chars = title.chars().count();
315
316 if chars > TITLE_LENGTH {
317 return Err(EmbedValidationError {
318 kind: EmbedValidationErrorType::TitleTooLarge { chars },
319 });
320 }
321 }
322
323 Ok(())
324}
325
326#[must_use]
328pub fn chars(embed: &Embed) -> usize {
329 let mut chars = 0;
330
331 if let Some(author) = &embed.author {
332 chars += author.name.chars().count();
333 }
334
335 if let Some(description) = &embed.description {
336 chars += description.chars().count();
337 }
338
339 if let Some(footer) = &embed.footer {
340 chars += footer.text.chars().count();
341 }
342
343 for field in &embed.fields {
344 chars += field.name.chars().count();
345 chars += field.value.chars().count();
346 }
347
348 if let Some(title) = &embed.title {
349 chars += title.chars().count();
350 }
351
352 chars
353}
354
355#[cfg(test)]
356mod tests {
357 use super::{EmbedValidationError, EmbedValidationErrorType};
358 use static_assertions::assert_impl_all;
359 use std::fmt::Debug;
360 use twilight_model::channel::message::{
361 Embed,
362 embed::{EmbedAuthor, EmbedField, EmbedFooter},
363 };
364
365 assert_impl_all!(EmbedValidationErrorType: Debug, Send, Sync);
366 assert_impl_all!(EmbedValidationError: Debug, Send, Sync);
367
368 fn base_embed() -> Embed {
369 Embed {
370 author: None,
371 color: None,
372 description: None,
373 fields: Vec::new(),
374 flags: None,
375 footer: None,
376 image: None,
377 kind: "rich".to_owned(),
378 provider: None,
379 thumbnail: None,
380 timestamp: None,
381 title: None,
382 url: None,
383 video: None,
384 }
385 }
386
387 #[test]
388 fn embed_base() {
389 let embed = base_embed();
390
391 assert!(super::embed(&embed).is_ok());
392 }
393
394 #[test]
395 fn embed_normal() {
396 let mut embed = base_embed();
397 embed.author.replace(EmbedAuthor {
398 icon_url: None,
399 name: "twilight".to_owned(),
400 proxy_icon_url: None,
401 url: None,
402 });
403 embed.color.replace(0xff_00_00);
404 embed.description.replace("a".repeat(100));
405 embed.fields.push(EmbedField {
406 inline: true,
407 name: "b".repeat(25),
408 value: "c".repeat(200),
409 });
410 embed.title.replace("this is a normal title".to_owned());
411
412 assert!(super::embed(&embed).is_ok());
413 }
414
415 #[test]
416 fn embed_author_name_limit() {
417 let mut embed = base_embed();
418 embed.author.replace(EmbedAuthor {
419 icon_url: None,
420 name: str::repeat("a", 256),
421 proxy_icon_url: None,
422 url: None,
423 });
424 assert!(super::embed(&embed).is_ok());
425
426 embed.author.replace(EmbedAuthor {
427 icon_url: None,
428 name: str::repeat("a", 257),
429 proxy_icon_url: None,
430 url: None,
431 });
432 assert!(matches!(
433 super::embed(&embed).unwrap_err().kind(),
434 EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
435 ));
436 }
437
438 #[test]
439 fn embed_description_limit() {
440 let mut embed = base_embed();
441 embed.description.replace(str::repeat("a", 2048));
442 assert!(super::embed(&embed).is_ok());
443
444 embed.description.replace(str::repeat("a", 4096));
445 assert!(super::embed(&embed).is_ok());
446
447 embed.description.replace(str::repeat("a", 4097));
448 assert!(matches!(
449 super::embed(&embed).unwrap_err().kind(),
450 EmbedValidationErrorType::DescriptionTooLarge { chars: 4097 }
451 ));
452 }
453
454 #[test]
455 fn embed_field_count_limit() {
456 let mut embed = base_embed();
457
458 for _ in 0..26 {
459 embed.fields.push(EmbedField {
460 inline: true,
461 name: "a".to_owned(),
462 value: "a".to_owned(),
463 });
464 }
465
466 assert!(matches!(
467 super::embed(&embed).unwrap_err().kind(),
468 EmbedValidationErrorType::TooManyFields { amount: 26 }
469 ));
470 }
471
472 #[test]
473 fn embed_field_name_limit() {
474 let mut embed = base_embed();
475 embed.fields.push(EmbedField {
476 inline: true,
477 name: str::repeat("a", 256),
478 value: "a".to_owned(),
479 });
480 assert!(super::embed(&embed).is_ok());
481
482 embed.fields.push(EmbedField {
483 inline: true,
484 name: str::repeat("a", 257),
485 value: "a".to_owned(),
486 });
487 assert!(matches!(
488 super::embed(&embed).unwrap_err().kind(),
489 EmbedValidationErrorType::FieldNameTooLarge { chars: 257 }
490 ));
491 }
492
493 #[test]
494 fn embed_field_value_limit() {
495 let mut embed = base_embed();
496 embed.fields.push(EmbedField {
497 inline: true,
498 name: "a".to_owned(),
499 value: str::repeat("a", 1024),
500 });
501 assert!(super::embed(&embed).is_ok());
502
503 embed.fields.push(EmbedField {
504 inline: true,
505 name: "a".to_owned(),
506 value: str::repeat("a", 1025),
507 });
508 assert!(matches!(
509 super::embed(&embed).unwrap_err().kind(),
510 EmbedValidationErrorType::FieldValueTooLarge { chars: 1025 }
511 ));
512 }
513
514 #[test]
515 fn embed_footer_text_limit() {
516 let mut embed = base_embed();
517 embed.footer.replace(EmbedFooter {
518 icon_url: None,
519 proxy_icon_url: None,
520 text: str::repeat("a", 2048),
521 });
522 assert!(super::embed(&embed).is_ok());
523
524 embed.footer.replace(EmbedFooter {
525 icon_url: None,
526 proxy_icon_url: None,
527 text: str::repeat("a", 2049),
528 });
529 assert!(matches!(
530 super::embed(&embed).unwrap_err().kind(),
531 EmbedValidationErrorType::FooterTextTooLarge { chars: 2049 }
532 ));
533 }
534
535 #[test]
536 fn embed_title_limit() {
537 let mut embed = base_embed();
538 embed.title.replace(str::repeat("a", 256));
539 assert!(super::embed(&embed).is_ok());
540
541 embed.title.replace(str::repeat("a", 257));
542 assert!(matches!(
543 super::embed(&embed).unwrap_err().kind(),
544 EmbedValidationErrorType::TitleTooLarge { chars: 257 }
545 ));
546 }
547
548 #[test]
549 fn embed_combined_limit() {
550 let mut embed = base_embed();
551 embed.description.replace(str::repeat("a", 2048));
552 embed.title.replace(str::repeat("a", 256));
553
554 for _ in 0..5 {
555 embed.fields.push(EmbedField {
556 inline: true,
557 name: str::repeat("a", 100),
558 value: str::repeat("a", 500),
559 });
560 }
561
562 assert!(super::embed(&embed).is_ok());
564
565 embed.footer.replace(EmbedFooter {
566 icon_url: None,
567 proxy_icon_url: None,
568 text: str::repeat("a", 1000),
569 });
570
571 assert!(matches!(
572 super::embed(&embed).unwrap_err().kind(),
573 EmbedValidationErrorType::EmbedTooLarge { chars: 6304 }
574 ));
575 }
576}