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/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..5f5814aa601 100644 --- a/http/src/request/channel/message/create_message.rs +++ b/http/src/request/channel/message/create_message.rs @@ -4,12 +4,44 @@ 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, + 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 { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ContentInvalid => None, + Self::EmbedTooLarge { + source, + } => Some(source), + } + } +} + #[derive(Default, Serialize)] pub(crate) struct CreateMessageFields { content: Option, @@ -42,16 +74,38 @@ 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 { + 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/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..9dfa5869c47 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, @@ -16,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 b840a713f5f..940def767d6 100644 --- a/http/src/request/channel/message/update_message.rs +++ b/http/src/request/channel/message/update_message.rs @@ -1,9 +1,41 @@ 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, + 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 { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ContentInvalid => None, + Self::EmbedTooLarge { + source, + } => Some(source), + } + } +} + #[derive(Default, Serialize)] struct UpdateMessageFields { // We don't serialize if this is Option::None, to avoid overwriting the @@ -39,7 +71,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 +86,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,19 +114,48 @@ 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()) + } + + fn _content(mut self, content: Option) -> Result { + if let Some(content) = content.as_ref() { + if !validate::content_limit(content) { + return Err(UpdateMessageError::ContentInvalid); + } + } - self + self.fields.content.replace(content); + + Ok(self) } /// 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()) + } + + 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); - self + Ok(self) } fn start(&mut self) -> Result<()> { 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..01baf9a1107 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 { - 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(UpdateChannelError::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/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..43f17c3b73c 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,27 +60,51 @@ 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, explicit_content_filter: None, icon: None, - name: name.into(), + name, region: None, roles: None, verification_level: None, }, 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..efa96f12e8c 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,12 +55,28 @@ 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, - name: name.into(), + name, nsfw: None, parent_id: None, permission_overwrites: 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..489bdfad770 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 { - self.fields.nick.replace(nick.into()); + /// 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()) + } - self + fn _nick(mut self, nick: String) -> Result { + if !validate::nickname(&nick) { + return Err(UpdateGuildMemberError::NicknameInvalid); + } + + self.fields.nick.replace(nick); + + 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..62d95b32f48 --- /dev/null +++ b/http/src/request/validate.rs @@ -0,0 +1,652 @@ +/// 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. +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, but the max is {}", + chars, + Self::AUTHOR_NAME_LENGTH + ), + Self::DescriptionTooLarge { + chars, + } => write!( + f, + "the description is {} characters long, but the max is {}", + chars, + Self::DESCRIPTION_LENGTH + ), + Self::EmbedTooLarge { + chars, + } => write!( + f, + "the combined total length of the embed is {} characters long, but the max is {}", + chars, + Self::EMBED_TOTAL_LENGTH + ), + Self::FieldNameTooLarge { + chars, + } => write!( + f, + "a field name is {} characters long, but the max is {}", + chars, + Self::FIELD_NAME_LENGTH + ), + Self::FieldValueTooLarge { + chars, + } => write!( + f, + "a field value is {} characters long, but the max is {}", + chars, + Self::FIELD_VALUE_LENGTH + ), + Self::FooterTextTooLarge { + chars, + } => write!( + f, + "the footer's text is {} characters long, but the max is {}", + chars, + Self::FOOTER_TEXT_LENGTH + ), + Self::TitleTooLarge { + chars, + } => write!( + f, + "the title's length is {} characters long, but 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 { + // + 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 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 +} + +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::*; + 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() { + 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_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)); + 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))); + } +} 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?; //! } //! } //! _ => {}