1use crate::{
6 component::{ComponentValidationErrorType, COMPONENT_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 } => {
104 Display::fmt(count, f)?;
105 f.write_str(" components were provided, but only ")?;
106 Display::fmt(&COMPONENT_COUNT, f)?;
107
108 f.write_str(" root components are allowed")
109 }
110 MessageValidationErrorType::ComponentInvalid { .. } => {
111 f.write_str("a provided component is invalid")
112 }
113 MessageValidationErrorType::ContentInvalid => f.write_str("message content is invalid"),
114 MessageValidationErrorType::EmbedInvalid { idx, .. } => {
115 f.write_str("embed at index ")?;
116 Display::fmt(idx, f)?;
117
118 f.write_str(" is invalid")
119 }
120 MessageValidationErrorType::StickersInvalid { len } => {
121 f.write_str("amount of stickers provided is ")?;
122 Display::fmt(len, f)?;
123 f.write_str(" but it must be at most ")?;
124
125 Display::fmt(&STICKER_MAX, f)
126 }
127 MessageValidationErrorType::TooManyEmbeds { .. } => {
128 f.write_str("message has too many embeds")
129 }
130 MessageValidationErrorType::WebhookUsername { .. } => {
131 if let Some(source) = self.source() {
132 Display::fmt(&source, f)
133 } else {
134 f.write_str("webhook username is invalid")
135 }
136 }
137 }
138 }
139}
140
141impl Error for MessageValidationError {}
142
143#[derive(Debug)]
145pub enum MessageValidationErrorType {
146 AttachmentFilename {
148 filename: String,
150 },
151 AttachmentDescriptionTooLarge {
153 chars: usize,
155 },
156 ComponentCount {
158 count: usize,
160 },
161 ComponentInvalid {
163 idx: usize,
165 kind: ComponentValidationErrorType,
167 },
168 ContentInvalid,
170 EmbedInvalid {
172 idx: usize,
174 kind: EmbedValidationErrorType,
176 },
177 StickersInvalid {
179 len: usize,
181 },
182 TooManyEmbeds,
186 WebhookUsername,
188}
189
190pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
203 attachment_filename(&attachment.filename)?;
204
205 if let Some(description) = &attachment.description {
206 attachment_description(description)?;
207 }
208
209 Ok(())
210}
211
212pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
221 let chars = description.as_ref().chars().count();
222 if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
223 Ok(())
224 } else {
225 Err(MessageValidationError {
226 kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
227 source: None,
228 })
229 }
230}
231
232pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
243 if filename
244 .as_ref()
245 .chars()
246 .all(|c| (c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE))
247 {
248 Ok(())
249 } else {
250 Err(MessageValidationError {
251 kind: MessageValidationErrorType::AttachmentFilename {
252 filename: filename.as_ref().to_string(),
253 },
254 source: None,
255 })
256 }
257}
258
259pub fn components(components: &[Component]) -> Result<(), MessageValidationError> {
271 let count = components.len();
272
273 if count > COMPONENT_COUNT {
274 Err(MessageValidationError {
275 kind: MessageValidationErrorType::ComponentCount { count },
276 source: None,
277 })
278 } else {
279 for (idx, component) in components.iter().enumerate() {
280 crate::component::component(component).map_err(|source| {
281 let (kind, source) = source.into_parts();
282
283 MessageValidationError {
284 kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
285 source,
286 }
287 })?;
288 }
289
290 Ok(())
291 }
292}
293
294pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
303 if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
305 Ok(())
306 } else {
307 Err(MessageValidationError {
308 kind: MessageValidationErrorType::ContentInvalid,
309 source: None,
310 })
311 }
312}
313
314pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
326 if embeds.len() > EMBED_COUNT_LIMIT {
327 Err(MessageValidationError {
328 kind: MessageValidationErrorType::TooManyEmbeds,
329 source: None,
330 })
331 } else {
332 let mut chars = 0;
333 for (idx, embed) in embeds.iter().enumerate() {
334 chars += embed_chars(embed);
335
336 if chars > EMBED_TOTAL_LENGTH {
337 return Err(MessageValidationError {
338 kind: MessageValidationErrorType::EmbedInvalid {
339 idx,
340 kind: EmbedValidationErrorType::EmbedTooLarge { chars },
341 },
342 source: None,
343 });
344 }
345
346 crate::embed::embed(embed).map_err(|source| {
347 let (kind, source) = source.into_parts();
348
349 MessageValidationError {
350 kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
351 source,
352 }
353 })?;
354 }
355
356 Ok(())
357 }
358}
359
360pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
372 let len = sticker_ids.len();
373
374 if len <= STICKER_MAX {
375 Ok(())
376 } else {
377 Err(MessageValidationError {
378 kind: MessageValidationErrorType::StickersInvalid { len },
379 source: None,
380 })
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn attachment_description_limit() {
390 assert!(attachment_description("").is_ok());
391 assert!(attachment_description(str::repeat("a", 1024)).is_ok());
392
393 assert!(matches!(
394 attachment_description(str::repeat("a", 1025))
395 .unwrap_err()
396 .kind(),
397 MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
398 ));
399 }
400
401 #[test]
402 fn attachment_allowed_filename() {
403 assert!(attachment_filename("one.jpg").is_ok());
404 assert!(attachment_filename("two.png").is_ok());
405 assert!(attachment_filename("three.gif").is_ok());
406 assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
407
408 assert!(attachment_filename("????????").is_err());
409 }
410
411 #[test]
412 fn content_length() {
413 assert!(content("").is_ok());
414 assert!(content("a".repeat(2000)).is_ok());
415
416 assert!(content("a".repeat(2001)).is_err());
417 }
418}