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
141 lines
4.8 KiB
Rust
141 lines
4.8 KiB
Rust
//! `WatchUser` (code 5): subscribe to a user's status and share statistics.
|
|
//!
|
|
//! Watching is how a client keeps a buddy list current. After a `WatchUser`
|
|
//! request the server answers on the same code with the user's current
|
|
//! presence and stats, and from then on pushes every later change as an
|
|
//! ordinary `GetUserStatus` (code 7). `UnwatchUser` (code 6) cancels the
|
|
//! subscription and has no reply.
|
|
//!
|
|
//! Note on codes: the protocol has no `AddUserList`/`WatchedUserAdded`
|
|
//! /`WatchedUserRemoved` messages — 141/142/143 are `EnableRoomInvitations`,
|
|
//! `ChangePassword` and `AddRoomOperator`. Watching is 5/6, per the reference
|
|
//! protocol documentation.
|
|
|
|
use crate::actor::server_actor::ServerMessage;
|
|
use crate::message::{Message, MessageHandler};
|
|
use std::sync::mpsc::Sender;
|
|
|
|
/// Receives the `WatchUser` reply (code 5): `username`, `exists`, and — when
|
|
/// the user exists — status plus the same share statistics `GetUserStats`
|
|
/// carries.
|
|
pub struct WatchUserHandler;
|
|
|
|
impl MessageHandler<ServerMessage> for WatchUserHandler {
|
|
fn get_code(&self) -> u8 {
|
|
5
|
|
}
|
|
|
|
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
|
let username = message.read_string();
|
|
let exists = message.read_bool();
|
|
let watched = if exists {
|
|
let status = message.read_int32();
|
|
let average_speed = message.read_int32();
|
|
let _upload_number = message.read_int64(); // obsolete
|
|
let shared_files = message.read_int32();
|
|
let shared_folders = message.read_int32();
|
|
ServerMessage::WatchedUserReceived {
|
|
username,
|
|
exists,
|
|
status: Some(status),
|
|
average_speed: Some(average_speed),
|
|
shared_files: Some(shared_files),
|
|
shared_folders: Some(shared_folders),
|
|
}
|
|
} else {
|
|
// An unknown username: the server stops after the flag, so there
|
|
// is nothing further to read and nothing to record.
|
|
ServerMessage::WatchedUserReceived {
|
|
username,
|
|
exists,
|
|
status: None,
|
|
average_speed: None,
|
|
shared_files: None,
|
|
shared_folders: None,
|
|
}
|
|
};
|
|
let _ = sender.send(watched);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::message::framed;
|
|
|
|
#[test]
|
|
fn existing_user_carries_status_and_stats() {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("alice")
|
|
.write_int8(1) // exists
|
|
.write_int32(2) // status: online
|
|
.write_int32(1024) // average speed
|
|
.write_int64(0) // obsolete upload number
|
|
.write_int32(4321) // shared files
|
|
.write_int32(77); // shared folders
|
|
});
|
|
|
|
WatchUserHandler.handle(&mut message, tx);
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::WatchedUserReceived {
|
|
username,
|
|
exists,
|
|
status,
|
|
average_speed,
|
|
shared_files,
|
|
shared_folders,
|
|
}) => {
|
|
assert_eq!(username, "alice");
|
|
assert!(exists);
|
|
assert_eq!(status, Some(2));
|
|
assert_eq!(average_speed, Some(1024));
|
|
assert_eq!(shared_files, Some(4321));
|
|
assert_eq!(shared_folders, Some(77));
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_user_reports_absence_without_stats() {
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("ghost").write_int8(0);
|
|
});
|
|
|
|
WatchUserHandler.handle(&mut message, tx);
|
|
match rx.try_recv() {
|
|
Ok(ServerMessage::WatchedUserReceived {
|
|
username,
|
|
exists,
|
|
status,
|
|
shared_files,
|
|
..
|
|
}) => {
|
|
assert_eq!(username, "ghost");
|
|
assert!(!exists);
|
|
assert_eq!(status, None);
|
|
assert_eq!(shared_files, None);
|
|
}
|
|
other => panic!("unexpected: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn truncated_payload_does_not_panic() {
|
|
// exists = 1 but the stats were cut off: the bounds-checked readers
|
|
// must yield zeros rather than panicking on a hostile frame.
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
let mut message = framed(|m| {
|
|
m.write_string("alice").write_int8(1).write_int32(2);
|
|
});
|
|
|
|
WatchUserHandler.handle(&mut message, tx);
|
|
assert!(matches!(
|
|
rx.try_recv(),
|
|
Ok(ServerMessage::WatchedUserReceived { .. })
|
|
));
|
|
}
|
|
}
|