feat(lib): watch and unwatch users (WatchUser 5 / UnwatchUser 6)

Adds the server-side watch subscription a buddy list needs: WatchUser (5)
asks the server to keep pushing a user's status, UnwatchUser (6) cancels it.
The reply to a watch carries the user's current status and share statistics,
which merge into the existing per-user snapshot read via Client::user_info.

Issue #47 asked for AddUserList (141) / WatchedUserAdded (142) /
WatchedUserRemoved (143). Those codes are EnableRoomInvitations,
ChangePassword and AddRoomOperator in the Soulseek protocol -- there are no
such messages on the wire. Watching is codes 5 and 6, so that is what this
implements.

- message/server/watch_user.rs: WatchUserHandler (code 5), tolerant of an
  absent user and of a truncated frame
- MessageFactory::build_watch_user / build_unwatch_user
- Client::watch_user / unwatch_user / watched_users
- The watch reply has no privileged flag, so it preserves whatever an
  earlier GetUserStatus recorded rather than asserting false
This commit is contained in:
Michel de Graaf
2026-08-28 12:44:43 +00:00
parent 0f91ffdcf3
commit b505718db9
8 changed files with 417 additions and 0 deletions
@@ -152,6 +152,20 @@ impl MessageFactory {
.clone()
}
/// Watch `username` (server code 5): the server replies with their
/// current status and share statistics, then pushes every later status
/// change as a `GetUserStatus` (code 7) until we unwatch them.
#[must_use]
pub fn build_watch_user(username: &str) -> Message {
Message::new().write_int32(5).write_string(username).clone()
}
/// Stop watching `username` (server code 6). The server sends no reply.
#[must_use]
pub fn build_unwatch_user(username: &str) -> Message {
Message::new().write_int32(6).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 {
@@ -424,3 +438,17 @@ fn test_build_file_search_message() {
.to_vec();
assert_eq!(expect, message.get_data());
}
#[test]
fn test_build_watch_user() {
let message = MessageFactory::build_watch_user("bob");
let expect: Vec<u8> = [5, 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_unwatch_user() {
let message = MessageFactory::build_unwatch_user("bob");
let expect: Vec<u8> = [6, 0, 0, 0, 3, 0, 0, 0, b'b', b'o', b'b'].to_vec();
assert_eq!(expect, message.get_data());
}