use std::{collections::HashMap, sync::Arc}; use async_std::sync::RwLock; use chrono::{DateTime, Duration, TimeDelta, Utc}; use mime::Mime; use result_extended::{error, fatal, ok, result_as_fatal, return_error, ResultExt}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use typeshare::typeshare; use uuid::Uuid; use crate::{ asset_db::{self, AssetId, Assets}, database::{CharacterId, Database, GameId, SessionId, UserId}, types::AccountState, types::{AppError, FatalError, GameOverview, Message, Rgb, Tabletop, User, UserOverview}, }; const DEFAULT_BACKGROUND_COLOR: Rgb = Rgb { red: 0xca, green: 0xb9, blue: 0xbb, }; #[derive(Clone, Serialize)] #[typeshare] pub struct Status { pub ok: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", content = "content")] #[typeshare] pub enum AuthResponse { Success(SessionId), PasswordReset(SessionId), Locked, } #[derive(Debug)] struct WebsocketClient { sender: Option<UnboundedSender<Message>>, } pub struct AppState { pub asset_store: Box<dyn Assets + Sync + Send + 'static>, pub db: Box<dyn Database + Sync + Send + 'static>, pub clients: HashMap<String, WebsocketClient>, pub tabletop: Tabletop, } #[derive(Clone)] pub struct Core(Arc<RwLock<AppState>>); impl Core { pub fn new<A, DB>(assetdb: A, db: DB) -> Self where A: Assets + Sync + Send + 'static, DB: Database + Sync + Send + 'static, { Self(Arc::new(RwLock::new(AppState { asset_store: Box::new(assetdb), db: Box::new(db), clients: HashMap::new(), tabletop: Tabletop { background_color: DEFAULT_BACKGROUND_COLOR, background_image: None, }, }))) } pub async fn status(&self) -> ResultExt<Status, AppError, FatalError> { /* let state = self.0.write().await; let admin_user = return_error!(match state.db.user(&UserId::from("admin")).await { Ok(Some(admin_user)) => ok(admin_user), Ok(None) => { return ok(Status { admin_enabled: false, }); } Err(err) => fatal(err), }); ok(Status { ok: !admin_user.password.is_empty(), }) */ ok(Status { ok: true }) } pub async fn register_client(&self) -> String { let mut state = self.0.write().await; let uuid = Uuid::new_v4().simple().to_string(); let client = WebsocketClient { sender: None }; state.clients.insert(uuid.clone(), client); uuid } pub async fn unregister_client(&self, client_id: String) { let mut state = self.0.write().await; let _ = state.clients.remove(&client_id); } pub async fn connect_client(&self, client_id: String) -> UnboundedReceiver<Message> { let mut state = self.0.write().await; match state.clients.get_mut(&client_id) { Some(client) => { let (tx, rx) = unbounded_channel(); client.sender = Some(tx); rx } None => { unimplemented!(); } } } pub async fn user_by_username( &self, username: &str, ) -> ResultExt<Option<User>, AppError, FatalError> { let state = self.0.read().await; match state.db.user_by_username(username).await { Ok(Some(user_row)) => ok(Some(User::from(user_row))), Ok(None) => ok(None), Err(err) => fatal(err), } } pub async fn list_users(&self) -> ResultExt<Vec<UserOverview>, AppError, FatalError> { let users = self.0.write().await.db.users().await; match users { Ok(users) => ok(users .into_iter() .map(|user| UserOverview { id: user.id, name: user.name, state: user.state, is_admin: user.admin, }) .collect()), Err(err) => fatal(err), } } pub async fn user( &self, user_id: UserId, ) -> ResultExt<Option<UserOverview>, AppError, FatalError> { let users = return_error!(self.list_users().await); match users.into_iter().find(|user| user.id == user_id) { Some(user) => ok(Some(user)), None => return ok(None), } } pub async fn create_user(&self, username: &str) -> ResultExt<UserId, AppError, FatalError> { let state = self.0.read().await; match return_error!(self.user_by_username(username).await) { Some(_) => error(AppError::UsernameUnavailable), None => match state .db .create_user(username, "", false, AccountState::PasswordReset(Utc::now() + Duration::minutes(60))) .await { Ok(user_id) => ok(user_id), Err(err) => fatal(err), }, } } pub async fn disable_user(&self, _userid: UserId) -> ResultExt<(), AppError, FatalError> { unimplemented!(); } pub async fn list_games(&self) -> ResultExt<Vec<GameOverview>, AppError, FatalError> { let games = self.0.read().await.db.games().await; match games { // Ok(games) => ok(games.into_iter().map(|g| Game::from(g)).collect()), Ok(games) => ok(games .into_iter() .map(|game| GameOverview { id: game.id, type_: "".to_owned(), name: game.name, gm: game.gm, players: game.players, }) .collect::<Vec<GameOverview>>()), Err(err) => fatal(err), } } pub async fn create_game( &self, gm: &UserId, game_type: &str, game_name: &str, ) -> ResultExt<GameId, AppError, FatalError> { let state = self.0.read().await; match state.db.create_game(gm, game_type, game_name).await { Ok(game_id) => ok(game_id), Err(err) => fatal(err), } } pub async fn tabletop(&self) -> Tabletop { self.0.read().await.tabletop.clone() } pub async fn get_asset( &self, asset_id: AssetId, ) -> ResultExt<(Mime, Vec<u8>), AppError, FatalError> { ResultExt::from( self.0 .read() .await .asset_store .get(asset_id.clone()) .map_err(|err| match err { asset_db::Error::NotFound => AppError::NotFound(format!("{}", asset_id)), asset_db::Error::Inaccessible => { AppError::Inaccessible(format!("{}", asset_id)) } asset_db::Error::Unexpected(err) => AppError::Inaccessible(format!("{}", err)), }), ) } pub async fn available_images(&self) -> Vec<AssetId> { self.0 .read() .await .asset_store .assets() .filter_map( |(asset_id, value)| match mime_guess::from_path(value).first() { Some(mime) if mime.type_() == mime::IMAGE => Some(asset_id.clone()), _ => None, }, ) .collect() } pub async fn set_background_image( &self, asset: AssetId, ) -> ResultExt<(), AppError, FatalError> { let tabletop = { let mut state = self.0.write().await; state.tabletop.background_image = Some(asset.clone()); state.tabletop.clone() }; self.publish(Message::UpdateTabletop(tabletop)).await; ok(()) } pub async fn get_charsheet( &self, id: CharacterId, ) -> ResultExt<Option<serde_json::Value>, AppError, FatalError> { let state = self.0.write().await; let cr = state.db.character(&id).await; match cr { Ok(Some(row)) => ok(Some(row.data)), Ok(None) => ok(None), Err(err) => fatal(err), } } pub async fn publish(&self, message: Message) { let state = self.0.read().await; state.clients.values().for_each(|client| { if let Some(ref sender) = client.sender { let _ = sender.send(message.clone()); } }); } pub async fn save_user( &self, id: UserId, name: &str, password: &str, admin: bool, account_state: AccountState, ) -> ResultExt<UserId, AppError, FatalError> { let state = self.0.read().await; match state .db .save_user(User { id, name: name.to_owned(), password: password.to_owned(), admin, state: account_state, }) .await { Ok(uuid) => ok(uuid), Err(err) => fatal(err), } } pub async fn set_password( &self, uuid: UserId, password: String, ) -> ResultExt<(), AppError, FatalError> { let state = self.0.write().await; let user = match state.db.user(&uuid).await { Ok(Some(row)) => row, Ok(None) => return error(AppError::NotFound(uuid.as_str().to_owned())), Err(err) => return fatal(err), }; match state .db .save_user(User { password, state: AccountState::Normal, ..user }) .await { Ok(_) => ok(()), Err(err) => fatal(err), } } pub async fn auth( &self, username: &str, password: &str, ) -> ResultExt<AuthResponse, AppError, FatalError> { let now = Utc::now(); let state = self.0.read().await; let user = state.db.user_by_username(username).await.unwrap().unwrap(); let user_info = return_error!(match state.db.user_by_username(username).await { Ok(Some(row)) if row.password == password => ok(row), Ok(_) => error(AppError::AuthFailed), Err(err) => fatal(err), }); match user_info.state { AccountState::Normal => result_as_fatal(state.db.create_session(&user_info.id).await) .map(|session_id| AuthResponse::Success(session_id)), AccountState::PasswordReset(exp) => { if exp < now { error(AppError::AuthFailed) } else { result_as_fatal(state.db.create_session(&user_info.id).await) .map(|session_id| AuthResponse::PasswordReset(session_id)) } } AccountState::Locked => ok(AuthResponse::Locked), } } pub async fn session( &self, session_id: &SessionId, ) -> ResultExt<Option<User>, AppError, FatalError> { let state = self.0.read().await; match state.db.session(session_id).await { Ok(Some(user_row)) => ok(Some(User::from(user_row))), Ok(None) => ok(None), Err(fatal_error) => fatal(fatal_error), } } pub async fn delete_session(&self, session_id: &SessionId) -> ResultExt<(), AppError, FatalError> { let state = self.0.read().await; match state.db.delete_session(session_id).await { Ok(_) => ok(()), Err(err) => fatal(err), } } } fn create_expiration_date() -> DateTime<Utc> { Utc::now() + TimeDelta::days(365) } #[cfg(test)] mod test { use std::path::PathBuf; use super::*; use cool_asserts::assert_matches; use crate::{asset_db::mocks::MemoryAssets, database::DbConn}; async fn test_core() -> Core { let assets = MemoryAssets::new(vec![ ( AssetId::from("asset_1"), "asset_1.png".to_owned(), String::from("abcdefg").into_bytes(), ), ( AssetId::from("asset_2"), "asset_2.jpg".to_owned(), String::from("abcdefg").into_bytes(), ), ( AssetId::from("asset_3"), "asset_3".to_owned(), String::from("abcdefg").into_bytes(), ), ( AssetId::from("asset_4"), "asset_4".to_owned(), String::from("abcdefg").into_bytes(), ), ( AssetId::from("asset_5"), "asset_5".to_owned(), String::from("abcdefg").into_bytes(), ), ]); let memory_db: Option<PathBuf> = None; let conn = DbConn::new(memory_db); conn.create_user("admin", "aoeu", true, AccountState::Normal) .await .unwrap(); conn.create_user( "gm_1", "aoeu", false, AccountState::PasswordReset(Utc::now()), ) .await .unwrap(); Core::new(assets, conn) } #[tokio::test] async fn it_lists_available_images() { let core = test_core().await; let image_paths = core.available_images().await; assert_eq!(image_paths.len(), 2); } #[tokio::test] async fn it_retrieves_an_asset() { let core = test_core().await; assert_matches!(core.get_asset(AssetId::from("asset_1")).await, ResultExt::Ok((mime, data)) => { assert_eq!(mime.type_(), mime::IMAGE); assert_eq!(data, "abcdefg".as_bytes()); }); } #[tokio::test] async fn it_can_retrieve_the_default_tabletop() { let core = test_core().await; assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, None); }); } #[tokio::test] async fn it_can_change_the_tabletop_background() { let core = test_core().await; assert_matches!( core.set_background_image(AssetId::from("asset_1")).await, ResultExt::Ok(()) ); assert_matches!(core.tabletop().await, Tabletop{ background_color, background_image } => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, Some(AssetId::from("asset_1"))); }); } #[tokio::test] async fn it_sends_notices_to_clients_on_tabletop_change() { let core = test_core().await; let client_id = core.register_client().await; let mut receiver = core.connect_client(client_id).await; assert_matches!( core.set_background_image(AssetId::from("asset_1")).await, ResultExt::Ok(()) ); match receiver.recv().await { Some(Message::UpdateTabletop(Tabletop { background_color, background_image, })) => { assert_eq!(background_color, DEFAULT_BACKGROUND_COLOR); assert_eq!(background_image, Some(AssetId::from("asset_1"))); } None => panic!("receiver did not get a message"), } } #[tokio::test] async fn it_creates_a_sessionid_on_successful_auth() { let core = test_core().await; match core.auth("admin", "aoeu").await { ResultExt::Ok(AuthResponse::Success(session_id)) => { let st = core.0.read().await; match st.db.session(&session_id).await { Ok(Some(user_row)) => assert_eq!(user_row.name, "admin"), Ok(None) => panic!("no matching user row for the session id"), Err(err) => panic!("{}", err), } } ResultExt::Ok(AuthResponse::PasswordReset(_)) => panic!("user is in password reset state"), ResultExt::Ok(AuthResponse::Locked) => panic!("user has been locked"), ResultExt::Err(err) => panic!("{}", err), ResultExt::Fatal(err) => panic!("{}", err), } } }