use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
};
use twilight_model::channel::message::Embed;
pub const AUTHOR_NAME_LENGTH: usize = 256;
pub const COLOR_MAXIMUM: u32 = 0xff_ff_ff;
pub const DESCRIPTION_LENGTH: usize = 4096;
pub const EMBED_TOTAL_LENGTH: usize = 6000;
pub const FIELD_COUNT: usize = 25;
pub const FIELD_NAME_LENGTH: usize = 256;
pub const FIELD_VALUE_LENGTH: usize = 1024;
pub const FOOTER_TEXT_LENGTH: usize = 2048;
pub const TITLE_LENGTH: usize = 256;
#[derive(Debug)]
pub struct EmbedValidationError {
kind: EmbedValidationErrorType,
}
impl EmbedValidationError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &EmbedValidationErrorType {
&self.kind
}
#[allow(clippy::unused_self)]
#[must_use = "consuming the error and retrieving the source has no effect if left unused"]
pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
None
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
EmbedValidationErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, None)
}
}
impl Display for EmbedValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
EmbedValidationErrorType::AuthorNameTooLarge { chars } => {
f.write_str("the author name is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&AUTHOR_NAME_LENGTH, f)
}
EmbedValidationErrorType::ColorNotRgb { color } => {
f.write_str("the color is ")?;
Display::fmt(color, f)?;
f.write_str(", but it must be less than ")?;
Display::fmt(&COLOR_MAXIMUM, f)
}
EmbedValidationErrorType::DescriptionTooLarge { chars } => {
f.write_str("the description is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&DESCRIPTION_LENGTH, f)
}
EmbedValidationErrorType::EmbedTooLarge { chars } => {
f.write_str("the combined total length of the embed is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&EMBED_TOTAL_LENGTH, f)
}
EmbedValidationErrorType::FieldNameTooLarge { chars } => {
f.write_str("a field name is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&FIELD_NAME_LENGTH, f)
}
EmbedValidationErrorType::FieldValueTooLarge { chars } => {
f.write_str("a field value is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&FIELD_VALUE_LENGTH, f)
}
EmbedValidationErrorType::FooterTextTooLarge { chars } => {
f.write_str("the footer's text is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&FOOTER_TEXT_LENGTH, f)
}
EmbedValidationErrorType::TitleTooLarge { chars } => {
f.write_str("the title's length is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&TITLE_LENGTH, f)
}
EmbedValidationErrorType::TooManyFields { amount } => {
f.write_str("there are ")?;
Display::fmt(amount, f)?;
f.write_str(" fields, but the maximum amount is ")?;
Display::fmt(&FIELD_COUNT, f)
}
}
}
}
impl Error for EmbedValidationError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum EmbedValidationErrorType {
AuthorNameTooLarge {
chars: usize,
},
ColorNotRgb {
color: u32,
},
DescriptionTooLarge {
chars: usize,
},
EmbedTooLarge {
chars: usize,
},
FieldNameTooLarge {
chars: usize,
},
FieldValueTooLarge {
chars: usize,
},
FooterTextTooLarge {
chars: usize,
},
TitleTooLarge {
chars: usize,
},
TooManyFields {
amount: usize,
},
}
pub fn embed(embed: &Embed) -> Result<(), EmbedValidationError> {
let chars = self::chars(embed);
if chars > EMBED_TOTAL_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::EmbedTooLarge { chars },
});
}
if let Some(color) = embed.color {
if color > COLOR_MAXIMUM {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::ColorNotRgb { color },
});
}
}
if let Some(description) = embed.description.as_ref() {
let chars = description.chars().count();
if chars > DESCRIPTION_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::DescriptionTooLarge { chars },
});
}
}
if embed.fields.len() > FIELD_COUNT {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::TooManyFields {
amount: embed.fields.len(),
},
});
}
for field in &embed.fields {
let name_chars = field.name.chars().count();
if name_chars > FIELD_NAME_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FieldNameTooLarge { chars: name_chars },
});
}
let value_chars = field.value.chars().count();
if value_chars > FIELD_VALUE_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FieldValueTooLarge { chars: value_chars },
});
}
}
if let Some(footer) = embed.footer.as_ref() {
let chars = footer.text.chars().count();
if chars > FOOTER_TEXT_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FooterTextTooLarge { chars },
});
}
}
if let Some(name) = embed.author.as_ref().map(|author| &author.name) {
let chars = name.chars().count();
if chars > AUTHOR_NAME_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::AuthorNameTooLarge { chars },
});
}
}
if let Some(title) = embed.title.as_ref() {
let chars = title.chars().count();
if chars > TITLE_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::TitleTooLarge { chars },
});
}
}
Ok(())
}
#[must_use]
pub fn chars(embed: &Embed) -> usize {
let mut chars = 0;
if let Some(author) = &embed.author {
chars += author.name.len();
}
if let Some(description) = &embed.description {
chars += description.len();
}
if let Some(footer) = &embed.footer {
chars += footer.text.len();
}
for field in &embed.fields {
chars += field.name.len();
chars += field.value.len();
}
if let Some(title) = &embed.title {
chars += title.len();
}
chars
}
#[cfg(test)]
mod tests {
use super::{EmbedValidationError, EmbedValidationErrorType};
use static_assertions::assert_impl_all;
use std::fmt::Debug;
use twilight_model::channel::message::{
embed::{EmbedAuthor, EmbedField, EmbedFooter},
Embed,
};
assert_impl_all!(EmbedValidationErrorType: Debug, Send, Sync);
assert_impl_all!(EmbedValidationError: Debug, Send, Sync);
fn base_embed() -> Embed {
Embed {
author: None,
color: None,
description: None,
fields: Vec::new(),
footer: None,
image: None,
kind: "rich".to_owned(),
provider: None,
thumbnail: None,
timestamp: None,
title: None,
url: None,
video: None,
}
}
#[test]
fn embed_base() {
let embed = base_embed();
assert!(super::embed(&embed).is_ok());
}
#[test]
fn embed_normal() {
let mut embed = base_embed();
embed.author.replace(EmbedAuthor {
icon_url: None,
name: "twilight".to_owned(),
proxy_icon_url: None,
url: None,
});
embed.color.replace(0xff_00_00);
embed.description.replace("a".repeat(100));
embed.fields.push(EmbedField {
inline: true,
name: "b".repeat(25),
value: "c".repeat(200),
});
embed.title.replace("this is a normal title".to_owned());
assert!(super::embed(&embed).is_ok());
}
#[test]
fn embed_author_name_limit() {
let mut embed = base_embed();
embed.author.replace(EmbedAuthor {
icon_url: None,
name: str::repeat("a", 256),
proxy_icon_url: None,
url: None,
});
assert!(super::embed(&embed).is_ok());
embed.author.replace(EmbedAuthor {
icon_url: None,
name: str::repeat("a", 257),
proxy_icon_url: None,
url: None,
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
));
}
#[test]
fn embed_description_limit() {
let mut embed = base_embed();
embed.description.replace(str::repeat("a", 2048));
assert!(super::embed(&embed).is_ok());
embed.description.replace(str::repeat("a", 4096));
assert!(super::embed(&embed).is_ok());
embed.description.replace(str::repeat("a", 4097));
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::DescriptionTooLarge { chars: 4097 }
));
}
#[test]
fn embed_field_count_limit() {
let mut embed = base_embed();
for _ in 0..26 {
embed.fields.push(EmbedField {
inline: true,
name: "a".to_owned(),
value: "a".to_owned(),
});
}
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::TooManyFields { amount: 26 }
));
}
#[test]
fn embed_field_name_limit() {
let mut embed = base_embed();
embed.fields.push(EmbedField {
inline: true,
name: str::repeat("a", 256),
value: "a".to_owned(),
});
assert!(super::embed(&embed).is_ok());
embed.fields.push(EmbedField {
inline: true,
name: str::repeat("a", 257),
value: "a".to_owned(),
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::FieldNameTooLarge { chars: 257 }
));
}
#[test]
fn embed_field_value_limit() {
let mut embed = base_embed();
embed.fields.push(EmbedField {
inline: true,
name: "a".to_owned(),
value: str::repeat("a", 1024),
});
assert!(super::embed(&embed).is_ok());
embed.fields.push(EmbedField {
inline: true,
name: "a".to_owned(),
value: str::repeat("a", 1025),
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::FieldValueTooLarge { chars: 1025 }
));
}
#[test]
fn embed_footer_text_limit() {
let mut embed = base_embed();
embed.footer.replace(EmbedFooter {
icon_url: None,
proxy_icon_url: None,
text: str::repeat("a", 2048),
});
assert!(super::embed(&embed).is_ok());
embed.footer.replace(EmbedFooter {
icon_url: None,
proxy_icon_url: None,
text: str::repeat("a", 2049),
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::FooterTextTooLarge { chars: 2049 }
));
}
#[test]
fn embed_title_limit() {
let mut embed = base_embed();
embed.title.replace(str::repeat("a", 256));
assert!(super::embed(&embed).is_ok());
embed.title.replace(str::repeat("a", 257));
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::TitleTooLarge { chars: 257 }
));
}
#[test]
fn embed_combined_limit() {
let mut embed = base_embed();
embed.description.replace(str::repeat("a", 2048));
embed.title.replace(str::repeat("a", 256));
for _ in 0..5 {
embed.fields.push(EmbedField {
inline: true,
name: str::repeat("a", 100),
value: str::repeat("a", 500),
});
}
assert!(super::embed(&embed).is_ok());
embed.footer.replace(EmbedFooter {
icon_url: None,
proxy_icon_url: None,
text: str::repeat("a", 1000),
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::EmbedTooLarge { chars: 6304 }
));
}
}