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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] ?? --- 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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); }