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.
122 lines
3.9 KiB
Rust
122 lines
3.9 KiB
Rust
//! `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:?}"),
|
|
}
|
|
}
|
|
}
|