use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
};
use twilight_model::channel::embed::Embed;
#[derive(Debug)]
pub struct EmbedValidationError {
kind: EmbedValidationErrorType,
}
impl EmbedValidationError {
pub const AUTHOR_NAME_LENGTH: usize = 256;
pub const DESCRIPTION_LENGTH: usize = 2048;
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;
#[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 } => write!(
f,
"the author name is {} characters long, but the max is {}",
chars,
Self::AUTHOR_NAME_LENGTH
),
EmbedValidationErrorType::DescriptionTooLarge { chars } => write!(
f,
"the description is {} characters long, but the max is {}",
chars,
Self::DESCRIPTION_LENGTH
),
EmbedValidationErrorType::EmbedTooLarge { chars } => write!(
f,
"the combined total length of the embed is {} characters long, but the max is {}",
chars,
Self::EMBED_TOTAL_LENGTH
),
EmbedValidationErrorType::FieldNameTooLarge { chars } => write!(
f,
"a field name is {} characters long, but the max is {}",
chars,
Self::FIELD_NAME_LENGTH
),
EmbedValidationErrorType::FieldValueTooLarge { chars } => write!(
f,
"a field value is {} characters long, but the max is {}",
chars,
Self::FIELD_VALUE_LENGTH
),
EmbedValidationErrorType::FooterTextTooLarge { chars } => write!(
f,
"the footer's text is {} characters long, but the max is {}",
chars,
Self::FOOTER_TEXT_LENGTH
),
EmbedValidationErrorType::TitleTooLarge { chars } => write!(
f,
"the title's length is {} characters long, but the max is {}",
chars,
Self::TITLE_LENGTH
),
EmbedValidationErrorType::TooManyFields { amount } => write!(
f,
"there are {} fields, but the maximum amount is {}",
amount,
Self::FIELD_COUNT
),
}
}
}
impl Error for EmbedValidationError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum EmbedValidationErrorType {
AuthorNameTooLarge {
chars: usize,
},
DescriptionTooLarge {
chars: usize,
},
EmbedTooLarge {
chars: usize,
},
FieldNameTooLarge {
chars: usize,
},
FieldValueTooLarge {
chars: usize,
},
FooterTextTooLarge {
chars: usize,
},
TitleTooLarge {
chars: usize,
},
TooManyFields {
amount: usize,
},
}
pub const fn ban_delete_message_days(value: u64) -> bool {
value <= 7
}
pub fn channel_name(value: impl AsRef<str>) -> bool {
_channel_name(value.as_ref())
}
fn _channel_name(value: &str) -> bool {
let len = value.chars().count();
(1..=100).contains(&len)
}
pub fn content_limit(value: impl AsRef<str>) -> bool {
_content_limit(value.as_ref())
}
fn _content_limit(value: &str) -> bool {
value.chars().count() <= 2000
}
pub fn embed(embed: &Embed) -> Result<(), EmbedValidationError> {
let mut total = 0;
if embed.fields.len() > EmbedValidationError::FIELD_COUNT {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::TooManyFields {
amount: embed.fields.len(),
},
});
}
if let Some(name) = embed
.author
.as_ref()
.and_then(|author| author.name.as_ref())
{
let chars = name.chars().count();
if chars > EmbedValidationError::AUTHOR_NAME_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::AuthorNameTooLarge { chars },
});
}
total += chars;
}
if let Some(description) = embed.description.as_ref() {
let chars = description.chars().count();
if chars > EmbedValidationError::DESCRIPTION_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::DescriptionTooLarge { chars },
});
}
total += chars;
}
if let Some(footer) = embed.footer.as_ref() {
let chars = footer.text.chars().count();
if chars > EmbedValidationError::FOOTER_TEXT_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FooterTextTooLarge { chars },
});
}
total += chars;
}
for field in &embed.fields {
let name_chars = field.name.chars().count();
if name_chars > EmbedValidationError::FIELD_NAME_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FieldNameTooLarge { chars: name_chars },
});
}
let value_chars = field.value.chars().count();
if value_chars > EmbedValidationError::FIELD_VALUE_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::FieldValueTooLarge { chars: value_chars },
});
}
total += name_chars + value_chars;
}
if let Some(title) = embed.title.as_ref() {
let chars = title.chars().count();
if chars > EmbedValidationError::TITLE_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::TitleTooLarge { chars },
});
}
total += chars;
}
if total > EmbedValidationError::EMBED_TOTAL_LENGTH {
return Err(EmbedValidationError {
kind: EmbedValidationErrorType::EmbedTooLarge { chars: total },
});
}
Ok(())
}
pub const fn get_audit_log_limit(value: u64) -> bool {
value >= 1 && value <= 100
}
pub const fn get_channel_messages_limit(value: u64) -> bool {
value >= 1 && value <= 100
}
pub const fn get_current_user_guilds_limit(value: u64) -> bool {
value >= 1 && value <= 100
}
pub const fn get_guild_members_limit(value: u64) -> bool {
value >= 1 && value <= 1000
}
pub const fn search_guild_members_limit(value: u64) -> bool {
value > 0 && value <= 1000
}
pub const fn get_reactions_limit(value: u64) -> bool {
value >= 1 && value <= 100
}
pub fn guild_name(value: impl AsRef<str>) -> bool {
_guild_name(value.as_ref())
}
fn _guild_name(value: &str) -> bool {
let len = value.chars().count();
(2..=100).contains(&len)
}
pub const fn guild_prune_days(value: u64) -> bool {
value > 0 && value <= 30
}
pub const fn invite_max_age(value: u64) -> bool {
value <= 604_800
}
pub const fn invite_max_uses(value: u64) -> bool {
value <= 100
}
pub fn nickname(value: impl AsRef<str>) -> bool {
_nickname(value.as_ref())
}
fn _nickname(value: &str) -> bool {
let len = value.chars().count();
(1..=32).contains(&len)
}
pub fn username(value: impl AsRef<str>) -> bool {
_username(value.as_ref())
}
fn _username(value: &str) -> bool {
let len = value.chars().count();
(2..=32).contains(&len)
}
pub fn template_name(value: impl AsRef<str>) -> bool {
_template_name(value.as_ref())
}
fn _template_name(value: &str) -> bool {
let len = value.chars().count();
(1..=100).contains(&len)
}
pub fn template_description(value: impl AsRef<str>) -> bool {
_template_name(value.as_ref())
}
fn _template_description(value: &str) -> bool {
let len = value.chars().count();
(0..=120).contains(&len)
}
pub fn stage_topic(value: impl AsRef<str>) -> bool {
_stage_topic(value.as_ref())
}
fn _stage_topic(value: &str) -> bool {
let len = value.chars().count();
(0..=120).contains(&len)
}
pub fn command_name(value: impl AsRef<str>) -> bool {
_command_name(value.as_ref())
}
fn _command_name(value: &str) -> bool {
let len = value.chars().count();
(3..=32).contains(&len)
}
pub fn command_description(value: impl AsRef<str>) -> bool {
_command_description(value.as_ref())
}
fn _command_description(value: &str) -> bool {
let len = value.chars().count();
(1..=100).contains(&len)
}
pub fn command_permissions(len: usize) -> bool {
(0..=10).contains(&len)
}
#[cfg(test)]
mod tests {
use super::*;
use twilight_model::channel::embed::{EmbedAuthor, EmbedField, EmbedFooter};
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 test_ban_delete_message_days() {
assert!(ban_delete_message_days(0));
assert!(ban_delete_message_days(1));
assert!(ban_delete_message_days(7));
assert!(!ban_delete_message_days(8));
}
#[test]
fn test_channel_name() {
assert!(channel_name("a"));
assert!(channel_name("a".repeat(100)));
assert!(!channel_name(""));
assert!(!channel_name("a".repeat(101)));
}
#[test]
fn test_content_limit() {
assert!(content_limit(""));
assert!(content_limit("a".repeat(2000)));
assert!(!content_limit("a".repeat(2001)));
}
#[test]
fn test_embed_base() {
let embed = base_embed();
assert!(super::embed(&embed).is_ok());
}
#[test]
fn test_embed_normal() {
let mut embed = base_embed();
embed.author.replace(EmbedAuthor {
icon_url: None,
name: Some("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 test_embed_author_name_limit() {
let mut embed = base_embed();
embed.author.replace(EmbedAuthor {
icon_url: None,
name: Some(str::repeat("a", 256)),
proxy_icon_url: None,
url: None,
});
assert!(super::embed(&embed).is_ok());
embed.author.replace(EmbedAuthor {
icon_url: None,
name: Some(str::repeat("a", 257)),
proxy_icon_url: None,
url: None,
});
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::AuthorNameTooLarge { chars: 257 }
));
}
#[test]
fn test_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", 2049));
assert!(matches!(
super::embed(&embed).unwrap_err().kind(),
EmbedValidationErrorType::DescriptionTooLarge { chars: 2049 }
));
}
#[test]
fn test_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 test_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 test_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 test_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 test_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 test_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 }
));
}
#[test]
fn test_get_audit_log_limit() {
assert!(get_audit_log_limit(1));
assert!(get_audit_log_limit(100));
assert!(!get_audit_log_limit(0));
assert!(!get_audit_log_limit(101));
}
#[test]
fn test_get_channels_limit() {
assert!(get_channel_messages_limit(1));
assert!(get_channel_messages_limit(100));
assert!(!get_channel_messages_limit(0));
assert!(!get_channel_messages_limit(101));
}
#[test]
fn test_get_current_user_guilds_limit() {
assert!(get_current_user_guilds_limit(1));
assert!(get_current_user_guilds_limit(100));
assert!(!get_current_user_guilds_limit(0));
assert!(!get_current_user_guilds_limit(101));
}
#[test]
fn test_get_guild_members_limit() {
assert!(get_guild_members_limit(1));
assert!(get_guild_members_limit(1000));
assert!(!get_guild_members_limit(0));
assert!(!get_guild_members_limit(1001));
}
#[test]
fn test_get_reactions_limit() {
assert!(get_reactions_limit(1));
assert!(get_reactions_limit(100));
assert!(!get_reactions_limit(0));
assert!(!get_reactions_limit(101));
}
#[test]
fn test_guild_name() {
assert!(guild_name("aa"));
assert!(guild_name("a".repeat(100)));
assert!(!guild_name(""));
assert!(!guild_name("a"));
assert!(!guild_name("a".repeat(101)));
}
#[test]
fn test_guild_prune_days() {
assert!(!guild_prune_days(0));
assert!(guild_prune_days(1));
assert!(guild_prune_days(30));
assert!(!guild_prune_days(31));
assert!(!guild_prune_days(100));
}
#[test]
fn test_invite_max_age() {
assert!(invite_max_age(0));
assert!(invite_max_age(86_400));
assert!(invite_max_age(604_800));
assert!(!invite_max_age(604_801));
}
#[test]
fn test_invite_max_uses() {
assert!(invite_max_uses(0));
assert!(invite_max_uses(100));
assert!(!invite_max_uses(101));
}
#[test]
fn test_nickname() {
assert!(nickname("a"));
assert!(nickname("a".repeat(32)));
assert!(!nickname(""));
assert!(!nickname("a".repeat(33)));
}
#[test]
fn test_username() {
assert!(username("aa"));
assert!(username("a".repeat(32)));
assert!(!username("a"));
assert!(!username("a".repeat(33)));
}
}