Merge with main

This commit is contained in:
kozabrada123 2023-07-28 17:33:23 +02:00
parent 32a42dae87
commit 942fecbec7
66 changed files with 4743 additions and 3090 deletions

View File

@ -15,20 +15,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install dependencies - name: Clone spacebar server
run: | run: |
sudo apt-get update
sudo apt-get install -y git python3 build-essential
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get install -y nodejs
git clone https://github.com/bitfl0wer/server.git git clone https://github.com/bitfl0wer/server.git
- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
cache-dependency-path: server/package-lock.json
- name: Prepare and start Spacebar server - name: Prepare and start Spacebar server
run: | run: |
npm install npm install
npm run setup npm run setup
npm run start & npm run start &
working-directory: ./server working-directory: ./server
- uses: Swatinem/rust-cache@v2
- name: Build - name: Build
run: cargo build --verbose run: cargo build --verbose
- name: Run tests - name: Run tests

View File

@ -1,20 +0,0 @@
name: Clippy check
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# Make sure CI fails on all warnings, including Clippy lints
env:
RUSTFLAGS: "-Dwarnings"
jobs:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Clippy
run: cargo clippy --all-targets --all-features

53
.github/workflows/rust-clippy.yml vendored Normal file
View File

@ -0,0 +1,53 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# rust-clippy is a tool that runs a bunch of lints to catch common
# mistakes in your Rust code and help improve your Rust code.
# More details at https://github.com/rust-lang/rust-clippy
# and https://rust-lang.github.io/rust-clippy/
name: rust-clippy analyze
on:
push:
branches: [ "main", "preserve/*" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
jobs:
rust-clippy-analyze:
name: Run rust-clippy analyzing
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Rust toolchain
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1
with:
profile: minimal
toolchain: stable
components: clippy
override: true
- name: Install required cargo
run: cargo install clippy-sarif sarif-fmt
- name: Run rust-clippy
run:
cargo clippy
--all-features
--message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
continue-on-error: true
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: rust-clippy-results.sarif
wait-for-processing: true

13
.gitignore vendored
View File

@ -2,19 +2,18 @@
# will have compiled files and executables # will have compiled files and executables
/target/ /target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# Added by cargo # Added by cargo
/target /target
### # IDE specific folders and configs
.vscode/** .vscode/**
.idea/** .idea/**
# macOS
**/.DS_Store

2561
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,31 +10,35 @@ backend = ["poem", "sqlx"]
client = [] client = []
[dependencies] [dependencies]
tokio = {version = "1.28.1", features = ["rt", "macros", "rt-multi-thread", "full"]} tokio = {version = "1.29.1", features = ["macros"]}
serde = {version = "1.0.163", features = ["derive"]} serde = {version = "1.0.171", features = ["derive"]}
serde_json = {version= "1.0.96", features = ["raw_value"]} serde_json = {version= "1.0.103", features = ["raw_value"]}
serde-aux = "4.2.0" serde-aux = "4.2.0"
serde_with = "3.0.0" serde_with = "3.0.0"
serde_repr = "0.1.12" serde_repr = "0.1.14"
reqwest = {version = "0.11.16", features = ["multipart"]} reqwest = {version = "0.11.18", features = ["multipart"]}
url = "2.3.1" url = "2.4.0"
chrono = {version = "0.4.24", features = ["serde"]} chrono = {version = "0.4.26", features = ["serde"]}
regex = "1.7.3" regex = "1.9.1"
custom_error = "1.9.2" custom_error = "1.9.2"
native-tls = "0.2.11" native-tls = "0.2.11"
tokio-tungstenite = {version = "0.19.0", features = ["native-tls"]} tokio-tungstenite = {version = "0.19.0", features = ["native-tls"]}
futures-util = "0.3.28" futures-util = "0.3.28"
http = "0.2.9" http = "0.2.9"
openssl = "0.10.52" openssl = "0.10.55"
base64 = "0.21.2" base64 = "0.21.2"
hostname = "0.3.1" hostname = "0.3.1"
bitflags = { version = "2.2.1", features = ["serde"] } bitflags = { version = "2.3.3", features = ["serde"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
poem = { version = "1.3.55", optional = true } poem = { version = "1.3.56", optional = true }
sqlx = { git = "https://github.com/zert3x/sqlx", branch="feature/skip", features = ["mysql", "sqlite", "json", "chrono", "ipnetwork", "runtime-tokio-native-tls", "any"], optional = true } sqlx = { git = "https://github.com/zert3x/sqlx", branch="feature/skip", features = ["mysql", "sqlite", "json", "chrono", "ipnetwork", "runtime-tokio-native-tls", "any"], optional = true }
thiserror = "1.0.40" thiserror = "1.0.43"
jsonwebtoken = "8.3.0" jsonwebtoken = "8.3.0"
log = "0.4.19"
async-trait = "0.1.71"
chorus-macros = {path = "chorus-macros"}
[dev-dependencies] [dev-dependencies]
tokio = {version = "1.29.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

@ -56,9 +56,9 @@ accepted, if it violates these guidelines or [our Code of Conduct](https://githu
- [x] Channel creation - [x] Channel creation
- [x] Channel deletion - [x] Channel deletion
- [x] [Channel management (name, description, icon, etc.)](https://github.com/polyphony-chat/chorus/issues/48) - [x] [Channel management (name, description, icon, etc.)](https://github.com/polyphony-chat/chorus/issues/48)
- [ ] [Join and Leave Guilds](https://github.com/polyphony-chat/chorus/issues/45) - [x] [Join and Leave Guilds](https://github.com/polyphony-chat/chorus/issues/45)
- [ ] [Start DMs](https://github.com/polyphony-chat/chorus/issues/45) - [x] [Start DMs](https://github.com/polyphony-chat/chorus/issues/45)
- [ ] [Group DM creation, deletion and member management](https://github.com/polyphony-chat/chorus/issues/89) - [x] [Group DM creation, deletion and member management](https://github.com/polyphony-chat/chorus/issues/89)
- [ ] [Deleting messages](https://github.com/polyphony-chat/chorus/issues/91) - [ ] [Deleting messages](https://github.com/polyphony-chat/chorus/issues/91)
- [ ] [Message threads](https://github.com/polyphony-chat/chorus/issues/90) - [ ] [Message threads](https://github.com/polyphony-chat/chorus/issues/90)
- [x] [Reactions](https://github.com/polyphony-chat/chorus/issues/85) - [x] [Reactions](https://github.com/polyphony-chat/chorus/issues/85)

46
chorus-macros/Cargo.lock generated Normal file
View File

@ -0,0 +1,46 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "chorus-macros"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"

11
chorus-macros/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "chorus-macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
quote = "1"
syn = "2"

18
chorus-macros/src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(Updateable)]
pub fn updateable_macro_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
// No need for macro hygiene, we're only using this in chorus
quote! {
impl Updateable for #name {
fn id(&self) -> Snowflake {
self.id
}
}
}
.into()
}

View File

@ -1,3 +1,4 @@
use async_trait::async_trait;
use chorus::{ use chorus::{
self, self,
gateway::{Gateway, Observer}, gateway::{Gateway, Observer},
@ -15,9 +16,10 @@ pub struct ExampleObserver {}
// This struct can observe GatewayReady events when subscribed, because it implements the trait Observer<GatewayReady>. // This struct can observe GatewayReady events when subscribed, because it implements the trait Observer<GatewayReady>.
// The Observer trait can be implemented for a struct for a given websocketevent to handle observing it // The Observer trait can be implemented for a struct for a given websocketevent to handle observing it
// One struct can be an observer of multiple websocketevents, if needed // One struct can be an observer of multiple websocketevents, if needed
#[async_trait]
impl Observer<GatewayReady> for ExampleObserver { impl Observer<GatewayReady> for ExampleObserver {
// After we subscribe to an event this function is called every time we receive it // After we subscribe to an event this function is called every time we receive it
fn update(&self, _data: &GatewayReady) { async fn update(&self, _data: &GatewayReady) {
println!("Observed Ready!"); println!("Observed Ready!");
} }
} }

View File

@ -2,64 +2,48 @@ 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::gateway::Gateway;
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::{GatewayIdentifyPayload, 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 =
let response = LimitedRequester::send_request( UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await;
request_builder, let login_result = chorus_request
LimitType::AuthRegister, .deserialize_response::<LoginResult>(&mut shell)
self, .await?;
&mut cloned_limits, let object = self.get_user(login_result.token.clone(), None).await?;
) if self.limits_information.is_some() {
.await; self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap();
if response.is_err() {
return Err(ChorusLibError::NoResponse);
} }
let mut identify = GatewayIdentifyPayload::common();
let response_unwrap = response.unwrap(); let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap();
let status = response_unwrap.status(); identify.token = login_result.token.clone();
let response_text_string = response_unwrap.text().await.unwrap(); gateway.send_identify(identify).await;
if status.is_client_error() {
let json: ErrorResponse = serde_json::from_str(&response_text_string).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_string).unwrap();
let object = self
.get_user(login_result.token.clone(), None)
.await
.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,
gateway,
); );
Ok(user) Ok(user)
} }
} }

View File

@ -1,14 +1,16 @@
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::gateway::Gateway;
use crate::types::GatewayIdentifyPayload;
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,51 +27,38 @@ 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 =
let response = LimitedRequester::send_request( UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await;
request_builder, let token = chorus_request
LimitType::AuthRegister, .deserialize_response::<Token>(&mut shell)
self, .await?
&mut cloned_limits, .token;
) if self.limits_information.is_some() {
.await; self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap();
if response.is_err() {
return Err(ChorusLibError::NoResponse);
}
let response_unwrap = response.unwrap();
let status = response_unwrap.status();
let response_unwrap_text = response_unwrap.text().await.unwrap();
let token = from_str::<Token>(&response_unwrap_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) let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?;
.await let mut identify = GatewayIdentifyPayload::common();
.unwrap(); let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap();
identify.token = token.clone();
gateway.send_identify(identify).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,
gateway,
); );
Ok(user) Ok(user)
} }

View File

@ -1,33 +1,25 @@
use reqwest::Client; use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::types::AddChannelRecipientSchema;
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 +36,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.
@ -70,43 +64,94 @@ impl Channel {
/// ///
/// A `Result` that contains a `Channel` object if the request was successful, or an `ChorusLibError` if an error occurred during the request. /// A `Result` that contains a `Channel` object if the request was successful, or an `ChorusLibError` if an error occurred during the request.
pub async fn modify( pub async fn modify(
&mut self, &self,
modify_data: ChannelModifySchema, modify_data: ChannelModifySchema,
channel_id: Snowflake, channel_id: Snowflake,
user: &mut UserMeta, user: &mut UserMeta,
) -> ChorusResult<()> { ) -> ChorusResult<Channel> {
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, chorus_request.deserialize_response::<Channel>(user).await
)
.await?;
let _ = std::mem::replace(self, new_channel);
Ok(())
} }
pub async fn messages( pub async fn messages(
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
}
/// # Reference:
/// Read: <https://discord-userdoccers.vercel.app/resources/channel#add-channel-recipient>
pub async fn add_channel_recipient(
&self,
recipient_id: Snowflake,
user: &mut UserMeta,
add_channel_recipient_schema: Option<AddChannelRecipientSchema>,
) -> ChorusResult<()> {
let mut request = Client::new()
.put(format!(
"{}/channels/{}/recipients/{}/",
user.belongs_to.borrow().urls.api,
self.id,
recipient_id
))
.bearer_auth(user.token());
if let Some(schema) = add_channel_recipient_schema {
request = request.body(to_string(&schema).unwrap());
}
ChorusRequest {
request,
limit_type: LimitType::Channel(self.id),
}
.handle_request_as_result(user)
.await
}
/// # Reference:
/// Read: <https://discord-userdoccers.vercel.app/resources/channel#remove-channel-recipient>
pub async fn remove_channel_recipient(
&self,
recipient_id: Snowflake,
user: &mut UserMeta,
) -> ChorusResult<()> {
let request = Client::new()
.delete(format!(
"{}/channels/{}/recipients/{}/",
user.belongs_to.borrow().urls.api,
self.id,
recipient_id
))
.bearer_auth(user.token());
ChorusRequest {
request,
limit_type: LimitType::Channel(self.id),
}
.handle_request_as_result(user)
.await
} }
} }

View File

@ -3,48 +3,39 @@ 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::types::{Message, MessageSendSchema, PartialDiscordFileAttachment, Snowflake}; use crate::ratelimiter::ChorusRequest;
use crate::types::{Message, MessageSendSchema, Snowflake};
impl Message { impl Message {
/**
Sends a message to the Spacebar server.
# Arguments
* `url_api` - The URL of the Spacebar server's API.
* `message` - The [`Message`] that will be sent to the Spacebar server.
* `limits_user` - The [`Limits`] of the user.
* `limits_instance` - The [`Limits`] of the instance.
* `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server.
# Errors
* [`ChorusLibError`] - If the message cannot be sent.
*/
pub async fn send( pub async fn send(
user: &mut UserMeta, user: &mut UserMeta,
channel_id: Snowflake, channel_id: Snowflake,
message: &mut MessageSendSchema, mut message: MessageSendSchema,
files: Option<Vec<PartialDiscordFileAttachment>>, ) -> Result<Message, crate::errors::ChorusError> {
) -> Result<Message, crate::errors::ChorusLibError> {
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 message.attachments.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);
} }
let mut form = reqwest::multipart::Form::new(); let mut form = reqwest::multipart::Form::new();
let payload_json = to_string(message).unwrap(); let payload_json = to_string(&message).unwrap();
let payload_field = reqwest::multipart::Part::text(payload_json); let payload_field = reqwest::multipart::Part::text(payload_json);
form = form.part("payload_json", payload_field); form = form.part("payload_json", payload_field);
for (index, attachment) in files.unwrap().into_iter().enumerate() { for (index, attachment) in message.attachments.unwrap().into_iter().enumerate() {
let (attachment_content, current_attachment) = attachment.move_content(); let (attachment_content, current_attachment) = attachment.move_content();
let (attachment_filename, _) = current_attachment.move_filename(); let (attachment_filename, _) = current_attachment.move_filename();
let part_name = format!("files[{}]", index); let part_name = format!("files[{}]", index);
@ -62,36 +53,24 @@ 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
} }
} }
} }
impl UserMeta { impl UserMeta {
/// Shorthand call for Message::send()
/**
Sends a message to the Spacebar server.
# Arguments
* `url_api` - The URL of the Spacebar server's API.
* `message` - The [`Message`] that will be sent to the Spacebar server.
* `limits_user` - The [`Limits`] of the user.
* `limits_instance` - The [`Limits`] of the instance.
* `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server.
# Errors
* [`ChorusLibError`] - If the message cannot be sent.
*/
pub async fn send_message( pub async fn send_message(
&mut self, &mut self,
message: &mut MessageSendSchema, message: MessageSendSchema,
channel_id: Snowflake, channel_id: Snowflake,
files: Option<Vec<PartialDiscordFileAttachment>>, ) -> Result<Message, crate::errors::ChorusError> {
) -> Result<Message, crate::errors::ChorusLibError> { Message::send(self, channel_id, message).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,10 +1,11 @@
use reqwest::Client; use reqwest::Client;
use crate::{ use crate::{
api::handle_request_as_result, api::LimitType,
errors::ChorusResult, errors::ChorusResult,
instance::UserMeta, instance::UserMeta,
types::{self, Snowflake}, ratelimiter::ChorusRequest,
types::{self, PublicUser, Snowflake},
}; };
/** /**
@ -16,20 +17,15 @@ pub struct ReactionMeta {
} }
impl ReactionMeta { impl ReactionMeta {
/** /// Deletes all reactions for a message.
Deletes all reactions for a message. /// This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user.
This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user. /// # Arguments
/// * `user` - A mutable reference to a [`UserMeta`] instance.
# Arguments /// # Returns
* `user` - A mutable reference to a [`UserMeta`] instance. /// A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong.
/// Fires a `Message Reaction Remove All` Gateway event.
# Returns /// # Reference
A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong. /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions](https://discord.com/developers/docs/resources/channel#delete-all-reactions)
Fires a `Message Reaction Remove All` Gateway event.
# Reference
See [https://discord.com/developers/docs/resources/channel#delete-all-reactions](https://discord.com/developers/docs/resources/channel#delete-all-reactions)
*/
pub async fn delete_all(&self, user: &mut UserMeta) -> ChorusResult<()> { pub async fn delete_all(&self, user: &mut UserMeta) -> ChorusResult<()> {
let url = format!( let url = format!(
"{}/channels/{}/messages/{}/reactions/", "{}/channels/{}/messages/{}/reactions/",
@ -37,26 +33,24 @@ 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. /// # Arguments
/// * `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or
# Arguments /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
* `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or /// format name:id with the emoji name and emoji id.
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the /// * `user` - A mutable reference to a [`UserMeta`] instance.
format name:id with the emoji name and emoji id. /// # Returns
* `user` - A mutable reference to a [`UserMeta`] instance. /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
/// # Reference
# Returns /// See [https://discord.com/developers/docs/resources/channel#get-reactions](https://discord.com/developers/docs/resources/channel#get-reactions)
A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<Vec<PublicUser>> {
# Reference
See [https://discord.com/developers/docs/resources/channel#get-reactions](https://discord.com/developers/docs/resources/channel#get-reactions)
*/
pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> {
let url = format!( let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/", "{}/channels/{}/messages/{}/reactions/{}/",
user.belongs_to.borrow().urls.api, user.belongs_to.borrow().urls.api,
@ -64,27 +58,27 @@ impl ReactionMeta {
self.message_id, self.message_id,
emoji emoji
); );
let request = Client::new().get(url).bearer_auth(user.token()); let chorus_request = ChorusRequest {
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await request: Client::new().get(url).bearer_auth(user.token()),
limit_type: LimitType::Channel(self.channel_id),
};
chorus_request
.deserialize_response::<Vec<PublicUser>>(user)
.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 /// MANAGE_MESSAGES permission to be present on the current user.
MANAGE_MESSAGES permission to be present on the current user. /// # Arguments
/// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
# Arguments /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
* `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or /// format name:id with the emoji name and emoji id.
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the /// * `user` - A mutable reference to a [`UserMeta`] instance.
format name:id with the emoji name and emoji id. /// # Returns
* `user` - A mutable reference to a [`UserMeta`] instance. /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
/// Fires a `Message Reaction Remove Emoji` Gateway event.
# Returns /// # Reference
A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji](https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji)
Fires a `Message Reaction Remove Emoji` Gateway event.
# Reference
See [https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji](https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji)
*/
pub async fn delete_emoji(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { pub async fn delete_emoji(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> {
let url = format!( let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/", "{}/channels/{}/messages/{}/reactions/{}/",
@ -93,29 +87,28 @@ 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. /// This endpoint requires the READ_MESSAGE_HISTORY permission
/// to be present on the current user. Additionally, if nobody else has reacted to the message using
This endpoint requires the READ_MESSAGE_HISTORY permission /// this emoji, this endpoint requires the ADD_REACTIONS permission to be present on the current
to be present on the current user. Additionally, if nobody else has reacted to the message using /// user.
this emoji, this endpoint requires the ADD_REACTIONS permission to be present on the current /// # Arguments
user. /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
# Arguments /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
* `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or /// format name:id with the emoji name and emoji id.
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the /// * `user` - A mutable reference to a [`UserMeta`] instance.
format name:id with the emoji name and emoji id. /// # Returns
* `user` - A mutable reference to a [`UserMeta`] instance. /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`].
/// # Reference
# Returns /// See [https://discord.com/developers/docs/resources/channel#create-reaction](https://discord.com/developers/docs/resources/channel#create-reaction)
A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. ///
# Reference
See [https://discord.com/developers/docs/resources/channel#create-reaction](https://discord.com/developers/docs/resources/channel#create-reaction)
*/
pub async fn create(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { pub async fn create(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> {
let url = format!( let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/@me/", "{}/channels/{}/messages/{}/reactions/{}/@me/",
@ -124,26 +117,24 @@ 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. /// # Arguments
/// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
# Arguments /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
* `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or /// format name:id with the emoji name and emoji id.
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the /// * `user` - A mutable reference to a [`UserMeta`] instance.
format name:id with the emoji name and emoji id. /// # Returns
* `user` - A mutable reference to a [`UserMeta`] instance. /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`].
/// Fires a `Message Reaction Remove` Gateway event.
# Returns /// # Reference
A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction)
Fires a `Message Reaction Remove` Gateway event.
# Reference
See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction)
*/
pub async fn remove(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { pub async fn remove(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> {
let url = format!( let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/@me/", "{}/channels/{}/messages/{}/reactions/{}/@me/",
@ -152,29 +143,26 @@ 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. /// This endpoint requires the MANAGE_MESSAGES permission to be present on the current user.
/// # Arguments
This endpoint requires the MANAGE_MESSAGES permission to be present on the current user. /// * `user_id` - ID of the user whose reaction is to be deleted.
/// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
# Arguments /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
* `user_id` - ID of the user whose reaction is to be deleted. /// format name:id with the emoji name and emoji id.
* `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or /// * `user` - A mutable reference to a [`UserMeta`] instance.
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the /// # Returns
format name:id with the emoji name and emoji id. /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`].
* `user` - A mutable reference to a [`UserMeta`] instance. /// Fires a Message Reaction Remove Gateway event.
/// # Reference
# Returns /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction)
A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`].
Fires a Message Reaction Remove Gateway event.
# Reference
See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction)
*/
pub async fn delete_user( pub async fn delete_user(
&self, &self,
user_id: Snowflake, user_id: Snowflake,
@ -189,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
} }
} }

73
src/api/invites/mod.rs Normal file
View File

@ -0,0 +1,73 @@
use reqwest::Client;
use serde_json::to_string;
use crate::errors::ChorusResult;
use crate::instance::UserMeta;
use crate::ratelimiter::ChorusRequest;
use crate::types::{CreateChannelInviteSchema, GuildInvite, Invite, Snowflake};
impl UserMeta {
/// # Arguments
/// - invite_code: The invite code to accept the invite for.
/// - session_id: The session ID that is accepting the invite, required for guest invites.
///
/// # Reference:
/// Read <https://discord-userdoccers.vercel.app/resources/invite#accept-invite>
pub async fn accept_invite(
&mut self,
invite_code: &str,
session_id: Option<&str>,
) -> ChorusResult<Invite> {
let mut request = ChorusRequest {
request: Client::new()
.post(format!(
"{}/invites/{}/",
self.belongs_to.borrow().urls.api,
invite_code
))
.bearer_auth(self.token()),
limit_type: super::LimitType::Global,
};
if session_id.is_some() {
request.request = request
.request
.body(to_string(session_id.unwrap()).unwrap());
}
request.deserialize_response::<Invite>(self).await
}
/// Note: Spacebar does not yet implement this endpoint.
pub async fn create_user_invite(&mut self, code: Option<&str>) -> ChorusResult<Invite> {
ChorusRequest {
request: Client::new()
.post(format!(
"{}/users/@me/invites/",
self.belongs_to.borrow().urls.api
))
.body(to_string(&code).unwrap())
.bearer_auth(self.token()),
limit_type: super::LimitType::Global,
}
.deserialize_response::<Invite>(self)
.await
}
pub async fn create_guild_invite(
&mut self,
create_channel_invite_schema: CreateChannelInviteSchema,
channel_id: Snowflake,
) -> ChorusResult<GuildInvite> {
ChorusRequest {
request: Client::new()
.post(format!(
"{}/channels/{}/invites/",
self.belongs_to.borrow().urls.api,
channel_id
))
.bearer_auth(self.token())
.body(to_string(&create_channel_invite_schema).unwrap()),
limit_type: super::LimitType::Channel(channel_id),
}
.deserialize_response::<GuildInvite>(self)
.await
}
}

View File

@ -1,12 +1,13 @@
pub use channels::messages::*; pub use channels::messages::*;
pub use common::*;
pub use guilds::*; pub use guilds::*;
pub use invites::*;
pub use policies::instance::instance::*; pub use policies::instance::instance::*;
pub use policies::instance::limits::*; pub use policies::instance::ratelimits::*;
pub use users::*;
pub mod auth; pub mod auth;
pub mod channels; pub mod channels;
pub mod common;
pub mod guilds; pub mod guilds;
pub mod invites;
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,37 @@
use std::hash::Hash;
use serde::{Deserialize, Serialize};
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, Serialize, Deserialize)]
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, Serialize, Deserialize)]
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;

32
src/api/users/channels.rs Normal file
View File

@ -0,0 +1,32 @@
use reqwest::Client;
use serde_json::to_string;
use crate::{
api::LimitType,
errors::ChorusResult,
instance::UserMeta,
ratelimiter::ChorusRequest,
types::{Channel, PrivateChannelCreateSchema},
};
impl UserMeta {
/// Creates a DM channel or group DM channel.
///
/// # Reference:
/// Read <https://discord-userdoccers.vercel.app/resources/channel#create-private-channel>
pub async fn create_private_channel(
&mut self,
create_private_channel_schema: PrivateChannelCreateSchema,
) -> ChorusResult<Channel> {
let url = format!("{}/users/@me/channels", self.belongs_to.borrow().urls.api);
ChorusRequest {
request: Client::new()
.post(url)
.bearer_auth(self.token())
.body(to_string(&create_private_channel_schema).unwrap()),
limit_type: LimitType::Global,
}
.deserialize_response::<Channel>(self)
.await
}
}

30
src/api/users/guilds.rs Normal file
View File

@ -0,0 +1,30 @@
use reqwest::Client;
use serde_json::to_string;
use crate::errors::ChorusResult;
use crate::instance::UserMeta;
use crate::ratelimiter::ChorusRequest;
use crate::types::Snowflake;
impl UserMeta {
/// # Arguments:
/// - lurking: Whether the user is lurking in the guild
///
/// # Reference:
/// Read <https://discord-userdoccers.vercel.app/resources/guild#leave-guild>
pub async fn leave_guild(&mut self, guild_id: &Snowflake, lurking: bool) -> ChorusResult<()> {
ChorusRequest {
request: Client::new()
.delete(format!(
"{}/users/@me/guilds/{}/",
self.belongs_to.borrow().urls.api,
guild_id
))
.bearer_auth(self.token())
.body(to_string(&lurking).unwrap()),
limit_type: crate::api::LimitType::Guild(*guild_id),
}
.handle_request_as_result(self)
.await
}
}

View File

@ -1,5 +1,9 @@
pub use channels::*;
pub use guilds::*;
pub use relationships::*; pub use relationships::*;
pub use users::*; pub use users::*;
pub mod channels;
pub mod guilds;
pub mod relationships; pub mod relationships;
pub mod users; pub mod users;

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,21 @@ 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 =
match LimitedRequester::send_request( UserMeta::shell(Rc::new(RefCell::new(instance.clone())), token.clone()).await;
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 +152,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).await;
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,27 +56,27 @@ custom_error! {
#[derive(Clone, PartialEq, Eq)] #[derive(Clone, 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}",
} }
custom_error! { custom_error! {

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,37 @@
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::gateway::{Gateway, GatewayHandle};
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, Serialize, Deserialize)]
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 +39,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,32 +89,14 @@ 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,
pub gateway: GatewayHandle,
} }
impl UserMeta { impl UserMeta {
@ -99,9 +111,10 @@ 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,
gateway: GatewayHandle,
) -> UserMeta { ) -> UserMeta {
UserMeta { UserMeta {
belongs_to, belongs_to,
@ -109,6 +122,32 @@ impl UserMeta {
limits, limits,
settings, settings,
object, object,
gateway,
}
}
/// 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. To use the [`GatewayHandle`], you will have to identify
/// first.
pub(crate) async fn shell(instance: Rc<RefCell<Instance>>, token: String) -> UserMeta {
let settings = UserSettings::default();
let object = User::default();
let wss_url = instance.borrow().urls.wss.clone();
// Dummy gateway object
let gateway = Gateway::new(wss_url).await.unwrap();
UserMeta {
token,
belongs_to: instance.clone(),
limits: instance
.borrow()
.limits_information
.as_ref()
.map(|info| info.ratelimits.clone()),
settings,
object,
gateway,
} }
} }
} }

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();
}
}

466
src/ratelimiter.rs Normal file
View File

@ -0,0 +1,466 @@
use std::collections::HashMap;
use log::{self, debug};
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) => {
debug!("Request successful: {:?}", 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?;
debug!("Got response: {:?}", response);
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 requested type T: {}. JSON Response: {}",
e, response_text
),
})
}
};
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

@ -10,7 +10,7 @@ use sqlx::{
database::{HasArguments, HasValueRef}, database::{HasArguments, HasValueRef},
encode::IsNull, encode::IsNull,
error::BoxDynError, error::BoxDynError,
Decode, Encode, MySql, Decode, MySql,
}; };
use crate::types::config::types::subconfigs::guild::{ use crate::types::config::types::subconfigs::guild::{
@ -139,7 +139,7 @@ pub enum GuildFeatures {
InvitesClosed, InvitesClosed,
} }
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq)]
pub struct GuildFeaturesList(Vec<GuildFeatures>); pub struct GuildFeaturesList(Vec<GuildFeatures>);
impl Deref for GuildFeaturesList { impl Deref for GuildFeaturesList {

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,68 +1,70 @@
use chorus_macros::Updateable;
use chrono::Utc; use chrono::Utc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_string_from_number; use serde_aux::prelude::deserialize_string_from_number;
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::gateway::Updateable;
use crate::types::{ use crate::types::{
entities::{GuildMember, User}, entities::{GuildMember, User},
utils::Snowflake, utils::Snowflake,
}; };
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Updateable)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Channel { pub struct Channel {
pub id: Snowflake,
pub created_at: Option<chrono::DateTime<Utc>>,
#[serde(rename = "type")]
pub channel_type: ChannelType,
pub guild_id: Option<Snowflake>,
pub position: Option<i32>,
#[cfg(feature = "sqlx")]
pub permission_overwrites: Option<sqlx::types::Json<Vec<PermissionOverwrite>>>,
#[cfg(not(feature = "sqlx"))]
pub permission_overwrites: Option<Vec<PermissionOverwrite>>,
pub name: Option<String>,
pub topic: Option<String>,
pub nsfw: Option<bool>,
pub last_message_id: Option<Snowflake>,
pub bitrate: Option<i32>,
pub user_limit: Option<i32>,
pub rate_limit_per_user: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub recipients: Option<Vec<User>>,
pub icon: Option<String>,
pub owner_id: Option<Snowflake>,
pub application_id: Option<Snowflake>, pub application_id: Option<Snowflake>,
pub managed: Option<bool>,
pub parent_id: Option<Snowflake>,
pub last_pin_timestamp: Option<String>,
pub rtc_region: Option<String>,
pub video_quality_mode: Option<i32>,
pub message_count: Option<i32>,
pub member_count: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub thread_metadata: Option<ThreadMetadata>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub member: Option<ThreadMember>,
pub default_auto_archive_duration: Option<i32>,
pub permissions: Option<String>,
pub flags: Option<i32>,
pub total_message_sent: Option<i32>,
#[cfg(feature = "sqlx")]
pub available_tags: Option<sqlx::types::Json<Vec<Tag>>>,
#[cfg(not(feature = "sqlx"))]
pub available_tags: Option<Vec<Tag>>,
#[cfg(feature = "sqlx")] #[cfg(feature = "sqlx")]
pub applied_tags: Option<sqlx::types::Json<Vec<String>>>, pub applied_tags: Option<sqlx::types::Json<Vec<String>>>,
#[cfg(not(feature = "sqlx"))] #[cfg(not(feature = "sqlx"))]
pub applied_tags: Option<Vec<String>>, pub applied_tags: Option<Vec<String>>,
#[cfg(feature = "sqlx")] #[cfg(feature = "sqlx")]
pub available_tags: Option<sqlx::types::Json<Vec<Tag>>>,
#[cfg(not(feature = "sqlx"))]
pub available_tags: Option<Vec<Tag>>,
pub bitrate: Option<i32>,
#[serde(rename = "type")]
pub channel_type: ChannelType,
pub created_at: Option<chrono::DateTime<Utc>>,
pub default_auto_archive_duration: Option<i32>,
pub default_forum_layout: Option<i32>,
#[cfg(feature = "sqlx")]
pub default_reaction_emoji: Option<sqlx::types::Json<DefaultReaction>>, pub default_reaction_emoji: Option<sqlx::types::Json<DefaultReaction>>,
#[cfg(not(feature = "sqlx"))] #[cfg(not(feature = "sqlx"))]
pub default_reaction_emoji: Option<DefaultReaction>, pub default_reaction_emoji: Option<DefaultReaction>,
pub default_thread_rate_limit_per_user: Option<i32>,
pub default_sort_order: Option<i32>, pub default_sort_order: Option<i32>,
pub default_forum_layout: Option<i32>, pub default_thread_rate_limit_per_user: Option<i32>,
pub flags: Option<i32>,
pub guild_id: Option<Snowflake>,
pub icon: Option<String>,
pub id: Snowflake,
pub last_message_id: Option<Snowflake>,
pub last_pin_timestamp: Option<String>,
pub managed: Option<bool>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub member: Option<ThreadMember>,
pub member_count: Option<i32>,
pub message_count: Option<i32>,
pub name: Option<String>,
pub nsfw: Option<bool>,
pub owner_id: Option<Snowflake>,
pub parent_id: Option<Snowflake>,
#[cfg(feature = "sqlx")]
pub permission_overwrites: Option<sqlx::types::Json<Vec<PermissionOverwrite>>>,
#[cfg(not(feature = "sqlx"))]
pub permission_overwrites: Option<Vec<PermissionOverwrite>>,
pub permissions: Option<String>,
pub position: Option<i32>,
pub rate_limit_per_user: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub recipients: Option<Vec<User>>,
pub rtc_region: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub thread_metadata: Option<ThreadMetadata>,
pub topic: Option<String>,
pub total_message_sent: Option<i32>,
pub user_limit: Option<i32>,
pub video_quality_mode: Option<i32>,
} }
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
@ -74,7 +76,7 @@ pub struct Tag {
pub emoji_name: Option<String>, pub emoji_name: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd)]
pub struct PermissionOverwrite { pub struct PermissionOverwrite {
pub id: Snowflake, pub id: Snowflake,
#[serde(rename = "type")] #[serde(rename = "type")]

View File

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::User; use crate::types::entities::User;
use crate::types::Snowflake; use crate::types::Snowflake;
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Emoji { pub struct Emoji {
pub id: Option<Snowflake>, pub id: Option<Snowflake>,

View File

@ -91,7 +91,7 @@ pub struct Guild {
} }
/// See https://docs.spacebar.chat/routes/#get-/guilds/-guild_id-/bans/-user- /// See https://docs.spacebar.chat/routes/#get-/guilds/-guild_id-/bans/-user-
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct GuildBan { pub struct GuildBan {
pub user_id: Snowflake, pub user_id: Snowflake,

View File

@ -0,0 +1,75 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::{Snowflake, WelcomeScreenObject};
use super::guild::GuildScheduledEvent;
use super::{Application, Channel, GuildMember, User};
/// Represents a code that when used, adds a user to a guild or group DM channel, or creates a relationship between two users.
/// See <https://discord-userdoccers.vercel.app/resources/invite#invite-object>
#[derive(Debug, Serialize, Deserialize)]
pub struct Invite {
pub approximate_member_count: Option<i32>,
pub approximate_presence_count: Option<i32>,
pub channel: Option<Channel>,
pub code: String,
pub created_at: Option<DateTime<Utc>>,
pub expires_at: Option<DateTime<Utc>>,
pub flags: Option<i32>,
pub guild: Option<InviteGuild>,
pub guild_id: Option<Snowflake>,
pub guild_scheduled_event: Option<GuildScheduledEvent>,
#[serde(rename = "type")]
pub invite_type: Option<i32>,
pub inviter: Option<User>,
pub max_age: Option<i32>,
pub max_uses: Option<i32>,
pub stage_instance: Option<InviteStageInstance>,
pub target_application: Option<Application>,
pub target_type: Option<i32>,
pub target_user: Option<User>,
pub temporary: Option<bool>,
pub uses: Option<i32>,
}
/// The guild an invite is for.
/// See <https://discord-userdoccers.vercel.app/resources/invite#invite-guild-object>
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteGuild {
pub id: Snowflake,
pub name: String,
pub icon: Option<String>,
pub splash: Option<String>,
pub verification_level: i32,
pub features: Vec<String>,
pub vanity_url_code: Option<String>,
pub description: Option<String>,
pub banner: Option<String>,
pub premium_subscription_count: Option<i32>,
#[serde(rename = "nsfw")]
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw_deprecated: Option<bool>,
pub nsfw_level: NSFWLevel,
pub welcome_screen: Option<WelcomeScreenObject>,
}
/// See <https://discord-userdoccers.vercel.app/resources/guild#nsfw-level> for an explanation on what
/// the levels mean.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum NSFWLevel {
Default = 0,
Explicit = 1,
Safe = 2,
AgeRestricted = 3,
}
/// See <https://discord-userdoccers.vercel.app/resources/invite#invite-stage-instance-object>
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteStageInstance {
pub members: Vec<GuildMember>,
pub participant_count: i32,
pub speaker_count: i32,
pub topic: String,
}

View File

@ -8,6 +8,7 @@ pub use emoji::*;
pub use guild::*; pub use guild::*;
pub use guild_member::*; pub use guild_member::*;
pub use integration::*; pub use integration::*;
pub use invite::*;
pub use message::*; pub use message::*;
pub use relationship::*; pub use relationship::*;
pub use role::*; pub use role::*;
@ -31,6 +32,7 @@ mod emoji;
mod guild; mod guild;
mod guild_member; mod guild_member;
mod integration; mod integration;
mod invite;
mod message; mod message;
mod relationship; mod relationship;
mod role; mod role;

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 {
@ -45,9 +43,9 @@ pub struct User {
pub bio: Option<String>, pub bio: Option<String>,
pub theme_colors: Option<Vec<u8>>, pub theme_colors: Option<Vec<u8>>,
pub phone: Option<String>, pub phone: Option<String>,
pub nsfw_allowed: bool, pub nsfw_allowed: Option<bool>,
pub premium: bool, pub premium: Option<bool>,
pub purchased_flags: i32, pub purchased_flags: Option<i32>,
pub premium_usage_flags: Option<i32>, pub premium_usage_flags: Option<i32>,
pub disabled: Option<bool>, pub disabled: Option<bool>,
} }
@ -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

@ -3,6 +3,8 @@ use crate::types::{entities::Channel, Snowflake};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::UpdateMessage;
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
/// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update /// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update
pub struct ChannelPinsUpdate { pub struct ChannelPinsUpdate {
@ -31,6 +33,15 @@ pub struct ChannelUpdate {
impl WebSocketEvent for ChannelUpdate {} impl WebSocketEvent for ChannelUpdate {}
impl UpdateMessage<Channel> for ChannelUpdate {
fn update(&self, object_to_update: &mut Channel) {
*object_to_update = self.channel.clone();
}
fn id(&self) -> Snowflake {
self.channel.id
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone)] #[derive(Debug, Default, Deserialize, Serialize, Clone)]
/// Officially undocumented. /// Officially undocumented.
/// Sends updates to client about a new message with its id /// Sends updates to client about a new message with its id

View File

@ -14,8 +14,7 @@ impl WebSocketEvent for GatewayHello {}
/// Contains info on how often the client should send heartbeats to the server; /// Contains info on how often the client should send heartbeats to the server;
pub struct HelloData { pub struct HelloData {
/// How often a client should send heartbeats, in milliseconds /// How often a client should send heartbeats, in milliseconds
// u128 because std used u128s for milliseconds pub heartbeat_interval: u64,
pub heartbeat_interval: u128,
} }
impl WebSocketEvent for HelloData {} impl WebSocketEvent for HelloData {}

View File

@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::gateway::Updateable;
pub use application::*; pub use application::*;
pub use auto_moderation::*; pub use auto_moderation::*;
pub use call::*; pub use call::*;
@ -25,9 +26,10 @@ pub use thread::*;
pub use user::*; pub use user::*;
pub use voice::*; pub use voice::*;
pub use webhooks::*; pub use webhooks::*;
pub use webrtc::*; pub use webrtc::*;
use super::Snowflake;
mod application; mod application;
mod auto_moderation; mod auto_moderation;
mod call; mod call;
@ -99,3 +101,23 @@ pub struct GatewayReceivePayload<'a> {
} }
impl<'a> WebSocketEvent for GatewayReceivePayload<'a> {} impl<'a> WebSocketEvent for GatewayReceivePayload<'a> {}
/// An [`UpdateMessage<T>`] represents a received Gateway Message which contains updated
/// information for an [`Updateable`] of Type T.
/// # Example:
/// ```rs
/// impl UpdateMessage<Channel> for ChannelUpdate {
/// fn update(...) {...}
/// fn id(...) {...}
/// }
/// ```
/// This would imply, that the [`WebSocketEvent`] "[`ChannelUpdate`]" contains new/updated information
/// about a [`Channel`]. The update method describes how this new information will be turned into
/// a [`Channel`] object.
pub(crate) trait UpdateMessage<T>: Clone
where
T: Updateable,
{
fn update(&self, object_to_update: &mut T);
fn id(&self) -> Snowflake;
}

View File

@ -1,122 +1,8 @@
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::FieldFormatError; #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
/**
A struct that represents a well-formed email address.
*/
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AuthEmail {
pub email: String,
}
impl AuthEmail {
/**
Returns a new [`Result<AuthEmail, FieldFormatError>`].
## Arguments
The email address you want to validate.
## Errors
You will receive a [`FieldFormatError`], if:
- The email address is not in a valid format.
*/
pub fn new(email: String) -> Result<AuthEmail, FieldFormatError> {
let regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
if !regex.is_match(email.as_str()) {
return Err(FieldFormatError::EmailError);
}
Ok(AuthEmail { email })
}
}
/**
A struct that represents a well-formed username.
## Arguments
Please use new() to create a new instance of this struct.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is not between 2 and 32 characters.
*/
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AuthUsername {
pub username: String,
}
impl AuthUsername {
/**
Returns a new [`Result<AuthUsername, FieldFormatError>`].
## Arguments
The username you want to validate.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is not between 2 and 32 characters.
*/
pub fn new(username: String) -> Result<AuthUsername, FieldFormatError> {
if username.len() < 2 || username.len() > 32 {
Err(FieldFormatError::UsernameError)
} else {
Ok(AuthUsername { username })
}
}
}
/**
A struct that represents a well-formed password.
## Arguments
Please use new() to create a new instance of this struct.
## Errors
You will receive a [`FieldFormatError`], if:
- The password is not between 1 and 72 characters.
*/
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AuthPassword {
pub password: String,
}
impl AuthPassword {
/**
Returns a new [`Result<AuthPassword, FieldFormatError>`].
## Arguments
The password you want to validate.
## Errors
You will receive a [`FieldFormatError`], if:
- The password is not between 1 and 72 characters.
*/
pub fn new(password: String) -> Result<AuthPassword, FieldFormatError> {
if password.is_empty() || password.len() > 72 {
Err(FieldFormatError::PasswordError)
} else {
Ok(AuthPassword { password })
}
}
}
/**
A struct that represents a well-formed register request.
## Arguments
Please use new() to create a new instance of this struct.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is not between 2 and 32 characters.
- The password is not between 1 and 72 characters.
*/
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct RegisterSchema { pub struct RegisterSchema {
username: String,
password: Option<String>,
consent: bool,
email: Option<String>,
fingerprint: Option<String>,
invite: Option<String>,
date_of_birth: Option<String>,
gift_code_sku_id: Option<String>,
captcha_key: Option<String>,
promotional_email_opt_in: Option<bool>,
}
pub struct RegisterSchemaOptions {
pub username: String, pub username: String,
pub password: Option<String>, pub password: Option<String>,
pub consent: bool, pub consent: bool,
@ -129,123 +15,21 @@ pub struct RegisterSchemaOptions {
pub promotional_email_opt_in: Option<bool>, pub promotional_email_opt_in: Option<bool>,
} }
impl RegisterSchema {
pub fn builder(username: impl Into<String>, consent: bool) -> RegisterSchemaOptions {
RegisterSchemaOptions {
username: username.into(),
password: None,
consent,
email: None,
fingerprint: None,
invite: None,
date_of_birth: None,
gift_code_sku_id: None,
captcha_key: None,
promotional_email_opt_in: None,
}
}
}
impl RegisterSchemaOptions {
/**
Create a new [`RegisterSchema`].
## Arguments
All but "String::username" and "bool::consent" are optional.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is less than 2 or more than 32 characters in length
- You supply a `password` which is less than 1 or more than 72 characters in length.
These constraints have been defined [in the Spacebar-API](https://docs.spacebar.chat/routes/)
*/
pub fn build(self) -> Result<RegisterSchema, FieldFormatError> {
let username = AuthUsername::new(self.username)?.username;
let email = if let Some(email) = self.email {
Some(AuthEmail::new(email)?.email)
} else {
None
};
let password = if let Some(password) = self.password {
Some(AuthPassword::new(password)?.password)
} else {
None
};
if !self.consent {
return Err(FieldFormatError::ConsentError);
}
Ok(RegisterSchema {
username,
password,
consent: self.consent,
email,
fingerprint: self.fingerprint,
invite: self.invite,
date_of_birth: self.date_of_birth,
gift_code_sku_id: self.gift_code_sku_id,
captcha_key: self.captcha_key,
promotional_email_opt_in: self.promotional_email_opt_in,
})
}
}
/**
A struct that represents a well-formed login request.
## Arguments
Please use new() to create a new instance of this struct.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is not between 2 and 32 characters.
- The password is not between 1 and 72 characters.
*/
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct LoginSchema { pub struct LoginSchema {
/// For Discord, usernames must be between 2 and 32 characters,
/// but other servers may have different limits.
pub login: String, pub login: String,
pub password: Option<String>, /// For Discord, must be between 1 and 72 characters,
/// but other servers may have different limits.
pub password: String,
pub undelete: Option<bool>, pub undelete: Option<bool>,
pub captcha_key: Option<String>, pub captcha_key: Option<String>,
pub login_source: Option<String>, pub login_source: Option<String>,
pub gift_code_sku_id: Option<String>, pub gift_code_sku_id: Option<String>,
} }
impl LoginSchema {
/**
Returns a new [`Result<LoginSchema, FieldFormatError>`].
## Arguments
login: The username you want to login with.
password: The password you want to login with.
undelete: Honestly no idea what this is for.
captcha_key: The captcha key you want to login with.
login_source: The login source.
gift_code_sku_id: The gift code sku id.
## Errors
You will receive a [`FieldFormatError`], if:
- The username is less than 2 or more than 32 characters in length
*/
pub fn new(
login: String,
password: Option<String>,
undelete: Option<bool>,
captcha_key: Option<String>,
login_source: Option<String>,
gift_code_sku_id: Option<String>,
) -> Result<LoginSchema, FieldFormatError> {
Ok(LoginSchema {
login,
password,
undelete,
captcha_key,
login_source,
gift_code_sku_id,
})
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct TotpSchema { pub struct TotpSchema {

View File

@ -1,8 +1,9 @@
use bitflags::bitflags;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::{entities::PermissionOverwrite, Snowflake}; use crate::types::{entities::PermissionOverwrite, Snowflake};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize, Default, PartialEq, PartialOrd)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct ChannelCreateSchema { pub struct ChannelCreateSchema {
pub name: String, pub name: String,
@ -26,7 +27,7 @@ pub struct ChannelCreateSchema {
pub video_quality_mode: Option<i32>, pub video_quality_mode: Option<i32>,
} }
#[derive(Debug, Deserialize, Serialize, Clone, Default)] #[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, PartialOrd)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct ChannelModifySchema { pub struct ChannelModifySchema {
pub name: Option<String>, pub name: Option<String>,
@ -48,7 +49,7 @@ pub struct ChannelModifySchema {
pub video_quality_mode: Option<i32>, pub video_quality_mode: Option<i32>,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
pub struct GetChannelMessagesSchema { pub struct GetChannelMessagesSchema {
/// Between 1 and 100, defaults to 50. /// Between 1 and 100, defaults to 50.
pub limit: Option<i32>, pub limit: Option<i32>,
@ -56,7 +57,7 @@ pub struct GetChannelMessagesSchema {
pub anchor: ChannelMessagesAnchor, pub anchor: ChannelMessagesAnchor,
} }
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ChannelMessagesAnchor { pub enum ChannelMessagesAnchor {
Before(Snowflake), Before(Snowflake),
@ -94,3 +95,56 @@ impl GetChannelMessagesSchema {
} }
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)]
pub struct CreateChannelInviteSchema {
pub flags: Option<InviteFlags>,
pub max_age: Option<u32>,
pub max_uses: Option<u8>,
pub temporary: Option<bool>,
pub unique: Option<bool>,
pub validate: Option<String>,
pub target_type: Option<InviteType>,
pub target_user_id: Option<Snowflake>,
pub target_application_id: Option<Snowflake>,
}
impl Default for CreateChannelInviteSchema {
fn default() -> Self {
Self {
flags: None,
max_age: Some(86400),
max_uses: Some(0),
temporary: Some(false),
unique: Some(false),
validate: None,
target_type: None,
target_user_id: None,
target_application_id: None,
}
}
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
pub struct InviteFlags: u64 {
const GUEST = 1 << 0;
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum InviteType {
#[default]
Stream = 1,
EmbeddedApplication = 2,
RoleSubscriptions = 3,
CreatorPage = 4,
}
/// See <https://discord-userdoccers.vercel.app/resources/channel#add-channel-recipient>
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialOrd, Ord, PartialEq, Eq)]
pub struct AddChannelRecipientSchema {
pub access_token: Option<String>,
pub nick: Option<String>,
}

View File

@ -15,76 +15,3 @@ mod message;
mod relationship; mod relationship;
mod role; mod role;
mod user; mod user;
#[cfg(test)]
mod schemas_tests {
use crate::errors::FieldFormatError;
use super::*;
#[test]
fn password_too_short() {
assert_eq!(
AuthPassword::new("".to_string()),
Err(FieldFormatError::PasswordError)
);
}
#[test]
fn password_too_long() {
let mut long_pw = String::new();
for _ in 0..73 {
long_pw += "a";
}
assert_eq!(
AuthPassword::new(long_pw),
Err(FieldFormatError::PasswordError)
);
}
#[test]
fn username_too_short() {
assert_eq!(
AuthUsername::new("T".to_string()),
Err(FieldFormatError::UsernameError)
);
}
#[test]
fn username_too_long() {
let mut long_un = String::new();
for _ in 0..33 {
long_un += "a";
}
assert_eq!(
AuthUsername::new(long_un),
Err(FieldFormatError::UsernameError)
);
}
#[test]
fn consent_false() {
assert_eq!(
RegisterSchema::builder("Test", false).build(),
Err(FieldFormatError::ConsentError)
);
}
#[test]
fn invalid_email() {
assert_eq!(
AuthEmail::new("p@p.p".to_string()),
Err(FieldFormatError::EmailError)
)
}
#[test]
fn valid_email() {
let reg = RegisterSchemaOptions {
email: Some("me@mail.de".to_string()),
..RegisterSchema::builder("Testy", true)
}
.build();
assert_ne!(reg, Err(FieldFormatError::EmailError));
}
}

View File

@ -1,5 +1,9 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::types::Snowflake;
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct UserModifySchema { pub struct UserModifySchema {
@ -14,3 +18,17 @@ pub struct UserModifySchema {
pub email: Option<String>, pub email: Option<String>,
pub discriminator: Option<i16>, pub discriminator: Option<i16>,
} }
/// # Attributes:
/// - recipients: The users to include in the private channel
/// - access_tokens: The access tokens of users that have granted your app the `gdm.join` scope. Only usable for OAuth2 requests (which can only create group DMs).
/// - nicks: A mapping of user IDs to their respective nicknames. Only usable for OAuth2 requests (which can only create group DMs).
///
/// # Reference:
/// Read: <https://discord-userdoccers.vercel.app/resources/channel#json-params>
#[derive(Debug, Deserialize, Serialize)]
pub struct PrivateChannelCreateSchema {
pub recipients: Option<Vec<Snowflake>>,
pub access_tokens: Option<Vec<String>>,
pub nicks: Option<HashMap<Snowflake, String>>,
}

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, PartialOrd, Ord)]
#[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

@ -1,16 +1,16 @@
use chorus::types::{RegisterSchema, RegisterSchemaOptions}; use chorus::types::RegisterSchema;
mod common; mod common;
#[tokio::test] #[tokio::test]
async fn test_registration() { async fn test_registration() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let reg = RegisterSchemaOptions { let reg = RegisterSchema {
username: "Hiiii".into(),
date_of_birth: Some("2000-01-01".to_string()), date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("Hiiii", true) consent: true,
} ..Default::default()
.build() };
.unwrap();
bundle.instance.register_account(&reg).await.unwrap(); bundle.instance.register_account(&reg).await.unwrap();
common::teardown(bundle).await; common::teardown(bundle).await;
} }

View File

@ -1,6 +1,6 @@
use chorus::types::{ use chorus::types::{
self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags,
PermissionOverwrite, Snowflake, PermissionOverwrite, PrivateChannelCreateSchema, RelationshipType, Snowflake,
}; };
mod common; mod common;
@ -28,10 +28,11 @@ async fn delete_channel() {
#[tokio::test] #[tokio::test]
async fn modify_channel() { async fn modify_channel() {
const CHANNEL_NAME: &str = "beepboop";
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let channel = &mut bundle.channel; let channel = &mut bundle.channel;
let modify_data: types::ChannelModifySchema = types::ChannelModifySchema { let modify_data: types::ChannelModifySchema = types::ChannelModifySchema {
name: Some("beepboop".to_string()), name: Some(CHANNEL_NAME.to_string()),
channel_type: None, channel_type: None,
topic: None, topic: None,
icon: None, icon: None,
@ -49,10 +50,10 @@ async fn modify_channel() {
default_thread_rate_limit_per_user: None, default_thread_rate_limit_per_user: None,
video_quality_mode: None, video_quality_mode: None,
}; };
Channel::modify(channel, modify_data, channel.id, &mut bundle.user) let modified_channel = Channel::modify(channel, modify_data, channel.id, &mut bundle.user)
.await .await
.unwrap(); .unwrap();
assert_eq!(channel.name, Some("beepboop".to_string())); assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string()));
let permission_override = PermissionFlags::from_vec(Vec::from([ let permission_override = PermissionFlags::from_vec(Vec::from([
PermissionFlags::MANAGE_CHANNELS, PermissionFlags::MANAGE_CHANNELS,
@ -89,12 +90,11 @@ async fn get_channel_messages() {
let _ = bundle let _ = bundle
.user .user
.send_message( .send_message(
&mut MessageSendSchema { MessageSendSchema {
content: Some("A Message!".to_string()), content: Some("A Message!".to_string()),
..Default::default() ..Default::default()
}, },
bundle.channel.id, bundle.channel.id,
None,
) )
.await .await
.unwrap(); .unwrap();
@ -136,3 +136,81 @@ async fn get_channel_messages() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test]
async fn create_dm() {
let mut bundle = common::setup().await;
let other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let private_channel_create_schema = PrivateChannelCreateSchema {
recipients: Some(Vec::from([other_user.object.id])),
access_tokens: None,
nicks: None,
};
let dm_channel = user
.create_private_channel(private_channel_create_schema)
.await
.unwrap();
assert!(dm_channel.recipients.is_some());
assert_eq!(
dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id,
other_user.object.id
);
assert_eq!(
dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id,
user.object.id
);
common::teardown(bundle).await;
}
// #[tokio::test]
// This test currently is broken due to an issue with the Spacebar Server.
#[allow(dead_code)]
async fn remove_add_person_from_to_dm() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let mut third_user = bundle.create_user("integrationtestuser3").await;
let user = &mut bundle.user;
let private_channel_create_schema = PrivateChannelCreateSchema {
recipients: Some(Vec::from([other_user.object.id, third_user.object.id])),
access_tokens: None,
nicks: None,
};
let dm_channel = user
.create_private_channel(private_channel_create_schema)
.await
.unwrap(); // Creates the Channel and stores the response Channel object
dm_channel
.remove_channel_recipient(other_user.object.id, user)
.await
.unwrap();
assert!(dm_channel.recipients.as_ref().unwrap().get(1).is_none());
other_user
.modify_user_relationship(user.object.id, RelationshipType::Friends)
.await
.unwrap();
user.modify_user_relationship(other_user.object.id, RelationshipType::Friends)
.await
.unwrap();
third_user
.modify_user_relationship(user.object.id, RelationshipType::Friends)
.await
.unwrap();
user.modify_user_relationship(third_user.object.id, RelationshipType::Friends)
.await
.unwrap();
// Users 1-2 and 1-3 are now friends
dm_channel
.add_channel_recipient(other_user.object.id, user, None)
.await
.unwrap();
assert!(dm_channel.recipients.is_some());
assert_eq!(
dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id,
other_user.object.id
);
assert_eq!(
dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id,
user.object.id
);
}

View File

@ -1,15 +1,16 @@
use chorus::gateway::Gateway;
use chorus::{ use chorus::{
errors::ChorusResult,
instance::{Instance, UserMeta}, instance::{Instance, UserMeta},
types::{ types::{
Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema, Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema,
RegisterSchemaOptions, RoleCreateModifySchema, RoleObject, RoleCreateModifySchema, RoleObject,
}, },
UrlBundle, UrlBundle,
}; };
#[allow(dead_code)]
#[derive(Debug)] #[derive(Debug)]
pub struct TestBundle { pub(crate) struct TestBundle {
pub urls: UrlBundle, pub urls: UrlBundle,
pub user: UserMeta, pub user: UserMeta,
pub instance: Instance, pub instance: Instance,
@ -18,21 +19,47 @@ pub struct TestBundle {
pub channel: Channel, pub channel: Channel,
} }
#[allow(unused)]
impl TestBundle {
pub(crate) async fn create_user(&mut self, username: &str) -> UserMeta {
let register_schema = RegisterSchema {
username: username.to_string(),
consent: true,
date_of_birth: Some("2000-01-01".to_string()),
..Default::default()
};
self.instance
.register_account(&register_schema)
.await
.unwrap()
}
pub(crate) async fn clone_user_without_gateway(&self) -> UserMeta {
UserMeta {
belongs_to: self.user.belongs_to.clone(),
token: self.user.token.clone(),
limits: self.user.limits.clone(),
settings: self.user.settings.clone(),
object: self.user.object.clone(),
gateway: Gateway::new(self.instance.urls.wss.clone()).await.unwrap(),
}
}
}
// Set up a test by creating an Instance and a User. Reduces Test boilerplate. // Set up a test by creating an Instance and a User. Reduces Test boilerplate.
pub async fn setup() -> TestBundle { pub(crate) async fn setup() -> TestBundle {
let urls = UrlBundle::new( let urls = UrlBundle::new(
"http://localhost:3001/api".to_string(), "http://localhost:3001/api".to_string(),
"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 = RegisterSchemaOptions { let reg = RegisterSchema {
username: "integrationtestuser".into(),
consent: true,
date_of_birth: Some("2000-01-01".to_string()), date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser", true) ..Default::default()
} };
.build()
.unwrap();
let guild_create_schema = GuildCreateSchema { let guild_create_schema = GuildCreateSchema {
name: Some("Test-Guild!".to_string()), name: Some("Test-Guild!".to_string()),
region: None, region: None,
@ -94,7 +121,7 @@ pub async fn setup() -> TestBundle {
// Teardown method to clean up after a test. // Teardown method to clean up after a test.
#[allow(dead_code)] #[allow(dead_code)]
pub async fn teardown(mut bundle: TestBundle) { pub(crate) async fn teardown(mut bundle: TestBundle) {
Guild::delete(&mut bundle.user, bundle.guild.id) Guild::delete(&mut bundle.user, bundle.guild.id)
.await .await
.unwrap(); .unwrap();

View File

@ -1,13 +1,15 @@
mod common; mod common;
use chorus::gateway::*; use chorus::gateway::*;
use chorus::types; use chorus::types::{self, Channel};
#[tokio::test] #[tokio::test]
/// Tests establishing a connection (hello and heartbeats) on the local gateway; /// Tests establishing a connection (hello and heartbeats) on the local gateway;
async fn test_gateway_establish() { async fn test_gateway_establish() {
let bundle = common::setup().await; let bundle = common::setup().await;
Gateway::new(bundle.urls.wss).await.unwrap(); Gateway::new(bundle.urls.wss.clone()).await.unwrap();
common::teardown(bundle).await
} }
#[tokio::test] #[tokio::test]
@ -15,10 +17,30 @@ async fn test_gateway_establish() {
async fn test_gateway_authenticate() { async fn test_gateway_authenticate() {
let bundle = common::setup().await; let bundle = common::setup().await;
let gateway = Gateway::new(bundle.urls.wss).await.unwrap(); let gateway = Gateway::new(bundle.urls.wss.clone()).await.unwrap();
let mut identify = types::GatewayIdentifyPayload::common(); let mut identify = types::GatewayIdentifyPayload::common();
identify.token = bundle.user.token; identify.token = bundle.user.token.clone();
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
common::teardown(bundle).await
}
#[tokio::test]
async fn test_self_updating_structs() {
let mut bundle = common::setup().await;
let channel_updater = bundle.user.gateway.observe(bundle.channel.clone()).await;
let received_channel = channel_updater.borrow().clone();
assert_eq!(received_channel, bundle.channel);
let channel = &mut bundle.channel;
let modify_data = types::ChannelModifySchema {
name: Some("beepboop".to_string()),
..Default::default()
};
Channel::modify(channel, modify_data, channel.id, &mut bundle.user)
.await
.unwrap();
let received_channel = channel_updater.borrow();
assert_eq!(received_channel.name.as_ref().unwrap(), "beepboop");
common::teardown(bundle).await
} }

23
tests/invites.rs Normal file
View File

@ -0,0 +1,23 @@
mod common;
use chorus::types::CreateChannelInviteSchema;
#[tokio::test]
async fn create_accept_invite() {
let mut bundle = common::setup().await;
let channel = bundle.channel.clone();
let mut other_user = bundle.create_user("testuser1312").await;
let user = &mut bundle.user;
let create_channel_invite_schema = CreateChannelInviteSchema::default();
assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user)
.await
.is_err());
let invite = user
.create_guild_invite(create_channel_invite_schema, channel.id)
.await
.unwrap();
other_user.accept_invite(&invite.code, None).await.unwrap();
assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user)
.await
.is_ok());
common::teardown(bundle).await;
}

View File

@ -8,13 +8,13 @@ mod common;
#[tokio::test] #[tokio::test]
async fn send_message() { async fn send_message() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let mut message = types::MessageSendSchema { let message = types::MessageSendSchema {
content: Some("A Message!".to_string()), content: Some("A Message!".to_string()),
..Default::default() ..Default::default()
}; };
let _ = bundle let _ = bundle
.user .user
.send_message(&mut message, bundle.channel.id, None) .send_message(message, bundle.channel.id)
.await .await
.unwrap(); .unwrap();
common::teardown(bundle).await common::teardown(bundle).await
@ -45,7 +45,7 @@ async fn send_message_attachment() {
content: buffer, content: buffer,
}; };
let mut message = types::MessageSendSchema { let message = types::MessageSendSchema {
content: Some("trans rights now".to_string()), content: Some("trans rights now".to_string()),
attachments: Some(vec![attachment.clone()]), attachments: Some(vec![attachment.clone()]),
..Default::default() ..Default::default()
@ -55,11 +55,7 @@ async fn send_message_attachment() {
let _arg = Some(&vec_attach); let _arg = Some(&vec_attach);
bundle bundle
.user .user
.send_message( .send_message(message, bundle.channel.id)
&mut message,
bundle.channel.id,
Some(vec![attachment.clone()]),
)
.await .await
.unwrap(); .unwrap();
common::teardown(bundle).await common::teardown(bundle).await

View File

@ -1,25 +1,17 @@
use chorus::types::{self, RegisterSchema, RegisterSchemaOptions, Relationship, RelationshipType}; use chorus::types::{self, Relationship, RelationshipType};
mod common; mod common;
#[tokio::test] #[tokio::test]
async fn test_get_mutual_relationships() { async fn test_get_mutual_relationships() {
let register_schema = RegisterSchemaOptions {
date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser2", true)
}
.build()
.unwrap();
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let belongs_to = &mut bundle.instance; let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
let friend_request_schema = types::FriendRequestSendSchema { let friend_request_schema = types::FriendRequestSendSchema {
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
@ -30,22 +22,17 @@ async fn test_get_mutual_relationships() {
#[tokio::test] #[tokio::test]
async fn test_get_relationships() { async fn test_get_relationships() {
let register_schema = RegisterSchemaOptions {
date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser2", true)
}
.build()
.unwrap();
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let belongs_to = &mut bundle.instance; let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
let friend_request_schema = types::FriendRequestSendSchema { let friend_request_schema = types::FriendRequestSendSchema {
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
@ -53,18 +40,10 @@ async fn test_get_relationships() {
#[tokio::test] #[tokio::test]
async fn test_modify_relationship_friends() { async fn test_modify_relationship_friends() {
let register_schema = RegisterSchemaOptions {
date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser2", true)
}
.build()
.unwrap();
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let belongs_to = &mut bundle.instance; let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap(); let _ = other_user
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 +58,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 +71,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()
@ -101,18 +81,10 @@ async fn test_modify_relationship_friends() {
#[tokio::test] #[tokio::test]
async fn test_modify_relationship_block() { async fn test_modify_relationship_block() {
let register_schema = RegisterSchemaOptions {
date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser2", true)
}
.build()
.unwrap();
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let belongs_to = &mut bundle.instance; let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user; let user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap(); let _ = other_user
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 +95,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()