Ratelimiter overhaul (#144)

* Rename limits and limit to have better names

* Remove empty lines

* Remove handle_request (moved to requestlimiter)

* Start working on new ratelimiter

* Make limits Option, add "limited?" to constructor

* Add missing logic to send_request

* Rename Limits

* Create Ratelimits and Limit Struct

* Define Limit

* Import Ratelimits

* Define get_rate_limits

* Remove unused import

* + check_rate_limits & limits_config_to_ratelimits

* Remove Absolute Limits
These limits are not meant to be tracked anyways.

* add ratelimits is_exhausted

* Add error handling and send request checking

* change limits to option ratelimits

* Add strum

* Change Ratelimits to Hashmap

* Remove ratelimits in favor of hashmap

* Change code from struct to hashmap

* start working on update rate limits

* Remove wrong import

* Rename ChorusLibError to ChorusError

* Documented the chorus errors

* Made error documentation docstring

* Make ReceivedErrorCodeError have error string

* Remove unneeded import

* Match changes in errors.rs

* Improve update_rate_limits and can_send_request

* add ratelimits.to_hash_map()

* use instances' client instead of new client

* add LimitsConfiguration to instance

* improve update_limits, change a method name

* Fix un-updated errors

* Get LimitConfiguration in a sane way

* Move common.rs into ratelimiter::ChorusRequest

* Delete common.rs

* Make instance.rs use overhauled errors

* Refactor to use new Rate limiting implementation

* Refactor to use new Rate limiting implementation

* Refactor to use new Rate limiting implementation

* Refactor to use new Rate limiting implementation

* Refactor to use new Rate limiting implementation

* Refactor to use new Rate limiting implementation

* update ratelimiter implementation across all files

* Fix remaining errors post-refactor

* Changed Enum case to be correct

* Use result

* Re-add missing body to request

* Remove unneeded late initalization

* Change visibility from pub to pub(crate)
I feel like these core methods don't need to be exposed as public API.

* Remove unnecessary import

* Fix clippy warnings

* Add docstring

* Change Error names across all files

* Update Cargo.toml

Strum is not needed

* Update ratelimits.rs

* Update ratelimits.rs

* Bug/discord instance info unavailable (#146)

* Change text to be more ambigous

* Use default Configuration instead of erroring out

* Emit warning log if instance config cant be gotten

* Remove import

* Update src/instance.rs

Co-authored-by: SpecificProtagonist <specificprotagonist@posteo.org>

* Add missing closing bracket

* Put limits and limits_configuration as one struct

* Derive Hash

* remove import

* rename limits and limits_configuration

* Save clone call

* Change LimitsConfiguration to RateLimits
`LimitsConfiguration` is in no way related to whether the instance has API rate limits enabled or not. Therefore, it has been replaced with what it should have been all along.

* Add ensure_limit_in_map(), add `window` to `Limit`

* Remove unneeded var

* Remove import

* Clean up unneeded things
Dead code warnings have been supressed, but flagged as FIXME so they don't get forgotten. Anyone using tools like TODO Tree in VSCode can still see that they are there, however, they will not be shown as warnings anymore

* Remove nested submodule `limit`

* Add doc comments

* Add more doc comments

* Add some log messages to some methods

---------

Co-authored-by: SpecificProtagonist <specificprotagonist@posteo.org>
This commit is contained in:
Flori 2023-07-09 18:38:02 +02:00 committed by GitHub
parent 08ea9e2dfb
commit 8c9ccdd997
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1049 additions and 1389 deletions

View File

@ -39,4 +39,4 @@ log = "0.4.19"
[dev-dependencies] [dev-dependencies]
tokio = {version = "1.28.1", features = ["full"]} tokio = {version = "1.28.1", features = ["full"]}
lazy_static = "1.4.0" lazy_static = "1.4.0"
rusty-hook = "0.11.2" rusty-hook = "0.11.2"

View File

@ -2,57 +2,41 @@ use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use reqwest::Client; use reqwest::Client;
use serde_json::{from_str, json}; use serde_json::to_string;
use crate::api::limits::LimitType; use crate::api::LimitType;
use crate::errors::{ChorusLibError, ChorusResult}; use crate::errors::ChorusResult;
use crate::instance::{Instance, UserMeta}; use crate::instance::{Instance, UserMeta};
use crate::limit::LimitedRequester; use crate::ratelimiter::ChorusRequest;
use crate::types::{ErrorResponse, LoginResult, LoginSchema}; use crate::types::{LoginResult, LoginSchema};
impl Instance { impl Instance {
pub async fn login_account(&mut self, login_schema: &LoginSchema) -> ChorusResult<UserMeta> { pub async fn login_account(&mut self, login_schema: &LoginSchema) -> ChorusResult<UserMeta> {
let json_schema = json!(login_schema);
let client = Client::new();
let endpoint_url = self.urls.api.clone() + "/auth/login"; let endpoint_url = self.urls.api.clone() + "/auth/login";
let request_builder = client.post(endpoint_url).body(json_schema.to_string()); let chorus_request = ChorusRequest {
request: Client::new()
.post(endpoint_url)
.body(to_string(login_schema).unwrap()),
limit_type: LimitType::AuthLogin,
};
// We do not have a user yet, and the UserRateLimits will not be affected by a login // We do not have a user yet, and the UserRateLimits will not be affected by a login
// request (since login is an instance wide limit), which is why we are just cloning the // request (since login is an instance wide limit), which is why we are just cloning the
// instances' limits to pass them on as user_rate_limits later. // instances' limits to pass them on as user_rate_limits later.
let mut cloned_limits = self.limits.clone(); let mut shell = UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string());
let response = LimitedRequester::send_request( let login_result = chorus_request
request_builder, .deserialize_response::<LoginResult>(&mut shell)
LimitType::AuthRegister, .await?;
self,
&mut cloned_limits,
)
.await?;
let status = response.status();
let response_text = response.text().await.unwrap();
if status.is_client_error() {
let json: ErrorResponse = serde_json::from_str(&response_text).unwrap();
let error_type = json.errors.errors.iter().next().unwrap().0.to_owned();
let mut error = "".to_string();
for (_, value) in json.errors.errors.iter() {
for error_item in value._errors.iter() {
error += &(error_item.message.to_string() + " (" + &error_item.code + ")");
}
}
return Err(ChorusLibError::InvalidFormBodyError { error_type, error });
}
let cloned_limits = self.limits.clone();
let login_result: LoginResult = from_str(&response_text).unwrap();
let object = self.get_user(login_result.token.clone(), None).await?; let object = self.get_user(login_result.token.clone(), None).await?;
if self.limits_information.is_some() {
self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap();
}
let user = UserMeta::new( let user = UserMeta::new(
Rc::new(RefCell::new(self.clone())), Rc::new(RefCell::new(self.clone())),
login_result.token, login_result.token,
cloned_limits, self.clone_limits_if_some(),
login_result.settings, login_result.settings,
object, object,
); );
Ok(user) Ok(user)
} }
} }

View File

@ -1,14 +1,14 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use reqwest::Client; use reqwest::Client;
use serde_json::{from_str, json}; use serde_json::to_string;
use crate::{ use crate::{
api::limits::LimitType, api::policies::instance::LimitType,
errors::{ChorusLibError, ChorusResult}, errors::ChorusResult,
instance::{Instance, Token, UserMeta}, instance::{Instance, Token, UserMeta},
limit::LimitedRequester, ratelimiter::ChorusRequest,
types::{ErrorResponse, RegisterSchema}, types::RegisterSchema,
}; };
impl Instance { impl Instance {
@ -25,43 +25,30 @@ impl Instance {
&mut self, &mut self,
register_schema: &RegisterSchema, register_schema: &RegisterSchema,
) -> ChorusResult<UserMeta> { ) -> ChorusResult<UserMeta> {
let json_schema = json!(register_schema);
let client = Client::new();
let endpoint_url = self.urls.api.clone() + "/auth/register"; let endpoint_url = self.urls.api.clone() + "/auth/register";
let request_builder = client.post(endpoint_url).body(json_schema.to_string()); let chorus_request = ChorusRequest {
request: Client::new()
.post(endpoint_url)
.body(to_string(register_schema).unwrap()),
limit_type: LimitType::AuthRegister,
};
// We do not have a user yet, and the UserRateLimits will not be affected by a login // We do not have a user yet, and the UserRateLimits will not be affected by a login
// request (since register is an instance wide limit), which is why we are just cloning // request (since register is an instance wide limit), which is why we are just cloning
// the instances' limits to pass them on as user_rate_limits later. // the instances' limits to pass them on as user_rate_limits later.
let mut cloned_limits = self.limits.clone(); let mut shell = UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string());
let response = LimitedRequester::send_request( let token = chorus_request
request_builder, .deserialize_response::<Token>(&mut shell)
LimitType::AuthRegister, .await?
self, .token;
&mut cloned_limits, if self.limits_information.is_some() {
) self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap();
.await?;
let status = response.status();
let response_text = response.text().await.unwrap();
let token = from_str::<Token>(&response_text).unwrap();
let token = token.token;
if status.is_client_error() {
let json: ErrorResponse = serde_json::from_str(&token).unwrap();
let error_type = json.errors.errors.iter().next().unwrap().0.to_owned();
let mut error = "".to_string();
for (_, value) in json.errors.errors.iter() {
for error_item in value._errors.iter() {
error += &(error_item.message.to_string() + " (" + &error_item.code + ")");
}
}
return Err(ChorusLibError::InvalidFormBodyError { error_type, error });
} }
let user_object = self.get_user(token.clone(), None).await.unwrap(); let user_object = self.get_user(token.clone(), None).await.unwrap();
let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?; let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?;
let user = UserMeta::new( let user = UserMeta::new(
Rc::new(RefCell::new(self.clone())), Rc::new(RefCell::new(self.clone())),
token.clone(), token.clone(),
cloned_limits, self.clone_limits_if_some(),
settings, settings,
user_object, user_object,
); );

View File

@ -2,32 +2,23 @@ use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::{ use crate::{
api::common, api::LimitType,
errors::{ChorusLibError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{Channel, ChannelModifySchema, GetChannelMessagesSchema, Message, Snowflake}, types::{Channel, ChannelModifySchema, GetChannelMessagesSchema, Message, Snowflake},
}; };
impl Channel { impl Channel {
pub async fn get(user: &mut UserMeta, channel_id: Snowflake) -> ChorusResult<Channel> { pub async fn get(user: &mut UserMeta, channel_id: Snowflake) -> ChorusResult<Channel> {
let url = user.belongs_to.borrow_mut().urls.api.clone(); let url = user.belongs_to.borrow().urls.api.clone();
let request = Client::new() let chorus_request = ChorusRequest {
.get(format!("{}/channels/{}/", url, channel_id)) request: Client::new()
.bearer_auth(user.token()); .get(format!("{}/channels/{}/", url, channel_id))
.bearer_auth(user.token()),
let result = common::deserialize_response::<Channel>( limit_type: LimitType::Channel(channel_id),
request, };
user, chorus_request.deserialize_response::<Channel>(user).await
crate::api::limits::LimitType::Channel,
)
.await;
if result.is_err() {
return Err(ChorusLibError::RequestErrorError {
url: format!("{}/channels/{}/", url, channel_id),
error: result.err().unwrap().to_string(),
});
}
Ok(result.unwrap())
} }
/// Deletes a channel. /// Deletes a channel.
@ -44,15 +35,17 @@ impl Channel {
/// ///
/// A `Result` that contains a `ChorusLibError` if an error occurred during the request, or `()` if the request was successful. /// A `Result` that contains a `ChorusLibError` if an error occurred during the request, or `()` if the request was successful.
pub async fn delete(self, user: &mut UserMeta) -> ChorusResult<()> { pub async fn delete(self, user: &mut UserMeta) -> ChorusResult<()> {
let request = Client::new() let chorus_request = ChorusRequest {
.delete(format!( request: Client::new()
"{}/channels/{}/", .delete(format!(
user.belongs_to.borrow_mut().urls.api, "{}/channels/{}/",
self.id user.belongs_to.borrow().urls.api,
)) self.id
.bearer_auth(user.token()); ))
common::handle_request_as_result(request, user, crate::api::limits::LimitType::Channel) .bearer_auth(user.token()),
.await limit_type: LimitType::Channel(self.id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Modifies a channel. /// Modifies a channel.
@ -75,20 +68,18 @@ impl Channel {
channel_id: Snowflake, channel_id: Snowflake,
user: &mut UserMeta, user: &mut UserMeta,
) -> ChorusResult<()> { ) -> ChorusResult<()> {
let request = Client::new() let chorus_request = ChorusRequest {
.patch(format!( request: Client::new()
"{}/channels/{}/", .patch(format!(
user.belongs_to.borrow().urls.api, "{}/channels/{}/",
channel_id user.belongs_to.borrow().urls.api,
)) channel_id
.bearer_auth(user.token()) ))
.body(to_string(&modify_data).unwrap()); .bearer_auth(user.token())
let new_channel = common::deserialize_response::<Channel>( .body(to_string(&modify_data).unwrap()),
request, limit_type: LimitType::Channel(channel_id),
user, };
crate::api::limits::LimitType::Channel, let new_channel = chorus_request.deserialize_response::<Channel>(user).await?;
)
.await?;
let _ = std::mem::replace(self, new_channel); let _ = std::mem::replace(self, new_channel);
Ok(()) Ok(())
} }
@ -97,16 +88,21 @@ impl Channel {
range: GetChannelMessagesSchema, range: GetChannelMessagesSchema,
channel_id: Snowflake, channel_id: Snowflake,
user: &mut UserMeta, user: &mut UserMeta,
) -> Result<Vec<Message>, ChorusLibError> { ) -> Result<Vec<Message>, ChorusError> {
let request = Client::new() let chorus_request = ChorusRequest {
.get(format!( request: Client::new()
"{}/channels/{}/messages", .get(format!(
user.belongs_to.borrow().urls.api, "{}/channels/{}/messages",
channel_id user.belongs_to.borrow().urls.api,
)) channel_id
.bearer_auth(user.token()) ))
.query(&range); .bearer_auth(user.token())
.query(&range),
limit_type: Default::default(),
};
common::deserialize_response::<Vec<Message>>(request, user, Default::default()).await chorus_request
.deserialize_response::<Vec<Message>>(user)
.await
} }
} }

View File

@ -3,8 +3,9 @@ use http::HeaderMap;
use reqwest::{multipart, Client}; use reqwest::{multipart, Client};
use serde_json::to_string; use serde_json::to_string;
use crate::api::deserialize_response; use crate::api::LimitType;
use crate::instance::UserMeta; use crate::instance::UserMeta;
use crate::ratelimiter::ChorusRequest;
use crate::types::{Message, MessageSendSchema, PartialDiscordFileAttachment, Snowflake}; use crate::types::{Message, MessageSendSchema, PartialDiscordFileAttachment, Snowflake};
impl Message { impl Message {
@ -24,16 +25,18 @@ impl Message {
channel_id: Snowflake, channel_id: Snowflake,
message: &mut MessageSendSchema, message: &mut MessageSendSchema,
files: Option<Vec<PartialDiscordFileAttachment>>, files: Option<Vec<PartialDiscordFileAttachment>>,
) -> Result<Message, crate::errors::ChorusLibError> { ) -> Result<Message, crate::errors::ChorusError> {
let url_api = user.belongs_to.borrow().urls.api.clone(); let url_api = user.belongs_to.borrow().urls.api.clone();
if files.is_none() { if files.is_none() {
let request = Client::new() let chorus_request = ChorusRequest {
.post(format!("{}/channels/{}/messages/", url_api, channel_id)) request: Client::new()
.bearer_auth(user.token()) .post(format!("{}/channels/{}/messages/", url_api, channel_id))
.body(to_string(message).unwrap()); .bearer_auth(user.token())
deserialize_response::<Message>(request, user, crate::api::limits::LimitType::Channel) .body(to_string(message).unwrap()),
.await limit_type: LimitType::Channel(channel_id),
};
chorus_request.deserialize_response::<Message>(user).await
} else { } else {
for (index, attachment) in message.attachments.iter_mut().enumerate() { for (index, attachment) in message.attachments.iter_mut().enumerate() {
attachment.get_mut(index).unwrap().set_id(index as i16); attachment.get_mut(index).unwrap().set_id(index as i16);
@ -62,13 +65,14 @@ impl Message {
form = form.part(part_name, part); form = form.part(part_name, part);
} }
let request = Client::new() let chorus_request = ChorusRequest {
.post(format!("{}/channels/{}/messages/", url_api, channel_id)) request: Client::new()
.bearer_auth(user.token()) .post(format!("{}/channels/{}/messages/", url_api, channel_id))
.multipart(form); .bearer_auth(user.token())
.multipart(form),
deserialize_response::<Message>(request, user, crate::api::limits::LimitType::Channel) limit_type: LimitType::Channel(channel_id),
.await };
chorus_request.deserialize_response::<Message>(user).await
} }
} }
} }
@ -91,7 +95,7 @@ impl UserMeta {
message: &mut MessageSendSchema, message: &mut MessageSendSchema,
channel_id: Snowflake, channel_id: Snowflake,
files: Option<Vec<PartialDiscordFileAttachment>>, files: Option<Vec<PartialDiscordFileAttachment>>,
) -> Result<Message, crate::errors::ChorusLibError> { ) -> Result<Message, crate::errors::ChorusError> {
Message::send(self, channel_id, message, files).await Message::send(self, channel_id, message, files).await
} }
} }

View File

@ -2,9 +2,10 @@ use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::{ use crate::{
api::handle_request_as_result, api::LimitType,
errors::{ChorusLibError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{self, PermissionOverwrite, Snowflake}, types::{self, PermissionOverwrite, Snowflake},
}; };
@ -25,24 +26,25 @@ impl types::Channel {
channel_id: Snowflake, channel_id: Snowflake,
overwrite: PermissionOverwrite, overwrite: PermissionOverwrite,
) -> ChorusResult<()> { ) -> ChorusResult<()> {
let url = { let url = format!(
format!( "{}/channels/{}/permissions/{}",
"{}/channels/{}/permissions/{}", user.belongs_to.borrow_mut().urls.api,
user.belongs_to.borrow_mut().urls.api, channel_id,
channel_id, overwrite.id
overwrite.id );
)
};
let body = match to_string(&overwrite) { let body = match to_string(&overwrite) {
Ok(string) => string, Ok(string) => string,
Err(e) => { Err(e) => {
return Err(ChorusLibError::FormCreationError { return Err(ChorusError::FormCreation {
error: e.to_string(), error: e.to_string(),
}); });
} }
}; };
let request = Client::new().put(url).bearer_auth(user.token()).body(body); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().put(url).bearer_auth(user.token()).body(body),
limit_type: LimitType::Channel(channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Deletes a permission overwrite for a channel. /// Deletes a permission overwrite for a channel.
@ -67,7 +69,10 @@ impl types::Channel {
channel_id, channel_id,
overwrite_id overwrite_id
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
} }

View File

@ -1,9 +1,10 @@
use reqwest::Client; use reqwest::Client;
use crate::{ use crate::{
api::{deserialize_response, handle_request_as_result}, api::LimitType,
errors::ChorusResult, errors::ChorusResult,
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{self, PublicUser, Snowflake}, types::{self, PublicUser, Snowflake},
}; };
@ -32,8 +33,11 @@ impl ReactionMeta {
self.channel_id, self.channel_id,
self.message_id self.message_id
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Gets a list of users that reacted with a specific emoji to a message. /// Gets a list of users that reacted with a specific emoji to a message.
@ -54,13 +58,13 @@ impl ReactionMeta {
self.message_id, self.message_id,
emoji emoji
); );
let request = Client::new().get(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
deserialize_response::<Vec<PublicUser>>( request: Client::new().get(url).bearer_auth(user.token()),
request, limit_type: LimitType::Channel(self.channel_id),
user, };
crate::api::limits::LimitType::Channel, chorus_request
) .deserialize_response::<Vec<PublicUser>>(user)
.await .await
} }
/// Deletes all the reactions for a given `emoji` on a message. This endpoint requires the /// Deletes all the reactions for a given `emoji` on a message. This endpoint requires the
@ -83,8 +87,11 @@ impl ReactionMeta {
self.message_id, self.message_id,
emoji emoji
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Create a reaction for the message. /// Create a reaction for the message.
@ -110,8 +117,11 @@ impl ReactionMeta {
self.message_id, self.message_id,
emoji emoji
); );
let request = Client::new().put(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().put(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Delete a reaction the current user has made for the message. /// Delete a reaction the current user has made for the message.
@ -133,8 +143,11 @@ impl ReactionMeta {
self.message_id, self.message_id,
emoji emoji
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Delete a user's reaction to a message. /// Delete a user's reaction to a message.
@ -164,7 +177,10 @@ impl ReactionMeta {
emoji, emoji,
user_id user_id
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request.handle_request_as_result(user).await
} }
} }

View File

@ -1,72 +0,0 @@
use reqwest::RequestBuilder;
use serde::Deserialize;
use serde_json::from_str;
use crate::{
errors::{ChorusLibError, ChorusResult},
instance::UserMeta,
limit::LimitedRequester,
};
use super::limits::LimitType;
/// Sends a request to wherever it needs to go and performs some basic error handling.
pub async fn handle_request(
request: RequestBuilder,
user: &mut UserMeta,
limit_type: LimitType,
) -> Result<reqwest::Response, crate::errors::ChorusLibError> {
LimitedRequester::send_request(
request,
limit_type,
&mut user.belongs_to.borrow_mut(),
&mut user.limits,
)
.await
}
/// Sends a request to wherever it needs to go. Returns [`Ok(())`] on success and
/// [`Err(ChorusLibError)`] on failure.
pub async fn handle_request_as_result(
request: RequestBuilder,
user: &mut UserMeta,
limit_type: LimitType,
) -> ChorusResult<()> {
match handle_request(request, user, limit_type).await {
Ok(_) => Ok(()),
Err(e) => Err(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
}
pub async fn deserialize_response<T: for<'a> Deserialize<'a>>(
request: RequestBuilder,
user: &mut UserMeta,
limit_type: LimitType,
) -> ChorusResult<T> {
let response = handle_request(request, user, limit_type).await.unwrap();
let response_text = match response.text().await {
Ok(string) => string,
Err(e) => {
return Err(ChorusLibError::InvalidResponseError {
error: format!(
"Error while trying to process the HTTP response into a String: {}",
e
),
});
}
};
let object = match from_str::<T>(&response_text) {
Ok(object) => object,
Err(e) => {
return Err(ChorusLibError::InvalidResponseError {
error: format!(
"Error while trying to deserialize the JSON response into T: {}",
e
),
})
}
};
Ok(object)
}

View File

@ -2,15 +2,11 @@ use reqwest::Client;
use serde_json::from_str; use serde_json::from_str;
use serde_json::to_string; use serde_json::to_string;
use crate::api::deserialize_response; use crate::api::LimitType;
use crate::api::handle_request; use crate::errors::ChorusError;
use crate::api::handle_request_as_result;
use crate::api::limits::Limits;
use crate::errors::ChorusLibError;
use crate::errors::ChorusResult; use crate::errors::ChorusResult;
use crate::instance::Instance;
use crate::instance::UserMeta; use crate::instance::UserMeta;
use crate::limit::LimitedRequester; use crate::ratelimiter::ChorusRequest;
use crate::types::Snowflake; use crate::types::Snowflake;
use crate::types::{Channel, ChannelCreateSchema, Guild, GuildCreateSchema}; use crate::types::{Channel, ChannelCreateSchema, Guild, GuildCreateSchema};
@ -36,11 +32,14 @@ impl Guild {
guild_create_schema: GuildCreateSchema, guild_create_schema: GuildCreateSchema,
) -> ChorusResult<Guild> { ) -> ChorusResult<Guild> {
let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api); let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api);
let request = reqwest::Client::new() let chorus_request = ChorusRequest {
.post(url.clone()) request: Client::new()
.bearer_auth(user.token.clone()) .post(url.clone())
.body(to_string(&guild_create_schema).unwrap()); .bearer_auth(user.token.clone())
deserialize_response::<Guild>(request, user, crate::api::limits::LimitType::Guild).await .body(to_string(&guild_create_schema).unwrap()),
limit_type: LimitType::Global,
};
chorus_request.deserialize_response::<Guild>(user).await
} }
/// Deletes a guild. /// Deletes a guild.
@ -73,10 +72,13 @@ impl Guild {
user.belongs_to.borrow().urls.api, user.belongs_to.borrow().urls.api,
guild_id guild_id
); );
let request = reqwest::Client::new() let chorus_request = ChorusRequest {
.post(url.clone()) request: Client::new()
.bearer_auth(user.token.clone()); .post(url.clone())
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await .bearer_auth(user.token.clone()),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(user).await
} }
/// Sends a request to create a new channel in the guild. /// Sends a request to create a new channel in the guild.
@ -97,14 +99,7 @@ impl Guild {
user: &mut UserMeta, user: &mut UserMeta,
schema: ChannelCreateSchema, schema: ChannelCreateSchema,
) -> ChorusResult<Channel> { ) -> ChorusResult<Channel> {
Channel::_create( Channel::create(user, self.id, schema).await
&user.token,
self.id,
schema,
&mut user.limits,
&mut user.belongs_to.borrow_mut(),
)
.await
} }
/// Returns a `Result` containing a vector of `Channel` structs if the request was successful, or an `ChorusLibError` if there was an error. /// Returns a `Result` containing a vector of `Channel` structs if the request was successful, or an `ChorusLibError` if there was an error.
@ -117,20 +112,21 @@ impl Guild {
/// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits.
/// ///
pub async fn channels(&self, user: &mut UserMeta) -> ChorusResult<Vec<Channel>> { pub async fn channels(&self, user: &mut UserMeta) -> ChorusResult<Vec<Channel>> {
let request = Client::new() let chorus_request = ChorusRequest {
.get(format!( request: Client::new()
"{}/guilds/{}/channels/", .get(format!(
user.belongs_to.borrow().urls.api, "{}/guilds/{}/channels/",
self.id user.belongs_to.borrow().urls.api,
)) self.id
.bearer_auth(user.token()); ))
let result = handle_request(request, user, crate::api::limits::LimitType::Channel) .bearer_auth(user.token()),
.await limit_type: LimitType::Channel(self.id),
.unwrap(); };
let result = chorus_request.send_request(user).await?;
let stringed_response = match result.text().await { let stringed_response = match result.text().await {
Ok(value) => value, Ok(value) => value,
Err(e) => { Err(e) => {
return Err(ChorusLibError::InvalidResponseError { return Err(ChorusError::InvalidResponse {
error: e.to_string(), error: e.to_string(),
}); });
} }
@ -138,7 +134,7 @@ impl Guild {
let _: Vec<Channel> = match from_str(&stringed_response) { let _: Vec<Channel> = match from_str(&stringed_response) {
Ok(result) => return Ok(result), Ok(result) => return Ok(result),
Err(e) => { Err(e) => {
return Err(ChorusLibError::InvalidResponseError { return Err(ChorusError::InvalidResponse {
error: e.to_string(), error: e.to_string(),
}); });
} }
@ -155,35 +151,19 @@ impl Guild {
/// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits.
/// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits.
/// ///
pub async fn get(user: &mut UserMeta, guild_id: Snowflake) -> ChorusResult<Guild> { pub async fn get(guild_id: Snowflake, user: &mut UserMeta) -> ChorusResult<Guild> {
let mut belongs_to = user.belongs_to.borrow_mut(); let chorus_request = ChorusRequest {
Guild::_get(guild_id, &user.token, &mut user.limits, &mut belongs_to).await request: Client::new()
} .get(format!(
"{}/guilds/{}/",
/// For internal use. Does the same as the public get method, but does not require a second, mutable user.belongs_to.borrow().urls.api,
/// borrow of `UserMeta::belongs_to`, when used in conjunction with other methods, which borrow `UserMeta::belongs_to`. guild_id
async fn _get( ))
guild_id: Snowflake, .bearer_auth(user.token()),
token: &str, limit_type: LimitType::Guild(guild_id),
limits_user: &mut Limits,
instance: &mut Instance,
) -> ChorusResult<Guild> {
let request = Client::new()
.get(format!("{}/guilds/{}/", instance.urls.api, guild_id))
.bearer_auth(token);
let response = match LimitedRequester::send_request(
request,
crate::api::limits::LimitType::Guild,
instance,
limits_user,
)
.await
{
Ok(response) => response,
Err(e) => return Err(e),
}; };
let guild: Guild = from_str(&response.text().await.unwrap()).unwrap(); let response = chorus_request.deserialize_response::<Guild>(user).await?;
Ok(guild) Ok(response)
} }
} }
@ -207,48 +187,17 @@ impl Channel {
guild_id: Snowflake, guild_id: Snowflake,
schema: ChannelCreateSchema, schema: ChannelCreateSchema,
) -> ChorusResult<Channel> { ) -> ChorusResult<Channel> {
let mut belongs_to = user.belongs_to.borrow_mut(); let chorus_request = ChorusRequest {
Channel::_create( request: Client::new()
&user.token, .post(format!(
guild_id, "{}/guilds/{}/channels/",
schema, user.belongs_to.borrow().urls.api,
&mut user.limits, guild_id
&mut belongs_to, ))
) .bearer_auth(user.token())
.await .body(to_string(&schema).unwrap()),
} limit_type: LimitType::Guild(guild_id),
async fn _create(
token: &str,
guild_id: Snowflake,
schema: ChannelCreateSchema,
limits_user: &mut Limits,
instance: &mut Instance,
) -> ChorusResult<Channel> {
let request = Client::new()
.post(format!(
"{}/guilds/{}/channels/",
instance.urls.api, guild_id
))
.bearer_auth(token)
.body(to_string(&schema).unwrap());
let result = match LimitedRequester::send_request(
request,
crate::api::limits::LimitType::Guild,
instance,
limits_user,
)
.await
{
Ok(result) => result,
Err(e) => return Err(e),
}; };
match from_str::<Channel>(&result.text().await.unwrap()) { chorus_request.deserialize_response::<Channel>(user).await
Ok(object) => Ok(object),
Err(e) => Err(ChorusLibError::RequestErrorError {
url: format!("{}/guilds/{}/channels/", instance.urls.api, guild_id),
error: e.to_string(),
}),
}
} }
} }

View File

@ -1,9 +1,10 @@
use reqwest::Client; use reqwest::Client;
use crate::{ use crate::{
api::{deserialize_response, handle_request_as_result}, api::LimitType,
errors::ChorusResult, errors::ChorusResult,
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{self, Snowflake}, types::{self, Snowflake},
}; };
@ -30,13 +31,13 @@ impl types::GuildMember {
guild_id, guild_id,
member_id member_id
); );
let request = Client::new().get(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
deserialize_response::<types::GuildMember>( request: Client::new().get(url).bearer_auth(user.token()),
request, limit_type: LimitType::Guild(guild_id),
user, };
crate::api::limits::LimitType::Guild, chorus_request
) .deserialize_response::<types::GuildMember>(user)
.await .await
} }
/// Adds a role to a guild member. /// Adds a role to a guild member.
@ -64,8 +65,11 @@ impl types::GuildMember {
member_id, member_id,
role_id role_id
); );
let request = Client::new().put(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await request: Client::new().put(url).bearer_auth(user.token()),
limit_type: LimitType::Guild(guild_id),
};
chorus_request.handle_request_as_result(user).await
} }
/// Removes a role from a guild member. /// Removes a role from a guild member.
@ -85,7 +89,7 @@ impl types::GuildMember {
guild_id: Snowflake, guild_id: Snowflake,
member_id: Snowflake, member_id: Snowflake,
role_id: Snowflake, role_id: Snowflake,
) -> Result<(), crate::errors::ChorusLibError> { ) -> Result<(), crate::errors::ChorusError> {
let url = format!( let url = format!(
"{}/guilds/{}/members/{}/roles/{}/", "{}/guilds/{}/members/{}/roles/{}/",
user.belongs_to.borrow().urls.api, user.belongs_to.borrow().urls.api,
@ -93,7 +97,10 @@ impl types::GuildMember {
member_id, member_id,
role_id role_id
); );
let request = Client::new().delete(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await request: Client::new().delete(url).bearer_auth(user.token()),
limit_type: LimitType::Guild(guild_id),
};
chorus_request.handle_request_as_result(user).await
} }
} }

View File

@ -2,9 +2,10 @@ use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::{ use crate::{
api::deserialize_response, api::LimitType,
errors::{ChorusLibError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{self, RoleCreateModifySchema, RoleObject, Snowflake}, types::{self, RoleCreateModifySchema, RoleObject, Snowflake},
}; };
@ -32,14 +33,14 @@ impl types::RoleObject {
user.belongs_to.borrow().urls.api, user.belongs_to.borrow().urls.api,
guild_id guild_id
); );
let request = Client::new().get(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
let roles = deserialize_response::<Vec<RoleObject>>( request: Client::new().get(url).bearer_auth(user.token()),
request, limit_type: LimitType::Guild(guild_id),
user, };
crate::api::limits::LimitType::Guild, let roles = chorus_request
) .deserialize_response::<Vec<RoleObject>>(user)
.await .await
.unwrap(); .unwrap();
if roles.is_empty() { if roles.is_empty() {
return Ok(None); return Ok(None);
} }
@ -72,8 +73,13 @@ impl types::RoleObject {
guild_id, guild_id,
role_id role_id
); );
let request = Client::new().get(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
deserialize_response(request, user, crate::api::limits::LimitType::Guild).await request: Client::new().get(url).bearer_auth(user.token()),
limit_type: LimitType::Guild(guild_id),
};
chorus_request
.deserialize_response::<RoleObject>(user)
.await
} }
/// Creates a new role for a given guild. /// Creates a new role for a given guild.
@ -102,12 +108,17 @@ impl types::RoleObject {
guild_id guild_id
); );
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| { let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
ChorusLibError::FormCreationError { ChorusError::FormCreation {
error: e.to_string(), error: e.to_string(),
} }
})?; })?;
let request = Client::new().post(url).bearer_auth(user.token()).body(body); let chorus_request = ChorusRequest {
deserialize_response(request, user, crate::api::limits::LimitType::Guild).await request: Client::new().post(url).bearer_auth(user.token()).body(body),
limit_type: LimitType::Guild(guild_id),
};
chorus_request
.deserialize_response::<RoleObject>(user)
.await
} }
/// Updates the position of a role in the guild's hierarchy. /// Updates the position of a role in the guild's hierarchy.
@ -135,16 +146,19 @@ impl types::RoleObject {
user.belongs_to.borrow().urls.api, user.belongs_to.borrow().urls.api,
guild_id guild_id
); );
let body = to_string(&role_position_update_schema).map_err(|e| { let body =
ChorusLibError::FormCreationError { to_string(&role_position_update_schema).map_err(|e| ChorusError::FormCreation {
error: e.to_string(), error: e.to_string(),
} })?;
})?; let chorus_request = ChorusRequest {
let request = Client::new() request: Client::new()
.patch(url) .patch(url)
.bearer_auth(user.token()) .bearer_auth(user.token())
.body(body); .body(body),
deserialize_response::<RoleObject>(request, user, crate::api::limits::LimitType::Guild) limit_type: LimitType::Guild(guild_id),
};
chorus_request
.deserialize_response::<RoleObject>(user)
.await .await
} }
@ -177,15 +191,19 @@ impl types::RoleObject {
role_id role_id
); );
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| { let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
ChorusLibError::FormCreationError { ChorusError::FormCreation {
error: e.to_string(), error: e.to_string(),
} }
})?; })?;
let request = Client::new() let chorus_request = ChorusRequest {
.patch(url) request: Client::new()
.bearer_auth(user.token()) .patch(url)
.body(body); .bearer_auth(user.token())
deserialize_response::<RoleObject>(request, user, crate::api::limits::LimitType::Guild) .body(body),
limit_type: LimitType::Guild(guild_id),
};
chorus_request
.deserialize_response::<RoleObject>(user)
.await .await
} }
} }

View File

@ -1,12 +1,10 @@
pub use channels::messages::*; pub use channels::messages::*;
pub use common::*;
pub use guilds::*; pub use guilds::*;
pub use policies::instance::instance::*; pub use policies::instance::instance::*;
pub use policies::instance::limits::*; pub use policies::instance::ratelimits::*;
pub mod auth; pub mod auth;
pub mod channels; pub mod channels;
pub mod common;
pub mod guilds; pub mod guilds;
pub mod policies; pub mod policies;
pub mod users; pub mod users;

View File

@ -1,7 +1,6 @@
use reqwest::Client;
use serde_json::from_str; use serde_json::from_str;
use crate::errors::{ChorusLibError, ChorusResult}; use crate::errors::{ChorusError, ChorusResult};
use crate::instance::Instance; use crate::instance::Instance;
use crate::types::GeneralConfiguration; use crate::types::GeneralConfiguration;
@ -10,21 +9,21 @@ impl Instance {
/// # Errors /// # Errors
/// [`ChorusLibError`] - If the request fails. /// [`ChorusLibError`] - If the request fails.
pub async fn general_configuration_schema(&self) -> ChorusResult<GeneralConfiguration> { pub async fn general_configuration_schema(&self) -> ChorusResult<GeneralConfiguration> {
let client = Client::new();
let endpoint_url = self.urls.api.clone() + "/policies/instance/"; let endpoint_url = self.urls.api.clone() + "/policies/instance/";
let request = match client.get(&endpoint_url).send().await { let request = match self.client.get(&endpoint_url).send().await {
Ok(result) => result, Ok(result) => result,
Err(e) => { Err(e) => {
return Err(ChorusLibError::RequestErrorError { return Err(ChorusError::RequestFailed {
url: endpoint_url, url: endpoint_url,
error: e.to_string(), error: e,
}); });
} }
}; };
if !request.status().as_str().starts_with('2') { if !request.status().as_str().starts_with('2') {
return Err(ChorusLibError::ReceivedErrorCodeError { return Err(ChorusError::ReceivedErrorCode {
error_code: request.status().to_string(), error_code: request.status().as_u16(),
error: request.text().await.unwrap(),
}); });
} }

View File

@ -1,499 +0,0 @@
pub mod limits {
use std::collections::HashMap;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::from_str;
#[derive(Clone, Copy, Eq, Hash, PartialEq, Debug, Default)]
pub enum LimitType {
AuthRegister,
AuthLogin,
AbsoluteMessage,
AbsoluteRegister,
#[default]
Global,
Ip,
Channel,
Error,
Guild,
Webhook,
}
impl ToString for LimitType {
fn to_string(&self) -> String {
match self {
LimitType::AuthRegister => "AuthRegister".to_string(),
LimitType::AuthLogin => "AuthLogin".to_string(),
LimitType::AbsoluteMessage => "AbsoluteMessage".to_string(),
LimitType::AbsoluteRegister => "AbsoluteRegister".to_string(),
LimitType::Global => "Global".to_string(),
LimitType::Ip => "Ip".to_string(),
LimitType::Channel => "Channel".to_string(),
LimitType::Error => "Error".to_string(),
LimitType::Guild => "Guild".to_string(),
LimitType::Webhook => "Webhook".to_string(),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct User {
pub maxGuilds: u64,
pub maxUsername: u64,
pub maxFriends: u64,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct Guild {
pub maxRoles: u64,
pub maxEmojis: u64,
pub maxMembers: u64,
pub maxChannels: u64,
pub maxChannelsInCategory: u64,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct Message {
pub maxCharacters: u64,
pub maxTTSCharacters: u64,
pub maxReactions: u64,
pub maxAttachmentSize: u64,
pub maxBulkDelete: u64,
pub maxEmbedDownloadSize: u64,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct Channel {
pub maxPins: u64,
pub maxTopic: u64,
pub maxWebhooks: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Rate {
pub enabled: bool,
pub ip: Window,
pub global: Window,
pub error: Window,
pub routes: Routes,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Window {
pub count: u64,
pub window: u64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Routes {
pub guild: Window,
pub webhook: Window,
pub channel: Window,
pub auth: AuthRoutes,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct AuthRoutes {
pub login: Window,
pub register: Window,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct AbsoluteRate {
pub register: AbsoluteWindow,
pub sendMessage: AbsoluteWindow,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AbsoluteWindow {
pub limit: u64,
pub window: u64,
pub enabled: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[allow(non_snake_case)]
pub struct Config {
pub user: User,
pub guild: Guild,
pub message: Message,
pub channel: Channel,
pub rate: Rate,
pub absoluteRate: AbsoluteRate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Limit {
pub bucket: LimitType,
pub limit: u64,
pub remaining: u64,
pub reset: u64,
}
impl std::fmt::Display for Limit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Bucket: {:?}, Limit: {}, Remaining: {}, Reset: {}",
self.bucket, self.limit, self.remaining, self.reset
)
}
}
impl Limit {
pub fn add_remaining(&mut self, remaining: i64) {
if remaining < 0 {
if (self.remaining as i64 + remaining) <= 0 {
self.remaining = 0;
return;
}
self.remaining -= remaining.unsigned_abs();
return;
}
self.remaining += remaining.unsigned_abs();
}
}
pub struct LimitsMutRef<'a> {
pub limit_absolute_messages: &'a mut Limit,
pub limit_absolute_register: &'a mut Limit,
pub limit_auth_login: &'a mut Limit,
pub limit_auth_register: &'a mut Limit,
pub limit_ip: &'a mut Limit,
pub limit_global: &'a mut Limit,
pub limit_error: &'a mut Limit,
pub limit_guild: &'a mut Limit,
pub limit_webhook: &'a mut Limit,
pub limit_channel: &'a mut Limit,
}
impl LimitsMutRef<'_> {
pub fn combine_mut_ref<'a>(
instance_rate_limits: &'a mut Limits,
user_rate_limits: &'a mut Limits,
) -> LimitsMutRef<'a> {
LimitsMutRef {
limit_absolute_messages: &mut instance_rate_limits.limit_absolute_messages,
limit_absolute_register: &mut instance_rate_limits.limit_absolute_register,
limit_auth_login: &mut instance_rate_limits.limit_auth_login,
limit_auth_register: &mut instance_rate_limits.limit_auth_register,
limit_channel: &mut user_rate_limits.limit_channel,
limit_error: &mut user_rate_limits.limit_error,
limit_global: &mut instance_rate_limits.limit_global,
limit_guild: &mut user_rate_limits.limit_guild,
limit_ip: &mut instance_rate_limits.limit_ip,
limit_webhook: &mut user_rate_limits.limit_webhook,
}
}
pub fn get_limit_ref(&self, limit_type: &LimitType) -> &Limit {
match limit_type {
LimitType::AbsoluteMessage => self.limit_absolute_messages,
LimitType::AbsoluteRegister => self.limit_absolute_register,
LimitType::AuthLogin => self.limit_auth_login,
LimitType::AuthRegister => self.limit_auth_register,
LimitType::Channel => self.limit_channel,
LimitType::Error => self.limit_error,
LimitType::Global => self.limit_global,
LimitType::Guild => self.limit_guild,
LimitType::Ip => self.limit_ip,
LimitType::Webhook => self.limit_webhook,
}
}
pub fn get_limit_mut_ref(&mut self, limit_type: &LimitType) -> &mut Limit {
match limit_type {
LimitType::AbsoluteMessage => self.limit_absolute_messages,
LimitType::AbsoluteRegister => self.limit_absolute_register,
LimitType::AuthLogin => self.limit_auth_login,
LimitType::AuthRegister => self.limit_auth_register,
LimitType::Channel => self.limit_channel,
LimitType::Error => self.limit_error,
LimitType::Global => self.limit_global,
LimitType::Guild => self.limit_guild,
LimitType::Ip => self.limit_ip,
LimitType::Webhook => self.limit_webhook,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Limits {
pub limit_absolute_messages: Limit,
pub limit_absolute_register: Limit,
pub limit_auth_login: Limit,
pub limit_auth_register: Limit,
pub limit_ip: Limit,
pub limit_global: Limit,
pub limit_error: Limit,
pub limit_guild: Limit,
pub limit_webhook: Limit,
pub limit_channel: Limit,
}
impl Limits {
pub fn combine(instance_rate_limits: &Limits, user_rate_limits: &Limits) -> Limits {
Limits {
limit_absolute_messages: instance_rate_limits.limit_absolute_messages,
limit_absolute_register: instance_rate_limits.limit_absolute_register,
limit_auth_login: instance_rate_limits.limit_auth_login,
limit_auth_register: instance_rate_limits.limit_auth_register,
limit_channel: user_rate_limits.limit_channel,
limit_error: user_rate_limits.limit_error,
limit_global: instance_rate_limits.limit_global,
limit_guild: user_rate_limits.limit_guild,
limit_ip: instance_rate_limits.limit_ip,
limit_webhook: user_rate_limits.limit_webhook,
}
}
pub fn get_limit_ref(&self, limit_type: &LimitType) -> &Limit {
match limit_type {
LimitType::AbsoluteMessage => &self.limit_absolute_messages,
LimitType::AbsoluteRegister => &self.limit_absolute_register,
LimitType::AuthLogin => &self.limit_auth_login,
LimitType::AuthRegister => &self.limit_auth_register,
LimitType::Channel => &self.limit_channel,
LimitType::Error => &self.limit_error,
LimitType::Global => &self.limit_global,
LimitType::Guild => &self.limit_guild,
LimitType::Ip => &self.limit_ip,
LimitType::Webhook => &self.limit_webhook,
}
}
pub fn get_limit_mut_ref(&mut self, limit_type: &LimitType) -> &mut Limit {
match limit_type {
LimitType::AbsoluteMessage => &mut self.limit_absolute_messages,
LimitType::AbsoluteRegister => &mut self.limit_absolute_register,
LimitType::AuthLogin => &mut self.limit_auth_login,
LimitType::AuthRegister => &mut self.limit_auth_register,
LimitType::Channel => &mut self.limit_channel,
LimitType::Error => &mut self.limit_error,
LimitType::Global => &mut self.limit_global,
LimitType::Guild => &mut self.limit_guild,
LimitType::Ip => &mut self.limit_ip,
LimitType::Webhook => &mut self.limit_webhook,
}
}
pub fn to_hash_map(&self) -> HashMap<LimitType, Limit> {
let mut map: HashMap<LimitType, Limit> = HashMap::new();
map.insert(LimitType::AbsoluteMessage, self.limit_absolute_messages);
map.insert(LimitType::AbsoluteRegister, self.limit_absolute_register);
map.insert(LimitType::AuthLogin, self.limit_auth_login);
map.insert(LimitType::AuthRegister, self.limit_auth_register);
map.insert(LimitType::Ip, self.limit_ip);
map.insert(LimitType::Global, self.limit_global);
map.insert(LimitType::Error, self.limit_error);
map.insert(LimitType::Guild, self.limit_guild);
map.insert(LimitType::Webhook, self.limit_webhook);
map.insert(LimitType::Channel, self.limit_channel);
map
}
pub fn get_as_mut(&mut self) -> &mut Limits {
self
}
/// 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) -> Limits {
let client = Client::new();
let url_parsed = crate::UrlBundle::parse_url(api_url) + "/policies/instance/limits";
let result = client
.get(url_parsed)
.send()
.await
.unwrap_or_else(|e| panic!("An error occured while performing the request: {}", e))
.text()
.await
.unwrap_or_else(|e| {
panic!(
"An error occured while parsing the request body string: {}",
e
)
});
let config: Config = from_str(&result).unwrap();
// If config.rate.enabled is false, then add return a Limits struct with all limits set to u64::MAX
let mut limits: Limits;
if !config.rate.enabled {
limits = Limits {
limit_absolute_messages: Limit {
bucket: LimitType::AbsoluteMessage,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_absolute_register: Limit {
bucket: LimitType::AbsoluteRegister,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_auth_login: Limit {
bucket: LimitType::AuthLogin,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_auth_register: Limit {
bucket: LimitType::AuthRegister,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_ip: Limit {
bucket: LimitType::Ip,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_global: Limit {
bucket: LimitType::Global,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_error: Limit {
bucket: LimitType::Error,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_guild: Limit {
bucket: LimitType::Guild,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_webhook: Limit {
bucket: LimitType::Webhook,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
limit_channel: Limit {
bucket: LimitType::Channel,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
},
};
} else {
limits = Limits {
limit_absolute_messages: Limit {
bucket: LimitType::AbsoluteMessage,
limit: config.absoluteRate.sendMessage.limit,
remaining: config.absoluteRate.sendMessage.limit,
reset: config.absoluteRate.sendMessage.window,
},
limit_absolute_register: Limit {
bucket: LimitType::AbsoluteRegister,
limit: config.absoluteRate.register.limit,
remaining: config.absoluteRate.register.limit,
reset: config.absoluteRate.register.window,
},
limit_auth_login: Limit {
bucket: LimitType::AuthLogin,
limit: config.rate.routes.auth.login.count,
remaining: config.rate.routes.auth.login.count,
reset: config.rate.routes.auth.login.window,
},
limit_auth_register: Limit {
bucket: LimitType::AuthRegister,
limit: config.rate.routes.auth.register.count,
remaining: config.rate.routes.auth.register.count,
reset: config.rate.routes.auth.register.window,
},
limit_ip: Limit {
bucket: LimitType::Ip,
limit: config.rate.ip.count,
remaining: config.rate.ip.count,
reset: config.rate.ip.window,
},
limit_global: Limit {
bucket: LimitType::Global,
limit: config.rate.global.count,
remaining: config.rate.global.count,
reset: config.rate.global.window,
},
limit_error: Limit {
bucket: LimitType::Error,
limit: config.rate.error.count,
remaining: config.rate.error.count,
reset: config.rate.error.window,
},
limit_guild: Limit {
bucket: LimitType::Guild,
limit: config.rate.routes.guild.count,
remaining: config.rate.routes.guild.count,
reset: config.rate.routes.guild.window,
},
limit_webhook: Limit {
bucket: LimitType::Webhook,
limit: config.rate.routes.webhook.count,
remaining: config.rate.routes.webhook.count,
reset: config.rate.routes.webhook.window,
},
limit_channel: Limit {
bucket: LimitType::Channel,
limit: config.rate.routes.channel.count,
remaining: config.rate.routes.channel.count,
reset: config.rate.routes.channel.window,
},
};
}
if !config.absoluteRate.register.enabled {
limits.limit_absolute_register = Limit {
bucket: LimitType::AbsoluteRegister,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
};
}
if !config.absoluteRate.sendMessage.enabled {
limits.limit_absolute_messages = Limit {
bucket: LimitType::AbsoluteMessage,
limit: u64::MAX,
remaining: u64::MAX,
reset: u64::MAX,
};
}
limits
}
}
}
#[cfg(test)]
mod instance_limits {
use crate::api::limits::{Limit, LimitType};
#[test]
fn limit_below_zero() {
let mut limit = Limit {
bucket: LimitType::AbsoluteMessage,
limit: 0,
remaining: 1,
reset: 0,
};
limit.add_remaining(-2);
assert_eq!(0_u64, limit.remaining);
limit.add_remaining(-2123123);
assert_eq!(0_u64, limit.remaining);
}
}

View File

@ -1,5 +1,5 @@
pub use instance::*; pub use instance::*;
pub use limits::*; pub use ratelimits::*;
pub mod instance; pub mod instance;
pub mod limits; pub mod ratelimits;

View File

@ -0,0 +1,35 @@
use std::hash::Hash;
use crate::types::Snowflake;
/// The different types of ratelimits that can be applied to a request. Includes "Baseline"-variants
/// for when the Snowflake is not yet known.
/// See <https://discord.com/developers/docs/topics/rate-limits#rate-limits> for more information.
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default, Hash)]
pub enum LimitType {
AuthRegister,
AuthLogin,
#[default]
Global,
Ip,
Channel(Snowflake),
ChannelBaseline,
Error,
Guild(Snowflake),
GuildBaseline,
Webhook(Snowflake),
WebhookBaseline,
}
/// A struct that represents the current ratelimits, either instance-wide or user-wide.
/// Unlike [`RateLimits`], this struct shows the current ratelimits, not the rate limit
/// configuration for the instance.
/// See <https://discord.com/developers/docs/topics/rate-limits#rate-limits> for more information.
#[derive(Debug, Clone)]
pub struct Limit {
pub bucket: LimitType,
pub limit: u64,
pub remaining: u64,
pub reset: u64,
pub window: u64,
}

View File

@ -1,3 +1,3 @@
pub use instance::limits::*; pub use instance::ratelimits::*;
pub mod instance; pub mod instance;

View File

@ -2,9 +2,10 @@ use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::{ use crate::{
api::{deserialize_response, handle_request_as_result}, api::LimitType,
errors::ChorusResult, errors::ChorusResult,
instance::UserMeta, instance::UserMeta,
ratelimiter::ChorusRequest,
types::{self, CreateUserRelationshipSchema, RelationshipType, Snowflake}, types::{self, CreateUserRelationshipSchema, RelationshipType, Snowflake},
}; };
@ -26,13 +27,13 @@ impl UserMeta {
self.belongs_to.borrow().urls.api, self.belongs_to.borrow().urls.api,
user_id user_id
); );
let request = Client::new().get(url).bearer_auth(self.token()); let chorus_request = ChorusRequest {
deserialize_response::<Vec<types::PublicUser>>( request: Client::new().get(url).bearer_auth(self.token()),
request, limit_type: LimitType::Global,
self, };
crate::api::limits::LimitType::Global, chorus_request
) .deserialize_response::<Vec<types::PublicUser>>(self)
.await .await
} }
/// Retrieves the authenticated user's relationships. /// Retrieves the authenticated user's relationships.
@ -44,13 +45,13 @@ impl UserMeta {
"{}/users/@me/relationships/", "{}/users/@me/relationships/",
self.belongs_to.borrow().urls.api self.belongs_to.borrow().urls.api
); );
let request = Client::new().get(url).bearer_auth(self.token()); let chorus_request = ChorusRequest {
deserialize_response::<Vec<types::Relationship>>( request: Client::new().get(url).bearer_auth(self.token()),
request, limit_type: LimitType::Global,
self, };
crate::api::limits::LimitType::Global, chorus_request
) .deserialize_response::<Vec<types::Relationship>>(self)
.await .await
} }
/// Sends a friend request to a user. /// Sends a friend request to a user.
@ -70,8 +71,11 @@ impl UserMeta {
self.belongs_to.borrow().urls.api self.belongs_to.borrow().urls.api
); );
let body = to_string(&schema).unwrap(); let body = to_string(&schema).unwrap();
let request = Client::new().post(url).bearer_auth(self.token()).body(body); let chorus_request = ChorusRequest {
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await request: Client::new().post(url).bearer_auth(self.token()).body(body),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(self).await
} }
/// Modifies the relationship between the authenticated user and the specified user. /// Modifies the relationship between the authenticated user and the specified user.
@ -96,10 +100,13 @@ impl UserMeta {
let api_url = self.belongs_to.borrow().urls.api.clone(); let api_url = self.belongs_to.borrow().urls.api.clone();
match relationship_type { match relationship_type {
RelationshipType::None => { RelationshipType::None => {
let request = Client::new() let chorus_request = ChorusRequest {
.delete(format!("{}/users/@me/relationships/{}/", api_url, user_id)) request: Client::new()
.bearer_auth(self.token()); .delete(format!("{}/users/@me/relationships/{}/", api_url, user_id))
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await .bearer_auth(self.token()),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(self).await
} }
RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => { RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => {
let body = CreateUserRelationshipSchema { let body = CreateUserRelationshipSchema {
@ -107,11 +114,14 @@ impl UserMeta {
from_friend_suggestion: None, from_friend_suggestion: None,
friend_token: None, friend_token: None,
}; };
let request = Client::new() let chorus_request = ChorusRequest {
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) request: Client::new()
.bearer_auth(self.token()) .put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
.body(to_string(&body).unwrap()); .bearer_auth(self.token())
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await .body(to_string(&body).unwrap()),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(self).await
} }
RelationshipType::Blocked => { RelationshipType::Blocked => {
let body = CreateUserRelationshipSchema { let body = CreateUserRelationshipSchema {
@ -119,11 +129,14 @@ impl UserMeta {
from_friend_suggestion: None, from_friend_suggestion: None,
friend_token: None, friend_token: None,
}; };
let request = Client::new() let chorus_request = ChorusRequest {
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) request: Client::new()
.bearer_auth(self.token()) .put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
.body(to_string(&body).unwrap()); .bearer_auth(self.token())
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await .body(to_string(&body).unwrap()),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(self).await
} }
RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()), RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()),
} }
@ -143,7 +156,10 @@ impl UserMeta {
self.belongs_to.borrow().urls.api, self.belongs_to.borrow().urls.api,
user_id user_id
); );
let request = Client::new().delete(url).bearer_auth(self.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await request: Client::new().delete(url).bearer_auth(self.token()),
limit_type: LimitType::Global,
};
chorus_request.handle_request_as_result(self).await
} }
} }

View File

@ -1,11 +1,13 @@
use std::{cell::RefCell, rc::Rc};
use reqwest::Client; use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::{ use crate::{
api::{deserialize_response, handle_request_as_result}, api::LimitType,
errors::{ChorusLibError, ChorusResult}, errors::{ChorusError, ChorusResult},
instance::{Instance, UserMeta}, instance::{Instance, UserMeta},
limit::LimitedRequester, ratelimiter::ChorusRequest,
types::{User, UserModifySchema, UserSettings}, types::{User, UserModifySchema, UserSettings},
}; };
@ -48,16 +50,20 @@ impl UserMeta {
|| modify_schema.email.is_some() || modify_schema.email.is_some()
|| modify_schema.code.is_some() || modify_schema.code.is_some()
{ {
return Err(ChorusLibError::PasswordRequiredError); return Err(ChorusError::PasswordRequired);
} }
let request = Client::new() let request = Client::new()
.patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api)) .patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api))
.body(to_string(&modify_schema).unwrap()) .body(to_string(&modify_schema).unwrap())
.bearer_auth(self.token()); .bearer_auth(self.token());
let user_updated = let chorus_request = ChorusRequest {
deserialize_response::<User>(request, self, crate::api::limits::LimitType::Ip) request,
.await limit_type: LimitType::default(),
.unwrap(); };
let user_updated = chorus_request
.deserialize_response::<User>(self)
.await
.unwrap();
let _ = std::mem::replace(&mut self.object, user_updated.clone()); let _ = std::mem::replace(&mut self.object, user_updated.clone());
Ok(user_updated) Ok(user_updated)
} }
@ -78,43 +84,28 @@ impl UserMeta {
self.belongs_to.borrow().urls.api self.belongs_to.borrow().urls.api
)) ))
.bearer_auth(self.token()); .bearer_auth(self.token());
handle_request_as_result(request, &mut self, crate::api::limits::LimitType::Ip).await let chorus_request = ChorusRequest {
request,
limit_type: LimitType::default(),
};
chorus_request.handle_request_as_result(&mut self).await
} }
} }
impl User { impl User {
pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult<User> { pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult<User> {
let mut belongs_to = user.belongs_to.borrow_mut(); let url_api = user.belongs_to.borrow().urls.api.clone();
User::_get(
&user.token(),
&format!("{}", belongs_to.urls.api),
&mut belongs_to,
id,
)
.await
}
async fn _get(
token: &str,
url_api: &str,
instance: &mut Instance,
id: Option<&String>,
) -> ChorusResult<User> {
let url = if id.is_none() { let url = if id.is_none() {
format!("{}/users/@me/", url_api) format!("{}/users/@me/", url_api)
} else { } else {
format!("{}/users/{}", url_api, id.unwrap()) format!("{}/users/{}", url_api, id.unwrap())
}; };
let request = reqwest::Client::new().get(url).bearer_auth(token); let request = reqwest::Client::new().get(url).bearer_auth(user.token());
let mut cloned_limits = instance.limits.clone(); let chorus_request = ChorusRequest {
match LimitedRequester::send_request(
request, request,
crate::api::limits::LimitType::Ip, limit_type: LimitType::Global,
instance, };
&mut cloned_limits, match chorus_request.send_request(user).await {
)
.await
{
Ok(result) => { Ok(result) => {
let result_text = result.text().await.unwrap(); let result_text = result.text().await.unwrap();
Ok(serde_json::from_str::<User>(&result_text).unwrap()) Ok(serde_json::from_str::<User>(&result_text).unwrap())
@ -131,18 +122,20 @@ impl User {
let request: reqwest::RequestBuilder = Client::new() let request: reqwest::RequestBuilder = Client::new()
.get(format!("{}/users/@me/settings/", url_api)) .get(format!("{}/users/@me/settings/", url_api))
.bearer_auth(token); .bearer_auth(token);
let mut cloned_limits = instance.limits.clone(); let mut user = UserMeta::shell(Rc::new(RefCell::new(instance.clone())), token.clone());
match LimitedRequester::send_request( let chorus_request = ChorusRequest {
request, request,
crate::api::limits::LimitType::Ip, limit_type: LimitType::Global,
instance, };
&mut cloned_limits, let result = match chorus_request.send_request(&mut user).await {
)
.await
{
Ok(result) => Ok(serde_json::from_str(&result.text().await.unwrap()).unwrap()), Ok(result) => Ok(serde_json::from_str(&result.text().await.unwrap()).unwrap()),
Err(e) => Err(e), Err(e) => Err(e),
};
if instance.limits_information.is_some() {
instance.limits_information.as_mut().unwrap().ratelimits =
user.belongs_to.borrow().clone_limits_if_some().unwrap();
} }
result
} }
} }
@ -158,6 +151,12 @@ impl Instance {
This function is a wrapper around [`User::get`]. This function is a wrapper around [`User::get`].
*/ */
pub async fn get_user(&mut self, token: String, id: Option<&String>) -> ChorusResult<User> { pub async fn get_user(&mut self, token: String, id: Option<&String>) -> ChorusResult<User> {
User::_get(&token, &self.urls.api.clone(), self, id).await let mut user = UserMeta::shell(Rc::new(RefCell::new(self.clone())), token);
let result = User::get(&mut user, id).await;
if self.limits_information.is_some() {
self.limits_information.as_mut().unwrap().ratelimits =
user.belongs_to.borrow().clone_limits_if_some().unwrap();
}
result
} }
} }

View File

@ -1,39 +1,50 @@
use custom_error::custom_error; use custom_error::custom_error;
use reqwest::Error;
custom_error! { custom_error! {
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub FieldFormatError pub RegistrationError
PasswordError = "Password must be between 1 and 72 characters.", Consent = "Consent must be 'true' to register.",
UsernameError = "Username must be between 2 and 32 characters.",
ConsentError = "Consent must be 'true' to register.",
EmailError = "The provided email address is in an invalid format.",
} }
pub type ChorusResult<T> = std::result::Result<T, ChorusLibError>; pub type ChorusResult<T> = std::result::Result<T, ChorusError>;
custom_error! { custom_error! {
#[derive(PartialEq, Eq)] pub ChorusError
pub ChorusLibError /// Server did not respond.
NoResponse = "Did not receive a response from the Server.", NoResponse = "Did not receive a response from the Server.",
RequestErrorError{url:String, error:String} = "An error occured while trying to GET from {url}: {error}", /// Reqwest returned an Error instead of a Response object.
ReceivedErrorCodeError{error_code:String} = "Received the following error code while requesting from the route: {error_code}", RequestFailed{url:String, error: Error} = "An error occured while trying to GET from {url}: {error}",
CantGetInfoError{error:String} = "Something seems to be wrong with the instance. Cannot get information about the instance: {error}", /// Response received, however, it was not of the successful responses type. Used when no other, special case applies.
InvalidFormBodyError{error_type: String, error:String} = "The server responded with: {error_type}: {error}", ReceivedErrorCode{error_code: u16, error: String} = "Received the following error code while requesting from the route: {error_code}",
/// Used when there is likely something wrong with the instance, the request was directed to.
CantGetInformation{error:String} = "Something seems to be wrong with the instance. Cannot get information about the instance: {error}",
/// The requests form body was malformed/invalid.
InvalidFormBody{error_type: String, error:String} = "The server responded with: {error_type}: {error}",
/// The request has not been processed by the server due to a relevant rate limit bucket being exhausted.
RateLimited{bucket:String} = "Ratelimited on Bucket {bucket}", RateLimited{bucket:String} = "Ratelimited on Bucket {bucket}",
MultipartCreationError{error: String} = "Got an error whilst creating the form: {error}", /// The multipart form could not be created.
FormCreationError{error: String} = "Got an error whilst creating the form: {error}", MultipartCreation{error: String} = "Got an error whilst creating the form: {error}",
/// The regular form could not be created.
FormCreation{error: String} = "Got an error whilst creating the form: {error}",
/// The token is invalid.
TokenExpired = "Token expired, invalid or not found.", TokenExpired = "Token expired, invalid or not found.",
/// No permission
NoPermission = "You do not have the permissions needed to perform this action.", NoPermission = "You do not have the permissions needed to perform this action.",
/// Resource not found
NotFound{error: String} = "The provided resource hasn't been found: {error}", NotFound{error: String} = "The provided resource hasn't been found: {error}",
PasswordRequiredError = "You need to provide your current password to authenticate for this action.", /// Used when you, for example, try to change your spacebar account password without providing your old password for verification.
InvalidResponseError{error: String} = "The response is malformed and cannot be processed. Error: {error}", PasswordRequired = "You need to provide your current password to authenticate for this action.",
InvalidArgumentsError{error: String} = "Invalid arguments were provided. Error: {error}" /// Malformed or unexpected response.
InvalidResponse{error: String} = "The response is malformed and cannot be processed. Error: {error}",
/// Invalid, insufficient or too many arguments provided.
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}"
} }
custom_error! { custom_error! {
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub ObserverError pub ObserverError
AlreadySubscribedError = "Each event can only be subscribed to once." AlreadySubscribed = "Each event can only be subscribed to once."
} }
custom_error! { custom_error! {
@ -45,25 +56,25 @@ custom_error! {
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
pub GatewayError pub GatewayError
// Errors we have received from the gateway // Errors we have received from the gateway
UnknownError = "We're not sure what went wrong. Try reconnecting?", Unknown = "We're not sure what went wrong. Try reconnecting?",
UnknownOpcodeError = "You sent an invalid Gateway opcode or an invalid payload for an opcode", UnknownOpcode = "You sent an invalid Gateway opcode or an invalid payload for an opcode",
DecodeError = "Gateway server couldn't decode payload", Decode = "Gateway server couldn't decode payload",
NotAuthenticatedError = "You sent a payload prior to identifying", NotAuthenticated = "You sent a payload prior to identifying",
AuthenticationFailedError = "The account token sent with your identify payload is invalid", AuthenticationFailed = "The account token sent with your identify payload is invalid",
AlreadyAuthenticatedError = "You've already identified, no need to reauthenticate", AlreadyAuthenticated = "You've already identified, no need to reauthenticate",
InvalidSequenceNumberError = "The sequence number sent when resuming the session was invalid. Reconnect and start a new session", InvalidSequenceNumber = "The sequence number sent when resuming the session was invalid. Reconnect and start a new session",
RateLimitedError = "You are being rate limited!", RateLimited = "You are being rate limited!",
SessionTimedOutError = "Your session timed out. Reconnect and start a new one", SessionTimedOut = "Your session timed out. Reconnect and start a new one",
InvalidShardError = "You sent us an invalid shard when identifying", InvalidShard = "You sent us an invalid shard when identifying",
ShardingRequiredError = "The session would have handled too many guilds - you are required to shard your connection in order to connect", ShardingRequired = "The session would have handled too many guilds - you are required to shard your connection in order to connect",
InvalidAPIVersionError = "You sent an invalid Gateway version", InvalidAPIVersion = "You sent an invalid Gateway version",
InvalidIntentsError = "You sent an invalid intent", InvalidIntents = "You sent an invalid intent",
DisallowedIntentsError = "You sent a disallowed intent. You may have tried to specify an intent that you have not enabled or are not approved for", DisallowedIntents = "You sent a disallowed intent. You may have tried to specify an intent that you have not enabled or are not approved for",
// Errors when initiating a gateway connection // Errors when initiating a gateway connection
CannotConnectError{error: String} = "Cannot connect due to a tungstenite error: {error}", CannotConnect{error: String} = "Cannot connect due to a tungstenite error: {error}",
NonHelloOnInitiateError{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong", NonHelloOnInitiate{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong",
// Other misc errors // Other misc errors
UnexpectedOpcodeReceivedError{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", UnexpectedOpcodeReceived{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}",
} }

View File

@ -8,7 +8,7 @@ use futures_util::stream::SplitSink;
use futures_util::stream::SplitStream; use futures_util::stream::SplitStream;
use futures_util::SinkExt; use futures_util::SinkExt;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, info, trace, warn}; use log::{info, trace, warn};
use native_tls::TlsConnector; use native_tls::TlsConnector;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::error::TryRecvError;
@ -95,25 +95,21 @@ impl GatewayMessage {
let processed_content = content.to_lowercase().replace('.', ""); let processed_content = content.to_lowercase().replace('.', "");
match processed_content.as_str() { match processed_content.as_str() {
"unknown error" | "4000" => Some(GatewayError::UnknownError), "unknown error" | "4000" => Some(GatewayError::Unknown),
"unknown opcode" | "4001" => Some(GatewayError::UnknownOpcodeError), "unknown opcode" | "4001" => Some(GatewayError::UnknownOpcode),
"decode error" | "error while decoding payload" | "4002" => { "decode error" | "error while decoding payload" | "4002" => Some(GatewayError::Decode),
Some(GatewayError::DecodeError) "not authenticated" | "4003" => Some(GatewayError::NotAuthenticated),
} "authentication failed" | "4004" => Some(GatewayError::AuthenticationFailed),
"not authenticated" | "4003" => Some(GatewayError::NotAuthenticatedError), "already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticated),
"authentication failed" | "4004" => Some(GatewayError::AuthenticationFailedError), "invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumber),
"already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticatedError), "rate limited" | "4008" => Some(GatewayError::RateLimited),
"invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumberError), "session timed out" | "4009" => Some(GatewayError::SessionTimedOut),
"rate limited" | "4008" => Some(GatewayError::RateLimitedError), "invalid shard" | "4010" => Some(GatewayError::InvalidShard),
"session timed out" | "4009" => Some(GatewayError::SessionTimedOutError), "sharding required" | "4011" => Some(GatewayError::ShardingRequired),
"invalid shard" | "4010" => Some(GatewayError::InvalidShardError), "invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersion),
"sharding required" | "4011" => Some(GatewayError::ShardingRequiredError), "invalid intent(s)" | "invalid intent" | "4013" => Some(GatewayError::InvalidIntents),
"invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersionError),
"invalid intent(s)" | "invalid intent" | "4013" => {
Some(GatewayError::InvalidIntentsError)
}
"disallowed intent(s)" | "disallowed intents" | "4014" => { "disallowed intent(s)" | "disallowed intents" | "4014" => {
Some(GatewayError::DisallowedIntentsError) Some(GatewayError::DisallowedIntents)
} }
_ => None, _ => None,
} }
@ -294,7 +290,7 @@ impl Gateway {
{ {
Ok(websocket_stream) => websocket_stream, Ok(websocket_stream) => websocket_stream,
Err(e) => { Err(e) => {
return Err(GatewayError::CannotConnectError { return Err(GatewayError::CannotConnect {
error: e.to_string(), error: e.to_string(),
}) })
} }
@ -314,7 +310,7 @@ impl Gateway {
serde_json::from_str(msg.to_text().unwrap()).unwrap(); serde_json::from_str(msg.to_text().unwrap()).unwrap();
if gateway_payload.op_code != GATEWAY_HELLO { if gateway_payload.op_code != GATEWAY_HELLO {
return Err(GatewayError::NonHelloOnInitiateError { return Err(GatewayError::NonHelloOnInitiate {
opcode: gateway_payload.op_code, opcode: gateway_payload.op_code,
}); });
} }
@ -1493,7 +1489,7 @@ impl Gateway {
| GATEWAY_REQUEST_GUILD_MEMBERS | GATEWAY_REQUEST_GUILD_MEMBERS
| GATEWAY_CALL_SYNC | GATEWAY_CALL_SYNC
| GATEWAY_LAZY_REQUEST => { | GATEWAY_LAZY_REQUEST => {
let error = GatewayError::UnexpectedOpcodeReceivedError { let error = GatewayError::UnexpectedOpcodeReceived {
opcode: gateway_payload.op_code, opcode: gateway_payload.op_code,
}; };
Err::<(), GatewayError>(error).unwrap(); Err::<(), GatewayError>(error).unwrap();
@ -1521,6 +1517,7 @@ impl Gateway {
} }
/// Handles sending heartbeats to the gateway in another thread /// Handles sending heartbeats to the gateway in another thread
#[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used
struct HeartbeatHandler { struct HeartbeatHandler {
/// The heartbeat interval in milliseconds /// The heartbeat interval in milliseconds
pub heartbeat_interval: u128, pub heartbeat_interval: u128,

View File

@ -1,26 +1,36 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::rc::Rc; use std::rc::Rc;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::api::limits::Limits; use crate::api::{Limit, LimitType};
use crate::errors::{ChorusLibError, ChorusResult, FieldFormatError}; use crate::errors::ChorusResult;
use crate::ratelimiter::ChorusRequest;
use crate::types::types::subconfigs::limits::rates::RateLimits;
use crate::types::{GeneralConfiguration, User, UserSettings}; use crate::types::{GeneralConfiguration, User, UserSettings};
use crate::UrlBundle; use crate::UrlBundle;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/** /**
The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server. The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server.
If `limits_information` is `None`, then the instance will not be rate limited.
*/ */
pub struct Instance { pub struct Instance {
pub urls: UrlBundle, pub urls: UrlBundle,
pub instance_info: GeneralConfiguration, pub instance_info: GeneralConfiguration,
pub limits: Limits, pub limits_information: Option<LimitsInformation>,
pub client: Client, pub client: Client,
} }
#[derive(Debug, Clone)]
pub struct LimitsInformation {
pub ratelimits: HashMap<LimitType, Limit>,
pub configuration: RateLimits,
}
impl Instance { impl Instance {
/// Creates a new [`Instance`]. /// Creates a new [`Instance`].
/// # Arguments /// # Arguments
@ -28,24 +38,43 @@ impl Instance {
/// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. /// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server.
/// # Errors /// # Errors
/// * [`InstanceError`] - If the instance cannot be created. /// * [`InstanceError`] - If the instance cannot be created.
pub async fn new(urls: UrlBundle) -> ChorusResult<Instance> { pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> {
let limits_information;
if limited {
let limits_configuration =
Some(ChorusRequest::get_limits_config(&urls.api).await?.rate);
let limits = Some(ChorusRequest::limits_config_to_hashmap(
limits_configuration.as_ref().unwrap(),
));
limits_information = Some(LimitsInformation {
ratelimits: limits.unwrap(),
configuration: limits_configuration.unwrap(),
});
} else {
limits_information = None;
}
let mut instance = Instance { let mut instance = Instance {
urls: urls.clone(), urls: urls.clone(),
// Will be overwritten in the next step // Will be overwritten in the next step
instance_info: GeneralConfiguration::default(), instance_info: GeneralConfiguration::default(),
limits: Limits::check_limits(urls.api).await, limits_information,
client: Client::new(), client: Client::new(),
}; };
instance.instance_info = match instance.general_configuration_schema().await { instance.instance_info = match instance.general_configuration_schema().await {
Ok(schema) => schema, Ok(schema) => schema,
Err(e) => { Err(e) => {
return Err(ChorusLibError::CantGetInfoError { log::warn!("Could not get instance configuration schema: {}", e);
error: e.to_string(), GeneralConfiguration::default()
});
} }
}; };
Ok(instance) Ok(instance)
} }
pub(crate) fn clone_limits_if_some(&self) -> Option<HashMap<LimitType, Limit>> {
if self.limits_information.is_some() {
return Some(self.limits_information.as_ref().unwrap().ratelimits.clone());
}
None
}
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -59,30 +88,11 @@ impl fmt::Display for Token {
} }
} }
#[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<Username, FieldFormatError> {
if username.len() < 2 || username.len() > 32 {
return Err(FieldFormatError::UsernameError);
}
Ok(Username { username })
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct UserMeta { pub struct UserMeta {
pub belongs_to: Rc<RefCell<Instance>>, pub belongs_to: Rc<RefCell<Instance>>,
pub token: String, pub token: String,
pub limits: Limits, pub limits: Option<HashMap<LimitType, Limit>>,
pub settings: UserSettings, pub settings: UserSettings,
pub object: User, pub object: User,
} }
@ -99,7 +109,7 @@ impl UserMeta {
pub fn new( pub fn new(
belongs_to: Rc<RefCell<Instance>>, belongs_to: Rc<RefCell<Instance>>,
token: String, token: String,
limits: Limits, limits: Option<HashMap<LimitType, Limit>>,
settings: UserSettings, settings: UserSettings,
object: User, object: User,
) -> UserMeta { ) -> UserMeta {
@ -111,4 +121,24 @@ impl UserMeta {
object, object,
} }
} }
/// Creates a new 'shell' of a user. The user does not exist as an object, and exists so that you have
/// a UserMeta object to make Rate Limited requests with. This is useful in scenarios like
/// registering or logging in to the Instance, where you do not yet have a User object, but still
/// need to make a RateLimited request.
pub(crate) fn shell(instance: Rc<RefCell<Instance>>, token: String) -> UserMeta {
let settings = UserSettings::default();
let object = User::default();
UserMeta {
belongs_to: instance.clone(),
token,
limits: instance
.borrow()
.limits_information
.as_ref()
.map(|info| info.ratelimits.clone()),
settings,
object,
}
}
} }

View File

@ -10,7 +10,7 @@ pub mod gateway;
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub mod instance; pub mod instance;
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub mod limit; pub mod ratelimiter;
pub mod types; pub mod types;
#[cfg(feature = "client")] #[cfg(feature = "client")]
pub mod voice; pub mod voice;

View File

@ -1,304 +0,0 @@
use reqwest::{RequestBuilder, Response};
use crate::{
api::limits::{Limit, LimitType, Limits, LimitsMutRef},
errors::{ChorusLibError, ChorusResult},
instance::Instance,
};
#[derive(Debug)]
pub struct LimitedRequester;
impl LimitedRequester {
/// Checks if a request can be sent without hitting API rate limits and sends it, if true.
/// Will automatically update the rate limits of the LimitedRequester the request has been
/// sent with.
///
/// # Arguments
///
/// * `request`: A `RequestBuilder` that contains a request ready to be sent. Unfinished or
/// invalid requests will result in the method panicing.
/// * `limit_type`: Because this library does not yet implement a way to check for which rate
/// limit will be used when the request gets send, you will have to specify this manually using
/// a `LimitType` enum.
///
/// # Returns
///
/// * `Response`: The `Response` gotten from sending the request to the server. This will be
/// returned if the Request was built and send successfully. Is wrapped in an `Option`.
/// * `None`: `None` will be returned if the rate limit has been hit, and the request could
/// therefore not have been sent.
///
/// # Errors
///
/// This method will error if:
///
/// * The request does not return a success status code (200-299)
/// * The supplied `RequestBuilder` contains invalid or incomplete information
/// * There has been an error with processing (unwrapping) the `Response`
/// * The call to `update_limits` yielded errors. Read the methods' Errors section for more
/// information.
pub async fn send_request(
request: RequestBuilder,
limit_type: LimitType,
instance: &mut Instance,
user_rate_limits: &mut Limits,
) -> ChorusResult<Response> {
if LimitedRequester::can_send_request(limit_type, &instance.limits, user_rate_limits) {
let built_request = match request.build() {
Ok(request) => request,
Err(e) => {
return Err(ChorusLibError::RequestErrorError {
url: "".to_string(),
error: e.to_string(),
});
}
};
let result = instance.client.execute(built_request).await;
let response = match result {
Ok(is_response) => is_response,
Err(e) => {
return Err(ChorusLibError::ReceivedErrorCodeError {
error_code: e.to_string(),
});
}
};
LimitedRequester::update_limits(
&response,
limit_type,
&mut instance.limits,
user_rate_limits,
);
if !response.status().is_success() {
match response.status().as_u16() {
401 => Err(ChorusLibError::TokenExpired),
403 => Err(ChorusLibError::TokenExpired),
_ => Err(ChorusLibError::ReceivedErrorCodeError {
error_code: response.status().as_str().to_string(),
}),
}
} else {
Ok(response)
}
} else {
Err(ChorusLibError::RateLimited {
bucket: limit_type.to_string(),
})
}
}
fn update_limit_entry(entry: &mut Limit, reset: u64, remaining: u64, limit: u64) {
if reset != entry.reset {
entry.reset = reset;
entry.remaining = limit;
entry.limit = limit;
} else {
entry.remaining = remaining;
entry.limit = limit;
}
}
fn can_send_request(
limit_type: LimitType,
instance_rate_limits: &Limits,
user_rate_limits: &Limits,
) -> bool {
// Check if all of the limits in this vec have at least one remaining request
let rate_limits = Limits::combine(instance_rate_limits, user_rate_limits);
let constant_limits: Vec<&LimitType> = [
&LimitType::Error,
&LimitType::Global,
&LimitType::Ip,
&limit_type,
]
.to_vec();
for limit in constant_limits.iter() {
match rate_limits.to_hash_map().get(limit) {
Some(limit) => {
if limit.remaining == 0 {
return false;
}
// AbsoluteRegister and AuthRegister can cancel each other out.
if limit.bucket == LimitType::AbsoluteRegister
&& rate_limits
.to_hash_map()
.get(&LimitType::AuthRegister)
.unwrap()
.remaining
== 0
{
return false;
}
if limit.bucket == LimitType::AuthRegister
&& rate_limits
.to_hash_map()
.get(&LimitType::AbsoluteRegister)
.unwrap()
.remaining
== 0
{
return false;
}
}
None => return false,
}
}
true
}
fn update_limits(
response: &Response,
limit_type: LimitType,
instance_rate_limits: &mut Limits,
user_rate_limits: &mut Limits,
) {
let mut rate_limits = LimitsMutRef::combine_mut_ref(instance_rate_limits, user_rate_limits);
let remaining = match response.headers().get("X-RateLimit-Remaining") {
Some(remaining) => remaining.to_str().unwrap().parse::<u64>().unwrap(),
None => rate_limits.get_limit_mut_ref(&limit_type).remaining - 1,
};
let limit = match response.headers().get("X-RateLimit-Limit") {
Some(limit) => limit.to_str().unwrap().parse::<u64>().unwrap(),
None => rate_limits.get_limit_mut_ref(&limit_type).limit,
};
let reset = match response.headers().get("X-RateLimit-Reset") {
Some(reset) => reset.to_str().unwrap().parse::<u64>().unwrap(),
None => rate_limits.get_limit_mut_ref(&limit_type).reset,
};
let status = response.status();
let status_str = status.as_str();
if status_str.starts_with('4') {
rate_limits
.get_limit_mut_ref(&LimitType::Error)
.add_remaining(-1);
}
rate_limits
.get_limit_mut_ref(&LimitType::Global)
.add_remaining(-1);
rate_limits
.get_limit_mut_ref(&LimitType::Ip)
.add_remaining(-1);
match limit_type {
LimitType::Error => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Error);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::Global => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Global);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::Ip => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Ip);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::AuthLogin => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::AuthLogin);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::AbsoluteRegister => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::AbsoluteRegister);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
// AbsoluteRegister and AuthRegister both need to be updated, if a Register event
// happens.
rate_limits
.get_limit_mut_ref(&LimitType::AuthRegister)
.remaining -= 1;
}
LimitType::AuthRegister => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::AuthRegister);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
// AbsoluteRegister and AuthRegister both need to be updated, if a Register event
// happens.
rate_limits
.get_limit_mut_ref(&LimitType::AbsoluteRegister)
.remaining -= 1;
}
LimitType::AbsoluteMessage => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::AbsoluteMessage);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::Channel => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Channel);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::Guild => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Guild);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
LimitType::Webhook => {
let entry = rate_limits.get_limit_mut_ref(&LimitType::Webhook);
LimitedRequester::update_limit_entry(entry, reset, remaining, limit);
}
}
}
}
#[cfg(test)]
mod rate_limit {
use serde_json::from_str;
use crate::{api::limits::Config, UrlBundle};
use super::*;
#[tokio::test]
async fn run_into_limit() {
let urls = UrlBundle::new(
String::from("http://localhost:3001/api/"),
String::from("wss://localhost:3001/"),
String::from("http://localhost:3001/cdn"),
);
let mut request: Option<ChorusResult<Response>> = None;
let mut instance = Instance::new(urls.clone()).await.unwrap();
let mut user_rate_limits = Limits::check_limits(urls.api.clone()).await;
for _ in 0..=50 {
let request_path = urls.api.clone() + "/some/random/nonexisting/path";
let request_builder = instance.client.get(request_path);
request = Some(
LimitedRequester::send_request(
request_builder,
LimitType::Channel,
&mut instance,
&mut user_rate_limits,
)
.await,
);
}
assert!(matches!(request, Some(Err(_))));
}
#[tokio::test]
async fn test_send_request() {
let urls = UrlBundle::new(
String::from("http://localhost:3001/api/"),
String::from("wss://localhost:3001/"),
String::from("http://localhost:3001/cdn"),
);
let mut instance = Instance::new(urls.clone()).await.unwrap();
let mut user_rate_limits = Limits::check_limits(urls.api.clone()).await;
let _requester = LimitedRequester;
let request_path = urls.api.clone() + "/policies/instance/limits";
let request_builder = instance.client.get(request_path);
let request = LimitedRequester::send_request(
request_builder,
LimitType::Channel,
&mut instance,
&mut user_rate_limits,
)
.await;
let result = match request {
Ok(result) => result,
Err(_) => panic!("Request failed"),
};
let _config: Config = from_str(result.text().await.unwrap().as_str()).unwrap();
}
}

462
src/ratelimiter.rs Normal file
View File

@ -0,0 +1,462 @@
use std::collections::HashMap;
use log;
use reqwest::{Client, RequestBuilder, Response};
use serde::Deserialize;
use serde_json::from_str;
use crate::{
api::{Limit, LimitType},
errors::{ChorusError, ChorusResult},
instance::UserMeta,
types::{types::subconfigs::limits::rates::RateLimits, LimitsConfiguration},
};
/// Chorus' request struct. This struct is used to send rate-limited requests to the Spacebar server.
/// See <https://discord.com/developers/docs/topics/rate-limits#rate-limits> for more information.
pub struct ChorusRequest {
pub request: RequestBuilder,
pub limit_type: LimitType,
}
impl ChorusRequest {
/// Sends a [`ChorusRequest`]. Checks if the user is rate limited, and if not, sends the request.
/// If the user is not rate limited and the instance has rate limits enabled, it will update the
/// rate limits.
#[allow(clippy::await_holding_refcell_ref)]
pub(crate) async fn send_request(self, user: &mut UserMeta) -> ChorusResult<Response> {
if !ChorusRequest::can_send_request(user, &self.limit_type) {
log::info!("Rate limit hit. Bucket: {:?}", self.limit_type);
return Err(ChorusError::RateLimited {
bucket: format!("{:?}", self.limit_type),
});
}
let belongs_to = user.belongs_to.borrow();
let result = match belongs_to
.client
.execute(self.request.build().unwrap())
.await
{
Ok(result) => result,
Err(error) => {
log::warn!("Request failed: {:?}", error);
return Err(ChorusError::RequestFailed {
url: error.url().unwrap().to_string(),
error,
});
}
};
drop(belongs_to);
if !result.status().is_success() {
if result.status().as_u16() == 429 {
log::warn!("Rate limit hit unexpectedly. Bucket: {:?}. Setting the instances' remaining global limit to 0 to have cooldown.", self.limit_type);
user.belongs_to
.borrow_mut()
.limits_information
.as_mut()
.unwrap()
.ratelimits
.get_mut(&LimitType::Global)
.unwrap()
.remaining = 0;
return Err(ChorusError::RateLimited {
bucket: format!("{:?}", self.limit_type),
});
}
log::warn!("Request failed: {:?}", result);
return Err(ChorusRequest::interpret_error(result).await);
}
ChorusRequest::update_rate_limits(user, &self.limit_type, !result.status().is_success());
Ok(result)
}
fn can_send_request(user: &mut UserMeta, limit_type: &LimitType) -> bool {
log::trace!("Checking if user or instance is rate-limited...");
let mut belongs_to = user.belongs_to.borrow_mut();
if belongs_to.limits_information.is_none() {
log::trace!("Instance indicates no rate limits are configured. Continuing.");
return true;
}
let instance_dictated_limits = [
&LimitType::AuthLogin,
&LimitType::AuthRegister,
&LimitType::Global,
&LimitType::Ip,
];
let limits = match instance_dictated_limits.contains(&limit_type) {
true => {
log::trace!(
"Limit type {:?} is dictated by the instance. Continuing.",
limit_type
);
belongs_to
.limits_information
.as_mut()
.unwrap()
.ratelimits
.clone()
}
false => {
log::trace!(
"Limit type {:?} is dictated by the user. Continuing.",
limit_type
);
ChorusRequest::ensure_limit_in_map(
&belongs_to
.limits_information
.as_ref()
.unwrap()
.configuration,
user.limits.as_mut().unwrap(),
limit_type,
);
user.limits.as_mut().unwrap().clone()
}
};
let global = belongs_to
.limits_information
.as_ref()
.unwrap()
.ratelimits
.get(&LimitType::Global)
.unwrap();
let ip = belongs_to
.limits_information
.as_ref()
.unwrap()
.ratelimits
.get(&LimitType::Ip)
.unwrap();
let limit_type_limit = limits.get(limit_type).unwrap();
global.remaining > 0 && ip.remaining > 0 && limit_type_limit.remaining > 0
}
fn ensure_limit_in_map(
rate_limits_config: &RateLimits,
map: &mut HashMap<LimitType, Limit>,
limit_type: &LimitType,
) {
log::trace!("Ensuring limit type {:?} is in the map.", limit_type);
let time: u64 = chrono::Utc::now().timestamp() as u64;
match limit_type {
LimitType::Channel(snowflake) => {
if map.get(&LimitType::Channel(*snowflake)).is_some() {
log::trace!(
"Limit type {:?} is already in the map. Returning.",
limit_type
);
return;
}
log::trace!("Limit type {:?} is not in the map. Adding it.", limit_type);
let channel_limit = &rate_limits_config.routes.channel;
map.insert(
LimitType::Channel(*snowflake),
Limit {
bucket: LimitType::Channel(*snowflake),
limit: channel_limit.count,
remaining: channel_limit.count,
reset: channel_limit.window + time,
window: channel_limit.window,
},
);
}
LimitType::Guild(snowflake) => {
if map.get(&LimitType::Guild(*snowflake)).is_some() {
return;
}
let guild_limit = &rate_limits_config.routes.guild;
map.insert(
LimitType::Guild(*snowflake),
Limit {
bucket: LimitType::Guild(*snowflake),
limit: guild_limit.count,
remaining: guild_limit.count,
reset: guild_limit.window + time,
window: guild_limit.window,
},
);
}
LimitType::Webhook(snowflake) => {
if map.get(&LimitType::Webhook(*snowflake)).is_some() {
return;
}
let webhook_limit = &rate_limits_config.routes.webhook;
map.insert(
LimitType::Webhook(*snowflake),
Limit {
bucket: LimitType::Webhook(*snowflake),
limit: webhook_limit.count,
remaining: webhook_limit.count,
reset: webhook_limit.window + time,
window: webhook_limit.window,
},
);
}
other_limit => {
if map.get(other_limit).is_some() {
return;
}
let limits_map = ChorusRequest::limits_config_to_hashmap(rate_limits_config);
map.insert(
*other_limit,
Limit {
bucket: *other_limit,
limit: limits_map.get(other_limit).as_ref().unwrap().limit,
remaining: limits_map.get(other_limit).as_ref().unwrap().remaining,
reset: limits_map.get(other_limit).as_ref().unwrap().reset,
window: limits_map.get(other_limit).as_ref().unwrap().window,
},
);
}
}
}
async fn interpret_error(response: reqwest::Response) -> ChorusError {
match response.status().as_u16() {
401..=403 | 407 => ChorusError::NoPermission,
404 => ChorusError::NotFound {
error: response.text().await.unwrap(),
},
405 | 408 | 409 => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap() },
411..=421 | 426 | 428 | 431 => ChorusError::InvalidArguments {
error: response.text().await.unwrap(),
},
429 => panic!("Illegal state: Rate limit exception should have been caught before this function call."),
451 => ChorusError::NoResponse,
500..=599 => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap() },
_ => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap()},
}
}
/// Updates the rate limits of the user. The following steps are performed:
/// 1. If the current unix timestamp is greater than the reset timestamp, the reset timestamp is
/// set to the current unix timestamp + the rate limit window. The remaining rate limit is
/// reset to the rate limit limit.
/// 2. The remaining rate limit is decreased by 1.
fn update_rate_limits(user: &mut UserMeta, limit_type: &LimitType, response_was_err: bool) {
let instance_dictated_limits = [
&LimitType::AuthLogin,
&LimitType::AuthRegister,
&LimitType::Global,
&LimitType::Ip,
];
// modify this to store something to look up the value with later, instead of storing a reference to the actual data itself.
let mut relevant_limits = Vec::new();
if instance_dictated_limits.contains(&limit_type) {
relevant_limits.push((LimitOrigin::Instance, *limit_type));
} else {
relevant_limits.push((LimitOrigin::User, *limit_type));
}
relevant_limits.push((LimitOrigin::Instance, LimitType::Global));
relevant_limits.push((LimitOrigin::Instance, LimitType::Ip));
if response_was_err {
relevant_limits.push((LimitOrigin::User, LimitType::Error));
}
let time: u64 = chrono::Utc::now().timestamp() as u64;
for relevant_limit in relevant_limits.iter() {
let mut belongs_to = user.belongs_to.borrow_mut();
let limit = match relevant_limit.0 {
LimitOrigin::Instance => {
log::trace!(
"Updating instance rate limit. Bucket: {:?}",
relevant_limit.1
);
belongs_to
.limits_information
.as_mut()
.unwrap()
.ratelimits
.get_mut(&relevant_limit.1)
.unwrap()
}
LimitOrigin::User => {
log::trace!("Updating user rate limit. Bucket: {:?}", relevant_limit.1);
user.limits
.as_mut()
.unwrap()
.get_mut(&relevant_limit.1)
.unwrap()
}
};
if time > limit.reset {
// Spacebar does not yet return rate limit information in its response headers. We
// therefore have to guess the next rate limit window. This is not ideal. Oh well!
log::trace!("Rate limit replenished. Bucket: {:?}", limit.bucket);
limit.reset += limit.window;
limit.remaining = limit.limit;
}
limit.remaining -= 1;
}
}
pub(crate) async fn get_limits_config(url_api: &str) -> ChorusResult<LimitsConfiguration> {
let request = Client::new()
.get(format!("{}/policies/instance/limits/", url_api))
.send()
.await;
let request = match request {
Ok(request) => request,
Err(e) => {
return Err(ChorusError::RequestFailed {
url: url_api.to_string(),
error: e,
})
}
};
let limits_configuration = match request.status().as_u16() {
200 => from_str::<LimitsConfiguration>(&request.text().await.unwrap()).unwrap(),
429 => {
return Err(ChorusError::RateLimited {
bucket: format!("{:?}", LimitType::Ip),
})
}
404 => return Err(ChorusError::NotFound { error: "Route \"/policies/instance/limits/\" not found. Are you perhaps trying to request the Limits configuration from an unsupported server?".to_string() }),
400..=u16::MAX => {
return Err(ChorusError::ReceivedErrorCode { error_code: request.status().as_u16(), error: request.text().await.unwrap() })
}
_ => {
return Err(ChorusError::InvalidResponse {
error: request.text().await.unwrap(),
})
}
};
Ok(limits_configuration)
}
pub(crate) fn limits_config_to_hashmap(
limits_configuration: &RateLimits,
) -> HashMap<LimitType, Limit> {
let config = limits_configuration.clone();
let routes = config.routes;
let mut map: HashMap<LimitType, Limit> = HashMap::new();
let time: u64 = chrono::Utc::now().timestamp() as u64;
map.insert(
LimitType::AuthLogin,
Limit {
bucket: LimitType::AuthLogin,
limit: routes.auth.login.count,
remaining: routes.auth.login.count,
reset: routes.auth.login.window + time,
window: routes.auth.login.window,
},
);
map.insert(
LimitType::AuthRegister,
Limit {
bucket: LimitType::AuthRegister,
limit: routes.auth.register.count,
remaining: routes.auth.register.count,
reset: routes.auth.register.window + time,
window: routes.auth.register.window,
},
);
map.insert(
LimitType::ChannelBaseline,
Limit {
bucket: LimitType::ChannelBaseline,
limit: routes.channel.count,
remaining: routes.channel.count,
reset: routes.channel.window + time,
window: routes.channel.window,
},
);
map.insert(
LimitType::Error,
Limit {
bucket: LimitType::Error,
limit: config.error.count,
remaining: config.error.count,
reset: config.error.window + time,
window: config.error.window,
},
);
map.insert(
LimitType::Global,
Limit {
bucket: LimitType::Global,
limit: config.global.count,
remaining: config.global.count,
reset: config.global.window + time,
window: config.global.window,
},
);
map.insert(
LimitType::Ip,
Limit {
bucket: LimitType::Ip,
limit: config.ip.count,
remaining: config.ip.count,
reset: config.ip.window + time,
window: config.ip.window,
},
);
map.insert(
LimitType::GuildBaseline,
Limit {
bucket: LimitType::GuildBaseline,
limit: routes.guild.count,
remaining: routes.guild.count,
reset: routes.guild.window + time,
window: routes.guild.window,
},
);
map.insert(
LimitType::WebhookBaseline,
Limit {
bucket: LimitType::WebhookBaseline,
limit: routes.webhook.count,
remaining: routes.webhook.count,
reset: routes.webhook.window + time,
window: routes.webhook.window,
},
);
map
}
/// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains nothing if the request
/// was successful, or a [`ChorusError`] if the request failed.
pub(crate) async fn handle_request_as_result(self, user: &mut UserMeta) -> ChorusResult<()> {
match self.send_request(user).await {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
/// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains a [`T`] if the request
/// was successful, or a [`ChorusError`] if the request failed.
pub(crate) async fn deserialize_response<T: for<'a> Deserialize<'a>>(
self,
user: &mut UserMeta,
) -> ChorusResult<T> {
let response = self.send_request(user).await?;
let response_text = match response.text().await {
Ok(string) => string,
Err(e) => {
return Err(ChorusError::InvalidResponse {
error: format!(
"Error while trying to process the HTTP response into a String: {}",
e
),
});
}
};
let object = match from_str::<T>(&response_text) {
Ok(object) => object,
Err(e) => {
return Err(ChorusError::InvalidResponse {
error: format!(
"Error while trying to deserialize the JSON response into T: {}",
e
),
})
}
};
Ok(object)
}
}
enum LimitOrigin {
Instance,
User,
}

View File

@ -18,10 +18,8 @@ pub struct GeneralConfiguration {
impl Default for GeneralConfiguration { impl Default for GeneralConfiguration {
fn default() -> Self { fn default() -> Self {
Self { Self {
instance_name: String::from("Spacebar Instance"), instance_name: String::from("Spacebar-compatible Instance"),
instance_description: Some(String::from( instance_description: Some(String::from("This is a spacebar-compatible instance.")),
"This is a Spacebar instance made in the pre-release days",
)),
front_page: None, front_page: None,
tos_page: None, tos_page: None,
correspondence_email: None, correspondence_email: None,

View File

@ -1,7 +1,12 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::config::types::subconfigs::limits::ratelimits::{ use crate::{
route::RouteRateLimit, RateLimitOptions, api::LimitType,
types::config::types::subconfigs::limits::ratelimits::{
route::RouteRateLimit, RateLimitOptions,
},
}; };
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -39,3 +44,18 @@ impl Default for RateLimits {
} }
} }
} }
impl RateLimits {
pub fn to_hash_map(&self) -> HashMap<LimitType, RateLimitOptions> {
let mut map = HashMap::new();
map.insert(LimitType::AuthLogin, self.routes.auth.login.clone());
map.insert(LimitType::AuthRegister, self.routes.auth.register.clone());
map.insert(LimitType::ChannelBaseline, self.routes.channel.clone());
map.insert(LimitType::Error, self.error.clone());
map.insert(LimitType::Global, self.global.clone());
map.insert(LimitType::Ip, self.ip.clone());
map.insert(LimitType::WebhookBaseline, self.routes.webhook.clone());
map.insert(LimitType::GuildBaseline, self.routes.guild.clone());
map
}
}

View File

@ -1,9 +1,8 @@
use crate::types::utils::Snowflake;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_option_number_from_string; use serde_aux::prelude::deserialize_option_number_from_string;
use crate::types::utils::Snowflake;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
pub struct UserData { pub struct UserData {
@ -16,7 +15,6 @@ impl User {
PublicUser::from(self) PublicUser::from(self)
} }
} }
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct User { pub struct User {
@ -89,6 +87,7 @@ impl From<User> for PublicUser {
} }
} }
#[allow(dead_code)] // FIXME: Remove this when we actually use this
const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32;
bitflags::bitflags! { bitflags::bitflags! {

View File

@ -73,6 +73,7 @@ impl Rights {
} }
} }
#[allow(dead_code)] // FIXME: Remove this when we use this
fn all_rights() -> Rights { fn all_rights() -> Rights {
Rights::OPERATOR Rights::OPERATOR
| Rights::MANAGE_APPLICATIONS | Rights::MANAGE_APPLICATIONS

View File

@ -12,7 +12,7 @@ const EPOCH: i64 = 1420070400000;
/// Unique identifier including a timestamp. /// Unique identifier including a timestamp.
/// See https://discord.com/developers/docs/reference#snowflakes /// See https://discord.com/developers/docs/reference#snowflakes
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "sqlx", derive(Type))] #[cfg_attr(feature = "sqlx", derive(Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))] #[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Snowflake(u64); pub struct Snowflake(u64);

View File

@ -24,7 +24,7 @@ pub async fn setup() -> TestBundle {
"ws://localhost:3001".to_string(), "ws://localhost:3001".to_string(),
"http://localhost:3001".to_string(), "http://localhost:3001".to_string(),
); );
let mut instance = Instance::new(urls.clone()).await.unwrap(); let mut instance = Instance::new(urls.clone(), true).await.unwrap();
// Requires the existance of the below user. // Requires the existance of the below user.
let reg = RegisterSchema { let reg = RegisterSchema {
username: "integrationtestuser".into(), username: "integrationtestuser".into(),

View File

@ -19,7 +19,7 @@ async fn test_get_mutual_relationships() {
username: user.object.username.clone(), username: user.object.username.clone(),
discriminator: Some(user.object.discriminator.clone()), discriminator: Some(user.object.discriminator.clone()),
}; };
other_user.send_friend_request(friend_request_schema).await; let _ = other_user.send_friend_request(friend_request_schema).await;
let relationships = user let relationships = user
.get_mutual_relationships(other_user.object.id) .get_mutual_relationships(other_user.object.id)
.await .await
@ -45,7 +45,10 @@ async fn test_get_relationships() {
username: user.object.username.clone(), username: user.object.username.clone(),
discriminator: Some(user.object.discriminator.clone()), discriminator: Some(user.object.discriminator.clone()),
}; };
other_user.send_friend_request(friend_request_schema).await; other_user
.send_friend_request(friend_request_schema)
.await
.unwrap();
let relationships = user.get_relationships().await.unwrap(); let relationships = user.get_relationships().await.unwrap();
assert_eq!(relationships.get(0).unwrap().id, other_user.object.id); assert_eq!(relationships.get(0).unwrap().id, other_user.object.id);
common::teardown(bundle).await common::teardown(bundle).await
@ -64,7 +67,7 @@ async fn test_modify_relationship_friends() {
let belongs_to = &mut bundle.instance; let belongs_to = &mut bundle.instance;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap(); let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
other_user let _ = other_user
.modify_user_relationship(user.object.id, types::RelationshipType::Friends) .modify_user_relationship(user.object.id, types::RelationshipType::Friends)
.await; .await;
let relationships = user.get_relationships().await.unwrap(); let relationships = user.get_relationships().await.unwrap();
@ -79,7 +82,8 @@ async fn test_modify_relationship_friends() {
relationships.get(0).unwrap().relationship_type, relationships.get(0).unwrap().relationship_type,
RelationshipType::Outgoing RelationshipType::Outgoing
); );
user.modify_user_relationship(other_user.object.id, RelationshipType::Friends) let _ = user
.modify_user_relationship(other_user.object.id, RelationshipType::Friends)
.await; .await;
assert_eq!( assert_eq!(
other_user other_user
@ -91,7 +95,7 @@ async fn test_modify_relationship_friends() {
.relationship_type, .relationship_type,
RelationshipType::Friends RelationshipType::Friends
); );
user.remove_relationship(other_user.object.id).await; let _ = user.remove_relationship(other_user.object.id).await;
assert_eq!( assert_eq!(
other_user.get_relationships().await.unwrap(), other_user.get_relationships().await.unwrap(),
Vec::<Relationship>::new() Vec::<Relationship>::new()
@ -112,7 +116,7 @@ async fn test_modify_relationship_block() {
let belongs_to = &mut bundle.instance; let belongs_to = &mut bundle.instance;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap(); let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
other_user let _ = other_user
.modify_user_relationship(user.object.id, types::RelationshipType::Blocked) .modify_user_relationship(user.object.id, types::RelationshipType::Blocked)
.await; .await;
let relationships = user.get_relationships().await.unwrap(); let relationships = user.get_relationships().await.unwrap();
@ -123,7 +127,7 @@ async fn test_modify_relationship_block() {
relationships.get(0).unwrap().relationship_type, relationships.get(0).unwrap().relationship_type,
RelationshipType::Blocked RelationshipType::Blocked
); );
other_user.remove_relationship(user.object.id).await; let _ = other_user.remove_relationship(user.object.id).await;
assert_eq!( assert_eq!(
other_user.get_relationships().await.unwrap(), other_user.get_relationships().await.unwrap(),
Vec::<Relationship>::new() Vec::<Relationship>::new()