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:
@@ -5,6 +5,8 @@ use crate::message::server::ConnectToPeerHandler;
|
||||
use crate::message::server::ExcludedSearchPhrasesHandler;
|
||||
use crate::message::server::FileSearchHandler;
|
||||
use crate::message::server::GetPeerAddressHandler;
|
||||
use crate::message::server::JoinRoomHandler;
|
||||
use crate::message::server::LeaveRoomHandler;
|
||||
use crate::message::server::LoginHandler;
|
||||
use crate::message::server::MessageFactory;
|
||||
use crate::message::server::MessageUser;
|
||||
@@ -12,11 +14,15 @@ use crate::message::server::ParentMinSpeedHandler;
|
||||
use crate::message::server::ParentSpeedRatioHandler;
|
||||
use crate::message::server::PrivilegedUsersHandler;
|
||||
use crate::message::server::RoomListHandler;
|
||||
use crate::message::server::SayChatroomHandler;
|
||||
use crate::message::server::UserJoinedRoomHandler;
|
||||
use crate::message::server::UserLeftRoomHandler;
|
||||
use crate::message::server::WishListIntervalHandler;
|
||||
use crate::message::{Handlers, MessageType};
|
||||
use crate::message::{Message, MessageReader};
|
||||
use crate::peer::ConnectionType;
|
||||
use crate::peer::Peer;
|
||||
use crate::types::{RoomEvent, RoomInfo};
|
||||
use crate::utils::lock::RwLockExt;
|
||||
|
||||
use std::io::{self, Error, Write};
|
||||
@@ -168,6 +174,27 @@ pub enum ServerMessage {
|
||||
obfuscated_port: u16,
|
||||
},
|
||||
PrivateMessageReceived(UserMessage),
|
||||
RoomListReceived(Vec<RoomInfo>),
|
||||
RoomJoined {
|
||||
room: String,
|
||||
users: Vec<String>,
|
||||
},
|
||||
RoomLeft {
|
||||
room: String,
|
||||
},
|
||||
RoomMessageReceived {
|
||||
room: String,
|
||||
username: String,
|
||||
message: String,
|
||||
},
|
||||
RoomUserJoined {
|
||||
room: String,
|
||||
username: String,
|
||||
},
|
||||
RoomUserLeft {
|
||||
room: String,
|
||||
username: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ServerActor {
|
||||
@@ -327,6 +354,11 @@ impl ServerActor {
|
||||
|
||||
handlers.register_handler(LoginHandler);
|
||||
handlers.register_handler(RoomListHandler);
|
||||
handlers.register_handler(JoinRoomHandler);
|
||||
handlers.register_handler(LeaveRoomHandler);
|
||||
handlers.register_handler(SayChatroomHandler);
|
||||
handlers.register_handler(UserJoinedRoomHandler);
|
||||
handlers.register_handler(UserLeftRoomHandler);
|
||||
handlers.register_handler(ExcludedSearchPhrasesHandler);
|
||||
handlers.register_handler(PrivilegedUsersHandler);
|
||||
handlers.register_handler(MessageUser);
|
||||
@@ -468,6 +500,35 @@ impl ServerActor {
|
||||
);
|
||||
}
|
||||
}
|
||||
ServerMessage::RoomListReceived(rooms) => {
|
||||
self.forward_room_event(RoomEvent::List(rooms));
|
||||
}
|
||||
ServerMessage::RoomJoined { room, users } => {
|
||||
self.forward_room_event(RoomEvent::Joined { room, users });
|
||||
}
|
||||
ServerMessage::RoomLeft { room } => {
|
||||
self.forward_room_event(RoomEvent::Left { room });
|
||||
}
|
||||
ServerMessage::RoomMessageReceived {
|
||||
room,
|
||||
username,
|
||||
message,
|
||||
} => {
|
||||
self.forward_room_event(RoomEvent::Message {
|
||||
room,
|
||||
username,
|
||||
message,
|
||||
});
|
||||
}
|
||||
ServerMessage::RoomUserJoined { room, username } => {
|
||||
self.forward_room_event(RoomEvent::UserJoined {
|
||||
room,
|
||||
username,
|
||||
});
|
||||
}
|
||||
ServerMessage::RoomUserLeft { room, username } => {
|
||||
self.forward_room_event(RoomEvent::UserLeft { room, username });
|
||||
}
|
||||
ServerMessage::ProcessRead => {
|
||||
self.process_read();
|
||||
}
|
||||
@@ -602,6 +663,15 @@ impl ServerActor {
|
||||
self.process_dispatcher_messages();
|
||||
}
|
||||
|
||||
/// Forward a chat-room event to the client operations loop.
|
||||
fn forward_room_event(&self, event: RoomEvent) {
|
||||
if let Err(e) =
|
||||
self.client_channel.send(ClientOperation::RoomEvent(event))
|
||||
{
|
||||
error!("[server] Error forwarding room event to client: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_message(&mut self, message: Message) {
|
||||
if let Some(sender) = &self.dispatcher_sender {
|
||||
match sender.send(ServerMessage::SendMessage(message)) {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::actor::server_actor::{
|
||||
PeerAddress, ServerActor, ServerMessage, UserMessage,
|
||||
};
|
||||
use crate::download_store::{DownloadStore, collect_failed_tokens};
|
||||
use crate::types::{DownloadMetadata, DownloadStatus};
|
||||
use crate::types::{DownloadMetadata, DownloadStatus, RoomEvent, RoomInfo};
|
||||
use crate::utils::logger;
|
||||
use crate::{
|
||||
Transfer,
|
||||
@@ -186,6 +186,9 @@ pub enum ClientOperation {
|
||||
/// established — the peer is likely firewalled, so fall back to asking the
|
||||
/// server to broker the connection. Carries the reporting actor's id.
|
||||
PeerConnectFailed(u64, String),
|
||||
/// Something happened in the chat-room subsystem (list refreshed, a room
|
||||
/// joined/left, a message said, a member joined/left).
|
||||
RoomEvent(RoomEvent),
|
||||
}
|
||||
pub struct ClientContext {
|
||||
pub peer_registry: Option<PeerRegistry>,
|
||||
@@ -209,6 +212,10 @@ pub struct ClientContext {
|
||||
pending_serves: HashMap<String, Vec<u32>>,
|
||||
/// Shared-file listings received from peers we browsed.
|
||||
browse_results: HashMap<String, Vec<SharedDirectory>>,
|
||||
/// Latest snapshot of the public chat-room list (from `RoomList`, code 64).
|
||||
room_list: Vec<RoomInfo>,
|
||||
/// Chat-room events awaiting consumption by the client/UI.
|
||||
room_events: Vec<RoomEvent>,
|
||||
actor_system: Arc<ActorSystem>,
|
||||
}
|
||||
impl Default for ClientContext {
|
||||
@@ -499,11 +506,34 @@ impl ClientContext {
|
||||
uploads: HashMap::new(),
|
||||
pending_serves: HashMap::new(),
|
||||
browse_results: HashMap::new(),
|
||||
room_list: Vec::new(),
|
||||
room_events: Vec::new(),
|
||||
downloads: DownloadStore::new(),
|
||||
actor_system,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a chat-room event: keep the room-list snapshot current and queue
|
||||
/// the event for the client/UI to drain.
|
||||
pub fn apply_room_event(&mut self, event: RoomEvent) {
|
||||
if let RoomEvent::List(rooms) = &event {
|
||||
self.room_list.clone_from(rooms);
|
||||
}
|
||||
self.room_events.push(event);
|
||||
}
|
||||
|
||||
/// The latest snapshot of the public chat-room list.
|
||||
#[must_use]
|
||||
pub fn room_list(&self) -> Vec<RoomInfo> {
|
||||
self.room_list.clone()
|
||||
}
|
||||
|
||||
/// Remove and return all chat-room events received since the last call.
|
||||
#[must_use]
|
||||
pub fn take_room_events(&mut self) -> Vec<RoomEvent> {
|
||||
std::mem::take(&mut self.room_events)
|
||||
}
|
||||
|
||||
/// Cache a peer's listen address learned from a GetPeerAddress response.
|
||||
pub fn cache_peer_address(
|
||||
&mut self,
|
||||
@@ -746,6 +776,93 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a raw server message via the server actor, mapping a dead channel
|
||||
/// to [`SoulseekRs::NotConnected`].
|
||||
fn send_server_message(
|
||||
&self,
|
||||
message: crate::message::Message,
|
||||
) -> Result<()> {
|
||||
self.server_handle
|
||||
.as_ref()
|
||||
.ok_or(SoulseekRs::NotConnected)?
|
||||
.send(ServerMessage::SendMessage(message))
|
||||
.map_err(|_| SoulseekRs::NotConnected)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ask the server for the list of public chat rooms. The response arrives
|
||||
/// asynchronously; read it with [`Client::room_list`] or by draining
|
||||
/// [`Client::take_room_events`] for a [`RoomEvent::List`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||
pub fn request_room_list(&self) -> Result<()> {
|
||||
self.send_server_message(
|
||||
crate::message::server::MessageFactory::build_room_list_request(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Join a public chat room. The membership list and subsequent messages
|
||||
/// arrive via [`Client::take_room_events`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||
pub fn join_room(&self, room: &str) -> Result<()> {
|
||||
self.send_server_message(
|
||||
crate::message::server::MessageFactory::build_join_room(
|
||||
room, false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// Leave a chat room previously joined with [`Client::join_room`].
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||
pub fn leave_room(&self, room: &str) -> Result<()> {
|
||||
self.send_server_message(
|
||||
crate::message::server::MessageFactory::build_leave_room(room),
|
||||
)
|
||||
}
|
||||
|
||||
/// Say `message` in chat room `room`. The server echoes it back as a
|
||||
/// [`RoomEvent::Message`], so the UI should render from that echo rather
|
||||
/// than optimistically.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`SoulseekRs::NotConnected`] if the client is not connected.
|
||||
pub fn say_in_room(&self, room: &str, message: &str) -> Result<()> {
|
||||
self.send_server_message(
|
||||
crate::message::server::MessageFactory::build_say_chatroom(
|
||||
room, message,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/// The latest snapshot of the public chat-room list.
|
||||
#[must_use]
|
||||
pub fn room_list(&self) -> Vec<RoomInfo> {
|
||||
match self.context.read_safe() {
|
||||
Ok(ctx) => ctx.room_list(),
|
||||
Err(e) => {
|
||||
error!("[client] room_list: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove and return all chat-room events received since the last call.
|
||||
#[must_use]
|
||||
pub fn take_room_events(&self) -> Vec<RoomEvent> {
|
||||
match self.context.write_safe() {
|
||||
Ok(mut ctx) => ctx.take_room_events(),
|
||||
Err(e) => {
|
||||
error!("[client] take_room_events: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request a peer's shared-file listing. When it arrives it can be
|
||||
/// retrieved with [`Client::take_browse_result`].
|
||||
///
|
||||
@@ -1654,6 +1771,15 @@ impl Client {
|
||||
e
|
||||
),
|
||||
},
|
||||
ClientOperation::RoomEvent(event) => {
|
||||
match client_context.write_safe() {
|
||||
Ok(mut ctx) => ctx.apply_room_event(event),
|
||||
Err(e) => error!(
|
||||
"[client] RoomEvent write: {}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
ClientOperation::PeerConnected(username) => {
|
||||
// An outbound control connection just handshook.
|
||||
// Flush any downloads that were queued for this
|
||||
|
||||
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use crate::{
|
||||
actor::server_actor::ServerMessage,
|
||||
message::{Message, MessageHandler},
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct LeaveRoomHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for LeaveRoomHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
15
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let room = message.read_string();
|
||||
let _ = sender.send(ServerMessage::RoomLeft { room });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn forwards_left_room() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.set_pointer(8);
|
||||
|
||||
LeaveRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::RoomLeft { room }) => assert_eq!(room, "jazz"),
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,38 @@ impl MessageFactory {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask the server (code 64) for the list of public chat rooms.
|
||||
#[must_use]
|
||||
pub fn build_room_list_request() -> Message {
|
||||
Message::new().write_int32(64).clone()
|
||||
}
|
||||
|
||||
/// Join a chat room (server code 14). `private` requests a private room.
|
||||
#[must_use]
|
||||
pub fn build_join_room(room: &str, private: bool) -> Message {
|
||||
Message::new()
|
||||
.write_int32(14)
|
||||
.write_string(room)
|
||||
.write_int32(u32::from(private))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Leave a chat room (server code 15).
|
||||
#[must_use]
|
||||
pub fn build_leave_room(room: &str) -> Message {
|
||||
Message::new().write_int32(15).write_string(room).clone()
|
||||
}
|
||||
|
||||
/// Say `message` in chat room `room` (server code 13).
|
||||
#[must_use]
|
||||
pub fn build_say_chatroom(room: &str, message: &str) -> Message {
|
||||
Message::new()
|
||||
.write_int32(13)
|
||||
.write_string(room)
|
||||
.write_string(message)
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Ask a peer for their shared-file listing (peer code 4, no body).
|
||||
#[must_use]
|
||||
pub fn build_get_share_file_list() -> Message {
|
||||
@@ -272,6 +304,47 @@ fn test_build_message_acked() {
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_join_room() {
|
||||
let message = MessageFactory::build_join_room("nicotine", false);
|
||||
let expect: Vec<u8> = [
|
||||
14, 0, 0, 0, // code
|
||||
8, 0, 0, 0, 110, 105, 99, 111, 116, 105, 110, 101, // "nicotine"
|
||||
0, 0, 0, 0, // private = 0
|
||||
]
|
||||
.to_vec();
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_leave_room() {
|
||||
let message = MessageFactory::build_leave_room("nicotine");
|
||||
let expect: Vec<u8> = [
|
||||
15, 0, 0, 0, // code
|
||||
8, 0, 0, 0, 110, 105, 99, 111, 116, 105, 110, 101, // "nicotine"
|
||||
]
|
||||
.to_vec();
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_say_chatroom() {
|
||||
let message = MessageFactory::build_say_chatroom("room", "hi");
|
||||
let expect: Vec<u8> = [
|
||||
13, 0, 0, 0, // code
|
||||
4, 0, 0, 0, 114, 111, 111, 109, // "room"
|
||||
2, 0, 0, 0, 104, 105, // "hi"
|
||||
]
|
||||
.to_vec();
|
||||
assert_eq!(expect, message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_room_list_request() {
|
||||
let message = MessageFactory::build_room_list_request();
|
||||
assert_eq!(vec![64, 0, 0, 0], message.get_data());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_file_search_message() {
|
||||
let message = MessageFactory::build_file_search_message(12, "trance wax");
|
||||
|
||||
@@ -2,6 +2,8 @@ mod connect_to_peer;
|
||||
mod excluded_search_phrases;
|
||||
mod file_search;
|
||||
mod get_peer_address;
|
||||
mod join_room;
|
||||
mod leave_room;
|
||||
mod login;
|
||||
mod message_factory;
|
||||
mod message_user;
|
||||
@@ -9,12 +11,17 @@ mod parent_min_speed;
|
||||
mod parent_speed_ratio;
|
||||
mod privileged_users;
|
||||
mod room_list;
|
||||
mod say_chatroom;
|
||||
mod user_joined_room;
|
||||
mod user_left_room;
|
||||
mod wish_list_interval;
|
||||
|
||||
pub use connect_to_peer::ConnectToPeerHandler;
|
||||
pub use excluded_search_phrases::ExcludedSearchPhrasesHandler;
|
||||
pub use file_search::FileSearchHandler;
|
||||
pub use get_peer_address::GetPeerAddressHandler;
|
||||
pub use join_room::JoinRoomHandler;
|
||||
pub use leave_room::LeaveRoomHandler;
|
||||
pub use login::LoginHandler;
|
||||
pub use message_factory::MessageFactory;
|
||||
pub use message_user::MessageUser;
|
||||
@@ -22,4 +29,7 @@ pub use parent_min_speed::ParentMinSpeedHandler;
|
||||
pub use parent_speed_ratio::ParentSpeedRatioHandler;
|
||||
pub use privileged_users::PrivilegedUsersHandler;
|
||||
pub use room_list::RoomListHandler;
|
||||
pub use say_chatroom::SayChatroomHandler;
|
||||
pub use user_joined_room::UserJoinedRoomHandler;
|
||||
pub use user_left_room::UserLeftRoomHandler;
|
||||
pub use wish_list_interval::WishListIntervalHandler;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
actor::server_actor::ServerMessage,
|
||||
message::{Message, MessageHandler},
|
||||
types::RoomInfo,
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
@@ -11,8 +12,101 @@ impl MessageHandler<ServerMessage> for RoomListHandler {
|
||||
64
|
||||
}
|
||||
|
||||
fn handle(&self, _message: &mut Message, _sender: Sender<ServerMessage>) {
|
||||
// Room listing is not yet exposed to the client; this handler exists
|
||||
// so the dispatcher can ack code 64 without logging an unknown message.
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let rooms = parse_room_list(message);
|
||||
let _ = sender.send(ServerMessage::RoomListReceived(rooms));
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the public rooms out of a `RoomList` (code 64) message: a vector of
|
||||
/// room names followed by a vector of user counts. The remaining private-room
|
||||
/// sections are ignored. `message` must be positioned at the payload (the
|
||||
/// dispatcher sets pointer 8).
|
||||
#[must_use]
|
||||
pub fn parse_room_list(message: &mut Message) -> Vec<RoomInfo> {
|
||||
let name_count = message.read_int32();
|
||||
let mut names = Vec::new();
|
||||
for _ in 0..name_count {
|
||||
names.push(message.read_string());
|
||||
}
|
||||
let count_count = message.read_int32();
|
||||
let mut counts = Vec::new();
|
||||
for _ in 0..count_count {
|
||||
counts.push(message.read_int32());
|
||||
}
|
||||
names
|
||||
.into_iter()
|
||||
.zip(counts)
|
||||
.map(|(name, user_count)| RoomInfo { name, user_count })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Build a received message (8-byte header + payload) and position it the
|
||||
/// way the dispatcher would.
|
||||
fn framed(build: impl FnOnce(&mut Message)) -> Message {
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
build(&mut message);
|
||||
message.set_pointer(8);
|
||||
message
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_names_zipped_with_counts() {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(2);
|
||||
m.write_string("nicotine");
|
||||
m.write_string("jazz");
|
||||
m.write_int32(2);
|
||||
m.write_int32(42);
|
||||
m.write_int32(7);
|
||||
});
|
||||
let rooms = parse_room_list(&mut message);
|
||||
assert_eq!(
|
||||
rooms,
|
||||
vec![
|
||||
RoomInfo {
|
||||
name: "nicotine".to_string(),
|
||||
user_count: 42
|
||||
},
|
||||
RoomInfo {
|
||||
name: "jazz".to_string(),
|
||||
user_count: 7
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_room_list_parses_to_empty() {
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(0);
|
||||
m.write_int32(0);
|
||||
});
|
||||
assert!(parse_room_list(&mut message).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_forwards_parsed_rooms() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = framed(|m| {
|
||||
m.write_int32(1);
|
||||
m.write_string("room");
|
||||
m.write_int32(1);
|
||||
m.write_int32(5);
|
||||
});
|
||||
RoomListHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::RoomListReceived(rooms)) => {
|
||||
assert_eq!(rooms.len(), 1);
|
||||
assert_eq!(rooms[0].name, "room");
|
||||
assert_eq!(rooms[0].user_count, 5);
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::{
|
||||
actor::server_actor::ServerMessage,
|
||||
message::{Message, MessageHandler},
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct SayChatroomHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for SayChatroomHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
13
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let room = message.read_string();
|
||||
let username = message.read_string();
|
||||
let message_text = message.read_string();
|
||||
let _ = sender.send(ServerMessage::RoomMessageReceived {
|
||||
room,
|
||||
username,
|
||||
message: message_text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn forwards_room_message() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("alice");
|
||||
message.write_string("hello everyone");
|
||||
message.set_pointer(8);
|
||||
|
||||
SayChatroomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::RoomMessageReceived {
|
||||
room,
|
||||
username,
|
||||
message,
|
||||
}) => {
|
||||
assert_eq!(room, "jazz");
|
||||
assert_eq!(username, "alice");
|
||||
assert_eq!(message, "hello everyone");
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::{
|
||||
actor::server_actor::ServerMessage,
|
||||
message::{Message, MessageHandler},
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct UserJoinedRoomHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for UserJoinedRoomHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
16
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
// UserJoinedRoom (code 16): room, username, then that user's stats,
|
||||
// which we don't need. Reading the first two fields is enough.
|
||||
let room = message.read_string();
|
||||
let username = message.read_string();
|
||||
let _ = sender.send(ServerMessage::RoomUserJoined { room, username });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn forwards_user_joined() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("carol");
|
||||
message.set_pointer(8);
|
||||
|
||||
UserJoinedRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::RoomUserJoined { room, username }) => {
|
||||
assert_eq!(room, "jazz");
|
||||
assert_eq!(username, "carol");
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::{
|
||||
actor::server_actor::ServerMessage,
|
||||
message::{Message, MessageHandler},
|
||||
};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub struct UserLeftRoomHandler;
|
||||
|
||||
impl MessageHandler<ServerMessage> for UserLeftRoomHandler {
|
||||
fn get_code(&self) -> u8 {
|
||||
17
|
||||
}
|
||||
|
||||
fn handle(&self, message: &mut Message, sender: Sender<ServerMessage>) {
|
||||
let room = message.read_string();
|
||||
let username = message.read_string();
|
||||
let _ = sender.send(ServerMessage::RoomUserLeft { room, username });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn forwards_user_left() {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut message = Message::new();
|
||||
message.write_raw_bytes(vec![0u8; 8]);
|
||||
message.write_string("jazz");
|
||||
message.write_string("carol");
|
||||
message.set_pointer(8);
|
||||
|
||||
UserLeftRoomHandler.handle(&mut message, tx);
|
||||
match rx.try_recv() {
|
||||
Ok(ServerMessage::RoomUserLeft { room, username }) => {
|
||||
assert_eq!(room, "jazz");
|
||||
assert_eq!(username, "carol");
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,6 +163,36 @@ pub enum DownloadStatus {
|
||||
Failed(Option<String>),
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
/// A public chat room advertised by the server (`RoomList`, code 64).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RoomInfo {
|
||||
pub name: String,
|
||||
pub user_count: u32,
|
||||
}
|
||||
|
||||
/// Something that happened in the chat-room subsystem, surfaced to the client
|
||||
/// so a UI can react to it. Drained via `Client::take_room_events`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RoomEvent {
|
||||
/// The full list of public rooms (supersedes any previous snapshot).
|
||||
List(Vec<RoomInfo>),
|
||||
/// We successfully joined `room`; carries the current member list.
|
||||
Joined { room: String, users: Vec<String> },
|
||||
/// We left `room`.
|
||||
Left { room: String },
|
||||
/// `username` said `message` in `room`.
|
||||
Message {
|
||||
room: String,
|
||||
username: String,
|
||||
message: String,
|
||||
},
|
||||
/// `username` joined `room`.
|
||||
UserJoined { room: String, username: String },
|
||||
/// `username` left `room`.
|
||||
UserLeft { room: String, username: String },
|
||||
}
|
||||
|
||||
impl Transfer {
|
||||
pub fn new_from_message(message: &mut Message) -> Self {
|
||||
let direction = message.read_int32();
|
||||
|
||||
Reference in New Issue
Block a user