Solstice client.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

473 lines
12 KiB

//! This module defines abstractions for handling the global set of rooms and
//! their state.
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};
#[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.
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<String>,
mut operators: Vec<String>,
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 this room.
/// Returns an error if its membership status is not `Member`,
#[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 this 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;
}
/// Adds the given user to this room's members.
pub fn insert_member(&mut self, user_name: String) {
self.state.members.insert(user_name);
}
/// Removes the given user from this room's members.
pub fn remove_member(&mut self, user_name: &str) {
self.state.members.remove(user_name);
}
/// Sets this room's "tickers" to the given list.
pub fn set_tickers(&mut self, tickers: Vec<(String, String)>) {
self.state.tickers = tickers;
}
}
/// 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<String, RoomEntry>,
}
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<RoomState> {
self
.map
.insert(name.clone(), RoomEntry::new(name, room))
.map(|entry| entry.into_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<String, RoomEntry>,
) {
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
}
}
#[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::<HashSet<String>>(),
user_count: 2,
members: users
.iter()
.map(|user| user.name.clone())
.collect::<HashSet<String>>(),
..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 entry_insert_member() {
let mut room = RoomEntry::new("bleep".to_string(), RoomState::default());
room.insert_member("shruti".to_string());
assert_eq!(
room.into_state(),
RoomState {
members: ["shruti".to_string()]
.into_iter()
.collect::<HashSet<String>>(),
..RoomState::default()
}
);
}
#[test]
fn entry_insert_member_twice() {
let mut room = RoomEntry::new("bleep".to_string(), RoomState::default());
room.insert_member("shruti".to_string());
room.insert_member("shruti".to_string());
assert_eq!(
room.into_state(),
RoomState {
members: ["shruti".to_string()]
.into_iter()
.collect::<HashSet<String>>(),
..RoomState::default()
}
);
}
#[test]
fn entry_remove_member() {
let mut room = RoomEntry::new(
"bleep".to_string(),
RoomState {
members: ["shruti".to_string()]
.into_iter()
.collect::<HashSet<String>>(),
..RoomState::default()
},
);
room.remove_member("shruti");
assert_eq!(room.into_state(), RoomState::default());
}
#[test]
fn entry_set_tickers() {
let mut room = RoomEntry::new("bleep".to_string(), RoomState::default());
let tickers = vec![
("shruti".to_string(), "hello".to_string()),
("karandeep".to_string(), "yo".to_string()),
];
room.set_tickers(tickers.clone());
assert_eq!(
room.into_state(),
RoomState {
tickers,
..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)
);
}
}