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:
@@ -5,7 +5,7 @@ use super::{Ctx, Session, confirm_flush};
|
||||
use crate::cli::BrowseArgs;
|
||||
use crate::output::{
|
||||
BrowseRecord, CliError, CliResult, Out, PrivateMessageRecord,
|
||||
RoomEventRecord, RoomRecord,
|
||||
RoomEventRecord, RoomMemberRecord, RoomRecord,
|
||||
};
|
||||
use soulseek_rs::types::RoomEvent;
|
||||
use soulseek_rs::{Client, SharedDirectory};
|
||||
@@ -132,6 +132,41 @@ pub fn room_say(ctx: &Ctx, room: &str, message: &str) -> CliResult {
|
||||
)))
|
||||
}
|
||||
|
||||
/// List a room's membership. Joining is what makes the server send it, so we
|
||||
/// join, wait for the roster, and leave again.
|
||||
pub fn room_users(ctx: &Ctx, room: &str, timeout: Duration) -> CliResult {
|
||||
let session = Session::open(ctx)?;
|
||||
session.client.join_room(room).map_err(|e| {
|
||||
CliError::connection(format!("cannot join {room}: {e}"))
|
||||
})?;
|
||||
ctx.out
|
||||
.status(&format!("waiting for {room}'s member list…"));
|
||||
|
||||
let deadline = Instant::now() + timeout;
|
||||
let mut members = Vec::new();
|
||||
while Instant::now() < deadline && members.is_empty() {
|
||||
members = session.client.room_members(room);
|
||||
if members.is_empty() {
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
}
|
||||
let _ = session.client.leave_room(room);
|
||||
|
||||
if members.is_empty() {
|
||||
return Err(CliError::timeout(format!(
|
||||
"{room} sent no member list within {}s",
|
||||
timeout.as_secs()
|
||||
)));
|
||||
}
|
||||
for user in members {
|
||||
ctx.out.emit(&RoomMemberRecord {
|
||||
room: room.to_string(),
|
||||
user,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_listen(ctx: &Ctx, room: &str, span: Option<Duration>) -> CliResult {
|
||||
let session = Session::open(ctx)?;
|
||||
session.client.join_room(room).map_err(|e| {
|
||||
|
||||
Reference in New Issue
Block a user