1use crate::{
6 component::{ComponentValidationErrorType, COMPONENT_COUNT, COMPONENT_V2_COUNT},
7 embed::{chars as embed_chars, EmbedValidationErrorType, EMBED_TOTAL_LENGTH},
8 request::ValidationError,
9};
10use std::{
11 error::Error,
12 fmt::{Display, Formatter, Result as FmtResult},
13};
14use twilight_model::{
15 channel::message::{Component, Embed},
16 http::attachment::Attachment,
17 id::{marker::StickerMarker, Id},
18};
19
20pub const ATTACHMENT_DESCIPTION_LENGTH_MAX: usize = 1024;
22
23pub const EMBED_COUNT_LIMIT: usize = 10;
25
26pub const MESSAGE_CONTENT_LENGTH_MAX: usize = 2000;
28
29pub const STICKER_MAX: usize = 3;
31
32const DASH: char = '-';
34
35const DOT: char = '.';
37
38const UNDERSCORE: char = '_';
40
41#[derive(Debug)]
43pub struct MessageValidationError {
44 kind: MessageValidationErrorType,
46 source: Option<Box<dyn Error + Send + Sync>>,
48}
49
50impl MessageValidationError {
51 #[must_use = "retrieving the type has no effect if left unused"]
53 pub const fn kind(&self) -> &MessageValidationErrorType {
54 &self.kind
55 }
56
57 #[must_use = "consuming the error and retrieving the source has no effect if left unused"]
59 pub fn into_source(self) -> Option<Box<dyn Error + Send + Sync>> {
60 self.source
61 }
62
63 #[must_use = "consuming the error into its parts has no effect if left unused"]
65 pub fn into_parts(
66 self,
67 ) -> (
68 MessageValidationErrorType,
69 Option<Box<dyn Error + Send + Sync>>,
70 ) {
71 (self.kind, self.source)
72 }
73
74 #[must_use = "has no effect if unused"]
76 pub fn from_validation_error(
77 kind: MessageValidationErrorType,
78 source: ValidationError,
79 ) -> Self {
80 Self {
81 kind,
82 source: Some(Box::new(source)),
83 }
84 }
85}
86
87impl Display for MessageValidationError {
88 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
89 match &self.kind {
90 MessageValidationErrorType::AttachmentDescriptionTooLarge { chars } => {
91 f.write_str("the attachment description is ")?;
92 Display::fmt(chars, f)?;
93 f.write_str(" characters long, but the max is ")?;
94
95 Display::fmt(&ATTACHMENT_DESCIPTION_LENGTH_MAX, f)
96 }
97 MessageValidationErrorType::AttachmentFilename { filename } => {
98 f.write_str("attachment filename `")?;
99 Display::fmt(filename, f)?;
100
101 f.write_str("`is invalid")
102 }
103 MessageValidationErrorType::ComponentCount { count, is_v2 } => {
104 Display::fmt(count, f)?;
105 f.write_str(" components were provided, but only ")?;
106 if *is_v2 {
107 Display::fmt(&COMPONENT_V2_COUNT, f)?;
108 } else {
109 Display::fmt(&COMPONENT_COUNT, f)?;
110 }
111
112 f.write_str(" root components are allowed")
113 }
114 MessageValidationErrorType::ComponentInvalid { .. } => {
115 f.write_str("a provided component is invalid")
116 }
117 MessageValidationErrorType::ContentInvalid => f.write_str("message content is invalid"),
118 MessageValidationErrorType::EmbedInvalid { idx, .. } => {
119 f.write_str("embed at index ")?;
120 Display::fmt(idx, f)?;
121
122 f.write_str(" is invalid")
123 }
124 MessageValidationErrorType::StickersInvalid { len } => {
125 f.write_str("amount of stickers provided is ")?;
126 Display::fmt(len, f)?;
127 f.write_str(" but it must be at most ")?;
128
129 Display::fmt(&STICKER_MAX, f)
130 }
131 MessageValidationErrorType::TooManyEmbeds => f.write_str("message has too many embeds"),
132 MessageValidationErrorType::WebhookUsername => {
133 if let Some(source) = self.source() {
134 Display::fmt(&source, f)
135 } else {
136 f.write_str("webhook username is invalid")
137 }
138 }
139 }
140 }
141}
142
143impl Error for MessageValidationError {}
144
145#[derive(Debug)]
147pub enum MessageValidationErrorType {
148 AttachmentFilename {
150 filename: String,
152 },
153 AttachmentDescriptionTooLarge {
155 chars: usize,
157 },
158 ComponentCount {
160 count: usize,
162 is_v2: bool,
164 },
165 ComponentInvalid {
167 idx: usize,
169 kind: ComponentValidationErrorType,
171 },
172 ContentInvalid,
174 EmbedInvalid {
176 idx: usize,
178 kind: EmbedValidationErrorType,
180 },
181 StickersInvalid {
183 len: usize,
185 },
186 TooManyEmbeds,
190 WebhookUsername,
192}
193
194pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
207 attachment_filename(&attachment.filename)?;
208
209 if let Some(description) = &attachment.description {
210 attachment_description(description)?;
211 }
212
213 Ok(())
214}
215
216pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
225 let chars = description.as_ref().chars().count();
226 if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
227 Ok(())
228 } else {
229 Err(MessageValidationError {
230 kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
231 source: None,
232 })
233 }
234}
235
236pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
247 if filename
248 .as_ref()
249 .chars()
250 .all(|c| c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE)
251 {
252 Ok(())
253 } else {
254 Err(MessageValidationError {
255 kind: MessageValidationErrorType::AttachmentFilename {
256 filename: filename.as_ref().to_string(),
257 },
258 source: None,
259 })
260 }
261}
262
263pub fn components(components: &[Component], is_v2: bool) -> Result<(), MessageValidationError> {
275 if is_v2 {
276 let count = components
277 .iter()
278 .map(Component::component_count)
279 .sum::<usize>();
280 if count > COMPONENT_V2_COUNT {
281 return Err(MessageValidationError {
282 kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
283 source: None,
284 });
285 }
286 } else {
287 let count = components.len();
288
289 if count > COMPONENT_COUNT {
290 return Err(MessageValidationError {
291 kind: MessageValidationErrorType::ComponentCount { count, is_v2 },
292 source: None,
293 });
294 }
295 }
296
297 let function = if is_v2 {
298 crate::component::component_v2
299 } else {
300 crate::component::component_v1
301 };
302 for (idx, component) in components.iter().enumerate() {
303 function(component).map_err(|source| {
304 let (kind, source) = source.into_parts();
305
306 MessageValidationError {
307 kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
308 source,
309 }
310 })?;
311 }
312
313 Ok(())
314}
315
316pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
325 if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
327 Ok(())
328 } else {
329 Err(MessageValidationError {
330 kind: MessageValidationErrorType::ContentInvalid,
331 source: None,
332 })
333 }
334}
335
336pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
348 if embeds.len() > EMBED_COUNT_LIMIT {
349 Err(MessageValidationError {
350 kind: MessageValidationErrorType::TooManyEmbeds,
351 source: None,
352 })
353 } else {
354 let mut chars = 0;
355 for (idx, embed) in embeds.iter().enumerate() {
356 chars += embed_chars(embed);
357
358 if chars > EMBED_TOTAL_LENGTH {
359 return Err(MessageValidationError {
360 kind: MessageValidationErrorType::EmbedInvalid {
361 idx,
362 kind: EmbedValidationErrorType::EmbedTooLarge { chars },
363 },
364 source: None,
365 });
366 }
367
368 crate::embed::embed(embed).map_err(|source| {
369 let (kind, source) = source.into_parts();
370
371 MessageValidationError {
372 kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
373 source,
374 }
375 })?;
376 }
377
378 Ok(())
379 }
380}
381
382pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
394 let len = sticker_ids.len();
395
396 if len <= STICKER_MAX {
397 Ok(())
398 } else {
399 Err(MessageValidationError {
400 kind: MessageValidationErrorType::StickersInvalid { len },
401 source: None,
402 })
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn attachment_description_limit() {
412 assert!(attachment_description("").is_ok());
413 assert!(attachment_description(str::repeat("a", 1024)).is_ok());
414
415 assert!(matches!(
416 attachment_description(str::repeat("a", 1025))
417 .unwrap_err()
418 .kind(),
419 MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
420 ));
421 }
422
423 #[test]
424 fn attachment_allowed_filename() {
425 assert!(attachment_filename("one.jpg").is_ok());
426 assert!(attachment_filename("two.png").is_ok());
427 assert!(attachment_filename("three.gif").is_ok());
428 assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
429
430 assert!(attachment_filename("????????").is_err());
431 }
432
433 #[test]
434 fn content_length() {
435 assert!(content("").is_ok());
436 assert!(content("a".repeat(2000)).is_ok());
437
438 assert!(content("a".repeat(2001)).is_err());
439 }
440}