diff --git a/soulseek-rs-lib/src/actor/server_actor.rs b/soulseek-rs-lib/src/actor/server_actor.rs index aef75ba..0c14298 100644 --- a/soulseek-rs-lib/src/actor/server_actor.rs +++ b/soulseek-rs-lib/src/actor/server_actor.rs @@ -24,7 +24,7 @@ use crate::message::{Handlers, MessageType}; use crate::message::{Message, MessageReader}; use crate::peer::ConnectionType; use crate::peer::Peer; -use crate::types::{RoomEvent, RoomInfo}; +use crate::types::{ClientVersion, RoomEvent, RoomInfo}; use crate::utils::lock::RwLockExt; use std::io::{self, Error, Write}; @@ -159,6 +159,7 @@ pub enum ServerMessage { Login { username: String, password: String, + version: ClientVersion, response: std::sync::mpsc::Sender>, }, FileSearch { @@ -533,9 +534,10 @@ impl ServerActor { ServerMessage::Login { username, password, + version, response, } => { - self.handle_login(username, password, response); + self.handle_login(username, password, version, response); } ServerMessage::FileSearch { token, query } => { self.file_search(token, &query); @@ -640,10 +642,11 @@ impl ServerActor { &mut self, username: String, password: String, + version: ClientVersion, response: std::sync::mpsc::Sender>, ) { self.queue_message(MessageFactory::build_login_message( - &username, &password, + &username, &password, version, )); let start = std::time::Instant::now(); diff --git a/soulseek-rs-lib/src/client/connection.rs b/soulseek-rs-lib/src/client/connection.rs index 3fd2e13..e4ade93 100644 --- a/soulseek-rs-lib/src/client/connection.rs +++ b/soulseek-rs-lib/src/client/connection.rs @@ -108,6 +108,7 @@ impl Client { let _ = handle.send(ServerMessage::Login { username: self.username.clone(), password: self.password.clone(), + version: self.version, response: tx, }); diff --git a/soulseek-rs-lib/src/client/mod.rs b/soulseek-rs-lib/src/client/mod.rs index bac458a..003fa0b 100644 --- a/soulseek-rs-lib/src/client/mod.rs +++ b/soulseek-rs-lib/src/client/mod.rs @@ -4,8 +4,8 @@ use crate::actor::server_actor::{ }; use crate::download_store::{DownloadStore, collect_failed_tokens}; use crate::types::{ - DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo, UserInfo, - UserPresence, UserStats, UserStatus, + ClientVersion, DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo, + UserInfo, UserPresence, UserStats, UserStatus, }; use crate::utils::logger; use crate::{ @@ -139,6 +139,10 @@ pub struct ClientSettings { /// Directories whose files are shared with (uploaded to) other peers. /// Empty means nothing is shared. pub shared_directories: Vec, + /// The version reported to the server on login. Defaults to the + /// soulseek-rs major version with minor version 1; clients and bots built + /// on this library should pick their own minor version. + pub version: ClientVersion, } impl ClientSettings { @@ -166,6 +170,7 @@ impl Default for ClientSettings { enable_listen: true, listen_port: DEFAULT_LISTEN_PORT, shared_directories: Vec::new(), + version: ClientVersion::default(), } } } @@ -762,6 +767,7 @@ pub struct Client { address: PeerAddress, username: String, password: String, + version: ClientVersion, shared_directories: Vec, server_handle: Option>, context: Arc>, @@ -784,6 +790,7 @@ impl Client { address: settings.server_address, username: settings.username, password: settings.password, + version: settings.version, shared_directories: settings.shared_directories, context: Arc::new(RwLock::new(ClientContext::new())), server_handle: None, diff --git a/soulseek-rs-lib/src/lib.rs b/soulseek-rs-lib/src/lib.rs index 4728b07..25e8d4c 100644 --- a/soulseek-rs-lib/src/lib.rs +++ b/soulseek-rs-lib/src/lib.rs @@ -15,9 +15,9 @@ pub mod utils; pub mod prelude { pub use crate::actor::server_actor::PeerAddress; pub use crate::types::{ - DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult, - Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats, - UserStatus, + ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search, + SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo, + UserPresence, UserStats, UserStatus, }; pub use crate::{debug, error, info, trace, warn}; } @@ -28,6 +28,7 @@ pub use client::{Client, ClientSettings}; pub use error::{Result, SoulseekRs}; pub use message::peer::SharedDirectory; pub use types::{ - DownloadStatus, File, RoomEvent, RoomInfo, Search, SearchResult, Transfer, - UploadInfo, UploadStatus, UserInfo, UserPresence, UserStats, UserStatus, + ClientVersion, DownloadStatus, File, RoomEvent, RoomInfo, Search, + SearchResult, Transfer, UploadInfo, UploadStatus, UserInfo, UserPresence, + UserStats, UserStatus, }; diff --git a/soulseek-rs-lib/src/message/server/message_factory.rs b/soulseek-rs-lib/src/message/server/message_factory.rs index 9137927..08be49f 100644 --- a/soulseek-rs-lib/src/message/server/message_factory.rs +++ b/soulseek-rs-lib/src/message/server/message_factory.rs @@ -1,5 +1,8 @@ use crate::{ - message::Message, peer::ConnectionType, types::Transfer, utils::md5::md5, + message::Message, + peer::ConnectionType, + types::{ClientVersion, Transfer}, + utils::md5::md5, }; pub struct MessageFactory; @@ -13,23 +16,11 @@ impl MessageFactory { message } #[must_use] - pub fn build_login_message(username: &str, password: &str) -> Message { - // Message::new_with_data( - // [ - // 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95, 116, 104, 101, - // 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, 55, 53, 49, 51, 55, 160, 0, 0, - // 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, 49, 100, 48, 51, 55, 57, 52, 51, 55, 56, 102, - // 56, 98, 98, 54, 51, 49, 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 17, 0, 0, - // 0, - // //0, // 84, 0, 0, 0, 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95, - // // 116, 104, 101, 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, 55, 53, 49, 51, - // // 55, 160, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, 49, 100, 48, 51, 55, 57, 52, 51, - // // 55, 56, 102, 56, 98, 98, 54, 51, 49, 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 17, 0, 0, - // // 0, - // ] - // .to_vec(), - // ) - // .clone()fac + pub fn build_login_message( + username: &str, + password: &str, + version: ClientVersion, + ) -> Message { let hash = md5([username, password].join("").as_str()); let mut message = Message::new(); @@ -38,9 +29,9 @@ impl MessageFactory { .write_int32(1) .write_string(username) .write_string(password) - .write_int32(157) // version + .write_int32(version.major) .write_string(&hash) - .write_int32(100) + .write_int32(version.minor) .clone() } @@ -268,15 +259,18 @@ fn test_build_watch_user() { #[test] fn test_build_login_message() { - let message = - MessageFactory::build_login_message("insane_in_the_brain2", "13375137"); + let message = MessageFactory::build_login_message( + "insane_in_the_brain2", + "13375137", + ClientVersion::default(), + ); let expect: Vec = [ 1, 0, 0, 0, 20, 0, 0, 0, 105, 110, 115, 97, 110, 101, 95, 105, 110, 95, 116, 104, 101, 95, 98, 114, 97, 105, 110, 50, 8, 0, 0, 0, 49, 51, 51, - 55, 53, 49, 51, 55, 157, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, + 55, 53, 49, 51, 55, 176, 0, 0, 0, 32, 0, 0, 0, 50, 101, 100, 102, 53, 49, 100, 48, 51, 55, 57, 52, 51, 55, 56, 102, 56, 98, 98, 54, 51, 49, - 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 100, 0, 0, 0, + 48, 100, 52, 54, 48, 99, 50, 50, 98, 49, 1, 0, 0, 0, ] .to_vec(); diff --git a/soulseek-rs-lib/src/types.rs b/soulseek-rs-lib/src/types.rs index e1b86c4..c29069d 100644 --- a/soulseek-rs-lib/src/types.rs +++ b/soulseek-rs-lib/src/types.rs @@ -10,6 +10,39 @@ pub struct File { pub size: u64, pub attribs: HashMap, } +/// The client version sent to the server on login. +/// +/// Major versions identify client projects on the Soulseek network and are +/// reserved per project for the project's lifetime: +/// . +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ClientVersion { + pub major: u32, + pub minor: u32, +} + +impl ClientVersion { + /// The major version reserved for the soulseek-rs project. Clients and + /// bots built on this library keep this major and pick their own non-zero + /// minor version. + pub const MAJOR: u32 = 176; + + /// The version the soulseek-rs reference client (CLI/TUI) logs in with. + pub const REFERENCE_CLIENT: Self = Self { + major: Self::MAJOR, + minor: 100, + }; +} + +impl Default for ClientVersion { + fn default() -> Self { + Self { + major: Self::MAJOR, + minor: 1, + } + } +} + pub struct UploadFailed { pub filename: String, } diff --git a/soulseek-rs-lib/tests/e2e.rs b/soulseek-rs-lib/tests/e2e.rs index 03fcbaf..9f6c4d9 100644 --- a/soulseek-rs-lib/tests/e2e.rs +++ b/soulseek-rs-lib/tests/e2e.rs @@ -28,7 +28,9 @@ use std::time::{Duration, Instant}; use soulseek_rs::message::Message; use soulseek_rs::message::server::MessageFactory; use soulseek_rs::peer::ConnectionType; -use soulseek_rs::{Client, ClientSettings, DownloadStatus, PeerAddress}; +use soulseek_rs::{ + Client, ClientSettings, ClientVersion, DownloadStatus, PeerAddress, +}; /// A Soulseek server to test against: either a child soulfind process we /// spawned, or an external server referenced by `SOULSEEK_TEST_SERVER`. @@ -97,6 +99,7 @@ impl TestServer { enable_listen: false, listen_port: 0, shared_directories: Vec::new(), + version: ClientVersion::default(), } } @@ -601,7 +604,12 @@ fn login_raw( let mut srv = connect_retry(server_addr, Duration::from_secs(5))?; srv.set_read_timeout(Some(Duration::from_secs(10)))?; srv.write_all( - &MessageFactory::build_login_message(username, password).get_buffer(), + &MessageFactory::build_login_message( + username, + password, + ClientVersion::default(), + ) + .get_buffer(), )?; srv.flush()?; loop { @@ -816,8 +824,12 @@ fn run_mock_direct_peer(cfg: &MockDirectUpload) -> std::io::Result<()> { let mut srv = connect_retry(&cfg.server_addr, Duration::from_secs(5))?; srv.set_read_timeout(Some(Duration::from_secs(10)))?; srv.write_all( - &MessageFactory::build_login_message(&cfg.username, &cfg.password) - .get_buffer(), + &MessageFactory::build_login_message( + &cfg.username, + &cfg.password, + ClientVersion::default(), + ) + .get_buffer(), )?; srv.flush()?; loop { diff --git a/soulseek-rs/src/main.rs b/soulseek-rs/src/main.rs index e26fa12..2aca61e 100644 --- a/soulseek-rs/src/main.rs +++ b/soulseek-rs/src/main.rs @@ -11,7 +11,7 @@ use clap::Parser; use cli::{Cli, Commands, ConfigCommand, SharesCommand, parse_server_address}; use commands::Ctx; use output::{CliError, CliResult, Exit, Out}; -use soulseek_rs::{ClientSettings, PeerAddress}; +use soulseek_rs::{ClientSettings, ClientVersion, PeerAddress}; use std::io::{BufRead, IsTerminal}; use std::path::PathBuf; use std::process::ExitCode; @@ -146,6 +146,7 @@ fn context( enable_listen: !resolved.disable_listener, listen_port: resolved.listener_port, shared_directories: shared_directories(resolved, out), + version: ClientVersion::REFERENCE_CLIENT, }, download_dir: resolved.download_dir.clone(), max_concurrent_downloads: resolved.max_concurrent_downloads, @@ -281,6 +282,7 @@ fn run_default_tui( enable_listen, listen_port, shared_directories: shared_directories.clone(), + version: ClientVersion::REFERENCE_CLIENT, }; // Clear screen and enable mouse capture before initializing TUI diff --git a/soulseek-rs/tests/cli_e2e.rs b/soulseek-rs/tests/cli_e2e.rs index a3032c0..937e3d3 100644 --- a/soulseek-rs/tests/cli_e2e.rs +++ b/soulseek-rs/tests/cli_e2e.rs @@ -442,6 +442,7 @@ impl TestServer { enable_listen: true, listen_port: port, shared_directories: shares, + version: soulseek_rs::ClientVersion::default(), }); client.connect().expect("peer connect"); assert!(client.login().expect("peer login"), "peer should log in");