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
+30
View File
@@ -18,6 +18,7 @@ use crate::message::server::ReloggedHandler;
use crate::message::server::SayChatroomHandler;
use crate::message::server::UserJoinedRoomHandler;
use crate::message::server::UserLeftRoomHandler;
use crate::message::server::WatchUserHandler;
use crate::message::server::WishListIntervalHandler;
use crate::message::server::{
GetUserStatsHandler, GetUserStatusHandler, RoomListHandler,
@@ -202,6 +203,17 @@ pub enum ServerMessage {
status: u32,
privileged: bool,
},
/// A `WatchUser` (code 5) reply: the initial snapshot for a user we just
/// started watching. The stats are absent when the server does not know
/// the username.
WatchedUserReceived {
username: String,
exists: bool,
status: Option<u32>,
average_speed: Option<u32>,
shared_files: Option<u32>,
shared_folders: Option<u32>,
},
/// A `GetUserStats` (code 36) reply.
UserStatsReceived {
username: String,
@@ -359,6 +371,7 @@ impl ServerActor {
handlers.register_handler(ReloggedHandler);
handlers.register_handler(RoomListHandler);
handlers.register_handler(GetUserStatusHandler);
handlers.register_handler(WatchUserHandler);
handlers.register_handler(GetUserStatsHandler);
handlers.register_handler(JoinRoomHandler);
handlers.register_handler(LeaveRoomHandler);
@@ -464,6 +477,23 @@ impl ServerActor {
privileged,
});
}
ServerMessage::WatchedUserReceived {
username,
exists,
status,
average_speed,
shared_files,
shared_folders,
} => {
self.forward_to_client(ClientOperation::WatchedUserReceived {
username,
exists,
status,
average_speed,
shared_files,
shared_folders,
});
}
ServerMessage::UserStatsReceived {
username,
average_speed,