diff --git a/Cargo.toml b/Cargo.toml index b1fe797..3f35601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,5 @@ serde = {version = "1.0.159", features = ["derive"]} serde_json = "1.0.95" reqwest = "0.11.16" url = "2.3.1" -chrono = "0.4.24" \ No newline at end of file +chrono = "0.4.24" +regex = "1.7.3" \ No newline at end of file diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs new file mode 100644 index 0000000..ee8788e --- /dev/null +++ b/src/api/auth/login.rs @@ -0,0 +1 @@ +pub mod login {} diff --git a/src/api/auth/mod.rs b/src/api/auth/mod.rs new file mode 100644 index 0000000..3093e74 --- /dev/null +++ b/src/api/auth/mod.rs @@ -0,0 +1,5 @@ +pub mod login; +pub mod register; + +pub use login::*; +pub use register::*; diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs new file mode 100644 index 0000000..f515703 --- /dev/null +++ b/src/api/auth/register.rs @@ -0,0 +1,7 @@ +pub mod register { + use crate::instance::Instance; + + impl Instance { + pub fn register() {} + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 0d7b94c..f619259 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,7 @@ +pub mod auth; pub mod policies; +pub mod schemas; +pub use policies::instance::instance::*; pub use policies::instance::limits::*; +pub use schemas::*; diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs new file mode 100644 index 0000000..6ed8b05 --- /dev/null +++ b/src/api/policies/instance/instance.rs @@ -0,0 +1,83 @@ +pub mod instance { + use std::fmt; + + use reqwest::Client; + use serde_json::from_str; + + use crate::{api::schemas::schemas::InstancePoliciesSchema, instance::Instance}; + + #[derive(Debug, PartialEq, Eq)] + pub struct InstancePoliciesError { + pub message: String, + } + + impl InstancePoliciesError { + fn new(message: String) -> Self { + InstancePoliciesError { message } + } + } + + impl fmt::Display for InstancePoliciesError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } + } + + impl std::error::Error for InstancePoliciesError {} + impl Instance { + /// Gets the instance policies schema. + /// # Errors + /// * [`InstancePoliciesError`] - If the request fails. + pub async fn instance_policies_schema( + &self, + ) -> Result { + let client = Client::new(); + let endpoint_url = self.urls.get_api().to_string() + "/policies/instance/"; + let request = match client.get(&endpoint_url).send().await { + Ok(result) => result, + Err(e) => { + return Err(InstancePoliciesError { + message: format!( + "An error occured while trying to GET from {}: {}", + endpoint_url, e + ), + }); + } + }; + + if request.status().as_str().chars().next().unwrap() != '2' { + return Err(InstancePoliciesError { + message: format!( + "Received the following error code while requesting from the route: {}", + request.status().as_str() + ), + }); + } + + let body = request.text().await.unwrap(); + let instance_policies_schema: InstancePoliciesSchema = from_str(&body).unwrap(); + Ok(instance_policies_schema) + } + } +} + +#[cfg(test)] +mod instance_policies_schema_test { + use crate::{instance::Instance, limit::LimitedRequester, URLBundle}; + + #[tokio::test] + async fn generate_instance_policies_schema() { + let urls = URLBundle::new( + "http://localhost:3001/api".to_string(), + "http://localhost:3001".to_string(), + "http://localhost:3001".to_string(), + ); + let limited_requester = LimitedRequester::new(urls.get_api().to_string()).await; + let test_instance = Instance::new(urls.clone(), limited_requester) + .await + .unwrap(); + + let schema = test_instance.instance_policies_schema().await.unwrap(); + println!("{}", schema); + } +} diff --git a/src/api/policies/instance/limits.rs b/src/api/policies/instance/limits.rs index c370cd0..a46a29f 100644 --- a/src/api/policies/instance/limits.rs +++ b/src/api/policies/instance/limits.rs @@ -197,6 +197,10 @@ pub mod limits { /// check_limits uses the API to get the current request limits of the instance. /// It returns a `Limits` struct containing all the limits. + /// If the rate limit is disabled, then the limit is set to `u64::MAX`. + /// # Errors + /// This function will panic if the request fails or if the response body cannot be parsed. + /// TODO: Change this to return a Result and handle the errors properly. pub async fn check_limits(api_url: String) -> HashMap { let client = Client::new(); let url_parsed = crate::URLBundle::parse_url(api_url) + "/policies/instance/limits"; diff --git a/src/api/policies/instance/mod.rs b/src/api/policies/instance/mod.rs index e695695..b05078e 100644 --- a/src/api/policies/instance/mod.rs +++ b/src/api/policies/instance/mod.rs @@ -1,5 +1,5 @@ -// src/api/policies/instance/mod.rs - +pub mod instance; pub mod limits; +pub use instance::*; pub use limits::*; diff --git a/src/api/schemas.rs b/src/api/schemas.rs new file mode 100644 index 0000000..1ca238e --- /dev/null +++ b/src/api/schemas.rs @@ -0,0 +1,331 @@ +pub mod schemas { + use std::fmt; + + use regex::Regex; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "snake_case")] + pub struct RegisterSchema { + username: String, + password: Option, + consent: bool, + email: Option, + fingerprint: Option, + invite: Option, + date_of_birth: Option, + gift_code_sku_id: Option, + captcha_key: Option, + promotional_email_opt_in: Option, + } + + #[derive(Debug, PartialEq, Eq)] + pub struct RegisterSchemaError { + pub message: String, + } + + impl RegisterSchemaError { + fn new(message: String) -> Self { + RegisterSchemaError { message } + } + } + + impl fmt::Display for RegisterSchemaError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } + } + + impl std::error::Error for RegisterSchemaError {} + + impl RegisterSchema { + /** + Returns a new [`Result`]. + ## Arguments + All but "String::username" and "bool::consent" are optional. + + ## Errors + You will receive a [`RegisterSchemaError`], if: + - The username is less than 2 or more than 32 characters in length + - You supply a `password` which is less than 1 or more than 72 characters in length. + + These constraints have been defined [in the Spacebar-API](https://docs.spacebar.chat/routes/) + */ + pub fn new( + username: String, + password: Option, + consent: bool, + email: Option, + fingerprint: Option, + invite: Option, + date_of_birth: Option, + gift_code_sku_id: Option, + captcha_key: Option, + promotional_email_opt_in: Option, + ) -> Result { + if username.len() < 2 || username.len() > 32 { + return Err(RegisterSchemaError::new( + "Username must be between 2 and 32 characters".to_string(), + )); + } + if password.is_some() + && (password.as_ref().unwrap().len() < 1 || password.as_ref().unwrap().len() > 72) + { + return Err(RegisterSchemaError { + message: "Password must be between 1 and 72 characters.".to_string(), + }); + } + if !consent { + return Err(RegisterSchemaError { + message: "Consent must be 'true' to register.".to_string(), + }); + } + + let regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); + if email.clone().is_some() && !regex.is_match(email.clone().unwrap().as_str()) { + return Err(RegisterSchemaError { + message: "The provided email address is in an invalid format.".to_string(), + }); + } + + return Ok(RegisterSchema { + username, + password, + consent, + email, + fingerprint, + invite, + date_of_birth, + gift_code_sku_id, + captcha_key, + promotional_email_opt_in, + }); + } + } + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + #[serde(rename_all = "snake_case")] + pub struct LoginSchema { + login: String, + password: String, + undelete: Option, + captcha_key: Option, + login_source: Option, + gift_code_sku_id: Option, + } + + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "snake_case")] + pub struct TotpSchema { + code: String, + ticket: String, + gift_code_sku_id: Option, + login_source: Option, + } + + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] + #[serde(rename_all = "camelCase")] + pub struct InstancePoliciesSchema { + instance_name: String, + instance_description: Option, + front_page: Option, + tos_page: Option, + correspondence_email: Option, + correspondence_user_id: Option, + image: Option, + instance_id: Option, + } + + impl InstancePoliciesSchema { + pub fn new( + instance_name: String, + instance_description: Option, + front_page: Option, + tos_page: Option, + correspondence_email: Option, + correspondence_user_id: Option, + image: Option, + instance_id: Option, + ) -> Self { + InstancePoliciesSchema { + instance_name, + instance_description, + front_page, + tos_page, + correspondence_email, + correspondence_user_id, + image, + instance_id, + } + } + } + + impl fmt::Display for InstancePoliciesSchema { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "InstancePoliciesSchema {{ instance_name: {}, instance_description: {}, front_page: {}, tos_page: {}, correspondence_email: {}, correspondence_user_id: {}, image: {}, instance_id: {} }}", + self.instance_name, + self.instance_description.clone().unwrap_or("None".to_string()), + self.front_page.clone().unwrap_or("None".to_string()), + self.tos_page.clone().unwrap_or("None".to_string()), + self.correspondence_email.clone().unwrap_or("None".to_string()), + self.correspondence_user_id.clone().unwrap_or("None".to_string()), + self.image.clone().unwrap_or("None".to_string()), + self.instance_id.clone().unwrap_or("None".to_string()), + ) + } + } +} + +// I know that some of these tests are... really really basic and unneccessary, but sometimes, I +// just feel like writing tests, so there you go :) -@bitfl0wer +#[cfg(test)] +mod schemas_tests { + use super::schemas::*; + + #[test] + fn password_too_short() { + assert_eq!( + RegisterSchema::new( + "Test".to_string(), + Some("".to_string()), + true, + None, + None, + None, + None, + None, + None, + None, + ), + Err(RegisterSchemaError { + message: "Password must be between 1 and 72 characters.".to_string() + }) + ); + } + + #[test] + fn password_too_long() { + let mut long_pw = String::new(); + for _ in 0..73 { + long_pw = long_pw + "a"; + } + assert_eq!( + RegisterSchema::new( + "Test".to_string(), + Some(long_pw), + true, + None, + None, + None, + None, + None, + None, + None, + ), + Err(RegisterSchemaError { + message: "Password must be between 1 and 72 characters.".to_string() + }) + ); + } + + #[test] + fn username_too_short() { + assert_eq!( + RegisterSchema::new( + "T".to_string(), + None, + true, + None, + None, + None, + None, + None, + None, + None, + ), + Err(RegisterSchemaError { + message: "Username must be between 2 and 32 characters".to_string() + }) + ); + } + + #[test] + fn username_too_long() { + let mut long_un = String::new(); + for _ in 0..33 { + long_un = long_un + "a"; + } + assert_eq!( + RegisterSchema::new(long_un, None, true, None, None, None, None, None, None, None,), + Err(RegisterSchemaError { + message: "Username must be between 2 and 32 characters".to_string() + }) + ); + } + + #[test] + fn consent_false() { + assert_eq!( + RegisterSchema::new( + "Test".to_string(), + None, + false, + None, + None, + None, + None, + None, + None, + None, + ), + Err(RegisterSchemaError { + message: "Consent must be 'true' to register.".to_string() + }) + ); + } + + #[test] + fn invalid_email() { + assert_eq!( + RegisterSchema::new( + "Test".to_string(), + None, + true, + Some("p@p.p".to_string()), + None, + None, + None, + None, + None, + None, + ), + Err(RegisterSchemaError { + message: "The provided email address is in an invalid format.".to_string() + }) + ) + } + + #[test] + fn valid_email() { + let reg = RegisterSchema::new( + "Test".to_string(), + None, + true, + Some("me@mail.xy".to_string()), + None, + None, + None, + None, + None, + None, + ); + assert_ne!( + reg, + Err(RegisterSchemaError { + message: "The provided email address is in an invalid format.".to_string() + }) + ); + } +} diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index 2033808..0000000 --- a/src/client.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::gateway::Gateway; -use crate::limit::LimitedRequester; - -struct ClientBuilder {} - -/* impl ClientBuilder { - fn build() -> Client {} -} */ - -struct Client {} diff --git a/src/gateway.rs b/src/gateway.rs index 5489064..e4ba3c9 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1 +1,2 @@ +#[derive(Debug)] pub struct Gateway {} diff --git a/src/instance.rs b/src/instance.rs new file mode 100644 index 0000000..f76edf8 --- /dev/null +++ b/src/instance.rs @@ -0,0 +1,122 @@ +use regex::internal::Inst; + +use crate::api::instance; +use crate::api::schemas::schemas::InstancePoliciesSchema; +use crate::gateway::Gateway; +use crate::limit::LimitedRequester; +use crate::URLBundle; + +use std::collections::HashMap; +use std::fmt; + +#[derive(Debug)] +/** +The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server. + */ +pub struct Instance { + pub urls: URLBundle, + pub instance_info: InstancePoliciesSchema, + pub requester: LimitedRequester, + //pub gateway: Gateway, + //pub users: HashMap, +} + +impl Instance { + /// Creates a new [`Instance`]. + /// # Arguments + /// * `urls` - The [`URLBundle`] that contains all the URLs that are needed to connect to the Spacebar server. + /// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. + /// # Errors + /// * [`InstanceError`] - If the instance cannot be created. + pub async fn new( + urls: URLBundle, + requester: LimitedRequester, + ) -> Result { + let mut instance = Instance { + urls, + instance_info: InstancePoliciesSchema::new( + // This is okay, because the instance_info will be overwritten by the instance_policies_schema() function. + "".to_string(), + None, + None, + None, + None, + None, + None, + None, + ), + requester, + //gateway: (), + //users: (), + }; + instance.instance_info = match instance.instance_policies_schema().await { + Ok(schema) => schema, + Err(e) => return Err(InstanceError{message: format!("Something seems to be wrong with the instance. Cannot get information about the instance: {}", e)}), + }; + Ok(instance) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct InstanceError { + pub message: String, +} + +impl InstanceError { + fn new(message: String) -> Self { + InstanceError { message } + } +} + +impl fmt::Display for InstanceError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for InstanceError {} + +#[derive(Debug, PartialEq, Eq)] +pub struct Token { + pub token: String, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Username { + pub username: String, +} + +impl Username { + /// Creates a new [`Username`]. + /// # Arguments + /// * `username` - The username that will be used to create the [`Username`]. + /// # Errors + /// * [`UsernameFormatError`] - If the username is not between 2 and 32 characters. + pub fn new(username: String) -> Result { + if username.len() < 2 || username.len() > 32 { + return Err(UsernameFormatError::new( + "Username must be between 2 and 32 characters".to_string(), + )); + } + return Ok(Username { username }); + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct UsernameFormatError { + pub message: String, +} + +impl UsernameFormatError { + fn new(message: String) -> Self { + UsernameFormatError { message } + } +} + +impl fmt::Display for UsernameFormatError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for UsernameFormatError {} diff --git a/src/lib.rs b/src/lib.rs index ee1ae54..838f31b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ mod api; -mod client; mod gateway; +mod instance; mod limit; mod voice; use url::{ParseError, Url}; -#[derive(Clone, Default, Debug)] +#[derive(Clone, Default, Debug, PartialEq, Eq)] /// A URLBundle is a struct which bundles together the API-, Gateway- and CDN-URLs of a Spacebar /// instance.