use crate::{
component::{COMPONENT_COUNT, COMPONENT_V2_COUNT, ComponentValidationErrorType},
embed::{EMBED_TOTAL_LENGTH, EmbedValidationErrorType, chars as embed_chars},
request::ValidationError,
};
use std::{
error::Error,
fmt::{Display, Formatter, Result as FmtResult},
};
use twilight_model::{
channel::message::{Component, Embed},
http::attachment::Attachment,
id::{Id, marker::StickerMarker},
};
pub const ATTACHMENT_DESCIPTION_LENGTH_MAX: usize = 1024;
pub const EMBED_COUNT_LIMIT: usize = 10;
pub const MESSAGE_CONTENT_LENGTH_MAX: usize = 2000;
pub const STICKER_MAX: usize = 3;
const DASH: char = '-';
const DOT: char = '.';
const UNDERSCORE: char = '_';
#[derive(Debug)]
pub struct MessageValidationError {
kind: MessageValidationErrorType,
source: Option<Box<dyn Error + Send + Sync>>,
}
impl MessageValidationError {
#[must_use = "retrieving the type has no effect if left unused"]
pub const fn kind(&self) -> &MessageValidationErrorType {
&self.kind
}
#[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>> {
self.source
}
#[must_use = "consuming the error into its parts has no effect if left unused"]
pub fn into_parts(
self,
) -> (
MessageValidationErrorType,
Option<Box<dyn Error + Send + Sync>>,
) {
(self.kind, self.source)
}
#[must_use = "has no effect if unused"]
pub fn from_validation_error(
kind: MessageValidationErrorType,
source: ValidationError,
) -> Self {
Self {
kind,
source: Some(Box::new(source)),
}
}
}
impl Display for MessageValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match &self.kind {
MessageValidationErrorType::AttachmentDescriptionTooLarge { chars } => {
f.write_str("the attachment description is ")?;
Display::fmt(chars, f)?;
f.write_str(" characters long, but the max is ")?;
Display::fmt(&ATTACHMENT_DESCIPTION_LENGTH_MAX, f)
}
MessageValidationErrorType::AttachmentFilename { filename } => {
f.write_str("attachment filename `")?;
Display::fmt(filename, f)?;
f.write_str("`is invalid")
}
MessageValidationErrorType::ComponentCount { count, is_v2 } => {
Display::fmt(count, f)?;
f.write_str(" components were provided, but only ")?;
if *is_v2 {
Display::fmt(&COMPONENT_V2_COUNT, f)?;
} else {
Display::fmt(&COMPONENT_COUNT, f)?;
}
f.write_str(" root components are allowed")
}
MessageValidationErrorType::ComponentInvalid { .. } => {
f.write_str("a provided component is invalid")
}
MessageValidationErrorType::ContentInvalid => f.write_str("message content is invalid"),
MessageValidationErrorType::EmbedInvalid { idx, .. } => {
f.write_str("embed at index ")?;
Display::fmt(idx, f)?;
f.write_str(" is invalid")
}
MessageValidationErrorType::StickersInvalid { len } => {
f.write_str("amount of stickers provided is ")?;
Display::fmt(len, f)?;
f.write_str(" but it must be at most ")?;
Display::fmt(&STICKER_MAX, f)
}
MessageValidationErrorType::TooManyEmbeds => f.write_str("message has too many embeds"),
MessageValidationErrorType::WebhookUsername => {
if let Some(source) = self.source() {
Display::fmt(&source, f)
} else {
f.write_str("webhook username is invalid")
}
}
}
}
}
impl Error for MessageValidationError {}
#[derive(Debug)]
pub enum MessageValidationErrorType {
AttachmentFilename {
filename: String,
},
AttachmentDescriptionTooLarge {
chars: usize,
},
ComponentCount {
count: usize,
is_v2: bool,
},
ComponentInvalid {
idx: usize,
kind: ComponentValidationErrorType,
},
ContentInvalid,
EmbedInvalid {
idx: usize,
kind: EmbedValidationErrorType,
},
StickersInvalid {
len: usize,
},
TooManyEmbeds,
WebhookUsername,
}
pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
attachment_filename(&attachment.filename)?;
if let Some(description) = &attachment.description {
attachment_description(description)?;
}
Ok(())
}
pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
let chars = description.as_ref().chars().count();
if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
Ok(())
} else {
Err(MessageValidationError {
kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
source: None,
})
}
}
pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
if filename
.as_ref()
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE)
{
Ok(())
} else {
Err(MessageValidationError {
kind: MessageValidationErrorType::AttachmentFilename {
filename: filename.as_ref().to_string(),
},
source: None,
})
}
}
pub fn components(components: &[Component], is_v2: bool) -> Result<(), MessageValidationError> {
if is_v2 {
let count = components
.iter()
.map(Component::component_count)
.sum::<usize>();
if count > COMPONENT_V2_COUNT {
return Err(MessageValidationError {
kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
source: None,
});
}
} else {
let count = components.len();
if count > COMPONENT_COUNT {
return Err(MessageValidationError {
kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
source: None,
});
}
}
let function = if is_v2 {
crate::component::component_v2
} else {
crate::component::component_v1
};
for (idx, component) in components.iter().enumerate() {
function(component).map_err(|source| {
let (kind, source) = source.into_parts();
MessageValidationError {
kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
source,
}
})?;
}
Ok(())
}
pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
Ok(())
} else {
Err(MessageValidationError {
kind: MessageValidationErrorType::ContentInvalid,
source: None,
})
}
}
pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
if embeds.len() > EMBED_COUNT_LIMIT {
Err(MessageValidationError {
kind: MessageValidationErrorType::TooManyEmbeds,
source: None,
})
} else {
let mut chars = 0;
for (idx, embed) in embeds.iter().enumerate() {
chars += embed_chars(embed);
if chars > EMBED_TOTAL_LENGTH {
return Err(MessageValidationError {
kind: MessageValidationErrorType::EmbedInvalid {
idx,
kind: EmbedValidationErrorType::EmbedTooLarge { chars },
},
source: None,
});
}
crate::embed::embed(embed).map_err(|source| {
let (kind, source) = source.into_parts();
MessageValidationError {
kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
source,
}
})?;
}
Ok(())
}
}
pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
let len = sticker_ids.len();
if len <= STICKER_MAX {
Ok(())
} else {
Err(MessageValidationError {
kind: MessageValidationErrorType::StickersInvalid { len },
source: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attachment_description_limit() {
assert!(attachment_description("").is_ok());
assert!(attachment_description(str::repeat("a", 1024)).is_ok());
assert!(matches!(
attachment_description(str::repeat("a", 1025))
.unwrap_err()
.kind(),
MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
));
}
#[test]
fn attachment_allowed_filename() {
assert!(attachment_filename("one.jpg").is_ok());
assert!(attachment_filename("two.png").is_ok());
assert!(attachment_filename("three.gif").is_ok());
assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
assert!(attachment_filename("????????").is_err());
}
#[test]
fn content_length() {
assert!(content("").is_ok());
assert!(content("a".repeat(2000)).is_ok());
assert!(content("a".repeat(2001)).is_err());
}
}