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:
@@ -2,6 +2,7 @@ use super::{
|
||||
Client, PeerMessage, Result, RoomEvent, RoomInfo, RwLockExt, ServerMessage,
|
||||
SharedDirectory, SoulseekRs, UserMessage, error,
|
||||
};
|
||||
use crate::types::UserInfo;
|
||||
|
||||
impl Client {
|
||||
/// Send a private message to another user via the server.
|
||||
@@ -101,6 +102,60 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the server what it knows about `username`: online status and share
|
||||
/// statistics.
|
||||
///
|
||||
/// Any previous answer for `username` is discarded first, so
|
||||
/// [`Client::user_info`] returns `None` until the replies to *this*
|
||||
/// request arrive — polling it cannot mistake a stale snapshot for a
|
||||
/// fresh one.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||
pub fn request_user_info(&self, username: &str) -> Result<()> {
|
||||
use crate::message::server::MessageFactory;
|
||||
self.context.write_safe()?.invalidate_user_info(username);
|
||||
self.send_server_message(MessageFactory::build_get_user_status(
|
||||
username,
|
||||
))?;
|
||||
self.send_server_message(MessageFactory::build_get_user_stats(username))
|
||||
}
|
||||
|
||||
/// What the server has told us about `username` since the last
|
||||
/// [`Client::request_user_info`], or `None` before any reply arrives.
|
||||
///
|
||||
/// Presence and statistics are separate replies, so a snapshot can carry
|
||||
/// one and not the other; [`UserInfo::is_complete`] reports when both
|
||||
/// have landed.
|
||||
#[must_use]
|
||||
pub fn user_info(&self, username: &str) -> Option<UserInfo> {
|
||||
match self.context.read_safe() {
|
||||
Ok(ctx) => ctx.user_info(username),
|
||||
Err(e) => {
|
||||
error!("[client] user_info: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Who is currently in `room`, sorted by name.
|
||||
///
|
||||
/// The roster is built from the membership the server sends when
|
||||
/// [`Client::join_room`] succeeds and kept current by the join and leave
|
||||
/// events after it, so it is populated shortly after joining rather than
|
||||
/// immediately. Returns an empty list for a room this client has not
|
||||
/// joined.
|
||||
#[must_use]
|
||||
pub fn room_members(&self, room: &str) -> Vec<String> {
|
||||
match self.context.read_safe() {
|
||||
Ok(ctx) => ctx.room_members(room),
|
||||
Err(e) => {
|
||||
error!("[client] room_members: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove and return all chat-room events received since the last call.
|
||||
#[must_use]
|
||||
pub fn take_room_events(&self) -> Vec<RoomEvent> {
|
||||
|
||||
Reference in New Issue
Block a user