Fixes leave on the server and reworked login flow.

This commit is contained in:
2025-06-27 14:45:36 +02:00
parent fbe456a0f5
commit f0883a0ff0
31 changed files with 472 additions and 71 deletions

View File

@@ -129,6 +129,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "clear_screen"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0114f83ad196a822188f44e3def7916956705df289e6634d326ad86b3f0e9c"
dependencies = [
"cc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -269,6 +278,7 @@ version = "0.1.0"
dependencies = [
"async-trait",
"chrono",
"clear_screen",
"dyn-clone",
"gamenight-api-client-rs",
"inquire",

View File

@@ -14,3 +14,4 @@ uuid = { version = "1.3.0", features = ["serde", "v4"] }
jsonwebtoken = "9.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clear_screen = "0.1"

View File

@@ -0,0 +1,85 @@
use std::{env, fs::{self}, path::{Path, PathBuf}};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub struct ConfigError(pub String);
impl From<serde_json::Error> for ConfigError {
fn from(value: serde_json::Error) -> Self {
Self(value.to_string())
}
}
impl From<std::io::Error> for ConfigError {
fn from(value: std::io::Error) -> Self {
Self(value.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Instance {
pub name: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub token: Option<String>
}
impl Instance {
pub fn new(name: String, url: String) -> Self {
Self {
name,
url,
token: None
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub instances: Vec<Instance>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub last_instance: Option<String>,
}
impl Config {
fn config_path() -> PathBuf {
let mut prefix = Path::new(&env::var("HOME").expect("HOME environment variable was not set")).join(".config");
if let Ok(config_home) = env::var("XDG_CONFIG_HOME") {
prefix = config_home.into();
}
prefix.join("gamenight-cli").join("config.json")
}
pub fn new() -> Config {
Config {
instances: vec![],
last_instance: None
}
}
pub fn load() -> Result<Config, ConfigError> {
let config_path = Self::config_path();
if !fs::exists(&config_path)? {
let config_error = ConfigError(format!("Cannot create parent directory for config file: {}", &config_path.display()).to_string());
let _ = fs::create_dir_all(&config_path.parent().ok_or(config_error)?)?;
let config = Config::new();
fs::write(&config_path, serde_json::to_string_pretty(&config)?.as_bytes())?;
Ok(config)
}
else {
let config_string = fs::read_to_string(Self::config_path())?;
Ok(serde_json::from_str(&config_string)?)
}
}
pub fn save(gamenight_configuration: &Config) -> Result<(), ConfigError> {
let config_path = Self::config_path();
fs::write(&config_path, serde_json::to_string_pretty(gamenight_configuration)?.as_bytes())?;
Ok(())
}
}

View File

@@ -13,8 +13,7 @@ pub struct Gamenight {
impl Display for Gamenight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"
Name: {}
write!(f, r#"Name: {}
When: {}"#, self.name, self.start_time.format("%d-%m-%Y %H:%M"))
}
}

View File

@@ -1,2 +1,4 @@
pub mod gamenight;
pub mod user;
pub mod user;
pub mod config;
pub mod participants;

View File

@@ -0,0 +1,12 @@
use std::fmt::Display;
use crate::domain::user::User;
pub struct Participants<'a>(pub &'a Vec<User>);
impl<'a> Display for Participants<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string_list: Vec<String> = self.0.iter().map(|x| {format!("{}", x)}).collect();
write!(f, "{}", string_list.join(", "))
}
}

View File

@@ -35,8 +35,9 @@ impl<'a> Flow<'a> for AddGamenight {
.to_utc()
.to_rfc3339());
post_gamenight(&state.configuration, Some(add_gamenight)).await?;
post_gamenight(&state.api_configuration, Some(add_gamenight)).await?;
clear_screen::clear();
Ok((FlowOutcome::Successful, state))
}
}

View File

@@ -0,0 +1,99 @@
use std::fmt::Display;
use async_trait::async_trait;
use inquire::Text;
use crate::{domain::config::{Config, Instance}, flows::{gamenight_menu::GamenightMenu, login::Login}};
use super::{Flow, FlowOutcome, FlowResult, GamenightState};
#[derive(Clone)]
pub struct Connect {
instance: Option<Instance>
}
impl Connect {
pub fn to(instance: Instance) -> Self {
Self {
instance: Some(instance)
}
}
pub fn new() -> Self {
Self {
instance: None
}
}
}
#[async_trait]
impl<'a> Flow<'a> for Connect {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let mut instance = if let Some(instance) = self.instance.clone() {
instance
} else {
let mut name: String;
loop {
name = Text::new("Name this instance: ").prompt()?;
if state.gamenight_configuration.instances.iter().find(|x| x.name == name).is_none() {
break;
}
clear_screen::clear();
println!("Name already in use, please provide a unique name");
}
let url = Text::new("What is the server URL: ").prompt()?;
Instance::new(name, url)
};
let instance_name = instance.name.clone();
state.api_configuration.base_path = instance.url.clone();
if let Some(token) = instance.token {
state.api_configuration.bearer_access_token = Some(token);
let result = gamenight_api_client_rs::apis::default_api::post_token(&state.api_configuration).await;
if let Ok(token) = result {
let instance = state.gamenight_configuration.instances.iter_mut().find(|x| x.name == instance_name).unwrap();
instance.token = token.jwt_token.clone();
state.api_configuration.bearer_access_token = token.jwt_token.clone();
Config::save(&state.gamenight_configuration)?;
let gamenight_menu_flow = GamenightMenu::new();
return gamenight_menu_flow.run(state).await
}
}
let login_flow = Login::new();
let (outcome, state) = login_flow.run(state).await?;
if outcome == FlowOutcome::Successful {
if self.instance.is_none() {
instance.token = Some(state.api_configuration.bearer_access_token.clone().unwrap());
state.gamenight_configuration.instances.push(instance.clone());
}
else {
let instance = state.gamenight_configuration.instances.iter_mut().find(|x| x.name == instance_name).unwrap();
instance.token = Some(state.api_configuration.bearer_access_token.clone().unwrap());
}
state.gamenight_configuration.last_instance = Some(instance_name);
Config::save(&state.gamenight_configuration)?;
let gamenight_menu_flow = GamenightMenu::new();
gamenight_menu_flow.run(state).await
}
else {
Ok((outcome, state))
}
}
}
impl Display for Connect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(instance) = &self.instance {
write!(f, "Connect to: {}", instance.name)
} else {
write!(f, "Connect")
}
}
}

View File

@@ -14,6 +14,7 @@ impl Exit {
#[async_trait]
impl<'a> Flow<'a> for Exit {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
clear_screen::clear();
Ok((FlowOutcome::Abort, state))
}
}

View File

@@ -1,29 +1,35 @@
use inquire::{ui::RenderConfig, Select};
use crate::flows::{add_gamenight::AddGamenight, exit::Exit, list_gamenights::ListGamenights};
use super::*;
#[derive(Clone)]
pub struct MainMenu {
pub menu: Vec<Box<dyn for<'a> Flow<'a> + Send>>
pub struct GamenightMenu {
}
impl MainMenu {
impl GamenightMenu {
pub fn new() -> Self {
MainMenu {
menu: vec![]
}
Self {}
}
}
unsafe impl Send for MainMenu {
unsafe impl Send for GamenightMenu {
}
#[async_trait]
impl<'a> Flow<'a> for MainMenu {
impl<'a> Flow<'a> for GamenightMenu {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let choice = Select::new("What would you like to do?", self.menu.clone())
let flows: Vec<Box<dyn Flow + Send>> = vec![
Box::new(ListGamenights::new()),
Box::new(AddGamenight::new()),
Box::new(Exit::new())
];
let choice = Select::new("What would you like to do?", flows)
.with_help_message("Select the action you want to take or quit the program")
.with_render_config(RenderConfig {
option_index_prefix: inquire::ui::IndexPrefix::Simple,
@@ -31,11 +37,12 @@ impl<'a> Flow<'a> for MainMenu {
})
.prompt_skippable()?;
handle_choice_option(&choice, self, state).await
clear_screen::clear();
handle_choice_option(&choice, self, state).await
}
}
impl Display for MainMenu {
impl Display for GamenightMenu {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Main menu")
}

View File

@@ -22,7 +22,9 @@ impl Join {
#[async_trait]
impl<'a> Flow<'a> for Join {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let _ = join_post(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?;
let _ = join_post(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?;
clear_screen::clear();
Ok((FlowOutcome::Successful, state))
}
}

View File

@@ -22,7 +22,9 @@ impl Leave {
#[async_trait]
impl<'a> Flow<'a> for Leave {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let _ = leave_post(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?;
let _ = leave_post(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight_id.to_string()})).await?;
clear_screen::clear();
Ok((FlowOutcome::Successful, state))
}
}

View File

@@ -22,7 +22,7 @@ impl ListGamenights {
#[async_trait]
impl<'a> Flow<'a> for ListGamenights {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let response = get_gamenights(&state.configuration).await?;
let response = get_gamenights(&state.api_configuration).await?;
let mut view_flows: Vec<Box<dyn Flow<'_> + Send>> = vec![];
@@ -41,6 +41,7 @@ impl<'a> Flow<'a> for ListGamenights {
let choice = Select::new("What gamenight would you like to view?", view_flows)
.prompt_skippable()?;
clear_screen::clear();
handle_choice_option(&choice, self, state).await
}
}

View File

@@ -28,8 +28,10 @@ impl<'a> Flow<'a> for Login {
let login = models::Login::new(username, password);
let result = get_token(&configuration, Some(login)).await?;
clear_screen::clear();
if let Some(token) = result.jwt_token {
state.configuration.bearer_access_token = Some(token);
state.api_configuration.bearer_access_token = Some(token);
Ok((FlowOutcome::Successful, state))
} else {
Err(FlowError{error: "Unexpected response".to_string()})

View File

@@ -1,22 +1,19 @@
use super::{exit::Exit, add_gamenight::AddGamenight, list_gamenights::ListGamenights, login::Login, main_menu::MainMenu, *};
use std::fmt::Display;
use inquire::{ui::RenderConfig, Select};
use crate::flows::{connect::Connect, exit::Exit, settings::Settings};
use super::*;
#[derive(Clone)]
pub struct Main {
login: Box<dyn for<'a> Flow<'a>>,
main_menu: MainMenu
}
impl Main {
pub fn new() -> Self {
let mut main_menu = MainMenu::new();
main_menu.menu.push(Box::new(ListGamenights::new()));
main_menu.menu.push(Box::new(AddGamenight::new()));
main_menu.menu.push(Box::new(Exit::new()));
Self {
login: Box::new(Login::new()),
main_menu
}
Self {}
}
}
@@ -29,9 +26,32 @@ impl Default for Main {
#[async_trait]
impl<'a> Flow<'a> for Main {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let (_outcome, state) = self.login.run(state).await?;
let (_outcome, state) = self.main_menu.run(state).await?;
Ok((FlowOutcome::Successful, state))
let config = Config::load()?;
state.gamenight_configuration = config;
let mut choices: Vec<Box<dyn Flow<'a> + Send>> = vec![];
if let Some(last_instance) = &state.gamenight_configuration.last_instance {
if let Some(instance) = state.gamenight_configuration.instances.iter().find(|instance| {instance.name.eq(last_instance)}) {
choices.push(Box::new(Connect::to(instance.clone())));
}
}
choices.push(Box::new(Connect::new()));
choices.push(Box::new(Settings::new()));
choices.push(Box::new(Exit::new()));
let choice = Select::new("What would you like to do?", choices)
.with_help_message("Select the action you want to take or quit the program")
.with_render_config(RenderConfig {
option_index_prefix: inquire::ui::IndexPrefix::Simple,
..Default::default()
})
.prompt_skippable()?;
clear_screen::clear();
handle_choice_option(&choice, self, state).await
}
}

View File

@@ -1,29 +1,36 @@
use std::fmt::Display;
use std::{fmt::Display, num::ParseIntError};
use async_trait::async_trait;
use chrono::ParseError;
use gamenight_api_client_rs::apis::{configuration::Configuration, Error};
use inquire::InquireError;
use dyn_clone::DynClone;
pub use clear_screen;
use crate::domain::config::{Config, ConfigError};
pub mod main;
mod login;
mod main_menu;
mod gamenight_menu;
mod exit;
mod list_gamenights;
mod add_gamenight;
mod view_gamenight;
mod join;
mod leave;
mod connect;
mod settings;
pub struct GamenightState {
configuration: Configuration,
api_configuration: Configuration,
gamenight_configuration: Config,
}
impl GamenightState {
pub fn new() -> Self{
Self {
configuration: Configuration::new()
api_configuration: Configuration::new(),
gamenight_configuration: Config::new()
}
}
}
@@ -71,6 +78,22 @@ impl From<uuid::Error> for FlowError {
}
}
impl From<ConfigError> for FlowError {
fn from(value: ConfigError) -> Self {
Self {
error: value.0
}
}
}
impl From<ParseIntError> for FlowError {
fn from(value: ParseIntError) -> Self {
Self {
error: value.to_string()
}
}
}
#[derive(PartialEq)]
pub enum FlowOutcome {
Successful,

View File

@@ -0,0 +1,29 @@
use std::fmt::Display;
use async_trait::async_trait;
use super::*;
#[derive(Clone)]
pub struct Settings {
}
impl Settings {
pub fn new() -> Self {
Self {
}
}
}
#[async_trait]
impl<'a> Flow<'a> for Settings {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
Ok((FlowOutcome::Successful, state))
}
}
impl Display for Settings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Settings")
}
}

View File

@@ -5,7 +5,7 @@ use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{domain::{gamenight::Gamenight, user::User}, flows::{exit::Exit, join::Join, leave::Leave}};
use crate::{domain::{gamenight::Gamenight, participants::Participants, user::User}, flows::{exit::Exit, join::Join, leave::Leave}};
use super::*;
@@ -23,11 +23,7 @@ impl ViewGamenight {
}
fn vec_user_to_usernames_string(users: &Vec<User>) -> String {
let string_list: Vec<String> = users.iter().map(|x| {format!("{}", x)}).collect();
string_list.join(", ")
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
@@ -47,31 +43,31 @@ impl From<jsonwebtoken::errors::Error> for FlowError {
impl<'a> Flow<'a> for ViewGamenight {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let participants = participants_get(&state.configuration, Some(GamenightId{gamenight_id: self.gamenight.id.to_string()})).await?;
let participants = participants_get(&state.api_configuration, Some(GamenightId{gamenight_id: self.gamenight.id.to_string()})).await?;
let mut users = vec![];
for participant in participants.participants.iter() {
let user = user_get(&state.configuration, Some(UserId{user_id: participant.clone()})).await?;
let user = user_get(&state.api_configuration, Some(UserId{user_id: participant.clone()})).await?;
users.push(User {
id: Uuid::parse_str(&user.id)?,
username: user.username
});
}
println!("{}\nwho: {}", self.gamenight, vec_user_to_usernames_string(&users));
println!("{}\nwho: {}", self.gamenight, Participants(&users));
let decoding_key = DecodingKey::from_secret(b"");
let mut validation = Validation::default();
validation.insecure_disable_signature_validation();
let claims = decode::<Claims>(
&state.configuration.bearer_access_token.as_ref().unwrap(),
&state.api_configuration.bearer_access_token.as_ref().unwrap(),
&decoding_key,
&validation)?.claims;
let join_or_leave: Box<dyn Flow<'a> + Send> =
if users.iter().map(|x| {x.id}).find(|x| *x == claims.uid) != None {
Box::new(Leave::new(claims.uid))
Box::new(Leave::new(self.gamenight.id))
}
else {
Box::new(Join::new(claims.uid))
Box::new(Join::new(self.gamenight.id))
};
let options: Vec<Box<dyn Flow<'a> + Send>> = vec![
@@ -81,6 +77,7 @@ impl<'a> Flow<'a> for ViewGamenight {
let choice = Select::new("What do you want to do:", options)
.prompt_skippable()?;
clear_screen::clear();
handle_choice_option(&choice, self, state).await
}
}

View File

@@ -2,11 +2,13 @@ pub mod flows;
pub mod domain;
use flows::{main::Main, Flow, GamenightState};
use clear_screen;
#[tokio::main]
async fn main() {
let mut state = GamenightState::new();
let mainflow = Main::new();
clear_screen::clear();
if let Err(x) = mainflow.run(&mut state).await {
println!("{}", x.error);
}