Merge branch 'main' into perpetual/gateway-dev

This commit is contained in:
kozabrada123 2023-06-20 19:12:39 +02:00
commit cfe4e2c7bb
48 changed files with 940 additions and 939 deletions

View File

@ -1,4 +1,4 @@
name: Rust
name: Build and Test
on:
push:
@ -10,7 +10,7 @@ env:
CARGO_TERM_COLOR: always
jobs:
build_and_test:
rust:
runs-on: ubuntu-latest

20
.github/workflows/clippy.yml vendored Normal file
View File

@ -0,0 +1,20 @@
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

View File

@ -29,9 +29,6 @@ openssl = "0.10.52"
base64 = "0.21.2"
hostname = "0.3.1"
bitflags = { version = "2.2.1", features = ["serde"] }
atomic = "0.5.3"
bigdecimal = "0.3.1"
num-bigint = "0.4.3"
lazy_static = "1.4.0"
poem = { version = "1.3.55", 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 }

171
README.md
View File

@ -5,6 +5,7 @@
[![Contributors][contributors-shield]][contributors-url]
[![Forks][forks-shield]][forks-url]
[![Issues][issues-shield]][issues-url]
<img src="https://img.shields.io/static/v1?label=Status&message=Early%20Development&color=blue">
</br>
<div align="center">
@ -30,94 +31,106 @@
</div>
## Progress Tracker/Roadmap:
### Core Functionality
- [x] Rate Limiter (hint: couldn't be fully tested due to [an Issue with the Spacebar Server](https://github.com/spacebarchat/server/issues/1022))
- [x] [Login (the conventional way)](https://github.com/polyphony-chat/chorus/issues/1)
- [ ] [2FA](https://github.com/polyphony-chat/chorus/issues/40)
- [x] [Registration](https://github.com/polyphony-chat/chorus/issues/1)
## About
### Messaging
- [x] [Sending messages](https://github.com/polyphony-chat/chorus/issues/23)
- [x] [Events (Message, User, Channel, etc.)](https://github.com/polyphony-chat/chorus/issues/51)
- [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)
- [ ] [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)
- [ ] Message Search
- [ ] Message history
- [ ] Emoji
- [ ] Stickers
- [ ] [Forum channels](https://github.com/polyphony-chat/chorus/issues/90)
Chorus is a Rust library that allows developers to interact with multiple Spacebar-compatible APIs and Gateways simultaneously. The library provides a simple and efficient way to communicate with these services, making it easier for developers to build applications that rely on them. Chorus is open-source and welcomes contributions from the community.
### User Management
- [ ] [User profile customization](https://github.com/polyphony-chat/chorus/issues/41)
- [x] Gettings users and user profiles
- [ ] [Friend requests](https://github.com/polyphony-chat/chorus/issues/92)
- [ ] [Blocking users](https://github.com/polyphony-chat/chorus/issues/92)
- [ ] User presence (online, offline, idle, etc.)
- [ ] User status (custom status, etc.)
- [x] Account deletion
## Contributing
### Additional Features
- [ ] Server discovery
- [ ] Server templates
If you would like to contribute, please feel free to open an Issue with the idea you have, or a
Pull Request. Please keep our [contribution guidelines](https://github.com/polyphony-chat/.github/blob/main/CONTRIBUTION_GUIDELINES.md) in mind. Your contribution might not be
accepted, if it violates these guidelines or [our Code of Conduct](https://github.com/polyphony-chat/.github/blob/main/CODE_OF_CONDUCT.md).
### Voice and Video
- [ ] [Voice chat support](https://github.com/polyphony-chat/chorus/issues/49)
- [ ] [Video chat support](https://github.com/polyphony-chat/chorus/issues/49)
<details>
<summary>Progress Tracker/Roadmap</summary>
### Permissions and Roles
- [x] [Role management](https://github.com/polyphony-chat/chorus/issues/46) (creation, deletion, modification)
- [x] [Permission management](https://github.com/polyphony-chat/chorus/issues/46) (assigning and revoking permissions)
- [x] [Channel-specific permissions](https://github.com/polyphony-chat/chorus/issues/88)
- [x] Role-based access control
### Core Functionality
- [x] Rate Limiter (hint: couldn't be fully tested due to [an Issue with the Spacebar Server](https://github.com/spacebarchat/server/issues/1022))
- [x] [Login (the conventional way)](https://github.com/polyphony-chat/chorus/issues/1)
- [ ] [2FA](https://github.com/polyphony-chat/chorus/issues/40)
- [x] [Registration](https://github.com/polyphony-chat/chorus/issues/1)
### Guild Management
- [x] Guild creation
- [x] Guild deletion
- [ ] [Guild settings (name, description, icon, etc.)](https://github.com/polyphony-chat/chorus/issues/43)
- [ ] Guild invites
### Messaging
- [x] [Sending messages](https://github.com/polyphony-chat/chorus/issues/23)
- [x] [Events (Message, User, Channel, etc.)](https://github.com/polyphony-chat/chorus/issues/51)
- [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)
- [ ] [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)
- [ ] Message Search
- [ ] Message history
- [ ] Emoji
- [ ] Stickers
- [ ] [Forum channels](https://github.com/polyphony-chat/chorus/issues/90)
### Moderation
- [ ] Channel moderation (slow mode, etc.)
- [ ] User sanctions (mute, kick, ban)
- [ ] Audit logs
### User Management
- [ ] [User profile customization](https://github.com/polyphony-chat/chorus/issues/41)
- [x] Gettings users and user profiles
- [x] [Friend requests](https://github.com/polyphony-chat/chorus/issues/92)
- [x] [Blocking users](https://github.com/polyphony-chat/chorus/issues/92)
- [ ] User presence (online, offline, idle, etc.)
- [ ] User status (custom status, etc.)
- [x] Account deletion
### Embeds and Rich Content
- [x] Sending rich content in messages (links, images, videos)
- [ ] Customizing embed appearance (title, description, color, fields)
### Additional Features
- [ ] Server discovery
- [ ] Server templates
### Notifications and Push Notifications
- [ ] Notification settings management
### Voice and Video
- [ ] [Voice chat support](https://github.com/polyphony-chat/chorus/issues/49)
- [ ] [Video chat support](https://github.com/polyphony-chat/chorus/issues/49)
### Webhooks
- [ ] Webhook creation and management
- [ ] Handling incoming webhook events
### Permissions and Roles
- [x] [Role management](https://github.com/polyphony-chat/chorus/issues/46) (creation, deletion, modification)
- [x] [Permission management](https://github.com/polyphony-chat/chorus/issues/46) (assigning and revoking permissions)
- [x] [Channel-specific permissions](https://github.com/polyphony-chat/chorus/issues/88)
- [x] Role-based access control
### Documentation and Examples
- [ ] Comprehensive documentation
- [ ] Example usage and code snippets
- [ ] Tutorials and guides
### Guild Management
- [x] Guild creation
- [x] Guild deletion
- [ ] [Guild settings (name, description, icon, etc.)](https://github.com/polyphony-chat/chorus/issues/43)
- [ ] Guild invites
[Rust]: https://img.shields.io/badge/Rust-orange?style=plastic&logo=rust
[Rust-url]: https://www.rust-lang.org/
[build-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/chorus/rust.yml?style=flat
[build-url]: https://github.com/polyphony-chat/chorus/blob/main/.github/workflows/rust.yml
[contributors-shield]: https://img.shields.io/github/contributors/polyphony-chat/chorus.svg?style=flat
[contributors-url]: https://github.com/polyphony-chat/chorus/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/polyphony-chat/chorus.svg?style=flat
[forks-url]: https://github.com/polyphony-chat/chorus/network/members
[stars-shield]: https://img.shields.io/github/stars/polyphony-chat/chorus.svg?style=flat
[stars-url]: https://github.com/polyphony-chat/chorus/stargazers
[issues-shield]: https://img.shields.io/github/issues/polyphony-chat/chorus.svg?style=flat
[issues-url]: https://github.com/polyphony-chat/chorus/issues
[license-shield]: https://img.shields.io/github/license/polyphony-chat/chorus.svg?style=f;at
[license-url]: https://github.com/polyphony-chat/chorus/blob/master/LICENSE
[Discord]: https://dcbadge.vercel.app/api/server/m3FpcapGDD?style=flat
[Discord-invite]: https://discord.com/invite/m3FpcapGDD
### Moderation
- [ ] Channel moderation (slow mode, etc.)
- [ ] User sanctions (mute, kick, ban)
- [ ] Audit logs
### Embeds and Rich Content
- [x] Sending rich content in messages (links, images, videos)
- [ ] Customizing embed appearance (title, description, color, fields)
### Webhooks
- [ ] Webhook creation and management
- [ ] Handling incoming webhook events
### Documentation and Examples
- [ ] Comprehensive documentation
- [ ] Example usage and code snippets
- [ ] Tutorials and guides
[Rust]: https://img.shields.io/badge/Rust-orange?style=plastic&logo=rust
[Rust-url]: https://www.rust-lang.org/
[build-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/chorus/build_and_test.yml?style=flat
[build-url]: https://github.com/polyphony-chat/chorus/blob/main/.github/workflows/build_and_test.yml
[clippy-shield]: https://img.shields.io/github/actions/workflow/status/polyphony-chat/chorus/clippy.yml?style=flat
[clippy-url]: https://github.com/polyphony-chat/chorus/blob/main/.github/workflows/clippy.yml
[contributors-shield]: https://img.shields.io/github/contributors/polyphony-chat/chorus.svg?style=flat
[contributors-url]: https://github.com/polyphony-chat/chorus/graphs/contributors
[forks-shield]: https://img.shields.io/github/forks/polyphony-chat/chorus.svg?style=flat
[forks-url]: https://github.com/polyphony-chat/chorus/network/members
[stars-shield]: https://img.shields.io/github/stars/polyphony-chat/chorus.svg?style=flat
[stars-url]: https://github.com/polyphony-chat/chorus/stargazers
[issues-shield]: https://img.shields.io/github/issues/polyphony-chat/chorus.svg?style=flat
[issues-url]: https://github.com/polyphony-chat/chorus/issues
[license-shield]: https://img.shields.io/github/license/polyphony-chat/chorus.svg?style=f;at
[license-url]: https://github.com/polyphony-chat/chorus/blob/master/LICENSE
[Discord]: https://dcbadge.vercel.app/api/server/m3FpcapGDD?style=flat
[Discord-invite]: https://discord.com/invite/m3FpcapGDD
</details>

View File

@ -3,8 +3,8 @@ use chorus::{
gateway::{Gateway, Observer},
types::{GatewayIdentifyPayload, GatewayReady},
};
use std::sync::Arc;
use tokio::{self, sync::Mutex};
use std::{sync::Arc, time::Duration};
use tokio::{self, time::sleep};
// This example creates a simple gateway connection and a basic observer struct
@ -17,7 +17,7 @@ pub struct ExampleObserver {}
// One struct can be an observer of multiple websocketevents, if needed
impl Observer<GatewayReady> for ExampleObserver {
// After we subscribe to an event this function is called every time we receive it
fn update(&mut self, _data: &GatewayReady) {
fn update(&self, _data: &GatewayReady) {
println!("Observed Ready!");
}
}
@ -33,8 +33,8 @@ async fn main() {
// Create an instance of our observer
let observer = ExampleObserver {};
// Because observers have to reside in between the main and gateway thread, (they have to be shared between both) we need to put them in an Arc<Mutex>
let shared_observer = Arc::new(Mutex::new(observer));
// Share ownership of the observer with the gateway
let shared_observer = Arc::new(observer);
// Subscribe our observer to the Ready event on this gateway
// From now on observer.update(data) will be called every time we receive the Ready event
@ -44,8 +44,7 @@ async fn main() {
.await
.session
.ready
.subscribe(shared_observer)
.unwrap();
.subscribe(shared_observer);
// Authenticate so we will receive any events
let token = "SecretToken".to_string();
@ -54,5 +53,7 @@ async fn main() {
gateway.send_identify(identify).await;
// Do something on the main thread so we don't quit
loop {}
loop {
sleep(Duration::MAX).await;
}
}

View File

@ -1,4 +1,7 @@
use std::time::Duration;
use chorus::{self, gateway::Gateway, types::GatewayIdentifyPayload};
use tokio::time::sleep;
/// This example creates a simple gateway connection and a session with an Identify event
#[tokio::main]
@ -26,5 +29,7 @@ async fn main() {
gateway.send_identify(identify).await;
// Do something on the main thread so we don't quit
loop {}
loop {
sleep(Duration::MAX).await;
}
}

View File

@ -17,7 +17,7 @@ impl Instance {
) -> Result<UserMeta, ChorusLibError> {
let json_schema = json!(login_schema);
let client = Client::new();
let endpoint_url = self.urls.get_api().to_string() + "/auth/login";
let endpoint_url = self.urls.api.clone() + "/auth/login";
let request_builder = client.post(endpoint_url).body(json_schema.to_string());
// 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

View File

@ -25,7 +25,7 @@ impl Instance {
) -> Result<UserMeta, ChorusLibError> {
let json_schema = json!(register_schema);
let client = Client::new();
let endpoint_url = self.urls.get_api().to_string() + "/auth/register";
let endpoint_url = self.urls.api.clone() + "/auth/register";
let request_builder = client.post(endpoint_url).body(json_schema.to_string());
// 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
@ -59,11 +59,10 @@ impl Instance {
return Err(ChorusLibError::InvalidFormBodyError { error_type, error });
}
let user_object = self.get_user(token.clone(), None).await.unwrap();
let settings =
UserMeta::get_settings(&token, &self.urls.get_api().to_string(), &mut self.limits)
.await
.unwrap();
let user: UserMeta = UserMeta::new(
let settings = UserMeta::get_settings(&token, &self.urls.api, &mut self.limits)
.await
.unwrap();
let user = UserMeta::new(
Rc::new(RefCell::new(self.clone())),
token.clone(),
cloned_limits,

View File

@ -10,9 +10,7 @@ use crate::{
impl Channel {
pub async fn get(user: &mut UserMeta, channel_id: &str) -> Result<Channel, ChorusLibError> {
let belongs_to = user.belongs_to.borrow_mut();
let url = belongs_to.urls.get_api().to_string();
drop(belongs_to);
let url = user.belongs_to.borrow_mut().urls.api.clone();
let request = Client::new()
.get(format!("{}/channels/{}/", url, channel_id))
.bearer_auth(user.token());
@ -44,24 +42,19 @@ impl Channel {
///
/// # Returns
///
/// An `Option` that contains an `ChorusLibError` if an error occurred during the request, or `None` if the request was successful.
pub async fn delete(self, user: &mut UserMeta) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow_mut();
/// 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) -> Result<(), ChorusLibError> {
let request = Client::new()
.delete(format!(
"{}/channels/{}/",
belongs_to.urls.get_api(),
self.id.to_string()
user.belongs_to.borrow_mut().urls.api,
self.id
))
.bearer_auth(user.token());
drop(belongs_to);
let response =
common::handle_request(request, user, crate::api::limits::LimitType::Channel).await;
if response.is_err() {
return Some(response.err().unwrap());
} else {
return None;
}
common::handle_request_as_result(request, user, crate::api::limits::LimitType::Channel)
.await;
response
}
/// Modifies a channel.
@ -83,16 +76,14 @@ impl Channel {
channel_id: &str,
user: &mut UserMeta,
) -> Result<Channel, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let request = Client::new()
.patch(format!(
"{}/channels/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
channel_id
))
.bearer_auth(user.token())
.body(to_string(&modify_data).unwrap());
drop(belongs_to);
let channel = common::deserialize_response::<Channel>(
request,
user,

View File

@ -25,9 +25,7 @@ impl Message {
message: &mut MessageSendSchema,
files: Option<Vec<PartialDiscordFileAttachment>>,
) -> Result<Message, crate::errors::ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url_api = belongs_to.urls.get_api().to_string();
drop(belongs_to);
let url_api = user.belongs_to.borrow().urls.api.clone();
if files.is_none() {
let request = Client::new()

View File

@ -2,7 +2,7 @@ use reqwest::Client;
use serde_json::to_string;
use crate::{
api::handle_request,
api::{handle_request, handle_request_as_result},
errors::ChorusLibError,
instance::UserMeta,
types::{self, PermissionOverwrite},
@ -19,35 +19,30 @@ impl types::Channel {
///
/// # Returns
///
/// This function returns [`None`] if the request is successful, otherwise it returns a [`ChorusLibError`] instance.
/// This function returns a result that is either [`Ok(())`] if the request is successful, or an [`Err(ChorusLibError)`].
pub async fn edit_permissions(
user: &mut UserMeta,
channel_id: &str,
overwrite: PermissionOverwrite,
) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow_mut();
let url = format!(
"{}/channels/{}/permissions/{}",
belongs_to.urls.get_api(),
channel_id,
overwrite.id
);
drop(belongs_to);
) -> Result<(), ChorusLibError> {
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 Some(ChorusLibError::FormCreationError {
return Err(ChorusLibError::FormCreationError {
error: e.to_string(),
});
}
};
let request = Client::new().put(url).bearer_auth(user.token()).body(body);
match handle_request(request, user, crate::api::limits::LimitType::Channel).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/// Deletes a permission overwrite for a channel.
@ -60,26 +55,19 @@ impl types::Channel {
///
/// # Returns
///
/// This function returns [`None`] if the request is successful, otherwise it returns a [`ChorusLibError`] instance.
/// This function returns a Result that is either [`Ok(())`] if the request is successfulm or an [`Err(ChorusLibError)`].
pub async fn delete_permission(
user: &mut UserMeta,
channel_id: &str,
overwrite_id: &str,
) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow_mut();
) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/permissions/{}",
belongs_to.urls.get_api(),
user.belongs_to.borrow_mut().urls.api,
channel_id,
overwrite_id
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
match handle_request(request, user, crate::api::limits::LimitType::Channel).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
}

View File

@ -1,7 +1,7 @@
use reqwest::Client;
use crate::{
api::{handle_request, handle_request_as_option},
api::{handle_request, handle_request_as_result},
errors::ChorusLibError,
instance::UserMeta,
types,
@ -24,28 +24,21 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
An `Option` [`crate::errors::ChorusLibError`] if something went wrong.
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) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
pub async fn delete_all(&self, user: &mut UserMeta) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
match handle_request(request, user, crate::api::limits::LimitType::Channel).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/**
@ -58,28 +51,21 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
A [`crate::errors::ChorusLibError`] if something went wrong.
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) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id,
emoji
);
drop(belongs_to);
let request = Client::new().get(url).bearer_auth(user.token());
match handle_request(request, user, crate::api::limits::LimitType::Channel).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/**
@ -93,29 +79,26 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
A [`crate::errors::ChorusLibError`] if something went wrong.
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) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
pub async fn delete_emoji(
&self,
emoji: &str,
user: &mut UserMeta,
) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id,
emoji
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
match handle_request(request, user, crate::api::limits::LimitType::Channel).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/**
@ -132,25 +115,21 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
A `Result` containing a [`reqwest::Response`] or a [`crate::errors::ChorusLibError`].
Returns a 204 empty response on success.
Fires a Message Reaction Add Gateway event.
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) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
pub async fn create(&self, emoji: &str, user: &mut UserMeta) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/@me/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id,
emoji
);
drop(belongs_to);
let request = Client::new().put(url).bearer_auth(user.token());
handle_request_as_option(request, user, crate::api::limits::LimitType::Channel).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/**
@ -163,25 +142,22 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
A `Result` containing a [`reqwest::Response`] or a [`crate::errors::ChorusLibError`].
Returns a 204 empty response on success.
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) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
pub async fn remove(&self, emoji: &str, user: &mut UserMeta) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/@me/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id,
emoji
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
handle_request_as_option(request, user, crate::api::limits::LimitType::Channel).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
/**
@ -197,8 +173,7 @@ impl ReactionMeta {
* `user` - A mutable reference to a [`UserMeta`] instance.
# Returns
A `Result` containing a [`reqwest::Response`] or a [`crate::errors::ChorusLibError`].
Returns a 204 empty response on success.
A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`].
Fires a Message Reaction Remove Gateway event.
# Reference
@ -209,18 +184,16 @@ impl ReactionMeta {
user_id: &str,
emoji: &str,
user: &mut UserMeta,
) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
) -> Result<(), ChorusLibError> {
let url = format!(
"{}/channels/{}/messages/{}/reactions/{}/{}",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
self.channel_id,
self.message_id,
emoji,
user_id
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
handle_request_as_option(request, user, crate::api::limits::LimitType::Channel).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await
}
}

View File

@ -12,30 +12,25 @@ pub async fn handle_request(
user: &mut UserMeta,
limit_type: LimitType,
) -> Result<reqwest::Response, crate::errors::ChorusLibError> {
let mut belongs_to = user.belongs_to.borrow_mut();
match LimitedRequester::send_request(
LimitedRequester::send_request(
request,
limit_type,
&mut belongs_to.limits,
&mut user.belongs_to.borrow_mut().limits,
&mut user.limits,
)
.await
{
Ok(response) => return Ok(response),
Err(e) => return Err(e),
}
}
/// Sends a request to wherever it needs to go. Returns [`None`] on success and
/// [`Some(ChorusLibError)`] on failure.
pub async fn handle_request_as_option(
/// 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,
) -> Option<ChorusLibError> {
) -> Result<(), ChorusLibError> {
match handle_request(request, user, limit_type).await {
Ok(_) => None,
Err(e) => Some(ChorusLibError::InvalidResponseError {
Ok(_) => Ok(()),
Err(e) => Err(ChorusLibError::InvalidResponseError {
error: e.to_string(),
}),
}
@ -53,7 +48,7 @@ pub async fn deserialize_response<T: for<'a> Deserialize<'a>>(
return Err(ChorusLibError::InvalidResponseError {
error: format!(
"Error while trying to process the HTTP response into a String: {}",
e.to_string()
e
),
});
}
@ -64,7 +59,7 @@ pub async fn deserialize_response<T: for<'a> Deserialize<'a>>(
return Err(ChorusLibError::InvalidResponseError {
error: format!(
"Error while trying to deserialize the JSON response into T: {}",
e.to_string()
e
),
})
}

View File

@ -4,7 +4,7 @@ use serde_json::to_string;
use crate::api::deserialize_response;
use crate::api::handle_request;
use crate::api::handle_request_as_option;
use crate::api::handle_request_as_result;
use crate::api::limits::Limits;
use crate::errors::ChorusLibError;
use crate::instance::UserMeta;
@ -32,9 +32,7 @@ impl Guild {
user: &mut UserMeta,
guild_create_schema: GuildCreateSchema,
) -> Result<Guild, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!("{}/guilds/", belongs_to.urls.get_api());
drop(belongs_to);
let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api);
let request = reqwest::Client::new()
.post(url.clone())
.bearer_auth(user.token.clone())
@ -52,7 +50,7 @@ impl Guild {
///
/// # Returns
///
/// An `Option` containing an `ChorusLibError` if an error occurred during the request, otherwise `None`.
/// An `Result` containing an `ChorusLibError` if an error occurred during the request, otherwise `()`.
///
/// # Example
///
@ -66,14 +64,16 @@ impl Guild {
/// None => println!("Guild deleted successfully"),
/// }
/// ```
pub async fn delete(user: &mut UserMeta, guild_id: &str) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!("{}/guilds/{}/delete/", belongs_to.urls.get_api(), guild_id);
drop(belongs_to);
pub async fn delete(user: &mut UserMeta, guild_id: &str) -> Result<(), ChorusLibError> {
let url = format!(
"{}/guilds/{}/delete/",
user.belongs_to.borrow().urls.api,
guild_id
);
let request = reqwest::Client::new()
.post(url.clone())
.bearer_auth(user.token.clone());
handle_request_as_option(request, user, crate::api::limits::LimitType::Guild).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
}
/// Sends a request to create a new channel in the guild.
@ -97,7 +97,7 @@ impl Guild {
let mut belongs_to = user.belongs_to.borrow_mut();
Channel::_create(
&user.token,
&format!("{}", belongs_to.urls.get_api()),
&format!("{}", belongs_to.urls.api),
&self.id.to_string(),
schema,
&mut user.limits,
@ -116,15 +116,13 @@ 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) -> Result<Vec<Channel>, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let request = Client::new()
.get(format!(
"{}/guilds/{}/channels/",
belongs_to.urls.get_api(),
self.id.to_string()
user.belongs_to.borrow().urls.api,
self.id
))
.bearer_auth(user.token());
drop(belongs_to);
let result = handle_request(request, user, crate::api::limits::LimitType::Channel)
.await
.unwrap();
@ -159,7 +157,7 @@ impl Guild {
pub async fn get(user: &mut UserMeta, guild_id: &str) -> Result<Guild, ChorusLibError> {
let mut belongs_to = user.belongs_to.borrow_mut();
Guild::_get(
&format!("{}", belongs_to.urls.get_api()),
&format!("{}", belongs_to.urls.api),
guild_id,
&user.token,
&mut user.limits,
@ -219,7 +217,7 @@ impl Channel {
let mut belongs_to = user.belongs_to.borrow_mut();
Channel::_create(
&user.token,
&format!("{}", belongs_to.urls.get_api()),
&format!("{}", belongs_to.urls.api),
guild_id,
schema,
&mut user.limits,

View File

@ -1,7 +1,7 @@
use reqwest::Client;
use crate::{
api::{deserialize_response, handle_request_as_option},
api::{deserialize_response, handle_request_as_result},
errors::ChorusLibError,
instance::UserMeta,
types,
@ -24,14 +24,12 @@ impl types::GuildMember {
guild_id: &str,
member_id: &str,
) -> Result<types::GuildMember, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!(
"{}/guilds/{}/members/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
guild_id,
member_id
);
drop(belongs_to);
let request = Client::new().get(url).bearer_auth(user.token());
deserialize_response::<types::GuildMember>(
request,
@ -52,24 +50,22 @@ impl types::GuildMember {
///
/// # Returns
///
/// An `Option` containing a `ChorusLibError` if the request fails, or `None` if the request succeeds.
/// An `Result` containing a `ChorusLibError` if the request fails, or `()` if the request succeeds.
pub async fn add_role(
user: &mut UserMeta,
guild_id: &str,
member_id: &str,
role_id: &str,
) -> Option<ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
) -> Result<(), ChorusLibError> {
let url = format!(
"{}/guilds/{}/members/{}/roles/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
guild_id,
member_id,
role_id
);
drop(belongs_to);
let request = Client::new().put(url).bearer_auth(user.token());
handle_request_as_option(request, user, crate::api::limits::LimitType::Guild).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
}
/// Removes a role from a guild member.
@ -83,23 +79,21 @@ impl types::GuildMember {
///
/// # Returns
///
/// An `Option` containing a `ChorusLibError` if the request fails, or `None` if the request succeeds.
/// A `Result` containing a `ChorusLibError` if the request fails, or `()` if the request succeeds.
pub async fn remove_role(
user: &mut UserMeta,
guild_id: &str,
member_id: &str,
role_id: &str,
) -> Option<crate::errors::ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
) -> Result<(), crate::errors::ChorusLibError> {
let url = format!(
"{}/guilds/{}/members/{}/roles/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
guild_id,
member_id,
role_id
);
drop(belongs_to);
let request = Client::new().delete(url).bearer_auth(user.token());
handle_request_as_option(request, user, crate::api::limits::LimitType::Guild).await
handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await
}
}

View File

@ -27,9 +27,11 @@ impl types::RoleObject {
user: &mut UserMeta,
guild_id: &str,
) -> Result<Option<Vec<RoleObject>>, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!("{}/guilds/{}/roles/", belongs_to.urls.get_api(), guild_id);
drop(belongs_to);
let url = format!(
"{}/guilds/{}/roles/",
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,
@ -64,14 +66,12 @@ impl types::RoleObject {
guild_id: &str,
role_id: &str,
) -> Result<RoleObject, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!(
"{}/guilds/{}/roles/{}/",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
guild_id,
role_id
);
drop(belongs_to);
let request = Client::new().get(url).bearer_auth(user.token());
deserialize_response(request, user, crate::api::limits::LimitType::Guild).await
}
@ -96,17 +96,16 @@ impl types::RoleObject {
guild_id: &str,
role_create_schema: RoleCreateModifySchema,
) -> Result<RoleObject, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!("{}/guilds/{}/roles/", belongs_to.urls.get_api(), guild_id);
drop(belongs_to);
let body = match to_string::<RoleCreateModifySchema>(&role_create_schema) {
Ok(string) => string,
Err(e) => {
return Err(ChorusLibError::FormCreationError {
error: e.to_string(),
});
let url = format!(
"{}/guilds/{}/roles/",
user.belongs_to.borrow().urls.api,
guild_id
);
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
ChorusLibError::FormCreationError {
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
}
@ -131,17 +130,16 @@ impl types::RoleObject {
guild_id: &str,
role_position_update_schema: types::RolePositionUpdateSchema,
) -> Result<RoleObject, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!("{}/guilds/{}/roles/", belongs_to.urls.get_api(), guild_id);
let body = match to_string(&role_position_update_schema) {
Ok(body) => body,
Err(e) => {
return Err(ChorusLibError::FormCreationError {
error: e.to_string(),
});
let url = format!(
"{}/guilds/{}/roles/",
user.belongs_to.borrow().urls.api,
guild_id
);
let body = to_string(&role_position_update_schema).map_err(|e| {
ChorusLibError::FormCreationError {
error: e.to_string(),
}
};
drop(belongs_to);
})?;
let request = Client::new()
.patch(url)
.bearer_auth(user.token())
@ -172,22 +170,17 @@ impl types::RoleObject {
role_id: &str,
role_create_schema: RoleCreateModifySchema,
) -> Result<RoleObject, ChorusLibError> {
let belongs_to = user.belongs_to.borrow();
let url = format!(
"{}/guilds/{}/roles/{}",
belongs_to.urls.get_api(),
user.belongs_to.borrow().urls.api,
guild_id,
role_id
);
drop(belongs_to);
let body = match to_string::<RoleCreateModifySchema>(&role_create_schema) {
Ok(string) => string,
Err(e) => {
return Err(ChorusLibError::FormCreationError {
error: e.to_string(),
});
let body = to_string::<RoleCreateModifySchema>(&role_create_schema).map_err(|e| {
ChorusLibError::FormCreationError {
error: e.to_string(),
}
};
})?;
let request = Client::new()
.patch(url)
.bearer_auth(user.token())

View File

@ -15,7 +15,7 @@ impl Instance {
&self,
) -> Result<GeneralConfiguration, ChorusLibError> {
let client = Client::new();
let endpoint_url = self.urls.get_api().to_string() + "/policies/instance/";
let endpoint_url = self.urls.api.clone() + "/policies/instance/";
let request = match client.get(&endpoint_url).send().await {
Ok(result) => result,
Err(e) => {
@ -33,7 +33,6 @@ impl Instance {
}
let body = request.text().await.unwrap();
let instance_policies_schema: GeneralConfiguration = from_str(&body).unwrap();
Ok(instance_policies_schema)
Ok(from_str::<GeneralConfiguration>(&body).unwrap())
}
}

View File

@ -195,31 +195,31 @@ pub mod limits {
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,
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,
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,
}
}
}
@ -256,31 +256,31 @@ pub mod limits {
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,
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,
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,
}
}
@ -311,7 +311,7 @@ pub mod limits {
/// 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 url_parsed = crate::UrlBundle::parse_url(api_url) + "/policies/instance/limits";
let result = client
.get(url_parsed)
.send()

View File

@ -1,6 +1,12 @@
use reqwest::Client;
use serde_json::to_string;
use crate::{api::deserialize_response, errors::ChorusLibError, instance::UserMeta, types};
use crate::{
api::{deserialize_response, handle_request_as_result},
errors::ChorusLibError,
instance::UserMeta,
types::{self, CreateUserRelationshipSchema, RelationshipType},
};
impl UserMeta {
/// Retrieves the mutual relationships between the authenticated user and the specified user.
@ -10,24 +16,134 @@ impl UserMeta {
/// * `user_id` - A string slice that holds the ID of the user to retrieve the mutual relationships with.
///
/// # Returns
/// This function returns a [`Option<Vec<Result<PublicUser, ChorusLibError>>>`].
/// This function returns a [`Result<Vec<PublicUser>, ChorusLibError>`].
pub async fn get_mutual_relationships(
&mut self,
user_id: &str,
) -> Result<Option<Vec<types::PublicUser>>, ChorusLibError> {
let belongs_to = self.belongs_to.borrow();
) -> Result<Vec<types::PublicUser>, ChorusLibError> {
let url = format!(
"{}/users/{}/relationships/",
belongs_to.urls.get_api(),
self.belongs_to.borrow().urls.api,
user_id
);
drop(belongs_to);
let request = Client::new().get(url).bearer_auth(self.token());
deserialize_response::<Option<Vec<types::PublicUser>>>(
deserialize_response::<Vec<types::PublicUser>>(
request,
self,
crate::api::limits::LimitType::Global,
)
.await
}
/// Retrieves the authenticated user's relationships.
///
/// # Returns
/// This function returns a [`Result<Vec<types::Relationship>, ChorusLibError>`].
pub async fn get_relationships(&mut self) -> Result<Vec<types::Relationship>, ChorusLibError> {
let url = format!(
"{}/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
}
/// Sends a friend request to a user.
///
/// # Arguments
///
/// * `schema` - A [`FriendRequestSendSchema`] struct that holds the information about the friend request to be sent.
///
/// # Returns
/// This function returns a [`Result`] that holds a [`ChorusLibError`] if the request fails.
pub async fn send_friend_request(
&mut self,
schema: types::FriendRequestSendSchema,
) -> Result<(), ChorusLibError> {
let url = format!(
"{}/users/@me/relationships/",
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
}
/// Modifies the relationship between the authenticated user and the specified user.
///
/// # Arguments
///
/// * `user_id` - A string slice that holds the ID of the user to modify the relationship with.
/// * `relationship_type` - A [`RelationshipType`] enum that specifies the type of relationship to modify.
/// * [`RelationshipType::None`]: Removes the relationship between the two users.
/// * [`RelationshipType::Friends`] | [`RelationshipType::Incoming`] | [`RelationshipType::Outgoing`]:
/// Either accepts an incoming friend request, or sends a new friend request, if there is no
/// incoming friend request from the specified `user_id`.
/// * [`RelationshipType::Blocked`]: Blocks the specified user_id.
///
/// # Returns
/// This function returns an [`Result`] that holds a [`ChorusLibError`] if the request fails.
pub async fn modify_user_relationship(
&mut self,
user_id: &str,
relationship_type: RelationshipType,
) -> Result<(), ChorusLibError> {
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
}
RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => {
let body = CreateUserRelationshipSchema {
relationship_type: None, // Selecting 'None' here will accept an incoming FR or send a new FR.
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
}
RelationshipType::Blocked => {
let body = CreateUserRelationshipSchema {
relationship_type: Some(RelationshipType::Blocked),
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
}
RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()),
}
}
/// Removes the relationship between the authenticated user and the specified user.
///
/// # Arguments
///
/// * `user_id` - A string slice that holds the ID of the user to remove the relationship with.
///
/// # Returns
/// This function returns a [`Result`] that holds a [`ChorusLibError`] if the request fails.
pub async fn remove_relationship(&mut self, user_id: &str) -> Result<(), ChorusLibError> {
let url = format!(
"{}/users/@me/relationships/{}/",
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
}
}

View File

@ -2,7 +2,7 @@ use reqwest::Client;
use serde_json::to_string;
use crate::{
api::{deserialize_response, handle_request_as_option, limits::Limits},
api::{deserialize_response, handle_request_as_result, limits::Limits},
errors::ChorusLibError,
instance::{Instance, UserMeta},
limit::LimitedRequester,
@ -52,10 +52,7 @@ impl UserMeta {
return Err(ChorusLibError::PasswordRequiredError);
}
let request = Client::new()
.patch(format!(
"{}/users/@me/",
self.belongs_to.borrow_mut().urls.get_api()
))
.patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api))
.body(to_string(&modify_schema).unwrap())
.bearer_auth(self.token());
let user_updated =
@ -74,14 +71,15 @@ impl UserMeta {
///
/// # Returns
///
/// Returns `None` if the user was successfully deleted, or an `ChorusLibError` if an error occurred.
pub async fn delete(mut self) -> Option<ChorusLibError> {
let belongs_to = self.belongs_to.borrow();
/// Returns `()` if the user was successfully deleted, or a `ChorusLibError` if an error occurred.
pub async fn delete(mut self) -> Result<(), ChorusLibError> {
let request = Client::new()
.post(format!("{}/users/@me/delete/", belongs_to.urls.get_api()))
.post(format!(
"{}/users/@me/delete/",
self.belongs_to.borrow().urls.api
))
.bearer_auth(self.token());
drop(belongs_to);
handle_request_as_option(request, &mut self, crate::api::limits::LimitType::Ip).await
handle_request_as_result(request, &mut self, crate::api::limits::LimitType::Ip).await
}
}
@ -90,7 +88,7 @@ impl User {
let mut belongs_to = user.belongs_to.borrow_mut();
User::_get(
&user.token(),
&format!("{}", belongs_to.urls.get_api()),
&format!("{}", belongs_to.urls.api),
&mut belongs_to.limits,
id,
)
@ -103,12 +101,11 @@ impl User {
limits_instance: &mut Limits,
id: Option<&String>,
) -> Result<User, ChorusLibError> {
let url: String;
if id.is_none() {
url = format!("{}/users/@me/", url_api);
let url = if id.is_none() {
format!("{}/users/@me/", url_api)
} else {
url = format!("{}/users/{}", url_api, id.unwrap());
}
format!("{}/users/{}", url_api, id.unwrap())
};
let request = reqwest::Client::new().get(url).bearer_auth(token);
let mut cloned_limits = limits_instance.clone();
match LimitedRequester::send_request(
@ -166,12 +163,6 @@ impl Instance {
token: String,
id: Option<&String>,
) -> Result<User, ChorusLibError> {
User::_get(
&token,
&self.urls.get_api().to_string(),
&mut self.limits,
id,
)
.await
User::_get(&token, &self.urls.api, &mut self.limits, id).await
}
}

View File

@ -18,13 +18,14 @@ custom_error! {
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}",
RateLimited{bucket:String} = "Ratelimited on Bucket {bucket}",
MultipartCreationError{error: String} = "Got an error whilst creating the form: {}",
FormCreationError{error: String} = "Got an error whilst creating the form: {}",
MultipartCreationError{error: String} = "Got an error whilst creating the form: {error}",
FormCreationError{error: String} = "Got an error whilst creating the form: {error}",
TokenExpired = "Token expired, invalid or not found.",
NoPermission = "You do not have the permissions needed to perform this action.",
NotFound{error: String} = "The provided resource hasn't been 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: {}",
InvalidResponseError{error: String} = "The response is malformed and cannot be processed. Error: {error}",
InvalidArgumentsError{error: String} = "Invalid arguments were provided. Error: {error}"
}
custom_error! {

View File

@ -3,6 +3,7 @@ use crate::errors::ObserverError;
use crate::gateway::events::Events;
use crate::types;
use crate::types::WebSocketEvent;
use std::any::Any;
use std::sync::Arc;
use futures_util::stream::SplitSink;
@ -72,11 +73,9 @@ const GATEWAY_LAZY_REQUEST: u8 = 14;
/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms
const HEARTBEAT_ACK_TIMEOUT: u128 = 2000;
/// Represents a messsage received from the gateway. This will be either a [GatewayReceivePayload], containing events, or a [GatewayError].
/// This struct is used internally when handling messages.
#[derive(Clone, Debug)]
/**
Represents a messsage received from the gateway. This will be either a [GatewayReceivePayload], containing events, or a [GatewayError].
This struct is used internally when handling messages.
*/
pub struct GatewayMessage {
/// The message we received from the server
message: tokio_tungstenite::tungstenite::Message,
@ -94,60 +93,36 @@ impl GatewayMessage {
let content = self.message.to_string();
// Some error strings have dots on the end, which we don't care about
let processed_content = content.clone().to_lowercase().replace(".", "");
let processed_content = content.to_lowercase().replace('.', "");
match processed_content.as_str() {
"unknown error" | "4000" => {
return Some(GatewayError::UnknownError);
}
"unknown opcode" | "4001" => {
return Some(GatewayError::UnknownOpcodeError);
}
"unknown error" | "4000" => Some(GatewayError::UnknownError),
"unknown opcode" | "4001" => Some(GatewayError::UnknownOpcodeError),
"decode error" | "error while decoding payload" | "4002" => {
return Some(GatewayError::DecodeError);
}
"not authenticated" | "4003" => {
return Some(GatewayError::NotAuthenticatedError);
}
"authentication failed" | "4004" => {
return Some(GatewayError::AuthenticationFailedError);
}
"already authenticated" | "4005" => {
return Some(GatewayError::AlreadyAuthenticatedError);
}
"invalid seq" | "4007" => {
return Some(GatewayError::InvalidSequenceNumberError);
}
"rate limited" | "4008" => {
return Some(GatewayError::RateLimitedError);
}
"session timed out" | "4009" => {
return Some(GatewayError::SessionTimedOutError);
}
"invalid shard" | "4010" => {
return Some(GatewayError::InvalidShardError);
}
"sharding required" | "4011" => {
return Some(GatewayError::ShardingRequiredError);
}
"invalid api version" | "4012" => {
return Some(GatewayError::InvalidAPIVersionError);
Some(GatewayError::DecodeError)
}
"not authenticated" | "4003" => Some(GatewayError::NotAuthenticatedError),
"authentication failed" | "4004" => Some(GatewayError::AuthenticationFailedError),
"already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticatedError),
"invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumberError),
"rate limited" | "4008" => Some(GatewayError::RateLimitedError),
"session timed out" | "4009" => Some(GatewayError::SessionTimedOutError),
"invalid shard" | "4010" => Some(GatewayError::InvalidShardError),
"sharding required" | "4011" => Some(GatewayError::ShardingRequiredError),
"invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersionError),
"invalid intent(s)" | "invalid intent" | "4013" => {
return Some(GatewayError::InvalidIntentsError);
Some(GatewayError::InvalidIntentsError)
}
"disallowed intent(s)" | "disallowed intents" | "4014" => {
return Some(GatewayError::DisallowedIntentsError);
}
_ => {
return None;
Some(GatewayError::DisallowedIntentsError)
}
_ => None,
}
}
/// Returns whether or not the message is an error
pub fn is_error(&self) -> bool {
return self.error().is_some();
self.error().is_some()
}
/// Parses the message as a payload;
@ -168,17 +143,15 @@ impl GatewayMessage {
/// Returns whether or not the message is empty
pub fn is_empty(&self) -> bool {
return self.message.is_empty();
self.message.is_empty()
}
}
/// Represents a handle to a Gateway connection. A Gateway connection will create observable
/// [`GatewayEvents`](GatewayEvent), which you can subscribe to. Gateway events include all currently
/// implemented [Types] with the trait [`WebSocketEvent`]
/// Using this handle you can also send Gateway Events directly.
#[derive(Debug)]
/**
Represents a handle to a Gateway connection. A Gateway connection will create observable
[`GatewayEvents`](GatewayEvent), which you can subscribe to. Gateway events include all currently
implemented [Types] with the trait [`WebSocketEvent`]
Using this handle you can also send Gateway Events directly.
*/
pub struct GatewayHandle {
pub url: String,
pub events: Arc<Mutex<Events>>,
@ -308,6 +281,7 @@ pub struct Gateway {
}
impl Gateway {
#[allow(clippy::new_ret_no_self)]
pub async fn new(websocket_url: String) -> Result<GatewayHandle, GatewayError> {
let (websocket_stream, _) = match connect_async_tls_with_config(
&websocket_url,
@ -371,13 +345,13 @@ impl Gateway {
gateway.gateway_listen_task().await;
});
return Ok(GatewayHandle {
Ok(GatewayHandle {
url: websocket_url.clone(),
events: shared_events,
websocket_send: shared_websocket_send.clone(),
handle,
kill_send: kill_send.clone(),
});
})
}
/// The main gateway listener task;
@ -388,14 +362,10 @@ impl Gateway {
let msg = self.websocket_receive.next().await;
// This if chain can be much better but if let is unstable on stable rust
if msg.as_ref().is_some() {
if msg.as_ref().unwrap().is_ok() {
let msg_unwrapped = msg.unwrap().unwrap();
self.handle_message(GatewayMessage::from_tungstenite_message(msg_unwrapped))
.await;
continue;
}
if let Some(Ok(message)) = msg {
self.handle_message(GatewayMessage::from_tungstenite_message(message))
.await;
continue;
}
// We couldn't receive the next message or it was an error, something is wrong with the websocket, close
@ -422,8 +392,8 @@ impl Gateway {
return Err(data_deserialize_result.err().unwrap());
}
event.update_data(data_deserialize_result.unwrap()).await;
return Ok(());
event.notify(data_deserialize_result.unwrap()).await;
Ok(())
}
/// This handles a message as a websocket event and updates its events along with the events' observers
@ -444,11 +414,9 @@ impl Gateway {
if msg.is_error() {
println!("GW: Received error, connection will close..");
let error = msg.error();
let _error = msg.error();
match error {
_ => {}
}
{}
self.close().await;
return;
@ -1399,13 +1367,7 @@ impl Gateway {
sessions: result.unwrap(),
};
self.events
.lock()
.await
.session
.replace
.update_data(data)
.await;
self.events.lock().await.session.replace.notify(data).await;
}
"USER_UPDATE" => {
let event = &mut self.events.lock().await.user.update;
@ -1561,9 +1523,7 @@ impl Gateway {
}
}
/**
Handles sending heartbeats to the gateway in another thread
*/
/// Handles sending heartbeats to the gateway in another thread
struct HeartbeatHandler {
/// The heartbeat interval in milliseconds
pub heartbeat_interval: u128,
@ -1698,10 +1658,8 @@ impl HeartbeatHandler {
}
}
/**
Used for communications between the heartbeat and gateway thread.
Either signifies a sequence number update, a heartbeat ACK or a Heartbeat request by the server
*/
/// Used for communications between the heartbeat and gateway thread.
/// Either signifies a sequence number update, a heartbeat ACK or a Heartbeat request by the server
#[derive(Clone, Copy, Debug)]
struct HeartbeatThreadCommunication {
/// The opcode for the communication we received, if relevant
@ -1710,89 +1668,47 @@ struct HeartbeatThreadCommunication {
sequence_number: Option<u64>,
}
/**
Trait which defines the behavior of an Observer. An Observer is an object which is subscribed to
an Observable. The Observer is notified when the Observable's data changes.
In this case, the Observable is a [`GatewayEvent`], which is a wrapper around a WebSocketEvent.
*/
pub trait Observer<T: types::WebSocketEvent>: std::fmt::Debug {
fn update(&mut self, data: &T);
/// Trait which defines the behavior of an Observer. An Observer is an object which is subscribed to
/// an Observable. The Observer is notified when the Observable's data changes.
/// In this case, the Observable is a [`GatewayEvent`], which is a wrapper around a WebSocketEvent.
/// Note that `Debug` is used to tell `Observer`s apart when unsubscribing.
pub trait Observer<T>: Sync + Send + std::fmt::Debug {
fn update(&self, data: &T);
}
/** GatewayEvent is a wrapper around a WebSocketEvent. It is used to notify the observers of a
change in the WebSocketEvent. GatewayEvents are observable.
*/
/// GatewayEvent is a wrapper around a WebSocketEvent. It is used to notify the observers of a
/// change in the WebSocketEvent. GatewayEvents are observable.
#[derive(Default, Debug)]
pub struct GatewayEvent<T: types::WebSocketEvent> {
observers: Vec<Arc<Mutex<dyn Observer<T> + Sync + Send>>>,
pub event_data: T,
pub is_observed: bool,
pub struct GatewayEvent<T: WebSocketEvent> {
observers: Vec<Arc<dyn Observer<T>>>,
}
impl<T: types::WebSocketEvent> GatewayEvent<T> {
fn new(event_data: T) -> Self {
Self {
is_observed: false,
observers: Vec::new(),
event_data,
}
}
/**
Returns true if the GatewayEvent is observed by at least one Observer.
*/
impl<T: WebSocketEvent> GatewayEvent<T> {
/// Returns true if the GatewayEvent is observed by at least one Observer.
pub fn is_observed(&self) -> bool {
self.is_observed
!self.observers.is_empty()
}
/**
Subscribes an Observer to the GatewayEvent. Returns an error if the GatewayEvent is already
observed.
# Errors
Returns an error if the GatewayEvent is already observed.
Error type: [`ObserverError::AlreadySubscribedError`]
*/
pub fn subscribe(
&mut self,
observable: Arc<Mutex<dyn Observer<T> + Sync + Send>>,
) -> Result<(), ObserverError> {
if self.is_observed {
return Err(ObserverError::AlreadySubscribedError);
}
self.is_observed = true;
/// Subscribes an Observer to the GatewayEvent.
pub fn subscribe(&mut self, observable: Arc<dyn Observer<T>>) {
self.observers.push(observable);
Ok(())
}
/**
Unsubscribes an Observer from the GatewayEvent.
*/
pub fn unsubscribe(&mut self, observable: Arc<Mutex<dyn Observer<T> + Sync + Send>>) {
/// Unsubscribes an Observer from the GatewayEvent.
pub fn unsubscribe(&mut self, observable: &dyn Observer<T>) {
// .retain()'s closure retains only those elements of the vector, which have a different
// pointer value than observable.
// The usage of the debug format to compare the generic T of observers is quite stupid, but the only thing to compare between them is T and if T == T they are the same
// anddd there is no way to do that without using format
let to_remove = format!("{:?}", observable);
self.observers
.retain(|obs| !(format!("{:?}", obs) == format!("{:?}", &observable)));
self.is_observed = !self.observers.is_empty();
.retain(|obs| format!("{:?}", obs) != to_remove);
}
/**
Updates the GatewayEvent's data and notifies the observers.
*/
async fn update_data(&mut self, new_event_data: T) {
self.event_data = new_event_data;
self.notify().await;
}
/**
Notifies the observers of the GatewayEvent.
*/
async fn notify(&self) {
/// Notifies the observers of the GatewayEvent.
async fn notify(&self, new_event_data: T) {
for observer in &self.observers {
let mut observer_lock = observer.lock().await;
observer_lock.update(&self.event_data);
drop(observer_lock);
observer.update(&new_event_data);
}
}
}
@ -1961,23 +1877,23 @@ mod events {
#[cfg(test)]
mod example {
use super::*;
use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
#[derive(Debug)]
struct Consumer;
struct Consumer {
name: String,
events_received: AtomicI32,
}
impl Observer<types::GatewayResume> for Consumer {
fn update(&mut self, data: &types::GatewayResume) {
println!("{}", data.token)
fn update(&self, _data: &types::GatewayResume) {
self.events_received.fetch_add(1, Relaxed);
}
}
#[tokio::test]
async fn test_observer_behavior() {
let mut event = GatewayEvent::new(types::GatewayResume {
token: "start".to_string(),
session_id: "start".to_string(),
seq: "start".to_string(),
});
let mut event = GatewayEvent::default();
let new_data = types::GatewayResume {
token: "token_3276ha37am3".to_string(),
@ -1985,25 +1901,23 @@ mod example {
seq: "3".to_string(),
};
let consumer = Consumer;
let arc_mut_consumer = Arc::new(Mutex::new(consumer));
let consumer = Arc::new(Consumer {
name: "first".into(),
events_received: 0.into(),
});
event.subscribe(consumer.clone());
event.subscribe(arc_mut_consumer.clone()).unwrap();
let second_consumer = Arc::new(Consumer {
name: "second".into(),
events_received: 0.into(),
});
event.subscribe(second_consumer.clone());
event.notify().await;
event.notify(new_data.clone()).await;
event.unsubscribe(&*consumer);
event.notify(new_data).await;
event.update_data(new_data).await;
let second_consumer = Consumer;
let arc_mut_second_consumer = Arc::new(Mutex::new(second_consumer));
match event.subscribe(arc_mut_second_consumer.clone()).err() {
None => assert!(false),
Some(err) => println!("You cannot subscribe twice: {}", err),
}
event.unsubscribe(arc_mut_consumer.clone());
event.subscribe(arc_mut_second_consumer.clone()).unwrap();
assert_eq!(consumer.events_received.load(Relaxed), 1);
assert_eq!(second_consumer.events_received.load(Relaxed), 2);
}
}

View File

@ -7,14 +7,14 @@ use serde::{Deserialize, Serialize};
use crate::api::limits::Limits;
use crate::errors::{ChorusLibError, FieldFormatError};
use crate::types::{GeneralConfiguration, User, UserSettings};
use crate::URLBundle;
use crate::UrlBundle;
#[derive(Debug, Clone)]
/**
The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server.
*/
pub struct Instance {
pub urls: URLBundle,
pub urls: UrlBundle,
pub instance_info: GeneralConfiguration,
pub limits: Limits,
}
@ -26,20 +26,11 @@ 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) -> Result<Instance, ChorusLibError> {
pub async fn new(urls: UrlBundle) -> Result<Instance, ChorusLibError> {
let mut instance = Instance {
urls: urls.clone(),
instance_info: GeneralConfiguration::new(
// This is okay, because the instance_info will be overwritten by the instance_policies_schema() function.
"".to_string(),
None,
None,
None,
None,
None,
None,
None,
),
// Will be overwritten in the next step
instance_info: GeneralConfiguration::default(),
limits: Limits::check_limits(urls.api).await,
};
instance.instance_info = match instance.general_configuration_schema().await {

View File

@ -1,3 +1,5 @@
#![allow(clippy::module_inception)]
use url::{ParseError, Url};
#[cfg(feature = "client")]
@ -16,18 +18,18 @@ pub mod voice;
#[derive(Clone, Default, Debug, PartialEq, Eq)]
/// A URLBundle is a struct which bundles together the API-, Gateway- and CDN-URLs of a Spacebar
/// instance.
pub struct URLBundle {
pub struct UrlBundle {
pub api: String,
pub wss: String,
pub cdn: String,
}
impl URLBundle {
impl UrlBundle {
pub fn new(api: String, wss: String, cdn: String) -> Self {
Self {
api: URLBundle::parse_url(api),
wss: URLBundle::parse_url(wss),
cdn: URLBundle::parse_url(cdn),
api: UrlBundle::parse_url(api),
wss: UrlBundle::parse_url(wss),
cdn: UrlBundle::parse_url(cdn),
}
}
@ -42,13 +44,13 @@ impl URLBundle {
let url = match Url::parse(&url) {
Ok(url) => {
if url.scheme() == "localhost" {
return URLBundle::parse_url(format!("http://{}", url));
return UrlBundle::parse_url(format!("http://{}", url));
}
url
}
Err(ParseError::RelativeUrlWithoutBase) => {
let url_fmt = format!("http://{}", url);
return URLBundle::parse_url(url_fmt);
return UrlBundle::parse_url(url_fmt);
}
Err(_) => panic!("Invalid URL"),
};
@ -59,18 +61,6 @@ impl URLBundle {
}
url_string
}
pub fn get_api(&self) -> &str {
&self.api
}
pub fn get_cdn(&self) -> &str {
&self.cdn
}
pub fn get_wss(&self) -> &str {
&self.wss
}
}
#[cfg(test)]
@ -79,13 +69,13 @@ mod lib {
#[test]
fn test_parse_url() {
let mut result = URLBundle::parse_url(String::from("localhost:3000/"));
let mut result = UrlBundle::parse_url(String::from("localhost:3000/"));
assert_eq!(result, String::from("http://localhost:3000"));
result = URLBundle::parse_url(String::from("https://some.url.com/"));
result = UrlBundle::parse_url(String::from("https://some.url.com/"));
assert_eq!(result, String::from("https://some.url.com"));
result = URLBundle::parse_url(String::from("https://some.url.com/"));
result = UrlBundle::parse_url(String::from("https://some.url.com/"));
assert_eq!(result, String::from("https://some.url.com"));
result = URLBundle::parse_url(String::from("https://some.url.com"));
result = UrlBundle::parse_url(String::from("https://some.url.com"));
assert_eq!(result, String::from("https://some.url.com"));
}
}

View File

@ -82,13 +82,11 @@ impl LimitedRequester {
);
if !response.status().is_success() {
match response.status().as_u16() {
401 => return Err(ChorusLibError::TokenExpired),
403 => return Err(ChorusLibError::TokenExpired),
_ => {
return Err(ChorusLibError::ReceivedErrorCodeError {
error_code: response.status().as_str().to_string(),
});
}
401 => Err(ChorusLibError::TokenExpired),
403 => Err(ChorusLibError::TokenExpired),
_ => Err(ChorusLibError::ReceivedErrorCodeError {
error_code: response.status().as_str().to_string(),
}),
}
} else {
Ok(response)
@ -258,13 +256,13 @@ impl LimitedRequester {
mod rate_limit {
use serde_json::from_str;
use crate::{api::limits::Config, URLBundle};
use crate::{api::limits::Config, UrlBundle};
use super::*;
#[tokio::test]
async fn run_into_limit() {
let urls = URLBundle::new(
let urls = UrlBundle::new(
String::from("http://localhost:3001/api/"),
String::from("wss://localhost:3001/"),
String::from("http://localhost:3001/cdn"),
@ -286,19 +284,12 @@ mod rate_limit {
.await,
);
}
if request.is_some() {
match request.unwrap() {
Ok(_) => assert!(false),
Err(_) => assert!(true),
}
} else {
assert!(false)
}
assert!(matches!(request, Some(Err(_))));
}
#[tokio::test]
async fn test_send_request() {
let urls = URLBundle::new(
let urls = UrlBundle::new(
String::from("http://localhost:3001/api/"),
String::from("wss://localhost:3001/"),
String::from("http://localhost:3001/cdn"),

View File

@ -89,15 +89,15 @@ fn generate_pairs(obj: &Value, key: &str) -> Vec<ConfigEntity> {
fn pairs_to_config(pairs: Vec<ConfigEntity>) -> ConfigValue {
let mut value = Value::Object(Map::new());
for p in pairs {
let keys: Vec<&str> = p.key.split('_').collect();
for pair in pairs {
let keys: Vec<&str> = pair.key.split('_').collect();
let mut path = vec![];
for (i, &key) in keys.iter().enumerate() {
path.push(key);
if i == keys.len() - 1 {
insert_into(&mut value, &path, p.value.clone().unwrap_or(Value::Null));
insert_into(&mut value, &path, pair.value.clone().unwrap_or(Value::Null));
} else if keys[i + 1].parse::<usize>().is_ok() {
if !path_exists(&value, &path) {
insert_into(&mut value, &path, Value::Array(Vec::new()));
@ -182,6 +182,7 @@ mod test {
let pairs = generate_pairs(&v, "");
let cfg = pairs_to_config(pairs);
assert_eq!(cfg, c)
}
}

View File

@ -31,27 +31,3 @@ impl Default for GeneralConfiguration {
}
}
}
impl GeneralConfiguration {
pub fn new(
instance_name: String,
instance_description: Option<String>,
front_page: Option<String>,
tos_page: Option<String>,
correspondence_email: Option<String>,
correspondence_user_id: Option<String>,
image: Option<String>,
instance_id: Option<Snowflake>,
) -> Self {
Self {
instance_name,
instance_description,
front_page,
tos_page,
correspondence_email,
correspondence_user_id,
image,
instance_id,
}
}
}

View File

@ -1,4 +1,5 @@
use std::fmt::{Display, Formatter};
#[cfg(feature = "sqlx")]
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;

View File

@ -1,4 +1,4 @@
use bitflags::{bitflags, Flags};
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};

View File

@ -6,7 +6,7 @@ use crate::types::Snowflake;
use super::PublicUser;
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, Eq)]
/// See https://discord-userdoccers.vercel.app/resources/user#relationship-structure
pub struct Relationship {
pub id: Snowflake,
@ -17,7 +17,7 @@ pub struct Relationship {
pub since: Option<DateTime<Utc>>,
}
#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default)]
#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Default, Eq, PartialEq)]
#[repr(u8)]
/// See https://discord-userdoccers.vercel.app/resources/user#relationship-type
pub enum RelationshipType {

View File

@ -108,6 +108,7 @@ impl PermissionFlags {
self.contains(permission) || self.contains(PermissionFlags::ADMINISTRATOR)
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
self.bits().to_string()
}
@ -129,7 +130,7 @@ impl PermissionFlags {
pub fn from_vec(flags: Vec<PermissionFlags>) -> String {
let mut permissions: PermissionFlags = Default::default();
for flag in flags.iter() {
permissions = permissions | flag.clone();
permissions |= flag.clone();
}
permissions.to_string()
}

View File

@ -51,16 +51,18 @@ impl GatewayIdentifyPayload {
impl GatewayIdentifyPayload {
/// Creates an identify payload with the same default capabilities as the official client
pub fn default_w_client_capabilities() -> Self {
let mut def = Self::default();
def.capabilities = Some(8189); // Default capabilities for a client
def
Self {
capabilities: Some(8189), // Default capabilities for a client
..Self::default()
}
}
/// Creates an identify payload with all possible capabilities
pub fn default_w_all_capabilities() -> Self {
let mut def = Self::default();
def.capabilities = Some(i32::MAX); // Since discord uses bitwise for capabilities, this has almost every bit as 1, so all capabilities
def
Self {
capabilities: Some(i32::MAX), // Since discord uses bitwise for capabilities, this has almost every bit as 1, so all capabilities
..Self::default()
}
}
}
@ -148,22 +150,18 @@ impl GatewayIdentifyConnectionProps {
/// Returns the most common connection props so we can't be tracked
pub fn common() -> Self {
let mut default = Self::minimal();
// See https://www.useragents.me/#most-common-desktop-useragents
// 25% of the web
//default.browser_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36".to_string();
default.browser = String::from("Chrome");
default.browser_version = String::from("113.0.0.0");
default.system_locale = String::from("en-US");
default.os = String::from("Windows");
default.os_version = Some(String::from("10"));
default.client_build_number = 199933;
default.release_channel = String::from("stable");
return default;
Self {
// See https://www.useragents.me/#most-common-desktop-useragents
// 25% of the web
//default.browser_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36".to_string();
browser: String::from("Chrome"),
browser_version: String::from("113.0.0.0"),
system_locale: String::from("en-US"),
os: String::from("Windows"),
os_version: Some(String::from("10")),
client_build_number: 199933,
release_channel: String::from("stable"),
..Self::minimal()
}
}
}

View File

@ -1,5 +1,5 @@
use crate::types::{events::WebSocketEvent, UserStatus};
use crate::types::{Activity, PublicUser, Snowflake};
use crate::types::{Activity, ClientStatusObject, PublicUser, Snowflake};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
@ -26,12 +26,4 @@ pub struct PresenceUpdate {
pub client_status: ClientStatusObject,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
/// See https://discord.com/developers/docs/topics/gateway-events#client-status-object
pub struct ClientStatusObject {
pub desktop: Option<String>,
pub mobile: Option<String>,
pub web: Option<String>,
}
impl WebSocketEvent for PresenceUpdate {}

View File

@ -1,7 +1,7 @@
use crate::types::events::WebSocketEvent;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Default)]
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GatewayResume {
pub token: String,
pub session_id: String,

View File

@ -116,9 +116,39 @@ pub struct RegisterSchema {
promotional_email_opt_in: Option<bool>,
}
pub struct RegisterSchemaOptions {
pub username: String,
pub password: Option<String>,
pub consent: bool,
pub email: Option<String>,
pub fingerprint: Option<String>,
pub invite: Option<String>,
pub date_of_birth: Option<String>,
pub gift_code_sku_id: Option<String>,
pub captcha_key: Option<String>,
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 {
/**
Returns a new [`Result<RegisterSchema, FieldFormatError>`].
Create a new [`RegisterSchema`].
## Arguments
All but "String::username" and "bool::consent" are optional.
@ -129,47 +159,36 @@ impl RegisterSchema {
These constraints have been defined [in the Spacebar-API](https://docs.spacebar.chat/routes/)
*/
pub fn new(
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>,
) -> Result<RegisterSchema, FieldFormatError> {
let username = AuthUsername::new(username)?.username;
pub fn build(self) -> Result<RegisterSchema, FieldFormatError> {
let username = AuthUsername::new(self.username)?.username;
let email = if let Some(email) = email {
let email = if let Some(email) = self.email {
Some(AuthEmail::new(email)?.email)
} else {
None
};
let password = if let Some(password) = password {
let password = if let Some(password) = self.password {
Some(AuthPassword::new(password)?.password)
} else {
None
};
if !consent {
if !self.consent {
return Err(FieldFormatError::ConsentError);
}
Ok(RegisterSchema {
username,
password,
consent,
consent: self.consent,
email,
fingerprint,
invite,
date_of_birth,
gift_code_sku_id,
captcha_key,
promotional_email_opt_in,
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,
})
}
}

View File

@ -4,47 +4,18 @@ use crate::types::entities::{
AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment,
};
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct MessageSendSchema {
#[serde(rename = "type")]
message_type: Option<i32>,
content: Option<String>,
nonce: Option<String>,
tts: Option<bool>,
embeds: Option<Vec<Embed>>,
allowed_mentions: Option<AllowedMention>,
message_reference: Option<MessageReference>,
components: Option<Vec<Component>>,
sticker_ids: Option<Vec<String>>,
pub message_type: Option<i32>,
pub content: Option<String>,
pub nonce: Option<String>,
pub tts: Option<bool>,
pub embeds: Option<Vec<Embed>>,
pub allowed_mentions: Option<AllowedMention>,
pub message_reference: Option<MessageReference>,
pub components: Option<Vec<Component>>,
pub sticker_ids: Option<Vec<String>>,
pub attachments: Option<Vec<PartialDiscordFileAttachment>>,
}
// make a new() method for MessageSendSchema
impl MessageSendSchema {
pub fn new(
message_type: Option<i32>,
content: Option<String>,
nonce: Option<String>,
tts: Option<bool>,
embeds: Option<Vec<Embed>>,
allowed_mentions: Option<AllowedMention>,
message_reference: Option<MessageReference>,
components: Option<Vec<Component>>,
sticker_ids: Option<Vec<String>>,
attachments: Option<Vec<PartialDiscordFileAttachment>>,
) -> MessageSendSchema {
MessageSendSchema {
message_type,
content,
nonce,
tts,
embeds,
allowed_mentions,
message_reference,
components,
sticker_ids,
attachments,
}
}
}

View File

@ -65,18 +65,7 @@ mod schemas_tests {
#[test]
fn consent_false() {
assert_eq!(
RegisterSchema::new(
"Test".to_string(),
None,
false,
None,
None,
None,
None,
None,
None,
None,
),
RegisterSchema::builder("Test", false).build(),
Err(FieldFormatError::ConsentError)
);
}
@ -91,18 +80,11 @@ mod schemas_tests {
#[test]
fn valid_email() {
let reg = RegisterSchema::new(
"Testy".to_string(),
None,
true,
Some("me@mail.de".to_string()),
None,
None,
None,
None,
None,
None,
);
let reg = RegisterSchemaOptions {
email: Some("me@mail.de".to_string()),
..RegisterSchema::builder("Testy", true)
}
.build();
assert_ne!(reg, Err(FieldFormatError::EmailError));
}
}

View File

@ -1 +1,25 @@
use serde::{Deserialize, Serialize};
use crate::types::RelationshipType;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FriendRequestSendSchema {
pub username: String,
pub discriminator: Option<String>,
}
/// Represents the schema for the Create User Relationship route.
/// # Arguments
///
/// * relationship_type: The [`RelationshipType`] to create (defaults to -1, which accepts an existing or creates a new friend request)
/// * from_friend_suggestion: Whether the relationship was created from a friend suggestion (default false)
/// * friend_token: The friend token of the user to add a direct friend relationship to
///
/// See: [https://discord-userdoccers.vercel.app/resources/user#create-user-relationship](https://discord-userdoccers.vercel.app/resources/user#create-user-relationship)
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct CreateUserRelationshipSchema {
#[serde(rename = "type")]
pub relationship_type: Option<RelationshipType>,
pub from_friend_suggestion: Option<bool>,
pub friend_token: Option<String>,
}

View File

@ -1,6 +1,6 @@
pub use regexes::*;
pub use rights::Rights;
pub use snowflake::{DeconstructedSnowflake, Snowflake};
pub use snowflake::Snowflake;
pub mod jwt;
mod regexes;

View File

@ -1,22 +1,41 @@
use std::fmt::Display;
use std::{
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
};
use atomic::Atomic;
use bigdecimal::{Num, ToPrimitive, Zero};
use num_bigint::{BigInt, ToBigInt};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, TimeZone, Utc};
#[cfg(feature = "sqlx")]
use sqlx::Type;
/// 2015-01-01
const EPOCH: i64 = 1420070400000;
static WORKER_ID: u128 = 0;
static PROCESS_ID: u128 = 1;
lazy_static::lazy_static! {
static ref INCREMENT: Atomic<u128> = Atomic::default();
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// Unique identifier including a timestamp.
/// See https://discord.com/developers/docs/reference#snowflakes
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "sqlx", derive(Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Snowflake(String);
pub struct Snowflake(u64);
impl Snowflake {
pub fn generate() -> Self {
const WORKER_ID: u64 = 0;
const PROCESS_ID: u64 = 1;
static INCREMENT: AtomicUsize = AtomicUsize::new(0);
let time = (Utc::now().naive_utc().timestamp_millis() - EPOCH) << 22;
let worker = WORKER_ID << 17;
let process = PROCESS_ID << 12;
let increment = INCREMENT.fetch_add(1, Ordering::Relaxed) as u64 % 32;
Self(time as u64 | worker | process | increment)
}
pub fn timestamp(self) -> DateTime<Utc> {
Utc.timestamp_millis_opt((self.0 >> 22) as i64 + EPOCH)
.unwrap()
}
}
impl Default for Snowflake {
fn default() -> Self {
@ -30,131 +49,59 @@ impl Display for Snowflake {
}
}
impl Snowflake {
pub fn to_binary(&self) -> String {
let self_len = self.0.len();
let high = self.0[..self_len - 10].parse::<u64>().unwrap_or(0);
let low = self.0[self_len - 10..].parse::<u64>().unwrap();
let mut low = low;
let mut high = high;
let mut bin = Vec::with_capacity(64);
while low > 0 || high > 0 {
bin.push((low & 1) as u8);
low >>= 1;
if high > 0 {
low += 5_000_000_000 * (high % 2);
high >>= 1;
}
}
bin.iter()
.rev()
.map(|b| char::from_digit(*b as u32, 10).unwrap())
.collect()
}
pub fn from_binary(num: &str) -> String {
let mut num = BigInt::from_str_radix(num, 2).unwrap();
let mut dec = Vec::with_capacity(18);
let ten = 10.to_bigint().unwrap();
let _two = 2.to_bigint().unwrap();
let _thirty_two = 32.to_bigint().unwrap();
while num.bits() > 50 {
let high: BigInt = &num >> 32;
let low: BigInt = (high.clone() % &ten) << 32 | &num & BigInt::from((1u64 << 32) - 1);
let next: BigInt = low.clone() % &ten;
dec.push(next.to_u8().unwrap());
num = (high / &ten) << 32 | (low / &ten);
}
while !num.is_zero() {
dec.push((num.clone() % &ten).to_u8().unwrap());
num /= &ten;
}
dec.iter()
.rev()
.map(|d| char::from_digit(*d as u32, 10).unwrap())
.collect()
}
pub fn generate_worker_process() -> u128 {
let time = (chrono::Utc::now().naive_utc().timestamp_millis() - EPOCH) << 22;
let worker = WORKER_ID << 17;
let process = PROCESS_ID << 12;
let increment = INCREMENT.load(atomic::Ordering::Relaxed);
INCREMENT.store(increment + 1, atomic::Ordering::Relaxed);
time as u128 | worker | process | increment
}
pub fn generate() -> Self {
Self(Self::generate_worker_process().to_string())
}
pub fn deconstruct(&self) -> DeconstructedSnowflake {
let binary = format!("{:0>64}", self.to_binary());
let ts = i64::from_str_radix(&binary[0..42], 2).unwrap() + EPOCH;
let wid = u64::from_str_radix(&binary[42..47], 2).unwrap();
let pid = u64::from_str_radix(&binary[47..52], 2).unwrap();
let increment = BigInt::from_str_radix(&binary[52..64], 2).unwrap();
DeconstructedSnowflake {
timestamp: ts,
worker_id: wid,
process_id: pid,
increment,
binary,
}
impl serde::Serialize for Snowflake {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.0.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeconstructedSnowflake {
pub timestamp: i64,
pub worker_id: u64,
pub process_id: u64,
pub increment: BigInt,
pub binary: String,
impl<'de> serde::Deserialize<'de> for Snowflake {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SnowflakeVisitor;
impl<'de> serde::de::Visitor<'de> for SnowflakeVisitor {
type Value = Snowflake;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("snowflake string")
}
fn visit_str<E>(self, value: &str) -> Result<Snowflake, E>
where
E: serde::de::Error,
{
match value.parse() {
Ok(value) => Ok(Snowflake(value)),
Err(_) => Err(serde::de::Error::custom("")),
}
}
}
deserializer.deserialize_str(SnowflakeVisitor)
}
}
#[cfg(test)]
mod test {
use chrono::{DateTime, Utc};
use super::Snowflake;
#[test]
fn test_new_snowflake() {
let snow = Snowflake::generate();
println!("{snow}");
fn generate() {
let snow_1 = Snowflake::generate();
let snow_2 = Snowflake::generate();
assert!(snow_1.0 < snow_2.0)
}
#[test]
fn snowflake_to_binary() {
let snowflake = super::Snowflake("1104339392517902336".to_string());
let bin = snowflake.to_binary();
println!("{bin}");
}
#[test]
fn binary_to_snowflake() {
let snowflake = super::Snowflake::from_binary(
"111101010011011001101101001110010010100000000001000000000000",
);
println!("{snowflake}");
}
#[test]
fn test_deconstruct() {
let new = super::Snowflake::generate();
println!("{:?}", new.deconstruct());
fn timestamp() {
let snow: Snowflake = serde_json::from_str("\"175928847299117063\"").unwrap();
let timestamp = "2016-04-30 11:18:25.796Z".parse::<DateTime<Utc>>().unwrap();
assert_eq!(snow.timestamp(), timestamp);
}
}

View File

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

View File

@ -21,7 +21,7 @@ async fn get_channel() {
async fn delete_channel() {
let mut bundle = common::setup().await;
let result = bundle.channel.clone().delete(&mut bundle.user).await;
assert!(result.is_none());
assert!(result.is_ok());
common::teardown(bundle).await
}
@ -72,14 +72,16 @@ async fn modify_channel() {
bundle.channel.id.to_string().as_str(),
permission_override.clone(),
)
.await;
.await
.unwrap();
Channel::delete_permission(
&mut bundle.user,
bundle.channel.id.to_string().as_str(),
&permission_override.id,
)
.await;
.await
.unwrap();
common::teardown(bundle).await
}

View File

@ -2,14 +2,14 @@ use chorus::{
instance::{Instance, UserMeta},
types::{
Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema,
RoleCreateModifySchema, RoleObject,
RegisterSchemaOptions, RoleCreateModifySchema, RoleObject,
},
URLBundle,
UrlBundle,
};
#[derive(Debug)]
pub struct TestBundle {
pub urls: URLBundle,
pub urls: UrlBundle,
pub user: UserMeta,
pub instance: Instance,
pub guild: Guild,
@ -19,25 +19,18 @@ pub struct TestBundle {
// Set up a test by creating an Instance and a User. Reduces Test boilerplate.
pub async fn setup() -> TestBundle {
let urls = URLBundle::new(
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();
// Requires the existance of the below user.
let reg = RegisterSchema::new(
"integrationtestuser".to_string(),
None,
true,
None,
None,
None,
Some("2000-01-01".to_string()),
None,
None,
None,
)
let reg = RegisterSchemaOptions {
date_of_birth: Some("2000-01-01".to_string()),
..RegisterSchema::builder("integrationtestuser", true)
}
.build()
.unwrap();
let guild_create_schema = GuildCreateSchema {
name: Some("Test-Guild!".to_string()),
@ -100,6 +93,7 @@ pub async fn setup() -> TestBundle {
}
// Teardown method to clean up after a test.
#[allow(dead_code)]
pub async fn teardown(mut bundle: TestBundle) {
Guild::delete(&mut bundle.user, &bundle.guild.id.to_string()).await;
bundle.user.delete().await;

View File

@ -20,10 +20,9 @@ async fn guild_creation_deletion() {
.await
.unwrap();
match Guild::delete(&mut bundle.user, &guild.id.to_string()).await {
None => assert!(true),
Some(_) => assert!(false),
}
assert!(Guild::delete(&mut bundle.user, &guild.id.to_string())
.await
.is_ok());
common::teardown(bundle).await
}

View File

@ -18,7 +18,7 @@ async fn add_remove_role() {
}
}
if !role_found {
assert!(false)
panic!()
}
chorus::types::GuildMember::remove_role(&mut bundle.user, guild_id, user_id, role_id).await;
let member = chorus::types::GuildMember::get(&mut bundle.user, guild_id, user_id)
@ -28,11 +28,11 @@ async fn add_remove_role() {
if role != role_id {
role_found = false;
} else {
assert!(false);
panic!();
}
}
if role_found {
assert!(false)
panic!()
}
common::teardown(bundle).await
}

View File

@ -8,18 +8,10 @@ mod common;
#[tokio::test]
async fn send_message() {
let mut bundle = common::setup().await;
let mut message = types::MessageSendSchema::new(
None,
Some("A Message!".to_string()),
None,
None,
None,
None,
None,
None,
None,
None,
);
let mut message = types::MessageSendSchema {
content: Some("A Message!".to_string()),
..Default::default()
};
let _ = bundle
.user
.send_message(&mut message, bundle.channel.id.to_string(), None)
@ -53,18 +45,11 @@ async fn send_message_attachment() {
content: buffer,
};
let mut message = types::MessageSendSchema::new(
None,
Some("trans rights now".to_string()),
None,
None,
None,
None,
None,
None,
None,
Some(vec![attachment.clone()]),
);
let mut message = types::MessageSendSchema {
content: Some("trans rights now".to_string()),
attachments: Some(vec![attachment.clone()]),
..Default::default()
};
let vec_attach = vec![attachment.clone()];
let _arg = Some(&vec_attach);
@ -79,3 +64,20 @@ async fn send_message_attachment() {
.unwrap();
common::teardown(bundle).await
}
#[tokio::test]
async fn read_messages() {
let mut bundle = common::setup().await;
// First create some messages to read
let mut message = types::MessageSendSchema {
content: Some("A Message!".to_string()),
..Default::default()
};
let _ = bundle
.user
.send_message(&mut message, bundle.channel.id.to_string(), None)
.await
.unwrap();
common::teardown(bundle).await
}

144
tests/relationships.rs Normal file
View File

@ -0,0 +1,144 @@
use chorus::types::{self, RegisterSchema, RegisterSchemaOptions, 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 user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_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 relationships = user
.get_mutual_relationships(&other_user.object.id.to_string())
.await
.unwrap();
println!("{:?}", relationships);
common::teardown(bundle).await
}
#[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 user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_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 relationships = user.get_relationships().await.unwrap();
assert_eq!(relationships.get(0).unwrap().id, other_user.object.id);
common::teardown(bundle).await
}
#[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 user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
other_user
.modify_user_relationship(
&user.object.id.to_string(),
types::RelationshipType::Friends,
)
.await;
let relationships = user.get_relationships().await.unwrap();
assert_eq!(relationships.get(0).unwrap().id, other_user.object.id);
assert_eq!(
relationships.get(0).unwrap().relationship_type,
RelationshipType::Incoming
);
let relationships = other_user.get_relationships().await.unwrap();
assert_eq!(relationships.get(0).unwrap().id, user.object.id);
assert_eq!(
relationships.get(0).unwrap().relationship_type,
RelationshipType::Outgoing
);
user.modify_user_relationship(
other_user.object.id.to_string().as_str(),
RelationshipType::Friends,
)
.await;
assert_eq!(
other_user
.get_relationships()
.await
.unwrap()
.get(0)
.unwrap()
.relationship_type,
RelationshipType::Friends
);
user.remove_relationship(other_user.object.id.to_string().as_str())
.await;
assert_eq!(
other_user.get_relationships().await.unwrap(),
Vec::<Relationship>::new()
);
common::teardown(bundle).await
}
#[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 user = &mut bundle.user;
let mut other_user = belongs_to.register_account(&register_schema).await.unwrap();
other_user
.modify_user_relationship(
&user.object.id.to_string(),
types::RelationshipType::Blocked,
)
.await;
let relationships = user.get_relationships().await.unwrap();
assert_eq!(relationships, Vec::<Relationship>::new());
let relationships = other_user.get_relationships().await.unwrap();
assert_eq!(relationships.get(0).unwrap().id, user.object.id);
assert_eq!(
relationships.get(0).unwrap().relationship_type,
RelationshipType::Blocked
);
other_user
.remove_relationship(user.object.id.to_string().as_str())
.await;
assert_eq!(
other_user.get_relationships().await.unwrap(),
Vec::<Relationship>::new()
);
common::teardown(bundle).await
}

View File

@ -25,10 +25,7 @@ async fn create_and_get_roles() {
let expected = types::RoleObject::get_all(&mut bundle.user, &guild_id)
.await
.unwrap()
.unwrap()
.iter()
.nth(2)
.unwrap()
.unwrap()[2]
.clone();
assert_eq!(role, expected);