wasm support, bugfixes (#446)

This PR fixes #430 and enables full support for the
`wasm32-unknown-unknown` compilation target, making chorus the first(?)
Rust Discord-API implementation to support this target.
This commit is contained in:
Flori 2023-11-22 17:30:20 +01:00 committed by GitHub
commit 9dd66767bf
45 changed files with 1262 additions and 669 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[target.wasm32-unknown-unknown]
runner = 'wasm-bindgen-test-runner'

View File

@ -2,7 +2,7 @@ name: Build and Test
on: on:
push: push:
branches: [ "main", "dev" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "main", "dev" ] branches: [ "main", "dev" ]
@ -10,7 +10,7 @@ env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
rust: linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -33,6 +33,7 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
cache-all-crates: "true" cache-all-crates: "true"
prefix-key: "linux"
- name: Build, Test and Publish Coverage - name: Build, Test and Publish Coverage
run: | run: |
if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then if [ -n "${{ secrets.COVERALLS_REPO_TOKEN }}" ]; then
@ -44,4 +45,87 @@ jobs:
cargo build --verbose --all-features cargo build --verbose --all-features
cargo test --verbose --all-features cargo test --verbose --all-features
fi fi
wasm-safari:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Clone spacebar server
run: |
git clone https://github.com/bitfl0wer/server.git
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: server/package-lock.json
- name: Prepare and start Spacebar server
run: |
npm install
npm run setup
npm run start &
working-directory: ./server
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
prefix-key: "macos"
- name: Run WASM tests with Safari, Firefox, Chrome
run: |
rustup target add wasm32-unknown-unknown
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
SAFARIDRIVER=$(which safaridriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt"
wasm-gecko:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Clone spacebar server
run: |
git clone https://github.com/bitfl0wer/server.git
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: server/package-lock.json
- name: Prepare and start Spacebar server
run: |
npm install
npm run setup
npm run start &
working-directory: ./server
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
prefix-key: "macos"
- name: Run WASM tests with Safari, Firefox, Chrome
run: |
rustup target add wasm32-unknown-unknown
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt"
wasm-chrome:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Clone spacebar server
run: |
git clone https://github.com/bitfl0wer/server.git
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
cache-dependency-path: server/package-lock.json
- name: Prepare and start Spacebar server
run: |
npm install
npm run setup
npm run start &
working-directory: ./server
- uses: Swatinem/rust-cache@v2
with:
cache-all-crates: "true"
prefix-key: "macos"
- name: Run WASM tests with Safari, Firefox, Chrome
run: |
rustup target add wasm32-unknown-unknown
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force
CHROMEDRIVER=$(which chromedriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt"

View File

@ -11,7 +11,7 @@ name: rust-clippy analyze
on: on:
push: push:
branches: [ "main", "preserve/*", "dev" ] branches: [ "main", "preserve/*" ]
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ "main", "dev" ] branches: [ "main", "dev" ]

792
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
[package] [package]
name = "chorus" name = "chorus"
description = "A library for interacting with multiple Spacebar-compatible Instances at once." description = "A library for interacting with multiple Spacebar-compatible Instances at once."
version = "0.11.0" version = "0.12.0"
license = "AGPL-3.0" license = "AGPL-3.0"
edition = "2021" edition = "2021"
repository = "https://github.com/polyphony-chat/chorus" repository = "https://github.com/polyphony-chat/chorus"
@ -11,33 +11,38 @@ website = ["https://discord.com/invite/m3FpcapGDD"]
[features] [features]
default = ["client"] default = ["client", "rt-multi-thread"]
backend = ["poem", "sqlx"] backend = ["dep:poem", "dep:sqlx"]
rt-multi-thread = ["tokio/rt-multi-thread"]
rt = ["tokio/rt"]
client = [] client = []
[dependencies] [dependencies]
tokio = { version = "1.29.1", features = ["macros"] } tokio = { version = "1.34.0", features = ["macros", "sync"] }
serde = { version = "1.0.188", features = ["derive", "rc"] } serde = { version = "1.0.188", features = ["derive", "rc"] }
serde_json = { version = "1.0.105", features = ["raw_value"] } serde_json = { version = "1.0.105", features = ["raw_value"] }
serde-aux = "4.2.0" serde-aux = "4.2.0"
serde_with = "3.3.0" serde_with = "3.3.0"
serde_repr = "0.1.16" serde_repr = "0.1.16"
reqwest = { version = "0.11.20", features = ["multipart", "json"] } reqwest = { git = "https://github.com/bitfl0wer/reqwest.git", branch = "wasm-headers", features = [
"multipart",
"json",
] } # reqwest versions > 0.11.22 will have adequate support for WASM. Until there is such a version, we will use a fork of reqwest v.0.11.22
url = "2.4.0" url = "2.4.0"
chrono = { version = "0.4.26", features = ["serde"] } chrono = { version = "0.4.26", features = ["serde"] }
regex = "1.9.4" regex = "1.9.4"
custom_error = "1.9.2" custom_error = "1.9.2"
tokio-tungstenite = { version = "0.20.1", features = [
"rustls-tls-native-roots",
"rustls-native-certs",
] }
futures-util = "0.3.28" futures-util = "0.3.28"
http = "0.2.9" http = "0.2.9"
base64 = "0.21.3" base64 = "0.21.3"
hostname = "0.3.1"
bitflags = { version = "2.4.0", features = ["serde"] } bitflags = { version = "2.4.0", features = ["serde"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
poem = { version = "1.3.57", optional = true } poem = { version = "1.3.57", optional = true }
thiserror = "1.0.47"
jsonwebtoken = "8.3.0"
log = "0.4.20"
async-trait = "0.1.73"
chorus-macros = "0.2.0"
sqlx = { version = "0.7.1", features = [ sqlx = { version = "0.7.1", features = [
"mysql", "mysql",
"sqlite", "sqlite",
@ -47,16 +52,25 @@ sqlx = { version = "0.7.1", features = [
"runtime-tokio-native-tls", "runtime-tokio-native-tls",
"any", "any",
], optional = true } ], optional = true }
thiserror = "1.0.47" safina-timer = "0.1.11"
jsonwebtoken = "8.3.0"
log = "0.4.20"
async-trait = "0.1.73"
chorus-macros = "0.2.0"
rustls = "0.21.8"
rustls-native-certs = "0.6.3"
rand = "0.8.5" rand = "0.8.5"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rustls = "0.21.8"
rustls-native-certs = "0.6.3"
tokio-tungstenite = { version = "0.20.1", features = [
"rustls-tls-native-roots",
"rustls-native-certs",
] }
native-tls = "0.2.11"
hostname = "0.3.1"
[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2.11", features = ["js"] }
ws_stream_wasm = "0.7.4"
wasm-bindgen-futures = "0.4.38"
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.32.0", features = ["full"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
rusty-hook = "0.11.2" wasm-bindgen-test = "0.3.38"
wasm-bindgen = "0.2.88"

View File

@ -98,8 +98,8 @@ dbg!(&user.object.read().unwrap().username);
## Supported Platforms ## Supported Platforms
All major desktop operating systems (Windows, macOS (aarch64/x86_64), Linux (aarch64/x86_64)) are supported. All major desktop operating systems (Windows, macOS (aarch64/x86_64), Linux (aarch64/x86_64)) are supported.
We are currently working on adding full support for `wasm32-unknown-unknown`. This will allow you to use Chorus in `wasm32-unknown-unknown` is a supported compilation target on versions `0.12.0` and up. This allows you to use
your browser, or in any other environment that supports WebAssembly. Chorus in your browser, or in any other environment that supports WebAssembly.
We recommend checking out the examples directory, as well as the documentation for more information. We recommend checking out the examples directory, as well as the documentation for more information.
@ -107,6 +107,25 @@ We recommend checking out the examples directory, as well as the documentation f
Rust **1.67.1**. This number might change at any point while Chorus is not yet at version 1.0.0. Rust **1.67.1**. This number might change at any point while Chorus is not yet at version 1.0.0.
## Development Setup
Make sure that you have at least Rust 1.67.1 installed. You can check your Rust version by running `cargo --version`
in your terminal. To compile for `wasm32-unknown-unknown`, you need to install the `wasm32-unknown-unknown` target.
You can do this by running `rustup target add wasm32-unknown-unknown`.
### Testing
In general, the tests will require you to run a local instance of the Spacebar server. You can find instructions on how
to do that [here](https://docs.spacebar.chat/setup/server/). You can find a pre-configured version of the server
[here](https://github.com/bitfl0wer/server). It is recommended to use the pre-configured version, as certain things
like "proxy connection checking" are already disabled on this version, which otherwise might break tests.
### wasm
To test for wasm, you will need to `cargo install wasm-pack`. You can then run
`wasm-pack test --<chrome/firefox/safari> --headless -- --target wasm32-unknown-unknown --features="rt, client" --no-default-features`
to run the tests for wasm.
## Versioning ## Versioning
This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read the specification [here](https://semver.org/spec/v2.0.0.html). This crate uses Semantic Versioning 2.0.0 as its versioning scheme. You can read the specification [here](https://semver.org/spec/v2.0.0.html).

View File

@ -1,11 +1,12 @@
use async_trait::async_trait; use async_trait::async_trait;
use chorus::gateway::Gateway;
use chorus::{ use chorus::{
self, self,
gateway::{Gateway, Observer}, gateway::Observer,
types::{GatewayIdentifyPayload, GatewayReady}, types::{GatewayIdentifyPayload, GatewayReady},
}; };
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use tokio::{self, time::sleep}; use tokio::{self};
// This example creates a simple gateway connection and a basic observer struct // This example creates a simple gateway connection and a basic observer struct
@ -24,13 +25,13 @@ impl Observer<GatewayReady> for ExampleObserver {
} }
} }
#[tokio::main] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
// Find the gateway websocket url of the server we want to connect to // Find the gateway websocket url of the server we want to connect to
let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string(); let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string();
// Initiate the gateway connection // Initiate the gateway connection
let gateway = Gateway::new(websocket_url_spacebar).await.unwrap(); let gateway = Gateway::spawn(websocket_url_spacebar).await.unwrap();
// Create an instance of our observer // Create an instance of our observer
let observer = ExampleObserver {}; let observer = ExampleObserver {};
@ -53,9 +54,10 @@ async fn main() {
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();
identify.token = token; identify.token = token;
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
safina_timer::start_timer_thread();
// Do something on the main thread so we don't quit // Do something on the main thread so we don't quit
loop { loop {
sleep(Duration::MAX).await; safina_timer::sleep_for(Duration::MAX).await
} }
} }

View File

@ -1,16 +1,16 @@
use std::time::Duration; use std::time::Duration;
use chorus::{self, gateway::Gateway, types::GatewayIdentifyPayload}; use chorus::gateway::Gateway;
use tokio::time::sleep; use chorus::{self, types::GatewayIdentifyPayload};
/// This example creates a simple gateway connection and a session with an Identify event /// This example creates a simple gateway connection and a session with an Identify event
#[tokio::main] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
// Find the gateway websocket url of the server we want to connect to // Find the gateway websocket url of the server we want to connect to
let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string(); let websocket_url_spacebar = "wss://gateway.old.server.spacebar.chat/".to_string();
// Initiate the gateway connection, starting a listener in one thread and a heartbeat handler in another // Initiate the gateway connection, starting a listener in one thread and a heartbeat handler in another
let gateway = Gateway::new(websocket_url_spacebar).await.unwrap(); let _ = Gateway::spawn(websocket_url_spacebar).await.unwrap();
// At this point, we are connected to the server and are sending heartbeats, however we still haven't authenticated // At this point, we are connected to the server and are sending heartbeats, however we still haven't authenticated
@ -26,10 +26,10 @@ async fn main() {
identify.token = token; identify.token = token;
// Send off the event // Send off the event
gateway.send_identify(identify).await; safina_timer::start_timer_thread();
// Do something on the main thread so we don't quit // Do something on the main thread so we don't quit
loop { loop {
sleep(Duration::MAX).await; safina_timer::sleep_for(Duration::MAX).await
} }
} }

View File

@ -1,7 +1,7 @@
use chorus::instance::Instance; use chorus::instance::Instance;
use chorus::UrlBundle; use chorus::UrlBundle;
#[tokio::main] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
let bundle = UrlBundle::new( let bundle = UrlBundle::new(
"https://example.com/api".to_string(), "https://example.com/api".to_string(),

View File

@ -2,7 +2,7 @@ use chorus::instance::Instance;
use chorus::types::LoginSchema; use chorus::types::LoginSchema;
use chorus::UrlBundle; use chorus::UrlBundle;
#[tokio::main] #[tokio::main(flavor = "current_thread")]
async fn main() { async fn main() {
let bundle = UrlBundle::new( let bundle = UrlBundle::new(
"https://example.com/api".to_string(), "https://example.com/api".to_string(),

View File

@ -36,7 +36,7 @@ impl Instance {
self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap(); self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap();
} }
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();
let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); let gateway = Gateway::spawn(self.urls.wss.clone()).await.unwrap();
identify.token = login_result.token.clone(); identify.token = login_result.token.clone();
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
let user = ChorusUser::new( let user = ChorusUser::new(

View File

@ -3,9 +3,9 @@ use std::sync::{Arc, RwLock};
pub use login::*; pub use login::*;
pub use register::*; pub use register::*;
use crate::gateway::Gateway;
use crate::{ use crate::{
errors::ChorusResult, errors::ChorusResult,
gateway::Gateway,
instance::{ChorusUser, Instance}, instance::{ChorusUser, Instance},
types::{GatewayIdentifyPayload, User}, types::{GatewayIdentifyPayload, User},
}; };
@ -25,7 +25,7 @@ impl Instance {
.await .await
.unwrap(); .unwrap();
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();
let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); let gateway = Gateway::spawn(self.urls.wss.clone()).await.unwrap();
identify.token = token.clone(); identify.token = token.clone();
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
let user = ChorusUser::new( let user = ChorusUser::new(

View File

@ -3,7 +3,7 @@ use std::sync::{Arc, RwLock};
use reqwest::Client; use reqwest::Client;
use serde_json::to_string; use serde_json::to_string;
use crate::gateway::Gateway; use crate::gateway::{Gateway, GatewayHandle};
use crate::types::GatewayIdentifyPayload; use crate::types::GatewayIdentifyPayload;
use crate::{ use crate::{
errors::ChorusResult, errors::ChorusResult,
@ -45,7 +45,7 @@ impl Instance {
let user_object = self.get_user(token.clone(), None).await.unwrap(); let user_object = self.get_user(token.clone(), None).await.unwrap();
let settings = ChorusUser::get_settings(&token, &self.urls.api.clone(), &mut self).await?; let settings = ChorusUser::get_settings(&token, &self.urls.api.clone(), &mut self).await?;
let mut identify = GatewayIdentifyPayload::common(); let mut identify = GatewayIdentifyPayload::common();
let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); let gateway: GatewayHandle = Gateway::spawn(self.urls.wss.clone()).await.unwrap();
identify.token = token.clone(); identify.token = token.clone();
gateway.send_identify(identify).await; gateway.send_identify(identify).await;
let user = ChorusUser::new( let user = ChorusUser::new(

View File

@ -0,0 +1,23 @@
#[cfg(all(not(target_arch = "wasm32"), feature = "client"))]
pub mod tungstenite;
#[cfg(all(not(target_arch = "wasm32"), feature = "client"))]
pub use tungstenite::*;
#[cfg(all(target_arch = "wasm32", feature = "client"))]
pub mod wasm;
#[cfg(all(target_arch = "wasm32", feature = "client"))]
pub use wasm::*;
#[cfg(all(not(target_arch = "wasm32"), feature = "client"))]
pub type Sink = tungstenite::TungsteniteSink;
#[cfg(all(not(target_arch = "wasm32"), feature = "client"))]
pub type Stream = tungstenite::TungsteniteStream;
#[cfg(all(not(target_arch = "wasm32"), feature = "client"))]
pub type WebSocketBackend = tungstenite::TungsteniteBackend;
#[cfg(all(target_arch = "wasm32", feature = "client"))]
pub type Sink = wasm::WasmSink;
#[cfg(all(target_arch = "wasm32", feature = "client"))]
pub type Stream = wasm::WasmStream;
#[cfg(all(target_arch = "wasm32", feature = "client"))]
pub type WebSocketBackend = wasm::WasmBackend;

View File

@ -0,0 +1,66 @@
use futures_util::{
stream::{SplitSink, SplitStream},
StreamExt,
};
use tokio::net::TcpStream;
use tokio_tungstenite::{
connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream,
};
use crate::errors::GatewayError;
use crate::gateway::GatewayMessage;
#[derive(Debug, Clone)]
pub struct TungsteniteBackend;
// These could be made into inherent associated types when that's stabilized
pub type TungsteniteSink =
SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, tungstenite::Message>;
pub type TungsteniteStream = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
impl TungsteniteBackend {
pub async fn connect(
websocket_url: &str,
) -> Result<(TungsteniteSink, TungsteniteStream), crate::errors::GatewayError> {
let mut roots = rustls::RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs")
{
roots.add(&rustls::Certificate(cert.0)).unwrap();
}
let (websocket_stream, _) = match connect_async_tls_with_config(
websocket_url,
None,
false,
Some(Connector::Rustls(
rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth()
.into(),
)),
)
.await
{
Ok(websocket_stream) => websocket_stream,
Err(e) => {
return Err(GatewayError::CannotConnect {
error: e.to_string(),
})
}
};
Ok(websocket_stream.split())
}
}
impl From<GatewayMessage> for tungstenite::Message {
fn from(message: GatewayMessage) -> Self {
Self::Text(message.0)
}
}
impl From<tungstenite::Message> for GatewayMessage {
fn from(value: tungstenite::Message) -> Self {
Self(value.to_string())
}
}

View File

@ -0,0 +1,50 @@
use futures_util::{
stream::{SplitSink, SplitStream},
StreamExt,
};
use ws_stream_wasm::*;
use crate::errors::GatewayError;
use crate::gateway::GatewayMessage;
#[derive(Debug, Clone)]
pub struct WasmBackend;
// These could be made into inherent associated types when that's stabilized
pub type WasmSink = SplitSink<WsStream, WsMessage>;
pub type WasmStream = SplitStream<WsStream>;
impl WasmBackend {
pub async fn connect(
websocket_url: &str,
) -> Result<(WasmSink, WasmStream), crate::errors::GatewayError> {
let (_, websocket_stream) = match WsMeta::connect(websocket_url, None).await {
Ok(stream) => Ok(stream),
Err(e) => Err(GatewayError::CannotConnect {
error: e.to_string(),
}),
}?;
Ok(websocket_stream.split())
}
}
impl From<GatewayMessage> for WsMessage {
fn from(message: GatewayMessage) -> Self {
Self::Text(message.0)
}
}
impl From<WsMessage> for GatewayMessage {
fn from(value: WsMessage) -> Self {
match value {
WsMessage::Text(text) => Self(text),
WsMessage::Binary(bin) => {
let mut text = String::new();
let _ = bin.iter().map(|v| text.push_str(&v.to_string()));
Self(text)
}
}
}
}

160
src/gateway/events.rs Normal file
View File

@ -0,0 +1,160 @@
use super::*;
use crate::types;
#[derive(Default, Debug)]
pub struct Events {
pub application: Application,
pub auto_moderation: AutoModeration,
pub session: Session,
pub message: Message,
pub user: User,
pub relationship: Relationship,
pub channel: Channel,
pub thread: Thread,
pub guild: Guild,
pub invite: Invite,
pub integration: Integration,
pub interaction: Interaction,
pub stage_instance: StageInstance,
pub call: Call,
pub voice: Voice,
pub webhooks: Webhooks,
pub gateway_identify_payload: GatewayEvent<types::GatewayIdentifyPayload>,
pub gateway_resume: GatewayEvent<types::GatewayResume>,
pub error: GatewayEvent<GatewayError>,
}
#[derive(Default, Debug)]
pub struct Application {
pub command_permissions_update: GatewayEvent<types::ApplicationCommandPermissionsUpdate>,
}
#[derive(Default, Debug)]
pub struct AutoModeration {
pub rule_create: GatewayEvent<types::AutoModerationRuleCreate>,
pub rule_update: GatewayEvent<types::AutoModerationRuleUpdate>,
pub rule_delete: GatewayEvent<types::AutoModerationRuleDelete>,
pub action_execution: GatewayEvent<types::AutoModerationActionExecution>,
}
#[derive(Default, Debug)]
pub struct Session {
pub ready: GatewayEvent<types::GatewayReady>,
pub ready_supplemental: GatewayEvent<types::GatewayReadySupplemental>,
pub replace: GatewayEvent<types::SessionsReplace>,
}
#[derive(Default, Debug)]
pub struct StageInstance {
pub create: GatewayEvent<types::StageInstanceCreate>,
pub update: GatewayEvent<types::StageInstanceUpdate>,
pub delete: GatewayEvent<types::StageInstanceDelete>,
}
#[derive(Default, Debug)]
pub struct Message {
pub create: GatewayEvent<types::MessageCreate>,
pub update: GatewayEvent<types::MessageUpdate>,
pub delete: GatewayEvent<types::MessageDelete>,
pub delete_bulk: GatewayEvent<types::MessageDeleteBulk>,
pub reaction_add: GatewayEvent<types::MessageReactionAdd>,
pub reaction_remove: GatewayEvent<types::MessageReactionRemove>,
pub reaction_remove_all: GatewayEvent<types::MessageReactionRemoveAll>,
pub reaction_remove_emoji: GatewayEvent<types::MessageReactionRemoveEmoji>,
pub ack: GatewayEvent<types::MessageACK>,
}
#[derive(Default, Debug)]
pub struct User {
pub update: GatewayEvent<types::UserUpdate>,
pub guild_settings_update: GatewayEvent<types::UserGuildSettingsUpdate>,
pub presence_update: GatewayEvent<types::PresenceUpdate>,
pub typing_start: GatewayEvent<types::TypingStartEvent>,
}
#[derive(Default, Debug)]
pub struct Relationship {
pub add: GatewayEvent<types::RelationshipAdd>,
pub remove: GatewayEvent<types::RelationshipRemove>,
}
#[derive(Default, Debug)]
pub struct Channel {
pub create: GatewayEvent<types::ChannelCreate>,
pub update: GatewayEvent<types::ChannelUpdate>,
pub unread_update: GatewayEvent<types::ChannelUnreadUpdate>,
pub delete: GatewayEvent<types::ChannelDelete>,
pub pins_update: GatewayEvent<types::ChannelPinsUpdate>,
}
#[derive(Default, Debug)]
pub struct Thread {
pub create: GatewayEvent<types::ThreadCreate>,
pub update: GatewayEvent<types::ThreadUpdate>,
pub delete: GatewayEvent<types::ThreadDelete>,
pub list_sync: GatewayEvent<types::ThreadListSync>,
pub member_update: GatewayEvent<types::ThreadMemberUpdate>,
pub members_update: GatewayEvent<types::ThreadMembersUpdate>,
}
#[derive(Default, Debug)]
pub struct Guild {
pub create: GatewayEvent<types::GuildCreate>,
pub update: GatewayEvent<types::GuildUpdate>,
pub delete: GatewayEvent<types::GuildDelete>,
pub audit_log_entry_create: GatewayEvent<types::GuildAuditLogEntryCreate>,
pub ban_add: GatewayEvent<types::GuildBanAdd>,
pub ban_remove: GatewayEvent<types::GuildBanRemove>,
pub emojis_update: GatewayEvent<types::GuildEmojisUpdate>,
pub stickers_update: GatewayEvent<types::GuildStickersUpdate>,
pub integrations_update: GatewayEvent<types::GuildIntegrationsUpdate>,
pub member_add: GatewayEvent<types::GuildMemberAdd>,
pub member_remove: GatewayEvent<types::GuildMemberRemove>,
pub member_update: GatewayEvent<types::GuildMemberUpdate>,
pub members_chunk: GatewayEvent<types::GuildMembersChunk>,
pub role_create: GatewayEvent<types::GuildRoleCreate>,
pub role_update: GatewayEvent<types::GuildRoleUpdate>,
pub role_delete: GatewayEvent<types::GuildRoleDelete>,
pub role_scheduled_event_create: GatewayEvent<types::GuildScheduledEventCreate>,
pub role_scheduled_event_update: GatewayEvent<types::GuildScheduledEventUpdate>,
pub role_scheduled_event_delete: GatewayEvent<types::GuildScheduledEventDelete>,
pub role_scheduled_event_user_add: GatewayEvent<types::GuildScheduledEventUserAdd>,
pub role_scheduled_event_user_remove: GatewayEvent<types::GuildScheduledEventUserRemove>,
pub passive_update_v1: GatewayEvent<types::PassiveUpdateV1>,
}
#[derive(Default, Debug)]
pub struct Invite {
pub create: GatewayEvent<types::InviteCreate>,
pub delete: GatewayEvent<types::InviteDelete>,
}
#[derive(Default, Debug)]
pub struct Integration {
pub create: GatewayEvent<types::IntegrationCreate>,
pub update: GatewayEvent<types::IntegrationUpdate>,
pub delete: GatewayEvent<types::IntegrationDelete>,
}
#[derive(Default, Debug)]
pub struct Interaction {
pub create: GatewayEvent<types::InteractionCreate>,
}
#[derive(Default, Debug)]
pub struct Call {
pub create: GatewayEvent<types::CallCreate>,
pub update: GatewayEvent<types::CallUpdate>,
pub delete: GatewayEvent<types::CallDelete>,
}
#[derive(Default, Debug)]
pub struct Voice {
pub state_update: GatewayEvent<types::VoiceStateUpdate>,
pub server_update: GatewayEvent<types::VoiceServerUpdate>,
}
#[derive(Default, Debug)]
pub struct Webhooks {
pub update: GatewayEvent<types::WebhooksUpdate>,
}

View File

@ -1,5 +1,13 @@
use std::time::Duration;
use futures_util::{SinkExt, StreamExt};
use log::*;
#[cfg(not(target_arch = "wasm32"))]
use tokio::task;
use self::event::Events; use self::event::Events;
use super::*; use super::*;
use super::{Sink, Stream};
use crate::types::{ use crate::types::{
self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete, self, AutoModerationRule, AutoModerationRuleUpdate, Channel, ChannelCreate, ChannelDelete,
ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, SourceUrlField, ChannelUpdate, Guild, GuildRoleCreate, GuildRoleUpdate, JsonField, RoleObject, SourceUrlField,
@ -10,15 +18,8 @@ use crate::types::{
pub struct Gateway { pub struct Gateway {
events: Arc<Mutex<Events>>, events: Arc<Mutex<Events>>,
heartbeat_handler: HeartbeatHandler, heartbeat_handler: HeartbeatHandler,
websocket_send: Arc< websocket_send: Arc<Mutex<Sink>>,
Mutex< websocket_receive: Stream,
SplitSink<
WebSocketStream<MaybeTlsStream<TcpStream>>,
tokio_tungstenite::tungstenite::Message,
>,
>,
>,
websocket_receive: SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>,
kill_send: tokio::sync::broadcast::Sender<()>, kill_send: tokio::sync::broadcast::Sender<()>,
store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>, store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>,
url: String, url: String,
@ -26,35 +27,9 @@ pub struct Gateway {
impl Gateway { impl Gateway {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub async fn new(websocket_url: String) -> Result<GatewayHandle, GatewayError> { pub async fn spawn(websocket_url: String) -> Result<GatewayHandle, GatewayError> {
let mut roots = rustls::RootCertStore::empty(); let (websocket_send, mut websocket_receive) =
for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") WebSocketBackend::connect(&websocket_url).await?;
{
roots.add(&rustls::Certificate(cert.0)).unwrap();
}
let (websocket_stream, _) = match connect_async_tls_with_config(
&websocket_url,
None,
false,
Some(Connector::Rustls(
rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(roots)
.with_no_client_auth()
.into(),
)),
)
.await
{
Ok(websocket_stream) => websocket_stream,
Err(e) => {
return Err(GatewayError::CannotConnect {
error: e.to_string(),
})
}
};
let (websocket_send, mut websocket_receive) = websocket_stream.split();
let shared_websocket_send = Arc::new(Mutex::new(websocket_send)); let shared_websocket_send = Arc::new(Mutex::new(websocket_send));
@ -63,9 +38,11 @@ impl Gateway {
// Wait for the first hello and then spawn both tasks so we avoid nested tasks // Wait for the first hello and then spawn both tasks so we avoid nested tasks
// This automatically spawns the heartbeat task, but from the main thread // This automatically spawns the heartbeat task, but from the main thread
let msg = websocket_receive.next().await.unwrap().unwrap(); #[cfg(not(target_arch = "wasm32"))]
let gateway_payload: types::GatewayReceivePayload = let msg: GatewayMessage = websocket_receive.next().await.unwrap().unwrap().into();
serde_json::from_str(msg.to_text().unwrap()).unwrap(); #[cfg(target_arch = "wasm32")]
let msg: GatewayMessage = websocket_receive.next().await.unwrap().into();
let gateway_payload: types::GatewayReceivePayload = serde_json::from_str(&msg.0).unwrap();
if gateway_payload.op_code != GATEWAY_HELLO { if gateway_payload.op_code != GATEWAY_HELLO {
return Err(GatewayError::NonHelloOnInitiate { return Err(GatewayError::NonHelloOnInitiate {
@ -98,9 +75,14 @@ impl Gateway {
}; };
// Now we can continuously check for messages in a different task, since we aren't going to receive another hello // Now we can continuously check for messages in a different task, since we aren't going to receive another hello
#[cfg(not(target_arch = "wasm32"))]
task::spawn(async move { task::spawn(async move {
gateway.gateway_listen_task().await; gateway.gateway_listen_task().await;
}); });
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(async move {
gateway.gateway_listen_task().await;
});
Ok(GatewayHandle { Ok(GatewayHandle {
url: websocket_url.clone(), url: websocket_url.clone(),
@ -118,10 +100,16 @@ impl Gateway {
loop { loop {
let msg = self.websocket_receive.next().await; let msg = self.websocket_receive.next().await;
// PRETTYFYME: Remove inline conditional compiling
// This if chain can be much better but if let is unstable on stable rust // This if chain can be much better but if let is unstable on stable rust
#[cfg(not(target_arch = "wasm32"))]
if let Some(Ok(message)) = msg { if let Some(Ok(message)) = msg {
self.handle_message(GatewayMessage::from_tungstenite_message(message)) self.handle_message(message.into()).await;
.await; continue;
}
#[cfg(target_arch = "wasm32")]
if let Some(message) = msg {
self.handle_message(message.into()).await;
continue; continue;
} }
@ -156,31 +144,23 @@ impl Gateway {
/// This handles a message as a websocket event and updates its events along with the events' observers /// This handles a message as a websocket event and updates its events along with the events' observers
pub async fn handle_message(&mut self, msg: GatewayMessage) { pub async fn handle_message(&mut self, msg: GatewayMessage) {
if msg.is_empty() { if msg.0.is_empty() {
return; return;
} }
if !msg.is_error() && !msg.is_payload() { let Ok(gateway_payload) = msg.payload() else {
if let Some(error) = msg.error() {
warn!("GW: Received error {:?}, connection will close..", error);
self.close().await;
self.events.lock().await.error.notify(error).await;
} else {
warn!( warn!(
"Message unrecognised: {:?}, please open an issue on the chorus github", "Message unrecognised: {:?}, please open an issue on the chorus github",
msg.message.to_string() msg.0
); );
return;
} }
if msg.is_error() {
let error = msg.error().unwrap();
warn!("GW: Received error {:?}, connection will close..", error);
self.close().await;
self.events.lock().await.error.notify(error).await;
return; return;
} };
let gateway_payload = msg.payload().unwrap();
// See https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes // See https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-opcodes
match gateway_payload.op_code { match gateway_payload.op_code {

View File

@ -1,3 +1,8 @@
use futures_util::SinkExt;
use log::*;
use std::fmt::Debug;
use super::{event::Events, *}; use super::{event::Events, *};
use crate::types::{self, Composite}; use crate::types::{self, Composite};
@ -9,14 +14,7 @@ use crate::types::{self, Composite};
pub struct GatewayHandle { pub struct GatewayHandle {
pub url: String, pub url: String,
pub events: Arc<Mutex<Events>>, pub events: Arc<Mutex<Events>>,
pub websocket_send: Arc< pub websocket_send: Arc<Mutex<Sink>>,
Mutex<
SplitSink<
WebSocketStream<MaybeTlsStream<TcpStream>>,
tokio_tungstenite::tungstenite::Message,
>,
>,
>,
/// Tells gateway tasks to close /// Tells gateway tasks to close
pub(super) kill_send: tokio::sync::broadcast::Sender<()>, pub(super) kill_send: tokio::sync::broadcast::Sender<()>,
pub(crate) store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>, pub(crate) store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>,
@ -32,13 +30,12 @@ impl GatewayHandle {
}; };
let payload_json = serde_json::to_string(&gateway_payload).unwrap(); let payload_json = serde_json::to_string(&gateway_payload).unwrap();
let message = GatewayMessage(payload_json);
let message = tokio_tungstenite::tungstenite::Message::text(payload_json);
self.websocket_send self.websocket_send
.lock() .lock()
.await .await
.send(message) .send(message.into())
.await .await
.unwrap(); .unwrap();
} }

View File

@ -1,6 +1,14 @@
use crate::types; use futures_util::SinkExt;
use log::*;
use std::time::{self, Duration, Instant};
use tokio::sync::mpsc::{Receiver, Sender};
use safina_timer::sleep_until;
#[cfg(not(target_arch = "wasm32"))]
use tokio::task;
use super::*; use super::*;
use crate::types;
/// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms
const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; const HEARTBEAT_ACK_TIMEOUT: u64 = 2000;
@ -13,40 +21,29 @@ pub(super) struct HeartbeatHandler {
pub heartbeat_interval: Duration, pub heartbeat_interval: Duration,
/// The send channel for the heartbeat thread /// The send channel for the heartbeat thread
pub send: Sender<HeartbeatThreadCommunication>, pub send: Sender<HeartbeatThreadCommunication>,
/// The handle of the thread
handle: JoinHandle<()>,
} }
impl HeartbeatHandler { impl HeartbeatHandler {
pub fn new( pub fn new(
heartbeat_interval: Duration, heartbeat_interval: Duration,
websocket_tx: Arc< websocket_tx: Arc<Mutex<Sink>>,
Mutex<
SplitSink<
WebSocketStream<MaybeTlsStream<TcpStream>>,
tokio_tungstenite::tungstenite::Message,
>,
>,
>,
kill_rc: tokio::sync::broadcast::Receiver<()>, kill_rc: tokio::sync::broadcast::Receiver<()>,
) -> HeartbeatHandler { ) -> Self {
let (send, receive) = tokio::sync::mpsc::channel(32); let (send, receive) = tokio::sync::mpsc::channel(32);
let kill_receive = kill_rc.resubscribe(); let kill_receive = kill_rc.resubscribe();
let handle: JoinHandle<()> = task::spawn(async move { #[cfg(not(target_arch = "wasm32"))]
HeartbeatHandler::heartbeat_task( task::spawn(async move {
websocket_tx, Self::heartbeat_task(websocket_tx, heartbeat_interval, receive, kill_receive).await;
heartbeat_interval, });
receive, #[cfg(target_arch = "wasm32")]
kill_receive, wasm_bindgen_futures::spawn_local(async move {
) Self::heartbeat_task(websocket_tx, heartbeat_interval, receive, kill_receive).await;
.await;
}); });
Self { Self {
heartbeat_interval, heartbeat_interval,
send, send,
handle,
} }
} }
@ -55,22 +52,17 @@ impl HeartbeatHandler {
/// Can be killed by the kill broadcast; /// Can be killed by the kill broadcast;
/// If the websocket is closed, will die out next time it tries to send a heartbeat; /// If the websocket is closed, will die out next time it tries to send a heartbeat;
pub async fn heartbeat_task( pub async fn heartbeat_task(
websocket_tx: Arc< websocket_tx: Arc<Mutex<Sink>>,
Mutex<
SplitSink<
WebSocketStream<MaybeTlsStream<TcpStream>>,
tokio_tungstenite::tungstenite::Message,
>,
>,
>,
heartbeat_interval: Duration, heartbeat_interval: Duration,
mut receive: tokio::sync::mpsc::Receiver<HeartbeatThreadCommunication>, mut receive: Receiver<HeartbeatThreadCommunication>,
mut kill_receive: tokio::sync::broadcast::Receiver<()>, mut kill_receive: tokio::sync::broadcast::Receiver<()>,
) { ) {
let mut last_heartbeat_timestamp: Instant = time::Instant::now(); let mut last_heartbeat_timestamp: Instant = time::Instant::now();
let mut last_heartbeat_acknowledged = true; let mut last_heartbeat_acknowledged = true;
let mut last_seq_number: Option<u64> = None; let mut last_seq_number: Option<u64> = None;
safina_timer::start_timer_thread();
loop { loop {
if kill_receive.try_recv().is_ok() { if kill_receive.try_recv().is_ok() {
trace!("GW: Closing heartbeat task"); trace!("GW: Closing heartbeat task");
@ -122,9 +114,9 @@ impl HeartbeatHandler {
let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); let heartbeat_json = serde_json::to_string(&heartbeat).unwrap();
let msg = tokio_tungstenite::tungstenite::Message::text(heartbeat_json); let msg = GatewayMessage(heartbeat_json);
let send_result = websocket_tx.lock().await.send(msg).await; let send_result = websocket_tx.lock().await.send(msg.into()).await;
if send_result.is_err() { if send_result.is_err() {
// We couldn't send, the websocket is broken // We couldn't send, the websocket is broken
warn!("GW: Couldnt send heartbeat, websocket seems broken"); warn!("GW: Couldnt send heartbeat, websocket seems broken");

View File

@ -5,24 +5,14 @@ use super::*;
/// Represents a messsage received from the gateway. This will be either a [types::GatewayReceivePayload], containing events, or a [GatewayError]. /// Represents a messsage received from the gateway. This will be either a [types::GatewayReceivePayload], containing events, or a [GatewayError].
/// This struct is used internally when handling messages. /// This struct is used internally when handling messages.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct GatewayMessage { pub struct GatewayMessage(pub String);
/// The message we received from the server
pub(super) message: tokio_tungstenite::tungstenite::Message,
}
impl GatewayMessage { impl GatewayMessage {
/// Creates self from a tungstenite message
pub fn from_tungstenite_message(message: tokio_tungstenite::tungstenite::Message) -> Self {
Self { message }
}
/// Parses the message as an error; /// Parses the message as an error;
/// Returns the error if succesfully parsed, None if the message isn't an error /// Returns the error if succesfully parsed, None if the message isn't an error
pub fn error(&self) -> Option<GatewayError> { pub fn error(&self) -> Option<GatewayError> {
let content = self.message.to_string();
// Some error strings have dots on the end, which we don't care about // Some error strings have dots on the end, which we don't care about
let processed_content = content.to_lowercase().replace('.', ""); let processed_content = self.0.to_lowercase().replace('.', "");
match processed_content.as_str() { match processed_content.as_str() {
"unknown error" | "4000" => Some(GatewayError::Unknown), "unknown error" | "4000" => Some(GatewayError::Unknown),
@ -45,29 +35,9 @@ impl GatewayMessage {
} }
} }
/// Returns whether or not the message is an error
pub fn is_error(&self) -> bool {
self.error().is_some()
}
/// Parses the message as a payload; /// Parses the message as a payload;
/// Returns a result of deserializing /// Returns a result of deserializing
pub fn payload(&self) -> Result<types::GatewayReceivePayload, serde_json::Error> { pub fn payload(&self) -> Result<types::GatewayReceivePayload, serde_json::Error> {
return serde_json::from_str(self.message.to_text().unwrap()); serde_json::from_str(&self.0)
}
/// Returns whether or not the message is a payload
pub fn is_payload(&self) -> bool {
// close messages are never payloads, payloads are only text messages
if self.message.is_close() | !self.message.is_text() {
return false;
}
return self.payload().is_ok();
}
/// Returns whether or not the message is empty
pub fn is_empty(&self) -> bool {
self.message.is_empty()
} }
} }

View File

@ -1,8 +1,13 @@
use async_trait::async_trait;
pub mod backends;
pub mod events;
pub mod gateway; pub mod gateway;
pub mod handle; pub mod handle;
pub mod heartbeat; pub mod heartbeat;
pub mod message; pub mod message;
pub use backends::*;
pub use gateway::*; pub use gateway::*;
pub use handle::*; pub use handle::*;
use heartbeat::*; use heartbeat::*;
@ -11,28 +16,11 @@ pub use message::*;
use crate::errors::GatewayError; use crate::errors::GatewayError;
use crate::types::{Snowflake, WebSocketEvent}; use crate::types::{Snowflake, WebSocketEvent};
use async_trait::async_trait;
use std::any::Any; use std::any::Any;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::time::Duration;
use tokio::time::sleep_until;
use futures_util::stream::SplitSink;
use futures_util::stream::SplitStream;
use futures_util::SinkExt;
use futures_util::StreamExt;
use log::{info, trace, warn};
use tokio::net::TcpStream;
use tokio::sync::mpsc::Sender;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::task;
use tokio::task::JoinHandle;
use tokio::time;
use tokio::time::Instant;
use tokio_tungstenite::MaybeTlsStream;
use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream};
// Gateway opcodes // Gateway opcodes
/// Opcode received when the server dispatches a [crate::types::WebSocketEvent] /// Opcode received when the server dispatches a [crate::types::WebSocketEvent]
@ -134,54 +122,3 @@ impl<T: WebSocketEvent> GatewayEvent<T> {
} }
} }
} }
#[cfg(test)]
mod example {
use crate::types;
use super::*;
use std::sync::atomic::{AtomicI32, Ordering::Relaxed};
#[derive(Debug)]
struct Consumer {
_name: String,
events_received: AtomicI32,
}
#[async_trait]
impl Observer<types::GatewayResume> for Consumer {
async fn update(&self, _data: &types::GatewayResume) {
self.events_received.fetch_add(1, Relaxed);
}
}
#[tokio::test]
async fn test_observer_behavior() {
let mut event = GatewayEvent::default();
let new_data = types::GatewayResume {
token: "token_3276ha37am3".to_string(),
session_id: "89346671230".to_string(),
seq: "3".to_string(),
};
let consumer = Arc::new(Consumer {
_name: "first".into(),
events_received: 0.into(),
});
event.subscribe(consumer.clone());
let second_consumer = Arc::new(Consumer {
_name: "second".into(),
events_received: 0.into(),
});
event.subscribe(second_consumer.clone());
event.notify(new_data.clone()).await;
event.unsubscribe(&*consumer);
event.notify(new_data).await;
assert_eq!(consumer.events_received.load(Relaxed), 1);
assert_eq!(second_consumer.events_received.load(Relaxed), 2);
}
}

View File

@ -36,14 +36,11 @@ impl Instance {
pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> { pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult<Instance> {
let limits_information; let limits_information;
if limited { if limited {
let limits_configuration = let limits_configuration = ChorusRequest::get_limits_config(&urls.api).await?.rate;
Some(ChorusRequest::get_limits_config(&urls.api).await?.rate); let limits = ChorusRequest::limits_config_to_hashmap(&limits_configuration);
let limits = Some(ChorusRequest::limits_config_to_hashmap(
limits_configuration.as_ref().unwrap(),
));
limits_information = Some(LimitsInformation { limits_information = Some(LimitsInformation {
ratelimits: limits.unwrap(), ratelimits: limits,
configuration: limits_configuration.unwrap(), configuration: limits_configuration,
}); });
} else { } else {
limits_information = None; limits_information = None;
@ -138,7 +135,7 @@ impl ChorusUser {
let object = Arc::new(RwLock::new(User::default())); let object = Arc::new(RwLock::new(User::default()));
let wss_url = instance.read().unwrap().urls.wss.clone(); let wss_url = instance.read().unwrap().urls.wss.clone();
// Dummy gateway object // Dummy gateway object
let gateway = Gateway::new(wss_url).await.unwrap(); let gateway = Gateway::spawn(wss_url).await.unwrap();
ChorusUser { ChorusUser {
token, token,
belongs_to: instance.clone(), belongs_to: instance.clone(),

View File

@ -14,6 +14,8 @@
clippy::new_without_default, clippy::new_without_default,
clippy::useless_conversion clippy::useless_conversion
)] )]
#[cfg(all(feature = "rt", feature = "rt_multi_thread"))]
compile_error!("feature \"rt\" and feature \"rt_multi_thread\" cannot be enabled at the same time");
use url::{ParseError, Url}; use url::{ParseError, Url};

View File

@ -11,6 +11,20 @@ pub struct SentryConfiguration {
pub environment: String, pub environment: String,
} }
impl SentryConfiguration {
#[cfg(not(target_arch = "wasm32"))]
fn get_hostname() -> std::io::Result<OsString> {
hostname::get()
}
#[cfg(target_arch = "wasm32")]
fn get_hostname() -> std::io::Result<OsString> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Unsupported: wasm targets do not have a hostname",
))
}
}
impl Default for SentryConfiguration { impl Default for SentryConfiguration {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -19,7 +33,7 @@ impl Default for SentryConfiguration {
"https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3",
), ),
trace_sample_rate: 1.0, trace_sample_rate: 1.0,
environment: hostname::get() environment: SentryConfiguration::get_hostname()
.unwrap_or_else(|_| OsString::new()) .unwrap_or_else(|_| OsString::new())
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),

View File

@ -15,7 +15,10 @@ use crate::types::{
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::GatewayHandle;
#[cfg(feature = "client")]
use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{observe_option_vec, Composite, Updateable}; use chorus_macros::{observe_option_vec, Composite, Updateable};

View File

@ -6,11 +6,14 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::User; use crate::types::entities::User;
use crate::types::Snowflake; use crate::types::Snowflake;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};

View File

@ -1,6 +1,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use bitflags::bitflags;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
@ -11,12 +12,11 @@ use crate::types::{
interfaces::WelcomeScreenObject, interfaces::WelcomeScreenObject,
utils::Snowflake, utils::Snowflake,
}; };
use bitflags::bitflags;
use super::PublicUser; use super::PublicUser;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable}; use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable};
@ -24,6 +24,9 @@ use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
/// See <https://discord.com/developers/docs/resources/guild> /// See <https://discord.com/developers/docs/resources/guild>
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "client", derive(Updateable, Composite))]

View File

@ -24,7 +24,10 @@ pub use voice_state::*;
pub use webhook::*; pub use webhook::*;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use async_trait::async_trait; use async_trait::async_trait;

View File

@ -9,11 +9,14 @@ use crate::types::utils::Snowflake;
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "client", derive(Updateable, Composite))]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]

View File

@ -5,11 +5,14 @@ use serde_aux::prelude::deserialize_option_number_from_string;
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};

View File

@ -1,13 +1,16 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::Composite;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::GatewayHandle;
#[cfg(feature = "client")]
use crate::gateway::Updateable;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,17 +21,20 @@ use crate::types::{
utils::Snowflake, utils::Snowflake,
}; };
/// The VoiceState struct. Note, that Discord does not have an `id` field for this, whereas Spacebar
/// does.
///
/// See <https://docs.spacebar.chat/routes/#cmp--schemas-voicestate> /// See <https://docs.spacebar.chat/routes/#cmp--schemas-voicestate>
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
#[cfg_attr(feature = "client", derive(Updateable, Composite))] #[cfg_attr(feature = "client", derive(Composite))]
pub struct VoiceState { pub struct VoiceState {
pub guild_id: Option<Snowflake>, pub guild_id: Option<Snowflake>,
pub guild: Option<Guild>, pub guild: Option<Guild>,
pub channel_id: Option<Snowflake>, pub channel_id: Option<Snowflake>,
pub user_id: Snowflake, pub user_id: Snowflake,
pub member: Option<Arc<RwLock<GuildMember>>>, pub member: Option<Arc<RwLock<GuildMember>>>,
pub session_id: Snowflake, pub session_id: String,
pub token: Option<String>, pub token: Option<String>,
pub deaf: bool, pub deaf: bool,
pub mute: bool, pub mute: bool,
@ -38,5 +44,15 @@ pub struct VoiceState {
pub self_video: bool, pub self_video: bool,
pub suppress: bool, pub suppress: bool,
pub request_to_speak_timestamp: Option<DateTime<Utc>>, pub request_to_speak_timestamp: Option<DateTime<Utc>>,
pub id: Snowflake, pub id: Option<Snowflake>, // Only exists on Spacebar
}
impl Updateable for VoiceState {
fn id(&self) -> Snowflake {
if let Some(id) = self.id {
id // ID exists: Only the case for Spacebar Server impls
} else {
self.user_id // ID doesn't exist: Discord does not have the ID field - ID is void
}
}
} }

View File

@ -4,7 +4,7 @@ use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::gateway::{GatewayHandle, Updateable}; use crate::gateway::Updateable;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use chorus_macros::{Composite, Updateable}; use chorus_macros::{Composite, Updateable};
@ -12,6 +12,9 @@ use chorus_macros::{Composite, Updateable};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::types::Composite; use crate::types::Composite;
#[cfg(feature = "client")]
use crate::gateway::GatewayHandle;
use crate::types::{ use crate::types::{
entities::{Guild, User}, entities::{Guild, User},
utils::Snowflake, utils::Snowflake,

View File

@ -1,8 +1,14 @@
use chorus::types::RegisterSchema; use chorus::types::RegisterSchema;
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
mod common; mod common;
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_registration() { async fn test_registration() {
let bundle = common::setup().await; let bundle = common::setup().await;
let reg = RegisterSchema { let reg = RegisterSchema {

View File

@ -4,8 +4,15 @@ use chorus::types::{
}; };
mod common; mod common;
// PRETTYFYME: Move common wasm setup to common.rs
#[tokio::test] #[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn get_channel() { async fn get_channel() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let bundle_channel = bundle.channel.read().unwrap().clone(); let bundle_channel = bundle.channel.read().unwrap().clone();
@ -18,7 +25,8 @@ async fn get_channel() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn delete_channel() { async fn delete_channel() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let channel_guard = bundle.channel.write().unwrap().clone(); let channel_guard = bundle.channel.write().unwrap().clone();
@ -27,7 +35,8 @@ async fn delete_channel() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn modify_channel() { async fn modify_channel() {
const CHANNEL_NAME: &str = "beepboop"; const CHANNEL_NAME: &str = "beepboop";
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
@ -85,7 +94,8 @@ async fn modify_channel() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn get_channel_messages() { async fn get_channel_messages() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let channel_id: Snowflake = bundle.channel.read().unwrap().id; let channel_id: Snowflake = bundle.channel.read().unwrap().id;
@ -141,7 +151,8 @@ async fn get_channel_messages() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn create_dm() { async fn create_dm() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let other_user = bundle.create_user("integrationtestuser2").await; let other_user = bundle.create_user("integrationtestuser2").await;

View File

@ -43,7 +43,9 @@ impl TestBundle {
limits: self.user.limits.clone(), limits: self.user.limits.clone(),
settings: self.user.settings.clone(), settings: self.user.settings.clone(),
object: self.user.object.clone(), object: self.user.object.clone(),
gateway: Gateway::new(self.instance.urls.wss.clone()).await.unwrap(), gateway: Gateway::spawn(self.instance.urls.wss.clone())
.await
.unwrap(),
} }
} }
} }

View File

@ -2,24 +2,32 @@ mod common;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use chorus::errors::GatewayError;
use chorus::gateway::*; use chorus::gateway::*;
use chorus::types::{self, ChannelModifySchema, RoleCreateModifySchema, RoleObject}; use chorus::types::{self, ChannelModifySchema, RoleCreateModifySchema, RoleObject};
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
/// Tests establishing a connection (hello and heartbeats) on the local gateway; /// Tests establishing a connection (hello and heartbeats) on the local gateway;
async fn test_gateway_establish() { async fn test_gateway_establish() {
let bundle = common::setup().await; let bundle = common::setup().await;
Gateway::new(bundle.urls.wss.clone()).await.unwrap(); let _: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone()).await.unwrap();
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
/// Tests establishing a connection and authenticating /// Tests establishing a connection and authenticating
async fn test_gateway_authenticate() { async fn test_gateway_authenticate() {
let bundle = common::setup().await; let bundle = common::setup().await;
let gateway = Gateway::new(bundle.urls.wss.clone()).await.unwrap(); let gateway: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone()).await.unwrap();
let mut identify = types::GatewayIdentifyPayload::common(); let mut identify = types::GatewayIdentifyPayload::common();
identify.token = bundle.user.token.clone(); identify.token = bundle.user.token.clone();
@ -28,7 +36,8 @@ async fn test_gateway_authenticate() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_self_updating_structs() { async fn test_self_updating_structs() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let received_channel = bundle let received_channel = bundle
@ -61,7 +70,8 @@ async fn test_self_updating_structs() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_recursive_self_updating_structs() { async fn test_recursive_self_updating_structs() {
// Setup // Setup
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
@ -116,3 +126,92 @@ async fn test_recursive_self_updating_structs() {
assert_eq!(guild_role_inner.name, "yippieee".to_string()); assert_eq!(guild_role_inner.name, "yippieee".to_string());
common::teardown(bundle).await; common::teardown(bundle).await;
} }
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn test_error() {
let error = GatewayMessage("4000".to_string()).error().unwrap();
assert_eq!(error, GatewayError::Unknown);
let error = GatewayMessage("4001".to_string()).error().unwrap();
assert_eq!(error, GatewayError::UnknownOpcode);
let error = GatewayMessage("4002".to_string()).error().unwrap();
assert_eq!(error, GatewayError::Decode);
let error = GatewayMessage("4003".to_string()).error().unwrap();
assert_eq!(error, GatewayError::NotAuthenticated);
let error = GatewayMessage("4004".to_string()).error().unwrap();
assert_eq!(error, GatewayError::AuthenticationFailed);
let error = GatewayMessage("4005".to_string()).error().unwrap();
assert_eq!(error, GatewayError::AlreadyAuthenticated);
let error = GatewayMessage("4007".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidSequenceNumber);
let error = GatewayMessage("4008".to_string()).error().unwrap();
assert_eq!(error, GatewayError::RateLimited);
let error = GatewayMessage("4009".to_string()).error().unwrap();
assert_eq!(error, GatewayError::SessionTimedOut);
let error = GatewayMessage("4010".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidShard);
let error = GatewayMessage("4011".to_string()).error().unwrap();
assert_eq!(error, GatewayError::ShardingRequired);
let error = GatewayMessage("4012".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidAPIVersion);
let error = GatewayMessage("4013".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidIntents);
let error = GatewayMessage("4014".to_string()).error().unwrap();
assert_eq!(error, GatewayError::DisallowedIntents);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn test_error_message() {
let error = GatewayMessage("Unknown Error".to_string()).error().unwrap();
assert_eq!(error, GatewayError::Unknown);
let error = GatewayMessage("Unknown Opcode".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::UnknownOpcode);
let error = GatewayMessage("Decode Error".to_string()).error().unwrap();
assert_eq!(error, GatewayError::Decode);
let error = GatewayMessage("Not Authenticated".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::NotAuthenticated);
let error = GatewayMessage("Authentication Failed".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::AuthenticationFailed);
let error = GatewayMessage("Already Authenticated".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::AlreadyAuthenticated);
let error = GatewayMessage("Invalid Seq".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidSequenceNumber);
let error = GatewayMessage("Rate Limited".to_string()).error().unwrap();
assert_eq!(error, GatewayError::RateLimited);
let error = GatewayMessage("Session Timed Out".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::SessionTimedOut);
let error = GatewayMessage("Invalid Shard".to_string()).error().unwrap();
assert_eq!(error, GatewayError::InvalidShard);
let error = GatewayMessage("Sharding Required".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::ShardingRequired);
let error = GatewayMessage("Invalid API Version".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::InvalidAPIVersion);
let error = GatewayMessage("Invalid Intent(s)".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::InvalidIntents);
let error = GatewayMessage("Disallowed Intent(s)".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::DisallowedIntents);
// Also test the dot thing
let error = GatewayMessage("Invalid Intent(s).".to_string())
.error()
.unwrap();
assert_eq!(error, GatewayError::InvalidIntents);
}

View File

@ -3,8 +3,14 @@ use chorus::types::{
}; };
mod common; mod common;
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn guild_creation_deletion() { async fn guild_creation_deletion() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
@ -26,7 +32,8 @@ async fn guild_creation_deletion() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn get_channels() { async fn get_channels() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let guild = bundle.guild.read().unwrap().clone(); let guild = bundle.guild.read().unwrap().clone();
@ -34,7 +41,8 @@ async fn get_channels() {
common::teardown(bundle).await; common::teardown(bundle).await;
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn guild_create_ban() { async fn guild_create_ban() {
// TODO: When routes exist to check if user x is on guild y, add this as an assertion to check // TODO: When routes exist to check if user x is on guild y, add this as an assertion to check
// if Spacebar actually bans the user. // if Spacebar actually bans the user.
@ -71,7 +79,8 @@ async fn guild_create_ban() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn modify_guild() { async fn modify_guild() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let schema = GuildModifySchema { let schema = GuildModifySchema {
@ -86,7 +95,8 @@ async fn modify_guild() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn guild_remove_member() { async fn guild_remove_member() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let channel = bundle.channel.read().unwrap().clone(); let channel = bundle.channel.read().unwrap().clone();

View File

@ -1,6 +1,12 @@
mod common; mod common;
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn generate_general_configuration_schema() { async fn generate_general_configuration_schema() {
let bundle = common::setup().await; let bundle = common::setup().await;
bundle bundle

View File

@ -1,6 +1,13 @@
mod common; mod common;
use chorus::types::CreateChannelInviteSchema; use chorus::types::CreateChannelInviteSchema;
#[tokio::test] // PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn create_accept_invite() { async fn create_accept_invite() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let channel = bundle.channel.read().unwrap().clone(); let channel = bundle.channel.read().unwrap().clone();

View File

@ -1,8 +1,14 @@
use chorus::{errors::ChorusResult, types::GuildMember}; use chorus::{errors::ChorusResult, types::GuildMember};
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
mod common; mod common;
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn add_remove_role() -> ChorusResult<()> { async fn add_remove_role() -> ChorusResult<()> {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let guild = bundle.guild.read().unwrap().id; let guild = bundle.guild.read().unwrap().id;

View File

@ -2,10 +2,16 @@ use std::fs::File;
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
use chorus::types::{self, Guild, Message, MessageSearchQuery}; use chorus::types::{self, Guild, Message, MessageSearchQuery};
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
mod common; mod common;
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn send_message() { async fn send_message() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let message = types::MessageSendSchema { let message = types::MessageSendSchema {
@ -17,8 +23,12 @@ async fn send_message() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn send_message_attachment() { async fn send_message_attachment() {
/// FIXME: Fix this test for wasm32. wasm32-unknown-unknown does not have a filesystem and therefore cannot read files.
#[cfg(target_arch = "wasm32")]
return;
let f = File::open("./README.md").unwrap(); let f = File::open("./README.md").unwrap();
let mut reader = BufReader::new(f); let mut reader = BufReader::new(f);
let mut buffer = Vec::new(); let mut buffer = Vec::new();
@ -54,8 +64,12 @@ async fn send_message_attachment() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn search_messages() { async fn search_messages() {
/// FIXME: Fix this test for wasm32. wasm32-unknown-unknown does not have a filesystem and therefore cannot read files.
#[cfg(target_arch = "wasm32")]
return;
let f = File::open("./README.md").unwrap(); let f = File::open("./README.md").unwrap();
let mut reader = BufReader::new(f); let mut reader = BufReader::new(f);
let mut buffer = Vec::new(); let mut buffer = Vec::new();
@ -100,7 +114,8 @@ async fn search_messages() {
assert_eq!(query_result.get(0).unwrap().id, message.id); assert_eq!(query_result.get(0).unwrap().id, message.id);
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_stickies() { async fn test_stickies() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let message = types::MessageSendSchema { let message = types::MessageSendSchema {

View File

@ -1,8 +1,14 @@
use chorus::types::{self, Relationship, RelationshipType}; use chorus::types::{self, Relationship, RelationshipType};
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
mod common; mod common;
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_get_mutual_relationships() { async fn test_get_mutual_relationships() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await; let mut other_user = bundle.create_user("integrationtestuser2").await;
@ -23,7 +29,8 @@ async fn test_get_mutual_relationships() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_get_relationships() { async fn test_get_relationships() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await; let mut other_user = bundle.create_user("integrationtestuser2").await;
@ -46,7 +53,8 @@ async fn test_get_relationships() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_modify_relationship_friends() { async fn test_modify_relationship_friends() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await; let mut other_user = bundle.create_user("integrationtestuser2").await;
@ -97,7 +105,8 @@ async fn test_modify_relationship_friends() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn test_modify_relationship_block() { async fn test_modify_relationship_block() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await; let mut other_user = bundle.create_user("integrationtestuser2").await;

View File

@ -1,8 +1,14 @@
use chorus::types::{self, RoleCreateModifySchema, RoleObject}; use chorus::types::{self, RoleCreateModifySchema, RoleObject};
// PRETTYFYME: Move common wasm setup to common.rs
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test_configure!(run_in_browser);
mod common; mod common;
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn create_and_get_roles() { async fn create_and_get_roles() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS; let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS;
@ -31,7 +37,8 @@ async fn create_and_get_roles() {
common::teardown(bundle).await common::teardown(bundle).await
} }
#[tokio::test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), tokio::test)]
async fn get_and_delete_role() { async fn get_and_delete_role() {
let mut bundle = common::setup().await; let mut bundle = common::setup().await;
let guild_id = bundle.guild.read().unwrap().id; let guild_id = bundle.guild.read().unwrap().id;

7
tests/wasm.rs Normal file
View File

@ -0,0 +1,7 @@
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn pass() {
let _ = String::new();
}