From 3365b27b48e16f277770af6db123db174b14e6f1 Mon Sep 17 00:00:00 2001 From: Vivian Hellyer Date: Mon, 25 May 2020 21:57:24 -0400 Subject: [PATCH 1/5] http: validate request parameters This patch is a continuation of PR #146 with the comments applied. When creating HTTP requests, validate the request parameters. This includes things like checking that the content length of a message is less than or equal to 2000 characters[1], that a new channel's name is within the range of 2-100 characters[2], and more. The request methods that do validation now return results with an error type that is local to the module. For example, the `http::request::channel::UpdateChannel::name` method can return an error, which is defined at `http::request::channel::update_channel::UpdateChannelError`. Validation functions are located in `http::request::validation`, which includes functions that simply return booleans of whether the input is valid or not. Each of these sources where the validation limits are documented. Some things, such as custom emoji names, don't have a documented length limit[3], so validation isn't done for them. [1]: https://discordapp.com/developers/docs/resources/channel#create-message-params [2]: https://discordapp.com/developers/docs/resources/channel#channel-object-channel-structure [3]: https://discordapp.com/developers/docs/resources/emoji#create-guild-emoji-json-params Closes issue #29. Signed-off-by: Vivian Hellyer --- http/examples/allowed-mentions/src/main.rs | 2 +- http/examples/get-message/src/main.rs | 1 + http/examples/proxy/src/main.rs | 1 + http/src/client/mod.rs | 41 +++- http/src/error.rs | 2 +- http/src/ratelimiting/error.rs | 2 +- .../request/channel/message/create_message.rs | 45 +++- .../channel/message/get_channel_messages.rs | 38 +++- .../get_channel_messages_configured.rs | 38 +++- http/src/request/channel/message/mod.rs | 9 +- .../request/channel/message/update_message.rs | 48 +++- http/src/request/channel/mod.rs | 2 +- .../request/channel/reaction/get_reactions.rs | 38 +++- http/src/request/channel/reaction/mod.rs | 3 +- http/src/request/channel/update_channel.rs | 98 ++++++++- http/src/request/guild/ban/create_ban.rs | 40 +++- http/src/request/guild/ban/mod.rs | 3 +- http/src/request/guild/create_guild.rs | 83 ++++++- .../src/request/guild/create_guild_channel.rs | 97 ++++++++- http/src/request/guild/create_guild_prune.rs | 39 +++- http/src/request/guild/get_audit_log.rs | 38 +++- .../request/guild/get_guild_prune_count.rs | 39 +++- .../request/guild/member/get_guild_members.rs | 37 +++- http/src/request/guild/member/mod.rs | 5 +- .../guild/member/update_guild_member.rs | 44 +++- http/src/request/guild/mod.rs | 10 +- http/src/request/guild/update_guild.rs | 46 +++- http/src/request/mod.rs | 1 + http/src/request/prelude.rs | 2 +- .../request/user/get_current_user_guilds.rs | 39 +++- http/src/request/user/mod.rs | 3 +- http/src/request/user/update_current_user.rs | 46 +++- http/src/request/validate.rs | 205 ++++++++++++++++++ 33 files changed, 1063 insertions(+), 82 deletions(-) create mode 100644 http/src/request/validate.rs diff --git a/http/examples/allowed-mentions/src/main.rs b/http/examples/allowed-mentions/src/main.rs index 82c0fa8f96a..a640dbf3413 100644 --- a/http/examples/allowed-mentions/src/main.rs +++ b/http/examples/allowed-mentions/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> Result<(), Box> { .content(format!( "<@{}> you are not allowed to ping @everyone!", user_id.0 - )) + ))? .allowed_mentions() .parse_specific_users(vec![user_id]) .build() diff --git a/http/examples/get-message/src/main.rs b/http/examples/get-message/src/main.rs index 187e5a705b6..00404174547 100644 --- a/http/examples/get-message/src/main.rs +++ b/http/examples/get-message/src/main.rs @@ -14,6 +14,7 @@ async fn main() -> Result<(), Box> { client .create_message(channel_id) .content(format!("Ping #{}", x)) + .expect("content not a valid length") })) .await; diff --git a/http/examples/proxy/src/main.rs b/http/examples/proxy/src/main.rs index c39719d4e32..004d88e0569 100644 --- a/http/examples/proxy/src/main.rs +++ b/http/examples/proxy/src/main.rs @@ -19,6 +19,7 @@ async fn main() -> Result<(), Box> { client .create_message(channel_id) .content(format!("Ping #{}", x)) + .expect("content not a valid length") })) .await; diff --git a/http/src/client/mod.rs b/http/src/client/mod.rs index a4b68a62311..a436a779ced 100644 --- a/http/src/client/mod.rs +++ b/http/src/client/mod.rs @@ -4,7 +4,12 @@ use self::config::ClientConfigBuilder; use crate::{ error::{Error, ResponseError, Result, UrlError}, ratelimiting::{RatelimitHeaders, Ratelimiter}, - request::{channel::message::allowed_mentions::AllowedMentions, prelude::*, Request}, + request::{ + channel::message::allowed_mentions::AllowedMentions, + guild::{create_guild::CreateGuildError, create_guild_channel::CreateGuildChannelError}, + prelude::*, + Request, + }, }; use log::{debug, warn}; use reqwest::{ @@ -20,6 +25,7 @@ use std::{ convert::TryFrom, fmt::{Debug, Formatter, Result as FmtResult}, ops::{Deref, DerefMut}, + result::Result as StdResult, sync::Arc, }; use twilight_model::{ @@ -177,7 +183,7 @@ impl Client { /// let guild_id = GuildId(377840580245585931); /// let user_id = UserId(114941315417899012); /// client.create_ban(guild_id, user_id) - /// .delete_message_days(1) + /// .delete_message_days(1)? /// .reason("memes") /// .await?; /// @@ -265,7 +271,7 @@ impl Client { /// let guilds = client.current_user_guilds() /// .after(after) /// .before(before) - /// .limit(25) + /// .limit(25)? /// .await?; /// /// println!("{:?}", guilds); @@ -359,7 +365,21 @@ impl Client { GetGuild::new(self, guild_id) } - pub fn create_guild(&self, name: impl Into) -> CreateGuild<'_> { + /// Create a new request to create a guild. + /// + /// The minimum length of the name is 2 UTF-16 characters and the maximum is + /// 100 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`CreateGuildError::NameInvalid`] if the name length is too + /// short or too long. + /// + /// [`CreateGuildError::NameInvalid`]: ../request/guild/enum.CreateGuildError.html#variant.NameInvalid + pub fn create_guild( + &self, + name: impl Into, + ) -> StdResult, CreateGuildError> { CreateGuild::new(self, name) } @@ -379,11 +399,22 @@ impl Client { GetGuildChannels::new(self, guild_id) } + /// Create a new request to create a guild channel. + /// + /// The minimum length of the name is 2 UTF-16 characters and the maximum is + /// 100 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`CreateGuildChannelError::NameInvalid`] if the name length is too + /// short or too long. + /// + /// [`CreateGuildChannelError::NameInvalid`]: ../request/guild/enum.CreateGuildChannelError.html#variant.NameInvalid pub fn create_guild_channel( &self, guild_id: GuildId, name: impl Into, - ) -> CreateGuildChannel<'_> { + ) -> StdResult, CreateGuildChannelError> { CreateGuildChannel::new(self, guild_id, name) } diff --git a/http/src/error.rs b/http/src/error.rs index 9fbfb832ba0..14fbf71fefe 100644 --- a/http/src/error.rs +++ b/http/src/error.rs @@ -10,7 +10,7 @@ use std::{ }; use url::ParseError as UrlParseError; -pub type Result = StdResult; +pub type Result = StdResult; #[derive(Debug)] pub enum ResponseError { diff --git a/http/src/ratelimiting/error.rs b/http/src/ratelimiting/error.rs index 511b7f73340..23472b9253f 100644 --- a/http/src/ratelimiting/error.rs +++ b/http/src/ratelimiting/error.rs @@ -48,7 +48,7 @@ impl Display for RatelimitError { name, value, .. - } => write!(f, "The header {:?} has invalid UTF-8: {:?}", name, value), + } => write!(f, "The header {:?} has invalid UTF-16: {:?}", name, value), Self::ParsingBoolText { name, text, diff --git a/http/src/request/channel/message/create_message.rs b/http/src/request/channel/message/create_message.rs index 6e87d8b14ea..a6784ced0b4 100644 --- a/http/src/request/channel/message/create_message.rs +++ b/http/src/request/channel/message/create_message.rs @@ -4,12 +4,31 @@ use reqwest::{ multipart::{Form, Part}, Body, }; -use std::collections::HashMap; +use std::{ + collections::HashMap, + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::{embed::Embed, Message}, id::ChannelId, }; +#[derive(Clone, Debug)] +pub enum CreateMessageError { + ContentInvalid, +} + +impl Display for CreateMessageError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::ContentInvalid => f.write_str("the message content is invalid"), + } + } +} + +impl Error for CreateMessageError {} + #[derive(Default, Serialize)] pub(crate) struct CreateMessageFields { content: Option, @@ -42,10 +61,28 @@ impl<'a> CreateMessage<'a> { } } - pub fn content(mut self, content: impl Into) -> Self { - self.fields.content.replace(content.into()); + /// Set the content of the message. + /// + /// The maximum length is 2000 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`CreateMessageError::ContentInvalid`] if the content length is + /// too long. + /// + /// [`CreateMessageError::ContentInvalid`]: enum.CreateMessageError.html#variant.ContentInvalid + pub fn content(self, content: impl Into) -> Result { + self._content(content.into()) + } - self + fn _content(mut self, content: String) -> Result { + if !validate::content_limit(&content) { + return Err(CreateMessageError::ContentInvalid); + } + + self.fields.content.replace(content); + + Ok(self) } pub fn embed(mut self, embed: Embed) -> Self { diff --git a/http/src/request/channel/message/get_channel_messages.rs b/http/src/request/channel/message/get_channel_messages.rs index 36858a67e05..01cf6b61efe 100644 --- a/http/src/request/channel/message/get_channel_messages.rs +++ b/http/src/request/channel/message/get_channel_messages.rs @@ -1,10 +1,30 @@ use super::GetChannelMessagesConfigured; use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::Message, id::{ChannelId, MessageId}, }; +#[derive(Clone, Debug)] +pub enum GetChannelMessagesError { + /// The maximum number of messages to retrieve is either 0 or more than 100. + LimitInvalid, +} + +impl Display for GetChannelMessagesError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetChannelMessagesError {} + #[derive(Default)] struct GetChannelMessagesFields { limit: Option, @@ -60,10 +80,24 @@ impl<'a> GetChannelMessages<'a> { ) } - pub fn limit(mut self, limit: u64) -> Self { + /// Set the maximum number of messages to retrieve. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns [`GetChannelMessages::LimitInvalid`] if the + /// amount is greater than 21600. + /// + /// [`GetChannelMessages::LimitInvalid`]: enum.GetChannelMessages.html#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + if !validate::get_channel_messages_limit(limit) { + return Err(GetChannelMessagesError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/channel/message/get_channel_messages_configured.rs b/http/src/request/channel/message/get_channel_messages_configured.rs index 0118029d95d..a078e549e22 100644 --- a/http/src/request/channel/message/get_channel_messages_configured.rs +++ b/http/src/request/channel/message/get_channel_messages_configured.rs @@ -1,9 +1,29 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::Message, id::{ChannelId, MessageId}, }; +#[derive(Clone, Debug)] +pub enum GetChannelMessagesConfiguredError { + /// The maximum number of messages to retrieve is either 0 or more than 100. + LimitInvalid, +} + +impl Display for GetChannelMessagesConfiguredError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetChannelMessagesConfiguredError {} + struct GetChannelMessagesConfiguredFields { limit: Option, } @@ -43,10 +63,24 @@ impl<'a> GetChannelMessagesConfigured<'a> { } } - pub fn limit(mut self, limit: u64) -> Self { + /// Set the maximum number of messages to retrieve. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns [`GetChannelMessagesConfiguredError::LimitInvalid`] if the + /// amount is greater than 21600. + /// + /// [`GetChannelMessagesConfiguredError::LimitInvalid`]: enum.GetChannelMessagesConfiguredError.html#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + if !validate::get_channel_messages_limit(limit) { + return Err(GetChannelMessagesConfiguredError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/channel/message/mod.rs b/http/src/request/channel/message/mod.rs index 846f83b1ec8..7c63076ff3f 100644 --- a/http/src/request/channel/message/mod.rs +++ b/http/src/request/channel/message/mod.rs @@ -1,11 +1,12 @@ pub mod allowed_mentions; -mod create_message; +pub mod create_message; +pub mod get_channel_messages; +pub mod get_channel_messages_configured; +pub mod update_message; + mod delete_message; mod delete_messages; -mod get_channel_messages; -mod get_channel_messages_configured; mod get_message; -mod update_message; pub use self::{ create_message::CreateMessage, diff --git a/http/src/request/channel/message/update_message.rs b/http/src/request/channel/message/update_message.rs index b840a713f5f..01c808a7e2e 100644 --- a/http/src/request/channel/message/update_message.rs +++ b/http/src/request/channel/message/update_message.rs @@ -1,9 +1,28 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::{embed::Embed, Message}, id::{ChannelId, MessageId}, }; +#[derive(Clone, Debug)] +pub enum UpdateMessageError { + ContentInvalid, +} + +impl Display for UpdateMessageError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::ContentInvalid => f.write_str("the message content is invalid"), + } + } +} + +impl Error for UpdateMessageError {} + #[derive(Default, Serialize)] struct UpdateMessageFields { // We don't serialize if this is Option::None, to avoid overwriting the @@ -39,7 +58,7 @@ struct UpdateMessageFields { /// # async fn main() -> Result<(), Box> { /// let client = Client::new("my token"); /// client.update_message(ChannelId(1), MessageId(2)) -/// .content("test update".to_owned()) +/// .content("test update".to_owned())? /// .await?; /// # Ok(()) } /// ``` @@ -54,7 +73,7 @@ struct UpdateMessageFields { /// # async fn main() -> Result<(), Box> { /// let client = Client::new("my token"); /// client.update_message(ChannelId(1), MessageId(2)) -/// .content(None) +/// .content(None)? /// .await?; /// # Ok(()) } /// ``` @@ -82,10 +101,29 @@ impl<'a> UpdateMessage<'a> { /// Set the content of the message. /// /// Pass `None` if you want to remove the message content. - pub fn content(mut self, content: impl Into>) -> Self { - self.fields.content.replace(content.into()); + /// + /// The maximum length is 2000 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`UpdateMessageError::ContentInvalid`] if the content length is + /// too long. + /// + /// [`UpdateMessageError::ContentInvalid`]: enum.UpdateMessageError.html#variant.ContentInvalid + pub fn content(self, content: impl Into>) -> Result { + self._content(content.into()) + } - self + fn _content(mut self, content: Option) -> Result { + if let Some(content) = content.as_ref() { + if !validate::content_limit(content) { + return Err(UpdateMessageError::ContentInvalid); + } + } + + self.fields.content.replace(content); + + Ok(self) } /// Set the embed of the message. diff --git a/http/src/request/channel/mod.rs b/http/src/request/channel/mod.rs index e9fcfc69352..56d96ed62c8 100644 --- a/http/src/request/channel/mod.rs +++ b/http/src/request/channel/mod.rs @@ -1,6 +1,7 @@ pub mod invite; pub mod message; pub mod reaction; +pub mod update_channel; pub mod webhook; mod create_pin; @@ -10,7 +11,6 @@ mod delete_channel_permission; mod delete_pin; mod get_channel; mod get_pins; -mod update_channel; mod update_channel_permission; mod update_channel_permission_configured; diff --git a/http/src/request/channel/reaction/get_reactions.rs b/http/src/request/channel/reaction/get_reactions.rs index 9286d5d1a17..e105537976d 100644 --- a/http/src/request/channel/reaction/get_reactions.rs +++ b/http/src/request/channel/reaction/get_reactions.rs @@ -1,9 +1,29 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ id::{ChannelId, MessageId, UserId}, user::User, }; +#[derive(Clone, Debug)] +pub enum GetReactionsError { + /// The maximum number of reactions to retrieve is 0 or more than 100. + LimitInvalid, +} + +impl Display for GetReactionsError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetReactionsError {} + #[derive(Default)] struct GetReactionsFields { after: Option, @@ -49,10 +69,24 @@ impl<'a> GetReactions<'a> { self } - pub fn limit(mut self, limit: u64) -> Self { + /// Set the maximum number of reactions to retrieve. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns [`GetReactionsError::LimitInvalid`] if the amount is greater + /// than 100. + /// + /// [`GetReactionsError::LimitInvalid`]: enum.GetReactionsError.hLml#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + if !validate::get_reactions_limit(limit) { + return Err(GetReactionsError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/channel/reaction/mod.rs b/http/src/request/channel/reaction/mod.rs index 8e7e980bdd8..2be2b870778 100644 --- a/http/src/request/channel/reaction/mod.rs +++ b/http/src/request/channel/reaction/mod.rs @@ -1,8 +1,9 @@ +pub mod get_reactions; + mod create_reaction; mod delete_all_reaction; mod delete_all_reactions; mod delete_reaction; -mod get_reactions; pub use self::{ create_reaction::CreateReaction, diff --git a/http/src/request/channel/update_channel.rs b/http/src/request/channel/update_channel.rs index ac61d6ceb84..4b232613115 100644 --- a/http/src/request/channel/update_channel.rs +++ b/http/src/request/channel/update_channel.rs @@ -1,9 +1,36 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::{permission_overwrite::PermissionOverwrite, Channel, ChannelType}, id::ChannelId, }; +#[derive(Clone, Debug)] +pub enum UpdateChannelError { + /// The length of the name is either fewer than 2 UTF-16 characters or + /// more than 100 UTF-16 characters. + NameInvalid, + /// The seconds of the rate limit per user is more than 21600. + RateLimitPerUserInvalid, + /// The length of the topic is more than 1024 UTF-16 characters. + TopicInvalid, +} + +impl Display for UpdateChannelError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::NameInvalid => f.write_str("the length of the name is invalid"), + Self::RateLimitPerUserInvalid => f.write_str("the rate limit per user is invalid"), + Self::TopicInvalid => f.write_str("the topic is invalid"), + } + } +} + +impl Error for UpdateChannelError {} + // The Discord API doesn't require the `name` and `kind` fields to be present, // but it does require them to be non-null. #[derive(Default, Serialize)] @@ -48,10 +75,29 @@ impl<'a> UpdateChannel<'a> { self } - pub fn name(mut self, name: impl Into) -> Self { - self.fields.name.replace(name.into()); + /// Set the name. + /// + /// The minimum length is 2 UTF-16 characters and the maximum is 100 UTF-16 + /// characters. + /// + /// # Errors + /// + /// Returns [`UpdateChannelError::NameInvalid`] if the name length is + /// too short or too long. + /// + /// [`UpdateChannelError::NameInvalid`]: enum.UpdateChannelError.html#variant.NameInvalid + pub fn name(self, name: impl Into) -> Result { + self._name(name.into()) + } - self + fn _name(mut self, name: String) -> Result { + if !validate::channel_name(&name) { + return Err(UpdateChannelError::NameInvalid); + } + + self.fields.name.replace(name); + + Ok(self) } pub fn nsfw(mut self, nsfw: bool) -> Self { @@ -83,16 +129,54 @@ impl<'a> UpdateChannel<'a> { self } - pub fn rate_limit_per_user(mut self, rate_limit_per_user: u64) -> Self { + /// Set the number of seconds that a user must wait before before able to + /// send a message again. + /// + /// The minimum is 0 and the maximum is 21600. + /// + /// # Errors + /// + /// Returns [`UpdateChannelError::RateLimitPerUserInvalid`] if the + /// amount is greater than 21600. + /// + /// [`UpdateChannelError::RateLimitPerUserInvalid`]: enum.UpdateChannelError.html#variant.RateLimitPerUserInvalid + pub fn rate_limit_per_user( + mut self, + rate_limit_per_user: u64, + ) -> Result { + // + if rate_limit_per_user > 21600 { + return Err(UpdateChannelError::RateLimitPerUserInvalid); + } + self.fields.rate_limit_per_user.replace(rate_limit_per_user); - self + Ok(self) } - pub fn topic(mut self, topic: impl Into) -> Self { + /// Set the topic. + /// + /// The maximum length is 1024 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`CreateGuildChannel::TopicInvalid`] if the topic length is + /// too long. + /// + /// [`CreateGuildChannel::TopicInvalid`]: enum.CreateGuildChannel.html#variant.TopicInvalid + pub fn topic(self, topic: impl Into) -> Result { + self._topic(topic.into()) + } + + fn _topic(mut self, topic: String) -> Result { + // + if topic.chars().count() > 1024 { + return Err(UpdateChannelError::TopicInvalid); + } + self.fields.topic.replace(topic.into()); - self + Ok(self) } pub fn user_limit(mut self, user_limit: u64) -> Self { diff --git a/http/src/request/guild/ban/create_ban.rs b/http/src/request/guild/ban/create_ban.rs index 4cfef928d8d..45e3742e43a 100644 --- a/http/src/request/guild/ban/create_ban.rs +++ b/http/src/request/guild/ban/create_ban.rs @@ -1,6 +1,28 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::id::{GuildId, UserId}; +#[derive(Clone, Debug)] +pub enum CreateBanError { + /// The number of days' worth of messages to delete is greater than 7. + DeleteMessageDaysInvalid, +} + +impl Display for CreateBanError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::DeleteMessageDaysInvalid => { + f.write_str("the number of days' worth of messages to delete is invalid") + }, + } + } +} + +impl Error for CreateBanError {} + #[derive(Default)] struct CreateBanFields { delete_message_days: Option, @@ -26,10 +48,24 @@ impl<'a> CreateBan<'a> { } } - pub fn delete_message_days(mut self, days: u64) -> Self { + /// Set the number of days' worth of messages to delete. + /// + /// The number of days must be less than or equal to 7. + /// + /// # Errors + /// + /// Returns [`CreateBanError::DeleteMessageDaysInvalid`] if the number of days + /// is greater than 7. + /// + /// [`CreateBanError::DeleteMessageDaysInvalid`]: enum.CreateBanError.html#variant.DeleteMessageDaysInvalid + pub fn delete_message_days(mut self, days: u64) -> Result { + if !validate::ban_delete_message_days(days) { + return Err(CreateBanError::DeleteMessageDaysInvalid); + } + self.fields.delete_message_days.replace(days); - self + Ok(self) } pub fn reason(mut self, reason: impl Into) -> Self { diff --git a/http/src/request/guild/ban/mod.rs b/http/src/request/guild/ban/mod.rs index 7ea2369fd73..8ce90715a53 100644 --- a/http/src/request/guild/ban/mod.rs +++ b/http/src/request/guild/ban/mod.rs @@ -1,4 +1,5 @@ -mod create_ban; +pub mod create_ban; + mod delete_ban; mod get_ban; mod get_bans; diff --git a/http/src/request/guild/create_guild.rs b/http/src/request/guild/create_guild.rs index bf1b751761b..6730c45c0fa 100644 --- a/http/src/request/guild/create_guild.rs +++ b/http/src/request/guild/create_guild.rs @@ -1,4 +1,8 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::GuildChannel, guild::{ @@ -10,6 +14,33 @@ use twilight_model::{ }, }; +#[derive(Clone, Debug)] +pub enum CreateGuildError { + /// The name of the guild is either fewer than 2 UTF-16 characters or more + /// than 100 UTF-16 characters. + NameInvalid, + /// The number of channels provided is too many. + /// + /// The maximum amount is 500. + TooManyChannels, + /// The number of roles provided is too many. + /// + /// The maximum amount is 250. + TooManyRoles, +} + +impl Display for CreateGuildError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::NameInvalid => f.write_str("the guild name is invalid"), + Self::TooManyChannels => f.write_str("too many channels were provided"), + Self::TooManyRoles => f.write_str("too many roles were provided"), + } + } +} + +impl Error for CreateGuildError {} + #[derive(Serialize)] struct CreateGuildFields { channels: Option>, @@ -29,8 +60,16 @@ pub struct CreateGuild<'a> { } impl<'a> CreateGuild<'a> { - pub(crate) fn new(http: &'a Client, name: impl Into) -> Self { - Self { + pub(crate) fn new(http: &'a Client, name: impl Into) -> Result { + Self::_new(http, name.into()) + } + + fn _new(http: &'a Client, name: String) -> Result { + if !validate::guild_name(&name) { + return Err(CreateGuildError::NameInvalid); + } + + Ok(Self { fields: CreateGuildFields { channels: None, default_message_notifications: None, @@ -43,13 +82,29 @@ impl<'a> CreateGuild<'a> { }, fut: None, http, - } + }) } - pub fn channels(mut self, channels: Vec) -> Self { + /// Set the channels to create with the guild. + /// + /// The maximum number of channels that can be provided is 500. + /// + /// # Errors + /// + /// Returns [`CreateGuildError::TooManyChannels`] if the number of channels + /// is over 500. + /// + /// [`CreateGuildError::TooManyChannels`]: enum.CreateGuildError.html#variant.TooManyChannels + pub fn channels(mut self, channels: Vec) -> Result { + // Error 30013 + // + if channels.len() > 500 { + return Err(CreateGuildError::TooManyChannels); + } + self.fields.channels.replace(channels); - self + Ok(self) } pub fn default_message_notifications( @@ -86,10 +141,24 @@ impl<'a> CreateGuild<'a> { self } - pub fn roles(mut self, roles: Vec) -> Self { + /// Set the roles to create with the guild. + /// + /// The maximum number of roles that can be provided is 250. + /// + /// # Errors + /// + /// Returns [`CreateGuildError::TooManyRoles`] if the number of roles is + /// over 250. + /// + /// [`CreateGuildError::TooManyRoles`]: enum.CreateGuildError.html#variant.TooManyRoles + pub fn roles(mut self, roles: Vec) -> Result { + if roles.len() > 250 { + return Err(CreateGuildError::TooManyRoles); + } + self.fields.roles.replace(roles); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/guild/create_guild_channel.rs b/http/src/request/guild/create_guild_channel.rs index 353c0f90dbf..1e1aae36282 100644 --- a/http/src/request/guild/create_guild_channel.rs +++ b/http/src/request/guild/create_guild_channel.rs @@ -1,9 +1,36 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ channel::{permission_overwrite::PermissionOverwrite, ChannelType, GuildChannel}, id::{ChannelId, GuildId}, }; +#[derive(Clone, Debug)] +pub enum CreateGuildChannelError { + /// The length of the name is either fewer than 2 UTF-16 characters or + /// more than 100 UTF-16 characters. + NameInvalid, + /// The seconds of the rate limit per user is more than 21600. + RateLimitPerUserInvalid, + /// The length of the topic is more than 1024 UTF-16 characters. + TopicInvalid, +} + +impl Display for CreateGuildChannelError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::NameInvalid => f.write_str("the length of the name is invalid"), + Self::RateLimitPerUserInvalid => f.write_str("the rate limit per user is invalid"), + Self::TopicInvalid => f.write_str("the topic is invalid"), + } + } +} + +impl Error for CreateGuildChannelError {} + #[derive(Serialize)] struct CreateGuildChannelFields { bitrate: Option, @@ -28,8 +55,24 @@ pub struct CreateGuildChannel<'a> { } impl<'a> CreateGuildChannel<'a> { - pub(crate) fn new(http: &'a Client, guild_id: GuildId, name: impl Into) -> Self { - Self { + pub(crate) fn new( + http: &'a Client, + guild_id: GuildId, + name: impl Into, + ) -> Result { + Self::_new(http, guild_id, name.into()) + } + + fn _new( + http: &'a Client, + guild_id: GuildId, + name: String, + ) -> Result { + if !validate::channel_name(&name) { + return Err(CreateGuildChannelError::NameInvalid); + } + + Ok(Self { fields: CreateGuildChannelFields { bitrate: None, kind: None, @@ -46,7 +89,7 @@ impl<'a> CreateGuildChannel<'a> { guild_id, http, reason: None, - } + }) } pub fn bitrate(mut self, bitrate: u64) -> Self { @@ -90,16 +133,54 @@ impl<'a> CreateGuildChannel<'a> { self } - pub fn rate_limit_per_user(mut self, rate_limit_per_user: u64) -> Self { + /// Set the number of seconds that a user must wait before before able to + /// send a message again. + /// + /// The minimum is 0 and the maximum is 21600. + /// + /// # Errors + /// + /// Returns [`GetGuildPruneCountError::RateLimitPerUserInvalid`] if the + /// amount is greater than 21600. + /// + /// [`GetGuildPruneCountError::RateLimitPerUserInvalid`]: enum.GetGuildPruneCountError.html#variant.RateLimitPerUserInvalid + pub fn rate_limit_per_user( + mut self, + rate_limit_per_user: u64, + ) -> Result { + // + if rate_limit_per_user > 21600 { + return Err(CreateGuildChannelError::RateLimitPerUserInvalid); + } + self.fields.rate_limit_per_user.replace(rate_limit_per_user); - self + Ok(self) } - pub fn topic(mut self, topic: impl Into) -> Self { - self.fields.topic.replace(topic.into()); + /// Set the topic. + /// + /// The maximum length is 1024 UTF-16 characters. + /// + /// # Errors + /// + /// Returns [`CreateGuildChannel::TopicInvalid`] if the topic length is + /// too long. + /// + /// [`CreateGuildChannel::TopicInvalid`]: enum.CreateGuildChannel.html#variant.TopicInvalid + pub fn topic(self, topic: impl Into) -> Result { + self._topic(topic.into()) + } - self + fn _topic(mut self, topic: String) -> Result { + // + if topic.chars().count() > 1024 { + return Err(CreateGuildChannelError::TopicInvalid); + } + + self.fields.topic.replace(topic); + + Ok(self) } pub fn user_limit(mut self, user_limit: u64) -> Self { diff --git a/http/src/request/guild/create_guild_prune.rs b/http/src/request/guild/create_guild_prune.rs index 230586bf7e1..555f2e4bb1c 100644 --- a/http/src/request/guild/create_guild_prune.rs +++ b/http/src/request/guild/create_guild_prune.rs @@ -1,6 +1,26 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{guild::GuildPrune, id::GuildId}; +#[derive(Clone, Debug)] +pub enum CreateGuildPruneError { + /// The number of days is 0. + DaysInvalid, +} + +impl Display for CreateGuildPruneError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::DaysInvalid => f.write_str("the number of days is invalid"), + } + } +} + +impl Error for CreateGuildPruneError {} + #[derive(Default)] struct CreateGuildPruneFields { compute_prune_count: Option, @@ -32,10 +52,25 @@ impl<'a> CreateGuildPrune<'a> { self } - pub fn days(mut self, days: u64) -> Self { + /// Set the number of days that a user must be inactive before being + /// pruned. + /// + /// The number of days must be greater than 0. + /// + /// # Errors + /// + /// Returns [`CreateGuildPruneError::DaysInvalid`] if the number of days is + /// 0. + /// + /// [`CreateGuildPruneError::DaysInvalid`]: enum.CreateGuildPruneError.html#variant.DaysInvalid + pub fn days(mut self, days: u64) -> Result { + if !validate::guild_prune_days(days) { + return Err(CreateGuildPruneError::DaysInvalid); + } + self.fields.days.replace(days); - self + Ok(self) } pub fn reason(mut self, reason: impl Into) -> Self { diff --git a/http/src/request/guild/get_audit_log.rs b/http/src/request/guild/get_audit_log.rs index 8a46bf2b6e6..3e7a4bb9e93 100644 --- a/http/src/request/guild/get_audit_log.rs +++ b/http/src/request/guild/get_audit_log.rs @@ -1,9 +1,29 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ guild::audit_log::{AuditLog, AuditLogEvent}, id::{GuildId, UserId}, }; +#[derive(Clone, Debug)] +pub enum GetAuditLogError { + /// The limit is either 0 or more than 100. + LimitInvalid, +} + +impl Display for GetAuditLogError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetAuditLogError {} + #[derive(Default)] struct GetAuditLogFields { action_type: Option, @@ -41,10 +61,24 @@ impl<'a> GetAuditLog<'a> { self } - pub fn limit(mut self, limit: u64) -> Self { + /// Set the maximum number of audit logs to retrieve. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns [`GetAuditLogError::LimitInvalid`] if the `limit` is 0 or + /// greater than 100. + /// + /// [`GetAuditLogError::LimitInvalid`]: enum.GetAuditLogError.html#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + if !validate::get_audit_log_limit(limit) { + return Err(GetAuditLogError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } pub fn user_id(mut self, user_id: UserId) -> Self { diff --git a/http/src/request/guild/get_guild_prune_count.rs b/http/src/request/guild/get_guild_prune_count.rs index 2d56961707d..76675cf0425 100644 --- a/http/src/request/guild/get_guild_prune_count.rs +++ b/http/src/request/guild/get_guild_prune_count.rs @@ -1,6 +1,26 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{guild::GuildPrune, id::GuildId}; +#[derive(Clone, Debug)] +pub enum GetGuildPruneCountError { + /// The number of days is 0. + DaysInvalid, +} + +impl Display for GetGuildPruneCountError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::DaysInvalid => f.write_str("the number of days is invalid"), + } + } +} + +impl Error for GetGuildPruneCountError {} + #[derive(Default)] struct GetGuildPruneCountFields { days: Option, @@ -23,10 +43,25 @@ impl<'a> GetGuildPruneCount<'a> { } } - pub fn days(mut self, days: u64) -> Self { + /// Set the number of days that a user must be inactive before being + /// able to be pruned. + /// + /// The number of days must be greater than 0. + /// + /// # Errors + /// + /// Returns [`GetGuildPruneCountError::DaysInvalid`] if the number of days + /// is 0. + /// + /// [`GetGuildPruneCountError::DaysInvalid`]: enum.GetGuildPruneCountError.html#variant.DaysInvalid + pub fn days(mut self, days: u64) -> Result { + if validate::guild_prune_days(days) { + return Err(GetGuildPruneCountError::DaysInvalid); + } + self.fields.days.replace(days); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/guild/member/get_guild_members.rs b/http/src/request/guild/member/get_guild_members.rs index 76f085276b4..452712e8072 100644 --- a/http/src/request/guild/member/get_guild_members.rs +++ b/http/src/request/guild/member/get_guild_members.rs @@ -1,9 +1,29 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ guild::Member, id::{GuildId, UserId}, }; +#[derive(Clone, Debug)] +pub enum GetGuildMembersError { + /// The limit is either 0 or more than 1000. + LimitInvalid, +} + +impl Display for GetGuildMembersError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetGuildMembersError {} + #[derive(Default)] struct GetGuildMembersFields { after: Option, @@ -60,11 +80,22 @@ impl<'a> GetGuildMembers<'a> { /// Sets the number of members to retrieve per request. /// - /// The maximum value accepted by the API is 1000. - pub fn limit(mut self, limit: u64) -> Self { + /// The limit must be greater than 0 and less than 1000. + /// + /// # Errors + /// + /// Returns [`GetGuildMembersError::LimitInvalid`] if the limit is 0 or + /// greater than 1000. + /// + /// [`GetGuildMembersError::LimitInvalid`]: enum.GetGuildMembersError.html#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + if !validate::get_guild_members_limit(limit) { + return Err(GetGuildMembersError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } /// Sets whether to retrieve matched member presences diff --git a/http/src/request/guild/member/mod.rs b/http/src/request/guild/member/mod.rs index 922d7d77518..d33b8b9a929 100644 --- a/http/src/request/guild/member/mod.rs +++ b/http/src/request/guild/member/mod.rs @@ -1,9 +1,10 @@ +pub mod get_guild_members; +pub mod update_guild_member; + mod add_role_to_member; -mod get_guild_members; mod get_member; mod remove_member; mod remove_role_from_member; -mod update_guild_member; pub use self::{ add_role_to_member::AddRoleToMember, diff --git a/http/src/request/guild/member/update_guild_member.rs b/http/src/request/guild/member/update_guild_member.rs index b2b5d405f0c..52b6c8a558f 100644 --- a/http/src/request/guild/member/update_guild_member.rs +++ b/http/src/request/guild/member/update_guild_member.rs @@ -1,9 +1,30 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ guild::Member, id::{ChannelId, GuildId, RoleId, UserId}, }; +#[derive(Clone, Debug)] +pub enum UpdateGuildMemberError { + /// The nickname is either empty or the length is more than 32 UTF-16 + /// characters. + NicknameInvalid, +} + +impl Display for UpdateGuildMemberError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::NicknameInvalid => f.write_str("the nickname length is invalid"), + } + } +} + +impl Error for UpdateGuildMemberError {} + #[derive(Default, Serialize)] struct UpdateGuildMemberFields { channel_id: Option, @@ -52,10 +73,29 @@ impl<'a> UpdateGuildMember<'a> { self } - pub fn nick(mut self, nick: impl Into) -> Self { + /// Set the nickname. + /// + /// The minimum length is 1 UTF-16 character and the maximum is 32 UTF-16 + /// characters. + /// + /// # Errors + /// + /// Returns [`UpdateGuildMemberError::NicknameInvalid`] if the nickname + /// length is too short or too long. + /// + /// [`UpdateGuildMemberError::NicknameInvalid`]: enum.UpdateGuildMemberError.html#variant.NicknameInvalid + pub fn nick(self, nick: impl Into) -> Result { + self._nick(nick.into()) + } + + fn _nick(mut self, nick: String) -> Result { + if !validate::nickname(&nick) { + return Err(UpdateGuildMemberError::NicknameInvalid); + } + self.fields.nick.replace(nick.into()); - self + Ok(self) } pub fn roles(mut self, roles: Vec) -> Self { diff --git a/http/src/request/guild/mod.rs b/http/src/request/guild/mod.rs index dd1b63cc93a..19d866b9ec2 100644 --- a/http/src/request/guild/mod.rs +++ b/http/src/request/guild/mod.rs @@ -1,20 +1,20 @@ pub mod ban; +pub mod create_guild; +pub mod create_guild_channel; +pub mod create_guild_prune; pub mod emoji; +pub mod get_audit_log; +pub mod get_guild_prune_count; pub mod integration; pub mod member; pub mod role; -mod create_guild; -mod create_guild_channel; -mod create_guild_prune; mod delete_guild; -mod get_audit_log; mod get_guild; mod get_guild_channels; mod get_guild_embed; mod get_guild_invites; mod get_guild_preview; -mod get_guild_prune_count; mod get_guild_vanity_url; mod get_guild_voice_regions; mod get_guild_webhooks; diff --git a/http/src/request/guild/update_guild.rs b/http/src/request/guild/update_guild.rs index 933dcce33f8..684997f6ac0 100644 --- a/http/src/request/guild/update_guild.rs +++ b/http/src/request/guild/update_guild.rs @@ -1,4 +1,8 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{ guild::{ DefaultMessageNotificationLevel, @@ -9,6 +13,23 @@ use twilight_model::{ id::{ChannelId, GuildId, UserId}, }; +#[derive(Clone, Debug)] +pub enum UpdateGuildError { + /// The name length is either fewer than 2 UTF-16 characters or more than + /// 100 UTF-16 characters. + NameInvalid, +} + +impl Display for UpdateGuildError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::NameInvalid => f.write_str("the name's length is invalid"), + } + } +} + +impl Error for UpdateGuildError {} + #[derive(Default, Serialize)] struct UpdateGuildFields { afk_channel_id: Option, @@ -86,10 +107,29 @@ impl<'a> UpdateGuild<'a> { self } - pub fn name(mut self, name: impl Into) -> Self { - self.fields.name.replace(name.into()); + /// Set the name of the guild. + /// + /// The minimum length is 2 UTF-16 characters and the maximum is 100 UTF-16 + /// characters. + /// + /// # Erroors + /// + /// Returns [`UpdateGuildError::NameInvalid`] if the name length is too + /// short or too long. + /// + /// [`UpdateGuildError::NameInvalid`]: enum.UpdateGuildError.html#variant.NameInvalid + pub fn name(self, name: impl Into) -> Result { + self._name(name.into()) + } + + fn _name(mut self, name: String) -> Result { + if !validate::guild_name(&name) { + return Err(UpdateGuildError::NameInvalid); + } - self + self.fields.name.replace(name); + + Ok(self) } pub fn owner_id(mut self, owner_id: impl Into) -> Self { diff --git a/http/src/request/mod.rs b/http/src/request/mod.rs index da6d723b60d..2b114bd3344 100644 --- a/http/src/request/mod.rs +++ b/http/src/request/mod.rs @@ -29,6 +29,7 @@ pub mod user; mod get_gateway; mod get_gateway_authed; mod get_voice_regions; +mod validate; pub use self::{ get_gateway::GetGateway, diff --git a/http/src/request/prelude.rs b/http/src/request/prelude.rs index 5c953859b08..82b8d9daab1 100644 --- a/http/src/request/prelude.rs +++ b/http/src/request/prelude.rs @@ -1,4 +1,4 @@ -pub(super) use super::{audit_header, Pending, Request}; +pub(super) use super::{audit_header, validate, Pending, Request}; pub use super::{ channel::{invite::*, message::*, reaction::*, webhook::*, *}, get_gateway::GetGateway, diff --git a/http/src/request/user/get_current_user_guilds.rs b/http/src/request/user/get_current_user_guilds.rs index 277d9612c5d..301bf956978 100644 --- a/http/src/request/user/get_current_user_guilds.rs +++ b/http/src/request/user/get_current_user_guilds.rs @@ -1,6 +1,26 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::{guild::PartialGuild, id::GuildId}; +#[derive(Clone, Debug)] +pub enum GetCurrentUserGuildsError { + /// The maximum number of guilds to retrieve is 0 or more than 100. + LimitInvalid, +} + +impl Display for GetCurrentUserGuildsError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::LimitInvalid => f.write_str("the limit is invalid"), + } + } +} + +impl Error for GetCurrentUserGuildsError {} + struct GetCurrentUserGuildsFields { after: Option, before: Option, @@ -38,10 +58,25 @@ impl<'a> GetCurrentUserGuilds<'a> { self } - pub fn limit(mut self, limit: u64) -> Self { + /// Set the maximum number of guilds to retrieve. + /// + /// The minimum is 1 and the maximum is 100. + /// + /// # Errors + /// + /// Returns [`GetCurrentUserGuildsError::LimitInvalid`] if the amount is greater + /// than 100. + /// + /// [`GetCurrentUserGuildsError::LimitInvalid`]: enum.GetCurrentUserGuildsError.hLml#variant.LimitInvalid + pub fn limit(mut self, limit: u64) -> Result { + // + if !validate::get_current_user_guilds_limit(limit) { + return Err(GetCurrentUserGuildsError::LimitInvalid); + } + self.fields.limit.replace(limit); - self + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/user/mod.rs b/http/src/request/user/mod.rs index aeee034092a..28a48aa57e1 100644 --- a/http/src/request/user/mod.rs +++ b/http/src/request/user/mod.rs @@ -1,3 +1,5 @@ +pub mod update_current_user; + mod create_private_channel; mod get_current_user; mod get_current_user_connections; @@ -5,7 +7,6 @@ mod get_current_user_guilds; mod get_current_user_private_channels; mod get_user; mod leave_guild; -mod update_current_user; pub use self::{ create_private_channel::CreatePrivateChannel, diff --git a/http/src/request/user/update_current_user.rs b/http/src/request/user/update_current_user.rs index 642a55ad8a9..183d8cb7ec9 100644 --- a/http/src/request/user/update_current_user.rs +++ b/http/src/request/user/update_current_user.rs @@ -1,6 +1,27 @@ use crate::request::prelude::*; +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; use twilight_model::user::User; +#[derive(Clone, Debug)] +pub enum UpdateCurrentUserError { + /// The length of the username is either fewer than 2 UTF-16 characters or + /// more than 32 UTF-16 characters. + UsernameInvalid, +} + +impl Display for UpdateCurrentUserError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::UsernameInvalid => f.write_str("the username length is invalid"), + } + } +} + +impl Error for UpdateCurrentUserError {} + #[derive(Default, Serialize)] struct UpdateCurrentUserFields { avatar: Option, @@ -28,10 +49,29 @@ impl<'a> UpdateCurrentUser<'a> { self } - pub fn username(mut self, username: impl Into) -> Self { - self.fields.username.replace(username.into()); + /// Set the username. + /// + /// The minimum length is 2 UTF-16 characters and the maximum is 32 UTF-16 + /// characters. + /// + /// # Errors + /// + /// Returns [`UpdateCurrentUserError::UsernameInvalid`] if the username + /// length is too short or too long. + /// + /// [`UpdateCurrentUserError::UsernameInvalid`]: enum.UpdateCurrentUserError.html#variant.UsernameInvalid + pub fn username(self, username: impl Into) -> Result { + self._username(username.into()) + } - self + fn _username(mut self, username: String) -> Result { + if !validate::username(&username) { + return Err(UpdateCurrentUserError::UsernameInvalid); + } + + self.fields.username.replace(username); + + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/validate.rs b/http/src/request/validate.rs new file mode 100644 index 00000000000..234dd10ff4d --- /dev/null +++ b/http/src/request/validate.rs @@ -0,0 +1,205 @@ +/// Contains all of the input validation functions for requests. +/// +/// This is in a centralised place so that the validation parameters can be kept +/// up-to-date more easily and because some of the checks are re-used across +/// different modules. + +pub fn ban_delete_message_days(value: u64) -> bool { + // + value <= 7 +} + +pub fn channel_name(value: impl AsRef) -> bool { + _channel_name(value.as_ref()) +} + +fn _channel_name(value: &str) -> bool { + let len = value.chars().count(); + + // + len >= 2 && len <= 100 +} + +pub fn content_limit(value: impl AsRef) -> bool { + _content_limit(value.as_ref()) +} + +fn _content_limit(value: &str) -> bool { + // + value.chars().count() <= 2000 +} + +pub fn get_audit_log_limit(value: u64) -> bool { + // + value > 0 && value <= 100 +} + +pub fn get_channel_messages_limit(value: u64) -> bool { + // + value > 0 && value <= 100 +} + +pub fn get_current_user_guilds_limit(value: u64) -> bool { + // + value > 0 && value <= 100 +} + +pub fn get_guild_members_limit(value: u64) -> bool { + // + value > 0 && value <= 1000 +} + +pub fn get_reactions_limit(value: u64) -> bool { + // + value > 0 && value <= 100 +} + +pub fn guild_name(value: impl AsRef) -> bool { + _guild_name(value.as_ref()) +} + +fn _guild_name(value: &str) -> bool { + let len = value.chars().count(); + + // + len >= 2 && len <= 100 +} + +pub fn guild_prune_days(value: u64) -> bool { + // + value > 0 +} + +pub fn nickname(value: impl AsRef) -> bool { + _nickname(value.as_ref()) +} + +fn _nickname(value: &str) -> bool { + let len = value.chars().count(); + + // + len > 0 && len <= 32 +} + +pub fn username(value: impl AsRef) -> bool { + // + _username(value.as_ref()) +} + +fn _username(value: &str) -> bool { + let len = value.chars().count(); + + len >= 2 && len <= 32 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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("aa")); + assert!(channel_name("a".repeat(100))); + + assert!(!channel_name("")); + assert!(!channel_name("a")); + 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_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(100)); + } + + #[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))); + } +} From 3748a6afcf6ce2bbe843adcfb3c5b0e4e1e5d882 Mon Sep 17 00:00:00 2001 From: Vivian Hellyer Date: Mon, 25 May 2020 22:07:21 -0400 Subject: [PATCH 2/5] cargo clippy Signed-off-by: Vivian Hellyer --- http/src/request/channel/update_channel.rs | 2 +- http/src/request/guild/create_guild.rs | 2 +- http/src/request/guild/create_guild_channel.rs | 2 +- http/src/request/guild/member/update_guild_member.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/http/src/request/channel/update_channel.rs b/http/src/request/channel/update_channel.rs index 4b232613115..01baf9a1107 100644 --- a/http/src/request/channel/update_channel.rs +++ b/http/src/request/channel/update_channel.rs @@ -174,7 +174,7 @@ impl<'a> UpdateChannel<'a> { return Err(UpdateChannelError::TopicInvalid); } - self.fields.topic.replace(topic.into()); + self.fields.topic.replace(topic); Ok(self) } diff --git a/http/src/request/guild/create_guild.rs b/http/src/request/guild/create_guild.rs index 6730c45c0fa..43f17c3b73c 100644 --- a/http/src/request/guild/create_guild.rs +++ b/http/src/request/guild/create_guild.rs @@ -75,7 +75,7 @@ impl<'a> CreateGuild<'a> { default_message_notifications: None, explicit_content_filter: None, icon: None, - name: name.into(), + name, region: None, roles: None, verification_level: None, diff --git a/http/src/request/guild/create_guild_channel.rs b/http/src/request/guild/create_guild_channel.rs index 1e1aae36282..efa96f12e8c 100644 --- a/http/src/request/guild/create_guild_channel.rs +++ b/http/src/request/guild/create_guild_channel.rs @@ -76,7 +76,7 @@ impl<'a> CreateGuildChannel<'a> { fields: CreateGuildChannelFields { bitrate: None, kind: None, - name: name.into(), + name, nsfw: None, parent_id: None, permission_overwrites: None, diff --git a/http/src/request/guild/member/update_guild_member.rs b/http/src/request/guild/member/update_guild_member.rs index 52b6c8a558f..489bdfad770 100644 --- a/http/src/request/guild/member/update_guild_member.rs +++ b/http/src/request/guild/member/update_guild_member.rs @@ -93,7 +93,7 @@ impl<'a> UpdateGuildMember<'a> { return Err(UpdateGuildMemberError::NicknameInvalid); } - self.fields.nick.replace(nick.into()); + self.fields.nick.replace(nick); Ok(self) } From a62838f1622c8f5a176111365fdadc6600371c57 Mon Sep 17 00:00:00 2001 From: Vivian Hellyer Date: Mon, 25 May 2020 22:20:02 -0400 Subject: [PATCH 3/5] fix test Signed-off-by: Vivian Hellyer --- twilight/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twilight/src/lib.rs b/twilight/src/lib.rs index c3c7986bbfd..db90a0f1926 100644 --- a/twilight/src/lib.rs +++ b/twilight/src/lib.rs @@ -117,7 +117,7 @@ //! } //! (_, Event::MessageCreate(msg)) => { //! if msg.content == "!ping" { -//! http.create_message(msg.channel_id).content("Pong!").await?; +//! http.create_message(msg.channel_id).content("Pong!")?.await?; //! } //! } //! _ => {} From 81d831331cb3358b09bff0671214462c29c7debd Mon Sep 17 00:00:00 2001 From: Vivian Hellyer Date: Tue, 26 May 2020 06:04:40 -0400 Subject: [PATCH 4/5] add embed validation Add embed validation for creating and updating messages. This checks the the author name, description, field names, field values, footer text, and title lengths to ensure they aren't too long. The total number of fields is checked as well as the total combined length of all of the above. This addresses comment . Limits are referenced in the documentation to point to . Signed-off-by: Vivian Hellyer --- http/Cargo.toml | 2 +- .../request/channel/message/create_message.rs | 23 +- http/src/request/channel/message/mod.rs | 1 + .../request/channel/message/update_message.rs | 31 +- http/src/request/validate.rs | 447 ++++++++++++++++++ 5 files changed, 496 insertions(+), 8 deletions(-) diff --git a/http/Cargo.toml b/http/Cargo.toml index 54785a118d0..a00cac4e118 100644 --- a/http/Cargo.toml +++ b/http/Cargo.toml @@ -24,4 +24,4 @@ percent-encoding = "2.1" url = "2" [dev-dependencies] -tokio = "0.2" +tokio = { features = ["macros"], version = "0.2" } diff --git a/http/src/request/channel/message/create_message.rs b/http/src/request/channel/message/create_message.rs index a6784ced0b4..5f5814aa601 100644 --- a/http/src/request/channel/message/create_message.rs +++ b/http/src/request/channel/message/create_message.rs @@ -17,17 +17,30 @@ use twilight_model::{ #[derive(Clone, Debug)] pub enum CreateMessageError { ContentInvalid, + EmbedTooLarge { source: EmbedValidationError }, } impl Display for CreateMessageError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { Self::ContentInvalid => f.write_str("the message content is invalid"), + Self::EmbedTooLarge { + .. + } => f.write_str("the embed's contents are too long"), } } } -impl Error for CreateMessageError {} +impl Error for CreateMessageError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ContentInvalid => None, + Self::EmbedTooLarge { + source, + } => Some(source), + } + } +} #[derive(Default, Serialize)] pub(crate) struct CreateMessageFields { @@ -85,10 +98,14 @@ impl<'a> CreateMessage<'a> { Ok(self) } - pub fn embed(mut self, embed: Embed) -> Self { + pub fn embed(mut self, embed: Embed) -> Result { + validate::embed(&embed).map_err(|source| CreateMessageError::EmbedTooLarge { + source, + })?; + self.fields.embed.replace(embed); - self + Ok(self) } pub fn allowed_mentions( diff --git a/http/src/request/channel/message/mod.rs b/http/src/request/channel/message/mod.rs index 7c63076ff3f..9dfa5869c47 100644 --- a/http/src/request/channel/message/mod.rs +++ b/http/src/request/channel/message/mod.rs @@ -17,3 +17,4 @@ pub use self::{ get_message::GetMessage, update_message::UpdateMessage, }; +pub use super::super::validate::EmbedValidationError; diff --git a/http/src/request/channel/message/update_message.rs b/http/src/request/channel/message/update_message.rs index 01c808a7e2e..940def767d6 100644 --- a/http/src/request/channel/message/update_message.rs +++ b/http/src/request/channel/message/update_message.rs @@ -11,17 +11,30 @@ use twilight_model::{ #[derive(Clone, Debug)] pub enum UpdateMessageError { ContentInvalid, + EmbedTooLarge { source: EmbedValidationError }, } impl Display for UpdateMessageError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { Self::ContentInvalid => f.write_str("the message content is invalid"), + Self::EmbedTooLarge { + .. + } => f.write_str("the embed's contents are too long"), } } } -impl Error for UpdateMessageError {} +impl Error for UpdateMessageError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ContentInvalid => None, + Self::EmbedTooLarge { + source, + } => Some(source), + } + } +} #[derive(Default, Serialize)] struct UpdateMessageFields { @@ -129,10 +142,20 @@ impl<'a> UpdateMessage<'a> { /// Set the embed of the message. /// /// Pass `None` if you want to remove the message embed. - pub fn embed(mut self, embed: impl Into>) -> Self { - self.fields.embed.replace(embed.into()); + pub fn embed(self, embed: impl Into>) -> Result { + self._embed(embed.into()) + } - self + fn _embed(mut self, embed: Option) -> Result { + if let Some(embed) = embed.as_ref() { + validate::embed(&embed).map_err(|source| UpdateMessageError::EmbedTooLarge { + source, + })?; + } + + self.fields.embed.replace(embed); + + Ok(self) } fn start(&mut self) -> Result<()> { diff --git a/http/src/request/validate.rs b/http/src/request/validate.rs index 234dd10ff4d..1aaa1c37414 100644 --- a/http/src/request/validate.rs +++ b/http/src/request/validate.rs @@ -3,6 +3,141 @@ /// This is in a centralised place so that the validation parameters can be kept /// up-to-date more easily and because some of the checks are re-used across /// different modules. +use std::{ + error::Error, + fmt::{Display, Formatter, Result as FmtResult}, +}; +use twilight_model::channel::embed::Embed; + +/// An embed is not valid. +/// +/// Referenced values are used from [the Discord docs][docs]. +/// +/// [docs]: https://discord.com/developers/docs/resources/channel#embed-limits +#[derive(Clone, Debug)] +pub enum EmbedValidationError { + /// The embed author's name is larger than + /// [the maximum][`AUTHOR_NAME_LENGTH`]. + /// + /// [`AUTHOR_NAME_LENGTH`]: #const.AUTHOR_NAME_LENGTH + AuthorNameTooLarge { chars: usize }, + /// The embed description is larger than + /// [the maximum][`DESCRIPTION_LENGTH`]. + /// + /// [`DESCRIPTION_LENGTH`]: #const.DESCRIPTION_LENGTH + DescriptionTooLarge { chars: usize }, + /// The combined content of all embed fields - author name, description, + /// footer, field names and values, and title - is larger than + /// [the maximum][`EMBED_TOTAL_LENGTH`]. + /// + /// [`EMBED_TOTAL_LENGTH`]: #const.EMBED_TOTAL_LENGTH + EmbedTooLarge { chars: usize }, + /// A field's name is larger than [the maximum][`FIELD_NAME_LENGTH`]. + /// + /// [`FIELD_NAME_LENGTH`]: #const.FIELD_NAME_LENGTH + FieldNameTooLarge { chars: usize }, + /// A field's value is larger than [the maximum][`FIELD_VALUE_LENGTH`]. + /// + /// [`FIELD_VALUE_LENGTH`]: #const.FIELD_VALUE_LENGTH + FieldValueTooLarge { chars: usize }, + /// The footer text is larger than [the maximum][`FOOTER_TEXT_LENGTH`]. + /// + /// [`FOOTER_TEXT_LENGTH`]: #const.FOOTER_TEXT_LENGTH + FooterTextTooLarge { chars: usize }, + /// The title is larger than [the maximum][`TITLE_LENGTH`]. + /// + /// [`TITLE_LENGTH`]: #const.TITLE_LENGTH + TitleTooLarge { chars: usize }, + /// There are more than [the maximum][`FIELD_COUNT`] number of fields in the + /// embed. + /// + /// [`FIELD_COUNT`]: #const.FIELD_COUNT + TooManyFields { amount: usize }, +} + +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; +} + +impl Display for EmbedValidationError { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::AuthorNameTooLarge { + chars, + } => write!( + f, + "the author name is {} characters long, the max is {}", + chars, + Self::AUTHOR_NAME_LENGTH + ), + Self::DescriptionTooLarge { + chars, + } => write!( + f, + "the description is {} characters long, the max is {}", + chars, + Self::DESCRIPTION_LENGTH + ), + Self::EmbedTooLarge { + chars, + } => write!( + f, + "the combined total length of the embed is {} characters long, the max is {}", + chars, + Self::EMBED_TOTAL_LENGTH + ), + Self::FieldNameTooLarge { + chars, + } => write!( + f, + "a field name is {} characters long, the max is {}", + chars, + Self::FIELD_NAME_LENGTH + ), + Self::FieldValueTooLarge { + chars, + } => write!( + f, + "a field value is {} characters long, the max is {}", + chars, + Self::FIELD_VALUE_LENGTH + ), + Self::FooterTextTooLarge { + chars, + } => write!( + f, + "the footer's text is {} characters long, the max is {}", + chars, + Self::FOOTER_TEXT_LENGTH + ), + Self::TitleTooLarge { + chars, + } => write!( + f, + "the title's length is {} characters long, the max is {}", + chars, + Self::TITLE_LENGTH + ), + Self::TooManyFields { + amount, + } => write!( + f, + "there are {} fields, but the maximum amount is {}", + amount, + Self::FIELD_COUNT + ), + } + } +} + +impl Error for EmbedValidationError {} pub fn ban_delete_message_days(value: u64) -> bool { // @@ -29,6 +164,96 @@ 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::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::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::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::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::FieldNameTooLarge { + chars: name_chars, + }); + } + + let value_chars = field.value.chars().count(); + + if value_chars > EmbedValidationError::FIELD_VALUE_LENGTH { + return Err(EmbedValidationError::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::TitleTooLarge { + chars, + }); + } + + total += chars; + } + + if total > EmbedValidationError::EMBED_TOTAL_LENGTH { + return Err(EmbedValidationError::EmbedTooLarge { + chars: total, + }); + } + + Ok(()) +} + pub fn get_audit_log_limit(value: u64) -> bool { // value > 0 && value <= 100 @@ -95,6 +320,25 @@ fn _username(value: &str) -> bool { #[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() { @@ -123,6 +367,209 @@ mod tests { 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(0xff0000); + 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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + Err(EmbedValidationError::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), + }) + } + + // we're at 5304 characters now + 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), + Err(EmbedValidationError::EmbedTooLarge { + chars: 6304, + }) + )); + } + #[test] fn test_get_audit_log_limit() { assert!(get_audit_log_limit(1)); From 0b48c250fdf1853fb74fd3f9f2a5ee8bd2be836a Mon Sep 17 00:00:00 2001 From: Vivian Hellyer Date: Tue, 26 May 2020 06:09:14 -0400 Subject: [PATCH 5/5] http/validate: consistent embed error messages Signed-off-by: Vivian Hellyer --- http/src/request/validate.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/http/src/request/validate.rs b/http/src/request/validate.rs index 1aaa1c37414..62d95b32f48 100644 --- a/http/src/request/validate.rs +++ b/http/src/request/validate.rs @@ -73,7 +73,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "the author name is {} characters long, the max is {}", + "the author name is {} characters long, but the max is {}", chars, Self::AUTHOR_NAME_LENGTH ), @@ -81,7 +81,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "the description is {} characters long, the max is {}", + "the description is {} characters long, but the max is {}", chars, Self::DESCRIPTION_LENGTH ), @@ -89,7 +89,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "the combined total length of the embed is {} characters long, the max is {}", + "the combined total length of the embed is {} characters long, but the max is {}", chars, Self::EMBED_TOTAL_LENGTH ), @@ -97,7 +97,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "a field name is {} characters long, the max is {}", + "a field name is {} characters long, but the max is {}", chars, Self::FIELD_NAME_LENGTH ), @@ -105,7 +105,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "a field value is {} characters long, the max is {}", + "a field value is {} characters long, but the max is {}", chars, Self::FIELD_VALUE_LENGTH ), @@ -113,7 +113,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "the footer's text is {} characters long, the max is {}", + "the footer's text is {} characters long, but the max is {}", chars, Self::FOOTER_TEXT_LENGTH ), @@ -121,7 +121,7 @@ impl Display for EmbedValidationError { chars, } => write!( f, - "the title's length is {} characters long, the max is {}", + "the title's length is {} characters long, but the max is {}", chars, Self::TITLE_LENGTH ),