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 if color > COLOR_MAXIMUM {
251 return Err(EmbedValidationError {
252 kind: EmbedValidationErrorType::ColorNotRgb { color },
253 });
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.len();
333 }
334
335 if let Some(description) = &embed.description {
336 chars += description.len();
337 }
338
339 if let Some(footer) = &embed.footer {
340 chars += footer.text.len();
341 }
342
343 for field in &embed.fields {
344 chars += field.name.len();
345 chars += field.value.len();
346 }
347
348 if let Some(title) = &embed.title {
349 chars += title.len();
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::{EmbedAuthor, EmbedField, EmbedFooter},
362 Embed,
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 footer: None,
375 image: None,
376 kind: "rich".to_owned(),
377 provider: None,
378 thumbnail: None,
379 timestamp: None,
380 title: None,
381 url: None,
382 video: None,
383 }
384 }
385
386 #[test]
387 fn embed_base() {
388 let embed = base_embed();
389
390 assert!(super::embed(&embed).is_ok());
391 }
392
393 #[test]
394 fn embed_normal() {
395 let mut embed = base_embed();
396 embed.author.replace(EmbedAuthor {
397 icon_url: None,
398 name: "twilight".to_owned(),
399 proxy_icon_url: None,
400 url: None,
401 });
402 embed.color.replace(0xff_00_00);
403 embed.description.replace("a".repeat(100));
404 embed.fields.push(EmbedField {
405 inline: true,
406 name: "b".repeat(25),
407 value: "c".repeat(200),
408 });
409 embed.title.replace("this is a normal title".to_owned());
410
411 assert!(super::embed(&embed).is_ok());
412 }
413
414 #[test]
415 fn embed_author_name_limit() {
416 let mut embed = base_embed();
417 embed.author.replace(EmbedAuthor {
418 icon_url: None,
419 name: str::repeat("a", 256),
420 proxy_icon_url: None,
421 url: None,
422 });
423 assert!(super::embed(&embed).is_ok());
424
425 embed.author.replace(EmbedAuthor {
426 icon_url: None,
427 name: str::repeat("a", 257),
428 proxy_icon_url: None,
429 url: None,
430 });
431 assert!(matches!(
432 super::embed(&embed).unwrap_err().kind(),
433 EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
434 ));
435 }
436
437 #[test]
438 fn embed_description_limit() {
439 let mut embed = base_embed();
440 embed.description.replace(str::repeat("a", 2048));
441 assert!(super::embed(&embed).is_ok());
442
443 embed.description.replace(str::repeat("a", 4096));
444 assert!(super::embed(&embed).is_ok());
445
446 embed.description.replace(str::repeat("a", 4097));
447 assert!(matches!(
448 super::embed(&embed).unwrap_err().kind(),
449 EmbedValidationErrorType::DescriptionTooLarge { chars: 4097 }
450 ));
451 }
452
453 #[test]
454 fn embed_field_count_limit() {
455 let mut embed = base_embed();
456
457 for _ in 0..26 {
458 embed.fields.push(EmbedField {
459 inline: true,
460 name: "a".to_owned(),
461 value: "a".to_owned(),
462 });
463 }
464
465 assert!(matches!(
466 super::embed(&embed).unwrap_err().kind(),
467 EmbedValidationErrorType::TooManyFields { amount: 26 }
468 ));
469 }
470
471 #[test]
472 fn embed_field_name_limit() {
473 let mut embed = base_embed();
474 embed.fields.push(EmbedField {
475 inline: true,
476 name: str::repeat("a", 256),
477 value: "a".to_owned(),
478 });
479 assert!(super::embed(&embed).is_ok());
480
481 embed.fields.push(EmbedField {
482 inline: true,
483 name: str::repeat("a", 257),
484 value: "a".to_owned(),
485 });
486 assert!(matches!(
487 super::embed(&embed).unwrap_err().kind(),
488 EmbedValidationErrorType::FieldNameTooLarge { chars: 257 }
489 ));
490 }
491
492 #[test]
493 fn embed_field_value_limit() {
494 let mut embed = base_embed();
495 embed.fields.push(EmbedField {
496 inline: true,
497 name: "a".to_owned(),
498 value: str::repeat("a", 1024),
499 });
500 assert!(super::embed(&embed).is_ok());
501
502 embed.fields.push(EmbedField {
503 inline: true,
504 name: "a".to_owned(),
505 value: str::repeat("a", 1025),
506 });
507 assert!(matches!(
508 super::embed(&embed).unwrap_err().kind(),
509 EmbedValidationErrorType::FieldValueTooLarge { chars: 1025 }
510 ));
511 }
512
513 #[test]
514 fn embed_footer_text_limit() {
515 let mut embed = base_embed();
516 embed.footer.replace(EmbedFooter {
517 icon_url: None,
518 proxy_icon_url: None,
519 text: str::repeat("a", 2048),
520 });
521 assert!(super::embed(&embed).is_ok());
522
523 embed.footer.replace(EmbedFooter {
524 icon_url: None,
525 proxy_icon_url: None,
526 text: str::repeat("a", 2049),
527 });
528 assert!(matches!(
529 super::embed(&embed).unwrap_err().kind(),
530 EmbedValidationErrorType::FooterTextTooLarge { chars: 2049 }
531 ));
532 }
533
534 #[test]
535 fn embed_title_limit() {
536 let mut embed = base_embed();
537 embed.title.replace(str::repeat("a", 256));
538 assert!(super::embed(&embed).is_ok());
539
540 embed.title.replace(str::repeat("a", 257));
541 assert!(matches!(
542 super::embed(&embed).unwrap_err().kind(),
543 EmbedValidationErrorType::TitleTooLarge { chars: 257 }
544 ));
545 }
546
547 #[test]
548 fn embed_combined_limit() {
549 let mut embed = base_embed();
550 embed.description.replace(str::repeat("a", 2048));
551 embed.title.replace(str::repeat("a", 256));
552
553 for _ in 0..5 {
554 embed.fields.push(EmbedField {
555 inline: true,
556 name: str::repeat("a", 100),
557 value: str::repeat("a", 500),
558 });
559 }
560
561 assert!(super::embed(&embed).is_ok());
563
564 embed.footer.replace(EmbedFooter {
565 icon_url: None,
566 proxy_icon_url: None,
567 text: str::repeat("a", 1000),
568 });
569
570 assert!(matches!(
571 super::embed(&embed).unwrap_err().kind(),
572 EmbedValidationErrorType::EmbedTooLarge { chars: 6304 }
573 ));
574 }
575}