Compare commits

...

8 Commits

Author SHA1 Message Date
xystrive e24816f500
Merge 317dbe1ed1 into 743f106ec6 2024-07-05 16:15:42 +00:00
xystrive 317dbe1ed1 fix: according to changes made to `ChorusUser` object field 4920c91e52 2024-07-05 17:15:24 +01:00
xystrive ee19cb762f fix: concatenate `Instance`'s api url with endpoint 2024-07-05 17:06:16 +01:00
xystrive c3c506bc1b fix: typo in `ChorusError::MfaRequired` `Display` message 2024-07-05 17:04:05 +01:00
kozabrada123 743f106ec6
Miscellaneous fixes (#514)
- fix `PATCH /users/@me` - It incorrectly returned a required password error, even if the current password was set
- fix `GET /users/@me/guilds` - It incorrectly sent body parameters instead of query ones
- don't log debug! for every successful ratelimited request - use trace! so it's less spamy
- update the max expected compression ratio (several times) from 20 to 200. let's hope that will be enough
- fix deserialization errors relating to guild folders in user settings
- fix a panic in `SqlxBitFlags` if there are extra flags. It now truncates them
- update `chorus_macros` to 0.4.1 (due to the above fix)
- log (trace!) event data if we fail to parse it or it's unrecognised, for debugging purposes
- fix a deserialization error in the `MessageACK` event
- fix `public_flags` in user objects not being `PublicFlags` bitflags
2024-06-28 14:05:59 +02:00
Quat3rnion d59161619c
Add custom deserializer for PermissionOverwriteType (#512)
* Add custom deserializer for PermissionOverwriteType
2024-06-27 20:36:39 +02:00
Quat3rnion 39e7f89c78
Backend/guilds (#509)
* Fix SQL encode/decode for GuildFeatures

* Use distinct PermissionFlags type

* Add Emoji schema types, modify GuildBan with feature lock

* Add Schemas for pruning guild members

* Add schemas for interfacing with stickers backend routes

* Add schemas for interfacing with vanity-url backend routes

* Add schema for interfacing with guilds/id/welcome-screen route

* Make all Option<Vec> types Vec types with #[serde(default)]

* Add various types to support guilds/* api routes

* Add missing enums and structs for searching messages

* Use proper distinct types

* Add EmbedType enum

* Use distinct PermissionFlags type

* Changes supporting backend for VoiceState

* Changes supporting backend for AuditLog's
2024-06-27 08:45:51 +02:00
kozabrada123 89333d6353
Implement gateway options, zlib-stream compression (#508)
* feat: add GatewayOptions

* feat: implement zlib-stream compression

This also changes how gateway messages work.
Now each gateway backend converts its message into an
intermediary RawGatewayMessage, from which we inflate
and parse GatewayMessages.

Thanks to ByteAlex and their zlib-stream-rs crate, which
helped me understand how to parse a compressed websocket stream
2024-06-23 17:23:13 +02:00
59 changed files with 1442 additions and 243 deletions

View File

@ -124,7 +124,7 @@ jobs:
cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.92" --force
GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt, voice_gateway"
wasm-chrome:
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

52
Cargo.lock generated
View File

@ -231,6 +231,7 @@ dependencies = [
"crypto_secretbox",
"custom_error",
"discortp",
"flate2",
"futures-util",
"getrandom",
"hostname",
@ -250,6 +251,7 @@ dependencies = [
"serde_json",
"serde_repr",
"serde_with",
"simple_logger",
"sqlx",
"thiserror",
"tokio",
@ -264,7 +266,7 @@ dependencies = [
[[package]]
name = "chorus-macros"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"async-trait",
"quote",
@ -353,6 +355,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
@ -553,6 +564,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.0"
@ -1251,6 +1272,24 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http 1.1.0",
"httparse",
"memchr",
"mime",
"spin 0.9.8",
"tokio",
"version_check",
]
[[package]]
name = "native-tls"
version = "0.2.11"
@ -1588,6 +1627,7 @@ dependencies = [
"hyper 1.3.1",
"hyper-util",
"mime",
"multer",
"nix 0.28.0",
"parking_lot",
"percent-encoding",
@ -2124,6 +2164,16 @@ dependencies = [
"time",
]
[[package]]
name = "simple_logger"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb"
dependencies = [
"log",
"windows-sys 0.48.0",
]
[[package]]
name = "slab"
version = "0.4.9"

View File

@ -16,7 +16,7 @@ default = ["client", "rt-multi-thread"]
backend = ["poem", "sqlx"]
rt-multi-thread = ["tokio/rt-multi-thread"]
rt = ["tokio/rt"]
client = []
client = ["flate2"]
voice = ["voice_udp", "voice_gateway"]
voice_udp = ["dep:discortp", "dep:crypto_secretbox"]
voice_gateway = []
@ -38,7 +38,7 @@ http = "0.2.11"
base64 = "0.21.7"
bitflags = { version = "2.4.1", features = ["serde"] }
lazy_static = "1.4.0"
poem = { version = "3.0.1", optional = true }
poem = { version = "3.0.1", features = ["multipart"], optional = true }
thiserror = "1.0.56"
jsonwebtoken = "8.3.0"
log = "0.4.20"
@ -56,6 +56,7 @@ sqlx = { version = "0.7.3", features = [
discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord", "demux"] }
crypto_secretbox = { version = "0.1.1", optional = true }
rand = "0.8.5"
flate2 = { version = "1.0.30", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rustls = "0.21.10"
@ -78,3 +79,4 @@ wasmtimer = "0.2.0"
lazy_static = "1.4.0"
wasm-bindgen-test = "0.3.42"
wasm-bindgen = "0.2.92"
simple_logger = { version = "5.0.0", default-features=false }

View File

@ -15,7 +15,7 @@ dependencies = [
[[package]]
name = "chorus-macros"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"async-trait",
"quote",

View File

@ -1,6 +1,6 @@
[package]
name = "chorus-macros"
version = "0.4.0"
version = "0.4.1"
edition = "2021"
license = "MPL-2.0"
description = "Macros for the chorus crate."

View File

@ -214,7 +214,9 @@ pub fn serde_bitflag_derive(input: TokenStream) -> TokenStream {
// let s = String::deserialize(deserializer)?.parse::<u64>().map_err(serde::de::Error::custom)?;
let s = crate::types::serde::string_or_u64(deserializer)?;
Ok(Self::from_bits(s).unwrap())
// Note: while truncating may not be ideal, it's better than a panic if there are
// extra flags
Ok(Self::from_bits_truncate(s))
}
}
}

View File

@ -3,6 +3,8 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This example showcase how to properly use gateway observers.
// (This assumes you have a manually created gateway, if you created
// a ChorusUser by e.g. logging in, you can access the gateway with user.gateway)
//
// To properly run it, you will need to change the token below.
@ -12,7 +14,7 @@ const TOKEN: &str = "";
const GATEWAY_URL: &str = "wss://gateway.old.server.spacebar.chat/";
use async_trait::async_trait;
use chorus::gateway::Gateway;
use chorus::gateway::{Gateway, GatewayOptions};
use chorus::{
self,
gateway::Observer,
@ -47,8 +49,14 @@ impl Observer<GatewayReady> for ExampleObserver {
async fn main() {
let gateway_websocket_url = GATEWAY_URL.to_string();
// These options specify the encoding format, compression, etc
//
// For most cases the defaults should work, though some implementations
// might only support some formats or not support compression
let options = GatewayOptions::default();
// Initiate the gateway connection
let gateway = Gateway::spawn(gateway_websocket_url).await.unwrap();
let gateway = Gateway::spawn(gateway_websocket_url, options).await.unwrap();
// Create an instance of our observer
let observer = ExampleObserver {};

View File

@ -3,7 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This example showcases how to initiate a gateway connection manually
// (e. g. not through ChorusUser)
// (e. g. not through ChorusUser or Instance)
//
// To properly run it, you will need to modify the token below.
@ -14,7 +14,7 @@ const GATEWAY_URL: &str = "wss://gateway.old.server.spacebar.chat/";
use std::time::Duration;
use chorus::gateway::Gateway;
use chorus::gateway::{Gateway, GatewayOptions};
use chorus::{self, types::GatewayIdentifyPayload};
#[cfg(not(target_arch = "wasm32"))]
@ -27,8 +27,14 @@ use wasmtimer::tokio::sleep;
async fn main() {
let gateway_websocket_url = GATEWAY_URL.to_string();
// These options specify the encoding format, compression, etc
//
// For most cases the defaults should work, though some implementations
// might only support some formats or not support compression
let options = GatewayOptions::default();
// Initiate the gateway connection, starting a listener in one thread and a heartbeat handler in another
let gateway = Gateway::spawn(gateway_websocket_url).await.unwrap();
let gateway = Gateway::spawn(gateway_websocket_url, options).await.unwrap();
// At this point, we are connected to the server and are sending heartbeats, however we still haven't authenticated

View File

@ -24,5 +24,5 @@ async fn main() {
.await
.expect("An error occurred during the login process");
dbg!(user.belongs_to);
dbg!(&user.object.read().unwrap().username);
dbg!(&user.object.unwrap().as_ref().read().unwrap().username);
}

View File

@ -44,6 +44,16 @@ impl ChorusUser {
&mut self,
query: Option<GetUserGuildSchema>,
) -> ChorusResult<Vec<Guild>> {
let query_parameters = {
if let Some(query_some) = query {
query_some.to_query()
}
else {
Vec::new()
}
};
let url = format!(
"{}/users/@me/guilds",
self.belongs_to.read().unwrap().urls.api,
@ -53,7 +63,7 @@ impl ChorusUser {
.get(url)
.header("Authorization", self.token())
.header("Content-Type", "application/json")
.body(to_string(&query).unwrap()),
.query(&query_parameters),
limit_type: LimitType::Global,
};

View File

@ -40,12 +40,18 @@ impl ChorusUser {
/// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/user#modify-current-user>
pub async fn modify(&mut self, modify_schema: UserModifySchema) -> ChorusResult<User> {
if modify_schema.new_password.is_some()
// See <https://docs.discord.sex/resources/user#json-params>, note 1
let requires_current_password = modify_schema.username.is_some()
|| modify_schema.discriminator.is_some()
|| modify_schema.email.is_some()
|| modify_schema.code.is_some()
{
|| modify_schema.date_of_birth.is_some()
|| modify_schema.new_password.is_some();
if requires_current_password && modify_schema.current_password.is_none() {
return Err(ChorusError::PasswordRequired);
}
let request = Client::new()
.patch(format!(
"{}/users/@me",
@ -132,4 +138,3 @@ impl User {
}
}
}

View File

@ -48,7 +48,7 @@ custom_error! {
/// Invalid, insufficient or too many arguments provided.
InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}",
/// The request requires MFA verification
MfaRequired {error: MfaRequiredSchema} = "Mfa verification required to perform this action"
MfaRequired {error: MfaRequiredSchema} = "Mfa verification is required to perform this action"
}
impl From<reqwest::Error> for ChorusError {

View File

@ -12,7 +12,7 @@ use tokio_tungstenite::{
connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream,
};
use crate::gateway::GatewayMessage;
use crate::gateway::{GatewayMessage, RawGatewayMessage};
#[derive(Debug, Clone)]
pub struct TungsteniteBackend;
@ -80,3 +80,22 @@ impl From<tungstenite::Message> for GatewayMessage {
Self(value.to_string())
}
}
impl From<RawGatewayMessage> for tungstenite::Message {
fn from(message: RawGatewayMessage) -> Self {
match message {
RawGatewayMessage::Text(text) => tungstenite::Message::Text(text),
RawGatewayMessage::Bytes(bytes) => tungstenite::Message::Binary(bytes),
}
}
}
impl From<tungstenite::Message> for RawGatewayMessage {
fn from(value: tungstenite::Message) -> Self {
match value {
tungstenite::Message::Binary(bytes) => RawGatewayMessage::Bytes(bytes),
tungstenite::Message::Text(text) => RawGatewayMessage::Text(text),
_ => RawGatewayMessage::Text(value.to_string()),
}
}
}

View File

@ -9,7 +9,7 @@ use futures_util::{
use ws_stream_wasm::*;
use crate::gateway::GatewayMessage;
use crate::gateway::{GatewayMessage, RawGatewayMessage};
#[derive(Debug, Clone)]
pub struct WasmBackend;
@ -46,3 +46,21 @@ impl From<WsMessage> for GatewayMessage {
}
}
}
impl From<RawGatewayMessage> for WsMessage {
fn from(message: RawGatewayMessage) -> Self {
match message {
RawGatewayMessage::Text(text) => WsMessage::Text(text),
RawGatewayMessage::Bytes(bytes) => WsMessage::Binary(bytes),
}
}
}
impl From<WsMessage> for RawGatewayMessage {
fn from(value: WsMessage) -> Self {
match value {
WsMessage::Binary(bytes) => RawGatewayMessage::Bytes(bytes),
WsMessage::Text(text) => RawGatewayMessage::Text(text),
}
}
}

View File

@ -4,6 +4,7 @@
use std::time::Duration;
use flate2::Decompress;
use futures_util::{SinkExt, StreamExt};
use log::*;
#[cfg(not(target_arch = "wasm32"))]
@ -19,6 +20,9 @@ use crate::types::{
WebSocketEvent,
};
/// Tells us we have received enough of the buffer to decompress it
const ZLIB_SUFFIX: [u8; 4] = [0, 0, 255, 255];
#[derive(Debug)]
pub struct Gateway {
events: Arc<Mutex<Events>>,
@ -28,21 +32,36 @@ pub struct Gateway {
kill_send: tokio::sync::broadcast::Sender<()>,
kill_receive: tokio::sync::broadcast::Receiver<()>,
store: Arc<Mutex<HashMap<Snowflake, Arc<RwLock<ObservableObject>>>>>,
/// Url which was used to initialize the gateway
url: String,
/// Options which were used to initialize the gateway
options: GatewayOptions,
zlib_inflate: Option<flate2::Decompress>,
zlib_buffer: Option<Vec<u8>>,
}
impl Gateway {
#[allow(clippy::new_ret_no_self)]
pub async fn spawn(websocket_url: String) -> Result<GatewayHandle, GatewayError> {
let (websocket_send, mut websocket_receive) =
match WebSocketBackend::connect(&websocket_url).await {
Ok(streams) => streams,
Err(e) => {
return Err(GatewayError::CannotConnect {
error: format!("{:?}", e),
})
}
};
/// Creates / opens a new gateway connection.
///
/// # Note
/// The websocket url should begin with the prefix wss:// or ws:// (for unsecure connections)
pub async fn spawn(
websocket_url: String,
options: GatewayOptions,
) -> Result<GatewayHandle, GatewayError> {
let url = options.add_to_url(websocket_url);
debug!("GW: Connecting to {}", url);
let (websocket_send, mut websocket_receive) = match WebSocketBackend::connect(&url).await {
Ok(streams) => streams,
Err(e) => {
return Err(GatewayError::CannotConnect {
error: format!("{:?}", e),
});
}
};
let shared_websocket_send = Arc::new(Mutex::new(websocket_send));
@ -52,10 +71,34 @@ impl Gateway {
// 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
#[cfg(not(target_arch = "wasm32"))]
let msg: GatewayMessage = websocket_receive.next().await.unwrap().unwrap().into();
let received: RawGatewayMessage = websocket_receive.next().await.unwrap().unwrap().into();
#[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();
let received: RawGatewayMessage = websocket_receive.next().await.unwrap().into();
let message: GatewayMessage;
let zlib_buffer;
let zlib_inflate;
match options.transport_compression {
GatewayTransportCompression::None => {
zlib_buffer = None;
zlib_inflate = None;
message = GatewayMessage::from_raw_json_message(received).unwrap();
}
GatewayTransportCompression::ZLibStream => {
zlib_buffer = Some(Vec::new());
let mut inflate = Decompress::new(true);
message =
GatewayMessage::from_zlib_stream_json_message(received, &mut inflate).unwrap();
zlib_inflate = Some(inflate);
}
}
let gateway_payload: types::GatewayReceivePayload =
serde_json::from_str(&message.0).unwrap();
if gateway_payload.op_code != GATEWAY_HELLO {
return Err(GatewayError::NonHelloOnInitiate {
@ -85,7 +128,10 @@ impl Gateway {
kill_send: kill_send.clone(),
kill_receive: kill_send.subscribe(),
store: store.clone(),
url: websocket_url.clone(),
url: url.clone(),
options,
zlib_inflate,
zlib_buffer,
};
// Now we can continuously check for messages in a different task, since we aren't going to receive another hello
@ -99,7 +145,7 @@ impl Gateway {
});
Ok(GatewayHandle {
url: websocket_url.clone(),
url: url.clone(),
events: shared_events,
websocket_send: shared_websocket_send.clone(),
kill_send: kill_send.clone(),
@ -108,7 +154,7 @@ impl Gateway {
}
/// The main gateway listener task;
pub async fn gateway_listen_task(&mut self) {
async fn gateway_listen_task(&mut self) {
loop {
let msg;
@ -125,12 +171,12 @@ impl Gateway {
// PRETTYFYME: Remove inline conditional compiling
#[cfg(not(target_arch = "wasm32"))]
if let Some(Ok(message)) = msg {
self.handle_message(message.into()).await;
self.handle_raw_message(message.into()).await;
continue;
}
#[cfg(target_arch = "wasm32")]
if let Some(message) = msg {
self.handle_message(message.into()).await;
self.handle_raw_message(message.into()).await;
continue;
}
@ -163,8 +209,42 @@ impl Gateway {
Ok(())
}
/// Takes a [RawGatewayMessage], converts it to [GatewayMessage] based
/// of connection options and calls handle_message
async fn handle_raw_message(&mut self, raw_message: RawGatewayMessage) {
let message;
match self.options.transport_compression {
GatewayTransportCompression::None => {
message = GatewayMessage::from_raw_json_message(raw_message).unwrap()
}
GatewayTransportCompression::ZLibStream => {
let message_bytes = raw_message.into_bytes();
let can_decompress = message_bytes.len() > 4
&& message_bytes[message_bytes.len() - 4..] == ZLIB_SUFFIX;
let zlib_buffer = self.zlib_buffer.as_mut().unwrap();
zlib_buffer.extend(message_bytes.clone());
if !can_decompress {
return;
}
let zlib_buffer = self.zlib_buffer.as_ref().unwrap();
let inflate = self.zlib_inflate.as_mut().unwrap();
message =
GatewayMessage::from_zlib_stream_json_bytes(zlib_buffer, inflate).unwrap();
self.zlib_buffer = Some(Vec::new());
}
};
self.handle_message(message).await;
}
/// 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) {
async fn handle_message(&mut self, msg: GatewayMessage) {
if msg.0.is_empty() {
return;
}
@ -201,7 +281,10 @@ impl Gateway {
let event = &mut self.events.lock().await.$($path).+;
let json = gateway_payload.event_data.unwrap().get();
match serde_json::from_str(json) {
Err(err) => warn!("Failed to parse gateway event {event_name} ({err})"),
Err(err) => {
warn!("Failed to parse gateway event {event_name} ({err})");
trace!("Event data: {json}");
},
Ok(message) => {
$(
let mut message: $message_type = message;
@ -237,15 +320,12 @@ impl Gateway {
},)*
"RESUMED" => (),
"SESSIONS_REPLACE" => {
let result: Result<Vec<types::Session>, serde_json::Error> =
serde_json::from_str(gateway_payload.event_data.unwrap().get());
let json = gateway_payload.event_data.unwrap().get();
let result: Result<Vec<types::Session>, serde_json::Error> = serde_json::from_str(json);
match result {
Err(err) => {
warn!(
"Failed to parse gateway event {} ({})",
event_name,
err
);
warn!("Failed to parse gateway event {event_name} ({err})");
trace!("Event data: {json}");
return;
}
Ok(sessions) => {
@ -257,6 +337,7 @@ impl Gateway {
},
_ => {
warn!("Received unrecognized gateway event ({event_name})! Please open an issue on the chorus github so we can implement it");
trace!("Event data: {}", gateway_payload.event_data.unwrap().get());
}
}
};

View File

@ -2,11 +2,41 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::string::FromUtf8Error;
use crate::types;
use super::*;
/// Represents a message received from the gateway. This will be either a [types::GatewayReceivePayload], containing events, or a [GatewayError].
/// Defines a raw gateway message, being either string json or bytes
///
/// This is used as an intermediary type between types from different websocket implementations
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum RawGatewayMessage {
Text(String),
Bytes(Vec<u8>),
}
impl RawGatewayMessage {
/// Attempt to consume the message into a String, will try to convert binary to utf8
pub fn into_text(self) -> Result<String, FromUtf8Error> {
match self {
RawGatewayMessage::Text(text) => Ok(text),
RawGatewayMessage::Bytes(bytes) => String::from_utf8(bytes),
}
}
/// Consume the message into bytes, will convert text to binary
pub fn into_bytes(self) -> Vec<u8> {
match self {
RawGatewayMessage::Text(text) => text.as_bytes().to_vec(),
RawGatewayMessage::Bytes(bytes) => bytes,
}
}
}
/// Represents a json message received from the gateway.
/// This will be either a [types::GatewayReceivePayload], containing events, or a [GatewayError].
/// This struct is used internally when handling messages.
#[derive(Clone, Debug)]
pub struct GatewayMessage(pub String);
@ -44,4 +74,48 @@ impl GatewayMessage {
pub fn payload(&self) -> Result<types::GatewayReceivePayload, serde_json::Error> {
serde_json::from_str(&self.0)
}
/// Create self from an uncompressed json [RawGatewayMessage]
pub(crate) fn from_raw_json_message(
message: RawGatewayMessage,
) -> Result<GatewayMessage, FromUtf8Error> {
let text = message.into_text()?;
Ok(GatewayMessage(text))
}
/// Attempt to create self by decompressing zlib-stream bytes
// Thanks to <https://github.com/ByteAlex/zlib-stream-rs>, their
// code helped a lot with the stream implementation
pub(crate) fn from_zlib_stream_json_bytes(
bytes: &[u8],
inflate: &mut flate2::Decompress,
) -> Result<GatewayMessage, std::io::Error> {
// Note: is there a better way to handle the size of this output buffer?
//
// This used to be 10, I measured it at 11.5, so a safe bet feels like 20
//
// ^ - This dude is naive. apparently not even 20x is okay. Measured at 47.9x!!!!
// If it is >100x ever, I will literally explode
//
// About an hour later, you ^ will literally explode.
// 133 vs 13994 -- 105.21805x ratio
// Let's hope it doesn't go above 200??
let mut output = Vec::with_capacity(bytes.len() * 200);
let _status = inflate.decompress_vec(bytes, &mut output, flate2::FlushDecompress::Sync)?;
output.shrink_to_fit();
let string = String::from_utf8(output).unwrap();
Ok(GatewayMessage(string))
}
/// Attempt to create self by decompressing a zlib-stream bytes raw message
pub(crate) fn from_zlib_stream_json_message(
message: RawGatewayMessage,
inflate: &mut flate2::Decompress,
) -> Result<GatewayMessage, std::io::Error> {
Self::from_zlib_stream_json_bytes(&message.into_bytes(), inflate)
}
}

View File

@ -10,12 +10,14 @@ pub mod gateway;
pub mod handle;
pub mod heartbeat;
pub mod message;
pub mod options;
pub use backends::*;
pub use gateway::*;
pub use handle::*;
use heartbeat::*;
pub use message::*;
pub use options::*;
use crate::errors::GatewayError;
use crate::types::{Snowflake, WebSocketEvent};

118
src/gateway/options.rs Normal file
View File

@ -0,0 +1,118 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Debug, Default)]
/// Options passed when initializing the gateway connection.
///
/// E.g. compression
///
/// # Note
///
/// Discord allows specifying the api version (v10, v9, ...) as well, but chorus is built upon one
/// main version (v9).
///
/// Similarly, discord also supports etf encoding, while chorus does not (yet).
/// We are looking into supporting it as an option, since it is faster and more lightweight.
///
/// See <https://docs.discord.sex/topics/gateway#connections>
pub struct GatewayOptions {
pub encoding: GatewayEncoding,
pub transport_compression: GatewayTransportCompression,
}
impl GatewayOptions {
/// Adds the options to an existing gateway url
///
/// Returns the new url
pub(crate) fn add_to_url(&self, url: String) -> String {
let mut url = url;
let mut parameters = Vec::with_capacity(2);
let encoding = self.encoding.to_url_parameter();
parameters.push(encoding);
let compression = self.transport_compression.to_url_parameter();
if let Some(some_compression) = compression {
parameters.push(some_compression);
}
let mut has_parameters = url.contains('?') && url.contains('=');
if !has_parameters {
// Insure it ends in a /, so we don't get a 400 error
if !url.ends_with('/') {
url.push('/');
}
// Lets hope that if it already has parameters the person knew to add '/'
}
for parameter in parameters {
if !has_parameters {
url = format!("{}?{}", url, parameter);
has_parameters = true;
}
else {
url = format!("{}&{}", url, parameter);
}
}
url
}
}
#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Debug, Default)]
/// Possible transport compression options for the gateway.
///
/// See <https://docs.discord.sex/topics/gateway#transport-compression>
pub enum GatewayTransportCompression {
/// Do not transport compress packets
None,
/// Transport compress using zlib stream
#[default]
ZLibStream,
}
impl GatewayTransportCompression {
/// Returns the option as a url parameter.
///
/// If set to [GatewayTransportCompression::None] returns [None].
///
/// If set to anything else, returns a string like "compress=zlib-stream"
pub(crate) fn to_url_parameter(self) -> Option<String> {
match self {
Self::None => None,
Self::ZLibStream => Some(String::from("compress=zlib-stream"))
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Debug, Default)]
/// See <https://docs.discord.sex/topics/gateway#encoding-and-compression>
pub enum GatewayEncoding {
/// Javascript object notation, a standard for websocket connections,
/// but contains a lot of overhead
#[default]
Json,
/// A binary format originating from Erlang
///
/// Should be lighter and faster than json.
///
/// !! Chorus does not implement ETF yet !!
ETF
}
impl GatewayEncoding {
/// Returns the option as a url parameter.
///
/// Returns a string like "encoding=json"
pub(crate) fn to_url_parameter(self) -> String {
match self {
Self::Json => String::from("encoding=json"),
Self::ETF => String::from("encoding=etf")
}
}
}

View File

@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
use chrono::Utc;
use crate::errors::ChorusResult;
use crate::gateway::{Gateway, GatewayHandle};
use crate::gateway::{Gateway, GatewayHandle, GatewayOptions};
use crate::ratelimiter::ChorusRequest;
use crate::types::types::subconfigs::limits::rates::RateLimits;
use crate::types::{
@ -33,6 +33,8 @@ pub struct Instance {
pub limits_information: Option<LimitsInformation>,
#[serde(skip)]
pub client: Client,
#[serde(skip)]
pub gateway_options: GatewayOptions,
}
impl PartialEq for Instance {
@ -106,6 +108,7 @@ impl Instance {
instance_info: GeneralConfiguration::default(),
limits_information: limit_information,
client: Client::new(),
gateway_options: GatewayOptions::default(),
};
instance.instance_info = match instance.general_configuration_schema().await {
Ok(schema) => schema,
@ -141,6 +144,13 @@ impl Instance {
Err(_) => Ok(None),
}
}
/// Sets the [`GatewayOptions`] the instance will use when spawning new connections.
///
/// These options are used on the gateways created when logging in and registering.
pub fn set_gateway_options(&mut self, options: GatewayOptions) {
self.gateway_options = options;
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -218,7 +228,9 @@ impl ChorusUser {
let settings = Arc::new(RwLock::new(UserSettings::default()));
let wss_url = instance.read().unwrap().urls.wss.clone();
// Dummy gateway object
let gateway = Gateway::spawn(wss_url).await.unwrap();
let gateway = Gateway::spawn(wss_url, GatewayOptions::default())
.await
.unwrap();
ChorusUser {
token,
mfa_token: None,
@ -244,10 +256,11 @@ impl ChorusUser {
///
/// The JWT token expires after 5 minutes.
pub async fn complete_mfa_challenge(&mut self, mfa_verify_schema: MfaVerifySchema) -> ChorusResult<()> {
let endpoint_url = "/mfa/finish";
let endpoint_url = self.belongs_to.read().unwrap().urls.api.clone() + "/mfa/finish";
let chorus_request = ChorusRequest {
request: Client::new()
.post(endpoint_url)
.header("Authorization", self.token())
.json(&mfa_verify_schema),
limit_type: match self.object.is_some() {
true => LimitType::Global,

View File

@ -87,7 +87,7 @@ impl ChorusRequest {
let client = user.belongs_to.read().unwrap().client.clone();
let result = match client.execute(self.request.build().unwrap()).await {
Ok(result) => {
debug!("Request successful: {:?}", result);
log::trace!("Request successful: {:?}", result);
result
}
Err(error) => {
@ -500,7 +500,7 @@ impl ChorusRequest {
user: &mut ChorusUser,
) -> ChorusResult<T> {
let response = self.send_request(user).await?;
debug!("Got response: {:?}", response);
log::trace!("Got response: {:?}", response);
let response_text = match response.text().await {
Ok(string) => string,
Err(e) => {

View File

@ -3,19 +3,10 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::fmt::{Display, Formatter};
#[cfg(feature = "sqlx")]
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::{
database::{HasArguments, HasValueRef},
encode::IsNull,
error::BoxDynError,
Decode, MySql,
};
use crate::types::config::types::subconfigs::guild::{
autojoin::AutoJoinConfiguration, discovery::DiscoverConfiguration,
@ -172,8 +163,8 @@ impl Display for GuildFeaturesList {
#[cfg(feature = "sqlx")]
impl<'r> sqlx::Decode<'r, sqlx::MySql> for GuildFeaturesList {
fn decode(value: <MySql as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
let v = <&str as Decode<sqlx::MySql>>::decode(value)?;
fn decode(value: <sqlx::MySql as sqlx::database::HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
let v = <String as sqlx::Decode<sqlx::MySql>>::decode(value)?;
Ok(Self(
v.split(',')
.filter(|f| !f.is_empty())
@ -185,9 +176,9 @@ impl<'r> sqlx::Decode<'r, sqlx::MySql> for GuildFeaturesList {
#[cfg(feature = "sqlx")]
impl<'q> sqlx::Encode<'q, sqlx::MySql> for GuildFeaturesList {
fn encode_by_ref(&self, buf: &mut <MySql as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
fn encode_by_ref(&self, buf: &mut <sqlx::MySql as sqlx::database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
if self.is_empty() {
return IsNull::Yes;
return sqlx::encode::IsNull::Yes;
}
let features = self
.iter()
@ -195,30 +186,18 @@ impl<'q> sqlx::Encode<'q, sqlx::MySql> for GuildFeaturesList {
.collect::<Vec<_>>()
.join(",");
let _ = buf.write(features.as_bytes());
IsNull::No
<String as sqlx::Encode<sqlx::MySql>>::encode_by_ref(&features, buf)
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<sqlx::MySql> for GuildFeaturesList {
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
<&str as sqlx::Type<sqlx::MySql>>::type_info()
<String as sqlx::Type<sqlx::MySql>>::type_info()
}
fn compatible(ty: &sqlx::mysql::MySqlTypeInfo) -> bool {
<&str as sqlx::Type<sqlx::MySql>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl sqlx::TypeInfo for GuildFeaturesList {
fn is_null(&self) -> bool {
false
}
fn name(&self) -> &str {
"TEXT"
<String as sqlx::Type<sqlx::MySql>>::compatible(ty)
}
}

View File

@ -3,21 +3,27 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::Shared;
use crate::types::{AutoModerationRuleTriggerType, IntegrationType, PermissionOverwriteType, Shared};
use crate::types::utils::Snowflake;
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
/// See <https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object>
pub struct AuditLogEntry {
pub target_id: Option<String>,
#[cfg(feature = "sqlx")]
pub changes: sqlx::types::Json<Option<Vec<Shared<AuditLogChange>>>>,
#[cfg(not(feature = "sqlx"))]
pub changes: Option<Vec<Shared<AuditLogChange>>>,
pub user_id: Option<Snowflake>,
pub id: Snowflake,
// to:do implement an enum for these types
pub action_type: u8,
// to:do add better options type
pub options: Option<serde_json::Value>,
pub action_type: AuditLogActionType,
#[cfg(feature = "sqlx")]
pub options: Option<sqlx::types::Json<AuditEntryInfo>>,
#[cfg(not(feature = "sqlx"))]
pub options: Option<AuditEntryInfo>,
pub reason: Option<String>,
}
@ -28,3 +34,164 @@ pub struct AuditLogChange {
pub old_value: Option<serde_json::Value>,
pub key: String,
}
#[derive(Default, Serialize_repr, Deserialize_repr, Debug, Clone, Copy)]
#[repr(u8)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
/// # Reference:
/// See <https://docs.discord.sex/resources/audit-log#audit-log-events>
pub enum AuditLogActionType {
#[default]
/// Guild settings were updated
GuildUpdate = 1,
/// Channel was created
ChannelCreate = 10,
/// Channel settings were updated
ChannelUpdate = 11,
/// Channel was deleted
ChannelDelete = 12,
/// Permission overwrite was added to a channel
ChannelOverwriteCreate = 13,
/// Permission overwrite was updated for a channel
ChannelOverwriteUpdate = 14,
/// Permission overwrite was deleted from a channel
ChannelOverwriteDelete = 15,
/// Member was removed from guild
MemberKick = 20,
/// Members were pruned from guild
MemberPrune = 21,
/// Member was banned from guild
MemberBanAdd = 22,
/// Member was unbanned from guild
MemberBanRemove = 23,
/// Member was updated in guild
MemberUpdate = 24,
/// Member was added or removed from a role
MemberRoleUpdate = 25,
/// Member was moved to a different voice channel
MemberMove = 26,
/// Member was disconnected from a voice channel
MemberDisconnect = 27,
/// Bot user was added to guild
BotAdd = 28,
/// Role was created
RoleCreate = 30,
/// Role was edited
RoleUpdate = 31,
/// Role was deleted
RoleDelete = 32,
/// Guild invite was created
InviteCreate = 40,
/// Guild invite was updated
InviteUpdate = 41,
/// Guild invite was deleted
InviteDelete = 42,
/// Webhook was created
WebhookCreate = 50,
/// Webhook properties or channel were updated
WebhookUpdate = 51,
/// Webhook was deleted
WebhookDelete = 52,
/// Emoji was created
EmojiCreate = 60,
/// Emoji name was updated
EmojiUpdate = 61,
/// Emoji was deleted
EmojiDelete = 62,
/// Single message was deleted
MessageDelete = 72,
/// Multiple messages were deleted
MessageBulkDelete = 73,
/// Message was pinned to a channel
MessagePin = 74,
/// Message was unpinned from a channel
MessageUnpin = 75,
/// Interaction was added to guild
IntegrationCreate = 80,
/// Integration was updated (e.g. its scopes were updated)
IntegrationUpdate = 81,
/// Integration was removed from guild
IntegrationDelete = 82,
/// Stage instance was created (stage channel becomes live)
StageInstanceCreate = 83,
/// Stage instance details were updated
StageInstanceUpdate = 84,
/// Stage instance was deleted (stage channel no longer live)
StageInstanceDelete = 85,
/// Sticker was created
StickerCreate = 90,
/// Sticker details were updated
StickerUpdate = 91,
/// Sticker was deleted
StickerDelete = 92,
/// Event was created
GuildScheduledEventCreate = 100,
/// Event was updated
GuildScheduledEventUpdate = 101,
/// Event was cancelled
GuildScheduledEventDelete = 102,
/// Thread was created in a channel
ThreadCreate = 110,
/// Thread was updated
ThreadUpdate = 111,
/// Thread was deleted
ThreadDelete = 112,
/// Permissions were updated for a command
ApplicationCommandPermissionUpdate = 121,
/// AutoMod rule created
AutoModerationRuleCreate = 140,
/// AutoMod rule was updated
AutoModerationRuleUpdate = 141,
/// AutoMod rule was deleted
AutoModerationRuleDelete = 142,
/// Message was blocked by AutoMod
AutoModerationBlockMessage = 143,
/// Message was flagged by AutoMod
AutoModerationFlagToChannel = 144,
/// Member was timed out by AutoMod
AutoModerationUserCommunicationDisabled = 145,
/// Member was quarantined by AutoMod
AutoModerationQuarantineUser = 146,
/// Creator monetization request was created
CreatorMonetizationRequestCreated = 150,
/// Creator monetization terms were accepted
CreatorMonetizationTermsAccepted = 151,
/// Onboarding prompt was created
OnboardingPromptCreate = 163,
/// Onboarding prompt was updated
OnboardingPromptUpdate = 164,
/// Onboarding prompt was deleted
OnboardingPromptDelete = 165,
/// Onboarding was created
OnboardingCreate = 166,
/// Onboarding was updated
OnboardingUpdate = 167,
/// Voice channel status was updated
VoiceChannelStatusUpdate = 192,
/// Voice channel status was deleted
VoiceChannelStatusDelete = 193
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct AuditEntryInfo {
pub application_id: Option<Snowflake>,
pub auto_moderation_rule_name: Option<String>,
pub auto_moderation_rule_trigger_type: Option<AutoModerationRuleTriggerType>,
pub channel_id: Option<Snowflake>,
// #[serde(option_string)]
pub count: Option<u64>,
// #[serde(option_string)]
pub delete_member_days: Option<u64>,
/// The ID of the overwritten entity
pub id: Option<Snowflake>,
pub integration_type: Option<IntegrationType>,
// #[serde(option_string)]
pub members_removed: Option<u64>,
// #[serde(option_string)]
pub message_id: Option<u64>,
pub role_name: Option<String>,
#[serde(rename = "type")]
pub overwrite_type: Option<PermissionOverwriteType>,
pub status: Option<String>
}

View File

@ -3,15 +3,16 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_string_from_number;
use serde::{Deserialize, Deserializer, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::fmt::Debug;
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
use crate::types::{
PermissionFlags, Shared,
entities::{GuildMember, User},
utils::Snowflake,
serde::string_or_u64
};
#[cfg(feature = "client")]
@ -25,6 +26,8 @@ use crate::gateway::Updateable;
#[cfg(feature = "client")]
use chorus_macros::{observe_option_vec, Composite, Updateable};
use serde::de::{Error, Visitor};
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
@ -156,14 +159,73 @@ pub struct PermissionOverwrite {
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, PartialEq, Eq, PartialOrd)]
#[derive(Debug, Serialize_repr, Clone, PartialEq, Eq, PartialOrd)]
#[repr(u8)]
/// # Reference
///
/// See <https://docs.discord.sex/resources/channel#permission-overwrite-type>
pub enum PermissionOverwriteType {
Role = 0,
Member = 1,
}
impl From<u8> for PermissionOverwriteType {
fn from(v: u8) -> Self {
match v {
0 => PermissionOverwriteType::Role,
1 => PermissionOverwriteType::Member,
_ => unreachable!(),
}
}
}
impl FromStr for PermissionOverwriteType {
type Err = serde::de::value::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"role" => Ok(PermissionOverwriteType::Role),
"member" => Ok(PermissionOverwriteType::Member),
_ => Err(Self::Err::custom("invalid permission overwrite type")),
}
}
}
struct PermissionOverwriteTypeVisitor;
impl<'de> Visitor<'de> for PermissionOverwriteTypeVisitor {
type Value = PermissionOverwriteType;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("a valid permission overwrite type")
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E> where E: Error {
Ok(PermissionOverwriteType::from(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> where E: Error {
self.visit_u8(v as u8)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: Error {
PermissionOverwriteType::from_str(v)
.map_err(E::custom)
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> where E: Error {
self.visit_str(v.as_str())
}
}
impl<'de> Deserialize<'de> for PermissionOverwriteType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
let val = deserializer.deserialize_any(PermissionOverwriteTypeVisitor)?;
Ok(val)
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/channel#thread-metadata-object>

View File

@ -23,7 +23,7 @@ use super::PublicUser;
use crate::gateway::Updateable;
#[cfg(feature = "client")]
use chorus_macros::{observe_option_vec, observe_vec, Composite, Updateable};
use chorus_macros::{observe_vec, Composite, Updateable};
#[cfg(feature = "client")]
use crate::types::Composite;
@ -46,10 +46,12 @@ pub struct Guild {
pub approximate_presence_count: Option<i32>,
pub banner: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub bans: Option<Vec<GuildBan>>,
#[serde(default)]
pub bans: Vec<GuildBan>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
#[cfg_attr(feature = "client", observe_option_vec)]
pub channels: Option<Vec<Shared<Channel>>>,
#[cfg_attr(feature = "client", observe_vec)]
#[serde(default)]
pub channels: Vec<Shared<Channel>>,
pub default_message_notifications: Option<MessageNotificationLevel>,
pub description: Option<String>,
pub discovery_splash: Option<String>,
@ -66,7 +68,8 @@ pub struct Guild {
pub icon_hash: Option<String>,
pub id: Snowflake,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub invites: Option<Vec<GuildInvite>>,
#[serde(default)]
pub invites: Vec<GuildInvite>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub joined_at: Option<DateTime<Utc>>,
pub large: Option<bool>,
@ -92,25 +95,29 @@ pub struct Guild {
pub public_updates_channel_id: Option<Snowflake>,
pub region: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
#[cfg_attr(feature = "client", observe_option_vec)]
pub roles: Option<Vec<Shared<RoleObject>>>,
#[cfg_attr(feature = "client", observe_vec)]
#[serde(default)]
pub roles: Vec<Shared<RoleObject>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub rules_channel: Option<String>,
pub rules_channel_id: Option<Snowflake>,
pub splash: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub stickers: Option<Vec<Sticker>>,
#[serde(default)]
pub stickers: Vec<Sticker>,
pub system_channel_flags: Option<SystemChannelFlags>,
pub system_channel_id: Option<Snowflake>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub vanity_url_code: Option<String>,
pub verification_level: Option<VerificationLevel>,
#[serde(default)]
#[cfg_attr(feature = "sqlx", sqlx(skip))]
#[cfg_attr(feature = "client", observe_option_vec)]
pub voice_states: Option<Vec<Shared<VoiceState>>>,
#[cfg_attr(feature = "client", observe_vec)]
pub voice_states: Vec<Shared<VoiceState>>,
#[serde(default)]
#[cfg_attr(feature = "sqlx", sqlx(skip))]
#[cfg_attr(feature = "client", observe_option_vec)]
pub webhooks: Option<Vec<Shared<Webhook>>>,
#[cfg_attr(feature = "client", observe_vec)]
pub webhooks: Vec<Shared<Webhook>>,
#[cfg(feature = "sqlx")]
pub welcome_screen: sqlx::types::Json<Option<WelcomeScreenObject>>,
#[cfg(not(feature = "sqlx"))]
@ -226,6 +233,7 @@ impl std::cmp::PartialEq for Guild {
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct GuildBan {
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub user: PublicUser,
pub reason: Option<String>,
}

View File

@ -5,7 +5,7 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::{GuildMemberFlags, Shared};
use crate::types::{GuildMemberFlags, PermissionFlags, Shared};
use crate::types::{entities::PublicUser, Snowflake};
#[derive(Debug, Deserialize, Default, Serialize, Clone)]
@ -27,6 +27,7 @@ pub struct GuildMember {
pub mute: bool,
pub flags: Option<GuildMemberFlags>,
pub pending: Option<bool>,
pub permissions: Option<String>,
#[serde(default)]
pub permissions: PermissionFlags,
pub communication_disabled_until: Option<DateTime<Utc>>,
}

View File

@ -18,7 +18,7 @@ pub struct Integration {
pub id: Snowflake,
pub name: String,
#[serde(rename = "type")]
pub integration_type: String,
pub integration_type: IntegrationType,
pub enabled: bool,
pub syncing: Option<bool>,
pub role_id: Option<String>,
@ -43,3 +43,15 @@ pub struct IntegrationAccount {
pub id: String,
pub name: String,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(rename_all = "snake_case"))]
pub enum IntegrationType {
#[default]
Twitch,
Youtube,
Discord,
GuildSubscription,
}

View File

@ -13,7 +13,7 @@ use super::{Application, Channel, GuildMember, NSFWLevel, User};
/// Represents a code that when used, adds a user to a guild or group DM channel, or creates a relationship between two users.
/// See <https://discord-userdoccers.vercel.app/resources/invite#invite-object>
#[derive(Debug, Serialize, Deserialize)]
#[derive(Default, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Invite {
#[cfg_attr(feature = "sqlx", sqlx(skip))]

View File

@ -177,7 +177,7 @@ pub struct ChannelMention {
pub struct Embed {
title: Option<String>,
#[serde(rename = "type")]
embed_type: Option<String>,
embed_type: Option<EmbedType>,
description: Option<String>,
url: Option<String>,
timestamp: Option<String>,
@ -191,6 +191,24 @@ pub struct Embed {
fields: Option<Vec<EmbedField>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum EmbedType {
#[deprecated]
ApplicationNews,
Article,
AutoModerationMessage,
AutoModerationNotification,
Gift,
#[serde(rename = "gifv")]
GifVideo,
Image,
Link,
PostPreview,
Rich,
Video
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct EmbedFooter {
text: String,
@ -291,7 +309,7 @@ pub enum MessageType {
Default = 0,
/// A message sent when a user is added to a group DM or thread
RecipientAdd = 1,
/// A message sent when a user is removed from a group DM or thread
/// A message sent when a user is removed from a group DM or thread
RecipientRemove = 2,
/// A message sent when a user creates a call in a private channel
Call = 3,
@ -335,7 +353,7 @@ pub enum MessageType {
ThreadStarterMessage = 21,
/// A message sent to remind users to invite friends to a guild
GuildInviteReminder = 22,
/// A message sent when a user uses a context menu command
/// A message sent when a user uses a context menu command
ContextMenuCommand = 23,
/// A message sent when auto moderation takes an action
AutoModerationAction = 24,

View File

@ -4,7 +4,7 @@
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use serde_aux::prelude::{deserialize_option_number_from_string, deserialize_string_from_number};
use serde_aux::prelude::deserialize_option_number_from_string;
use std::fmt::Debug;
use crate::types::utils::Snowflake;
@ -34,8 +34,7 @@ pub struct RoleObject {
pub unicode_emoji: Option<String>,
pub position: u16,
#[serde(default)]
#[serde(deserialize_with = "deserialize_string_from_number")]
pub permissions: String,
pub permissions: PermissionFlags,
pub managed: bool,
pub mentionable: bool,
#[cfg(feature = "sqlx")]

View File

@ -3,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::{entities::User, utils::Snowflake, Shared};
@ -18,15 +19,17 @@ pub struct Sticker {
pub pack_id: Option<Snowflake>,
pub name: String,
pub description: Option<String>,
pub tags: String,
pub tags: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub asset: Option<String>,
#[serde(rename = "type")]
pub sticker_type: u8,
pub format_type: u8,
pub sticker_type: StickerType,
pub format_type: StickerFormatType,
pub available: Option<bool>,
pub guild_id: Option<Snowflake>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub user: Option<Shared<User>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub sort_value: Option<u8>,
}
@ -108,6 +111,18 @@ impl PartialOrd for Sticker {
}
}
impl Sticker {
pub fn tags(&self) -> Vec<String> {
self.tags
.as_ref()
.map_or(vec![], |s|
s.split(',')
.map(|tag| tag.trim().to_string())
.collect()
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
/// A partial sticker object.
///
@ -118,5 +133,61 @@ impl PartialOrd for Sticker {
pub struct StickerItem {
pub id: Snowflake,
pub name: String,
pub format_type: u8,
pub format_type: StickerFormatType,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[serde(rename = "SCREAMING_SNAKE_CASE")]
/// # Reference
/// See <https://docs.discord.sex/resources/sticker#sticker-types>
pub enum StickerType {
/// An official sticker in a current or legacy purchasable pack
Standard = 1,
#[default]
/// A sticker uploaded to a guild for the guild's members
Guild = 2,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
/// # Reference
/// See <https://docs.discord.sex/resources/sticker#sticker-format-types>
pub enum StickerFormatType {
#[default]
/// A PNG image
PNG = 1,
/// An animated PNG image, using the APNG format - uses CDN
APNG = 2,
/// A lottie animation; requires the VERIFIED and/or PARTNERED guild feature - uses CDN
LOTTIE = 3,
/// An animated GIF image - does not use CDN
GIF = 4,
}
impl StickerFormatType {
pub fn is_animated(&self) -> bool {
matches!(self, StickerFormatType::APNG | StickerFormatType::LOTTIE | StickerFormatType::GIF)
}
pub const fn to_mime(&self) -> &'static str {
match self {
StickerFormatType::PNG => "image/png",
StickerFormatType::APNG => "image/apng",
StickerFormatType::LOTTIE => "application/json",
StickerFormatType::GIF => "image/gif",
}
}
pub fn from_mime(mime: &str) -> Option<Self> {
match mime {
"image/png" => Some(StickerFormatType::PNG),
"image/apng" => Some(StickerFormatType::APNG),
"application/json" => Some(StickerFormatType::LOTTIE),
"image/gif" => Some(StickerFormatType::GIF),
_ => None,
}
}
}

View File

@ -4,11 +4,9 @@
use crate::types::utils::Snowflake;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_option_number_from_string;
use std::fmt::Debug;
use std::num::ParseIntError;
use std::str::FromStr;
#[cfg(feature = "client")]
use crate::gateway::Updateable;
@ -47,7 +45,7 @@ pub struct User {
pub bot: Option<bool>,
pub system: Option<bool>,
pub mfa_enabled: Option<bool>,
pub accent_color: Option<u8>,
pub accent_color: Option<u32>,
#[cfg_attr(feature = "sqlx", sqlx(default))]
pub locale: Option<String>,
pub verified: Option<bool>,
@ -60,10 +58,10 @@ pub struct User {
pub premium_since: Option<DateTime<Utc>>,
pub premium_type: Option<u8>,
pub pronouns: Option<String>,
pub public_flags: Option<u32>,
pub public_flags: Option<UserFlags>,
pub banner: Option<String>,
pub bio: Option<String>,
pub theme_colors: Option<Vec<u8>>,
pub theme_colors: Option<Vec<u32>>,
pub phone: Option<String>,
pub nsfw_allowed: Option<bool>,
pub premium: Option<bool>,
@ -78,15 +76,15 @@ pub struct PublicUser {
pub username: Option<String>,
pub discriminator: Option<String>,
pub avatar: Option<String>,
pub accent_color: Option<u8>,
pub accent_color: Option<u32>,
pub banner: Option<String>,
pub theme_colors: Option<Vec<u8>>,
pub theme_colors: Option<Vec<u32>>,
pub pronouns: Option<String>,
pub bot: Option<bool>,
pub bio: Option<String>,
pub premium_type: Option<u8>,
pub premium_since: Option<DateTime<Utc>>,
pub public_flags: Option<u32>,
pub public_flags: Option<UserFlags>,
}
impl From<User> for PublicUser {

View File

@ -6,6 +6,7 @@ use chrono::{serde::ts_milliseconds_option, Utc};
use serde::{Deserialize, Serialize};
use crate::types::Shared;
use serde_aux::field_attributes::deserialize_option_number_from_string;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
@ -37,7 +38,7 @@ pub enum UserTheme {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct UserSettings {
pub afk_timeout: u16,
pub afk_timeout: Option<u16>,
pub allow_accessibility_detection: bool,
pub animate_emoji: bool,
pub animate_stickers: u8,
@ -90,7 +91,7 @@ pub struct UserSettings {
impl Default for UserSettings {
fn default() -> Self {
Self {
afk_timeout: 3600,
afk_timeout: Some(3600),
allow_accessibility_detection: true,
animate_emoji: true,
animate_stickers: 0,
@ -148,10 +149,17 @@ impl Default for FriendSourceFlags {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuildFolder {
pub color: u32,
pub color: Option<u32>,
pub guild_ids: Vec<String>,
pub id: u16,
pub name: String,
// FIXME: What is this thing?
// It's not a snowflake, and it's sometimes a string and sometimes an integer.
//
// Ex: 1249181105
//
// It can also be negative somehow? Ex: -1176643795
#[serde(deserialize_with = "deserialize_option_number_from_string")]
pub id: Option<i64>,
pub name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@ -34,9 +34,11 @@ use crate::types::{
#[cfg_attr(feature = "client", derive(Composite))]
pub struct VoiceState {
pub guild_id: Option<Snowflake>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub guild: Option<Guild>,
pub channel_id: Option<Snowflake>,
pub user_id: Snowflake,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub member: Option<Shared<GuildMember>>,
/// Includes alphanumeric characters, not a snowflake
pub session_id: String,

View File

@ -49,11 +49,7 @@ impl UpdateMessage<Guild> for ChannelCreate {
fn update(&mut self, object_to_update: Shared<Guild>) {
let mut write = object_to_update.write().unwrap();
let update = self.channel.clone().into_shared();
if write.channels.is_some() {
write.channels.as_mut().unwrap().push(update);
} else {
write.channels = Some(Vec::from([update]));
}
write.channels.push(update);
}
}
@ -122,12 +118,12 @@ impl UpdateMessage<Guild> for ChannelDelete {
return;
}
let mut write = object_to_update.write().unwrap();
if write.channels.is_none() {
if write.channels.is_empty() {
return;
}
for (iteration, item) in (0_u32..).zip(write.channels.as_mut().unwrap().iter()) {
for (iteration, item) in (0_u32..).zip(write.channels.iter()) {
if item.read().unwrap().id == self.id().unwrap() {
write.channels.as_mut().unwrap().remove(iteration as usize);
write.channels.remove(iteration as usize);
return;
}
}

View File

@ -214,15 +214,9 @@ impl UpdateMessage<Guild> for GuildRoleCreate {
fn update(&mut self, object_to_update: Shared<Guild>) {
let mut object_to_update = object_to_update.write().unwrap();
if object_to_update.roles.is_some() {
object_to_update
.roles
.as_mut()
.unwrap()
.push(self.role.clone().into_shared());
} else {
object_to_update.roles = Some(Vec::from([self.role.clone().into_shared()]));
}
object_to_update
.roles
.push(self.role.clone().into_shared());
}
}

View File

@ -121,8 +121,8 @@ pub struct MessageReactionRemoveEmoji {
///
/// {"t":"MESSAGE_ACK","s":3,"op":0,"d":{"version":52,"message_id":"1107236673638633472","last_viewed":null,"flags":null,"channel_id":"967363950217936897"}}
pub struct MessageACK {
/// ?
pub version: u16,
// No ideas. See 206933
pub version: u32,
pub message_id: Snowflake,
/// This is an integer???
/// Not even unix, see '3070'???

View File

@ -12,9 +12,12 @@ use crate::types::{GuildMember, Snowflake, VoiceState};
///
/// Seems to be passively set to update the client on guild details (though, why not just send the update events?)
pub struct PassiveUpdateV1 {
#[serde(default)]
pub voice_states: Vec<VoiceState>,
pub members: Option<Vec<GuildMember>>,
#[serde(default)]
pub members: Vec<GuildMember>,
pub guild_id: Snowflake,
#[serde(default)]
pub channels: Vec<ChannelUnreadUpdateObject>,
}

View File

@ -20,12 +20,15 @@ pub struct UpdatePresence {
#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, WebSocketEvent)]
/// Received to tell the client that a user updated their presence / status
///
/// See <https://discord.com/developers/docs/topics/gateway-events#presence-update-presence-update-event-fields>
/// (Same structure as <https://docs.discord.sex/resources/presence#presence-object>)
pub struct PresenceUpdate {
pub user: PublicUser,
#[serde(default)]
pub guild_id: Option<Snowflake>,
pub status: UserStatus,
#[serde(default)]
pub activities: Vec<Activity>,
pub client_status: ClientStatusObject,
}

View File

@ -6,13 +6,14 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::{Guild, User};
use crate::types::events::{Session, WebSocketEvent};
use crate::types::interfaces::ClientStatusObject;
use crate::types::{Activity, GuildMember, PresenceUpdate, VoiceState};
use crate::types::{Activity, Channel, ClientStatusObject, GuildMember, PresenceUpdate, Snowflake, VoiceState};
#[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)]
/// 1/2 half documented;
/// 1/2 officially documented;
/// Received after identifying, provides initial user info;
/// See <https://discord.com/developers/docs/topics/gateway-events#ready;>
///
/// See <https://docs.discord.sex/topics/gateway-events#ready> and <https://discord.com/developers/docs/topics/gateway-events#ready>
// TODO: There are a LOT of fields missing here
pub struct GatewayReady {
pub analytics_token: Option<String>,
pub auth_session_id_hash: Option<String>,
@ -32,36 +33,47 @@ pub struct GatewayReady {
#[derive(Debug, Deserialize, Serialize, Default, Clone, WebSocketEvent)]
/// Officially Undocumented;
/// Sent after the READY event when a client is a user, seems to somehow add onto the ready event;
/// Sent after the READY event when a client is a user,
/// seems to somehow add onto the ready event;
///
/// See <https://docs.discord.sex/topics/gateway-events#ready-supplemental>
pub struct GatewayReadySupplemental {
/// The presences of the user's relationships and guild presences sent at startup
pub merged_presences: MergedPresences,
pub merged_members: Vec<Vec<GuildMember>>,
// ?
pub lazy_private_channels: Vec<serde_json::Value>,
pub lazy_private_channels: Vec<Channel>,
pub guilds: Vec<SupplementalGuild>,
// ? pomelo
// "Upcoming changes that the client should disclose to the user" (discord.sex)
pub disclose: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
/// See <https://docs.discord.sex/topics/gateway-events#merged-presences-structure>
pub struct MergedPresences {
/// "Presences of the user's guilds in the same order as the guilds array in ready"
/// (discord.sex)
pub guilds: Vec<Vec<MergedPresenceGuild>>,
/// "Presences of the user's friends and implicit relationships" (discord.sex)
pub friends: Vec<MergedPresenceFriend>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
/// Not documented even unofficially
pub struct MergedPresenceFriend {
pub user_id: String,
pub user_id: Snowflake,
pub status: String,
/// Looks like ms??
pub last_modified: u128,
// Looks like ms??
//
// Not always sent
pub last_modified: Option<u128>,
pub client_status: ClientStatusObject,
pub activities: Vec<Activity>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
/// Not documented even unofficially
pub struct MergedPresenceGuild {
pub user_id: String,
pub user_id: Snowflake,
pub status: String,
// ?
pub game: Option<serde_json::Value>,
@ -70,8 +82,10 @@ pub struct MergedPresenceGuild {
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
/// See <https://docs.discord.sex/topics/gateway-events#supplemental-guild-structure>
pub struct SupplementalGuild {
pub id: Snowflake,
pub voice_states: Option<Vec<VoiceState>>,
pub id: String,
/// Field not documented even unofficially
pub embedded_activities: Vec<serde_json::Value>,
}

View File

@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
use crate::types::{ApplicationCommand, AuditLogActionType, AuditLogEntry, AutoModerationRule, Channel, GuildScheduledEvent, Integration, Snowflake, User, Webhook};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct AuditLogObject {
pub audit_log_entries: Vec<AuditLogEntry>,
pub application_commands: Vec<ApplicationCommand>,
pub auto_moderation_rules: Vec<AutoModerationRule>,
pub guild_scheduled_events: Vec<GuildScheduledEvent>,
pub integrations: Vec<Integration>,
pub threads: Vec<Channel>,
pub users: Vec<User>,
pub webhooks: Vec<Webhook>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct GetAuditLogsQuery {
pub before: Option<Snowflake>,
pub after: Option<Snowflake>,
pub limit: Option<u8>,
pub user_id: Option<Snowflake>,
pub action_type: Option<AuditLogActionType>
}

View File

@ -3,10 +3,9 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use bitflags::bitflags;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::de::Visitor;
use serde::{Deserialize, Serialize};
use crate::types::{ChannelType, DefaultReaction, Error, entities::PermissionOverwrite, Snowflake};
use crate::types::{ChannelType, DefaultReaction, entities::PermissionOverwrite, Snowflake};
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, PartialOrd)]
#[serde(rename_all = "snake_case")]

View File

@ -2,16 +2,14 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use std::collections::HashMap;
use bitflags::bitflags;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::entities::Channel;
use crate::types::types::guild_configuration::GuildFeatures;
use crate::types::{
Emoji, ExplicitContentFilterLevel, MessageNotificationLevel, Snowflake, Sticker,
SystemChannelFlags, VerificationLevel,
};
use crate::types::{Emoji, ExplicitContentFilterLevel, GenericSearchQueryWithLimit, MessageNotificationLevel, Snowflake, Sticker, StickerFormatType, SystemChannelFlags, VerificationLevel, WelcomeScreenChannel};
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
@ -32,10 +30,20 @@ pub struct GuildCreateSchema {
/// Represents the schema which needs to be sent to create a Guild Ban.
/// See: <https://discord-userdoccers.vercel.app/resources/guild#create-guild-ban>
pub struct GuildBanCreateSchema {
/// Deprecated
pub delete_message_days: Option<u8>,
pub delete_message_seconds: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
/// Represents the schema which needs to be sent to create a Guild Ban.
/// See: <https://discord-userdoccers.vercel.app/resources/guild#create-guild-ban>
pub struct GuildBanBulkCreateSchema {
pub user_ids: Vec<Snowflake>,
pub delete_message_seconds: Option<u32>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
/// Represents the schema used to modify a guild.
@ -77,6 +85,31 @@ pub struct GetUserGuildSchema {
pub with_counts: Option<bool>,
}
impl GetUserGuildSchema {
/// Converts self to query string parameters
pub fn to_query(self) -> Vec<(&'static str, String)> {
let mut query = Vec::with_capacity(4);
if let Some(before) = self.before {
query.push(("before", before.to_string()));
}
if let Some(after) = self.after {
query.push(("after", after.to_string()));
}
if let Some(limit) = self.limit {
query.push(("limit", limit.to_string()));
}
if let Some(with_counts) = self.with_counts {
query.push(("with_counts", with_counts.to_string()));
}
query
}
}
impl std::default::Default for GetUserGuildSchema {
fn default() -> Self {
Self {
@ -119,6 +152,12 @@ impl Default for GuildMemberSearchSchema {
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct GuildGetMembersQuery {
pub limit: Option<u16>,
pub after: Option<Snowflake>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct ModifyGuildMemberSchema {
pub nick: Option<String>,
@ -131,7 +170,7 @@ pub struct ModifyGuildMemberSchema {
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// Represents the flags of a Guild Member.
///
@ -174,3 +213,200 @@ pub struct GuildBansQuery {
pub after: Option<Snowflake>,
pub limit: Option<u16>,
}
/// Max query length is 32 characters.
/// The limit argument is a number between 1 and 10, defaults to 10.
pub type GuildBansSearchQuery = GenericSearchQueryWithLimit;
/// Query is partial or full, username or nickname.
/// Limit argument is a number between 1 and 1000, defaults to 1.
pub type GuildMembersSearchQuery = GenericSearchQueryWithLimit;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// A guild's progress on meeting the requirements of joining discovery.
///
/// Certain guilds, such as those that are verified, are exempt from discovery requirements. These guilds will not have a fully populated discovery requirements object, and are guaranteed to receive only sufficient and sufficient_without_grace_period.
///
/// # Reference:
/// See <https://docs.discord.sex/resources/discovery#discovery-requirements-object>
pub struct GuildDiscoveryRequirements {
pub guild_id: Option<Snowflake>,
pub safe_environment: Option<bool>,
pub healthy: Option<bool>,
pub health_score_pending: Option<bool>,
pub size: Option<bool>,
pub nsfw_properties: Option<GuildDiscoveryNsfwProperties>,
pub protected: Option<bool>,
pub sufficient: Option<bool>,
pub sufficient_without_grace_period: Option<bool>,
pub valid_rules_channel: Option<bool>,
pub retention_healthy: Option<bool>,
pub engagement_healthy: Option<bool>,
pub age: Option<bool>,
pub minimum_age: Option<u16>,
pub health_score: Option<GuildDiscoveryHealthScore>,
pub minimum_size: Option<u64>,
pub grace_period_end_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/discovery#discovery-nsfw-properties-structure>
pub struct GuildDiscoveryNsfwProperties {
pub channels: Vec<Snowflake>,
pub channel_banned_keywords: HashMap<Snowflake, Vec<String>>,
pub name: Option<String>,
pub name_banned_keywords: Vec<String>,
pub description: Option<String>,
pub description_banned_keywords: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// Activity metrics are recalculated weekly, as an 8-week rolling average. If they are not yet eligible to be calculated, all fields will be null.
///
/// # Reference:
/// See <https://docs.discord.sex/resources/discovery#discovery-health-score-structure>
pub struct GuildDiscoveryHealthScore {
pub avg_nonnew_communicators: u64,
pub avg_nonnew_participators: u64,
pub num_intentful_joiners: u64,
pub perc_ret_w1_intentful: f64,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/emoji#create-guild-emoji>
pub struct EmojiCreateSchema {
pub name: Option<String>,
/// # Reference:
/// See <https://docs.discord.sex/reference#cdn-data>
pub image: String,
#[serde(default)]
pub roles: Vec<Snowflake>
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/emoji#modify-guild-emoji>
pub struct EmojiModifySchema {
pub name: Option<String>,
pub roles: Option<Vec<Snowflake>>
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild#get-guild-prune>
pub struct GuildPruneQuerySchema {
pub days: u8,
/// Only used on POST
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compute_prune_count: Option<bool>,
#[serde(default)]
pub include_roles: Vec<Snowflake>
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild#get-guild-prune>
pub struct GuildPruneResult {
/// Null if compute_prune_count is false
pub pruned: Option<usize>,
}
#[derive(Default, Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/sticker#create-guild-sticker>
pub struct GuildCreateStickerSchema {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Option<String>,
pub file_data: Vec<u8>,
#[serde(skip)]
pub sticker_format_type: StickerFormatType
}
impl GuildCreateStickerSchema {
#[cfg(feature = "poem")]
pub async fn from_multipart(mut multipart: poem::web::Multipart) -> Result<Self, poem::Error> {
let mut _self = GuildCreateStickerSchema::default();
while let Some(field) = multipart.next_field().await? {
let name = field.name().ok_or(poem::Error::from_string("All fields must be named", poem::http::StatusCode::BAD_REQUEST))?;
match name {
"name" => {
_self.name = field.text().await?;
}
"description" => {
_self.description = Some(field.text().await?);
}
"tags" => {
_self.tags = Some(field.text().await?);
}
"file_data" => {
if _self.name.is_empty() {
_self.name = field.file_name().map(String::from).ok_or(poem::Error::from_string("File name must be set", poem::http::StatusCode::BAD_REQUEST))?;
}
_self.sticker_format_type = StickerFormatType::from_mime(field.content_type().ok_or(poem::Error::from_string("Content type must be set", poem::http::StatusCode::BAD_REQUEST))?).ok_or(poem::Error::from_string("Unknown sticker format", poem::http::StatusCode::BAD_REQUEST))?;
_self.file_data = field.bytes().await?;
}
_ => {}
}
}
if _self.name.is_empty() || _self.file_data.is_empty() {
return Err(poem::Error::from_string("At least the name and file_data are required", poem::http::StatusCode::BAD_REQUEST));
}
Ok(_self)
}
// #[cfg(feature = "client")]
pub fn to_multipart(&self) -> reqwest::multipart::Form {
let mut form = reqwest::multipart::Form::new()
.text("name", self.name.clone())
.part("file_data", reqwest::multipart::Part::bytes(self.file_data.clone()).mime_str(self.sticker_format_type.to_mime()).unwrap());
if let Some(description) = &self.description {
form = form.text("description", description.to_owned());
}
if let Some(tags) = &self.tags {
form = form.text("tags", tags.to_owned())
}
form
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/sticker#modify-guild-sticker>
pub struct GuildModifyStickerSchema {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub tags: Option<String>
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild#modify-guild-welcome-screen>
pub struct GuildModifyWelcomeScreenSchema {
pub enabled: Option<bool>,
pub description: Option<String>,
/// Max of 5
pub welcome_channels: Option<Vec<WelcomeScreenChannel>>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild-template#create-guild-template>
pub struct GuildTemplateCreateSchema {
/// Name of the template (1-100 characters)
pub name: String,
/// Description of the template (max 120 characters)
pub description: Option<String>
}

View File

@ -8,3 +8,19 @@ use serde::{Deserialize, Serialize};
pub struct GetInvitesSchema {
pub with_counts: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild#get-guild-vanity-invite>
pub struct GuildVanityInviteResponse {
pub code: String,
#[serde(default)]
pub uses: Option<u32>
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
/// # Reference:
/// See <https://docs.discord.sex/resources/guild#modify-guild-vanity-invite>
pub struct GuildCreateVanitySchema {
pub code: String,
}

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::{
AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment,
};
use crate::types::{Attachment, MessageFlags, MessageType, ReactionType, Snowflake};
use crate::types::{Attachment, EmbedType, Message, MessageFlags, MessageType, ReactionType, Snowflake};
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
@ -54,13 +54,13 @@ pub struct MessageSearchQuery {
pub attachment_extension: Option<Vec<String>>,
pub attachment_filename: Option<Vec<String>>,
pub author_id: Option<Vec<Snowflake>>,
pub author_type: Option<Vec<String>>,
pub author_type: Option<Vec<AuthorType>>,
pub channel_id: Option<Vec<Snowflake>>,
pub command_id: Option<Vec<Snowflake>>,
pub content: Option<String>,
pub embed_provider: Option<Vec<String>>,
pub embed_type: Option<Vec<String>>,
pub has: Option<Vec<String>>,
pub embed_type: Option<Vec<EmbedType>>,
pub has: Option<Vec<HasType>>,
pub include_nsfw: Option<bool>,
pub limit: Option<i32>,
pub link_hostname: Option<Vec<String>>,
@ -70,8 +70,8 @@ pub struct MessageSearchQuery {
pub min_id: Option<String>,
pub offset: Option<i32>,
pub pinned: Option<bool>,
pub sort_by: Option<String>,
pub sort_order: Option<String>,
pub sort_by: Option<SortType>,
pub sort_order: Option<SortOrder>,
}
impl std::default::Default for MessageSearchQuery {
@ -102,6 +102,75 @@ impl std::default::Default for MessageSearchQuery {
}
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum AuthorType {
User,
#[serde(rename = "-user")]
NotUser,
Bot,
#[serde(rename = "-bot")]
NotBot,
Webhook,
#[serde(rename = "-webhook")]
NotWebhook,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum HasType {
Image,
#[serde(rename = "-image")]
NotImage,
Sound,
#[serde(rename = "-sound")]
NotSound,
Video,
#[serde(rename = "-video")]
NotVideo,
File,
#[serde(rename = "-file")]
NotFile,
Sticker,
#[serde(rename = "-sticker")]
NotSticker,
Embed,
#[serde(rename = "-embed")]
NotEmbed,
Link,
#[serde(rename = "-link")]
NotLink,
Poll,
#[serde(rename = "-poll")]
NotPoll,
Snapshot,
#[serde(rename = "-snapshot")]
NotSnapshot,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum SortType {
#[default]
Timestamp,
Relevance
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum SortOrder {
#[default]
#[serde(rename = "desc")]
Descending,
#[serde(rename = "asc")]
Ascending,
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
pub struct MessageSearchResponse {
pub messages: Vec<Message>,
pub total_results: u64,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct CreateGreetMessage {
pub sticker_ids: Vec<Snowflake>,

View File

@ -3,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
pub use apierror::*;
pub use audit_log::*;
pub use auth::*;
pub use mfa::*;
pub use channel::*;
@ -12,8 +13,10 @@ pub use relationship::*;
pub use role::*;
pub use user::*;
pub use invites::*;
pub use voice_state::*;
mod apierror;
mod audit_log;
mod auth;
mod mfa;
mod channel;
@ -23,3 +26,10 @@ mod relationship;
mod role;
mod user;
mod invites;
mod voice_state;
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct GenericSearchQueryWithLimit {
pub query: String,
pub limit: Option<u16>,
}

View File

@ -3,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use serde::{Deserialize, Serialize};
use crate::types::{PermissionFlags, Snowflake};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "snake_case")]
@ -10,8 +11,8 @@ use serde::{Deserialize, Serialize};
/// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolemodifyschema)
pub struct RoleCreateModifySchema {
pub name: Option<String>,
pub permissions: Option<String>,
pub color: Option<u32>,
pub permissions: Option<PermissionFlags>,
pub color: Option<f64>,
pub hoist: Option<bool>,
pub icon: Option<Vec<u8>>,
pub unicode_emoji: Option<String>,
@ -24,6 +25,6 @@ pub struct RoleCreateModifySchema {
/// Represents the schema which needs to be sent to update a roles' position.
/// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema)
pub struct RolePositionUpdateSchema {
pub id: String,
pub id: Snowflake,
pub position: u16,
}

View File

@ -4,24 +4,91 @@
use std::collections::HashMap;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::types::Snowflake;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
/// A schema used to modify a user.
///
/// See <https://docs.discord.sex/resources/user#json-params>
pub struct UserModifySchema {
/// The user's new username (2-32 characters)
///
/// Requires that `current_password` is set.
pub username: Option<String>,
// TODO: Maybe add a special discriminator type?
/// Requires that `current_password` is set.
pub discriminator: Option<String>,
/// The user's display name (1-32 characters)
///
/// # Note
///
/// This is not yet implemented on Spacebar
pub global_name: Option<String>,
// TODO: Add a CDN data type
pub avatar: Option<String>,
pub bio: Option<String>,
pub accent_color: Option<u64>,
pub banner: Option<String>,
pub current_password: Option<String>,
pub new_password: Option<String>,
pub code: Option<String>,
/// Note: This is not yet implemented on Spacebar
pub avatar_decoration_id: Option<Snowflake>,
/// Note: This is not yet implemented on Spacebar
pub avatar_decoration_sku_id: Option<Snowflake>,
/// The user's email address; if changing from a verified email, email_token must be provided
///
/// Requires that `current_password` is set.
// TODO: Is ^ up to date? One would think this may not be the case, since email_token exists
pub email: Option<String>,
pub discriminator: Option<i16>,
/// The user's email token from their previous email, required if a new email is set.
///
/// See <https://docs.discord.sex/resources/user#modify-user-email> and <https://docs.discord.sex/resources/user#verify-user-email-change>
/// for changing the user's email.
///
/// # Note
///
/// This is not yet implemented on Spacebar
pub email_token: Option<String>,
/// The user's pronouns (max 40 characters)
///
/// # Note
///
/// This is not yet implemented on Spacebar
pub pronouns: Option<String>,
/// The user's banner.
///
/// Can only be changed for premium users
pub banner: Option<String>,
/// The user's bio (max 190 characters)
pub bio: Option<String>,
/// The user's accent color, as a hex integer
pub accent_color: Option<u64>,
/// The user's [UserFlags].
///
/// Only [UserFlags::PREMIUM_PROMO_DISMISSED], [UserFlags::HAS_UNREAD_URGENT_MESSAGES]
/// and DISABLE_PREMIUM can be set.
///
/// # Note
///
/// This is not yet implemented on Spacebar
pub flags: Option<u64>,
/// The user's date of birth, can only be set once
///
/// Requires that `current_password` is set.
pub date_of_birth: Option<NaiveDate>,
/// The user's current password (if the account does not have a password, this sets it)
///
/// Required for updating `username`, `discriminator`, `email`, `date_of_birth` and
/// `new_password`
#[serde(rename = "password")]
pub current_password: Option<String>,
/// The user's new password (8-72 characters)
///
/// Requires that `current_password` is set.
///
/// Regenerates the user's token
pub new_password: Option<String>,
/// Spacebar only field, potentially same as `email_token`
pub code: Option<String>,
}
/// A schema used to create a private channel.
@ -33,7 +100,7 @@ pub struct UserModifySchema {
///
/// # Reference:
/// Read: <https://discord-userdoccers.vercel.app/resources/channel#json-params>
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct PrivateChannelCreateSchema {
pub recipients: Option<Vec<Snowflake>>,
pub access_tokens: Option<Vec<String>>,

View File

@ -0,0 +1,15 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::Snowflake;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)]
/// # Reference:
/// See <https://docs.discord.sex/resources/voice#json-params>
pub struct VoiceStateUpdateSchema {
/// The ID of the channel the user is currently in
pub channel_id: Option<Snowflake>,
/// Whether to suppress the user
pub suppress: Option<bool>,
/// The time at which the user requested to speak
pub request_to_speak_timestamp: Option<DateTime<Utc>>,
}

View File

@ -23,3 +23,4 @@ impl From<WsMessage> for VoiceGatewayMessage {
}
}
}

View File

@ -44,7 +44,7 @@ impl VoiceGateway {
pub async fn spawn(websocket_url: String) -> Result<VoiceGatewayHandle, VoiceGatewayError> {
// Append the needed things to the websocket url
let processed_url = format!("wss://{}/?v=7", websocket_url);
trace!("Created voice socket url: {}", processed_url.clone());
trace!("VGW: Connecting to {}", processed_url.clone());
let (websocket_send, mut websocket_receive) =
match WebSocketBackend::connect(&processed_url).await {

View File

@ -85,8 +85,10 @@ async fn test_login_with_token() {
.await
.unwrap();
assert_eq!(
bundle.user.object.read().unwrap().id,
other_user.object.read().unwrap().id
bundle.user.object.as_ref().unwrap()
.read().unwrap()
.id,
other_user.object.unwrap().read().unwrap().id
);
assert_eq!(bundle.user.token, other_user.token);

View File

@ -2,7 +2,11 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use chorus::types::{self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, PermissionOverwrite, PermissionOverwriteType, PrivateChannelCreateSchema, RelationshipType, Snowflake};
use chorus::types::{
self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags,
PermissionOverwrite, PermissionOverwriteType, PrivateChannelCreateSchema, RelationshipType,
Snowflake,
};
mod common;
@ -67,7 +71,7 @@ async fn modify_channel() {
assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string()));
let permission_override = PermissionFlags::MANAGE_CHANNELS | PermissionFlags::MANAGE_MESSAGES;
let user_id: types::Snowflake = bundle.user.object.read().unwrap().id;
let user_id: types::Snowflake = bundle.user.object.as_ref().unwrap().read().unwrap().id;
let permission_override = PermissionOverwrite {
id: user_id,
overwrite_type: PermissionOverwriteType::Member,
@ -155,7 +159,13 @@ async fn create_dm() {
let other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let private_channel_create_schema = PrivateChannelCreateSchema {
recipients: Some(Vec::from([other_user.object.read().unwrap().id])),
recipients: Some(Vec::from([other_user
.object
.as_ref()
.unwrap()
.read()
.unwrap()
.id])),
access_tokens: None,
nicks: None,
};
@ -175,7 +185,7 @@ async fn create_dm() {
.unwrap()
.id
.clone(),
other_user.object.read().unwrap().id
other_user.object.unwrap().read().unwrap().id
);
assert_eq!(
dm_channel
@ -188,7 +198,7 @@ async fn create_dm() {
.unwrap()
.id
.clone(),
user.object.read().unwrap().id.clone()
user.object.as_ref().unwrap().read().unwrap().id.clone()
);
common::teardown(bundle).await;
}
@ -200,9 +210,9 @@ async fn remove_add_person_from_to_dm() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let mut third_user = bundle.create_user("integrationtestuser3").await;
let third_user_id = third_user.object.read().unwrap().id;
let other_user_id = other_user.object.read().unwrap().id;
let user_id = bundle.user.object.read().unwrap().id;
let third_user_id = third_user.object.as_ref().unwrap().read().unwrap().id;
let other_user_id = other_user.object.as_ref().unwrap().read().unwrap().id;
let user_id = bundle.user.object.as_ref().unwrap().read().unwrap().id;
let user = &mut bundle.user;
let private_channel_create_schema = PrivateChannelCreateSchema {
recipients: Some(Vec::from([other_user_id, third_user_id])),

View File

@ -4,8 +4,8 @@
use std::str::FromStr;
use chorus::gateway::Gateway;
use chorus::types::IntoShared;
use chorus::gateway::{Gateway, GatewayOptions};
use chorus::types::{IntoShared, PermissionFlags};
use chorus::{
instance::{ChorusUser, Instance},
types::{
@ -47,10 +47,11 @@ impl TestBundle {
ChorusUser {
belongs_to: self.user.belongs_to.clone(),
token: self.user.token.clone(),
mfa_token: None,
limits: self.user.limits.clone(),
settings: self.user.settings.clone(),
object: self.user.object.clone(),
gateway: Gateway::spawn(self.instance.urls.wss.clone())
gateway: Gateway::spawn(self.instance.urls.wss.clone(), GatewayOptions::default())
.await
.unwrap(),
}
@ -59,6 +60,10 @@ impl TestBundle {
// Set up a test by creating an Instance and a User. Reduces Test boilerplate.
pub(crate) async fn setup() -> TestBundle {
// So we can get logs when tests fail
let _ = simple_logger::SimpleLogger::with_level(simple_logger::SimpleLogger::new(), log::LevelFilter::Debug).init();
let instance = Instance::new("http://localhost:3001/api").await.unwrap();
// Requires the existence of the below user.
let reg = RegisterSchema {
@ -104,7 +109,7 @@ pub(crate) async fn setup() -> TestBundle {
let role_create_schema: chorus::types::RoleCreateModifySchema = RoleCreateModifySchema {
name: Some("Bundle role".to_string()),
permissions: Some("8".to_string()), // Administrator permissions
permissions: PermissionFlags::from_bits(8), // Administrator permissions
hoist: Some(true),
icon: None,
unicode_emoji: Some("".to_string()),
@ -119,7 +124,7 @@ pub(crate) async fn setup() -> TestBundle {
let urls = UrlBundle::new(
"http://localhost:3001/api".to_string(),
"http://localhost:3001/api".to_string(),
"ws://localhost:3001".to_string(),
"ws://localhost:3001/".to_string(),
"http://localhost:3001".to_string(),
);
TestBundle {

View File

@ -30,7 +30,7 @@ use wasmtimer::tokio::sleep;
async fn test_gateway_establish() {
let bundle = common::setup().await;
let _: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone()).await.unwrap();
let _: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone(), GatewayOptions::default()).await.unwrap();
common::teardown(bundle).await
}
@ -52,7 +52,7 @@ impl Observer<GatewayReady> for GatewayReadyObserver {
async fn test_gateway_authenticate() {
let bundle = common::setup().await;
let gateway: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone()).await.unwrap();
let gateway: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone(), GatewayOptions::default()).await.unwrap();
let (ready_send, mut ready_receive) = tokio::sync::mpsc::channel(1);
@ -79,9 +79,9 @@ async fn test_gateway_authenticate() {
println!("Timed out waiting for event, failing..");
assert!(false);
}
// Sucess, we have received it
// Success, we have received it
Some(_) = ready_receive.recv() => {}
};
}
common::teardown(bundle).await
}
@ -125,7 +125,7 @@ async fn test_self_updating_structs() {
.gateway
.observe_and_into_inner(bundle.guild.clone())
.await;
assert!(guild.channels.is_none());
assert!(guild.channels.is_empty());
Channel::create(
&mut bundle.user,
@ -145,8 +145,8 @@ async fn test_self_updating_structs() {
.gateway
.observe_and_into_inner(guild.into_shared())
.await;
assert!(guild.channels.is_some());
assert!(guild.channels.as_ref().unwrap().len() == 1);
assert!(!guild.channels.is_empty());
assert_eq!(guild.channels.len(), 1);
common::teardown(bundle).await
}
@ -160,13 +160,12 @@ async fn test_recursive_self_updating_structs() {
// Observe Guild, make sure it has no channels
let guild = bundle.user.gateway.observe(guild.clone()).await;
let inner_guild = guild.read().unwrap().clone();
assert!(inner_guild.roles.is_none());
assert!(inner_guild.roles.is_empty());
// Create Role
let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS;
let permissions = Some(permissions.to_string());
let mut role_create_schema: types::RoleCreateModifySchema = RoleCreateModifySchema {
name: Some("cool person".to_string()),
permissions,
permissions: Some(permissions),
hoist: Some(true),
icon: None,
unicode_emoji: Some("".to_string()),
@ -186,7 +185,7 @@ async fn test_recursive_self_updating_structs() {
.await;
// Update Guild and check for Guild
let inner_guild = guild.read().unwrap().clone();
assert!(inner_guild.roles.is_some());
assert!(!inner_guild.roles.is_empty());
// Update the Role
role_create_schema.name = Some("yippieee".to_string());
RoleObject::modify(&mut bundle.user, guild_id, role.id, role_create_schema)
@ -202,8 +201,7 @@ async fn test_recursive_self_updating_structs() {
let guild = bundle.user.gateway.observe(bundle.guild.clone()).await;
let inner_guild = guild.read().unwrap().clone();
let guild_roles = inner_guild.roles;
let guild_role = guild_roles.unwrap();
let guild_role_inner = guild_role.get(0).unwrap().read().unwrap().clone();
let guild_role_inner = guild_roles.get(0).unwrap().read().unwrap().clone();
assert_eq!(guild_role_inner.name, "yippieee".to_string());
common::teardown(bundle).await;
}

View File

@ -60,7 +60,8 @@ async fn guild_create_ban() {
.await
.unwrap();
other_user.accept_invite(&invite.code, None).await.unwrap();
let other_user_id = other_user.object.read().unwrap().id;
let other_user_id = other_user.object.as_ref().unwrap()
.read().unwrap().id;
Guild::create_ban(
guild.id,
other_user_id,
@ -112,7 +113,9 @@ async fn guild_remove_member() {
.await
.unwrap();
other_user.accept_invite(&invite.code, None).await.unwrap();
let other_user_id = other_user.object.read().unwrap().id;
let other_user_id = other_user.object
.as_ref().unwrap()
.read().unwrap().id;
Guild::remove_member(guild.id, other_user_id, None, &mut bundle.user)
.await
.unwrap();

View File

@ -16,7 +16,8 @@ async fn add_remove_role() -> ChorusResult<()> {
let mut bundle = common::setup().await;
let guild = bundle.guild.read().unwrap().id;
let role = bundle.role.read().unwrap().id;
let member_id = bundle.user.object.read().unwrap().id;
let member_id = bundle.user.object.as_ref().unwrap()
.read().unwrap().id;
GuildMember::add_role(&mut bundle.user, guild, member_id, role).await?;
let member = GuildMember::get(&mut bundle.user, guild, member_id)
.await

View File

@ -106,7 +106,7 @@ async fn search_messages() {
let _arg = Some(&vec_attach);
let message = bundle.user.send_message(message, channel.id).await.unwrap();
let query = MessageSearchQuery {
author_id: Some(Vec::from([bundle.user.object.read().unwrap().id])),
author_id: Some(Vec::from([bundle.user.object.as_ref().unwrap().read().unwrap().id])),
..Default::default()
};
let guild_id = bundle.guild.read().unwrap().id;

View File

@ -16,9 +16,10 @@ async fn test_get_mutual_relationships() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let username = user.object.read().unwrap().username.clone();
let discriminator = user.object.read().unwrap().discriminator.clone();
let other_user_id: types::Snowflake = other_user.object.read().unwrap().id;
let username = user.object.as_ref().unwrap().read().unwrap().username.clone();
let discriminator = user.object.as_ref().unwrap().read().unwrap().discriminator.clone();
let other_user_id: types::Snowflake = other_user.object.as_ref().unwrap().read().unwrap().id;
let friend_request_schema = types::FriendRequestSendSchema {
username,
discriminator: Some(discriminator),
@ -38,8 +39,8 @@ async fn test_get_relationships() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let username = user.object.read().unwrap().username.clone();
let discriminator = user.object.read().unwrap().discriminator.clone();
let username = user.object.as_ref().unwrap().read().unwrap().username.clone();
let discriminator = user.object.as_ref().unwrap().read().unwrap().discriminator.clone();
let friend_request_schema = types::FriendRequestSendSchema {
username,
discriminator: Some(discriminator),
@ -51,7 +52,7 @@ async fn test_get_relationships() {
let relationships = user.get_relationships().await.unwrap();
assert_eq!(
relationships.get(0).unwrap().id,
other_user.object.read().unwrap().id
other_user.object.unwrap().read().unwrap().id
);
common::teardown(bundle).await
}
@ -62,8 +63,8 @@ async fn test_modify_relationship_friends() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let user_id: types::Snowflake = user.object.read().unwrap().id;
let other_user_id: types::Snowflake = other_user.object.read().unwrap().id;
let user_id: types::Snowflake = user.object.as_ref().unwrap().read().unwrap().id;
let other_user_id: types::Snowflake = other_user.object.as_ref().unwrap().read().unwrap().id;
other_user
.modify_user_relationship(user_id, types::RelationshipType::Friends)
@ -72,7 +73,7 @@ async fn test_modify_relationship_friends() {
let relationships = user.get_relationships().await.unwrap();
assert_eq!(
relationships.get(0).unwrap().id,
other_user.object.read().unwrap().id
other_user.object.as_ref().unwrap().read().unwrap().id
);
assert_eq!(
relationships.get(0).unwrap().relationship_type,
@ -81,7 +82,7 @@ async fn test_modify_relationship_friends() {
let relationships = other_user.get_relationships().await.unwrap();
assert_eq!(
relationships.get(0).unwrap().id,
user.object.read().unwrap().id
user.object.as_ref().unwrap().read().unwrap().id
);
assert_eq!(
relationships.get(0).unwrap().relationship_type,
@ -114,7 +115,7 @@ async fn test_modify_relationship_block() {
let mut bundle = common::setup().await;
let mut other_user = bundle.create_user("integrationtestuser2").await;
let user = &mut bundle.user;
let user_id: types::Snowflake = user.object.read().unwrap().id;
let user_id: types::Snowflake = user.object.as_ref().unwrap().read().unwrap().id;
other_user
.modify_user_relationship(user_id, types::RelationshipType::Blocked)
@ -125,7 +126,7 @@ async fn test_modify_relationship_block() {
let relationships = other_user.get_relationships().await.unwrap();
assert_eq!(
relationships.get(0).unwrap().id,
user.object.read().unwrap().id
user.object.as_ref().unwrap().read().unwrap().id
);
assert_eq!(
relationships.get(0).unwrap().relationship_type,

View File

@ -15,10 +15,9 @@ mod common;
async fn create_and_get_roles() {
let mut bundle = common::setup().await;
let permissions = types::PermissionFlags::CONNECT | types::PermissionFlags::MANAGE_EVENTS;
let permissions = Some(permissions.to_string());
let role_create_schema: types::RoleCreateModifySchema = RoleCreateModifySchema {
name: Some("cool person".to_string()),
permissions,
permissions: Some(permissions),
hoist: Some(true),
icon: None,
unicode_emoji: Some("".to_string()),