use std::collections::{HashMap, HashSet}; use std::mem; use std::time::SystemTime; use log::{error, info, warn}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use solstice_proto::{server, User}; use thiserror::Error; /// This enumeration is the list of possible membership states for a chat room. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum Membership { /// The user is not a member of this room. NonMember, /// The user has requested to join the room, but hasn't heard back from the /// server yet. Joining, /// The user is a member of the room. Member, /// The user has request to leave the room, but hasn't heard back from the /// server yet. Leaving, } /// This enumeration is the list of visibility types for rooms that the user is /// a member of. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum Visibility { /// This room is visible to any user. Public, /// This room is visible only to members, and the user owns it. PrivateOwned, /// This room is visible only to members, and someone else owns it. PrivateOther, } /// A message sent to a chat room. #[derive( Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, )] pub struct Message { /// Time at which the message was received by this client. /// /// We use `SystemTime` instead of `Instant` because this is serialized and /// sent to API clients. /// /// Defined first in order for the `Ord` and `PartialOrd` derived traits to /// sort by this key first. pub received_at: SystemTime, /// The user name of the message sender. pub user_name: String, /// The contents of the message. pub message: String, } /// The history of messages sent for a single chat room. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct MessageHistory { /// Messages, sorted in increasing order. messages: Vec, } // MessageHistory should be transparent for serialization purposes. impl<'de> Deserialize<'de> for MessageHistory { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let messages = Vec::::deserialize(deserializer)?; Ok(Self::new(messages)) } } // MessageHistory should be transparent for serialization purposes. impl Serialize for MessageHistory { fn serialize(&self, serializer: S) -> Result where S: Serializer, { self.messages.serialize(serializer) } } impl MessageHistory { pub fn new(mut messages: Vec) -> Self { messages.sort(); Self { messages } } /// Inserts a `message` into this history. pub fn insert(&mut self, message: Message) { self.messages.push(message); // This could be terrible for performance in the general case, but we know // that messages should be coming in almost-always sorted since // `received_at` is usually set to `now()`. self.messages.sort(); } #[cfg(test)] /// Returns the list of messages sorted in increasing chronological order. pub fn to_vec(&self) -> Vec { return self.messages.clone(); } } /// This structure contains the last known information about a chat room. /// It does not store the name, as that is stored implicitly as the key in the /// room hash table. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Room { /// The membership state of the user for the room. pub membership: Membership, /// The visibility of the room. pub visibility: Visibility, /// True if the user is one of the room's operators, False if the user is a /// regular member. pub operated: bool, /// The number of users that are members of the room. pub user_count: usize, /// The name of the room's owner, if any. pub owner: Option, /// The names of the room's operators. pub operators: HashSet, /// The names of the room's members. pub members: HashSet, /// The messages sent to this chat room. pub messages: MessageHistory, /// The tickers displayed in this room. pub tickers: Vec<(String, String)>, } impl Room { /// Creates a new room with the given visibility and user count. pub fn new(visibility: Visibility, user_count: usize) -> Self { Room { membership: Membership::NonMember, visibility: visibility, operated: false, user_count: user_count, owner: None, operators: HashSet::new(), members: HashSet::new(), messages: MessageHistory::default(), tickers: Vec::new(), } } } /// The error returned by RoomMap functions. #[derive(Debug, Error)] pub enum RoomError { #[error("room {0} not found")] RoomNotFound(String), #[error("cannot change membership from {0:?} to {1:?}")] MembershipChangeInvalid(Membership, Membership), } /// 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()`. #[cfg(test)] pub fn insert(&mut self, name: String, room: Room) -> Option { self.map.insert(name, room) } /// 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. pub fn get_strict(&self, room_name: &str) -> Result<&Room, RoomError> { match self.map.get(room_name) { Some(room) => Ok(room), None => Err(RoomError::RoomNotFound(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 Room, RoomError> { match self.map.get_mut(room_name) { Some(room) => Ok(room), None => Err(RoomError::RoomNotFound(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: Visibility, user_count: u32, old_map: &mut HashMap, ) { let room = match old_map.remove(&name) { None => Room::new(Visibility::Public, user_count as usize), Some(mut room) => { room.visibility = visibility; room.user_count = user_count as usize; room } }; if let Some(_) = self.map.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, Visibility::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, Visibility::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, Visibility::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.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, Room)> { let mut rooms = Vec::new(); for (room_name, room) in self.map.iter() { rooms.push((room_name.clone(), room.clone())); } rooms } /// Records that we are now trying to join the given room. /// If the room is not found, or if its membership is not `NonMember`, /// returns an error. pub fn start_joining(&mut self, room_name: &str) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; match room.membership { Membership::NonMember => { room.membership = Membership::Joining; Ok(()) } membership => Err(RoomError::MembershipChangeInvalid( membership, Membership::Joining, )), } } /// Records that we are now a member of the given room and updates the room /// information. pub fn join( &mut self, room_name: &str, owner: Option, mut operators: Vec, members: &[User], ) -> Result<&Room, RoomError> { // First look up the room struct. let room = self.get_mut_strict(room_name)?; // Log what's happening. if let Membership::Joining = room.membership { info!("Joined room {:?}", room_name); } else { warn!( "Joined room {:?} but membership was already {:?}", room_name, room.membership ); } // Update the room struct. room.membership = Membership::Member; room.user_count = members.len(); room.owner = owner; room.operators.clear(); for user_name in operators.drain(..) { room.operators.insert(user_name); } room.members.clear(); for user in members { room.members.insert(user.name.clone()); } Ok(room) } /// 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. pub fn start_leaving(&mut self, room_name: &str) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; match room.membership { Membership::Member => { room.membership = Membership::Leaving; Ok(()) } membership => Err(RoomError::MembershipChangeInvalid( membership, Membership::Leaving, )), } } /// Records that we have now left the given room. pub fn leave(&mut self, room_name: &str) -> Result<(), RoomError> { let room = self.get_mut_strict(room_name)?; match room.membership { Membership::Leaving => info!("Left room {:?}", room_name), membership => warn!( "Left room {:?} with wrong membership: {:?}", room_name, membership ), } room.membership = Membership::NonMember; Ok(()) } /// 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.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.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.tickers = tickers; Ok(()) } } #[cfg(test)] mod tests { use std::time::{Duration, SystemTime}; use solstice_proto::server::RoomListResponse; use super::{Membership, Message, MessageHistory, Room, RoomMap, Visibility}; #[test] fn deserialize_membership() { assert_eq!( serde_json::from_str::(r#""Member""#).unwrap(), Membership::Member ); } #[test] fn deserialize_visibility() { assert_eq!( serde_json::from_str::(r#""Public""#).unwrap(), Visibility::Public ); } #[test] fn deserialize_message() { assert_eq!( serde_json::from_str::( r#"{ "received_at": { "secs_since_epoch": 42, "nanos_since_epoch": 1337 }, "user_name":"karandeep", "message":"namaste" }"# ) .unwrap(), Message { received_at: SystemTime::UNIX_EPOCH + Duration::from_secs(42) + Duration::from_nanos(1337), user_name: "karandeep".to_string(), message: "namaste".to_string() } ); } #[test] fn deserialize_room() { assert_eq!( serde_json::from_str::( r#"{ "membership": "Joining", "visibility": "PrivateOwned", "operated": false, "user_count": 3, "owner": null, "operators": ["op1", "op2"], "members": ["m1", "m2"], "messages": [ { "received_at": { "secs_since_epoch": 43, "nanos_since_epoch": 0 }, "user_name": "u2", "message": "msg2" }, { "received_at": { "secs_since_epoch": 42, "nanos_since_epoch": 0 }, "user_name": "u1", "message": "msg1" } ], "tickers": [["t11", "t12"], ["t21", "t22"]] }"# ) .unwrap(), Room { membership: Membership::Joining, visibility: Visibility::PrivateOwned, operated: false, user_count: 3, owner: None, operators: ["op1".to_string(), "op2".to_string()] .iter() .cloned() .collect(), members: ["m1".to_string(), "m2".to_string()] .iter() .cloned() .collect(), messages: MessageHistory::new(vec![ Message { received_at: SystemTime::UNIX_EPOCH + Duration::from_secs(42), user_name: "u1".to_string(), message: "msg1".to_string(), }, Message { received_at: SystemTime::UNIX_EPOCH + Duration::from_secs(43), user_name: "u2".to_string(), message: "msg2".to_string(), } ]), tickers: vec![ ("t11".to_string(), "t12".to_string()), ("t21".to_string(), "t22".to_string()), ], } ); } #[test] fn room_map_new_is_empty() { assert_eq!(RoomMap::new().get_room_list(), vec![]); } #[test] fn room_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(), &Room::new(Visibility::Public, 42) ); } }