feat(cli): expose sharing, peer lookup and settings as one-shot commands
An agent driving the binary could search, fetch and chat, but it could not be a peer: nothing kept a session online to serve files, nothing reported an upload, shares could only be set at startup, and there was no way to ask whether a peer was worth queueing to or to check credentials without doing something else. New commands: serve [--follow] stay online sharing; one record per upload whoami credentials, connection and what we offer user <NAME> a peer's presence and share counts room users <ROOM> the member roster shares list|add|remove|status|reindex config path|list|get|set `config` and `shares list|add|remove` need no account, so they run before the credential check — asking a script to log in for a `config get` would be a poor trade. `serve` is the only place uploads are visible: they live in the process serving them, so a one-shot `uploads list` could print nothing but an empty list. Library additions, all request/poll pairs matching the existing conventions: - Client::room_members, kept current from the join and leave events - Client::request_user_info / user_info, backed by new handlers for GetUserStatus (7) and GetUserStats (36) — codes the dispatcher previously logged as unhandled. The wire format is taken from soulfind's own source and pinned by an e2e test that asserts a peer's real share count. - UserInfo carries presence and stats as Options, so a reply that never arrived cannot be read as "offline" or "shares nothing" - request_user_info clears the previous answer, so the documented poll loop observes this request rather than the last one - ServerMessage and ClientOperation are now #[non_exhaustive]: every protocol message the client learns adds a variant, and that should not break callers - the crate root and prelude re-export every type a Client method returns Also pins the output contract with tests: under --json, stdout carries only newline-delimited JSON across fifteen command shapes while progress still goes to stderr, and a failing command writes nothing to stdout at all.
This commit is contained in:
@@ -122,6 +122,21 @@ impl MessageFactory {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask the server (code 7) for a user's online status.
|
||||
#[must_use]
|
||||
pub fn build_get_user_status(username: &str) -> Message {
|
||||
Message::new().write_int32(7).write_string(username).clone()
|
||||
}
|
||||
|
||||
/// Ask the server (code 36) for a user's share statistics.
|
||||
#[must_use]
|
||||
pub fn build_get_user_stats(username: &str) -> Message {
|
||||
Message::new()
|
||||
.write_int32(36)
|
||||
.write_string(username)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask the server (code 64) for the list of public chat rooms.
|
||||
#[must_use]
|
||||
pub fn build_room_list_request() -> Message {
|
||||
@@ -228,6 +243,20 @@ impl MessageFactory {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_get_user_status() {
|
||||
let message = MessageFactory::build_get_user_status("bob");
|
||||
let expect: Vec<u8> = [7, 0, 0, 0, 3, 0, 0, 0, b'b', b'o', b'b'].to_vec();
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_get_user_stats() {
|
||||
let message = MessageFactory::build_get_user_stats("bob");
|
||||
let expect: Vec<u8> = [36, 0, 0, 0, 3, 0, 0, 0, b'b', b'o', b'b'].to_vec();
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_watch_user() {
|
||||
let token: u32 = 223;
|
||||
|
||||
@@ -12,6 +12,7 @@ mod parent_speed_ratio;
|
||||
mod privileged_users;
|
||||
mod room_list;
|
||||
mod say_chatroom;
|
||||
mod user_info;
|
||||
mod user_joined_room;
|
||||
mod user_left_room;
|
||||
mod wish_list_interval;
|
||||
@@ -30,6 +31,7 @@ pub use parent_speed_ratio::ParentSpeedRatioHandler;
|
||||
pub use privileged_users::PrivilegedUsersHandler;
|
||||
pub use room_list::RoomListHandler;
|
||||
pub use say_chatroom::SayChatroomHandler;
|
||||
pub use user_info::{GetUserStatsHandler, GetUserStatusHandler};
|
||||
pub use user_joined_room::UserJoinedRoomHandler;
|
||||
pub use user_left_room::UserLeftRoomHandler;
|
||||
pub use wish_list_interval::WishListIntervalHandler;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! `GetUserStatus` (code 7) and `GetUserStats` (code 36): what the server
|
||||
//! knows about another user.
|
||||
//!
|
||||
//! The two answers arrive as separate messages, so a caller that wants a full
|
||||
//! picture asks for both and merges them — see `Client::request_user_info`.
|
||||
|
||||
use crate::actor::server_actor::ServerMessage;
|
||||
use crate::message::{Message, MessageHandler};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
/// Receives `GetUserStatus` (code 7): `username`, `status`, `privileged`.
|
||||
pub struct GetUserStatusHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for GetUserStatusHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
7
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let username = message.read_string();
|
||||
let status = message.read_int32();
|
||||
let privileged = message.read_bool();
|
||||
let _ = sender.send(ServerMessage::UserStatusReceived {
|
||||
username,
|
||||
status,
|
||||
privileged,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Receives `GetUserStats` (code 36): `username`, `average upload speed`, two
|
||||
/// obsolete fields, then the shared file and folder counts.
|
||||
pub struct GetUserStatsHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for GetUserStatsHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
36
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let username = message.read_string();
|
||||
let average_speed = message.read_int32();
|
||||
let _upload_number = message.read_int32(); // obsolete, always 0
|
||||
let _unknown = message.read_int32(); // obsolete, always 0
|
||||
let shared_files = message.read_int32();
|
||||
let shared_folders = message.read_int32();
|
||||
let _ = sender.send(ServerMessage::UserStatsReceived {
|
||||
username,
|
||||
average_speed,
|
||||
shared_files,
|
||||
shared_folders,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a payload the dispatcher would hand a handler: 8 bytes of frame
|
||||
/// header, then the fields, with the pointer parked at the payload.
|
||||
fn payload(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_status_is_parsed_with_its_privileged_flag() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = payload(|m| {
|
||||
m.write_string("alice").write_int32(2).write_int8(1);
|
||||
});
|
||||
|
||||
GetUserStatusHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::UserStatusReceived {
|
||||
username,
|
||||
status,
|
||||
privileged,
|
||||
}) => {
|
||||
assert_eq!(username, "alice");
|
||||
assert_eq!(status, 2);
|
||||
assert!(privileged);
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_stats_skip_the_two_obsolete_fields() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
// username, speed, obsolete, obsolete, files, folders
|
||||
let mut message = payload(|m| {
|
||||
m.write_string("bob")
|
||||
.write_int32(1024)
|
||||
.write_int32(0)
|
||||
.write_int32(0)
|
||||
.write_int32(4321)
|
||||
.write_int32(77);
|
||||
});
|
||||
|
||||
GetUserStatsHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::UserStatsReceived {
|
||||
username,
|
||||
average_speed,
|
||||
shared_files,
|
||||
shared_folders,
|
||||
}) => {
|
||||
assert_eq!(username, "bob");
|
||||
assert_eq!(average_speed, 1024);
|
||||
assert_eq!(shared_files, 4321, "must skip the obsolete pair");
|
||||
assert_eq!(shared_folders, 77);
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user