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:
@@ -622,3 +622,53 @@ fn a_replayed_transfer_response_does_not_start_a_second_transfer() {
|
||||
"a replayed TransferResponse must not dial the peer again"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn room_member_stats_are_returned_sorted_and_scoped_to_their_room() {
|
||||
use crate::types::{RoomUserStats, UserStatus};
|
||||
let stat = |username: &str| RoomUserStats {
|
||||
username: username.to_string(),
|
||||
status: UserStatus::Online,
|
||||
average_speed: 1,
|
||||
shared_files: 2,
|
||||
shared_folders: 3,
|
||||
slots_free: 1,
|
||||
country: None,
|
||||
};
|
||||
|
||||
let mut context = ClientContext::new();
|
||||
context.apply_room_member_stats(
|
||||
"jazz".to_string(),
|
||||
vec![stat("carol"), stat("alice")],
|
||||
);
|
||||
|
||||
let names: Vec<String> = context
|
||||
.room_member_stats("jazz")
|
||||
.into_iter()
|
||||
.map(|s| s.username)
|
||||
.collect();
|
||||
assert_eq!(names, vec!["alice", "carol"]);
|
||||
assert!(context.room_member_stats("unjoined").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejoining_replaces_the_previous_member_stats() {
|
||||
use crate::types::{RoomUserStats, UserStatus};
|
||||
let stat = |username: &str| RoomUserStats {
|
||||
username: username.to_string(),
|
||||
status: UserStatus::Online,
|
||||
average_speed: 1,
|
||||
shared_files: 2,
|
||||
shared_folders: 3,
|
||||
slots_free: 0,
|
||||
country: None,
|
||||
};
|
||||
|
||||
let mut context = ClientContext::new();
|
||||
context.apply_room_member_stats("jazz".to_string(), vec![stat("alice")]);
|
||||
context.apply_room_member_stats("jazz".to_string(), vec![stat("bob")]);
|
||||
|
||||
let stats = context.room_member_stats("jazz");
|
||||
assert_eq!(stats.len(), 1, "a rejoin supersedes the old snapshot");
|
||||
assert_eq!(stats[0].username, "bob");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user