use std::collections::HashMap; use std::mem; use log::{error, info, warn}; use solstice_proto::{server, User}; use thiserror::Error; use crate::room::{RoomMembership, RoomMessage, RoomState, RoomVisibility}; /// The error returned by RoomMap functions. #[derive(Debug, Error)] pub enum RoomError { #[error(transparent)] RoomNotFound(#[from] RoomNotFoundError), #[error(transparent)] MembershipChangeInvalid(#[from] RoomMembershipChangeError), } #[derive(Debug, Error)] #[error("cannot change membership from {0:?} to {1:?}")] pub struct RoomMembershipChangeError(RoomMembership, RoomMembership); #[derive(Debug, Error)] #[error("room {0} not found")] pub struct RoomNotFoundError(String); /// An entry in the chat room map. #[derive(Debug)] pub struct RoomEntry { name: String, state: RoomState, } impl RoomEntry { /// Creates a new entry with the given state. pub fn new(name: String, state: RoomState) -> Self { Self { name, state } } /// Returns a copy of the room state contained in this entry. pub fn clone_state(&self) -> RoomState { self.state.clone() } /// Returns the state contained in this entry. #[cfg(test)] pub fn into_state(self) -> RoomState { self.state } /// Inserts the given message in this chat room's history. pub fn insert_message(&mut self, message: RoomMessage) { self.state.messages.insert(message) } /// Records that we are now trying to join this room. /// Returns an error if its membership is not `NonMember`, pub fn start_joining(&mut self) -> Result<(), RoomMembershipChangeError> { match self.state.membership { RoomMembership::NonMember => { self.state.membership = RoomMembership::Joining; Ok(()) } membership => Err(RoomMembershipChangeError( membership, RoomMembership::Joining, )), } } /// Records that we are now a member of this room and updates its state. pub fn join( &mut self, owner: Option, mut operators: Vec, members: &[User], ) { // Log what's happening. if let RoomMembership::Joining = self.state.membership { info!("Joined room {:?}", self.name); } else { warn!( "Joined room {:?} but membership was already {:?}", self.name, self.state.membership ); } // Update the room state. self.state.membership = RoomMembership::Member; self.state.user_count = members.len(); self.state.owner = owner; self.state.operators = operators.drain(..).collect(); self.state.members = members.iter().map(|user| user.name.clone()).collect(); } /// Records that we are now trying to leave the given room. /// If the room is not found, or if its membership status is not `Member`, /// returns an error. #[cfg(test)] pub fn start_leaving(&mut self) -> Result<(), RoomMembershipChangeError> { match self.state.membership { RoomMembership::Member => { self.state.membership = RoomMembership::Leaving; Ok(()) } membership => Err(RoomMembershipChangeError( membership, RoomMembership::Leaving, )), } } /// Records that we have now left the given room. pub fn leave(&mut self) { match self.state.membership { RoomMembership::Leaving => info!("Left room {:?}", self.name), membership => warn!( "Left room {:?} with wrong membership: {:?}", self.name, membership ), } self.state.membership = RoomMembership::NonMember; } } /// Contains the mapping from room names to room data and provides a clean /// interface to interact with it. #[derive(Debug, Default)] pub struct RoomMap { /// The actual map from room names to room data. map: HashMap, } impl RoomMap { /// Creates an empty mapping. pub fn new() -> Self { Self::default() } /// Inserts the given room in the map under the given name. /// Same semantics as `std::collections::HashMap::insert()`. pub fn insert(&mut self, name: String, room: RoomState) -> Option { self .map .insert(name.clone(), RoomEntry::new(name, room)) .map(|entry| entry.state) } /// Looks up the given room name in the map, returning an immutable /// reference to the associated data if found, or an error if not found. #[cfg(test)] pub fn get_strict( &self, room_name: &str, ) -> Result<&RoomEntry, RoomNotFoundError> { match self.map.get(room_name) { Some(room) => Ok(room), None => Err(RoomNotFoundError(room_name.to_string())), } } /// Looks up the given room name in the map, returning a mutable /// reference to the associated data if found, or an error if not found. pub fn get_mut_strict( &mut self, room_name: &str, ) -> Result<&mut RoomEntry, RoomNotFoundError> { match self.map.get_mut(room_name) { Some(room) => Ok(room), None => Err(RoomNotFoundError(room_name.to_string())), } } /// Updates one room in the map based on the information received in /// a RoomListResponse and the potential previously stored information. fn update_one( &mut self, name: String, visibility: RoomVisibility, user_count: u32, old_map: &mut HashMap, ) { let room = match old_map.remove(&name) { None => RoomState::new(RoomVisibility::Public, user_count as usize), Some(mut entry) => { entry.state.visibility = visibility; entry.state.user_count = user_count as usize; entry.state } }; if let Some(_) = self.insert(name, room) { error!("Room present twice in room list response"); } } /// Updates the map to reflect the information contained in the given /// server response. pub fn set_room_list(&mut self, mut response: server::RoomListResponse) { // Replace the old mapping with an empty one. let mut old_map = mem::replace(&mut self.map, HashMap::new()); // Add all public rooms. for (name, user_count) in response.rooms.drain(..) { self.update_one(name, RoomVisibility::Public, user_count, &mut old_map); } // Add all private, owned, rooms. for (name, user_count) in response.owned_private_rooms.drain(..) { self.update_one( name, RoomVisibility::PrivateOwned, user_count, &mut old_map, ); } // Add all private, unowned, rooms. for (name, user_count) in response.other_private_rooms.drain(..) { self.update_one( name, RoomVisibility::PrivateOther, user_count, &mut old_map, ); } // Mark all operated rooms as necessary. for name in response.operated_private_room_names.iter() { match self.map.get_mut(name) { Some(room) => room.state.operated = true, None => error!("Room {} is operated but does not exist", name), } } } /// Returns the list of (room name, room data) representing all known rooms. pub fn get_room_list(&self) -> Vec<(String, RoomState)> { let mut rooms = Vec::new(); for (room_name, room) in self.map.iter() { rooms.push((room_name.clone(), room.clone_state())); } rooms } // TODO: Move remaining methods to `RoomEntry`. /// Inserts the given user in the given room's set of members. /// Returns an error if the room is not found. pub fn insert_member( &mut self, room_name: &str, user_name: String, ) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; room.state.members.insert(user_name); Ok(()) } /// Removes the given user from the given room's set of members. /// Returns an error if the room is not found. pub fn remove_member( &mut self, room_name: &str, user_name: &str, ) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; room.state.members.remove(user_name); Ok(()) } /*---------* * Tickers * *---------*/ pub fn set_tickers( &mut self, room_name: &str, tickers: Vec<(String, String)>, ) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; room.state.tickers = tickers; Ok(()) } } #[cfg(test)] mod tests { use std::collections::HashSet; use solstice_proto::server::RoomListResponse; use crate::room::{RoomMembership, RoomState, RoomVisibility}; use super::*; #[test] fn entry_start_joining_error() { let initial_state = RoomState { membership: RoomMembership::Member, ..RoomState::default() }; let mut room = RoomEntry::new("bleep".to_string(), initial_state.clone()); room.start_joining().unwrap_err(); assert_eq!(room.into_state(), initial_state); } #[test] fn entry_start_joining_success() { let mut room = RoomEntry::new("bleep".to_string(), RoomState::default()); room.start_joining().unwrap(); assert_eq!( room.into_state(), RoomState { membership: RoomMembership::Joining, ..RoomState::default() } ); } #[test] fn entry_join() { let mut room = RoomEntry::new("bleep".to_string(), RoomState::default()); let owner = Some("owner".to_string()); let mut operators = vec!["operator1".to_string(), "operator2".to_string()]; let users = [ User { name: "shruti".to_string(), ..User::default() }, User { name: "kim".to_string(), ..User::default() }, ]; room.join(owner.clone(), operators.clone(), &users); assert_eq!( room.into_state(), RoomState { membership: RoomMembership::Member, owner: owner, operators: operators.drain(..).collect::>(), user_count: 2, members: users .iter() .map(|user| user.name.clone()) .collect::>(), ..RoomState::default() } ); } #[test] fn entry_start_leaving_error() { let mut room = RoomEntry::new("bleep".to_string(), RoomState::default()); room.start_leaving().unwrap_err(); assert_eq!(room.into_state(), RoomState::default()); } #[test] fn entry_start_leaving_success() { let initial_state = RoomState { membership: RoomMembership::Member, ..RoomState::default() }; let mut room = RoomEntry::new("bleep".to_string(), initial_state.clone()); room.start_leaving().unwrap(); assert_eq!( room.into_state(), RoomState { membership: RoomMembership::Leaving, ..initial_state } ); } #[test] fn entry_leave() { let mut room = RoomEntry::new( "bleep".to_string(), RoomState { membership: RoomMembership::Member, ..RoomState::default() }, ); room.leave(); assert_eq!( room.into_state(), RoomState { membership: RoomMembership::NonMember, ..RoomState::default() } ); } #[test] fn map_new_is_empty() { assert_eq!(RoomMap::new().get_room_list(), vec![]); } #[test] fn map_get_strict() { let mut rooms = RoomMap::new(); rooms.set_room_list(RoomListResponse { rooms: vec![("room a".to_string(), 42), ("room b".to_string(), 1337)], owned_private_rooms: vec![], other_private_rooms: vec![], operated_private_room_names: vec![], }); assert_eq!( rooms.get_strict("room a").unwrap().clone_state(), RoomState::new(RoomVisibility::Public, 42) ); } }