|
|
|
@ -0,0 +1,313 @@ |
|
|
|
use std::mem;
|
|
|
|
|
|
|
|
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<ServerRequest>,
|
|
|
|
|
|
|
|
// Immutable, see accessor methods.
|
|
|
|
user_name: String,
|
|
|
|
|
|
|
|
// TODO: Remove annotation.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
motd: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Initial values from which a `ServerLoggedInContext` can be built.
|
|
|
|
pub struct ServerLoggedInContextOptions {
|
|
|
|
/// See similarly-named fields in `ServerLoggedInContext`.
|
|
|
|
pub user_name: String,
|
|
|
|
pub motd: String,
|
|
|
|
pub rooms: RoomMap,
|
|
|
|
pub users: UserMap,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ServerLoggedInContext {
|
|
|
|
/// Mainly for use in tests.
|
|
|
|
// TODO: Mark as #[cfg(test)]?
|
|
|
|
pub fn new(
|
|
|
|
request_tx: Sender<ServerRequest>,
|
|
|
|
options: ServerLoggedInContextOptions,
|
|
|
|
) -> Self {
|
|
|
|
Self {
|
|
|
|
rooms: options.rooms,
|
|
|
|
users: options.users,
|
|
|
|
user_name: options.user_name,
|
|
|
|
motd: options.motd,
|
|
|
|
request_tx,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The user name with which we logged in.
|
|
|
|
#[cfg(test)]
|
|
|
|
pub fn user_name(&self) -> &str {
|
|
|
|
&self.user_name
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The message of the day sent by the server when we logged in.
|
|
|
|
#[cfg(test)]
|
|
|
|
pub fn motd(&self) -> &str {
|
|
|
|
&self.motd
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Records that we have logged out.
|
|
|
|
#[cfg(test)]
|
|
|
|
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<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Information provided upon a successful login.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
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<ServerRequest>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ServerLoggedOutContext {
|
|
|
|
/// The user name with which we will attempt or have attempted to log in.
|
|
|
|
#[cfg(test)]
|
|
|
|
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.
|
|
|
|
#[cfg(test)]
|
|
|
|
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.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
pub fn set_error(&mut self, error: String) {
|
|
|
|
self.error = Some(error)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Records that we logged in successfully.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
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.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
LoggedOut(ServerLoggedOutContext),
|
|
|
|
|
|
|
|
/// We are logged in to the server.
|
|
|
|
LoggedIn(ServerLoggedInContext),
|
|
|
|
}
|
|
|
|
|
|
|
|
fn server_logged_out_error(error: &Option<String>) -> 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<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[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.
|
|
|
|
#[cfg(test)]
|
|
|
|
pub fn new(user_name: String) -> Self {
|
|
|
|
Self::LoggedOut(ServerLoggedOutContext {
|
|
|
|
user_name,
|
|
|
|
error: None,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Attempts to record a successful login. Fails if we are already logged in.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
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.
|
|
|
|
#[cfg(test)]
|
|
|
|
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.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
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.
|
|
|
|
#[allow(dead_code)]
|
|
|
|
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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use solstice_proto::{User, UserStatus};
|
|
|
|
use tokio::sync::mpsc::channel;
|
|
|
|
|
|
|
|
use crate::server::room::{RoomMap, RoomState, RoomVisibility};
|
|
|
|
use crate::server::user::UserMap;
|
|
|
|
|
|
|
|
use super::{ServerLoggedInContext, ServerLoggedInContextOptions};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn server_logged_in_context_new() {
|
|
|
|
let mut rooms = RoomMap::default();
|
|
|
|
rooms.insert(
|
|
|
|
"foo".to_string(),
|
|
|
|
RoomState::new(RoomVisibility::Public, 42),
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut users = UserMap::default();
|
|
|
|
users.insert(User {
|
|
|
|
name: "kim".to_string(),
|
|
|
|
status: UserStatus::Online,
|
|
|
|
average_speed: 1,
|
|
|
|
num_downloads: 2,
|
|
|
|
unknown: 3,
|
|
|
|
num_files: 4,
|
|
|
|
num_folders: 5,
|
|
|
|
num_free_slots: 6,
|
|
|
|
country: "KR".to_string(),
|
|
|
|
});
|
|
|
|
|
|
|
|
let (tx, _rx) = channel(100);
|
|
|
|
|
|
|
|
let context = ServerLoggedInContext::new(
|
|
|
|
tx.clone(),
|
|
|
|
ServerLoggedInContextOptions {
|
|
|
|
users: users.clone(),
|
|
|
|
rooms: rooms.clone(),
|
|
|
|
user_name: "bob".to_string(),
|
|
|
|
motd: "hey".to_string(),
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(context.user_name(), "bob");
|
|
|
|
assert_eq!(context.motd(), "hey");
|
|
|
|
assert!(context.request_tx.same_channel(&tx));
|
|
|
|
assert_eq!(context.rooms, rooms);
|
|
|
|
assert_eq!(context.users, users);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn server_logged_in_context_logout() {
|
|
|
|
let (tx, _rx) = channel(100);
|
|
|
|
|
|
|
|
let context = ServerLoggedInContext::new(
|
|
|
|
tx,
|
|
|
|
ServerLoggedInContextOptions {
|
|
|
|
users: UserMap::new(),
|
|
|
|
rooms: RoomMap::new(),
|
|
|
|
user_name: "bob".to_string(),
|
|
|
|
motd: "hey".to_string(),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.logout();
|
|
|
|
|
|
|
|
assert_eq!(context.user_name(), "bob");
|
|
|
|
assert_eq!(context.error(), None);
|
|
|
|
}
|
|
|
|
}
|