Merge with main
This commit is contained in:
parent
32a42dae87
commit
942fecbec7
|
@ -16,19 +16,21 @@ jobs:
|
|||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
- name: Clone spacebar server
|
||||
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
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
cache: 'npm'
|
||||
cache-dependency-path: server/package-lock.json
|
||||
- name: Prepare and start Spacebar server
|
||||
run: |
|
||||
npm install
|
||||
npm run setup
|
||||
npm run start &
|
||||
working-directory: ./server
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -2,19 +2,18 @@
|
|||
# will have compiled files and executables
|
||||
/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
|
||||
**/*.rs.bk
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
||||
###
|
||||
# IDE specific folders and configs
|
||||
|
||||
.vscode/**
|
||||
.idea/**
|
||||
|
||||
# macOS
|
||||
|
||||
**/.DS_Store
|
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
|
@ -10,31 +10,35 @@ backend = ["poem", "sqlx"]
|
|||
client = []
|
||||
|
||||
[dependencies]
|
||||
tokio = {version = "1.28.1", features = ["rt", "macros", "rt-multi-thread", "full"]}
|
||||
serde = {version = "1.0.163", features = ["derive"]}
|
||||
serde_json = {version= "1.0.96", features = ["raw_value"]}
|
||||
tokio = {version = "1.29.1", features = ["macros"]}
|
||||
serde = {version = "1.0.171", features = ["derive"]}
|
||||
serde_json = {version= "1.0.103", features = ["raw_value"]}
|
||||
serde-aux = "4.2.0"
|
||||
serde_with = "3.0.0"
|
||||
serde_repr = "0.1.12"
|
||||
reqwest = {version = "0.11.16", features = ["multipart"]}
|
||||
url = "2.3.1"
|
||||
chrono = {version = "0.4.24", features = ["serde"]}
|
||||
regex = "1.7.3"
|
||||
serde_repr = "0.1.14"
|
||||
reqwest = {version = "0.11.18", features = ["multipart"]}
|
||||
url = "2.4.0"
|
||||
chrono = {version = "0.4.26", features = ["serde"]}
|
||||
regex = "1.9.1"
|
||||
custom_error = "1.9.2"
|
||||
native-tls = "0.2.11"
|
||||
tokio-tungstenite = {version = "0.19.0", features = ["native-tls"]}
|
||||
futures-util = "0.3.28"
|
||||
http = "0.2.9"
|
||||
openssl = "0.10.52"
|
||||
openssl = "0.10.55"
|
||||
base64 = "0.21.2"
|
||||
hostname = "0.3.1"
|
||||
bitflags = { version = "2.2.1", features = ["serde"] }
|
||||
bitflags = { version = "2.3.3", features = ["serde"] }
|
||||
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 }
|
||||
thiserror = "1.0.40"
|
||||
thiserror = "1.0.43"
|
||||
jsonwebtoken = "8.3.0"
|
||||
log = "0.4.19"
|
||||
async-trait = "0.1.71"
|
||||
chorus-macros = {path = "chorus-macros"}
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = {version = "1.29.1", features = ["full"]}
|
||||
lazy_static = "1.4.0"
|
||||
rusty-hook = "0.11.2"
|
|
@ -56,9 +56,9 @@ accepted, if it violates these guidelines or [our Code of Conduct](https://githu
|
|||
- [x] Channel creation
|
||||
- [x] Channel deletion
|
||||
- [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)
|
||||
- [ ] [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] [Join and Leave Guilds](https://github.com/polyphony-chat/chorus/issues/45)
|
||||
- [x] [Start DMs](https://github.com/polyphony-chat/chorus/issues/45)
|
||||
- [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)
|
||||
- [ ] [Message threads](https://github.com/polyphony-chat/chorus/issues/90)
|
||||
- [x] [Reactions](https://github.com/polyphony-chat/chorus/issues/85)
|
||||
|
|
|
@ -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"
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "chorus-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
quote = "1"
|
||||
syn = "2"
|
|
@ -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()
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use async_trait::async_trait;
|
||||
use chorus::{
|
||||
self,
|
||||
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>.
|
||||
// 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
|
||||
#[async_trait]
|
||||
impl Observer<GatewayReady> for ExampleObserver {
|
||||
// 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!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,64 +2,48 @@ use std::cell::RefCell;
|
|||
use std::rc::Rc;
|
||||
|
||||
use reqwest::Client;
|
||||
use serde_json::{from_str, json};
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::api::limits::LimitType;
|
||||
use crate::errors::{ChorusLibError, ChorusResult};
|
||||
use crate::api::LimitType;
|
||||
use crate::errors::ChorusResult;
|
||||
use crate::gateway::Gateway;
|
||||
use crate::instance::{Instance, UserMeta};
|
||||
use crate::limit::LimitedRequester;
|
||||
use crate::types::{ErrorResponse, LoginResult, LoginSchema};
|
||||
use crate::ratelimiter::ChorusRequest;
|
||||
use crate::types::{GatewayIdentifyPayload, LoginResult, LoginSchema};
|
||||
|
||||
impl Instance {
|
||||
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 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
|
||||
// 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.
|
||||
let mut cloned_limits = self.limits.clone();
|
||||
let response = LimitedRequester::send_request(
|
||||
request_builder,
|
||||
LimitType::AuthRegister,
|
||||
self,
|
||||
&mut cloned_limits,
|
||||
)
|
||||
.await;
|
||||
if response.is_err() {
|
||||
return Err(ChorusLibError::NoResponse);
|
||||
let mut shell =
|
||||
UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await;
|
||||
let login_result = chorus_request
|
||||
.deserialize_response::<LoginResult>(&mut shell)
|
||||
.await?;
|
||||
let object = self.get_user(login_result.token.clone(), None).await?;
|
||||
if self.limits_information.is_some() {
|
||||
self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap();
|
||||
}
|
||||
|
||||
let response_unwrap = response.unwrap();
|
||||
let status = response_unwrap.status();
|
||||
let response_text_string = response_unwrap.text().await.unwrap();
|
||||
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 mut identify = GatewayIdentifyPayload::common();
|
||||
let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap();
|
||||
identify.token = login_result.token.clone();
|
||||
gateway.send_identify(identify).await;
|
||||
let user = UserMeta::new(
|
||||
Rc::new(RefCell::new(self.clone())),
|
||||
login_result.token,
|
||||
cloned_limits,
|
||||
self.clone_limits_if_some(),
|
||||
login_result.settings,
|
||||
object,
|
||||
gateway,
|
||||
);
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use reqwest::Client;
|
||||
use serde_json::{from_str, json};
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::gateway::Gateway;
|
||||
use crate::types::GatewayIdentifyPayload;
|
||||
use crate::{
|
||||
api::limits::LimitType,
|
||||
errors::{ChorusLibError, ChorusResult},
|
||||
api::policies::instance::LimitType,
|
||||
errors::ChorusResult,
|
||||
instance::{Instance, Token, UserMeta},
|
||||
limit::LimitedRequester,
|
||||
types::{ErrorResponse, RegisterSchema},
|
||||
ratelimiter::ChorusRequest,
|
||||
types::RegisterSchema,
|
||||
};
|
||||
|
||||
impl Instance {
|
||||
|
@ -25,51 +27,38 @@ impl Instance {
|
|||
&mut self,
|
||||
register_schema: &RegisterSchema,
|
||||
) -> ChorusResult<UserMeta> {
|
||||
let json_schema = json!(register_schema);
|
||||
let client = Client::new();
|
||||
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
|
||||
// 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.
|
||||
let mut cloned_limits = self.limits.clone();
|
||||
let response = LimitedRequester::send_request(
|
||||
request_builder,
|
||||
LimitType::AuthRegister,
|
||||
self,
|
||||
&mut cloned_limits,
|
||||
)
|
||||
.await;
|
||||
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 mut shell =
|
||||
UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await;
|
||||
let token = chorus_request
|
||||
.deserialize_response::<Token>(&mut shell)
|
||||
.await?
|
||||
.token;
|
||||
if self.limits_information.is_some() {
|
||||
self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap();
|
||||
}
|
||||
let user_object = self.get_user(token.clone(), None).await.unwrap();
|
||||
let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self)
|
||||
.await
|
||||
.unwrap();
|
||||
let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?;
|
||||
let mut identify = GatewayIdentifyPayload::common();
|
||||
let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap();
|
||||
identify.token = token.clone();
|
||||
gateway.send_identify(identify).await;
|
||||
let user = UserMeta::new(
|
||||
Rc::new(RefCell::new(self.clone())),
|
||||
token.clone(),
|
||||
cloned_limits,
|
||||
self.clone_limits_if_some(),
|
||||
settings,
|
||||
user_object,
|
||||
gateway,
|
||||
);
|
||||
Ok(user)
|
||||
}
|
||||
|
|
|
@ -1,33 +1,25 @@
|
|||
use reqwest::Client;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::types::AddChannelRecipientSchema;
|
||||
use crate::{
|
||||
api::common,
|
||||
errors::{ChorusLibError, ChorusResult},
|
||||
api::LimitType,
|
||||
errors::{ChorusError, ChorusResult},
|
||||
instance::UserMeta,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{Channel, ChannelModifySchema, GetChannelMessagesSchema, Message, Snowflake},
|
||||
};
|
||||
|
||||
impl Channel {
|
||||
pub async fn get(user: &mut UserMeta, channel_id: Snowflake) -> ChorusResult<Channel> {
|
||||
let url = user.belongs_to.borrow_mut().urls.api.clone();
|
||||
let request = Client::new()
|
||||
.get(format!("{}/channels/{}/", url, channel_id))
|
||||
.bearer_auth(user.token());
|
||||
|
||||
let result = common::deserialize_response::<Channel>(
|
||||
request,
|
||||
user,
|
||||
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())
|
||||
let url = user.belongs_to.borrow().urls.api.clone();
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.get(format!("{}/channels/{}/", url, channel_id))
|
||||
.bearer_auth(user.token()),
|
||||
limit_type: LimitType::Channel(channel_id),
|
||||
};
|
||||
chorus_request.deserialize_response::<Channel>(user).await
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn delete(self, user: &mut UserMeta) -> ChorusResult<()> {
|
||||
let request = Client::new()
|
||||
.delete(format!(
|
||||
"{}/channels/{}/",
|
||||
user.belongs_to.borrow_mut().urls.api,
|
||||
self.id
|
||||
))
|
||||
.bearer_auth(user.token());
|
||||
common::handle_request_as_result(request, user, crate::api::limits::LimitType::Channel)
|
||||
.await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.delete(format!(
|
||||
"{}/channels/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
self.id
|
||||
))
|
||||
.bearer_auth(user.token()),
|
||||
limit_type: LimitType::Channel(self.id),
|
||||
};
|
||||
chorus_request.handle_request_as_result(user).await
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn modify(
|
||||
&mut self,
|
||||
&self,
|
||||
modify_data: ChannelModifySchema,
|
||||
channel_id: Snowflake,
|
||||
user: &mut UserMeta,
|
||||
) -> ChorusResult<()> {
|
||||
let request = Client::new()
|
||||
.patch(format!(
|
||||
"{}/channels/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
channel_id
|
||||
))
|
||||
.bearer_auth(user.token())
|
||||
.body(to_string(&modify_data).unwrap());
|
||||
let new_channel = common::deserialize_response::<Channel>(
|
||||
request,
|
||||
user,
|
||||
crate::api::limits::LimitType::Channel,
|
||||
)
|
||||
.await?;
|
||||
let _ = std::mem::replace(self, new_channel);
|
||||
Ok(())
|
||||
) -> ChorusResult<Channel> {
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.patch(format!(
|
||||
"{}/channels/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
channel_id
|
||||
))
|
||||
.bearer_auth(user.token())
|
||||
.body(to_string(&modify_data).unwrap()),
|
||||
limit_type: LimitType::Channel(channel_id),
|
||||
};
|
||||
chorus_request.deserialize_response::<Channel>(user).await
|
||||
}
|
||||
|
||||
pub async fn messages(
|
||||
range: GetChannelMessagesSchema,
|
||||
channel_id: Snowflake,
|
||||
user: &mut UserMeta,
|
||||
) -> Result<Vec<Message>, ChorusLibError> {
|
||||
let request = Client::new()
|
||||
.get(format!(
|
||||
"{}/channels/{}/messages",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
channel_id
|
||||
))
|
||||
.bearer_auth(user.token())
|
||||
.query(&range);
|
||||
) -> Result<Vec<Message>, ChorusError> {
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.get(format!(
|
||||
"{}/channels/{}/messages",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
channel_id
|
||||
))
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,48 +3,39 @@ use http::HeaderMap;
|
|||
use reqwest::{multipart, Client};
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::api::deserialize_response;
|
||||
use crate::api::LimitType;
|
||||
use crate::instance::UserMeta;
|
||||
use crate::types::{Message, MessageSendSchema, PartialDiscordFileAttachment, Snowflake};
|
||||
use crate::ratelimiter::ChorusRequest;
|
||||
use crate::types::{Message, MessageSendSchema, Snowflake};
|
||||
|
||||
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(
|
||||
user: &mut UserMeta,
|
||||
channel_id: Snowflake,
|
||||
message: &mut MessageSendSchema,
|
||||
files: Option<Vec<PartialDiscordFileAttachment>>,
|
||||
) -> Result<Message, crate::errors::ChorusLibError> {
|
||||
mut message: MessageSendSchema,
|
||||
) -> Result<Message, crate::errors::ChorusError> {
|
||||
let url_api = user.belongs_to.borrow().urls.api.clone();
|
||||
|
||||
if files.is_none() {
|
||||
let request = Client::new()
|
||||
.post(format!("{}/channels/{}/messages/", url_api, channel_id))
|
||||
.bearer_auth(user.token())
|
||||
.body(to_string(message).unwrap());
|
||||
deserialize_response::<Message>(request, user, crate::api::limits::LimitType::Channel)
|
||||
.await
|
||||
if message.attachments.is_none() {
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.post(format!("{}/channels/{}/messages/", url_api, channel_id))
|
||||
.bearer_auth(user.token())
|
||||
.body(to_string(&message).unwrap()),
|
||||
limit_type: LimitType::Channel(channel_id),
|
||||
};
|
||||
chorus_request.deserialize_response::<Message>(user).await
|
||||
} else {
|
||||
for (index, attachment) in message.attachments.iter_mut().enumerate() {
|
||||
attachment.get_mut(index).unwrap().set_id(index as i16);
|
||||
}
|
||||
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);
|
||||
|
||||
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_filename, _) = current_attachment.move_filename();
|
||||
let part_name = format!("files[{}]", index);
|
||||
|
@ -62,36 +53,24 @@ impl Message {
|
|||
form = form.part(part_name, part);
|
||||
}
|
||||
|
||||
let request = Client::new()
|
||||
.post(format!("{}/channels/{}/messages/", url_api, channel_id))
|
||||
.bearer_auth(user.token())
|
||||
.multipart(form);
|
||||
|
||||
deserialize_response::<Message>(request, user, crate::api::limits::LimitType::Channel)
|
||||
.await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.post(format!("{}/channels/{}/messages/", url_api, channel_id))
|
||||
.bearer_auth(user.token())
|
||||
.multipart(form),
|
||||
limit_type: LimitType::Channel(channel_id),
|
||||
};
|
||||
chorus_request.deserialize_response::<Message>(user).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
message: &mut MessageSendSchema,
|
||||
message: MessageSendSchema,
|
||||
channel_id: Snowflake,
|
||||
files: Option<Vec<PartialDiscordFileAttachment>>,
|
||||
) -> Result<Message, crate::errors::ChorusLibError> {
|
||||
Message::send(self, channel_id, message, files).await
|
||||
) -> Result<Message, crate::errors::ChorusError> {
|
||||
Message::send(self, channel_id, message).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ use reqwest::Client;
|
|||
use serde_json::to_string;
|
||||
|
||||
use crate::{
|
||||
api::handle_request_as_result,
|
||||
errors::{ChorusLibError, ChorusResult},
|
||||
api::LimitType,
|
||||
errors::{ChorusError, ChorusResult},
|
||||
instance::UserMeta,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{self, PermissionOverwrite, Snowflake},
|
||||
};
|
||||
|
||||
|
@ -25,24 +26,25 @@ impl types::Channel {
|
|||
channel_id: Snowflake,
|
||||
overwrite: PermissionOverwrite,
|
||||
) -> ChorusResult<()> {
|
||||
let url = {
|
||||
format!(
|
||||
"{}/channels/{}/permissions/{}",
|
||||
user.belongs_to.borrow_mut().urls.api,
|
||||
channel_id,
|
||||
overwrite.id
|
||||
)
|
||||
};
|
||||
let url = format!(
|
||||
"{}/channels/{}/permissions/{}",
|
||||
user.belongs_to.borrow_mut().urls.api,
|
||||
channel_id,
|
||||
overwrite.id
|
||||
);
|
||||
let body = match to_string(&overwrite) {
|
||||
Ok(string) => string,
|
||||
Err(e) => {
|
||||
return Err(ChorusLibError::FormCreationError {
|
||||
return Err(ChorusError::FormCreation {
|
||||
error: e.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let request = Client::new().put(url).bearer_auth(user.token()).body(body);
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
@ -67,7 +69,10 @@ impl types::Channel {
|
|||
channel_id,
|
||||
overwrite_id
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().delete(url).bearer_auth(user.token()),
|
||||
limit_type: LimitType::Channel(channel_id),
|
||||
};
|
||||
chorus_request.handle_request_as_result(user).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use reqwest::Client;
|
||||
|
||||
use crate::{
|
||||
api::handle_request_as_result,
|
||||
api::LimitType,
|
||||
errors::ChorusResult,
|
||||
instance::UserMeta,
|
||||
types::{self, Snowflake},
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{self, PublicUser, Snowflake},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -16,20 +17,15 @@ pub struct ReactionMeta {
|
|||
}
|
||||
|
||||
impl ReactionMeta {
|
||||
/**
|
||||
Deletes all reactions for a message.
|
||||
This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user.
|
||||
|
||||
# Arguments
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong.
|
||||
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)
|
||||
*/
|
||||
/// Deletes all reactions for a message.
|
||||
/// This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user.
|
||||
/// # Arguments
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong.
|
||||
/// 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<()> {
|
||||
let url = format!(
|
||||
"{}/channels/{}/messages/{}/reactions/",
|
||||
|
@ -37,26 +33,24 @@ impl ReactionMeta {
|
|||
self.channel_id,
|
||||
self.message_id
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
||||
# Arguments
|
||||
* `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or
|
||||
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
format name:id with the emoji name and emoji id.
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
|
||||
|
||||
# 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<()> {
|
||||
/// 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
|
||||
/// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
/// format name:id with the emoji name and emoji id.
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
|
||||
/// # 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<Vec<PublicUser>> {
|
||||
let url = format!(
|
||||
"{}/channels/{}/messages/{}/reactions/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
|
@ -64,27 +58,27 @@ impl ReactionMeta {
|
|||
self.message_id,
|
||||
emoji
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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
|
||||
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
|
||||
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
format name:id with the emoji name and emoji id.
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
|
||||
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)
|
||||
*/
|
||||
/// 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.
|
||||
/// # Arguments
|
||||
/// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
|
||||
/// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
/// format name:id with the emoji name and emoji id.
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong.
|
||||
/// 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<()> {
|
||||
let url = format!(
|
||||
"{}/channels/{}/messages/{}/reactions/{}/",
|
||||
|
@ -93,29 +87,28 @@ impl ReactionMeta {
|
|||
self.message_id,
|
||||
emoji
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
||||
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 emoji, this endpoint requires the ADD_REACTIONS 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
|
||||
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
format name:id with the emoji name and emoji id.
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
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)
|
||||
*/
|
||||
/// 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 emoji, this endpoint requires the ADD_REACTIONS 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
|
||||
/// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
/// format name:id with the emoji name and emoji id.
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// 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<()> {
|
||||
let url = format!(
|
||||
"{}/channels/{}/messages/{}/reactions/{}/@me/",
|
||||
|
@ -124,26 +117,24 @@ impl ReactionMeta {
|
|||
self.message_id,
|
||||
emoji
|
||||
);
|
||||
let request = Client::new().put(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
||||
# Arguments
|
||||
* `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or
|
||||
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
format name:id with the emoji name and emoji id.
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
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)
|
||||
*/
|
||||
/// 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
|
||||
/// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
/// format name:id with the emoji name and emoji id.
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// 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 remove(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> {
|
||||
let url = format!(
|
||||
"{}/channels/{}/messages/{}/reactions/{}/@me/",
|
||||
|
@ -152,29 +143,26 @@ impl ReactionMeta {
|
|||
self.message_id,
|
||||
emoji
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
||||
This endpoint requires the MANAGE_MESSAGES permission to be present on the current user.
|
||||
|
||||
# Arguments
|
||||
* `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
|
||||
the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
format name:id with the emoji name and emoji id.
|
||||
* `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
|
||||
# Returns
|
||||
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)
|
||||
*/
|
||||
/// Delete a user's reaction to a message.
|
||||
/// This endpoint requires the MANAGE_MESSAGES permission to be present on the current user.
|
||||
/// # Arguments
|
||||
/// * `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
|
||||
/// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the
|
||||
/// format name:id with the emoji name and emoji id.
|
||||
/// * `user` - A mutable reference to a [`UserMeta`] instance.
|
||||
/// # Returns
|
||||
/// 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(
|
||||
&self,
|
||||
user_id: Snowflake,
|
||||
|
@ -189,7 +177,10 @@ impl ReactionMeta {
|
|||
emoji,
|
||||
user_id
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().delete(url).bearer_auth(user.token()),
|
||||
limit_type: LimitType::Channel(self.channel_id),
|
||||
};
|
||||
chorus_request.handle_request_as_result(user).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -2,15 +2,11 @@ use reqwest::Client;
|
|||
use serde_json::from_str;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::api::deserialize_response;
|
||||
use crate::api::handle_request;
|
||||
use crate::api::handle_request_as_result;
|
||||
use crate::api::limits::Limits;
|
||||
use crate::errors::ChorusLibError;
|
||||
use crate::api::LimitType;
|
||||
use crate::errors::ChorusError;
|
||||
use crate::errors::ChorusResult;
|
||||
use crate::instance::Instance;
|
||||
use crate::instance::UserMeta;
|
||||
use crate::limit::LimitedRequester;
|
||||
use crate::ratelimiter::ChorusRequest;
|
||||
use crate::types::Snowflake;
|
||||
use crate::types::{Channel, ChannelCreateSchema, Guild, GuildCreateSchema};
|
||||
|
||||
|
@ -36,11 +32,14 @@ impl Guild {
|
|||
guild_create_schema: GuildCreateSchema,
|
||||
) -> ChorusResult<Guild> {
|
||||
let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api);
|
||||
let request = reqwest::Client::new()
|
||||
.post(url.clone())
|
||||
.bearer_auth(user.token.clone())
|
||||
.body(to_string(&guild_create_schema).unwrap());
|
||||
deserialize_response::<Guild>(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.post(url.clone())
|
||||
.bearer_auth(user.token.clone())
|
||||
.body(to_string(&guild_create_schema).unwrap()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request.deserialize_response::<Guild>(user).await
|
||||
}
|
||||
|
||||
/// Deletes a guild.
|
||||
|
@ -73,10 +72,13 @@ impl Guild {
|
|||
user.belongs_to.borrow().urls.api,
|
||||
guild_id
|
||||
);
|
||||
let request = reqwest::Client::new()
|
||||
.post(url.clone())
|
||||
.bearer_auth(user.token.clone());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.post(url.clone())
|
||||
.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.
|
||||
|
@ -97,14 +99,7 @@ impl Guild {
|
|||
user: &mut UserMeta,
|
||||
schema: ChannelCreateSchema,
|
||||
) -> ChorusResult<Channel> {
|
||||
Channel::_create(
|
||||
&user.token,
|
||||
self.id,
|
||||
schema,
|
||||
&mut user.limits,
|
||||
&mut user.belongs_to.borrow_mut(),
|
||||
)
|
||||
.await
|
||||
Channel::create(user, self.id, schema).await
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
pub async fn channels(&self, user: &mut UserMeta) -> ChorusResult<Vec<Channel>> {
|
||||
let request = Client::new()
|
||||
.get(format!(
|
||||
"{}/guilds/{}/channels/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
self.id
|
||||
))
|
||||
.bearer_auth(user.token());
|
||||
let result = handle_request(request, user, crate::api::limits::LimitType::Channel)
|
||||
.await
|
||||
.unwrap();
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.get(format!(
|
||||
"{}/guilds/{}/channels/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
self.id
|
||||
))
|
||||
.bearer_auth(user.token()),
|
||||
limit_type: LimitType::Channel(self.id),
|
||||
};
|
||||
let result = chorus_request.send_request(user).await?;
|
||||
let stringed_response = match result.text().await {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
return Err(ChorusLibError::InvalidResponseError {
|
||||
return Err(ChorusError::InvalidResponse {
|
||||
error: e.to_string(),
|
||||
});
|
||||
}
|
||||
|
@ -138,7 +134,7 @@ impl Guild {
|
|||
let _: Vec<Channel> = match from_str(&stringed_response) {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
return Err(ChorusLibError::InvalidResponseError {
|
||||
return Err(ChorusError::InvalidResponse {
|
||||
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_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> {
|
||||
let mut belongs_to = user.belongs_to.borrow_mut();
|
||||
Guild::_get(guild_id, &user.token, &mut user.limits, &mut belongs_to).await
|
||||
}
|
||||
|
||||
/// For internal use. Does the same as the public get method, but does not require a second, mutable
|
||||
/// borrow of `UserMeta::belongs_to`, when used in conjunction with other methods, which borrow `UserMeta::belongs_to`.
|
||||
async fn _get(
|
||||
guild_id: Snowflake,
|
||||
token: &str,
|
||||
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),
|
||||
pub async fn get(guild_id: Snowflake, user: &mut UserMeta) -> ChorusResult<Guild> {
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.get(format!(
|
||||
"{}/guilds/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
guild_id
|
||||
))
|
||||
.bearer_auth(user.token()),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
let guild: Guild = from_str(&response.text().await.unwrap()).unwrap();
|
||||
Ok(guild)
|
||||
let response = chorus_request.deserialize_response::<Guild>(user).await?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,48 +187,17 @@ impl Channel {
|
|||
guild_id: Snowflake,
|
||||
schema: ChannelCreateSchema,
|
||||
) -> ChorusResult<Channel> {
|
||||
let mut belongs_to = user.belongs_to.borrow_mut();
|
||||
Channel::_create(
|
||||
&user.token,
|
||||
guild_id,
|
||||
schema,
|
||||
&mut user.limits,
|
||||
&mut belongs_to,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
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),
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.post(format!(
|
||||
"{}/guilds/{}/channels/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
guild_id
|
||||
))
|
||||
.bearer_auth(user.token())
|
||||
.body(to_string(&schema).unwrap()),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
match from_str::<Channel>(&result.text().await.unwrap()) {
|
||||
Ok(object) => Ok(object),
|
||||
Err(e) => Err(ChorusLibError::RequestErrorError {
|
||||
url: format!("{}/guilds/{}/channels/", instance.urls.api, guild_id),
|
||||
error: e.to_string(),
|
||||
}),
|
||||
}
|
||||
chorus_request.deserialize_response::<Channel>(user).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use reqwest::Client;
|
||||
|
||||
use crate::{
|
||||
api::{deserialize_response, handle_request_as_result},
|
||||
api::LimitType,
|
||||
errors::ChorusResult,
|
||||
instance::UserMeta,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{self, Snowflake},
|
||||
};
|
||||
|
||||
|
@ -30,13 +31,13 @@ impl types::GuildMember {
|
|||
guild_id,
|
||||
member_id
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(user.token());
|
||||
deserialize_response::<types::GuildMember>(
|
||||
request,
|
||||
user,
|
||||
crate::api::limits::LimitType::Guild,
|
||||
)
|
||||
.await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().get(url).bearer_auth(user.token()),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
chorus_request
|
||||
.deserialize_response::<types::GuildMember>(user)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds a role to a guild member.
|
||||
|
@ -64,8 +65,11 @@ impl types::GuildMember {
|
|||
member_id,
|
||||
role_id
|
||||
);
|
||||
let request = Client::new().put(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
@ -85,7 +89,7 @@ impl types::GuildMember {
|
|||
guild_id: Snowflake,
|
||||
member_id: Snowflake,
|
||||
role_id: Snowflake,
|
||||
) -> Result<(), crate::errors::ChorusLibError> {
|
||||
) -> Result<(), crate::errors::ChorusError> {
|
||||
let url = format!(
|
||||
"{}/guilds/{}/members/{}/roles/{}/",
|
||||
user.belongs_to.borrow().urls.api,
|
||||
|
@ -93,7 +97,10 @@ impl types::GuildMember {
|
|||
member_id,
|
||||
role_id
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(user.token());
|
||||
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().delete(url).bearer_auth(user.token()),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
chorus_request.handle_request_as_result(user).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ use reqwest::Client;
|
|||
use serde_json::to_string;
|
||||
|
||||
use crate::{
|
||||
api::deserialize_response,
|
||||
errors::{ChorusLibError, ChorusResult},
|
||||
api::LimitType,
|
||||
errors::{ChorusError, ChorusResult},
|
||||
instance::UserMeta,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{self, RoleCreateModifySchema, RoleObject, Snowflake},
|
||||
};
|
||||
|
||||
|
@ -32,14 +33,14 @@ impl types::RoleObject {
|
|||
user.belongs_to.borrow().urls.api,
|
||||
guild_id
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(user.token());
|
||||
let roles = deserialize_response::<Vec<RoleObject>>(
|
||||
request,
|
||||
user,
|
||||
crate::api::limits::LimitType::Guild,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().get(url).bearer_auth(user.token()),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
let roles = chorus_request
|
||||
.deserialize_response::<Vec<RoleObject>>(user)
|
||||
.await
|
||||
.unwrap();
|
||||
if roles.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -72,8 +73,13 @@ impl types::RoleObject {
|
|||
guild_id,
|
||||
role_id
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(user.token());
|
||||
deserialize_response(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
@ -102,12 +108,17 @@ impl types::RoleObject {
|
|||
guild_id
|
||||
);
|
||||
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
|
||||
ChorusLibError::FormCreationError {
|
||||
ChorusError::FormCreation {
|
||||
error: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let request = Client::new().post(url).bearer_auth(user.token()).body(body);
|
||||
deserialize_response(request, user, crate::api::limits::LimitType::Guild).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
@ -135,16 +146,19 @@ impl types::RoleObject {
|
|||
user.belongs_to.borrow().urls.api,
|
||||
guild_id
|
||||
);
|
||||
let body = to_string(&role_position_update_schema).map_err(|e| {
|
||||
ChorusLibError::FormCreationError {
|
||||
let body =
|
||||
to_string(&role_position_update_schema).map_err(|e| ChorusError::FormCreation {
|
||||
error: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let request = Client::new()
|
||||
.patch(url)
|
||||
.bearer_auth(user.token())
|
||||
.body(body);
|
||||
deserialize_response::<RoleObject>(request, user, crate::api::limits::LimitType::Guild)
|
||||
})?;
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.patch(url)
|
||||
.bearer_auth(user.token())
|
||||
.body(body),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
chorus_request
|
||||
.deserialize_response::<RoleObject>(user)
|
||||
.await
|
||||
}
|
||||
|
||||
|
@ -177,15 +191,19 @@ impl types::RoleObject {
|
|||
role_id
|
||||
);
|
||||
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
|
||||
ChorusLibError::FormCreationError {
|
||||
ChorusError::FormCreation {
|
||||
error: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
let request = Client::new()
|
||||
.patch(url)
|
||||
.bearer_auth(user.token())
|
||||
.body(body);
|
||||
deserialize_response::<RoleObject>(request, user, crate::api::limits::LimitType::Guild)
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.patch(url)
|
||||
.bearer_auth(user.token())
|
||||
.body(body),
|
||||
limit_type: LimitType::Guild(guild_id),
|
||||
};
|
||||
chorus_request
|
||||
.deserialize_response::<RoleObject>(user)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
pub use channels::messages::*;
|
||||
pub use common::*;
|
||||
pub use guilds::*;
|
||||
pub use invites::*;
|
||||
pub use policies::instance::instance::*;
|
||||
pub use policies::instance::limits::*;
|
||||
pub use policies::instance::ratelimits::*;
|
||||
pub use users::*;
|
||||
|
||||
pub mod auth;
|
||||
pub mod channels;
|
||||
pub mod common;
|
||||
pub mod guilds;
|
||||
pub mod invites;
|
||||
pub mod policies;
|
||||
pub mod users;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use reqwest::Client;
|
||||
use serde_json::from_str;
|
||||
|
||||
use crate::errors::{ChorusLibError, ChorusResult};
|
||||
use crate::errors::{ChorusError, ChorusResult};
|
||||
use crate::instance::Instance;
|
||||
use crate::types::GeneralConfiguration;
|
||||
|
||||
|
@ -10,21 +9,21 @@ impl Instance {
|
|||
/// # Errors
|
||||
/// [`ChorusLibError`] - If the request fails.
|
||||
pub async fn general_configuration_schema(&self) -> ChorusResult<GeneralConfiguration> {
|
||||
let client = Client::new();
|
||||
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,
|
||||
Err(e) => {
|
||||
return Err(ChorusLibError::RequestErrorError {
|
||||
return Err(ChorusError::RequestFailed {
|
||||
url: endpoint_url,
|
||||
error: e.to_string(),
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !request.status().as_str().starts_with('2') {
|
||||
return Err(ChorusLibError::ReceivedErrorCodeError {
|
||||
error_code: request.status().to_string(),
|
||||
return Err(ChorusError::ReceivedErrorCode {
|
||||
error_code: request.status().as_u16(),
|
||||
error: request.text().await.unwrap(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
pub use instance::*;
|
||||
pub use limits::*;
|
||||
pub use ratelimits::*;
|
||||
|
||||
pub mod instance;
|
||||
pub mod limits;
|
||||
pub mod ratelimits;
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
pub use instance::limits::*;
|
||||
pub use instance::ratelimits::*;
|
||||
|
||||
pub mod instance;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
pub use channels::*;
|
||||
pub use guilds::*;
|
||||
pub use relationships::*;
|
||||
pub use users::*;
|
||||
|
||||
pub mod channels;
|
||||
pub mod guilds;
|
||||
pub mod relationships;
|
||||
pub mod users;
|
||||
|
|
|
@ -2,9 +2,10 @@ use reqwest::Client;
|
|||
use serde_json::to_string;
|
||||
|
||||
use crate::{
|
||||
api::{deserialize_response, handle_request_as_result},
|
||||
api::LimitType,
|
||||
errors::ChorusResult,
|
||||
instance::UserMeta,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{self, CreateUserRelationshipSchema, RelationshipType, Snowflake},
|
||||
};
|
||||
|
||||
|
@ -26,13 +27,13 @@ impl UserMeta {
|
|||
self.belongs_to.borrow().urls.api,
|
||||
user_id
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(self.token());
|
||||
deserialize_response::<Vec<types::PublicUser>>(
|
||||
request,
|
||||
self,
|
||||
crate::api::limits::LimitType::Global,
|
||||
)
|
||||
.await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().get(url).bearer_auth(self.token()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request
|
||||
.deserialize_response::<Vec<types::PublicUser>>(self)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Retrieves the authenticated user's relationships.
|
||||
|
@ -44,13 +45,13 @@ impl UserMeta {
|
|||
"{}/users/@me/relationships/",
|
||||
self.belongs_to.borrow().urls.api
|
||||
);
|
||||
let request = Client::new().get(url).bearer_auth(self.token());
|
||||
deserialize_response::<Vec<types::Relationship>>(
|
||||
request,
|
||||
self,
|
||||
crate::api::limits::LimitType::Global,
|
||||
)
|
||||
.await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().get(url).bearer_auth(self.token()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request
|
||||
.deserialize_response::<Vec<types::Relationship>>(self)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends a friend request to a user.
|
||||
|
@ -70,8 +71,11 @@ impl UserMeta {
|
|||
self.belongs_to.borrow().urls.api
|
||||
);
|
||||
let body = to_string(&schema).unwrap();
|
||||
let request = Client::new().post(url).bearer_auth(self.token()).body(body);
|
||||
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await
|
||||
let chorus_request = ChorusRequest {
|
||||
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.
|
||||
|
@ -96,10 +100,13 @@ impl UserMeta {
|
|||
let api_url = self.belongs_to.borrow().urls.api.clone();
|
||||
match relationship_type {
|
||||
RelationshipType::None => {
|
||||
let request = Client::new()
|
||||
.delete(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token());
|
||||
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.delete(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request.handle_request_as_result(self).await
|
||||
}
|
||||
RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => {
|
||||
let body = CreateUserRelationshipSchema {
|
||||
|
@ -107,11 +114,14 @@ impl UserMeta {
|
|||
from_friend_suggestion: None,
|
||||
friend_token: None,
|
||||
};
|
||||
let request = Client::new()
|
||||
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token())
|
||||
.body(to_string(&body).unwrap());
|
||||
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token())
|
||||
.body(to_string(&body).unwrap()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request.handle_request_as_result(self).await
|
||||
}
|
||||
RelationshipType::Blocked => {
|
||||
let body = CreateUserRelationshipSchema {
|
||||
|
@ -119,11 +129,14 @@ impl UserMeta {
|
|||
from_friend_suggestion: None,
|
||||
friend_token: None,
|
||||
};
|
||||
let request = Client::new()
|
||||
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token())
|
||||
.body(to_string(&body).unwrap());
|
||||
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new()
|
||||
.put(format!("{}/users/@me/relationships/{}/", api_url, user_id))
|
||||
.bearer_auth(self.token())
|
||||
.body(to_string(&body).unwrap()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request.handle_request_as_result(self).await
|
||||
}
|
||||
RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()),
|
||||
}
|
||||
|
@ -143,7 +156,10 @@ impl UserMeta {
|
|||
self.belongs_to.borrow().urls.api,
|
||||
user_id
|
||||
);
|
||||
let request = Client::new().delete(url).bearer_auth(self.token());
|
||||
handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await
|
||||
let chorus_request = ChorusRequest {
|
||||
request: Client::new().delete(url).bearer_auth(self.token()),
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
chorus_request.handle_request_as_result(self).await
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use reqwest::Client;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::{
|
||||
api::{deserialize_response, handle_request_as_result},
|
||||
errors::{ChorusLibError, ChorusResult},
|
||||
api::LimitType,
|
||||
errors::{ChorusError, ChorusResult},
|
||||
instance::{Instance, UserMeta},
|
||||
limit::LimitedRequester,
|
||||
ratelimiter::ChorusRequest,
|
||||
types::{User, UserModifySchema, UserSettings},
|
||||
};
|
||||
|
||||
|
@ -48,16 +50,20 @@ impl UserMeta {
|
|||
|| modify_schema.email.is_some()
|
||||
|| modify_schema.code.is_some()
|
||||
{
|
||||
return Err(ChorusLibError::PasswordRequiredError);
|
||||
return Err(ChorusError::PasswordRequired);
|
||||
}
|
||||
let request = Client::new()
|
||||
.patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api))
|
||||
.body(to_string(&modify_schema).unwrap())
|
||||
.bearer_auth(self.token());
|
||||
let user_updated =
|
||||
deserialize_response::<User>(request, self, crate::api::limits::LimitType::Ip)
|
||||
.await
|
||||
.unwrap();
|
||||
let chorus_request = ChorusRequest {
|
||||
request,
|
||||
limit_type: LimitType::default(),
|
||||
};
|
||||
let user_updated = chorus_request
|
||||
.deserialize_response::<User>(self)
|
||||
.await
|
||||
.unwrap();
|
||||
let _ = std::mem::replace(&mut self.object, user_updated.clone());
|
||||
Ok(user_updated)
|
||||
}
|
||||
|
@ -78,43 +84,28 @@ impl UserMeta {
|
|||
self.belongs_to.borrow().urls.api
|
||||
))
|
||||
.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 {
|
||||
pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult<User> {
|
||||
let mut belongs_to = user.belongs_to.borrow_mut();
|
||||
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_api = user.belongs_to.borrow().urls.api.clone();
|
||||
let url = if id.is_none() {
|
||||
format!("{}/users/@me/", url_api)
|
||||
} else {
|
||||
format!("{}/users/{}", url_api, id.unwrap())
|
||||
};
|
||||
let request = reqwest::Client::new().get(url).bearer_auth(token);
|
||||
let mut cloned_limits = instance.limits.clone();
|
||||
match LimitedRequester::send_request(
|
||||
let request = reqwest::Client::new().get(url).bearer_auth(user.token());
|
||||
let chorus_request = ChorusRequest {
|
||||
request,
|
||||
crate::api::limits::LimitType::Ip,
|
||||
instance,
|
||||
&mut cloned_limits,
|
||||
)
|
||||
.await
|
||||
{
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
match chorus_request.send_request(user).await {
|
||||
Ok(result) => {
|
||||
let result_text = result.text().await.unwrap();
|
||||
Ok(serde_json::from_str::<User>(&result_text).unwrap())
|
||||
|
@ -131,18 +122,21 @@ impl User {
|
|||
let request: reqwest::RequestBuilder = Client::new()
|
||||
.get(format!("{}/users/@me/settings/", url_api))
|
||||
.bearer_auth(token);
|
||||
let mut cloned_limits = instance.limits.clone();
|
||||
match LimitedRequester::send_request(
|
||||
let mut user =
|
||||
UserMeta::shell(Rc::new(RefCell::new(instance.clone())), token.clone()).await;
|
||||
let chorus_request = ChorusRequest {
|
||||
request,
|
||||
crate::api::limits::LimitType::Ip,
|
||||
instance,
|
||||
&mut cloned_limits,
|
||||
)
|
||||
.await
|
||||
{
|
||||
limit_type: LimitType::Global,
|
||||
};
|
||||
let result = match chorus_request.send_request(&mut user).await {
|
||||
Ok(result) => Ok(serde_json::from_str(&result.text().await.unwrap()).unwrap()),
|
||||
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`].
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,50 @@
|
|||
use custom_error::custom_error;
|
||||
use reqwest::Error;
|
||||
|
||||
custom_error! {
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub FieldFormatError
|
||||
PasswordError = "Password must be between 1 and 72 characters.",
|
||||
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 RegistrationError
|
||||
Consent = "Consent must be 'true' to register.",
|
||||
}
|
||||
|
||||
pub type ChorusResult<T> = std::result::Result<T, ChorusLibError>;
|
||||
pub type ChorusResult<T> = std::result::Result<T, ChorusError>;
|
||||
|
||||
custom_error! {
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub ChorusLibError
|
||||
pub ChorusError
|
||||
/// Server did not respond.
|
||||
NoResponse = "Did not receive a response from the Server.",
|
||||
RequestErrorError{url:String, error:String} = "An error occured while trying to GET from {url}: {error}",
|
||||
ReceivedErrorCodeError{error_code:String} = "Received the following error code while requesting from the route: {error_code}",
|
||||
CantGetInfoError{error:String} = "Something seems to be wrong with the instance. Cannot get information about the instance: {error}",
|
||||
InvalidFormBodyError{error_type: String, error:String} = "The server responded with: {error_type}: {error}",
|
||||
/// Reqwest returned an Error instead of a Response object.
|
||||
RequestFailed{url:String, error: Error} = "An error occured while trying to GET from {url}: {error}",
|
||||
/// Response received, however, it was not of the successful responses type. Used when no other, special case applies.
|
||||
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}",
|
||||
MultipartCreationError{error: String} = "Got an error whilst creating the form: {error}",
|
||||
FormCreationError{error: String} = "Got an error whilst creating the form: {error}",
|
||||
/// The multipart form could not be created.
|
||||
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.",
|
||||
/// No permission
|
||||
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}",
|
||||
PasswordRequiredError = "You need to provide your current password to authenticate for this action.",
|
||||
InvalidResponseError{error: String} = "The response is malformed and cannot be processed. Error: {error}",
|
||||
InvalidArgumentsError{error: String} = "Invalid arguments were provided. Error: {error}"
|
||||
/// Used when you, for example, try to change your spacebar account password without providing your old password for verification.
|
||||
PasswordRequired = "You need to provide your current password to authenticate for this action.",
|
||||
/// 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! {
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub ObserverError
|
||||
AlreadySubscribedError = "Each event can only be subscribed to once."
|
||||
AlreadySubscribed = "Each event can only be subscribed to once."
|
||||
}
|
||||
|
||||
custom_error! {
|
||||
|
@ -45,27 +56,27 @@ custom_error! {
|
|||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub GatewayError
|
||||
// Errors we have received from the gateway
|
||||
UnknownError = "We're not sure what went wrong. Try reconnecting?",
|
||||
UnknownOpcodeError = "You sent an invalid Gateway opcode or an invalid payload for an opcode",
|
||||
DecodeError = "Gateway server couldn't decode payload",
|
||||
NotAuthenticatedError = "You sent a payload prior to identifying",
|
||||
AuthenticationFailedError = "The account token sent with your identify payload is invalid",
|
||||
AlreadyAuthenticatedError = "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",
|
||||
RateLimitedError = "You are being rate limited!",
|
||||
SessionTimedOutError = "Your session timed out. Reconnect and start a new one",
|
||||
InvalidShardError = "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",
|
||||
InvalidAPIVersionError = "You sent an invalid Gateway version",
|
||||
InvalidIntentsError = "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",
|
||||
Unknown = "We're not sure what went wrong. Try reconnecting?",
|
||||
UnknownOpcode = "You sent an invalid Gateway opcode or an invalid payload for an opcode",
|
||||
Decode = "Gateway server couldn't decode payload",
|
||||
NotAuthenticated = "You sent a payload prior to identifying",
|
||||
AuthenticationFailed = "The account token sent with your identify payload is invalid",
|
||||
AlreadyAuthenticated = "You've already identified, no need to reauthenticate",
|
||||
InvalidSequenceNumber = "The sequence number sent when resuming the session was invalid. Reconnect and start a new session",
|
||||
RateLimited = "You are being rate limited!",
|
||||
SessionTimedOut = "Your session timed out. Reconnect and start a new one",
|
||||
InvalidShard = "You sent us an invalid shard when identifying",
|
||||
ShardingRequired = "The session would have handled too many guilds - you are required to shard your connection in order to connect",
|
||||
InvalidAPIVersion = "You sent an invalid Gateway version",
|
||||
InvalidIntents = "You sent an invalid intent",
|
||||
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
|
||||
CannotConnectError{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",
|
||||
CannotConnect{error: String} = "Cannot connect due to a tungstenite error: {error}",
|
||||
NonHelloOnInitiate{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong",
|
||||
|
||||
// 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! {
|
||||
|
|
1353
src/gateway.rs
1353
src/gateway.rs
File diff suppressed because it is too large
Load Diff
|
@ -1,26 +1,37 @@
|
|||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::limits::Limits;
|
||||
use crate::errors::{ChorusLibError, ChorusResult, FieldFormatError};
|
||||
use crate::api::{Limit, LimitType};
|
||||
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::UrlBundle;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/**
|
||||
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 urls: UrlBundle,
|
||||
pub instance_info: GeneralConfiguration,
|
||||
pub limits: Limits,
|
||||
pub limits_information: Option<LimitsInformation>,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LimitsInformation {
|
||||
pub ratelimits: HashMap<LimitType, Limit>,
|
||||
pub configuration: RateLimits,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
/// Creates a new [`Instance`].
|
||||
/// # Arguments
|
||||
|
@ -28,24 +39,43 @@ impl Instance {
|
|||
/// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server.
|
||||
/// # Errors
|
||||
/// * [`InstanceError`] - If the instance cannot be created.
|
||||
pub async fn new(urls: UrlBundle) -> 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 {
|
||||
urls: urls.clone(),
|
||||
// Will be overwritten in the next step
|
||||
instance_info: GeneralConfiguration::default(),
|
||||
limits: Limits::check_limits(urls.api).await,
|
||||
limits_information,
|
||||
client: Client::new(),
|
||||
};
|
||||
instance.instance_info = match instance.general_configuration_schema().await {
|
||||
Ok(schema) => schema,
|
||||
Err(e) => {
|
||||
return Err(ChorusLibError::CantGetInfoError {
|
||||
error: e.to_string(),
|
||||
});
|
||||
log::warn!("Could not get instance configuration schema: {}", e);
|
||||
GeneralConfiguration::default()
|
||||
}
|
||||
};
|
||||
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)]
|
||||
|
@ -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)]
|
||||
pub struct UserMeta {
|
||||
pub belongs_to: Rc<RefCell<Instance>>,
|
||||
pub token: String,
|
||||
pub limits: Limits,
|
||||
pub limits: Option<HashMap<LimitType, Limit>>,
|
||||
pub settings: UserSettings,
|
||||
pub object: User,
|
||||
pub gateway: GatewayHandle,
|
||||
}
|
||||
|
||||
impl UserMeta {
|
||||
|
@ -99,9 +111,10 @@ impl UserMeta {
|
|||
pub fn new(
|
||||
belongs_to: Rc<RefCell<Instance>>,
|
||||
token: String,
|
||||
limits: Limits,
|
||||
limits: Option<HashMap<LimitType, Limit>>,
|
||||
settings: UserSettings,
|
||||
object: User,
|
||||
gateway: GatewayHandle,
|
||||
) -> UserMeta {
|
||||
UserMeta {
|
||||
belongs_to,
|
||||
|
@ -109,6 +122,32 @@ impl UserMeta {
|
|||
limits,
|
||||
settings,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ pub mod gateway;
|
|||
#[cfg(feature = "client")]
|
||||
pub mod instance;
|
||||
#[cfg(feature = "client")]
|
||||
pub mod limit;
|
||||
pub mod ratelimiter;
|
||||
pub mod types;
|
||||
#[cfg(feature = "client")]
|
||||
pub mod voice;
|
||||
|
|
304
src/limit.rs
304
src/limit.rs
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -18,10 +18,8 @@ pub struct GeneralConfiguration {
|
|||
impl Default for GeneralConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
instance_name: String::from("Spacebar Instance"),
|
||||
instance_description: Some(String::from(
|
||||
"This is a Spacebar instance made in the pre-release days",
|
||||
)),
|
||||
instance_name: String::from("Spacebar-compatible Instance"),
|
||||
instance_description: Some(String::from("This is a spacebar-compatible instance.")),
|
||||
front_page: None,
|
||||
tos_page: None,
|
||||
correspondence_email: None,
|
||||
|
|
|
@ -10,7 +10,7 @@ use sqlx::{
|
|||
database::{HasArguments, HasValueRef},
|
||||
encode::IsNull,
|
||||
error::BoxDynError,
|
||||
Decode, Encode, MySql,
|
||||
Decode, MySql,
|
||||
};
|
||||
|
||||
use crate::types::config::types::subconfigs::guild::{
|
||||
|
@ -139,7 +139,7 @@ pub enum GuildFeatures {
|
|||
InvitesClosed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq)]
|
||||
pub struct GuildFeaturesList(Vec<GuildFeatures>);
|
||||
|
||||
impl Deref for GuildFeaturesList {
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::config::types::subconfigs::limits::ratelimits::{
|
||||
route::RouteRateLimit, RateLimitOptions,
|
||||
use crate::{
|
||||
api::LimitType,
|
||||
types::config::types::subconfigs::limits::ratelimits::{
|
||||
route::RouteRateLimit, RateLimitOptions,
|
||||
},
|
||||
};
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,68 +1,70 @@
|
|||
use chorus_macros::Updateable;
|
||||
use chrono::Utc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_aux::prelude::deserialize_string_from_number;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::gateway::Updateable;
|
||||
use crate::types::{
|
||||
entities::{GuildMember, User},
|
||||
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))]
|
||||
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 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")]
|
||||
pub applied_tags: Option<sqlx::types::Json<Vec<String>>>,
|
||||
#[cfg(not(feature = "sqlx"))]
|
||||
pub applied_tags: Option<Vec<String>>,
|
||||
#[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>>,
|
||||
#[cfg(not(feature = "sqlx"))]
|
||||
pub default_reaction_emoji: Option<DefaultReaction>,
|
||||
pub default_thread_rate_limit_per_user: 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)]
|
||||
|
@ -74,7 +76,7 @@ pub struct Tag {
|
|||
pub emoji_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd)]
|
||||
pub struct PermissionOverwrite {
|
||||
pub id: Snowflake,
|
||||
#[serde(rename = "type")]
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||
use crate::types::entities::User;
|
||||
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))]
|
||||
pub struct Emoji {
|
||||
pub id: Option<Snowflake>,
|
||||
|
|
|
@ -91,7 +91,7 @@ pub struct Guild {
|
|||
}
|
||||
|
||||
/// 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))]
|
||||
pub struct GuildBan {
|
||||
pub user_id: Snowflake,
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -8,6 +8,7 @@ pub use emoji::*;
|
|||
pub use guild::*;
|
||||
pub use guild_member::*;
|
||||
pub use integration::*;
|
||||
pub use invite::*;
|
||||
pub use message::*;
|
||||
pub use relationship::*;
|
||||
pub use role::*;
|
||||
|
@ -31,6 +32,7 @@ mod emoji;
|
|||
mod guild;
|
||||
mod guild_member;
|
||||
mod integration;
|
||||
mod invite;
|
||||
mod message;
|
||||
mod relationship;
|
||||
mod role;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use crate::types::utils::Snowflake;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_aux::prelude::deserialize_option_number_from_string;
|
||||
|
||||
use crate::types::utils::Snowflake;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
|
||||
pub struct UserData {
|
||||
|
@ -16,7 +15,6 @@ impl User {
|
|||
PublicUser::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct User {
|
||||
|
@ -45,9 +43,9 @@ pub struct User {
|
|||
pub bio: Option<String>,
|
||||
pub theme_colors: Option<Vec<u8>>,
|
||||
pub phone: Option<String>,
|
||||
pub nsfw_allowed: bool,
|
||||
pub premium: bool,
|
||||
pub purchased_flags: i32,
|
||||
pub nsfw_allowed: Option<bool>,
|
||||
pub premium: Option<bool>,
|
||||
pub purchased_flags: Option<i32>,
|
||||
pub premium_usage_flags: Option<i32>,
|
||||
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;
|
||||
|
||||
bitflags::bitflags! {
|
||||
|
|
|
@ -3,6 +3,8 @@ use crate::types::{entities::Channel, Snowflake};
|
|||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::UpdateMessage;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
/// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update
|
||||
pub struct ChannelPinsUpdate {
|
||||
|
@ -31,6 +33,15 @@ pub struct 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)]
|
||||
/// Officially undocumented.
|
||||
/// Sends updates to client about a new message with its id
|
||||
|
|
|
@ -14,8 +14,7 @@ impl WebSocketEvent for GatewayHello {}
|
|||
/// Contains info on how often the client should send heartbeats to the server;
|
||||
pub struct HelloData {
|
||||
/// How often a client should send heartbeats, in milliseconds
|
||||
// u128 because std used u128s for milliseconds
|
||||
pub heartbeat_interval: u128,
|
||||
pub heartbeat_interval: u64,
|
||||
}
|
||||
|
||||
impl WebSocketEvent for HelloData {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::gateway::Updateable;
|
||||
pub use application::*;
|
||||
pub use auto_moderation::*;
|
||||
pub use call::*;
|
||||
|
@ -25,9 +26,10 @@ pub use thread::*;
|
|||
pub use user::*;
|
||||
pub use voice::*;
|
||||
pub use webhooks::*;
|
||||
|
||||
pub use webrtc::*;
|
||||
|
||||
use super::Snowflake;
|
||||
|
||||
mod application;
|
||||
mod auto_moderation;
|
||||
mod call;
|
||||
|
@ -99,3 +101,23 @@ pub struct 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;
|
||||
}
|
||||
|
|
|
@ -1,122 +1,8 @@
|
|||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::errors::FieldFormatError;
|
||||
|
||||
/**
|
||||
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)]
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
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 password: Option<String>,
|
||||
pub consent: bool,
|
||||
|
@ -129,123 +15,21 @@ pub struct RegisterSchemaOptions {
|
|||
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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct LoginSchema {
|
||||
/// For Discord, usernames must be between 2 and 32 characters,
|
||||
/// but other servers may have different limits.
|
||||
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 captcha_key: Option<String>,
|
||||
pub login_source: 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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TotpSchema {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use bitflags::bitflags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::{entities::PermissionOverwrite, Snowflake};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, PartialOrd)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ChannelCreateSchema {
|
||||
pub name: String,
|
||||
|
@ -26,7 +27,7 @@ pub struct ChannelCreateSchema {
|
|||
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")]
|
||||
pub struct ChannelModifySchema {
|
||||
pub name: Option<String>,
|
||||
|
@ -48,7 +49,7 @@ pub struct ChannelModifySchema {
|
|||
pub video_quality_mode: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct GetChannelMessagesSchema {
|
||||
/// Between 1 and 100, defaults to 50.
|
||||
pub limit: Option<i32>,
|
||||
|
@ -56,7 +57,7 @@ pub struct GetChannelMessagesSchema {
|
|||
pub anchor: ChannelMessagesAnchor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChannelMessagesAnchor {
|
||||
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>,
|
||||
}
|
||||
|
|
|
@ -15,76 +15,3 @@ mod message;
|
|||
mod relationship;
|
||||
mod role;
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::Snowflake;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct UserModifySchema {
|
||||
|
@ -14,3 +18,17 @@ pub struct UserModifySchema {
|
|||
pub email: Option<String>,
|
||||
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>>,
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ impl Rights {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // FIXME: Remove this when we use this
|
||||
fn all_rights() -> Rights {
|
||||
Rights::OPERATOR
|
||||
| Rights::MANAGE_APPLICATIONS
|
||||
|
|
|
@ -12,7 +12,7 @@ const EPOCH: i64 = 1420070400000;
|
|||
|
||||
/// Unique identifier including a timestamp.
|
||||
/// 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", sqlx(transparent))]
|
||||
pub struct Snowflake(u64);
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
use chorus::types::{RegisterSchema, RegisterSchemaOptions};
|
||||
use chorus::types::RegisterSchema;
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_registration() {
|
||||
let mut bundle = common::setup().await;
|
||||
let reg = RegisterSchemaOptions {
|
||||
let reg = RegisterSchema {
|
||||
username: "Hiiii".into(),
|
||||
date_of_birth: Some("2000-01-01".to_string()),
|
||||
..RegisterSchema::builder("Hiiii", true)
|
||||
}
|
||||
.build()
|
||||
.unwrap();
|
||||
consent: true,
|
||||
..Default::default()
|
||||
};
|
||||
bundle.instance.register_account(®).await.unwrap();
|
||||
common::teardown(bundle).await;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use chorus::types::{
|
||||
self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags,
|
||||
PermissionOverwrite, Snowflake,
|
||||
PermissionOverwrite, PrivateChannelCreateSchema, RelationshipType, Snowflake,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
@ -28,10 +28,11 @@ async fn delete_channel() {
|
|||
|
||||
#[tokio::test]
|
||||
async fn modify_channel() {
|
||||
const CHANNEL_NAME: &str = "beepboop";
|
||||
let mut bundle = common::setup().await;
|
||||
let channel = &mut bundle.channel;
|
||||
let modify_data: types::ChannelModifySchema = types::ChannelModifySchema {
|
||||
name: Some("beepboop".to_string()),
|
||||
name: Some(CHANNEL_NAME.to_string()),
|
||||
channel_type: None,
|
||||
topic: None,
|
||||
icon: None,
|
||||
|
@ -49,10 +50,10 @@ async fn modify_channel() {
|
|||
default_thread_rate_limit_per_user: 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
|
||||
.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([
|
||||
PermissionFlags::MANAGE_CHANNELS,
|
||||
|
@ -89,12 +90,11 @@ async fn get_channel_messages() {
|
|||
let _ = bundle
|
||||
.user
|
||||
.send_message(
|
||||
&mut MessageSendSchema {
|
||||
MessageSendSchema {
|
||||
content: Some("A Message!".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
bundle.channel.id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -136,3 +136,81 @@ async fn get_channel_messages() {
|
|||
|
||||
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
|
||||
);
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
use chorus::gateway::Gateway;
|
||||
use chorus::{
|
||||
errors::ChorusResult,
|
||||
instance::{Instance, UserMeta},
|
||||
types::{
|
||||
Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema,
|
||||
RegisterSchemaOptions, RoleCreateModifySchema, RoleObject,
|
||||
RoleCreateModifySchema, RoleObject,
|
||||
},
|
||||
UrlBundle,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct TestBundle {
|
||||
pub(crate) struct TestBundle {
|
||||
pub urls: UrlBundle,
|
||||
pub user: UserMeta,
|
||||
pub instance: Instance,
|
||||
|
@ -18,21 +19,47 @@ pub struct TestBundle {
|
|||
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(®ister_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.
|
||||
pub async fn setup() -> TestBundle {
|
||||
pub(crate) async fn setup() -> TestBundle {
|
||||
let urls = UrlBundle::new(
|
||||
"http://localhost:3001/api".to_string(),
|
||||
"ws://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.
|
||||
let reg = RegisterSchemaOptions {
|
||||
let reg = RegisterSchema {
|
||||
username: "integrationtestuser".into(),
|
||||
consent: true,
|
||||
date_of_birth: Some("2000-01-01".to_string()),
|
||||
..RegisterSchema::builder("integrationtestuser", true)
|
||||
}
|
||||
.build()
|
||||
.unwrap();
|
||||
..Default::default()
|
||||
};
|
||||
let guild_create_schema = GuildCreateSchema {
|
||||
name: Some("Test-Guild!".to_string()),
|
||||
region: None,
|
||||
|
@ -94,7 +121,7 @@ pub async fn setup() -> TestBundle {
|
|||
|
||||
// Teardown method to clean up after a test.
|
||||
#[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)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
mod common;
|
||||
|
||||
use chorus::gateway::*;
|
||||
use chorus::types;
|
||||
use chorus::types::{self, Channel};
|
||||
|
||||
#[tokio::test]
|
||||
/// Tests establishing a connection (hello and heartbeats) on the local gateway;
|
||||
async fn test_gateway_establish() {
|
||||
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]
|
||||
|
@ -15,10 +17,30 @@ async fn test_gateway_establish() {
|
|||
async fn test_gateway_authenticate() {
|
||||
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();
|
||||
identify.token = bundle.user.token;
|
||||
identify.token = bundle.user.token.clone();
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -8,13 +8,13 @@ mod common;
|
|||
#[tokio::test]
|
||||
async fn send_message() {
|
||||
let mut bundle = common::setup().await;
|
||||
let mut message = types::MessageSendSchema {
|
||||
let message = types::MessageSendSchema {
|
||||
content: Some("A Message!".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = bundle
|
||||
.user
|
||||
.send_message(&mut message, bundle.channel.id, None)
|
||||
.send_message(message, bundle.channel.id)
|
||||
.await
|
||||
.unwrap();
|
||||
common::teardown(bundle).await
|
||||
|
@ -45,7 +45,7 @@ async fn send_message_attachment() {
|
|||
content: buffer,
|
||||
};
|
||||
|
||||
let mut message = types::MessageSendSchema {
|
||||
let message = types::MessageSendSchema {
|
||||
content: Some("trans rights now".to_string()),
|
||||
attachments: Some(vec![attachment.clone()]),
|
||||
..Default::default()
|
||||
|
@ -55,11 +55,7 @@ async fn send_message_attachment() {
|
|||
let _arg = Some(&vec_attach);
|
||||
bundle
|
||||
.user
|
||||
.send_message(
|
||||
&mut message,
|
||||
bundle.channel.id,
|
||||
Some(vec![attachment.clone()]),
|
||||
)
|
||||
.send_message(message, bundle.channel.id)
|
||||
.await
|
||||
.unwrap();
|
||||
common::teardown(bundle).await
|
|
@ -1,25 +1,17 @@
|
|||
use chorus::types::{self, RegisterSchema, RegisterSchemaOptions, Relationship, RelationshipType};
|
||||
use chorus::types::{self, Relationship, RelationshipType};
|
||||
|
||||
mod common;
|
||||
|
||||
#[tokio::test]
|
||||
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 belongs_to = &mut bundle.instance;
|
||||
let mut other_user = bundle.create_user("integrationtestuser2").await;
|
||||
let user = &mut bundle.user;
|
||||
let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap();
|
||||
let friend_request_schema = types::FriendRequestSendSchema {
|
||||
username: user.object.username.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
|
||||
.get_mutual_relationships(other_user.object.id)
|
||||
.await
|
||||
|
@ -30,22 +22,17 @@ async fn test_get_mutual_relationships() {
|
|||
|
||||
#[tokio::test]
|
||||
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 belongs_to = &mut bundle.instance;
|
||||
let mut other_user = bundle.create_user("integrationtestuser2").await;
|
||||
let user = &mut bundle.user;
|
||||
let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap();
|
||||
let friend_request_schema = types::FriendRequestSendSchema {
|
||||
username: user.object.username.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();
|
||||
assert_eq!(relationships.get(0).unwrap().id, other_user.object.id);
|
||||
common::teardown(bundle).await
|
||||
|
@ -53,18 +40,10 @@ async fn test_get_relationships() {
|
|||
|
||||
#[tokio::test]
|
||||
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 belongs_to = &mut bundle.instance;
|
||||
let mut other_user = bundle.create_user("integrationtestuser2").await;
|
||||
let user = &mut bundle.user;
|
||||
let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap();
|
||||
other_user
|
||||
let _ = other_user
|
||||
.modify_user_relationship(user.object.id, types::RelationshipType::Friends)
|
||||
.await;
|
||||
let relationships = user.get_relationships().await.unwrap();
|
||||
|
@ -79,7 +58,8 @@ async fn test_modify_relationship_friends() {
|
|||
relationships.get(0).unwrap().relationship_type,
|
||||
RelationshipType::Outgoing
|
||||
);
|
||||
user.modify_user_relationship(other_user.object.id, RelationshipType::Friends)
|
||||
let _ = user
|
||||
.modify_user_relationship(other_user.object.id, RelationshipType::Friends)
|
||||
.await;
|
||||
assert_eq!(
|
||||
other_user
|
||||
|
@ -91,7 +71,7 @@ async fn test_modify_relationship_friends() {
|
|||
.relationship_type,
|
||||
RelationshipType::Friends
|
||||
);
|
||||
user.remove_relationship(other_user.object.id).await;
|
||||
let _ = user.remove_relationship(other_user.object.id).await;
|
||||
assert_eq!(
|
||||
other_user.get_relationships().await.unwrap(),
|
||||
Vec::<Relationship>::new()
|
||||
|
@ -101,18 +81,10 @@ async fn test_modify_relationship_friends() {
|
|||
|
||||
#[tokio::test]
|
||||
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 belongs_to = &mut bundle.instance;
|
||||
let mut other_user = bundle.create_user("integrationtestuser2").await;
|
||||
let user = &mut bundle.user;
|
||||
let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap();
|
||||
other_user
|
||||
let _ = other_user
|
||||
.modify_user_relationship(user.object.id, types::RelationshipType::Blocked)
|
||||
.await;
|
||||
let relationships = user.get_relationships().await.unwrap();
|
||||
|
@ -123,7 +95,7 @@ async fn test_modify_relationship_block() {
|
|||
relationships.get(0).unwrap().relationship_type,
|
||||
RelationshipType::Blocked
|
||||
);
|
||||
other_user.remove_relationship(user.object.id).await;
|
||||
let _ = other_user.remove_relationship(user.object.id).await;
|
||||
assert_eq!(
|
||||
other_user.get_relationships().await.unwrap(),
|
||||
Vec::<Relationship>::new()
|
||||
|
|
Loading…
Reference in New Issue