feat(lib): chat-room protocol (list, join, leave, say) with events

Implement the Soulseek chat-room server messages against soulfind's
wire format:
- MessageFactory builders: room-list request (64), join (14),
  leave (15), say (13).
- Handlers parsing the server side: RoomList (64) names+counts,
  JoinRoom (14) membership, SayChatroom (13), UserJoinedRoom (16),
  UserLeftRoom (17), LeaveRoom (15).
- Surface everything to the client as a drained RoomEvent stream plus
  a room-list snapshot; new Client API: request_room_list, join_room,
  leave_room, say_in_room, room_list, take_room_events.

Unit tests cover each builder and handler; server-gated e2e tests
exercise message delivery and room listing against soulfind.
This commit is contained in:
michel
2026-07-19 19:47:58 +02:00
parent ae28f4236a
commit 7bc0ee2c3d
12 changed files with 718 additions and 4 deletions
@@ -0,0 +1,52 @@
use crate::{
actor::server_actor::ServerMessage,
message::{Message, MessageHandler},
};
use std::sync::mpsc::Sender;
pub struct JoinRoomHandler;
impl MessageHandler<ServerMessage> for JoinRoomHandler {
fn get_code(&self) -> u8 {
14
}
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
// JoinRoom (code 14): room name, then a vector of member usernames.
// Per-user stat vectors follow but are not needed here, so we stop
// after reading the names.
let room = message.read_string();
let user_count = message.read_int32();
let mut users = Vec::new();
for _ in 0..user_count {
users.push(message.read_string());
}
let _ = sender.send(ServerMessage::RoomJoined { room, users });
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn forwards_room_and_member_list() {
let (tx, rx) = std::sync::mpsc::channel();
let mut message = Message::new();
message.write_raw_bytes(vec![0u8; 8]);
message.write_string("nicotine");
message.write_int32(2);
message.write_string("alice");
message.write_string("bob");
message.set_pointer(8);
JoinRoomHandler.handle(&mut message, tx);
match rx.try_recv() {
Ok(ServerMessage::RoomJoined { room, users }) => {
assert_eq!(room, "nicotine");
assert_eq!(users, vec!["alice", "bob"]);
}
other => panic!("unexpected: {other:?}"),
}
}
}