From 1431aba363006f227d580c65e40ada68ac5e8aa5 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 20 Jun 2023 19:12:21 +0200 Subject: [PATCH 01/72] Add Webrtc Identify & Ready --- src/types/events/identify.rs | 2 +- src/types/events/mod.rs | 4 ++++ src/types/events/voice.rs | 4 ++-- src/types/events/webrtc/identify.rs | 18 ++++++++++++++++++ src/types/events/webrtc/mod.rs | 5 +++++ src/types/events/webrtc/ready.rs | 29 +++++++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/types/events/webrtc/identify.rs create mode 100644 src/types/events/webrtc/mod.rs create mode 100644 src/types/events/webrtc/ready.rs diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index dcd3a8a..c014cee 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,7 +2,7 @@ use crate::types::events::{PresenceUpdate, WebSocketEvent}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct GatewayIdentifyPayload { pub token: String, pub properties: GatewayIdentifyConnectionProps, diff --git a/src/types/events/mod.rs b/src/types/events/mod.rs index 6333544..e3547e9 100644 --- a/src/types/events/mod.rs +++ b/src/types/events/mod.rs @@ -26,6 +26,8 @@ pub use user::*; pub use voice::*; pub use webhooks::*; +pub use webrtc::*; + mod application; mod auto_moderation; mod call; @@ -52,6 +54,8 @@ mod user; mod voice; mod webhooks; +mod webrtc; + pub trait WebSocketEvent {} #[derive(Debug, Default, Serialize, Clone)] diff --git a/src/types/events/voice.rs b/src/types/events/voice.rs index e896393..86bd97f 100644 --- a/src/types/events/voice.rs +++ b/src/types/events/voice.rs @@ -1,4 +1,4 @@ -use crate::types::{events::WebSocketEvent, VoiceState}; +use crate::types::{events::WebSocketEvent, Snowflake, VoiceState}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default)] @@ -34,7 +34,7 @@ impl WebSocketEvent for VoiceStateUpdate {} /// Received to indicate which voice endpoint, token and guild_id to use; pub struct VoiceServerUpdate { pub token: String, - pub guild_id: String, + pub guild_id: Snowflake, pub endpoint: Option, } diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs new file mode 100644 index 0000000..f41017d --- /dev/null +++ b/src/types/events/webrtc/identify.rs @@ -0,0 +1,18 @@ +use crate::types::{Snowflake, WebSocketEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +/// The identify payload for the webrtc stream; +/// Contains info to begin a webrtc connection; +/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-identify-payload; +pub struct VoiceIdentify { + server_id: Snowflake, + user_id: Snowflake, + session_id: String, + token: String, + #[serde(skip_serializing_if = "Option::is_none")] + /// Undocumented field, but is also in discord client comms + video: Option, +} + +impl WebSocketEvent for VoiceIdentify {} diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs new file mode 100644 index 0000000..ebaf7b2 --- /dev/null +++ b/src/types/events/webrtc/mod.rs @@ -0,0 +1,5 @@ +pub use identify::*; +pub use ready::*; + +mod identify; +mod ready; diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs new file mode 100644 index 0000000..c805593 --- /dev/null +++ b/src/types/events/webrtc/ready.rs @@ -0,0 +1,29 @@ +use std::net::Ipv4Addr; + +use crate::types::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// The ready event for the webrtc stream; +/// Used to give info after the identify event; +/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; +pub struct VoiceReady { + ssrc: u8, + ip: Ipv4Addr, + port: u8, + modes: Vec, + // Heartbeat interval is also sent, but is "an erroneous field and should be ignored. The correct heartbeat_interval value comes from the Hello payload." +} + +impl Default for VoiceReady { + fn default() -> Self { + VoiceReady { + ssrc: 1, + ip: Ipv4Addr::UNSPECIFIED, + port: 0, + modes: Vec::new(), + } + } +} + +impl WebSocketEvent for VoiceReady {} From b4a4e1f5d5effef2d472ee6336ce8c4d8b0af06b Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 20 Jun 2023 19:51:28 +0200 Subject: [PATCH 02/72] Add more webrtc typings --- src/types/events/webrtc/mod.rs | 26 ++++++++++++++++++++++ src/types/events/webrtc/ready.rs | 10 ++++++--- src/types/events/webrtc/select_protocol.rs | 25 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/types/events/webrtc/select_protocol.rs diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index ebaf7b2..69d8e32 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -1,5 +1,31 @@ pub use identify::*; pub use ready::*; +pub use select_protocol::*; +use serde::{Deserialize, Serialize}; mod identify; mod ready; +mod select_protocol; + +/// The modes of encryption available in webrtc connections; +/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes; +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum WebrtcEncryptionMode { + XSalsa20Poly1305, + XSalsa20Poly1305Suffix, + XSalsa20Poly1305Lite, +} + +// The various voice opcodes +pub const VOICE_IDENTIFY: u8 = 0; +pub const VOICE_SELECT_PROTOCOL: u8 = 1; +pub const VOICE_READY: u8 = 2; +pub const VOICE_HEARTBEAT: u8 = 3; +pub const VOICE_SESSION_DESCRIPTION: u8 = 4; +pub const VOICE_SPEAKING: u8 = 5; +pub const VOICE_HEARTBEAT_ACK: u8 = 6; +pub const VOICE_RESUME: u8 = 7; +pub const VOICE_HELLO: u8 = 8; +pub const VOICE_RESUMED: u8 = 9; +pub const VOICE_CLIENT_DISCONENCT: u8 = 13; diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index c805593..ff57eae 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -3,15 +3,19 @@ use std::net::Ipv4Addr; use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; +use super::WebrtcEncryptionMode; + #[derive(Debug, Deserialize, Serialize, Clone)] /// The ready event for the webrtc stream; /// Used to give info after the identify event; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; pub struct VoiceReady { - ssrc: u8, + /// See https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpStreamStats/ssrc + ssrc: i32, ip: Ipv4Addr, - port: u8, - modes: Vec, + port: u32, + /// The available encryption modes for the webrtc connection + modes: Vec, // Heartbeat interval is also sent, but is "an erroneous field and should be ignored. The correct heartbeat_interval value comes from the Hello payload." } diff --git a/src/types/events/webrtc/select_protocol.rs b/src/types/events/webrtc/select_protocol.rs new file mode 100644 index 0000000..0966cd8 --- /dev/null +++ b/src/types/events/webrtc/select_protocol.rs @@ -0,0 +1,25 @@ +use std::net::Ipv4Addr; + +use serde::{Deserialize, Serialize}; + +use super::WebrtcEncryptionMode; + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// An event sent by the client to the webrtc server, detailing what protocol, address and encryption to use; +/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-example-select-protocol-payload +pub struct SelectProtocol { + /// The protocol to use. The only option detailed in discord docs is "udp" + pub protocol: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +/// The data field of the SelectProtocol Event +/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-example-select-protocol-payload; +pub struct SelectProtocolData { + /// Our external ip + pub address: Ipv4Addr, + /// Our external udp port + pub port: u32, + /// The mode of encryption to use + pub mode: WebrtcEncryptionMode, +} From 9dc37c946986c6f53c8883570ba69671fe57594b Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:26:00 +0200 Subject: [PATCH 03/72] Attempt an untested voice gateway implementation --- src/errors.rs | 30 +- src/gateway.rs | 4 +- src/types/events/webrtc/mod.rs | 49 +- .../events/webrtc/session_description.rs | 14 + src/types/events/webrtc/speaking.rs | 35 ++ src/voice.rs | 568 ++++++++++++++++++ 6 files changed, 695 insertions(+), 5 deletions(-) create mode 100644 src/types/events/webrtc/session_description.rs create mode 100644 src/types/events/webrtc/speaking.rs diff --git a/src/errors.rs b/src/errors.rs index 057f57f..37c165c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -40,7 +40,7 @@ custom_error! { /// Supposed to be sent as numbers, though they are sent as string most of the time? /// /// Also includes errors when initiating a connection and unexpected opcodes - #[derive(PartialEq, Eq)] + #[derive(Clone, PartialEq, Eq)] pub GatewayError // Errors we have received from the gateway UnknownError = "We're not sure what went wrong. Try reconnecting?", @@ -65,3 +65,31 @@ custom_error! { // Other misc errors UnexpectedOpcodeReceivedError{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", } + +custom_error! { + // Like GatewayError for webrtc errors + // See https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice; + // Also supposed to be sent by numbers, but discord is asdfghgfjkkjldf when it comes to their errors + #[derive(Clone, PartialEq, Eq)] + pub VoiceGatewayError + // Errors we receive + UnknownOpcodeError = "You sent an invalid opcode", + FailedToDecodePayloadError = "You sent an invalid payload in your identifying to the (Webrtc) Gateway", + NotAuthenticatedError = "You sent a payload before identifying with the (Webrtc) Gateway", + AuthenticationFailedError = "The token you sent in your identify payload is incorrect", + AlreadyAuthenticatedError = "You sent more than one identify payload", + SessionNoLongerValidError = "Your session is no longer valid", + SessionTimeoutError = "Your session has timed out", + ServerNotFoundError = "We can't find the server you're trying to connect to", + UnknownProtocolError = "We didn't recognize the protocol you sent", + DisconnectedError = "Channel was deleted, you were kicked, voice server changed, or the main gateway session was dropped. Should not reconnect.", + VoiceServerCrashedError = "The server crashed, try resuming", + UnknownEncryptionModeError = "Server failed to decrypt data", + + // Errors when initiating a gateway connection + CannotConnectError{error: String} = "Cannot connect due to a tungstenite error: {error}", + NonHelloOnInitiateError{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong", + + // Other misc errors + UnexpectedOpcodeReceivedError{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}" +} diff --git a/src/gateway.rs b/src/gateway.rs index 2f90217..badf845 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -69,7 +69,7 @@ const GATEWAY_CALL_SYNC: u8 = 13; const GATEWAY_LAZY_REQUEST: u8 = 14; /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms -const HEARTBEAT_ACK_TIMEOUT: u128 = 2000; +pub const HEARTBEAT_ACK_TIMEOUT: u128 = 2000; /// Represents a messsage received from the gateway. This will be either a [GatewayReceivePayload], containing events, or a [GatewayError]. /// This struct is used internally when handling messages. @@ -1704,7 +1704,7 @@ impl GatewayEvent { } /// Notifies the observers of the GatewayEvent. - async fn notify(&self, new_event_data: T) { + pub async fn notify(&self, new_event_data: T) { for observer in &self.observers { observer.update(&new_event_data); } diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index 69d8e32..53735e0 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -1,17 +1,57 @@ +use super::WebSocketEvent; +use serde::{Deserialize, Serialize}; + pub use identify::*; pub use ready::*; pub use select_protocol::*; -use serde::{Deserialize, Serialize}; +pub use session_description::*; +pub use speaking::*; mod identify; mod ready; mod select_protocol; +mod session_description; +mod speaking; + +#[derive(Debug, Default, Serialize, Clone)] +/// The payload used for sending events to the webrtc gateway +/// Not tha this is very similar to the regular gateway, except we no longer have a sequence number +/// +/// Similar to [WebrtcReceivePayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] +/// Also, we never need to send the event name +pub struct VoiceGatewaySendPayload { + #[serde(rename = "op")] + pub op_code: u8, + + #[serde(rename = "d")] + pub data: serde_json::Value, +} + +impl WebSocketEvent for VoiceGatewaySendPayload {} + +#[derive(Debug, Deserialize, Clone)] +/// The payload used for receiving events from the webrtc gateway +/// Note that this is very similar to the regular gateway, except we no longer have s or t +/// +/// Similar to [WebrtcSendPayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] +/// Also, we never need to sent the event name +pub struct VoiceGatewayReceivePayload<'a> { + #[serde(rename = "op")] + pub op_code: u8, + + #[serde(borrow)] + #[serde(rename = "d")] + pub data: &'a serde_json::value::RawValue, +} + +impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} /// The modes of encryption available in webrtc connections; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes; -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum WebrtcEncryptionMode { + #[default] XSalsa20Poly1305, XSalsa20Poly1305Suffix, XSalsa20Poly1305Lite, @@ -28,4 +68,9 @@ pub const VOICE_HEARTBEAT_ACK: u8 = 6; pub const VOICE_RESUME: u8 = 7; pub const VOICE_HELLO: u8 = 8; pub const VOICE_RESUMED: u8 = 9; +/// See https://discord-userdoccers.vercel.app/topics/opcodes-and-status-codes#voice-opcodes +pub const VOICE_VIDEO: u8 = 12; pub const VOICE_CLIENT_DISCONENCT: u8 = 13; +/// See https://discord-userdoccers.vercel.app/topics/opcodes-and-status-codes#voice-opcodes; +/// Sent with empty data from the client, the server responds with the voice backend version; +pub const VOICE_BACKEND_VERSION: u8 = 16; diff --git a/src/types/events/webrtc/session_description.rs b/src/types/events/webrtc/session_description.rs new file mode 100644 index 0000000..cda07fd --- /dev/null +++ b/src/types/events/webrtc/session_description.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use crate::types::WebSocketEvent; +use super::WebrtcEncryptionMode; + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +/// Event that describes our encryption mode and secret key for encryption +pub struct SessionDescription { + /// The encryption mode we're using in webrtc + pub mode: WebrtcEncryptionMode, + /// The secret key we'll use for encryption + pub secret_key: [u8; 32], +} + +impl WebSocketEvent for SessionDescription {} \ No newline at end of file diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/webrtc/speaking.rs new file mode 100644 index 0000000..3778266 --- /dev/null +++ b/src/types/events/webrtc/speaking.rs @@ -0,0 +1,35 @@ +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; + +/// Event that tells the server we are speaking; +/// Essentially, what allows us to send udp data and lights up the green circle around your avatar; +/// See https://discord.com/developers/docs/topics/voice-connections#speaking-example-speaking-payload +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct Speaking { + /// Data about the audio we're transmitting, its type + speaking: SpeakingBitflags, + /// Assuming delay in milliseconds for the audio, should be 0 most of the time + delay: u64, + ssrc: i32, +} + +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 { + /// Whether we'll be transmitting normal voice audio + const MICROPHONE = 1 << 0; + /// Whether we'll be transmitting context audio for video, no speaking indicator + const SOUNDSHARE = 1 << 1; + /// Whether we are a priority speaker, lowering audio of other speakers + const PRIORITY = 1 << 2; + } +} + +impl Default for SpeakingBitflags { + /// Returns the default value for these flags, assuming normal microphone audio and not being a priority speaker + fn default() -> Self { + Self::MICROPHONE + } +} diff --git a/src/voice.rs b/src/voice.rs index 8b13789..09fa3c4 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -1 +1,569 @@ +use futures_util::stream::{SplitSink, SplitStream}; +use futures_util::SinkExt; +use futures_util::StreamExt; +use native_tls::TlsConnector; +use serde_json::json; +use std::sync::Arc; +use tokio::net::TcpStream; +use tokio::sync::mpsc::error::TryRecvError; +use tokio::sync::mpsc::Sender; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tokio::time; +use tokio::time::Instant; +use tokio_tungstenite::MaybeTlsStream; +use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream}; +use crate::errors::VoiceGatewayError; +use crate::gateway::{GatewayEvent, HEARTBEAT_ACK_TIMEOUT}; +use crate::types::{ + self, SelectProtocol, Speaking, VoiceGatewayReceivePayload, VoiceGatewaySendPayload, + VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, + VOICE_HELLO, VOICE_IDENTIFY, VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, + VOICE_SESSION_DESCRIPTION, VOICE_SPEAKING, +}; + +/// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. +/// This struct is used internally when handling messages. +#[derive(Clone, Debug)] +pub struct VoiceGatewayMesssage { + /// The message we received from the server + message: tokio_tungstenite::tungstenite::Message, +} + +impl VoiceGatewayMesssage { + /// Creates self from a tungstenite message + pub fn from_tungstenite_message(message: tokio_tungstenite::tungstenite::Message) -> Self { + Self { message } + } + + /// Parses the message as an error; + /// Returns the error if succesfully parsed, None if the message isn't an error + pub fn error(&self) -> Option { + let content = self.message.to_string(); + + // Some error strings have dots on the end, which we don't care about + let processed_content = content.to_lowercase().replace('.', ""); + + match processed_content.as_str() { + "unknown opcode" | "4001" => Some(VoiceGatewayError::UnknownOpcodeError), + "decode error" | "failed to decode payload" | "4002" => { + Some(VoiceGatewayError::FailedToDecodePayloadError) + } + "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticatedError), + "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailedError), + "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticatedError), + "session no longer valid" | "4006" => { + Some(VoiceGatewayError::SessionNoLongerValidError) + } + "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeoutError), + "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFoundError), + "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocolError), + "disconnected" | "4014" => Some(VoiceGatewayError::DisconnectedError), + "voice server crashed" | "4015" => Some(VoiceGatewayError::VoiceServerCrashedError), + "unknown encryption mode" | "4016" => { + Some(VoiceGatewayError::UnknownEncryptionModeError) + } + _ => None, + } + } + + /// Returns whether or not the message is an error + pub fn is_error(&self) -> bool { + self.error().is_some() + } + + /// Parses the message as a payload; + /// Returns a result of deserializing + pub fn payload(&self) -> Result { + return serde_json::from_str(self.message.to_text().unwrap()); + } + + /// Returns whether or not the message is a payload + pub fn is_payload(&self) -> bool { + // close messages are never payloads, payloads are only text messages + if self.message.is_close() | !self.message.is_text() { + return false; + } + + return self.payload().is_ok(); + } + + /// Returns whether or not the message is empty + pub fn is_empty(&self) -> bool { + self.message.is_empty() + } +} + +/// Represents a handle to a Voice Gateway connection. +/// Using this handle you can send Gateway Events directly. +#[derive(Debug)] +pub struct VoiceGatewayHandle { + pub url: String, + pub events: Arc>, + pub websocket_send: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + pub handle: JoinHandle<()>, + /// Tells gateway tasks to close + kill_send: tokio::sync::broadcast::Sender<()>, +} + +impl VoiceGatewayHandle { + /// Sends json to the gateway with an opcode + async fn send_json(&self, op_code: u8, to_send: serde_json::Value) { + let gateway_payload = VoiceGatewaySendPayload { + op_code, + data: to_send, + }; + + let payload_json = serde_json::to_string(&gateway_payload).unwrap(); + + let message = tokio_tungstenite::tungstenite::Message::text(payload_json); + + self.websocket_send + .lock() + .await + .send(message) + .await + .unwrap(); + } + + /// Sends a voice identify event to the gateway + pub async fn send_identify(&self, to_send: VoiceIdentify) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + println!("VGW: Sending Identify.."); + + self.send_json(VOICE_IDENTIFY, to_send_value).await; + } + + /// Sends a select protocol event to the gateway + pub async fn send_select_protocol(&self, to_send: SelectProtocol) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + println!("VGW: Sending Select Protocol"); + + self.send_json(VOICE_SELECT_PROTOCOL, to_send_value).await; + } + + /// Sends a speaking event to the gateway + pub async fn send_speaking(&self, to_send: Speaking) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + println!("VGW: Sending Speaking"); + + self.send_json(VOICE_SPEAKING, to_send_value).await; + } + + /// Sends a voice backend version request to the gateway + pub async fn send_voice_backend_version_request(&self) { + let data_empty_object = json!("{}"); + + println!("VGW: Requesting voice backend version"); + + self.send_json(VOICE_BACKEND_VERSION, data_empty_object) + .await; + } + + /// Closes the websocket connection and stops all gateway tasks; + /// + /// Esentially pulls the plug on the voice gateway, leaving it possible to resume; + pub async fn close(&self) { + self.kill_send.send(()).unwrap(); + self.websocket_send.lock().await.close().await.unwrap(); + } +} +pub struct VoiceGateway { + pub events: Arc>, + heartbeat_handler: VoiceHeartbeatHandler, + pub websocket_send: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + pub websocket_receive: SplitStream>>, + kill_send: tokio::sync::broadcast::Sender<()>, +} + +impl VoiceGateway { + #[allow(clippy::new_ret_no_self)] + pub async fn new(websocket_url: String) -> Result { + // Append the needed things to the websocket url + let processed_url = format!("wss://{}?v=4", websocket_url); + + let (websocket_stream, _) = match connect_async_tls_with_config( + &processed_url, + None, + false, + Some(Connector::NativeTls( + TlsConnector::builder().build().unwrap(), + )), + ) + .await + { + Ok(websocket_stream) => websocket_stream, + Err(e) => { + return Err(VoiceGatewayError::CannotConnectError { + error: e.to_string(), + }) + } + }; + + let (websocket_send, mut websocket_receive) = websocket_stream.split(); + + let shared_websocket_send = Arc::new(Mutex::new(websocket_send)); + + // Create a shared broadcast channel for killing all gateway tasks + let (kill_send, mut _kill_receive) = tokio::sync::broadcast::channel::<()>(16); + + // 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 + let msg = websocket_receive.next().await.unwrap().unwrap(); + let gateway_payload: VoiceGatewayReceivePayload = + serde_json::from_str(msg.to_text().unwrap()).unwrap(); + + if gateway_payload.op_code != VOICE_HELLO { + return Err(VoiceGatewayError::NonHelloOnInitiateError { + opcode: gateway_payload.op_code, + }); + } + + println!("VGW: Received Hello"); + + // The hello data is the same on voice and normal gateway + let gateway_hello: types::HelloData = + serde_json::from_str(gateway_payload.data.get()).unwrap(); + + let voice_events = voice_events::VoiceEvents::default(); + let shared_events = Arc::new(Mutex::new(voice_events)); + + let mut gateway = VoiceGateway { + events: shared_events.clone(), + heartbeat_handler: VoiceHeartbeatHandler::new( + gateway_hello.heartbeat_interval, + 1, // to:do actually compute nonce + shared_websocket_send.clone(), + kill_send.subscribe(), + ), + websocket_send: shared_websocket_send.clone(), + websocket_receive, + kill_send: kill_send.clone(), + }; + + // Now we can continuously check for messages in a different task, since we aren't going to receive another hello + let handle: JoinHandle<()> = tokio::spawn(async move { + gateway.gateway_listen_task().await; + }); + + Ok(VoiceGatewayHandle { + url: websocket_url.clone(), + events: shared_events, + websocket_send: shared_websocket_send.clone(), + handle, + kill_send: kill_send.clone(), + }) + } + + /// The main gateway listener task; + /// + /// Can only be stopped by closing the websocket, cannot be made to listen for kill + pub async fn gateway_listen_task(&mut self) { + loop { + let msg = self.websocket_receive.next().await; + + if let Some(Ok(message)) = msg { + self.handle_message(VoiceGatewayMesssage::from_tungstenite_message(message)) + .await; + continue; + } + + // We couldn't receive the next message or it was an error, something is wrong with the websocket, close + println!("VGW: Websocket is broken, stopping gateway"); + break; + } + } + + /// Closes the websocket connection and stops all tasks + async fn close(&mut self) { + self.kill_send.send(()).unwrap(); + self.websocket_send.lock().await.close().await.unwrap(); + } + + /// Deserializes and updates a dispatched event, when we already know its type; + /// (Called for every event in handle_message) + async fn handle_event<'a, T: WebSocketEvent + serde::Deserialize<'a>>( + data: &'a str, + event: &mut GatewayEvent, + ) -> Result<(), serde_json::Error> { + let data_deserialize_result: Result = serde_json::from_str(data); + + if data_deserialize_result.is_err() { + return Err(data_deserialize_result.err().unwrap()); + } + + event.notify(data_deserialize_result.unwrap()).await; + Ok(()) + } + + /// This handles a message as a websocket event and updates its events along with the events' observers + pub async fn handle_message(&mut self, msg: VoiceGatewayMesssage) { + if msg.is_empty() { + return; + } + + if !msg.is_error() && !msg.is_payload() { + println!( + "Message unrecognised: {:?}, please open an issue on the chorus github", + msg.message.to_string() + ); + return; + } + + // To:do: handle errors in a good way, maybe observers like events? + if msg.is_error() { + println!("VGW: Received error, connection will close.."); + + let _error = msg.error(); + + {} + + self.close().await; + return; + } + + let gateway_payload = msg.payload().unwrap(); + + match gateway_payload.op_code { + VOICE_READY => { + let event = &mut self.events.lock().await.voice_ready; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + println!("Failed to parse VOICE_READY ({})", result.err().unwrap()); + return; + } + } + VOICE_SESSION_DESCRIPTION => { + let event = &mut self.events.lock().await.session_description; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + println!( + "Failed to parse VOICE_SELECT_PROTOCOL ({})", + result.err().unwrap() + ); + return; + } + } + // We received a heartbeat from the server + // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." + VOICE_HEARTBEAT => { + println!("VGW: Received Heartbeat // Heartbeat Request"); + + // Tell the heartbeat handler it should send a heartbeat right away + let heartbeat_communication = VoiceHeartbeatThreadCommunication { + updated_nonce: None, + op_code: Some(VOICE_HEARTBEAT), + }; + + self.heartbeat_handler + .send + .send(heartbeat_communication) + .await + .unwrap(); + } + VOICE_HEARTBEAT_ACK => { + println!("VGW: Received Heartbeat ACK"); + + // Tell the heartbeat handler we received an ack + + let heartbeat_communication = VoiceHeartbeatThreadCommunication { + updated_nonce: None, + op_code: Some(VOICE_HEARTBEAT_ACK), + }; + + self.heartbeat_handler + .send + .send(heartbeat_communication) + .await + .unwrap(); + } + VOICE_IDENTIFY | VOICE_SELECT_PROTOCOL | VOICE_RESUME => { + let error = VoiceGatewayError::UnexpectedOpcodeReceivedError { + opcode: gateway_payload.op_code, + }; + Err::<(), VoiceGatewayError>(error).unwrap(); + } + _ => { + println!("Received unrecognized voice gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); + } + } + } +} + +/// Handles sending heartbeats to the voice gateway in another thread +struct VoiceHeartbeatHandler { + /// The heartbeat interval in milliseconds + pub heartbeat_interval: u128, + /// The send channel for the heartbeat thread + pub send: Sender, + /// The handle of the thread + handle: JoinHandle<()>, +} + +impl VoiceHeartbeatHandler { + pub fn new( + heartbeat_interval: u128, + starting_nonce: u64, + websocket_tx: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + kill_rc: tokio::sync::broadcast::Receiver<()>, + ) -> Self { + let (send, receive) = tokio::sync::mpsc::channel(32); + let kill_receive = kill_rc.resubscribe(); + + let handle: JoinHandle<()> = tokio::spawn(async move { + Self::heartbeat_task( + websocket_tx, + heartbeat_interval, + starting_nonce, + receive, + kill_receive, + ) + .await; + }); + + Self { + heartbeat_interval, + send, + handle, + } + } + + /// The main heartbeat task; + /// + /// Can be killed by the kill broadcast; + /// If the websocket is closed, will die out next time it tries to send a heartbeat; + pub async fn heartbeat_task( + websocket_tx: Arc< + Mutex< + SplitSink< + WebSocketStream>, + tokio_tungstenite::tungstenite::Message, + >, + >, + >, + heartbeat_interval: u128, + starting_nonce: u64, + mut receive: tokio::sync::mpsc::Receiver, + mut kill_receive: tokio::sync::broadcast::Receiver<()>, + ) { + let mut last_heartbeat_timestamp: Instant = time::Instant::now(); + let mut last_heartbeat_acknowledged = true; + let mut nonce: u64 = starting_nonce; + + loop { + let should_shutdown = kill_receive.try_recv().is_ok(); + if should_shutdown { + break; + } + + let mut should_send; + + let time_to_send = last_heartbeat_timestamp.elapsed().as_millis() >= heartbeat_interval; + + should_send = time_to_send; + + let received_communication: Result = + receive.try_recv(); + if received_communication.is_ok() { + let communication = received_communication.unwrap(); + + // If we received a nonce update, use that nonce now + if communication.updated_nonce.is_some() { + nonce = communication.updated_nonce.unwrap(); + } + + if communication.op_code.is_some() { + match communication.op_code.unwrap() { + VOICE_HEARTBEAT => { + // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately + should_send = true; + } + VOICE_HEARTBEAT_ACK => { + // The server received our heartbeat + last_heartbeat_acknowledged = true; + } + _ => {} + } + } + } + + // If the server hasn't acknowledged our heartbeat we should resend it + if !last_heartbeat_acknowledged + && last_heartbeat_timestamp.elapsed().as_millis() > HEARTBEAT_ACK_TIMEOUT + { + should_send = true; + println!("VGW: Timed out waiting for a heartbeat ack, resending"); + } + + if should_send { + println!("VGW: Sending Heartbeat.."); + + let heartbeat = VoiceGatewaySendPayload { + op_code: VOICE_HEARTBEAT, + data: nonce.into(), + }; + + let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); + + let msg = tokio_tungstenite::tungstenite::Message::text(heartbeat_json); + + let send_result = websocket_tx.lock().await.send(msg).await; + if send_result.is_err() { + // We couldn't send, the websocket is broken + println!("VGW: Couldnt send heartbeat, websocket seems broken"); + break; + } + + last_heartbeat_timestamp = time::Instant::now(); + last_heartbeat_acknowledged = false; + } + } + } +} + +/// Used for communications between the voice heartbeat and voice gateway thread. +/// Either signifies a nonce update, a heartbeat ACK or a Heartbeat request by the server +#[derive(Clone, Copy, Debug)] +struct VoiceHeartbeatThreadCommunication { + /// The opcode for the communication we received, if relevant + op_code: Option, + /// The new nonce to use, if any + updated_nonce: Option, +} + +mod voice_events { + use crate::types::{SessionDescription, VoiceReady}; + + use super::*; + + #[derive(Default, Debug)] + pub struct VoiceEvents { + pub voice_ready: GatewayEvent, + pub session_description: GatewayEvent, + } +} From 5e037121cdce6c10c17a47920323faa8da84ef29 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Wed, 21 Jun 2023 14:26:52 +0200 Subject: [PATCH 04/72] fmt --- src/types/events/webrtc/session_description.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/events/webrtc/session_description.rs b/src/types/events/webrtc/session_description.rs index cda07fd..dfef1eb 100644 --- a/src/types/events/webrtc/session_description.rs +++ b/src/types/events/webrtc/session_description.rs @@ -1,6 +1,6 @@ -use serde::{Deserialize, Serialize}; -use crate::types::WebSocketEvent; use super::WebrtcEncryptionMode; +use crate::types::WebSocketEvent; +use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] /// Event that describes our encryption mode and secret key for encryption @@ -11,4 +11,4 @@ pub struct SessionDescription { pub secret_key: [u8; 32], } -impl WebSocketEvent for SessionDescription {} \ No newline at end of file +impl WebSocketEvent for SessionDescription {} From 89d4348498ac80a12c36de557c45c04db4339834 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 28 Jul 2023 17:33:23 +0200 Subject: [PATCH 05/72] Merge with main --- .github/workflows/build_and_test.yml | 14 +- .github/workflows/clippy.yml | 20 - .github/workflows/rust-clippy.yml | 53 + .gitignore | 13 +- Cargo.lock | 2561 +++++++++++++++++ Cargo.toml | 30 +- README.md | 6 +- chorus-macros/Cargo.lock | 46 + chorus-macros/Cargo.toml | 11 + chorus-macros/src/lib.rs | 18 + examples/gateway_observers.rs | 4 +- src/api/auth/login.rs | 68 +- src/api/auth/register.rs | 67 +- src/api/channels/channels.rs | 159 +- src/api/channels/messages.rs | 75 +- src/api/channels/permissions.rs | 35 +- src/api/channels/reactions.rs | 221 +- src/api/common.rs | 72 - src/api/guilds/guilds.rs | 161 +- src/api/guilds/member.rs | 33 +- src/api/guilds/roles.rs | 78 +- src/api/invites/mod.rs | 73 + src/api/mod.rs | 7 +- src/api/policies/instance/instance.rs | 15 +- src/api/policies/instance/limits.rs | 499 ---- src/api/policies/instance/mod.rs | 4 +- src/api/policies/instance/ratelimits.rs | 37 + src/api/policies/mod.rs | 2 +- src/api/users/channels.rs | 32 + src/api/users/guilds.rs | 30 + src/api/users/mod.rs | 4 + src/api/users/relationships.rs | 82 +- src/api/users/users.rs | 86 +- src/errors.rs | 81 +- src/gateway.rs | 1353 ++------- src/instance.rs | 97 +- src/lib.rs | 2 +- src/limit.rs | 304 -- src/ratelimiter.rs | 466 +++ .../config/types/general_configuration.rs | 6 +- src/types/config/types/guild_configuration.rs | 4 +- .../config/types/subconfigs/limits/rates.rs | 24 +- src/types/entities/channel.rs | 90 +- src/types/entities/emoji.rs | 2 +- src/types/entities/guild.rs | 2 +- src/types/entities/invite.rs | 75 + src/types/entities/mod.rs | 2 + src/types/entities/user.rs | 11 +- src/types/events/channel.rs | 11 + src/types/events/hello.rs | 3 +- src/types/events/mod.rs | 24 +- src/types/schema/auth.rs | 228 +- src/types/schema/channel.rs | 62 +- src/types/schema/mod.rs | 73 - src/types/schema/user.rs | 18 + src/types/utils/rights.rs | 1 + src/types/utils/snowflake.rs | 2 +- tests/auth.rs | 12 +- tests/{channel.rs => channels.rs} | 90 +- tests/common/mod.rs | 49 +- tests/gateway.rs | 30 +- tests/{guild.rs => guilds.rs} | 0 tests/invites.rs | 23 + tests/{member.rs => members.rs} | 0 tests/{message.rs => messages.rs} | 12 +- tests/relationships.rs | 60 +- 66 files changed, 4743 insertions(+), 3090 deletions(-) delete mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/rust-clippy.yml create mode 100644 Cargo.lock create mode 100644 chorus-macros/Cargo.lock create mode 100644 chorus-macros/Cargo.toml create mode 100644 chorus-macros/src/lib.rs delete mode 100644 src/api/common.rs create mode 100644 src/api/invites/mod.rs delete mode 100644 src/api/policies/instance/limits.rs create mode 100644 src/api/policies/instance/ratelimits.rs create mode 100644 src/api/users/channels.rs create mode 100644 src/api/users/guilds.rs delete mode 100644 src/limit.rs create mode 100644 src/ratelimiter.rs create mode 100644 src/types/entities/invite.rs rename tests/{channel.rs => channels.rs} (52%) rename tests/{guild.rs => guilds.rs} (100%) create mode 100644 tests/invites.rs rename tests/{member.rs => members.rs} (100%) rename tests/{message.rs => messages.rs} (82%) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 4e98355..166d4c8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -15,20 +15,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Install dependencies + - uses: actions/checkout@v3 + - name: Clone spacebar server run: | - sudo apt-get update - sudo apt-get install -y git python3 build-essential - curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - - sudo apt-get install -y nodejs git clone https://github.com/bitfl0wer/server.git + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'npm' + cache-dependency-path: server/package-lock.json - name: Prepare and start Spacebar server run: | npm install npm run setup npm run start & working-directory: ./server + - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --verbose - name: Run tests diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index ba12407..0000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Clippy check - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - - -# Make sure CI fails on all warnings, including Clippy lints -env: - RUSTFLAGS: "-Dwarnings" - -jobs: - clippy_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Run Clippy - run: cargo clippy --all-targets --all-features \ No newline at end of file diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml new file mode 100644 index 0000000..dbbba76 --- /dev/null +++ b/.github/workflows/rust-clippy.yml @@ -0,0 +1,53 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# rust-clippy is a tool that runs a bunch of lints to catch common +# mistakes in your Rust code and help improve your Rust code. +# More details at https://github.com/rust-lang/rust-clippy +# and https://rust-lang.github.io/rust-clippy/ + +name: rust-clippy analyze + +on: + push: + branches: [ "main", "preserve/*" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + +jobs: + rust-clippy-analyze: + name: Run rust-clippy analyzing + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + + - name: Install required cargo + run: cargo install clippy-sarif sarif-fmt + + - name: Run rust-clippy + run: + cargo clippy + --all-features + --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + continue-on-error: true + + - name: Upload analysis results to GitHub + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: rust-clippy-results.sarif + wait-for-processing: true diff --git a/.gitignore b/.gitignore index ef337e7..d3170e2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,19 +2,18 @@ # will have compiled files and executables /target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk - # Added by cargo /target -### +# IDE specific folders and configs .vscode/** -.idea/** \ No newline at end of file +.idea/** + +# macOS + +**/.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d193cf4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2561 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chorus" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.21.2", + "bitflags 2.3.3", + "chorus-macros", + "chrono", + "custom_error", + "futures-util", + "hostname", + "http", + "jsonwebtoken", + "lazy_static", + "log", + "native-tls", + "openssl", + "poem", + "regex", + "reqwest", + "rusty-hook", + "serde", + "serde-aux", + "serde_json", + "serde_repr", + "serde_with", + "sqlx", + "thiserror", + "tokio", + "tokio-tungstenite", + "url", +] + +[[package]] +name = "chorus-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn 2.0.26", +] + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "ci_info" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f638c70e8c5753795cc9a8c07c44da91554a09e4cf11a7326e8161b0a3c45e" +dependencies = [ + "envmnt", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "custom_error" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.26", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", + "crypto-bigint", + "pem-rfc7468", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "envmnt" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d328fc287c61314c4a61af7cfdcbd7e678e39778488c7cb13ec133ce0f4059" +dependencies = [ + "fsio", + "indexmap 1.9.3", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsio" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3" + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", +] + +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "ipnetwork" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f84f1612606f3753f205a4e9a2efd6fe5b4c573a6269b2cc6c3003d44a0d127" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.2", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nias" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "pem-rfc7468" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +dependencies = [ + "der", + "pkcs8", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der", + "spki", + "zeroize", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "poem" +version = "1.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a56df40b79ebdccf7986b337f9b0e51ac55cd5e9d21fb20b6aa7c7d49741854" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "mime", + "parking_lot 0.12.1", + "percent-encoding", + "pin-project-lite", + "poem-derive", + "regex", + "rfc7239", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "thiserror", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "poem-derive" +version = "1.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1701f977a2d650a03df42c053686ea0efdb83554f34c7b026b89383c0a1b7846" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rfc7239" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" +dependencies = [ + "uncased", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rusty-hook" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96cee9be61be7e1cbadd851e58ed7449c29c620f00b23df937cb9cbc04ac21a3" +dependencies = [ + "ci_info", + "getopts", + "nias", + "toml", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3dfe1b7eb6f9dcf011bd6fad169cdeaae75eda0d61b1a99a3f015b41b0cae39" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "serde_derive" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.23", +] + +[[package]] +name = "serde_with_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.23", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.3" +source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +dependencies = [ + "ahash 0.7.6", + "atoi", + "bitflags 1.3.2", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "digest", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "generic-array", + "hashlink", + "hex", + "indexmap 1.9.3", + "ipnetwork", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "num-bigint", + "once_cell", + "paste", + "percent-encoding", + "rand", + "rsa", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +dependencies = [ + "dotenvy", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "git+https://github.com/zert3x/sqlx?branch=feature/skip#b417a65842442b177e2abb39f3940a7c95265d90" +dependencies = [ + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +dependencies = [ + "autocfg", + "cfg-if", + "fastrand", + "redox_syscall 0.3.5", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tungstenite" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.26", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index cece3e2..8dbb172 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,31 +10,35 @@ backend = ["poem", "sqlx"] client = [] [dependencies] -tokio = {version = "1.28.1", features = ["rt", "macros", "rt-multi-thread", "full"]} -serde = {version = "1.0.163", features = ["derive"]} -serde_json = {version= "1.0.96", features = ["raw_value"]} +tokio = {version = "1.29.1", features = ["macros"]} +serde = {version = "1.0.171", features = ["derive"]} +serde_json = {version= "1.0.103", features = ["raw_value"]} serde-aux = "4.2.0" serde_with = "3.0.0" -serde_repr = "0.1.12" -reqwest = {version = "0.11.16", features = ["multipart"]} -url = "2.3.1" -chrono = {version = "0.4.24", features = ["serde"]} -regex = "1.7.3" +serde_repr = "0.1.14" +reqwest = {version = "0.11.18", features = ["multipart"]} +url = "2.4.0" +chrono = {version = "0.4.26", features = ["serde"]} +regex = "1.9.1" custom_error = "1.9.2" native-tls = "0.2.11" tokio-tungstenite = {version = "0.19.0", features = ["native-tls"]} futures-util = "0.3.28" http = "0.2.9" -openssl = "0.10.52" +openssl = "0.10.55" base64 = "0.21.2" hostname = "0.3.1" -bitflags = { version = "2.2.1", features = ["serde"] } +bitflags = { version = "2.3.3", features = ["serde"] } lazy_static = "1.4.0" -poem = { version = "1.3.55", optional = true } +poem = { version = "1.3.56", optional = true } sqlx = { git = "https://github.com/zert3x/sqlx", branch="feature/skip", features = ["mysql", "sqlite", "json", "chrono", "ipnetwork", "runtime-tokio-native-tls", "any"], optional = true } -thiserror = "1.0.40" +thiserror = "1.0.43" jsonwebtoken = "8.3.0" +log = "0.4.19" +async-trait = "0.1.71" +chorus-macros = {path = "chorus-macros"} [dev-dependencies] +tokio = {version = "1.29.1", features = ["full"]} lazy_static = "1.4.0" -rusty-hook = "0.11.2" \ No newline at end of file +rusty-hook = "0.11.2" diff --git a/README.md b/README.md index 9a75a54..9292a46 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ accepted, if it violates these guidelines or [our Code of Conduct](https://githu - [x] Channel creation - [x] Channel deletion - [x] [Channel management (name, description, icon, etc.)](https://github.com/polyphony-chat/chorus/issues/48) - - [ ] [Join and Leave Guilds](https://github.com/polyphony-chat/chorus/issues/45) - - [ ] [Start DMs](https://github.com/polyphony-chat/chorus/issues/45) - - [ ] [Group DM creation, deletion and member management](https://github.com/polyphony-chat/chorus/issues/89) + - [x] [Join and Leave Guilds](https://github.com/polyphony-chat/chorus/issues/45) + - [x] [Start DMs](https://github.com/polyphony-chat/chorus/issues/45) + - [x] [Group DM creation, deletion and member management](https://github.com/polyphony-chat/chorus/issues/89) - [ ] [Deleting messages](https://github.com/polyphony-chat/chorus/issues/91) - [ ] [Message threads](https://github.com/polyphony-chat/chorus/issues/90) - [x] [Reactions](https://github.com/polyphony-chat/chorus/issues/85) diff --git a/chorus-macros/Cargo.lock b/chorus-macros/Cargo.lock new file mode 100644 index 0000000..4aa96d3 --- /dev/null +++ b/chorus-macros/Cargo.lock @@ -0,0 +1,46 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "chorus-macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" diff --git a/chorus-macros/Cargo.toml b/chorus-macros/Cargo.toml new file mode 100644 index 0000000..cffffe1 --- /dev/null +++ b/chorus-macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "chorus-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = "1" +syn = "2" diff --git a/chorus-macros/src/lib.rs b/chorus-macros/src/lib.rs new file mode 100644 index 0000000..c8d5e87 --- /dev/null +++ b/chorus-macros/src/lib.rs @@ -0,0 +1,18 @@ +use proc_macro::TokenStream; +use quote::quote; + +#[proc_macro_derive(Updateable)] +pub fn updateable_macro_derive(input: TokenStream) -> TokenStream { + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + + let name = &ast.ident; + // No need for macro hygiene, we're only using this in chorus + quote! { + impl Updateable for #name { + fn id(&self) -> Snowflake { + self.id + } + } + } + .into() +} diff --git a/examples/gateway_observers.rs b/examples/gateway_observers.rs index d8762e0..1572aa9 100644 --- a/examples/gateway_observers.rs +++ b/examples/gateway_observers.rs @@ -1,3 +1,4 @@ +use async_trait::async_trait; use chorus::{ self, gateway::{Gateway, Observer}, @@ -15,9 +16,10 @@ pub struct ExampleObserver {} // This struct can observe GatewayReady events when subscribed, because it implements the trait Observer. // The Observer trait can be implemented for a struct for a given websocketevent to handle observing it // One struct can be an observer of multiple websocketevents, if needed +#[async_trait] impl Observer for ExampleObserver { // After we subscribe to an event this function is called every time we receive it - fn update(&self, _data: &GatewayReady) { + async fn update(&self, _data: &GatewayReady) { println!("Observed Ready!"); } } diff --git a/src/api/auth/login.rs b/src/api/auth/login.rs index 9670d15..68604d9 100644 --- a/src/api/auth/login.rs +++ b/src/api/auth/login.rs @@ -2,64 +2,48 @@ use std::cell::RefCell; use std::rc::Rc; use reqwest::Client; -use serde_json::{from_str, json}; +use serde_json::to_string; -use crate::api::limits::LimitType; -use crate::errors::{ChorusLibError, ChorusResult}; +use crate::api::LimitType; +use crate::errors::ChorusResult; +use crate::gateway::Gateway; use crate::instance::{Instance, UserMeta}; -use crate::limit::LimitedRequester; -use crate::types::{ErrorResponse, LoginResult, LoginSchema}; +use crate::ratelimiter::ChorusRequest; +use crate::types::{GatewayIdentifyPayload, LoginResult, LoginSchema}; impl Instance { pub async fn login_account(&mut self, login_schema: &LoginSchema) -> ChorusResult { - let json_schema = json!(login_schema); - let client = Client::new(); let endpoint_url = self.urls.api.clone() + "/auth/login"; - let request_builder = client.post(endpoint_url).body(json_schema.to_string()); + let chorus_request = ChorusRequest { + request: Client::new() + .post(endpoint_url) + .body(to_string(login_schema).unwrap()), + limit_type: LimitType::AuthLogin, + }; // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since login is an instance wide limit), which is why we are just cloning the // instances' limits to pass them on as user_rate_limits later. - let mut cloned_limits = self.limits.clone(); - let response = LimitedRequester::send_request( - request_builder, - LimitType::AuthRegister, - self, - &mut cloned_limits, - ) - .await; - if response.is_err() { - return Err(ChorusLibError::NoResponse); + let mut shell = + UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await; + let login_result = chorus_request + .deserialize_response::(&mut shell) + .await?; + let object = self.get_user(login_result.token.clone(), None).await?; + if self.limits_information.is_some() { + self.limits_information.as_mut().unwrap().ratelimits = shell.limits.clone().unwrap(); } - - let response_unwrap = response.unwrap(); - let status = response_unwrap.status(); - let response_text_string = response_unwrap.text().await.unwrap(); - if status.is_client_error() { - let json: ErrorResponse = serde_json::from_str(&response_text_string).unwrap(); - let error_type = json.errors.errors.iter().next().unwrap().0.to_owned(); - let mut error = "".to_string(); - for (_, value) in json.errors.errors.iter() { - for error_item in value._errors.iter() { - error += &(error_item.message.to_string() + " (" + &error_item.code + ")"); - } - } - return Err(ChorusLibError::InvalidFormBodyError { error_type, error }); - } - - let cloned_limits = self.limits.clone(); - let login_result: LoginResult = from_str(&response_text_string).unwrap(); - let object = self - .get_user(login_result.token.clone(), None) - .await - .unwrap(); + let mut identify = GatewayIdentifyPayload::common(); + let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); + identify.token = login_result.token.clone(); + gateway.send_identify(identify).await; let user = UserMeta::new( Rc::new(RefCell::new(self.clone())), login_result.token, - cloned_limits, + self.clone_limits_if_some(), login_result.settings, object, + gateway, ); - Ok(user) } } diff --git a/src/api/auth/register.rs b/src/api/auth/register.rs index b22d6ce..e818d82 100644 --- a/src/api/auth/register.rs +++ b/src/api/auth/register.rs @@ -1,14 +1,16 @@ use std::{cell::RefCell, rc::Rc}; use reqwest::Client; -use serde_json::{from_str, json}; +use serde_json::to_string; +use crate::gateway::Gateway; +use crate::types::GatewayIdentifyPayload; use crate::{ - api::limits::LimitType, - errors::{ChorusLibError, ChorusResult}, + api::policies::instance::LimitType, + errors::ChorusResult, instance::{Instance, Token, UserMeta}, - limit::LimitedRequester, - types::{ErrorResponse, RegisterSchema}, + ratelimiter::ChorusRequest, + types::RegisterSchema, }; impl Instance { @@ -25,51 +27,38 @@ impl Instance { &mut self, register_schema: &RegisterSchema, ) -> ChorusResult { - let json_schema = json!(register_schema); - let client = Client::new(); let endpoint_url = self.urls.api.clone() + "/auth/register"; - let request_builder = client.post(endpoint_url).body(json_schema.to_string()); + let chorus_request = ChorusRequest { + request: Client::new() + .post(endpoint_url) + .body(to_string(register_schema).unwrap()), + limit_type: LimitType::AuthRegister, + }; // We do not have a user yet, and the UserRateLimits will not be affected by a login // request (since register is an instance wide limit), which is why we are just cloning // the instances' limits to pass them on as user_rate_limits later. - let mut cloned_limits = self.limits.clone(); - let response = LimitedRequester::send_request( - request_builder, - LimitType::AuthRegister, - self, - &mut cloned_limits, - ) - .await; - if response.is_err() { - return Err(ChorusLibError::NoResponse); - } - - let response_unwrap = response.unwrap(); - let status = response_unwrap.status(); - let response_unwrap_text = response_unwrap.text().await.unwrap(); - let token = from_str::(&response_unwrap_text).unwrap(); - let token = token.token; - if status.is_client_error() { - let json: ErrorResponse = serde_json::from_str(&token).unwrap(); - let error_type = json.errors.errors.iter().next().unwrap().0.to_owned(); - let mut error = "".to_string(); - for (_, value) in json.errors.errors.iter() { - for error_item in value._errors.iter() { - error += &(error_item.message.to_string() + " (" + &error_item.code + ")"); - } - } - return Err(ChorusLibError::InvalidFormBodyError { error_type, error }); + let mut shell = + UserMeta::shell(Rc::new(RefCell::new(self.clone())), "None".to_string()).await; + let token = chorus_request + .deserialize_response::(&mut shell) + .await? + .token; + if self.limits_information.is_some() { + self.limits_information.as_mut().unwrap().ratelimits = shell.limits.unwrap(); } let user_object = self.get_user(token.clone(), None).await.unwrap(); - let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self) - .await - .unwrap(); + let settings = UserMeta::get_settings(&token, &self.urls.api.clone(), self).await?; + let mut identify = GatewayIdentifyPayload::common(); + let gateway = Gateway::new(self.urls.wss.clone()).await.unwrap(); + identify.token = token.clone(); + gateway.send_identify(identify).await; let user = UserMeta::new( Rc::new(RefCell::new(self.clone())), token.clone(), - cloned_limits, + self.clone_limits_if_some(), settings, user_object, + gateway, ); Ok(user) } diff --git a/src/api/channels/channels.rs b/src/api/channels/channels.rs index cbe481c..df8e290 100644 --- a/src/api/channels/channels.rs +++ b/src/api/channels/channels.rs @@ -1,33 +1,25 @@ use reqwest::Client; use serde_json::to_string; +use crate::types::AddChannelRecipientSchema; use crate::{ - api::common, - errors::{ChorusLibError, ChorusResult}, + api::LimitType, + errors::{ChorusError, ChorusResult}, instance::UserMeta, + ratelimiter::ChorusRequest, types::{Channel, ChannelModifySchema, GetChannelMessagesSchema, Message, Snowflake}, }; impl Channel { pub async fn get(user: &mut UserMeta, channel_id: Snowflake) -> ChorusResult { - let url = user.belongs_to.borrow_mut().urls.api.clone(); - let request = Client::new() - .get(format!("{}/channels/{}/", url, channel_id)) - .bearer_auth(user.token()); - - let result = common::deserialize_response::( - request, - user, - crate::api::limits::LimitType::Channel, - ) - .await; - if result.is_err() { - return Err(ChorusLibError::RequestErrorError { - url: format!("{}/channels/{}/", url, channel_id), - error: result.err().unwrap().to_string(), - }); - } - Ok(result.unwrap()) + let url = user.belongs_to.borrow().urls.api.clone(); + let chorus_request = ChorusRequest { + request: Client::new() + .get(format!("{}/channels/{}/", url, channel_id)) + .bearer_auth(user.token()), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.deserialize_response::(user).await } /// Deletes a channel. @@ -44,15 +36,17 @@ impl Channel { /// /// A `Result` that contains a `ChorusLibError` if an error occurred during the request, or `()` if the request was successful. pub async fn delete(self, user: &mut UserMeta) -> ChorusResult<()> { - let request = Client::new() - .delete(format!( - "{}/channels/{}/", - user.belongs_to.borrow_mut().urls.api, - self.id - )) - .bearer_auth(user.token()); - common::handle_request_as_result(request, user, crate::api::limits::LimitType::Channel) - .await + let chorus_request = ChorusRequest { + request: Client::new() + .delete(format!( + "{}/channels/{}/", + user.belongs_to.borrow().urls.api, + self.id + )) + .bearer_auth(user.token()), + limit_type: LimitType::Channel(self.id), + }; + chorus_request.handle_request_as_result(user).await } /// Modifies a channel. @@ -70,43 +64,94 @@ impl Channel { /// /// A `Result` that contains a `Channel` object if the request was successful, or an `ChorusLibError` if an error occurred during the request. pub async fn modify( - &mut self, + &self, modify_data: ChannelModifySchema, channel_id: Snowflake, user: &mut UserMeta, - ) -> ChorusResult<()> { - let request = Client::new() - .patch(format!( - "{}/channels/{}/", - user.belongs_to.borrow().urls.api, - channel_id - )) - .bearer_auth(user.token()) - .body(to_string(&modify_data).unwrap()); - let new_channel = common::deserialize_response::( - request, - user, - crate::api::limits::LimitType::Channel, - ) - .await?; - let _ = std::mem::replace(self, new_channel); - Ok(()) + ) -> ChorusResult { + let chorus_request = ChorusRequest { + request: Client::new() + .patch(format!( + "{}/channels/{}/", + user.belongs_to.borrow().urls.api, + channel_id + )) + .bearer_auth(user.token()) + .body(to_string(&modify_data).unwrap()), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.deserialize_response::(user).await } pub async fn messages( range: GetChannelMessagesSchema, channel_id: Snowflake, user: &mut UserMeta, - ) -> Result, ChorusLibError> { - let request = Client::new() - .get(format!( - "{}/channels/{}/messages", - user.belongs_to.borrow().urls.api, - channel_id - )) - .bearer_auth(user.token()) - .query(&range); + ) -> Result, ChorusError> { + let chorus_request = ChorusRequest { + request: Client::new() + .get(format!( + "{}/channels/{}/messages", + user.belongs_to.borrow().urls.api, + channel_id + )) + .bearer_auth(user.token()) + .query(&range), + limit_type: Default::default(), + }; - common::deserialize_response::>(request, user, Default::default()).await + chorus_request + .deserialize_response::>(user) + .await + } + + /// # Reference: + /// Read: + pub async fn add_channel_recipient( + &self, + recipient_id: Snowflake, + user: &mut UserMeta, + add_channel_recipient_schema: Option, + ) -> ChorusResult<()> { + let mut request = Client::new() + .put(format!( + "{}/channels/{}/recipients/{}/", + user.belongs_to.borrow().urls.api, + self.id, + recipient_id + )) + .bearer_auth(user.token()); + if let Some(schema) = add_channel_recipient_schema { + request = request.body(to_string(&schema).unwrap()); + } + ChorusRequest { + request, + limit_type: LimitType::Channel(self.id), + } + .handle_request_as_result(user) + .await + } + + /// # Reference: + /// Read: + pub async fn remove_channel_recipient( + &self, + recipient_id: Snowflake, + user: &mut UserMeta, + ) -> ChorusResult<()> { + let request = Client::new() + .delete(format!( + "{}/channels/{}/recipients/{}/", + user.belongs_to.borrow().urls.api, + self.id, + recipient_id + )) + .bearer_auth(user.token()); + ChorusRequest { + request, + limit_type: LimitType::Channel(self.id), + } + .handle_request_as_result(user) + .await } } diff --git a/src/api/channels/messages.rs b/src/api/channels/messages.rs index c61756a..6beec7f 100644 --- a/src/api/channels/messages.rs +++ b/src/api/channels/messages.rs @@ -3,48 +3,39 @@ use http::HeaderMap; use reqwest::{multipart, Client}; use serde_json::to_string; -use crate::api::deserialize_response; +use crate::api::LimitType; use crate::instance::UserMeta; -use crate::types::{Message, MessageSendSchema, PartialDiscordFileAttachment, Snowflake}; +use crate::ratelimiter::ChorusRequest; +use crate::types::{Message, MessageSendSchema, Snowflake}; impl Message { - /** - Sends a message to the Spacebar server. - # Arguments - * `url_api` - The URL of the Spacebar server's API. - * `message` - The [`Message`] that will be sent to the Spacebar server. - * `limits_user` - The [`Limits`] of the user. - * `limits_instance` - The [`Limits`] of the instance. - * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. - # Errors - * [`ChorusLibError`] - If the message cannot be sent. - */ pub async fn send( user: &mut UserMeta, channel_id: Snowflake, - message: &mut MessageSendSchema, - files: Option>, - ) -> Result { + mut message: MessageSendSchema, + ) -> Result { let url_api = user.belongs_to.borrow().urls.api.clone(); - if files.is_none() { - let request = Client::new() - .post(format!("{}/channels/{}/messages/", url_api, channel_id)) - .bearer_auth(user.token()) - .body(to_string(message).unwrap()); - deserialize_response::(request, user, crate::api::limits::LimitType::Channel) - .await + if message.attachments.is_none() { + let chorus_request = ChorusRequest { + request: Client::new() + .post(format!("{}/channels/{}/messages/", url_api, channel_id)) + .bearer_auth(user.token()) + .body(to_string(&message).unwrap()), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.deserialize_response::(user).await } else { for (index, attachment) in message.attachments.iter_mut().enumerate() { attachment.get_mut(index).unwrap().set_id(index as i16); } let mut form = reqwest::multipart::Form::new(); - let payload_json = to_string(message).unwrap(); + let payload_json = to_string(&message).unwrap(); let payload_field = reqwest::multipart::Part::text(payload_json); form = form.part("payload_json", payload_field); - for (index, attachment) in files.unwrap().into_iter().enumerate() { + for (index, attachment) in message.attachments.unwrap().into_iter().enumerate() { let (attachment_content, current_attachment) = attachment.move_content(); let (attachment_filename, _) = current_attachment.move_filename(); let part_name = format!("files[{}]", index); @@ -62,36 +53,24 @@ impl Message { form = form.part(part_name, part); } - let request = Client::new() - .post(format!("{}/channels/{}/messages/", url_api, channel_id)) - .bearer_auth(user.token()) - .multipart(form); - - deserialize_response::(request, user, crate::api::limits::LimitType::Channel) - .await + let chorus_request = ChorusRequest { + request: Client::new() + .post(format!("{}/channels/{}/messages/", url_api, channel_id)) + .bearer_auth(user.token()) + .multipart(form), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.deserialize_response::(user).await } } } impl UserMeta { - /// Shorthand call for Message::send() - /** - Sends a message to the Spacebar server. - # Arguments - * `url_api` - The URL of the Spacebar server's API. - * `message` - The [`Message`] that will be sent to the Spacebar server. - * `limits_user` - The [`Limits`] of the user. - * `limits_instance` - The [`Limits`] of the instance. - * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. - # Errors - * [`ChorusLibError`] - If the message cannot be sent. - */ pub async fn send_message( &mut self, - message: &mut MessageSendSchema, + message: MessageSendSchema, channel_id: Snowflake, - files: Option>, - ) -> Result { - Message::send(self, channel_id, message, files).await + ) -> Result { + Message::send(self, channel_id, message).await } } diff --git a/src/api/channels/permissions.rs b/src/api/channels/permissions.rs index 0958821..bc666ff 100644 --- a/src/api/channels/permissions.rs +++ b/src/api/channels/permissions.rs @@ -2,9 +2,10 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::handle_request_as_result, - errors::{ChorusLibError, ChorusResult}, + api::LimitType, + errors::{ChorusError, ChorusResult}, instance::UserMeta, + ratelimiter::ChorusRequest, types::{self, PermissionOverwrite, Snowflake}, }; @@ -25,24 +26,25 @@ impl types::Channel { channel_id: Snowflake, overwrite: PermissionOverwrite, ) -> ChorusResult<()> { - let url = { - format!( - "{}/channels/{}/permissions/{}", - user.belongs_to.borrow_mut().urls.api, - channel_id, - overwrite.id - ) - }; + let url = format!( + "{}/channels/{}/permissions/{}", + user.belongs_to.borrow_mut().urls.api, + channel_id, + overwrite.id + ); let body = match to_string(&overwrite) { Ok(string) => string, Err(e) => { - return Err(ChorusLibError::FormCreationError { + return Err(ChorusError::FormCreation { error: e.to_string(), }); } }; - let request = Client::new().put(url).bearer_auth(user.token()).body(body); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().put(url).bearer_auth(user.token()).body(body), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.handle_request_as_result(user).await } /// Deletes a permission overwrite for a channel. @@ -67,7 +69,10 @@ impl types::Channel { channel_id, overwrite_id ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(channel_id), + }; + chorus_request.handle_request_as_result(user).await } } diff --git a/src/api/channels/reactions.rs b/src/api/channels/reactions.rs index c445a8b..35dbb94 100644 --- a/src/api/channels/reactions.rs +++ b/src/api/channels/reactions.rs @@ -1,10 +1,11 @@ use reqwest::Client; use crate::{ - api::handle_request_as_result, + api::LimitType, errors::ChorusResult, instance::UserMeta, - types::{self, Snowflake}, + ratelimiter::ChorusRequest, + types::{self, PublicUser, Snowflake}, }; /** @@ -16,20 +17,15 @@ pub struct ReactionMeta { } impl ReactionMeta { - /** - Deletes all reactions for a message. - This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user. - - # Arguments - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong. - Fires a `Message Reaction Remove All` Gateway event. - - # Reference - See [https://discord.com/developers/docs/resources/channel#delete-all-reactions](https://discord.com/developers/docs/resources/channel#delete-all-reactions) - */ + /// Deletes all reactions for a message. + /// This endpoint requires the `MANAGE_MESSAGES` permission to be present on the current user. + /// # Arguments + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A `Result` [`()`] [`crate::errors::ChorusLibError`] if something went wrong. + /// Fires a `Message Reaction Remove All` Gateway event. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions](https://discord.com/developers/docs/resources/channel#delete-all-reactions) pub async fn delete_all(&self, user: &mut UserMeta) -> ChorusResult<()> { let url = format!( "{}/channels/{}/messages/{}/reactions/", @@ -37,26 +33,24 @@ impl ReactionMeta { self.channel_id, self.message_id ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request.handle_request_as_result(user).await } - /** - Gets a list of users that reacted with a specific emoji to a message. - - # Arguments - * `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or - the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - format name:id with the emoji name and emoji id. - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. - - # Reference - See [https://discord.com/developers/docs/resources/channel#get-reactions](https://discord.com/developers/docs/resources/channel#get-reactions) - */ - pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { + /// Gets a list of users that reacted with a specific emoji to a message. + /// # Arguments + /// * `emoji` - A string slice containing the emoji to search for. The emoji must be URL Encoded or + /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the + /// format name:id with the emoji name and emoji id. + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#get-reactions](https://discord.com/developers/docs/resources/channel#get-reactions) + pub async fn get(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult> { let url = format!( "{}/channels/{}/messages/{}/reactions/{}/", user.belongs_to.borrow().urls.api, @@ -64,27 +58,27 @@ impl ReactionMeta { self.message_id, emoji ); - let request = Client::new().get(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request + .deserialize_response::>(user) + .await } - /** - Deletes all the reactions for a given `emoji` on a message. This endpoint requires the - MANAGE_MESSAGES permission to be present on the current user. - - # Arguments - * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - format name:id with the emoji name and emoji id. - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. - Fires a `Message Reaction Remove Emoji` Gateway event. - - # Reference - See [https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji](https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji) - */ + /// Deletes all the reactions for a given `emoji` on a message. This endpoint requires the + /// MANAGE_MESSAGES permission to be present on the current user. + /// # Arguments + /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or + /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the + /// format name:id with the emoji name and emoji id. + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A Result that is [`Err(crate::errors::ChorusLibError)`] if something went wrong. + /// Fires a `Message Reaction Remove Emoji` Gateway event. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji](https://discord.com/developers/docs/resources/channel#delete-all-reactions-for-emoji) pub async fn delete_emoji(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { let url = format!( "{}/channels/{}/messages/{}/reactions/{}/", @@ -93,29 +87,28 @@ impl ReactionMeta { self.message_id, emoji ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request.handle_request_as_result(user).await } - /** - Create a reaction for the message. - - This endpoint requires the READ_MESSAGE_HISTORY permission - to be present on the current user. Additionally, if nobody else has reacted to the message using - this emoji, this endpoint requires the ADD_REACTIONS permission to be present on the current - user. - # Arguments - * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - format name:id with the emoji name and emoji id. - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - - # Reference - See [https://discord.com/developers/docs/resources/channel#create-reaction](https://discord.com/developers/docs/resources/channel#create-reaction) - */ + /// Create a reaction for the message. + /// This endpoint requires the READ_MESSAGE_HISTORY permission + /// to be present on the current user. Additionally, if nobody else has reacted to the message using + /// this emoji, this endpoint requires the ADD_REACTIONS permission to be present on the current + /// user. + /// # Arguments + /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or + /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the + /// format name:id with the emoji name and emoji id. + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#create-reaction](https://discord.com/developers/docs/resources/channel#create-reaction) + /// pub async fn create(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { let url = format!( "{}/channels/{}/messages/{}/reactions/{}/@me/", @@ -124,26 +117,24 @@ impl ReactionMeta { self.message_id, emoji ); - let request = Client::new().put(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().put(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request.handle_request_as_result(user).await } - /** - Delete a reaction the current user has made for the message. - - # Arguments - * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - format name:id with the emoji name and emoji id. - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - Fires a `Message Reaction Remove` Gateway event. - - # Reference - See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) - */ + /// Delete a reaction the current user has made for the message. + /// # Arguments + /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or + /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the + /// format name:id with the emoji name and emoji id. + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. + /// Fires a `Message Reaction Remove` Gateway event. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) pub async fn remove(&self, emoji: &str, user: &mut UserMeta) -> ChorusResult<()> { let url = format!( "{}/channels/{}/messages/{}/reactions/{}/@me/", @@ -152,29 +143,26 @@ impl ReactionMeta { self.message_id, emoji ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request.handle_request_as_result(user).await } - /** - Delete a user's reaction to a message. - - This endpoint requires the MANAGE_MESSAGES permission to be present on the current user. - - # Arguments - * `user_id` - ID of the user whose reaction is to be deleted. - * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or - the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the - format name:id with the emoji name and emoji id. - * `user` - A mutable reference to a [`UserMeta`] instance. - - # Returns - A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. - Fires a Message Reaction Remove Gateway event. - - # Reference - See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) - */ + /// Delete a user's reaction to a message. + /// This endpoint requires the MANAGE_MESSAGES permission to be present on the current user. + /// # Arguments + /// * `user_id` - ID of the user whose reaction is to be deleted. + /// * `emoji` - A string slice containing the emoji to delete. The `emoji` must be URL Encoded or + /// the request will fail with 10014: Unknown Emoji. To use custom emoji, you must encode it in the + /// format name:id with the emoji name and emoji id. + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// # Returns + /// A `Result` containing [`()`] or a [`crate::errors::ChorusLibError`]. + /// Fires a Message Reaction Remove Gateway event. + /// # Reference + /// See [https://discord.com/developers/docs/resources/channel#delete-own-reaction](https://discord.com/developers/docs/resources/channel#delete-own-reaction) pub async fn delete_user( &self, user_id: Snowflake, @@ -189,7 +177,10 @@ impl ReactionMeta { emoji, user_id ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Channel).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Channel(self.channel_id), + }; + chorus_request.handle_request_as_result(user).await } } diff --git a/src/api/common.rs b/src/api/common.rs deleted file mode 100644 index 86a972e..0000000 --- a/src/api/common.rs +++ /dev/null @@ -1,72 +0,0 @@ -use reqwest::RequestBuilder; -use serde::Deserialize; -use serde_json::from_str; - -use crate::{ - errors::{ChorusLibError, ChorusResult}, - instance::UserMeta, - limit::LimitedRequester, -}; - -use super::limits::LimitType; - -/// Sends a request to wherever it needs to go and performs some basic error handling. -pub async fn handle_request( - request: RequestBuilder, - user: &mut UserMeta, - limit_type: LimitType, -) -> Result { - LimitedRequester::send_request( - request, - limit_type, - &mut user.belongs_to.borrow_mut(), - &mut user.limits, - ) - .await -} - -/// Sends a request to wherever it needs to go. Returns [`Ok(())`] on success and -/// [`Err(ChorusLibError)`] on failure. -pub async fn handle_request_as_result( - request: RequestBuilder, - user: &mut UserMeta, - limit_type: LimitType, -) -> ChorusResult<()> { - match handle_request(request, user, limit_type).await { - Ok(_) => Ok(()), - Err(e) => Err(ChorusLibError::InvalidResponseError { - error: e.to_string(), - }), - } -} - -pub async fn deserialize_response Deserialize<'a>>( - request: RequestBuilder, - user: &mut UserMeta, - limit_type: LimitType, -) -> ChorusResult { - let response = handle_request(request, user, limit_type).await.unwrap(); - let response_text = match response.text().await { - Ok(string) => string, - Err(e) => { - return Err(ChorusLibError::InvalidResponseError { - error: format!( - "Error while trying to process the HTTP response into a String: {}", - e - ), - }); - } - }; - let object = match from_str::(&response_text) { - Ok(object) => object, - Err(e) => { - return Err(ChorusLibError::InvalidResponseError { - error: format!( - "Error while trying to deserialize the JSON response into T: {}", - e - ), - }) - } - }; - Ok(object) -} diff --git a/src/api/guilds/guilds.rs b/src/api/guilds/guilds.rs index ac00df1..3654698 100644 --- a/src/api/guilds/guilds.rs +++ b/src/api/guilds/guilds.rs @@ -2,15 +2,11 @@ use reqwest::Client; use serde_json::from_str; use serde_json::to_string; -use crate::api::deserialize_response; -use crate::api::handle_request; -use crate::api::handle_request_as_result; -use crate::api::limits::Limits; -use crate::errors::ChorusLibError; +use crate::api::LimitType; +use crate::errors::ChorusError; use crate::errors::ChorusResult; -use crate::instance::Instance; use crate::instance::UserMeta; -use crate::limit::LimitedRequester; +use crate::ratelimiter::ChorusRequest; use crate::types::Snowflake; use crate::types::{Channel, ChannelCreateSchema, Guild, GuildCreateSchema}; @@ -36,11 +32,14 @@ impl Guild { guild_create_schema: GuildCreateSchema, ) -> ChorusResult { let url = format!("{}/guilds/", user.belongs_to.borrow().urls.api); - let request = reqwest::Client::new() - .post(url.clone()) - .bearer_auth(user.token.clone()) - .body(to_string(&guild_create_schema).unwrap()); - deserialize_response::(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new() + .post(url.clone()) + .bearer_auth(user.token.clone()) + .body(to_string(&guild_create_schema).unwrap()), + limit_type: LimitType::Global, + }; + chorus_request.deserialize_response::(user).await } /// Deletes a guild. @@ -73,10 +72,13 @@ impl Guild { user.belongs_to.borrow().urls.api, guild_id ); - let request = reqwest::Client::new() - .post(url.clone()) - .bearer_auth(user.token.clone()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new() + .post(url.clone()) + .bearer_auth(user.token.clone()), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(user).await } /// Sends a request to create a new channel in the guild. @@ -97,14 +99,7 @@ impl Guild { user: &mut UserMeta, schema: ChannelCreateSchema, ) -> ChorusResult { - Channel::_create( - &user.token, - self.id, - schema, - &mut user.limits, - &mut user.belongs_to.borrow_mut(), - ) - .await + Channel::create(user, self.id, schema).await } /// Returns a `Result` containing a vector of `Channel` structs if the request was successful, or an `ChorusLibError` if there was an error. @@ -117,20 +112,21 @@ impl Guild { /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. /// pub async fn channels(&self, user: &mut UserMeta) -> ChorusResult> { - let request = Client::new() - .get(format!( - "{}/guilds/{}/channels/", - user.belongs_to.borrow().urls.api, - self.id - )) - .bearer_auth(user.token()); - let result = handle_request(request, user, crate::api::limits::LimitType::Channel) - .await - .unwrap(); + let chorus_request = ChorusRequest { + request: Client::new() + .get(format!( + "{}/guilds/{}/channels/", + user.belongs_to.borrow().urls.api, + self.id + )) + .bearer_auth(user.token()), + limit_type: LimitType::Channel(self.id), + }; + let result = chorus_request.send_request(user).await?; let stringed_response = match result.text().await { Ok(value) => value, Err(e) => { - return Err(ChorusLibError::InvalidResponseError { + return Err(ChorusError::InvalidResponse { error: e.to_string(), }); } @@ -138,7 +134,7 @@ impl Guild { let _: Vec = match from_str(&stringed_response) { Ok(result) => return Ok(result), Err(e) => { - return Err(ChorusLibError::InvalidResponseError { + return Err(ChorusError::InvalidResponse { error: e.to_string(), }); } @@ -155,35 +151,19 @@ impl Guild { /// * `limits_user` - A mutable reference to a `Limits` struct containing the user's rate limits. /// * `limits_instance` - A mutable reference to a `Limits` struct containing the instance's rate limits. /// - pub async fn get(user: &mut UserMeta, guild_id: Snowflake) -> ChorusResult { - let mut belongs_to = user.belongs_to.borrow_mut(); - Guild::_get(guild_id, &user.token, &mut user.limits, &mut belongs_to).await - } - - /// For internal use. Does the same as the public get method, but does not require a second, mutable - /// borrow of `UserMeta::belongs_to`, when used in conjunction with other methods, which borrow `UserMeta::belongs_to`. - async fn _get( - guild_id: Snowflake, - token: &str, - limits_user: &mut Limits, - instance: &mut Instance, - ) -> ChorusResult { - let request = Client::new() - .get(format!("{}/guilds/{}/", instance.urls.api, guild_id)) - .bearer_auth(token); - let response = match LimitedRequester::send_request( - request, - crate::api::limits::LimitType::Guild, - instance, - limits_user, - ) - .await - { - Ok(response) => response, - Err(e) => return Err(e), + pub async fn get(guild_id: Snowflake, user: &mut UserMeta) -> ChorusResult { + let chorus_request = ChorusRequest { + request: Client::new() + .get(format!( + "{}/guilds/{}/", + user.belongs_to.borrow().urls.api, + guild_id + )) + .bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), }; - let guild: Guild = from_str(&response.text().await.unwrap()).unwrap(); - Ok(guild) + let response = chorus_request.deserialize_response::(user).await?; + Ok(response) } } @@ -207,48 +187,17 @@ impl Channel { guild_id: Snowflake, schema: ChannelCreateSchema, ) -> ChorusResult { - let mut belongs_to = user.belongs_to.borrow_mut(); - Channel::_create( - &user.token, - guild_id, - schema, - &mut user.limits, - &mut belongs_to, - ) - .await - } - - async fn _create( - token: &str, - guild_id: Snowflake, - schema: ChannelCreateSchema, - limits_user: &mut Limits, - instance: &mut Instance, - ) -> ChorusResult { - let request = Client::new() - .post(format!( - "{}/guilds/{}/channels/", - instance.urls.api, guild_id - )) - .bearer_auth(token) - .body(to_string(&schema).unwrap()); - let result = match LimitedRequester::send_request( - request, - crate::api::limits::LimitType::Guild, - instance, - limits_user, - ) - .await - { - Ok(result) => result, - Err(e) => return Err(e), + let chorus_request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/guilds/{}/channels/", + user.belongs_to.borrow().urls.api, + guild_id + )) + .bearer_auth(user.token()) + .body(to_string(&schema).unwrap()), + limit_type: LimitType::Guild(guild_id), }; - match from_str::(&result.text().await.unwrap()) { - Ok(object) => Ok(object), - Err(e) => Err(ChorusLibError::RequestErrorError { - url: format!("{}/guilds/{}/channels/", instance.urls.api, guild_id), - error: e.to_string(), - }), - } + chorus_request.deserialize_response::(user).await } } diff --git a/src/api/guilds/member.rs b/src/api/guilds/member.rs index e110cfa..5fa99cd 100644 --- a/src/api/guilds/member.rs +++ b/src/api/guilds/member.rs @@ -1,9 +1,10 @@ use reqwest::Client; use crate::{ - api::{deserialize_response, handle_request_as_result}, + api::LimitType, errors::ChorusResult, instance::UserMeta, + ratelimiter::ChorusRequest, types::{self, Snowflake}, }; @@ -30,13 +31,13 @@ impl types::GuildMember { guild_id, member_id ); - let request = Client::new().get(url).bearer_auth(user.token()); - deserialize_response::( - request, - user, - crate::api::limits::LimitType::Guild, - ) - .await + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request + .deserialize_response::(user) + .await } /// Adds a role to a guild member. @@ -64,8 +65,11 @@ impl types::GuildMember { member_id, role_id ); - let request = Client::new().put(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new().put(url).bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request.handle_request_as_result(user).await } /// Removes a role from a guild member. @@ -85,7 +89,7 @@ impl types::GuildMember { guild_id: Snowflake, member_id: Snowflake, role_id: Snowflake, - ) -> Result<(), crate::errors::ChorusLibError> { + ) -> Result<(), crate::errors::ChorusError> { let url = format!( "{}/guilds/{}/members/{}/roles/{}/", user.belongs_to.borrow().urls.api, @@ -93,7 +97,10 @@ impl types::GuildMember { member_id, role_id ); - let request = Client::new().delete(url).bearer_auth(user.token()); - handle_request_as_result(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request.handle_request_as_result(user).await } } diff --git a/src/api/guilds/roles.rs b/src/api/guilds/roles.rs index 80dddad..1d1bc68 100644 --- a/src/api/guilds/roles.rs +++ b/src/api/guilds/roles.rs @@ -2,9 +2,10 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::deserialize_response, - errors::{ChorusLibError, ChorusResult}, + api::LimitType, + errors::{ChorusError, ChorusResult}, instance::UserMeta, + ratelimiter::ChorusRequest, types::{self, RoleCreateModifySchema, RoleObject, Snowflake}, }; @@ -32,14 +33,14 @@ impl types::RoleObject { user.belongs_to.borrow().urls.api, guild_id ); - let request = Client::new().get(url).bearer_auth(user.token()); - let roles = deserialize_response::>( - request, - user, - crate::api::limits::LimitType::Guild, - ) - .await - .unwrap(); + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), + }; + let roles = chorus_request + .deserialize_response::>(user) + .await + .unwrap(); if roles.is_empty() { return Ok(None); } @@ -72,8 +73,13 @@ impl types::RoleObject { guild_id, role_id ); - let request = Client::new().get(url).bearer_auth(user.token()); - deserialize_response(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(user.token()), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request + .deserialize_response::(user) + .await } /// Creates a new role for a given guild. @@ -102,12 +108,17 @@ impl types::RoleObject { guild_id ); let body = to_string::(&role_create_schema).map_err(|e| { - ChorusLibError::FormCreationError { + ChorusError::FormCreation { error: e.to_string(), } })?; - let request = Client::new().post(url).bearer_auth(user.token()).body(body); - deserialize_response(request, user, crate::api::limits::LimitType::Guild).await + let chorus_request = ChorusRequest { + request: Client::new().post(url).bearer_auth(user.token()).body(body), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request + .deserialize_response::(user) + .await } /// Updates the position of a role in the guild's hierarchy. @@ -135,16 +146,19 @@ impl types::RoleObject { user.belongs_to.borrow().urls.api, guild_id ); - let body = to_string(&role_position_update_schema).map_err(|e| { - ChorusLibError::FormCreationError { + let body = + to_string(&role_position_update_schema).map_err(|e| ChorusError::FormCreation { error: e.to_string(), - } - })?; - let request = Client::new() - .patch(url) - .bearer_auth(user.token()) - .body(body); - deserialize_response::(request, user, crate::api::limits::LimitType::Guild) + })?; + let chorus_request = ChorusRequest { + request: Client::new() + .patch(url) + .bearer_auth(user.token()) + .body(body), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request + .deserialize_response::(user) .await } @@ -177,15 +191,19 @@ impl types::RoleObject { role_id ); let body = to_string::(&role_create_schema).map_err(|e| { - ChorusLibError::FormCreationError { + ChorusError::FormCreation { error: e.to_string(), } })?; - let request = Client::new() - .patch(url) - .bearer_auth(user.token()) - .body(body); - deserialize_response::(request, user, crate::api::limits::LimitType::Guild) + let chorus_request = ChorusRequest { + request: Client::new() + .patch(url) + .bearer_auth(user.token()) + .body(body), + limit_type: LimitType::Guild(guild_id), + }; + chorus_request + .deserialize_response::(user) .await } } diff --git a/src/api/invites/mod.rs b/src/api/invites/mod.rs new file mode 100644 index 0000000..b766a5b --- /dev/null +++ b/src/api/invites/mod.rs @@ -0,0 +1,73 @@ +use reqwest::Client; +use serde_json::to_string; + +use crate::errors::ChorusResult; +use crate::instance::UserMeta; +use crate::ratelimiter::ChorusRequest; +use crate::types::{CreateChannelInviteSchema, GuildInvite, Invite, Snowflake}; + +impl UserMeta { + /// # Arguments + /// - invite_code: The invite code to accept the invite for. + /// - session_id: The session ID that is accepting the invite, required for guest invites. + /// + /// # Reference: + /// Read + pub async fn accept_invite( + &mut self, + invite_code: &str, + session_id: Option<&str>, + ) -> ChorusResult { + let mut request = ChorusRequest { + request: Client::new() + .post(format!( + "{}/invites/{}/", + self.belongs_to.borrow().urls.api, + invite_code + )) + .bearer_auth(self.token()), + limit_type: super::LimitType::Global, + }; + if session_id.is_some() { + request.request = request + .request + .body(to_string(session_id.unwrap()).unwrap()); + } + request.deserialize_response::(self).await + } + /// Note: Spacebar does not yet implement this endpoint. + pub async fn create_user_invite(&mut self, code: Option<&str>) -> ChorusResult { + ChorusRequest { + request: Client::new() + .post(format!( + "{}/users/@me/invites/", + self.belongs_to.borrow().urls.api + )) + .body(to_string(&code).unwrap()) + .bearer_auth(self.token()), + limit_type: super::LimitType::Global, + } + .deserialize_response::(self) + .await + } + + pub async fn create_guild_invite( + &mut self, + create_channel_invite_schema: CreateChannelInviteSchema, + channel_id: Snowflake, + ) -> ChorusResult { + ChorusRequest { + request: Client::new() + .post(format!( + "{}/channels/{}/invites/", + self.belongs_to.borrow().urls.api, + channel_id + )) + .bearer_auth(self.token()) + .body(to_string(&create_channel_invite_schema).unwrap()), + limit_type: super::LimitType::Channel(channel_id), + } + .deserialize_response::(self) + .await + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 56abb5f..9fd954d 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,12 +1,13 @@ pub use channels::messages::*; -pub use common::*; pub use guilds::*; +pub use invites::*; pub use policies::instance::instance::*; -pub use policies::instance::limits::*; +pub use policies::instance::ratelimits::*; +pub use users::*; pub mod auth; pub mod channels; -pub mod common; pub mod guilds; +pub mod invites; pub mod policies; pub mod users; diff --git a/src/api/policies/instance/instance.rs b/src/api/policies/instance/instance.rs index 0ea3699..75f832c 100644 --- a/src/api/policies/instance/instance.rs +++ b/src/api/policies/instance/instance.rs @@ -1,7 +1,6 @@ -use reqwest::Client; use serde_json::from_str; -use crate::errors::{ChorusLibError, ChorusResult}; +use crate::errors::{ChorusError, ChorusResult}; use crate::instance::Instance; use crate::types::GeneralConfiguration; @@ -10,21 +9,21 @@ impl Instance { /// # Errors /// [`ChorusLibError`] - If the request fails. pub async fn general_configuration_schema(&self) -> ChorusResult { - let client = Client::new(); let endpoint_url = self.urls.api.clone() + "/policies/instance/"; - let request = match client.get(&endpoint_url).send().await { + let request = match self.client.get(&endpoint_url).send().await { Ok(result) => result, Err(e) => { - return Err(ChorusLibError::RequestErrorError { + return Err(ChorusError::RequestFailed { url: endpoint_url, - error: e.to_string(), + error: e, }); } }; if !request.status().as_str().starts_with('2') { - return Err(ChorusLibError::ReceivedErrorCodeError { - error_code: request.status().to_string(), + return Err(ChorusError::ReceivedErrorCode { + error_code: request.status().as_u16(), + error: request.text().await.unwrap(), }); } diff --git a/src/api/policies/instance/limits.rs b/src/api/policies/instance/limits.rs deleted file mode 100644 index 3c06d29..0000000 --- a/src/api/policies/instance/limits.rs +++ /dev/null @@ -1,499 +0,0 @@ -pub mod limits { - use std::collections::HashMap; - - use reqwest::Client; - use serde::{Deserialize, Serialize}; - use serde_json::from_str; - - #[derive(Clone, Copy, Eq, Hash, PartialEq, Debug, Default)] - pub enum LimitType { - AuthRegister, - AuthLogin, - AbsoluteMessage, - AbsoluteRegister, - #[default] - Global, - Ip, - Channel, - Error, - Guild, - Webhook, - } - - impl ToString for LimitType { - fn to_string(&self) -> String { - match self { - LimitType::AuthRegister => "AuthRegister".to_string(), - LimitType::AuthLogin => "AuthLogin".to_string(), - LimitType::AbsoluteMessage => "AbsoluteMessage".to_string(), - LimitType::AbsoluteRegister => "AbsoluteRegister".to_string(), - LimitType::Global => "Global".to_string(), - LimitType::Ip => "Ip".to_string(), - LimitType::Channel => "Channel".to_string(), - LimitType::Error => "Error".to_string(), - LimitType::Guild => "Guild".to_string(), - LimitType::Webhook => "Webhook".to_string(), - } - } - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct User { - pub maxGuilds: u64, - pub maxUsername: u64, - pub maxFriends: u64, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct Guild { - pub maxRoles: u64, - pub maxEmojis: u64, - pub maxMembers: u64, - pub maxChannels: u64, - pub maxChannelsInCategory: u64, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct Message { - pub maxCharacters: u64, - pub maxTTSCharacters: u64, - pub maxReactions: u64, - pub maxAttachmentSize: u64, - pub maxBulkDelete: u64, - pub maxEmbedDownloadSize: u64, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct Channel { - pub maxPins: u64, - pub maxTopic: u64, - pub maxWebhooks: u64, - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct Rate { - pub enabled: bool, - pub ip: Window, - pub global: Window, - pub error: Window, - pub routes: Routes, - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct Window { - pub count: u64, - pub window: u64, - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct Routes { - pub guild: Window, - pub webhook: Window, - pub channel: Window, - pub auth: AuthRoutes, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct AuthRoutes { - pub login: Window, - pub register: Window, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct AbsoluteRate { - pub register: AbsoluteWindow, - pub sendMessage: AbsoluteWindow, - } - - #[derive(Debug, Deserialize, Serialize)] - pub struct AbsoluteWindow { - pub limit: u64, - pub window: u64, - pub enabled: bool, - } - - #[derive(Debug, Deserialize, Serialize)] - #[allow(non_snake_case)] - pub struct Config { - pub user: User, - pub guild: Guild, - pub message: Message, - pub channel: Channel, - pub rate: Rate, - pub absoluteRate: AbsoluteRate, - } - - #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] - pub struct Limit { - pub bucket: LimitType, - pub limit: u64, - pub remaining: u64, - pub reset: u64, - } - - impl std::fmt::Display for Limit { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Bucket: {:?}, Limit: {}, Remaining: {}, Reset: {}", - self.bucket, self.limit, self.remaining, self.reset - ) - } - } - - impl Limit { - pub fn add_remaining(&mut self, remaining: i64) { - if remaining < 0 { - if (self.remaining as i64 + remaining) <= 0 { - self.remaining = 0; - return; - } - self.remaining -= remaining.unsigned_abs(); - return; - } - self.remaining += remaining.unsigned_abs(); - } - } - - pub struct LimitsMutRef<'a> { - pub limit_absolute_messages: &'a mut Limit, - pub limit_absolute_register: &'a mut Limit, - pub limit_auth_login: &'a mut Limit, - pub limit_auth_register: &'a mut Limit, - pub limit_ip: &'a mut Limit, - pub limit_global: &'a mut Limit, - pub limit_error: &'a mut Limit, - pub limit_guild: &'a mut Limit, - pub limit_webhook: &'a mut Limit, - pub limit_channel: &'a mut Limit, - } - - impl LimitsMutRef<'_> { - pub fn combine_mut_ref<'a>( - instance_rate_limits: &'a mut Limits, - user_rate_limits: &'a mut Limits, - ) -> LimitsMutRef<'a> { - LimitsMutRef { - limit_absolute_messages: &mut instance_rate_limits.limit_absolute_messages, - limit_absolute_register: &mut instance_rate_limits.limit_absolute_register, - limit_auth_login: &mut instance_rate_limits.limit_auth_login, - limit_auth_register: &mut instance_rate_limits.limit_auth_register, - limit_channel: &mut user_rate_limits.limit_channel, - limit_error: &mut user_rate_limits.limit_error, - limit_global: &mut instance_rate_limits.limit_global, - limit_guild: &mut user_rate_limits.limit_guild, - limit_ip: &mut instance_rate_limits.limit_ip, - limit_webhook: &mut user_rate_limits.limit_webhook, - } - } - - pub fn get_limit_ref(&self, limit_type: &LimitType) -> &Limit { - match limit_type { - LimitType::AbsoluteMessage => self.limit_absolute_messages, - LimitType::AbsoluteRegister => self.limit_absolute_register, - LimitType::AuthLogin => self.limit_auth_login, - LimitType::AuthRegister => self.limit_auth_register, - LimitType::Channel => self.limit_channel, - LimitType::Error => self.limit_error, - LimitType::Global => self.limit_global, - LimitType::Guild => self.limit_guild, - LimitType::Ip => self.limit_ip, - LimitType::Webhook => self.limit_webhook, - } - } - - pub fn get_limit_mut_ref(&mut self, limit_type: &LimitType) -> &mut Limit { - match limit_type { - LimitType::AbsoluteMessage => self.limit_absolute_messages, - LimitType::AbsoluteRegister => self.limit_absolute_register, - LimitType::AuthLogin => self.limit_auth_login, - LimitType::AuthRegister => self.limit_auth_register, - LimitType::Channel => self.limit_channel, - LimitType::Error => self.limit_error, - LimitType::Global => self.limit_global, - LimitType::Guild => self.limit_guild, - LimitType::Ip => self.limit_ip, - LimitType::Webhook => self.limit_webhook, - } - } - } - - #[derive(Debug, Clone, Default)] - pub struct Limits { - pub limit_absolute_messages: Limit, - pub limit_absolute_register: Limit, - pub limit_auth_login: Limit, - pub limit_auth_register: Limit, - pub limit_ip: Limit, - pub limit_global: Limit, - pub limit_error: Limit, - pub limit_guild: Limit, - pub limit_webhook: Limit, - pub limit_channel: Limit, - } - - impl Limits { - pub fn combine(instance_rate_limits: &Limits, user_rate_limits: &Limits) -> Limits { - Limits { - limit_absolute_messages: instance_rate_limits.limit_absolute_messages, - limit_absolute_register: instance_rate_limits.limit_absolute_register, - limit_auth_login: instance_rate_limits.limit_auth_login, - limit_auth_register: instance_rate_limits.limit_auth_register, - limit_channel: user_rate_limits.limit_channel, - limit_error: user_rate_limits.limit_error, - limit_global: instance_rate_limits.limit_global, - limit_guild: user_rate_limits.limit_guild, - limit_ip: instance_rate_limits.limit_ip, - limit_webhook: user_rate_limits.limit_webhook, - } - } - - pub fn get_limit_ref(&self, limit_type: &LimitType) -> &Limit { - match limit_type { - LimitType::AbsoluteMessage => &self.limit_absolute_messages, - LimitType::AbsoluteRegister => &self.limit_absolute_register, - LimitType::AuthLogin => &self.limit_auth_login, - LimitType::AuthRegister => &self.limit_auth_register, - LimitType::Channel => &self.limit_channel, - LimitType::Error => &self.limit_error, - LimitType::Global => &self.limit_global, - LimitType::Guild => &self.limit_guild, - LimitType::Ip => &self.limit_ip, - LimitType::Webhook => &self.limit_webhook, - } - } - - pub fn get_limit_mut_ref(&mut self, limit_type: &LimitType) -> &mut Limit { - match limit_type { - LimitType::AbsoluteMessage => &mut self.limit_absolute_messages, - LimitType::AbsoluteRegister => &mut self.limit_absolute_register, - LimitType::AuthLogin => &mut self.limit_auth_login, - LimitType::AuthRegister => &mut self.limit_auth_register, - LimitType::Channel => &mut self.limit_channel, - LimitType::Error => &mut self.limit_error, - LimitType::Global => &mut self.limit_global, - LimitType::Guild => &mut self.limit_guild, - LimitType::Ip => &mut self.limit_ip, - LimitType::Webhook => &mut self.limit_webhook, - } - } - - pub fn to_hash_map(&self) -> HashMap { - let mut map: HashMap = HashMap::new(); - map.insert(LimitType::AbsoluteMessage, self.limit_absolute_messages); - map.insert(LimitType::AbsoluteRegister, self.limit_absolute_register); - map.insert(LimitType::AuthLogin, self.limit_auth_login); - map.insert(LimitType::AuthRegister, self.limit_auth_register); - map.insert(LimitType::Ip, self.limit_ip); - map.insert(LimitType::Global, self.limit_global); - map.insert(LimitType::Error, self.limit_error); - map.insert(LimitType::Guild, self.limit_guild); - map.insert(LimitType::Webhook, self.limit_webhook); - map.insert(LimitType::Channel, self.limit_channel); - map - } - - pub fn get_as_mut(&mut self) -> &mut Limits { - self - } - - /// check_limits uses the API to get the current request limits of the instance. - /// It returns a `Limits` struct containing all the limits. - /// If the rate limit is disabled, then the limit is set to `u64::MAX`. - /// # Errors - /// This function will panic if the request fails or if the response body cannot be parsed. - /// TODO: Change this to return a Result and handle the errors properly. - pub async fn check_limits(api_url: String) -> Limits { - let client = Client::new(); - let url_parsed = crate::UrlBundle::parse_url(api_url) + "/policies/instance/limits"; - let result = client - .get(url_parsed) - .send() - .await - .unwrap_or_else(|e| panic!("An error occured while performing the request: {}", e)) - .text() - .await - .unwrap_or_else(|e| { - panic!( - "An error occured while parsing the request body string: {}", - e - ) - }); - let config: Config = from_str(&result).unwrap(); - // If config.rate.enabled is false, then add return a Limits struct with all limits set to u64::MAX - let mut limits: Limits; - if !config.rate.enabled { - limits = Limits { - limit_absolute_messages: Limit { - bucket: LimitType::AbsoluteMessage, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_absolute_register: Limit { - bucket: LimitType::AbsoluteRegister, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_auth_login: Limit { - bucket: LimitType::AuthLogin, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_auth_register: Limit { - bucket: LimitType::AuthRegister, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_ip: Limit { - bucket: LimitType::Ip, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_global: Limit { - bucket: LimitType::Global, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_error: Limit { - bucket: LimitType::Error, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_guild: Limit { - bucket: LimitType::Guild, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_webhook: Limit { - bucket: LimitType::Webhook, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - limit_channel: Limit { - bucket: LimitType::Channel, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }, - }; - } else { - limits = Limits { - limit_absolute_messages: Limit { - bucket: LimitType::AbsoluteMessage, - limit: config.absoluteRate.sendMessage.limit, - remaining: config.absoluteRate.sendMessage.limit, - reset: config.absoluteRate.sendMessage.window, - }, - limit_absolute_register: Limit { - bucket: LimitType::AbsoluteRegister, - limit: config.absoluteRate.register.limit, - remaining: config.absoluteRate.register.limit, - reset: config.absoluteRate.register.window, - }, - limit_auth_login: Limit { - bucket: LimitType::AuthLogin, - limit: config.rate.routes.auth.login.count, - remaining: config.rate.routes.auth.login.count, - reset: config.rate.routes.auth.login.window, - }, - limit_auth_register: Limit { - bucket: LimitType::AuthRegister, - limit: config.rate.routes.auth.register.count, - remaining: config.rate.routes.auth.register.count, - reset: config.rate.routes.auth.register.window, - }, - limit_ip: Limit { - bucket: LimitType::Ip, - limit: config.rate.ip.count, - remaining: config.rate.ip.count, - reset: config.rate.ip.window, - }, - limit_global: Limit { - bucket: LimitType::Global, - limit: config.rate.global.count, - remaining: config.rate.global.count, - reset: config.rate.global.window, - }, - limit_error: Limit { - bucket: LimitType::Error, - limit: config.rate.error.count, - remaining: config.rate.error.count, - reset: config.rate.error.window, - }, - limit_guild: Limit { - bucket: LimitType::Guild, - limit: config.rate.routes.guild.count, - remaining: config.rate.routes.guild.count, - reset: config.rate.routes.guild.window, - }, - limit_webhook: Limit { - bucket: LimitType::Webhook, - limit: config.rate.routes.webhook.count, - remaining: config.rate.routes.webhook.count, - reset: config.rate.routes.webhook.window, - }, - limit_channel: Limit { - bucket: LimitType::Channel, - limit: config.rate.routes.channel.count, - remaining: config.rate.routes.channel.count, - reset: config.rate.routes.channel.window, - }, - }; - } - - if !config.absoluteRate.register.enabled { - limits.limit_absolute_register = Limit { - bucket: LimitType::AbsoluteRegister, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }; - } - - if !config.absoluteRate.sendMessage.enabled { - limits.limit_absolute_messages = Limit { - bucket: LimitType::AbsoluteMessage, - limit: u64::MAX, - remaining: u64::MAX, - reset: u64::MAX, - }; - } - - limits - } - } -} - -#[cfg(test)] -mod instance_limits { - use crate::api::limits::{Limit, LimitType}; - - #[test] - fn limit_below_zero() { - let mut limit = Limit { - bucket: LimitType::AbsoluteMessage, - limit: 0, - remaining: 1, - reset: 0, - }; - limit.add_remaining(-2); - assert_eq!(0_u64, limit.remaining); - limit.add_remaining(-2123123); - assert_eq!(0_u64, limit.remaining); - } -} diff --git a/src/api/policies/instance/mod.rs b/src/api/policies/instance/mod.rs index 7be9605..0a1f245 100644 --- a/src/api/policies/instance/mod.rs +++ b/src/api/policies/instance/mod.rs @@ -1,5 +1,5 @@ pub use instance::*; -pub use limits::*; +pub use ratelimits::*; pub mod instance; -pub mod limits; +pub mod ratelimits; diff --git a/src/api/policies/instance/ratelimits.rs b/src/api/policies/instance/ratelimits.rs new file mode 100644 index 0000000..e32a835 --- /dev/null +++ b/src/api/policies/instance/ratelimits.rs @@ -0,0 +1,37 @@ +use std::hash::Hash; + +use serde::{Deserialize, Serialize}; + +use crate::types::Snowflake; + +/// The different types of ratelimits that can be applied to a request. Includes "Baseline"-variants +/// for when the Snowflake is not yet known. +/// See for more information. +#[derive(Clone, Copy, Eq, PartialEq, Debug, Default, Hash, Serialize, Deserialize)] +pub enum LimitType { + AuthRegister, + AuthLogin, + #[default] + Global, + Ip, + Channel(Snowflake), + ChannelBaseline, + Error, + Guild(Snowflake), + GuildBaseline, + Webhook(Snowflake), + WebhookBaseline, +} + +/// A struct that represents the current ratelimits, either instance-wide or user-wide. +/// Unlike [`RateLimits`], this struct shows the current ratelimits, not the rate limit +/// configuration for the instance. +/// See for more information. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Limit { + pub bucket: LimitType, + pub limit: u64, + pub remaining: u64, + pub reset: u64, + pub window: u64, +} diff --git a/src/api/policies/mod.rs b/src/api/policies/mod.rs index 3e25d8c..d0c29f1 100644 --- a/src/api/policies/mod.rs +++ b/src/api/policies/mod.rs @@ -1,3 +1,3 @@ -pub use instance::limits::*; +pub use instance::ratelimits::*; pub mod instance; diff --git a/src/api/users/channels.rs b/src/api/users/channels.rs new file mode 100644 index 0000000..806bd9f --- /dev/null +++ b/src/api/users/channels.rs @@ -0,0 +1,32 @@ +use reqwest::Client; +use serde_json::to_string; + +use crate::{ + api::LimitType, + errors::ChorusResult, + instance::UserMeta, + ratelimiter::ChorusRequest, + types::{Channel, PrivateChannelCreateSchema}, +}; + +impl UserMeta { + /// Creates a DM channel or group DM channel. + /// + /// # Reference: + /// Read + pub async fn create_private_channel( + &mut self, + create_private_channel_schema: PrivateChannelCreateSchema, + ) -> ChorusResult { + let url = format!("{}/users/@me/channels", self.belongs_to.borrow().urls.api); + ChorusRequest { + request: Client::new() + .post(url) + .bearer_auth(self.token()) + .body(to_string(&create_private_channel_schema).unwrap()), + limit_type: LimitType::Global, + } + .deserialize_response::(self) + .await + } +} diff --git a/src/api/users/guilds.rs b/src/api/users/guilds.rs new file mode 100644 index 0000000..735ed9e --- /dev/null +++ b/src/api/users/guilds.rs @@ -0,0 +1,30 @@ +use reqwest::Client; +use serde_json::to_string; + +use crate::errors::ChorusResult; +use crate::instance::UserMeta; +use crate::ratelimiter::ChorusRequest; +use crate::types::Snowflake; + +impl UserMeta { + /// # Arguments: + /// - lurking: Whether the user is lurking in the guild + /// + /// # Reference: + /// Read + pub async fn leave_guild(&mut self, guild_id: &Snowflake, lurking: bool) -> ChorusResult<()> { + ChorusRequest { + request: Client::new() + .delete(format!( + "{}/users/@me/guilds/{}/", + self.belongs_to.borrow().urls.api, + guild_id + )) + .bearer_auth(self.token()) + .body(to_string(&lurking).unwrap()), + limit_type: crate::api::LimitType::Guild(*guild_id), + } + .handle_request_as_result(self) + .await + } +} diff --git a/src/api/users/mod.rs b/src/api/users/mod.rs index 84ea0ed..ba789ba 100644 --- a/src/api/users/mod.rs +++ b/src/api/users/mod.rs @@ -1,5 +1,9 @@ +pub use channels::*; +pub use guilds::*; pub use relationships::*; pub use users::*; +pub mod channels; +pub mod guilds; pub mod relationships; pub mod users; diff --git a/src/api/users/relationships.rs b/src/api/users/relationships.rs index 36cabd2..39c75d8 100644 --- a/src/api/users/relationships.rs +++ b/src/api/users/relationships.rs @@ -2,9 +2,10 @@ use reqwest::Client; use serde_json::to_string; use crate::{ - api::{deserialize_response, handle_request_as_result}, + api::LimitType, errors::ChorusResult, instance::UserMeta, + ratelimiter::ChorusRequest, types::{self, CreateUserRelationshipSchema, RelationshipType, Snowflake}, }; @@ -26,13 +27,13 @@ impl UserMeta { self.belongs_to.borrow().urls.api, user_id ); - let request = Client::new().get(url).bearer_auth(self.token()); - deserialize_response::>( - request, - self, - crate::api::limits::LimitType::Global, - ) - .await + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(self.token()), + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::>(self) + .await } /// Retrieves the authenticated user's relationships. @@ -44,13 +45,13 @@ impl UserMeta { "{}/users/@me/relationships/", self.belongs_to.borrow().urls.api ); - let request = Client::new().get(url).bearer_auth(self.token()); - deserialize_response::>( - request, - self, - crate::api::limits::LimitType::Global, - ) - .await + let chorus_request = ChorusRequest { + request: Client::new().get(url).bearer_auth(self.token()), + limit_type: LimitType::Global, + }; + chorus_request + .deserialize_response::>(self) + .await } /// Sends a friend request to a user. @@ -70,8 +71,11 @@ impl UserMeta { self.belongs_to.borrow().urls.api ); let body = to_string(&schema).unwrap(); - let request = Client::new().post(url).bearer_auth(self.token()).body(body); - handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await + let chorus_request = ChorusRequest { + request: Client::new().post(url).bearer_auth(self.token()).body(body), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(self).await } /// Modifies the relationship between the authenticated user and the specified user. @@ -96,10 +100,13 @@ impl UserMeta { let api_url = self.belongs_to.borrow().urls.api.clone(); match relationship_type { RelationshipType::None => { - let request = Client::new() - .delete(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()); - handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await + let chorus_request = ChorusRequest { + request: Client::new() + .delete(format!("{}/users/@me/relationships/{}/", api_url, user_id)) + .bearer_auth(self.token()), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(self).await } RelationshipType::Friends | RelationshipType::Incoming | RelationshipType::Outgoing => { let body = CreateUserRelationshipSchema { @@ -107,11 +114,14 @@ impl UserMeta { from_friend_suggestion: None, friend_token: None, }; - let request = Client::new() - .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()) - .body(to_string(&body).unwrap()); - handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await + let chorus_request = ChorusRequest { + request: Client::new() + .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) + .bearer_auth(self.token()) + .body(to_string(&body).unwrap()), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(self).await } RelationshipType::Blocked => { let body = CreateUserRelationshipSchema { @@ -119,11 +129,14 @@ impl UserMeta { from_friend_suggestion: None, friend_token: None, }; - let request = Client::new() - .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) - .bearer_auth(self.token()) - .body(to_string(&body).unwrap()); - handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await + let chorus_request = ChorusRequest { + request: Client::new() + .put(format!("{}/users/@me/relationships/{}/", api_url, user_id)) + .bearer_auth(self.token()) + .body(to_string(&body).unwrap()), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(self).await } RelationshipType::Suggestion | RelationshipType::Implicit => Ok(()), } @@ -143,7 +156,10 @@ impl UserMeta { self.belongs_to.borrow().urls.api, user_id ); - let request = Client::new().delete(url).bearer_auth(self.token()); - handle_request_as_result(request, self, crate::api::limits::LimitType::Global).await + let chorus_request = ChorusRequest { + request: Client::new().delete(url).bearer_auth(self.token()), + limit_type: LimitType::Global, + }; + chorus_request.handle_request_as_result(self).await } } diff --git a/src/api/users/users.rs b/src/api/users/users.rs index cd777dc..af6d2ce 100644 --- a/src/api/users/users.rs +++ b/src/api/users/users.rs @@ -1,11 +1,13 @@ +use std::{cell::RefCell, rc::Rc}; + use reqwest::Client; use serde_json::to_string; use crate::{ - api::{deserialize_response, handle_request_as_result}, - errors::{ChorusLibError, ChorusResult}, + api::LimitType, + errors::{ChorusError, ChorusResult}, instance::{Instance, UserMeta}, - limit::LimitedRequester, + ratelimiter::ChorusRequest, types::{User, UserModifySchema, UserSettings}, }; @@ -48,16 +50,20 @@ impl UserMeta { || modify_schema.email.is_some() || modify_schema.code.is_some() { - return Err(ChorusLibError::PasswordRequiredError); + return Err(ChorusError::PasswordRequired); } let request = Client::new() .patch(format!("{}/users/@me/", self.belongs_to.borrow().urls.api)) .body(to_string(&modify_schema).unwrap()) .bearer_auth(self.token()); - let user_updated = - deserialize_response::(request, self, crate::api::limits::LimitType::Ip) - .await - .unwrap(); + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + let user_updated = chorus_request + .deserialize_response::(self) + .await + .unwrap(); let _ = std::mem::replace(&mut self.object, user_updated.clone()); Ok(user_updated) } @@ -78,43 +84,28 @@ impl UserMeta { self.belongs_to.borrow().urls.api )) .bearer_auth(self.token()); - handle_request_as_result(request, &mut self, crate::api::limits::LimitType::Ip).await + let chorus_request = ChorusRequest { + request, + limit_type: LimitType::default(), + }; + chorus_request.handle_request_as_result(&mut self).await } } impl User { pub async fn get(user: &mut UserMeta, id: Option<&String>) -> ChorusResult { - let mut belongs_to = user.belongs_to.borrow_mut(); - User::_get( - &user.token(), - &format!("{}", belongs_to.urls.api), - &mut belongs_to, - id, - ) - .await - } - - async fn _get( - token: &str, - url_api: &str, - instance: &mut Instance, - id: Option<&String>, - ) -> ChorusResult { + let url_api = user.belongs_to.borrow().urls.api.clone(); let url = if id.is_none() { format!("{}/users/@me/", url_api) } else { format!("{}/users/{}", url_api, id.unwrap()) }; - let request = reqwest::Client::new().get(url).bearer_auth(token); - let mut cloned_limits = instance.limits.clone(); - match LimitedRequester::send_request( + let request = reqwest::Client::new().get(url).bearer_auth(user.token()); + let chorus_request = ChorusRequest { request, - crate::api::limits::LimitType::Ip, - instance, - &mut cloned_limits, - ) - .await - { + limit_type: LimitType::Global, + }; + match chorus_request.send_request(user).await { Ok(result) => { let result_text = result.text().await.unwrap(); Ok(serde_json::from_str::(&result_text).unwrap()) @@ -131,18 +122,21 @@ impl User { let request: reqwest::RequestBuilder = Client::new() .get(format!("{}/users/@me/settings/", url_api)) .bearer_auth(token); - let mut cloned_limits = instance.limits.clone(); - match LimitedRequester::send_request( + let mut user = + UserMeta::shell(Rc::new(RefCell::new(instance.clone())), token.clone()).await; + let chorus_request = ChorusRequest { request, - crate::api::limits::LimitType::Ip, - instance, - &mut cloned_limits, - ) - .await - { + limit_type: LimitType::Global, + }; + let result = match chorus_request.send_request(&mut user).await { Ok(result) => Ok(serde_json::from_str(&result.text().await.unwrap()).unwrap()), Err(e) => Err(e), + }; + if instance.limits_information.is_some() { + instance.limits_information.as_mut().unwrap().ratelimits = + user.belongs_to.borrow().clone_limits_if_some().unwrap(); } + result } } @@ -158,6 +152,12 @@ impl Instance { This function is a wrapper around [`User::get`]. */ pub async fn get_user(&mut self, token: String, id: Option<&String>) -> ChorusResult { - User::_get(&token, &self.urls.api.clone(), self, id).await + let mut user = UserMeta::shell(Rc::new(RefCell::new(self.clone())), token).await; + let result = User::get(&mut user, id).await; + if self.limits_information.is_some() { + self.limits_information.as_mut().unwrap().ratelimits = + user.belongs_to.borrow().clone_limits_if_some().unwrap(); + } + result } } diff --git a/src/errors.rs b/src/errors.rs index 0a689f7..595b91f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,39 +1,50 @@ use custom_error::custom_error; +use reqwest::Error; custom_error! { #[derive(PartialEq, Eq)] - pub FieldFormatError - PasswordError = "Password must be between 1 and 72 characters.", - UsernameError = "Username must be between 2 and 32 characters.", - ConsentError = "Consent must be 'true' to register.", - EmailError = "The provided email address is in an invalid format.", + pub RegistrationError + Consent = "Consent must be 'true' to register.", } -pub type ChorusResult = std::result::Result; +pub type ChorusResult = std::result::Result; custom_error! { - #[derive(PartialEq, Eq)] - pub ChorusLibError + pub ChorusError + /// Server did not respond. NoResponse = "Did not receive a response from the Server.", - RequestErrorError{url:String, error:String} = "An error occured while trying to GET from {url}: {error}", - ReceivedErrorCodeError{error_code:String} = "Received the following error code while requesting from the route: {error_code}", - CantGetInfoError{error:String} = "Something seems to be wrong with the instance. Cannot get information about the instance: {error}", - InvalidFormBodyError{error_type: String, error:String} = "The server responded with: {error_type}: {error}", + /// Reqwest returned an Error instead of a Response object. + RequestFailed{url:String, error: Error} = "An error occured while trying to GET from {url}: {error}", + /// Response received, however, it was not of the successful responses type. Used when no other, special case applies. + ReceivedErrorCode{error_code: u16, error: String} = "Received the following error code while requesting from the route: {error_code}", + /// Used when there is likely something wrong with the instance, the request was directed to. + CantGetInformation{error:String} = "Something seems to be wrong with the instance. Cannot get information about the instance: {error}", + /// The requests form body was malformed/invalid. + InvalidFormBody{error_type: String, error:String} = "The server responded with: {error_type}: {error}", + /// The request has not been processed by the server due to a relevant rate limit bucket being exhausted. RateLimited{bucket:String} = "Ratelimited on Bucket {bucket}", - MultipartCreationError{error: String} = "Got an error whilst creating the form: {error}", - FormCreationError{error: String} = "Got an error whilst creating the form: {error}", + /// The multipart form could not be created. + MultipartCreation{error: String} = "Got an error whilst creating the form: {error}", + /// The regular form could not be created. + FormCreation{error: String} = "Got an error whilst creating the form: {error}", + /// The token is invalid. TokenExpired = "Token expired, invalid or not found.", + /// No permission NoPermission = "You do not have the permissions needed to perform this action.", + /// Resource not found NotFound{error: String} = "The provided resource hasn't been found: {error}", - PasswordRequiredError = "You need to provide your current password to authenticate for this action.", - InvalidResponseError{error: String} = "The response is malformed and cannot be processed. Error: {error}", - InvalidArgumentsError{error: String} = "Invalid arguments were provided. Error: {error}" + /// Used when you, for example, try to change your spacebar account password without providing your old password for verification. + PasswordRequired = "You need to provide your current password to authenticate for this action.", + /// Malformed or unexpected response. + InvalidResponse{error: String} = "The response is malformed and cannot be processed. Error: {error}", + /// Invalid, insufficient or too many arguments provided. + InvalidArguments{error: String} = "Invalid arguments were provided. Error: {error}" } custom_error! { #[derive(PartialEq, Eq)] pub ObserverError - AlreadySubscribedError = "Each event can only be subscribed to once." + AlreadySubscribed = "Each event can only be subscribed to once." } custom_error! { @@ -45,27 +56,27 @@ custom_error! { #[derive(Clone, PartialEq, Eq)] pub GatewayError // Errors we have received from the gateway - UnknownError = "We're not sure what went wrong. Try reconnecting?", - UnknownOpcodeError = "You sent an invalid Gateway opcode or an invalid payload for an opcode", - DecodeError = "Gateway server couldn't decode payload", - NotAuthenticatedError = "You sent a payload prior to identifying", - AuthenticationFailedError = "The account token sent with your identify payload is invalid", - AlreadyAuthenticatedError = "You've already identified, no need to reauthenticate", - InvalidSequenceNumberError = "The sequence number sent when resuming the session was invalid. Reconnect and start a new session", - RateLimitedError = "You are being rate limited!", - SessionTimedOutError = "Your session timed out. Reconnect and start a new one", - InvalidShardError = "You sent us an invalid shard when identifying", - ShardingRequiredError = "The session would have handled too many guilds - you are required to shard your connection in order to connect", - InvalidAPIVersionError = "You sent an invalid Gateway version", - InvalidIntentsError = "You sent an invalid intent", - DisallowedIntentsError = "You sent a disallowed intent. You may have tried to specify an intent that you have not enabled or are not approved for", + Unknown = "We're not sure what went wrong. Try reconnecting?", + UnknownOpcode = "You sent an invalid Gateway opcode or an invalid payload for an opcode", + Decode = "Gateway server couldn't decode payload", + NotAuthenticated = "You sent a payload prior to identifying", + AuthenticationFailed = "The account token sent with your identify payload is invalid", + AlreadyAuthenticated = "You've already identified, no need to reauthenticate", + InvalidSequenceNumber = "The sequence number sent when resuming the session was invalid. Reconnect and start a new session", + RateLimited = "You are being rate limited!", + SessionTimedOut = "Your session timed out. Reconnect and start a new one", + InvalidShard = "You sent us an invalid shard when identifying", + ShardingRequired = "The session would have handled too many guilds - you are required to shard your connection in order to connect", + InvalidAPIVersion = "You sent an invalid Gateway version", + InvalidIntents = "You sent an invalid intent", + DisallowedIntents = "You sent a disallowed intent. You may have tried to specify an intent that you have not enabled or are not approved for", // Errors when initiating a gateway connection - CannotConnectError{error: String} = "Cannot connect due to a tungstenite error: {error}", - NonHelloOnInitiateError{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong", + CannotConnect{error: String} = "Cannot connect due to a tungstenite error: {error}", + NonHelloOnInitiate{opcode: u8} = "Received non hello on initial gateway connection ({opcode}), something is definitely wrong", // Other misc errors - UnexpectedOpcodeReceivedError{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", + UnexpectedOpcodeReceived{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", } custom_error! { diff --git a/src/gateway.rs b/src/gateway.rs index badf845..8e60dd0 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -1,16 +1,23 @@ use crate::errors::GatewayError; use crate::gateway::events::Events; -use crate::types; -use crate::types::WebSocketEvent; +use crate::types::{self, Channel, ChannelUpdate, Snowflake}; +use crate::types::{UpdateMessage, WebSocketEvent}; +use async_trait::async_trait; +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Debug; use std::sync::Arc; +use std::time::Duration; +use tokio::sync::watch; +use tokio::time::sleep_until; use futures_util::stream::SplitSink; use futures_util::stream::SplitStream; use futures_util::SinkExt; use futures_util::StreamExt; +use log::{info, trace, warn}; use native_tls::TlsConnector; use tokio::net::TcpStream; -use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::Sender; use tokio::sync::Mutex; use tokio::task; @@ -69,7 +76,7 @@ const GATEWAY_CALL_SYNC: u8 = 13; const GATEWAY_LAZY_REQUEST: u8 = 14; /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms -pub const HEARTBEAT_ACK_TIMEOUT: u128 = 2000; +pub(crate) const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; /// Represents a messsage received from the gateway. This will be either a [GatewayReceivePayload], containing events, or a [GatewayError]. /// This struct is used internally when handling messages. @@ -94,25 +101,21 @@ impl GatewayMessage { let processed_content = content.to_lowercase().replace('.', ""); match processed_content.as_str() { - "unknown error" | "4000" => Some(GatewayError::UnknownError), - "unknown opcode" | "4001" => Some(GatewayError::UnknownOpcodeError), - "decode error" | "error while decoding payload" | "4002" => { - Some(GatewayError::DecodeError) - } - "not authenticated" | "4003" => Some(GatewayError::NotAuthenticatedError), - "authentication failed" | "4004" => Some(GatewayError::AuthenticationFailedError), - "already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticatedError), - "invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumberError), - "rate limited" | "4008" => Some(GatewayError::RateLimitedError), - "session timed out" | "4009" => Some(GatewayError::SessionTimedOutError), - "invalid shard" | "4010" => Some(GatewayError::InvalidShardError), - "sharding required" | "4011" => Some(GatewayError::ShardingRequiredError), - "invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersionError), - "invalid intent(s)" | "invalid intent" | "4013" => { - Some(GatewayError::InvalidIntentsError) - } + "unknown error" | "4000" => Some(GatewayError::Unknown), + "unknown opcode" | "4001" => Some(GatewayError::UnknownOpcode), + "decode error" | "error while decoding payload" | "4002" => Some(GatewayError::Decode), + "not authenticated" | "4003" => Some(GatewayError::NotAuthenticated), + "authentication failed" | "4004" => Some(GatewayError::AuthenticationFailed), + "already authenticated" | "4005" => Some(GatewayError::AlreadyAuthenticated), + "invalid seq" | "4007" => Some(GatewayError::InvalidSequenceNumber), + "rate limited" | "4008" => Some(GatewayError::RateLimited), + "session timed out" | "4009" => Some(GatewayError::SessionTimedOut), + "invalid shard" | "4010" => Some(GatewayError::InvalidShard), + "sharding required" | "4011" => Some(GatewayError::ShardingRequired), + "invalid api version" | "4012" => Some(GatewayError::InvalidAPIVersion), + "invalid intent(s)" | "invalid intent" | "4013" => Some(GatewayError::InvalidIntents), "disallowed intent(s)" | "disallowed intents" | "4014" => { - Some(GatewayError::DisallowedIntentsError) + Some(GatewayError::DisallowedIntents) } _ => None, } @@ -164,6 +167,12 @@ pub struct GatewayHandle { pub handle: JoinHandle<()>, /// Tells gateway tasks to close kill_send: tokio::sync::broadcast::Sender<()>, + store: Arc>>>, +} + +/// An entity type which is supposed to be updateable via the Gateway. This is implemented for all such types chorus supports, implementing it for your own types is likely a mistake. +pub trait Updateable: 'static + Send + Sync { + fn id(&self) -> Snowflake; } impl GatewayHandle { @@ -187,11 +196,32 @@ impl GatewayHandle { .unwrap(); } + pub async fn observe(&self, object: T) -> watch::Receiver { + let mut store = self.store.lock().await; + if let Some(channel) = store.get(&object.id()) { + let (_, rx) = channel + .downcast_ref::<(watch::Sender, watch::Receiver)>() + .unwrap_or_else(|| { + panic!( + "Snowflake {} already exists in the store, but it is not of type T.", + object.id() + ) + }); + rx.clone() + } else { + let id = object.id(); + let channel = watch::channel(object); + let receiver = channel.1.clone(); + store.insert(id, Box::new(channel)); + receiver + } + } + /// Sends an identify event to the gateway pub async fn send_identify(&self, to_send: types::GatewayIdentifyPayload) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Identify.."); + trace!("GW: Sending Identify.."); self.send_json_event(GATEWAY_IDENTIFY, to_send_value).await; } @@ -200,7 +230,7 @@ impl GatewayHandle { pub async fn send_resume(&self, to_send: types::GatewayResume) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Resume.."); + trace!("GW: Sending Resume.."); self.send_json_event(GATEWAY_RESUME, to_send_value).await; } @@ -209,7 +239,7 @@ impl GatewayHandle { pub async fn send_update_presence(&self, to_send: types::UpdatePresence) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Update Presence.."); + trace!("GW: Sending Update Presence.."); self.send_json_event(GATEWAY_UPDATE_PRESENCE, to_send_value) .await; @@ -219,7 +249,7 @@ impl GatewayHandle { pub async fn send_request_guild_members(&self, to_send: types::GatewayRequestGuildMembers) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Request Guild Members.."); + trace!("GW: Sending Request Guild Members.."); self.send_json_event(GATEWAY_REQUEST_GUILD_MEMBERS, to_send_value) .await; @@ -229,7 +259,7 @@ impl GatewayHandle { pub async fn send_update_voice_state(&self, to_send: types::UpdateVoiceState) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Update Voice State.."); + trace!("GW: Sending Update Voice State.."); self.send_json_event(GATEWAY_UPDATE_VOICE_STATE, to_send_value) .await; @@ -239,7 +269,7 @@ impl GatewayHandle { pub async fn send_call_sync(&self, to_send: types::CallSync) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Call Sync.."); + trace!("GW: Sending Call Sync.."); self.send_json_event(GATEWAY_CALL_SYNC, to_send_value).await; } @@ -248,7 +278,7 @@ impl GatewayHandle { pub async fn send_lazy_request(&self, to_send: types::LazyRequest) { let to_send_value = serde_json::to_value(&to_send).unwrap(); - println!("GW: Sending Lazy Request.."); + trace!("GW: Sending Lazy Request.."); self.send_json_event(GATEWAY_LAZY_REQUEST, to_send_value) .await; @@ -264,9 +294,9 @@ impl GatewayHandle { } pub struct Gateway { - pub events: Arc>, + events: Arc>, heartbeat_handler: HeartbeatHandler, - pub websocket_send: Arc< + websocket_send: Arc< Mutex< SplitSink< WebSocketStream>, @@ -274,8 +304,9 @@ pub struct Gateway { >, >, >, - pub websocket_receive: SplitStream>>, + websocket_receive: SplitStream>>, kill_send: tokio::sync::broadcast::Sender<()>, + store: Arc>>>, } impl Gateway { @@ -293,7 +324,7 @@ impl Gateway { { Ok(websocket_stream) => websocket_stream, Err(e) => { - return Err(GatewayError::CannotConnectError { + return Err(GatewayError::CannotConnect { error: e.to_string(), }) } @@ -313,12 +344,12 @@ impl Gateway { serde_json::from_str(msg.to_text().unwrap()).unwrap(); if gateway_payload.op_code != GATEWAY_HELLO { - return Err(GatewayError::NonHelloOnInitiateError { + return Err(GatewayError::NonHelloOnInitiate { opcode: gateway_payload.op_code, }); } - println!("GW: Received Hello"); + info!("GW: Received Hello"); let gateway_hello: types::HelloData = serde_json::from_str(gateway_payload.event_data.unwrap().get()).unwrap(); @@ -326,16 +357,19 @@ impl Gateway { let events = Events::default(); let shared_events = Arc::new(Mutex::new(events)); + let store = Arc::new(Mutex::new(HashMap::new())); + let mut gateway = Gateway { events: shared_events.clone(), heartbeat_handler: HeartbeatHandler::new( - gateway_hello.heartbeat_interval, + Duration::from_millis(gateway_hello.heartbeat_interval), shared_websocket_send.clone(), kill_send.subscribe(), ), websocket_send: shared_websocket_send.clone(), websocket_receive, kill_send: kill_send.clone(), + store: store.clone(), }; // Now we can continuously check for messages in a different task, since we aren't going to receive another hello @@ -349,6 +383,7 @@ impl Gateway { websocket_send: shared_websocket_send.clone(), handle, kill_send: kill_send.clone(), + store, }) } @@ -367,7 +402,7 @@ impl Gateway { } // We couldn't receive the next message or it was an error, something is wrong with the websocket, close - println!("GW: Websocket is broken, stopping gateway"); + warn!("GW: Websocket is broken, stopping gateway"); break; } } @@ -380,6 +415,7 @@ impl Gateway { /// Deserializes and updates a dispatched event, when we already know its type; /// (Called for every event in handle_message) + #[allow(dead_code)] // TODO: Remove this allow annotation async fn handle_event<'a, T: WebSocketEvent + serde::Deserialize<'a>>( data: &'a str, event: &mut GatewayEvent, @@ -401,21 +437,19 @@ impl Gateway { } if !msg.is_error() && !msg.is_payload() { - println!( + warn!( "Message unrecognised: {:?}, please open an issue on the chorus github", msg.message.to_string() ); return; } - // To:do: handle errors in a good way, maybe observers like events? + // Todo: handle errors in a good way, maybe observers like events? if msg.is_error() { - println!("GW: Received error, connection will close.."); + warn!("GW: Received error, connection will close.."); let _error = msg.error(); - {} - self.close().await; return; } @@ -426,1026 +460,140 @@ impl Gateway { match gateway_payload.op_code { // An event was dispatched, we need to look at the gateway event name t GATEWAY_DISPATCH => { - let gateway_payload_t = gateway_payload.clone().event_name.unwrap(); + let Some(event_name) = gateway_payload.event_name else { + warn!("Gateway dispatch op without event_name"); + return + }; - println!("GW: Received {}..", gateway_payload_t); + trace!("Gateway: Received {event_name}"); - //println!("Event data dump: {}", gateway_payload.d.clone().unwrap().get()); + macro_rules! handle { + ($($name:literal => $($path:ident).+ $( $message_type:ty: $update_type:ty)?),*) => { + match event_name.as_str() { + $($name => { + let event = &mut self.events.lock().await.$($path).+; + match serde_json::from_str(gateway_payload.event_data.unwrap().get()) { + Err(err) => warn!("Failed to parse gateway event {event_name} ({err})"), + Ok(message) => { + $( + let message: $message_type = message; + if let Some(to_update) = self.store.lock().await.get(&message.id()) { + if let Some((tx, _)) = to_update.downcast_ref::<(watch::Sender<$update_type>, watch::Receiver<$update_type>)>() { + tx.send_modify(|object| message.update(object)); + } else { + warn!("Received {} for {}, but it has been observed to be a different type!", $name, message.id()) + } + } + )? + event.notify(message).await; + } + } + },)* + "RESUMED" => (), + "SESSIONS_REPLACE" => { + let result: Result, serde_json::Error> = + serde_json::from_str(gateway_payload.event_data.unwrap().get()); + match result { + Err(err) => { + warn!( + "Failed to parse gateway event {} ({})", + event_name, + err + ); + return; + } + Ok(sessions) => { + self.events.lock().await.session.replace.notify( + types::SessionsReplace {sessions} + ).await; + } + } + }, + _ => { + warn!("Received unrecognized gateway event ({event_name})! Please open an issue on the chorus github so we can implement it"); + } + } + }; + } // See https://discord.com/developers/docs/topics/gateway-events#receive-events // "Some" of these are undocumented - match gateway_payload_t.as_str() { - "READY" => { - let event = &mut self.events.lock().await.session.ready; - - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "READY_SUPPLEMENTAL" => { - let event = &mut self.events.lock().await.session.ready_supplemental; - - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "RESUMED" => {} - "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => { - let event = &mut self - .events - .lock() - .await - .application - .command_permissions_update; - - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "AUTO_MODERATION_RULE_CREATE" => { - let event = &mut self.events.lock().await.auto_moderation.rule_create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "AUTO_MODERATION_RULE_UPDATE" => { - let event = &mut self.events.lock().await.auto_moderation.rule_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "AUTO_MODERATION_RULE_DELETE" => { - let event = &mut self.events.lock().await.auto_moderation.rule_delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "AUTO_MODERATION_ACTION_EXECUTION" => { - let event = &mut self.events.lock().await.auto_moderation.action_execution; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CHANNEL_CREATE" => { - let event = &mut self.events.lock().await.channel.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CHANNEL_UPDATE" => { - let event = &mut self.events.lock().await.channel.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CHANNEL_UNREAD_UPDATE" => { - let event = &mut self.events.lock().await.channel.unread_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CHANNEL_DELETE" => { - let event = &mut self.events.lock().await.channel.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CHANNEL_PINS_UPDATE" => { - let event = &mut self.events.lock().await.channel.pins_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CALL_CREATE" => { - let event = &mut self.events.lock().await.call.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CALL_UPDATE" => { - let event = &mut self.events.lock().await.call.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "CALL_DELETE" => { - let event = &mut self.events.lock().await.call.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_CREATE" => { - let event = &mut self.events.lock().await.thread.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_UPDATE" => { - let event = &mut self.events.lock().await.thread.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_DELETE" => { - let event = &mut self.events.lock().await.thread.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_LIST_SYNC" => { - let event = &mut self.events.lock().await.thread.list_sync; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_MEMBER_UPDATE" => { - let event = &mut self.events.lock().await.thread.member_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "THREAD_MEMBERS_UPDATE" => { - let event = &mut self.events.lock().await.thread.members_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_CREATE" => { - let event = &mut self.events.lock().await.guild.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_UPDATE" => { - let event = &mut self.events.lock().await.guild.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_DELETE" => { - let event = &mut self.events.lock().await.guild.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_AUDIT_LOG_ENTRY_CREATE" => { - let event = &mut self.events.lock().await.guild.audit_log_entry_create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_BAN_ADD" => { - let event = &mut self.events.lock().await.guild.ban_add; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_BAN_REMOVE" => { - let event = &mut self.events.lock().await.guild.ban_remove; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_EMOJIS_UPDATE" => { - let event = &mut self.events.lock().await.guild.emojis_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_STICKERS_UPDATE" => { - let event = &mut self.events.lock().await.guild.stickers_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_INTEGRATIONS_UPDATE" => { - let event = &mut self.events.lock().await.guild.integrations_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_MEMBER_ADD" => { - let event = &mut self.events.lock().await.guild.member_add; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_MEMBER_REMOVE" => { - let event = &mut self.events.lock().await.guild.member_remove; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_MEMBER_UPDATE" => { - let event = &mut self.events.lock().await.guild.member_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_MEMBERS_CHUNK" => { - let event = &mut self.events.lock().await.guild.members_chunk; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_ROLE_CREATE" => { - let event = &mut self.events.lock().await.guild.role_create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_ROLE_UPDATE" => { - let event = &mut self.events.lock().await.guild.role_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_ROLE_DELETE" => { - let event = &mut self.events.lock().await.guild.role_delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_SCHEDULED_EVENT_CREATE" => { - let event = &mut self.events.lock().await.guild.role_scheduled_event_create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_SCHEDULED_EVENT_UPDATE" => { - let event = &mut self.events.lock().await.guild.role_scheduled_event_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_SCHEDULED_EVENT_DELETE" => { - let event = &mut self.events.lock().await.guild.role_scheduled_event_delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_SCHEDULED_EVENT_USER_ADD" => { - let event = - &mut self.events.lock().await.guild.role_scheduled_event_user_add; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "GUILD_SCHEDULED_EVENT_USER_REMOVE" => { - let event = &mut self - .events - .lock() - .await - .guild - .role_scheduled_event_user_remove; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "PASSIVE_UPDATE_V1" => { - let event = &mut self.events.lock().await.guild.passive_update_v1; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INTEGRATION_CREATE" => { - let event = &mut self.events.lock().await.integration.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INTEGRATION_UPDATE" => { - let event = &mut self.events.lock().await.integration.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INTEGRATION_DELETE" => { - let event = &mut self.events.lock().await.integration.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INTERACTION_CREATE" => { - let event = &mut self.events.lock().await.interaction.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INVITE_CREATE" => { - let event = &mut self.events.lock().await.invite.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "INVITE_DELETE" => { - let event = &mut self.events.lock().await.invite.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_CREATE" => { - let event = &mut self.events.lock().await.message.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_UPDATE" => { - let event = &mut self.events.lock().await.message.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_DELETE" => { - let event = &mut self.events.lock().await.message.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_DELETE_BULK" => { - let event = &mut self.events.lock().await.message.delete_bulk; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_REACTION_ADD" => { - let event = &mut self.events.lock().await.message.reaction_add; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_REACTION_REMOVE" => { - let event = &mut self.events.lock().await.message.reaction_remove; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_REACTION_REMOVE_ALL" => { - let event = &mut self.events.lock().await.message.reaction_remove_all; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_REACTION_REMOVE_EMOJI" => { - let event = &mut self.events.lock().await.message.reaction_remove_emoji; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "MESSAGE_ACK" => { - let event = &mut self.events.lock().await.message.ack; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "PRESENCE_UPDATE" => { - let event = &mut self.events.lock().await.user.presence_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "RELATIONSHIP_ADD" => { - let event = &mut self.events.lock().await.relationship.add; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "RELATIONSHIP_REMOVE" => { - let event = &mut self.events.lock().await.relationship.remove; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "STAGE_INSTANCE_CREATE" => { - let event = &mut self.events.lock().await.stage_instance.create; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "STAGE_INSTANCE_UPDATE" => { - let event = &mut self.events.lock().await.stage_instance.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "STAGE_INSTANCE_DELETE" => { - let event = &mut self.events.lock().await.stage_instance.delete; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "SESSIONS_REPLACE" => { - let result: Result, serde_json::Error> = - serde_json::from_str(gateway_payload.event_data.unwrap().get()); - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - - let data = types::SessionsReplace { - sessions: result.unwrap(), - }; - - self.events.lock().await.session.replace.notify(data).await; - } - "USER_UPDATE" => { - let event = &mut self.events.lock().await.user.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "USER_GUILD_SETTINGS_UPDATE" => { - let event = &mut self.events.lock().await.user.guild_settings_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "VOICE_STATE_UPDATE" => { - let event = &mut self.events.lock().await.voice.state_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "VOICE_SERVER_UPDATE" => { - let event = &mut self.events.lock().await.voice.server_update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - "WEBHOOKS_UPDATE" => { - let event = &mut self.events.lock().await.webhooks.update; - let result = - Gateway::handle_event(gateway_payload.event_data.unwrap().get(), event) - .await; - if result.is_err() { - println!( - "Failed to parse gateway event {} ({})", - gateway_payload_t, - result.err().unwrap() - ); - return; - } - } - _ => { - println!("Received unrecognized gateway event ({})! Please open an issue on the chorus github so we can implement it", &gateway_payload_t); - } - } + handle!( + "READY" => session.ready, + "READY_SUPPLEMENTAL" => session.ready_supplemental, + "APPLICATION_COMMAND_PERMISSIONS_UPDATE" => application.command_permissions_update, + "AUTO_MODERATION_RULE_CREATE" =>auto_moderation.rule_create, + "AUTO_MODERATION_RULE_UPDATE" =>auto_moderation.rule_update, + "AUTO_MODERATION_RULE_DELETE" => auto_moderation.rule_delete, + "AUTO_MODERATION_ACTION_EXECUTION" => auto_moderation.action_execution, + "CHANNEL_CREATE" => channel.create, + "CHANNEL_UPDATE" => channel.update ChannelUpdate: Channel, + "CHANNEL_UNREAD_UPDATE" => channel.unread_update, + "CHANNEL_DELETE" => channel.delete, + "CHANNEL_PINS_UPDATE" => channel.pins_update, + "CALL_CREATE" => call.create, + "CALL_UPDATE" => call.update, + "CALL_DELETE" => call.delete, + "THREAD_CREATE" => thread.create, + "THREAD_UPDATE" => thread.update, + "THREAD_DELETE" => thread.delete, + "THREAD_LIST_SYNC" => thread.list_sync, + "THREAD_MEMBER_UPDATE" => thread.member_update, + "THREAD_MEMBERS_UPDATE" => thread.members_update, + "GUILD_CREATE" => guild.create, + "GUILD_UPDATE" => guild.update, + "GUILD_DELETE" => guild.delete, + "GUILD_AUDIT_LOG_ENTRY_CREATE" => guild.audit_log_entry_create, + "GUILD_BAN_ADD" => guild.ban_add, + "GUILD_BAN_REMOVE" => guild.ban_remove, + "GUILD_EMOJIS_UPDATE" => guild.emojis_update, + "GUILD_STICKERS_UPDATE" => guild.stickers_update, + "GUILD_INTEGRATIONS_UPDATE" => guild.integrations_update, + "GUILD_MEMBER_ADD" => guild.member_add, + "GUILD_MEMBER_REMOVE" => guild.member_remove, + "GUILD_MEMBER_UPDATE" => guild.member_update, + "GUILD_MEMBERS_CHUNK" => guild.members_chunk, + "GUILD_ROLE_CREATE" => guild.role_create, + "GUILD_ROLE_UPDATE" => guild.role_update, + "GUILD_ROLE_DELETE" => guild.role_delete, + "GUILD_SCHEDULED_EVENT_CREATE" => guild.role_scheduled_event_create, + "GUILD_SCHEDULED_EVENT_UPDATE" => guild.role_scheduled_event_update, + "GUILD_SCHEDULED_EVENT_DELETE" => guild.role_scheduled_event_delete, + "GUILD_SCHEDULED_EVENT_USER_ADD" => guild.role_scheduled_event_user_add, + "GUILD_SCHEDULED_EVENT_USER_REMOVE" => guild.role_scheduled_event_user_remove, + "PASSIVE_UPDATE_V1" => guild.passive_update_v1, + "INTEGRATION_CREATE" => integration.create, + "INTEGRATION_UPDATE" => integration.update, + "INTEGRATION_DELETE" => integration.delete, + "INTERACTION_CREATE" => interaction.create, + "INVITE_CREATE" => invite.create, + "INVITE_DELETE" => invite.delete, + "MESSAGE_CREATE" => message.create, + "MESSAGE_UPDATE" => message.update, + "MESSAGE_DELETE" => message.delete, + "MESSAGE_DELETE_BULK" => message.delete_bulk, + "MESSAGE_REACTION_ADD" => message.reaction_add, + "MESSAGE_REACTION_REMOVE" => message.reaction_remove, + "MESSAGE_REACTION_REMOVE_ALL" => message.reaction_remove_all, + "MESSAGE_REACTION_REMOVE_EMOJI" => message.reaction_remove_emoji, + "MESSAGE_ACK" => message.ack, + "PRESENCE_UPDATE" => user.presence_update, + "RELATIONSHIP_ADD" => relationship.add, + "RELATIONSHIP_REMOVE" => relationship.remove, + "STAGE_INSTANCE_CREATE" => stage_instance.create, + "STAGE_INSTANCE_UPDATE" => stage_instance.update, + "STAGE_INSTANCE_DELETE" => stage_instance.delete, + "USER_UPDATE" => user.update, + "USER_GUILD_SETTINGS_UPDATE" => user.guild_settings_update, + "VOICE_STATE_UPDATE" => voice.state_update, + "VOICE_SERVER_UPDATE" => voice.server_update, + "WEBHOOKS_UPDATE" => webhooks.update + ); } // We received a heartbeat from the server // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." GATEWAY_HEARTBEAT => { - println!("GW: Received Heartbeat // Heartbeat Request"); + trace!("GW: Received Heartbeat // Heartbeat Request"); // Tell the heartbeat handler it should send a heartbeat right away @@ -1469,10 +617,10 @@ impl Gateway { // Starts our heartbeat // We should have already handled this in gateway init GATEWAY_HELLO => { - panic!("Received hello when it was unexpected"); + warn!("Received hello when it was unexpected"); } GATEWAY_HEARTBEAT_ACK => { - println!("GW: Received Heartbeat ACK"); + trace!("GW: Received Heartbeat ACK"); // Tell the heartbeat handler we received an ack @@ -1494,20 +642,20 @@ impl Gateway { | GATEWAY_REQUEST_GUILD_MEMBERS | GATEWAY_CALL_SYNC | GATEWAY_LAZY_REQUEST => { - let error = GatewayError::UnexpectedOpcodeReceivedError { + let error = GatewayError::UnexpectedOpcodeReceived { opcode: gateway_payload.op_code, }; Err::<(), GatewayError>(error).unwrap(); } _ => { - println!("Received unrecognized gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); + warn!("Received unrecognized gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); } } // If we we received a seq number we should let it know - if gateway_payload.sequence_number.is_some() { + if let Some(seq_num) = gateway_payload.sequence_number { let heartbeat_communication = HeartbeatThreadCommunication { - sequence_number: Some(gateway_payload.sequence_number.unwrap()), + sequence_number: Some(seq_num), // Op code is irrelevant here op_code: None, }; @@ -1522,9 +670,10 @@ impl Gateway { } /// Handles sending heartbeats to the gateway in another thread +#[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used struct HeartbeatHandler { - /// The heartbeat interval in milliseconds - pub heartbeat_interval: u128, + /// How ofter heartbeats need to be sent at a minimum + pub heartbeat_interval: Duration, /// The send channel for the heartbeat thread pub send: Sender, /// The handle of the thread @@ -1533,7 +682,7 @@ struct HeartbeatHandler { impl HeartbeatHandler { pub fn new( - heartbeat_interval: u128, + heartbeat_interval: Duration, websocket_tx: Arc< Mutex< SplitSink< @@ -1577,7 +726,7 @@ impl HeartbeatHandler { >, >, >, - heartbeat_interval: u128, + heartbeat_interval: Duration, mut receive: tokio::sync::mpsc::Receiver, mut kill_receive: tokio::sync::broadcast::Receiver<()>, ) { @@ -1586,52 +735,48 @@ impl HeartbeatHandler { let mut last_seq_number: Option = None; loop { - let should_shutdown = kill_receive.try_recv().is_ok(); - if should_shutdown { + if kill_receive.try_recv().is_ok() { + trace!("GW: Closing heartbeat task"); break; } - let mut should_send; + let timeout = if last_heartbeat_acknowledged { + heartbeat_interval + } else { + // If the server hasn't acknowledged our heartbeat we should resend it + Duration::from_millis(HEARTBEAT_ACK_TIMEOUT) + }; - let time_to_send = last_heartbeat_timestamp.elapsed().as_millis() >= heartbeat_interval; + let mut should_send = false; - should_send = time_to_send; - - let received_communication: Result = - receive.try_recv(); - if received_communication.is_ok() { - let communication = received_communication.unwrap(); - - // If we received a seq number update, use that as the last seq number - if communication.sequence_number.is_some() { - last_seq_number = Some(communication.sequence_number.unwrap()); + tokio::select! { + () = sleep_until(last_heartbeat_timestamp + timeout) => { + should_send = true; } + Some(communication) = receive.recv() => { + // If we received a seq number update, use that as the last seq number + if communication.sequence_number.is_some() { + last_seq_number = communication.sequence_number; + } - if communication.op_code.is_some() { - match communication.op_code.unwrap() { - GATEWAY_HEARTBEAT => { - // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately - should_send = true; + if let Some(op_code) = communication.op_code { + match op_code { + GATEWAY_HEARTBEAT => { + // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately + should_send = true; + } + GATEWAY_HEARTBEAT_ACK => { + // The server received our heartbeat + last_heartbeat_acknowledged = true; + } + _ => {} } - GATEWAY_HEARTBEAT_ACK => { - // The server received our heartbeat - last_heartbeat_acknowledged = true; - } - _ => {} } } } - // If the server hasn't acknowledged our heartbeat we should resend it - if !last_heartbeat_acknowledged - && last_heartbeat_timestamp.elapsed().as_millis() > HEARTBEAT_ACK_TIMEOUT - { - should_send = true; - println!("GW: Timed out waiting for a heartbeat ack, resending"); - } - if should_send { - println!("GW: Sending Heartbeat.."); + trace!("GW: Sending Heartbeat.."); let heartbeat = types::GatewayHeartbeat { op: GATEWAY_HEARTBEAT, @@ -1645,7 +790,7 @@ impl HeartbeatHandler { let send_result = websocket_tx.lock().await.send(msg).await; if send_result.is_err() { // We couldn't send, the websocket is broken - println!("GW: Couldnt send heartbeat, websocket seems broken"); + warn!("GW: Couldnt send heartbeat, websocket seems broken"); break; } @@ -1670,8 +815,9 @@ struct HeartbeatThreadCommunication { /// an Observable. The Observer is notified when the Observable's data changes. /// In this case, the Observable is a [`GatewayEvent`], which is a wrapper around a WebSocketEvent. /// Note that `Debug` is used to tell `Observer`s apart when unsubscribing. +#[async_trait] pub trait Observer: Sync + Send + std::fmt::Debug { - fn update(&self, data: &T); + async fn update(&self, data: &T); } /// GatewayEvent is a wrapper around a WebSocketEvent. It is used to notify the observers of a @@ -1706,7 +852,7 @@ impl GatewayEvent { /// Notifies the observers of the GatewayEvent. pub async fn notify(&self, new_event_data: T) { for observer in &self.observers { - observer.update(&new_event_data); + observer.update(&new_event_data).await; } } } @@ -1879,12 +1025,13 @@ mod example { #[derive(Debug)] struct Consumer { - name: String, + _name: String, events_received: AtomicI32, } + #[async_trait] impl Observer for Consumer { - fn update(&self, _data: &types::GatewayResume) { + async fn update(&self, _data: &types::GatewayResume) { self.events_received.fetch_add(1, Relaxed); } } @@ -1900,13 +1047,13 @@ mod example { }; let consumer = Arc::new(Consumer { - name: "first".into(), + _name: "first".into(), events_received: 0.into(), }); event.subscribe(consumer.clone()); let second_consumer = Arc::new(Consumer { - name: "second".into(), + _name: "second".into(), events_received: 0.into(), }); event.subscribe(second_consumer.clone()); diff --git a/src/instance.rs b/src/instance.rs index 2477217..9795557 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,26 +1,37 @@ use std::cell::RefCell; +use std::collections::HashMap; use std::fmt; use std::rc::Rc; use reqwest::Client; use serde::{Deserialize, Serialize}; -use crate::api::limits::Limits; -use crate::errors::{ChorusLibError, ChorusResult, FieldFormatError}; +use crate::api::{Limit, LimitType}; +use crate::errors::ChorusResult; +use crate::gateway::{Gateway, GatewayHandle}; +use crate::ratelimiter::ChorusRequest; +use crate::types::types::subconfigs::limits::rates::RateLimits; use crate::types::{GeneralConfiguration, User, UserSettings}; use crate::UrlBundle; #[derive(Debug, Clone)] /** The [`Instance`] what you will be using to perform all sorts of actions on the Spacebar server. +If `limits_information` is `None`, then the instance will not be rate limited. */ pub struct Instance { pub urls: UrlBundle, pub instance_info: GeneralConfiguration, - pub limits: Limits, + pub limits_information: Option, pub client: Client, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LimitsInformation { + pub ratelimits: HashMap, + pub configuration: RateLimits, +} + impl Instance { /// Creates a new [`Instance`]. /// # Arguments @@ -28,24 +39,43 @@ impl Instance { /// * `requester` - The [`LimitedRequester`] that will be used to make requests to the Spacebar server. /// # Errors /// * [`InstanceError`] - If the instance cannot be created. - pub async fn new(urls: UrlBundle) -> ChorusResult { + pub async fn new(urls: UrlBundle, limited: bool) -> ChorusResult { + let limits_information; + if limited { + let limits_configuration = + Some(ChorusRequest::get_limits_config(&urls.api).await?.rate); + let limits = Some(ChorusRequest::limits_config_to_hashmap( + limits_configuration.as_ref().unwrap(), + )); + limits_information = Some(LimitsInformation { + ratelimits: limits.unwrap(), + configuration: limits_configuration.unwrap(), + }); + } else { + limits_information = None; + } let mut instance = Instance { urls: urls.clone(), // Will be overwritten in the next step instance_info: GeneralConfiguration::default(), - limits: Limits::check_limits(urls.api).await, + limits_information, client: Client::new(), }; instance.instance_info = match instance.general_configuration_schema().await { Ok(schema) => schema, Err(e) => { - return Err(ChorusLibError::CantGetInfoError { - error: e.to_string(), - }); + log::warn!("Could not get instance configuration schema: {}", e); + GeneralConfiguration::default() } }; Ok(instance) } + pub(crate) fn clone_limits_if_some(&self) -> Option> { + if self.limits_information.is_some() { + return Some(self.limits_information.as_ref().unwrap().ratelimits.clone()); + } + None + } } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -59,32 +89,14 @@ impl fmt::Display for Token { } } -#[derive(Debug, PartialEq, Eq)] -pub struct Username { - pub username: String, -} - -impl Username { - /// Creates a new [`Username`]. - /// # Arguments - /// * `username` - The username that will be used to create the [`Username`]. - /// # Errors - /// * [`UsernameFormatError`] - If the username is not between 2 and 32 characters. - pub fn new(username: String) -> Result { - if username.len() < 2 || username.len() > 32 { - return Err(FieldFormatError::UsernameError); - } - Ok(Username { username }) - } -} - #[derive(Debug)] pub struct UserMeta { pub belongs_to: Rc>, pub token: String, - pub limits: Limits, + pub limits: Option>, pub settings: UserSettings, pub object: User, + pub gateway: GatewayHandle, } impl UserMeta { @@ -99,9 +111,10 @@ impl UserMeta { pub fn new( belongs_to: Rc>, token: String, - limits: Limits, + limits: Option>, settings: UserSettings, object: User, + gateway: GatewayHandle, ) -> UserMeta { UserMeta { belongs_to, @@ -109,6 +122,32 @@ impl UserMeta { limits, settings, object, + gateway, + } + } + + /// Creates a new 'shell' of a user. The user does not exist as an object, and exists so that you have + /// a UserMeta object to make Rate Limited requests with. This is useful in scenarios like + /// registering or logging in to the Instance, where you do not yet have a User object, but still + /// need to make a RateLimited request. To use the [`GatewayHandle`], you will have to identify + /// first. + pub(crate) async fn shell(instance: Rc>, token: String) -> UserMeta { + let settings = UserSettings::default(); + let object = User::default(); + let wss_url = instance.borrow().urls.wss.clone(); + // Dummy gateway object + let gateway = Gateway::new(wss_url).await.unwrap(); + UserMeta { + token, + belongs_to: instance.clone(), + limits: instance + .borrow() + .limits_information + .as_ref() + .map(|info| info.ratelimits.clone()), + settings, + object, + gateway, } } } diff --git a/src/lib.rs b/src/lib.rs index bfda613..77425b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod gateway; #[cfg(feature = "client")] pub mod instance; #[cfg(feature = "client")] -pub mod limit; +pub mod ratelimiter; pub mod types; #[cfg(feature = "client")] pub mod voice; diff --git a/src/limit.rs b/src/limit.rs deleted file mode 100644 index 4415e0c..0000000 --- a/src/limit.rs +++ /dev/null @@ -1,304 +0,0 @@ -use reqwest::{RequestBuilder, Response}; - -use crate::{ - api::limits::{Limit, LimitType, Limits, LimitsMutRef}, - errors::{ChorusLibError, ChorusResult}, - instance::Instance, -}; - -#[derive(Debug)] -pub struct LimitedRequester; - -impl LimitedRequester { - /// Checks if a request can be sent without hitting API rate limits and sends it, if true. - /// Will automatically update the rate limits of the LimitedRequester the request has been - /// sent with. - /// - /// # Arguments - /// - /// * `request`: A `RequestBuilder` that contains a request ready to be sent. Unfinished or - /// invalid requests will result in the method panicing. - /// * `limit_type`: Because this library does not yet implement a way to check for which rate - /// limit will be used when the request gets send, you will have to specify this manually using - /// a `LimitType` enum. - /// - /// # Returns - /// - /// * `Response`: The `Response` gotten from sending the request to the server. This will be - /// returned if the Request was built and send successfully. Is wrapped in an `Option`. - /// * `None`: `None` will be returned if the rate limit has been hit, and the request could - /// therefore not have been sent. - /// - /// # Errors - /// - /// This method will error if: - /// - /// * The request does not return a success status code (200-299) - /// * The supplied `RequestBuilder` contains invalid or incomplete information - /// * There has been an error with processing (unwrapping) the `Response` - /// * The call to `update_limits` yielded errors. Read the methods' Errors section for more - /// information. - pub async fn send_request( - request: RequestBuilder, - limit_type: LimitType, - instance: &mut Instance, - user_rate_limits: &mut Limits, - ) -> ChorusResult { - if LimitedRequester::can_send_request(limit_type, &instance.limits, user_rate_limits) { - let built_request = match request.build() { - Ok(request) => request, - Err(e) => { - return Err(ChorusLibError::RequestErrorError { - url: "".to_string(), - error: e.to_string(), - }); - } - }; - let result = instance.client.execute(built_request).await; - let response = match result { - Ok(is_response) => is_response, - Err(e) => { - return Err(ChorusLibError::ReceivedErrorCodeError { - error_code: e.to_string(), - }); - } - }; - LimitedRequester::update_limits( - &response, - limit_type, - &mut instance.limits, - user_rate_limits, - ); - if !response.status().is_success() { - match response.status().as_u16() { - 401 => Err(ChorusLibError::TokenExpired), - 403 => Err(ChorusLibError::TokenExpired), - _ => Err(ChorusLibError::ReceivedErrorCodeError { - error_code: response.status().as_str().to_string(), - }), - } - } else { - Ok(response) - } - } else { - Err(ChorusLibError::RateLimited { - bucket: limit_type.to_string(), - }) - } - } - - fn update_limit_entry(entry: &mut Limit, reset: u64, remaining: u64, limit: u64) { - if reset != entry.reset { - entry.reset = reset; - entry.remaining = limit; - entry.limit = limit; - } else { - entry.remaining = remaining; - entry.limit = limit; - } - } - - fn can_send_request( - limit_type: LimitType, - instance_rate_limits: &Limits, - user_rate_limits: &Limits, - ) -> bool { - // Check if all of the limits in this vec have at least one remaining request - - let rate_limits = Limits::combine(instance_rate_limits, user_rate_limits); - - let constant_limits: Vec<&LimitType> = [ - &LimitType::Error, - &LimitType::Global, - &LimitType::Ip, - &limit_type, - ] - .to_vec(); - for limit in constant_limits.iter() { - match rate_limits.to_hash_map().get(limit) { - Some(limit) => { - if limit.remaining == 0 { - return false; - } - // AbsoluteRegister and AuthRegister can cancel each other out. - if limit.bucket == LimitType::AbsoluteRegister - && rate_limits - .to_hash_map() - .get(&LimitType::AuthRegister) - .unwrap() - .remaining - == 0 - { - return false; - } - if limit.bucket == LimitType::AuthRegister - && rate_limits - .to_hash_map() - .get(&LimitType::AbsoluteRegister) - .unwrap() - .remaining - == 0 - { - return false; - } - } - None => return false, - } - } - true - } - - fn update_limits( - response: &Response, - limit_type: LimitType, - instance_rate_limits: &mut Limits, - user_rate_limits: &mut Limits, - ) { - let mut rate_limits = LimitsMutRef::combine_mut_ref(instance_rate_limits, user_rate_limits); - - let remaining = match response.headers().get("X-RateLimit-Remaining") { - Some(remaining) => remaining.to_str().unwrap().parse::().unwrap(), - None => rate_limits.get_limit_mut_ref(&limit_type).remaining - 1, - }; - let limit = match response.headers().get("X-RateLimit-Limit") { - Some(limit) => limit.to_str().unwrap().parse::().unwrap(), - None => rate_limits.get_limit_mut_ref(&limit_type).limit, - }; - let reset = match response.headers().get("X-RateLimit-Reset") { - Some(reset) => reset.to_str().unwrap().parse::().unwrap(), - None => rate_limits.get_limit_mut_ref(&limit_type).reset, - }; - - let status = response.status(); - let status_str = status.as_str(); - - if status_str.starts_with('4') { - rate_limits - .get_limit_mut_ref(&LimitType::Error) - .add_remaining(-1); - } - - rate_limits - .get_limit_mut_ref(&LimitType::Global) - .add_remaining(-1); - - rate_limits - .get_limit_mut_ref(&LimitType::Ip) - .add_remaining(-1); - - match limit_type { - LimitType::Error => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Error); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::Global => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Global); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::Ip => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Ip); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::AuthLogin => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::AuthLogin); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::AbsoluteRegister => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::AbsoluteRegister); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - // AbsoluteRegister and AuthRegister both need to be updated, if a Register event - // happens. - rate_limits - .get_limit_mut_ref(&LimitType::AuthRegister) - .remaining -= 1; - } - LimitType::AuthRegister => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::AuthRegister); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - // AbsoluteRegister and AuthRegister both need to be updated, if a Register event - // happens. - rate_limits - .get_limit_mut_ref(&LimitType::AbsoluteRegister) - .remaining -= 1; - } - LimitType::AbsoluteMessage => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::AbsoluteMessage); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::Channel => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Channel); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::Guild => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Guild); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - LimitType::Webhook => { - let entry = rate_limits.get_limit_mut_ref(&LimitType::Webhook); - LimitedRequester::update_limit_entry(entry, reset, remaining, limit); - } - } - } -} - -#[cfg(test)] -mod rate_limit { - use serde_json::from_str; - - use crate::{api::limits::Config, UrlBundle}; - - use super::*; - - #[tokio::test] - async fn run_into_limit() { - let urls = UrlBundle::new( - String::from("http://localhost:3001/api/"), - String::from("wss://localhost:3001/"), - String::from("http://localhost:3001/cdn"), - ); - let mut request: Option> = None; - let mut instance = Instance::new(urls.clone()).await.unwrap(); - let mut user_rate_limits = Limits::check_limits(urls.api.clone()).await; - - for _ in 0..=50 { - let request_path = urls.api.clone() + "/some/random/nonexisting/path"; - let request_builder = instance.client.get(request_path); - request = Some( - LimitedRequester::send_request( - request_builder, - LimitType::Channel, - &mut instance, - &mut user_rate_limits, - ) - .await, - ); - } - assert!(matches!(request, Some(Err(_)))); - } - - #[tokio::test] - async fn test_send_request() { - let urls = UrlBundle::new( - String::from("http://localhost:3001/api/"), - String::from("wss://localhost:3001/"), - String::from("http://localhost:3001/cdn"), - ); - let mut instance = Instance::new(urls.clone()).await.unwrap(); - let mut user_rate_limits = Limits::check_limits(urls.api.clone()).await; - let _requester = LimitedRequester; - let request_path = urls.api.clone() + "/policies/instance/limits"; - let request_builder = instance.client.get(request_path); - let request = LimitedRequester::send_request( - request_builder, - LimitType::Channel, - &mut instance, - &mut user_rate_limits, - ) - .await; - let result = match request { - Ok(result) => result, - Err(_) => panic!("Request failed"), - }; - let _config: Config = from_str(result.text().await.unwrap().as_str()).unwrap(); - } -} diff --git a/src/ratelimiter.rs b/src/ratelimiter.rs new file mode 100644 index 0000000..9227737 --- /dev/null +++ b/src/ratelimiter.rs @@ -0,0 +1,466 @@ +use std::collections::HashMap; + +use log::{self, debug}; +use reqwest::{Client, RequestBuilder, Response}; +use serde::Deserialize; +use serde_json::from_str; + +use crate::{ + api::{Limit, LimitType}, + errors::{ChorusError, ChorusResult}, + instance::UserMeta, + types::{types::subconfigs::limits::rates::RateLimits, LimitsConfiguration}, +}; + +/// Chorus' request struct. This struct is used to send rate-limited requests to the Spacebar server. +/// See for more information. +pub struct ChorusRequest { + pub request: RequestBuilder, + pub limit_type: LimitType, +} + +impl ChorusRequest { + /// Sends a [`ChorusRequest`]. Checks if the user is rate limited, and if not, sends the request. + /// If the user is not rate limited and the instance has rate limits enabled, it will update the + /// rate limits. + #[allow(clippy::await_holding_refcell_ref)] + pub(crate) async fn send_request(self, user: &mut UserMeta) -> ChorusResult { + if !ChorusRequest::can_send_request(user, &self.limit_type) { + log::info!("Rate limit hit. Bucket: {:?}", self.limit_type); + return Err(ChorusError::RateLimited { + bucket: format!("{:?}", self.limit_type), + }); + } + let belongs_to = user.belongs_to.borrow(); + let result = match belongs_to + .client + .execute(self.request.build().unwrap()) + .await + { + Ok(result) => { + debug!("Request successful: {:?}", result); + result + } + Err(error) => { + log::warn!("Request failed: {:?}", error); + return Err(ChorusError::RequestFailed { + url: error.url().unwrap().to_string(), + error, + }); + } + }; + drop(belongs_to); + if !result.status().is_success() { + if result.status().as_u16() == 429 { + log::warn!("Rate limit hit unexpectedly. Bucket: {:?}. Setting the instances' remaining global limit to 0 to have cooldown.", self.limit_type); + user.belongs_to + .borrow_mut() + .limits_information + .as_mut() + .unwrap() + .ratelimits + .get_mut(&LimitType::Global) + .unwrap() + .remaining = 0; + return Err(ChorusError::RateLimited { + bucket: format!("{:?}", self.limit_type), + }); + } + log::warn!("Request failed: {:?}", result); + return Err(ChorusRequest::interpret_error(result).await); + } + ChorusRequest::update_rate_limits(user, &self.limit_type, !result.status().is_success()); + Ok(result) + } + + fn can_send_request(user: &mut UserMeta, limit_type: &LimitType) -> bool { + log::trace!("Checking if user or instance is rate-limited..."); + let mut belongs_to = user.belongs_to.borrow_mut(); + if belongs_to.limits_information.is_none() { + log::trace!("Instance indicates no rate limits are configured. Continuing."); + return true; + } + let instance_dictated_limits = [ + &LimitType::AuthLogin, + &LimitType::AuthRegister, + &LimitType::Global, + &LimitType::Ip, + ]; + let limits = match instance_dictated_limits.contains(&limit_type) { + true => { + log::trace!( + "Limit type {:?} is dictated by the instance. Continuing.", + limit_type + ); + belongs_to + .limits_information + .as_mut() + .unwrap() + .ratelimits + .clone() + } + false => { + log::trace!( + "Limit type {:?} is dictated by the user. Continuing.", + limit_type + ); + ChorusRequest::ensure_limit_in_map( + &belongs_to + .limits_information + .as_ref() + .unwrap() + .configuration, + user.limits.as_mut().unwrap(), + limit_type, + ); + user.limits.as_mut().unwrap().clone() + } + }; + let global = belongs_to + .limits_information + .as_ref() + .unwrap() + .ratelimits + .get(&LimitType::Global) + .unwrap(); + let ip = belongs_to + .limits_information + .as_ref() + .unwrap() + .ratelimits + .get(&LimitType::Ip) + .unwrap(); + let limit_type_limit = limits.get(limit_type).unwrap(); + global.remaining > 0 && ip.remaining > 0 && limit_type_limit.remaining > 0 + } + + fn ensure_limit_in_map( + rate_limits_config: &RateLimits, + map: &mut HashMap, + limit_type: &LimitType, + ) { + log::trace!("Ensuring limit type {:?} is in the map.", limit_type); + let time: u64 = chrono::Utc::now().timestamp() as u64; + match limit_type { + LimitType::Channel(snowflake) => { + if map.get(&LimitType::Channel(*snowflake)).is_some() { + log::trace!( + "Limit type {:?} is already in the map. Returning.", + limit_type + ); + return; + } + log::trace!("Limit type {:?} is not in the map. Adding it.", limit_type); + let channel_limit = &rate_limits_config.routes.channel; + map.insert( + LimitType::Channel(*snowflake), + Limit { + bucket: LimitType::Channel(*snowflake), + limit: channel_limit.count, + remaining: channel_limit.count, + reset: channel_limit.window + time, + window: channel_limit.window, + }, + ); + } + LimitType::Guild(snowflake) => { + if map.get(&LimitType::Guild(*snowflake)).is_some() { + return; + } + let guild_limit = &rate_limits_config.routes.guild; + map.insert( + LimitType::Guild(*snowflake), + Limit { + bucket: LimitType::Guild(*snowflake), + limit: guild_limit.count, + remaining: guild_limit.count, + reset: guild_limit.window + time, + window: guild_limit.window, + }, + ); + } + LimitType::Webhook(snowflake) => { + if map.get(&LimitType::Webhook(*snowflake)).is_some() { + return; + } + let webhook_limit = &rate_limits_config.routes.webhook; + map.insert( + LimitType::Webhook(*snowflake), + Limit { + bucket: LimitType::Webhook(*snowflake), + limit: webhook_limit.count, + remaining: webhook_limit.count, + reset: webhook_limit.window + time, + window: webhook_limit.window, + }, + ); + } + other_limit => { + if map.get(other_limit).is_some() { + return; + } + let limits_map = ChorusRequest::limits_config_to_hashmap(rate_limits_config); + map.insert( + *other_limit, + Limit { + bucket: *other_limit, + limit: limits_map.get(other_limit).as_ref().unwrap().limit, + remaining: limits_map.get(other_limit).as_ref().unwrap().remaining, + reset: limits_map.get(other_limit).as_ref().unwrap().reset, + window: limits_map.get(other_limit).as_ref().unwrap().window, + }, + ); + } + } + } + + async fn interpret_error(response: reqwest::Response) -> ChorusError { + match response.status().as_u16() { + 401..=403 | 407 => ChorusError::NoPermission, + 404 => ChorusError::NotFound { + error: response.text().await.unwrap(), + }, + 405 | 408 | 409 => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap() }, + 411..=421 | 426 | 428 | 431 => ChorusError::InvalidArguments { + error: response.text().await.unwrap(), + }, + 429 => panic!("Illegal state: Rate limit exception should have been caught before this function call."), + 451 => ChorusError::NoResponse, + 500..=599 => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap() }, + _ => ChorusError::ReceivedErrorCode { error_code: response.status().as_u16(), error: response.text().await.unwrap()}, + } + } + + /// Updates the rate limits of the user. The following steps are performed: + /// 1. If the current unix timestamp is greater than the reset timestamp, the reset timestamp is + /// set to the current unix timestamp + the rate limit window. The remaining rate limit is + /// reset to the rate limit limit. + /// 2. The remaining rate limit is decreased by 1. + fn update_rate_limits(user: &mut UserMeta, limit_type: &LimitType, response_was_err: bool) { + let instance_dictated_limits = [ + &LimitType::AuthLogin, + &LimitType::AuthRegister, + &LimitType::Global, + &LimitType::Ip, + ]; + // modify this to store something to look up the value with later, instead of storing a reference to the actual data itself. + let mut relevant_limits = Vec::new(); + if instance_dictated_limits.contains(&limit_type) { + relevant_limits.push((LimitOrigin::Instance, *limit_type)); + } else { + relevant_limits.push((LimitOrigin::User, *limit_type)); + } + relevant_limits.push((LimitOrigin::Instance, LimitType::Global)); + relevant_limits.push((LimitOrigin::Instance, LimitType::Ip)); + if response_was_err { + relevant_limits.push((LimitOrigin::User, LimitType::Error)); + } + let time: u64 = chrono::Utc::now().timestamp() as u64; + for relevant_limit in relevant_limits.iter() { + let mut belongs_to = user.belongs_to.borrow_mut(); + let limit = match relevant_limit.0 { + LimitOrigin::Instance => { + log::trace!( + "Updating instance rate limit. Bucket: {:?}", + relevant_limit.1 + ); + belongs_to + .limits_information + .as_mut() + .unwrap() + .ratelimits + .get_mut(&relevant_limit.1) + .unwrap() + } + LimitOrigin::User => { + log::trace!("Updating user rate limit. Bucket: {:?}", relevant_limit.1); + user.limits + .as_mut() + .unwrap() + .get_mut(&relevant_limit.1) + .unwrap() + } + }; + if time > limit.reset { + // Spacebar does not yet return rate limit information in its response headers. We + // therefore have to guess the next rate limit window. This is not ideal. Oh well! + log::trace!("Rate limit replenished. Bucket: {:?}", limit.bucket); + limit.reset += limit.window; + limit.remaining = limit.limit; + } + limit.remaining -= 1; + } + } + + pub(crate) async fn get_limits_config(url_api: &str) -> ChorusResult { + let request = Client::new() + .get(format!("{}/policies/instance/limits/", url_api)) + .send() + .await; + let request = match request { + Ok(request) => request, + Err(e) => { + return Err(ChorusError::RequestFailed { + url: url_api.to_string(), + error: e, + }) + } + }; + let limits_configuration = match request.status().as_u16() { + 200 => from_str::(&request.text().await.unwrap()).unwrap(), + 429 => { + return Err(ChorusError::RateLimited { + bucket: format!("{:?}", LimitType::Ip), + }) + } + 404 => return Err(ChorusError::NotFound { error: "Route \"/policies/instance/limits/\" not found. Are you perhaps trying to request the Limits configuration from an unsupported server?".to_string() }), + 400..=u16::MAX => { + return Err(ChorusError::ReceivedErrorCode { error_code: request.status().as_u16(), error: request.text().await.unwrap() }) + } + _ => { + return Err(ChorusError::InvalidResponse { + error: request.text().await.unwrap(), + }) + } + }; + + Ok(limits_configuration) + } + + pub(crate) fn limits_config_to_hashmap( + limits_configuration: &RateLimits, + ) -> HashMap { + let config = limits_configuration.clone(); + let routes = config.routes; + let mut map: HashMap = HashMap::new(); + let time: u64 = chrono::Utc::now().timestamp() as u64; + map.insert( + LimitType::AuthLogin, + Limit { + bucket: LimitType::AuthLogin, + limit: routes.auth.login.count, + remaining: routes.auth.login.count, + reset: routes.auth.login.window + time, + window: routes.auth.login.window, + }, + ); + map.insert( + LimitType::AuthRegister, + Limit { + bucket: LimitType::AuthRegister, + limit: routes.auth.register.count, + remaining: routes.auth.register.count, + reset: routes.auth.register.window + time, + window: routes.auth.register.window, + }, + ); + map.insert( + LimitType::ChannelBaseline, + Limit { + bucket: LimitType::ChannelBaseline, + limit: routes.channel.count, + remaining: routes.channel.count, + reset: routes.channel.window + time, + window: routes.channel.window, + }, + ); + map.insert( + LimitType::Error, + Limit { + bucket: LimitType::Error, + limit: config.error.count, + remaining: config.error.count, + reset: config.error.window + time, + window: config.error.window, + }, + ); + map.insert( + LimitType::Global, + Limit { + bucket: LimitType::Global, + limit: config.global.count, + remaining: config.global.count, + reset: config.global.window + time, + window: config.global.window, + }, + ); + map.insert( + LimitType::Ip, + Limit { + bucket: LimitType::Ip, + limit: config.ip.count, + remaining: config.ip.count, + reset: config.ip.window + time, + window: config.ip.window, + }, + ); + map.insert( + LimitType::GuildBaseline, + Limit { + bucket: LimitType::GuildBaseline, + limit: routes.guild.count, + remaining: routes.guild.count, + reset: routes.guild.window + time, + window: routes.guild.window, + }, + ); + map.insert( + LimitType::WebhookBaseline, + Limit { + bucket: LimitType::WebhookBaseline, + limit: routes.webhook.count, + remaining: routes.webhook.count, + reset: routes.webhook.window + time, + window: routes.webhook.window, + }, + ); + map + } + + /// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains nothing if the request + /// was successful, or a [`ChorusError`] if the request failed. + pub(crate) async fn handle_request_as_result(self, user: &mut UserMeta) -> ChorusResult<()> { + match self.send_request(user).await { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + + /// Sends a [`ChorusRequest`] and returns a [`ChorusResult`] that contains a [`T`] if the request + /// was successful, or a [`ChorusError`] if the request failed. + pub(crate) async fn deserialize_response Deserialize<'a>>( + self, + user: &mut UserMeta, + ) -> ChorusResult { + let response = self.send_request(user).await?; + debug!("Got response: {:?}", response); + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to process the HTTP response into a String: {}", + e + ), + }); + } + }; + let object = match from_str::(&response_text) { + Ok(object) => object, + Err(e) => { + return Err(ChorusError::InvalidResponse { + error: format!( + "Error while trying to deserialize the JSON response into requested type T: {}. JSON Response: {}", + e, response_text + ), + }) + } + }; + Ok(object) + } +} + +enum LimitOrigin { + Instance, + User, +} diff --git a/src/types/config/types/general_configuration.rs b/src/types/config/types/general_configuration.rs index 07444b0..13b3aa8 100644 --- a/src/types/config/types/general_configuration.rs +++ b/src/types/config/types/general_configuration.rs @@ -18,10 +18,8 @@ pub struct GeneralConfiguration { impl Default for GeneralConfiguration { fn default() -> Self { Self { - instance_name: String::from("Spacebar Instance"), - instance_description: Some(String::from( - "This is a Spacebar instance made in the pre-release days", - )), + instance_name: String::from("Spacebar-compatible Instance"), + instance_description: Some(String::from("This is a spacebar-compatible instance.")), front_page: None, tos_page: None, correspondence_email: None, diff --git a/src/types/config/types/guild_configuration.rs b/src/types/config/types/guild_configuration.rs index a854460..96e6ea8 100644 --- a/src/types/config/types/guild_configuration.rs +++ b/src/types/config/types/guild_configuration.rs @@ -10,7 +10,7 @@ use sqlx::{ database::{HasArguments, HasValueRef}, encode::IsNull, error::BoxDynError, - Decode, Encode, MySql, + Decode, MySql, }; use crate::types::config::types::subconfigs::guild::{ @@ -139,7 +139,7 @@ pub enum GuildFeatures { InvitesClosed, } -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Eq)] pub struct GuildFeaturesList(Vec); impl Deref for GuildFeaturesList { diff --git a/src/types/config/types/subconfigs/limits/rates.rs b/src/types/config/types/subconfigs/limits/rates.rs index 9d0cab1..ce1ea60 100644 --- a/src/types/config/types/subconfigs/limits/rates.rs +++ b/src/types/config/types/subconfigs/limits/rates.rs @@ -1,7 +1,12 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; -use crate::types::config::types::subconfigs::limits::ratelimits::{ - route::RouteRateLimit, RateLimitOptions, +use crate::{ + api::LimitType, + types::config::types::subconfigs::limits::ratelimits::{ + route::RouteRateLimit, RateLimitOptions, + }, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -39,3 +44,18 @@ impl Default for RateLimits { } } } + +impl RateLimits { + pub fn to_hash_map(&self) -> HashMap { + let mut map = HashMap::new(); + map.insert(LimitType::AuthLogin, self.routes.auth.login.clone()); + map.insert(LimitType::AuthRegister, self.routes.auth.register.clone()); + map.insert(LimitType::ChannelBaseline, self.routes.channel.clone()); + map.insert(LimitType::Error, self.error.clone()); + map.insert(LimitType::Global, self.global.clone()); + map.insert(LimitType::Ip, self.ip.clone()); + map.insert(LimitType::WebhookBaseline, self.routes.webhook.clone()); + map.insert(LimitType::GuildBaseline, self.routes.guild.clone()); + map + } +} diff --git a/src/types/entities/channel.rs b/src/types/entities/channel.rs index 5471e22..154b83c 100644 --- a/src/types/entities/channel.rs +++ b/src/types/entities/channel.rs @@ -1,68 +1,70 @@ +use chorus_macros::Updateable; use chrono::Utc; use serde::{Deserialize, Serialize}; use serde_aux::prelude::deserialize_string_from_number; use serde_repr::{Deserialize_repr, Serialize_repr}; +use crate::gateway::Updateable; use crate::types::{ entities::{GuildMember, User}, utils::Snowflake, }; -#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Updateable)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Channel { - pub id: Snowflake, - pub created_at: Option>, - #[serde(rename = "type")] - pub channel_type: ChannelType, - pub guild_id: Option, - pub position: Option, - #[cfg(feature = "sqlx")] - pub permission_overwrites: Option>>, - #[cfg(not(feature = "sqlx"))] - pub permission_overwrites: Option>, - pub name: Option, - pub topic: Option, - pub nsfw: Option, - pub last_message_id: Option, - pub bitrate: Option, - pub user_limit: Option, - pub rate_limit_per_user: Option, - #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub recipients: Option>, - pub icon: Option, - pub owner_id: Option, pub application_id: Option, - pub managed: Option, - pub parent_id: Option, - pub last_pin_timestamp: Option, - pub rtc_region: Option, - pub video_quality_mode: Option, - pub message_count: Option, - pub member_count: Option, - #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub thread_metadata: Option, - #[cfg_attr(feature = "sqlx", sqlx(skip))] - pub member: Option, - pub default_auto_archive_duration: Option, - pub permissions: Option, - pub flags: Option, - pub total_message_sent: Option, - #[cfg(feature = "sqlx")] - pub available_tags: Option>>, - #[cfg(not(feature = "sqlx"))] - pub available_tags: Option>, #[cfg(feature = "sqlx")] pub applied_tags: Option>>, #[cfg(not(feature = "sqlx"))] pub applied_tags: Option>, #[cfg(feature = "sqlx")] + pub available_tags: Option>>, + #[cfg(not(feature = "sqlx"))] + pub available_tags: Option>, + pub bitrate: Option, + #[serde(rename = "type")] + pub channel_type: ChannelType, + pub created_at: Option>, + pub default_auto_archive_duration: Option, + pub default_forum_layout: Option, + #[cfg(feature = "sqlx")] pub default_reaction_emoji: Option>, #[cfg(not(feature = "sqlx"))] pub default_reaction_emoji: Option, - pub default_thread_rate_limit_per_user: Option, pub default_sort_order: Option, - pub default_forum_layout: Option, + pub default_thread_rate_limit_per_user: Option, + pub flags: Option, + pub guild_id: Option, + pub icon: Option, + pub id: Snowflake, + pub last_message_id: Option, + pub last_pin_timestamp: Option, + pub managed: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] + pub member: Option, + pub member_count: Option, + pub message_count: Option, + pub name: Option, + pub nsfw: Option, + pub owner_id: Option, + pub parent_id: Option, + #[cfg(feature = "sqlx")] + pub permission_overwrites: Option>>, + #[cfg(not(feature = "sqlx"))] + pub permission_overwrites: Option>, + pub permissions: Option, + pub position: Option, + pub rate_limit_per_user: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] + pub recipients: Option>, + pub rtc_region: Option, + #[cfg_attr(feature = "sqlx", sqlx(skip))] + pub thread_metadata: Option, + pub topic: Option, + pub total_message_sent: Option, + pub user_limit: Option, + pub video_quality_mode: Option, } #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] @@ -74,7 +76,7 @@ pub struct Tag { pub emoji_name: Option, } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd)] pub struct PermissionOverwrite { pub id: Snowflake, #[serde(rename = "type")] diff --git a/src/types/entities/emoji.rs b/src/types/entities/emoji.rs index dbe998b..a0e8d14 100644 --- a/src/types/entities/emoji.rs +++ b/src/types/entities/emoji.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use crate::types::entities::User; use crate::types::Snowflake; -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Default)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct Emoji { pub id: Option, diff --git a/src/types/entities/guild.rs b/src/types/entities/guild.rs index 857ed2a..67e7f44 100644 --- a/src/types/entities/guild.rs +++ b/src/types/entities/guild.rs @@ -91,7 +91,7 @@ pub struct Guild { } /// See https://docs.spacebar.chat/routes/#get-/guilds/-guild_id-/bans/-user- -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct GuildBan { pub user_id: Snowflake, diff --git a/src/types/entities/invite.rs b/src/types/entities/invite.rs new file mode 100644 index 0000000..6d7b570 --- /dev/null +++ b/src/types/entities/invite.rs @@ -0,0 +1,75 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::types::{Snowflake, WelcomeScreenObject}; + +use super::guild::GuildScheduledEvent; +use super::{Application, Channel, GuildMember, User}; + +/// Represents a code that when used, adds a user to a guild or group DM channel, or creates a relationship between two users. +/// See +#[derive(Debug, Serialize, Deserialize)] +pub struct Invite { + pub approximate_member_count: Option, + pub approximate_presence_count: Option, + pub channel: Option, + pub code: String, + pub created_at: Option>, + pub expires_at: Option>, + pub flags: Option, + pub guild: Option, + pub guild_id: Option, + pub guild_scheduled_event: Option, + #[serde(rename = "type")] + pub invite_type: Option, + pub inviter: Option, + pub max_age: Option, + pub max_uses: Option, + pub stage_instance: Option, + pub target_application: Option, + pub target_type: Option, + pub target_user: Option, + pub temporary: Option, + pub uses: Option, +} + +/// The guild an invite is for. +/// See +#[derive(Debug, Serialize, Deserialize)] +pub struct InviteGuild { + pub id: Snowflake, + pub name: String, + pub icon: Option, + pub splash: Option, + pub verification_level: i32, + pub features: Vec, + pub vanity_url_code: Option, + pub description: Option, + pub banner: Option, + pub premium_subscription_count: Option, + #[serde(rename = "nsfw")] + #[serde(skip_serializing_if = "Option::is_none")] + pub nsfw_deprecated: Option, + pub nsfw_level: NSFWLevel, + pub welcome_screen: Option, +} + +/// See for an explanation on what +/// the levels mean. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum NSFWLevel { + Default = 0, + Explicit = 1, + Safe = 2, + AgeRestricted = 3, +} + +/// See +#[derive(Debug, Serialize, Deserialize)] +pub struct InviteStageInstance { + pub members: Vec, + pub participant_count: i32, + pub speaker_count: i32, + pub topic: String, +} diff --git a/src/types/entities/mod.rs b/src/types/entities/mod.rs index b63bb9a..6f1f58b 100644 --- a/src/types/entities/mod.rs +++ b/src/types/entities/mod.rs @@ -8,6 +8,7 @@ pub use emoji::*; pub use guild::*; pub use guild_member::*; pub use integration::*; +pub use invite::*; pub use message::*; pub use relationship::*; pub use role::*; @@ -31,6 +32,7 @@ mod emoji; mod guild; mod guild_member; mod integration; +mod invite; mod message; mod relationship; mod role; diff --git a/src/types/entities/user.rs b/src/types/entities/user.rs index d0059fc..469c45e 100644 --- a/src/types/entities/user.rs +++ b/src/types/entities/user.rs @@ -1,9 +1,8 @@ +use crate::types::utils::Snowflake; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_aux::prelude::deserialize_option_number_from_string; -use crate::types::utils::Snowflake; - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "sqlx", derive(sqlx::Type))] pub struct UserData { @@ -16,7 +15,6 @@ impl User { PublicUser::from(self) } } - #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] pub struct User { @@ -45,9 +43,9 @@ pub struct User { pub bio: Option, pub theme_colors: Option>, pub phone: Option, - pub nsfw_allowed: bool, - pub premium: bool, - pub purchased_flags: i32, + pub nsfw_allowed: Option, + pub premium: Option, + pub purchased_flags: Option, pub premium_usage_flags: Option, pub disabled: Option, } @@ -89,6 +87,7 @@ impl From for PublicUser { } } +#[allow(dead_code)] // FIXME: Remove this when we actually use this const CUSTOM_USER_FLAG_OFFSET: u64 = 1 << 32; bitflags::bitflags! { diff --git a/src/types/events/channel.rs b/src/types/events/channel.rs index 99c7640..b595d57 100644 --- a/src/types/events/channel.rs +++ b/src/types/events/channel.rs @@ -3,6 +3,8 @@ use crate::types::{entities::Channel, Snowflake}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use super::UpdateMessage; + #[derive(Debug, Default, Deserialize, Serialize)] /// See https://discord.com/developers/docs/topics/gateway-events#channel-pins-update pub struct ChannelPinsUpdate { @@ -31,6 +33,15 @@ pub struct ChannelUpdate { impl WebSocketEvent for ChannelUpdate {} +impl UpdateMessage for ChannelUpdate { + fn update(&self, object_to_update: &mut Channel) { + *object_to_update = self.channel.clone(); + } + fn id(&self) -> Snowflake { + self.channel.id + } +} + #[derive(Debug, Default, Deserialize, Serialize, Clone)] /// Officially undocumented. /// Sends updates to client about a new message with its id diff --git a/src/types/events/hello.rs b/src/types/events/hello.rs index e370f9c..44f1e4f 100644 --- a/src/types/events/hello.rs +++ b/src/types/events/hello.rs @@ -14,8 +14,7 @@ impl WebSocketEvent for GatewayHello {} /// Contains info on how often the client should send heartbeats to the server; pub struct HelloData { /// How often a client should send heartbeats, in milliseconds - // u128 because std used u128s for milliseconds - pub heartbeat_interval: u128, + pub heartbeat_interval: u64, } impl WebSocketEvent for HelloData {} diff --git a/src/types/events/mod.rs b/src/types/events/mod.rs index e3547e9..26dcd80 100644 --- a/src/types/events/mod.rs +++ b/src/types/events/mod.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; +use crate::gateway::Updateable; pub use application::*; pub use auto_moderation::*; pub use call::*; @@ -25,9 +26,10 @@ pub use thread::*; pub use user::*; pub use voice::*; pub use webhooks::*; - pub use webrtc::*; +use super::Snowflake; + mod application; mod auto_moderation; mod call; @@ -99,3 +101,23 @@ pub struct GatewayReceivePayload<'a> { } impl<'a> WebSocketEvent for GatewayReceivePayload<'a> {} + +/// An [`UpdateMessage`] represents a received Gateway Message which contains updated +/// information for an [`Updateable`] of Type T. +/// # Example: +/// ```rs +/// impl UpdateMessage for ChannelUpdate { +/// fn update(...) {...} +/// fn id(...) {...} +/// } +/// ``` +/// This would imply, that the [`WebSocketEvent`] "[`ChannelUpdate`]" contains new/updated information +/// about a [`Channel`]. The update method describes how this new information will be turned into +/// a [`Channel`] object. +pub(crate) trait UpdateMessage: Clone +where + T: Updateable, +{ + fn update(&self, object_to_update: &mut T); + fn id(&self) -> Snowflake; +} diff --git a/src/types/schema/auth.rs b/src/types/schema/auth.rs index 8b8a601..9a3b9d6 100644 --- a/src/types/schema/auth.rs +++ b/src/types/schema/auth.rs @@ -1,122 +1,8 @@ -use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::errors::FieldFormatError; - -/** -A struct that represents a well-formed email address. - */ -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct AuthEmail { - pub email: String, -} - -impl AuthEmail { - /** - Returns a new [`Result`]. - ## Arguments - The email address you want to validate. - ## Errors - You will receive a [`FieldFormatError`], if: - - The email address is not in a valid format. - - */ - pub fn new(email: String) -> Result { - let regex = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); - if !regex.is_match(email.as_str()) { - return Err(FieldFormatError::EmailError); - } - Ok(AuthEmail { email }) - } -} - -/** -A struct that represents a well-formed username. -## Arguments -Please use new() to create a new instance of this struct. -## Errors -You will receive a [`FieldFormatError`], if: -- The username is not between 2 and 32 characters. - */ -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct AuthUsername { - pub username: String, -} - -impl AuthUsername { - /** - Returns a new [`Result`]. - ## Arguments - The username you want to validate. - ## Errors - You will receive a [`FieldFormatError`], if: - - The username is not between 2 and 32 characters. - */ - pub fn new(username: String) -> Result { - if username.len() < 2 || username.len() > 32 { - Err(FieldFormatError::UsernameError) - } else { - Ok(AuthUsername { username }) - } - } -} - -/** -A struct that represents a well-formed password. -## Arguments -Please use new() to create a new instance of this struct. -## Errors -You will receive a [`FieldFormatError`], if: -- The password is not between 1 and 72 characters. - */ -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct AuthPassword { - pub password: String, -} - -impl AuthPassword { - /** - Returns a new [`Result`]. - ## Arguments - The password you want to validate. - ## Errors - You will receive a [`FieldFormatError`], if: - - The password is not between 1 and 72 characters. - */ - pub fn new(password: String) -> Result { - if password.is_empty() || password.len() > 72 { - Err(FieldFormatError::PasswordError) - } else { - Ok(AuthPassword { password }) - } - } -} - -/** -A struct that represents a well-formed register request. -## Arguments -Please use new() to create a new instance of this struct. -## Errors -You will receive a [`FieldFormatError`], if: -- The username is not between 2 and 32 characters. -- The password is not between 1 and 72 characters. - */ -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct RegisterSchema { - username: String, - password: Option, - consent: bool, - email: Option, - fingerprint: Option, - invite: Option, - date_of_birth: Option, - gift_code_sku_id: Option, - captcha_key: Option, - promotional_email_opt_in: Option, -} - -pub struct RegisterSchemaOptions { pub username: String, pub password: Option, pub consent: bool, @@ -129,123 +15,21 @@ pub struct RegisterSchemaOptions { pub promotional_email_opt_in: Option, } -impl RegisterSchema { - pub fn builder(username: impl Into, consent: bool) -> RegisterSchemaOptions { - RegisterSchemaOptions { - username: username.into(), - password: None, - consent, - email: None, - fingerprint: None, - invite: None, - date_of_birth: None, - gift_code_sku_id: None, - captcha_key: None, - promotional_email_opt_in: None, - } - } -} - -impl RegisterSchemaOptions { - /** - Create a new [`RegisterSchema`]. - ## Arguments - All but "String::username" and "bool::consent" are optional. - - ## Errors - You will receive a [`FieldFormatError`], if: - - The username is less than 2 or more than 32 characters in length - - You supply a `password` which is less than 1 or more than 72 characters in length. - - These constraints have been defined [in the Spacebar-API](https://docs.spacebar.chat/routes/) - */ - pub fn build(self) -> Result { - let username = AuthUsername::new(self.username)?.username; - - let email = if let Some(email) = self.email { - Some(AuthEmail::new(email)?.email) - } else { - None - }; - - let password = if let Some(password) = self.password { - Some(AuthPassword::new(password)?.password) - } else { - None - }; - - if !self.consent { - return Err(FieldFormatError::ConsentError); - } - - Ok(RegisterSchema { - username, - password, - consent: self.consent, - email, - fingerprint: self.fingerprint, - invite: self.invite, - date_of_birth: self.date_of_birth, - gift_code_sku_id: self.gift_code_sku_id, - captcha_key: self.captcha_key, - promotional_email_opt_in: self.promotional_email_opt_in, - }) - } -} - -/** -A struct that represents a well-formed login request. -## Arguments -Please use new() to create a new instance of this struct. -## Errors -You will receive a [`FieldFormatError`], if: -- The username is not between 2 and 32 characters. -- The password is not between 1 and 72 characters. - */ #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub struct LoginSchema { + /// For Discord, usernames must be between 2 and 32 characters, + /// but other servers may have different limits. pub login: String, - pub password: Option, + /// For Discord, must be between 1 and 72 characters, + /// but other servers may have different limits. + pub password: String, pub undelete: Option, pub captcha_key: Option, pub login_source: Option, pub gift_code_sku_id: Option, } -impl LoginSchema { - /** - Returns a new [`Result`]. - ## Arguments - login: The username you want to login with. - password: The password you want to login with. - undelete: Honestly no idea what this is for. - captcha_key: The captcha key you want to login with. - login_source: The login source. - gift_code_sku_id: The gift code sku id. - ## Errors - You will receive a [`FieldFormatError`], if: - - The username is less than 2 or more than 32 characters in length - */ - pub fn new( - login: String, - password: Option, - undelete: Option, - captcha_key: Option, - login_source: Option, - gift_code_sku_id: Option, - ) -> Result { - Ok(LoginSchema { - login, - password, - undelete, - captcha_key, - login_source, - gift_code_sku_id, - }) - } -} - #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct TotpSchema { diff --git a/src/types/schema/channel.rs b/src/types/schema/channel.rs index 2a78142..27c78ee 100644 --- a/src/types/schema/channel.rs +++ b/src/types/schema/channel.rs @@ -1,8 +1,9 @@ +use bitflags::bitflags; use serde::{Deserialize, Serialize}; use crate::types::{entities::PermissionOverwrite, Snowflake}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Default, PartialEq, PartialOrd)] #[serde(rename_all = "snake_case")] pub struct ChannelCreateSchema { pub name: String, @@ -26,7 +27,7 @@ pub struct ChannelCreateSchema { pub video_quality_mode: Option, } -#[derive(Debug, Deserialize, Serialize, Clone, Default)] +#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialEq, PartialOrd)] #[serde(rename_all = "snake_case")] pub struct ChannelModifySchema { pub name: Option, @@ -48,7 +49,7 @@ pub struct ChannelModifySchema { pub video_quality_mode: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] pub struct GetChannelMessagesSchema { /// Between 1 and 100, defaults to 50. pub limit: Option, @@ -56,7 +57,7 @@ pub struct GetChannelMessagesSchema { pub anchor: ChannelMessagesAnchor, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] #[serde(rename_all = "snake_case")] pub enum ChannelMessagesAnchor { Before(Snowflake), @@ -94,3 +95,56 @@ impl GetChannelMessagesSchema { } } } + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, PartialOrd)] +pub struct CreateChannelInviteSchema { + pub flags: Option, + pub max_age: Option, + pub max_uses: Option, + pub temporary: Option, + pub unique: Option, + pub validate: Option, + pub target_type: Option, + pub target_user_id: Option, + pub target_application_id: Option, +} + +impl Default for CreateChannelInviteSchema { + fn default() -> Self { + Self { + flags: None, + max_age: Some(86400), + max_uses: Some(0), + temporary: Some(false), + unique: Some(false), + validate: None, + target_type: None, + target_user_id: None, + target_application_id: None, + } + } +} + +bitflags! { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)] + pub struct InviteFlags: u64 { + const GUEST = 1 << 0; + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InviteType { + #[default] + Stream = 1, + EmbeddedApplication = 2, + RoleSubscriptions = 3, + CreatorPage = 4, +} + +/// See +#[derive(Debug, Deserialize, Serialize, Clone, Default, PartialOrd, Ord, PartialEq, Eq)] +pub struct AddChannelRecipientSchema { + pub access_token: Option, + pub nick: Option, +} diff --git a/src/types/schema/mod.rs b/src/types/schema/mod.rs index 1069428..08dae05 100644 --- a/src/types/schema/mod.rs +++ b/src/types/schema/mod.rs @@ -15,76 +15,3 @@ mod message; mod relationship; mod role; mod user; - -#[cfg(test)] -mod schemas_tests { - use crate::errors::FieldFormatError; - - use super::*; - - #[test] - fn password_too_short() { - assert_eq!( - AuthPassword::new("".to_string()), - Err(FieldFormatError::PasswordError) - ); - } - - #[test] - fn password_too_long() { - let mut long_pw = String::new(); - for _ in 0..73 { - long_pw += "a"; - } - assert_eq!( - AuthPassword::new(long_pw), - Err(FieldFormatError::PasswordError) - ); - } - - #[test] - fn username_too_short() { - assert_eq!( - AuthUsername::new("T".to_string()), - Err(FieldFormatError::UsernameError) - ); - } - - #[test] - fn username_too_long() { - let mut long_un = String::new(); - for _ in 0..33 { - long_un += "a"; - } - assert_eq!( - AuthUsername::new(long_un), - Err(FieldFormatError::UsernameError) - ); - } - - #[test] - fn consent_false() { - assert_eq!( - RegisterSchema::builder("Test", false).build(), - Err(FieldFormatError::ConsentError) - ); - } - - #[test] - fn invalid_email() { - assert_eq!( - AuthEmail::new("p@p.p".to_string()), - Err(FieldFormatError::EmailError) - ) - } - - #[test] - fn valid_email() { - let reg = RegisterSchemaOptions { - email: Some("me@mail.de".to_string()), - ..RegisterSchema::builder("Testy", true) - } - .build(); - assert_ne!(reg, Err(FieldFormatError::EmailError)); - } -} diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index ab85a16..c8cf5bb 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -1,5 +1,9 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; +use crate::types::Snowflake; + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub struct UserModifySchema { @@ -14,3 +18,17 @@ pub struct UserModifySchema { pub email: Option, pub discriminator: Option, } + +/// # Attributes: +/// - recipients: The users to include in the private channel +/// - access_tokens: The access tokens of users that have granted your app the `gdm.join` scope. Only usable for OAuth2 requests (which can only create group DMs). +/// - nicks: A mapping of user IDs to their respective nicknames. Only usable for OAuth2 requests (which can only create group DMs). +/// +/// # Reference: +/// Read: +#[derive(Debug, Deserialize, Serialize)] +pub struct PrivateChannelCreateSchema { + pub recipients: Option>, + pub access_tokens: Option>, + pub nicks: Option>, +} diff --git a/src/types/utils/rights.rs b/src/types/utils/rights.rs index 0198af6..fecf268 100644 --- a/src/types/utils/rights.rs +++ b/src/types/utils/rights.rs @@ -73,6 +73,7 @@ impl Rights { } } +#[allow(dead_code)] // FIXME: Remove this when we use this fn all_rights() -> Rights { Rights::OPERATOR | Rights::MANAGE_APPLICATIONS diff --git a/src/types/utils/snowflake.rs b/src/types/utils/snowflake.rs index 8502275..77514a1 100644 --- a/src/types/utils/snowflake.rs +++ b/src/types/utils/snowflake.rs @@ -12,7 +12,7 @@ const EPOCH: i64 = 1420070400000; /// Unique identifier including a timestamp. /// See https://discord.com/developers/docs/reference#snowflakes -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "sqlx", derive(Type))] #[cfg_attr(feature = "sqlx", sqlx(transparent))] pub struct Snowflake(u64); diff --git a/tests/auth.rs b/tests/auth.rs index 6972ace..c26552f 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -1,16 +1,16 @@ -use chorus::types::{RegisterSchema, RegisterSchemaOptions}; +use chorus::types::RegisterSchema; mod common; #[tokio::test] async fn test_registration() { let mut bundle = common::setup().await; - let reg = RegisterSchemaOptions { + let reg = RegisterSchema { + username: "Hiiii".into(), date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("Hiiii", true) - } - .build() - .unwrap(); + consent: true, + ..Default::default() + }; bundle.instance.register_account(®).await.unwrap(); common::teardown(bundle).await; } diff --git a/tests/channel.rs b/tests/channels.rs similarity index 52% rename from tests/channel.rs rename to tests/channels.rs index 06a3330..c8564d7 100644 --- a/tests/channel.rs +++ b/tests/channels.rs @@ -1,6 +1,6 @@ use chorus::types::{ self, Channel, GetChannelMessagesSchema, MessageSendSchema, PermissionFlags, - PermissionOverwrite, Snowflake, + PermissionOverwrite, PrivateChannelCreateSchema, RelationshipType, Snowflake, }; mod common; @@ -28,10 +28,11 @@ async fn delete_channel() { #[tokio::test] async fn modify_channel() { + const CHANNEL_NAME: &str = "beepboop"; let mut bundle = common::setup().await; let channel = &mut bundle.channel; let modify_data: types::ChannelModifySchema = types::ChannelModifySchema { - name: Some("beepboop".to_string()), + name: Some(CHANNEL_NAME.to_string()), channel_type: None, topic: None, icon: None, @@ -49,10 +50,10 @@ async fn modify_channel() { default_thread_rate_limit_per_user: None, video_quality_mode: None, }; - Channel::modify(channel, modify_data, channel.id, &mut bundle.user) + let modified_channel = Channel::modify(channel, modify_data, channel.id, &mut bundle.user) .await .unwrap(); - assert_eq!(channel.name, Some("beepboop".to_string())); + assert_eq!(modified_channel.name, Some(CHANNEL_NAME.to_string())); let permission_override = PermissionFlags::from_vec(Vec::from([ PermissionFlags::MANAGE_CHANNELS, @@ -89,12 +90,11 @@ async fn get_channel_messages() { let _ = bundle .user .send_message( - &mut MessageSendSchema { + MessageSendSchema { content: Some("A Message!".to_string()), ..Default::default() }, bundle.channel.id, - None, ) .await .unwrap(); @@ -136,3 +136,81 @@ async fn get_channel_messages() { common::teardown(bundle).await } + +#[tokio::test] +async fn create_dm() { + let mut bundle = common::setup().await; + let other_user = bundle.create_user("integrationtestuser2").await; + let user = &mut bundle.user; + let private_channel_create_schema = PrivateChannelCreateSchema { + recipients: Some(Vec::from([other_user.object.id])), + access_tokens: None, + nicks: None, + }; + let dm_channel = user + .create_private_channel(private_channel_create_schema) + .await + .unwrap(); + assert!(dm_channel.recipients.is_some()); + assert_eq!( + dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id, + other_user.object.id + ); + assert_eq!( + dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id, + user.object.id + ); + common::teardown(bundle).await; +} + +// #[tokio::test] +// This test currently is broken due to an issue with the Spacebar Server. +#[allow(dead_code)] +async fn remove_add_person_from_to_dm() { + let mut bundle = common::setup().await; + let mut other_user = bundle.create_user("integrationtestuser2").await; + let mut third_user = bundle.create_user("integrationtestuser3").await; + let user = &mut bundle.user; + let private_channel_create_schema = PrivateChannelCreateSchema { + recipients: Some(Vec::from([other_user.object.id, third_user.object.id])), + access_tokens: None, + nicks: None, + }; + let dm_channel = user + .create_private_channel(private_channel_create_schema) + .await + .unwrap(); // Creates the Channel and stores the response Channel object + dm_channel + .remove_channel_recipient(other_user.object.id, user) + .await + .unwrap(); + assert!(dm_channel.recipients.as_ref().unwrap().get(1).is_none()); + other_user + .modify_user_relationship(user.object.id, RelationshipType::Friends) + .await + .unwrap(); + user.modify_user_relationship(other_user.object.id, RelationshipType::Friends) + .await + .unwrap(); + third_user + .modify_user_relationship(user.object.id, RelationshipType::Friends) + .await + .unwrap(); + user.modify_user_relationship(third_user.object.id, RelationshipType::Friends) + .await + .unwrap(); + // Users 1-2 and 1-3 are now friends + dm_channel + .add_channel_recipient(other_user.object.id, user, None) + .await + .unwrap(); + assert!(dm_channel.recipients.is_some()); + assert_eq!( + dm_channel.recipients.as_ref().unwrap().get(0).unwrap().id, + other_user.object.id + ); + assert_eq!( + dm_channel.recipients.as_ref().unwrap().get(1).unwrap().id, + user.object.id + ); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 900e31e..747a8cd 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,15 +1,16 @@ +use chorus::gateway::Gateway; use chorus::{ - errors::ChorusResult, instance::{Instance, UserMeta}, types::{ Channel, ChannelCreateSchema, Guild, GuildCreateSchema, RegisterSchema, - RegisterSchemaOptions, RoleCreateModifySchema, RoleObject, + RoleCreateModifySchema, RoleObject, }, UrlBundle, }; +#[allow(dead_code)] #[derive(Debug)] -pub struct TestBundle { +pub(crate) struct TestBundle { pub urls: UrlBundle, pub user: UserMeta, pub instance: Instance, @@ -18,21 +19,47 @@ pub struct TestBundle { pub channel: Channel, } +#[allow(unused)] +impl TestBundle { + pub(crate) async fn create_user(&mut self, username: &str) -> UserMeta { + let register_schema = RegisterSchema { + username: username.to_string(), + consent: true, + date_of_birth: Some("2000-01-01".to_string()), + ..Default::default() + }; + self.instance + .register_account(®ister_schema) + .await + .unwrap() + } + pub(crate) async fn clone_user_without_gateway(&self) -> UserMeta { + UserMeta { + belongs_to: self.user.belongs_to.clone(), + token: self.user.token.clone(), + limits: self.user.limits.clone(), + settings: self.user.settings.clone(), + object: self.user.object.clone(), + gateway: Gateway::new(self.instance.urls.wss.clone()).await.unwrap(), + } + } +} + // Set up a test by creating an Instance and a User. Reduces Test boilerplate. -pub async fn setup() -> TestBundle { +pub(crate) async fn setup() -> TestBundle { let urls = UrlBundle::new( "http://localhost:3001/api".to_string(), "ws://localhost:3001".to_string(), "http://localhost:3001".to_string(), ); - let mut instance = Instance::new(urls.clone()).await.unwrap(); + let mut instance = Instance::new(urls.clone(), true).await.unwrap(); // Requires the existance of the below user. - let reg = RegisterSchemaOptions { + let reg = RegisterSchema { + username: "integrationtestuser".into(), + consent: true, date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("integrationtestuser", true) - } - .build() - .unwrap(); + ..Default::default() + }; let guild_create_schema = GuildCreateSchema { name: Some("Test-Guild!".to_string()), region: None, @@ -94,7 +121,7 @@ pub async fn setup() -> TestBundle { // Teardown method to clean up after a test. #[allow(dead_code)] -pub async fn teardown(mut bundle: TestBundle) { +pub(crate) async fn teardown(mut bundle: TestBundle) { Guild::delete(&mut bundle.user, bundle.guild.id) .await .unwrap(); diff --git a/tests/gateway.rs b/tests/gateway.rs index c6f46dd..21a2018 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -1,13 +1,15 @@ mod common; + use chorus::gateway::*; -use chorus::types; +use chorus::types::{self, Channel}; #[tokio::test] /// Tests establishing a connection (hello and heartbeats) on the local gateway; async fn test_gateway_establish() { let bundle = common::setup().await; - Gateway::new(bundle.urls.wss).await.unwrap(); + Gateway::new(bundle.urls.wss.clone()).await.unwrap(); + common::teardown(bundle).await } #[tokio::test] @@ -15,10 +17,30 @@ async fn test_gateway_establish() { async fn test_gateway_authenticate() { let bundle = common::setup().await; - let gateway = Gateway::new(bundle.urls.wss).await.unwrap(); + let gateway = Gateway::new(bundle.urls.wss.clone()).await.unwrap(); let mut identify = types::GatewayIdentifyPayload::common(); - identify.token = bundle.user.token; + identify.token = bundle.user.token.clone(); gateway.send_identify(identify).await; + common::teardown(bundle).await +} + +#[tokio::test] +async fn test_self_updating_structs() { + let mut bundle = common::setup().await; + let channel_updater = bundle.user.gateway.observe(bundle.channel.clone()).await; + let received_channel = channel_updater.borrow().clone(); + assert_eq!(received_channel, bundle.channel); + let channel = &mut bundle.channel; + let modify_data = types::ChannelModifySchema { + name: Some("beepboop".to_string()), + ..Default::default() + }; + Channel::modify(channel, modify_data, channel.id, &mut bundle.user) + .await + .unwrap(); + let received_channel = channel_updater.borrow(); + assert_eq!(received_channel.name.as_ref().unwrap(), "beepboop"); + common::teardown(bundle).await } diff --git a/tests/guild.rs b/tests/guilds.rs similarity index 100% rename from tests/guild.rs rename to tests/guilds.rs diff --git a/tests/invites.rs b/tests/invites.rs new file mode 100644 index 0000000..d19be61 --- /dev/null +++ b/tests/invites.rs @@ -0,0 +1,23 @@ +mod common; +use chorus::types::CreateChannelInviteSchema; +#[tokio::test] +async fn create_accept_invite() { + let mut bundle = common::setup().await; + let channel = bundle.channel.clone(); + let mut other_user = bundle.create_user("testuser1312").await; + let user = &mut bundle.user; + let create_channel_invite_schema = CreateChannelInviteSchema::default(); + assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user) + .await + .is_err()); + let invite = user + .create_guild_invite(create_channel_invite_schema, channel.id) + .await + .unwrap(); + + other_user.accept_invite(&invite.code, None).await.unwrap(); + assert!(chorus::types::Guild::get(bundle.guild.id, &mut other_user) + .await + .is_ok()); + common::teardown(bundle).await; +} diff --git a/tests/member.rs b/tests/members.rs similarity index 100% rename from tests/member.rs rename to tests/members.rs diff --git a/tests/message.rs b/tests/messages.rs similarity index 82% rename from tests/message.rs rename to tests/messages.rs index 94f3734..af6ee5b 100644 --- a/tests/message.rs +++ b/tests/messages.rs @@ -8,13 +8,13 @@ mod common; #[tokio::test] async fn send_message() { let mut bundle = common::setup().await; - let mut message = types::MessageSendSchema { + let message = types::MessageSendSchema { content: Some("A Message!".to_string()), ..Default::default() }; let _ = bundle .user - .send_message(&mut message, bundle.channel.id, None) + .send_message(message, bundle.channel.id) .await .unwrap(); common::teardown(bundle).await @@ -45,7 +45,7 @@ async fn send_message_attachment() { content: buffer, }; - let mut message = types::MessageSendSchema { + let message = types::MessageSendSchema { content: Some("trans rights now".to_string()), attachments: Some(vec![attachment.clone()]), ..Default::default() @@ -55,11 +55,7 @@ async fn send_message_attachment() { let _arg = Some(&vec_attach); bundle .user - .send_message( - &mut message, - bundle.channel.id, - Some(vec![attachment.clone()]), - ) + .send_message(message, bundle.channel.id) .await .unwrap(); common::teardown(bundle).await diff --git a/tests/relationships.rs b/tests/relationships.rs index 81f3230..00ef9cf 100644 --- a/tests/relationships.rs +++ b/tests/relationships.rs @@ -1,25 +1,17 @@ -use chorus::types::{self, RegisterSchema, RegisterSchemaOptions, Relationship, RelationshipType}; +use chorus::types::{self, Relationship, RelationshipType}; mod common; #[tokio::test] async fn test_get_mutual_relationships() { - let register_schema = RegisterSchemaOptions { - date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("integrationtestuser2", true) - } - .build() - .unwrap(); - let mut bundle = common::setup().await; - let belongs_to = &mut bundle.instance; + let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap(); let friend_request_schema = types::FriendRequestSendSchema { username: user.object.username.clone(), discriminator: Some(user.object.discriminator.clone()), }; - other_user.send_friend_request(friend_request_schema).await; + let _ = other_user.send_friend_request(friend_request_schema).await; let relationships = user .get_mutual_relationships(other_user.object.id) .await @@ -30,22 +22,17 @@ async fn test_get_mutual_relationships() { #[tokio::test] async fn test_get_relationships() { - let register_schema = RegisterSchemaOptions { - date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("integrationtestuser2", true) - } - .build() - .unwrap(); - let mut bundle = common::setup().await; - let belongs_to = &mut bundle.instance; + let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap(); let friend_request_schema = types::FriendRequestSendSchema { username: user.object.username.clone(), discriminator: Some(user.object.discriminator.clone()), }; - other_user.send_friend_request(friend_request_schema).await; + other_user + .send_friend_request(friend_request_schema) + .await + .unwrap(); let relationships = user.get_relationships().await.unwrap(); assert_eq!(relationships.get(0).unwrap().id, other_user.object.id); common::teardown(bundle).await @@ -53,18 +40,10 @@ async fn test_get_relationships() { #[tokio::test] async fn test_modify_relationship_friends() { - let register_schema = RegisterSchemaOptions { - date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("integrationtestuser2", true) - } - .build() - .unwrap(); - let mut bundle = common::setup().await; - let belongs_to = &mut bundle.instance; + let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap(); - other_user + let _ = other_user .modify_user_relationship(user.object.id, types::RelationshipType::Friends) .await; let relationships = user.get_relationships().await.unwrap(); @@ -79,7 +58,8 @@ async fn test_modify_relationship_friends() { relationships.get(0).unwrap().relationship_type, RelationshipType::Outgoing ); - user.modify_user_relationship(other_user.object.id, RelationshipType::Friends) + let _ = user + .modify_user_relationship(other_user.object.id, RelationshipType::Friends) .await; assert_eq!( other_user @@ -91,7 +71,7 @@ async fn test_modify_relationship_friends() { .relationship_type, RelationshipType::Friends ); - user.remove_relationship(other_user.object.id).await; + let _ = user.remove_relationship(other_user.object.id).await; assert_eq!( other_user.get_relationships().await.unwrap(), Vec::::new() @@ -101,18 +81,10 @@ async fn test_modify_relationship_friends() { #[tokio::test] async fn test_modify_relationship_block() { - let register_schema = RegisterSchemaOptions { - date_of_birth: Some("2000-01-01".to_string()), - ..RegisterSchema::builder("integrationtestuser2", true) - } - .build() - .unwrap(); - let mut bundle = common::setup().await; - let belongs_to = &mut bundle.instance; + let mut other_user = bundle.create_user("integrationtestuser2").await; let user = &mut bundle.user; - let mut other_user = belongs_to.register_account(®ister_schema).await.unwrap(); - other_user + let _ = other_user .modify_user_relationship(user.object.id, types::RelationshipType::Blocked) .await; let relationships = user.get_relationships().await.unwrap(); @@ -123,7 +95,7 @@ async fn test_modify_relationship_block() { relationships.get(0).unwrap().relationship_type, RelationshipType::Blocked ); - other_user.remove_relationship(user.object.id).await; + let _ = other_user.remove_relationship(user.object.id).await; assert_eq!( other_user.get_relationships().await.unwrap(), Vec::::new() From b2d125104a2f80e4b749c855bd9bf7e307bfd5a6 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:04:16 +0200 Subject: [PATCH 06/72] Same allow as for voice as normal gateway --- src/voice.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/voice.rs b/src/voice.rs index 4e9eb70..1af8747 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -408,6 +408,7 @@ impl VoiceGateway { } /// Handles sending heartbeats to the voice gateway in another thread +#[allow(dead_code)] // FIXME: Remove this, once all fields of VoiceHeartbeatHandler are used struct VoiceHeartbeatHandler { /// The heartbeat interval in milliseconds pub heartbeat_interval: Duration, From 3b3ba4f3cf9d155c91a41b9af685c3e9e53851ed Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:44:47 +0200 Subject: [PATCH 07/72] Test error observer --- src/errors.rs | 7 ++++++- src/gateway.rs | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index eebfae3..26cc6b2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -2,6 +2,8 @@ use custom_error::custom_error; use reqwest::Error; +use crate::types::WebSocketEvent; + custom_error! { #[derive(PartialEq, Eq)] pub RegistrationError @@ -54,9 +56,10 @@ custom_error! { /// Supposed to be sent as numbers, though they are sent as string most of the time? /// /// Also includes errors when initiating a connection and unexpected opcodes - #[derive(PartialEq, Eq)] + #[derive(PartialEq, Eq, Default)] pub GatewayError // Errors we have received from the gateway + #[default] Unknown = "We're not sure what went wrong. Try reconnecting?", UnknownOpcode = "You sent an invalid Gateway opcode or an invalid payload for an opcode", Decode = "Gateway server couldn't decode payload", @@ -79,3 +82,5 @@ custom_error! { // Other misc errors UnexpectedOpcodeReceived{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", } + +impl WebSocketEvent for GatewayError {} diff --git a/src/gateway.rs b/src/gateway.rs index 68205b5..6913e6a 100644 --- a/src/gateway.rs +++ b/src/gateway.rs @@ -481,13 +481,15 @@ impl Gateway { return; } - // Todo: handle errors in a good way, maybe observers like events? if msg.is_error() { - warn!("GW: Received error, connection will close.."); + let error = msg.error().unwrap(); - let _error = msg.error(); + warn!("GW: Received error {:?}, connection will close..", error); self.close().await; + + self.events.lock().await.error.notify(error).await; + return; } @@ -937,6 +939,7 @@ mod events { pub webhooks: Webhooks, pub gateway_identify_payload: GatewayEvent, pub gateway_resume: GatewayEvent, + pub error: GatewayEvent, } #[derive(Default, Debug)] From cea362f50645df86c04f5b58a1de946e101e22a2 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:49:30 +0200 Subject: [PATCH 08/72] Minor updates --- src/errors.rs | 2 +- src/types/events/call.rs | 6 +++--- src/types/events/identify.rs | 4 ++-- src/types/events/presence.rs | 4 ++-- src/types/events/user.rs | 6 +++--- src/types/events/webrtc/ready.rs | 2 +- src/types/interfaces/activity.rs | 12 ++++++------ 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 26cc6b2..642a3ba 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -56,7 +56,7 @@ custom_error! { /// Supposed to be sent as numbers, though they are sent as string most of the time? /// /// Also includes errors when initiating a connection and unexpected opcodes - #[derive(PartialEq, Eq, Default)] + #[derive(PartialEq, Eq, Default, Clone)] pub GatewayError // Errors we have received from the gateway #[default] diff --git a/src/types/events/call.rs b/src/types/events/call.rs index e37c19d..508aae2 100644 --- a/src/types/events/call.rs +++ b/src/types/events/call.rs @@ -21,7 +21,7 @@ pub struct CallCreate { impl WebSocketEvent for CallCreate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; /// Updates the client on which calls are ringing, along with a specific call?; /// @@ -38,7 +38,7 @@ pub struct CallUpdate { impl WebSocketEvent for CallUpdate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; /// Deletes a ringing call; /// Ex: {"t":"CALL_DELETE","s":8,"op":0,"d":{"channel_id":"837609115475771392"}} @@ -48,7 +48,7 @@ pub struct CallDelete { impl WebSocketEvent for CallDelete {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Officially Undocumented; /// See ; /// diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index 4642fb6..70e1721 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,7 +2,7 @@ use crate::types::events::{PresenceUpdate, WebSocketEvent}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct GatewayIdentifyPayload { pub token: String, pub properties: GatewayIdentifyConnectionProps, @@ -68,7 +68,7 @@ impl GatewayIdentifyPayload { impl WebSocketEvent for GatewayIdentifyPayload {} -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde_as] pub struct GatewayIdentifyConnectionProps { /// Almost always sent diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs index c2d985e..d356a2a 100644 --- a/src/types/events/presence.rs +++ b/src/types/events/presence.rs @@ -2,7 +2,7 @@ use crate::types::{events::WebSocketEvent, UserStatus}; use crate::types::{Activity, ClientStatusObject, PublicUser, Snowflake}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Sent by the client to update its status and presence; /// See pub struct UpdatePresence { @@ -14,7 +14,7 @@ pub struct UpdatePresence { pub afk: bool, } -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// Received to tell the client that a user updated their presence / status /// See pub struct PresenceUpdate { diff --git a/src/types/events/user.rs b/src/types/events/user.rs index e3ce99a..7165812 100644 --- a/src/types/events/user.rs +++ b/src/types/events/user.rs @@ -4,7 +4,7 @@ use crate::types::entities::PublicUser; use crate::types::events::WebSocketEvent; use crate::types::utils::Snowflake; -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] /// See ; /// Sent to indicate updates to a user object; (name changes, discriminator changes, etc); pub struct UserUpdate { @@ -14,7 +14,7 @@ pub struct UserUpdate { impl WebSocketEvent for UserUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] /// Undocumented; /// /// Possibly an update for muted guild / channel settings for the current user; @@ -39,7 +39,7 @@ pub struct UserGuildSettingsUpdate { impl WebSocketEvent for UserGuildSettingsUpdate {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] /// Undocumented; /// /// Received in [UserGuildSettingsUpdate]; diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index c805593..4c9aebf 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -3,7 +3,7 @@ use std::net::Ipv4Addr; use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] /// The ready event for the webrtc stream; /// Used to give info after the identify event; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; diff --git a/src/types/interfaces/activity.rs b/src/types/interfaces/activity.rs index 1a48dfd..c538731 100644 --- a/src/types/interfaces/activity.rs +++ b/src/types/interfaces/activity.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{entities::Emoji, Snowflake}; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct Activity { name: String, #[serde(rename = "type")] @@ -22,19 +22,19 @@ pub struct Activity { buttons: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityTimestamps { start: Option, end: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityParty { id: Option, size: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityAssets { large_image: Option, large_text: Option, @@ -42,7 +42,7 @@ struct ActivityAssets { small_text: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivitySecrets { join: Option, spectate: Option, @@ -50,7 +50,7 @@ struct ActivitySecrets { match_string: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] struct ActivityButton { label: String, url: String, From 2cd48a948c516134a7162e21ff80a484b71fba3e Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:53:48 +0200 Subject: [PATCH 09/72] More derives --- src/types/events/webrtc/identify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index f41017d..45f1037 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -1,7 +1,7 @@ use crate::types::{Snowflake, WebSocketEvent}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// The identify payload for the webrtc stream; /// Contains info to begin a webrtc connection; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-identify-payload; From b04a906112f40bacc794a22a2e47861c19712bb5 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 18:16:45 +0200 Subject: [PATCH 10/72] Even more derives --- src/types/events/hello.rs | 4 ++-- src/types/events/identify.rs | 6 +++--- src/types/events/presence.rs | 4 ++-- src/types/events/voice.rs | 4 ++-- src/types/interfaces/activity.rs | 4 ++-- src/types/interfaces/status.rs | 2 +- src/types/schema/user.rs | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/types/events/hello.rs b/src/types/events/hello.rs index 44f1e4f..fef3e22 100644 --- a/src/types/events/hello.rs +++ b/src/types/events/hello.rs @@ -2,7 +2,7 @@ use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; /// Received on gateway init, tells the client how often to send heartbeats; -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct GatewayHello { pub op: i32, pub d: HelloData, @@ -10,7 +10,7 @@ pub struct GatewayHello { impl WebSocketEvent for GatewayHello {} -#[derive(Debug, Default, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq, Copy)] /// Contains info on how often the client should send heartbeats to the server; pub struct HelloData { /// How often a client should send heartbeats, in milliseconds diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index 70e1721..1353860 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,7 +2,7 @@ use crate::types::events::{PresenceUpdate, WebSocketEvent}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct GatewayIdentifyPayload { pub token: String, pub properties: GatewayIdentifyConnectionProps, @@ -144,7 +144,7 @@ impl GatewayIdentifyConnectionProps { referring_domain: None, referrer_current: None, release_channel: String::from("stable"), - client_build_number: 199933, + client_build_number: 0, } } @@ -159,7 +159,7 @@ impl GatewayIdentifyConnectionProps { system_locale: String::from("en-US"), os: String::from("Windows"), os_version: Some(String::from("10")), - client_build_number: 199933, + client_build_number: 222963, release_channel: String::from("stable"), ..Self::minimal() } diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs index d356a2a..c2d985e 100644 --- a/src/types/events/presence.rs +++ b/src/types/events/presence.rs @@ -2,7 +2,7 @@ use crate::types::{events::WebSocketEvent, UserStatus}; use crate::types::{Activity, ClientStatusObject, PublicUser, Snowflake}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Default, Clone)] /// Sent by the client to update its status and presence; /// See pub struct UpdatePresence { @@ -14,7 +14,7 @@ pub struct UpdatePresence { pub afk: bool, } -#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Default, Clone)] /// Received to tell the client that a user updated their presence / status /// See pub struct PresenceUpdate { diff --git a/src/types/events/voice.rs b/src/types/events/voice.rs index ff13b73..2618ee1 100644 --- a/src/types/events/voice.rs +++ b/src/types/events/voice.rs @@ -1,7 +1,7 @@ use crate::types::{events::WebSocketEvent, Snowflake, VoiceState}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, Copy, PartialEq, Eq)] /// /// Sent to the server to indicate an update of the voice state (leave voice channel, join voice channel, mute, deafen); /// @@ -28,7 +28,7 @@ pub struct VoiceStateUpdate { impl WebSocketEvent for VoiceStateUpdate {} -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// See ; /// /// Received to indicate which voice endpoint, token and guild_id to use; diff --git a/src/types/interfaces/activity.rs b/src/types/interfaces/activity.rs index c538731..23fa406 100644 --- a/src/types/interfaces/activity.rs +++ b/src/types/interfaces/activity.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{entities::Emoji, Snowflake}; -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Activity { name: String, #[serde(rename = "type")] @@ -22,7 +22,7 @@ pub struct Activity { buttons: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] struct ActivityTimestamps { start: Option, end: Option, diff --git a/src/types/interfaces/status.rs b/src/types/interfaces/status.rs index fadaf68..d5c07b6 100644 --- a/src/types/interfaces/status.rs +++ b/src/types/interfaces/status.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// See pub struct ClientStatusObject { pub desktop: Option, diff --git a/src/types/schema/user.rs b/src/types/schema/user.rs index 9946e73..5584cf4 100644 --- a/src/types/schema/user.rs +++ b/src/types/schema/user.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::types::Snowflake; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] #[serde(rename_all = "snake_case")] /// A schema used to modify a user. pub struct UserModifySchema { @@ -29,7 +29,7 @@ pub struct UserModifySchema { /// /// # Reference: /// Read: -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] pub struct PrivateChannelCreateSchema { pub recipients: Option>, pub access_tokens: Option>, From bbe24d60b9eac88e201fa4ca193d54a5eb298d91 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 18:18:48 +0200 Subject: [PATCH 11/72] Small types update --- src/types/events/webrtc/ready.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index 4c9aebf..008e41e 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize}; /// Used to give info after the identify event; /// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; pub struct VoiceReady { - ssrc: u8, + ssrc: i32, ip: Ipv4Addr, - port: u8, + port: u32, modes: Vec, // Heartbeat interval is also sent, but is "an erroneous field and should be ignored. The correct heartbeat_interval value comes from the Hello payload." } From 795cd5b9b5d264195110eb1b83c0ae86cef5ba24 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Tue, 29 Aug 2023 18:25:21 +0200 Subject: [PATCH 12/72] e --- src/types/events/identify.rs | 2 +- src/types/events/presence.rs | 2 +- src/types/interfaces/activity.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/events/identify.rs b/src/types/events/identify.rs index 1353860..12bc369 100644 --- a/src/types/events/identify.rs +++ b/src/types/events/identify.rs @@ -2,7 +2,7 @@ use crate::types::events::{PresenceUpdate, WebSocketEvent}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct GatewayIdentifyPayload { pub token: String, pub properties: GatewayIdentifyConnectionProps, diff --git a/src/types/events/presence.rs b/src/types/events/presence.rs index c2d985e..e9a7dee 100644 --- a/src/types/events/presence.rs +++ b/src/types/events/presence.rs @@ -14,7 +14,7 @@ pub struct UpdatePresence { pub afk: bool, } -#[derive(Debug, Deserialize, Serialize, Default, Clone)] +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq)] /// Received to tell the client that a user updated their presence / status /// See pub struct PresenceUpdate { diff --git a/src/types/interfaces/activity.rs b/src/types/interfaces/activity.rs index 23fa406..0da4747 100644 --- a/src/types/interfaces/activity.rs +++ b/src/types/interfaces/activity.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::types::{entities::Emoji, Snowflake}; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct Activity { name: String, #[serde(rename = "type")] From 68b6ff4ca7d9fdeed48f2ab3012a3ef47e1edca2 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 08:53:31 +0200 Subject: [PATCH 13/72] Minor doc fixes --- src/types/events/webrtc/identify.rs | 4 +++- src/types/events/webrtc/mod.rs | 25 +++++++++++----------- src/types/events/webrtc/ready.rs | 6 ++++-- src/types/events/webrtc/select_protocol.rs | 7 ++++-- src/types/events/webrtc/speaking.rs | 9 +++++--- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index 45f1037..a4e887c 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -3,8 +3,10 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] /// The identify payload for the webrtc stream; +/// /// Contains info to begin a webrtc connection; -/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-identify-payload; +/// +/// See pub struct VoiceIdentify { server_id: Snowflake, user_id: Snowflake, diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index ecfe3da..9c4e13c 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -1,5 +1,6 @@ use super::WebSocketEvent; use serde::{Deserialize, Serialize}; +use serde_json::{value::RawValue, Value}; pub use identify::*; pub use ready::*; @@ -14,40 +15,38 @@ mod session_description; mod speaking; #[derive(Debug, Default, Serialize, Clone)] -/// The payload used for sending events to the webrtc gateway -/// Not tha this is very similar to the regular gateway, except we no longer have a sequence number +/// The payload used for sending events to the webrtc gateway. /// -/// Similar to [WebrtcReceivePayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] -/// Also, we never need to send the event name +/// Similar to [VoiceGatewayReceivePayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] pub struct VoiceGatewaySendPayload { #[serde(rename = "op")] pub op_code: u8, #[serde(rename = "d")] - pub data: serde_json::Value, + pub data: Value, } impl WebSocketEvent for VoiceGatewaySendPayload {} #[derive(Debug, Deserialize, Clone)] -/// The payload used for receiving events from the webrtc gateway -/// Note that this is very similar to the regular gateway, except we no longer have s or t +/// The payload used for receiving events from the webrtc gateway. /// -/// Similar to [WebrtcSendPayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] -/// Also, we never need to sent the event name +/// Note that this is similar to the regular gateway, except we no longer have s or t +/// +/// Similar to [VoiceGatewaySendPayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] pub struct VoiceGatewayReceivePayload<'a> { #[serde(rename = "op")] pub op_code: u8, #[serde(borrow)] #[serde(rename = "d")] - pub data: &'a serde_json::value::RawValue, + pub data: &'a RawValue, } impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} /// The modes of encryption available in webrtc connections; -/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-encryption-modes; +/// See #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum WebrtcEncryptionMode { @@ -68,9 +67,9 @@ pub const VOICE_HEARTBEAT_ACK: u8 = 6; pub const VOICE_RESUME: u8 = 7; pub const VOICE_HELLO: u8 = 8; pub const VOICE_RESUMED: u8 = 9; -/// See https://discord-userdoccers.vercel.app/topics/opcodes-and-status-codes#voice-opcodes +/// See pub const VOICE_VIDEO: u8 = 12; pub const VOICE_CLIENT_DISCONENCT: u8 = 13; -/// See https://discord-userdoccers.vercel.app/topics/opcodes-and-status-codes#voice-opcodes; +/// See /// Sent with empty data from the client, the server responds with the voice backend version; pub const VOICE_BACKEND_VERSION: u8 = 16; diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index 5e7286d..8a879b9 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -7,10 +7,12 @@ use super::WebrtcEncryptionMode; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] /// The ready event for the webrtc stream; +/// /// Used to give info after the identify event; -/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-websocket-connection-example-voice-ready-payload; +/// +/// See pub struct VoiceReady { - /// See https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpStreamStats/ssrc + /// See ssrc: i32, ip: Ipv4Addr, port: u32, diff --git a/src/types/events/webrtc/select_protocol.rs b/src/types/events/webrtc/select_protocol.rs index 0966cd8..731c0d8 100644 --- a/src/types/events/webrtc/select_protocol.rs +++ b/src/types/events/webrtc/select_protocol.rs @@ -6,15 +6,18 @@ use super::WebrtcEncryptionMode; #[derive(Debug, Deserialize, Serialize, Clone)] /// An event sent by the client to the webrtc server, detailing what protocol, address and encryption to use; -/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-example-select-protocol-payload +/// +/// See pub struct SelectProtocol { /// The protocol to use. The only option detailed in discord docs is "udp" pub protocol: String, + pub data: SelectProtocolData, } #[derive(Debug, Deserialize, Serialize, Clone)] /// The data field of the SelectProtocol Event -/// See https://discord.com/developers/docs/topics/voice-connections#establishing-a-voice-udp-connection-example-select-protocol-payload; +/// +/// See pub struct SelectProtocolData { /// Our external ip pub address: Ipv4Addr, diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/webrtc/speaking.rs index 3778266..dab9aba 100644 --- a/src/types/events/webrtc/speaking.rs +++ b/src/types/events/webrtc/speaking.rs @@ -2,8 +2,10 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; /// Event that tells the server we are speaking; -/// Essentially, what allows us to send udp data and lights up the green circle around your avatar; -/// See https://discord.com/developers/docs/topics/voice-connections#speaking-example-speaking-payload +/// +/// Essentially, what allows us to send udp data and lights up the green circle around your avatar. +/// +/// See #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct Speaking { /// Data about the audio we're transmitting, its type @@ -15,7 +17,8 @@ pub struct Speaking { bitflags! { /// Bitflags of speaking types; - /// See https://discord.com/developers/docs/topics/voice-connections#speaking; + /// + /// See #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] pub struct SpeakingBitflags: u8 { /// Whether we'll be transmitting normal voice audio From e4f0a3840aaa7886411270b3d186d181444d4076 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 09:58:26 +0200 Subject: [PATCH 14/72] Modernise voice gateway --- src/errors.rs | 2 ++ src/voice.rs | 37 ++++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 4a251c9..e8075c2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -114,3 +114,5 @@ custom_error! { // Other misc errors UnexpectedOpcodeReceived{opcode: u8} = "Received an opcode we weren't expecting to receive: {opcode}", } + +impl WebSocketEvent for VoiceGatewayError {} diff --git a/src/voice.rs b/src/voice.rs index 14e883d..e6583b0 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -24,6 +24,8 @@ use crate::types::{ VOICE_SESSION_DESCRIPTION, VOICE_SPEAKING, }; +use self::voice_events::VoiceEvents; + /// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. /// This struct is used internally when handling messages. #[derive(Clone, Debug)] @@ -94,10 +96,10 @@ impl VoiceGatewayMesssage { /// Represents a handle to a Voice Gateway connection. /// Using this handle you can send Gateway Events directly. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct VoiceGatewayHandle { pub url: String, - pub events: Arc>, + pub events: Arc>, pub websocket_send: Arc< Mutex< SplitSink< @@ -106,7 +108,6 @@ pub struct VoiceGatewayHandle { >, >, >, - pub handle: JoinHandle<()>, /// Tells gateway tasks to close kill_send: tokio::sync::broadcast::Sender<()>, } @@ -179,7 +180,7 @@ impl VoiceGatewayHandle { #[derive(Debug)] pub struct VoiceGateway { - events: Arc>, + events: Arc>, heartbeat_handler: VoiceHeartbeatHandler, websocket_send: Arc< Mutex< @@ -191,6 +192,7 @@ pub struct VoiceGateway { >, websocket_receive: SplitStream>>, kill_send: tokio::sync::broadcast::Sender<()>, + url: String, } impl VoiceGateway { @@ -242,7 +244,7 @@ impl VoiceGateway { let gateway_hello: types::HelloData = serde_json::from_str(gateway_payload.data.get()).unwrap(); - let voice_events = voice_events::VoiceEvents::default(); + let voice_events = VoiceEvents::default(); let shared_events = Arc::new(Mutex::new(voice_events)); let mut gateway = VoiceGateway { @@ -256,10 +258,11 @@ impl VoiceGateway { websocket_send: shared_websocket_send.clone(), websocket_receive, kill_send: kill_send.clone(), + url: websocket_url.clone(), }; // Now we can continuously check for messages in a different task, since we aren't going to receive another hello - let handle: JoinHandle<()> = tokio::spawn(async move { + tokio::spawn(async move { gateway.gateway_listen_task().await; }); @@ -267,7 +270,6 @@ impl VoiceGateway { url: websocket_url.clone(), events: shared_events, websocket_send: shared_websocket_send.clone(), - handle, kill_send: kill_send.clone(), }) } @@ -327,18 +329,21 @@ impl VoiceGateway { return; } - // To:do: handle errors in a good way, maybe observers like events? if msg.is_error() { + let error = msg.error().unwrap(); + warn!("VGW: Received error, connection will close.."); - let _error = msg.error(); - self.close().await; + + self.events.lock().await.error.notify(error).await; + return; } let gateway_payload = msg.payload().unwrap(); + // See match gateway_payload.op_code { VOICE_READY => { let event = &mut self.events.lock().await.voice_ready; @@ -493,14 +498,11 @@ impl VoiceHeartbeatHandler { () = sleep_until(last_heartbeat_timestamp + timeout) => { should_send = true; } - - Some(communication) = receive.recv() => { - // If we received a nonce update, use that nonce now - if communication.updated_nonce.is_some() { - nonce = communication.updated_nonce.unwrap(); - } + if communication.updated_nonce.is_some() { + nonce = communication.updated_nonce.unwrap(); + } if let Some(op_code) = communication.op_code { match op_code { @@ -554,7 +556,7 @@ struct VoiceHeartbeatThreadCommunication { updated_nonce: Option, } -mod voice_events { +pub mod voice_events { use crate::types::{SessionDescription, VoiceReady}; use super::*; @@ -563,5 +565,6 @@ mod voice_events { pub struct VoiceEvents { pub voice_ready: GatewayEvent, pub session_description: GatewayEvent, + pub error: GatewayEvent, } } From 1639d4e00fa10b88ba661eeeb0961504332afd60 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 10:25:28 +0200 Subject: [PATCH 15/72] Add default impl for voicegatewayerror --- src/errors.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/errors.rs b/src/errors.rs index e8075c2..07bd5b3 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -91,9 +91,10 @@ custom_error! { /// Similar to [GatewayError]. /// /// See https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice; - #[derive(Clone, PartialEq, Eq)] + #[derive(Clone, Default, PartialEq, Eq)] pub VoiceGatewayError // Errors we receive + #[default] UnknownOpcode = "You sent an invalid opcode", FailedToDecodePayload = "You sent an invalid payload in your identifying to the (Webrtc) Gateway", NotAuthenticated = "You sent a payload before identifying with the (Webrtc) Gateway", From cdcc6a52708596898699e1ba632330d5fedc82fd Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 10:29:05 +0200 Subject: [PATCH 16/72] Make voice event fields pub --- src/types/events/webrtc/identify.rs | 10 +++++----- src/types/events/webrtc/ready.rs | 8 ++++---- src/types/events/webrtc/speaking.rs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index a4e887c..9bc72ed 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -8,13 +8,13 @@ use serde::{Deserialize, Serialize}; /// /// See pub struct VoiceIdentify { - server_id: Snowflake, - user_id: Snowflake, - session_id: String, - token: String, + pub server_id: Snowflake, + pub user_id: Snowflake, + pub session_id: String, + pub token: String, #[serde(skip_serializing_if = "Option::is_none")] /// Undocumented field, but is also in discord client comms - video: Option, + pub video: Option, } impl WebSocketEvent for VoiceIdentify {} diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index 8a879b9..8b46e83 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -13,11 +13,11 @@ use super::WebrtcEncryptionMode; /// See pub struct VoiceReady { /// See - ssrc: i32, - ip: Ipv4Addr, - port: u32, + pub ssrc: i32, + pub ip: Ipv4Addr, + pub port: u32, /// The available encryption modes for the webrtc connection - modes: Vec, + pub modes: Vec, // Heartbeat interval is also sent, but is "an erroneous field and should be ignored. The correct heartbeat_interval value comes from the Hello payload." } diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/webrtc/speaking.rs index dab9aba..5b48efa 100644 --- a/src/types/events/webrtc/speaking.rs +++ b/src/types/events/webrtc/speaking.rs @@ -9,10 +9,10 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct Speaking { /// Data about the audio we're transmitting, its type - speaking: SpeakingBitflags, + pub speaking: SpeakingBitflags, /// Assuming delay in milliseconds for the audio, should be 0 most of the time - delay: u64, - ssrc: i32, + pub delay: u64, + pub ssrc: i32, } bitflags! { From 7b8bcffafa60903787cacdd3810815a1d96da794 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 10:43:02 +0200 Subject: [PATCH 17/72] Event updates via the scientific method --- src/types/events/voice.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/events/voice.rs b/src/types/events/voice.rs index 2618ee1..cb258fc 100644 --- a/src/types/events/voice.rs +++ b/src/types/events/voice.rs @@ -34,7 +34,8 @@ impl WebSocketEvent for VoiceStateUpdate {} /// Received to indicate which voice endpoint, token and guild_id to use; pub struct VoiceServerUpdate { pub token: String, - pub guild_id: Snowflake, + /// Can be None in dm calls + pub guild_id: Option, pub endpoint: Option, } From 8ab75e313a045b0db7ee7338b21a59e56683ea23 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 10:49:51 +0200 Subject: [PATCH 18/72] ?? --- src/types/entities/voice_state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/entities/voice_state.rs b/src/types/entities/voice_state.rs index e764296..676c355 100644 --- a/src/types/entities/voice_state.rs +++ b/src/types/entities/voice_state.rs @@ -28,7 +28,8 @@ pub struct VoiceState { pub channel_id: Option, pub user_id: Snowflake, pub member: Option>>, - pub session_id: Snowflake, + /// Includes alphanumeric characters, not a snowflake + pub session_id: String, pub token: Option, pub deaf: bool, pub mute: bool, @@ -38,5 +39,6 @@ pub struct VoiceState { pub self_video: bool, pub suppress: bool, pub request_to_speak_timestamp: Option>, + // FIXME: This is not sent in practice????????? pub id: Snowflake, } From 5608d96a5f104d42772aeee92511159624b859bd Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 11:03:50 +0200 Subject: [PATCH 19/72] Fix bad request in voice gateway init --- src/voice.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/voice.rs b/src/voice.rs index e6583b0..76aaddb 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -199,7 +199,8 @@ impl VoiceGateway { #[allow(clippy::new_ret_no_self)] pub async fn new(websocket_url: String) -> Result { // Append the needed things to the websocket url - let processed_url = format!("wss://{}?v=4", websocket_url); + let processed_url = format!("wss://{}/?v=4", websocket_url); + debug!("Created voice socket url: {}", processed_url.clone()); let (websocket_stream, _) = match connect_async_tls_with_config( &processed_url, From e5c4cc3df944e705137f28f98c160c99b688dec1 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 11:51:31 +0200 Subject: [PATCH 20/72] Voice gateway updates --- src/types/events/webrtc/hello.rs | 13 +++++++++++++ src/types/events/webrtc/mod.rs | 14 +++++++++++--- src/voice.rs | 15 ++++++++++----- 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 src/types/events/webrtc/hello.rs diff --git a/src/types/events/webrtc/hello.rs b/src/types/events/webrtc/hello.rs new file mode 100644 index 0000000..bf73d84 --- /dev/null +++ b/src/types/events/webrtc/hello.rs @@ -0,0 +1,13 @@ +use crate::types::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Copy)] +/// Contains info on how often the client should send heartbeats to the server; +/// +/// Differs from the normal hello data in that discord sends heartbeat interval as a float. +pub struct VoiceHelloData { + /// How often a client should send heartbeats, in milliseconds + pub heartbeat_interval: f64, +} + +impl WebSocketEvent for VoiceHelloData {} diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index 9c4e13c..ff475e2 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -2,12 +2,14 @@ use super::WebSocketEvent; use serde::{Deserialize, Serialize}; use serde_json::{value::RawValue, Value}; +pub use hello::*; pub use identify::*; pub use ready::*; pub use select_protocol::*; pub use session_description::*; pub use speaking::*; +mod hello; mod identify; mod ready; mod select_protocol; @@ -51,9 +53,15 @@ impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} #[serde(rename_all = "snake_case")] pub enum WebrtcEncryptionMode { #[default] - XSalsa20Poly1305, - XSalsa20Poly1305Suffix, - XSalsa20Poly1305Lite, + // Documented + Xsalsa20Poly1305, + Xsalsa20Poly1305Suffix, + Xsalsa20Poly1305Lite, + // Undocumented + Xsalsa20Poly1305LiteRtpsize, + AeadAes256Gcm, + AeadAes256GcmRtpsize, + AeadXchacha20Poly1305Rtpsize, } // The various voice opcodes diff --git a/src/voice.rs b/src/voice.rs index 76aaddb..b55889d 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -200,7 +200,7 @@ impl VoiceGateway { pub async fn new(websocket_url: String) -> Result { // Append the needed things to the websocket url let processed_url = format!("wss://{}/?v=4", websocket_url); - debug!("Created voice socket url: {}", processed_url.clone()); + trace!("Created voice socket url: {}", processed_url.clone()); let (websocket_stream, _) = match connect_async_tls_with_config( &processed_url, @@ -241,9 +241,10 @@ impl VoiceGateway { info!("VGW: Received Hello"); - // The hello data is the same on voice and normal gateway - let gateway_hello: types::HelloData = + // The hello data for voice gateways is in float milliseconds, so we convert it to f64 seconds + let gateway_hello: types::VoiceHelloData = serde_json::from_str(gateway_payload.data.get()).unwrap(); + let heartbeat_interval_seconds: f64 = gateway_hello.heartbeat_interval / 1000.0; let voice_events = VoiceEvents::default(); let shared_events = Arc::new(Mutex::new(voice_events)); @@ -251,7 +252,7 @@ impl VoiceGateway { let mut gateway = VoiceGateway { events: shared_events.clone(), heartbeat_handler: VoiceHeartbeatHandler::new( - Duration::from_millis(gateway_hello.heartbeat_interval), + Duration::from_secs_f64(heartbeat_interval_seconds), 1, // to:do actually compute nonce shared_websocket_send.clone(), kill_send.subscribe(), @@ -347,6 +348,8 @@ impl VoiceGateway { // See match gateway_payload.op_code { VOICE_READY => { + trace!("VGW: Received READY!"); + let event = &mut self.events.lock().await.voice_ready; let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; if result.is_err() { @@ -355,6 +358,8 @@ impl VoiceGateway { } } VOICE_SESSION_DESCRIPTION => { + trace!("VGW: Received Session Description"); + let event = &mut self.events.lock().await.session_description; let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; if result.is_err() { @@ -383,7 +388,7 @@ impl VoiceGateway { .unwrap(); } VOICE_HEARTBEAT_ACK => { - debug!("VGW: Received Heartbeat ACK"); + trace!("VGW: Received Heartbeat ACK"); // Tell the heartbeat handler we received an ack From cf70147500625d415a082cbb82bcb6ffb7e16a80 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 14 Oct 2023 18:13:09 +0200 Subject: [PATCH 21/72] Fix error failing to 'deserialize' properly --- src/voice.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/voice.rs b/src/voice.rs index b55889d..a39a6d8 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -56,7 +56,7 @@ impl VoiceGatewayMesssage { "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticated), "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailed), "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticated), - "session no longer valid" | "4006" => Some(VoiceGatewayError::SessionNoLongerValid), + "session is no longer valid" | "4006" => Some(VoiceGatewayError::SessionNoLongerValid), "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeout), "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFound), "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocol), From fa3c3b76aecf5da1b4ae7db8e6908a42a75e4969 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 15 Oct 2023 09:47:08 +0200 Subject: [PATCH 22/72] Update voice identify --- src/types/events/webrtc/identify.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index 9bc72ed..a83822c 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize}; /// /// See pub struct VoiceIdentify { - pub server_id: Snowflake, + /// Not needed when in a dm call + pub server_id: Option, pub user_id: Snowflake, pub session_id: String, pub token: String, From feb8d4610ccb18c50d8127c6a07b9ec3f83388e0 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 15 Oct 2023 09:47:36 +0200 Subject: [PATCH 23/72] Clarify FIXME related to #430 --- src/types/entities/voice_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/entities/voice_state.rs b/src/types/entities/voice_state.rs index 676c355..1a5f176 100644 --- a/src/types/entities/voice_state.rs +++ b/src/types/entities/voice_state.rs @@ -39,6 +39,6 @@ pub struct VoiceState { pub self_video: bool, pub suppress: bool, pub request_to_speak_timestamp: Option>, - // FIXME: This is not sent in practice????????? + // FIXME: This is a Spacebar only field and is not sent on DDC, see [#430](https://github.com/polyphony-chat/chorus/issues/430) pub id: Snowflake, } From fad04da125eb4a8fdf7bb91487b68ac11863ff4c Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:51:59 +0200 Subject: [PATCH 24/72] Update to v7 --- src/types/events/voice.rs | 4 +- src/types/events/webrtc/client_connect.rs | 34 +++++++ src/types/events/webrtc/client_disconnect.rs | 14 +++ src/types/events/webrtc/hello.rs | 7 ++ src/types/events/webrtc/identify.rs | 8 +- src/types/events/webrtc/mod.rs | 40 +++++++- src/types/events/webrtc/ready.rs | 10 +- src/types/events/webrtc/select_protocol.rs | 45 +++++++-- .../events/webrtc/session_description.rs | 31 +++++- src/types/events/webrtc/speaking.rs | 14 ++- .../events/webrtc/voice_backend_version.rs | 17 ++++ src/voice.rs | 96 ++++++++++++++++++- 12 files changed, 289 insertions(+), 31 deletions(-) create mode 100644 src/types/events/webrtc/client_connect.rs create mode 100644 src/types/events/webrtc/client_disconnect.rs create mode 100644 src/types/events/webrtc/voice_backend_version.rs diff --git a/src/types/events/voice.rs b/src/types/events/voice.rs index cb258fc..ac7df7a 100644 --- a/src/types/events/voice.rs +++ b/src/types/events/voice.rs @@ -34,8 +34,10 @@ impl WebSocketEvent for VoiceStateUpdate {} /// Received to indicate which voice endpoint, token and guild_id to use; pub struct VoiceServerUpdate { pub token: String, - /// Can be None in dm calls + /// The guild this voice server update is for pub guild_id: Option, + /// The private channel this voice server update is for + pub channel_id: Option, pub endpoint: Option, } diff --git a/src/types/events/webrtc/client_connect.rs b/src/types/events/webrtc/client_connect.rs new file mode 100644 index 0000000..d367ff3 --- /dev/null +++ b/src/types/events/webrtc/client_connect.rs @@ -0,0 +1,34 @@ +use crate::types::{Snowflake, WebSocketEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Copy)] +/// Sent when another user connects to the voice server. +/// +/// Contains the user id and "flags". +/// +/// Not documented anywhere, if you know what this is, please reach out +/// +/// {"op":18,"d":{"user_id":"1234567890","flags":2}} +pub struct VoiceClientConnectFlags { + pub user_id: Snowflake, + // Likely some sort of bitflags + pub flags: u8, +} + +impl WebSocketEvent for VoiceClientConnectFlags {} + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Copy)] +/// Sent when another user connects to the voice server. +/// +/// Contains the user id and "platform". +/// +/// Not documented anywhere, if you know what this is, please reach out +/// +/// {"op":20,"d":{"user_id":"1234567890","platform":0}} +pub struct VoiceClientConnectPlatform { + pub user_id: Snowflake, + // Likely an enum + pub platform: u8, +} + +impl WebSocketEvent for VoiceClientConnectPlatform {} diff --git a/src/types/events/webrtc/client_disconnect.rs b/src/types/events/webrtc/client_disconnect.rs new file mode 100644 index 0000000..6e30716 --- /dev/null +++ b/src/types/events/webrtc/client_disconnect.rs @@ -0,0 +1,14 @@ +use crate::types::{Snowflake, WebSocketEvent}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Copy)] +/// Sent when another user disconnects from the voice server. +/// +/// When received, the SSRC of the user should be discarded. +/// +/// See +pub struct VoiceClientDisconnection { + pub user_id: Snowflake, +} + +impl WebSocketEvent for VoiceClientDisconnection {} diff --git a/src/types/events/webrtc/hello.rs b/src/types/events/webrtc/hello.rs index bf73d84..3d39fb8 100644 --- a/src/types/events/webrtc/hello.rs +++ b/src/types/events/webrtc/hello.rs @@ -5,7 +5,14 @@ use serde::{Deserialize, Serialize}; /// Contains info on how often the client should send heartbeats to the server; /// /// Differs from the normal hello data in that discord sends heartbeat interval as a float. +/// +/// See pub struct VoiceHelloData { + /// The voice gateway version. + /// + /// Note: no idea why this is sent, we already specify the version when establishing a connection. + #[serde(rename = "v")] + pub version: u8, /// How often a client should send heartbeats, in milliseconds pub heartbeat_interval: f64, } diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/webrtc/identify.rs index a83822c..4021c28 100644 --- a/src/types/events/webrtc/identify.rs +++ b/src/types/events/webrtc/identify.rs @@ -6,16 +6,16 @@ use serde::{Deserialize, Serialize}; /// /// Contains info to begin a webrtc connection; /// -/// See +/// See pub struct VoiceIdentify { - /// Not needed when in a dm call - pub server_id: Option, + /// The ID of the guild or the private channel being connected to + pub server_id: Snowflake, pub user_id: Snowflake, pub session_id: String, pub token: String, #[serde(skip_serializing_if = "Option::is_none")] - /// Undocumented field, but is also in discord client comms pub video: Option, + // TODO: Add video streams } impl WebSocketEvent for VoiceIdentify {} diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index ff475e2..3c87aa0 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -2,19 +2,25 @@ use super::WebSocketEvent; use serde::{Deserialize, Serialize}; use serde_json::{value::RawValue, Value}; +pub use client_connect::*; +pub use client_disconnect::*; pub use hello::*; pub use identify::*; pub use ready::*; pub use select_protocol::*; pub use session_description::*; pub use speaking::*; +pub use voice_backend_version::*; +mod client_connect; +mod client_disconnect; mod hello; mod identify; mod ready; mod select_protocol; mod session_description; mod speaking; +mod voice_backend_version; #[derive(Debug, Default, Serialize, Clone)] /// The payload used for sending events to the webrtc gateway. @@ -48,22 +54,41 @@ pub struct VoiceGatewayReceivePayload<'a> { impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} /// The modes of encryption available in webrtc connections; -/// See +/// +/// See and #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] #[serde(rename_all = "snake_case")] -pub enum WebrtcEncryptionMode { +pub enum VoiceEncryptionMode { #[default] - // Documented + // Officially Documented Xsalsa20Poly1305, Xsalsa20Poly1305Suffix, Xsalsa20Poly1305Lite, - // Undocumented + // Officially Undocumented Xsalsa20Poly1305LiteRtpsize, AeadAes256Gcm, AeadAes256GcmRtpsize, AeadXchacha20Poly1305Rtpsize, } +/// The possible audio codecs to use +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AudioCodec { + #[default] + Opus, +} + +/// The possible video codecs to use +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "UPPERCASE")] +pub enum VideoCodec { + #[default] + VP8, + VP9, + H264, +} + // The various voice opcodes pub const VOICE_IDENTIFY: u8 = 0; pub const VOICE_SELECT_PROTOCOL: u8 = 1; @@ -77,7 +102,12 @@ pub const VOICE_HELLO: u8 = 8; pub const VOICE_RESUMED: u8 = 9; /// See pub const VOICE_VIDEO: u8 = 12; -pub const VOICE_CLIENT_DISCONENCT: u8 = 13; +pub const VOICE_CLIENT_DISCONNECT: u8 = 13; +pub const VOICE_SESSION_UPDATE: u8 = 14; /// See /// Sent with empty data from the client, the server responds with the voice backend version; pub const VOICE_BACKEND_VERSION: u8 = 16; + +// These two get simultaenously fired when a user joins, one has flags and one has a platform +pub const VOICE_CLIENT_CONNECT_FLAGS: u8 = 18; +pub const VOICE_CLIENT_CONNECT_PLATFORM: u8 = 20; diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index 8b46e83..1a39750 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -3,21 +3,24 @@ use std::net::Ipv4Addr; use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; -use super::WebrtcEncryptionMode; +use super::VoiceEncryptionMode; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] /// The ready event for the webrtc stream; /// /// Used to give info after the identify event; /// -/// See +/// See pub struct VoiceReady { /// See pub ssrc: i32, pub ip: Ipv4Addr, pub port: u32, /// The available encryption modes for the webrtc connection - pub modes: Vec, + pub modes: Vec, + #[serde(default)] + pub experiments: Vec, + // TODO: Add video streams // Heartbeat interval is also sent, but is "an erroneous field and should be ignored. The correct heartbeat_interval value comes from the Hello payload." } @@ -28,6 +31,7 @@ impl Default for VoiceReady { ip: Ipv4Addr::UNSPECIFIED, port: 0, modes: Vec::new(), + experiments: Vec::new(), } } } diff --git a/src/types/events/webrtc/select_protocol.rs b/src/types/events/webrtc/select_protocol.rs index 731c0d8..3cda068 100644 --- a/src/types/events/webrtc/select_protocol.rs +++ b/src/types/events/webrtc/select_protocol.rs @@ -2,27 +2,58 @@ use std::net::Ipv4Addr; use serde::{Deserialize, Serialize}; -use super::WebrtcEncryptionMode; +use super::VoiceEncryptionMode; -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Default)] /// An event sent by the client to the webrtc server, detailing what protocol, address and encryption to use; /// -/// See +/// See pub struct SelectProtocol { - /// The protocol to use. The only option detailed in discord docs is "udp" - pub protocol: String, + /// The protocol to use. The only option chorus supports is [VoiceProtocol::Udp]. + pub protocol: VoiceProtocol, pub data: SelectProtocolData, + /// The UUID4 RTC connection ID, used for analytics. + /// + /// Note: Not recommended to set this + pub rtc_connection_id: Option, + // TODO: Add codecs, what is a codec object + /// The possible experiments we want to enable + #[serde(rename = "experiments")] + pub enabled_experiments: Vec, +} + +/// The possible protocol for sending a receiving voice data. +/// +/// See +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum VoiceProtocol { + #[default] + /// Sending data via UDP, documented and the only protocol chorus supports. + Udp, + // Possible value, yet NOT RECOMMENED, AS CHORUS DOES NOT SUPPORT WEBRTC + //Webrtc, } #[derive(Debug, Deserialize, Serialize, Clone)] /// The data field of the SelectProtocol Event /// -/// See +/// See pub struct SelectProtocolData { /// Our external ip pub address: Ipv4Addr, /// Our external udp port pub port: u32, /// The mode of encryption to use - pub mode: WebrtcEncryptionMode, + pub mode: VoiceEncryptionMode, +} + +impl Default for SelectProtocolData { + fn default() -> Self { + SelectProtocolData { + address: Ipv4Addr::UNSPECIFIED, + port: 0, + mode: Default::default(), + } + } } diff --git a/src/types/events/webrtc/session_description.rs b/src/types/events/webrtc/session_description.rs index dfef1eb..9c1b3d4 100644 --- a/src/types/events/webrtc/session_description.rs +++ b/src/types/events/webrtc/session_description.rs @@ -1,14 +1,39 @@ -use super::WebrtcEncryptionMode; +use super::{AudioCodec, VideoCodec, VoiceEncryptionMode}; use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Clone, Default)] /// Event that describes our encryption mode and secret key for encryption +/// +/// See pub struct SessionDescription { - /// The encryption mode we're using in webrtc - pub mode: WebrtcEncryptionMode, + pub audio_codec: AudioCodec, + pub video_codec: VideoCodec, + pub media_session_id: String, + /// The encryption mode to use + #[serde(rename = "mode")] + pub encryption_mode: VoiceEncryptionMode, /// The secret key we'll use for encryption pub secret_key: [u8; 32], + /// The keyframe interval in milliseconds + pub keyframe_interval: Option, } impl WebSocketEvent for SessionDescription {} + +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +/// Event that might be sent to update session parameters +/// +/// See +pub struct SessionUpdate { + #[serde(rename = "audio_codec")] + pub new_audio_codec: Option, + + #[serde(rename = "video_codec")] + pub new_video_codec: Option, + + #[serde(rename = "media_session_id")] + pub new_media_session_id: Option, +} + +impl WebSocketEvent for SessionUpdate {} diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/webrtc/speaking.rs index 5b48efa..b854876 100644 --- a/src/types/events/webrtc/speaking.rs +++ b/src/types/events/webrtc/speaking.rs @@ -1,20 +1,28 @@ use bitflags::bitflags; use serde::{Deserialize, Serialize}; +use crate::types::{Snowflake, WebSocketEvent}; + /// Event that tells the server we are speaking; /// /// Essentially, what allows us to send udp data and lights up the green circle around your avatar. /// -/// See +/// See #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct Speaking { /// Data about the audio we're transmitting, its type pub speaking: SpeakingBitflags, - /// Assuming delay in milliseconds for the audio, should be 0 most of the time - pub delay: u64, pub ssrc: i32, + /// The user id of the speaking user, only sent by the server + #[serde(skip_serializing)] + pub user_id: Option, + /// Delay in milliseconds, not sent by the server + #[serde(default)] + pub delay: u64, } +impl WebSocketEvent for Speaking {} + bitflags! { /// Bitflags of speaking types; /// diff --git a/src/types/events/webrtc/voice_backend_version.rs b/src/types/events/webrtc/voice_backend_version.rs new file mode 100644 index 0000000..6b788ea --- /dev/null +++ b/src/types/events/webrtc/voice_backend_version.rs @@ -0,0 +1,17 @@ +use crate::types::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] +/// Received from the server to describe the backend version. +/// +/// See +pub struct VoiceBackendVersion { + /// The voice backend's version + #[serde(rename = "voice")] + pub voice_version: String, + /// The WebRTC worker's version + #[serde(rename = "rtc_worker")] + pub rtc_worker_version: String, +} + +impl WebSocketEvent for VoiceBackendVersion {} diff --git a/src/voice.rs b/src/voice.rs index a39a6d8..edca815 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -19,9 +19,10 @@ use crate::errors::VoiceGatewayError; use crate::gateway::{GatewayEvent, HEARTBEAT_ACK_TIMEOUT}; use crate::types::{ self, SelectProtocol, Speaking, VoiceGatewayReceivePayload, VoiceGatewaySendPayload, - VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, + VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_CLIENT_CONNECT_FLAGS, + VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, VOICE_HELLO, VOICE_IDENTIFY, VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, - VOICE_SESSION_DESCRIPTION, VOICE_SPEAKING, + VOICE_SESSION_DESCRIPTION, VOICE_SESSION_UPDATE, VOICE_SPEAKING, }; use self::voice_events::VoiceEvents; @@ -199,7 +200,7 @@ impl VoiceGateway { #[allow(clippy::new_ret_no_self)] pub async fn new(websocket_url: String) -> Result { // Append the needed things to the websocket url - let processed_url = format!("wss://{}/?v=4", websocket_url); + let processed_url = format!("wss://{}/?v=7", websocket_url); trace!("Created voice socket url: {}", processed_url.clone()); let (websocket_stream, _) = match connect_async_tls_with_config( @@ -357,6 +358,19 @@ impl VoiceGateway { return; } } + VOICE_BACKEND_VERSION => { + trace!("VGW: Received Backend Version"); + + let event = &mut self.events.lock().await.backend_version; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_BACKEND_VERSION ({})", + result.err().unwrap() + ); + return; + } + } VOICE_SESSION_DESCRIPTION => { trace!("VGW: Received Session Description"); @@ -364,12 +378,75 @@ impl VoiceGateway { let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; if result.is_err() { warn!( - "Failed to parse VOICE_SELECT_PROTOCOL ({})", + "Failed to parse VOICE_SESSION_DESCRIPTION ({})", result.err().unwrap() ); return; } } + VOICE_SESSION_UPDATE => { + trace!("VGW: Received Session Update"); + + let event = &mut self.events.lock().await.session_update; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_SESSION_UPDATE ({})", + result.err().unwrap() + ); + return; + } + } + VOICE_SPEAKING => { + trace!("VGW: Received Speaking"); + + let event = &mut self.events.lock().await.speaking; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!("Failed to parse VOICE_SPEAKING ({})", result.err().unwrap()); + return; + } + } + VOICE_CLIENT_DISCONNECT => { + trace!("VGW: Received Client Disconnect"); + + let event = &mut self.events.lock().await.client_disconnect; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_DISCONNECT ({})", + result.err().unwrap() + ); + return; + } + } + VOICE_CLIENT_CONNECT_FLAGS => { + trace!("VGW: Received Client Connect Flags"); + + let event = &mut self.events.lock().await.client_connect_flags; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_CONNECT_FLAGS ({})", + result.err().unwrap() + ); + return; + } + } + VOICE_CLIENT_CONNECT_PLATFORM => { + trace!("VGW: Received Client Connect Platform"); + + let event = &mut self.events.lock().await.client_connect_platform; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_CONNECT_PLATFORM ({})", + result.err().unwrap() + ); + return; + } + } + // We received a heartbeat from the server // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." VOICE_HEARTBEAT => { @@ -563,14 +640,23 @@ struct VoiceHeartbeatThreadCommunication { } pub mod voice_events { - use crate::types::{SessionDescription, VoiceReady}; + use crate::types::{ + SessionDescription, SessionUpdate, VoiceBackendVersion, VoiceClientConnectFlags, + VoiceClientConnectPlatform, VoiceClientDisconnection, VoiceReady, + }; use super::*; #[derive(Default, Debug)] pub struct VoiceEvents { pub voice_ready: GatewayEvent, + pub backend_version: GatewayEvent, pub session_description: GatewayEvent, + pub session_update: GatewayEvent, + pub speaking: GatewayEvent, + pub client_disconnect: GatewayEvent, + pub client_connect_flags: GatewayEvent, + pub client_connect_platform: GatewayEvent, pub error: GatewayEvent, } } From ef1d314291c5a6a0c398d33de4df6cca64c96072 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:50:07 +0100 Subject: [PATCH 25/72] Create seperate voice_gateway.rs and voice_udp.rs --- src/lib.rs | 4 +++- src/{voice.rs => voice_gateway.rs} | 0 src/voice_udp.rs | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) rename src/{voice.rs => voice_gateway.rs} (100%) create mode 100644 src/voice_udp.rs diff --git a/src/lib.rs b/src/lib.rs index e63c41d..f3bc937 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,9 @@ pub mod instance; pub mod ratelimiter; pub mod types; #[cfg(feature = "client")] -pub mod voice; +pub mod voice_gateway; +#[cfg(feature = "client")] +pub mod voice_udp; #[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] /// A URLBundle bundles together the API-, Gateway- and CDN-URLs of a Spacebar instance. diff --git a/src/voice.rs b/src/voice_gateway.rs similarity index 100% rename from src/voice.rs rename to src/voice_gateway.rs diff --git a/src/voice_udp.rs b/src/voice_udp.rs new file mode 100644 index 0000000..ec1b08a --- /dev/null +++ b/src/voice_udp.rs @@ -0,0 +1 @@ +/// Defines voice raw udp socket handling From b0ae70077563dd564362578d7ed7c89748bb00a2 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:52:42 +0100 Subject: [PATCH 26/72] Restructure voice to new module --- src/lib.rs | 4 +--- src/{voice_gateway.rs => voice/gateway.rs} | 0 src/voice/mod.rs | 4 ++++ src/voice/udp.rs | 1 + src/voice_udp.rs | 1 - 5 files changed, 6 insertions(+), 4 deletions(-) rename src/{voice_gateway.rs => voice/gateway.rs} (100%) create mode 100644 src/voice/mod.rs create mode 100644 src/voice/udp.rs delete mode 100644 src/voice_udp.rs diff --git a/src/lib.rs b/src/lib.rs index f3bc937..e63c41d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,9 +28,7 @@ pub mod instance; pub mod ratelimiter; pub mod types; #[cfg(feature = "client")] -pub mod voice_gateway; -#[cfg(feature = "client")] -pub mod voice_udp; +pub mod voice; #[derive(Clone, Default, Debug, PartialEq, Eq, Hash)] /// A URLBundle bundles together the API-, Gateway- and CDN-URLs of a Spacebar instance. diff --git a/src/voice_gateway.rs b/src/voice/gateway.rs similarity index 100% rename from src/voice_gateway.rs rename to src/voice/gateway.rs diff --git a/src/voice/mod.rs b/src/voice/mod.rs new file mode 100644 index 0000000..417fb0e --- /dev/null +++ b/src/voice/mod.rs @@ -0,0 +1,4 @@ +//! Module for all voice functionality within chorus. + +pub mod gateway; +pub mod udp; diff --git a/src/voice/udp.rs b/src/voice/udp.rs new file mode 100644 index 0000000..461d2f1 --- /dev/null +++ b/src/voice/udp.rs @@ -0,0 +1 @@ +//! Defines voice raw udp socket handling diff --git a/src/voice_udp.rs b/src/voice_udp.rs deleted file mode 100644 index ec1b08a..0000000 --- a/src/voice_udp.rs +++ /dev/null @@ -1 +0,0 @@ -/// Defines voice raw udp socket handling From fb42b6b71382bd0f370079611f90fa4ca82230d6 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:53:23 +0100 Subject: [PATCH 27/72] fix: deserialization error in speaking bitflags --- src/types/events/webrtc/speaking.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/webrtc/speaking.rs index b854876..adbbe00 100644 --- a/src/types/events/webrtc/speaking.rs +++ b/src/types/events/webrtc/speaking.rs @@ -10,8 +10,10 @@ use crate::types::{Snowflake, WebSocketEvent}; /// See #[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct Speaking { - /// Data about the audio we're transmitting, its type - pub speaking: SpeakingBitflags, + /// Data about the audio we're transmitting. + /// + /// See [SpeakingBitFlags] + pub speaking: u8, pub ssrc: i32, /// The user id of the speaking user, only sent by the server #[serde(skip_serializing)] From 8278cc2162292b2eeaf3bbfed784b5fdef44f486 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 12:54:32 +0100 Subject: [PATCH 28/72] feat: kinda janky ip discovery impl --- Cargo.lock | 641 +++++++++++++++++-------------- Cargo.toml | 3 +- src/types/events/webrtc/ready.rs | 4 +- src/voice/udp.rs | 114 ++++++ 4 files changed, 465 insertions(+), 297 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df89f42..bac7fc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,21 +19,22 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -61,13 +62,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -108,9 +109,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -126,9 +127,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -144,21 +145,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" @@ -180,11 +181,12 @@ name = "chorus" version = "0.9.0" dependencies = [ "async-trait", - "base64 0.21.3", - "bitflags 2.4.0", + "base64 0.21.5", + "bitflags 2.4.1", "chorus-macros", "chrono", "custom_error", + "discortp", "futures-util", "hostname", "http", @@ -217,21 +219,20 @@ checksum = "a81545a60b926f815517dadbbd40cd502294ae2baea25fa8194d854d607512b0" dependencies = [ "async-trait", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "chrono" -version = "0.4.28" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", "windows-targets", ] @@ -269,9 +270,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -287,9 +288,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-queue" @@ -347,7 +348,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -358,7 +359,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -380,10 +381,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ + "powerfmt", "serde", ] @@ -399,6 +401,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "discortp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524b9439c09174aede2c88d58cfc6b83575b06569d1af4d07562f76595b2896b" +dependencies = [ + "pnet_macros", + "pnet_macros_support", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -441,25 +453,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -479,19 +480,24 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -533,9 +539,9 @@ checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3" [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -543,15 +549,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -571,38 +577,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-core", "futures-io", @@ -636,13 +642,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -678,9 +684,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", "allocator-api2", @@ -692,7 +698,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.2", ] [[package]] @@ -701,7 +707,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -730,9 +736,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -780,9 +786,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150" dependencies = [ "bytes", "fnv", @@ -829,7 +835,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -851,16 +857,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -901,20 +907,20 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.2", "serde", ] [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "ipnetwork" @@ -927,9 +933,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -942,9 +948,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -955,7 +961,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "pem", "ring", "serde", @@ -974,15 +980,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" @@ -997,15 +1003,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1025,18 +1031,19 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if", "digest", ] [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -1071,12 +1078,12 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys", ] @@ -1104,6 +1111,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0" +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "nom" version = "7.1.3" @@ -1165,9 +1178,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -1200,11 +1213,11 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -1221,7 +1234,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -1232,9 +1245,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.92" +version = "0.9.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" dependencies = [ "cc", "libc", @@ -1254,9 +1267,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", @@ -1295,26 +1308,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.31", -] - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1354,6 +1347,36 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "pnet_base" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d3a993d49e5fd5d4d854d6999d4addca1f72d86c65adf224a36757161c02b6" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_macros" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd52a5211fac27e7acb14cfc9f30ae16ae0e956b7b779c8214c74559cef4c3" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", +] + +[[package]] +name = "pnet_macros_support" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89de095dc7739349559913aed1ef6a11e73ceade4897dadc77c5e09de6740750" +dependencies = [ + "pnet_base", +] + [[package]] name = "poem" version = "1.3.58" @@ -1392,9 +1415,15 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1413,9 +1442,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1461,18 +1490,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -1482,9 +1511,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -1493,17 +1522,17 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -1525,6 +1554,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-native-tls", "tower-service", @@ -1561,16 +1591,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" dependencies = [ - "byteorder", "const-oid", "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", @@ -1589,11 +1617,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.11" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -1658,9 +1686,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] @@ -1678,20 +1706,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -1700,13 +1728,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -1723,38 +1751,38 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.3", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.0", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", - "time 0.3.28", + "time", ] [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -1763,9 +1791,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1800,7 +1828,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.28", + "time", ] [[package]] @@ -1814,15 +1842,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -1830,9 +1858,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys", @@ -1865,9 +1893,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ "itertools", "nom", @@ -1876,9 +1904,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" +checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1889,9 +1917,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" +checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" dependencies = [ "ahash", "atoi", @@ -1910,7 +1938,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.0", + "indexmap 2.1.0", "ipnetwork", "log", "memchr", @@ -1932,9 +1960,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" +checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" dependencies = [ "proc-macro2", "quote", @@ -1945,9 +1973,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" +checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" dependencies = [ "dotenvy", "either", @@ -1971,13 +1999,13 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" +checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", - "base64 0.21.3", - "bitflags 2.4.0", + "base64 0.21.5", + "bitflags 2.4.1", "byteorder", "bytes", "chrono", @@ -2014,13 +2042,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" +checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", - "base64 0.21.3", - "bitflags 2.4.0", + "base64 0.21.5", + "bitflags 2.4.1", "byteorder", "chrono", "crc", @@ -2055,9 +2083,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" +checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" dependencies = [ "atoi", "chrono", @@ -2078,10 +2106,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -2111,9 +2140,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -2121,10 +2150,31 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.8.0" +name = "system-configuration" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", @@ -2135,43 +2185,33 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "time" -version = "0.1.45" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -2179,15 +2219,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2209,9 +2249,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -2221,20 +2261,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] @@ -2260,9 +2300,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -2274,9 +2314,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2297,17 +2337,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.1.0", "toml_datetime", "winnow", ] @@ -2320,11 +2360,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2333,20 +2372,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -2359,9 +2398,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", @@ -2379,9 +2418,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uncased" @@ -2409,9 +2448,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2430,9 +2469,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unicode_categories" @@ -2484,12 +2523,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2498,9 +2531,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2508,24 +2541,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" dependencies = [ "cfg-if", "js-sys", @@ -2535,9 +2568,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2545,28 +2578,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -2601,10 +2634,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -2677,9 +2710,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] @@ -2694,6 +2727,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "zerocopy" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 42a5130..3106f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ website = ["https://discord.com/invite/m3FpcapGDD"] [features] default = ["client"] backend = ["poem", "sqlx"] -client = [] +client = ["discortp"] [dependencies] tokio = { version = "1.29.1", features = ["macros"] } @@ -51,6 +51,7 @@ jsonwebtoken = "8.3.0" log = "0.4.20" async-trait = "0.1.73" chorus-macros = "0.2.0" +discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord"] } [dev-dependencies] tokio = { version = "1.32.0", features = ["full"] } diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/webrtc/ready.rs index 1a39750..8b6ef8e 100644 --- a/src/types/events/webrtc/ready.rs +++ b/src/types/events/webrtc/ready.rs @@ -13,9 +13,9 @@ use super::VoiceEncryptionMode; /// See pub struct VoiceReady { /// See - pub ssrc: i32, + pub ssrc: u32, pub ip: Ipv4Addr, - pub port: u32, + pub port: u16, /// The available encryption modes for the webrtc connection pub modes: Vec, #[serde(default)] diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 461d2f1..b974add 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -1 +1,115 @@ //! Defines voice raw udp socket handling + +use std::net::SocketAddr; + +use log::{info, warn}; +use tokio::net::UdpSocket; + +use discortp::{discord::IpDiscoveryPacket, MutablePacket, Packet}; + +#[derive(Debug)] +pub struct UdpHandler { + url: SocketAddr, + socket: UdpSocket, +} + +impl UdpHandler { + pub async fn new(url: SocketAddr, ssrc: u32) { + // Bind with a port number of 0, so the os assigns this listener a port + let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); + + udp_socket.connect(url).await.unwrap(); + + // First perform ip discovery + let ip_discovery = discortp::discord::IpDiscovery { + pkt_type: discortp::discord::IpDiscoveryType::Request, + ssrc, + length: 70, + address: Vec::new(), + port: 0, + payload: Vec::new(), + }; + + let mut buf: Vec = Vec::new(); + + let size = IpDiscoveryPacket::minimum_packet_size() + 64; + + // wtf + for _i in 0..size { + buf.push(0); + } + + let mut ip_discovery_packet = + discortp::discord::MutableIpDiscoveryPacket::new(&mut buf).expect("FUcking kill me"); + + ip_discovery_packet.populate(&ip_discovery); + + let data = ip_discovery_packet.packet(); + + info!("VUDP: Sending Ip Discovery {:?}", &data); + + udp_socket.send(&data).await.unwrap(); + + info!("VUDP: Sent packet discovery request"); + + // Handle the ip discovery response + let receieved_size = udp_socket.recv(&mut buf).await.unwrap(); + info!( + "VUDP: Receiving messsage: {:?} - (expected {} vs real {})", + buf.clone(), + size, + receieved_size + ); + + let receieved_ip_discovery = discortp::discord::IpDiscoveryPacket::new(&buf).unwrap(); + + info!( + "VUDP: Received ip discovery!!! {:?}", + receieved_ip_discovery + ); + + let mut handler = UdpHandler { + url, + socket: udp_socket, + }; + + // Now we can continuously check for messages in a different task + tokio::spawn(async move { + handler.listen_task().await; + }); + } + + /// The main listen task; + /// + /// Receives udp messages and parses them. + pub async fn listen_task(&mut self) { + loop { + let mut buf: Vec = Vec::new(); + + let size = IpDiscoveryPacket::minimum_packet_size() + 64; + + // wtf + for _i in 0..size { + buf.push(0); + } + let msg = self.socket.recv(&mut buf).await; + if let Ok(size) = msg { + info!("VUDP: Receiving messsage: {:?} - {}", buf.clone(), size); + self.handle_message(&buf[0..size]).await; + continue; + } + + if let Err(e) = msg { + warn!("VUDP: {:?}", e); + } + + //warn!("VUDP: Voice UDP is broken, closing connection"); + //break; + } + } + + /// Handles a message buf + async fn handle_message(&self, buf: &[u8]) { + info!("VUDP: Received messsage {:?}", buf); + } +} From b973ecb447c4857fee212e0eca8d792392a3da34 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 13:33:29 +0100 Subject: [PATCH 29/72] feat: return ip discovery data + minor update --- src/types/events/webrtc/select_protocol.rs | 6 +++--- src/voice/udp.rs | 24 ++++++++++++++-------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/types/events/webrtc/select_protocol.rs b/src/types/events/webrtc/select_protocol.rs index 3cda068..6d57bbf 100644 --- a/src/types/events/webrtc/select_protocol.rs +++ b/src/types/events/webrtc/select_protocol.rs @@ -41,9 +41,9 @@ pub enum VoiceProtocol { /// See pub struct SelectProtocolData { /// Our external ip - pub address: Ipv4Addr, + pub address: Vec, /// Our external udp port - pub port: u32, + pub port: u16, /// The mode of encryption to use pub mode: VoiceEncryptionMode, } @@ -51,7 +51,7 @@ pub struct SelectProtocolData { impl Default for SelectProtocolData { fn default() -> Self { SelectProtocolData { - address: Ipv4Addr::UNSPECIFIED, + address: Vec::new(), port: 0, mode: Default::default(), } diff --git a/src/voice/udp.rs b/src/voice/udp.rs index b974add..7543f71 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -5,7 +5,10 @@ use std::net::SocketAddr; use log::{info, warn}; use tokio::net::UdpSocket; -use discortp::{discord::IpDiscoveryPacket, MutablePacket, Packet}; +use discortp::{ + discord::{IpDiscovery, IpDiscoveryPacket}, + MutablePacket, Packet, +}; #[derive(Debug)] pub struct UdpHandler { @@ -14,7 +17,7 @@ pub struct UdpHandler { } impl UdpHandler { - pub async fn new(url: SocketAddr, ssrc: u32) { + pub async fn new(url: SocketAddr, ssrc: u32) -> IpDiscovery { // Bind with a port number of 0, so the os assigns this listener a port let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); @@ -77,6 +80,15 @@ impl UdpHandler { tokio::spawn(async move { handler.listen_task().await; }); + + return IpDiscovery { + pkt_type: receieved_ip_discovery.get_pkt_type(), + length: receieved_ip_discovery.get_length(), + ssrc: receieved_ip_discovery.get_ssrc(), + address: receieved_ip_discovery.get_address(), + port: receieved_ip_discovery.get_port(), + payload: Vec::new(), + }; } /// The main listen task; @@ -99,12 +111,8 @@ impl UdpHandler { continue; } - if let Err(e) = msg { - warn!("VUDP: {:?}", e); - } - - //warn!("VUDP: Voice UDP is broken, closing connection"); - //break; + warn!("VUDP: Voice UDP is broken, closing connection"); + break; } } From 9460219d14591c5bd902466bef8cd22c1a4b9260 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:59:28 +0100 Subject: [PATCH 30/72] feat: packet parsing! --- Cargo.toml | 2 +- src/voice/udp.rs | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3106f1e..d824218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ jsonwebtoken = "8.3.0" log = "0.4.20" async-trait = "0.1.73" chorus-macros = "0.2.0" -discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord"] } +discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord", "demux"] } [dev-dependencies] tokio = { version = "1.32.0", features = ["full"] } diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 7543f71..44dee7a 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -6,7 +6,9 @@ use log::{info, warn}; use tokio::net::UdpSocket; use discortp::{ + demux::{demux, Demuxed}, discord::{IpDiscovery, IpDiscoveryPacket}, + rtp::RtpPacket, MutablePacket, Packet, }; @@ -98,15 +100,12 @@ impl UdpHandler { loop { let mut buf: Vec = Vec::new(); - let size = IpDiscoveryPacket::minimum_packet_size() + 64; - // wtf - for _i in 0..size { + for _i in 0..1_000 { buf.push(0); } let msg = self.socket.recv(&mut buf).await; if let Ok(size) = msg { - info!("VUDP: Receiving messsage: {:?} - {}", buf.clone(), size); self.handle_message(&buf[0..size]).await; continue; } @@ -118,6 +117,26 @@ impl UdpHandler { /// Handles a message buf async fn handle_message(&self, buf: &[u8]) { - info!("VUDP: Received messsage {:?}", buf); + info!("VUDP: Received messsage"); + + let parsed = demux(buf); + + match parsed { + Demuxed::Rtp(rtp) => { + let data = buf[11..buf.len()].to_vec(); + info!("VUDP: Parsed packet as rtp! {:?}; data: {:?}", rtp, data); + } + Demuxed::Rtcp(rtcp) => { + info!("VUDP: Parsed packet as rtcp! {:?}", rtcp); + } + Demuxed::FailedParse(e) => { + warn!("VUDP: Failed to parse packet: {:?}", e); + } + Demuxed::TooSmall => { + unreachable!() + } + } + + return; } } From f5c5e1cc5ec9c3bf281a2ee37e06df9c1a9f31df Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:17:45 +0100 Subject: [PATCH 31/72] fix: voice works again --- src/gateway/heartbeat.rs | 2 +- src/gateway/mod.rs | 2 +- src/voice/gateway.rs | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index dd162b7..00fa090 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -3,7 +3,7 @@ use crate::types; use super::*; /// The amount of time we wait for a heartbeat ack before resending our heartbeat in ms -const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; +pub const HEARTBEAT_ACK_TIMEOUT: u64 = 2000; /// Handles sending heartbeats to the gateway in another thread #[allow(dead_code)] // FIXME: Remove this, once HeartbeatHandler is used diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ebd06cc..3a14a23 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -128,7 +128,7 @@ impl GatewayEvent { } /// Notifies the observers of the GatewayEvent. - async fn notify(&self, new_event_data: T) { + pub(crate) async fn notify(&self, new_event_data: T) { for observer in &self.observers { observer.update(&new_event_data).await; } diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index edca815..ec6019d 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -2,7 +2,6 @@ use futures_util::stream::{SplitSink, SplitStream}; use futures_util::SinkExt; use futures_util::StreamExt; use log::{debug, info, trace, warn}; -use native_tls::TlsConnector; use serde_json::json; use std::sync::Arc; use std::time::Duration; @@ -16,7 +15,7 @@ use tokio_tungstenite::MaybeTlsStream; use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream}; use crate::errors::VoiceGatewayError; -use crate::gateway::{GatewayEvent, HEARTBEAT_ACK_TIMEOUT}; +use crate::gateway::{heartbeat::HEARTBEAT_ACK_TIMEOUT, GatewayEvent}; use crate::types::{ self, SelectProtocol, Speaking, VoiceGatewayReceivePayload, VoiceGatewaySendPayload, VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_CLIENT_CONNECT_FLAGS, @@ -203,12 +202,21 @@ impl VoiceGateway { let processed_url = format!("wss://{}/?v=7", websocket_url); trace!("Created voice socket url: {}", processed_url.clone()); + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") + { + roots.add(&rustls::Certificate(cert.0)).unwrap(); + } let (websocket_stream, _) = match connect_async_tls_with_config( &processed_url, None, false, - Some(Connector::NativeTls( - TlsConnector::builder().build().unwrap(), + Some(Connector::Rustls( + rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth() + .into(), )), ) .await From 7b3beaf23c17dbc98009a6172c302843cfeb7182 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Wed, 22 Nov 2023 19:27:46 +0100 Subject: [PATCH 32/72] feat: add voice_media_sink_wants (comitting uncommited changes to merge) --- src/types/events/webrtc/media_sink_wants.rs | 14 ++++++++++++++ src/types/events/webrtc/mod.rs | 9 +++++++++ src/voice/gateway.rs | 19 ++++++++++++++++--- 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/types/events/webrtc/media_sink_wants.rs diff --git a/src/types/events/webrtc/media_sink_wants.rs b/src/types/events/webrtc/media_sink_wants.rs new file mode 100644 index 0000000..8731d85 --- /dev/null +++ b/src/types/events/webrtc/media_sink_wants.rs @@ -0,0 +1,14 @@ +use crate::types::WebSocketEvent; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Copy)] +/// What does this do? +/// +/// {"op":15,"d":{"any":100}} +/// +/// Opcode from +pub struct VoiceMediaSinkWants { + pub any: u16, +} + +impl WebSocketEvent for VoiceMediaSinkWants {} diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/webrtc/mod.rs index 3c87aa0..3cd13eb 100644 --- a/src/types/events/webrtc/mod.rs +++ b/src/types/events/webrtc/mod.rs @@ -6,6 +6,7 @@ pub use client_connect::*; pub use client_disconnect::*; pub use hello::*; pub use identify::*; +pub use media_sink_wants::*; pub use ready::*; pub use select_protocol::*; pub use session_description::*; @@ -16,6 +17,7 @@ mod client_connect; mod client_disconnect; mod hello; mod identify; +mod media_sink_wants; mod ready; mod select_protocol; mod session_description; @@ -104,6 +106,13 @@ pub const VOICE_RESUMED: u8 = 9; pub const VOICE_VIDEO: u8 = 12; pub const VOICE_CLIENT_DISCONNECT: u8 = 13; pub const VOICE_SESSION_UPDATE: u8 = 14; + +/// What is this? +/// +/// {"op":15,"d":{"any":100}} +/// +/// Opcode from +pub const VOICE_MEDIA_SINK_WANTS: u8 = 15; /// See /// Sent with empty data from the client, the server responds with the voice backend version; pub const VOICE_BACKEND_VERSION: u8 = 16; diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index ec6019d..8e66033 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -20,8 +20,8 @@ use crate::types::{ self, SelectProtocol, Speaking, VoiceGatewayReceivePayload, VoiceGatewaySendPayload, VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_CLIENT_CONNECT_FLAGS, VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, - VOICE_HELLO, VOICE_IDENTIFY, VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, - VOICE_SESSION_DESCRIPTION, VOICE_SESSION_UPDATE, VOICE_SPEAKING, + VOICE_HELLO, VOICE_IDENTIFY, VOICE_MEDIA_SINK_WANTS, VOICE_READY, VOICE_RESUME, + VOICE_SELECT_PROTOCOL, VOICE_SESSION_DESCRIPTION, VOICE_SESSION_UPDATE, VOICE_SPEAKING, }; use self::voice_events::VoiceEvents; @@ -454,7 +454,19 @@ impl VoiceGateway { return; } } + VOICE_MEDIA_SINK_WANTS => { + trace!("VGW: Received Media Sink Wants"); + let event = &mut self.events.lock().await.media_sink_wants; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_MEDIA_SINK_WANTS ({})", + result.err().unwrap() + ); + return; + } + } // We received a heartbeat from the server // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." VOICE_HEARTBEAT => { @@ -650,7 +662,7 @@ struct VoiceHeartbeatThreadCommunication { pub mod voice_events { use crate::types::{ SessionDescription, SessionUpdate, VoiceBackendVersion, VoiceClientConnectFlags, - VoiceClientConnectPlatform, VoiceClientDisconnection, VoiceReady, + VoiceClientConnectPlatform, VoiceClientDisconnection, VoiceMediaSinkWants, VoiceReady, }; use super::*; @@ -665,6 +677,7 @@ pub mod voice_events { pub client_disconnect: GatewayEvent, pub client_connect_flags: GatewayEvent, pub client_connect_platform: GatewayEvent, + pub media_sink_wants: GatewayEvent, pub error: GatewayEvent, } } From b8d344d7458f41d89fece172d972940b6f710f0e Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 09:34:30 +0100 Subject: [PATCH 33/72] chore: rename events/webrtc to events/voice_gateway --- src/types/events/mod.rs | 4 ++-- src/types/events/{webrtc => voice_gateway}/client_connect.rs | 0 .../events/{webrtc => voice_gateway}/client_disconnect.rs | 0 src/types/events/{webrtc => voice_gateway}/hello.rs | 0 src/types/events/{webrtc => voice_gateway}/identify.rs | 0 .../events/{webrtc => voice_gateway}/media_sink_wants.rs | 0 src/types/events/{webrtc => voice_gateway}/mod.rs | 0 src/types/events/{webrtc => voice_gateway}/ready.rs | 0 src/types/events/{webrtc => voice_gateway}/select_protocol.rs | 0 .../events/{webrtc => voice_gateway}/session_description.rs | 0 src/types/events/{webrtc => voice_gateway}/speaking.rs | 0 .../events/{webrtc => voice_gateway}/voice_backend_version.rs | 0 12 files changed, 2 insertions(+), 2 deletions(-) rename src/types/events/{webrtc => voice_gateway}/client_connect.rs (100%) rename src/types/events/{webrtc => voice_gateway}/client_disconnect.rs (100%) rename src/types/events/{webrtc => voice_gateway}/hello.rs (100%) rename src/types/events/{webrtc => voice_gateway}/identify.rs (100%) rename src/types/events/{webrtc => voice_gateway}/media_sink_wants.rs (100%) rename src/types/events/{webrtc => voice_gateway}/mod.rs (100%) rename src/types/events/{webrtc => voice_gateway}/ready.rs (100%) rename src/types/events/{webrtc => voice_gateway}/select_protocol.rs (100%) rename src/types/events/{webrtc => voice_gateway}/session_description.rs (100%) rename src/types/events/{webrtc => voice_gateway}/speaking.rs (100%) rename src/types/events/{webrtc => voice_gateway}/voice_backend_version.rs (100%) diff --git a/src/types/events/mod.rs b/src/types/events/mod.rs index f4e926c..63ea13a 100644 --- a/src/types/events/mod.rs +++ b/src/types/events/mod.rs @@ -24,8 +24,8 @@ pub use stage_instance::*; pub use thread::*; pub use user::*; pub use voice::*; +pub use voice_gateway::*; pub use webhooks::*; -pub use webrtc::*; #[cfg(feature = "client")] use super::Snowflake; @@ -72,7 +72,7 @@ mod user; mod voice; mod webhooks; -mod webrtc; +mod voice_gateway; pub trait WebSocketEvent: Send + Sync + Debug {} diff --git a/src/types/events/webrtc/client_connect.rs b/src/types/events/voice_gateway/client_connect.rs similarity index 100% rename from src/types/events/webrtc/client_connect.rs rename to src/types/events/voice_gateway/client_connect.rs diff --git a/src/types/events/webrtc/client_disconnect.rs b/src/types/events/voice_gateway/client_disconnect.rs similarity index 100% rename from src/types/events/webrtc/client_disconnect.rs rename to src/types/events/voice_gateway/client_disconnect.rs diff --git a/src/types/events/webrtc/hello.rs b/src/types/events/voice_gateway/hello.rs similarity index 100% rename from src/types/events/webrtc/hello.rs rename to src/types/events/voice_gateway/hello.rs diff --git a/src/types/events/webrtc/identify.rs b/src/types/events/voice_gateway/identify.rs similarity index 100% rename from src/types/events/webrtc/identify.rs rename to src/types/events/voice_gateway/identify.rs diff --git a/src/types/events/webrtc/media_sink_wants.rs b/src/types/events/voice_gateway/media_sink_wants.rs similarity index 100% rename from src/types/events/webrtc/media_sink_wants.rs rename to src/types/events/voice_gateway/media_sink_wants.rs diff --git a/src/types/events/webrtc/mod.rs b/src/types/events/voice_gateway/mod.rs similarity index 100% rename from src/types/events/webrtc/mod.rs rename to src/types/events/voice_gateway/mod.rs diff --git a/src/types/events/webrtc/ready.rs b/src/types/events/voice_gateway/ready.rs similarity index 100% rename from src/types/events/webrtc/ready.rs rename to src/types/events/voice_gateway/ready.rs diff --git a/src/types/events/webrtc/select_protocol.rs b/src/types/events/voice_gateway/select_protocol.rs similarity index 100% rename from src/types/events/webrtc/select_protocol.rs rename to src/types/events/voice_gateway/select_protocol.rs diff --git a/src/types/events/webrtc/session_description.rs b/src/types/events/voice_gateway/session_description.rs similarity index 100% rename from src/types/events/webrtc/session_description.rs rename to src/types/events/voice_gateway/session_description.rs diff --git a/src/types/events/webrtc/speaking.rs b/src/types/events/voice_gateway/speaking.rs similarity index 100% rename from src/types/events/webrtc/speaking.rs rename to src/types/events/voice_gateway/speaking.rs diff --git a/src/types/events/webrtc/voice_backend_version.rs b/src/types/events/voice_gateway/voice_backend_version.rs similarity index 100% rename from src/types/events/webrtc/voice_backend_version.rs rename to src/types/events/voice_gateway/voice_backend_version.rs From 03d47aebe8783ae7257a43b5d60f29c1e2140578 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 09:39:50 +0100 Subject: [PATCH 34/72] Add UdpHandle --- src/voice/udp.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 44dee7a..b12b431 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -1,9 +1,9 @@ //! Defines voice raw udp socket handling -use std::net::SocketAddr; +use std::{net::SocketAddr, sync::Arc}; use log::{info, warn}; -use tokio::net::UdpSocket; +use tokio::{net::UdpSocket, sync::Mutex}; use discortp::{ demux::{demux, Demuxed}, @@ -12,14 +12,24 @@ use discortp::{ MutablePacket, Packet, }; +/// Handle to a voice udp connection +/// +/// Can be safely cloned and will still correspond to the same connection. +#[derive(Debug, Clone)] +pub struct UdpHandle { + /// Ip discovery data we received on init + pub ip_discovery: IpDiscovery, + socket: Arc, +} + #[derive(Debug)] pub struct UdpHandler { url: SocketAddr, - socket: UdpSocket, + socket: Arc, } impl UdpHandler { - pub async fn new(url: SocketAddr, ssrc: u32) -> IpDiscovery { + pub async fn new(url: SocketAddr, ssrc: u32) -> UdpHandle { // Bind with a port number of 0, so the os assigns this listener a port let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); @@ -39,13 +49,13 @@ impl UdpHandler { let size = IpDiscoveryPacket::minimum_packet_size() + 64; - // wtf for _i in 0..size { buf.push(0); } - let mut ip_discovery_packet = - discortp::discord::MutableIpDiscoveryPacket::new(&mut buf).expect("FUcking kill me"); + // TODO: Make this not panic everything + let mut ip_discovery_packet = discortp::discord::MutableIpDiscoveryPacket::new(&mut buf) + .expect("Mangled ip discovery packet"); ip_discovery_packet.populate(&ip_discovery); @@ -73,9 +83,11 @@ impl UdpHandler { receieved_ip_discovery ); + let socket = Arc::new(udp_socket); + let mut handler = UdpHandler { url, - socket: udp_socket, + socket: socket.clone(), }; // Now we can continuously check for messages in a different task @@ -83,7 +95,7 @@ impl UdpHandler { handler.listen_task().await; }); - return IpDiscovery { + let ip_discovery = IpDiscovery { pkt_type: receieved_ip_discovery.get_pkt_type(), length: receieved_ip_discovery.get_length(), ssrc: receieved_ip_discovery.get_ssrc(), @@ -91,6 +103,11 @@ impl UdpHandler { port: receieved_ip_discovery.get_port(), payload: Vec::new(), }; + + return UdpHandle { + ip_discovery, + socket, + }; } /// The main listen task; @@ -100,7 +117,7 @@ impl UdpHandler { loop { let mut buf: Vec = Vec::new(); - // wtf + // FIXME: is there a better way to do this? for _i in 0..1_000 { buf.push(0); } From a3ad3cce0baf1ef837d2a1121844522b21953d75 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 11:23:03 +0100 Subject: [PATCH 35/72] chore: clippy + other misc updates --- .../events/voice_gateway/select_protocol.rs | 14 +---------- src/voice/gateway.rs | 14 +++++------ src/voice/udp.rs | 25 ++++++++----------- 3 files changed, 17 insertions(+), 36 deletions(-) diff --git a/src/types/events/voice_gateway/select_protocol.rs b/src/types/events/voice_gateway/select_protocol.rs index 6d57bbf..38d674a 100644 --- a/src/types/events/voice_gateway/select_protocol.rs +++ b/src/types/events/voice_gateway/select_protocol.rs @@ -1,5 +1,3 @@ -use std::net::Ipv4Addr; - use serde::{Deserialize, Serialize}; use super::VoiceEncryptionMode; @@ -35,7 +33,7 @@ pub enum VoiceProtocol { //Webrtc, } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Default, Deserialize, Serialize, Clone)] /// The data field of the SelectProtocol Event /// /// See @@ -47,13 +45,3 @@ pub struct SelectProtocolData { /// The mode of encryption to use pub mode: VoiceEncryptionMode, } - -impl Default for SelectProtocolData { - fn default() -> Self { - SelectProtocolData { - address: Vec::new(), - port: 0, - mode: Default::default(), - } - } -} diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index 8e66033..00bebe4 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -192,12 +192,11 @@ pub struct VoiceGateway { >, websocket_receive: SplitStream>>, kill_send: tokio::sync::broadcast::Sender<()>, - url: String, } impl VoiceGateway { #[allow(clippy::new_ret_no_self)] - pub async fn new(websocket_url: String) -> Result { + pub async fn spawn(websocket_url: String) -> Result { // 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()); @@ -269,7 +268,6 @@ impl VoiceGateway { websocket_send: shared_websocket_send.clone(), websocket_receive, kill_send: kill_send.clone(), - url: websocket_url.clone(), }; // Now we can continuously check for messages in a different task, since we aren't going to receive another hello @@ -501,13 +499,13 @@ impl VoiceGateway { .unwrap(); } VOICE_IDENTIFY | VOICE_SELECT_PROTOCOL | VOICE_RESUME => { - let error = VoiceGatewayError::UnexpectedOpcodeReceived { - opcode: gateway_payload.op_code, - }; - Err::<(), VoiceGatewayError>(error).unwrap(); + info!( + "VGW: Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation and is likely not the fault of chorus.", + gateway_payload.op_code + ); } _ => { - warn!("Received unrecognized voice gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); + warn!("VGW: Received unrecognized voice gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); } } } diff --git a/src/voice/udp.rs b/src/voice/udp.rs index b12b431..29b690b 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -3,13 +3,12 @@ use std::{net::SocketAddr, sync::Arc}; use log::{info, warn}; -use tokio::{net::UdpSocket, sync::Mutex}; +use tokio::net::UdpSocket; use discortp::{ demux::{demux, Demuxed}, - discord::{IpDiscovery, IpDiscoveryPacket}, - rtp::RtpPacket, - MutablePacket, Packet, + discord::{IpDiscovery, IpDiscoveryPacket, IpDiscoveryType, MutableIpDiscoveryPacket}, + Packet, }; /// Handle to a voice udp connection @@ -24,20 +23,19 @@ pub struct UdpHandle { #[derive(Debug)] pub struct UdpHandler { - url: SocketAddr, socket: Arc, } impl UdpHandler { - pub async fn new(url: SocketAddr, ssrc: u32) -> UdpHandle { + pub async fn spawn(url: SocketAddr, ssrc: u32) -> UdpHandle { // Bind with a port number of 0, so the os assigns this listener a port let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); udp_socket.connect(url).await.unwrap(); // First perform ip discovery - let ip_discovery = discortp::discord::IpDiscovery { - pkt_type: discortp::discord::IpDiscoveryType::Request, + let ip_discovery = IpDiscovery { + pkt_type: IpDiscoveryType::Request, ssrc, length: 70, address: Vec::new(), @@ -54,8 +52,8 @@ impl UdpHandler { } // TODO: Make this not panic everything - let mut ip_discovery_packet = discortp::discord::MutableIpDiscoveryPacket::new(&mut buf) - .expect("Mangled ip discovery packet"); + let mut ip_discovery_packet = + MutableIpDiscoveryPacket::new(&mut buf).expect("Mangled ip discovery packet"); ip_discovery_packet.populate(&ip_discovery); @@ -63,7 +61,7 @@ impl UdpHandler { info!("VUDP: Sending Ip Discovery {:?}", &data); - udp_socket.send(&data).await.unwrap(); + udp_socket.send(data).await.unwrap(); info!("VUDP: Sent packet discovery request"); @@ -76,7 +74,7 @@ impl UdpHandler { receieved_size ); - let receieved_ip_discovery = discortp::discord::IpDiscoveryPacket::new(&buf).unwrap(); + let receieved_ip_discovery = IpDiscoveryPacket::new(&buf).unwrap(); info!( "VUDP: Received ip discovery!!! {:?}", @@ -86,7 +84,6 @@ impl UdpHandler { let socket = Arc::new(udp_socket); let mut handler = UdpHandler { - url, socket: socket.clone(), }; @@ -153,7 +150,5 @@ impl UdpHandler { unreachable!() } } - - return; } } From 6a5d58329ded7fc7da9e06fcd9f432a65f5d8dd3 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 11:25:33 +0100 Subject: [PATCH 36/72] fix: attempt to fix failing wasm build --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2498e7b..22947fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,8 +54,6 @@ sqlx = { version = "0.7.1", features = [ ], optional = true } thiserror = "1.0.47" discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord", "demux"] } -rustls = "0.21.8" -rustls-native-certs = "0.6.3" safina-timer = "0.1.11" rand = "0.8.5" From 5abd143145933c820f891ca9d6f92e605502fb5c Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 11:38:40 +0100 Subject: [PATCH 37/72] chore: yes clippy, that is indeed an unneeded return statement --- src/voice/gateway.rs | 9 --------- src/voice/udp.rs | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index 00bebe4..8880f3c 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -361,7 +361,6 @@ impl VoiceGateway { let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; if result.is_err() { warn!("Failed to parse VOICE_READY ({})", result.err().unwrap()); - return; } } VOICE_BACKEND_VERSION => { @@ -374,7 +373,6 @@ impl VoiceGateway { "Failed to parse VOICE_BACKEND_VERSION ({})", result.err().unwrap() ); - return; } } VOICE_SESSION_DESCRIPTION => { @@ -387,7 +385,6 @@ impl VoiceGateway { "Failed to parse VOICE_SESSION_DESCRIPTION ({})", result.err().unwrap() ); - return; } } VOICE_SESSION_UPDATE => { @@ -400,7 +397,6 @@ impl VoiceGateway { "Failed to parse VOICE_SESSION_UPDATE ({})", result.err().unwrap() ); - return; } } VOICE_SPEAKING => { @@ -410,7 +406,6 @@ impl VoiceGateway { let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; if result.is_err() { warn!("Failed to parse VOICE_SPEAKING ({})", result.err().unwrap()); - return; } } VOICE_CLIENT_DISCONNECT => { @@ -423,7 +418,6 @@ impl VoiceGateway { "Failed to parse VOICE_CLIENT_DISCONNECT ({})", result.err().unwrap() ); - return; } } VOICE_CLIENT_CONNECT_FLAGS => { @@ -436,7 +430,6 @@ impl VoiceGateway { "Failed to parse VOICE_CLIENT_CONNECT_FLAGS ({})", result.err().unwrap() ); - return; } } VOICE_CLIENT_CONNECT_PLATFORM => { @@ -449,7 +442,6 @@ impl VoiceGateway { "Failed to parse VOICE_CLIENT_CONNECT_PLATFORM ({})", result.err().unwrap() ); - return; } } VOICE_MEDIA_SINK_WANTS => { @@ -462,7 +454,6 @@ impl VoiceGateway { "Failed to parse VOICE_MEDIA_SINK_WANTS ({})", result.err().unwrap() ); - return; } } // We received a heartbeat from the server diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 29b690b..7830613 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -101,10 +101,10 @@ impl UdpHandler { payload: Vec::new(), }; - return UdpHandle { + UdpHandle { ip_discovery, socket, - }; + } } /// The main listen task; From 51ce2b8ef8ca5481327d6ecc4eeba9b26491738e Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 12:19:06 +0100 Subject: [PATCH 38/72] feat: add VoiceData struct --- src/voice/mod.rs | 1 + src/voice/voice_data.rs | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/voice/voice_data.rs diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 417fb0e..3b598dc 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -2,3 +2,4 @@ pub mod gateway; pub mod udp; +pub mod voice_data; diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs new file mode 100644 index 0000000..f18ce5e --- /dev/null +++ b/src/voice/voice_data.rs @@ -0,0 +1,13 @@ +use discortp::discord::IpDiscovery; + +use crate::types::{Snowflake, VoiceReady, VoiceServerUpdate}; + +#[derive(Debug, Default)] +/// Saves data shared between parts of the voice architecture +pub struct VoiceData { + pub server_data: Option, + pub ready_data: Option, + pub user_id: Snowflake, + pub session_id: String, + pub ip_discovery: Option, +} From 66f14a1c218e90a9a1a1f032c8b6de3087bb5ea3 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 12:20:02 +0100 Subject: [PATCH 39/72] feat: add VoiceData reference to UdpHandler --- src/voice/udp.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 7830613..d384b0d 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -3,7 +3,7 @@ use std::{net::SocketAddr, sync::Arc}; use log::{info, warn}; -use tokio::net::UdpSocket; +use tokio::{net::UdpSocket, sync::Mutex}; use discortp::{ demux::{demux, Demuxed}, @@ -11,6 +11,8 @@ use discortp::{ Packet, }; +use super::voice_data::VoiceData; + /// Handle to a voice udp connection /// /// Can be safely cloned and will still correspond to the same connection. @@ -23,11 +25,16 @@ pub struct UdpHandle { #[derive(Debug)] pub struct UdpHandler { + data: Arc>, socket: Arc, } impl UdpHandler { - pub async fn spawn(url: SocketAddr, ssrc: u32) -> UdpHandle { + pub async fn spawn( + data_reference: Arc>, + url: SocketAddr, + ssrc: u32, + ) -> UdpHandle { // Bind with a port number of 0, so the os assigns this listener a port let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); @@ -84,6 +91,7 @@ impl UdpHandler { let socket = Arc::new(udp_socket); let mut handler = UdpHandler { + data: data_reference, socket: socket.clone(), }; From c86a3126158c3f5e1399d6cbec3f4075a7868187 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 13:46:29 +0100 Subject: [PATCH 40/72] feat: decryption? --- Cargo.lock | 84 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/voice/udp.rs | 67 +++++++++++++++++++++++++++++--- src/voice/voice_data.rs | 3 +- 4 files changed, 149 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17b54fe..e98b15d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.8.6" @@ -206,6 +216,7 @@ dependencies = [ "bitflags 2.4.1", "chorus-macros", "chrono", + "crypto_secretbox", "custom_error", "discortp", "futures-util", @@ -265,6 +276,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -347,9 +369,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "custom_error" version = "1.9.2" @@ -658,6 +696,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -938,6 +977,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -1238,6 +1286,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.61" @@ -1457,6 +1511,17 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1747,6 +1812,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "schannel" version = "0.1.22" @@ -2595,6 +2669,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 22947fd..ce87394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ backend = ["dep:poem", "dep:sqlx"] rt-multi-thread = ["tokio/rt-multi-thread"] rt = ["tokio/rt"] client = ["voice"] -voice = ["dep:discortp"] +voice = ["dep:discortp", "dep:crypto_secretbox"] [dependencies] tokio = { version = "1.34.0", features = ["macros", "sync"] } @@ -54,6 +54,7 @@ sqlx = { version = "0.7.1", features = [ ], optional = true } thiserror = "1.0.47" discortp = { version = "0.5.0", optional = true, features = ["rtp", "discord", "demux"] } +crypto_secretbox = {version = "0.1.1", optional = true} safina-timer = "0.1.11" rand = "0.8.5" diff --git a/src/voice/udp.rs b/src/voice/udp.rs index d384b0d..36703df 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -2,9 +2,13 @@ use std::{net::SocketAddr, sync::Arc}; -use log::{info, warn}; +use log::{debug, info, warn, trace}; use tokio::{net::UdpSocket, sync::Mutex}; +use crypto_secretbox::{ + aead::Aead, cipher::generic_array::GenericArray, KeyInit, XSalsa20Poly1305, +}; + use discortp::{ demux::{demux, Demuxed}, discord::{IpDiscovery, IpDiscoveryPacket, IpDiscoveryType, MutableIpDiscoveryPacket}, @@ -139,20 +143,71 @@ impl UdpHandler { /// Handles a message buf async fn handle_message(&self, buf: &[u8]) { - info!("VUDP: Received messsage"); let parsed = demux(buf); match parsed { Demuxed::Rtp(rtp) => { - let data = buf[11..buf.len()].to_vec(); - info!("VUDP: Parsed packet as rtp! {:?}; data: {:?}", rtp, data); + let ciphertext = buf[12..buf.len()].to_vec(); + trace!( + "VUDP: Parsed packet as rtp! {:?}; data: {:?}", + rtp, ciphertext + ); + + let data_lock = self.data.lock().await; + + let session_description_result = data_lock.session_description.clone(); + + if session_description_result.is_none() { + warn!("VUDP: Received encyrpted voice data, but no encryption key, CANNOT DECRYPT!"); + return; + } + + let session_description = session_description_result.unwrap(); + + let nonce; + + let mut rtp_header = buf[0..12].to_vec(); + + match session_description.encryption_mode { + crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { + + // The header is only 12 bytes, but the nonce has to be 24 + // This actually works mind you, and anything else doesn't + for _i in 0..12 { + rtp_header.push(0); + } + + nonce = GenericArray::from_slice(&rtp_header); + } + _ => { + unimplemented!(); + } + } + + let key = GenericArray::from_slice(&session_description.secret_key); + + let decryptor = XSalsa20Poly1305::new(key); + + let decryption_result = decryptor.decrypt(nonce, ciphertext.as_ref()); + + if let Err(decryption_error) = decryption_result { + warn!( + "VUDP: Failed to decypt voice data! ({:?})", + decryption_error + ); + return; + } + + let decrypted = decryption_result.unwrap(); + + info!("VUDP: SUCCESSFULLY DECRYPTED VOICE DATA!!! {:?}", decrypted); } Demuxed::Rtcp(rtcp) => { - info!("VUDP: Parsed packet as rtcp! {:?}", rtcp); + trace!("VUDP: Parsed packet as rtcp! {:?}", rtcp); } Demuxed::FailedParse(e) => { - warn!("VUDP: Failed to parse packet: {:?}", e); + trace!("VUDP: Failed to parse packet: {:?}", e); } Demuxed::TooSmall => { unreachable!() diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs index f18ce5e..a4740b1 100644 --- a/src/voice/voice_data.rs +++ b/src/voice/voice_data.rs @@ -1,12 +1,13 @@ use discortp::discord::IpDiscovery; -use crate::types::{Snowflake, VoiceReady, VoiceServerUpdate}; +use crate::types::{Snowflake, VoiceReady, VoiceServerUpdate, SessionDescription}; #[derive(Debug, Default)] /// Saves data shared between parts of the voice architecture pub struct VoiceData { pub server_data: Option, pub ready_data: Option, + pub session_description: Option, pub user_id: Snowflake, pub session_id: String, pub ip_discovery: Option, From 13c9e558fb0e92f061e9220dc72edaf26807e6ed Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:31:48 +0100 Subject: [PATCH 41/72] chore: formatting --- src/voice/udp.rs | 7 +++---- src/voice/voice_data.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 36703df..03e784f 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -2,7 +2,7 @@ use std::{net::SocketAddr, sync::Arc}; -use log::{debug, info, warn, trace}; +use log::{debug, info, trace, warn}; use tokio::{net::UdpSocket, sync::Mutex}; use crypto_secretbox::{ @@ -143,7 +143,6 @@ impl UdpHandler { /// Handles a message buf async fn handle_message(&self, buf: &[u8]) { - let parsed = demux(buf); match parsed { @@ -151,7 +150,8 @@ impl UdpHandler { let ciphertext = buf[12..buf.len()].to_vec(); trace!( "VUDP: Parsed packet as rtp! {:?}; data: {:?}", - rtp, ciphertext + rtp, + ciphertext ); let data_lock = self.data.lock().await; @@ -171,7 +171,6 @@ impl UdpHandler { match session_description.encryption_mode { crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - // The header is only 12 bytes, but the nonce has to be 24 // This actually works mind you, and anything else doesn't for _i in 0..12 { diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs index a4740b1..b2bc175 100644 --- a/src/voice/voice_data.rs +++ b/src/voice/voice_data.rs @@ -1,6 +1,6 @@ use discortp::discord::IpDiscovery; -use crate::types::{Snowflake, VoiceReady, VoiceServerUpdate, SessionDescription}; +use crate::types::{SessionDescription, Snowflake, VoiceReady, VoiceServerUpdate}; #[derive(Debug, Default)] /// Saves data shared between parts of the voice architecture From 2cd4dda9f4a005679f87369fdd2552ab52dde4a2 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:55:29 +0100 Subject: [PATCH 42/72] feat: add ssrc definition (op 12) --- src/types/events/voice_gateway/mod.rs | 5 ++- .../events/voice_gateway/ssrc_definition.rs | 39 +++++++++++++++++++ src/voice/gateway.rs | 33 +++++++++++++--- 3 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/types/events/voice_gateway/ssrc_definition.rs diff --git a/src/types/events/voice_gateway/mod.rs b/src/types/events/voice_gateway/mod.rs index 3cd13eb..39e0342 100644 --- a/src/types/events/voice_gateway/mod.rs +++ b/src/types/events/voice_gateway/mod.rs @@ -12,6 +12,7 @@ pub use select_protocol::*; pub use session_description::*; pub use speaking::*; pub use voice_backend_version::*; +pub use ssrc_definition::*; mod client_connect; mod client_disconnect; @@ -23,6 +24,7 @@ mod select_protocol; mod session_description; mod speaking; mod voice_backend_version; +mod ssrc_definition; #[derive(Debug, Default, Serialize, Clone)] /// The payload used for sending events to the webrtc gateway. @@ -102,8 +104,7 @@ pub const VOICE_HEARTBEAT_ACK: u8 = 6; pub const VOICE_RESUME: u8 = 7; pub const VOICE_HELLO: u8 = 8; pub const VOICE_RESUMED: u8 = 9; -/// See -pub const VOICE_VIDEO: u8 = 12; +pub const VOICE_SSRC_DEFINITION: u8 = 12; pub const VOICE_CLIENT_DISCONNECT: u8 = 13; pub const VOICE_SESSION_UPDATE: u8 = 14; diff --git a/src/types/events/voice_gateway/ssrc_definition.rs b/src/types/events/voice_gateway/ssrc_definition.rs new file mode 100644 index 0000000..08192fb --- /dev/null +++ b/src/types/events/voice_gateway/ssrc_definition.rs @@ -0,0 +1,39 @@ +use crate::types::{Snowflake, WebSocketEvent}; +use serde::{Serialize, Deserialize}; + +/// Defines an event which provides ssrcs for voice and video for a user id. +/// +/// This event is sent via opcode 12. +/// +/// Examples of the event: +/// +/// When receiving: +/// ``` +/// {"op":12,"d":{"video_ssrc":0,"user_id":"463640391196082177","streams":[{"ssrc":26595,"rtx_ssrc":26596,"rid":"100","quality":100,"max_resolution":{"width":1280,"type":"fixed","height":720},"max_framerate":30,"active":false}],"audio_ssrc":26597}}{"op":12,"d":{"video_ssrc":0,"user_id":"463640391196082177","streams":[{"ssrc":26595,"rtx_ssrc":26596,"rid":"100","quality":100,"max_resolution":{"width":1280,"type":"fixed","height":720},"max_framerate":30,"active":false}],"audio_ssrc":26597}} +/// ``` +/// +/// When sending: +/// ``` +/// {"op":12,"d":{"audio_ssrc":2307250864,"video_ssrc":0,"rtx_ssrc":0,"streams":[{"type":"video","rid":"100","ssrc":26595,"active":false,"quality":100,"rtx_ssrc":26596,"max_bitrate":2500000,"max_framerate":30,"max_resolution":{"type":"fixed","width":1280,"height":720}}]}} +/// ``` +#[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] +pub struct SsrcDefinition { + /// The ssrc used for video communications. + /// + /// Is always sent and received, though is 0 if describing only the audio ssrc. + #[serde(default)] + pub video_ssrc: usize, + /// The ssrc used for audio communications. + /// + /// Is always sent and received, though is 0 if describing only the video ssrc. + #[serde(default)] + pub audio_ssrc: usize, + /// The user id these ssrcs apply to. + /// + /// Is never sent by the user and is filled in by the server + #[serde(skip_serializing)] + pub user_id: Option, + // TODO: Add video streams +} + +impl WebSocketEvent for SsrcDefinition {} diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index 8880f3c..c8f374a 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -17,11 +17,12 @@ use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStrea use crate::errors::VoiceGatewayError; use crate::gateway::{heartbeat::HEARTBEAT_ACK_TIMEOUT, GatewayEvent}; use crate::types::{ - self, SelectProtocol, Speaking, VoiceGatewayReceivePayload, VoiceGatewaySendPayload, - VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, VOICE_CLIENT_CONNECT_FLAGS, - VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, - VOICE_HELLO, VOICE_IDENTIFY, VOICE_MEDIA_SINK_WANTS, VOICE_READY, VOICE_RESUME, - VOICE_SELECT_PROTOCOL, VOICE_SESSION_DESCRIPTION, VOICE_SESSION_UPDATE, VOICE_SPEAKING, + self, SelectProtocol, Speaking, SsrcDefinition, VoiceGatewayReceivePayload, + VoiceGatewaySendPayload, VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, + VOICE_CLIENT_CONNECT_FLAGS, VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, + VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, VOICE_HELLO, VOICE_IDENTIFY, VOICE_MEDIA_SINK_WANTS, + VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, VOICE_SESSION_DESCRIPTION, + VOICE_SESSION_UPDATE, VOICE_SPEAKING, VOICE_SSRC_DEFINITION, }; use self::voice_events::VoiceEvents; @@ -159,6 +160,15 @@ impl VoiceGatewayHandle { self.send_json(VOICE_SPEAKING, to_send_value).await; } + /// Sends an ssrc definition event + pub async fn send_ssrc_definition(&self, to_send: SsrcDefinition) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("VGW: Sending SsrcDefinition"); + + self.send_json(VOICE_SSRC_DEFINITION, to_send_value).await; + } + /// Sends a voice backend version request to the gateway pub async fn send_voice_backend_version_request(&self) { let data_empty_object = json!("{}"); @@ -408,6 +418,18 @@ impl VoiceGateway { warn!("Failed to parse VOICE_SPEAKING ({})", result.err().unwrap()); } } + VOICE_SSRC_DEFINITION => { + trace!("VGW: Received Ssrc Definition"); + + let event = &mut self.events.lock().await.ssrc_definition; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_SSRC_DEFINITION ({})", + result.err().unwrap() + ); + } + } VOICE_CLIENT_DISCONNECT => { trace!("VGW: Received Client Disconnect"); @@ -663,6 +685,7 @@ pub mod voice_events { pub session_description: GatewayEvent, pub session_update: GatewayEvent, pub speaking: GatewayEvent, + pub ssrc_definition: GatewayEvent, pub client_disconnect: GatewayEvent, pub client_connect_flags: GatewayEvent, pub client_connect_platform: GatewayEvent, From 17f54568411c1bbd2fe45af796810f32ea695c5a Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 20:19:09 +0100 Subject: [PATCH 43/72] feat: add untested sending & asbtract nonce generation --- src/types/events/voice_gateway/mod.rs | 4 +- .../events/voice_gateway/ssrc_definition.rs | 2 +- src/voice/crypto.rs | 19 +++ src/voice/mod.rs | 4 + src/voice/udp.rs | 156 ++++++++++++++---- 5 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 src/voice/crypto.rs diff --git a/src/types/events/voice_gateway/mod.rs b/src/types/events/voice_gateway/mod.rs index 39e0342..e23e38d 100644 --- a/src/types/events/voice_gateway/mod.rs +++ b/src/types/events/voice_gateway/mod.rs @@ -11,8 +11,8 @@ pub use ready::*; pub use select_protocol::*; pub use session_description::*; pub use speaking::*; -pub use voice_backend_version::*; pub use ssrc_definition::*; +pub use voice_backend_version::*; mod client_connect; mod client_disconnect; @@ -23,8 +23,8 @@ mod ready; mod select_protocol; mod session_description; mod speaking; -mod voice_backend_version; mod ssrc_definition; +mod voice_backend_version; #[derive(Debug, Default, Serialize, Clone)] /// The payload used for sending events to the webrtc gateway. diff --git a/src/types/events/voice_gateway/ssrc_definition.rs b/src/types/events/voice_gateway/ssrc_definition.rs index 08192fb..738f483 100644 --- a/src/types/events/voice_gateway/ssrc_definition.rs +++ b/src/types/events/voice_gateway/ssrc_definition.rs @@ -1,5 +1,5 @@ use crate::types::{Snowflake, WebSocketEvent}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; /// Defines an event which provides ssrcs for voice and video for a user id. /// diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs new file mode 100644 index 0000000..6bba0ad --- /dev/null +++ b/src/voice/crypto.rs @@ -0,0 +1,19 @@ +//! Defines cryptography functions used within the voice implementation. +//! +//! All functions in this module return a 24 byte long [Vec]. + +use discortp::Packet; + +/// Gets an xsalsa20poly1305 nonce from an rtppacket. +pub(crate) fn get_xsalsa20_poly1305_nonce(packet: discortp::rtp::RtpPacket) -> Vec { + + let mut rtp_header = packet.packet()[0..12].to_vec(); + + // The header is only 12 bytes, but the nonce has to be 24 + // This actually works mind you, and anything else doesn't + for _i in 0..12 { + rtp_header.push(0); + } + + return rtp_header; +} diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 3b598dc..d1c08f7 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -3,3 +3,7 @@ pub mod gateway; pub mod udp; pub mod voice_data; +mod crypto; + +// Pub use this so users can interact with packet types if they want +pub use discortp; diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 03e784f..1c9d2d6 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -1,9 +1,11 @@ //! Defines voice raw udp socket handling +use super::crypto; + use std::{net::SocketAddr, sync::Arc}; use log::{debug, info, trace, warn}; -use tokio::{net::UdpSocket, sync::Mutex}; +use tokio::{net::UdpSocket, sync::RwLock}; use crypto_secretbox::{ aead::Aead, cipher::generic_array::GenericArray, KeyInit, XSalsa20Poly1305, @@ -17,25 +19,123 @@ use discortp::{ use super::voice_data::VoiceData; +/// See +/// This always adds up to 12 +const RTP_HEADER_SIZE: u8 = 12; + /// Handle to a voice udp connection /// /// Can be safely cloned and will still correspond to the same connection. #[derive(Debug, Clone)] pub struct UdpHandle { - /// Ip discovery data we received on init - pub ip_discovery: IpDiscovery, socket: Arc, + pub data: Arc>, +} + +impl UdpHandle { + /// Constructs and sends encoded opus rtp data. + /// + /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. + pub async fn send_opus_data(&self, sequence: u16, timestamp: u32, payload: Vec) { + let data_lock = self.data.read().await; + let ssrc = data_lock.ready_data.clone().unwrap().ssrc; + + let payload_len = payload.len(); + + let rtp_data = discortp::rtp::Rtp { + // Always the same + version: 2, + padding: 0, + extension: 1, + csrc_count: 0, + csrc_list: Vec::new(), + marker: 0, + payload_type: discortp::rtp::RtpType::Dynamic(120), + // Actually variable + sequence: sequence.into(), + timestamp: timestamp.into(), + ssrc, + payload, + }; + + let mut buffer = Vec::new(); + + let buffer_size = payload_len + RTP_HEADER_SIZE as usize; + + // Fill the buffer + for _i in 0..buffer_size { + buffer.push(0); + } + + let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); + rtp_packet.populate(&rtp_data); + + self.send_rtp_packet(rtp_packet).await; + } + + /// Encrypts and sends and rtp packet. + pub async fn send_rtp_packet(&self, packet: discortp::rtp::MutableRtpPacket<'_>) { + let mut mutable_packet = packet; + self.encrypt_rtp_packet(&mut mutable_packet).await; + self.send_encrypted_rtp_packet(mutable_packet.consume_to_immutable()) + .await; + } + + /// Encrypts an unecnrypted rtp packet, mutating its payload. + pub async fn encrypt_rtp_packet(&self, packet: &mut discortp::rtp::MutableRtpPacket<'_>) { + let payload = packet.payload(); + + let data_lock = self.data.read().await; + + let session_description_result = data_lock.session_description.clone(); + + if session_description_result.is_none() { + // FIXME: Make this function reutrn a result with a proper error type for these kinds + // of functions + panic!("Trying to encrypt packet but no key provided yet"); + } + + let session_description = session_description_result.unwrap(); + + let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.to_immutable()); + let nonce = GenericArray::from_slice(&nonce_bytes); + + let key = GenericArray::from_slice(&session_description.secret_key); + + let encryptor = XSalsa20Poly1305::new(key); + + let encryption_result = encryptor.encrypt(nonce, payload); + + if encryption_result.is_err() { + // FIXME: See above fixme + panic!("Encryption error"); + } + + let encrypted_payload = encryption_result.unwrap(); + + packet.set_payload(&encrypted_payload); + } + + /// Sends an (already encrypted) rtp packet to the connection. + pub async fn send_encrypted_rtp_packet(&self, packet: discortp::rtp::RtpPacket<'_>) { + let raw_bytes = packet.packet(); + + self.socket.send(raw_bytes).await.unwrap(); + } } #[derive(Debug)] pub struct UdpHandler { - data: Arc>, + pub data: Arc>, socket: Arc, } impl UdpHandler { + /// Spawns a new udp handler and performs ip discovery. + /// + /// Mutates the given data_reference with the ip discovery data. pub async fn spawn( - data_reference: Arc>, + data_reference: Arc>, url: SocketAddr, ssrc: u32, ) -> UdpHandle { @@ -92,18 +192,6 @@ impl UdpHandler { receieved_ip_discovery ); - let socket = Arc::new(udp_socket); - - let mut handler = UdpHandler { - data: data_reference, - socket: socket.clone(), - }; - - // Now we can continuously check for messages in a different task - tokio::spawn(async move { - handler.listen_task().await; - }); - let ip_discovery = IpDiscovery { pkt_type: receieved_ip_discovery.get_pkt_type(), length: receieved_ip_discovery.get_length(), @@ -113,9 +201,25 @@ impl UdpHandler { payload: Vec::new(), }; + let mut data_reference_lock = data_reference.write().await; + data_reference_lock.ip_discovery = Some(ip_discovery); + drop(data_reference_lock); + + let socket = Arc::new(udp_socket); + + let mut handler = UdpHandler { + data: data_reference.clone(), + socket: socket.clone(), + }; + + // Now we can continuously check for messages in a different task + tokio::spawn(async move { + handler.listen_task().await; + }); + UdpHandle { - ip_discovery, socket, + data: data_reference, } } @@ -154,7 +258,7 @@ impl UdpHandler { ciphertext ); - let data_lock = self.data.lock().await; + let data_lock = self.data.read().await; let session_description_result = data_lock.session_description.clone(); @@ -165,25 +269,19 @@ impl UdpHandler { let session_description = session_description_result.unwrap(); - let nonce; - - let mut rtp_header = buf[0..12].to_vec(); + let nonce_bytes; match session_description.encryption_mode { crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - // The header is only 12 bytes, but the nonce has to be 24 - // This actually works mind you, and anything else doesn't - for _i in 0..12 { - rtp_header.push(0); - } - - nonce = GenericArray::from_slice(&rtp_header); + nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(rtp); } _ => { unimplemented!(); } } + let nonce = GenericArray::from_slice(&nonce_bytes); + let key = GenericArray::from_slice(&session_description.secret_key); let decryptor = XSalsa20Poly1305::new(key); From ba4818dbad9c4608a5aad361412f2f99e4060ada Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:56:04 +0100 Subject: [PATCH 44/72] feat: Public api! (sorta) --- src/gateway/mod.rs | 6 + src/types/events/voice_gateway/speaking.rs | 2 +- src/voice/crypto.rs | 7 +- src/voice/gateway.rs | 13 +- src/voice/mod.rs | 2 +- src/voice/udp.rs | 138 ++++++++++++++++++--- 6 files changed, 140 insertions(+), 28 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index a4f27a1..7586d6f 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -94,6 +94,12 @@ pub struct GatewayEvent { } impl GatewayEvent { + pub fn new() -> Self { + Self { + observers: Vec::new(), + } + } + /// Returns true if the GatewayEvent is observed by at least one Observer. pub fn is_observed(&self) -> bool { !self.observers.is_empty() diff --git a/src/types/events/voice_gateway/speaking.rs b/src/types/events/voice_gateway/speaking.rs index adbbe00..c31e7e1 100644 --- a/src/types/events/voice_gateway/speaking.rs +++ b/src/types/events/voice_gateway/speaking.rs @@ -14,7 +14,7 @@ pub struct Speaking { /// /// See [SpeakingBitFlags] pub speaking: u8, - pub ssrc: i32, + pub ssrc: u32, /// The user id of the speaking user, only sent by the server #[serde(skip_serializing)] pub user_id: Option, diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 6bba0ad..ccf39b6 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -2,12 +2,9 @@ //! //! All functions in this module return a 24 byte long [Vec]. -use discortp::Packet; - /// Gets an xsalsa20poly1305 nonce from an rtppacket. -pub(crate) fn get_xsalsa20_poly1305_nonce(packet: discortp::rtp::RtpPacket) -> Vec { - - let mut rtp_header = packet.packet()[0..12].to_vec(); +pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { + let mut rtp_header = packet[0..12].to_vec(); // The header is only 12 bytes, but the nonce has to be 24 // This actually works mind you, and anything else doesn't diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs index c8f374a..3616cd3 100644 --- a/src/voice/gateway.rs +++ b/src/voice/gateway.rs @@ -671,13 +671,16 @@ struct VoiceHeartbeatThreadCommunication { } pub mod voice_events { - use crate::types::{ - SessionDescription, SessionUpdate, VoiceBackendVersion, VoiceClientConnectFlags, - VoiceClientConnectPlatform, VoiceClientDisconnection, VoiceMediaSinkWants, VoiceReady, + use crate::{ + errors::VoiceGatewayError, + gateway::GatewayEvent, + types::{ + SessionDescription, SessionUpdate, Speaking, SsrcDefinition, VoiceBackendVersion, + VoiceClientConnectFlags, VoiceClientConnectPlatform, VoiceClientDisconnection, + VoiceMediaSinkWants, VoiceReady, + }, }; - use super::*; - #[derive(Default, Debug)] pub struct VoiceEvents { pub voice_ready: GatewayEvent, diff --git a/src/voice/mod.rs b/src/voice/mod.rs index d1c08f7..8d84a0a 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -1,9 +1,9 @@ //! Module for all voice functionality within chorus. +mod crypto; pub mod gateway; pub mod udp; pub mod voice_data; -mod crypto; // Pub use this so users can interact with packet types if they want pub use discortp; diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 1c9d2d6..126cc0a 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -1,11 +1,16 @@ //! Defines voice raw udp socket handling +use self::voice_udp_events::VoiceUDPEvents; + use super::crypto; use std::{net::SocketAddr, sync::Arc}; use log::{debug, info, trace, warn}; -use tokio::{net::UdpSocket, sync::RwLock}; +use tokio::{ + net::UdpSocket, + sync::{Mutex, RwLock}, +}; use crypto_secretbox::{ aead::Aead, cipher::generic_array::GenericArray, KeyInit, XSalsa20Poly1305, @@ -14,6 +19,7 @@ use crypto_secretbox::{ use discortp::{ demux::{demux, Demuxed}, discord::{IpDiscovery, IpDiscoveryPacket, IpDiscoveryType, MutableIpDiscoveryPacket}, + rtcp::report::{ReceiverReport, SenderReport}, Packet, }; @@ -28,6 +34,7 @@ const RTP_HEADER_SIZE: u8 = 12; /// Can be safely cloned and will still correspond to the same connection. #[derive(Debug, Clone)] pub struct UdpHandle { + pub events: Arc>, socket: Arc, pub data: Arc>, } @@ -75,14 +82,17 @@ impl UdpHandle { /// Encrypts and sends and rtp packet. pub async fn send_rtp_packet(&self, packet: discortp::rtp::MutableRtpPacket<'_>) { - let mut mutable_packet = packet; - self.encrypt_rtp_packet(&mut mutable_packet).await; - self.send_encrypted_rtp_packet(mutable_packet.consume_to_immutable()) + let mut buffer = self.encrypt_rtp_packet_payload(&packet).await; + let new_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); + self.send_encrypted_rtp_packet(new_packet.consume_to_immutable()) .await; } - /// Encrypts an unecnrypted rtp packet, mutating its payload. - pub async fn encrypt_rtp_packet(&self, packet: &mut discortp::rtp::MutableRtpPacket<'_>) { + /// Encrypts an unencrypted rtp packet, returning an encrypted copy if its payload. + pub async fn encrypt_rtp_packet_payload( + &self, + packet: &discortp::rtp::MutableRtpPacket<'_>, + ) -> Vec { let payload = packet.payload(); let data_lock = self.data.read().await; @@ -97,7 +107,7 @@ impl UdpHandle { let session_description = session_description_result.unwrap(); - let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.to_immutable()); + let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.packet()); let nonce = GenericArray::from_slice(&nonce_bytes); let key = GenericArray::from_slice(&session_description.secret_key); @@ -113,7 +123,18 @@ impl UdpHandle { let encrypted_payload = encryption_result.unwrap(); - packet.set_payload(&encrypted_payload); + // We need to allocate a new buffer, since the old one is too small for our new encrypted + // data + let mut new_buffer = packet.packet().to_vec(); + + let buffer_size = encrypted_payload.len() + RTP_HEADER_SIZE as usize; + + // Fill the buffer + while new_buffer.len() <= buffer_size { + new_buffer.push(0); + } + + new_buffer } /// Sends an (already encrypted) rtp packet to the connection. @@ -121,11 +142,14 @@ impl UdpHandle { let raw_bytes = packet.packet(); self.socket.send(raw_bytes).await.unwrap(); + + debug!("VUDP: Sent rtp packet!"); } } #[derive(Debug)] pub struct UdpHandler { + events: Arc>, pub data: Arc>, socket: Arc, } @@ -207,7 +231,11 @@ impl UdpHandler { let socket = Arc::new(udp_socket); + let events = VoiceUDPEvents::default(); + let shared_events = Arc::new(Mutex::new(events)); + let mut handler = UdpHandler { + events: shared_events.clone(), data: data_reference.clone(), socket: socket.clone(), }; @@ -218,6 +246,7 @@ impl UdpHandler { }); UdpHandle { + events: shared_events, socket, data: data_reference, } @@ -252,11 +281,7 @@ impl UdpHandler { match parsed { Demuxed::Rtp(rtp) => { let ciphertext = buf[12..buf.len()].to_vec(); - trace!( - "VUDP: Parsed packet as rtp! {:?}; data: {:?}", - rtp, - ciphertext - ); + trace!("VUDP: Parsed packet as rtp!"); let data_lock = self.data.read().await; @@ -273,7 +298,7 @@ impl UdpHandler { match session_description.encryption_mode { crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(rtp); + nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(rtp.packet()); } _ => { unimplemented!(); @@ -298,10 +323,66 @@ impl UdpHandler { let decrypted = decryption_result.unwrap(); - info!("VUDP: SUCCESSFULLY DECRYPTED VOICE DATA!!! {:?}", decrypted); + debug!("VUDP: Successfully decrypted voice data!"); + + let rtp_with_decrypted_data = discortp::rtp::Rtp { + ssrc: rtp.get_ssrc(), + marker: rtp.get_marker(), + version: rtp.get_version(), + padding: rtp.get_padding(), + sequence: rtp.get_sequence(), + extension: rtp.get_extension(), + timestamp: rtp.get_timestamp(), + csrc_list: rtp.get_csrc_list(), + csrc_count: rtp.get_csrc_count(), + payload_type: rtp.get_payload_type(), + payload: decrypted, + }; + + self.events + .lock() + .await + .rtp + .notify(rtp_with_decrypted_data) + .await; } Demuxed::Rtcp(rtcp) => { - trace!("VUDP: Parsed packet as rtcp! {:?}", rtcp); + trace!("VUDP: Parsed packet as rtcp!"); + + let rtcp_data; + + match rtcp { + discortp::rtcp::RtcpPacket::KnownType(knowntype) => { + rtcp_data = discortp::rtcp::Rtcp::KnownType(knowntype); + } + discortp::rtcp::RtcpPacket::SenderReport(senderreport) => { + rtcp_data = discortp::rtcp::Rtcp::SenderReport(SenderReport { + payload: senderreport.payload().to_vec(), + padding: senderreport.get_padding(), + version: senderreport.get_version(), + ssrc: senderreport.get_ssrc(), + pkt_length: senderreport.get_pkt_length(), + packet_type: senderreport.get_packet_type(), + rx_report_count: senderreport.get_rx_report_count(), + }); + } + discortp::rtcp::RtcpPacket::ReceiverReport(receiverreport) => { + rtcp_data = discortp::rtcp::Rtcp::ReceiverReport(ReceiverReport { + payload: receiverreport.payload().to_vec(), + padding: receiverreport.get_padding(), + version: receiverreport.get_version(), + ssrc: receiverreport.get_ssrc(), + pkt_length: receiverreport.get_pkt_length(), + packet_type: receiverreport.get_packet_type(), + rx_report_count: receiverreport.get_rx_report_count(), + }); + } + _ => { + unreachable!(); + } + } + + self.events.lock().await.rtcp.notify(rtcp_data).await; } Demuxed::FailedParse(e) => { trace!("VUDP: Failed to parse packet: {:?}", e); @@ -312,3 +393,28 @@ impl UdpHandler { } } } + +pub mod voice_udp_events { + + use discortp::{rtcp::Rtcp, rtp::Rtp}; + + use crate::{gateway::GatewayEvent, types::WebSocketEvent}; + + impl WebSocketEvent for Rtp {} + impl WebSocketEvent for Rtcp {} + + #[derive(Debug)] + pub struct VoiceUDPEvents { + pub rtp: GatewayEvent, + pub rtcp: GatewayEvent, + } + + impl Default for VoiceUDPEvents { + fn default() -> Self { + Self { + rtp: GatewayEvent::new(), + rtcp: GatewayEvent::new(), + } + } + } +} From 3875e2e7ee67a9f6535965243bcfebefa9a89f7d Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 17 Dec 2023 11:51:02 +0100 Subject: [PATCH 45/72] small updates --- src/voice/udp.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 126cc0a..97897bf 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -53,7 +53,7 @@ impl UdpHandle { // Always the same version: 2, padding: 0, - extension: 1, + extension: 0, csrc_count: 0, csrc_list: Vec::new(), marker: 0, @@ -65,6 +65,8 @@ impl UdpHandle { payload, }; + debug!("VUDP: Constructed udp data: {:?}", rtp_data); + let mut buffer = Vec::new(); let buffer_size = payload_len + RTP_HEADER_SIZE as usize; @@ -88,7 +90,8 @@ impl UdpHandle { .await; } - /// Encrypts an unencrypted rtp packet, returning an encrypted copy if its payload. + /// Encrypts an unencrypted rtp packet, returning a copy of the packet's bytes with an + /// encrypted payload pub async fn encrypt_rtp_packet_payload( &self, packet: &discortp::rtp::MutableRtpPacket<'_>, @@ -263,8 +266,8 @@ impl UdpHandler { for _i in 0..1_000 { buf.push(0); } - let msg = self.socket.recv(&mut buf).await; - if let Ok(size) = msg { + let result = self.socket.recv(&mut buf).await; + if let Ok(size) = result { self.handle_message(&buf[0..size]).await; continue; } From 19dc9c8ffd3bc41f066cd389f96bc931a0bf288f Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sun, 17 Dec 2023 13:47:11 +0100 Subject: [PATCH 46/72] feat: add sequence number --- src/voice/udp.rs | 19 +++++++------------ src/voice/voice_data.rs | 2 ++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 97897bf..6d12176 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -43,9 +43,10 @@ impl UdpHandle { /// Constructs and sends encoded opus rtp data. /// /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. - pub async fn send_opus_data(&self, sequence: u16, timestamp: u32, payload: Vec) { - let data_lock = self.data.read().await; - let ssrc = data_lock.ready_data.clone().unwrap().ssrc; + pub async fn send_opus_data(&self, timestamp: u32, payload: Vec) { + let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc.clone(); + let sequence_number = self.data.read().await.last_sequence_number.clone().wrapping_add(1); + self.data.write().await.last_sequence_number = sequence_number; let payload_len = payload.len(); @@ -59,14 +60,12 @@ impl UdpHandle { marker: 0, payload_type: discortp::rtp::RtpType::Dynamic(120), // Actually variable - sequence: sequence.into(), + sequence: sequence_number.into(), timestamp: timestamp.into(), ssrc, payload, }; - debug!("VUDP: Constructed udp data: {:?}", rtp_data); - let mut buffer = Vec::new(); let buffer_size = payload_len + RTP_HEADER_SIZE as usize; @@ -98,9 +97,7 @@ impl UdpHandle { ) -> Vec { let payload = packet.payload(); - let data_lock = self.data.read().await; - - let session_description_result = data_lock.session_description.clone(); + let session_description_result = self.data.read().await.session_description.clone(); if session_description_result.is_none() { // FIXME: Make this function reutrn a result with a proper error type for these kinds @@ -286,9 +283,7 @@ impl UdpHandler { let ciphertext = buf[12..buf.len()].to_vec(); trace!("VUDP: Parsed packet as rtp!"); - let data_lock = self.data.read().await; - - let session_description_result = data_lock.session_description.clone(); + let session_description_result = self.data.read().await.session_description.clone(); if session_description_result.is_none() { warn!("VUDP: Received encyrpted voice data, but no encryption key, CANNOT DECRYPT!"); diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs index b2bc175..5d37d9c 100644 --- a/src/voice/voice_data.rs +++ b/src/voice/voice_data.rs @@ -10,5 +10,7 @@ pub struct VoiceData { pub session_description: Option, pub user_id: Snowflake, pub session_id: String, + /// The last sequence number we used, has to be incremeted by one every time we send a message + pub last_sequence_number: u16, pub ip_discovery: Option, } From 8aefa65093881351b2c0757eb5636c631e4cd5db Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:22:53 +0100 Subject: [PATCH 47/72] chore: yes --- src/voice/udp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 6d12176..38d71c2 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -44,8 +44,8 @@ impl UdpHandle { /// /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. pub async fn send_opus_data(&self, timestamp: u32, payload: Vec) { - let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc.clone(); - let sequence_number = self.data.read().await.last_sequence_number.clone().wrapping_add(1); + let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc; + let sequence_number = self.data.read().await.last_sequence_number.wrapping_add(1); self.data.write().await.last_sequence_number = sequence_number; let payload_len = payload.len(); From db4dcae5793a84c720ffa2b70a70edec70d3e748 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Wed, 27 Dec 2023 21:48:35 +0100 Subject: [PATCH 48/72] feat: merge VoiceHandler into official development --- src/voice/handler.rs | 134 +++++++++++++++++++++++++++++++++++++++++++ src/voice/mod.rs | 1 + 2 files changed, 135 insertions(+) create mode 100644 src/voice/handler.rs diff --git a/src/voice/handler.rs b/src/voice/handler.rs new file mode 100644 index 0000000..e757f9a --- /dev/null +++ b/src/voice/handler.rs @@ -0,0 +1,134 @@ +use std::{sync::Arc, net::SocketAddrV4}; + +use async_trait::async_trait; +use tokio::sync::{Mutex, RwLock}; + +use crate::{gateway::Observer, types::{VoiceServerUpdate, VoiceIdentify, VoiceReady, SessionDescription, GatewayReady, Snowflake, SelectProtocol, VoiceProtocol, SelectProtocolData, VoiceEncryptionMode}}; + +use super::{gateway::{VoiceGatewayHandle, VoiceGateway}, udp::UdpHandle, udp::UdpHandler, voice_data::VoiceData}; + +/// Handles inbetween connections between the gateway and udp modules +#[derive(Debug, Clone)] +pub struct VoiceHandler { + pub voice_gateway_connection: Arc>>, + pub voice_udp_connection: Arc>>, + pub data: Arc>, +} + +impl VoiceHandler { + /// Creates a new voicehandler, only initializing the data + pub fn new() -> VoiceHandler { + Self { + data: Arc::new(RwLock::new(VoiceData::default())), + voice_gateway_connection: Arc::new(Mutex::new(None)), + voice_udp_connection: Arc::new(Mutex::new(None)), + } + } +} + +#[async_trait] +// On [VoiceServerUpdate] we get our starting data and url for the voice gateway server. +impl Observer for VoiceHandler { + async fn update(&self, data: &VoiceServerUpdate) { + let mut data_lock = self.data.write().await; + data_lock.server_data = Some(data.clone()); + let user_id = data_lock.user_id.clone(); + let session_id = data_lock.session_id.clone(); + drop(data_lock); + + let voice_gateway_handle = VoiceGateway::spawn(data.endpoint.clone().unwrap()) + .await + .unwrap(); + + let server_id: Snowflake; + + if data.guild_id.is_some() { + server_id = data.guild_id.clone().unwrap(); + } else { + server_id = data.channel_id.clone().unwrap(); + } + + let voice_identify = VoiceIdentify { + server_id, + user_id, + session_id, + token: data.token.clone(), + video: Some(false), + }; + + voice_gateway_handle.send_identify(voice_identify).await; + + let cloned_gateway_handle = voice_gateway_handle.clone(); + + let mut voice_events = cloned_gateway_handle.events.lock().await; + + let self_reference = Arc::new(self.clone()); + + voice_events.voice_ready.subscribe(self_reference.clone()); + voice_events.session_description.subscribe(self_reference.clone()); + + *self.voice_gateway_connection.lock().await = Some(voice_gateway_handle); + } +} + +#[async_trait] +// On [VoiceReady] we get info for establishing a UDP connection, and we immedietly need said UDP +// connection for ip discovery. +impl Observer for VoiceHandler { + async fn update(&self, data: &VoiceReady) { + + let mut data_lock = self.data.write().await; + data_lock.ready_data = Some(data.clone()); + drop(data_lock); + + let udp_handle = UdpHandler::spawn( + self.data.clone(), + std::net::SocketAddr::V4(SocketAddrV4::new(data.ip.clone(), data.port)), + data.ssrc, + ) + .await; + + let ip_discovery = self.data.read().await.ip_discovery.clone().unwrap(); + + *self.voice_udp_connection.lock().await = Some(udp_handle.clone()); + + self.voice_gateway_connection + .lock() + .await + .clone() + .unwrap() + .send_select_protocol(SelectProtocol { + protocol: VoiceProtocol::Udp, + data: SelectProtocolData { + address: ip_discovery.address, + port: ip_discovery.port, + mode: VoiceEncryptionMode::Xsalsa20Poly1305, + }, + ..Default::default() + }) + .await; + } +} + +#[async_trait] +// Session descryption gives us final info regarding codecs and our encryption key +impl Observer for VoiceHandler { + async fn update(&self, data: &SessionDescription) { + + let mut data_write = self.data.write().await; + + data_write.session_description = Some(data.clone()); + + drop(data_write); + } +} + +#[async_trait] +impl Observer for VoiceHandler { + async fn update(&self, data: &GatewayReady) { + let mut lock = self.data.write().await; + lock.user_id = data.user.id.clone(); + lock.session_id = data.session_id.clone(); + drop(lock); + } +} diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 8d84a0a..e672ea8 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -4,6 +4,7 @@ mod crypto; pub mod gateway; pub mod udp; pub mod voice_data; +pub mod handler; // Pub use this so users can interact with packet types if they want pub use discortp; From a6d68383cc9f385e2e3da54550d641dd13f1dcd4 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:10:16 +0100 Subject: [PATCH 49/72] chore: yes clippy, you are special --- src/voice/crypto.rs | 2 +- src/voice/handler.rs | 43 ++++++++++++++++++++++++++++++------------- src/voice/mod.rs | 2 +- src/voice/udp.rs | 26 +++++++++++--------------- 4 files changed, 43 insertions(+), 30 deletions(-) diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index ccf39b6..1365ca7 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -12,5 +12,5 @@ pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { rtp_header.push(0); } - return rtp_header; + rtp_header } diff --git a/src/voice/handler.rs b/src/voice/handler.rs index e757f9a..c8ddd8c 100644 --- a/src/voice/handler.rs +++ b/src/voice/handler.rs @@ -1,11 +1,22 @@ -use std::{sync::Arc, net::SocketAddrV4}; +use std::{net::SocketAddrV4, sync::Arc}; use async_trait::async_trait; use tokio::sync::{Mutex, RwLock}; -use crate::{gateway::Observer, types::{VoiceServerUpdate, VoiceIdentify, VoiceReady, SessionDescription, GatewayReady, Snowflake, SelectProtocol, VoiceProtocol, SelectProtocolData, VoiceEncryptionMode}}; +use crate::{ + gateway::Observer, + types::{ + GatewayReady, SelectProtocol, SelectProtocolData, SessionDescription, Snowflake, + VoiceEncryptionMode, VoiceIdentify, VoiceProtocol, VoiceReady, VoiceServerUpdate, + }, +}; -use super::{gateway::{VoiceGatewayHandle, VoiceGateway}, udp::UdpHandle, udp::UdpHandler, voice_data::VoiceData}; +use super::{ + gateway::{VoiceGateway, VoiceGatewayHandle}, + udp::UdpHandle, + udp::UdpHandler, + voice_data::VoiceData, +}; /// Handles inbetween connections between the gateway and udp modules #[derive(Debug, Clone)] @@ -26,13 +37,19 @@ impl VoiceHandler { } } +impl Default for VoiceHandler { + fn default() -> Self { + Self::new() + } +} + #[async_trait] // On [VoiceServerUpdate] we get our starting data and url for the voice gateway server. impl Observer for VoiceHandler { async fn update(&self, data: &VoiceServerUpdate) { let mut data_lock = self.data.write().await; data_lock.server_data = Some(data.clone()); - let user_id = data_lock.user_id.clone(); + let user_id = data_lock.user_id; let session_id = data_lock.session_id.clone(); drop(data_lock); @@ -43,9 +60,9 @@ impl Observer for VoiceHandler { let server_id: Snowflake; if data.guild_id.is_some() { - server_id = data.guild_id.clone().unwrap(); + server_id = data.guild_id.unwrap(); } else { - server_id = data.channel_id.clone().unwrap(); + server_id = data.channel_id.unwrap(); } let voice_identify = VoiceIdentify { @@ -65,7 +82,9 @@ impl Observer for VoiceHandler { let self_reference = Arc::new(self.clone()); voice_events.voice_ready.subscribe(self_reference.clone()); - voice_events.session_description.subscribe(self_reference.clone()); + voice_events + .session_description + .subscribe(self_reference.clone()); *self.voice_gateway_connection.lock().await = Some(voice_gateway_handle); } @@ -76,14 +95,13 @@ impl Observer for VoiceHandler { // connection for ip discovery. impl Observer for VoiceHandler { async fn update(&self, data: &VoiceReady) { - let mut data_lock = self.data.write().await; data_lock.ready_data = Some(data.clone()); - drop(data_lock); + drop(data_lock); let udp_handle = UdpHandler::spawn( self.data.clone(), - std::net::SocketAddr::V4(SocketAddrV4::new(data.ip.clone(), data.port)), + std::net::SocketAddr::V4(SocketAddrV4::new(data.ip, data.port)), data.ssrc, ) .await; @@ -114,7 +132,6 @@ impl Observer for VoiceHandler { // Session descryption gives us final info regarding codecs and our encryption key impl Observer for VoiceHandler { async fn update(&self, data: &SessionDescription) { - let mut data_write = self.data.write().await; data_write.session_description = Some(data.clone()); @@ -127,8 +144,8 @@ impl Observer for VoiceHandler { impl Observer for VoiceHandler { async fn update(&self, data: &GatewayReady) { let mut lock = self.data.write().await; - lock.user_id = data.user.id.clone(); + lock.user_id = data.user.id; lock.session_id = data.session_id.clone(); drop(lock); } -} +} diff --git a/src/voice/mod.rs b/src/voice/mod.rs index e672ea8..621d3c0 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -2,9 +2,9 @@ mod crypto; pub mod gateway; +pub mod handler; pub mod udp; pub mod voice_data; -pub mod handler; // Pub use this so users can interact with packet types if they want pub use discortp; diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 38d71c2..9b6f9cf 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -292,16 +292,14 @@ impl UdpHandler { let session_description = session_description_result.unwrap(); - let nonce_bytes; - - match session_description.encryption_mode { + let nonce_bytes = match session_description.encryption_mode { crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(rtp.packet()); + crypto::get_xsalsa20_poly1305_nonce(rtp.packet()) } _ => { unimplemented!(); } - } + }; let nonce = GenericArray::from_slice(&nonce_bytes); @@ -346,15 +344,13 @@ impl UdpHandler { } Demuxed::Rtcp(rtcp) => { trace!("VUDP: Parsed packet as rtcp!"); - - let rtcp_data; - - match rtcp { + + let rtcp_data = match rtcp { discortp::rtcp::RtcpPacket::KnownType(knowntype) => { - rtcp_data = discortp::rtcp::Rtcp::KnownType(knowntype); + discortp::rtcp::Rtcp::KnownType(knowntype) } discortp::rtcp::RtcpPacket::SenderReport(senderreport) => { - rtcp_data = discortp::rtcp::Rtcp::SenderReport(SenderReport { + discortp::rtcp::Rtcp::SenderReport(SenderReport { payload: senderreport.payload().to_vec(), padding: senderreport.get_padding(), version: senderreport.get_version(), @@ -362,10 +358,10 @@ impl UdpHandler { pkt_length: senderreport.get_pkt_length(), packet_type: senderreport.get_packet_type(), rx_report_count: senderreport.get_rx_report_count(), - }); + }) } discortp::rtcp::RtcpPacket::ReceiverReport(receiverreport) => { - rtcp_data = discortp::rtcp::Rtcp::ReceiverReport(ReceiverReport { + discortp::rtcp::Rtcp::ReceiverReport(ReceiverReport { payload: receiverreport.payload().to_vec(), padding: receiverreport.get_padding(), version: receiverreport.get_version(), @@ -373,12 +369,12 @@ impl UdpHandler { pkt_length: receiverreport.get_pkt_length(), packet_type: receiverreport.get_packet_type(), rx_report_count: receiverreport.get_rx_report_count(), - }); + }) } _ => { unreachable!(); } - } + }; self.events.lock().await.rtcp.notify(rtcp_data).await; } From 33400daa7473cd984bf740645a7108f9af51600d Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:21:08 +0100 Subject: [PATCH 50/72] fix: duplicated gateway events --- src/gateway/gateway.rs | 165 +---------------------------------------- src/gateway/handle.rs | 2 +- 2 files changed, 2 insertions(+), 165 deletions(-) diff --git a/src/gateway/gateway.rs b/src/gateway/gateway.rs index 684b9d2..e29abdb 100644 --- a/src/gateway/gateway.rs +++ b/src/gateway/gateway.rs @@ -5,7 +5,7 @@ use log::*; #[cfg(not(target_arch = "wasm32"))] use tokio::task; -use self::event::Events; +use super::events::Events; use super::*; use super::{Sink, Stream}; use crate::types::{ @@ -101,7 +101,6 @@ impl Gateway { let msg = self.websocket_receive.next().await; // PRETTYFYME: Remove inline conditional compiling - // This if chain can be much better but if let is unstable on stable rust #[cfg(not(target_arch = "wasm32"))] if let Some(Ok(message)) = msg { self.handle_message(message.into()).await; @@ -394,165 +393,3 @@ impl Gateway { } } } - -pub mod event { - use super::*; - - #[derive(Default, Debug)] - pub struct Events { - pub application: Application, - pub auto_moderation: AutoModeration, - pub session: Session, - pub message: Message, - pub user: User, - pub relationship: Relationship, - pub channel: Channel, - pub thread: Thread, - pub guild: Guild, - pub invite: Invite, - pub integration: Integration, - pub interaction: Interaction, - pub stage_instance: StageInstance, - pub call: Call, - pub voice: Voice, - pub webhooks: Webhooks, - pub gateway_identify_payload: GatewayEvent, - pub gateway_resume: GatewayEvent, - pub error: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Application { - pub command_permissions_update: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct AutoModeration { - pub rule_create: GatewayEvent, - pub rule_update: GatewayEvent, - pub rule_delete: GatewayEvent, - pub action_execution: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Session { - pub ready: GatewayEvent, - pub ready_supplemental: GatewayEvent, - pub replace: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct StageInstance { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Message { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - pub delete_bulk: GatewayEvent, - pub reaction_add: GatewayEvent, - pub reaction_remove: GatewayEvent, - pub reaction_remove_all: GatewayEvent, - pub reaction_remove_emoji: GatewayEvent, - pub ack: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct User { - pub update: GatewayEvent, - pub guild_settings_update: GatewayEvent, - pub presence_update: GatewayEvent, - pub typing_start: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Relationship { - pub add: GatewayEvent, - pub remove: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Channel { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub unread_update: GatewayEvent, - pub delete: GatewayEvent, - pub pins_update: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Thread { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - pub list_sync: GatewayEvent, - pub member_update: GatewayEvent, - pub members_update: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Guild { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - pub audit_log_entry_create: GatewayEvent, - pub ban_add: GatewayEvent, - pub ban_remove: GatewayEvent, - pub emojis_update: GatewayEvent, - pub stickers_update: GatewayEvent, - pub integrations_update: GatewayEvent, - pub member_add: GatewayEvent, - pub member_remove: GatewayEvent, - pub member_update: GatewayEvent, - pub members_chunk: GatewayEvent, - pub role_create: GatewayEvent, - pub role_update: GatewayEvent, - pub role_delete: GatewayEvent, - pub role_scheduled_event_create: GatewayEvent, - pub role_scheduled_event_update: GatewayEvent, - pub role_scheduled_event_delete: GatewayEvent, - pub role_scheduled_event_user_add: GatewayEvent, - pub role_scheduled_event_user_remove: GatewayEvent, - pub passive_update_v1: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Invite { - pub create: GatewayEvent, - pub delete: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Integration { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Interaction { - pub create: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Call { - pub create: GatewayEvent, - pub update: GatewayEvent, - pub delete: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Voice { - pub state_update: GatewayEvent, - pub server_update: GatewayEvent, - } - - #[derive(Default, Debug)] - pub struct Webhooks { - pub update: GatewayEvent, - } -} diff --git a/src/gateway/handle.rs b/src/gateway/handle.rs index 620faba..dede40e 100644 --- a/src/gateway/handle.rs +++ b/src/gateway/handle.rs @@ -3,7 +3,7 @@ use log::*; use std::fmt::Debug; -use super::{event::Events, *}; +use super::{events::Events, *}; use crate::types::{self, Composite}; /// Represents a handle to a Gateway connection. A Gateway connection will create observable From ef4d6cffdbb94ef2066f695c718fcb6a9665ff91 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:21:47 +0100 Subject: [PATCH 51/72] feat: first try at vgw wasm compat --- src/voice/gateway.rs | 698 ---------------------- src/voice/gateway/backends/mod.rs | 23 + src/voice/gateway/backends/tungstenite.rs | 65 ++ src/voice/gateway/backends/wasm.rs | 48 ++ src/voice/gateway/events.rs | 24 + src/voice/gateway/gateway.rs | 335 +++++++++++ src/voice/gateway/handle.rs | 101 ++++ src/voice/gateway/heartbeat.rs | 160 +++++ src/voice/gateway/message.rs | 39 ++ src/voice/gateway/mod.rs | 11 + src/voice/udp.rs | 2 +- 11 files changed, 807 insertions(+), 699 deletions(-) delete mode 100644 src/voice/gateway.rs create mode 100644 src/voice/gateway/backends/mod.rs create mode 100644 src/voice/gateway/backends/tungstenite.rs create mode 100644 src/voice/gateway/backends/wasm.rs create mode 100644 src/voice/gateway/events.rs create mode 100644 src/voice/gateway/gateway.rs create mode 100644 src/voice/gateway/handle.rs create mode 100644 src/voice/gateway/heartbeat.rs create mode 100644 src/voice/gateway/message.rs create mode 100644 src/voice/gateway/mod.rs diff --git a/src/voice/gateway.rs b/src/voice/gateway.rs deleted file mode 100644 index 3616cd3..0000000 --- a/src/voice/gateway.rs +++ /dev/null @@ -1,698 +0,0 @@ -use futures_util::stream::{SplitSink, SplitStream}; -use futures_util::SinkExt; -use futures_util::StreamExt; -use log::{debug, info, trace, warn}; -use serde_json::json; -use std::sync::Arc; -use std::time::Duration; -use tokio::net::TcpStream; -use tokio::sync::mpsc::Sender; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio::time::Instant; -use tokio::time::{self, sleep_until}; -use tokio_tungstenite::MaybeTlsStream; -use tokio_tungstenite::{connect_async_tls_with_config, Connector, WebSocketStream}; - -use crate::errors::VoiceGatewayError; -use crate::gateway::{heartbeat::HEARTBEAT_ACK_TIMEOUT, GatewayEvent}; -use crate::types::{ - self, SelectProtocol, Speaking, SsrcDefinition, VoiceGatewayReceivePayload, - VoiceGatewaySendPayload, VoiceIdentify, WebSocketEvent, VOICE_BACKEND_VERSION, - VOICE_CLIENT_CONNECT_FLAGS, VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, - VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, VOICE_HELLO, VOICE_IDENTIFY, VOICE_MEDIA_SINK_WANTS, - VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, VOICE_SESSION_DESCRIPTION, - VOICE_SESSION_UPDATE, VOICE_SPEAKING, VOICE_SSRC_DEFINITION, -}; - -use self::voice_events::VoiceEvents; - -/// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. -/// This struct is used internally when handling messages. -#[derive(Clone, Debug)] -pub struct VoiceGatewayMesssage { - /// The message we received from the server - message: tokio_tungstenite::tungstenite::Message, -} - -impl VoiceGatewayMesssage { - /// Creates self from a tungstenite message - pub fn from_tungstenite_message(message: tokio_tungstenite::tungstenite::Message) -> Self { - Self { message } - } - - /// Parses the message as an error; - /// Returns the error if succesfully parsed, None if the message isn't an error - pub fn error(&self) -> Option { - let content = self.message.to_string(); - - // Some error strings have dots on the end, which we don't care about - let processed_content = content.to_lowercase().replace('.', ""); - - match processed_content.as_str() { - "unknown opcode" | "4001" => Some(VoiceGatewayError::UnknownOpcode), - "decode error" | "failed to decode payload" | "4002" => { - Some(VoiceGatewayError::FailedToDecodePayload) - } - "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticated), - "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailed), - "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticated), - "session is no longer valid" | "4006" => Some(VoiceGatewayError::SessionNoLongerValid), - "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeout), - "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFound), - "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocol), - "disconnected" | "4014" => Some(VoiceGatewayError::Disconnected), - "voice server crashed" | "4015" => Some(VoiceGatewayError::VoiceServerCrashed), - "unknown encryption mode" | "4016" => Some(VoiceGatewayError::UnknownEncryptionMode), - _ => None, - } - } - - /// Returns whether or not the message is an error - pub fn is_error(&self) -> bool { - self.error().is_some() - } - - /// Parses the message as a payload; - /// Returns a result of deserializing - pub fn payload(&self) -> Result { - return serde_json::from_str(self.message.to_text().unwrap()); - } - - /// Returns whether or not the message is a payload - pub fn is_payload(&self) -> bool { - // close messages are never payloads, payloads are only text messages - if self.message.is_close() | !self.message.is_text() { - return false; - } - - return self.payload().is_ok(); - } - - /// Returns whether or not the message is empty - pub fn is_empty(&self) -> bool { - self.message.is_empty() - } -} - -/// Represents a handle to a Voice Gateway connection. -/// Using this handle you can send Gateway Events directly. -#[derive(Debug, Clone)] -pub struct VoiceGatewayHandle { - pub url: String, - pub events: Arc>, - pub websocket_send: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - /// Tells gateway tasks to close - kill_send: tokio::sync::broadcast::Sender<()>, -} - -impl VoiceGatewayHandle { - /// Sends json to the gateway with an opcode - async fn send_json(&self, op_code: u8, to_send: serde_json::Value) { - let gateway_payload = VoiceGatewaySendPayload { - op_code, - data: to_send, - }; - - let payload_json = serde_json::to_string(&gateway_payload).unwrap(); - - let message = tokio_tungstenite::tungstenite::Message::text(payload_json); - - self.websocket_send - .lock() - .await - .send(message) - .await - .unwrap(); - } - - /// Sends a voice identify event to the gateway - pub async fn send_identify(&self, to_send: VoiceIdentify) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("VGW: Sending Identify.."); - - self.send_json(VOICE_IDENTIFY, to_send_value).await; - } - - /// Sends a select protocol event to the gateway - pub async fn send_select_protocol(&self, to_send: SelectProtocol) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("VGW: Sending Select Protocol"); - - self.send_json(VOICE_SELECT_PROTOCOL, to_send_value).await; - } - - /// Sends a speaking event to the gateway - pub async fn send_speaking(&self, to_send: Speaking) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("VGW: Sending Speaking"); - - self.send_json(VOICE_SPEAKING, to_send_value).await; - } - - /// Sends an ssrc definition event - pub async fn send_ssrc_definition(&self, to_send: SsrcDefinition) { - let to_send_value = serde_json::to_value(&to_send).unwrap(); - - trace!("VGW: Sending SsrcDefinition"); - - self.send_json(VOICE_SSRC_DEFINITION, to_send_value).await; - } - - /// Sends a voice backend version request to the gateway - pub async fn send_voice_backend_version_request(&self) { - let data_empty_object = json!("{}"); - - trace!("VGW: Requesting voice backend version"); - - self.send_json(VOICE_BACKEND_VERSION, data_empty_object) - .await; - } - - /// Closes the websocket connection and stops all gateway tasks; - /// - /// Esentially pulls the plug on the voice gateway, leaving it possible to resume; - pub async fn close(&self) { - self.kill_send.send(()).unwrap(); - self.websocket_send.lock().await.close().await.unwrap(); - } -} - -#[derive(Debug)] -pub struct VoiceGateway { - events: Arc>, - heartbeat_handler: VoiceHeartbeatHandler, - websocket_send: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - websocket_receive: SplitStream>>, - kill_send: tokio::sync::broadcast::Sender<()>, -} - -impl VoiceGateway { - #[allow(clippy::new_ret_no_self)] - pub async fn spawn(websocket_url: String) -> Result { - // 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()); - - let mut roots = rustls::RootCertStore::empty(); - for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") - { - roots.add(&rustls::Certificate(cert.0)).unwrap(); - } - let (websocket_stream, _) = match connect_async_tls_with_config( - &processed_url, - None, - false, - Some(Connector::Rustls( - rustls::ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(roots) - .with_no_client_auth() - .into(), - )), - ) - .await - { - Ok(websocket_stream) => websocket_stream, - Err(e) => { - return Err(VoiceGatewayError::CannotConnect { - error: e.to_string(), - }) - } - }; - - let (websocket_send, mut websocket_receive) = websocket_stream.split(); - - let shared_websocket_send = Arc::new(Mutex::new(websocket_send)); - - // Create a shared broadcast channel for killing all gateway tasks - let (kill_send, mut _kill_receive) = tokio::sync::broadcast::channel::<()>(16); - - // 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 - let msg = websocket_receive.next().await.unwrap().unwrap(); - let gateway_payload: VoiceGatewayReceivePayload = - serde_json::from_str(msg.to_text().unwrap()).unwrap(); - - if gateway_payload.op_code != VOICE_HELLO { - return Err(VoiceGatewayError::NonHelloOnInitiate { - opcode: gateway_payload.op_code, - }); - } - - info!("VGW: Received Hello"); - - // The hello data for voice gateways is in float milliseconds, so we convert it to f64 seconds - let gateway_hello: types::VoiceHelloData = - serde_json::from_str(gateway_payload.data.get()).unwrap(); - let heartbeat_interval_seconds: f64 = gateway_hello.heartbeat_interval / 1000.0; - - let voice_events = VoiceEvents::default(); - let shared_events = Arc::new(Mutex::new(voice_events)); - - let mut gateway = VoiceGateway { - events: shared_events.clone(), - heartbeat_handler: VoiceHeartbeatHandler::new( - Duration::from_secs_f64(heartbeat_interval_seconds), - 1, // to:do actually compute nonce - shared_websocket_send.clone(), - kill_send.subscribe(), - ), - websocket_send: shared_websocket_send.clone(), - websocket_receive, - kill_send: kill_send.clone(), - }; - - // Now we can continuously check for messages in a different task, since we aren't going to receive another hello - tokio::spawn(async move { - gateway.gateway_listen_task().await; - }); - - Ok(VoiceGatewayHandle { - url: websocket_url.clone(), - events: shared_events, - websocket_send: shared_websocket_send.clone(), - kill_send: kill_send.clone(), - }) - } - - /// The main gateway listener task; - /// - /// Can only be stopped by closing the websocket, cannot be made to listen for kill - pub async fn gateway_listen_task(&mut self) { - loop { - let msg = self.websocket_receive.next().await; - - if let Some(Ok(message)) = msg { - self.handle_message(VoiceGatewayMesssage::from_tungstenite_message(message)) - .await; - continue; - } - - // We couldn't receive the next message or it was an error, something is wrong with the websocket, close - warn!("VGW: Websocket is broken, stopping gateway"); - break; - } - } - - /// Closes the websocket connection and stops all tasks - async fn close(&mut self) { - self.kill_send.send(()).unwrap(); - self.websocket_send.lock().await.close().await.unwrap(); - } - - /// Deserializes and updates a dispatched event, when we already know its type; - /// (Called for every event in handle_message) - async fn handle_event<'a, T: WebSocketEvent + serde::Deserialize<'a>>( - data: &'a str, - event: &mut GatewayEvent, - ) -> Result<(), serde_json::Error> { - let data_deserialize_result: Result = serde_json::from_str(data); - - if data_deserialize_result.is_err() { - return Err(data_deserialize_result.err().unwrap()); - } - - event.notify(data_deserialize_result.unwrap()).await; - Ok(()) - } - - /// This handles a message as a websocket event and updates its events along with the events' observers - pub async fn handle_message(&mut self, msg: VoiceGatewayMesssage) { - if msg.is_empty() { - return; - } - - if !msg.is_error() && !msg.is_payload() { - warn!( - "Message unrecognised: {:?}, please open an issue on the chorus github", - msg.message.to_string() - ); - return; - } - - if msg.is_error() { - let error = msg.error().unwrap(); - - warn!("VGW: Received error, connection will close.."); - - self.close().await; - - self.events.lock().await.error.notify(error).await; - - return; - } - - let gateway_payload = msg.payload().unwrap(); - - // See - match gateway_payload.op_code { - VOICE_READY => { - trace!("VGW: Received READY!"); - - let event = &mut self.events.lock().await.voice_ready; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!("Failed to parse VOICE_READY ({})", result.err().unwrap()); - } - } - VOICE_BACKEND_VERSION => { - trace!("VGW: Received Backend Version"); - - let event = &mut self.events.lock().await.backend_version; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_BACKEND_VERSION ({})", - result.err().unwrap() - ); - } - } - VOICE_SESSION_DESCRIPTION => { - trace!("VGW: Received Session Description"); - - let event = &mut self.events.lock().await.session_description; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_SESSION_DESCRIPTION ({})", - result.err().unwrap() - ); - } - } - VOICE_SESSION_UPDATE => { - trace!("VGW: Received Session Update"); - - let event = &mut self.events.lock().await.session_update; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_SESSION_UPDATE ({})", - result.err().unwrap() - ); - } - } - VOICE_SPEAKING => { - trace!("VGW: Received Speaking"); - - let event = &mut self.events.lock().await.speaking; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!("Failed to parse VOICE_SPEAKING ({})", result.err().unwrap()); - } - } - VOICE_SSRC_DEFINITION => { - trace!("VGW: Received Ssrc Definition"); - - let event = &mut self.events.lock().await.ssrc_definition; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_SSRC_DEFINITION ({})", - result.err().unwrap() - ); - } - } - VOICE_CLIENT_DISCONNECT => { - trace!("VGW: Received Client Disconnect"); - - let event = &mut self.events.lock().await.client_disconnect; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_CLIENT_DISCONNECT ({})", - result.err().unwrap() - ); - } - } - VOICE_CLIENT_CONNECT_FLAGS => { - trace!("VGW: Received Client Connect Flags"); - - let event = &mut self.events.lock().await.client_connect_flags; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_CLIENT_CONNECT_FLAGS ({})", - result.err().unwrap() - ); - } - } - VOICE_CLIENT_CONNECT_PLATFORM => { - trace!("VGW: Received Client Connect Platform"); - - let event = &mut self.events.lock().await.client_connect_platform; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_CLIENT_CONNECT_PLATFORM ({})", - result.err().unwrap() - ); - } - } - VOICE_MEDIA_SINK_WANTS => { - trace!("VGW: Received Media Sink Wants"); - - let event = &mut self.events.lock().await.media_sink_wants; - let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; - if result.is_err() { - warn!( - "Failed to parse VOICE_MEDIA_SINK_WANTS ({})", - result.err().unwrap() - ); - } - } - // We received a heartbeat from the server - // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." - VOICE_HEARTBEAT => { - trace!("VGW: Received Heartbeat // Heartbeat Request"); - - // Tell the heartbeat handler it should send a heartbeat right away - let heartbeat_communication = VoiceHeartbeatThreadCommunication { - updated_nonce: None, - op_code: Some(VOICE_HEARTBEAT), - }; - - self.heartbeat_handler - .send - .send(heartbeat_communication) - .await - .unwrap(); - } - VOICE_HEARTBEAT_ACK => { - trace!("VGW: Received Heartbeat ACK"); - - // Tell the heartbeat handler we received an ack - - let heartbeat_communication = VoiceHeartbeatThreadCommunication { - updated_nonce: None, - op_code: Some(VOICE_HEARTBEAT_ACK), - }; - - self.heartbeat_handler - .send - .send(heartbeat_communication) - .await - .unwrap(); - } - VOICE_IDENTIFY | VOICE_SELECT_PROTOCOL | VOICE_RESUME => { - info!( - "VGW: Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation and is likely not the fault of chorus.", - gateway_payload.op_code - ); - } - _ => { - warn!("VGW: Received unrecognized voice gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); - } - } - } -} - -/// Handles sending heartbeats to the voice gateway in another thread -#[allow(dead_code)] // FIXME: Remove this, once all fields of VoiceHeartbeatHandler are used -#[derive(Debug)] -struct VoiceHeartbeatHandler { - /// The heartbeat interval in milliseconds - pub heartbeat_interval: Duration, - /// The send channel for the heartbeat thread - pub send: Sender, - /// The handle of the thread - handle: JoinHandle<()>, -} - -impl VoiceHeartbeatHandler { - pub fn new( - heartbeat_interval: Duration, - starting_nonce: u64, - websocket_tx: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - kill_rc: tokio::sync::broadcast::Receiver<()>, - ) -> Self { - let (send, receive) = tokio::sync::mpsc::channel(32); - let kill_receive = kill_rc.resubscribe(); - - let handle: JoinHandle<()> = tokio::spawn(async move { - Self::heartbeat_task( - websocket_tx, - heartbeat_interval, - starting_nonce, - receive, - kill_receive, - ) - .await; - }); - - Self { - heartbeat_interval, - send, - handle, - } - } - - /// The main heartbeat task; - /// - /// Can be killed by the kill broadcast; - /// If the websocket is closed, will die out next time it tries to send a heartbeat; - pub async fn heartbeat_task( - websocket_tx: Arc< - Mutex< - SplitSink< - WebSocketStream>, - tokio_tungstenite::tungstenite::Message, - >, - >, - >, - heartbeat_interval: Duration, - starting_nonce: u64, - mut receive: tokio::sync::mpsc::Receiver, - mut kill_receive: tokio::sync::broadcast::Receiver<()>, - ) { - let mut last_heartbeat_timestamp: Instant = time::Instant::now(); - let mut last_heartbeat_acknowledged = true; - let mut nonce: u64 = starting_nonce; - - loop { - if kill_receive.try_recv().is_ok() { - trace!("VGW: Closing heartbeat task"); - break; - } - - let timeout = if last_heartbeat_acknowledged { - heartbeat_interval - } else { - // If the server hasn't acknowledged our heartbeat we should resend it - Duration::from_millis(HEARTBEAT_ACK_TIMEOUT) - }; - - let mut should_send = false; - - tokio::select! { - () = sleep_until(last_heartbeat_timestamp + timeout) => { - should_send = true; - } - Some(communication) = receive.recv() => { - // If we received a nonce update, use that nonce now - if communication.updated_nonce.is_some() { - nonce = communication.updated_nonce.unwrap(); - } - - if let Some(op_code) = communication.op_code { - match op_code { - VOICE_HEARTBEAT => { - // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately - should_send = true; - } - VOICE_HEARTBEAT_ACK => { - // The server received our heartbeat - last_heartbeat_acknowledged = true; - } - _ => {} - } - } - } - } - - if should_send { - trace!("VGW: Sending Heartbeat.."); - - let heartbeat = VoiceGatewaySendPayload { - op_code: VOICE_HEARTBEAT, - data: nonce.into(), - }; - - let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); - - let msg = tokio_tungstenite::tungstenite::Message::text(heartbeat_json); - - let send_result = websocket_tx.lock().await.send(msg).await; - if send_result.is_err() { - // We couldn't send, the websocket is broken - warn!("VGW: Couldnt send heartbeat, websocket seems broken"); - break; - } - - last_heartbeat_timestamp = time::Instant::now(); - last_heartbeat_acknowledged = false; - } - } - } -} - -/// Used for communications between the voice heartbeat and voice gateway thread. -/// Either signifies a nonce update, a heartbeat ACK or a Heartbeat request by the server -#[derive(Clone, Copy, Debug)] -struct VoiceHeartbeatThreadCommunication { - /// The opcode for the communication we received, if relevant - op_code: Option, - /// The new nonce to use, if any - updated_nonce: Option, -} - -pub mod voice_events { - use crate::{ - errors::VoiceGatewayError, - gateway::GatewayEvent, - types::{ - SessionDescription, SessionUpdate, Speaking, SsrcDefinition, VoiceBackendVersion, - VoiceClientConnectFlags, VoiceClientConnectPlatform, VoiceClientDisconnection, - VoiceMediaSinkWants, VoiceReady, - }, - }; - - #[derive(Default, Debug)] - pub struct VoiceEvents { - pub voice_ready: GatewayEvent, - pub backend_version: GatewayEvent, - pub session_description: GatewayEvent, - pub session_update: GatewayEvent, - pub speaking: GatewayEvent, - pub ssrc_definition: GatewayEvent, - pub client_disconnect: GatewayEvent, - pub client_connect_flags: GatewayEvent, - pub client_connect_platform: GatewayEvent, - pub media_sink_wants: GatewayEvent, - pub error: GatewayEvent, - } -} diff --git a/src/voice/gateway/backends/mod.rs b/src/voice/gateway/backends/mod.rs new file mode 100644 index 0000000..edb5dc9 --- /dev/null +++ b/src/voice/gateway/backends/mod.rs @@ -0,0 +1,23 @@ +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub mod tungstenite; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub use tungstenite::*; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub mod wasm; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub use wasm::*; + +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type Sink = tungstenite::TungsteniteSink; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type Stream = tungstenite::TungsteniteStream; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type WebSocketBackend = tungstenite::TungsteniteBackend; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type Sink = wasm::WasmSink; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type Stream = wasm::WasmStream; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type WebSocketBackend = wasm::WasmBackend; diff --git a/src/voice/gateway/backends/tungstenite.rs b/src/voice/gateway/backends/tungstenite.rs new file mode 100644 index 0000000..090fdb9 --- /dev/null +++ b/src/voice/gateway/backends/tungstenite.rs @@ -0,0 +1,65 @@ +use futures_util::{ + stream::{SplitSink, SplitStream}, + StreamExt, +}; +use tokio::net::TcpStream; +use tokio_tungstenite::{ + connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream, +}; + +use crate::{errors::VoiceGatewayError, voice::gateway::VoiceGatewayMesssage}; + +#[derive(Debug, Clone)] +pub struct TungsteniteBackend; + +// These could be made into inherent associated types when that's stabilized +pub type TungsteniteSink = + SplitSink>, tungstenite::Message>; +pub type TungsteniteStream = SplitStream>>; + +impl TungsteniteBackend { + pub async fn connect( + websocket_url: &str, + ) -> Result<(TungsteniteSink, TungsteniteStream), crate::errors::VoiceGatewayError> { + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") + { + roots.add(&rustls::Certificate(cert.0)).unwrap(); + } + let (websocket_stream, _) = match connect_async_tls_with_config( + websocket_url, + None, + false, + Some(Connector::Rustls( + rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth() + .into(), + )), + ) + .await + { + Ok(websocket_stream) => websocket_stream, + Err(e) => { + return Err(VoiceGatewayError::CannotConnect { + error: e.to_string(), + }) + } + }; + + Ok(websocket_stream.split()) + } +} + +impl From for tungstenite::Message { + fn from(message: VoiceGatewayMesssage) -> Self { + Self::Text(message.0) + } +} + +impl From for VoiceGatewayMesssage { + fn from(value: tungstenite::Message) -> Self { + Self(value.to_string()) + } +} diff --git a/src/voice/gateway/backends/wasm.rs b/src/voice/gateway/backends/wasm.rs new file mode 100644 index 0000000..588c882 --- /dev/null +++ b/src/voice/gateway/backends/wasm.rs @@ -0,0 +1,48 @@ +use futures_util::{ + stream::{SplitSink, SplitStream}, + StreamExt, +}; + +use ws_stream_wasm::*; + +use crate::errors::VoiceGatewayError; +use crate::voice::gateway::VoiceGatewayMessage; + +#[derive(Debug, Clone)] +pub struct WasmBackend; + +// These could be made into inherent associated types when that's stabilized +pub type WasmSink = SplitSink; +pub type WasmStream = SplitStream; + +impl WasmBackend { + pub async fn connect(websocket_url: &str) -> Result<(WasmSink, WasmStream), VoiceGatewayError> { + let (_, websocket_stream) = match WsMeta::connect(websocket_url, None).await { + Ok(stream) => Ok(stream), + Err(e) => Err(VoiceGatewayError::CannotConnect { + error: e.to_string(), + }), + }?; + + Ok(websocket_stream.split()) + } +} + +impl From for WsMessage { + fn from(message: VoiceGatewayMessage) -> Self { + Self::Text(message.0) + } +} + +impl From for VoiceGatewayMessage { + fn from(value: WsMessage) -> Self { + match value { + WsMessage::Text(text) => Self(text), + WsMessage::Binary(bin) => { + let mut text = String::new(); + let _ = bin.iter().map(|v| text.push_str(&v.to_string())); + Self(text) + } + } + } +} diff --git a/src/voice/gateway/events.rs b/src/voice/gateway/events.rs new file mode 100644 index 0000000..a0f018c --- /dev/null +++ b/src/voice/gateway/events.rs @@ -0,0 +1,24 @@ +use crate::{ + errors::VoiceGatewayError, + gateway::GatewayEvent, + types::{ + SessionDescription, SessionUpdate, Speaking, SsrcDefinition, VoiceBackendVersion, + VoiceClientConnectFlags, VoiceClientConnectPlatform, VoiceClientDisconnection, + VoiceMediaSinkWants, VoiceReady, + }, +}; + +#[derive(Default, Debug)] +pub struct VoiceEvents { + pub voice_ready: GatewayEvent, + pub backend_version: GatewayEvent, + pub session_description: GatewayEvent, + pub session_update: GatewayEvent, + pub speaking: GatewayEvent, + pub ssrc_definition: GatewayEvent, + pub client_disconnect: GatewayEvent, + pub client_connect_flags: GatewayEvent, + pub client_connect_platform: GatewayEvent, + pub media_sink_wants: GatewayEvent, + pub error: GatewayEvent, +} diff --git a/src/voice/gateway/gateway.rs b/src/voice/gateway/gateway.rs new file mode 100644 index 0000000..e6960e9 --- /dev/null +++ b/src/voice/gateway/gateway.rs @@ -0,0 +1,335 @@ +use std::{sync::Arc, time::Duration}; + +use log::*; + +use tokio::sync::Mutex; + +use futures_util::SinkExt; +use futures_util::StreamExt; + +use crate::{ + errors::VoiceGatewayError, + gateway::GatewayEvent, + types::{ + VoiceGatewayReceivePayload, VoiceHelloData, WebSocketEvent, VOICE_BACKEND_VERSION, + VOICE_CLIENT_CONNECT_FLAGS, VOICE_CLIENT_CONNECT_PLATFORM, VOICE_CLIENT_DISCONNECT, + VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK, VOICE_HELLO, VOICE_IDENTIFY, VOICE_MEDIA_SINK_WANTS, + VOICE_READY, VOICE_RESUME, VOICE_SELECT_PROTOCOL, VOICE_SESSION_DESCRIPTION, + VOICE_SESSION_UPDATE, VOICE_SPEAKING, VOICE_SSRC_DEFINITION, + }, + voice::gateway::{ + heartbeat::VoiceHeartbeatThreadCommunication, VoiceGatewayMesssage, WebSocketBackend, + }, +}; + +use super::{ + events::VoiceEvents, heartbeat::VoiceHeartbeatHandler, Sink, Stream, VoiceGatewayHandle, +}; + +#[derive(Debug)] +pub struct VoiceGateway { + events: Arc>, + heartbeat_handler: VoiceHeartbeatHandler, + websocket_send: Arc>, + websocket_receive: Stream, + kill_send: tokio::sync::broadcast::Sender<()>, +} + +impl VoiceGateway { + #[allow(clippy::new_ret_no_self)] + pub async fn spawn(websocket_url: String) -> Result { + // 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()); + + let (websocket_send, mut websocket_receive) = + WebSocketBackend::connect(&websocket_url).await?; + + let shared_websocket_send = Arc::new(Mutex::new(websocket_send)); + + // Create a shared broadcast channel for killing all gateway tasks + let (kill_send, mut _kill_receive) = tokio::sync::broadcast::channel::<()>(16); + + // 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: VoiceGatewayMesssage = websocket_receive.next().await.unwrap().unwrap().into(); + #[cfg(target_arch = "wasm32")] + let msg: VoiceGatewayMessage = websocket_receive.next().await.unwrap().into(); + let gateway_payload: VoiceGatewayReceivePayload = serde_json::from_str(&msg.0).unwrap(); + + if gateway_payload.op_code != VOICE_HELLO { + return Err(VoiceGatewayError::NonHelloOnInitiate { + opcode: gateway_payload.op_code, + }); + } + + info!("VGW: Received Hello"); + + // The hello data for voice gateways is in float milliseconds, so we convert it to f64 seconds + let gateway_hello: VoiceHelloData = + serde_json::from_str(gateway_payload.data.get()).unwrap(); + let heartbeat_interval_seconds: f64 = gateway_hello.heartbeat_interval / 1000.0; + + let voice_events = VoiceEvents::default(); + let shared_events = Arc::new(Mutex::new(voice_events)); + + let mut gateway = VoiceGateway { + events: shared_events.clone(), + heartbeat_handler: VoiceHeartbeatHandler::new( + Duration::from_secs_f64(heartbeat_interval_seconds), + 1, // to:do actually compute nonce + shared_websocket_send.clone(), + kill_send.subscribe(), + ), + websocket_send: shared_websocket_send.clone(), + websocket_receive, + kill_send: kill_send.clone(), + }; + + // Now we can continuously check for messages in a different task, since we aren't going to receive another hello + #[cfg(not(target_arch = "wasm32"))] + tokio::task::spawn(async move { + gateway.gateway_listen_task().await; + }); + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move { + gateway.gateway_listen_task().await; + }); + + Ok(VoiceGatewayHandle { + url: websocket_url.clone(), + events: shared_events, + websocket_send: shared_websocket_send.clone(), + kill_send: kill_send.clone(), + }) + } + + /// The main gateway listener task; + /// + /// Can only be stopped by closing the websocket, cannot be made to listen for kill + pub async fn gateway_listen_task(&mut self) { + loop { + let msg = self.websocket_receive.next().await; + + // PRETTYFYME: Remove inline conditional compiling + #[cfg(not(target_arch = "wasm32"))] + if let Some(Ok(message)) = msg { + self.handle_message(message.into()).await; + continue; + } + #[cfg(target_arch = "wasm32")] + if let Some(message) = msg { + self.handle_message(message.into()).await; + continue; + } + + // We couldn't receive the next message or it was an error, something is wrong with the websocket, close + warn!("VGW: Websocket is broken, stopping gateway"); + break; + } + } + + /// Closes the websocket connection and stops all tasks + async fn close(&mut self) { + self.kill_send.send(()).unwrap(); + self.websocket_send.lock().await.close().await.unwrap(); + } + + /// Deserializes and updates a dispatched event, when we already know its type; + /// (Called for every event in handle_message) + async fn handle_event<'a, T: WebSocketEvent + serde::Deserialize<'a>>( + data: &'a str, + event: &mut GatewayEvent, + ) -> Result<(), serde_json::Error> { + let data_deserialize_result: Result = serde_json::from_str(data); + + if data_deserialize_result.is_err() { + return Err(data_deserialize_result.err().unwrap()); + } + + event.notify(data_deserialize_result.unwrap()).await; + Ok(()) + } + + /// This handles a message as a websocket event and updates its events along with the events' observers + pub async fn handle_message(&mut self, msg: VoiceGatewayMesssage) { + if msg.0.is_empty() { + return; + } + + let Ok(gateway_payload) = msg.payload() else { + if let Some(error) = msg.error() { + warn!("GW: Received error {:?}, connection will close..", error); + self.close().await; + self.events.lock().await.error.notify(error).await; + } else { + warn!( + "Message unrecognised: {:?}, please open an issue on the chorus github", + msg.0 + ); + } + return; + }; + + // See + match gateway_payload.op_code { + VOICE_READY => { + trace!("VGW: Received READY!"); + + let event = &mut self.events.lock().await.voice_ready; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!("Failed to parse VOICE_READY ({})", result.err().unwrap()); + } + } + VOICE_BACKEND_VERSION => { + trace!("VGW: Received Backend Version"); + + let event = &mut self.events.lock().await.backend_version; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_BACKEND_VERSION ({})", + result.err().unwrap() + ); + } + } + VOICE_SESSION_DESCRIPTION => { + trace!("VGW: Received Session Description"); + + let event = &mut self.events.lock().await.session_description; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_SESSION_DESCRIPTION ({})", + result.err().unwrap() + ); + } + } + VOICE_SESSION_UPDATE => { + trace!("VGW: Received Session Update"); + + let event = &mut self.events.lock().await.session_update; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_SESSION_UPDATE ({})", + result.err().unwrap() + ); + } + } + VOICE_SPEAKING => { + trace!("VGW: Received Speaking"); + + let event = &mut self.events.lock().await.speaking; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!("Failed to parse VOICE_SPEAKING ({})", result.err().unwrap()); + } + } + VOICE_SSRC_DEFINITION => { + trace!("VGW: Received Ssrc Definition"); + + let event = &mut self.events.lock().await.ssrc_definition; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_SSRC_DEFINITION ({})", + result.err().unwrap() + ); + } + } + VOICE_CLIENT_DISCONNECT => { + trace!("VGW: Received Client Disconnect"); + + let event = &mut self.events.lock().await.client_disconnect; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_DISCONNECT ({})", + result.err().unwrap() + ); + } + } + VOICE_CLIENT_CONNECT_FLAGS => { + trace!("VGW: Received Client Connect Flags"); + + let event = &mut self.events.lock().await.client_connect_flags; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_CONNECT_FLAGS ({})", + result.err().unwrap() + ); + } + } + VOICE_CLIENT_CONNECT_PLATFORM => { + trace!("VGW: Received Client Connect Platform"); + + let event = &mut self.events.lock().await.client_connect_platform; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_CLIENT_CONNECT_PLATFORM ({})", + result.err().unwrap() + ); + } + } + VOICE_MEDIA_SINK_WANTS => { + trace!("VGW: Received Media Sink Wants"); + + let event = &mut self.events.lock().await.media_sink_wants; + let result = VoiceGateway::handle_event(gateway_payload.data.get(), event).await; + if result.is_err() { + warn!( + "Failed to parse VOICE_MEDIA_SINK_WANTS ({})", + result.err().unwrap() + ); + } + } + // We received a heartbeat from the server + // "Discord may send the app a Heartbeat (opcode 1) event, in which case the app should send a Heartbeat event immediately." + VOICE_HEARTBEAT => { + trace!("VGW: Received Heartbeat // Heartbeat Request"); + + // Tell the heartbeat handler it should send a heartbeat right away + let heartbeat_communication = VoiceHeartbeatThreadCommunication { + updated_nonce: None, + op_code: Some(VOICE_HEARTBEAT), + }; + + self.heartbeat_handler + .send + .send(heartbeat_communication) + .await + .unwrap(); + } + VOICE_HEARTBEAT_ACK => { + trace!("VGW: Received Heartbeat ACK"); + + // Tell the heartbeat handler we received an ack + + let heartbeat_communication = VoiceHeartbeatThreadCommunication { + updated_nonce: None, + op_code: Some(VOICE_HEARTBEAT_ACK), + }; + + self.heartbeat_handler + .send + .send(heartbeat_communication) + .await + .unwrap(); + } + VOICE_IDENTIFY | VOICE_SELECT_PROTOCOL | VOICE_RESUME => { + info!( + "VGW: Received unexpected opcode ({}) for current state. This might be due to a faulty server implementation and is likely not the fault of chorus.", + gateway_payload.op_code + ); + } + _ => { + warn!("VGW: Received unrecognized voice gateway op code ({})! Please open an issue on the chorus github so we can implement it", gateway_payload.op_code); + } + } + } +} diff --git a/src/voice/gateway/handle.rs b/src/voice/gateway/handle.rs new file mode 100644 index 0000000..d24adbf --- /dev/null +++ b/src/voice/gateway/handle.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; + +use log::*; + +use futures_util::SinkExt; + +use serde_json::json; +use tokio::sync::Mutex; + +use crate::types::{ + SelectProtocol, Speaking, SsrcDefinition, VoiceGatewaySendPayload, VoiceIdentify, + VOICE_BACKEND_VERSION, VOICE_IDENTIFY, VOICE_SELECT_PROTOCOL, VOICE_SPEAKING, + VOICE_SSRC_DEFINITION, +}; + +use super::{events::VoiceEvents, Sink, VoiceGatewayMesssage}; + +/// Represents a handle to a Voice Gateway connection. +/// Using this handle you can send Gateway Events directly. +#[derive(Debug, Clone)] +pub struct VoiceGatewayHandle { + pub url: String, + pub events: Arc>, + pub websocket_send: Arc>, + /// Tells gateway tasks to close + pub(super) kill_send: tokio::sync::broadcast::Sender<()>, +} + +impl VoiceGatewayHandle { + /// Sends json to the gateway with an opcode + async fn send_json(&self, op_code: u8, to_send: serde_json::Value) { + let gateway_payload = VoiceGatewaySendPayload { + op_code, + data: to_send, + }; + + let payload_json = serde_json::to_string(&gateway_payload).unwrap(); + let message = VoiceGatewayMesssage(payload_json); + + self.websocket_send + .lock() + .await + .send(message.into()) + .await + .unwrap(); + } + + /// Sends a voice identify event to the gateway + pub async fn send_identify(&self, to_send: VoiceIdentify) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("VGW: Sending Identify.."); + + self.send_json(VOICE_IDENTIFY, to_send_value).await; + } + + /// Sends a select protocol event to the gateway + pub async fn send_select_protocol(&self, to_send: SelectProtocol) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("VGW: Sending Select Protocol"); + + self.send_json(VOICE_SELECT_PROTOCOL, to_send_value).await; + } + + /// Sends a speaking event to the gateway + pub async fn send_speaking(&self, to_send: Speaking) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("VGW: Sending Speaking"); + + self.send_json(VOICE_SPEAKING, to_send_value).await; + } + + /// Sends an ssrc definition event + pub async fn send_ssrc_definition(&self, to_send: SsrcDefinition) { + let to_send_value = serde_json::to_value(&to_send).unwrap(); + + trace!("VGW: Sending SsrcDefinition"); + + self.send_json(VOICE_SSRC_DEFINITION, to_send_value).await; + } + + /// Sends a voice backend version request to the gateway + pub async fn send_voice_backend_version_request(&self) { + let data_empty_object = json!("{}"); + + trace!("VGW: Requesting voice backend version"); + + self.send_json(VOICE_BACKEND_VERSION, data_empty_object) + .await; + } + + /// Closes the websocket connection and stops all gateway tasks; + /// + /// Esentially pulls the plug on the voice gateway, leaving it possible to resume; + pub async fn close(&self) { + self.kill_send.send(()).unwrap(); + self.websocket_send.lock().await.close().await.unwrap(); + } +} diff --git a/src/voice/gateway/heartbeat.rs b/src/voice/gateway/heartbeat.rs new file mode 100644 index 0000000..afd7033 --- /dev/null +++ b/src/voice/gateway/heartbeat.rs @@ -0,0 +1,160 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use futures_util::SinkExt; +use log::*; +use safina_timer::sleep_until; +use tokio::sync::{mpsc::Sender, Mutex}; + +use crate::{ + gateway::heartbeat::HEARTBEAT_ACK_TIMEOUT, + types::{VoiceGatewaySendPayload, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK}, + voice::gateway::VoiceGatewayMesssage, +}; + +use super::Sink; + +/// Handles sending heartbeats to the voice gateway in another thread +#[allow(dead_code)] // FIXME: Remove this, once all fields of VoiceHeartbeatHandler are used +#[derive(Debug)] +pub(super) struct VoiceHeartbeatHandler { + /// The heartbeat interval in milliseconds + pub heartbeat_interval: Duration, + /// The send channel for the heartbeat thread + pub send: Sender, +} + +impl VoiceHeartbeatHandler { + pub fn new( + heartbeat_interval: Duration, + starting_nonce: u64, + websocket_tx: Arc>, + kill_rc: tokio::sync::broadcast::Receiver<()>, + ) -> Self { + let (send, receive) = tokio::sync::mpsc::channel(32); + let kill_receive = kill_rc.resubscribe(); + + #[cfg(not(target_arch = "wasm32"))] + tokio::task::spawn(async move { + Self::heartbeat_task( + websocket_tx, + heartbeat_interval, + starting_nonce, + receive, + kill_receive, + ) + .await; + }); + #[cfg(target_arch = "wasm32")] + wasm_bindgen_futures::spawn_local(async move { + Self::heartbeat_task( + websocket_tx, + heartbeat_interval, + starting_nonce, + receive, + kill_receive, + ) + .await; + }); + + Self { + heartbeat_interval, + send, + } + } + + /// The main heartbeat task; + /// + /// Can be killed by the kill broadcast; + /// If the websocket is closed, will die out next time it tries to send a heartbeat; + pub async fn heartbeat_task( + websocket_tx: Arc>, + heartbeat_interval: Duration, + starting_nonce: u64, + mut receive: tokio::sync::mpsc::Receiver, + mut kill_receive: tokio::sync::broadcast::Receiver<()>, + ) { + let mut last_heartbeat_timestamp: Instant = Instant::now(); + let mut last_heartbeat_acknowledged = true; + let mut nonce: u64 = starting_nonce; + + safina_timer::start_timer_thread(); + + loop { + if kill_receive.try_recv().is_ok() { + trace!("VGW: Closing heartbeat task"); + break; + } + + let timeout = if last_heartbeat_acknowledged { + heartbeat_interval + } else { + // If the server hasn't acknowledged our heartbeat we should resend it + Duration::from_millis(HEARTBEAT_ACK_TIMEOUT) + }; + + let mut should_send = false; + + tokio::select! { + () = sleep_until(last_heartbeat_timestamp + timeout) => { + should_send = true; + } + Some(communication) = receive.recv() => { + // If we received a nonce update, use that nonce now + if communication.updated_nonce.is_some() { + nonce = communication.updated_nonce.unwrap(); + } + + if let Some(op_code) = communication.op_code { + match op_code { + VOICE_HEARTBEAT => { + // As per the api docs, if the server sends us a Heartbeat, that means we need to respond with a heartbeat immediately + should_send = true; + } + VOICE_HEARTBEAT_ACK => { + // The server received our heartbeat + last_heartbeat_acknowledged = true; + } + _ => {} + } + } + } + } + + if should_send { + trace!("VGW: Sending Heartbeat.."); + + let heartbeat = VoiceGatewaySendPayload { + op_code: VOICE_HEARTBEAT, + data: nonce.into(), + }; + + let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); + + let msg = VoiceGatewayMesssage(heartbeat_json); + + let send_result = websocket_tx.lock().await.send(msg.into()).await; + if send_result.is_err() { + // We couldn't send, the websocket is broken + warn!("VGW: Couldnt send heartbeat, websocket seems broken"); + break; + } + + last_heartbeat_timestamp = Instant::now(); + last_heartbeat_acknowledged = false; + } + } + } +} + +/// Used for communications between the voice heartbeat and voice gateway thread. +/// Either signifies a nonce update, a heartbeat ACK or a Heartbeat request by the server +#[derive(Clone, Copy, Debug)] +pub(super) struct VoiceHeartbeatThreadCommunication { + /// The opcode for the communication we received, if relevant + pub(super) op_code: Option, + /// The new nonce to use, if any + pub(super) updated_nonce: Option, +} diff --git a/src/voice/gateway/message.rs b/src/voice/gateway/message.rs new file mode 100644 index 0000000..ff723c3 --- /dev/null +++ b/src/voice/gateway/message.rs @@ -0,0 +1,39 @@ +use crate::{errors::VoiceGatewayError, types::VoiceGatewayReceivePayload}; + +/// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. +/// This struct is used internally when handling messages. +#[derive(Clone, Debug)] +pub struct VoiceGatewayMesssage(pub String); + +impl VoiceGatewayMesssage { + /// Parses the message as an error; + /// Returns the error if succesfully parsed, None if the message isn't an error + pub fn error(&self) -> Option { + // Some error strings have dots on the end, which we don't care about + let processed_content = self.0.to_lowercase().replace('.', ""); + + match processed_content.as_str() { + "unknown opcode" | "4001" => Some(VoiceGatewayError::UnknownOpcode), + "decode error" | "failed to decode payload" | "4002" => { + Some(VoiceGatewayError::FailedToDecodePayload) + } + "not authenticated" | "4003" => Some(VoiceGatewayError::NotAuthenticated), + "authentication failed" | "4004" => Some(VoiceGatewayError::AuthenticationFailed), + "already authenticated" | "4005" => Some(VoiceGatewayError::AlreadyAuthenticated), + "session is no longer valid" | "4006" => Some(VoiceGatewayError::SessionNoLongerValid), + "session timeout" | "4009" => Some(VoiceGatewayError::SessionTimeout), + "server not found" | "4011" => Some(VoiceGatewayError::ServerNotFound), + "unknown protocol" | "4012" => Some(VoiceGatewayError::UnknownProtocol), + "disconnected" | "4014" => Some(VoiceGatewayError::Disconnected), + "voice server crashed" | "4015" => Some(VoiceGatewayError::VoiceServerCrashed), + "unknown encryption mode" | "4016" => Some(VoiceGatewayError::UnknownEncryptionMode), + _ => None, + } + } + + /// Parses the message as a payload; + /// Returns a result of deserializing + pub fn payload(&self) -> Result { + return serde_json::from_str(&self.0); + } +} diff --git a/src/voice/gateway/mod.rs b/src/voice/gateway/mod.rs new file mode 100644 index 0000000..4819663 --- /dev/null +++ b/src/voice/gateway/mod.rs @@ -0,0 +1,11 @@ +pub mod backends; +pub mod events; +pub mod gateway; +pub mod handle; +pub mod heartbeat; +pub mod message; + +pub use backends::*; +pub use gateway::*; +pub use handle::*; +pub use message::*; diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 9b6f9cf..9fbfd79 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -344,7 +344,7 @@ impl UdpHandler { } Demuxed::Rtcp(rtcp) => { trace!("VUDP: Parsed packet as rtcp!"); - + let rtcp_data = match rtcp { discortp::rtcp::RtcpPacket::KnownType(knowntype) => { discortp::rtcp::Rtcp::KnownType(knowntype) From a5e4170641285dbb3544da937649f87c789abb26 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:29:49 +0100 Subject: [PATCH 52/72] fix: blunder --- src/voice/gateway/backends/tungstenite.rs | 8 ++++---- src/voice/gateway/backends/wasm.rs | 2 +- src/voice/gateway/gateway.rs | 6 +++--- src/voice/gateway/handle.rs | 4 ++-- src/voice/gateway/heartbeat.rs | 4 ++-- src/voice/gateway/message.rs | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/voice/gateway/backends/tungstenite.rs b/src/voice/gateway/backends/tungstenite.rs index 090fdb9..6e2be02 100644 --- a/src/voice/gateway/backends/tungstenite.rs +++ b/src/voice/gateway/backends/tungstenite.rs @@ -7,7 +7,7 @@ use tokio_tungstenite::{ connect_async_tls_with_config, tungstenite, Connector, MaybeTlsStream, WebSocketStream, }; -use crate::{errors::VoiceGatewayError, voice::gateway::VoiceGatewayMesssage}; +use crate::{errors::VoiceGatewayError, voice::gateway::VoiceGatewayMessage}; #[derive(Debug, Clone)] pub struct TungsteniteBackend; @@ -52,13 +52,13 @@ impl TungsteniteBackend { } } -impl From for tungstenite::Message { - fn from(message: VoiceGatewayMesssage) -> Self { +impl From for tungstenite::Message { + fn from(message: VoiceGatewayMessage) -> Self { Self::Text(message.0) } } -impl From for VoiceGatewayMesssage { +impl From for VoiceGatewayMessage { fn from(value: tungstenite::Message) -> Self { Self(value.to_string()) } diff --git a/src/voice/gateway/backends/wasm.rs b/src/voice/gateway/backends/wasm.rs index 588c882..6a7f3d4 100644 --- a/src/voice/gateway/backends/wasm.rs +++ b/src/voice/gateway/backends/wasm.rs @@ -28,7 +28,7 @@ impl WasmBackend { } } -impl From for WsMessage { +impl From for WsMessage { fn from(message: VoiceGatewayMessage) -> Self { Self::Text(message.0) } diff --git a/src/voice/gateway/gateway.rs b/src/voice/gateway/gateway.rs index e6960e9..bf643ba 100644 --- a/src/voice/gateway/gateway.rs +++ b/src/voice/gateway/gateway.rs @@ -18,7 +18,7 @@ use crate::{ VOICE_SESSION_UPDATE, VOICE_SPEAKING, VOICE_SSRC_DEFINITION, }, voice::gateway::{ - heartbeat::VoiceHeartbeatThreadCommunication, VoiceGatewayMesssage, WebSocketBackend, + heartbeat::VoiceHeartbeatThreadCommunication, VoiceGatewayMessage, WebSocketBackend, }, }; @@ -53,7 +53,7 @@ impl VoiceGateway { // 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: VoiceGatewayMesssage = websocket_receive.next().await.unwrap().unwrap().into(); + let msg: VoiceGatewayMessage = websocket_receive.next().await.unwrap().unwrap().into(); #[cfg(target_arch = "wasm32")] let msg: VoiceGatewayMessage = websocket_receive.next().await.unwrap().into(); let gateway_payload: VoiceGatewayReceivePayload = serde_json::from_str(&msg.0).unwrap(); @@ -153,7 +153,7 @@ impl VoiceGateway { } /// 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: VoiceGatewayMesssage) { + pub async fn handle_message(&mut self, msg: VoiceGatewayMessage) { if msg.0.is_empty() { return; } diff --git a/src/voice/gateway/handle.rs b/src/voice/gateway/handle.rs index d24adbf..e526b54 100644 --- a/src/voice/gateway/handle.rs +++ b/src/voice/gateway/handle.rs @@ -13,7 +13,7 @@ use crate::types::{ VOICE_SSRC_DEFINITION, }; -use super::{events::VoiceEvents, Sink, VoiceGatewayMesssage}; +use super::{events::VoiceEvents, Sink, VoiceGatewayMessage}; /// Represents a handle to a Voice Gateway connection. /// Using this handle you can send Gateway Events directly. @@ -35,7 +35,7 @@ impl VoiceGatewayHandle { }; let payload_json = serde_json::to_string(&gateway_payload).unwrap(); - let message = VoiceGatewayMesssage(payload_json); + let message = VoiceGatewayMessage(payload_json); self.websocket_send .lock() diff --git a/src/voice/gateway/heartbeat.rs b/src/voice/gateway/heartbeat.rs index afd7033..566a778 100644 --- a/src/voice/gateway/heartbeat.rs +++ b/src/voice/gateway/heartbeat.rs @@ -11,7 +11,7 @@ use tokio::sync::{mpsc::Sender, Mutex}; use crate::{ gateway::heartbeat::HEARTBEAT_ACK_TIMEOUT, types::{VoiceGatewaySendPayload, VOICE_HEARTBEAT, VOICE_HEARTBEAT_ACK}, - voice::gateway::VoiceGatewayMesssage, + voice::gateway::VoiceGatewayMessage, }; use super::Sink; @@ -133,7 +133,7 @@ impl VoiceHeartbeatHandler { let heartbeat_json = serde_json::to_string(&heartbeat).unwrap(); - let msg = VoiceGatewayMesssage(heartbeat_json); + let msg = VoiceGatewayMessage(heartbeat_json); let send_result = websocket_tx.lock().await.send(msg.into()).await; if send_result.is_err() { diff --git a/src/voice/gateway/message.rs b/src/voice/gateway/message.rs index ff723c3..ebda848 100644 --- a/src/voice/gateway/message.rs +++ b/src/voice/gateway/message.rs @@ -3,9 +3,9 @@ use crate::{errors::VoiceGatewayError, types::VoiceGatewayReceivePayload}; /// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. /// This struct is used internally when handling messages. #[derive(Clone, Debug)] -pub struct VoiceGatewayMesssage(pub String); +pub struct VoiceGatewayMessage(pub String); -impl VoiceGatewayMesssage { +impl VoiceGatewayMessage { /// Parses the message as an error; /// Returns the error if succesfully parsed, None if the message isn't an error pub fn error(&self) -> Option { @@ -34,6 +34,6 @@ impl VoiceGatewayMesssage { /// Parses the message as a payload; /// Returns a result of deserializing pub fn payload(&self) -> Result { - return serde_json::from_str(&self.0); + serde_json::from_str(&self.0) } } From a5283c7780b5af5546b8c536f06e1f63458afb86 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 29 Dec 2023 10:08:37 +0100 Subject: [PATCH 53/72] fix: gateway connect using wrong url --- src/voice/gateway/gateway.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/voice/gateway/gateway.rs b/src/voice/gateway/gateway.rs index bf643ba..9ba1720 100644 --- a/src/voice/gateway/gateway.rs +++ b/src/voice/gateway/gateway.rs @@ -43,7 +43,7 @@ impl VoiceGateway { trace!("Created voice socket url: {}", processed_url.clone()); let (websocket_send, mut websocket_receive) = - WebSocketBackend::connect(&websocket_url).await?; + WebSocketBackend::connect(&processed_url).await?; let shared_websocket_send = Arc::new(Mutex::new(websocket_send)); From 9039e216be2481596485580b2dc25596f2d93e19 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 29 Dec 2023 10:09:06 +0100 Subject: [PATCH 54/72] fix: properly using encrypted data, bad practice for buffer creation --- src/voice/udp.rs | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/voice/udp.rs b/src/voice/udp.rs index 9fbfd79..b75241d 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp.rs @@ -66,14 +66,9 @@ impl UdpHandle { payload, }; - let mut buffer = Vec::new(); - let buffer_size = payload_len + RTP_HEADER_SIZE as usize; - // Fill the buffer - for _i in 0..buffer_size { - buffer.push(0); - } + let mut buffer = vec![0; buffer_size]; let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); rtp_packet.populate(&rtp_data); @@ -121,18 +116,18 @@ impl UdpHandle { panic!("Encryption error"); } - let encrypted_payload = encryption_result.unwrap(); + let mut encrypted_payload = encryption_result.unwrap(); // We need to allocate a new buffer, since the old one is too small for our new encrypted // data - let mut new_buffer = packet.packet().to_vec(); - let buffer_size = encrypted_payload.len() + RTP_HEADER_SIZE as usize; - // Fill the buffer - while new_buffer.len() <= buffer_size { - new_buffer.push(0); - } + let mut new_buffer: Vec = Vec::with_capacity(buffer_size); + + let mut rtp_header = packet.packet().to_vec()[0..RTP_HEADER_SIZE as usize].to_vec(); + + new_buffer.append(&mut rtp_header); + new_buffer.append(&mut encrypted_payload); new_buffer } @@ -257,12 +252,10 @@ impl UdpHandler { /// Receives udp messages and parses them. pub async fn listen_task(&mut self) { loop { - let mut buf: Vec = Vec::new(); + // FIXME: is there a max size for these packets? + // Allocating 512 bytes seems a bit extreme + let mut buf: Vec = vec![0; 512]; - // FIXME: is there a better way to do this? - for _i in 0..1_000 { - buf.push(0); - } let result = self.socket.recv(&mut buf).await; if let Ok(size) = result { self.handle_message(&buf[0..size]).await; @@ -280,7 +273,7 @@ impl UdpHandler { match parsed { Demuxed::Rtp(rtp) => { - let ciphertext = buf[12..buf.len()].to_vec(); + let ciphertext = buf[(RTP_HEADER_SIZE as usize)..buf.len()].to_vec(); trace!("VUDP: Parsed packet as rtp!"); let session_description_result = self.data.read().await.session_description.clone(); From 8413b66e2260f42e98d2cd2bcfd4321025169bbc Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 29 Dec 2023 11:33:14 +0100 Subject: [PATCH 55/72] chore: split voice udp --- src/voice/crypto.rs | 5 +- src/voice/udp/events.rs | 21 +++ src/voice/udp/handle.rs | 127 ++++++++++++++++++ src/voice/{udp.rs => udp/handler.rs} | 188 ++++----------------------- src/voice/udp/mod.rs | 12 ++ 5 files changed, 185 insertions(+), 168 deletions(-) create mode 100644 src/voice/udp/events.rs create mode 100644 src/voice/udp/handle.rs rename src/voice/{udp.rs => udp/handler.rs} (60%) create mode 100644 src/voice/udp/mod.rs diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 1365ca7..172abf3 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -4,10 +4,11 @@ /// Gets an xsalsa20poly1305 nonce from an rtppacket. pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { - let mut rtp_header = packet[0..12].to_vec(); + + let mut rtp_header = Vec::with_capacity(24); + rtp_header.append(&mut packet[0..12].to_vec()); // The header is only 12 bytes, but the nonce has to be 24 - // This actually works mind you, and anything else doesn't for _i in 0..12 { rtp_header.push(0); } diff --git a/src/voice/udp/events.rs b/src/voice/udp/events.rs new file mode 100644 index 0000000..1ae06eb --- /dev/null +++ b/src/voice/udp/events.rs @@ -0,0 +1,21 @@ +use discortp::{rtcp::Rtcp, rtp::Rtp}; + +use crate::{gateway::GatewayEvent, types::WebSocketEvent}; + +impl WebSocketEvent for Rtp {} +impl WebSocketEvent for Rtcp {} + +#[derive(Debug)] +pub struct VoiceUDPEvents { + pub rtp: GatewayEvent, + pub rtcp: GatewayEvent, +} + +impl Default for VoiceUDPEvents { + fn default() -> Self { + Self { + rtp: GatewayEvent::new(), + rtcp: GatewayEvent::new(), + } + } +} diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs new file mode 100644 index 0000000..f402ef0 --- /dev/null +++ b/src/voice/udp/handle.rs @@ -0,0 +1,127 @@ +use std::sync::Arc; + +use crypto_secretbox::{ + aead::Aead, cipher::generic_array::GenericArray, KeyInit, XSalsa20Poly1305, +}; +use discortp::Packet; + +use log::*; + +use tokio::{net::UdpSocket, sync::Mutex, sync::RwLock}; + +use crate::voice::{crypto, voice_data::VoiceData}; + +use super::{events::VoiceUDPEvents, RTP_HEADER_SIZE}; + +/// Handle to a voice udp connection +/// +/// Can be safely cloned and will still correspond to the same connection. +#[derive(Debug, Clone)] +pub struct UdpHandle { + pub events: Arc>, + pub(super) socket: Arc, + pub data: Arc>, +} + +impl UdpHandle { + /// Constructs and sends encoded opus rtp data. + /// + /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. + pub async fn send_opus_data(&self, timestamp: u32, payload: Vec) { + let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc; + let sequence_number = self.data.read().await.last_sequence_number.wrapping_add(1); + self.data.write().await.last_sequence_number = sequence_number; + + let payload_len = payload.len(); + + let rtp_data = discortp::rtp::Rtp { + // Always the same + version: 2, + padding: 0, + extension: 0, + csrc_count: 0, + csrc_list: Vec::new(), + marker: 0, + payload_type: discortp::rtp::RtpType::Dynamic(120), + // Actually variable + sequence: sequence_number.into(), + timestamp: timestamp.into(), + ssrc, + payload, + }; + + let buffer_size = payload_len + RTP_HEADER_SIZE as usize; + + let mut buffer = vec![0; buffer_size]; + + let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); + rtp_packet.populate(&rtp_data); + + self.send_rtp_packet(rtp_packet).await; + } + + /// Encrypts and sends and rtp packet. + pub async fn send_rtp_packet(&self, packet: discortp::rtp::MutableRtpPacket<'_>) { + let mut buffer = self.encrypt_rtp_packet_payload(&packet).await; + let new_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); + self.send_encrypted_rtp_packet(new_packet.consume_to_immutable()) + .await; + } + + /// Encrypts an unencrypted rtp packet, returning a copy of the packet's bytes with an + /// encrypted payload + pub async fn encrypt_rtp_packet_payload( + &self, + packet: &discortp::rtp::MutableRtpPacket<'_>, + ) -> Vec { + let payload = packet.payload(); + + let session_description_result = self.data.read().await.session_description.clone(); + + if session_description_result.is_none() { + // FIXME: Make this function reutrn a result with a proper error type for these kinds + // of functions + panic!("Trying to encrypt packet but no key provided yet"); + } + + let session_description = session_description_result.unwrap(); + + let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.packet()); + let nonce = GenericArray::from_slice(&nonce_bytes); + + let key = GenericArray::from_slice(&session_description.secret_key); + + let encryptor = XSalsa20Poly1305::new(key); + + let encryption_result = encryptor.encrypt(nonce, payload); + + if encryption_result.is_err() { + // FIXME: See above fixme + panic!("Encryption error"); + } + + let mut encrypted_payload = encryption_result.unwrap(); + + // We need to allocate a new buffer, since the old one is too small for our new encrypted + // data + let buffer_size = encrypted_payload.len() + RTP_HEADER_SIZE as usize; + + let mut new_buffer: Vec = Vec::with_capacity(buffer_size); + + let mut rtp_header = packet.packet().to_vec()[0..RTP_HEADER_SIZE as usize].to_vec(); + + new_buffer.append(&mut rtp_header); + new_buffer.append(&mut encrypted_payload); + + new_buffer + } + + /// Sends an (already encrypted) rtp packet to the connection. + pub async fn send_encrypted_rtp_packet(&self, packet: discortp::rtp::RtpPacket<'_>) { + let raw_bytes = packet.packet(); + + self.socket.send(raw_bytes).await.unwrap(); + + debug!("VUDP: Sent rtp packet!"); + } +} diff --git a/src/voice/udp.rs b/src/voice/udp/handler.rs similarity index 60% rename from src/voice/udp.rs rename to src/voice/udp/handler.rs index b75241d..af9f4c3 100644 --- a/src/voice/udp.rs +++ b/src/voice/udp/handler.rs @@ -1,148 +1,32 @@ -//! Defines voice raw udp socket handling - -use self::voice_udp_events::VoiceUDPEvents; - -use super::crypto; - use std::{net::SocketAddr, sync::Arc}; -use log::{debug, info, trace, warn}; +use crypto_secretbox::aead::Aead; +use crypto_secretbox::cipher::generic_array::GenericArray; +use crypto_secretbox::KeyInit; +use crypto_secretbox::XSalsa20Poly1305; + +use discortp::demux::Demuxed; +use discortp::discord::{ + IpDiscovery, IpDiscoveryPacket, IpDiscoveryType, MutableIpDiscoveryPacket, +}; +use discortp::rtcp::report::ReceiverReport; +use discortp::rtcp::report::SenderReport; +use discortp::{demux::demux, Packet}; use tokio::{ net::UdpSocket, sync::{Mutex, RwLock}, }; -use crypto_secretbox::{ - aead::Aead, cipher::generic_array::GenericArray, KeyInit, XSalsa20Poly1305, -}; +use crate::voice::crypto::get_xsalsa20_poly1305_nonce; +use super::RTP_HEADER_SIZE; +use crate::voice::voice_data::VoiceData; -use discortp::{ - demux::{demux, Demuxed}, - discord::{IpDiscovery, IpDiscoveryPacket, IpDiscoveryType, MutableIpDiscoveryPacket}, - rtcp::report::{ReceiverReport, SenderReport}, - Packet, -}; +use super::{events::VoiceUDPEvents, UdpHandle}; -use super::voice_data::VoiceData; - -/// See -/// This always adds up to 12 -const RTP_HEADER_SIZE: u8 = 12; - -/// Handle to a voice udp connection -/// -/// Can be safely cloned and will still correspond to the same connection. -#[derive(Debug, Clone)] -pub struct UdpHandle { - pub events: Arc>, - socket: Arc, - pub data: Arc>, -} - -impl UdpHandle { - /// Constructs and sends encoded opus rtp data. - /// - /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. - pub async fn send_opus_data(&self, timestamp: u32, payload: Vec) { - let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc; - let sequence_number = self.data.read().await.last_sequence_number.wrapping_add(1); - self.data.write().await.last_sequence_number = sequence_number; - - let payload_len = payload.len(); - - let rtp_data = discortp::rtp::Rtp { - // Always the same - version: 2, - padding: 0, - extension: 0, - csrc_count: 0, - csrc_list: Vec::new(), - marker: 0, - payload_type: discortp::rtp::RtpType::Dynamic(120), - // Actually variable - sequence: sequence_number.into(), - timestamp: timestamp.into(), - ssrc, - payload, - }; - - let buffer_size = payload_len + RTP_HEADER_SIZE as usize; - - let mut buffer = vec![0; buffer_size]; - - let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); - rtp_packet.populate(&rtp_data); - - self.send_rtp_packet(rtp_packet).await; - } - - /// Encrypts and sends and rtp packet. - pub async fn send_rtp_packet(&self, packet: discortp::rtp::MutableRtpPacket<'_>) { - let mut buffer = self.encrypt_rtp_packet_payload(&packet).await; - let new_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); - self.send_encrypted_rtp_packet(new_packet.consume_to_immutable()) - .await; - } - - /// Encrypts an unencrypted rtp packet, returning a copy of the packet's bytes with an - /// encrypted payload - pub async fn encrypt_rtp_packet_payload( - &self, - packet: &discortp::rtp::MutableRtpPacket<'_>, - ) -> Vec { - let payload = packet.payload(); - - let session_description_result = self.data.read().await.session_description.clone(); - - if session_description_result.is_none() { - // FIXME: Make this function reutrn a result with a proper error type for these kinds - // of functions - panic!("Trying to encrypt packet but no key provided yet"); - } - - let session_description = session_description_result.unwrap(); - - let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.packet()); - let nonce = GenericArray::from_slice(&nonce_bytes); - - let key = GenericArray::from_slice(&session_description.secret_key); - - let encryptor = XSalsa20Poly1305::new(key); - - let encryption_result = encryptor.encrypt(nonce, payload); - - if encryption_result.is_err() { - // FIXME: See above fixme - panic!("Encryption error"); - } - - let mut encrypted_payload = encryption_result.unwrap(); - - // We need to allocate a new buffer, since the old one is too small for our new encrypted - // data - let buffer_size = encrypted_payload.len() + RTP_HEADER_SIZE as usize; - - let mut new_buffer: Vec = Vec::with_capacity(buffer_size); - - let mut rtp_header = packet.packet().to_vec()[0..RTP_HEADER_SIZE as usize].to_vec(); - - new_buffer.append(&mut rtp_header); - new_buffer.append(&mut encrypted_payload); - - new_buffer - } - - /// Sends an (already encrypted) rtp packet to the connection. - pub async fn send_encrypted_rtp_packet(&self, packet: discortp::rtp::RtpPacket<'_>) { - let raw_bytes = packet.packet(); - - self.socket.send(raw_bytes).await.unwrap(); - - debug!("VUDP: Sent rtp packet!"); - } -} +use log::*; #[derive(Debug)] +/// The main UDP struct, which handles receiving, parsing and decrypting the rtp packets pub struct UdpHandler { events: Arc>, pub data: Arc>, @@ -173,15 +57,12 @@ impl UdpHandler { payload: Vec::new(), }; - let mut buf: Vec = Vec::new(); - let size = IpDiscoveryPacket::minimum_packet_size() + 64; - for _i in 0..size { - buf.push(0); - } + let mut buf: Vec = vec![0; size]; // TODO: Make this not panic everything + // Actually, if this panics, something is very, very wrong let mut ip_discovery_packet = MutableIpDiscoveryPacket::new(&mut buf).expect("Mangled ip discovery packet"); @@ -250,7 +131,7 @@ impl UdpHandler { /// The main listen task; /// /// Receives udp messages and parses them. - pub async fn listen_task(&mut self) { + async fn listen_task(&mut self) { loop { // FIXME: is there a max size for these packets? // Allocating 512 bytes seems a bit extreme @@ -287,7 +168,7 @@ impl UdpHandler { let nonce_bytes = match session_description.encryption_mode { crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - crypto::get_xsalsa20_poly1305_nonce(rtp.packet()) + get_xsalsa20_poly1305_nonce(rtp.packet()) } _ => { unimplemented!(); @@ -380,28 +261,3 @@ impl UdpHandler { } } } - -pub mod voice_udp_events { - - use discortp::{rtcp::Rtcp, rtp::Rtp}; - - use crate::{gateway::GatewayEvent, types::WebSocketEvent}; - - impl WebSocketEvent for Rtp {} - impl WebSocketEvent for Rtcp {} - - #[derive(Debug)] - pub struct VoiceUDPEvents { - pub rtp: GatewayEvent, - pub rtcp: GatewayEvent, - } - - impl Default for VoiceUDPEvents { - fn default() -> Self { - Self { - rtp: GatewayEvent::new(), - rtcp: GatewayEvent::new(), - } - } - } -} diff --git a/src/voice/udp/mod.rs b/src/voice/udp/mod.rs new file mode 100644 index 0000000..3d0b276 --- /dev/null +++ b/src/voice/udp/mod.rs @@ -0,0 +1,12 @@ +//! Defines the udp component of voice communications, sending and receiving raw rtp data. + +/// See +/// This always adds up to 12 bytes +const RTP_HEADER_SIZE: u8 = 12; + +pub mod handle; +pub mod events; +pub mod handler; + +pub use handle::*; +pub use handler::*; From e9ef2444d5bc722babf20c7cc69c40a2c0a84220 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 29 Dec 2023 12:48:22 +0100 Subject: [PATCH 56/72] feat: udp error handling, create udp/backends --- src/errors.rs | 20 ++++++++ src/voice/crypto.rs | 1 - src/voice/handler.rs | 3 +- src/voice/udp/backends/mod.rs | 19 ++++++++ src/voice/udp/backends/tokio.rs | 33 +++++++++++++ src/voice/udp/backends/wasm.rs | 13 +++++ src/voice/udp/handle.rs | 86 ++++++++++++++++++++++++++------- src/voice/udp/handler.rs | 51 +++++++++++-------- src/voice/udp/mod.rs | 4 +- 9 files changed, 190 insertions(+), 40 deletions(-) create mode 100644 src/voice/udp/backends/mod.rs create mode 100644 src/voice/udp/backends/tokio.rs create mode 100644 src/voice/udp/backends/wasm.rs diff --git a/src/errors.rs b/src/errors.rs index 15c5b44..bf3727c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -129,3 +129,23 @@ custom_error! { } impl WebSocketEvent for VoiceGatewayError {} + +custom_error! { + /// Voice UDP errors. + #[derive(Clone, PartialEq, Eq)] + pub VoiceUdpError + + // General errors + BrokenSocket{error: String} = "Could not write / read from udp socket: {error}", + NoData = "We have not set received the necessary data to perform this operation.", + + // Encryption errors + NoKey = "Tried to encrypt / decrypt rtp data, but no key has been received yet", + FailedEncryption = "Tried to encrypt rtp data, but failed. Most likely this is an issue chorus' nonce generation. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new", + + // Errors when initiating a socket connection + CannotBind{error: String} = "Cannot bind socket due to a udp error: {error}", + CannotConnect{error: String} = "Cannot connect due to a udp error: {error}", +} + +impl WebSocketEvent for VoiceUdpError {} diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 172abf3..7bcc056 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -4,7 +4,6 @@ /// Gets an xsalsa20poly1305 nonce from an rtppacket. pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { - let mut rtp_header = Vec::with_capacity(24); rtp_header.append(&mut packet[0..12].to_vec()); diff --git a/src/voice/handler.rs b/src/voice/handler.rs index c8ddd8c..aa3abcb 100644 --- a/src/voice/handler.rs +++ b/src/voice/handler.rs @@ -104,7 +104,8 @@ impl Observer for VoiceHandler { std::net::SocketAddr::V4(SocketAddrV4::new(data.ip, data.port)), data.ssrc, ) - .await; + .await + .unwrap(); let ip_discovery = self.data.read().await.ip_discovery.clone().unwrap(); diff --git a/src/voice/udp/backends/mod.rs b/src/voice/udp/backends/mod.rs new file mode 100644 index 0000000..521d085 --- /dev/null +++ b/src/voice/udp/backends/mod.rs @@ -0,0 +1,19 @@ +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub mod tokio; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub use tokio::*; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub mod wasm; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub use wasm::*; + +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type UdpSocket = tokio::TokioSocket; +#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +pub type UdpBackend = tokio::TokioBackend; + +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type UdpSocket = wasm::WasmSocket; +#[cfg(all(target_arch = "wasm32", feature = "client"))] +pub type UdpBackend = wasm::WasmBackend; diff --git a/src/voice/udp/backends/tokio.rs b/src/voice/udp/backends/tokio.rs new file mode 100644 index 0000000..eec1386 --- /dev/null +++ b/src/voice/udp/backends/tokio.rs @@ -0,0 +1,33 @@ +use std::net::SocketAddr; + +use crate::errors::VoiceUdpError; + +#[derive(Debug, Clone)] +pub struct TokioBackend; + +pub type TokioSocket = tokio::net::UdpSocket; + +impl TokioBackend { + pub async fn connect(url: SocketAddr) -> Result { + // Bind with a port number of 0, so the os assigns this listener a port + let udp_socket_result = TokioSocket::bind("0.0.0.0:0").await; + + if let Err(e) = udp_socket_result { + return Err(VoiceUdpError::CannotBind { + error: format!("{:?}", e), + }); + } + + let udp_socket = udp_socket_result.unwrap(); + + let connection_result = udp_socket.connect(url).await; + + if let Err(e) = connection_result { + return Err(VoiceUdpError::CannotConnect { + error: format!("{:?}", e), + }); + } + + Ok(udp_socket) + } +} diff --git a/src/voice/udp/backends/wasm.rs b/src/voice/udp/backends/wasm.rs new file mode 100644 index 0000000..502987b --- /dev/null +++ b/src/voice/udp/backends/wasm.rs @@ -0,0 +1,13 @@ +use std::net::SocketAddr; + +// TODO: Add wasm websockets +compile_error!("Udp voice support is not implemented yet for wasm."); + +#[derive(Debug, Clone)] +pub struct WasmBackend; + +pub type WasmSocket; + +impl WasmBackend { + pub async fn connect(url: SocketAddr) -> Result {} +} diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs index f402ef0..e597f11 100644 --- a/src/voice/udp/handle.rs +++ b/src/voice/udp/handle.rs @@ -7,9 +7,14 @@ use discortp::Packet; use log::*; -use tokio::{net::UdpSocket, sync::Mutex, sync::RwLock}; +use tokio::{sync::Mutex, sync::RwLock}; -use crate::voice::{crypto, voice_data::VoiceData}; +use super::UdpSocket; + +use crate::{ + errors::VoiceUdpError, + voice::{crypto, voice_data::VoiceData}, +}; use super::{events::VoiceUDPEvents, RTP_HEADER_SIZE}; @@ -27,8 +32,25 @@ impl UdpHandle { /// Constructs and sends encoded opus rtp data. /// /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. - pub async fn send_opus_data(&self, timestamp: u32, payload: Vec) { - let ssrc = self.data.read().await.ready_data.clone().unwrap().ssrc; + /// + /// # Errors + /// If we do not have VoiceReady data, which contains our ssrc, this returns a + /// [VoiceUdpError::NoData] error. + /// + /// If we have not received an encryption key, this returns a [VoiceUdpError::NoKey] error. + /// + /// If the Udp socket is broken, this returns a [VoiceUdpError::BrokenSocket] error. + pub async fn send_opus_data( + &self, + timestamp: u32, + payload: Vec, + ) -> Result<(), VoiceUdpError> { + let voice_ready_data_result = self.data.read().await.ready_data.clone(); + if let None = voice_ready_data_result { + return Err(VoiceUdpError::NoData); + } + + let ssrc = voice_ready_data_result.unwrap().ssrc; let sequence_number = self.data.read().await.last_sequence_number.wrapping_add(1); self.data.write().await.last_sequence_number = sequence_number; @@ -54,34 +76,46 @@ impl UdpHandle { let mut buffer = vec![0; buffer_size]; - let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); + let mut rtp_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).expect("Mangled rtp packet creation buffer, something is very wrong. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new"); rtp_packet.populate(&rtp_data); - self.send_rtp_packet(rtp_packet).await; + self.send_rtp_packet(rtp_packet).await } /// Encrypts and sends and rtp packet. - pub async fn send_rtp_packet(&self, packet: discortp::rtp::MutableRtpPacket<'_>) { - let mut buffer = self.encrypt_rtp_packet_payload(&packet).await; + /// + /// # Errors + /// If we have not received an encryption key, this returns a [VoiceUdpError::NoKey] error. + /// + /// If the Udp socket is broken, this returns a [VoiceUdpError::BrokenSocket] error. + pub async fn send_rtp_packet( + &self, + packet: discortp::rtp::MutableRtpPacket<'_>, + ) -> Result<(), VoiceUdpError> { + let mut buffer = self.encrypt_rtp_packet_payload(&packet).await?; let new_packet = discortp::rtp::MutableRtpPacket::new(&mut buffer).unwrap(); self.send_encrypted_rtp_packet(new_packet.consume_to_immutable()) - .await; + .await?; + Ok(()) } /// Encrypts an unencrypted rtp packet, returning a copy of the packet's bytes with an /// encrypted payload + /// + /// # Errors + /// If we have not received an encryption key, this returns a [VoiceUdpError::NoKey] error. pub async fn encrypt_rtp_packet_payload( &self, packet: &discortp::rtp::MutableRtpPacket<'_>, - ) -> Vec { + ) -> Result, VoiceUdpError> { let payload = packet.payload(); let session_description_result = self.data.read().await.session_description.clone(); + // We are trying to encrypt, but have not received SessionDescription yet, + // which contains the secret key. if session_description_result.is_none() { - // FIXME: Make this function reutrn a result with a proper error type for these kinds - // of functions - panic!("Trying to encrypt packet but no key provided yet"); + return Err(VoiceUdpError::NoKey); } let session_description = session_description_result.unwrap(); @@ -96,8 +130,11 @@ impl UdpHandle { let encryption_result = encryptor.encrypt(nonce, payload); if encryption_result.is_err() { - // FIXME: See above fixme - panic!("Encryption error"); + // Safety: If encryption errors here, it's chorus' fault, and it makes no sense to + // return the error to the user. + // + // This is not an error the user should account for, which is why we throw it here. + panic!("{}", VoiceUdpError::FailedEncryption); } let mut encrypted_payload = encryption_result.unwrap(); @@ -113,15 +150,28 @@ impl UdpHandle { new_buffer.append(&mut rtp_header); new_buffer.append(&mut encrypted_payload); - new_buffer + Ok(new_buffer) } /// Sends an (already encrypted) rtp packet to the connection. - pub async fn send_encrypted_rtp_packet(&self, packet: discortp::rtp::RtpPacket<'_>) { + /// + /// # Errors + /// If the Udp socket is broken, this returns a [VoiceUdpError::BrokenSocket] error. + pub async fn send_encrypted_rtp_packet( + &self, + packet: discortp::rtp::RtpPacket<'_>, + ) -> Result<(), VoiceUdpError> { let raw_bytes = packet.packet(); - self.socket.send(raw_bytes).await.unwrap(); + let send_res = self.socket.send(raw_bytes).await; + if let Err(e) = send_res { + return Err(VoiceUdpError::BrokenSocket { + error: format!("{:?}", e), + }); + } debug!("VUDP: Sent rtp packet!"); + + Ok(()) } } diff --git a/src/voice/udp/handler.rs b/src/voice/udp/handler.rs index af9f4c3..a7b05b2 100644 --- a/src/voice/udp/handler.rs +++ b/src/voice/udp/handler.rs @@ -12,13 +12,14 @@ use discortp::discord::{ use discortp::rtcp::report::ReceiverReport; use discortp::rtcp::report::SenderReport; use discortp::{demux::demux, Packet}; -use tokio::{ - net::UdpSocket, - sync::{Mutex, RwLock}, -}; +use tokio::sync::{Mutex, RwLock}; + +use super::UdpBackend; +use super::UdpSocket; -use crate::voice::crypto::get_xsalsa20_poly1305_nonce; use super::RTP_HEADER_SIZE; +use crate::errors::VoiceUdpError; +use crate::voice::crypto::get_xsalsa20_poly1305_nonce; use crate::voice::voice_data::VoiceData; use super::{events::VoiceUDPEvents, UdpHandle}; @@ -41,11 +42,8 @@ impl UdpHandler { data_reference: Arc>, url: SocketAddr, ssrc: u32, - ) -> UdpHandle { - // Bind with a port number of 0, so the os assigns this listener a port - let udp_socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); - - udp_socket.connect(url).await.unwrap(); + ) -> Result { + let udp_socket = UdpBackend::connect(url).await?; // First perform ip discovery let ip_discovery = IpDiscovery { @@ -57,14 +55,15 @@ impl UdpHandler { payload: Vec::new(), }; + // Minimum size with an empty Address value, + 64 bytes for the actual address size let size = IpDiscoveryPacket::minimum_packet_size() + 64; let mut buf: Vec = vec![0; size]; - // TODO: Make this not panic everything - // Actually, if this panics, something is very, very wrong + // Safety: expect is justified here, since this is an error which should never happen. + // If this errors, the code at fault is the buffer size calculation. let mut ip_discovery_packet = - MutableIpDiscoveryPacket::new(&mut buf).expect("Mangled ip discovery packet"); + MutableIpDiscoveryPacket::new(&mut buf).expect("Mangled ip discovery packet creation buffer, something is very wrong. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new"); ip_discovery_packet.populate(&ip_discovery); @@ -72,20 +71,34 @@ impl UdpHandler { info!("VUDP: Sending Ip Discovery {:?}", &data); - udp_socket.send(data).await.unwrap(); + let send_res = udp_socket.send(data).await; + if let Err(e) = send_res { + return Err(VoiceUdpError::BrokenSocket { + error: format!("{:?}", e), + }); + } info!("VUDP: Sent packet discovery request"); // Handle the ip discovery response - let receieved_size = udp_socket.recv(&mut buf).await.unwrap(); + let received_size_or_err = udp_socket.recv(&mut buf).await; + + if let Err(e) = received_size_or_err { + return Err(VoiceUdpError::BrokenSocket { + error: format!("{:?}", e), + }); + } + + let received_size = received_size_or_err.unwrap(); + info!( "VUDP: Receiving messsage: {:?} - (expected {} vs real {})", buf.clone(), size, - receieved_size + received_size ); - let receieved_ip_discovery = IpDiscoveryPacket::new(&buf).unwrap(); + let receieved_ip_discovery = IpDiscoveryPacket::new(&buf).expect("Could not make ipdiscovery packet from received data, something is very wrong. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new"); info!( "VUDP: Received ip discovery!!! {:?}", @@ -121,11 +134,11 @@ impl UdpHandler { handler.listen_task().await; }); - UdpHandle { + Ok(UdpHandle { events: shared_events, socket, data: data_reference, - } + }) } /// The main listen task; diff --git a/src/voice/udp/mod.rs b/src/voice/udp/mod.rs index 3d0b276..37f021c 100644 --- a/src/voice/udp/mod.rs +++ b/src/voice/udp/mod.rs @@ -4,9 +4,11 @@ /// This always adds up to 12 bytes const RTP_HEADER_SIZE: u8 = 12; -pub mod handle; +pub mod backends; pub mod events; +pub mod handle; pub mod handler; +pub use backends::*; pub use handle::*; pub use handler::*; From 65213bb0fb6a7080a80601ce5502f574b5d859ae Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 29 Dec 2023 12:54:46 +0100 Subject: [PATCH 57/72] fix: its the same --- src/voice/udp/handle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs index e597f11..7b2cca9 100644 --- a/src/voice/udp/handle.rs +++ b/src/voice/udp/handle.rs @@ -46,7 +46,7 @@ impl UdpHandle { payload: Vec, ) -> Result<(), VoiceUdpError> { let voice_ready_data_result = self.data.read().await.ready_data.clone(); - if let None = voice_ready_data_result { + if voice_ready_data_result.is_none() { return Err(VoiceUdpError::NoData); } From 2b729dc8fd6a8012d1a520d4e52642edc2887c42 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 30 Dec 2023 11:42:44 +0100 Subject: [PATCH 58/72] chore: clarify UDP on WASM --- Cargo.toml | 2 +- src/lib.rs | 2 +- src/voice/udp/backends/mod.rs | 19 ++++++------------- src/voice/udp/backends/wasm.rs | 13 ------------- 4 files changed, 8 insertions(+), 28 deletions(-) delete mode 100644 src/voice/udp/backends/wasm.rs diff --git a/Cargo.toml b/Cargo.toml index ce87394..ae57d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ default = ["client", "rt-multi-thread"] backend = ["dep:poem", "dep:sqlx"] rt-multi-thread = ["tokio/rt-multi-thread"] rt = ["tokio/rt"] -client = ["voice"] +client = [] voice = ["dep:discortp", "dep:crypto_secretbox"] [dependencies] diff --git a/src/lib.rs b/src/lib.rs index 0bddc3a..1fd125c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,7 @@ pub mod instance; #[cfg(feature = "client")] pub mod ratelimiter; pub mod types; -#[cfg(feature = "client")] +#[cfg(all(feature = "client", feature = "voice"))] pub mod voice; #[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/voice/udp/backends/mod.rs b/src/voice/udp/backends/mod.rs index 521d085..bbf8d9a 100644 --- a/src/voice/udp/backends/mod.rs +++ b/src/voice/udp/backends/mod.rs @@ -1,19 +1,12 @@ -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] pub mod tokio; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] pub use tokio::*; -#[cfg(all(target_arch = "wasm32", feature = "client"))] -pub mod wasm; -#[cfg(all(target_arch = "wasm32", feature = "client"))] -pub use wasm::*; - -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] pub type UdpSocket = tokio::TokioSocket; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] pub type UdpBackend = tokio::TokioBackend; -#[cfg(all(target_arch = "wasm32", feature = "client"))] -pub type UdpSocket = wasm::WasmSocket; -#[cfg(all(target_arch = "wasm32", feature = "client"))] -pub type UdpBackend = wasm::WasmBackend; +#[cfg(target_arch = "wasm32")] +compile_error!("UDP Voice support is not (and will likely never be) supported for WASM. This is because UDP cannot be used in the browser. We are however looking into Webrtc for WASM voice support."); diff --git a/src/voice/udp/backends/wasm.rs b/src/voice/udp/backends/wasm.rs deleted file mode 100644 index 502987b..0000000 --- a/src/voice/udp/backends/wasm.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::net::SocketAddr; - -// TODO: Add wasm websockets -compile_error!("Udp voice support is not implemented yet for wasm."); - -#[derive(Debug, Clone)] -pub struct WasmBackend; - -pub type WasmSocket; - -impl WasmBackend { - pub async fn connect(url: SocketAddr) -> Result {} -} From cdba76bcf977c95eee7b9d21d1fab4e056ef0730 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Sat, 30 Dec 2023 13:17:12 +0100 Subject: [PATCH 59/72] api: split voice gateway and udp features, test for voice gateway in WASM --- .github/workflows/build_and_test.yml | 4 ++-- Cargo.toml | 4 +++- README.md | 2 +- src/lib.rs | 2 +- src/voice/gateway/backends/mod.rs | 20 ++++++++++---------- src/voice/mod.rs | 7 ++++++- src/voice/udp/backends/mod.rs | 10 +++++----- src/voice/voice_data.rs | 4 +++- 8 files changed, 31 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 0cf6fd9..3732402 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -100,7 +100,7 @@ jobs: rustup target add wasm32-unknown-unknown curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force - GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" + GECKODRIVER=$(which geckodriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt, voice_gateway" wasm-chrome: runs-on: macos-latest steps: @@ -128,4 +128,4 @@ jobs: rustup target add wasm32-unknown-unknown curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash cargo binstall --no-confirm wasm-bindgen-cli --version "0.2.88" --force - CHROMEDRIVER=$(which chromedriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt" \ No newline at end of file + CHROMEDRIVER=$(which chromedriver) cargo test --target wasm32-unknown-unknown --no-default-features --features="client, rt, voice_gateway" diff --git a/Cargo.toml b/Cargo.toml index ae57d07..0c88387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,9 @@ backend = ["dep:poem", "dep:sqlx"] rt-multi-thread = ["tokio/rt-multi-thread"] rt = ["tokio/rt"] client = [] -voice = ["dep:discortp", "dep:crypto_secretbox"] +voice = ["voice_udp", "voice_gateway"] +voice_udp = ["dep:discortp", "dep:crypto_secretbox"] +voice_gateway = [] [dependencies] tokio = { version = "1.34.0", features = ["macros", "sync"] } diff --git a/README.md b/README.md index c02da77..af2ab6e 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,7 @@ like "proxy connection checking" are already disabled on this version, which oth ### wasm To test for wasm, you will need to `cargo install wasm-pack`. You can then run -`wasm-pack test -- --headless -- --target wasm32-unknown-unknown --features="rt, client" --no-default-features` +`wasm-pack test -- --headless -- --target wasm32-unknown-unknown --features="rt, client, voice_gateway" --no-default-features` to run the tests for wasm. ## Versioning diff --git a/src/lib.rs b/src/lib.rs index 1fd125c..abe8101 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,7 @@ pub mod instance; #[cfg(feature = "client")] pub mod ratelimiter; pub mod types; -#[cfg(all(feature = "client", feature = "voice"))] +#[cfg(all(feature = "client", any(feature = "voice_udp", feature = "voice_gateway")))] pub mod voice; #[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/voice/gateway/backends/mod.rs b/src/voice/gateway/backends/mod.rs index edb5dc9..dfd00b5 100644 --- a/src/voice/gateway/backends/mod.rs +++ b/src/voice/gateway/backends/mod.rs @@ -1,23 +1,23 @@ -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_gateway"))] pub mod tungstenite; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_gateway"))] pub use tungstenite::*; -#[cfg(all(target_arch = "wasm32", feature = "client"))] +#[cfg(all(target_arch = "wasm32", feature = "voice_gateway"))] pub mod wasm; -#[cfg(all(target_arch = "wasm32", feature = "client"))] +#[cfg(all(target_arch = "wasm32", feature = "voice_gateway"))] pub use wasm::*; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_gateway"))] pub type Sink = tungstenite::TungsteniteSink; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_gateway"))] pub type Stream = tungstenite::TungsteniteStream; -#[cfg(all(not(target_arch = "wasm32"), feature = "client"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_gateway"))] pub type WebSocketBackend = tungstenite::TungsteniteBackend; -#[cfg(all(target_arch = "wasm32", feature = "client"))] +#[cfg(all(target_arch = "wasm32", feature = "voice_gateway"))] pub type Sink = wasm::WasmSink; -#[cfg(all(target_arch = "wasm32", feature = "client"))] +#[cfg(all(target_arch = "wasm32", feature = "voice_gateway"))] pub type Stream = wasm::WasmStream; -#[cfg(all(target_arch = "wasm32", feature = "client"))] +#[cfg(all(target_arch = "wasm32", feature = "voice_gateway"))] pub type WebSocketBackend = wasm::WasmBackend; diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 621d3c0..660f806 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -1,10 +1,15 @@ //! Module for all voice functionality within chorus. -mod crypto; +#[cfg(feature = "voice_gateway")] pub mod gateway; +mod crypto; +#[cfg(all(feature = "voice_udp", feature = "voice_gateway"))] pub mod handler; +#[cfg(feature = "voice_udp")] pub mod udp; +#[cfg(feature = "voice_udp")] pub mod voice_data; // Pub use this so users can interact with packet types if they want +#[cfg(feature = "voice_udp")] pub use discortp; diff --git a/src/voice/udp/backends/mod.rs b/src/voice/udp/backends/mod.rs index bbf8d9a..4059fbe 100644 --- a/src/voice/udp/backends/mod.rs +++ b/src/voice/udp/backends/mod.rs @@ -1,12 +1,12 @@ -#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_udp"))] pub mod tokio; -#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_udp"))] pub use tokio::*; -#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_udp"))] pub type UdpSocket = tokio::TokioSocket; -#[cfg(all(not(target_arch = "wasm32"), feature = "voice"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "voice_udp"))] pub type UdpBackend = tokio::TokioBackend; -#[cfg(target_arch = "wasm32")] +#[cfg(all(target_arch = "wasm32", feature = "voice_udp"))] compile_error!("UDP Voice support is not (and will likely never be) supported for WASM. This is because UDP cannot be used in the browser. We are however looking into Webrtc for WASM voice support."); diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs index 5d37d9c..064ebda 100644 --- a/src/voice/voice_data.rs +++ b/src/voice/voice_data.rs @@ -3,7 +3,9 @@ use discortp::discord::IpDiscovery; use crate::types::{SessionDescription, Snowflake, VoiceReady, VoiceServerUpdate}; #[derive(Debug, Default)] -/// Saves data shared between parts of the voice architecture +/// Saves data shared between parts of the voice architecture; +/// +/// Struct used to give the Udp connection data received from the gateway. pub struct VoiceData { pub server_data: Option, pub ready_data: Option, From 03fd1a67873283279db6a75335c8d31c518cca4f Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:45:56 +0100 Subject: [PATCH 60/72] feat: new encryption modes, minor code quality --- Cargo.toml | 1 + src/errors.rs | 2 + src/lib.rs | 5 +- .../events/voice_gateway/client_connect.rs | 4 +- .../events/voice_gateway/ssrc_definition.rs | 10 ++ src/voice/crypto.rs | 35 +++++- src/voice/mod.rs | 2 +- src/voice/udp/handle.rs | 53 +++++++- src/voice/udp/handler.rs | 115 +++++++++++++----- src/voice/voice_data.rs | 3 + 10 files changed, 191 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c88387..6f55a7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ tokio-tungstenite = { version = "0.20.1", features = [ ] } native-tls = "0.2.11" hostname = "0.3.1" +getrandom = { version = "0.2.11" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.11", features = ["js"] } diff --git a/src/errors.rs b/src/errors.rs index bf3727c..e129cf6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -142,6 +142,8 @@ custom_error! { // Encryption errors NoKey = "Tried to encrypt / decrypt rtp data, but no key has been received yet", FailedEncryption = "Tried to encrypt rtp data, but failed. Most likely this is an issue chorus' nonce generation. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new", + FailedDecryption = "Tried to decrypt rtp data, but failed. Most likely this is an issue chorus' nonce generation. Please open an issue on the chorus github: https://github.com/polyphony-chat/chorus/issues/new", + FailedNonceGeneration{error: String} = "Tried to generate nonce, but failed due to error: {error}.", // Errors when initiating a socket connection CannotBind{error: String} = "Cannot bind socket due to a udp error: {error}", diff --git a/src/lib.rs b/src/lib.rs index abe8101..f1a3591 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,10 @@ pub mod instance; #[cfg(feature = "client")] pub mod ratelimiter; pub mod types; -#[cfg(all(feature = "client", any(feature = "voice_udp", feature = "voice_gateway")))] +#[cfg(all( + feature = "client", + any(feature = "voice_udp", feature = "voice_gateway") +))] pub mod voice; #[derive(Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/types/events/voice_gateway/client_connect.rs b/src/types/events/voice_gateway/client_connect.rs index d367ff3..5b54797 100644 --- a/src/types/events/voice_gateway/client_connect.rs +++ b/src/types/events/voice_gateway/client_connect.rs @@ -12,7 +12,9 @@ use serde::{Deserialize, Serialize}; pub struct VoiceClientConnectFlags { pub user_id: Snowflake, // Likely some sort of bitflags - pub flags: u8, + // + // Not always sent, sometimes null? + pub flags: Option, } impl WebSocketEvent for VoiceClientConnectFlags {} diff --git a/src/types/events/voice_gateway/ssrc_definition.rs b/src/types/events/voice_gateway/ssrc_definition.rs index 738f483..e19e563 100644 --- a/src/types/events/voice_gateway/ssrc_definition.rs +++ b/src/types/events/voice_gateway/ssrc_definition.rs @@ -3,6 +3,10 @@ use serde::{Deserialize, Serialize}; /// Defines an event which provides ssrcs for voice and video for a user id. /// +/// This event is sent when we begin to speak. +/// +/// It must be sent before sending audio, or else clients will not be able to play the stream. +/// /// This event is sent via opcode 12. /// /// Examples of the event: @@ -28,12 +32,18 @@ pub struct SsrcDefinition { /// Is always sent and received, though is 0 if describing only the video ssrc. #[serde(default)] pub audio_ssrc: usize, + // Not sure what this is + // It is usually 0 + #[serde(default)] + pub rtx_ssrc: usize, /// The user id these ssrcs apply to. /// /// Is never sent by the user and is filled in by the server #[serde(skip_serializing)] pub user_id: Option, // TODO: Add video streams + #[serde(default)] + pub streams: Vec, } impl WebSocketEvent for SsrcDefinition {} diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 7bcc056..7657383 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -2,15 +2,46 @@ //! //! All functions in this module return a 24 byte long [Vec]. -/// Gets an xsalsa20poly1305 nonce from an rtppacket. +use crypto_secretbox::cipher::typenum::Len; + +/// Gets an xsalsa20_poly1305 nonce from an rtppacket. +/// +/// See pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { let mut rtp_header = Vec::with_capacity(24); rtp_header.append(&mut packet[0..12].to_vec()); // The header is only 12 bytes, but the nonce has to be 24 - for _i in 0..12 { + while rtp_header.len() < 24 { rtp_header.push(0); } rtp_header } + +/// Gets an xsalsa20_poly1305_suffix nonce from an rtppacket. +/// +/// See +pub(crate) fn get_xsalsa20_poly1305_suffix_nonce(packet: &[u8]) -> Vec { + let mut nonce = Vec::with_capacity(24); + + nonce.append(&mut packet[(packet.len() - 24)..packet.len()].to_vec()); + + nonce +} + +/// Gets an xsalsa20_poly1305_lite nonce from an rtppacket. +/// +/// See +pub(crate) fn get_xsalsa20_poly1305_lite_nonce(packet: &[u8]) -> Vec { + let mut nonce = Vec::with_capacity(24); + + nonce.append(&mut packet[(packet.len() - 4)..packet.len()].to_vec()); + + // The suffix is only 4 bytes, but the nonce has to be 24 + while nonce.len() < 24 { + nonce.push(0); + } + + nonce +} diff --git a/src/voice/mod.rs b/src/voice/mod.rs index 660f806..8731fd1 100644 --- a/src/voice/mod.rs +++ b/src/voice/mod.rs @@ -1,8 +1,8 @@ //! Module for all voice functionality within chorus. +mod crypto; #[cfg(feature = "voice_gateway")] pub mod gateway; -mod crypto; #[cfg(all(feature = "voice_udp", feature = "voice_gateway"))] pub mod handler; #[cfg(feature = "voice_udp")] diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs index 7b2cca9..87550d3 100644 --- a/src/voice/udp/handle.rs +++ b/src/voice/udp/handle.rs @@ -5,6 +5,7 @@ use crypto_secretbox::{ }; use discortp::Packet; +use getrandom::getrandom; use log::*; use tokio::{sync::Mutex, sync::RwLock}; @@ -13,7 +14,11 @@ use super::UdpSocket; use crate::{ errors::VoiceUdpError, - voice::{crypto, voice_data::VoiceData}, + types::VoiceEncryptionMode, + voice::{ + crypto::{self, get_xsalsa20_poly1305_nonce}, + voice_data::VoiceData, + }, }; use super::{events::VoiceUDPEvents, RTP_HEADER_SIZE}; @@ -104,6 +109,8 @@ impl UdpHandle { /// /// # Errors /// If we have not received an encryption key, this returns a [VoiceUdpError::NoKey] error. + /// + /// When using voice encryption modes which require special nonce generation, and said generation fails, this returns a [VoiceUdpError::FailedNonceGeneration] error. pub async fn encrypt_rtp_packet_payload( &self, packet: &discortp::rtp::MutableRtpPacket<'_>, @@ -120,7 +127,42 @@ impl UdpHandle { let session_description = session_description_result.unwrap(); - let nonce_bytes = crypto::get_xsalsa20_poly1305_nonce(packet.packet()); + let mut nonce_bytes = match session_description.encryption_mode { + VoiceEncryptionMode::Xsalsa20Poly1305 => get_xsalsa20_poly1305_nonce(packet.packet()), + VoiceEncryptionMode::Xsalsa20Poly1305Suffix => { + // Generate 24 random bytes + let mut random_destinaton: Vec = vec![0; 24]; + let random_result = getrandom(&mut random_destinaton); + if let Err(e) = random_result { + return Err(VoiceUdpError::FailedNonceGeneration { + error: format!("{:?}", e), + }); + } + random_destinaton + } + VoiceEncryptionMode::Xsalsa20Poly1305Lite => { + // "Incremental 4 bytes (32bit) int value" + let mut data_lock = self.data.write().await; + let nonce = data_lock + .last_udp_encryption_nonce + .unwrap_or_default() + .wrapping_add(1); + data_lock.last_udp_encryption_nonce = Some(nonce); + drop(data_lock); + // TODO: Is le correct? This is not documented anywhere + let mut bytes = nonce.to_le_bytes().to_vec(); + // This is 4 bytes, it has to be 24, so we need to append 20 + while bytes.len() < 24 { + bytes.push(0); + } + bytes + } + _ => { + // TODO: Implement aead_aes256_gcm + todo!("This voice encryption mode is not yet implemented."); + } + }; + let nonce = GenericArray::from_slice(&nonce_bytes); let key = GenericArray::from_slice(&session_description.secret_key); @@ -139,6 +181,13 @@ impl UdpHandle { let mut encrypted_payload = encryption_result.unwrap(); + // Append the nonce bytes, if needed + // All other encryption modes have an explicit nonce, where as Xsalsa20Poly1305 + // has the nonce as the rtp header. + if session_description.encryption_mode != VoiceEncryptionMode::Xsalsa20Poly1305 { + encrypted_payload.append(&mut nonce_bytes); + } + // We need to allocate a new buffer, since the old one is too small for our new encrypted // data let buffer_size = encrypted_payload.len() + RTP_HEADER_SIZE as usize; diff --git a/src/voice/udp/handler.rs b/src/voice/udp/handler.rs index a7b05b2..67f4f3e 100644 --- a/src/voice/udp/handler.rs +++ b/src/voice/udp/handler.rs @@ -19,7 +19,10 @@ use super::UdpSocket; use super::RTP_HEADER_SIZE; use crate::errors::VoiceUdpError; +use crate::types::VoiceEncryptionMode; +use crate::voice::crypto::get_xsalsa20_poly1305_lite_nonce; use crate::voice::crypto::get_xsalsa20_poly1305_nonce; +use crate::voice::crypto::get_xsalsa20_poly1305_suffix_nonce; use crate::voice::voice_data::VoiceData; use super::{events::VoiceUDPEvents, UdpHandle}; @@ -167,41 +170,24 @@ impl UdpHandler { match parsed { Demuxed::Rtp(rtp) => { - let ciphertext = buf[(RTP_HEADER_SIZE as usize)..buf.len()].to_vec(); - trace!("VUDP: Parsed packet as rtp!"); + trace!("VUDP: Parsed packet as rtp! {:?}", buf); - let session_description_result = self.data.read().await.session_description.clone(); + let decryption_result = self.decrypt_rtp_packet_payload(&rtp).await; - if session_description_result.is_none() { - warn!("VUDP: Received encyrpted voice data, but no encryption key, CANNOT DECRYPT!"); - return; - } - - let session_description = session_description_result.unwrap(); - - let nonce_bytes = match session_description.encryption_mode { - crate::types::VoiceEncryptionMode::Xsalsa20Poly1305 => { - get_xsalsa20_poly1305_nonce(rtp.packet()) + if let Err(err) = decryption_result { + match err { + VoiceUdpError::NoKey => { + warn!("VUDP: Received encyrpted voice data, but no encryption key, CANNOT DECRYPT!"); + return; + } + VoiceUdpError::FailedDecryption => { + warn!("VUDP: Failed to decrypt voice data!"); + return; + } + _ => { + unreachable!(); + } } - _ => { - unimplemented!(); - } - }; - - let nonce = GenericArray::from_slice(&nonce_bytes); - - let key = GenericArray::from_slice(&session_description.secret_key); - - let decryptor = XSalsa20Poly1305::new(key); - - let decryption_result = decryptor.decrypt(nonce, ciphertext.as_ref()); - - if let Err(decryption_error) = decryption_result { - warn!( - "VUDP: Failed to decypt voice data! ({:?})", - decryption_error - ); - return; } let decrypted = decryption_result.unwrap(); @@ -273,4 +259,69 @@ impl UdpHandler { } } } + + /// Decrypts an encrypted rtp packet, returning a decrypted copy of the packet's payload + /// bytes. + /// + /// # Errors + /// If we have not received an encryption key, this returns a [VoiceUdpError::NoKey] error. + /// + /// If the decryption fails, this returns a [VoiceUdpError::FailedDecryption]. + pub async fn decrypt_rtp_packet_payload( + &self, + rtp: &discortp::rtp::RtpPacket<'_>, + ) -> Result, VoiceUdpError> { + let packet_bytes = rtp.packet(); + + let mut ciphertext: Vec = + packet_bytes[(RTP_HEADER_SIZE as usize)..packet_bytes.len()].to_vec(); + + let session_description_result = self.data.read().await.session_description.clone(); + + // We are trying to decrypt, but have not received SessionDescription yet, + // which contains the secret key + if session_description_result.is_none() { + return Err(VoiceUdpError::NoKey); + } + + let session_description = session_description_result.unwrap(); + + let nonce_bytes = match session_description.encryption_mode { + VoiceEncryptionMode::Xsalsa20Poly1305 => get_xsalsa20_poly1305_nonce(packet_bytes), + VoiceEncryptionMode::Xsalsa20Poly1305Suffix => { + // Remove the suffix from the ciphertext + ciphertext = ciphertext[0..ciphertext.len() - 24].to_vec(); + get_xsalsa20_poly1305_suffix_nonce(packet_bytes) + } + // Note: Rtpsize is documented by userdoccers to be the same, yet decryption + // doesn't work. + // + // I have no idea how Rtpsize works. + VoiceEncryptionMode::Xsalsa20Poly1305Lite => { + // Remove the suffix from the ciphertext + ciphertext = ciphertext[0..ciphertext.len() - 4].to_vec(); + get_xsalsa20_poly1305_lite_nonce(packet_bytes) + } + _ => { + // TODO: Implement aead_aes256_gcm + todo!("This voice encryption mode is not yet implemented."); + } + }; + + let nonce = GenericArray::from_slice(&nonce_bytes); + + let key = GenericArray::from_slice(&session_description.secret_key); + + let decryptor = XSalsa20Poly1305::new(key); + + let decryption_result = decryptor.decrypt(nonce, ciphertext.as_ref()); + + // Note: this may seem like we are throwing away valuable error handling data, + // but the decryption error provides no extra info. + if decryption_result.is_err() { + return Err(VoiceUdpError::FailedDecryption); + } + + Ok(decryption_result.unwrap()) + } } diff --git a/src/voice/voice_data.rs b/src/voice/voice_data.rs index 064ebda..5252ac5 100644 --- a/src/voice/voice_data.rs +++ b/src/voice/voice_data.rs @@ -15,4 +15,7 @@ pub struct VoiceData { /// The last sequence number we used, has to be incremeted by one every time we send a message pub last_sequence_number: u16, pub ip_discovery: Option, + + /// The last udp encryption nonce, if we are using an encryption mode with incremental nonces. + pub last_udp_encryption_nonce: Option, } From 2b4d07d0206330f68f2bebedccbb89e18d1aeecc Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:57:51 +0100 Subject: [PATCH 61/72] docs: document voice encryption modes --- src/types/events/voice_gateway/mod.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/types/events/voice_gateway/mod.rs b/src/types/events/voice_gateway/mod.rs index e23e38d..23571f5 100644 --- a/src/types/events/voice_gateway/mod.rs +++ b/src/types/events/voice_gateway/mod.rs @@ -57,7 +57,11 @@ pub struct VoiceGatewayReceivePayload<'a> { impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} -/// The modes of encryption available in webrtc connections; +/// The modes of encryption available in voice udp connections; +/// +/// Not all encryption modes are implemented; it is generally recommended +/// to use either [[VoiceEncryptionMode::Xsalsa20Poly1305]] or +/// [[VoiceEncryptionMode::Xsalsa20Poly1305Suffix]] /// /// See and #[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] @@ -65,13 +69,26 @@ impl<'a> WebSocketEvent for VoiceGatewayReceivePayload<'a> {} pub enum VoiceEncryptionMode { #[default] // Officially Documented + /// Use XSalsa20Poly1305 encryption, using the rtp header as a nonce. + /// + /// Fully implemented Xsalsa20Poly1305, + /// Use XSalsa20Poly1305 encryption, using a random 24 byte suffix as a nonce. + /// + /// Fully implemented Xsalsa20Poly1305Suffix, + /// Use XSalsa20Poly1305 encryption, using a 4 byte incremental value as a nonce. + /// + /// Fully implemented Xsalsa20Poly1305Lite, // Officially Undocumented + /// Not implemented yet, we have no idea what the rtpsize nonces are. Xsalsa20Poly1305LiteRtpsize, + /// Not implemented yet AeadAes256Gcm, + /// Not implemented yet AeadAes256GcmRtpsize, + /// Not implemented yet, we have no idea what the rtpsize nonces are. AeadXchacha20Poly1305Rtpsize, } From e9506597856d1539dd7fa973835f30102533385a Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:00:24 +0100 Subject: [PATCH 62/72] chore: unused imports --- src/voice/crypto.rs | 2 -- src/voice/udp/handle.rs | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 7657383..995a26b 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -2,8 +2,6 @@ //! //! All functions in this module return a 24 byte long [Vec]. -use crypto_secretbox::cipher::typenum::Len; - /// Gets an xsalsa20_poly1305 nonce from an rtppacket. /// /// See diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs index 87550d3..1fab085 100644 --- a/src/voice/udp/handle.rs +++ b/src/voice/udp/handle.rs @@ -15,10 +15,7 @@ use super::UdpSocket; use crate::{ errors::VoiceUdpError, types::VoiceEncryptionMode, - voice::{ - crypto::{self, get_xsalsa20_poly1305_nonce}, - voice_data::VoiceData, - }, + voice::{crypto::get_xsalsa20_poly1305_nonce, voice_data::VoiceData}, }; use super::{events::VoiceUDPEvents, RTP_HEADER_SIZE}; From 7a41667ad4ceea3393e5ac8c2e69000acf7a0c48 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:40:09 +0100 Subject: [PATCH 63/72] chore: update getrandom version to match wasm version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7e43efb..169bd02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ tokio-tungstenite = { version = "0.20.1", features = [ ] } native-tls = "0.2.11" hostname = "0.3.1" -getrandom = { version = "0.2.11" } +getrandom = { version = "0.2.12" } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.12", features = ["js"] } From 7a3a7dcd8e7331f48ebe20a245d479b51c022b22 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:40:40 +0100 Subject: [PATCH 64/72] chore: update on packet size FIXME --- src/voice/udp/handler.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/voice/udp/handler.rs b/src/voice/udp/handler.rs index 67f4f3e..3b26e28 100644 --- a/src/voice/udp/handler.rs +++ b/src/voice/udp/handler.rs @@ -151,6 +151,12 @@ impl UdpHandler { loop { // FIXME: is there a max size for these packets? // Allocating 512 bytes seems a bit extreme + // + // Update: see + // > "The RTP standard does not set a maximum size.." + // + // The theorhetical max for this buffer would be 1458 bytes, but that is imo + // unreasonable to allocate for every message. let mut buf: Vec = vec![0; 512]; let result = self.socket.recv(&mut buf).await; From f2aa22329a99e3b0cda9f209ce9dbaae346755a0 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:45:49 +0100 Subject: [PATCH 65/72] drop buf asap --- src/voice/udp/handler.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/voice/udp/handler.rs b/src/voice/udp/handler.rs index 3b26e28..1b9f63f 100644 --- a/src/voice/udp/handler.rs +++ b/src/voice/udp/handler.rs @@ -161,7 +161,9 @@ impl UdpHandler { let result = self.socket.recv(&mut buf).await; if let Ok(size) = result { - self.handle_message(&buf[0..size]).await; + let message_bytes = buf[0..size]; + drop(buf); + self.handle_message(&message_bytes).await; continue; } From c6919d464cdde9d709d19a8b275fc36756b30d49 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:50:44 +0100 Subject: [PATCH 66/72] Okay can't do that actually --- src/voice/udp/handler.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/voice/udp/handler.rs b/src/voice/udp/handler.rs index 1b9f63f..3b26e28 100644 --- a/src/voice/udp/handler.rs +++ b/src/voice/udp/handler.rs @@ -161,9 +161,7 @@ impl UdpHandler { let result = self.socket.recv(&mut buf).await; if let Ok(size) = result { - let message_bytes = buf[0..size]; - drop(buf); - self.handle_message(&message_bytes).await; + self.handle_message(&buf[0..size]).await; continue; } From b9e5ee6d16208c01fa15a9c355bfc11b2bb322bb Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:26:27 +0100 Subject: [PATCH 67/72] tests: add nonce test --- src/voice/crypto.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 995a26b..6ff7d96 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -43,3 +43,50 @@ pub(crate) fn get_xsalsa20_poly1305_lite_nonce(packet: &[u8]) -> Vec { nonce } + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_test::*; +#[cfg(target_arch = "wasm32")] +wasm_bindgen_test_configure!(run_in_browser); + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] +#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +// Asserts all functions that retrieve a nonce from packet bytes +async fn test_packet_nonce_derives() { + let test_packet_bytes = vec![ + 144, 120, 98, 5, 71, 174, 52, 64, 0, 4, 85, 36, 178, 8, 37, 146, 35, 154, 141, 36, 125, 15, + 65, 179, 227, 108, 165, 56, 68, 68, 3, 62, 87, 233, 7, 81, 147, 93, 22, 95, 115, 202, 48, + 66, 190, 229, 69, 146, 66, 108, 60, 114, 2, 228, 111, 40, 108, 5, 68, 226, 76, 240, 20, + 231, 210, 214, 123, 175, 188, 161, 10, 125, 13, 196, 114, 248, 50, 84, 103, 139, 86, 223, + 82, 173, 8, 209, 78, 188, 169, 151, 157, 42, 189, 153, 228, 105, 199, 19, 185, 16, 33, 133, + 113, 253, 145, 36, 106, 14, 222, 128, 226, 239, 10, 39, 72, 113, 33, 113, + ]; + + let nonce_1 = get_xsalsa20_poly1305_nonce(&test_packet_bytes); + let nonce_1_expected = vec![ + 144, 120, 98, 5, 71, 174, 52, 64, 0, 4, 85, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + let nonce_2 = get_xsalsa20_poly1305_suffix_nonce(&test_packet_bytes); + let nonce_2_expected = vec![ + 228, 105, 199, 19, 185, 16, 33, 133, 113, 253, 145, 36, 106, 14, 222, 128, 226, 239, 10, + 39, 72, 113, 33, 113, + ]; + + let nonce_3 = get_xsalsa20_poly1305_lite_nonce(&test_packet_bytes); + let nonce_3_expected = vec![ + 72, 113, 33, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + println!("nonce 1: {:?}", nonce_1); + println!("nonce 2: {:?}", nonce_2); + println!("nonce 3: {:?}", nonce_3); + + assert_eq!(nonce_1.len(), 24); + assert_eq!(nonce_2.len(), 24); + assert_eq!(nonce_3.len(), 24); + + assert_eq!(nonce_1, nonce_1_expected); + assert_eq!(nonce_2, nonce_2_expected); + assert_eq!(nonce_3, nonce_3_expected); +} From 0d8fc2410c44df05ab537364ef6b7122aca3ef9f Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:41:10 +0100 Subject: [PATCH 68/72] normal tests work? --- src/voice/crypto.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 6ff7d96..581d938 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -44,15 +44,9 @@ pub(crate) fn get_xsalsa20_poly1305_lite_nonce(packet: &[u8]) -> Vec { nonce } -#[cfg(target_arch = "wasm32")] -use wasm_bindgen_test::*; -#[cfg(target_arch = "wasm32")] -wasm_bindgen_test_configure!(run_in_browser); - -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] -#[cfg_attr(not(target_arch = "wasm32"), tokio::test)] +#[test] // Asserts all functions that retrieve a nonce from packet bytes -async fn test_packet_nonce_derives() { +fn test_packet_nonce_derives() { let test_packet_bytes = vec![ 144, 120, 98, 5, 71, 174, 52, 64, 0, 4, 85, 36, 178, 8, 37, 146, 35, 154, 141, 36, 125, 15, 65, 179, 227, 108, 165, 56, 68, 68, 3, 62, 87, 233, 7, 81, 147, 93, 22, 95, 115, 202, 48, From cadaca90a1a0a9d36fdddfc557954560131b9392 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:24:19 +0100 Subject: [PATCH 69/72] docs: fix doc warning, fix incorrect refrences to 'webrtc' --- src/api/guilds/messages.rs | 2 +- src/errors.rs | 8 ++++---- src/types/events/voice_gateway/identify.rs | 4 ++-- src/types/events/voice_gateway/mod.rs | 4 ++-- src/types/events/voice_gateway/ready.rs | 9 ++++++--- src/types/events/voice_gateway/select_protocol.rs | 4 +++- src/types/events/voice_gateway/speaking.rs | 2 +- src/types/events/voice_gateway/voice_backend_version.rs | 2 +- src/voice/crypto.rs | 8 ++++---- src/voice/gateway/message.rs | 5 ++++- src/voice/udp/handle.rs | 2 +- 11 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/api/guilds/messages.rs b/src/api/guilds/messages.rs index 60fd4e3..246d3cc 100644 --- a/src/api/guilds/messages.rs +++ b/src/api/guilds/messages.rs @@ -9,7 +9,7 @@ impl Guild { /// permission to be present on the current user. /// /// If the guild/channel you are searching is not yet indexed, the endpoint will return a 202 accepted response. - /// In this case, the method will return a [`ChorusError::InvalidResponse`] error. + /// In this case, the method will return a [`ChorusError::InvalidResponse`](crate::errors::ChorusError::InvalidResponse) error. /// /// # Reference: /// See diff --git a/src/errors.rs b/src/errors.rs index e129cf6..41cc97c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -63,7 +63,7 @@ custom_error! { } custom_error! { - /// For errors we receive from the gateway, see https://discord-userdoccers.vercel.app/topics/opcodes-and-status-codes#gateway-close-event-codes; + /// For errors we receive from the gateway, see ; /// /// Supposed to be sent as numbers, though they are sent as string most of the time? /// @@ -102,14 +102,14 @@ custom_error! { /// /// Similar to [GatewayError]. /// - /// See https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice; + /// See ; #[derive(Clone, Default, PartialEq, Eq)] pub VoiceGatewayError // Errors we receive #[default] UnknownOpcode = "You sent an invalid opcode", - FailedToDecodePayload = "You sent an invalid payload in your identifying to the (Webrtc) Gateway", - NotAuthenticated = "You sent a payload before identifying with the (Webrtc) Gateway", + FailedToDecodePayload = "You sent an invalid payload in your identifying to the (Voice) Gateway", + NotAuthenticated = "You sent a payload before identifying with the (Voice) Gateway", AuthenticationFailed = "The token you sent in your identify payload is incorrect", AlreadyAuthenticated = "You sent more than one identify payload", SessionNoLongerValid = "Your session is no longer valid", diff --git a/src/types/events/voice_gateway/identify.rs b/src/types/events/voice_gateway/identify.rs index 4021c28..686834e 100644 --- a/src/types/events/voice_gateway/identify.rs +++ b/src/types/events/voice_gateway/identify.rs @@ -2,9 +2,9 @@ use crate::types::{Snowflake, WebSocketEvent}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] -/// The identify payload for the webrtc stream; +/// The identify payload for the voice gateway connection; /// -/// Contains info to begin a webrtc connection; +/// Contains authentication info and context to authenticate to the voice gateway. /// /// See pub struct VoiceIdentify { diff --git a/src/types/events/voice_gateway/mod.rs b/src/types/events/voice_gateway/mod.rs index 23571f5..a4c344a 100644 --- a/src/types/events/voice_gateway/mod.rs +++ b/src/types/events/voice_gateway/mod.rs @@ -27,7 +27,7 @@ mod ssrc_definition; mod voice_backend_version; #[derive(Debug, Default, Serialize, Clone)] -/// The payload used for sending events to the webrtc gateway. +/// The payload used for sending events to the voice gateway. /// /// Similar to [VoiceGatewayReceivePayload], except we send a [Value] for d whilst we receive a [serde_json::value::RawValue] pub struct VoiceGatewaySendPayload { @@ -41,7 +41,7 @@ pub struct VoiceGatewaySendPayload { impl WebSocketEvent for VoiceGatewaySendPayload {} #[derive(Debug, Deserialize, Clone)] -/// The payload used for receiving events from the webrtc gateway. +/// The payload used for receiving events from the voice gateway. /// /// Note that this is similar to the regular gateway, except we no longer have s or t /// diff --git a/src/types/events/voice_gateway/ready.rs b/src/types/events/voice_gateway/ready.rs index 8b6ef8e..eb3d433 100644 --- a/src/types/events/voice_gateway/ready.rs +++ b/src/types/events/voice_gateway/ready.rs @@ -6,9 +6,12 @@ use serde::{Deserialize, Serialize}; use super::VoiceEncryptionMode; #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] -/// The ready event for the webrtc stream; +/// The voice gateway's ready event; /// -/// Used to give info after the identify event; +/// Gives the user info about the udp connection ip and port, srrc to use, +/// available encryption modes and other data. +/// +/// Sent in response to an Identify event. /// /// See pub struct VoiceReady { @@ -16,7 +19,7 @@ pub struct VoiceReady { pub ssrc: u32, pub ip: Ipv4Addr, pub port: u16, - /// The available encryption modes for the webrtc connection + /// The available encryption modes for the udp connection pub modes: Vec, #[serde(default)] pub experiments: Vec, diff --git a/src/types/events/voice_gateway/select_protocol.rs b/src/types/events/voice_gateway/select_protocol.rs index 38d674a..afdd86a 100644 --- a/src/types/events/voice_gateway/select_protocol.rs +++ b/src/types/events/voice_gateway/select_protocol.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use super::VoiceEncryptionMode; #[derive(Debug, Deserialize, Serialize, Clone, Default)] -/// An event sent by the client to the webrtc server, detailing what protocol, address and encryption to use; +/// An event sent by the client to the voice gateway server, +/// detailing what protocol, address and encryption to use; /// /// See pub struct SelectProtocol { @@ -39,6 +40,7 @@ pub enum VoiceProtocol { /// See pub struct SelectProtocolData { /// Our external ip + // FIXME: This is a string pub address: Vec, /// Our external udp port pub port: u16, diff --git a/src/types/events/voice_gateway/speaking.rs b/src/types/events/voice_gateway/speaking.rs index c31e7e1..95810a0 100644 --- a/src/types/events/voice_gateway/speaking.rs +++ b/src/types/events/voice_gateway/speaking.rs @@ -12,7 +12,7 @@ use crate::types::{Snowflake, WebSocketEvent}; pub struct Speaking { /// Data about the audio we're transmitting. /// - /// See [SpeakingBitFlags] + /// See [SpeakingBitflags] pub speaking: u8, pub ssrc: u32, /// The user id of the speaking user, only sent by the server diff --git a/src/types/events/voice_gateway/voice_backend_version.rs b/src/types/events/voice_gateway/voice_backend_version.rs index 6b788ea..6ee0370 100644 --- a/src/types/events/voice_gateway/voice_backend_version.rs +++ b/src/types/events/voice_gateway/voice_backend_version.rs @@ -2,7 +2,7 @@ use crate::types::WebSocketEvent; use serde::{Deserialize, Serialize}; #[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)] -/// Received from the server to describe the backend version. +/// Received from the voice gateway server to describe the backend version. /// /// See pub struct VoiceBackendVersion { diff --git a/src/voice/crypto.rs b/src/voice/crypto.rs index 581d938..86b1303 100644 --- a/src/voice/crypto.rs +++ b/src/voice/crypto.rs @@ -1,8 +1,8 @@ //! Defines cryptography functions used within the voice implementation. //! -//! All functions in this module return a 24 byte long [Vec]. +//! All functions in this module return a 24 byte long `Vec`. -/// Gets an xsalsa20_poly1305 nonce from an rtppacket. +/// Gets an `xsalsa20_poly1305` nonce from an rtppacket. /// /// See pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { @@ -17,7 +17,7 @@ pub(crate) fn get_xsalsa20_poly1305_nonce(packet: &[u8]) -> Vec { rtp_header } -/// Gets an xsalsa20_poly1305_suffix nonce from an rtppacket. +/// Gets an `xsalsa20_poly1305_suffix` nonce from an rtppacket. /// /// See pub(crate) fn get_xsalsa20_poly1305_suffix_nonce(packet: &[u8]) -> Vec { @@ -28,7 +28,7 @@ pub(crate) fn get_xsalsa20_poly1305_suffix_nonce(packet: &[u8]) -> Vec { nonce } -/// Gets an xsalsa20_poly1305_lite nonce from an rtppacket. +/// Gets an `xsalsa20_poly1305_lite` nonce from an rtppacket. /// /// See pub(crate) fn get_xsalsa20_poly1305_lite_nonce(packet: &[u8]) -> Vec { diff --git a/src/voice/gateway/message.rs b/src/voice/gateway/message.rs index ebda848..623f849 100644 --- a/src/voice/gateway/message.rs +++ b/src/voice/gateway/message.rs @@ -1,6 +1,9 @@ use crate::{errors::VoiceGatewayError, types::VoiceGatewayReceivePayload}; -/// Represents a messsage received from the webrtc socket. This will be either a [GatewayReceivePayload], containing webrtc events, or a [WebrtcError]. +/// Represents a messsage received from the voice websocket connection. +/// +/// This will be either a [VoiceGatewayReceivePayload], containing voice gateway events, or a [VoiceGatewayError]. +/// /// This struct is used internally when handling messages. #[derive(Clone, Debug)] pub struct VoiceGatewayMessage(pub String); diff --git a/src/voice/udp/handle.rs b/src/voice/udp/handle.rs index 1fab085..c45d5c2 100644 --- a/src/voice/udp/handle.rs +++ b/src/voice/udp/handle.rs @@ -33,7 +33,7 @@ pub struct UdpHandle { impl UdpHandle { /// Constructs and sends encoded opus rtp data. /// - /// Automatically makes an [RtpPacket](discorrtp::rtp::RtpPacket), encrypts it and sends it. + /// Automatically makes an [RtpPacket](discortp::rtp::RtpPacket), encrypts it and sends it. /// /// # Errors /// If we do not have VoiceReady data, which contains our ssrc, this returns a From 6f9ed86a4ca1976f3d8e7c22eae2f7c950c6881f Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:27:07 +0100 Subject: [PATCH 70/72] chore: json isn't a doc test --- src/types/events/voice_gateway/ssrc_definition.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/events/voice_gateway/ssrc_definition.rs b/src/types/events/voice_gateway/ssrc_definition.rs index e19e563..5bf12c3 100644 --- a/src/types/events/voice_gateway/ssrc_definition.rs +++ b/src/types/events/voice_gateway/ssrc_definition.rs @@ -12,12 +12,12 @@ use serde::{Deserialize, Serialize}; /// Examples of the event: /// /// When receiving: -/// ``` +/// ```json /// {"op":12,"d":{"video_ssrc":0,"user_id":"463640391196082177","streams":[{"ssrc":26595,"rtx_ssrc":26596,"rid":"100","quality":100,"max_resolution":{"width":1280,"type":"fixed","height":720},"max_framerate":30,"active":false}],"audio_ssrc":26597}}{"op":12,"d":{"video_ssrc":0,"user_id":"463640391196082177","streams":[{"ssrc":26595,"rtx_ssrc":26596,"rid":"100","quality":100,"max_resolution":{"width":1280,"type":"fixed","height":720},"max_framerate":30,"active":false}],"audio_ssrc":26597}} /// ``` /// /// When sending: -/// ``` +/// ```json /// {"op":12,"d":{"audio_ssrc":2307250864,"video_ssrc":0,"rtx_ssrc":0,"streams":[{"type":"video","rid":"100","ssrc":26595,"active":false,"quality":100,"rtx_ssrc":26596,"max_bitrate":2500000,"max_framerate":30,"max_resolution":{"type":"fixed","width":1280,"height":720}}]}} /// ``` #[derive(Debug, Deserialize, Serialize, Default, Clone, PartialEq, Eq)] From f7a2285dd584b17f54be31d4062cd0c78db8151a Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:27:45 +0100 Subject: [PATCH 71/72] tests: better gateway auth test --- tests/gateway.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/gateway.rs b/tests/gateway.rs index 5bf5865..c23408a 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -1,10 +1,12 @@ mod common; use std::sync::{Arc, RwLock}; +use std::time::Duration; +use async_trait::async_trait; use chorus::errors::GatewayError; use chorus::gateway::*; -use chorus::types::{self, ChannelModifySchema, RoleCreateModifySchema, RoleObject}; +use chorus::types::{self, ChannelModifySchema, GatewayReady, RoleCreateModifySchema, RoleObject}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; #[cfg(target_arch = "wasm32")] @@ -20,6 +22,18 @@ async fn test_gateway_establish() { common::teardown(bundle).await } +#[derive(Debug)] +struct GatewayReadyObserver { + channel: tokio::sync::mpsc::Sender<()>, +} + +#[async_trait] +impl Observer for GatewayReadyObserver { + async fn update(&self, _data: &GatewayReady) { + self.channel.send(()).await.unwrap(); + } +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] /// Tests establishing a connection and authenticating @@ -28,10 +42,35 @@ async fn test_gateway_authenticate() { let gateway: GatewayHandle = Gateway::spawn(bundle.urls.wss.clone()).await.unwrap(); + let (ready_send, mut ready_receive) = tokio::sync::mpsc::channel(1); + + let observer = Arc::new(GatewayReadyObserver { + channel: ready_send, + }); + + gateway + .events + .lock() + .await + .session + .ready + .subscribe(observer); + let mut identify = types::GatewayIdentifyPayload::common(); identify.token = bundle.user.token.clone(); gateway.send_identify(identify).await; + + tokio::select! { + // Fail, we timed out waiting for it + () = safina_timer::sleep_for(Duration::from_secs(20)) => { + println!("Timed out waiting for event, failing.."); + assert!(false); + } + // Sucess, we have received it + Some(_) = ready_receive.recv() => {} + }; + common::teardown(bundle).await } From badf3e9d478901aba59b534b4f393c5c64a12d68 Mon Sep 17 00:00:00 2001 From: kozabrada123 <59031733+kozabrada123@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:10:07 +0100 Subject: [PATCH 72/72] testing tests --- tests/gateway.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/gateway.rs b/tests/gateway.rs index c23408a..de78eb2 100644 --- a/tests/gateway.rs +++ b/tests/gateway.rs @@ -61,9 +61,11 @@ async fn test_gateway_authenticate() { gateway.send_identify(identify).await; + let current_time = std::time::Instant::now(); + tokio::select! { // Fail, we timed out waiting for it - () = safina_timer::sleep_for(Duration::from_secs(20)) => { + () = safina_timer::sleep_until(current_time + Duration::from_secs(20)) => { println!("Timed out waiting for event, failing.."); assert!(false); }