Compare commits

..

40 Commits

Author SHA1 Message Date
kozabrada123 f8fe1c404b Merge branch 'dev' into some-routes 2024-06-23 17:24:11 +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
kozabrada123 4a0bb54d49 Merge branch 'dev' into some-routes 2024-06-23 15:39:18 +02:00
kozabrada123 b913cb5eb2 fix: update / fix PATCH /users/@me 2024-06-23 15:39:08 +02:00
kozabrada123 9700564adb Small changes 2024-06-23 15:37:22 +02:00
Quat3rnion b4a8082f29
Update and add some types in support of the backend (#507) 2024-06-19 19:59:39 +02:00
kozabrada123 a7fc00df29
Bump chorus macros to 0.4.0 (#506)
- Bumps chorus macros to 0.4.0, followup to #505
- Makes builds within the repo use the local path and published releases
use a set version of the macros crate
2024-06-18 17:59:41 +02:00
kozabrada123 b3e62b2c33 always update to latest release of macros 2024-06-18 17:48:57 +02:00
kozabrada123 8d56259eac chorus macros 0.4.0 2024-06-18 17:35:39 +02:00
kozabrada123 3aec5881e5
Merge Quat3rnion/backend/messages (#505)
- Adds a multitude of new Types, Flags, and a few Objects relating to
sending and managing messages.
- Adds new SqlxBitFlag macro, which handles BitFlag objects being read
from/written to the database.
- Modifies some entities to use (new) Distinct Types instead of
primitives
2024-06-18 17:30:50 +02:00
kozabrada123 4430a7c4e6
Fix deserialization error w/ guild features list 2024-06-18 17:18:20 +02:00
Quat3rnion a8b7d9dfb3 u8 -> u64 2024-06-18 10:07:56 -04:00
Quat3rnion e553d10f25 Add new SerdeBitFlags derive macro, to help reduce repetitive code 2024-06-18 10:04:00 -04:00
Quat3rnion c9a36ce725 Maybe fix tests, make UserFlags able to be deserialized from String or u64 2024-06-18 08:46:07 -04:00
Quat3rnion d7de1d2fb7 Fix compilation for real, no dirty hack 2024-06-18 05:46:27 -04:00
Quat3rnion c27bc8d575 Fix test
I feel silly.
2024-06-18 05:34:03 -04:00
Quat3rnion abc4608be5 Dirty hack 2024-06-18 05:32:27 -04:00
Quat3rnion 77004abcfa Feature lock the macro 2024-06-18 05:24:42 -04:00
Quat3rnion 06b4dc3abd forgot a file :( 2024-06-18 05:19:51 -04:00
Quat3rnion 3810170400 Remove unused imports, feature locks in macro 2024-06-18 05:14:28 -04:00
Quat3rnion dfffcf4313 Add forgotten feature locks 2024-06-18 05:09:00 -04:00
Quat3rnion 6261cebfdf Fix test 2024-06-18 05:03:55 -04:00
Quat3rnion d8560093f5 Use chorus_macros from path, since it's there anyway 2024-06-18 04:48:50 -04:00
Quat3rnion bec0269e70 Add partial emoji and custom reaction types, refine SQLx mapping 2024-06-17 15:22:58 -04:00
Quat3rnion 590a6d6828 Make UserFlags deserialize from string 2024-06-07 14:08:47 -04:00
Quat3rnion 9bd55b9b5f Fix error in macro 2024-06-07 14:08:24 -04:00
Quat3rnion d3e2ef947c Add distinct MessageType enum 2024-06-07 13:14:25 -04:00
Quat3rnion d89975819a Utilize new macros and use distinct Flag types 2024-06-07 13:13:27 -04:00
Quat3rnion 656e5e31c0 Add SqlxBitFlags derive macro 2024-06-07 13:03:44 -04:00
Quat3rnion c2035585ae Update Cargo.lock 2024-06-07 13:02:17 -04:00
Quat3rnion 96c3f53961 Merge branch 'dev' into backend/messages 2024-06-07 13:00:04 -04:00
Quat3rnion ed5365ade4 Fix inverted type wrapping 2024-06-07 02:08:41 -04:00
Quat3rnion 0468292ec6 Add type locks 2024-06-07 02:08:21 -04:00
kozabrada123 3237db708c
Backend/invites (#503)
- Adds implementation to convert `Guild` to `InviteGuild`
- Adds GetInviteSchema
- Replaces primitives with distinct types on `InviteGuild`
2024-06-07 06:54:33 +02:00
Quat3rnion 83a8f080b7
Update docs aesthetics
Co-authored-by: kozabrada123 <59031733+kozabrada123@users.noreply.github.com>
2024-06-07 00:46:53 -04:00
Quat3rnion a0cddbf3ae Fix compile error 2024-06-05 21:10:29 -04:00
Quat3rnion 556fbb9ded Add forgotten import and pub use. 2024-06-05 14:50:38 -04:00
Quat3rnion eb87bd6ffc Add `GetInvitesSchema` 2024-06-05 14:50:10 -04:00
Quat3rnion 5110e9bfdb Implement `From<Guild> for InviteGuild` 2024-06-05 14:49:34 -04:00
Quat3rnion c8bde0c9ec Use distinct types in `InviteGuild` object. 2024-06-05 14:49:17 -04:00
45 changed files with 903 additions and 229 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

35
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,9 +266,7 @@ dependencies = [
[[package]]
name = "chorus-macros"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de4221700bc486c6e6bc261fdea478936d33067a06325895f5d2a8cde5917272"
version = "0.4.0"
dependencies = [
"async-trait",
"quote",
@ -355,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"
@ -555,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"
@ -2126,6 +2145,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 = []
@ -43,7 +43,7 @@ thiserror = "1.0.56"
jsonwebtoken = "8.3.0"
log = "0.4.20"
async-trait = "0.1.77"
chorus-macros = "0.3.0"
chorus-macros = { path = "./chorus-macros", version = "0" } # Note: version here is used when releasing. This will use the latest release. Make sure to republish the crate when code in macros is changed!
sqlx = { version = "0.7.3", features = [
"mysql",
"sqlite",
@ -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.2.1"
version = "0.4.0"
dependencies = [
"async-trait",
"quote",

View File

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

View File

@ -155,3 +155,68 @@ pub fn composite_derive(input: TokenStream) -> TokenStream {
_ => panic!("Composite derive macro only supports structs"),
}
}
#[proc_macro_derive(SqlxBitFlags)]
pub fn sqlx_bitflag_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
quote!{
#[cfg(feature = "sqlx")]
impl sqlx::Type<sqlx::MySql> for #name {
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
u64::type_info()
}
}
#[cfg(feature = "sqlx")]
impl<'q> sqlx::Encode<'q, sqlx::MySql> for #name {
fn encode_by_ref(&self, buf: &mut <sqlx::MySql as sqlx::database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
u64::encode_by_ref(&self.bits(), buf)
}
}
#[cfg(feature = "sqlx")]
impl<'q> sqlx::Decode<'q, sqlx::MySql> for #name {
fn decode(value: <sqlx::MySql as sqlx::database::HasValueRef<'q>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
u64::decode(value).map(|d| #name::from_bits(d).unwrap())
}
}
}
.into()
}
#[proc_macro_derive(SerdeBitFlags)]
pub fn serde_bitflag_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
quote! {
impl std::str::FromStr for #name {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<#name, Self::Err> {
s.parse::<u64>().map(#name::from_bits).map(|f| f.unwrap_or(#name::empty()))
}
}
impl serde::Serialize for #name {
fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.bits().to_string())
}
}
impl<'de> serde::Deserialize<'de> for #name {
fn deserialize<D>(deserializer: D) -> Result<#name, D::Error> where D: serde::de::Deserializer<'de> + Sized {
// 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())
}
}
}
.into()
}

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"))]
@ -26,9 +26,15 @@ use wasmtimer::tokio::sleep;
#[tokio::main(flavor = "current_thread")]
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

@ -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,32 @@ 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 +126,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 +143,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 +152,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 +169,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 +207,41 @@ 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;
}

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,41 @@ 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
let mut output = Vec::with_capacity(bytes.len() * 20);
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

@ -13,7 +13,7 @@ use reqwest::Client;
use serde::{Deserialize, Serialize};
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::{
@ -31,6 +31,8 @@ pub struct Instance {
pub limits_information: Option<LimitsInformation>,
#[serde(skip)]
pub client: Client,
#[serde(skip)]
pub gateway_options: GatewayOptions,
}
impl PartialEq for Instance {
@ -104,6 +106,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,
@ -139,6 +142,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)]
@ -215,7 +225,9 @@ impl ChorusUser {
let object = Arc::new(RwLock::new(User::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,
belongs_to: instance.clone(),

View File

@ -88,7 +88,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) => {
@ -494,7 +494,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,6 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_number_from_string;
use crate::types::{config::types::subconfigs::register::{
DateOfBirthConfiguration, PasswordConfiguration, RegistrationEmailConfiguration,
@ -22,6 +23,7 @@ pub struct RegisterConfiguration {
pub allow_multiple_accounts: bool,
pub block_proxies: bool,
pub incrementing_discriminators: bool,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub default_rights: Rights,
}

View File

@ -31,7 +31,7 @@ pub struct Application {
pub verify_key: String,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub owner: Shared<User>,
pub flags: u64,
pub flags: ApplicationFlags,
#[cfg(feature = "sqlx")]
pub redirect_uris: Option<sqlx::types::Json<Vec<String>>>,
#[cfg(not(feature = "sqlx"))]
@ -73,7 +73,7 @@ impl Default for Application {
bot_require_code_grant: false,
verify_key: "".to_string(),
owner: Default::default(),
flags: 0,
flags: ApplicationFlags::empty(),
redirect_uris: None,
rpc_application_state: 0,
store_application_state: 1,
@ -93,12 +93,6 @@ impl Default for Application {
}
}
impl Application {
pub fn flags(&self) -> ApplicationFlags {
ApplicationFlags::from_bits(self.flags.to_owned()).unwrap()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
/// # Reference
/// See <https://discord.com/developers/docs/resources/application#install-params-object>
@ -108,7 +102,8 @@ pub struct InstallParams {
}
bitflags! {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// # Reference
/// See <https://discord.com/developers/docs/resources/application#application-object-application-flags>
pub struct ApplicationFlags: u64 {

View File

@ -4,12 +4,11 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::deserialize_string_from_number;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::fmt::Debug;
use crate::types::Shared;
use crate::types::{
PermissionFlags, Shared,
entities::{GuildMember, User},
utils::Snowflake,
};
@ -64,7 +63,9 @@ pub struct Channel {
pub managed: Option<bool>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub member: Option<ThreadMember>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub member_count: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub message_count: Option<i32>,
pub name: Option<String>,
pub nsfw: Option<bool>,
@ -75,6 +76,7 @@ pub struct Channel {
#[cfg(not(feature = "sqlx"))]
#[cfg_attr(feature = "client", observe_option_vec)]
pub permission_overwrites: Option<Vec<Shared<PermissionOverwrite>>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub permissions: Option<String>,
pub position: Option<i32>,
pub rate_limit_per_user: Option<i32>,
@ -85,6 +87,7 @@ pub struct Channel {
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub thread_metadata: Option<ThreadMetadata>,
pub topic: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub total_message_sent: Option<i32>,
pub user_limit: Option<i32>,
pub video_quality_mode: Option<i32>,
@ -144,14 +147,20 @@ pub struct Tag {
pub struct PermissionOverwrite {
pub id: Snowflake,
#[serde(rename = "type")]
#[serde(deserialize_with = "deserialize_string_from_number")]
pub overwrite_type: String,
pub overwrite_type: PermissionOverwriteType,
#[serde(default)]
#[serde(deserialize_with = "deserialize_string_from_number")]
pub allow: String,
pub allow: PermissionFlags,
#[serde(default)]
#[serde(deserialize_with = "deserialize_string_from_number")]
pub deny: String,
pub deny: PermissionFlags,
}
#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, PartialEq, Eq, PartialOrd)]
#[repr(u8)]
/// # Reference
pub enum PermissionOverwriteType {
Role = 0,
Member = 1,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
@ -256,3 +265,12 @@ pub enum ChannelType {
// TODO: Couldn't find reference
Unhandled = 255,
}
/// # Reference
/// See <https://docs.discord.sex/resources/message#followed-channel-object>
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct FollowedChannel {
pub channel_id: Snowflake,
pub webhook_id: Snowflake
}

View File

@ -6,7 +6,7 @@ use std::fmt::Debug;
use serde::{Deserialize, Serialize};
use crate::types::Shared;
use crate::types::{PartialEmoji, Shared};
use crate::types::entities::User;
use crate::types::Snowflake;
@ -66,3 +66,18 @@ impl PartialEq for Emoji {
|| self.available != other.available)
}
}
impl From<PartialEmoji> for Emoji {
fn from(value: PartialEmoji) -> Self {
Self {
id: value.id.unwrap_or_default(), // TODO: this should be handled differently
name: Some(value.name),
roles: None,
user: None,
require_colons: Some(value.animated),
managed: None,
animated: Some(value.animated),
available: None,
}
}
}

View File

@ -59,7 +59,8 @@ pub struct Guild {
pub emojis: Vec<Shared<Emoji>>,
pub explicit_content_filter: Option<ExplicitContentFilterLevel>,
//#[cfg_attr(feature = "sqlx", sqlx(try_from = "String"))]
pub features: Option<GuildFeaturesList>,
#[serde(default)]
pub features: GuildFeaturesList,
pub icon: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub icon_hash: Option<String>,
@ -99,7 +100,7 @@ pub struct Guild {
pub splash: Option<String>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub stickers: Option<Vec<Sticker>>,
pub system_channel_flags: Option<u64>,
pub system_channel_flags: Option<SystemChannelFlags>,
pub system_channel_id: Option<Snowflake>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub vanity_url_code: Option<String>,
@ -111,7 +112,7 @@ pub struct Guild {
#[cfg_attr(feature = "client", observe_option_vec)]
pub webhooks: Option<Vec<Shared<Webhook>>>,
#[cfg(feature = "sqlx")]
pub welcome_screen: Option<sqlx::types::Json<WelcomeScreenObject>>,
pub welcome_screen: sqlx::types::Json<Option<WelcomeScreenObject>>,
#[cfg(not(feature = "sqlx"))]
pub welcome_screen: Option<WelcomeScreenObject>,
pub widget_channel_id: Option<Snowflake>,
@ -422,7 +423,8 @@ pub enum PremiumTier {
}
bitflags! {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/guild#system-channel-flags>
pub struct SystemChannelFlags: u64 {

View File

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

View File

@ -5,7 +5,8 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::types::{Snowflake, WelcomeScreenObject, Shared, InviteFlags, InviteType, InviteTargetType};
use crate::types::{Snowflake, WelcomeScreenObject, Shared, InviteFlags, InviteType, InviteTargetType, Guild, VerificationLevel};
use crate::types::types::guild_configuration::GuildFeaturesList;
use super::guild::GuildScheduledEvent;
use super::{Application, Channel, GuildMember, NSFWLevel, User};
@ -15,7 +16,9 @@ use super::{Application, Channel, GuildMember, NSFWLevel, User};
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct Invite {
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub approximate_member_count: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub approximate_presence_count: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub channel: Option<Channel>,
@ -44,7 +47,7 @@ pub struct Invite {
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub target_user: Option<User>,
pub temporary: Option<bool>,
pub uses: Option<i32>,
pub uses: Option<u32>,
}
/// The guild an invite is for.
@ -55,8 +58,8 @@ pub struct InviteGuild {
pub name: String,
pub icon: Option<String>,
pub splash: Option<String>,
pub verification_level: i32,
pub features: Vec<String>,
pub verification_level: VerificationLevel,
pub features: GuildFeaturesList,
pub vanity_url_code: Option<String>,
pub description: Option<String>,
pub banner: Option<String>,
@ -68,6 +71,29 @@ pub struct InviteGuild {
pub welcome_screen: Option<WelcomeScreenObject>,
}
impl From<Guild> for InviteGuild {
fn from(value: Guild) -> Self {
Self {
id: value.id,
name: value.name.unwrap_or_default(),
icon: value.icon,
splash: value.splash,
verification_level: value.verification_level.unwrap_or_default(),
features: value.features,
vanity_url_code: value.vanity_url_code,
description: value.description,
banner: value.banner,
premium_subscription_count: value.premium_subscription_count,
nsfw_deprecated: None,
nsfw_level: value.nsfw_level.unwrap_or_default(),
#[cfg(feature = "sqlx")]
welcome_screen: value.welcome_screen.0,
#[cfg(not(feature = "sqlx"))]
welcome_screen: value.welcome_screen,
}
}
}
/// See <https://discord-userdoccers.vercel.app/resources/invite#invite-stage-instance-object>
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteStageInstance {

View File

@ -2,8 +2,10 @@
// 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 bitflags::bitflags;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::types::{
Shared,
@ -39,7 +41,7 @@ pub struct Message {
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub attachments: Option<Vec<Attachment>>,
#[cfg(feature = "sqlx")]
pub embeds: Vec<sqlx::types::Json<Embed>>,
pub embeds: sqlx::types::Json<Vec<Embed>>,
#[cfg(not(feature = "sqlx"))]
pub embeds: Option<Vec<Embed>>,
#[cfg(feature = "sqlx")]
@ -50,7 +52,7 @@ pub struct Message {
pub pinned: bool,
pub webhook_id: Option<Snowflake>,
#[serde(rename = "type")]
pub message_type: i32,
pub message_type: MessageType,
#[cfg(feature = "sqlx")]
pub activity: Option<sqlx::types::Json<MessageActivity>>,
#[cfg(not(feature = "sqlx"))]
@ -62,14 +64,22 @@ pub struct Message {
pub message_reference: Option<sqlx::types::Json<MessageReference>>,
#[cfg(not(feature = "sqlx"))]
pub message_reference: Option<MessageReference>,
pub flags: Option<u64>,
pub flags: Option<MessageFlags>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub referenced_message: Option<Box<Message>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub interaction: Option<MessageInteraction>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub thread: Option<Channel>,
#[cfg(feature = "sqlx")]
pub components: Option<sqlx::types::Json<Vec<Component>>>,
#[cfg(not(feature = "sqlx"))]
pub components: Option<Vec<Component>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub sticker_items: Option<Vec<StickerItem>>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub stickers: Option<Vec<Sticker>>,
pub position: Option<i32>,
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub role_subscription_data: Option<RoleSubscriptionData>,
}
@ -103,7 +113,7 @@ impl PartialEq for Message {
&& self.thread == other.thread
&& self.components == other.components
&& self.sticker_items == other.sticker_items
&& self.position == other.position
// && self.position == other.position
&& self.role_subscription_data == other.role_subscription_data
}
}
@ -112,12 +122,22 @@ impl PartialEq for Message {
/// # Reference
/// See <https://discord-userdoccers.vercel.app/resources/message#message-reference-object>
pub struct MessageReference {
#[serde(rename = "type")]
pub reference_type: MessageReferenceType,
pub message_id: Snowflake,
pub channel_id: Snowflake,
pub guild_id: Option<Snowflake>,
pub fail_if_not_exists: Option<bool>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Ord, PartialOrd)]
pub enum MessageReferenceType {
/// A standard reference used by replies and system messages
Default = 0,
/// A reference used to point to a message at a point in time
Forward = 1,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessageInteraction {
pub id: Snowflake,
@ -227,10 +247,15 @@ pub struct EmbedField {
pub struct Reaction {
pub count: u32,
pub burst_count: u32,
#[serde(default)]
pub me: bool,
#[serde(default)]
pub burst_me: bool,
pub burst_colors: Vec<String>,
pub emoji: Emoji,
#[cfg(feature = "sqlx")]
#[serde(skip)]
pub user_ids: Vec<Snowflake>
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Eq, PartialOrd, Ord)]
@ -253,3 +278,155 @@ pub struct MessageActivity {
pub activity_type: i64,
pub party_id: Option<String>,
}
#[derive(Debug, Default, PartialEq, Clone, Copy, Serialize_repr, Deserialize_repr, Eq, PartialOrd, Ord)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[repr(u8)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
/// # Reference
/// See <https://docs.discord.sex/resources/message#message-type>
pub enum MessageType {
/// A default message
#[default]
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
RecipientRemove = 2,
/// A message sent when a user creates a call in a private channel
Call = 3,
/// A message sent when a group DM or thread's name is changed
ChannelNameChange = 4,
/// A message sent when a group DM's icon is changed
ChannelIconChange = 5,
/// A message sent when a message is pinned in a channel
ChannelPinnedMessage = 6,
/// A message sent when a user joins a guild
GuildMemberJoin = 7,
/// A message sent when a user subscribes to (boosts) a guild
UserPremiumGuildSubscription = 8,
/// A message sent when a user subscribes to (boosts) a guild to tier 1
UserPremiumGuildSubscriptionTier1 = 9,
/// A message sent when a user subscribes to (boosts) a guild to tier 2
UserPremiumGuildSubscriptionTier2 = 10,
/// A message sent when a user subscribes to (boosts) a guild to tier 3
UserPremiumGuildSubscriptionTier3 = 11,
/// A message sent when a news channel is followed
ChannelFollowAdd = 12,
/// A message sent when a user starts streaming in a guild (deprecated)
#[deprecated]
GuildStream = 13,
/// A message sent when a guild is disqualified from discovery
GuildDiscoveryDisqualified = 14,
/// A message sent when a guild requalifies for discovery
GuildDiscoveryRequalified = 15,
/// A message sent when a guild has failed discovery requirements for a week
GuildDiscoveryGracePeriodInitial = 16,
/// A message sent when a guild has failed discovery requirements for 3 weeks
GuildDiscoveryGracePeriodFinal = 17,
/// A message sent when a thread is created
ThreadCreated = 18,
/// A message sent when a user replies to a message
Reply = 19,
/// A message sent when a user uses a slash command
#[serde(rename = "CHAT_INPUT_COMMAND")]
ApplicationCommand = 20,
/// A message sent when a thread starter message is added to a thread
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
ContextMenuCommand = 23,
/// A message sent when auto moderation takes an action
AutoModerationAction = 24,
/// A message sent when a user purchases or renews a role subscription
RoleSubscriptionPurchase = 25,
/// A message sent when a user is upsold to a premium interaction
InteractionPremiumUpsell = 26,
/// A message sent when a stage channel starts
StageStart = 27,
/// A message sent when a stage channel ends
StageEnd = 28,
/// A message sent when a user starts speaking in a stage channel
StageSpeaker = 29,
/// A message sent when a user raises their hand in a stage channel
StageRaiseHand = 30,
/// A message sent when a stage channel's topic is changed
StageTopic = 31,
/// A message sent when a user purchases an application premium subscription
GuildApplicationPremiumSubscription = 32,
/// A message sent when a user adds an application to group DM
PrivateChannelIntegrationAdded = 33,
/// A message sent when a user removed an application from a group DM
PrivateChannelIntegrationRemoved = 34,
/// A message sent when a user gifts a premium (Nitro) referral
PremiumReferral = 35,
/// A message sent when a user enabled lockdown for the guild
GuildIncidentAlertModeEnabled = 36,
/// A message sent when a user disables lockdown for the guild
GuildIncidentAlertModeDisabled = 37,
/// A message sent when a user reports a raid for the guild
GuildIncidentReportRaid = 38,
/// A message sent when a user reports a false alarm for the guild
GuildIncidentReportFalseAlarm = 39,
/// A message sent when no one sends a message in the current channel for 1 hour
GuildDeadchatRevivePrompt = 40,
/// A message sent when a user buys another user a gift
CustomGift = 41,
GuildGamingStatsPrompt = 42,
/// A message sent when a user purchases a guild product
PurchaseNotification = 44
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// # Reference
/// See <https://docs.discord.sex/resources/message#message-type>
pub struct MessageFlags: u64 {
/// This message has been published to subscribed channels (via Channel Following)
const CROSSPOSTED = 1 << 0;
/// This message originated from a message in another channel (via Channel Following)
const IS_CROSSPOST = 1 << 1;
/// Embeds will not be included when serializing this message
const SUPPRESS_EMBEDS = 1 << 2;
/// The source message for this crosspost has been deleted (via Channel Following)
const SOURCE_MESSAGE_DELETED = 1 << 3;
/// This message came from the urgent message system
const URGENT = 1 << 4;
/// This message has an associated thread, with the same ID as the message
const HAS_THREAD = 1 << 5;
/// This message is only visible to the user who invoked the interaction
const EPHEMERAL = 1 << 6;
/// This message is an interaction response and the bot is "thinking"
const LOADING = 1 << 7;
/// Some roles were not mentioned and added to the thread
const FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8;
/// This message contains a link that impersonates Discord
const SHOULD_SHOW_LINK_NOT_DISCORD_WARNING = 1 << 10;
/// This message will not trigger push and desktop notifications
const SUPPRESS_NOTIFICATIONS = 1 << 12;
/// This message's audio attachments are rendered as voice messages
const VOICE_MESSAGE = 1 << 13;
/// This message has a forwarded message snapshot attached
const HAS_SNAPSHOT = 1 << 14;
}
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct PartialEmoji {
#[serde(default)]
pub id: Option<Snowflake>,
pub name: String,
#[serde(default)]
pub animated: bool
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, PartialOrd)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[repr(u8)]
pub enum ReactionType {
Normal = 0,
Burst = 1, // The dreaded super reactions
}

View File

@ -71,7 +71,8 @@ pub struct RoleTags {
}
bitflags! {
#[derive(Debug, Default, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Hash, PartialEq, Eq, PartialOrd, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// Permissions limit what users of certain roles can do on a Guild to Guild basis.
///
/// # Reference:

View File

@ -45,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>,
@ -54,14 +54,14 @@ pub struct User {
/// So we need to account for that
#[serde(default)]
#[serde(deserialize_with = "deserialize_option_number_from_string")]
pub flags: Option<i32>,
pub flags: Option<UserFlags>,
pub premium_since: Option<DateTime<Utc>>,
pub premium_type: Option<u8>,
pub pronouns: Option<String>,
pub public_flags: Option<u32>,
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>,
@ -76,9 +76,9 @@ 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>,
@ -111,8 +111,8 @@ impl From<User> for PublicUser {
const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
pub struct UserFlags: u64 {
const DISCORD_EMPLOYEE = 1 << 0;
const PARTNERED_SERVER_OWNER = 1 << 1;

View File

@ -5,7 +5,7 @@
use chrono::{serde::ts_milliseconds_option, Utc};
use serde::{Deserialize, Serialize};
use crate::types::Shared;
use crate::types::{Shared, Snowflake};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
@ -37,7 +37,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 +90,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 +148,10 @@ 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,
pub id: Option<Snowflake>,
pub name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@ -32,13 +32,13 @@ use crate::types::{
pub struct Webhook {
pub id: Snowflake,
#[serde(rename = "type")]
pub webhook_type: i32,
pub webhook_type: WebhookType,
pub name: String,
pub avatar: String,
pub token: String,
pub guild_id: Snowflake,
pub guild_id: Snowflake,
pub channel_id: Snowflake,
pub application_id: Snowflake,
pub application_id: Option<Snowflake>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "sqlx", sqlx(skip))]
pub user: Option<Shared<User>>,
@ -48,3 +48,13 @@ pub struct Webhook {
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy)]
#[repr(u8)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
pub enum WebhookType {
#[default]
Incoming = 1,
ChannelFollower = 2,
Application = 3,
}

View File

@ -20,7 +20,9 @@ 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)]

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 {
pub guilds: Vec<Vec<MergedPresenceGuild>>,
/// "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

@ -32,8 +32,8 @@ bitflags! {
/// Bitflags of speaking types;
///
/// See <https://discord.com/developers/docs/topics/voice-connections#speaking>
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)]
pub struct SpeakingBitflags: u8 {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, chorus_macros::SerdeBitFlags)]
pub struct SpeakingBitflags: u64 {
/// Whether we'll be transmitting normal voice audio
const MICROPHONE = 1 << 0;
/// Whether we'll be transmitting context audio for video, no speaking indicator

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")]
@ -59,7 +58,7 @@ pub struct GetChannelMessagesSchema {
/// Between 1 and 100, defaults to 50.
pub limit: Option<i32>,
#[serde(flatten)]
pub anchor: ChannelMessagesAnchor,
pub anchor: Option<ChannelMessagesAnchor>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
@ -74,21 +73,21 @@ impl GetChannelMessagesSchema {
pub fn before(anchor: Snowflake) -> Self {
Self {
limit: None,
anchor: ChannelMessagesAnchor::Before(anchor),
anchor: Some(ChannelMessagesAnchor::Before(anchor)),
}
}
pub fn around(anchor: Snowflake) -> Self {
Self {
limit: None,
anchor: ChannelMessagesAnchor::Around(anchor),
anchor: Some(ChannelMessagesAnchor::Around(anchor)),
}
}
pub fn after(anchor: Snowflake) -> Self {
Self {
limit: None,
anchor: ChannelMessagesAnchor::After(anchor),
anchor: Some(ChannelMessagesAnchor::After(anchor)),
}
}
@ -131,63 +130,14 @@ impl Default for CreateChannelInviteSchema {
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
pub struct InviteFlags: u64 {
const GUEST = 1 << 0;
const VIEWED = 1 << 1;
}
}
impl Serialize for InviteFlags {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.bits().to_string().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for InviteFlags {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de> {
struct FlagsVisitor;
impl<'de> Visitor<'de> for FlagsVisitor
{
type Value = InviteFlags;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a raw u64 value of flags")
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
InviteFlags::from_bits(v).ok_or(serde::de::Error::custom(Error::InvalidFlags(v)))
}
}
deserializer.deserialize_u64(FlagsVisitor)
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<sqlx::MySql> for InviteFlags {
fn type_info() -> sqlx::mysql::MySqlTypeInfo {
u64::type_info()
}
}
#[cfg(feature = "sqlx")]
impl<'q> sqlx::Encode<'q, sqlx::MySql> for InviteFlags {
fn encode_by_ref(&self, buf: &mut <sqlx::MySql as sqlx::database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
u64::encode_by_ref(&self.0.0, buf)
}
}
#[cfg(feature = "sqlx")]
impl<'r> sqlx::Decode<'r, sqlx::MySql> for InviteFlags {
fn decode(value: <sqlx::MySql as sqlx::database::HasValueRef<'r>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
let raw = u64::decode(value)?;
Ok(Self::from_bits(raw).unwrap())
}
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq)]
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
@ -226,3 +176,15 @@ pub struct ModifyChannelPositionsSchema {
pub lock_permissions: Option<bool>,
pub parent_id: Option<Snowflake>,
}
/// See <https://docs.discord.sex/resources/channel#follow-channel>
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialOrd, Ord, PartialEq, Eq)]
pub struct AddFollowingChannelSchema {
pub webhook_channel_id: Snowflake,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialOrd, Ord, PartialEq, Eq)]
pub struct CreateWebhookSchema {
pub name: String,
pub avatar: Option<String>,
}

View File

@ -157,6 +157,7 @@ pub struct ModifyGuildMemberSchema {
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
/// Represents the flags of a Guild Member.
///
/// # Reference:

View File

@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
/// Query parameters for the `Get Invite` route.
///
/// # Reference:
/// Read: <https://docs.discord.sex/resources/invite#query-string-params>
pub struct GetInvitesSchema {
pub with_counts: Option<bool>,
}

View File

@ -7,13 +7,13 @@ use serde::{Deserialize, Serialize};
use crate::types::entities::{
AllowedMention, Component, Embed, MessageReference, PartialDiscordFileAttachment,
};
use crate::types::{Attachment, Snowflake};
use crate::types::{Attachment, MessageFlags, MessageType, ReactionType, Snowflake};
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct MessageSendSchema {
#[serde(rename = "type")]
pub message_type: Option<i32>,
pub message_type: Option<MessageType>,
pub content: Option<String>,
pub nonce: Option<String>,
pub tts: Option<bool>,
@ -118,13 +118,21 @@ pub struct MessageAck {
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)]
pub struct MessageModifySchema {
content: Option<String>,
embeds: Option<Vec<Embed>>,
embed: Option<Embed>,
allowed_mentions: Option<AllowedMention>,
components: Option<Vec<Component>>,
flags: Option<i32>,
files: Option<Vec<u8>>,
payload_json: Option<String>,
attachments: Option<Vec<Attachment>>,
pub content: Option<String>,
pub embeds: Option<Vec<Embed>>,
pub embed: Option<Embed>,
pub allowed_mentions: Option<AllowedMention>,
pub components: Option<Vec<Component>>,
pub flags: Option<MessageFlags>,
pub files: Option<Vec<u8>>,
pub payload_json: Option<String>,
pub attachments: Option<Vec<Attachment>>,
}
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)]
pub struct ReactionQuerySchema {
pub after: Option<Snowflake>,
pub limit: Option<u32>,
#[serde(rename = "type")]
pub reaction_type: Option<ReactionType>
}

View File

@ -10,6 +10,7 @@ pub use message::*;
pub use relationship::*;
pub use role::*;
pub use user::*;
pub use invites::*;
mod apierror;
mod auth;
@ -19,3 +20,4 @@ mod message;
mod relationship;
mod role;
mod user;
mod invites;

View File

@ -2,11 +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 std::num::ParseIntError;
use std::str::FromStr;
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::{{Decode, Encode, MySql}, database::{HasArguments, HasValueRef}, encode::IsNull, error::BoxDynError, mysql::MySqlValueRef};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::types::UserFlags;
bitflags! {
/// Rights are instance-wide, per-user permissions for everything you may perform on the instance,
@ -18,7 +18,8 @@ bitflags! {
///
/// # Reference
/// See <https://docs.spacebar.chat/setup/server/security/rights/>
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, chorus_macros::SerdeBitFlags)]
#[cfg_attr(feature = "sqlx", derive(chorus_macros::SqlxBitFlags))]
pub struct Rights: u64 {
/// All rights
const OPERATOR = 1 << 0;
@ -132,33 +133,6 @@ bitflags! {
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<MySql> for Rights {
fn type_info() -> <sqlx::MySql as sqlx::Database>::TypeInfo {
u64::type_info()
}
fn compatible(ty: &<sqlx::MySql as sqlx::Database>::TypeInfo) -> bool {
u64::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'q> Encode<'q, MySql> for Rights {
fn encode_by_ref(&self, buf: &mut <MySql as HasArguments<'q>>::ArgumentBuffer) -> IsNull {
<u64 as Encode<MySql>>::encode_by_ref(&self.0.0, buf)
}
}
#[cfg(feature = "sqlx")]
impl<'r> Decode<'r, MySql> for Rights {
fn decode(value: <MySql as HasValueRef<'r>>::ValueRef) -> Result<Self, BoxDynError> {
let raw = <u64 as Decode<MySql>>::decode(value)?;
Ok(Rights::from_bits(raw).unwrap())
}
}
impl Rights {
pub fn any(&self, permission: Rights, check_operator: bool) -> bool {
(check_operator && self.contains(Rights::OPERATOR)) || self.contains(permission)

View File

@ -1,6 +1,7 @@
use core::fmt;
use chrono::{LocalResult, NaiveDateTime};
use serde::de;
use serde::{de, Deserialize, Deserializer};
use serde::de::Error;
#[doc(hidden)]
#[derive(Debug)]
@ -259,4 +260,19 @@ pub(crate) fn serde_from<T, E, V>(me: LocalResult<T>, _ts: &V) -> Result<T, E>
}
LocalResult::Single(val) => Ok(val),
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
enum StringOrU64 {
String(String),
U64(u64),
}
pub fn string_or_u64<'de, D>(d: D) -> Result<u64, D::Error>
where D: Deserializer<'de> {
match StringOrU64::deserialize(d)? {
StringOrU64::String(s) => s.parse::<u64>().map_err(D::Error::custom),
StringOrU64::U64(u) => Ok(u)
}
}

View File

@ -8,8 +8,6 @@ use std::{
};
use chrono::{DateTime, TimeZone, Utc};
#[cfg(feature = "sqlx")]
use sqlx::Type;
/// 2015-01-01
const EPOCH: i64 = 1420070400000;
@ -19,8 +17,6 @@ const EPOCH: i64 = 1420070400000;
/// # Reference
/// See <https://discord.com/developers/docs/reference#snowflakes>
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "sqlx", derive(Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
pub struct Snowflake(pub u64);
impl Snowflake {
@ -102,6 +98,27 @@ impl<'de> serde::Deserialize<'de> for Snowflake {
}
}
#[cfg(feature = "sqlx")]
impl sqlx::Type<sqlx::MySql> for Snowflake {
fn type_info() -> <sqlx::MySql as sqlx::Database>::TypeInfo {
<String as sqlx::Type<sqlx::MySql>>::type_info()
}
}
#[cfg(feature = "sqlx")]
impl<'q> sqlx::Encode<'q, sqlx::MySql> for Snowflake {
fn encode_by_ref(&self, buf: &mut <sqlx::MySql as sqlx::database::HasArguments<'q>>::ArgumentBuffer) -> sqlx::encode::IsNull {
<String as sqlx::Encode<'q, sqlx::MySql>>::encode_by_ref(&self.0.to_string(), buf)
}
}
#[cfg(feature = "sqlx")]
impl<'d> sqlx::Decode<'d, sqlx::MySql> for Snowflake {
fn decode(value: <sqlx::MySql as sqlx::database::HasValueRef<'d>>::ValueRef) -> Result<Self, sqlx::error::BoxDynError> {
<String as sqlx::Decode<'d, sqlx::MySql>>::decode(value).map(|s| s.parse::<u64>().map(Snowflake).unwrap())
}
}
#[cfg(test)]
mod test {
use chrono::{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

@ -2,10 +2,7 @@
// 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, PrivateChannelCreateSchema, RelationshipType, Snowflake,
};
use chorus::types::{self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, PermissionOverwrite, PermissionOverwriteType, PrivateChannelCreateSchema, RelationshipType, Snowflake};
mod common;
@ -69,16 +66,13 @@ async fn modify_channel() {
.unwrap();
assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string()));
let permission_override = PermissionFlags::from_vec(Vec::from([
PermissionFlags::MANAGE_CHANNELS,
PermissionFlags::MANAGE_MESSAGES,
]));
let permission_override = PermissionFlags::MANAGE_CHANNELS | PermissionFlags::MANAGE_MESSAGES;
let user_id: types::Snowflake = bundle.user.object.read().unwrap().id;
let permission_override = PermissionOverwrite {
id: user_id,
overwrite_type: "1".to_string(),
overwrite_type: PermissionOverwriteType::Member,
allow: permission_override,
deny: "0".to_string(),
deny: PermissionFlags::empty(),
};
let channel_id: Snowflake = bundle.channel.read().unwrap().id;
Channel::modify_permissions(

View File

@ -4,7 +4,7 @@
use std::str::FromStr;
use chorus::gateway::Gateway;
use chorus::gateway::{Gateway, GatewayOptions};
use chorus::types::IntoShared;
use chorus::{
instance::{ChorusUser, Instance},
@ -50,7 +50,7 @@ impl TestBundle {
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 +59,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 {
@ -119,7 +123,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,7 +79,7 @@ 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() => {}
};

View File

@ -920,7 +920,7 @@ mod entities {
.clone()
);
let flags = ApplicationFlags::from_bits(0).unwrap();
assert!(application.flags() == flags);
assert!(application.flags == flags);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]