diff --git a/client/src/context.rs b/client/src/context.rs index 6cf0f5c..dde100e 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -9,25 +9,40 @@ use crate::clock::SimulatedSystemClock; use crate::clock::SystemClock; use crate::control::Response as ControlResponse; use crate::peer::PeerMap; +use crate::server::context::{ServerContext, ServerLoginInfo}; use crate::server::room::RoomMap; use crate::server::user::UserMap; use crate::Config; /// Contains all the different bits of client state. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct State { pub rooms: RoomMap, pub users: UserMap, pub peers: PeerMap, } +impl Default for State { + fn default() -> Self { + Self { + rooms: RoomMap::default(), + users: UserMap::default(), + peers: PeerMap::default(), + } + } +} + /// Holds process-wide context for message handlers to execute against. #[derive(Debug)] pub struct Context { /// Mutable state. pub state: State, + /// Server-related context bits. + pub server: ServerContext, + /// The user name with which we logged in to the server. + // TODO: Use the value in `server` instead. pub user_name: String, /// Sender half of a channel used to send requests to the server. @@ -101,7 +116,19 @@ impl ContextBundle { Self { context: Context { - state: options.initial_state, + state: State { + rooms: options.initial_state.rooms.clone(), + users: options.initial_state.users.clone(), + peers: options.initial_state.peers, + }, + // TODO: Do not login immediately, wait for a login event. + server: ServerContext::new_logged_in( + options.user_name.clone(), + ServerLoginInfo { + motd: "unimplemented".to_string(), + request_tx: server_request_tx.clone(), + }, + ), user_name: options.user_name, server_request_tx, control_response_tx, diff --git a/client/src/event.rs b/client/src/event.rs index ec694a4..16e6298 100644 --- a/client/src/event.rs +++ b/client/src/event.rs @@ -8,6 +8,7 @@ use crate::control; /// The type of events affecting the client. #[derive(Debug, Eq, PartialEq)] pub enum Event { + // TODO: events for login stuff ControlRequest(control::Request), ServerResponse(ServerResponse), } diff --git a/client/src/server/context.rs b/client/src/server/context.rs new file mode 100644 index 0000000..fa4b589 --- /dev/null +++ b/client/src/server/context.rs @@ -0,0 +1,214 @@ +use std::mem; + +use anyhow::anyhow; +use solstice_proto::ServerRequest; +use thiserror::Error; +use tokio::sync::mpsc::Sender; + +use crate::server::room::RoomMap; +use crate::server::user::UserMap; + +/// Server-related state stored when we are logged in. +#[derive(Debug)] +pub struct ServerLoggedInContext { + /// Chat rooms hosted by the server. + pub rooms: RoomMap, + + /// Users connected to the server. + pub users: UserMap, + + /// Sends requests to the server. + pub request_tx: Sender, + + // Immutable, see accessor methods. + user_name: String, + motd: String, +} + +impl ServerLoggedInContext { + /// The user name with which we logged in. + pub fn user_name(&self) -> &str { + &self.user_name + } + + /// The message of the day sent by the server when we logged in. + pub fn motd(&self) -> &str { + &self.motd + } + + /// Records that we have logged out. + pub fn logout(self) -> ServerLoggedOutContext { + ServerLoggedOutContext { + user_name: self.user_name, + error: None, + } + } +} + +/// Server-related state stored when we are logged out. +#[derive(Debug)] +pub struct ServerLoggedOutContext { + // Immutable, see accessor method. + user_name: String, + + // Once set to `Some(...)`, never set to `None` again. + error: Option, +} + +/// Information provided upon a successful login. +pub struct ServerLoginInfo { + /// The message of the day sent by the server when we logged in. + pub motd: String, + + /// Sends requests to the server. + pub request_tx: Sender, +} + +impl ServerLoggedOutContext { + /// The user name with which we will attempt or have attempted to log in. + pub fn user_name(&self) -> &str { + &self.user_name + } + + /// The error received during the last login attempt, if any. + /// `None` if we have never attempted to log in. + pub fn error(&self) -> Option<&str> { + self.error.as_deref() + } + + /// Records that we failed to log in, and the server sent us the given error. + pub fn set_error(&mut self, error: String) { + self.error = Some(error) + } + + /// Records that we logged in successfully. + pub fn login(self, info: ServerLoginInfo) -> ServerLoggedInContext { + ServerLoggedInContext { + user_name: self.user_name, + motd: info.motd, + request_tx: info.request_tx, + rooms: RoomMap::default(), + users: UserMap::default(), + } + } +} + +/// Server-related client state. +#[derive(Debug)] +pub enum ServerContext { + /// We are logged out of the server. + LoggedOut(ServerLoggedOutContext), + + /// We are logged in to the server. + LoggedIn(ServerLoggedInContext), +} + +fn server_logged_out_error(error: &Option) -> String { + match error { + Some(error) => format!("login error: {}", error), + None => "not logged in".to_string(), + } +} + +#[derive(Debug, Error)] +#[error("{}", server_logged_out_error(.error))] +pub struct ServerLoggedOutError { + error: Option, +} + +#[derive(Debug, Error)] +#[error("logged in as {user_name}")] +pub struct ServerLoggedInError { + user_name: String, +} + +impl ServerContext { + /// Constructs a new logged out context with the given user name. + pub fn new(user_name: String) -> Self { + Self::LoggedOut(ServerLoggedOutContext { + user_name, + error: None, + }) + } + + /// Constructs a new logged in context with the given information. + pub fn new_logged_in(user_name: String, login: ServerLoginInfo, rooms: RoomMap, users: UserMap) -> Self { + ServerLoggedOutContext { + user_name, + error: None, + } + .login(login), + Self::LoggedIn( + ) + } + + /// Attempts to record a successful login. Fails if we are already logged in. + pub fn login( + &mut self, + info: ServerLoginInfo, + ) -> Result<(), ServerLoggedInError> { + let context = match self { + Self::LoggedIn(context) => { + return Err(ServerLoggedInError { + user_name: context.user_name.clone(), + }) + } + // Replace the previous value with a dummy that we will immediately + // overwrite. This allows us to move the previous context out from under + // a mutable reference. + Self::LoggedOut(context) => mem::replace( + context, + ServerLoggedOutContext { + user_name: String::new(), + error: None, + }, + ), + }; + + *self = Self::LoggedIn(context.login(info)); + Ok(()) + } + + /// Attempts to record that we logged out. Fails if we are not logged in. + pub fn logout(&mut self) -> Result<(), ServerLoggedOutError> { + if let Self::LoggedOut(context) = self { + return Err(ServerLoggedOutError { + error: context.error.clone(), + }); + } + + // See `mem::replace()` call in `login()`. + let old_self = mem::replace(self, Self::new(String::new())); + let context = match old_self { + Self::LoggedOut(context) => unreachable!(), + Self::LoggedIn(context) => context, + }; + + *self = Self::LoggedOut(context.logout()); + Ok(()) + } + + /// Returns a reference to the logged in context. + pub fn logged_in( + &mut self, + ) -> Result<&mut ServerLoggedInContext, ServerLoggedOutError> { + match self { + Self::LoggedIn(context) => Ok(context), + Self::LoggedOut(context) => Err(ServerLoggedOutError { + error: context.error.clone(), + }), + } + } + + /// Returns a reference to the logged out context. + pub fn logged_out( + &mut self, + ) -> Result<&mut ServerLoggedOutContext, ServerLoggedInError> { + match self { + Self::LoggedIn(context) => Err(ServerLoggedInError { + user_name: context.user_name.clone(), + }), + Self::LoggedOut(context) => Ok(context), + } + } +} diff --git a/client/src/server/login.rs b/client/src/server/login.rs index 8333318..39e20b8 100644 --- a/client/src/server/login.rs +++ b/client/src/server/login.rs @@ -1,3 +1,8 @@ +use anyhow::bail; + +use crate::context::Context; +use crate::event::EventHandler; + /// Represents the status of the login operation. /// /// In order to interact with the server, a client cannot simply open a network @@ -20,3 +25,51 @@ pub enum LoginStatus { /// Stores the error message as received from the server. Failure(String), } + +pub enum LoginEvent { + Success(String), + Failure(String), + Disconnected, + StatusRequest, +} + +pub struct LoginEventHandler {} + +fn handle_login_success( + context: &mut Context, + motd: String, +) -> anyhow::Result<()> { + bail!("unimplemented") +} + +fn handle_login_failure( + context: &mut Context, + error: String, +) -> anyhow::Result<()> { + bail!("unimplemented") +} + +fn handle_disconnected(context: &mut Context) -> anyhow::Result<()> { + bail!("unimplemented") +} + +fn handle_status_request(context: &mut Context) -> anyhow::Result<()> { + bail!("unimplemented") +} + +impl EventHandler for LoginEventHandler { + type Event = LoginEvent; + + fn handle( + &mut self, + context: &mut Context, + event: LoginEvent, + ) -> anyhow::Result<()> { + match event { + LoginEvent::Success(motd) => handle_login_success(context, motd), + LoginEvent::Failure(error) => handle_login_failure(context, error), + LoginEvent::Disconnected => handle_disconnected(context), + LoginEvent::StatusRequest => handle_status_request(context), + } + } +} diff --git a/client/src/server/mod.rs b/client/src/server/mod.rs index 3e75ae2..82a6674 100644 --- a/client/src/server/mod.rs +++ b/client/src/server/mod.rs @@ -1,3 +1,4 @@ +pub mod context; mod login; pub mod room; pub mod user; diff --git a/client/src/server/room/event.rs b/client/src/server/room/event.rs index 9975106..b53a8be 100644 --- a/client/src/server/room/event.rs +++ b/client/src/server/room/event.rs @@ -220,6 +220,7 @@ mod tests { // Close the channel, so we can observe it was empty without hanging. drop(bundle.context.server_request_tx); + bundle.context.server.logout().expect("logging out"); assert_eq!(bundle.server_request_rx.blocking_recv(), None); } diff --git a/client/src/server/room/map.rs b/client/src/server/room/map.rs index f75904e..d715680 100644 --- a/client/src/server/room/map.rs +++ b/client/src/server/room/map.rs @@ -21,7 +21,7 @@ pub struct RoomMembershipChangeError(RoomMembership, RoomMembership); pub struct RoomNotFoundError(String); /// An entry in the chat room map. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct RoomEntry { name: String, state: RoomState, @@ -138,7 +138,7 @@ impl RoomEntry { /// Contains the mapping from room names to room data and provides a clean /// interface to interact with it. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct RoomMap { /// The actual map from room names to room data. map: HashMap, diff --git a/client/src/server/user/map.rs b/client/src/server/user/map.rs index 7d5a966..881c360 100644 --- a/client/src/server/user/map.rs +++ b/client/src/server/user/map.rs @@ -11,7 +11,7 @@ pub struct UserNotFoundError(String); /// Contains the mapping from user names to user data and provides a clean /// interface to interact with it. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct UserMap { /// The actual map from user names to user data and privileged status. map: collections::HashMap,