feat(lib): per-member room statistics from the JoinRoom reply

The JoinRoom (code 14) reply carries, after the membership list, four
parallel vectors describing each member: status, share statistics, free
upload slots and country. The handler stopped after the usernames, so all of
it was discarded. It is now parsed and exposed as Client::room_member_stats.

Issue #48 asked for RoomStats (57) and RoomListStats (55). Those codes are
UserInterests and MyRecommendations in the Soulseek protocol; there is no
RoomStats message on the wire. Per-room user counts already arrive with
RoomList (64), which the client handles, and the per-member statistics are
part of the JoinRoom reply -- so that is where this adds them.

- join_room.rs: parse the stat vectors, each bounded by the payload so a
  hostile count cannot hang or overallocate
- types::RoomUserStats; every field looked up positionally so a server
  sending short vectors (or omitting countries) does not shift the roster
- ServerMessage/ClientOperation::RoomMemberStats and the context snapshot
  behind Client::room_member_stats, replaced on each rejoin
This commit is contained in:
Michel de Graaf
2026-08-28 12:44:37 +00:00
parent 0f91ffdcf3
commit fa49ebd2eb
7 changed files with 391 additions and 16 deletions
+13 -1
View File
@@ -27,7 +27,8 @@ use crate::message::{Message, MessageReader};
use crate::peer::ConnectionType;
use crate::peer::Peer;
use crate::types::{
ClientVersion, RoomEvent, RoomInfo, SessionLoss, SessionWatch,
ClientVersion, RoomEvent, RoomInfo, RoomUserStats, SessionLoss,
SessionWatch,
};
use crate::utils::lock::RwLockExt;
@@ -213,6 +214,11 @@ pub enum ServerMessage {
room: String,
users: Vec<String>,
},
/// Per-member statistics carried by the same `JoinRoom` (code 14) reply.
RoomMemberStats {
room: String,
stats: Vec<RoomUserStats>,
},
RoomLeft {
room: String,
},
@@ -480,6 +486,12 @@ impl ServerActor {
ServerMessage::RoomJoined { room, users } => {
self.forward_room_event(RoomEvent::Joined { room, users });
}
ServerMessage::RoomMemberStats { room, stats } => {
self.forward_to_client(ClientOperation::RoomMemberStats {
room,
stats,
});
}
ServerMessage::RoomLeft { room } => {
self.forward_room_event(RoomEvent::Left { room });
}