From 9ad905198c32c23ebfaf98d5f6de3664d0ac5162 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sun, 10 Mar 2024 19:28:26 -0400 Subject: [PATCH 01/70] feat(lavalink)!: Adding updated structs and enums for v4 of lavalink --- twilight-lavalink/src/http.rs | 209 ++++++++++++++++++++++++---------- 1 file changed, 149 insertions(+), 60 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index 76dee1b5264..f06d61eaa59 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -9,66 +9,69 @@ use percent_encoding::NON_ALPHANUMERIC; use serde::{Deserialize, Deserializer, Serialize}; use std::net::{IpAddr, SocketAddr}; -/// The type of search result given. +/// Information about the track returned or playing on lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum LoadType { - /// Loading the results failed. - LoadFailed, - /// There were no matches. - NoMatches, - /// A playlist was found. - PlaylistLoaded, - /// Some results were found. - SearchResult, - /// A single track was found. - TrackLoaded, +#[serde(rename_all = "camelCase")] +pub struct TrackInfo { + /// The track identifier. + pub identifier: String, + /// Whether the track is seekable. + pub is_seekable: bool, + /// The track author. + pub author: String, + /// The track length in milliseconds. + pub length: u64, + /// Whether the track is a stream. + pub is_stream: bool, + /// The track position in milliseconds. + pub position: u64, + /// The track title. + pub title: String, + /// The track uri. + pub uri: Option, + /// The track artwork url. + pub artwork_url: Option, + /// The track [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). + pub isrc: Option, + /// The track source name. + pub source_name: String, } -/// A track within a search result. + + +/// A track object for lavalink to consume and read. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Track { - /// Details about a track, such as the author and title. - pub info: TrackInfo, - /// The base64 track string that you use in the [`Play`] event. - /// - /// [`Play`]: crate::model::outgoing::Play - pub track: String, + /// The base64 encoded track to play + pub encoded: String, + /// Info about the track + pub info: TrackInfo + } -/// Additional information about a track, such as the author. +/// The track on the player. The encoded and identifier are mutually exclusive. Using only enocded for now. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] -pub struct TrackInfo { - /// The name of the author, if provided. - pub author: Option, - /// The identifier of the source of the track. - pub identifier: String, - /// Whether the source is seekable. - pub is_seekable: bool, - /// Whether the source is a stream. - pub is_stream: bool, - /// The length of the audio in milliseconds. - pub length: u64, - /// The position of the audio. - pub position: u64, - /// The title, if provided. - pub title: Option, - /// The source URI of the track. - pub uri: String, +pub struct UpdatePlayerTrack { + /// The base64 encoded track to play. null stops the current track + pub encoded: String, + /// Additional track data to be sent back in the Track Object + pub user_data: Track, + } + /// Information about a playlist from a search result. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlaylistInfo { - /// The name of the playlist, if available. - pub name: Option, + /// The name of the playlist + pub name: String, /// The selected track within the playlist, if available. #[serde(default, deserialize_with = "deserialize_selected_track")] pub selected_track: Option, @@ -85,17 +88,89 @@ where .and_then(|selected| u64::try_from(selected).ok())) } +/// The levels of severity that an exception can have. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum Severity { + /// The cause is known and expected, indicates that there is nothing wrong with the library itself. + Common, + /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. + Suspicious, + /// The probable cause is an issue with the library or there is no way to tell what the cause might be. This is the default level and other levels are used in cases where the thrower has more in-depth knowledge about the error. + Fault, +} + + +/// The excpetion with the details attached on what happened when making a query to lavalink. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Exception { + /// The message of the exception. + pub message: Option, + /// The severity of the exception. + pub severity: Severity, + /// The cause of the exception. + pub cause: String, +} + +/// The type of search result given. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum LoadResultName { + /// A track has been loaded. + Track, + /// A playlist has been loaded. + Playlist, + /// A search result has been loaded. + Search, + /// There has been no matches for your identifier. + Empty, + /// Loading has failed with an error. + Error, +} + + +/// The type of search result given. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum LoadResultData { + /// Track result with the track info. + Track(Track), + /// The playlist results with the play list info and tracks in the playlist. + Playlist(PlaylistResult), + /// The list of tracks based on the search. + Search(Vec), + /// Empty data response. + Empty(), + /// The exception that was thrown when searching. + Error(Exception), + +} + +/// The playlist with the provided tracks. Currently plugin info isn't supported +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct PlaylistResult { + /// The info of the playlist. + pub info: PlaylistInfo, + /// The tracks of the playlist. + pub tracks: Vec, +} + /// Possible track results for a query. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct LoadedTracks { /// The type of search result, such as a list of tracks or a playlist. - pub load_type: LoadType, - /// Information about the playlist, if provided. - pub playlist_info: PlaylistInfo, - /// The list of tracks returned for the search query. - pub tracks: Vec, + pub load_type: LoadResultName, + /// The data of the result. + pub data: LoadResultData, } /// A failing IP address within the planner. @@ -258,7 +333,32 @@ pub fn load_track( ) -> Result, HttpError> { let identifier = percent_encoding::percent_encode(identifier.as_ref().as_bytes(), NON_ALPHANUMERIC); - let url = format!("http://{address}/loadtracks?identifier={identifier}"); + let url = format!("http://{address}/v4/loadtracks?identifier={identifier}"); + + let mut req = Request::get(url); + + let auth_value = HeaderValue::from_str(authorization.as_ref())?; + req = req.header(AUTHORIZATION, auth_value); + + req.body(b"") +} + +/// Decode a single track into its info +/// +/// The response will include a body which can be deserialized into a +/// [`Track`]. +/// +/// # Errors +/// +/// See the documentation for [`http::Error`]. +pub fn decode_track( + address: SocketAddr, + encoded: impl AsRef, + authorization: impl AsRef, +) -> Result, HttpError> { + let identifier = + percent_encoding::percent_encode(encoded.as_ref().as_bytes(), NON_ALPHANUMERIC); + let url = format!("http://{address}/v4/decodetrack?encodedTrack={identifier}"); let mut req = Request::get(url); @@ -317,7 +417,7 @@ pub fn unmark_failed_address( #[cfg(test)] mod tests { use super::{ - FailingAddress, IpBlock, IpBlockType, LoadType, LoadedTracks, NanoIpDetails, + FailingAddress, IpBlock, IpBlockType, LoadedTracks, NanoIpDetails, NanoIpRoutePlanner, PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner, RotatingNanoIpDetails, RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track, TrackInfo, @@ -359,17 +459,6 @@ mod tests { Serialize, Sync, ); - assert_impl_all!( - LoadType: Clone, - Debug, - Deserialize<'static>, - Eq, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(LoadedTracks: load_type, playlist_info, tracks); assert_impl_all!( LoadedTracks: Clone, Debug, @@ -512,7 +601,7 @@ mod tests { Serialize, Sync ); - assert_fields!(Track: info, track); + assert_fields!(Track: encoded, info); assert_impl_all!( Track: Clone, Debug, @@ -527,7 +616,7 @@ mod tests { #[test] pub fn test_deserialize_playlist_info_negative_selected_track() { let value = PlaylistInfo { - name: Some("Test Playlist".to_owned()), + name: "Test Playlist".to_owned(), selected_track: None, }; From d309466a31c20b8a98cae5468553a76f85943f40 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sun, 10 Mar 2024 20:05:42 -0400 Subject: [PATCH 02/70] feat(lavalink)!: Updating model structs. Removed old OP struct not in the version 4 api. Fixed the example for now and updated tests --- examples/lavalink-basic-bot.rs | 63 ++++++++--- twilight-lavalink/src/model.rs | 201 +++++++++++++++------------------ twilight-lavalink/src/node.rs | 3 +- 3 files changed, 135 insertions(+), 132 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index ccc27bb1e25..f4ebeab9f9e 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -1,3 +1,4 @@ +use twilight_lavalink::http::LoadResultData::{Search, Track, Playlist}; use http_body_util::{BodyExt, Full}; use hyper::{body::Bytes, Request}; use hyper_util::{ @@ -194,26 +195,52 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { let loaded = serde_json::from_slice::(&response_bytes)?; - if let Some(track) = loaded.tracks.first() { - player.send(Play::from((guild_id, &track.track)))?; - - let content = format!( - "Playing **{:?}** by **{:?}**", - track.info.title, track.info.author - ); - state - .http - .create_message(msg.channel_id) - .content(&content) - .await?; - } else { - state - .http - .create_message(msg.channel_id) - .content("Didn't find any results") - .await?; + match loaded.data { + Track (track) => { + player.send(Play::from((guild_id, &track.encoded)))?; + + let content = format!( + "Playing **{:?}** by **{:?}**", + track.info.title, track.info.author + ); + state + .http + .create_message(msg.channel_id) + .content(&content) + .await?; + }, + Playlist(_) => {todo!("Write example for playlist result.")}, + Search(_) => {todo!("Write example for search result.")}, + _ => { + state + .http + .create_message(msg.channel_id) + .content("Didn't find any results") + .await?; + } } + + // if let Some(track) = loaded.tracks.first() { + // player.send(Play::from((guild_id, &track.track)))?; + + // let content = format!( + // "Playing **{:?}** by **{:?}**", + // track.info.title, track.info.author + // ); + // state + // .http + // .create_message(msg.channel_id) + // .content(&content) + // .await?; + // } else { + // state + // .http + // .create_message(msg.channel_id) + // .content("Didn't find any results") + // .await?; + // } + Ok(()) } diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 1b94d3fdec7..51a444825ad 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1,41 +1,8 @@ //! Models to (de)serialize incoming/outgoing websocket events and HTTP //! responses. -use serde::{Deserialize, Serialize}; - -/// The type of event that something is. -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub enum Opcode { - /// Destroy a player from a node. - Destroy, - /// Equalize a player. - Equalizer, - /// Meta information about a track starting or ending. - Event, - /// Pause a player. - Pause, - /// Play a track. - Play, - /// An update about a player's current track. - PlayerUpdate, - /// Seek a player's active track to a new position. - Seek, - /// Updated statistics about a node. - Stats, - /// Stop a player. - Stop, - /// A combined voice server and voice state update. - VoiceUpdate, - /// Set the volume of a player. - Volume, -} - pub mod outgoing { //! Events that clients send to Lavalink. - - use super::Opcode; use serde::{Deserialize, Serialize}; use twilight_model::{ gateway::payload::incoming::VoiceServerUpdate, @@ -120,8 +87,6 @@ pub mod outgoing { pub struct Destroy { /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, } impl Destroy { @@ -129,7 +94,6 @@ pub mod outgoing { pub const fn new(guild_id: Id) -> Self { Self { guild_id, - op: Opcode::Destroy, } } } @@ -138,11 +102,12 @@ pub mod outgoing { fn from(guild_id: Id) -> Self { Self { guild_id, - op: Opcode::Destroy, } } } + + /// Equalize a player. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] @@ -152,8 +117,6 @@ pub mod outgoing { pub bands: Vec, /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, } impl Equalizer { @@ -168,7 +131,6 @@ pub mod outgoing { Self { bands, guild_id, - op: Opcode::Equalizer, } } } @@ -204,8 +166,6 @@ pub mod outgoing { pub struct Pause { /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, /// Whether to pause the player. /// /// Set to `true` to pause or `false` to resume. @@ -225,7 +185,6 @@ pub mod outgoing { fn from((guild_id, pause): (Id, bool)) -> Self { Self { guild_id, - op: Opcode::Pause, pause, } } @@ -251,8 +210,6 @@ pub mod outgoing { /// Set to `true` to keep playing the current playing track, or `false` /// to replace the current playing track with a new one. pub no_replace: bool, - /// The opcode of the event. - pub op: Opcode, /// The position in milliseconds to start the track from. /// /// For example, set to 5000 to start the track 5 seconds in. @@ -305,7 +262,6 @@ pub mod outgoing { end_time: end_time.into(), guild_id, no_replace, - op: Opcode::Play, start_time: start_time.into(), track: track.into(), } @@ -319,8 +275,6 @@ pub mod outgoing { pub struct Seek { /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, /// The position in milliseconds to seek to. pub position: i64, } @@ -336,7 +290,6 @@ pub mod outgoing { fn from((guild_id, position): (Id, i64)) -> Self { Self { guild_id, - op: Opcode::Seek, position, } } @@ -347,8 +300,6 @@ pub mod outgoing { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Stop { - /// The opcode of the event. - pub op: Opcode, /// The guild ID of the player. pub guild_id: Id, } @@ -364,7 +315,6 @@ pub mod outgoing { fn from(guild_id: Id) -> Self { Self { guild_id, - op: Opcode::Stop, } } } @@ -378,8 +328,6 @@ pub mod outgoing { pub event: VoiceServerUpdate, /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, /// The session ID of the voice channel. pub session_id: String, } @@ -400,7 +348,6 @@ pub mod outgoing { Self { event, guild_id, - op: Opcode::VoiceUpdate, session_id: session_id.into(), } } @@ -413,8 +360,6 @@ pub mod outgoing { pub struct Volume { /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, /// The volume of the player from 0 to 1000. 100 is the default. pub volume: i64, } @@ -430,7 +375,6 @@ pub mod outgoing { fn from((guild_id, volume): (Id, i64)) -> Self { Self { guild_id, - op: Opcode::Volume, volume, } } @@ -440,7 +384,7 @@ pub mod outgoing { pub mod incoming { //! Events that Lavalink sends to clients. - use super::Opcode; + use crate::http::{Track, Exception}; use serde::{Deserialize, Serialize}; use twilight_model::id::{marker::GuildMarker, Id}; @@ -473,17 +417,37 @@ pub mod incoming { } } - /// An update about the information of a player. + /// The discord voice information that lavalink uses for connection and sending information. + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct VoiceState { + /// The Discord voice token to authenticate with. + pub token: String, + /// The Discord voice endpoint to connect to. + pub endpoint: String, + /// The Discord voice session id to authenticate with. Note this is seperate from the lavalink session id. + pub session_id: String, + } + + /// An update about the information of a player. Filters are currently unsupported #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlayerUpdate { /// The guild ID of the player. pub guild_id: Id, - /// The opcode of the event. - pub op: Opcode, + /// The currently playing track. + pub track: Option, + /// The volume of the player, range 0-1000, in percentage. + pub volume: u64, + /// Whether the player is paused. + pub paused: bool, /// The new state of the player. pub state: PlayerUpdateState, + /// The voice state of the player. + pub voice: VoiceState, + } /// New statistics about a node and its host. @@ -516,8 +480,6 @@ pub mod incoming { pub players: u64, /// The current number of active players within the node. pub playing_players: u64, - /// The opcode of the event. - pub op: Opcode, /// The uptime of the Lavalink server in seconds. pub uptime: u64, } @@ -578,24 +540,33 @@ pub mod incoming { WebsocketClosed, } - /// A track ended. + /// The reason for the track ending. + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub enum TrackEndReason { + /// The track finished playing. + Finished, + /// The track failed to load. + LoadFailed, + /// The track was stopped. + Stopped, + /// The track was replaced + Replaced, + /// The track was cleaned up. + Cleanup, + } + + + /// A track ended event from lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackEnd { - /// The guild ID of the player. - pub guild_id: Id, - /// The type of track event. - #[serde(rename = "type")] - pub kind: TrackEventType, - /// The opcode of the event. - pub op: Opcode, + /// The track that ended playing. + pub track: Track, /// The reason that the track ended. - /// - /// For example, this may be `"FINISHED"`. - pub reason: String, - /// The base64 track that was affected. - pub track: String, + pub reason: TrackEndReason, } /// A track started. @@ -603,42 +574,51 @@ pub mod incoming { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackStart { - /// The guild ID of the player. - pub guild_id: Id, - /// The type of track event. - #[serde(rename = "type")] - pub kind: TrackEventType, - /// The opcode of the event. - pub op: Opcode, - /// The base64 track that was affected. - pub track: String, + /// The track that started playing. + pub track: Track, + } + + /// Dispatched when a track throws an exception. + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct TrackException { + /// The track that threw the exception. + pub track: Track, + /// The occurred exception. + pub exception: Exception, + } + + /// Dispatched when a track gets stuck while playing. + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct TrackStuck { + /// The track that got stuck. + pub track: Track, + /// The threshold in milliseconds that was exceeded. + pub threshold_ms: u64, } + /// The voice websocket connection to Discord has been closed. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct WebsocketClosed { - /// Guild ID of the associated player. - pub guild_id: Id, - /// Type of track event. - #[serde(rename = "type")] - pub kind: TrackEventType, - /// Lavalink websocket opcode of the event. - pub op: Opcode, - /// Discord websocket opcode that closed the connection. + /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. pub code: u64, - /// True if Discord closed the connection, false if Lavalink closed it. - pub by_remote: bool, /// Reason the connection was closed. pub reason: String, + /// True if Discord closed the connection, false if Lavalink closed it. + pub by_remote: bool, } } pub use self::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, StatsMemory, - TrackEnd, TrackEventType, TrackStart, WebsocketClosed, + TrackEnd, TrackEventType, TrackStart, TrackStuck, TrackException, WebsocketClosed, }, outgoing::{ Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, @@ -657,7 +637,6 @@ mod tests { Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, Volume, }, - Opcode, }; use serde::{Deserialize, Serialize}; use serde_test::Token; @@ -668,7 +647,7 @@ mod tests { id::{marker::GuildMarker, Id}, }; - assert_fields!(Destroy: guild_id, op); + assert_fields!(Destroy: guild_id); assert_impl_all!( Destroy: Clone, Debug, @@ -691,7 +670,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Equalizer: bands, guild_id, op); + assert_fields!(Equalizer: bands, guild_id); assert_impl_all!( Equalizer: Clone, Debug, @@ -730,7 +709,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Pause: guild_id, op, pause); + assert_fields!(Pause: guild_id, pause); assert_impl_all!( Pause: Clone, Debug, @@ -753,7 +732,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(PlayerUpdate: guild_id, op, state); + assert_fields!(PlayerUpdate: guild_id, state); assert_impl_all!( PlayerUpdate: Clone, Debug, @@ -764,7 +743,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Play: end_time, guild_id, no_replace, op, start_time, track); + assert_fields!(Play: end_time, guild_id, no_replace, start_time, track); assert_impl_all!( Play: Clone, Debug, @@ -786,7 +765,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Seek: guild_id, op, position); + assert_fields!(Seek: guild_id, position); assert_impl_all!( Seek: Clone, Debug, @@ -804,7 +783,6 @@ mod tests { memory, players, playing_players, - op, uptime ); assert_impl_all!( @@ -846,7 +824,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Stop: guild_id, op); + assert_fields!(Stop: guild_id); assert_impl_all!( Stop: Clone, Debug, @@ -858,7 +836,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(TrackEnd: guild_id, kind, op, reason, track); + assert_fields!(TrackEnd: reason, track); assert_impl_all!( TrackEnd: Clone, Debug, @@ -878,7 +856,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(TrackStart: guild_id, kind, op, track); + assert_fields!(TrackStart: track); assert_impl_all!( TrackStart: Clone, Debug, @@ -888,7 +866,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(WebsocketClosed: guild_id, kind, op, code, reason, by_remote); + assert_fields!(WebsocketClosed: code, reason, by_remote); assert_impl_all!( WebsocketClosed: Clone, Debug, @@ -898,7 +876,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(VoiceUpdate: event, guild_id, op, session_id); + assert_fields!(VoiceUpdate: event, guild_id, session_id); assert_impl_all!( VoiceUpdate: Clone, Debug, @@ -910,7 +888,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Volume: guild_id, op, volume); + assert_fields!(Volume: guild_id, volume); assert_impl_all!( Volume: Clone, Debug, @@ -946,7 +924,6 @@ mod tests { }, players: 0, playing_players: 0, - op: Opcode::Stats, uptime: 18589, }; diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 154ed7bb296..4b315a59881 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -18,7 +18,7 @@ //! [`Lavalink`]: crate::client::Lavalink use crate::{ - model::{IncomingEvent, Opcode, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, + model::{IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, player::PlayerManager, }; use futures_util::{ @@ -375,7 +375,6 @@ impl Node { }, players: 0, playing_players: 0, - op: Opcode::Stats, uptime: 0, }); From b0996604e54e788a4bb1cdb0117ca3090e280e81 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sun, 10 Mar 2024 20:06:15 -0400 Subject: [PATCH 03/70] Remove commented out code in example lavalink bot --- examples/lavalink-basic-bot.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index f4ebeab9f9e..e8db450028a 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -220,27 +220,6 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { } } - - // if let Some(track) = loaded.tracks.first() { - // player.send(Play::from((guild_id, &track.track)))?; - - // let content = format!( - // "Playing **{:?}** by **{:?}**", - // track.info.title, track.info.author - // ); - // state - // .http - // .create_message(msg.channel_id) - // .content(&content) - // .await?; - // } else { - // state - // .http - // .create_message(msg.channel_id) - // .content("Didn't find any results") - // .await?; - // } - Ok(()) } From 338cfa43227f9cae9533ae51978ff78d0cb5db43 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sun, 10 Mar 2024 22:25:44 -0400 Subject: [PATCH 04/70] fix(lavalink): Fixed the play, stop, seek, pause, volume, etc stucts for the new v4 --- twilight-lavalink/src/http.rs | 9 +++-- twilight-lavalink/src/model.rs | 70 ++++++++++++++++++++++----------- twilight-lavalink/src/player.rs | 2 +- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index f06d61eaa59..c0877cc9206 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -53,14 +53,15 @@ pub struct Track { } /// The track on the player. The encoded and identifier are mutually exclusive. Using only enocded for now. +/// Encoded was chosen since that was previously used in the v3 implementation. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct UpdatePlayerTrack { /// The base64 encoded track to play. null stops the current track - pub encoded: String, + pub encoded: Option, /// Additional track data to be sent back in the Track Object - pub user_data: Track, + pub user_data: Option, } @@ -380,7 +381,7 @@ pub fn get_route_planner( address: SocketAddr, authorization: impl AsRef, ) -> Result, HttpError> { - let mut req = Request::get(format!("{address}/routeplanner/status")); + let mut req = Request::get(format!("{address}/v4/routeplanner/status")); let auth_value = HeaderValue::from_str(authorization.as_ref())?; req = req.header(AUTHORIZATION, auth_value); @@ -401,7 +402,7 @@ pub fn unmark_failed_address( authorization: impl AsRef, route_address: impl Into, ) -> Result>, HttpError> { - let mut req = Request::post(format!("{}/routeplanner/status", node_address.into())); + let mut req = Request::post(format!("{}/v4/routeplanner/status", node_address.into())); let auth_value = HeaderValue::from_str(authorization.as_ref())?; req = req.header(AUTHORIZATION, auth_value); diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 51a444825ad..dea358f9bb9 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -9,6 +9,8 @@ pub mod outgoing { id::{marker::GuildMarker, Id}, }; + use crate::http::UpdatePlayerTrack; + /// An outgoing event to send to Lavalink. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] @@ -169,7 +171,7 @@ pub mod outgoing { /// Whether to pause the player. /// /// Set to `true` to pause or `false` to resume. - pub pause: bool, + pub paused: bool, } impl Pause { @@ -185,38 +187,42 @@ pub mod outgoing { fn from((guild_id, pause): (Id, bool)) -> Self { Self { guild_id, - pause, + paused: pause, } } } - /// Play a track, optionally specifying to not skip the current track. + + // TODO: Might need to fix this struct to abstract the guild_id to another struct pending on what the server sends back with it included. + /// Play a track, optionally specifying to not skip the current track. Filters are not supported at the moment. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Play { + /// Information about the track to play. + pub track: UpdatePlayerTrack, + /// The position in milliseconds to start the track from. + #[serde(skip_serializing_if = "Option::is_none")] + pub position: Option, /// The position in milliseconds to end the track. - /// - /// This currently [does nothing] as of this writing. - /// - /// [does nothing]: https://github.com/freyacodes/Lavalink/issues/179 #[serde(skip_serializing_if = "Option::is_none")] - pub end_time: Option, + pub end_time: Option>, + /// The player volume, in percentage, from 0 to 1000 + #[serde(skip_serializing_if = "Option::is_none")] + pub volume: Option, + /// Whether the player is paused + #[serde(skip_serializing_if = "Option::is_none")] + pub paused: Option, /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, /// Whether or not to replace the currently playing track with this new /// track. /// /// Set to `true` to keep playing the current playing track, or `false` /// to replace the current playing track with a new one. + #[serde(skip_serializing)] pub no_replace: bool, - /// The position in milliseconds to start the track from. - /// - /// For example, set to 5000 to start the track 5 seconds in. - #[serde(skip_serializing_if = "Option::is_none")] - pub start_time: Option, - /// The base64 track information. - pub track: String, } impl Play { @@ -259,11 +265,16 @@ pub mod outgoing { (guild_id, track, start_time, end_time, no_replace): (Id, T, S, E, bool), ) -> Self { Self { - end_time: end_time.into(), guild_id, no_replace, - start_time: start_time.into(), - track: track.into(), + position: start_time.into(), + end_time: Some(end_time.into()), + volume: None, + paused: None, + track: UpdatePlayerTrack{ + encoded: Some(track.into()), + user_data: None + }, } } } @@ -274,6 +285,7 @@ pub mod outgoing { #[serde(rename_all = "camelCase")] pub struct Seek { /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, /// The position in milliseconds to seek to. pub position: i64, @@ -301,7 +313,10 @@ pub mod outgoing { #[serde(rename_all = "camelCase")] pub struct Stop { /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, + /// The track object to pass set to null + pub track: UpdatePlayerTrack, } impl Stop { @@ -315,6 +330,10 @@ pub mod outgoing { fn from(guild_id: Id) -> Self { Self { guild_id, + track: UpdatePlayerTrack { + encoded: None, + user_data: None + }, } } } @@ -324,11 +343,14 @@ pub mod outgoing { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct VoiceUpdate { - /// The inner event being forwarded to a node. - pub event: VoiceServerUpdate, /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, - /// The session ID of the voice channel. + /// The Discord voice token to authenticate with. + pub token: String, + /// The Discord voice endpoint to connect to. + pub endpoint: String, + /// The Discord voice session id to authenticate with. This is seperate from the session id of lavalink. pub session_id: String, } @@ -346,8 +368,9 @@ pub mod outgoing { impl> From<(Id, T, VoiceServerUpdate)> for VoiceUpdate { fn from((guild_id, session_id, event): (Id, T, VoiceServerUpdate)) -> Self { Self { - event, - guild_id, + guild_id: guild_id, + token: event.token, + endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), session_id: session_id.into(), } } @@ -359,6 +382,7 @@ pub mod outgoing { #[serde(rename_all = "camelCase")] pub struct Volume { /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, /// The volume of the player from 0 to 1000. 100 is the default. pub volume: i64, diff --git a/twilight-lavalink/src/player.rs b/twilight-lavalink/src/player.rs index 40e29d50ff2..a67644e3eef 100644 --- a/twilight-lavalink/src/player.rs +++ b/twilight-lavalink/src/player.rs @@ -149,7 +149,7 @@ impl Player { tracing::debug!("sending event on guild player {}: {event:?}", self.guild_id); match &event { - OutgoingEvent::Pause(event) => self.paused.store(event.pause, Ordering::Release), + OutgoingEvent::Pause(event) => self.paused.store(event.paused, Ordering::Release), OutgoingEvent::Volume(event) => { #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] self.volume.store(event.volume, Ordering::Release); From a7866f1b979fc37008e602ef2c8d4a0a07c2b93f Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 17:19:16 -0400 Subject: [PATCH 05/70] This is the first stab at figuring out the session id. I fixed the ready so it connects now. Still need to get the correct session id from lavalink not the discord api --- examples/lavalink-basic-bot.rs | 2 +- twilight-lavalink/Cargo.toml | 3 + twilight-lavalink/src/client.rs | 8 ++ twilight-lavalink/src/model.rs | 215 ++++++++++++++++++++------------ twilight-lavalink/src/node.rs | 112 ++++++++++++++--- 5 files changed, 242 insertions(+), 98 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index e8db450028a..ecd14be9c79 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -197,7 +197,7 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { match loaded.data { Track (track) => { - player.send(Play::from((guild_id, &track.encoded)))?; + player.send(Play::from((guild_id, state.lavalink.lookup_session(guild_id).unwrap(), &track.encoded)))?; let content = format!( "Playing **{:?}** by **{:?}**", diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 9bab2dfb32a..995f1e17b68 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -17,6 +17,9 @@ version = "0.16.0-rc.1" dashmap = { default-features = false, version = "5.3" } futures-util = { default-features = false, features = ["bilock", "sink", "std", "unstable"], version = "0.3" } http = { default-features = false, version = "1" } +hyper = { default-features = false, version = "1" } +hyper-util = { default-features = false, features = ["client-legacy"], version = "0.1.2" } +http-body-util = "0.1" serde = { default-features = false, features = ["derive", "std"], version = "1" } serde_json = { default-features = false, features = ["std"], version = "1" } tokio = { default-features = false, features = ["macros", "net", "rt", "sync", "time"], version = "1.0" } diff --git a/twilight-lavalink/src/client.rs b/twilight-lavalink/src/client.rs index 5b98e9e0845..3ea2138459b 100644 --- a/twilight-lavalink/src/client.rs +++ b/twilight-lavalink/src/client.rs @@ -350,6 +350,14 @@ impl Lavalink { &self.players } + /// Find the session for this guild + pub fn lookup_session(&self, guild_id: Id) -> Option { + match self.sessions.get(&guild_id) { + Some(session_id) => Some(session_id.to_string()), + None => None, + } + } + /// Retrieve a player for the guild. /// /// Creates a player configured to use the best available node if a player diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index dea358f9bb9..2c339b716e2 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -216,6 +216,9 @@ pub mod outgoing { /// The guild ID of the player. #[serde(skip_serializing)] pub guild_id: Id, + /// The lavalink session id to send this event to. + #[serde(skip_serializing)] + pub session_id: String, /// Whether or not to replace the currently playing track with this new /// track. /// @@ -229,43 +232,45 @@ pub mod outgoing { /// Create a new play event. pub fn new( guild_id: Id, + session_id: impl Into, track: impl Into, start_time: impl Into>, end_time: impl Into>, no_replace: bool, ) -> Self { - Self::from((guild_id, track, start_time, end_time, no_replace)) + Self::from((guild_id, session_id, track, start_time, end_time, no_replace)) } } - impl> From<(Id, T)> for Play { - fn from((guild_id, track): (Id, T)) -> Self { - Self::from((guild_id, track, None, None, true)) + impl, I: Into> From<(Id, I, T)> for Play { + fn from((guild_id, session_id, track): (Id, I, T)) -> Self { + Self::from((guild_id, session_id, track, None, None, true)) } } - impl, S: Into>> From<(Id, T, S)> for Play { - fn from((guild_id, track, start_time): (Id, T, S)) -> Self { - Self::from((guild_id, track, start_time, None, true)) + impl, I: Into, S: Into>> From<(Id, I, T, S)> for Play { + fn from((guild_id, session_id, track, start_time): (Id, I, T, S)) -> Self { + Self::from((guild_id, session_id, track, start_time, None, true)) } } - impl, S: Into>, E: Into>> - From<(Id, T, S, E)> for Play + impl, I: Into, S: Into>, E: Into>> + From<(Id, I, T, S, E)> for Play { - fn from((guild_id, track, start_time, end_time): (Id, T, S, E)) -> Self { - Self::from((guild_id, track, start_time, end_time, true)) + fn from((guild_id, session_id, track, start_time, end_time): (Id, I, T, S, E)) -> Self { + Self::from((guild_id, session_id, track, start_time, end_time, true)) } } - impl, S: Into>, E: Into>> - From<(Id, T, S, E, bool)> for Play + impl, I: Into, S: Into>, E: Into>> + From<(Id, I, T, S, E, bool)> for Play { fn from( - (guild_id, track, start_time, end_time, no_replace): (Id, T, S, E, bool), + (guild_id, session_id, track, start_time, end_time, no_replace): (Id, I, T, S, E, bool), ) -> Self { Self { guild_id, + session_id: session_id.into(), no_replace, position: start_time.into(), end_time: Some(end_time.into()), @@ -337,15 +342,11 @@ pub mod outgoing { } } } - - /// A combined voice server and voice state update. + /// The voice payload for the combined server and state to send to lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] - pub struct VoiceUpdate { - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, + pub struct Voice { /// The Discord voice token to authenticate with. pub token: String, /// The Discord voice endpoint to connect to. @@ -354,6 +355,18 @@ pub mod outgoing { pub session_id: String, } + /// A combined voice server and voice state update. + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct VoiceUpdate { + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// The voice payload for the combined server and state to send to lavalink. + pub voice: Voice, + } + impl VoiceUpdate { /// Create a new voice update event. pub fn new( @@ -369,9 +382,11 @@ pub mod outgoing { fn from((guild_id, session_id, event): (Id, T, VoiceServerUpdate)) -> Self { Self { guild_id: guild_id, - token: event.token, - endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), - session_id: session_id.into(), + voice: Voice{ + token: event.token, + endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), + session_id: session_id.into(), + } } } } @@ -408,6 +423,22 @@ pub mod outgoing { pub mod incoming { //! Events that Lavalink sends to clients. + /// The type of event that something is. + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub enum Opcode { + /// Lavalink is connected and ready. + Ready, + /// An update about a player's current track. + PlayerUpdate, + /// Updated statistics about a node. + Stats, + /// Meta information about a track starting or ending. + Event, + } + + use crate::http::{Track, Exception}; use serde::{Deserialize, Serialize}; use twilight_model::id::{marker::GuildMarker, Id}; @@ -417,18 +448,23 @@ pub mod incoming { #[non_exhaustive] #[serde(untagged)] pub enum IncomingEvent { + /// Dispatched when you successfully connect to the Lavalink node. + Ready(Ready), /// An update about the information of a player. PlayerUpdate(PlayerUpdate), /// New statistics about a node and its host. Stats(Stats), - /// A track ended. - TrackEnd(TrackEnd), - /// A track started. - TrackStart(TrackStart), - /// The voice websocket connection was closed. - WeboscketClosed(WebsocketClosed), + // /// Dispatched when player or voice events occur. + // Event(Event), + } + + impl From for IncomingEvent { + fn from(event: Ready) -> IncomingEvent { + Self::Ready(event) + } } + impl From for IncomingEvent { fn from(event: PlayerUpdate) -> IncomingEvent { Self::PlayerUpdate(event) @@ -459,18 +495,12 @@ pub mod incoming { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlayerUpdate { + /// Op code for this websocket event. + pub op: Opcode, /// The guild ID of the player. pub guild_id: Id, - /// The currently playing track. - pub track: Option, - /// The volume of the player, range 0-1000, in percentage. - pub volume: u64, - /// Whether the player is paused. - pub paused: bool, /// The new state of the player. pub state: PlayerUpdateState, - /// The voice state of the player. - pub voice: VoiceState, } @@ -479,12 +509,27 @@ pub mod incoming { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlayerUpdateState { - /// True when the player is connected to the voice gateway. - pub connected: bool, /// Unix timestamp of the player in milliseconds. pub time: i64, /// Track position in milliseconds. None if not playing anything. - pub position: Option, + pub position: i64, + /// True when the player is connected to the voice gateway. + pub connected: bool, + /// The ping of the node to the Discord voice server in milliseconds (-1 if not connected). + pub ping: bool, + } + + /// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct Ready { + /// Op code for this websocket event. + pub op: Opcode, + /// Whether this session was resumed. + pub resumed: bool, + /// The Lavalink session id of this connection. Not to be confused with a Discord voice session id. + pub session_id: String, } /// Statistics about a node and its host. @@ -492,11 +537,13 @@ pub mod incoming { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Stats { + /// Op code for this websocket event. + pub op: Opcode, /// CPU information about the node's host. pub cpu: StatsCpu, /// Statistics about audio frames. #[serde(rename = "frameStats", skip_serializing_if = "Option::is_none")] - pub frames: Option, + pub frame_stats: Option, /// Memory information about the node's host. pub memory: StatsMemory, /// The current number of total players (active and not active) within @@ -549,21 +596,39 @@ pub mod incoming { pub used: u64, } - /// The type of track event that was received. + /// Server dispatched an event. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] - pub enum TrackEventType { - /// A track for a player ended. - #[serde(rename = "TrackEndEvent")] - End, - /// A track for a player started. - #[serde(rename = "TrackStartEvent")] - Start, - /// The voice websocket connection to Discord has been closed. - #[serde(rename = "WebSocketClosedEvent")] - WebsocketClosed, + pub enum EventType { + /// Dispatched when a track starts playing. + TrackStartEvent, + /// Dispatched when a track ends. + TrackEndEvent, + /// Dispatched when a track throws an exception. + TrackExceptionEvent, + /// Dispatched when a track gets stuck while playing. + TrackStuckEvent, + /// Dispatched when the websocket connection to Discord voice servers is closed. + WebsocketClosedEvent, } + /// Server dispatched an event. + #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] + #[non_exhaustive] + pub enum EventData { + /// Dispatched when a track starts playing. + TrackStartEvent(TrackStart), + /// Dispatched when a track ends. + TrackEndEvent(TrackEnd), + /// Dispatched when a track throws an exception. + TrackExceptionEvent(TrackException), + /// Dispatched when a track gets stuck while playing. + TrackStuckEvent(TrackStuck), + /// Dispatched when the websocket connection to Discord voice servers is closed. + WebsocketClosedEvent(WebsocketClosed), + } + + /// The reason for the track ending. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -642,7 +707,7 @@ pub mod incoming { pub use self::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, StatsMemory, - TrackEnd, TrackEventType, TrackStart, TrackStuck, TrackException, WebsocketClosed, + TrackEnd, TrackStart, TrackStuck, TrackException, WebsocketClosed, }, outgoing::{ Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, @@ -655,7 +720,7 @@ mod tests { use super::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, - StatsMemory, TrackEnd, TrackEventType, TrackStart, WebsocketClosed, + StatsMemory, TrackEnd, TrackStart, WebsocketClosed, }, outgoing::{ Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, @@ -733,7 +798,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(Pause: guild_id, pause); + assert_fields!(Pause: guild_id, paused); assert_impl_all!( Pause: Clone, Debug, @@ -767,23 +832,23 @@ mod tests { Serialize, Sync, ); - assert_fields!(Play: end_time, guild_id, no_replace, start_time, track); + assert_fields!(Play: end_time, guild_id, no_replace, position, track); assert_impl_all!( Play: Clone, Debug, Deserialize<'static>, Eq, - From<(Id, String)>, - From<(Id, String, Option)>, - From<(Id, String, u64)>, - From<(Id, String, Option, Option)>, - From<(Id, String, Option, u64)>, - From<(Id, String, u64, Option)>, - From<(Id, String, u64, u64)>, - From<(Id, String, Option, Option, bool)>, - From<(Id, String, Option, u64, bool)>, - From<(Id, String, u64, Option, bool)>, - From<(Id, String, u64, u64, bool)>, + From<(Id, String, String)>, + From<(Id, String, String, Option)>, + From<(Id, String, String, u64)>, + From<(Id, String, String, Option, Option)>, + From<(Id, String, String, Option, u64)>, + From<(Id, String, String, u64, Option)>, + From<(Id, String, String, u64, u64)>, + From<(Id, String, String, Option, Option, bool)>, + From<(Id, String, String, Option, u64, bool)>, + From<(Id, String, String, u64, Option, bool)>, + From<(Id, String, String, u64, u64, bool)>, PartialEq, Send, Serialize, @@ -803,7 +868,7 @@ mod tests { ); assert_fields!( Stats: cpu, - frames, + frame_stats, memory, players, playing_players, @@ -870,16 +935,6 @@ mod tests { Serialize, Sync, ); - assert_impl_all!( - TrackEventType: Clone, - Copy, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); assert_fields!(TrackStart: track); assert_impl_all!( TrackStart: Clone, @@ -900,7 +955,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(VoiceUpdate: event, guild_id, session_id); + assert_fields!(VoiceUpdate: guild_id, session_id); assert_impl_all!( VoiceUpdate: Clone, Debug, @@ -939,7 +994,7 @@ mod tests { lavalink_load: LAVALINK_LOAD, system_load: SYSTEM_LOAD, }, - frames: None, + frame_stats: None, memory: StatsMemory { allocated: MEM_ALLOCATED, free: MEM_FREE, diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 4b315a59881..825ba9739a9 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,16 +17,20 @@ //! //! [`Lavalink`]: crate::client::Lavalink +use hyper::{Request, Method}; +use hyper_util::rt::TokioIo; + use crate::{ model::{IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, player::PlayerManager, + model::OutgoingEvent::{VoiceUpdate, Play} }; use futures_util::{ lock::BiLock, sink::SinkExt, stream::{Stream, StreamExt}, }; -use http::header::{HeaderName, AUTHORIZATION}; +use http::header::{HeaderName, HeaderValue, AUTHORIZATION}; use std::{ error::Error, fmt::{Debug, Display, Formatter, Result as FmtResult}, @@ -361,12 +365,13 @@ impl Node { players: PlayerManager, ) -> Result<(Self, IncomingEvents), NodeError> { let (bilock_left, bilock_right) = BiLock::new(Stats { + op: crate::model::incoming::Opcode::Stats, cpu: StatsCpu { cores: 0, lavalink_load: 0f64, system_load: 0f64, }, - frames: None, + frame_stats: None, memory: StatsMemory { allocated: 0, free: 0, @@ -447,11 +452,11 @@ impl Node { let (deficit_frame, null_frame) = ( 1.03f64 - .powf(500f64 * (stats.frames.as_ref().map_or(0, |f| f.deficit) as f64 / 3000f64)) + .powf(500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.deficit) as f64 / 3000f64)) * 300f64 - 300f64, (1.03f64 - .powf(500f64 * (stats.frames.as_ref().map_or(0, |f| f.nulled) as f64 / 3000f64)) + .powf(500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.nulled) as f64 / 3000f64)) * 300f64 - 300f64) * 2f64, @@ -515,17 +520,7 @@ impl Connection { } outgoing = self.node_from.recv() => { if let Some(outgoing) = outgoing { - tracing::debug!( - "forwarding event to {}: {outgoing:?}", - self.config.address, - ); - - let payload = serde_json::to_string(&outgoing).map_err(|source| NodeError { - kind: NodeErrorType::SerializingMessage { message: outgoing }, - source: Some(Box::new(source)), - })?; - let msg = Message::text(payload); - self.stream.send(msg).await.unwrap(); + self.outgoing(outgoing).await?; } else { tracing::debug!("node {} closed, ending connection", self.config.address); @@ -538,6 +533,82 @@ impl Connection { Ok(()) } + async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { + let address = self.config.address; + let guild_id = match outgoing.clone() { + VoiceUpdate(voice_update) => voice_update.guild_id, + Play(play) => play.guild_id, + _ => todo!(), + }; + + let session_id = match outgoing.clone() { + VoiceUpdate(voice_update) => voice_update.voice.session_id, + Play(play) => play.session_id, + _ => todo!(), + }; + + // let payload = serde_json::to_string(&outgoing).map_err(|source| NodeError { + // kind: NodeErrorType::SerializingMessage { message: outgoing.clone() }, + // source: Some(Box::new(source)) + // }); + let payload = serde_json::to_string(&outgoing).unwrap(); + + + tracing::debug!( + "forwarding event to {}: {outgoing:?}", + address, + ); + let url = format!("http://{address}/v4/sessions/{}/players/{guild_id}?noReplace=true", session_id.clone()).parse::().unwrap(); + + tracing::debug!( + "converted payload: {payload:?}" + ); + + // Get the host and the port + let host = url.host().expect("uri has no host"); + let port = url.port_u16().unwrap_or(80); + + let address = format!("{}:{}", host, port); + + // Open a TCP connection to the remote host + let stream = TcpStream::connect(address).await.unwrap(); + + // Use an adapter to access something implementing `tokio::io` traits as if they implement + // `hyper::rt` IO traits. + let io = TokioIo::new(stream); + + // Create the Hyper client + let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap(); + tokio::task::spawn(async move { + if let Err(err) = conn.await { + println!("Connection failed: {:?}", err); + } + }); + + // The authority of our URL will be the hostname of the httpbin remote + let authority = url.authority().unwrap().clone(); + + // Create an HTTP request with an empty body and a HOST header + let req = Request::builder() + .uri(url) + .method(Method::PATCH) + .header(hyper::header::HOST, authority.as_str()) + .header(AUTHORIZATION, self.config.authorization.as_str()) + .header("Content-Type", "application/json") + .body(payload) + .unwrap(); + + tracing::debug!( + "Request: {req:?}" + ); + + // Await the response... + let res = sender.send_request(req).await.unwrap(); + + tracing::debug!("Response status: {}", res.status()); + Ok(()) + } + async fn incoming(&mut self, incoming: Message) -> Result { tracing::debug!( "received message from {}: {incoming:?}", @@ -587,7 +658,7 @@ impl Connection { return Ok(()); }; - player.set_position(update.state.position.unwrap_or(0)); + player.set_position(update.state.position); player.set_time(update.state.time); Ok(()) @@ -610,8 +681,11 @@ impl Drop for Connection { } fn connect_request(state: &NodeConfig) -> Result { + let crate_version = env!("CARGO_PKG_VERSION"); + let client_name = format!("twilight-lavalink/{}", crate_version); + let mut builder = ClientBuilder::new() - .uri(&format!("ws://{}", state.address)) + .uri(&format!("ws://{}/v4/websocket", state.address)) .map_err(|source| NodeError { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), @@ -620,6 +694,10 @@ fn connect_request(state: &NodeConfig) -> Result { .add_header( HeaderName::from_static("user-id"), state.user_id.get().into(), + ) + .add_header( + HeaderName::from_static("client-name"), + HeaderValue::from_str(&client_name).unwrap(), ); if state.resume.is_some() { From d52bf3239a8a67f0deac8fddb40eb4ab8f435e04 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 17:31:06 -0400 Subject: [PATCH 06/70] fix(lavalink): Removing the session implementation and renamed the sessions to discord_sessions in the lavalink struct. These are seperate from the session with lavalink --- examples/lavalink-basic-bot.rs | 2 +- twilight-lavalink/src/client.rs | 20 ++++-------- twilight-lavalink/src/model.rs | 58 +++++++++++++++------------------ twilight-lavalink/src/node.rs | 5 --- 4 files changed, 34 insertions(+), 51 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index ecd14be9c79..e8db450028a 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -197,7 +197,7 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { match loaded.data { Track (track) => { - player.send(Play::from((guild_id, state.lavalink.lookup_session(guild_id).unwrap(), &track.encoded)))?; + player.send(Play::from((guild_id, &track.encoded)))?; let content = format!( "Playing **{:?}** by **{:?}**", diff --git a/twilight-lavalink/src/client.rs b/twilight-lavalink/src/client.rs index 3ea2138459b..d221993e78c 100644 --- a/twilight-lavalink/src/client.rs +++ b/twilight-lavalink/src/client.rs @@ -103,7 +103,7 @@ pub struct Lavalink { shard_count: u32, user_id: Id, server_updates: DashMap, VoiceServerUpdate>, - sessions: DashMap, Box>, + discord_sessions: DashMap, Box>, } impl Lavalink { @@ -145,7 +145,7 @@ impl Lavalink { shard_count, user_id, server_updates: DashMap::new(), - sessions: DashMap::new(), + discord_sessions: DashMap::new(), } } @@ -199,10 +199,10 @@ impl Lavalink { } if e.channel_id.is_none() { - self.sessions.remove(&guild_id); + self.discord_sessions.remove(&guild_id); self.server_updates.remove(&guild_id); } else { - self.sessions + self.discord_sessions .insert(guild_id, e.session_id.clone().into_boxed_str()); } guild_id @@ -218,7 +218,7 @@ impl Lavalink { let update = { let server = self.server_updates.get(&guild_id); - let session = self.sessions.get(&guild_id); + let session = self.discord_sessions.get(&guild_id); match (server, session) { (Some(server), Some(session)) => { let server = server.value(); @@ -350,14 +350,6 @@ impl Lavalink { &self.players } - /// Find the session for this guild - pub fn lookup_session(&self, guild_id: Id) -> Option { - match self.sessions.get(&guild_id) { - Some(session_id) => Some(session_id.to_string()), - None => None, - } - } - /// Retrieve a player for the guild. /// /// Creates a player configured to use the best available node if a player @@ -394,7 +386,7 @@ impl Lavalink { self.server_updates .retain(|k, _| (k.get() >> 22) % shard_count != u64::from(shard_id)); - self.sessions + self.discord_sessions .retain(|k, _| (k.get() >> 22) % shard_count != u64::from(shard_id)); } } diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 2c339b716e2..74b8d638123 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -216,9 +216,6 @@ pub mod outgoing { /// The guild ID of the player. #[serde(skip_serializing)] pub guild_id: Id, - /// The lavalink session id to send this event to. - #[serde(skip_serializing)] - pub session_id: String, /// Whether or not to replace the currently playing track with this new /// track. /// @@ -232,45 +229,43 @@ pub mod outgoing { /// Create a new play event. pub fn new( guild_id: Id, - session_id: impl Into, track: impl Into, start_time: impl Into>, end_time: impl Into>, no_replace: bool, ) -> Self { - Self::from((guild_id, session_id, track, start_time, end_time, no_replace)) + Self::from((guild_id, track, start_time, end_time, no_replace)) } } - impl, I: Into> From<(Id, I, T)> for Play { - fn from((guild_id, session_id, track): (Id, I, T)) -> Self { - Self::from((guild_id, session_id, track, None, None, true)) + impl> From<(Id, T)> for Play { + fn from((guild_id, track): (Id, T)) -> Self { + Self::from((guild_id, track, None, None, true)) } } - impl, I: Into, S: Into>> From<(Id, I, T, S)> for Play { - fn from((guild_id, session_id, track, start_time): (Id, I, T, S)) -> Self { - Self::from((guild_id, session_id, track, start_time, None, true)) + impl, S: Into>> From<(Id, T, S)> for Play { + fn from((guild_id, track, start_time): (Id, T, S)) -> Self { + Self::from((guild_id, track, start_time, None, true)) } } - impl, I: Into, S: Into>, E: Into>> - From<(Id, I, T, S, E)> for Play + impl, S: Into>, E: Into>> + From<(Id, T, S, E)> for Play { - fn from((guild_id, session_id, track, start_time, end_time): (Id, I, T, S, E)) -> Self { - Self::from((guild_id, session_id, track, start_time, end_time, true)) + fn from((guild_id, track, start_time, end_time): (Id, T, S, E)) -> Self { + Self::from((guild_id, track, start_time, end_time, true)) } } - impl, I: Into, S: Into>, E: Into>> - From<(Id, I, T, S, E, bool)> for Play + impl, S: Into>, E: Into>> + From<(Id, T, S, E, bool)> for Play { fn from( - (guild_id, session_id, track, start_time, end_time, no_replace): (Id, I, T, S, E, bool), + (guild_id, track, start_time, end_time, no_replace): (Id, T, S, E, bool), ) -> Self { Self { guild_id, - session_id: session_id.into(), no_replace, position: start_time.into(), end_time: Some(end_time.into()), @@ -838,17 +833,17 @@ mod tests { Debug, Deserialize<'static>, Eq, - From<(Id, String, String)>, - From<(Id, String, String, Option)>, - From<(Id, String, String, u64)>, - From<(Id, String, String, Option, Option)>, - From<(Id, String, String, Option, u64)>, - From<(Id, String, String, u64, Option)>, - From<(Id, String, String, u64, u64)>, - From<(Id, String, String, Option, Option, bool)>, - From<(Id, String, String, Option, u64, bool)>, - From<(Id, String, String, u64, Option, bool)>, - From<(Id, String, String, u64, u64, bool)>, + From<(Id, String)>, + From<(Id, String, Option)>, + From<(Id, String, u64)>, + From<(Id, String, Option, Option)>, + From<(Id, String, Option, u64)>, + From<(Id, String, u64, Option)>, + From<(Id, String, u64, u64)>, + From<(Id, String, Option, Option, bool)>, + From<(Id, String, Option, u64, bool)>, + From<(Id, String, u64, Option, bool)>, + From<(Id, String, u64, u64, bool)>, PartialEq, Send, Serialize, @@ -955,7 +950,7 @@ mod tests { Serialize, Sync, ); - assert_fields!(VoiceUpdate: guild_id, session_id); + assert_fields!(VoiceUpdate: guild_id, voice); assert_impl_all!( VoiceUpdate: Clone, Debug, @@ -989,6 +984,7 @@ mod tests { const SYSTEM_LOAD: f64 = 0.195_380_536_378_835_9; let expected = Stats { + op: crate::model::incoming::Opcode::Stats, cpu: StatsCpu { cores: 4, lavalink_load: LAVALINK_LOAD, diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 825ba9739a9..1e5ea83fcdc 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -543,14 +543,9 @@ impl Connection { let session_id = match outgoing.clone() { VoiceUpdate(voice_update) => voice_update.voice.session_id, - Play(play) => play.session_id, _ => todo!(), }; - // let payload = serde_json::to_string(&outgoing).map_err(|source| NodeError { - // kind: NodeErrorType::SerializingMessage { message: outgoing.clone() }, - // source: Some(Box::new(source)) - // }); let payload = serde_json::to_string(&outgoing).unwrap(); From ee4f52317866769e19ad905aa21c5f9746e8e4e5 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 17:47:58 -0400 Subject: [PATCH 07/70] fix(lavalink): Fixed the lavalink v4 playerupdatestat ping field. --- twilight-lavalink/src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 74b8d638123..869399a6228 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -511,7 +511,7 @@ pub mod incoming { /// True when the player is connected to the voice gateway. pub connected: bool, /// The ping of the node to the Discord voice server in milliseconds (-1 if not connected). - pub ping: bool, + pub ping: i64, } /// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. From 0534c02a365089e9d0fb2a4fce76dc0d3cb53b9f Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 17:48:40 -0400 Subject: [PATCH 08/70] feat(lavalink): Now can read the lavalink session in the connection to send the outgoing messages to. This involved reading the READY response on the websocket --- twilight-lavalink/src/node.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 1e5ea83fcdc..18a2fa77c3b 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -473,6 +473,7 @@ struct Connection { node_to: UnboundedSender, players: PlayerManager, stats: BiLock, + lavalink_session_id: BiLock>, } impl Connection { @@ -501,6 +502,7 @@ impl Connection { node_to: to_node, players, stats, + lavalink_session_id: BiLock::new(None).0, }, to_lavalink, from_lavalink, @@ -540,20 +542,14 @@ impl Connection { Play(play) => play.guild_id, _ => todo!(), }; - - let session_id = match outgoing.clone() { - VoiceUpdate(voice_update) => voice_update.voice.session_id, - _ => todo!(), - }; - + let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); let payload = serde_json::to_string(&outgoing).unwrap(); - tracing::debug!( "forwarding event to {}: {outgoing:?}", address, ); - let url = format!("http://{address}/v4/sessions/{}/players/{guild_id}?noReplace=true", session_id.clone()).parse::().unwrap(); + let url = format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace=true").parse::().unwrap(); tracing::debug!( "converted payload: {payload:?}" @@ -630,8 +626,8 @@ impl Connection { match &event { IncomingEvent::PlayerUpdate(update) => self.player_update(update)?, + IncomingEvent::Ready(ready) => *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()), IncomingEvent::Stats(stats) => self.stats(stats).await?, - _ => {} } // It's fine if the rx end dropped, often users don't need to care about From b92b2f34c4e940248cdaefdee9356845a894a53b Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 18:16:48 -0400 Subject: [PATCH 09/70] fix(lavalink): Fixed the player track model to remove user_data since we don't have any. untagged the loadresults. Working play functionality now --- twilight-lavalink/src/http.rs | 5 ++--- twilight-lavalink/src/model.rs | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index c0877cc9206..35bed91af19 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -54,14 +54,13 @@ pub struct Track { /// The track on the player. The encoded and identifier are mutually exclusive. Using only enocded for now. /// Encoded was chosen since that was previously used in the v3 implementation. +/// We don't support userData field currently. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct UpdatePlayerTrack { /// The base64 encoded track to play. null stops the current track pub encoded: Option, - /// Additional track data to be sent back in the Track Object - pub user_data: Option, } @@ -137,7 +136,7 @@ pub enum LoadResultName { /// The type of search result given. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] -#[serde(rename_all = "camelCase")] +#[serde(untagged)] pub enum LoadResultData { /// Track result with the track info. Track(Track), diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 869399a6228..c1cb6e10e5b 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -273,7 +273,6 @@ pub mod outgoing { paused: None, track: UpdatePlayerTrack{ encoded: Some(track.into()), - user_data: None }, } } @@ -332,7 +331,6 @@ pub mod outgoing { guild_id, track: UpdatePlayerTrack { encoded: None, - user_data: None }, } } From 98e76ccd476dfc95766a619d398b45fef742fcb6 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 19:23:23 -0400 Subject: [PATCH 10/70] fix(lavalink): Working event op code now for trackStartEvent --- twilight-lavalink/src/model.rs | 25 ++++++++++++++++++++++++- twilight-lavalink/src/node.rs | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index c1cb6e10e5b..c1f59b1571c 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -448,7 +448,7 @@ pub mod incoming { /// New statistics about a node and its host. Stats(Stats), // /// Dispatched when player or voice events occur. - // Event(Event), + Event(Event), } impl From for IncomingEvent { @@ -458,6 +458,12 @@ pub mod incoming { } + impl From for IncomingEvent { + fn from(event: Event) -> IncomingEvent { + Self::Event(event) + } + } + impl From for IncomingEvent { fn from(event: PlayerUpdate) -> IncomingEvent { Self::PlayerUpdate(event) @@ -589,6 +595,22 @@ pub mod incoming { pub used: u64, } + /// Server dispatched an event. See the Event Types section for more information. + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub struct Event { + /// Op code for this websocket event. + pub op: Opcode, + /// The guild id that this was recieved from. + pub guild_id: String, + /// The type of event. + pub r#type: EventType, + /// The data of the event type. + #[serde(flatten)] + pub data: EventData, + } + /// Server dispatched an event. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -608,6 +630,7 @@ pub mod incoming { /// Server dispatched an event. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] + #[serde(untagged)] pub enum EventData { /// Dispatched when a track starts playing. TrackStartEvent(TrackStart), diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 18a2fa77c3b..737b8478bb7 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -628,6 +628,7 @@ impl Connection { IncomingEvent::PlayerUpdate(update) => self.player_update(update)?, IncomingEvent::Ready(ready) => *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()), IncomingEvent::Stats(stats) => self.stats(stats).await?, + &IncomingEvent::Event(_) => {}, } // It's fine if the rx end dropped, often users don't need to care about From 5fc885016ae9e3626516c9c2c39dfa36ea1aabb5 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 19:29:16 -0400 Subject: [PATCH 11/70] fix(lavalink): Adding todos to the guild ids for the outgoing events to parse --- twilight-lavalink/src/node.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 737b8478bb7..c8ba8952746 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -23,7 +23,6 @@ use hyper_util::rt::TokioIo; use crate::{ model::{IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, player::PlayerManager, - model::OutgoingEvent::{VoiceUpdate, Play} }; use futures_util::{ lock::BiLock, @@ -538,9 +537,14 @@ impl Connection { async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { let address = self.config.address; let guild_id = match outgoing.clone() { - VoiceUpdate(voice_update) => voice_update.guild_id, - Play(play) => play.guild_id, - _ => todo!(), + OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, + OutgoingEvent::Play(play) => play.guild_id, + OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), + OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalize guild_id."), + OutgoingEvent::Pause(_pause) => todo!("Need to implement Pause guild_id."), + OutgoingEvent::Seek(_seek) => todo!("Need to implement Seek guild_id."), + OutgoingEvent::Stop(_stop) => todo!("Need to implement Stop guild_id."), + OutgoingEvent::Volume(_volume) => todo!("Need to implement Volume guild_id."), }; let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); let payload = serde_json::to_string(&outgoing).unwrap(); From cbf10ad9ccda31e1587c49165de4c1397cb18b30 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 23:42:12 -0400 Subject: [PATCH 12/70] fix(lavalink): Adding the guild_id as no serialize for equalizer --- twilight-lavalink/src/model.rs | 1 + twilight-lavalink/src/node.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index c1f59b1571c..de0af1a59ff 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -118,6 +118,7 @@ pub mod outgoing { /// The bands to use as part of the equalizer. pub bands: Vec, /// The guild ID of the player. + #[serde(skip_serializing)] pub guild_id: Id, } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index c8ba8952746..fde2469bbcb 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -540,7 +540,7 @@ impl Connection { OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, OutgoingEvent::Play(play) => play.guild_id, OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), - OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalize guild_id."), + OutgoingEvent::Equalizer(equalize) => equalize.guild_id, OutgoingEvent::Pause(_pause) => todo!("Need to implement Pause guild_id."), OutgoingEvent::Seek(_seek) => todo!("Need to implement Seek guild_id."), OutgoingEvent::Stop(_stop) => todo!("Need to implement Stop guild_id."), From 1a5b5dd131173c9aa9c6a8c3019a767939722f07 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 23:42:44 -0400 Subject: [PATCH 13/70] test(lavalink): Adding a serialize test for string comparison of the structs in lavalink --- twilight-lavalink/src/model.rs | 72 +++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index de0af1a59ff..eae7ac520b2 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -733,7 +733,7 @@ pub use self::{ }; #[cfg(test)] -mod tests { +mod lavalink_struct_tests { use super::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, @@ -1073,3 +1073,73 @@ mod tests { ); } } + +#[cfg(test)] +mod lavalink_model_serialization_tests { + use twilight_model::id::{ + Id, + marker::GuildMarker, + }; + + use super::{ + incoming::{ + Opcode, Ready, PlayerUpdate, PlayerUpdateState + }, + }; + + #[test] + fn test_ready_serialization_deserialization() { + let ready = Ready { + op: Opcode::Ready, + resumed: false, + session_id: "la3kfsdf5eafe848".to_string(), + }; + + // Serialize + let serialized = serde_json::to_string(&ready).unwrap(); + let expected_serialized = + r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#; + assert_eq!(serialized, expected_serialized); + + // Deserialize + let deserialized: Ready = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, ready); + } + + #[test] + fn name() { + let update = PlayerUpdate { + op: Opcode::PlayerUpdate, + guild_id: Id::::new(987654321), + state: PlayerUpdateState{ + time: 1710214147839, + position: 534, + connected: true, + ping: 0, + }, + }; + + // Serialize + let serialized = serde_json::to_string(&update).unwrap(); + let expected_serialized = + r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#; + assert_eq!(serialized, expected_serialized); + + // Deserialize + let deserialized: PlayerUpdate = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, update); + } + + // #[test] + // fn test_enum_serialization_deserialization() { + // let input = MyEnum::Variant2(42); + + // // Serialize + // let serialized = serde_json::to_string(&input).unwrap(); + // assert_eq!(serialized, r#""Variant2""#); + + // // Deserialize + // let deserialized: MyEnum = serde_json::from_str(&serialized).unwrap(); + // assert_eq!(deserialized, input); + // } +} From e7435e1cd336039d1b77af016ddeeb8693b3bfb4 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 11 Mar 2024 23:51:14 -0400 Subject: [PATCH 14/70] refactor(lavalink): Cleaned up tests to have a function do the string comparision --- twilight-lavalink/src/model.rs | 59 ++++++++++++++-------------------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index eae7ac520b2..87775ab74dd 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1087,27 +1087,35 @@ mod lavalink_model_serialization_tests { }, }; + + + fn compare_json_payload serde::Deserialize<'a> + std::cmp::PartialEq> + (data_struct: T, json_payload: String) { + // Serialize + let serialized = serde_json::to_string(&data_struct).unwrap(); + let expected_serialized = json_payload; + assert_eq!(serialized, expected_serialized); + + // Deserialize + let deserialized: T = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, data_struct); + } + #[test] - fn test_ready_serialization_deserialization() { + fn should_serialize_a_ready_response() { let ready = Ready { op: Opcode::Ready, resumed: false, session_id: "la3kfsdf5eafe848".to_string(), }; - - // Serialize - let serialized = serde_json::to_string(&ready).unwrap(); - let expected_serialized = - r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#; - assert_eq!(serialized, expected_serialized); - - // Deserialize - let deserialized: Ready = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, ready); + compare_json_payload( + ready, + r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#.to_string() + ); } #[test] - fn name() { + fn should_serialize_a_player_update_response() { let update = PlayerUpdate { op: Opcode::PlayerUpdate, guild_id: Id::::new(987654321), @@ -1118,28 +1126,9 @@ mod lavalink_model_serialization_tests { ping: 0, }, }; - - // Serialize - let serialized = serde_json::to_string(&update).unwrap(); - let expected_serialized = - r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#; - assert_eq!(serialized, expected_serialized); - - // Deserialize - let deserialized: PlayerUpdate = serde_json::from_str(&serialized).unwrap(); - assert_eq!(deserialized, update); + compare_json_payload( + update, + r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#.to_string() + ); } - - // #[test] - // fn test_enum_serialization_deserialization() { - // let input = MyEnum::Variant2(42); - - // // Serialize - // let serialized = serde_json::to_string(&input).unwrap(); - // assert_eq!(serialized, r#""Variant2""#); - - // // Deserialize - // let deserialized: MyEnum = serde_json::from_str(&serialized).unwrap(); - // assert_eq!(deserialized, input); - // } } From 1cc0047d4ce6cb278363369f422ba88a3992eadc Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 00:21:24 -0400 Subject: [PATCH 15/70] tests(lavalink): Adding a test for track_start_event --- twilight-lavalink/src/model.rs | 91 +++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 87775ab74dd..d5cee9fe324 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1075,29 +1075,25 @@ mod lavalink_struct_tests { } #[cfg(test)] -mod lavalink_model_serialization_tests { +mod lavalink_incoming_model_tests { + use crate::model::TrackStart; use twilight_model::id::{ Id, marker::GuildMarker, }; - use super::{ - incoming::{ - Opcode, Ready, PlayerUpdate, PlayerUpdateState - }, - }; + use crate::http::{Track, TrackInfo}; + use super::incoming::{ + Event, EventType, EventData, Opcode, PlayerUpdate, PlayerUpdateState, Ready + }; + // These are incoming so we only need to check that the input json can deserialize into the struct. fn compare_json_payload serde::Deserialize<'a> + std::cmp::PartialEq> (data_struct: T, json_payload: String) { - // Serialize - let serialized = serde_json::to_string(&data_struct).unwrap(); - let expected_serialized = json_payload; - assert_eq!(serialized, expected_serialized); - // Deserialize - let deserialized: T = serde_json::from_str(&serialized).unwrap(); + let deserialized: T = serde_json::from_str(&json_payload).unwrap(); assert_eq!(deserialized, data_struct); } @@ -1131,4 +1127,75 @@ mod lavalink_model_serialization_tests { r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#.to_string() ); } + + #[test] + fn should_serialize_track_start_event() { + let track_start_event = Event { + op: Opcode::Event, + r#type: EventType::TrackStartEvent, + guild_id: Id::::new(987654321).to_string(), + data: EventData::TrackStartEvent( + TrackStart { track: Track { + encoded: "QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string(), + info: TrackInfo { + identifier: "OnuuYcqhzCE".to_string(), + is_seekable: true, + author: "Linkin Park".to_string(), + length: 169000, + is_stream: false, + position: 0, + title: "Bleed It Out [Official Music Video] - Linkin Park".to_string(), + uri:Some("https://www.youtube.com/watch?v=OnuuYcqhzCE".to_string()), + source_name:"youtube".to_string(), + artwork_url:Some("https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg".to_string()), + isrc: None + } + } }) + + }; + compare_json_payload( + track_start_event.clone(), + r#"{"op":"event","guildId":"987654321","type":"TrackStartEvent","track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA","info":{"identifier":"OnuuYcqhzCE","isSeekable":true,"author":"Linkin Park","length":169000,"isStream":false,"position":0,"title":"Bleed It Out [Official Music Video] - Linkin Park","uri":"https://www.youtube.com/watch?v=OnuuYcqhzCE","artworkUrl":"https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{},"userData":{}}}"#.to_string() + ); + } +} + + +#[cfg(test)] +mod lavalink_outgoing_model_tests { + use twilight_model::id::{ + Id, + marker::GuildMarker, + }; + + use super::outgoing::{ + VoiceUpdate, Voice + }; + + + // For some of the outgoing we have fields that don't get deserialized. We only need + // to check weather the serialization is working. + fn compare_json_payload + (data_struct: T, json_payload: String) { + + let serialized = serde_json::to_string(&data_struct).unwrap(); + let expected_serialized = json_payload; + assert_eq!(serialized, expected_serialized); + } + + #[test] + fn should_serialize_an_outgoing_voice_update() { + let voice = VoiceUpdate { + guild_id: Id::::new(987654321), + voice: Voice{ + token: String::from("863ea8ef2ads8ef2"), + endpoint: String::from("eu-centra654863.discord.media:443"), + session_id: String::from("asdf5w1efa65feaf315e8a8effsa1e5f"), + }, + }; + compare_json_payload( + voice, + r#"{"voice":{"token":"863ea8ef2ads8ef2","endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f"}}"#.to_string() + ); + } } From c26de2212227df1d349371e4b74b4cce782dfa2f Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 00:35:28 -0400 Subject: [PATCH 16/70] tests(lavalink): The play struct and command is tested against a good json string to lavalink --- twilight-lavalink/src/model.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index d5cee9fe324..1361dac8946 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1163,13 +1163,16 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { + use crate::model::Play; + use crate::http::UpdatePlayerTrack; + use twilight_model::id::{ Id, marker::GuildMarker, }; use super::outgoing::{ - VoiceUpdate, Voice + OutgoingEvent, VoiceUpdate, Voice, }; @@ -1198,4 +1201,23 @@ mod lavalink_outgoing_model_tests { r#"{"voice":{"token":"863ea8ef2ads8ef2","endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f"}}"#.to_string() ); } + + #[test] + fn should_serialize_an_outgoing_play() { + let play = OutgoingEvent::Play(Play{ + track: UpdatePlayerTrack { + encoded: Some("QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string()), + }, + position: None, + end_time: Some(None), + volume: None, + paused: None, + guild_id: Id::::new(987654321), + no_replace: true, + }); + compare_json_payload( + play, + r#"{"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"},"endTime":null}"#.to_string() + ); + } } From e33c8ceb12421364450b0a8fbf7772db168749c3 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 00:46:54 -0400 Subject: [PATCH 17/70] feat(lavalink): Working stop now. Needed to put no replace in to false in the endpoint --- twilight-lavalink/src/model.rs | 277 ++------------------------------- twilight-lavalink/src/node.rs | 12 +- 2 files changed, 22 insertions(+), 267 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 1361dac8946..4cea4efb66c 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -734,267 +734,8 @@ pub use self::{ #[cfg(test)] mod lavalink_struct_tests { - use super::{ - incoming::{ - IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, - StatsMemory, TrackEnd, TrackStart, WebsocketClosed, - }, - outgoing::{ - Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, - Volume, - }, - }; - use serde::{Deserialize, Serialize}; + use super::incoming::{ Stats, StatsCpu, StatsMemory, }; use serde_test::Token; - use static_assertions::{assert_fields, assert_impl_all}; - use std::fmt::Debug; - use twilight_model::{ - gateway::payload::incoming::VoiceServerUpdate, - id::{marker::GuildMarker, Id}, - }; - - assert_fields!(Destroy: guild_id); - assert_impl_all!( - Destroy: Clone, - Debug, - Deserialize<'static>, - Eq, - From>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(EqualizerBand: band, gain); - assert_impl_all!( - EqualizerBand: Clone, - Debug, - Deserialize<'static>, - From<(i64, f64)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Equalizer: bands, guild_id); - assert_impl_all!( - Equalizer: Clone, - Debug, - Deserialize<'static>, - From<(Id, Vec)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_impl_all!( - IncomingEvent: Clone, - Debug, - Deserialize<'static>, - From, - From, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_impl_all!( - OutgoingEvent: Clone, - Debug, - Deserialize<'static>, - From, - From, - From, - From, - From, - From, - From, - From, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Pause: guild_id, paused); - assert_impl_all!( - Pause: Clone, - Debug, - Deserialize<'static>, - Eq, - From<(Id, bool)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(PlayerUpdateState: position, time); - assert_impl_all!( - PlayerUpdateState: Clone, - Debug, - Deserialize<'static>, - Eq, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(PlayerUpdate: guild_id, state); - assert_impl_all!( - PlayerUpdate: Clone, - Debug, - Deserialize<'static>, - Eq, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Play: end_time, guild_id, no_replace, position, track); - assert_impl_all!( - Play: Clone, - Debug, - Deserialize<'static>, - Eq, - From<(Id, String)>, - From<(Id, String, Option)>, - From<(Id, String, u64)>, - From<(Id, String, Option, Option)>, - From<(Id, String, Option, u64)>, - From<(Id, String, u64, Option)>, - From<(Id, String, u64, u64)>, - From<(Id, String, Option, Option, bool)>, - From<(Id, String, Option, u64, bool)>, - From<(Id, String, u64, Option, bool)>, - From<(Id, String, u64, u64, bool)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Seek: guild_id, position); - assert_impl_all!( - Seek: Clone, - Debug, - Deserialize<'static>, - Eq, - From<(Id, i64)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!( - Stats: cpu, - frame_stats, - memory, - players, - playing_players, - uptime - ); - assert_impl_all!( - Stats: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(StatsCpu: cores, lavalink_load, system_load); - assert_impl_all!( - StatsCpu: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(StatsFrames: deficit, nulled, sent); - assert_impl_all!( - StatsFrames: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(StatsMemory: allocated, free, reservable, used); - assert_impl_all!( - StatsMemory: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Stop: guild_id); - assert_impl_all!( - Stop: Clone, - Debug, - Deserialize<'static>, - Eq, - From>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(TrackEnd: reason, track); - assert_impl_all!( - TrackEnd: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(TrackStart: track); - assert_impl_all!( - TrackStart: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(WebsocketClosed: code, reason, by_remote); - assert_impl_all!( - WebsocketClosed: Clone, - Debug, - Deserialize<'static>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(VoiceUpdate: guild_id, voice); - assert_impl_all!( - VoiceUpdate: Clone, - Debug, - Deserialize<'static>, - Eq, - From<(Id, String, VoiceServerUpdate)>, - PartialEq, - Send, - Serialize, - Sync, - ); - assert_fields!(Volume: guild_id, volume); - assert_impl_all!( - Volume: Clone, - Debug, - Deserialize<'static>, - Eq, - PartialEq, - Send, - Serialize, - Sync, - ); #[test] fn stats_frames_not_provided() { @@ -1163,7 +904,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::Play; + use crate::model::{Play, Stop}; use crate::http::UpdatePlayerTrack; use twilight_model::id::{ @@ -1220,4 +961,18 @@ mod lavalink_outgoing_model_tests { r#"{"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"},"endTime":null}"#.to_string() ); } + + #[test] + fn should_serialize_an_outgoing_stop() { + let stop = OutgoingEvent::Stop(Stop{ + track: UpdatePlayerTrack { + encoded: None, + }, + guild_id: Id::::new(987654321), + }); + compare_json_payload( + stop, + r#"{"track":{"encoded":null}}"#.to_string() + ); + } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index fde2469bbcb..e7c9f1e63d1 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -536,14 +536,14 @@ impl Connection { async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { let address = self.config.address; - let guild_id = match outgoing.clone() { - OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, - OutgoingEvent::Play(play) => play.guild_id, + let (guild_id, no_replace) = match outgoing.clone() { + OutgoingEvent::VoiceUpdate(voice_update) => (voice_update.guild_id, true), + OutgoingEvent::Play(play) => (play.guild_id, play.no_replace), OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), - OutgoingEvent::Equalizer(equalize) => equalize.guild_id, + OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalizer guild_id."), OutgoingEvent::Pause(_pause) => todo!("Need to implement Pause guild_id."), OutgoingEvent::Seek(_seek) => todo!("Need to implement Seek guild_id."), - OutgoingEvent::Stop(_stop) => todo!("Need to implement Stop guild_id."), + OutgoingEvent::Stop(stop) => (stop.guild_id, false), OutgoingEvent::Volume(_volume) => todo!("Need to implement Volume guild_id."), }; let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); @@ -553,7 +553,7 @@ impl Connection { "forwarding event to {}: {outgoing:?}", address, ); - let url = format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace=true").parse::().unwrap(); + let url = format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}").parse::().unwrap(); tracing::debug!( "converted payload: {payload:?}" From ec4369726530fa8fadaf435f93804f85b7b2b2cb Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 00:51:20 -0400 Subject: [PATCH 18/70] feat(lavalink): Working pause function in new v4 --- twilight-lavalink/src/model.rs | 14 +++++++++++++- twilight-lavalink/src/node.rs | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 4cea4efb66c..dc1d16c7d76 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -904,7 +904,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::{Play, Stop}; + use crate::model::{Play, Stop, Pause}; use crate::http::UpdatePlayerTrack; use twilight_model::id::{ @@ -975,4 +975,16 @@ mod lavalink_outgoing_model_tests { r#"{"track":{"encoded":null}}"#.to_string() ); } + + #[test] + fn should_serialize_an_outgoing_pause() { + let pause = OutgoingEvent::Pause(Pause{ + paused: true, + guild_id: Id::::new(987654321), + }); + compare_json_payload( + pause, + r#"{"guildId":"987654321","paused":true}"#.to_string() + ); + } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index e7c9f1e63d1..b3bdeeedf16 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -541,7 +541,7 @@ impl Connection { OutgoingEvent::Play(play) => (play.guild_id, play.no_replace), OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalizer guild_id."), - OutgoingEvent::Pause(_pause) => todo!("Need to implement Pause guild_id."), + OutgoingEvent::Pause(pause) => (pause.guild_id, true), OutgoingEvent::Seek(_seek) => todo!("Need to implement Seek guild_id."), OutgoingEvent::Stop(stop) => (stop.guild_id, false), OutgoingEvent::Volume(_volume) => todo!("Need to implement Volume guild_id."), From 4d87d9113b2eef38bea716ae331ea669a634bb6e Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 00:57:49 -0400 Subject: [PATCH 19/70] feat(lavalink): Working seek and volume now. --- twilight-lavalink/src/model.rs | 26 +++++++++++++++++++++++++- twilight-lavalink/src/node.rs | 4 ++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index dc1d16c7d76..194f2f5146c 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -904,7 +904,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::{Play, Stop, Pause}; + use crate::model::{Play, Stop, Pause, Volume, Seek}; use crate::http::UpdatePlayerTrack; use twilight_model::id::{ @@ -987,4 +987,28 @@ mod lavalink_outgoing_model_tests { r#"{"guildId":"987654321","paused":true}"#.to_string() ); } + + #[test] + fn should_serialize_an_outgoing_seek() { + let seek = OutgoingEvent::Seek(Seek{ + position: 66000, + guild_id: Id::::new(987654321), + }); + compare_json_payload( + seek, + r#"{"position":66000}"#.to_string() + ); + } + + #[test] + fn should_serialize_an_outgoing_volume() { + let volume = OutgoingEvent::Volume(Volume{ + volume: 50, + guild_id: Id::::new(987654321), + }); + compare_json_payload( + volume, + r#"{"volume":50}"#.to_string() + ); + } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index b3bdeeedf16..f1513ff0fd8 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -542,9 +542,9 @@ impl Connection { OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalizer guild_id."), OutgoingEvent::Pause(pause) => (pause.guild_id, true), - OutgoingEvent::Seek(_seek) => todo!("Need to implement Seek guild_id."), + OutgoingEvent::Seek(seek) => (seek.guild_id, true), OutgoingEvent::Stop(stop) => (stop.guild_id, false), - OutgoingEvent::Volume(_volume) => todo!("Need to implement Volume guild_id."), + OutgoingEvent::Volume(volume) => (volume.guild_id, true), }; let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); let payload = serde_json::to_string(&outgoing).unwrap(); From 09e57faeaa67c988ce388e2c9d80af3f2c2f67de Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 01:18:08 -0400 Subject: [PATCH 20/70] feat(lavalink): Stop now works in v4. Refactored the node some to change the endpoints based on the event. Destroy changes the method and uri --- twilight-lavalink/src/model.rs | 14 +++++++++++++- twilight-lavalink/src/node.rs | 31 +++++++++++++++++-------------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 194f2f5146c..1b87877cce7 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -904,7 +904,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::{Play, Stop, Pause, Volume, Seek}; + use crate::model::{Play, Stop, Pause, Volume, Seek, Destroy}; use crate::http::UpdatePlayerTrack; use twilight_model::id::{ @@ -1011,4 +1011,16 @@ mod lavalink_outgoing_model_tests { r#"{"volume":50}"#.to_string() ); } + + + #[test] + fn should_serialize_an_outgoing_destroy_aka_leave() { + let destroy = OutgoingEvent::Destroy(Destroy{ + guild_id: Id::::new(987654321), + }); + compare_json_payload( + destroy, + r#"{"guildId":"987654321"}"#.to_string() + ); + } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index f1513ff0fd8..7f800e4d99a 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -534,12 +534,17 @@ impl Connection { Ok(()) } - async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { + async fn get_outgoing_endpoint_based_on_event(&self, outgoing: &OutgoingEvent) -> (Method, hyper::Uri) { let address = self.config.address; + tracing::debug!( + "forwarding event to {}: {outgoing:?}", + address, + ); + let (guild_id, no_replace) = match outgoing.clone() { OutgoingEvent::VoiceUpdate(voice_update) => (voice_update.guild_id, true), OutgoingEvent::Play(play) => (play.guild_id, play.no_replace), - OutgoingEvent::Destroy(_destroy) => todo!("This is a unique case that has a different endpoint."), + OutgoingEvent::Destroy(destroy) => (destroy.guild_id, true), OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalizer guild_id."), OutgoingEvent::Pause(pause) => (pause.guild_id, true), OutgoingEvent::Seek(seek) => (seek.guild_id, true), @@ -547,29 +552,28 @@ impl Connection { OutgoingEvent::Volume(volume) => (volume.guild_id, true), }; let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); - let payload = serde_json::to_string(&outgoing).unwrap(); - tracing::debug!( - "forwarding event to {}: {outgoing:?}", - address, - ); - let url = format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}").parse::().unwrap(); + match outgoing.clone() { + OutgoingEvent::Destroy(_) => (Method::DELETE, format!("http://{address}/v4/sessions/{session}/players/{guild_id}").parse::().unwrap()), + _ => (Method::PATCH, format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}").parse::().unwrap()), + } + } + + async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { + let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await; + let payload = serde_json::to_string(&outgoing).unwrap(); tracing::debug!( "converted payload: {payload:?}" ); - // Get the host and the port let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); let address = format!("{}:{}", host, port); - // Open a TCP connection to the remote host let stream = TcpStream::connect(address).await.unwrap(); - // Use an adapter to access something implementing `tokio::io` traits as if they implement - // `hyper::rt` IO traits. let io = TokioIo::new(stream); // Create the Hyper client @@ -586,7 +590,7 @@ impl Connection { // Create an HTTP request with an empty body and a HOST header let req = Request::builder() .uri(url) - .method(Method::PATCH) + .method(method) .header(hyper::header::HOST, authority.as_str()) .header(AUTHORIZATION, self.config.authorization.as_str()) .header("Content-Type", "application/json") @@ -597,7 +601,6 @@ impl Connection { "Request: {req:?}" ); - // Await the response... let res = sender.send_request(req).await.unwrap(); tracing::debug!("Response status: {}", res.status()); From b48ec2b5db6dcfe9d342620d65e7de112baa106b Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 16:42:13 -0400 Subject: [PATCH 21/70] test(lavalink): Adding stats tests for the stats structures --- twilight-lavalink/src/model.rs | 51 +++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 1b87877cce7..0783e503cfe 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -543,7 +543,7 @@ pub mod incoming { pub cpu: StatsCpu, /// Statistics about audio frames. #[serde(rename = "frameStats", skip_serializing_if = "Option::is_none")] - pub frame_stats: Option, + pub frame_stats: Option, /// Memory information about the node's host. pub memory: StatsMemory, /// The current number of total players (active and not active) within @@ -572,13 +572,13 @@ pub mod incoming { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] - pub struct StatsFrames { + pub struct StatsFrame { /// The number of CPU cores. - pub sent: u64, + pub sent: i64, /// The load of the Lavalink server. - pub nulled: u64, + pub nulled: i64, /// The load of the system as a whole. - pub deficit: u64, + pub deficit: i64, } /// Memory information about a node and its host. @@ -723,7 +723,7 @@ pub mod incoming { pub use self::{ incoming::{ - IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrames, StatsMemory, + IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame, StatsMemory, TrackEnd, TrackStart, TrackStuck, TrackException, WebsocketClosed, }, outgoing::{ @@ -826,7 +826,8 @@ mod lavalink_incoming_model_tests { use crate::http::{Track, TrackInfo}; use super::incoming::{ - Event, EventType, EventData, Opcode, PlayerUpdate, PlayerUpdateState, Ready + Event, EventType, EventData, Opcode, PlayerUpdate, PlayerUpdateState, Ready, + Stats, StatsCpu, StatsMemory, StatsFrame }; @@ -869,6 +870,42 @@ mod lavalink_incoming_model_tests { ); } + #[test] + fn should_serialize_stat_event() { + let stat_event = Stats { + op: Opcode::Stats, + players: 0, + playing_players: 0, + uptime: 1139738, + cpu: StatsCpu { cores: 16, lavalink_load: 3.497090420769919E-5, system_load: 0.05597997834786306 }, + frame_stats: None, + memory: StatsMemory { allocated: 331350016, free: 228139904, reservable: 8396996608, used: 103210112 } + + }; + compare_json_payload( + stat_event.clone(), + r#"{"op":"stats","frameStats":null,"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#.to_string() + ); + } + + #[test] + fn should_serialize_stat_event_with_frame_stat() { + let stat_event = Stats { + op: Opcode::Stats, + players: 0, + playing_players: 0, + uptime: 1139738, + cpu: StatsCpu { cores: 16, lavalink_load: 3.497090420769919E-5, system_load: 0.05597997834786306 }, + frame_stats: Some(StatsFrame{ sent: 6000, nulled: 10, deficit: -3010}), + memory: StatsMemory { allocated: 331350016, free: 228139904, reservable: 8396996608, used: 103210112 }, + + }; + compare_json_payload( + stat_event.clone(), + r#"{"op":"stats","frameStats":{"sent":6000,"nulled":10,"deficit":-3010},"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#.to_string() + ); + } + #[test] fn should_serialize_track_start_event() { let track_start_event = Event { From a498dd9a751738b6923781c7c081eb5f00b9f259 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 16:44:29 -0400 Subject: [PATCH 22/70] refactor(lavalink): fixed the test names for the incoming to be only deserialize. Fixed warning about missing doc --- twilight-lavalink/src/model.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 0783e503cfe..1404ec0f225 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -448,7 +448,7 @@ pub mod incoming { PlayerUpdate(PlayerUpdate), /// New statistics about a node and its host. Stats(Stats), - // /// Dispatched when player or voice events occur. + /// Dispatched when player or voice events occur. Event(Event), } @@ -832,7 +832,7 @@ mod lavalink_incoming_model_tests { // These are incoming so we only need to check that the input json can deserialize into the struct. - fn compare_json_payload serde::Deserialize<'a> + std::cmp::PartialEq> + fn compare_json_payload serde::Deserialize<'a> + std::cmp::PartialEq> (data_struct: T, json_payload: String) { // Deserialize let deserialized: T = serde_json::from_str(&json_payload).unwrap(); @@ -840,7 +840,7 @@ mod lavalink_incoming_model_tests { } #[test] - fn should_serialize_a_ready_response() { + fn should_deserialize_a_ready_response() { let ready = Ready { op: Opcode::Ready, resumed: false, @@ -853,7 +853,7 @@ mod lavalink_incoming_model_tests { } #[test] - fn should_serialize_a_player_update_response() { + fn should_deserialize_a_player_update_response() { let update = PlayerUpdate { op: Opcode::PlayerUpdate, guild_id: Id::::new(987654321), @@ -871,7 +871,7 @@ mod lavalink_incoming_model_tests { } #[test] - fn should_serialize_stat_event() { + fn should_deserialize_stat_event() { let stat_event = Stats { op: Opcode::Stats, players: 0, @@ -889,7 +889,7 @@ mod lavalink_incoming_model_tests { } #[test] - fn should_serialize_stat_event_with_frame_stat() { + fn should_deserialize_stat_event_with_frame_stat() { let stat_event = Stats { op: Opcode::Stats, players: 0, @@ -907,7 +907,7 @@ mod lavalink_incoming_model_tests { } #[test] - fn should_serialize_track_start_event() { + fn should_deserialize_track_start_event() { let track_start_event = Event { op: Opcode::Event, r#type: EventType::TrackStartEvent, From 2c5e10ee46e1a2b896faaf0d0acb7bbd70616398 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 17:06:28 -0400 Subject: [PATCH 23/70] feat(lavalink): Adding in support and tests for equalizer. Added a command to the basic bot to test this out as well. --- examples/lavalink-basic-bot.rs | 51 +++++++++++++++++++++++++++++++++- twilight-lavalink/src/model.rs | 30 +++++++++++++++++--- twilight-lavalink/src/node.rs | 2 +- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index e8db450028a..e55b6c11e2f 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -1,4 +1,4 @@ -use twilight_lavalink::http::LoadResultData::{Search, Track, Playlist}; +use twilight_lavalink::{http::LoadResultData::{Playlist, Search, Track}, model::{Equalizer, EqualizerBand}}; use http_body_util::{BodyExt, Full}; use hyper::{body::Bytes, Request}; use hyper_util::{ @@ -97,6 +97,7 @@ async fn main() -> anyhow::Result<()> { Some("!seek") => spawn(seek(msg.0, Arc::clone(&state))), Some("!stop") => spawn(stop(msg.0, Arc::clone(&state))), Some("!volume") => spawn(volume(msg.0, Arc::clone(&state))), + Some("!equalize") => spawn(equalize(msg.0, Arc::clone(&state))), _ => continue, } } @@ -280,6 +281,54 @@ async fn seek(msg: Message, state: State) -> anyhow::Result<()> { Ok(()) } +async fn equalize(msg: Message, state: State) -> anyhow::Result<()> { + tracing::debug!( + "equalize command in channel {} by {}", + msg.channel_id, + msg.author.name + ); + state + .http + .create_message(msg.channel_id) + .content("What band do you want to equalize (0-14)?") + .await?; + + let author_id = msg.author.id; + let band_msg = state + .standby + .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { + new_msg.author.id == author_id + }) + .await?; + let guild_id = msg.guild_id.unwrap(); + let band = band_msg.content.parse::()?; + + state + .http + .create_message(msg.channel_id) + .content("What gain do you want to equalize (-0.25 to 1.0)?") + .await?; + + let gain_msg = state + .standby + .wait_for_message(msg.channel_id, move |new_msg: &MessageCreate| { + new_msg.author.id == author_id + }) + .await?; + let gain = gain_msg.content.parse::()?; + + let player = state.lavalink.player(guild_id).await.unwrap(); + player.send(Equalizer::from((guild_id, vec![EqualizerBand::new(band, gain)])))?; + + state + .http + .create_message(msg.channel_id) + .content(&format!("Changed gain level to {gain} on band {band}.")) + .await?; + + Ok(()) +} + async fn stop(msg: Message, state: State) -> anyhow::Result<()> { tracing::debug!( "stop command in channel {} by {}", diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 1404ec0f225..1a44150cb98 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -108,6 +108,14 @@ pub mod outgoing { } } + /// Filters to pass to the update player endpoint. Currently only Equalizer is supported. + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] + #[non_exhaustive] + #[serde(rename_all = "camelCase")] + pub enum Filters { + /// Adjusts 15 different bands + Equalizer(Equalizer), + } /// Equalize a player. @@ -116,7 +124,7 @@ pub mod outgoing { #[serde(rename_all = "camelCase")] pub struct Equalizer { /// The bands to use as part of the equalizer. - pub bands: Vec, + pub equalizer: Vec, /// The guild ID of the player. #[serde(skip_serializing)] pub guild_id: Id, @@ -132,7 +140,7 @@ pub mod outgoing { impl From<(Id, Vec)> for Equalizer { fn from((guild_id, bands): (Id, Vec)) -> Self { Self { - bands, + equalizer: bands, guild_id, } } @@ -941,7 +949,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::{Play, Stop, Pause, Volume, Seek, Destroy}; + use crate::model::{Play, Stop, Pause, Volume, Seek, Destroy, Equalizer}; use crate::http::UpdatePlayerTrack; use twilight_model::id::{ @@ -950,8 +958,9 @@ mod lavalink_outgoing_model_tests { }; use super::outgoing::{ - OutgoingEvent, VoiceUpdate, Voice, + OutgoingEvent, VoiceUpdate, Voice }; + use super::EqualizerBand; // For some of the outgoing we have fields that don't get deserialized. We only need @@ -1060,4 +1069,17 @@ mod lavalink_outgoing_model_tests { r#"{"guildId":"987654321"}"#.to_string() ); } + + #[test] + fn should_serialize_an_outgoing_equalize() { + let equalize = OutgoingEvent::Equalizer(Equalizer { + equalizer: vec![EqualizerBand::new(5, -0.15)], + guild_id: Id::::new(987654321), + }); + compare_json_payload( + equalize, + r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#.to_string() + ); + + } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 7f800e4d99a..2f22ab31065 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -545,7 +545,7 @@ impl Connection { OutgoingEvent::VoiceUpdate(voice_update) => (voice_update.guild_id, true), OutgoingEvent::Play(play) => (play.guild_id, play.no_replace), OutgoingEvent::Destroy(destroy) => (destroy.guild_id, true), - OutgoingEvent::Equalizer(_equalize) => todo!("Need to implement Equalizer guild_id."), + OutgoingEvent::Equalizer(equalize) => (equalize.guild_id, true), OutgoingEvent::Pause(pause) => (pause.guild_id, true), OutgoingEvent::Seek(seek) => (seek.guild_id, true), OutgoingEvent::Stop(stop) => (stop.guild_id, false), From c3f79de7fb32519d7534d2f819d0e8f700199d23 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 17:44:50 -0400 Subject: [PATCH 24/70] refactor(lavalink): moved the modules from model.rs into their own files incoming.rs, and outgoing.rs --- twilight-lavalink/src/model.rs | 729 +----------------------- twilight-lavalink/src/model/incoming.rs | 304 ++++++++++ twilight-lavalink/src/model/outgoing.rs | 418 ++++++++++++++ 3 files changed, 724 insertions(+), 727 deletions(-) create mode 100644 twilight-lavalink/src/model/incoming.rs create mode 100644 twilight-lavalink/src/model/outgoing.rs diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 1a44150cb98..951b6e4457c 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1,733 +1,8 @@ //! Models to (de)serialize incoming/outgoing websocket events and HTTP //! responses. -pub mod outgoing { - //! Events that clients send to Lavalink. - use serde::{Deserialize, Serialize}; - use twilight_model::{ - gateway::payload::incoming::VoiceServerUpdate, - id::{marker::GuildMarker, Id}, - }; - - use crate::http::UpdatePlayerTrack; - - /// An outgoing event to send to Lavalink. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(untagged)] - pub enum OutgoingEvent { - /// Destroy a player for a guild. - Destroy(Destroy), - /// Equalize a player. - Equalizer(Equalizer), - /// Pause or unpause a player. - Pause(Pause), - /// Play a track. - Play(Play), - /// Seek a player's active track to a new position. - Seek(Seek), - /// Stop a player. - Stop(Stop), - /// A combined voice server and voice state update. - VoiceUpdate(VoiceUpdate), - /// Set the volume of a player. - Volume(Volume), - } - - impl From for OutgoingEvent { - fn from(event: Destroy) -> OutgoingEvent { - Self::Destroy(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Equalizer) -> OutgoingEvent { - Self::Equalizer(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Pause) -> OutgoingEvent { - Self::Pause(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Play) -> OutgoingEvent { - Self::Play(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Seek) -> OutgoingEvent { - Self::Seek(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Stop) -> OutgoingEvent { - Self::Stop(event) - } - } - - impl From for OutgoingEvent { - fn from(event: VoiceUpdate) -> OutgoingEvent { - Self::VoiceUpdate(event) - } - } - - impl From for OutgoingEvent { - fn from(event: Volume) -> OutgoingEvent { - Self::Volume(event) - } - } - - /// Destroy a player from a node. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Destroy { - /// The guild ID of the player. - pub guild_id: Id, - } - - impl Destroy { - /// Create a new destroy event. - pub const fn new(guild_id: Id) -> Self { - Self { - guild_id, - } - } - } - - impl From> for Destroy { - fn from(guild_id: Id) -> Self { - Self { - guild_id, - } - } - } - - /// Filters to pass to the update player endpoint. Currently only Equalizer is supported. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub enum Filters { - /// Adjusts 15 different bands - Equalizer(Equalizer), - } - - - /// Equalize a player. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Equalizer { - /// The bands to use as part of the equalizer. - pub equalizer: Vec, - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - } - - impl Equalizer { - /// Create a new equalizer event. - pub fn new(guild_id: Id, bands: Vec) -> Self { - Self::from((guild_id, bands)) - } - } - - impl From<(Id, Vec)> for Equalizer { - fn from((guild_id, bands): (Id, Vec)) -> Self { - Self { - equalizer: bands, - guild_id, - } - } - } - - /// A band of the equalizer event. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct EqualizerBand { - /// The band. - pub band: i64, - /// The gain. - pub gain: f64, - } - - impl EqualizerBand { - /// Create a new equalizer band. - pub fn new(band: i64, gain: f64) -> Self { - Self::from((band, gain)) - } - } - - impl From<(i64, f64)> for EqualizerBand { - fn from((band, gain): (i64, f64)) -> Self { - Self { band, gain } - } - } - - /// Pause or unpause a player. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Pause { - /// The guild ID of the player. - pub guild_id: Id, - /// Whether to pause the player. - /// - /// Set to `true` to pause or `false` to resume. - pub paused: bool, - } - - impl Pause { - /// Create a new pause event. - /// - /// Set to `true` to pause the player or `false` to resume it. - pub fn new(guild_id: Id, pause: bool) -> Self { - Self::from((guild_id, pause)) - } - } - - impl From<(Id, bool)> for Pause { - fn from((guild_id, pause): (Id, bool)) -> Self { - Self { - guild_id, - paused: pause, - } - } - } - - - // TODO: Might need to fix this struct to abstract the guild_id to another struct pending on what the server sends back with it included. - /// Play a track, optionally specifying to not skip the current track. Filters are not supported at the moment. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Play { - /// Information about the track to play. - pub track: UpdatePlayerTrack, - /// The position in milliseconds to start the track from. - #[serde(skip_serializing_if = "Option::is_none")] - pub position: Option, - /// The position in milliseconds to end the track. - #[serde(skip_serializing_if = "Option::is_none")] - pub end_time: Option>, - /// The player volume, in percentage, from 0 to 1000 - #[serde(skip_serializing_if = "Option::is_none")] - pub volume: Option, - /// Whether the player is paused - #[serde(skip_serializing_if = "Option::is_none")] - pub paused: Option, - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - /// Whether or not to replace the currently playing track with this new - /// track. - /// - /// Set to `true` to keep playing the current playing track, or `false` - /// to replace the current playing track with a new one. - #[serde(skip_serializing)] - pub no_replace: bool, - } - - impl Play { - /// Create a new play event. - pub fn new( - guild_id: Id, - track: impl Into, - start_time: impl Into>, - end_time: impl Into>, - no_replace: bool, - ) -> Self { - Self::from((guild_id, track, start_time, end_time, no_replace)) - } - } - - impl> From<(Id, T)> for Play { - fn from((guild_id, track): (Id, T)) -> Self { - Self::from((guild_id, track, None, None, true)) - } - } - - impl, S: Into>> From<(Id, T, S)> for Play { - fn from((guild_id, track, start_time): (Id, T, S)) -> Self { - Self::from((guild_id, track, start_time, None, true)) - } - } - - impl, S: Into>, E: Into>> - From<(Id, T, S, E)> for Play - { - fn from((guild_id, track, start_time, end_time): (Id, T, S, E)) -> Self { - Self::from((guild_id, track, start_time, end_time, true)) - } - } - - impl, S: Into>, E: Into>> - From<(Id, T, S, E, bool)> for Play - { - fn from( - (guild_id, track, start_time, end_time, no_replace): (Id, T, S, E, bool), - ) -> Self { - Self { - guild_id, - no_replace, - position: start_time.into(), - end_time: Some(end_time.into()), - volume: None, - paused: None, - track: UpdatePlayerTrack{ - encoded: Some(track.into()), - }, - } - } - } - - /// Seek a player's active track to a new position. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Seek { - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - /// The position in milliseconds to seek to. - pub position: i64, - } - - impl Seek { - /// Create a new seek event. - pub fn new(guild_id: Id, position: i64) -> Self { - Self::from((guild_id, position)) - } - } - - impl From<(Id, i64)> for Seek { - fn from((guild_id, position): (Id, i64)) -> Self { - Self { - guild_id, - position, - } - } - } - - /// Stop a player. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Stop { - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - /// The track object to pass set to null - pub track: UpdatePlayerTrack, - } - - impl Stop { - /// Create a new stop event. - pub fn new(guild_id: Id) -> Self { - Self::from(guild_id) - } - } - - impl From> for Stop { - fn from(guild_id: Id) -> Self { - Self { - guild_id, - track: UpdatePlayerTrack { - encoded: None, - }, - } - } - } - /// The voice payload for the combined server and state to send to lavalink. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Voice { - /// The Discord voice token to authenticate with. - pub token: String, - /// The Discord voice endpoint to connect to. - pub endpoint: String, - /// The Discord voice session id to authenticate with. This is seperate from the session id of lavalink. - pub session_id: String, - } - - /// A combined voice server and voice state update. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct VoiceUpdate { - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - /// The voice payload for the combined server and state to send to lavalink. - pub voice: Voice, - } - - impl VoiceUpdate { - /// Create a new voice update event. - pub fn new( - guild_id: Id, - session_id: impl Into, - event: VoiceServerUpdate, - ) -> Self { - Self::from((guild_id, session_id, event)) - } - } - - impl> From<(Id, T, VoiceServerUpdate)> for VoiceUpdate { - fn from((guild_id, session_id, event): (Id, T, VoiceServerUpdate)) -> Self { - Self { - guild_id: guild_id, - voice: Voice{ - token: event.token, - endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), - session_id: session_id.into(), - } - } - } - } - - /// Set the volume of a player. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Volume { - /// The guild ID of the player. - #[serde(skip_serializing)] - pub guild_id: Id, - /// The volume of the player from 0 to 1000. 100 is the default. - pub volume: i64, - } - - impl Volume { - /// Create a new volume event. - pub fn new(guild_id: Id, volume: i64) -> Self { - Self::from((guild_id, volume)) - } - } - - impl From<(Id, i64)> for Volume { - fn from((guild_id, volume): (Id, i64)) -> Self { - Self { - guild_id, - volume, - } - } - } -} - -pub mod incoming { - //! Events that Lavalink sends to clients. - - /// The type of event that something is. - #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub enum Opcode { - /// Lavalink is connected and ready. - Ready, - /// An update about a player's current track. - PlayerUpdate, - /// Updated statistics about a node. - Stats, - /// Meta information about a track starting or ending. - Event, - } - - - use crate::http::{Track, Exception}; - use serde::{Deserialize, Serialize}; - use twilight_model::id::{marker::GuildMarker, Id}; - - /// An incoming event from a Lavalink node. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(untagged)] - pub enum IncomingEvent { - /// Dispatched when you successfully connect to the Lavalink node. - Ready(Ready), - /// An update about the information of a player. - PlayerUpdate(PlayerUpdate), - /// New statistics about a node and its host. - Stats(Stats), - /// Dispatched when player or voice events occur. - Event(Event), - } - - impl From for IncomingEvent { - fn from(event: Ready) -> IncomingEvent { - Self::Ready(event) - } - } - - - impl From for IncomingEvent { - fn from(event: Event) -> IncomingEvent { - Self::Event(event) - } - } - - impl From for IncomingEvent { - fn from(event: PlayerUpdate) -> IncomingEvent { - Self::PlayerUpdate(event) - } - } - - impl From for IncomingEvent { - fn from(event: Stats) -> IncomingEvent { - Self::Stats(event) - } - } - - /// The discord voice information that lavalink uses for connection and sending information. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct VoiceState { - /// The Discord voice token to authenticate with. - pub token: String, - /// The Discord voice endpoint to connect to. - pub endpoint: String, - /// The Discord voice session id to authenticate with. Note this is seperate from the lavalink session id. - pub session_id: String, - } - - /// An update about the information of a player. Filters are currently unsupported - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct PlayerUpdate { - /// Op code for this websocket event. - pub op: Opcode, - /// The guild ID of the player. - pub guild_id: Id, - /// The new state of the player. - pub state: PlayerUpdateState, - - } - - /// New statistics about a node and its host. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct PlayerUpdateState { - /// Unix timestamp of the player in milliseconds. - pub time: i64, - /// Track position in milliseconds. None if not playing anything. - pub position: i64, - /// True when the player is connected to the voice gateway. - pub connected: bool, - /// The ping of the node to the Discord voice server in milliseconds (-1 if not connected). - pub ping: i64, - } - - /// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Ready { - /// Op code for this websocket event. - pub op: Opcode, - /// Whether this session was resumed. - pub resumed: bool, - /// The Lavalink session id of this connection. Not to be confused with a Discord voice session id. - pub session_id: String, - } - - /// Statistics about a node and its host. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Stats { - /// Op code for this websocket event. - pub op: Opcode, - /// CPU information about the node's host. - pub cpu: StatsCpu, - /// Statistics about audio frames. - #[serde(rename = "frameStats", skip_serializing_if = "Option::is_none")] - pub frame_stats: Option, - /// Memory information about the node's host. - pub memory: StatsMemory, - /// The current number of total players (active and not active) within - /// the node. - pub players: u64, - /// The current number of active players within the node. - pub playing_players: u64, - /// The uptime of the Lavalink server in seconds. - pub uptime: u64, - } - - /// CPU information about a node and its host. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct StatsCpu { - /// The number of CPU cores. - pub cores: usize, - /// The load of the Lavalink server. - pub lavalink_load: f64, - /// The load of the system as a whole. - pub system_load: f64, - } - - /// CPU information about a node and its host. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct StatsFrame { - /// The number of CPU cores. - pub sent: i64, - /// The load of the Lavalink server. - pub nulled: i64, - /// The load of the system as a whole. - pub deficit: i64, - } - - /// Memory information about a node and its host. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct StatsMemory { - /// The number of bytes allocated. - pub allocated: u64, - /// The number of bytes free. - pub free: u64, - /// The number of bytes reservable. - pub reservable: u64, - /// The number of bytes used. - pub used: u64, - } - - /// Server dispatched an event. See the Event Types section for more information. - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct Event { - /// Op code for this websocket event. - pub op: Opcode, - /// The guild id that this was recieved from. - pub guild_id: String, - /// The type of event. - pub r#type: EventType, - /// The data of the event type. - #[serde(flatten)] - pub data: EventData, - } - - /// Server dispatched an event. - #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - pub enum EventType { - /// Dispatched when a track starts playing. - TrackStartEvent, - /// Dispatched when a track ends. - TrackEndEvent, - /// Dispatched when a track throws an exception. - TrackExceptionEvent, - /// Dispatched when a track gets stuck while playing. - TrackStuckEvent, - /// Dispatched when the websocket connection to Discord voice servers is closed. - WebsocketClosedEvent, - } - - /// Server dispatched an event. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(untagged)] - pub enum EventData { - /// Dispatched when a track starts playing. - TrackStartEvent(TrackStart), - /// Dispatched when a track ends. - TrackEndEvent(TrackEnd), - /// Dispatched when a track throws an exception. - TrackExceptionEvent(TrackException), - /// Dispatched when a track gets stuck while playing. - TrackStuckEvent(TrackStuck), - /// Dispatched when the websocket connection to Discord voice servers is closed. - WebsocketClosedEvent(WebsocketClosed), - } - - - /// The reason for the track ending. - #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub enum TrackEndReason { - /// The track finished playing. - Finished, - /// The track failed to load. - LoadFailed, - /// The track was stopped. - Stopped, - /// The track was replaced - Replaced, - /// The track was cleaned up. - Cleanup, - } - - - /// A track ended event from lavalink. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct TrackEnd { - /// The track that ended playing. - pub track: Track, - /// The reason that the track ended. - pub reason: TrackEndReason, - } - - /// A track started. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct TrackStart { - /// The track that started playing. - pub track: Track, - } - - /// Dispatched when a track throws an exception. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct TrackException { - /// The track that threw the exception. - pub track: Track, - /// The occurred exception. - pub exception: Exception, - } - - /// Dispatched when a track gets stuck while playing. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct TrackStuck { - /// The track that got stuck. - pub track: Track, - /// The threshold in milliseconds that was exceeded. - pub threshold_ms: u64, - } - - - /// The voice websocket connection to Discord has been closed. - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] - #[non_exhaustive] - #[serde(rename_all = "camelCase")] - pub struct WebsocketClosed { - /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. - pub code: u64, - /// Reason the connection was closed. - pub reason: String, - /// True if Discord closed the connection, false if Lavalink closed it. - pub by_remote: bool, - } -} +pub mod outgoing; +pub mod incoming; pub use self::{ incoming::{ diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs new file mode 100644 index 00000000000..3bf76c62bdf --- /dev/null +++ b/twilight-lavalink/src/model/incoming.rs @@ -0,0 +1,304 @@ +//! Events that Lavalink sends to clients. + +/// The type of event that something is. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum Opcode { + /// Lavalink is connected and ready. + Ready, + /// An update about a player's current track. + PlayerUpdate, + /// Updated statistics about a node. + Stats, + /// Meta information about a track starting or ending. + Event, +} + + +use crate::http::{Track, Exception}; +use serde::{Deserialize, Serialize}; +use twilight_model::id::{marker::GuildMarker, Id}; + +/// An incoming event from a Lavalink node. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum IncomingEvent { + /// Dispatched when you successfully connect to the Lavalink node. + Ready(Ready), + /// An update about the information of a player. + PlayerUpdate(PlayerUpdate), + /// New statistics about a node and its host. + Stats(Stats), + /// Dispatched when player or voice events occur. + Event(Event), +} + +impl From for IncomingEvent { + fn from(event: Ready) -> IncomingEvent { + Self::Ready(event) + } +} + + +impl From for IncomingEvent { + fn from(event: Event) -> IncomingEvent { + Self::Event(event) + } +} + +impl From for IncomingEvent { + fn from(event: PlayerUpdate) -> IncomingEvent { + Self::PlayerUpdate(event) + } +} + +impl From for IncomingEvent { + fn from(event: Stats) -> IncomingEvent { + Self::Stats(event) + } +} + +/// The discord voice information that lavalink uses for connection and sending information. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct VoiceState { + /// The Discord voice token to authenticate with. + pub token: String, + /// The Discord voice endpoint to connect to. + pub endpoint: String, + /// The Discord voice session id to authenticate with. Note this is seperate from the lavalink session id. + pub session_id: String, +} + +/// An update about the information of a player. Filters are currently unsupported +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct PlayerUpdate { + /// Op code for this websocket event. + pub op: Opcode, + /// The guild ID of the player. + pub guild_id: Id, + /// The new state of the player. + pub state: PlayerUpdateState, + +} + +/// New statistics about a node and its host. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct PlayerUpdateState { + /// Unix timestamp of the player in milliseconds. + pub time: i64, + /// Track position in milliseconds. None if not playing anything. + pub position: i64, + /// True when the player is connected to the voice gateway. + pub connected: bool, + /// The ping of the node to the Discord voice server in milliseconds (-1 if not connected). + pub ping: i64, +} + +/// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Ready { + /// Op code for this websocket event. + pub op: Opcode, + /// Whether this session was resumed. + pub resumed: bool, + /// The Lavalink session id of this connection. Not to be confused with a Discord voice session id. + pub session_id: String, +} + +/// Statistics about a node and its host. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Stats { + /// Op code for this websocket event. + pub op: Opcode, + /// CPU information about the node's host. + pub cpu: StatsCpu, + /// Statistics about audio frames. + #[serde(rename = "frameStats", skip_serializing_if = "Option::is_none")] + pub frame_stats: Option, + /// Memory information about the node's host. + pub memory: StatsMemory, + /// The current number of total players (active and not active) within + /// the node. + pub players: u64, + /// The current number of active players within the node. + pub playing_players: u64, + /// The uptime of the Lavalink server in seconds. + pub uptime: u64, +} + +/// CPU information about a node and its host. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct StatsCpu { + /// The number of CPU cores. + pub cores: usize, + /// The load of the Lavalink server. + pub lavalink_load: f64, + /// The load of the system as a whole. + pub system_load: f64, +} + +/// CPU information about a node and its host. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct StatsFrame { + /// The number of CPU cores. + pub sent: i64, + /// The load of the Lavalink server. + pub nulled: i64, + /// The load of the system as a whole. + pub deficit: i64, +} + +/// Memory information about a node and its host. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct StatsMemory { + /// The number of bytes allocated. + pub allocated: u64, + /// The number of bytes free. + pub free: u64, + /// The number of bytes reservable. + pub reservable: u64, + /// The number of bytes used. + pub used: u64, +} + +/// Server dispatched an event. See the Event Types section for more information. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Event { + /// Op code for this websocket event. + pub op: Opcode, + /// The guild id that this was recieved from. + pub guild_id: String, + /// The type of event. + pub r#type: EventType, + /// The data of the event type. + #[serde(flatten)] + pub data: EventData, +} + +/// Server dispatched an event. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +pub enum EventType { + /// Dispatched when a track starts playing. + TrackStartEvent, + /// Dispatched when a track ends. + TrackEndEvent, + /// Dispatched when a track throws an exception. + TrackExceptionEvent, + /// Dispatched when a track gets stuck while playing. + TrackStuckEvent, + /// Dispatched when the websocket connection to Discord voice servers is closed. + WebsocketClosedEvent, +} + +/// Server dispatched an event. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum EventData { + /// Dispatched when a track starts playing. + TrackStartEvent(TrackStart), + /// Dispatched when a track ends. + TrackEndEvent(TrackEnd), + /// Dispatched when a track throws an exception. + TrackExceptionEvent(TrackException), + /// Dispatched when a track gets stuck while playing. + TrackStuckEvent(TrackStuck), + /// Dispatched when the websocket connection to Discord voice servers is closed. + WebsocketClosedEvent(WebsocketClosed), +} + + +/// The reason for the track ending. +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum TrackEndReason { + /// The track finished playing. + Finished, + /// The track failed to load. + LoadFailed, + /// The track was stopped. + Stopped, + /// The track was replaced + Replaced, + /// The track was cleaned up. + Cleanup, +} + + +/// A track ended event from lavalink. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TrackEnd { + /// The track that ended playing. + pub track: Track, + /// The reason that the track ended. + pub reason: TrackEndReason, +} + +/// A track started. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TrackStart { + /// The track that started playing. + pub track: Track, +} + +/// Dispatched when a track throws an exception. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TrackException { + /// The track that threw the exception. + pub track: Track, + /// The occurred exception. + pub exception: Exception, +} + +/// Dispatched when a track gets stuck while playing. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TrackStuck { + /// The track that got stuck. + pub track: Track, + /// The threshold in milliseconds that was exceeded. + pub threshold_ms: u64, +} + + +/// The voice websocket connection to Discord has been closed. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct WebsocketClosed { + /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. + pub code: u64, + /// Reason the connection was closed. + pub reason: String, + /// True if Discord closed the connection, false if Lavalink closed it. + pub by_remote: bool, +} diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs new file mode 100644 index 00000000000..cad4d90bc0e --- /dev/null +++ b/twilight-lavalink/src/model/outgoing.rs @@ -0,0 +1,418 @@ +//! Events that clients send to Lavalink. +use serde::{Deserialize, Serialize}; +use twilight_model::{ + gateway::payload::incoming::VoiceServerUpdate, + id::{marker::GuildMarker, Id}, +}; + +use crate::http::UpdatePlayerTrack; + +/// An outgoing event to send to Lavalink. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(untagged)] +pub enum OutgoingEvent { + /// Destroy a player for a guild. + Destroy(Destroy), + /// Equalize a player. + Equalizer(Equalizer), + /// Pause or unpause a player. + Pause(Pause), + /// Play a track. + Play(Play), + /// Seek a player's active track to a new position. + Seek(Seek), + /// Stop a player. + Stop(Stop), + /// A combined voice server and voice state update. + VoiceUpdate(VoiceUpdate), + /// Set the volume of a player. + Volume(Volume), +} + +impl From for OutgoingEvent { + fn from(event: Destroy) -> OutgoingEvent { + Self::Destroy(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Equalizer) -> OutgoingEvent { + Self::Equalizer(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Pause) -> OutgoingEvent { + Self::Pause(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Play) -> OutgoingEvent { + Self::Play(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Seek) -> OutgoingEvent { + Self::Seek(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Stop) -> OutgoingEvent { + Self::Stop(event) + } +} + +impl From for OutgoingEvent { + fn from(event: VoiceUpdate) -> OutgoingEvent { + Self::VoiceUpdate(event) + } +} + +impl From for OutgoingEvent { + fn from(event: Volume) -> OutgoingEvent { + Self::Volume(event) + } +} + +/// Destroy a player from a node. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Destroy { + /// The guild ID of the player. + pub guild_id: Id, +} + +impl Destroy { + /// Create a new destroy event. + pub const fn new(guild_id: Id) -> Self { + Self { + guild_id, + } + } +} + +impl From> for Destroy { + fn from(guild_id: Id) -> Self { + Self { + guild_id, + } + } +} + +/// Filters to pass to the update player endpoint. Currently only Equalizer is supported. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum Filters { + /// Adjusts 15 different bands + Equalizer(Equalizer), +} + + +/// Equalize a player. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Equalizer { + /// The bands to use as part of the equalizer. + pub equalizer: Vec, + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, +} + +impl Equalizer { + /// Create a new equalizer event. + pub fn new(guild_id: Id, bands: Vec) -> Self { + Self::from((guild_id, bands)) + } +} + +impl From<(Id, Vec)> for Equalizer { + fn from((guild_id, bands): (Id, Vec)) -> Self { + Self { + equalizer: bands, + guild_id, + } + } +} + +/// A band of the equalizer event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct EqualizerBand { + /// The band. + pub band: i64, + /// The gain. + pub gain: f64, +} + +impl EqualizerBand { + /// Create a new equalizer band. + pub fn new(band: i64, gain: f64) -> Self { + Self::from((band, gain)) + } +} + +impl From<(i64, f64)> for EqualizerBand { + fn from((band, gain): (i64, f64)) -> Self { + Self { band, gain } + } +} + +/// Pause or unpause a player. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Pause { + /// The guild ID of the player. + pub guild_id: Id, + /// Whether to pause the player. + /// + /// Set to `true` to pause or `false` to resume. + pub paused: bool, +} + +impl Pause { + /// Create a new pause event. + /// + /// Set to `true` to pause the player or `false` to resume it. + pub fn new(guild_id: Id, pause: bool) -> Self { + Self::from((guild_id, pause)) + } +} + +impl From<(Id, bool)> for Pause { + fn from((guild_id, pause): (Id, bool)) -> Self { + Self { + guild_id, + paused: pause, + } + } +} + + +// TODO: Might need to fix this struct to abstract the guild_id to another struct pending on what the server sends back with it included. +/// Play a track, optionally specifying to not skip the current track. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Play { + /// Information about the track to play. + pub track: UpdatePlayerTrack, + /// The position in milliseconds to start the track from. + #[serde(skip_serializing_if = "Option::is_none")] + pub position: Option, + /// The position in milliseconds to end the track. + #[serde(skip_serializing_if = "Option::is_none")] + pub end_time: Option>, + /// The player volume, in percentage, from 0 to 1000 + #[serde(skip_serializing_if = "Option::is_none")] + pub volume: Option, + /// Whether the player is paused + #[serde(skip_serializing_if = "Option::is_none")] + pub paused: Option, + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// Whether or not to replace the currently playing track with this new + /// track. + /// + /// Set to `true` to keep playing the current playing track, or `false` + /// to replace the current playing track with a new one. + #[serde(skip_serializing)] + pub no_replace: bool, +} + +impl Play { + /// Create a new play event. + pub fn new( + guild_id: Id, + track: impl Into, + start_time: impl Into>, + end_time: impl Into>, + no_replace: bool, + ) -> Self { + Self::from((guild_id, track, start_time, end_time, no_replace)) + } +} + +impl> From<(Id, T)> for Play { + fn from((guild_id, track): (Id, T)) -> Self { + Self::from((guild_id, track, None, None, true)) + } +} + +impl, S: Into>> From<(Id, T, S)> for Play { + fn from((guild_id, track, start_time): (Id, T, S)) -> Self { + Self::from((guild_id, track, start_time, None, true)) + } +} + +impl, S: Into>, E: Into>> + From<(Id, T, S, E)> for Play +{ + fn from((guild_id, track, start_time, end_time): (Id, T, S, E)) -> Self { + Self::from((guild_id, track, start_time, end_time, true)) + } +} + +impl, S: Into>, E: Into>> + From<(Id, T, S, E, bool)> for Play +{ + fn from( + (guild_id, track, start_time, end_time, no_replace): (Id, T, S, E, bool), + ) -> Self { + Self { + guild_id, + no_replace, + position: start_time.into(), + end_time: Some(end_time.into()), + volume: None, + paused: None, + track: UpdatePlayerTrack{ + encoded: Some(track.into()), + }, + } + } +} + +/// Seek a player's active track to a new position. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Seek { + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// The position in milliseconds to seek to. + pub position: i64, +} + +impl Seek { + /// Create a new seek event. + pub fn new(guild_id: Id, position: i64) -> Self { + Self::from((guild_id, position)) + } +} + +impl From<(Id, i64)> for Seek { + fn from((guild_id, position): (Id, i64)) -> Self { + Self { + guild_id, + position, + } + } +} + +/// Stop a player. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Stop { + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// The track object to pass set to null + pub track: UpdatePlayerTrack, +} + +impl Stop { + /// Create a new stop event. + pub fn new(guild_id: Id) -> Self { + Self::from(guild_id) + } +} + +impl From> for Stop { + fn from(guild_id: Id) -> Self { + Self { + guild_id, + track: UpdatePlayerTrack { + encoded: None, + }, + } + } +} +/// The voice payload for the combined server and state to send to lavalink. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Voice { + /// The Discord voice token to authenticate with. + pub token: String, + /// The Discord voice endpoint to connect to. + pub endpoint: String, + /// The Discord voice session id to authenticate with. This is seperate from the session id of lavalink. + pub session_id: String, +} + +/// A combined voice server and voice state update. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct VoiceUpdate { + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// The voice payload for the combined server and state to send to lavalink. + pub voice: Voice, +} + +impl VoiceUpdate { + /// Create a new voice update event. + pub fn new( + guild_id: Id, + session_id: impl Into, + event: VoiceServerUpdate, + ) -> Self { + Self::from((guild_id, session_id, event)) + } +} + +impl> From<(Id, T, VoiceServerUpdate)> for VoiceUpdate { + fn from((guild_id, session_id, event): (Id, T, VoiceServerUpdate)) -> Self { + Self { + guild_id: guild_id, + voice: Voice{ + token: event.token, + endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), + session_id: session_id.into(), + } + } + } +} + +/// Set the volume of a player. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Volume { + /// The guild ID of the player. + #[serde(skip_serializing)] + pub guild_id: Id, + /// The volume of the player from 0 to 1000. 100 is the default. + pub volume: i64, +} + +impl Volume { + /// Create a new volume event. + pub fn new(guild_id: Id, volume: i64) -> Self { + Self::from((guild_id, volume)) + } +} + +impl From<(Id, i64)> for Volume { + fn from((guild_id, volume): (Id, i64)) -> Self { + Self { + guild_id, + volume, + } + } +} From 9b75e439c17abe15eb3f231a781195fc61615600 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 17:46:01 -0400 Subject: [PATCH 25/70] doc(lavalink): Updating the README with the changes of v4 and what did and didn't get ported forward. I added some high level context as well for new beginners since it took me awhile to find my bearings. --- twilight-lavalink/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index 3800e077ba9..48ac5b7c80d 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -13,6 +13,17 @@ handle sending voice channel updates to Lavalink by processing events via the [client's `process` method][`Lavalink::process`], which you must call with every Voice State Update and Voice Server Update you receive. +A breakdown of how this functions: +- The client is [`Lavalink`](crate::client::Lavalink) that forwards the required events from Discord. + - We read the [Voice State and Voice Server Updates](https://discord.com/developers/docs/topics/gateway-events#voice) from discord to format the data to send to a Lavalink VoiceUpdate Event. + - There is a lower level [node](crate::node) that processes this for you. It isn't recommended to use this but rather the lavalink struct with the players. If you don't find functionality please open up and issue to expose what you need. +- You send the client an [outgoing event](crate::model::outgoing). These include play, pause, seek, etc. You send these through the [player](crate::player) that is attached to Lavalink. +- If you want to search or load you need to create a http client currently and then you can use [these helpers functions](crate::http#functions) to generate the http uri and body to send over your http client. you will then get response you can deserialize as json into the structs in the [http module](crate::http). + +***NOTE: We currently only support `v4` of Lavlink. Support for `v3` is dropped. There was big changes in the api meaning the outgoing are now using a http client instead of websockets. The json request and responses all changed naming and fields changed.*** + +Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. There are some unsupported end points that were added yet such as [Lavalink Info](https://lavalink.dev/api/rest.html#get-lavalink-version) or [Session Api](https://lavalink.dev/api/rest.html#session-api) that weren't previously available. If you would like native support for something please reach out and open an issue for that feature. The porting only ported the functionality of the previous `v3` forward. + ## Features ### `http-support` From f532c438a9cb55fa8f7cf430ef4a9024c8cb016e Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 12 Mar 2024 19:59:40 -0400 Subject: [PATCH 26/70] fix(lavalink): Fixed typo in WebSocketClosedEvent with a lowercase `s`. Updated and added a test to parse the incoming now --- twilight-lavalink/src/model.rs | 64 +++++++++++++++++-------- twilight-lavalink/src/model/incoming.rs | 4 +- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 951b6e4457c..2f32d1a1a07 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -108,10 +108,9 @@ mod lavalink_incoming_model_tests { use crate::http::{Track, TrackInfo}; - use super::incoming::{ - Event, EventType, EventData, Opcode, PlayerUpdate, PlayerUpdateState, Ready, - Stats, StatsCpu, StatsMemory, StatsFrame - }; + use super::{incoming::{ + Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, StatsCpu, StatsFrame, StatsMemory + }, WebsocketClosed}; // These are incoming so we only need to check that the input json can deserialize into the struct. @@ -196,22 +195,25 @@ mod lavalink_incoming_model_tests { r#type: EventType::TrackStartEvent, guild_id: Id::::new(987654321).to_string(), data: EventData::TrackStartEvent( - TrackStart { track: Track { - encoded: "QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string(), - info: TrackInfo { - identifier: "OnuuYcqhzCE".to_string(), - is_seekable: true, - author: "Linkin Park".to_string(), - length: 169000, - is_stream: false, - position: 0, - title: "Bleed It Out [Official Music Video] - Linkin Park".to_string(), - uri:Some("https://www.youtube.com/watch?v=OnuuYcqhzCE".to_string()), - source_name:"youtube".to_string(), - artwork_url:Some("https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg".to_string()), - isrc: None + TrackStart { + track: Track { + encoded: "QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string(), + info: TrackInfo { + identifier: "OnuuYcqhzCE".to_string(), + is_seekable: true, + author: "Linkin Park".to_string(), + length: 169000, + is_stream: false, + position: 0, + title: "Bleed It Out [Official Music Video] - Linkin Park".to_string(), + uri:Some("https://www.youtube.com/watch?v=OnuuYcqhzCE".to_string()), + source_name:"youtube".to_string(), + artwork_url:Some("https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg".to_string()), + isrc: None + } } - } }) + } + ) }; compare_json_payload( @@ -219,6 +221,30 @@ mod lavalink_incoming_model_tests { r#"{"op":"event","guildId":"987654321","type":"TrackStartEvent","track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA","info":{"identifier":"OnuuYcqhzCE","isSeekable":true,"author":"Linkin Park","length":169000,"isStream":false,"position":0,"title":"Bleed It Out [Official Music Video] - Linkin Park","uri":"https://www.youtube.com/watch?v=OnuuYcqhzCE","artworkUrl":"https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{},"userData":{}}}"#.to_string() ); } + + + #[test] + fn should_deserialize_websocketclosed_event() { + let websocket_closed_event = Event { + op: Opcode::Event, + r#type: EventType::WebSocketClosedEvent, + guild_id: Id::::new(987654321).to_string(), + data: EventData::WebSocketClosedEvent( + WebsocketClosed { + code: 1000, + reason: "".to_string(), + by_remote: false, + } + + ) + + }; + compare_json_payload( + websocket_closed_event.clone(), + r#"{"op":"event","type":"WebSocketClosedEvent","guildId":"987654321","code":1000,"reason":"","byRemote":false}"#.to_string() + ); + + } } diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 3bf76c62bdf..686a2d01a99 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -208,7 +208,7 @@ pub enum EventType { /// Dispatched when a track gets stuck while playing. TrackStuckEvent, /// Dispatched when the websocket connection to Discord voice servers is closed. - WebsocketClosedEvent, + WebSocketClosedEvent, } /// Server dispatched an event. @@ -225,7 +225,7 @@ pub enum EventData { /// Dispatched when a track gets stuck while playing. TrackStuckEvent(TrackStuck), /// Dispatched when the websocket connection to Discord voice servers is closed. - WebsocketClosedEvent(WebsocketClosed), + WebSocketClosedEvent(WebsocketClosed), } From 17d4a841dcd98ff0f09d1905a9acd2dfdace5de9 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 13 Mar 2024 20:49:31 -0400 Subject: [PATCH 27/70] fix(lavalink): Track events now decoded correctly and the WebSocketClosed typo is fixed --- twilight-lavalink/src/model.rs | 123 ++++++++++++++++++++++-- twilight-lavalink/src/model/incoming.rs | 8 +- 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 2f32d1a1a07..54e293707e6 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -7,7 +7,7 @@ pub mod incoming; pub use self::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame, StatsMemory, - TrackEnd, TrackStart, TrackStuck, TrackException, WebsocketClosed, + TrackEnd, TrackStart, TrackStuck, TrackException, WebSocketClosed, }, outgoing::{ Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, @@ -100,17 +100,17 @@ mod lavalink_struct_tests { #[cfg(test)] mod lavalink_incoming_model_tests { - use crate::model::TrackStart; + use crate::model::{TrackException, TrackStart, TrackStuck, TrackEnd}; use twilight_model::id::{ Id, marker::GuildMarker, }; - use crate::http::{Track, TrackInfo}; + use crate::http::{Track, TrackInfo, Exception, Severity}; use super::{incoming::{ - Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, StatsCpu, StatsFrame, StatsMemory - }, WebsocketClosed}; + Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, StatsCpu, StatsFrame, StatsMemory, TrackEndReason + }, WebSocketClosed}; // These are incoming so we only need to check that the input json can deserialize into the struct. @@ -222,6 +222,117 @@ mod lavalink_incoming_model_tests { ); } + #[test] + fn should_deserialize_track_exception_event() { + let track_exception_event = Event { + op: Opcode::Event, + r#type: EventType::TrackExceptionEvent, + guild_id: Id::::new(987654321).to_string(), + data: EventData::TrackExceptionEvent( + TrackException { + track: Track { + encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(), + info: TrackInfo { + identifier: "dQw4w9WgXcQ".to_string(), + is_seekable: true, + author: "RickAstleyVEVO".to_string(), + length: 212000, + is_stream: false, + position: 0, + title: "Rick Astley - Never Gonna Give You Up".to_string(), + uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()), + source_name:"youtube".to_string(), + artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()), + isrc: None + } + }, + exception: Exception { + message: Some("".to_string()), + severity: Severity::Common, + cause: "No video found.".to_string(), + } + + } + ) + + }; + compare_json_payload( + track_exception_event.clone(), + r#"{"op":"event","type":"TrackExceptionEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"exception":{"message":"","severity":"common","cause":"No video found."}}"#.to_string() + ); + } + + #[test] + fn should_deserialize_track_stuck_event() { + let track_stuck_event = Event { + op: Opcode::Event, + r#type: EventType::TrackStuckEvent, + guild_id: Id::::new(987654321).to_string(), + data: EventData::TrackStuckEvent( + TrackStuck { + track: Track { + encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(), + info: TrackInfo { + identifier: "dQw4w9WgXcQ".to_string(), + is_seekable: true, + author: "RickAstleyVEVO".to_string(), + length: 212000, + is_stream: false, + position: 0, + title: "Rick Astley - Never Gonna Give You Up".to_string(), + uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()), + source_name:"youtube".to_string(), + artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()), + isrc: None + } + }, + threshold_ms: 123456789, + + } + ) + + }; + compare_json_payload( + track_stuck_event.clone(), + r#"{"op":"event","type":"TrackStuckEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"thresholdMs":123456789}"#.to_string() + ); + } + + #[test] + fn should_deserialize_track_end_event() { + let track_stuck_event = Event { + op: Opcode::Event, + r#type: EventType::TrackEndEvent, + guild_id: Id::::new(987654321).to_string(), + data: EventData::TrackEndEvent( + TrackEnd { + track: Track { + encoded: "QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==".to_string(), + info: TrackInfo { + identifier: "dQw4w9WgXcQ".to_string(), + is_seekable: true, + author: "RickAstleyVEVO".to_string(), + length: 212000, + is_stream: false, + position: 0, + title: "Rick Astley - Never Gonna Give You Up".to_string(), + uri:Some("https://www.youtube.com/watch?v=dQw4w9WgXcQ".to_string()), + source_name:"youtube".to_string(), + artwork_url:Some("https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg".to_string()), + isrc: None + } + }, + reason: TrackEndReason::Finished, + } + ) + + }; + compare_json_payload( + track_stuck_event.clone(), + r#"{"op":"event","type":"TrackEndEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"reason":"finished"}"#.to_string() + ); + } + #[test] fn should_deserialize_websocketclosed_event() { @@ -230,7 +341,7 @@ mod lavalink_incoming_model_tests { r#type: EventType::WebSocketClosedEvent, guild_id: Id::::new(987654321).to_string(), data: EventData::WebSocketClosedEvent( - WebsocketClosed { + WebSocketClosed { code: 1000, reason: "".to_string(), by_remote: false, diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 686a2d01a99..e7f7e618fed 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -216,16 +216,16 @@ pub enum EventType { #[non_exhaustive] #[serde(untagged)] pub enum EventData { - /// Dispatched when a track starts playing. - TrackStartEvent(TrackStart), /// Dispatched when a track ends. TrackEndEvent(TrackEnd), /// Dispatched when a track throws an exception. TrackExceptionEvent(TrackException), /// Dispatched when a track gets stuck while playing. TrackStuckEvent(TrackStuck), + /// Dispatched when a track starts playing. + TrackStartEvent(TrackStart), /// Dispatched when the websocket connection to Discord voice servers is closed. - WebSocketClosedEvent(WebsocketClosed), + WebSocketClosedEvent(WebSocketClosed), } @@ -294,7 +294,7 @@ pub struct TrackStuck { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] -pub struct WebsocketClosed { +pub struct WebSocketClosed { /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. pub code: u64, /// Reason the connection was closed. From fad04356eb24e2b1f196daffba5053dfda053a78 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 13 Mar 2024 21:03:05 -0400 Subject: [PATCH 28/70] fix(lavalink): spelling errors --- twilight-lavalink/src/http.rs | 4 ++-- twilight-lavalink/src/model/incoming.rs | 4 ++-- twilight-lavalink/src/model/outgoing.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index 35bed91af19..98f1299a7c8 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -52,7 +52,7 @@ pub struct Track { } -/// The track on the player. The encoded and identifier are mutually exclusive. Using only enocded for now. +/// The track on the player. The encoded and identifier are mutually exclusive. Using only encoded for now. /// Encoded was chosen since that was previously used in the v3 implementation. /// We don't support userData field currently. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -102,7 +102,7 @@ pub enum Severity { } -/// The excpetion with the details attached on what happened when making a query to lavalink. +/// The exception with the details attached on what happened when making a query to lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index e7f7e618fed..9422ff15a51 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -69,7 +69,7 @@ pub struct VoiceState { pub token: String, /// The Discord voice endpoint to connect to. pub endpoint: String, - /// The Discord voice session id to authenticate with. Note this is seperate from the lavalink session id. + /// The Discord voice session id to authenticate with. Note this is separate from the lavalink session id. pub session_id: String, } @@ -186,7 +186,7 @@ pub struct StatsMemory { pub struct Event { /// Op code for this websocket event. pub op: Opcode, - /// The guild id that this was recieved from. + /// The guild id that this was received from. pub guild_id: String, /// The type of event. pub r#type: EventType, diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index cad4d90bc0e..5fa74c7b38e 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -349,7 +349,7 @@ pub struct Voice { pub token: String, /// The Discord voice endpoint to connect to. pub endpoint: String, - /// The Discord voice session id to authenticate with. This is seperate from the session id of lavalink. + /// The Discord voice session id to authenticate with. This is separate from the session id of lavalink. pub session_id: String, } From a82950aeee5573c5ea3abababe3600ee21b3abc5 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 13 Mar 2024 21:04:25 -0400 Subject: [PATCH 29/70] fix(lavalink): ran formater on the code --- examples/lavalink-basic-bot.rs | 22 +++- twilight-lavalink/src/http.rs | 17 +-- twilight-lavalink/src/model.rs | 156 ++++++++++++------------ twilight-lavalink/src/model/incoming.rs | 8 +- twilight-lavalink/src/model/outgoing.rs | 36 ++---- twilight-lavalink/src/node.rs | 45 +++---- 6 files changed, 132 insertions(+), 152 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index e55b6c11e2f..e2e1febfe4a 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -1,4 +1,3 @@ -use twilight_lavalink::{http::LoadResultData::{Playlist, Search, Track}, model::{Equalizer, EqualizerBand}}; use http_body_util::{BodyExt, Full}; use hyper::{body::Bytes, Request}; use hyper_util::{ @@ -10,6 +9,10 @@ use twilight_gateway::{ Event, EventTypeFlags, Intents, MessageSender, Shard, ShardId, StreamExt as _, }; use twilight_http::Client as HttpClient; +use twilight_lavalink::{ + http::LoadResultData::{Playlist, Search, Track}, + model::{Equalizer, EqualizerBand}, +}; use twilight_lavalink::{ http::LoadedTracks, model::{Destroy, Pause, Play, Seek, Stop, Volume}, @@ -197,7 +200,7 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { let loaded = serde_json::from_slice::(&response_bytes)?; match loaded.data { - Track (track) => { + Track(track) => { player.send(Play::from((guild_id, &track.encoded)))?; let content = format!( @@ -209,9 +212,13 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { .create_message(msg.channel_id) .content(&content) .await?; - }, - Playlist(_) => {todo!("Write example for playlist result.")}, - Search(_) => {todo!("Write example for search result.")}, + } + Playlist(_) => { + todo!("Write example for playlist result.") + } + Search(_) => { + todo!("Write example for search result.") + } _ => { state .http @@ -318,7 +325,10 @@ async fn equalize(msg: Message, state: State) -> anyhow::Result<()> { let gain = gain_msg.content.parse::()?; let player = state.lavalink.player(guild_id).await.unwrap(); - player.send(Equalizer::from((guild_id, vec![EqualizerBand::new(band, gain)])))?; + player.send(Equalizer::from(( + guild_id, + vec![EqualizerBand::new(band, gain)], + )))?; state .http diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index 98f1299a7c8..76859e4fe3c 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -38,8 +38,6 @@ pub struct TrackInfo { pub source_name: String, } - - /// A track object for lavalink to consume and read. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -48,8 +46,7 @@ pub struct Track { /// The base64 encoded track to play pub encoded: String, /// Info about the track - pub info: TrackInfo - + pub info: TrackInfo, } /// The track on the player. The encoded and identifier are mutually exclusive. Using only encoded for now. @@ -61,10 +58,8 @@ pub struct Track { pub struct UpdatePlayerTrack { /// The base64 encoded track to play. null stops the current track pub encoded: Option, - } - /// Information about a playlist from a search result. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -101,7 +96,6 @@ pub enum Severity { Fault, } - /// The exception with the details attached on what happened when making a query to lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -132,7 +126,6 @@ pub enum LoadResultName { Error, } - /// The type of search result given. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -148,7 +141,6 @@ pub enum LoadResultData { Empty(), /// The exception that was thrown when searching. Error(Exception), - } /// The playlist with the provided tracks. Currently plugin info isn't supported @@ -417,10 +409,9 @@ pub fn unmark_failed_address( #[cfg(test)] mod tests { use super::{ - FailingAddress, IpBlock, IpBlockType, LoadedTracks, NanoIpDetails, - NanoIpRoutePlanner, PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner, - RotatingNanoIpDetails, RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track, - TrackInfo, + FailingAddress, IpBlock, IpBlockType, LoadedTracks, NanoIpDetails, NanoIpRoutePlanner, + PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner, RotatingNanoIpDetails, + RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track, TrackInfo, }; use serde::{Deserialize, Serialize}; use serde_test::Token; diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 54e293707e6..18c6f77baa8 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1,13 +1,13 @@ //! Models to (de)serialize incoming/outgoing websocket events and HTTP //! responses. -pub mod outgoing; pub mod incoming; +pub mod outgoing; pub use self::{ incoming::{ IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame, StatsMemory, - TrackEnd, TrackStart, TrackStuck, TrackException, WebSocketClosed, + TrackEnd, TrackException, TrackStart, TrackStuck, WebSocketClosed, }, outgoing::{ Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, @@ -17,7 +17,7 @@ pub use self::{ #[cfg(test)] mod lavalink_struct_tests { - use super::incoming::{ Stats, StatsCpu, StatsMemory, }; + use super::incoming::{Stats, StatsCpu, StatsMemory}; use serde_test::Token; #[test] @@ -100,22 +100,26 @@ mod lavalink_struct_tests { #[cfg(test)] mod lavalink_incoming_model_tests { - use crate::model::{TrackException, TrackStart, TrackStuck, TrackEnd}; - use twilight_model::id::{ - Id, - marker::GuildMarker, - }; + use crate::model::{TrackEnd, TrackException, TrackStart, TrackStuck}; + use twilight_model::id::{marker::GuildMarker, Id}; - use crate::http::{Track, TrackInfo, Exception, Severity}; - - use super::{incoming::{ - Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, StatsCpu, StatsFrame, StatsMemory, TrackEndReason - }, WebSocketClosed}; + use crate::http::{Exception, Severity, Track, TrackInfo}; + use super::{ + incoming::{ + Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, + StatsCpu, StatsFrame, StatsMemory, TrackEndReason, + }, + WebSocketClosed, + }; // These are incoming so we only need to check that the input json can deserialize into the struct. - fn compare_json_payload serde::Deserialize<'a> + std::cmp::PartialEq> - (data_struct: T, json_payload: String) { + fn compare_json_payload< + T: std::fmt::Debug + for<'a> serde::Deserialize<'a> + std::cmp::PartialEq, + >( + data_struct: T, + json_payload: String, + ) { // Deserialize let deserialized: T = serde_json::from_str(&json_payload).unwrap(); assert_eq!(deserialized, data_struct); @@ -130,8 +134,8 @@ mod lavalink_incoming_model_tests { }; compare_json_payload( ready, - r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#.to_string() - ); + r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#.to_string(), + ); } #[test] @@ -139,7 +143,7 @@ mod lavalink_incoming_model_tests { let update = PlayerUpdate { op: Opcode::PlayerUpdate, guild_id: Id::::new(987654321), - state: PlayerUpdateState{ + state: PlayerUpdateState { time: 1710214147839, position: 534, connected: true, @@ -159,10 +163,18 @@ mod lavalink_incoming_model_tests { players: 0, playing_players: 0, uptime: 1139738, - cpu: StatsCpu { cores: 16, lavalink_load: 3.497090420769919E-5, system_load: 0.05597997834786306 }, + cpu: StatsCpu { + cores: 16, + lavalink_load: 3.497090420769919E-5, + system_load: 0.05597997834786306, + }, frame_stats: None, - memory: StatsMemory { allocated: 331350016, free: 228139904, reservable: 8396996608, used: 103210112 } - + memory: StatsMemory { + allocated: 331350016, + free: 228139904, + reservable: 8396996608, + used: 103210112, + }, }; compare_json_payload( stat_event.clone(), @@ -177,10 +189,22 @@ mod lavalink_incoming_model_tests { players: 0, playing_players: 0, uptime: 1139738, - cpu: StatsCpu { cores: 16, lavalink_load: 3.497090420769919E-5, system_load: 0.05597997834786306 }, - frame_stats: Some(StatsFrame{ sent: 6000, nulled: 10, deficit: -3010}), - memory: StatsMemory { allocated: 331350016, free: 228139904, reservable: 8396996608, used: 103210112 }, - + cpu: StatsCpu { + cores: 16, + lavalink_load: 3.497090420769919E-5, + system_load: 0.05597997834786306, + }, + frame_stats: Some(StatsFrame { + sent: 6000, + nulled: 10, + deficit: -3010, + }), + memory: StatsMemory { + allocated: 331350016, + free: 228139904, + reservable: 8396996608, + used: 103210112, + }, }; compare_json_payload( stat_event.clone(), @@ -333,53 +357,41 @@ mod lavalink_incoming_model_tests { ); } - #[test] fn should_deserialize_websocketclosed_event() { let websocket_closed_event = Event { op: Opcode::Event, r#type: EventType::WebSocketClosedEvent, guild_id: Id::::new(987654321).to_string(), - data: EventData::WebSocketClosedEvent( - WebSocketClosed { - code: 1000, - reason: "".to_string(), - by_remote: false, - } - - ) - + data: EventData::WebSocketClosedEvent(WebSocketClosed { + code: 1000, + reason: "".to_string(), + by_remote: false, + }), }; compare_json_payload( websocket_closed_event.clone(), r#"{"op":"event","type":"WebSocketClosedEvent","guildId":"987654321","code":1000,"reason":"","byRemote":false}"#.to_string() ); - } } - #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::model::{Play, Stop, Pause, Volume, Seek, Destroy, Equalizer}; use crate::http::UpdatePlayerTrack; + use crate::model::{Destroy, Equalizer, Pause, Play, Seek, Stop, Volume}; - use twilight_model::id::{ - Id, - marker::GuildMarker, - }; + use twilight_model::id::{marker::GuildMarker, Id}; - use super::outgoing::{ - OutgoingEvent, VoiceUpdate, Voice - }; + use super::outgoing::{OutgoingEvent, Voice, VoiceUpdate}; use super::EqualizerBand; - // For some of the outgoing we have fields that don't get deserialized. We only need // to check weather the serialization is working. - fn compare_json_payload - (data_struct: T, json_payload: String) { - + fn compare_json_payload( + data_struct: T, + json_payload: String, + ) { let serialized = serde_json::to_string(&data_struct).unwrap(); let expected_serialized = json_payload; assert_eq!(serialized, expected_serialized); @@ -389,7 +401,7 @@ mod lavalink_outgoing_model_tests { fn should_serialize_an_outgoing_voice_update() { let voice = VoiceUpdate { guild_id: Id::::new(987654321), - voice: Voice{ + voice: Voice { token: String::from("863ea8ef2ads8ef2"), endpoint: String::from("eu-centra654863.discord.media:443"), session_id: String::from("asdf5w1efa65feaf315e8a8effsa1e5f"), @@ -422,64 +434,49 @@ mod lavalink_outgoing_model_tests { #[test] fn should_serialize_an_outgoing_stop() { - let stop = OutgoingEvent::Stop(Stop{ - track: UpdatePlayerTrack { - encoded: None, - }, + let stop = OutgoingEvent::Stop(Stop { + track: UpdatePlayerTrack { encoded: None }, guild_id: Id::::new(987654321), }); - compare_json_payload( - stop, - r#"{"track":{"encoded":null}}"#.to_string() - ); + compare_json_payload(stop, r#"{"track":{"encoded":null}}"#.to_string()); } #[test] fn should_serialize_an_outgoing_pause() { - let pause = OutgoingEvent::Pause(Pause{ + let pause = OutgoingEvent::Pause(Pause { paused: true, guild_id: Id::::new(987654321), }); compare_json_payload( pause, - r#"{"guildId":"987654321","paused":true}"#.to_string() - ); + r#"{"guildId":"987654321","paused":true}"#.to_string(), + ); } #[test] fn should_serialize_an_outgoing_seek() { - let seek = OutgoingEvent::Seek(Seek{ + let seek = OutgoingEvent::Seek(Seek { position: 66000, guild_id: Id::::new(987654321), }); - compare_json_payload( - seek, - r#"{"position":66000}"#.to_string() - ); + compare_json_payload(seek, r#"{"position":66000}"#.to_string()); } #[test] fn should_serialize_an_outgoing_volume() { - let volume = OutgoingEvent::Volume(Volume{ + let volume = OutgoingEvent::Volume(Volume { volume: 50, guild_id: Id::::new(987654321), }); - compare_json_payload( - volume, - r#"{"volume":50}"#.to_string() - ); + compare_json_payload(volume, r#"{"volume":50}"#.to_string()); } - #[test] fn should_serialize_an_outgoing_destroy_aka_leave() { - let destroy = OutgoingEvent::Destroy(Destroy{ + let destroy = OutgoingEvent::Destroy(Destroy { guild_id: Id::::new(987654321), }); - compare_json_payload( - destroy, - r#"{"guildId":"987654321"}"#.to_string() - ); + compare_json_payload(destroy, r#"{"guildId":"987654321"}"#.to_string()); } #[test] @@ -490,8 +487,7 @@ mod lavalink_outgoing_model_tests { }); compare_json_payload( equalize, - r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#.to_string() - ); - + r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#.to_string(), + ); } } diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 9422ff15a51..98090810eb5 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -15,8 +15,7 @@ pub enum Opcode { Event, } - -use crate::http::{Track, Exception}; +use crate::http::{Exception, Track}; use serde::{Deserialize, Serialize}; use twilight_model::id::{marker::GuildMarker, Id}; @@ -41,7 +40,6 @@ impl From for IncomingEvent { } } - impl From for IncomingEvent { fn from(event: Event) -> IncomingEvent { Self::Event(event) @@ -84,7 +82,6 @@ pub struct PlayerUpdate { pub guild_id: Id, /// The new state of the player. pub state: PlayerUpdateState, - } /// New statistics about a node and its host. @@ -228,7 +225,6 @@ pub enum EventData { WebSocketClosedEvent(WebSocketClosed), } - /// The reason for the track ending. #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -246,7 +242,6 @@ pub enum TrackEndReason { Cleanup, } - /// A track ended event from lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -289,7 +284,6 @@ pub struct TrackStuck { pub threshold_ms: u64, } - /// The voice websocket connection to Discord has been closed. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 5fa74c7b38e..53d01835743 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -90,17 +90,13 @@ pub struct Destroy { impl Destroy { /// Create a new destroy event. pub const fn new(guild_id: Id) -> Self { - Self { - guild_id, - } + Self { guild_id } } } impl From> for Destroy { fn from(guild_id: Id) -> Self { - Self { - guild_id, - } + Self { guild_id } } } @@ -113,7 +109,6 @@ pub enum Filters { Equalizer(Equalizer), } - /// Equalize a player. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] @@ -197,7 +192,6 @@ impl From<(Id, bool)> for Pause { } } - // TODO: Might need to fix this struct to abstract the guild_id to another struct pending on what the server sends back with it included. /// Play a track, optionally specifying to not skip the current track. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -255,8 +249,8 @@ impl, S: Into>> From<(Id, T, S)> for Pl } } -impl, S: Into>, E: Into>> - From<(Id, T, S, E)> for Play +impl, S: Into>, E: Into>> From<(Id, T, S, E)> + for Play { fn from((guild_id, track, start_time, end_time): (Id, T, S, E)) -> Self { Self::from((guild_id, track, start_time, end_time, true)) @@ -276,7 +270,7 @@ impl, S: Into>, E: Into>> end_time: Some(end_time.into()), volume: None, paused: None, - track: UpdatePlayerTrack{ + track: UpdatePlayerTrack { encoded: Some(track.into()), }, } @@ -304,10 +298,7 @@ impl Seek { impl From<(Id, i64)> for Seek { fn from((guild_id, position): (Id, i64)) -> Self { - Self { - guild_id, - position, - } + Self { guild_id, position } } } @@ -334,9 +325,7 @@ impl From> for Stop { fn from(guild_id: Id) -> Self { Self { guild_id, - track: UpdatePlayerTrack { - encoded: None, - }, + track: UpdatePlayerTrack { encoded: None }, } } } @@ -379,12 +368,12 @@ impl VoiceUpdate { impl> From<(Id, T, VoiceServerUpdate)> for VoiceUpdate { fn from((guild_id, session_id, event): (Id, T, VoiceServerUpdate)) -> Self { Self { - guild_id: guild_id, - voice: Voice{ + guild_id, + voice: Voice { token: event.token, endpoint: event.endpoint.unwrap_or("NO_ENDPOINT_RETURNED".to_string()), session_id: session_id.into(), - } + }, } } } @@ -410,9 +399,6 @@ impl Volume { impl From<(Id, i64)> for Volume { fn from((guild_id, volume): (Id, i64)) -> Self { - Self { - guild_id, - volume, - } + Self { guild_id, volume } } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 2f22ab31065..1e8e60a5b3f 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,7 +17,7 @@ //! //! [`Lavalink`]: crate::client::Lavalink -use hyper::{Request, Method}; +use hyper::{Method, Request}; use hyper_util::rt::TokioIo; use crate::{ @@ -450,13 +450,13 @@ impl Node { let cpu = 1.05f64.powf(100f64 * stats.cpu.system_load) * 10f64 - 10f64; let (deficit_frame, null_frame) = ( - 1.03f64 - .powf(500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.deficit) as f64 / 3000f64)) - * 300f64 + 1.03f64.powf( + 500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.deficit) as f64 / 3000f64), + ) * 300f64 - 300f64, - (1.03f64 - .powf(500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.nulled) as f64 / 3000f64)) - * 300f64 + (1.03f64.powf( + 500f64 * (stats.frame_stats.as_ref().map_or(0, |f| f.nulled) as f64 / 3000f64), + ) * 300f64 - 300f64) * 2f64, ); @@ -534,12 +534,12 @@ impl Connection { Ok(()) } - async fn get_outgoing_endpoint_based_on_event(&self, outgoing: &OutgoingEvent) -> (Method, hyper::Uri) { + async fn get_outgoing_endpoint_based_on_event( + &self, + outgoing: &OutgoingEvent, + ) -> (Method, hyper::Uri) { let address = self.config.address; - tracing::debug!( - "forwarding event to {}: {outgoing:?}", - address, - ); + tracing::debug!("forwarding event to {}: {outgoing:?}", address,); let (guild_id, no_replace) = match outgoing.clone() { OutgoingEvent::VoiceUpdate(voice_update) => (voice_update.guild_id, true), @@ -551,7 +551,12 @@ impl Connection { OutgoingEvent::Stop(stop) => (stop.guild_id, false), OutgoingEvent::Volume(volume) => (volume.guild_id, true), }; - let session = self.lavalink_session_id.lock().await.clone().unwrap_or("NO_SESSION".to_string()); + let session = self + .lavalink_session_id + .lock() + .await + .clone() + .unwrap_or("NO_SESSION".to_string()); match outgoing.clone() { OutgoingEvent::Destroy(_) => (Method::DELETE, format!("http://{address}/v4/sessions/{session}/players/{guild_id}").parse::().unwrap()), @@ -563,9 +568,7 @@ impl Connection { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await; let payload = serde_json::to_string(&outgoing).unwrap(); - tracing::debug!( - "converted payload: {payload:?}" - ); + tracing::debug!("converted payload: {payload:?}"); let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); @@ -597,9 +600,7 @@ impl Connection { .body(payload) .unwrap(); - tracing::debug!( - "Request: {req:?}" - ); + tracing::debug!("Request: {req:?}"); let res = sender.send_request(req).await.unwrap(); @@ -633,9 +634,11 @@ impl Connection { match &event { IncomingEvent::PlayerUpdate(update) => self.player_update(update)?, - IncomingEvent::Ready(ready) => *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()), + IncomingEvent::Ready(ready) => { + *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()) + } IncomingEvent::Stats(stats) => self.stats(stats).await?, - &IncomingEvent::Event(_) => {}, + &IncomingEvent::Event(_) => {} } // It's fine if the rx end dropped, often users don't need to care about From 7ae3b51beea5d2edd15da1f6995728f7fdbbfc3b Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 14 Mar 2024 06:35:37 -0400 Subject: [PATCH 30/70] fix(lavalink): clippy errors are now resolved --- twilight-lavalink/src/node.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 1e8e60a5b3f..4130e011165 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -573,7 +573,7 @@ impl Connection { let host = url.host().expect("uri has no host"); let port = url.port_u16().unwrap_or(80); - let address = format!("{}:{}", host, port); + let address = format!("{host}:{port}"); let stream = TcpStream::connect(address).await.unwrap(); @@ -583,7 +583,7 @@ impl Connection { let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap(); tokio::task::spawn(async move { if let Err(err) = conn.await { - println!("Connection failed: {:?}", err); + println!("Connection failed: {err:?}"); } }); @@ -591,7 +591,7 @@ impl Connection { let authority = url.authority().unwrap().clone(); // Create an HTTP request with an empty body and a HOST header - let req = Request::builder() + let request = Request::builder() .uri(url) .method(method) .header(hyper::header::HOST, authority.as_str()) @@ -600,11 +600,11 @@ impl Connection { .body(payload) .unwrap(); - tracing::debug!("Request: {req:?}"); + tracing::debug!("Request: {request:?}"); - let res = sender.send_request(req).await.unwrap(); + let response = sender.send_request(request).await.unwrap(); - tracing::debug!("Response status: {}", res.status()); + tracing::debug!("Response status: {}", response.status()); Ok(()) } @@ -635,7 +635,7 @@ impl Connection { match &event { IncomingEvent::PlayerUpdate(update) => self.player_update(update)?, IncomingEvent::Ready(ready) => { - *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()) + *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()); } IncomingEvent::Stats(stats) => self.stats(stats).await?, &IncomingEvent::Event(_) => {} @@ -684,7 +684,7 @@ impl Drop for Connection { fn connect_request(state: &NodeConfig) -> Result { let crate_version = env!("CARGO_PKG_VERSION"); - let client_name = format!("twilight-lavalink/{}", crate_version); + let client_name = format!("twilight-lavalink/{crate_version}"); let mut builder = ClientBuilder::new() .uri(&format!("ws://{}/v4/websocket", state.address)) From bab628cbece917f68c370e98a160330870cf7c71 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 14 Mar 2024 07:30:59 -0400 Subject: [PATCH 31/70] fix(lavalink): fixing the tests and minimal versions --- twilight-lavalink/Cargo.toml | 4 ++-- twilight-lavalink/src/http.rs | 1 - twilight-lavalink/src/model.rs | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 995f1e17b68..6b4952c302b 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -17,8 +17,8 @@ version = "0.16.0-rc.1" dashmap = { default-features = false, version = "5.3" } futures-util = { default-features = false, features = ["bilock", "sink", "std", "unstable"], version = "0.3" } http = { default-features = false, version = "1" } -hyper = { default-features = false, version = "1" } -hyper-util = { default-features = false, features = ["client-legacy"], version = "0.1.2" } +hyper = { default-features = false, features = ["http1"], version = "1" } +hyper-util = { default-features = false, features = ["client-legacy", "tokio"], version = "0.1.2" } http-body-util = "0.1" serde = { default-features = false, features = ["derive", "std"], version = "1" } serde_json = { default-features = false, features = ["std"], version = "1" } diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index 76859e4fe3c..fb280a7f3e0 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -619,7 +619,6 @@ mod tests { len: 13, }, Token::Str("name"), - Token::Some, Token::Str("Test Playlist"), Token::Str("selectedTrack"), Token::I64(-1), diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 18c6f77baa8..2dd0b5d49c8 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1,6 +1,7 @@ //! Models to (de)serialize incoming/outgoing websocket events and HTTP //! responses. + pub mod incoming; pub mod outgoing; From dfd42ce6518f8ed9b2b73b96eddd07a1787017c5 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 14 Mar 2024 16:13:27 -0400 Subject: [PATCH 32/70] fix(lavalink): Fixing formatting and clippy warnings for lavalink tests --- twilight-lavalink/src/model.rs | 167 ++++++++++++++++----------------- 1 file changed, 80 insertions(+), 87 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 2dd0b5d49c8..9cff060cc27 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -1,7 +1,6 @@ //! Models to (de)serialize incoming/outgoing websocket events and HTTP //! responses. - pub mod incoming; pub mod outgoing; @@ -118,12 +117,12 @@ mod lavalink_incoming_model_tests { fn compare_json_payload< T: std::fmt::Debug + for<'a> serde::Deserialize<'a> + std::cmp::PartialEq, >( - data_struct: T, - json_payload: String, + data_struct: &T, + json_payload: &str, ) { // Deserialize - let deserialized: T = serde_json::from_str(&json_payload).unwrap(); - assert_eq!(deserialized, data_struct); + let deserialized: T = serde_json::from_str(json_payload).unwrap(); + assert_eq!(deserialized, *data_struct); } #[test] @@ -134,8 +133,8 @@ mod lavalink_incoming_model_tests { session_id: "la3kfsdf5eafe848".to_string(), }; compare_json_payload( - ready, - r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#.to_string(), + &ready, + r#"{"op":"ready","resumed":false,"sessionId":"la3kfsdf5eafe848"}"#, ); } @@ -143,18 +142,18 @@ mod lavalink_incoming_model_tests { fn should_deserialize_a_player_update_response() { let update = PlayerUpdate { op: Opcode::PlayerUpdate, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), state: PlayerUpdateState { - time: 1710214147839, + time: 1_710_214_147_839, position: 534, connected: true, ping: 0, }, }; compare_json_payload( - update, - r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#.to_string() - ); + &update, + r#"{"op":"playerUpdate","guildId":"987654321","state":{"time":1710214147839,"position":534,"connected":true,"ping":0}}"#, + ); } #[test] @@ -163,24 +162,24 @@ mod lavalink_incoming_model_tests { op: Opcode::Stats, players: 0, playing_players: 0, - uptime: 1139738, + uptime: 1_139_738, cpu: StatsCpu { cores: 16, - lavalink_load: 3.497090420769919E-5, - system_load: 0.05597997834786306, + lavalink_load: 3.497_090_420_769_919E-5, + system_load: 0.055_979_978_347_863_06, }, frame_stats: None, memory: StatsMemory { - allocated: 331350016, - free: 228139904, - reservable: 8396996608, - used: 103210112, + allocated: 331_350_016, + free: 228_139_904, + reservable: 8_396_996_608, + used: 103_210_112, }, }; compare_json_payload( - stat_event.clone(), - r#"{"op":"stats","frameStats":null,"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#.to_string() - ); + &stat_event.clone(), + r#"{"op":"stats","frameStats":null,"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#, + ); } #[test] @@ -189,11 +188,11 @@ mod lavalink_incoming_model_tests { op: Opcode::Stats, players: 0, playing_players: 0, - uptime: 1139738, + uptime: 1_139_738, cpu: StatsCpu { cores: 16, - lavalink_load: 3.497090420769919E-5, - system_load: 0.05597997834786306, + lavalink_load: 3.497_090_420_769_919E-5, + system_load: 0.055_979_978_347_863_06, }, frame_stats: Some(StatsFrame { sent: 6000, @@ -201,16 +200,16 @@ mod lavalink_incoming_model_tests { deficit: -3010, }), memory: StatsMemory { - allocated: 331350016, - free: 228139904, - reservable: 8396996608, - used: 103210112, + allocated: 331_350_016, + free: 228_139_904, + reservable: 8_396_996_608, + used: 103_210_112, }, }; compare_json_payload( - stat_event.clone(), - r#"{"op":"stats","frameStats":{"sent":6000,"nulled":10,"deficit":-3010},"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#.to_string() - ); + &stat_event.clone(), + r#"{"op":"stats","frameStats":{"sent":6000,"nulled":10,"deficit":-3010},"players":0,"playingPlayers":0,"uptime":1139738,"memory":{"free":228139904,"used":103210112,"allocated":331350016,"reservable":8396996608},"cpu":{"cores":16,"systemLoad":0.05597997834786306,"lavalinkLoad":3.497090420769919E-5}}"#, + ); } #[test] @@ -218,7 +217,7 @@ mod lavalink_incoming_model_tests { let track_start_event = Event { op: Opcode::Event, r#type: EventType::TrackStartEvent, - guild_id: Id::::new(987654321).to_string(), + guild_id: Id::::new(987_654_321).to_string(), data: EventData::TrackStartEvent( TrackStart { track: Track { @@ -227,7 +226,7 @@ mod lavalink_incoming_model_tests { identifier: "OnuuYcqhzCE".to_string(), is_seekable: true, author: "Linkin Park".to_string(), - length: 169000, + length: 169_000, is_stream: false, position: 0, title: "Bleed It Out [Official Music Video] - Linkin Park".to_string(), @@ -242,9 +241,9 @@ mod lavalink_incoming_model_tests { }; compare_json_payload( - track_start_event.clone(), - r#"{"op":"event","guildId":"987654321","type":"TrackStartEvent","track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA","info":{"identifier":"OnuuYcqhzCE","isSeekable":true,"author":"Linkin Park","length":169000,"isStream":false,"position":0,"title":"Bleed It Out [Official Music Video] - Linkin Park","uri":"https://www.youtube.com/watch?v=OnuuYcqhzCE","artworkUrl":"https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{},"userData":{}}}"#.to_string() - ); + &track_start_event.clone(), + r#"{"op":"event","guildId":"987654321","type":"TrackStartEvent","track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA","info":{"identifier":"OnuuYcqhzCE","isSeekable":true,"author":"Linkin Park","length":169000,"isStream":false,"position":0,"title":"Bleed It Out [Official Music Video] - Linkin Park","uri":"https://www.youtube.com/watch?v=OnuuYcqhzCE","artworkUrl":"https://i.ytimg.com/vi/OnuuYcqhzCE/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{},"userData":{}}}"#, + ); } #[test] @@ -252,7 +251,7 @@ mod lavalink_incoming_model_tests { let track_exception_event = Event { op: Opcode::Event, r#type: EventType::TrackExceptionEvent, - guild_id: Id::::new(987654321).to_string(), + guild_id: Id::::new(987_654_321).to_string(), data: EventData::TrackExceptionEvent( TrackException { track: Track { @@ -261,7 +260,7 @@ mod lavalink_incoming_model_tests { identifier: "dQw4w9WgXcQ".to_string(), is_seekable: true, author: "RickAstleyVEVO".to_string(), - length: 212000, + length: 212_000, is_stream: false, position: 0, title: "Rick Astley - Never Gonna Give You Up".to_string(), @@ -272,7 +271,7 @@ mod lavalink_incoming_model_tests { } }, exception: Exception { - message: Some("".to_string()), + message: Some(String::new()), severity: Severity::Common, cause: "No video found.".to_string(), } @@ -282,9 +281,9 @@ mod lavalink_incoming_model_tests { }; compare_json_payload( - track_exception_event.clone(), - r#"{"op":"event","type":"TrackExceptionEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"exception":{"message":"","severity":"common","cause":"No video found."}}"#.to_string() - ); + &track_exception_event.clone(), + r#"{"op":"event","type":"TrackExceptionEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"exception":{"message":"","severity":"common","cause":"No video found."}}"#, + ); } #[test] @@ -292,7 +291,7 @@ mod lavalink_incoming_model_tests { let track_stuck_event = Event { op: Opcode::Event, r#type: EventType::TrackStuckEvent, - guild_id: Id::::new(987654321).to_string(), + guild_id: Id::::new(987_654_321).to_string(), data: EventData::TrackStuckEvent( TrackStuck { track: Track { @@ -301,7 +300,7 @@ mod lavalink_incoming_model_tests { identifier: "dQw4w9WgXcQ".to_string(), is_seekable: true, author: "RickAstleyVEVO".to_string(), - length: 212000, + length: 212_000, is_stream: false, position: 0, title: "Rick Astley - Never Gonna Give You Up".to_string(), @@ -311,16 +310,16 @@ mod lavalink_incoming_model_tests { isrc: None } }, - threshold_ms: 123456789, + threshold_ms: 123_456_789, } ) }; compare_json_payload( - track_stuck_event.clone(), - r#"{"op":"event","type":"TrackStuckEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"thresholdMs":123456789}"#.to_string() - ); + &track_stuck_event.clone(), + r#"{"op":"event","type":"TrackStuckEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"thresholdMs":123456789}"#, + ); } #[test] @@ -328,7 +327,7 @@ mod lavalink_incoming_model_tests { let track_stuck_event = Event { op: Opcode::Event, r#type: EventType::TrackEndEvent, - guild_id: Id::::new(987654321).to_string(), + guild_id: Id::::new(987_654_321).to_string(), data: EventData::TrackEndEvent( TrackEnd { track: Track { @@ -337,7 +336,7 @@ mod lavalink_incoming_model_tests { identifier: "dQw4w9WgXcQ".to_string(), is_seekable: true, author: "RickAstleyVEVO".to_string(), - length: 212000, + length: 212_000, is_stream: false, position: 0, title: "Rick Astley - Never Gonna Give You Up".to_string(), @@ -353,9 +352,9 @@ mod lavalink_incoming_model_tests { }; compare_json_payload( - track_stuck_event.clone(), - r#"{"op":"event","type":"TrackEndEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"reason":"finished"}"#.to_string() - ); + &track_stuck_event.clone(), + r#"{"op":"event","type":"TrackEndEvent","guildId":"987654321","track":{"encoded":"QAAAjQIAJVJpY2sgQXN0bGV5IC0gTmV2ZXIgR29ubmEgR2l2ZSBZb3UgVXAADlJpY2tBc3RsZXlWRVZPAAAAAAADPCAAC2RRdzR3OVdnWGNRAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9ZFF3NHc5V2dYY1EAB3lvdXR1YmUAAAAAAAAAAA==","info":{"identifier":"dQw4w9WgXcQ","isSeekable":true,"author":"RickAstleyVEVO","length":212000,"isStream":false,"position":0,"title":"Rick Astley - Never Gonna Give You Up","uri":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","artworkUrl":"https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg","isrc":null,"sourceName":"youtube"},"pluginInfo":{}},"reason":"finished"}"#, + ); } #[test] @@ -363,17 +362,17 @@ mod lavalink_incoming_model_tests { let websocket_closed_event = Event { op: Opcode::Event, r#type: EventType::WebSocketClosedEvent, - guild_id: Id::::new(987654321).to_string(), + guild_id: Id::::new(987_654_321).to_string(), data: EventData::WebSocketClosedEvent(WebSocketClosed { code: 1000, - reason: "".to_string(), + reason: String::new(), by_remote: false, }), }; compare_json_payload( - websocket_closed_event.clone(), - r#"{"op":"event","type":"WebSocketClosedEvent","guildId":"987654321","code":1000,"reason":"","byRemote":false}"#.to_string() - ); + &websocket_closed_event.clone(), + r#"{"op":"event","type":"WebSocketClosedEvent","guildId":"987654321","code":1000,"reason":"","byRemote":false}"#, + ); } } @@ -390,8 +389,8 @@ mod lavalink_outgoing_model_tests { // For some of the outgoing we have fields that don't get deserialized. We only need // to check weather the serialization is working. fn compare_json_payload( - data_struct: T, - json_payload: String, + data_struct: &T, + json_payload: &str, ) { let serialized = serde_json::to_string(&data_struct).unwrap(); let expected_serialized = json_payload; @@ -401,7 +400,7 @@ mod lavalink_outgoing_model_tests { #[test] fn should_serialize_an_outgoing_voice_update() { let voice = VoiceUpdate { - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), voice: Voice { token: String::from("863ea8ef2ads8ef2"), endpoint: String::from("eu-centra654863.discord.media:443"), @@ -409,9 +408,9 @@ mod lavalink_outgoing_model_tests { }, }; compare_json_payload( - voice, - r#"{"voice":{"token":"863ea8ef2ads8ef2","endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f"}}"#.to_string() - ); + &voice, + r#"{"voice":{"token":"863ea8ef2ads8ef2","endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f"}}"#, + ); } #[test] @@ -424,71 +423,65 @@ mod lavalink_outgoing_model_tests { end_time: Some(None), volume: None, paused: None, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), no_replace: true, }); compare_json_payload( - play, - r#"{"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"},"endTime":null}"#.to_string() - ); + &play, + r#"{"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"},"endTime":null}"#, + ); } #[test] fn should_serialize_an_outgoing_stop() { let stop = OutgoingEvent::Stop(Stop { track: UpdatePlayerTrack { encoded: None }, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload(stop, r#"{"track":{"encoded":null}}"#.to_string()); + compare_json_payload(&stop, r#"{"track":{"encoded":null}}"#); } #[test] fn should_serialize_an_outgoing_pause() { let pause = OutgoingEvent::Pause(Pause { paused: true, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload( - pause, - r#"{"guildId":"987654321","paused":true}"#.to_string(), - ); + compare_json_payload(&pause, r#"{"guildId":"987654321","paused":true}"#); } #[test] fn should_serialize_an_outgoing_seek() { let seek = OutgoingEvent::Seek(Seek { position: 66000, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload(seek, r#"{"position":66000}"#.to_string()); + compare_json_payload(&seek, r#"{"position":66000}"#); } #[test] fn should_serialize_an_outgoing_volume() { let volume = OutgoingEvent::Volume(Volume { volume: 50, - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload(volume, r#"{"volume":50}"#.to_string()); + compare_json_payload(&volume, r#"{"volume":50}"#); } #[test] fn should_serialize_an_outgoing_destroy_aka_leave() { let destroy = OutgoingEvent::Destroy(Destroy { - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload(destroy, r#"{"guildId":"987654321"}"#.to_string()); + compare_json_payload(&destroy, r#"{"guildId":"987654321"}"#); } #[test] fn should_serialize_an_outgoing_equalize() { let equalize = OutgoingEvent::Equalizer(Equalizer { equalizer: vec![EqualizerBand::new(5, -0.15)], - guild_id: Id::::new(987654321), + guild_id: Id::::new(987_654_321), }); - compare_json_payload( - equalize, - r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#.to_string(), - ); + compare_json_payload(&equalize, r#"{"equalizer":[{"band":5,"gain":-0.15}]}"#); } } From 38f2aaaac2f1a95cb3e1ffa355e5cf85187486b5 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 14 Mar 2024 16:31:53 -0400 Subject: [PATCH 33/70] fix(lavalink): Fixing the no default config of http. If that wasn't enabled then some structs weren't visible in the model since they were required. Moved the Track, Exception, Severity, and TrackInfo into the correct incoming or outgoing model files --- twilight-lavalink/src/http.rs | 81 +------------------------ twilight-lavalink/src/model.rs | 17 +++--- twilight-lavalink/src/model/incoming.rs | 67 +++++++++++++++++++- twilight-lavalink/src/model/outgoing.rs | 11 +++- 4 files changed, 86 insertions(+), 90 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index fb280a7f3e0..f0305254bc7 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -1,6 +1,7 @@ //! Models to deserialize responses into and functions to create `http` crate //! requests. +use crate::model::incoming::{Exception, Track}; use http::{ header::{HeaderValue, AUTHORIZATION}, Error as HttpError, Request, @@ -9,57 +10,6 @@ use percent_encoding::NON_ALPHANUMERIC; use serde::{Deserialize, Deserializer, Serialize}; use std::net::{IpAddr, SocketAddr}; -/// Information about the track returned or playing on lavalink. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub struct TrackInfo { - /// The track identifier. - pub identifier: String, - /// Whether the track is seekable. - pub is_seekable: bool, - /// The track author. - pub author: String, - /// The track length in milliseconds. - pub length: u64, - /// Whether the track is a stream. - pub is_stream: bool, - /// The track position in milliseconds. - pub position: u64, - /// The track title. - pub title: String, - /// The track uri. - pub uri: Option, - /// The track artwork url. - pub artwork_url: Option, - /// The track [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). - pub isrc: Option, - /// The track source name. - pub source_name: String, -} - -/// A track object for lavalink to consume and read. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub struct Track { - /// The base64 encoded track to play - pub encoded: String, - /// Info about the track - pub info: TrackInfo, -} - -/// The track on the player. The encoded and identifier are mutually exclusive. Using only encoded for now. -/// Encoded was chosen since that was previously used in the v3 implementation. -/// We don't support userData field currently. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub struct UpdatePlayerTrack { - /// The base64 encoded track to play. null stops the current track - pub encoded: Option, -} - /// Information about a playlist from a search result. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -83,32 +33,6 @@ where .and_then(|selected| u64::try_from(selected).ok())) } -/// The levels of severity that an exception can have. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub enum Severity { - /// The cause is known and expected, indicates that there is nothing wrong with the library itself. - Common, - /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. - Suspicious, - /// The probable cause is an issue with the library or there is no way to tell what the cause might be. This is the default level and other levels are used in cases where the thrower has more in-depth knowledge about the error. - Fault, -} - -/// The exception with the details attached on what happened when making a query to lavalink. -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[non_exhaustive] -#[serde(rename_all = "camelCase")] -pub struct Exception { - /// The message of the exception. - pub message: Option, - /// The severity of the exception. - pub severity: Severity, - /// The cause of the exception. - pub cause: String, -} - /// The type of search result given. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] @@ -411,8 +335,9 @@ mod tests { use super::{ FailingAddress, IpBlock, IpBlockType, LoadedTracks, NanoIpDetails, NanoIpRoutePlanner, PlaylistInfo, RotatingIpDetails, RotatingIpRoutePlanner, RotatingNanoIpDetails, - RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track, TrackInfo, + RotatingNanoIpRoutePlanner, RoutePlanner, RoutePlannerType, Track, }; + use crate::model::incoming::TrackInfo; use serde::{Deserialize, Serialize}; use serde_test::Token; use static_assertions::{assert_fields, assert_impl_all}; diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index 9cff060cc27..b5527a18c4b 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -6,12 +6,12 @@ pub mod outgoing; pub use self::{ incoming::{ - IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame, StatsMemory, - TrackEnd, TrackException, TrackStart, TrackStuck, WebSocketClosed, + Exception, IncomingEvent, PlayerUpdate, PlayerUpdateState, Stats, StatsCpu, StatsFrame, + StatsMemory, Track, TrackEnd, TrackException, TrackStart, TrackStuck, WebSocketClosed, }, outgoing::{ - Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, VoiceUpdate, - Volume, + Destroy, Equalizer, EqualizerBand, OutgoingEvent, Pause, Play, Seek, Stop, + UpdatePlayerTrack, VoiceUpdate, Volume, }, }; @@ -103,12 +103,10 @@ mod lavalink_incoming_model_tests { use crate::model::{TrackEnd, TrackException, TrackStart, TrackStuck}; use twilight_model::id::{marker::GuildMarker, Id}; - use crate::http::{Exception, Severity, Track, TrackInfo}; - use super::{ incoming::{ - Event, EventData, EventType, Opcode, PlayerUpdate, PlayerUpdateState, Ready, Stats, - StatsCpu, StatsFrame, StatsMemory, TrackEndReason, + Event, EventData, EventType, Exception, Opcode, PlayerUpdate, PlayerUpdateState, Ready, + Severity, Stats, StatsCpu, StatsFrame, StatsMemory, Track, TrackEndReason, TrackInfo, }, WebSocketClosed, }; @@ -378,12 +376,11 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { - use crate::http::UpdatePlayerTrack; use crate::model::{Destroy, Equalizer, Pause, Play, Seek, Stop, Volume}; use twilight_model::id::{marker::GuildMarker, Id}; - use super::outgoing::{OutgoingEvent, Voice, VoiceUpdate}; + use super::outgoing::{OutgoingEvent, UpdatePlayerTrack, Voice, VoiceUpdate}; use super::EqualizerBand; // For some of the outgoing we have fields that don't get deserialized. We only need diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 98090810eb5..16e5da23891 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -15,10 +15,35 @@ pub enum Opcode { Event, } -use crate::http::{Exception, Track}; use serde::{Deserialize, Serialize}; use twilight_model::id::{marker::GuildMarker, Id}; +/// The levels of severity that an exception can have. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub enum Severity { + /// The cause is known and expected, indicates that there is nothing wrong with the library itself. + Common, + /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. + Suspicious, + /// The probable cause is an issue with the library or there is no way to tell what the cause might be. This is the default level and other levels are used in cases where the thrower has more in-depth knowledge about the error. + Fault, +} + +/// The exception with the details attached on what happened when making a query to lavalink. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Exception { + /// The message of the exception. + pub message: Option, + /// The severity of the exception. + pub severity: Severity, + /// The cause of the exception. + pub cause: String, +} + /// An incoming event from a Lavalink node. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] @@ -176,6 +201,46 @@ pub struct StatsMemory { pub used: u64, } +/// Information about the track returned or playing on lavalink. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct TrackInfo { + /// The track identifier. + pub identifier: String, + /// Whether the track is seekable. + pub is_seekable: bool, + /// The track author. + pub author: String, + /// The track length in milliseconds. + pub length: u64, + /// Whether the track is a stream. + pub is_stream: bool, + /// The track position in milliseconds. + pub position: u64, + /// The track title. + pub title: String, + /// The track uri. + pub uri: Option, + /// The track artwork url. + pub artwork_url: Option, + /// The track [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). + pub isrc: Option, + /// The track source name. + pub source_name: String, +} + +/// A track object for lavalink to consume and read. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct Track { + /// The base64 encoded track to play + pub encoded: String, + /// Info about the track + pub info: TrackInfo, +} + /// Server dispatched an event. See the Event Types section for more information. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 53d01835743..bc047853bcc 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -5,7 +5,16 @@ use twilight_model::{ id::{marker::GuildMarker, Id}, }; -use crate::http::UpdatePlayerTrack; +/// The track on the player. The encoded and identifier are mutually exclusive. Using only encoded for now. +/// Encoded was chosen since that was previously used in the v3 implementation. +/// We don't support userData field currently. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[non_exhaustive] +#[serde(rename_all = "camelCase")] +pub struct UpdatePlayerTrack { + /// The base64 encoded track to play. null stops the current track + pub encoded: Option, +} /// An outgoing event to send to Lavalink. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] From 3147e92af033614a428058aafdb84090a766e525 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 15:58:18 -0400 Subject: [PATCH 34/70] refactor(lavalink): Move the guild id and no replace with helper functions on the enum OutgoingEvent --- twilight-lavalink/src/model/outgoing.rs | 28 +++++++++++++++++++++++++ twilight-lavalink/src/node.rs | 13 +++--------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index bc047853bcc..352f92220b2 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -87,6 +87,34 @@ impl From for OutgoingEvent { } } +impl OutgoingEvent { + pub const fn guild_id(event: &OutgoingEvent) -> Id { + return match event { + OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, + OutgoingEvent::Play(play) => play.guild_id, + OutgoingEvent::Destroy(destroy) => destroy.guild_id, + OutgoingEvent::Equalizer(equalize) => equalize.guild_id, + OutgoingEvent::Pause(pause) => pause.guild_id, + OutgoingEvent::Seek(seek) => seek.guild_id, + OutgoingEvent::Stop(stop) => stop.guild_id, + OutgoingEvent::Volume(volume) => volume.guild_id, + }; + } + + pub const fn no_replace(event: &OutgoingEvent) -> bool { + return match event { + OutgoingEvent::VoiceUpdate(_) => true, + OutgoingEvent::Play(play) => play.no_replace, + OutgoingEvent::Destroy(_) => true, + OutgoingEvent::Equalizer(_) => true, + OutgoingEvent::Pause(_) => true, + OutgoingEvent::Seek(_) => true, + OutgoingEvent::Stop(_) => false, + OutgoingEvent::Volume(_) => true, + }; + } +} + /// Destroy a player from a node. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 4130e011165..6fa2468fbdd 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -541,16 +541,9 @@ impl Connection { let address = self.config.address; tracing::debug!("forwarding event to {}: {outgoing:?}", address,); - let (guild_id, no_replace) = match outgoing.clone() { - OutgoingEvent::VoiceUpdate(voice_update) => (voice_update.guild_id, true), - OutgoingEvent::Play(play) => (play.guild_id, play.no_replace), - OutgoingEvent::Destroy(destroy) => (destroy.guild_id, true), - OutgoingEvent::Equalizer(equalize) => (equalize.guild_id, true), - OutgoingEvent::Pause(pause) => (pause.guild_id, true), - OutgoingEvent::Seek(seek) => (seek.guild_id, true), - OutgoingEvent::Stop(stop) => (stop.guild_id, false), - OutgoingEvent::Volume(volume) => (volume.guild_id, true), - }; + let guild_id = OutgoingEvent::guild_id(outgoing); + let no_replace = OutgoingEvent::no_replace(outgoing); + let session = self .lavalink_session_id .lock() From b08ccbdf38e79f03dc06507907d727a595954311 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 16:08:03 -0400 Subject: [PATCH 35/70] refactor(lavalink): Use Uri builder for the outgoing events instead --- twilight-lavalink/src/node.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 6fa2468fbdd..28fd3a614f0 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,7 +17,7 @@ //! //! [`Lavalink`]: crate::client::Lavalink -use hyper::{Method, Request}; +use hyper::{Method, Request, Uri}; use hyper_util::rt::TokioIo; use crate::{ @@ -552,9 +552,27 @@ impl Connection { .unwrap_or("NO_SESSION".to_string()); match outgoing.clone() { - OutgoingEvent::Destroy(_) => (Method::DELETE, format!("http://{address}/v4/sessions/{session}/players/{guild_id}").parse::().unwrap()), - _ => (Method::PATCH, format!("http://{address}/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}").parse::().unwrap()), + OutgoingEvent::Destroy(_) => { + let destroy_uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) + .build() + .unwrap(); + return (Method::DELETE, destroy_uri); + }, + _ => { + let destroy_uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}")) + .build() + .unwrap(); + return (Method::PATCH, destroy_uri); + }, } + + } async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { From 5da3c958303379f9fc886793a7d1e4686ed4d3f9 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 16:08:43 -0400 Subject: [PATCH 36/70] refactor: Format code --- twilight-lavalink/src/node.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 28fd3a614f0..be5c339915e 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -554,25 +554,25 @@ impl Connection { match outgoing.clone() { OutgoingEvent::Destroy(_) => { let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) - .build() - .unwrap(); + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) + .build() + .unwrap(); return (Method::DELETE, destroy_uri); - }, + } _ => { let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}")) - .build() - .unwrap(); + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!( + "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" + )) + .build() + .unwrap(); return (Method::PATCH, destroy_uri); - }, + } } - - } async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { From b0277aac9aa883ea16987bd95bd31322de8f605b Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 16:35:11 -0400 Subject: [PATCH 37/70] feat(lavalink): Added OutgoingEventHasNoSession for when we try to connect to the api and no session was established. --- twilight-lavalink/src/node.rs | 61 ++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index be5c339915e..6cfc732d1f4 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -83,6 +83,7 @@ impl Display for NodeError { f.write_str("failed to build connection request") } NodeErrorType::Connecting { .. } => f.write_str("Failed to connect to the node"), + NodeErrorType::OutgoingEventHasNoSession { .. } => f.write_str("No session id found for connection to lavalink api."), NodeErrorType::SerializingMessage { .. } => { f.write_str("failed to serialize outgoing message as json") } @@ -112,6 +113,8 @@ pub enum NodeErrorType { BuildingConnectionRequest, /// Connecting to the Lavalink server failed after several backoff attempts. Connecting, + /// The error for outgoing having no session id to connect to. You can potentially have no valid session before trying to outgoing events + OutgoingEventHasNoSession, /// Serializing a JSON message to be sent to a Lavalink node failed. SerializingMessage { /// The message that couldn't be serialized. @@ -537,46 +540,52 @@ impl Connection { async fn get_outgoing_endpoint_based_on_event( &self, outgoing: &OutgoingEvent, - ) -> (Method, hyper::Uri) { + ) -> Result<(Method, hyper::Uri), NodeError> { let address = self.config.address; tracing::debug!("forwarding event to {}: {outgoing:?}", address,); let guild_id = OutgoingEvent::guild_id(outgoing); let no_replace = OutgoingEvent::no_replace(outgoing); - let session = self + let session_id = self .lavalink_session_id .lock() .await - .clone() - .unwrap_or("NO_SESSION".to_string()); - - match outgoing.clone() { - OutgoingEvent::Destroy(_) => { - let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) - .build() - .unwrap(); - return (Method::DELETE, destroy_uri); - } - _ => { - let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!( - "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" - )) - .build() - .unwrap(); - return (Method::PATCH, destroy_uri); + .take(); + + if let Some(session) = session_id { + match outgoing.clone() { + OutgoingEvent::Destroy(_) => { + let destroy_uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) + .build() + .unwrap(); + return Ok((Method::DELETE, destroy_uri)); + } + _ => { + let destroy_uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!( + "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" + )) + .build() + .unwrap(); + return Ok((Method::PATCH, destroy_uri)); + } } + } else { + return Err(NodeError { + kind: NodeErrorType::OutgoingEventHasNoSession, + source: None, + }); } } async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { - let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await; + let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; let payload = serde_json::to_string(&outgoing).unwrap(); tracing::debug!("converted payload: {payload:?}"); From 422cb430d2d1f85de19550338cb9964319fddfef Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 16:37:14 -0400 Subject: [PATCH 38/70] doc(lavalink): Adding documentation to the new helper functions for outgoing event --- twilight-lavalink/src/model/outgoing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 352f92220b2..a65175a1134 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -88,6 +88,7 @@ impl From for OutgoingEvent { } impl OutgoingEvent { + /// Helper function to get the guild_id attached to the event type. pub const fn guild_id(event: &OutgoingEvent) -> Id { return match event { OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, @@ -101,6 +102,7 @@ impl OutgoingEvent { }; } + /// Helper function to get wether or not to replace the current track in the lavalink api based on the event type. pub const fn no_replace(event: &OutgoingEvent) -> bool { return match event { OutgoingEvent::VoiceUpdate(_) => true, From a37e113af9060e049c8b28580e77a65fee7d2931 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 16:52:00 -0400 Subject: [PATCH 39/70] refactor(lavalink): Adding debug tracing, spelling fix, Replacing the unwraps with map_err to forward to user. --- twilight-lavalink/src/model/outgoing.rs | 2 +- twilight-lavalink/src/node.rs | 44 ++++++++++++++++++------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index a65175a1134..a291b6ca1cd 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -102,7 +102,7 @@ impl OutgoingEvent { }; } - /// Helper function to get wether or not to replace the current track in the lavalink api based on the event type. + /// Helper function to get whether or not to replace the current track in the lavalink api based on the event type. pub const fn no_replace(event: &OutgoingEvent) -> bool { return match event { OutgoingEvent::VoiceUpdate(_) => true, diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 6cfc732d1f4..56cb31b0f9e 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -554,6 +554,8 @@ impl Connection { .take(); if let Some(session) = session_id { + tracing::debug!("Found session id {}. Generating the url and method for event type.", session); + match outgoing.clone() { OutgoingEvent::Destroy(_) => { let destroy_uri = Uri::builder() @@ -577,6 +579,7 @@ impl Connection { } } } else { + tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); return Err(NodeError { kind: NodeErrorType::OutgoingEventHasNoSession, source: None, @@ -588,6 +591,7 @@ impl Connection { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; let payload = serde_json::to_string(&outgoing).unwrap(); + tracing::debug!("Sending request to {url:?} using method {method:?}."); tracing::debug!("converted payload: {payload:?}"); let host = url.host().expect("uri has no host"); @@ -595,11 +599,13 @@ impl Connection { let address = format!("{host}:{port}"); - let stream = TcpStream::connect(address).await.unwrap(); + let stream = TcpStream::connect(address).await.map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; let io = TokioIo::new(stream); - // Create the Hyper client let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap(); tokio::task::spawn(async move { if let Err(err) = conn.await { @@ -607,10 +613,8 @@ impl Connection { } }); - // The authority of our URL will be the hostname of the httpbin remote - let authority = url.authority().unwrap().clone(); + let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header.").clone(); - // Create an HTTP request with an empty body and a HOST header let request = Request::builder() .uri(url) .method(method) @@ -618,7 +622,10 @@ impl Connection { .header(AUTHORIZATION, self.config.authorization.as_str()) .header("Content-Type", "application/json") .body(payload) - .unwrap(); + .map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; tracing::debug!("Request: {request:?}"); @@ -712,20 +719,29 @@ fn connect_request(state: &NodeConfig) -> Result { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), })? - .add_header(AUTHORIZATION, state.authorization.parse().unwrap()) + .add_header(AUTHORIZATION, state.authorization.parse().map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?) .add_header( HeaderName::from_static("user-id"), state.user_id.get().into(), ) .add_header( HeaderName::from_static("client-name"), - HeaderValue::from_str(&client_name).unwrap(), + HeaderValue::from_str(&client_name).map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?, ); if state.resume.is_some() { builder = builder.add_header( HeaderName::from_static("resume-key"), - state.address.to_string().parse().unwrap(), + state.address.to_string().parse().map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?, ); } @@ -751,9 +767,15 @@ async fn reconnect( "key": config.address, "timeout": resume.timeout, }); - let msg = Message::text(serde_json::to_string(&payload).unwrap()); + let msg = Message::text(serde_json::to_string(&payload).map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?); - stream.send(msg).await.unwrap(); + stream.send(msg).await.map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; } else { tracing::debug!("session to {} resumed", config.address); } From 25c60b6f367e6634ccea49781c1dfca792933317 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 17:00:25 -0400 Subject: [PATCH 40/70] refactor(lavalink): format and clippy warnings and error cleanup --- twilight-lavalink/src/model/outgoing.rs | 17 ++-- twilight-lavalink/src/node.rs | 126 +++++++++++++----------- 2 files changed, 72 insertions(+), 71 deletions(-) diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index a291b6ca1cd..6eebf11d190 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -88,9 +88,9 @@ impl From for OutgoingEvent { } impl OutgoingEvent { - /// Helper function to get the guild_id attached to the event type. + /// Helper function to get the `guild_id` attached to the event type. pub const fn guild_id(event: &OutgoingEvent) -> Id { - return match event { + match event { OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, OutgoingEvent::Play(play) => play.guild_id, OutgoingEvent::Destroy(destroy) => destroy.guild_id, @@ -99,21 +99,16 @@ impl OutgoingEvent { OutgoingEvent::Seek(seek) => seek.guild_id, OutgoingEvent::Stop(stop) => stop.guild_id, OutgoingEvent::Volume(volume) => volume.guild_id, - }; + } } /// Helper function to get whether or not to replace the current track in the lavalink api based on the event type. pub const fn no_replace(event: &OutgoingEvent) -> bool { - return match event { - OutgoingEvent::VoiceUpdate(_) => true, + match event { OutgoingEvent::Play(play) => play.no_replace, - OutgoingEvent::Destroy(_) => true, - OutgoingEvent::Equalizer(_) => true, - OutgoingEvent::Pause(_) => true, - OutgoingEvent::Seek(_) => true, OutgoingEvent::Stop(_) => false, - OutgoingEvent::Volume(_) => true, - }; + _ => true, + } } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 56cb31b0f9e..4dc53820c6d 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -83,7 +83,9 @@ impl Display for NodeError { f.write_str("failed to build connection request") } NodeErrorType::Connecting { .. } => f.write_str("Failed to connect to the node"), - NodeErrorType::OutgoingEventHasNoSession { .. } => f.write_str("No session id found for connection to lavalink api."), + NodeErrorType::OutgoingEventHasNoSession { .. } => { + f.write_str("No session id found for connection to lavalink api.") + } NodeErrorType::SerializingMessage { .. } => { f.write_str("failed to serialize outgoing message as json") } @@ -547,44 +549,38 @@ impl Connection { let guild_id = OutgoingEvent::guild_id(outgoing); let no_replace = OutgoingEvent::no_replace(outgoing); - let session_id = self - .lavalink_session_id - .lock() - .await - .take(); + let session_id = self.lavalink_session_id.lock().await.take(); if let Some(session) = session_id { - tracing::debug!("Found session id {}. Generating the url and method for event type.", session); - - match outgoing.clone() { - OutgoingEvent::Destroy(_) => { - let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) - .build() - .unwrap(); - return Ok((Method::DELETE, destroy_uri)); - } - _ => { - let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!( - "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" - )) - .build() - .unwrap(); - return Ok((Method::PATCH, destroy_uri)); - } + tracing::debug!( + "Found session id {}. Generating the url and method for event type.", + session + ); + + if let OutgoingEvent::Destroy(_) = outgoing.clone() { + let destroy_uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) + .build() + .unwrap(); + return Ok((Method::DELETE, destroy_uri)); } - } else { - tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); - return Err(NodeError { - kind: NodeErrorType::OutgoingEventHasNoSession, - source: None, - }); + let uri = Uri::builder() + .scheme("http") + .authority(address.to_string()) + .path_and_query(format!( + "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" + )) + .build() + .unwrap(); + return Ok((Method::PATCH, uri)); } + tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); + Err(NodeError { + kind: NodeErrorType::OutgoingEventHasNoSession, + source: None, + }) } async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { @@ -599,10 +595,12 @@ impl Connection { let address = format!("{host}:{port}"); - let stream = TcpStream::connect(address).await.map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; + let stream = TcpStream::connect(address) + .await + .map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; let io = TokioIo::new(stream); @@ -623,9 +621,9 @@ impl Connection { .header("Content-Type", "application/json") .body(payload) .map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; tracing::debug!("Request: {request:?}"); @@ -719,10 +717,13 @@ fn connect_request(state: &NodeConfig) -> Result { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), })? - .add_header(AUTHORIZATION, state.authorization.parse().map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?) + .add_header( + AUTHORIZATION, + state.authorization.parse().map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?, + ) .add_header( HeaderName::from_static("user-id"), state.user_id.get().into(), @@ -730,18 +731,22 @@ fn connect_request(state: &NodeConfig) -> Result { .add_header( HeaderName::from_static("client-name"), HeaderValue::from_str(&client_name).map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?, + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?, ); if state.resume.is_some() { builder = builder.add_header( HeaderName::from_static("resume-key"), - state.address.to_string().parse().map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?, + state + .address + .to_string() + .parse() + .map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?, ); } @@ -767,15 +772,16 @@ async fn reconnect( "key": config.address, "timeout": resume.timeout, }); - let msg = Message::text(serde_json::to_string(&payload).map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?); + let msg = + Message::text(serde_json::to_string(&payload).map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?); stream.send(msg).await.map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; } else { tracing::debug!("session to {} resumed", config.address); } From 7ba6c162d8ca8af7f9b9995d06f8a2b966d0312d Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 17:47:52 -0400 Subject: [PATCH 41/70] fix(lavalink): Session was being taken causing broken outgoing. --- twilight-lavalink/src/node.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 4dc53820c6d..c7b486aaf1b 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -549,9 +549,7 @@ impl Connection { let guild_id = OutgoingEvent::guild_id(outgoing); let no_replace = OutgoingEvent::no_replace(outgoing); - let session_id = self.lavalink_session_id.lock().await.take(); - - if let Some(session) = session_id { + if let Some(session) = self.lavalink_session_id.lock().await.clone() { tracing::debug!( "Found session id {}. Generating the url and method for event type.", session From 8f50b4adf7122486c4c83e08113f3cd78d94346d Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 19:04:49 -0400 Subject: [PATCH 42/70] feat(lavalink): Updating the todo's for the examples in the basic bot --- examples/lavalink-basic-bot.rs | 44 ++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index e2e1febfe4a..ec63687d0a6 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -213,11 +213,47 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { .content(&content) .await?; } - Playlist(_) => { - todo!("Write example for playlist result.") + Playlist(playist) => { + if let Some(top_track) = playist.tracks.first() { + player.send(Play::from((guild_id, &top_track.encoded)))?; + + let content = format!( + "Playing **{:?}** by **{:?}**", + top_track.info.title, top_track.info.author + ); + state + .http + .create_message(msg.channel_id) + .content(&content) + .await?; + } + + state + .http + .create_message(msg.channel_id) + .content("Didn't find any playlist results") + .await?; } - Search(_) => { - todo!("Write example for search result.") + Search(result) => { + if let Some(first_result) = result.first() { + player.send(Play::from((guild_id, &first_result.encoded)))?; + + let content = format!( + "Playing **{:?}** by **{:?}**", + first_result.info.title, first_result.info.author + ); + state + .http + .create_message(msg.channel_id) + .content(&content) + .await?; + } + + state + .http + .create_message(msg.channel_id) + .content("Didn't find any search results") + .await?; } _ => { state From dd66b21aca7535e719fcb75ad1b32066c72f202d Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Mon, 25 Mar 2024 19:22:22 -0400 Subject: [PATCH 43/70] refactor(lavalink): Alphabetized the structs and enums in incoming --- twilight-lavalink/src/model/incoming.rs | 106 ++++++++++++------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 16e5da23891..52e1a8d747d 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -5,14 +5,14 @@ #[non_exhaustive] #[serde(rename_all = "camelCase")] pub enum Opcode { - /// Lavalink is connected and ready. - Ready, + /// Meta information about a track starting or ending. + Event, /// An update about a player's current track. PlayerUpdate, + /// Lavalink is connected and ready. + Ready, /// Updated statistics about a node. Stats, - /// Meta information about a track starting or ending. - Event, } use serde::{Deserialize, Serialize}; @@ -25,10 +25,10 @@ use twilight_model::id::{marker::GuildMarker, Id}; pub enum Severity { /// The cause is known and expected, indicates that there is nothing wrong with the library itself. Common, - /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. - Suspicious, /// The probable cause is an issue with the library or there is no way to tell what the cause might be. This is the default level and other levels are used in cases where the thrower has more in-depth knowledge about the error. Fault, + /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. + Suspicious, } /// The exception with the details attached on what happened when making a query to lavalink. @@ -36,12 +36,12 @@ pub enum Severity { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Exception { + /// The cause of the exception. + pub cause: String, /// The message of the exception. pub message: Option, /// The severity of the exception. pub severity: Severity, - /// The cause of the exception. - pub cause: String, } /// An incoming event from a Lavalink node. @@ -49,14 +49,14 @@ pub struct Exception { #[non_exhaustive] #[serde(untagged)] pub enum IncomingEvent { + /// Dispatched when player or voice events occur. + Event(Event), /// Dispatched when you successfully connect to the Lavalink node. Ready(Ready), - /// An update about the information of a player. - PlayerUpdate(PlayerUpdate), /// New statistics about a node and its host. Stats(Stats), - /// Dispatched when player or voice events occur. - Event(Event), + /// An update about the information of a player. + PlayerUpdate(PlayerUpdate), } impl From for IncomingEvent { @@ -88,12 +88,12 @@ impl From for IncomingEvent { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct VoiceState { - /// The Discord voice token to authenticate with. - pub token: String, /// The Discord voice endpoint to connect to. pub endpoint: String, /// The Discord voice session id to authenticate with. Note this is separate from the lavalink session id. pub session_id: String, + /// The Discord voice token to authenticate with. + pub token: String, } /// An update about the information of a player. Filters are currently unsupported @@ -101,10 +101,10 @@ pub struct VoiceState { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlayerUpdate { - /// Op code for this websocket event. - pub op: Opcode, /// The guild ID of the player. pub guild_id: Id, + /// Op code for this websocket event. + pub op: Opcode, /// The new state of the player. pub state: PlayerUpdateState, } @@ -114,14 +114,14 @@ pub struct PlayerUpdate { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct PlayerUpdateState { - /// Unix timestamp of the player in milliseconds. - pub time: i64, - /// Track position in milliseconds. None if not playing anything. - pub position: i64, /// True when the player is connected to the voice gateway. pub connected: bool, /// The ping of the node to the Discord voice server in milliseconds (-1 if not connected). pub ping: i64, + /// Track position in milliseconds. None if not playing anything. + pub position: i64, + /// Unix timestamp of the player in milliseconds. + pub time: i64, } /// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. @@ -142,8 +142,6 @@ pub struct Ready { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Stats { - /// Op code for this websocket event. - pub op: Opcode, /// CPU information about the node's host. pub cpu: StatsCpu, /// Statistics about audio frames. @@ -151,6 +149,8 @@ pub struct Stats { pub frame_stats: Option, /// Memory information about the node's host. pub memory: StatsMemory, + /// Op code for this websocket event. + pub op: Opcode, /// The current number of total players (active and not active) within /// the node. pub players: u64, @@ -178,12 +178,12 @@ pub struct StatsCpu { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct StatsFrame { - /// The number of CPU cores. - pub sent: i64, - /// The load of the Lavalink server. - pub nulled: i64, /// The load of the system as a whole. pub deficit: i64, + /// The load of the Lavalink server. + pub nulled: i64, + /// The number of CPU cores. + pub sent: i64, } /// Memory information about a node and its host. @@ -206,28 +206,28 @@ pub struct StatsMemory { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackInfo { + /// The track artwork url. + pub artwork_url: Option, + /// The track author. + pub author: String, + /// The track [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). + pub isrc: Option, /// The track identifier. pub identifier: String, /// Whether the track is seekable. pub is_seekable: bool, - /// The track author. - pub author: String, - /// The track length in milliseconds. - pub length: u64, /// Whether the track is a stream. pub is_stream: bool, + /// The track length in milliseconds. + pub length: u64, /// The track position in milliseconds. pub position: u64, + /// The track source name. + pub source_name: String, /// The track title. pub title: String, /// The track uri. pub uri: Option, - /// The track artwork url. - pub artwork_url: Option, - /// The track [ISRC](https://en.wikipedia.org/wiki/International_Standard_Recording_Code). - pub isrc: Option, - /// The track source name. - pub source_name: String, } /// A track object for lavalink to consume and read. @@ -246,15 +246,15 @@ pub struct Track { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Event { - /// Op code for this websocket event. - pub op: Opcode, + /// The data of the event type. + #[serde(flatten)] + pub data: EventData, /// The guild id that this was received from. pub guild_id: String, + /// Op code for this websocket event. + pub op: Opcode, /// The type of event. pub r#type: EventType, - /// The data of the event type. - #[serde(flatten)] - pub data: EventData, } /// Server dispatched an event. @@ -295,16 +295,16 @@ pub enum EventData { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub enum TrackEndReason { + /// The track was cleaned up. + Cleanup, /// The track finished playing. Finished, /// The track failed to load. LoadFailed, - /// The track was stopped. - Stopped, /// The track was replaced Replaced, - /// The track was cleaned up. - Cleanup, + /// The track was stopped. + Stopped, } /// A track ended event from lavalink. @@ -312,10 +312,10 @@ pub enum TrackEndReason { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackEnd { - /// The track that ended playing. - pub track: Track, /// The reason that the track ended. pub reason: TrackEndReason, + /// The track that ended playing. + pub track: Track, } /// A track started. @@ -332,10 +332,10 @@ pub struct TrackStart { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackException { - /// The track that threw the exception. - pub track: Track, /// The occurred exception. pub exception: Exception, + /// The track that threw the exception. + pub track: Track, } /// Dispatched when a track gets stuck while playing. @@ -343,10 +343,10 @@ pub struct TrackException { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TrackStuck { - /// The track that got stuck. - pub track: Track, /// The threshold in milliseconds that was exceeded. pub threshold_ms: u64, + /// The track that got stuck. + pub track: Track, } /// The voice websocket connection to Discord has been closed. @@ -354,10 +354,10 @@ pub struct TrackStuck { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct WebSocketClosed { + /// True if Discord closed the connection, false if Lavalink closed it. + pub by_remote: bool, /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. pub code: u64, /// Reason the connection was closed. pub reason: String, - /// True if Discord closed the connection, false if Lavalink closed it. - pub by_remote: bool, } From 4964939300fb88777f88169794a63d3c52ac96f8 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 26 Mar 2024 16:08:44 -0400 Subject: [PATCH 44/70] refactor(lavalink): alphabetize enums and structs for http and model --- twilight-lavalink/src/http.rs | 28 ++++++++++++------------- twilight-lavalink/src/model/outgoing.rs | 26 +++++++++++------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index f0305254bc7..527758cffdf 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -38,16 +38,16 @@ where #[non_exhaustive] #[serde(rename_all = "camelCase")] pub enum LoadResultName { - /// A track has been loaded. - Track, - /// A playlist has been loaded. - Playlist, - /// A search result has been loaded. - Search, /// There has been no matches for your identifier. Empty, /// Loading has failed with an error. Error, + /// A playlist has been loaded. + Playlist, + /// A search result has been loaded. + Search, + /// A track has been loaded. + Track, } /// The type of search result given. @@ -55,16 +55,16 @@ pub enum LoadResultName { #[non_exhaustive] #[serde(untagged)] pub enum LoadResultData { - /// Track result with the track info. - Track(Track), - /// The playlist results with the play list info and tracks in the playlist. - Playlist(PlaylistResult), - /// The list of tracks based on the search. - Search(Vec), /// Empty data response. Empty(), /// The exception that was thrown when searching. Error(Exception), + /// The playlist results with the play list info and tracks in the playlist. + Playlist(PlaylistResult), + /// The list of tracks based on the search. + Search(Vec), + /// Track result with the track info. + Track(Track), } /// The playlist with the provided tracks. Currently plugin info isn't supported @@ -83,10 +83,10 @@ pub struct PlaylistResult { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct LoadedTracks { - /// The type of search result, such as a list of tracks or a playlist. - pub load_type: LoadResultName, /// The data of the result. pub data: LoadResultData, + /// The type of search result, such as a list of tracks or a playlist. + pub load_type: LoadResultName, } /// A failing IP address within the planner. diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 6eebf11d190..80665f6c355 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -232,20 +232,9 @@ impl From<(Id, bool)> for Pause { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Play { - /// Information about the track to play. - pub track: UpdatePlayerTrack, - /// The position in milliseconds to start the track from. - #[serde(skip_serializing_if = "Option::is_none")] - pub position: Option, /// The position in milliseconds to end the track. #[serde(skip_serializing_if = "Option::is_none")] pub end_time: Option>, - /// The player volume, in percentage, from 0 to 1000 - #[serde(skip_serializing_if = "Option::is_none")] - pub volume: Option, - /// Whether the player is paused - #[serde(skip_serializing_if = "Option::is_none")] - pub paused: Option, /// The guild ID of the player. #[serde(skip_serializing)] pub guild_id: Id, @@ -256,6 +245,17 @@ pub struct Play { /// to replace the current playing track with a new one. #[serde(skip_serializing)] pub no_replace: bool, + /// The position in milliseconds to start the track from. + #[serde(skip_serializing_if = "Option::is_none")] + pub position: Option, + /// Whether the player is paused + #[serde(skip_serializing_if = "Option::is_none")] + pub paused: Option, + /// Information about the track to play. + pub track: UpdatePlayerTrack, + /// The player volume, in percentage, from 0 to 1000 + #[serde(skip_serializing_if = "Option::is_none")] + pub volume: Option, } impl Play { @@ -368,12 +368,12 @@ impl From> for Stop { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct Voice { - /// The Discord voice token to authenticate with. - pub token: String, /// The Discord voice endpoint to connect to. pub endpoint: String, /// The Discord voice session id to authenticate with. This is separate from the session id of lavalink. pub session_id: String, + /// The Discord voice token to authenticate with. + pub token: String, } /// A combined voice server and voice state update. From 588b4d2f9134279741e91fa52b698686eb7fd91c Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Tue, 26 Mar 2024 16:13:15 -0400 Subject: [PATCH 45/70] fix(lavalink): Remove the unnecessary rename for the serde on frame_stats --- twilight-lavalink/src/model/incoming.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 52e1a8d747d..3a0b851ab8b 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -145,7 +145,7 @@ pub struct Stats { /// CPU information about the node's host. pub cpu: StatsCpu, /// Statistics about audio frames. - #[serde(rename = "frameStats", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub frame_stats: Option, /// Memory information about the node's host. pub memory: StatsMemory, From 2fa60e28e01c5864109147ff02fcedfac0f7c0aa Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 06:11:35 -0400 Subject: [PATCH 46/70] feat(lavalink): Adding a feature flag to enable http2 support for lavalink. default is http1 --- twilight-lavalink/Cargo.toml | 3 +- twilight-lavalink/src/node.rs | 76 ++++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 6b4952c302b..508ad007291 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -17,7 +17,7 @@ version = "0.16.0-rc.1" dashmap = { default-features = false, version = "5.3" } futures-util = { default-features = false, features = ["bilock", "sink", "std", "unstable"], version = "0.3" } http = { default-features = false, version = "1" } -hyper = { default-features = false, features = ["http1"], version = "1" } +hyper = { default-features = false, features = ["client", "http1", "http2"], version = "1" } hyper-util = { default-features = false, features = ["client-legacy", "tokio"], version = "0.1.2" } http-body-util = "0.1" serde = { default-features = false, features = ["derive", "std"], version = "1" } @@ -45,6 +45,7 @@ http-support = ["dep:percent-encoding"] native-tls = ["tokio-websockets/native-tls", "tokio-websockets/openssl"] rustls-native-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-native-roots"] rustls-webpki-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-webpki-roots"] +lavalink-http2 = [] [package.metadata.docs.rs] all-features = true diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index c7b486aaf1b..7002a2027d9 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,6 +17,16 @@ //! //! [`Lavalink`]: crate::client::Lavalink +#[cfg(feature = "lavalink-http2")] +use hyper::client::conn::http2::SendRequest as Http2SendRequest; + +#[cfg(not(feature = "lavalink-http2"))] +use hyper::client::conn::http1::SendRequest as Http1SendRequest; + +#[cfg(feature = "lavalink-http2")] +use hyper_util::rt::TokioExecutor; + + use hyper::{Method, Request, Uri}; use hyper_util::rt::TokioIo; @@ -539,6 +549,55 @@ impl Connection { Ok(()) } + #[cfg(feature = "lavalink-http2")] + async fn get_http_connection(&self, address: &str) -> Result, NodeError> { + let stream = TcpStream::connect(address) + .await + .map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; + + let io = TokioIo::new(stream); + let exec = TokioExecutor::new(); + + let (sender, conn) = hyper::client::conn::http2::handshake(exec, io).await.map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; + tokio::task::spawn(async move { + if let Err(err) = conn.await { + tracing::error!("Connection failed: {err:?}"); + } + }); + + Ok(sender) + } + + #[cfg(not(feature = "lavalink-http2"))] + async fn get_http_connection(&self, address: &str) -> Result, NodeError> { + let stream = TcpStream::connect(address) + .await + .map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; + + let io = TokioIo::new(stream); + + let (sender, conn) = hyper::client::conn::http1::handshake(io).await.map_err(|source| NodeError { + kind: NodeErrorType::BuildingConnectionRequest, + source: Some(Box::new(source)), + })?; + tokio::task::spawn(async move { + if let Err(err) = conn.await { + tracing::error!("Connection failed: {err:?}"); + } + }); + + Ok(sender) + } + async fn get_outgoing_endpoint_based_on_event( &self, outgoing: &OutgoingEvent, @@ -574,6 +633,7 @@ impl Connection { .unwrap(); return Ok((Method::PATCH, uri)); } + tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); Err(NodeError { kind: NodeErrorType::OutgoingEventHasNoSession, @@ -593,21 +653,7 @@ impl Connection { let address = format!("{host}:{port}"); - let stream = TcpStream::connect(address) - .await - .map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; - - let io = TokioIo::new(stream); - - let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap(); - tokio::task::spawn(async move { - if let Err(err) = conn.await { - println!("Connection failed: {err:?}"); - } - }); + let mut sender = self.get_http_connection(&address).await?; let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header.").clone(); From e027013d94daf9e5f31a75f9035c72213f1bfa28 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 06:23:51 -0400 Subject: [PATCH 47/70] refactor(lavalink): Adding documentation to README about flag and renamed to lavalink-protocol-http2. --- twilight-lavalink/Cargo.toml | 2 +- twilight-lavalink/README.md | 8 ++++++++ twilight-lavalink/src/node.rs | 10 +++++----- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 508ad007291..16b371075b8 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -45,7 +45,7 @@ http-support = ["dep:percent-encoding"] native-tls = ["tokio-websockets/native-tls", "tokio-websockets/openssl"] rustls-native-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-native-roots"] rustls-webpki-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-webpki-roots"] -lavalink-http2 = [] +lavalink-protocol-http2 = [] [package.metadata.docs.rs] all-features = true diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index 48ac5b7c80d..72bb05a2e41 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -31,6 +31,14 @@ Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. The `http-support` feature adds support for the `http` module to return request types from the [`http`] crate. This is enabled by default. +### `lavalink-protocol-http2` + +The `lavalink-protocol-http2` switches the underlying protocol to communicate with the lavalink server. +If enabled, http2 will be used. By default, http1 is used. You will need to enable http2 support +in your lavalink server configuration if you want to use this feature because by default it is disabled. + +***NOTE: This is not to be confused with the `http-support` support flag or crate. This is seperated and doesn't depend on the use of that feature.*** + ### TLS `twilight-lavalink` has features to enable [`tokio-websockets`]' TLS diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 7002a2027d9..b2825418b76 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,13 +17,13 @@ //! //! [`Lavalink`]: crate::client::Lavalink -#[cfg(feature = "lavalink-http2")] +#[cfg(feature = "lavalink-protocol-http2")] use hyper::client::conn::http2::SendRequest as Http2SendRequest; -#[cfg(not(feature = "lavalink-http2"))] +#[cfg(not(feature = "lavalink-protocol-http2"))] use hyper::client::conn::http1::SendRequest as Http1SendRequest; -#[cfg(feature = "lavalink-http2")] +#[cfg(feature = "lavalink-protocol-http2")] use hyper_util::rt::TokioExecutor; @@ -549,7 +549,7 @@ impl Connection { Ok(()) } - #[cfg(feature = "lavalink-http2")] + #[cfg(feature = "lavalink-protocol-http2")] async fn get_http_connection(&self, address: &str) -> Result, NodeError> { let stream = TcpStream::connect(address) .await @@ -574,7 +574,7 @@ impl Connection { Ok(sender) } - #[cfg(not(feature = "lavalink-http2"))] + #[cfg(not(feature = "lavalink-protocol-http2"))] async fn get_http_connection(&self, address: &str) -> Result, NodeError> { let stream = TcpStream::connect(address) .await From 89d472daef6837d0171d4f7b7c51591551be4bd8 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 16:16:40 -0400 Subject: [PATCH 48/70] refactor(lavalink): Use the legacy build for client on hyper and store in the connection object to avoid continuously connecting / creating the client. --- twilight-lavalink/src/node.rs | 93 ++++++++--------------------------- 1 file changed, 21 insertions(+), 72 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index b2825418b76..45ae56f24fd 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,18 +17,19 @@ //! //! [`Lavalink`]: crate::client::Lavalink -#[cfg(feature = "lavalink-protocol-http2")] -use hyper::client::conn::http2::SendRequest as Http2SendRequest; - -#[cfg(not(feature = "lavalink-protocol-http2"))] -use hyper::client::conn::http1::SendRequest as Http1SendRequest; - -#[cfg(feature = "lavalink-protocol-http2")] -use hyper_util::rt::TokioExecutor; - +use hyper::{ + Method, Request, Uri, + body::Bytes, +}; +use hyper_util::{ + rt::TokioExecutor, + client::legacy::{ + Client as HyperClient, + connect::HttpConnector, + }, +}; -use hyper::{Method, Request, Uri}; -use hyper_util::rt::TokioIo; +use http_body_util::Full; use crate::{ model::{IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, @@ -483,6 +484,7 @@ impl Node { struct Connection { config: NodeConfig, stream: WebSocketStream>, + lavalink_http: HyperClient>, node_from: UnboundedReceiver, node_to: UnboundedSender, players: PlayerManager, @@ -507,11 +509,15 @@ impl Connection { let (to_node, from_lavalink) = mpsc::unbounded_channel(); let (to_lavalink, from_node) = mpsc::unbounded_channel(); + let lavalink_http = HyperClient::builder(TokioExecutor::new()) + .http2_only(cfg!(feature = "lavalink-protocol-http2")) + .build_http(); Ok(( Self { config, stream, + lavalink_http, node_from: from_node, node_to: to_node, players, @@ -539,7 +545,6 @@ impl Connection { self.outgoing(outgoing).await?; } else { tracing::debug!("node {} closed, ending connection", self.config.address); - break; } } @@ -549,55 +554,6 @@ impl Connection { Ok(()) } - #[cfg(feature = "lavalink-protocol-http2")] - async fn get_http_connection(&self, address: &str) -> Result, NodeError> { - let stream = TcpStream::connect(address) - .await - .map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; - - let io = TokioIo::new(stream); - let exec = TokioExecutor::new(); - - let (sender, conn) = hyper::client::conn::http2::handshake(exec, io).await.map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; - tokio::task::spawn(async move { - if let Err(err) = conn.await { - tracing::error!("Connection failed: {err:?}"); - } - }); - - Ok(sender) - } - - #[cfg(not(feature = "lavalink-protocol-http2"))] - async fn get_http_connection(&self, address: &str) -> Result, NodeError> { - let stream = TcpStream::connect(address) - .await - .map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; - - let io = TokioIo::new(stream); - - let (sender, conn) = hyper::client::conn::http1::handshake(io).await.map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?; - tokio::task::spawn(async move { - if let Err(err) = conn.await { - tracing::error!("Connection failed: {err:?}"); - } - }); - - Ok(sender) - } - async fn get_outgoing_endpoint_based_on_event( &self, outgoing: &OutgoingEvent, @@ -648,30 +604,23 @@ impl Connection { tracing::debug!("Sending request to {url:?} using method {method:?}."); tracing::debug!("converted payload: {payload:?}"); - let host = url.host().expect("uri has no host"); - let port = url.port_u16().unwrap_or(80); - - let address = format!("{host}:{port}"); - - let mut sender = self.get_http_connection(&address).await?; - let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header.").clone(); - let request = Request::builder() + let req = Request::builder() .uri(url) .method(method) .header(hyper::header::HOST, authority.as_str()) .header(AUTHORIZATION, self.config.authorization.as_str()) .header("Content-Type", "application/json") - .body(payload) + .body(Full::from(payload)) .map_err(|source| NodeError { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), })?; - tracing::debug!("Request: {request:?}"); + tracing::debug!("Request: {req:?}"); - let response = sender.send_request(request).await.unwrap(); + let response = self.lavalink_http.request(req).await.unwrap(); tracing::debug!("Response status: {}", response.status()); Ok(()) From 2e780996a1a507ea7df30e85b2142a8d533f324f Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 16:21:15 -0400 Subject: [PATCH 49/70] fix: Spelling and formatting corrected --- examples/lavalink-basic-bot.rs | 4 ++-- twilight-lavalink/README.md | 2 +- twilight-lavalink/src/node.rs | 10 ++-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index ec63687d0a6..a402a4f760e 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -213,8 +213,8 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { .content(&content) .await?; } - Playlist(playist) => { - if let Some(top_track) = playist.tracks.first() { + Playlist(playlist) => { + if let Some(top_track) = playlist.tracks.first() { player.send(Play::from((guild_id, &top_track.encoded)))?; let content = format!( diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index 72bb05a2e41..bebecf74098 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -37,7 +37,7 @@ The `lavalink-protocol-http2` switches the underlying protocol to communicate wi If enabled, http2 will be used. By default, http1 is used. You will need to enable http2 support in your lavalink server configuration if you want to use this feature because by default it is disabled. -***NOTE: This is not to be confused with the `http-support` support flag or crate. This is seperated and doesn't depend on the use of that feature.*** +***NOTE: This is not to be confused with the `http-support` support flag or crate. This is separate and doesn't depend on the use of that feature.*** ### TLS diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 45ae56f24fd..1b7dcb7e4bc 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,16 +17,10 @@ //! //! [`Lavalink`]: crate::client::Lavalink -use hyper::{ - Method, Request, Uri, - body::Bytes, -}; +use hyper::{body::Bytes, Method, Request, Uri}; use hyper_util::{ + client::legacy::{connect::HttpConnector, Client as HyperClient}, rt::TokioExecutor, - client::legacy::{ - Client as HyperClient, - connect::HttpConnector, - }, }; use http_body_util::Full; From 9477c59cebd03a56a9a2d9535f4fc6b3279a2200 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 16:34:57 -0400 Subject: [PATCH 50/70] fix: Tests now pass based on new struct order --- twilight-lavalink/src/model.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index b5527a18c4b..c5b013b55c6 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -406,7 +406,7 @@ mod lavalink_outgoing_model_tests { }; compare_json_payload( &voice, - r#"{"voice":{"token":"863ea8ef2ads8ef2","endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f"}}"#, + r#"{"voice":{"endpoint":"eu-centra654863.discord.media:443","sessionId":"asdf5w1efa65feaf315e8a8effsa1e5f","token":"863ea8ef2ads8ef2"}}"#, ); } @@ -425,7 +425,7 @@ mod lavalink_outgoing_model_tests { }); compare_json_payload( &play, - r#"{"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"},"endTime":null}"#, + r#"{"endTime":null,"track":{"encoded":"QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA"}}"#, ); } From 22b6ae961b4c7d629f5053e392086ec1954e3f5f Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Wed, 27 Mar 2024 16:48:43 -0400 Subject: [PATCH 51/70] fix: hyper_utils missing feature flags --- twilight-lavalink/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 16b371075b8..787974c9daf 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -18,7 +18,7 @@ dashmap = { default-features = false, version = "5.3" } futures-util = { default-features = false, features = ["bilock", "sink", "std", "unstable"], version = "0.3" } http = { default-features = false, version = "1" } hyper = { default-features = false, features = ["client", "http1", "http2"], version = "1" } -hyper-util = { default-features = false, features = ["client-legacy", "tokio"], version = "0.1.2" } +hyper-util = { default-features = false, features = ["client-legacy", "client", "http1", "http2", "tokio"], version = "0.1" } http-body-util = "0.1" serde = { default-features = false, features = ["derive", "std"], version = "1" } serde_json = { default-features = false, features = ["std"], version = "1" } From 7095e46338cba287919d33a5c53ba6df3e802dcb Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sat, 6 Apr 2024 11:55:55 -0400 Subject: [PATCH 52/70] refactor(lavalink): Adding suggested changes based on feedback Updating some grammar errors, formatting, spelling, and some code reduction. --- examples/lavalink-basic-bot.rs | 85 +++++++------------------ twilight-lavalink/Cargo.toml | 5 +- twilight-lavalink/README.md | 13 +--- twilight-lavalink/src/http.rs | 2 +- twilight-lavalink/src/model/incoming.rs | 2 +- twilight-lavalink/src/node.rs | 76 +++++++++++----------- 6 files changed, 68 insertions(+), 115 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index a402a4f760e..f4b6288ab8b 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -199,69 +199,32 @@ async fn play(msg: Message, state: State) -> anyhow::Result<()> { let loaded = serde_json::from_slice::(&response_bytes)?; - match loaded.data { - Track(track) => { - player.send(Play::from((guild_id, &track.encoded)))?; - - let content = format!( - "Playing **{:?}** by **{:?}**", - track.info.title, track.info.author - ); - state - .http - .create_message(msg.channel_id) - .content(&content) - .await?; - } - Playlist(playlist) => { - if let Some(top_track) = playlist.tracks.first() { - player.send(Play::from((guild_id, &top_track.encoded)))?; - - let content = format!( - "Playing **{:?}** by **{:?}**", - top_track.info.title, top_track.info.author - ); - state - .http - .create_message(msg.channel_id) - .content(&content) - .await?; - } + let track = match loaded.data { + Track(track) => Some(track), + Playlist(top_track) => top_track.tracks.first().cloned(), + Search(result) => result.first().cloned(), + _ => None, + }; - state - .http - .create_message(msg.channel_id) - .content("Didn't find any playlist results") - .await?; - } - Search(result) => { - if let Some(first_result) = result.first() { - player.send(Play::from((guild_id, &first_result.encoded)))?; - - let content = format!( - "Playing **{:?}** by **{:?}**", - first_result.info.title, first_result.info.author - ); - state - .http - .create_message(msg.channel_id) - .content(&content) - .await?; - } + if let Some(track) = track { + player.send(Play::from((guild_id, &track.encoded)))?; - state - .http - .create_message(msg.channel_id) - .content("Didn't find any search results") - .await?; - } - _ => { - state - .http - .create_message(msg.channel_id) - .content("Didn't find any results") - .await?; - } + let content = format!( + "Playing **{:?}** by **{:?}**", + track.info.title, track.info.author + ); + + state + .http + .create_message(msg.channel_id) + .content(&content) + .await?; + } else { + state + .http + .create_message(msg.channel_id) + .content("Didn't find any results") + .await?; } Ok(()) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 787974c9daf..9d56249231d 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -40,12 +40,11 @@ twilight-gateway = { default-features = false, features = ["rustls-native-roots" twilight-http = { default-features = false, features = ["rustls-native-roots"], path = "../twilight-http", version = "0.16.0-rc.1" } [features] -default = ["http-support", "rustls-native-roots"] -http-support = ["dep:percent-encoding"] +default = ["rustls-native-roots"] native-tls = ["tokio-websockets/native-tls", "tokio-websockets/openssl"] rustls-native-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-native-roots"] rustls-webpki-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-webpki-roots"] -lavalink-protocol-http2 = [] +http2 = ["hyper/http2", "hyper-util/http2"] [package.metadata.docs.rs] all-features = true diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index bebecf74098..03eb730b4be 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -26,19 +26,12 @@ Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. ## Features -### `http-support` +### `http2` -The `http-support` feature adds support for the `http` module to return -request types from the [`http`] crate. This is enabled by default. - -### `lavalink-protocol-http2` - -The `lavalink-protocol-http2` switches the underlying protocol to communicate with the lavalink server. -If enabled, http2 will be used. By default, http1 is used. You will need to enable http2 support +The `http2` switches the underlying protocol to communicate with the lavalink server. +If enabled, http2 will be used instead. You will need to enable http2 support in your lavalink server configuration if you want to use this feature because by default it is disabled. -***NOTE: This is not to be confused with the `http-support` support flag or crate. This is separate and doesn't depend on the use of that feature.*** - ### TLS `twilight-lavalink` has features to enable [`tokio-websockets`]' TLS diff --git a/twilight-lavalink/src/http.rs b/twilight-lavalink/src/http.rs index 527758cffdf..f7d66fe3849 100644 --- a/twilight-lavalink/src/http.rs +++ b/twilight-lavalink/src/http.rs @@ -56,7 +56,7 @@ pub enum LoadResultName { #[serde(untagged)] pub enum LoadResultData { /// Empty data response. - Empty(), + Empty, /// The exception that was thrown when searching. Error(Exception), /// The playlist results with the play list info and tracks in the playlist. diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 3a0b851ab8b..4d666de31e0 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -96,7 +96,7 @@ pub struct VoiceState { pub token: String, } -/// An update about the information of a player. Filters are currently unsupported +/// An update of a player's status and state. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 1b7dcb7e4bc..35119e72dd4 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -17,16 +17,8 @@ //! //! [`Lavalink`]: crate::client::Lavalink -use hyper::{body::Bytes, Method, Request, Uri}; -use hyper_util::{ - client::legacy::{connect::HttpConnector, Client as HyperClient}, - rt::TokioExecutor, -}; - -use http_body_util::Full; - use crate::{ - model::{IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, + model::{incoming, IncomingEvent, OutgoingEvent, PlayerUpdate, Stats, StatsCpu, StatsMemory}, player::PlayerManager, }; use futures_util::{ @@ -35,7 +27,13 @@ use futures_util::{ stream::{Stream, StreamExt}, }; use http::header::{HeaderName, HeaderValue, AUTHORIZATION}; +use hyper::{body::Bytes, header, Method, Request, Uri}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client as HyperClient}, + rt::TokioExecutor, +}; use std::{ + borrow::Borrow, error::Error, fmt::{Debug, Display, Formatter, Result as FmtResult}, net::SocketAddr, @@ -53,6 +51,8 @@ use tokio_websockets::{ }; use twilight_model::id::{marker::UserMarker, Id}; +use http_body_util::Full; + /// An error occurred while either initializing a connection or while running /// its event loop. #[derive(Debug)] @@ -120,7 +120,9 @@ pub enum NodeErrorType { BuildingConnectionRequest, /// Connecting to the Lavalink server failed after several backoff attempts. Connecting, - /// The error for outgoing having no session id to connect to. You can potentially have no valid session before trying to outgoing events + /// You can potentially have no valid session before trying to send outgoing + /// events. The session id is obtained in the startup sequence of the node. + /// If you attempt to send events before connecting you will error out. OutgoingEventHasNoSession, /// Serializing a JSON message to be sent to a Lavalink node failed. SerializingMessage { @@ -374,7 +376,7 @@ impl Node { players: PlayerManager, ) -> Result<(Self, IncomingEvents), NodeError> { let (bilock_left, bilock_right) = BiLock::new(Stats { - op: crate::model::incoming::Opcode::Stats, + op: incoming::Opcode::Stats, cpu: StatsCpu { cores: 0, lavalink_load: 0f64, @@ -504,7 +506,7 @@ impl Connection { let (to_node, from_lavalink) = mpsc::unbounded_channel(); let (to_lavalink, from_node) = mpsc::unbounded_channel(); let lavalink_http = HyperClient::builder(TokioExecutor::new()) - .http2_only(cfg!(feature = "lavalink-protocol-http2")) + .http2_only(cfg!(feature = "http2")) .build_http(); Ok(( @@ -553,7 +555,7 @@ impl Connection { outgoing: &OutgoingEvent, ) -> Result<(Method, hyper::Uri), NodeError> { let address = self.config.address; - tracing::debug!("forwarding event to {}: {outgoing:?}", address,); + tracing::debug!("forwarding event to {}: {outgoing:?}", address); let guild_id = OutgoingEvent::guild_id(outgoing); let no_replace = OutgoingEvent::no_replace(outgoing); @@ -564,27 +566,25 @@ impl Connection { session ); - if let OutgoingEvent::Destroy(_) = outgoing.clone() { - let destroy_uri = Uri::builder() - .scheme("http") - .authority(address.to_string()) - .path_and_query(format!("/v4/sessions/{session}/players/{guild_id}")) - .build() - .unwrap(); - return Ok((Method::DELETE, destroy_uri)); + let mut path = format!("/v4/sessions/{session}/players/{guild_id}"); + if !matches!(outgoing, OutgoingEvent::Destroy(_)) { + path.push_str(&format!("?noReplace={no_replace}")); } let uri = Uri::builder() .scheme("http") .authority(address.to_string()) - .path_and_query(format!( - "/v4/sessions/{session}/players/{guild_id}?noReplace={no_replace}" - )) + .path_and_query(path) .build() - .unwrap(); - return Ok((Method::PATCH, uri)); + .expect("uri is valid"); + return if matches!(outgoing, OutgoingEvent::Destroy(_)) { + Ok((Method::DELETE, uri)) + } else { + Ok((Method::PATCH, uri)) + }; } tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); + Err(NodeError { kind: NodeErrorType::OutgoingEventHasNoSession, source: None, @@ -593,26 +593,26 @@ impl Connection { async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; - let payload = serde_json::to_string(&outgoing).unwrap(); + let payload = serde_json::to_string(&outgoing).expect("serialization cannot fail"); tracing::debug!("Sending request to {url:?} using method {method:?}."); tracing::debug!("converted payload: {payload:?}"); - let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header.").clone(); + let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header."); let req = Request::builder() - .uri(url) + .uri(url.borrow()) .method(method) - .header(hyper::header::HOST, authority.as_str()) - .header(AUTHORIZATION, self.config.authorization.as_str()) - .header("Content-Type", "application/json") + .header(header::HOST, authority.as_str()) + .header(header::AUTHORIZATION, self.config.authorization.as_str()) + .header(header::CONTENT_TYPE, "application/json") .body(Full::from(payload)) .map_err(|source| NodeError { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), })?; - tracing::debug!("Request: {req:?}"); + tracing::trace!("Request: {req:?}"); let response = self.lavalink_http.request(req).await.unwrap(); @@ -759,14 +759,12 @@ async fn reconnect( "key": config.address, "timeout": resume.timeout, }); - let msg = - Message::text(serde_json::to_string(&payload).map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?); + let msg = Message::text( + serde_json::to_string(&payload).expect("Serialize can't panic here."), + ); stream.send(msg).await.map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, + kind: NodeErrorType::Connecting, source: Some(Box::new(source)), })?; } else { From cd4906c32669fcf35f5e0fdf9385469fcb4e45bf Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sat, 6 Apr 2024 12:00:48 -0400 Subject: [PATCH 53/70] refactor(lavalink): Adding new error for the failed to request over http on lavalink connection --- twilight-lavalink/src/node.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 35119e72dd4..2cd97727fd2 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -87,6 +87,9 @@ impl Display for NodeError { NodeErrorType::BuildingConnectionRequest { .. } => { f.write_str("failed to build connection request") } + NodeErrorType::HttpRequestFailed { .. } => { + f.write_str("failed to send http request to lavalink server") + } NodeErrorType::Connecting { .. } => f.write_str("Failed to connect to the node"), NodeErrorType::OutgoingEventHasNoSession { .. } => { f.write_str("No session id found for connection to lavalink api.") @@ -118,6 +121,8 @@ impl Error for NodeError { pub enum NodeErrorType { /// Building the HTTP request to initialize a connection failed. BuildingConnectionRequest, + /// The request to send to lavalink has failed, + HttpRequestFailed, /// Connecting to the Lavalink server failed after several backoff attempts. Connecting, /// You can potentially have no valid session before trying to send outgoing @@ -614,7 +619,14 @@ impl Connection { tracing::trace!("Request: {req:?}"); - let response = self.lavalink_http.request(req).await.unwrap(); + let response = self + .lavalink_http + .request(req) + .await + .map_err(|source| NodeError { + kind: NodeErrorType::HttpRequestFailed, + source: Some(Box::new(source)), + })?; tracing::debug!("Response status: {}", response.status()); Ok(()) From ddbd6f4307bc5d94721ea479e21633e6706f757d Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Sat, 6 Apr 2024 12:17:22 -0400 Subject: [PATCH 54/70] fix(lavalink): Removed the optional dependencies after removing the feature flag `http-support` --- twilight-lavalink/Cargo.toml | 4 +--- twilight-lavalink/src/lib.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 9d56249231d..0f4c43a77eb 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -26,9 +26,7 @@ tokio = { default-features = false, features = ["macros", "net", "rt", "sync", " tokio-websockets = { default-features = false, features = ["client", "fastrand", "sha1_smol", "simd"], version = "0.7" } tracing = { default-features = false, features = ["std", "attributes"], version = "0.1" } twilight-model = { default-features = false, path = "../twilight-model", version = "0.16.0-rc.1" } - -# Optional dependencies. -percent-encoding = { default-features = false, optional = true, version = "2" } +percent-encoding = "2" [dev-dependencies] anyhow = { default-features = false, features = ["std"], version = "1" } diff --git a/twilight-lavalink/src/lib.rs b/twilight-lavalink/src/lib.rs index 367a4bf3ce8..7e660a79065 100644 --- a/twilight-lavalink/src/lib.rs +++ b/twilight-lavalink/src/lib.rs @@ -13,11 +13,9 @@ )] pub mod client; +pub mod http; pub mod model; pub mod node; pub mod player; -#[cfg(feature = "http-support")] -pub mod http; - pub use self::{client::Lavalink, node::Node, player::PlayerManager}; From f8d66acfd86ba3ee39d97a8758c286aefc994568 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:02:09 +0200 Subject: [PATCH 55/70] Use `self` in getters --- twilight-lavalink/src/model/outgoing.rs | 28 ++++++++++++------------- twilight-lavalink/src/node.rs | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 80665f6c355..0dd0e88e0d3 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -89,24 +89,24 @@ impl From for OutgoingEvent { impl OutgoingEvent { /// Helper function to get the `guild_id` attached to the event type. - pub const fn guild_id(event: &OutgoingEvent) -> Id { - match event { - OutgoingEvent::VoiceUpdate(voice_update) => voice_update.guild_id, - OutgoingEvent::Play(play) => play.guild_id, - OutgoingEvent::Destroy(destroy) => destroy.guild_id, - OutgoingEvent::Equalizer(equalize) => equalize.guild_id, - OutgoingEvent::Pause(pause) => pause.guild_id, - OutgoingEvent::Seek(seek) => seek.guild_id, - OutgoingEvent::Stop(stop) => stop.guild_id, - OutgoingEvent::Volume(volume) => volume.guild_id, + pub const fn guild_id(&self) -> Id { + match self { + Self::VoiceUpdate(voice_update) => voice_update.guild_id, + Self::Play(play) => play.guild_id, + Self::Destroy(destroy) => destroy.guild_id, + Self::Equalizer(equalize) => equalize.guild_id, + Self::Pause(pause) => pause.guild_id, + Self::Seek(seek) => seek.guild_id, + Self::Stop(stop) => stop.guild_id, + Self::Volume(volume) => volume.guild_id, } } /// Helper function to get whether or not to replace the current track in the lavalink api based on the event type. - pub const fn no_replace(event: &OutgoingEvent) -> bool { - match event { - OutgoingEvent::Play(play) => play.no_replace, - OutgoingEvent::Stop(_) => false, + pub const fn no_replace(&self) -> bool { + match self { + Self::Play(play) => play.no_replace, + Self::Stop(_) => false, _ => true, } } diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 2cd97727fd2..397d0769c29 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -562,8 +562,8 @@ impl Connection { let address = self.config.address; tracing::debug!("forwarding event to {}: {outgoing:?}", address); - let guild_id = OutgoingEvent::guild_id(outgoing); - let no_replace = OutgoingEvent::no_replace(outgoing); + let guild_id = outgoing.guild_id(); + let no_replace = outgoing.no_replace(); if let Some(session) = self.lavalink_session_id.lock().await.clone() { tracing::debug!( From 99a00b70f5aba19d04ef19bcd0710439c30707dd Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:08:13 +0200 Subject: [PATCH 56/70] fmt imports --- twilight-lavalink/src/node.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 397d0769c29..8837567b39b 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -27,6 +27,7 @@ use futures_util::{ stream::{Stream, StreamExt}, }; use http::header::{HeaderName, HeaderValue, AUTHORIZATION}; +use http_body_util::Full; use hyper::{body::Bytes, header, Method, Request, Uri}; use hyper_util::{ client::legacy::{connect::HttpConnector, Client as HyperClient}, @@ -51,8 +52,6 @@ use tokio_websockets::{ }; use twilight_model::id::{marker::UserMarker, Id}; -use http_body_util::Full; - /// An error occurred while either initializing a connection or while running /// its event loop. #[derive(Debug)] From 885c4d89890b27aae8f8de8dcd8c26edb291dca5 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:13:50 +0200 Subject: [PATCH 57/70] Remove node session_id lock --- twilight-lavalink/src/node.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 8837567b39b..7b4e9288404 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -489,7 +489,7 @@ struct Connection { node_to: UnboundedSender, players: PlayerManager, stats: BiLock, - lavalink_session_id: BiLock>, + lavalink_session_id: Option>, } impl Connection { @@ -522,7 +522,7 @@ impl Connection { node_to: to_node, players, stats, - lavalink_session_id: BiLock::new(None).0, + lavalink_session_id: None, }, to_lavalink, from_lavalink, @@ -555,7 +555,7 @@ impl Connection { } async fn get_outgoing_endpoint_based_on_event( - &self, + &mut self, outgoing: &OutgoingEvent, ) -> Result<(Method, hyper::Uri), NodeError> { let address = self.config.address; @@ -564,7 +564,7 @@ impl Connection { let guild_id = outgoing.guild_id(); let no_replace = outgoing.no_replace(); - if let Some(session) = self.lavalink_session_id.lock().await.clone() { + if let Some(session) = &self.lavalink_session_id { tracing::debug!( "Found session id {}. Generating the url and method for event type.", session @@ -595,7 +595,7 @@ impl Connection { }) } - async fn outgoing(&self, outgoing: OutgoingEvent) -> Result<(), NodeError> { + async fn outgoing(&mut self, outgoing: OutgoingEvent) -> Result<(), NodeError> { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; let payload = serde_json::to_string(&outgoing).expect("serialization cannot fail"); @@ -658,7 +658,7 @@ impl Connection { match &event { IncomingEvent::PlayerUpdate(update) => self.player_update(update)?, IncomingEvent::Ready(ready) => { - *self.lavalink_session_id.lock().await = Some(ready.session_id.clone()); + self.lavalink_session_id = Some(ready.session_id.clone().into_boxed_str()); } IncomingEvent::Stats(stats) => self.stats(stats).await?, &IncomingEvent::Event(_) => {} From a999a13032ce3075cdb0c458aa1ba9155a7170bb Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:33:19 +0200 Subject: [PATCH 58/70] Conditionally enable HTTP/2 --- twilight-lavalink/Cargo.toml | 8 ++++---- twilight-lavalink/src/node.rs | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 0f4c43a77eb..241811b8417 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -17,8 +17,8 @@ version = "0.16.0-rc.1" dashmap = { default-features = false, version = "5.3" } futures-util = { default-features = false, features = ["bilock", "sink", "std", "unstable"], version = "0.3" } http = { default-features = false, version = "1" } -hyper = { default-features = false, features = ["client", "http1", "http2"], version = "1" } -hyper-util = { default-features = false, features = ["client-legacy", "client", "http1", "http2", "tokio"], version = "0.1" } +hyper = { default-features = false, features = ["client", "http1"], version = "1" } +hyper-util = { default-features = false, features = ["client-legacy", "client", "http1", "tokio"], version = "0.1" } http-body-util = "0.1" serde = { default-features = false, features = ["derive", "std"], version = "1" } serde_json = { default-features = false, features = ["std"], version = "1" } @@ -38,11 +38,11 @@ twilight-gateway = { default-features = false, features = ["rustls-native-roots" twilight-http = { default-features = false, features = ["rustls-native-roots"], path = "../twilight-http", version = "0.16.0-rc.1" } [features] -default = ["rustls-native-roots"] +default = ["http2", "rustls-native-roots"] +http2 = ["hyper/http2", "hyper-util/http2"] native-tls = ["tokio-websockets/native-tls", "tokio-websockets/openssl"] rustls-native-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-native-roots"] rustls-webpki-roots = ["tokio-websockets/ring", "tokio-websockets/rustls-webpki-roots"] -http2 = ["hyper/http2", "hyper-util/http2"] [package.metadata.docs.rs] all-features = true diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 7b4e9288404..156d4eb6cf3 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -509,9 +509,7 @@ impl Connection { let (to_node, from_lavalink) = mpsc::unbounded_channel(); let (to_lavalink, from_node) = mpsc::unbounded_channel(); - let lavalink_http = HyperClient::builder(TokioExecutor::new()) - .http2_only(cfg!(feature = "http2")) - .build_http(); + let lavalink_http = HyperClient::builder(TokioExecutor::new()).build_http(); Ok(( Self { From 115937289ed73afb3962f5c820489bcabce06eec Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:34:19 +0200 Subject: [PATCH 59/70] Make client-name const --- twilight-lavalink/src/node.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 156d4eb6cf3..1a882fdb5ef 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -703,10 +703,9 @@ impl Drop for Connection { } } -fn connect_request(state: &NodeConfig) -> Result { - let crate_version = env!("CARGO_PKG_VERSION"); - let client_name = format!("twilight-lavalink/{crate_version}"); +const TWILIGHT_CLIENT_NAME: &str = concat!("twilight-lavalink/", env!("CARGO_PKG_VERSION")); +fn connect_request(state: &NodeConfig) -> Result { let mut builder = ClientBuilder::new() .uri(&format!("ws://{}/v4/websocket", state.address)) .map_err(|source| NodeError { @@ -726,10 +725,7 @@ fn connect_request(state: &NodeConfig) -> Result { ) .add_header( HeaderName::from_static("client-name"), - HeaderValue::from_str(&client_name).map_err(|source| NodeError { - kind: NodeErrorType::BuildingConnectionRequest, - source: Some(Box::new(source)), - })?, + HeaderValue::from_static(TWILIGHT_CLIENT_NAME), ); if state.resume.is_some() { From a4c7af637cb785137bc6fa7a5515907711ded92b Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:38:16 +0200 Subject: [PATCH 60/70] Avoid leaking authorization Authorization is to be redacted. In this case, the request is not that interesting so the entire event is removed --- twilight-lavalink/src/node.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 1a882fdb5ef..470f537b788 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -614,8 +614,6 @@ impl Connection { source: Some(Box::new(source)), })?; - tracing::trace!("Request: {req:?}"); - let response = self .lavalink_http .request(req) From 2510e166d259ce51d99527e301d7430f73d39626 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 11:49:10 +0200 Subject: [PATCH 61/70] Remove overly verbose tracing events --- twilight-lavalink/src/node.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 470f537b788..70b69d6fd6f 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -557,17 +557,12 @@ impl Connection { outgoing: &OutgoingEvent, ) -> Result<(Method, hyper::Uri), NodeError> { let address = self.config.address; - tracing::debug!("forwarding event to {}: {outgoing:?}", address); + tracing::debug!("forwarding event to {address}: {outgoing:?}"); let guild_id = outgoing.guild_id(); let no_replace = outgoing.no_replace(); if let Some(session) = &self.lavalink_session_id { - tracing::debug!( - "Found session id {}. Generating the url and method for event type.", - session - ); - let mut path = format!("/v4/sessions/{session}/players/{guild_id}"); if !matches!(outgoing, OutgoingEvent::Destroy(_)) { path.push_str(&format!("?noReplace={no_replace}")); @@ -597,9 +592,6 @@ impl Connection { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; let payload = serde_json::to_string(&outgoing).expect("serialization cannot fail"); - tracing::debug!("Sending request to {url:?} using method {method:?}."); - tracing::debug!("converted payload: {payload:?}"); - let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header."); let req = Request::builder() @@ -614,8 +606,7 @@ impl Connection { source: Some(Box::new(source)), })?; - let response = self - .lavalink_http + self.lavalink_http .request(req) .await .map_err(|source| NodeError { @@ -623,7 +614,6 @@ impl Connection { source: Some(Box::new(source)), })?; - tracing::debug!("Response status: {}", response.status()); Ok(()) } From d12aed80621e2acd49140ebee7beca14656fa1c2 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Sun, 16 Jun 2024 12:08:48 +0200 Subject: [PATCH 62/70] Improve module documentation --- twilight-lavalink/src/model/incoming.rs | 29 +++++++++++++++++-------- twilight-lavalink/src/model/outgoing.rs | 14 ++++++------ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/twilight-lavalink/src/model/incoming.rs b/twilight-lavalink/src/model/incoming.rs index 4d666de31e0..f280ef8cf21 100644 --- a/twilight-lavalink/src/model/incoming.rs +++ b/twilight-lavalink/src/model/incoming.rs @@ -23,15 +23,22 @@ use twilight_model::id::{marker::GuildMarker, Id}; #[non_exhaustive] #[serde(rename_all = "camelCase")] pub enum Severity { - /// The cause is known and expected, indicates that there is nothing wrong with the library itself. + /// The cause is known and expected, indicates that there is nothing wrong + /// with the library itself. Common, - /// The probable cause is an issue with the library or there is no way to tell what the cause might be. This is the default level and other levels are used in cases where the thrower has more in-depth knowledge about the error. + /// The probable cause is an issue with the library or there is no way to + /// tell what the cause might be. This is the default level and other + /// levels are used in cases where the thrower has more in-depth knowledge + /// about the error. Fault, - /// The cause might not be exactly known, but is possibly caused by outside factors. For example when an outside service responds in a format that we do not expect. + /// The cause might not be exactly known, but is possibly caused by outside + /// factors. For example when an outside service responds in a format that + /// we do not expect. Suspicious, } -/// The exception with the details attached on what happened when making a query to lavalink. +/// The exception with the details attached on what happened when making a query +/// to lavalink. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] @@ -83,14 +90,16 @@ impl From for IncomingEvent { } } -/// The discord voice information that lavalink uses for connection and sending information. +/// The discord voice information that lavalink uses for connection and sending +/// information. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct VoiceState { /// The Discord voice endpoint to connect to. pub endpoint: String, - /// The Discord voice session id to authenticate with. Note this is separate from the lavalink session id. + /// The Discord voice session id to authenticate with. Note this is separate + /// from the lavalink session id. pub session_id: String, /// The Discord voice token to authenticate with. pub token: String, @@ -124,7 +133,7 @@ pub struct PlayerUpdateState { pub time: i64, } -/// Dispatched by Lavalink upon successful connection and authorization. Contains fields determining if resuming was successful, as well as the session id. +/// Dispatched by Lavalink upon successful connection and authorization. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] @@ -133,7 +142,8 @@ pub struct Ready { pub op: Opcode, /// Whether this session was resumed. pub resumed: bool, - /// The Lavalink session id of this connection. Not to be confused with a Discord voice session id. + /// The Lavalink session id of this connection. Not to be confused with a + /// Discord voice session id. pub session_id: String, } @@ -356,7 +366,8 @@ pub struct TrackStuck { pub struct WebSocketClosed { /// True if Discord closed the connection, false if Lavalink closed it. pub by_remote: bool, - /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) that closed the connection. + /// [Discord websocket opcode](https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes) + /// that closed the connection. pub code: u64, /// Reason the connection was closed. pub reason: String, diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 0dd0e88e0d3..5b882432536 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -5,14 +5,14 @@ use twilight_model::{ id::{marker::GuildMarker, Id}, }; -/// The track on the player. The encoded and identifier are mutually exclusive. Using only encoded for now. -/// Encoded was chosen since that was previously used in the v3 implementation. -/// We don't support userData field currently. +/// The track on the player. The encoded and identifier are mutually exclusive. +/// Using only encoded for now. Encoded was chosen since that was previously +/// used in the v3 implementation. We don't support userData field currently. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct UpdatePlayerTrack { - /// The base64 encoded track to play. null stops the current track + /// The base64 encoded track to play. pub encoded: Option, } @@ -88,7 +88,7 @@ impl From for OutgoingEvent { } impl OutgoingEvent { - /// Helper function to get the `guild_id` attached to the event type. + /// ID of the destination guild of this event. pub const fn guild_id(&self) -> Id { match self { Self::VoiceUpdate(voice_update) => voice_update.guild_id, @@ -102,8 +102,8 @@ impl OutgoingEvent { } } - /// Helper function to get whether or not to replace the current track in the lavalink api based on the event type. - pub const fn no_replace(&self) -> bool { + /// Whether this event replaces the currently playing track. + pub(crate) const fn no_replace(&self) -> bool { match self { Self::Play(play) => play.no_replace, Self::Stop(_) => false, From 199190d51d527bdf01de2b723fa6d5eb9511a384 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Mon, 17 Jun 2024 10:48:48 +0200 Subject: [PATCH 63/70] Add no default features to deps --- twilight-lavalink/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twilight-lavalink/Cargo.toml b/twilight-lavalink/Cargo.toml index 241811b8417..4eb087f0fd6 100644 --- a/twilight-lavalink/Cargo.toml +++ b/twilight-lavalink/Cargo.toml @@ -26,7 +26,7 @@ tokio = { default-features = false, features = ["macros", "net", "rt", "sync", " tokio-websockets = { default-features = false, features = ["client", "fastrand", "sha1_smol", "simd"], version = "0.7" } tracing = { default-features = false, features = ["std", "attributes"], version = "0.1" } twilight-model = { default-features = false, path = "../twilight-model", version = "0.16.0-rc.1" } -percent-encoding = "2" +percent-encoding = { default-features = false, version = "2" } [dev-dependencies] anyhow = { default-features = false, features = ["std"], version = "1" } From 3038739979080a1c2d988924a3153d30854cf22f Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Mon, 17 Jun 2024 11:41:38 +0200 Subject: [PATCH 64/70] Update some README documentation --- twilight-lavalink/README.md | 18 +++++++++++------- twilight-lavalink/src/lib.rs | 3 +++ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index 03eb730b4be..a2604d60931 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -15,22 +15,24 @@ with every Voice State Update and Voice Server Update you receive. A breakdown of how this functions: - The client is [`Lavalink`](crate::client::Lavalink) that forwards the required events from Discord. - - We read the [Voice State and Voice Server Updates](https://discord.com/developers/docs/topics/gateway-events#voice) from discord to format the data to send to a Lavalink VoiceUpdate Event. + - We read the [Voice State and Voice Server Updates](https://discord.com/developers/docs/topics/gateway-events#voice) from discord to format the data to send to a Lavalink `VoiceUpdate` Event. - There is a lower level [node](crate::node) that processes this for you. It isn't recommended to use this but rather the lavalink struct with the players. If you don't find functionality please open up and issue to expose what you need. - You send the client an [outgoing event](crate::model::outgoing). These include play, pause, seek, etc. You send these through the [player](crate::player) that is attached to Lavalink. - If you want to search or load you need to create a http client currently and then you can use [these helpers functions](crate::http#functions) to generate the http uri and body to send over your http client. you will then get response you can deserialize as json into the structs in the [http module](crate::http). -***NOTE: We currently only support `v4` of Lavlink. Support for `v3` is dropped. There was big changes in the api meaning the outgoing are now using a http client instead of websockets. The json request and responses all changed naming and fields changed.*** - -Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. There are some unsupported end points that were added yet such as [Lavalink Info](https://lavalink.dev/api/rest.html#get-lavalink-version) or [Session Api](https://lavalink.dev/api/rest.html#session-api) that weren't previously available. If you would like native support for something please reach out and open an issue for that feature. The porting only ported the functionality of the previous `v3` forward. +Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. +Some endpoints such as [Lavalink Info] and [Update Session] have also not yet +been implemented. Please reach out and open an issue for any missing feature you +would like to use. The Lavalink V4 port did not add support for any new features +not previously found in V3. ## Features ### `http2` -The `http2` switches the underlying protocol to communicate with the lavalink server. -If enabled, http2 will be used instead. You will need to enable http2 support -in your lavalink server configuration if you want to use this feature because by default it is disabled. +The `http2` feature enables support for communicating with the Lavalink server +over HTTP/2. You will also need to enable http2 support in your Lavalink server +configuration as it is disabled by default. ### TLS @@ -116,6 +118,8 @@ There is also an example of a basic bot located in the [root of the `twilight` repository][github examples link]. [Lavalink]: https://github.com/freyacodes/Lavalink +[Lavalink Info]: https://lavalink.dev/api/rest.html#get-lavalink-version +[Update Session]: https://lavalink.dev/api/rest#update-session [`http`]: https://crates.io/crates/http [`rustls`]: https://crates.io/crates/rustls [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs diff --git a/twilight-lavalink/src/lib.rs b/twilight-lavalink/src/lib.rs index 7e660a79065..8d7db101d90 100644 --- a/twilight-lavalink/src/lib.rs +++ b/twilight-lavalink/src/lib.rs @@ -19,3 +19,6 @@ pub mod node; pub mod player; pub use self::{client::Lavalink, node::Node, player::PlayerManager}; + +/// Lavalink API version used by this crate. +pub const API_VERSION: u8 = 4; From 28c720f9e0b6050dd4def0d9122c21af851a7063 Mon Sep 17 00:00:00 2001 From: "vilgotf@pm.me" Date: Mon, 17 Jun 2024 11:46:00 +0200 Subject: [PATCH 65/70] Clippy suggestions --- twilight-lavalink/src/node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 70b69d6fd6f..46d2a8ee3f3 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -552,7 +552,7 @@ impl Connection { Ok(()) } - async fn get_outgoing_endpoint_based_on_event( + fn get_outgoing_endpoint_based_on_event( &mut self, outgoing: &OutgoingEvent, ) -> Result<(Method, hyper::Uri), NodeError> { @@ -589,7 +589,7 @@ impl Connection { } async fn outgoing(&mut self, outgoing: OutgoingEvent) -> Result<(), NodeError> { - let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing).await?; + let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing)?; let payload = serde_json::to_string(&outgoing).expect("serialization cannot fail"); let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header."); From 9de565674cf92266b01e87782037909c785ceb6b Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 11 Jul 2024 18:31:18 -0400 Subject: [PATCH 66/70] feat: Adding support for Identifier to the UpdatePlayerTrack struct Added the ability to either include the identifier or the encoded track on and update. Each let you do different things for the server. --- twilight-lavalink/src/model.rs | 7 +++++-- twilight-lavalink/src/model/outgoing.rs | 27 +++++++++++++++++++------ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/twilight-lavalink/src/model.rs b/twilight-lavalink/src/model.rs index c5b013b55c6..dac9296c348 100644 --- a/twilight-lavalink/src/model.rs +++ b/twilight-lavalink/src/model.rs @@ -376,6 +376,7 @@ mod lavalink_incoming_model_tests { #[cfg(test)] mod lavalink_outgoing_model_tests { + use crate::model::outgoing::TrackOption; use crate::model::{Destroy, Equalizer, Pause, Play, Seek, Stop, Volume}; use twilight_model::id::{marker::GuildMarker, Id}; @@ -414,7 +415,7 @@ mod lavalink_outgoing_model_tests { fn should_serialize_an_outgoing_play() { let play = OutgoingEvent::Play(Play{ track: UpdatePlayerTrack { - encoded: Some("QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string()), + track_string: TrackOption::Encoded(Some("QAAAzgMAMUJsZWVkIEl0IE91dCBbT2ZmaWNpYWwgTXVzaWMgVmlkZW9dIC0gTGlua2luIFBhcmsAC0xpbmtpbiBQYXJrAAAAAAAClCgAC09udXVZY3FoekNFAAEAK2h0dHBzOi8vd3d3LnlvdXR1YmUuY29tL3dhdGNoP3Y9T251dVljcWh6Q0UBADRodHRwczovL2kueXRpbWcuY29tL3ZpL09udXVZY3FoekNFL21heHJlc2RlZmF1bHQuanBnAAAHeW91dHViZQAAAAAAAAAA".to_string())), }, position: None, end_time: Some(None), @@ -432,7 +433,9 @@ mod lavalink_outgoing_model_tests { #[test] fn should_serialize_an_outgoing_stop() { let stop = OutgoingEvent::Stop(Stop { - track: UpdatePlayerTrack { encoded: None }, + track: UpdatePlayerTrack { + track_string: TrackOption::Encoded(None), + }, guild_id: Id::::new(987_654_321), }); compare_json_payload(&stop, r#"{"track":{"encoded":null}}"#); diff --git a/twilight-lavalink/src/model/outgoing.rs b/twilight-lavalink/src/model/outgoing.rs index 5b882432536..86c8687a9ba 100644 --- a/twilight-lavalink/src/model/outgoing.rs +++ b/twilight-lavalink/src/model/outgoing.rs @@ -6,14 +6,27 @@ use twilight_model::{ }; /// The track on the player. The encoded and identifier are mutually exclusive. -/// Using only encoded for now. Encoded was chosen since that was previously -/// used in the v3 implementation. We don't support userData field currently. +/// We don't support userData field currently. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct UpdatePlayerTrack { - /// The base64 encoded track to play. - pub encoded: Option, + /// The string of the track to play. + #[serde(flatten)] + pub track_string: TrackOption, +} + +/// Used to play a specific track. These are mutually exclusive. +/// When identifier is used, Lavalink will try to resolve the identifier as a +/// single track. An HTTP 400 error is returned when resolving a playlist, +/// search result, or no tracks. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum TrackOption { + /// The base64 encoded track to play. null stops the current track. + Encoded(Option), + /// The identifier of the track to play. + Identifier(String), } /// An outgoing event to send to Lavalink. @@ -305,7 +318,7 @@ impl, S: Into>, E: Into>> volume: None, paused: None, track: UpdatePlayerTrack { - encoded: Some(track.into()), + track_string: TrackOption::Encoded(Some(track.into())), }, } } @@ -359,7 +372,9 @@ impl From> for Stop { fn from(guild_id: Id) -> Self { Self { guild_id, - track: UpdatePlayerTrack { encoded: None }, + track: UpdatePlayerTrack { + track_string: TrackOption::Encoded(None), + }, } } } From 61b8cd509711e7f731450fb60af4ae61443584b7 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 11 Jul 2024 18:46:22 -0400 Subject: [PATCH 67/70] refactor: Updating the README for lavalink. Moved the comments to the basic example for a bit more documentation --- examples/lavalink-basic-bot.rs | 5 +++++ twilight-lavalink/README.md | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/lavalink-basic-bot.rs b/examples/lavalink-basic-bot.rs index f4b6288ab8b..6ebb864b46f 100644 --- a/examples/lavalink-basic-bot.rs +++ b/examples/lavalink-basic-bot.rs @@ -57,6 +57,7 @@ async fn main() -> anyhow::Result<()> { let http = HttpClient::new(token.clone()); let user_id = http.current_user().await?.model().await?.id; + // The client is [`Lavalink`](crate::client::Lavalink) that forwards the required events from Discord. let lavalink = Lavalink::new(user_id, shard_count); lavalink.add(lavalink_host, lavalink_auth).await?; @@ -85,6 +86,10 @@ async fn main() -> anyhow::Result<()> { }; state.standby.process(&event); + + // We read the [Voice State and Voice Server Updates](https://discord.com/developers/docs/topics/gateway-events#voice) from discord to format the data to send to a Lavalink `VoiceUpdate` Event. + // There is a lower level [node](crate::node) that processes this for you. It isn't recommended to use this but rather the lavalink struct with the players. If you don't find functionality please open up and issue to expose what you need. + // See the command functions for where we use the players. state.lavalink.process(&event).await?; if let Event::MessageCreate(msg) = event { diff --git a/twilight-lavalink/README.md b/twilight-lavalink/README.md index a2604d60931..6f39edf0c97 100644 --- a/twilight-lavalink/README.md +++ b/twilight-lavalink/README.md @@ -13,13 +13,6 @@ handle sending voice channel updates to Lavalink by processing events via the [client's `process` method][`Lavalink::process`], which you must call with every Voice State Update and Voice Server Update you receive. -A breakdown of how this functions: -- The client is [`Lavalink`](crate::client::Lavalink) that forwards the required events from Discord. - - We read the [Voice State and Voice Server Updates](https://discord.com/developers/docs/topics/gateway-events#voice) from discord to format the data to send to a Lavalink `VoiceUpdate` Event. - - There is a lower level [node](crate::node) that processes this for you. It isn't recommended to use this but rather the lavalink struct with the players. If you don't find functionality please open up and issue to expose what you need. -- You send the client an [outgoing event](crate::model::outgoing). These include play, pause, seek, etc. You send these through the [player](crate::player) that is attached to Lavalink. -- If you want to search or load you need to create a http client currently and then you can use [these helpers functions](crate::http#functions) to generate the http uri and body to send over your http client. you will then get response you can deserialize as json into the structs in the [http module](crate::http). - Currently some [Filters](crate::model::outgoing::Filters) are not yet supported. Some endpoints such as [Lavalink Info] and [Update Session] have also not yet been implemented. Please reach out and open an issue for any missing feature you From fd521d90304dd62ce26dbc60b88e522798dacb25 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 11 Jul 2024 19:14:29 -0400 Subject: [PATCH 68/70] fix: Consistency in the capitalization of tracing statements --- twilight-lavalink/src/node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 46d2a8ee3f3..6016833e102 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -580,7 +580,7 @@ impl Connection { }; } - tracing::error!("No session id is found. Session id should have been provided from the websocket connection already."); + tracing::error!("no session id is found. Session id should have been provided from the websocket connection already."); Err(NodeError { kind: NodeErrorType::OutgoingEventHasNoSession, @@ -592,7 +592,7 @@ impl Connection { let (method, url) = self.get_outgoing_endpoint_based_on_event(&outgoing)?; let payload = serde_json::to_string(&outgoing).expect("serialization cannot fail"); - let authority = url.authority().expect("Authority comes from endpoint. We should have a valid authority and is just used in the header."); + let authority = url.authority().expect("Authority comes from endpoint"); let req = Request::builder() .uri(url.borrow()) From 571355e31b3a911ff69c6a24e55f9db30787972a Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 25 Jul 2024 18:10:57 -0400 Subject: [PATCH 69/70] fix: Setting the correct websocket address if TLS is enabled --- twilight-lavalink/src/node.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 6016833e102..8e77884121b 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -694,8 +694,18 @@ impl Drop for Connection { const TWILIGHT_CLIENT_NAME: &str = concat!("twilight-lavalink/", env!("CARGO_PKG_VERSION")); fn connect_request(state: &NodeConfig) -> Result { + let websocket = if cfg!(any( + feature = "native-tls", + feature = "rustls-native-roots", + feature = "rustls-webpki-roots" + )) { + "wss" + } else { + "ws" + }; + let mut builder = ClientBuilder::new() - .uri(&format!("ws://{}/v4/websocket", state.address)) + .uri(&format!("{}://{}/v4/websocket", websocket, state.address)) .map_err(|source| NodeError { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)), From 9d7ad7b8dc176d2d62cd98d92ca6dafa12518766 Mon Sep 17 00:00:00 2001 From: Devon Adair Date: Thu, 25 Jul 2024 18:10:57 -0400 Subject: [PATCH 70/70] Revert "fix: Setting the correct websocket address if TLS is enabled" This reverts commit 1aa6b9b000c7255d157f8e86dd005f4fbc2ee0e2. --- twilight-lavalink/src/node.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/twilight-lavalink/src/node.rs b/twilight-lavalink/src/node.rs index 8e77884121b..6016833e102 100644 --- a/twilight-lavalink/src/node.rs +++ b/twilight-lavalink/src/node.rs @@ -694,18 +694,8 @@ impl Drop for Connection { const TWILIGHT_CLIENT_NAME: &str = concat!("twilight-lavalink/", env!("CARGO_PKG_VERSION")); fn connect_request(state: &NodeConfig) -> Result { - let websocket = if cfg!(any( - feature = "native-tls", - feature = "rustls-native-roots", - feature = "rustls-webpki-roots" - )) { - "wss" - } else { - "ws" - }; - let mut builder = ClientBuilder::new() - .uri(&format!("{}://{}/v4/websocket", websocket, state.address)) + .uri(&format!("ws://{}/v4/websocket", state.address)) .map_err(|source| NodeError { kind: NodeErrorType::BuildingConnectionRequest, source: Some(Box::new(source)),