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 => f.write_str("message has too many embeds"),
128 MessageValidationErrorType::WebhookUsername => {
129 if let Some(source) = self.source() {
130 Display::fmt(&source, f)
131 } else {
132 f.write_str("webhook username is invalid")
133 }
134 }
135 }
136 }
137}
138
139impl Error for MessageValidationError {}
140
141#[derive(Debug)]
143pub enum MessageValidationErrorType {
144 AttachmentFilename {
146 filename: String,
148 },
149 AttachmentDescriptionTooLarge {
151 chars: usize,
153 },
154 ComponentCount {
156 count: usize,
158 },
159 ComponentInvalid {
161 idx: usize,
163 kind: ComponentValidationErrorType,
165 },
166 ContentInvalid,
168 EmbedInvalid {
170 idx: usize,
172 kind: EmbedValidationErrorType,
174 },
175 StickersInvalid {
177 len: usize,
179 },
180 TooManyEmbeds,
184 WebhookUsername,
186}
187
188pub fn attachment(attachment: &Attachment) -> Result<(), MessageValidationError> {
201 attachment_filename(&attachment.filename)?;
202
203 if let Some(description) = &attachment.description {
204 attachment_description(description)?;
205 }
206
207 Ok(())
208}
209
210pub fn attachment_description(description: impl AsRef<str>) -> Result<(), MessageValidationError> {
219 let chars = description.as_ref().chars().count();
220 if chars <= ATTACHMENT_DESCIPTION_LENGTH_MAX {
221 Ok(())
222 } else {
223 Err(MessageValidationError {
224 kind: MessageValidationErrorType::AttachmentDescriptionTooLarge { chars },
225 source: None,
226 })
227 }
228}
229
230pub fn attachment_filename(filename: impl AsRef<str>) -> Result<(), MessageValidationError> {
241 if filename
242 .as_ref()
243 .chars()
244 .all(|c| (c.is_ascii_alphanumeric() || c == DOT || c == DASH || c == UNDERSCORE))
245 {
246 Ok(())
247 } else {
248 Err(MessageValidationError {
249 kind: MessageValidationErrorType::AttachmentFilename {
250 filename: filename.as_ref().to_string(),
251 },
252 source: None,
253 })
254 }
255}
256
257pub fn components(components: &[Component]) -> Result<(), MessageValidationError> {
269 let count = components.len();
270
271 if count > COMPONENT_COUNT {
272 Err(MessageValidationError {
273 kind: MessageValidationErrorType::ComponentCount { count },
274 source: None,
275 })
276 } else {
277 for (idx, component) in components.iter().enumerate() {
278 crate::component::component(component).map_err(|source| {
279 let (kind, source) = source.into_parts();
280
281 MessageValidationError {
282 kind: MessageValidationErrorType::ComponentInvalid { idx, kind },
283 source,
284 }
285 })?;
286 }
287
288 Ok(())
289 }
290}
291
292pub fn content(value: impl AsRef<str>) -> Result<(), MessageValidationError> {
301 if value.as_ref().chars().count() <= MESSAGE_CONTENT_LENGTH_MAX {
303 Ok(())
304 } else {
305 Err(MessageValidationError {
306 kind: MessageValidationErrorType::ContentInvalid,
307 source: None,
308 })
309 }
310}
311
312pub fn embeds(embeds: &[Embed]) -> Result<(), MessageValidationError> {
324 if embeds.len() > EMBED_COUNT_LIMIT {
325 Err(MessageValidationError {
326 kind: MessageValidationErrorType::TooManyEmbeds,
327 source: None,
328 })
329 } else {
330 let mut chars = 0;
331 for (idx, embed) in embeds.iter().enumerate() {
332 chars += embed_chars(embed);
333
334 if chars > EMBED_TOTAL_LENGTH {
335 return Err(MessageValidationError {
336 kind: MessageValidationErrorType::EmbedInvalid {
337 idx,
338 kind: EmbedValidationErrorType::EmbedTooLarge { chars },
339 },
340 source: None,
341 });
342 }
343
344 crate::embed::embed(embed).map_err(|source| {
345 let (kind, source) = source.into_parts();
346
347 MessageValidationError {
348 kind: MessageValidationErrorType::EmbedInvalid { idx, kind },
349 source,
350 }
351 })?;
352 }
353
354 Ok(())
355 }
356}
357
358pub fn sticker_ids(sticker_ids: &[Id<StickerMarker>]) -> Result<(), MessageValidationError> {
370 let len = sticker_ids.len();
371
372 if len <= STICKER_MAX {
373 Ok(())
374 } else {
375 Err(MessageValidationError {
376 kind: MessageValidationErrorType::StickersInvalid { len },
377 source: None,
378 })
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385
386 #[test]
387 fn attachment_description_limit() {
388 assert!(attachment_description("").is_ok());
389 assert!(attachment_description(str::repeat("a", 1024)).is_ok());
390
391 assert!(matches!(
392 attachment_description(str::repeat("a", 1025))
393 .unwrap_err()
394 .kind(),
395 MessageValidationErrorType::AttachmentDescriptionTooLarge { chars: 1025 }
396 ));
397 }
398
399 #[test]
400 fn attachment_allowed_filename() {
401 assert!(attachment_filename("one.jpg").is_ok());
402 assert!(attachment_filename("two.png").is_ok());
403 assert!(attachment_filename("three.gif").is_ok());
404 assert!(attachment_filename(".dots-dashes_underscores.gif").is_ok());
405
406 assert!(attachment_filename("????????").is_err());
407 }
408
409 #[test]
410 fn content_length() {
411 assert!(content("").is_ok());
412 assert!(content("a".repeat(2000)).is_ok());
413
414 assert!(content("a".repeat(2001)).is_err());
415 }
416}