[Add] Split terms Terms Checker & Consent State

This commit is contained in:
Alex Emmet 2026-02-05 10:18:15 +01:00
commit 6b957f8eaa
4 changed files with 183 additions and 173 deletions

View file

@ -28,6 +28,7 @@ use crate::langu::language_creator;
use crate::langu::language_manager::format; use crate::langu::language_manager::format;
use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection}; use crate::omikron::omikron_connection::{OMIKRON_CONNECTION, OmikronConnection};
use crate::server::server::start; use crate::server::server::start;
use crate::terms::consent_state;
use crate::terms::terms_checker; use crate::terms::terms_checker;
use crate::users::user_manager; use crate::users::user_manager;
use crate::util::config_util::CONFIG; use crate::util::config_util::CONFIG;
@ -48,7 +49,7 @@ async fn main() {
*SHUTDOWN.write().await = false; *SHUTDOWN.write().await = false;
// EULA // EULA
let (tos, pp) = terms_checker::ConsentManager::check().await; let (tos, pp) = consent_state::ConsentManager::check().await;
if !tos { if !tos {
println!("You need to accept our End User Licence Agreement before launching!"); println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!"); println!("You can find this at 'agreements'!");

137
src/terms/consent_state.rs Normal file
View file

@ -0,0 +1,137 @@
use crate::{
terms::{terms_checker::run_consent_ui, terms_getter::get_current_docs},
util::file_util::{load_file, save_file},
};
use std::time::{SystemTime, UNIX_EPOCH};
pub struct ConsentManager;
impl ConsentManager {
pub async fn check() -> (bool, bool) {
let file = load_file("", "agreements");
let existing = ConsentUiState::from_str(&file).sanitize();
let final_state = if existing.eula {
existing
} else {
let choice = run_consent_ui().await;
let state = match choice {
UserChoice::Deny => ConsentUiState::denied(),
UserChoice::AcceptEULA => ConsentUiState {
eula: true,
tos: false,
pp: false,
},
UserChoice::AcceptAll => ConsentUiState {
eula: true,
tos: true,
pp: true,
},
};
let state = state.sanitize();
if let Ok(string) = state.to_string().await {
save_file("", "agreements", &string);
}
state
};
(final_state.eula, final_state.pp && final_state.tos)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum UserChoice {
Deny,
AcceptEULA,
AcceptAll,
}
#[derive(Debug, Clone, Copy)]
pub struct ConsentUiState {
pub eula: bool,
pub tos: bool,
pub pp: bool,
}
impl ConsentUiState {
fn denied() -> Self {
Self {
eula: false,
pp: false,
tos: false,
}
}
fn sanitize(mut self) -> Self {
if !self.eula {
self.tos = false;
self.pp = false;
}
self
}
async fn to_string(self) -> Result<String, ()> {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some((eula, tos, pp)) = get_current_docs().await {
Ok(format!(
"\
\"EULA=true\" indicates that you read, understood and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
\nEULA={}\
\nEULA-VERSION={}\
\nEULA-HASH={}\
\n\"PrivacyPolicy=true\" indicates that you read, understood and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\
\nPrivacyPolicy={}\
\nPrivacyPolicy-VERSION={}\
\nPrivacyPolicy-HASH={}\
\n\"ToS=true\" indicates that you read, understood and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\
\nToS={}\
\nToS-VERSION={}\
\nToS-HASH={}\
\nThis file reflects the current consent state used by the application.\
\nIt may be regenerated or overwritten by the application.\
\nThis file was last edited by Tensamin at:\
\nUNIX-SECOND={}\
",
self.eula,
eula.get_version(),
eula.get_hash(),
self.tos,
tos.get_version(),
tos.get_hash(),
self.pp,
pp.get_version(),
pp.get_hash(),
current_secs
))
} else {
Err(())
}
}
fn from_str(s: &str) -> Self {
let mut eula = false;
let mut pp = false;
let mut tos = false;
for line in s.lines() {
if let Some(v) = line.strip_prefix("EULA=") {
eula = v == "true";
} else if let Some(v) = line.strip_prefix("ToS=") {
tos = v == "true";
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
pp = v == "true";
}
}
Self { eula, pp, tos }.sanitize()
}
pub fn can_continue(&self) -> bool {
self.eula
}
pub fn can_continue_all(&self) -> bool {
self.eula && self.pp && self.tos
}
}

View file

@ -1,3 +1,4 @@
pub mod consent_state;
pub mod doc; pub mod doc;
pub mod md_viewer; pub mod md_viewer;
pub mod terms_checker; pub mod terms_checker;

View file

@ -1,9 +1,7 @@
use crate::{ use crate::terms::{
terms::{ consent_state::{ConsentUiState, UserChoice},
md_viewer::FileViewer, md_viewer::FileViewer,
terms_getter::{Type, get_current_docs, get_link, get_terms}, terms_getter::{Type, get_link, get_terms},
},
util::file_util::{load_file, save_file},
}; };
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use ratatui::{ use ratatui::{
@ -13,61 +11,9 @@ use ratatui::{
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
use std::time::Duration; use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct ConsentManager;
impl ConsentManager {
pub async fn check() -> (bool, bool) {
let file = load_file("", "agreements");
let existing = ConsentUiState::from_str(&file).sanitize();
let final_state = if existing.eula {
existing
} else {
let choice = run_consent_ui().await;
let state = match choice {
UserChoice::Deny => ConsentUiState::denied(),
UserChoice::AcceptEULA => ConsentUiState {
eula: true,
tos: false,
pp: false,
focus: Focus::Cancel,
},
UserChoice::AcceptAll => ConsentUiState {
eula: true,
tos: true,
pp: true,
focus: Focus::Cancel,
},
};
let state = state.sanitize();
if let Ok(string) = state.to_string().await {
save_file("", "agreements", &string);
}
state
};
(final_state.eula, final_state.pp && final_state.tos)
}
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
enum UserChoice { pub enum Focus {
Deny,
AcceptEULA,
AcceptAll,
}
#[derive(Debug, Clone, Copy)]
struct ConsentUiState {
eula: bool,
tos: bool,
pp: bool,
focus: Focus,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Focus {
Eula, Eula,
Tos, Tos,
Pp, Pp,
@ -75,117 +21,41 @@ enum Focus {
Continue, Continue,
ContinueAll, ContinueAll,
} }
impl Focus {
impl ConsentUiState { fn next(&mut self, state: ConsentUiState) {
fn denied() -> Self { *self = match self {
Self {
eula: false,
pp: false,
tos: false,
focus: Focus::Cancel,
}
}
fn sanitize(mut self) -> Self {
if !self.eula {
self.tos = false;
self.pp = false;
}
self
}
async fn to_string(self) -> Result<String, ()> {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
if let Some((eula, tos, pp)) = get_current_docs().await {
Ok(format!(
"\
\"EULA=true\" indicates that you read, understood and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
\nEULA={}\
\nEULA-VERSION={}\
\nEULA-HASH={}\
\n\"PrivacyPolicy=true\" indicates that you read, understood and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\
\nPrivacyPolicy={}\
\nPrivacyPolicy-VERSION={}\
\nPrivacyPolicy-HASH={}\
\n\"ToS=true\" indicates that you read, understood and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\
\nToS={}\
\nToS-VERSION={}\
\nToS-HASH={}\
\nThis file reflects the current consent state used by the application.\
\nIt may be regenerated or overwritten by the application.\
\nThis file was last edited by Tensamin at:\
\nUNIX-SECOND={}\
",
self.eula,
eula.get_version(),
eula.get_hash(),
self.tos,
tos.get_version(),
tos.get_hash(),
self.pp,
pp.get_version(),
pp.get_hash(),
current_secs
))
} else {
Err(())
}
}
fn from_str(s: &str) -> Self {
let mut eula = false;
let mut pp = false;
let mut tos = false;
for line in s.lines() {
if let Some(v) = line.strip_prefix("EULA=") {
eula = v == "true";
} else if let Some(v) = line.strip_prefix("ToS=") {
tos = v == "true";
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
pp = v == "true";
}
}
Self {
eula,
pp,
tos,
focus: Focus::Cancel,
}
.sanitize()
}
fn can_continue(&self) -> bool {
self.eula
}
fn can_continue_all(&self) -> bool {
self.eula && self.pp && self.tos
}
fn next(&mut self) {
self.focus = match self.focus {
Focus::Eula => Focus::Tos, Focus::Eula => Focus::Tos,
Focus::Tos => Focus::Pp, Focus::Tos => Focus::Pp,
Focus::Pp => Focus::Cancel, Focus::Pp => Focus::Cancel,
Focus::Cancel => { Focus::Cancel => {
if self.can_continue() { if state.can_continue() {
Focus::Continue Focus::Continue
} else { } else {
Focus::Eula Focus::Eula
} }
} }
Focus::Continue => Focus::ContinueAll, Focus::Continue => {
if state.can_continue_all() {
Focus::ContinueAll
} else {
Focus::Eula
}
}
Focus::ContinueAll => Focus::Eula, Focus::ContinueAll => Focus::Eula,
}; };
} }
fn prev(&mut self) { fn prev(&mut self, state: ConsentUiState) {
self.focus = match self.focus { *self = match self {
Focus::Eula => Focus::ContinueAll, Focus::Eula => {
if state.can_continue_all() {
Focus::ContinueAll
} else if state.can_continue() {
Focus::Continue
} else {
Focus::Cancel
}
}
Focus::Tos => Focus::Eula, Focus::Tos => Focus::Eula,
Focus::Pp => Focus::Tos, Focus::Pp => Focus::Tos,
Focus::Cancel => Focus::Pp, Focus::Cancel => Focus::Pp,
@ -195,15 +65,15 @@ impl ConsentUiState {
} }
} }
async fn run_consent_ui() -> UserChoice { pub async fn run_consent_ui() -> UserChoice {
let mut terminal = ratatui::init(); let mut terminal = ratatui::init();
let mut state = ConsentUiState { let mut state = ConsentUiState {
eula: false, eula: false,
tos: false, tos: false,
pp: false, pp: false,
focus: Focus::Eula,
}; };
let mut focus = Focus::Eula;
let result = loop { let result = loop {
let mut too_small = false; let mut too_small = false;
@ -311,9 +181,9 @@ async fn run_consent_ui() -> UserChoice {
) )
}; };
let mut text_lines = vec![ let mut text_lines = vec![
checkbox(eula_text, state.eula, state.focus == Focus::Eula, true), checkbox(eula_text, state.eula, focus == Focus::Eula, true),
checkbox(tos_text, state.tos, state.focus == Focus::Tos, state.eula), checkbox(tos_text, state.tos, focus == Focus::Tos, state.eula),
checkbox(pp_text, state.pp, state.focus == Focus::Pp, state.eula), checkbox(pp_text, state.pp, focus == Focus::Pp, state.eula),
Line::from(""), Line::from(""),
Line::from("¹ Necessary, ² Optional"), Line::from("¹ Necessary, ² Optional"),
]; ];
@ -395,11 +265,11 @@ async fn run_consent_ui() -> UserChoice {
} }
match key.code { match key.code {
KeyCode::Esc => break UserChoice::Deny, KeyCode::Esc => break UserChoice::Deny,
KeyCode::Up | KeyCode::Left => state.prev(), KeyCode::Up | KeyCode::Left => focus.prev(state),
KeyCode::Down | KeyCode::Right | KeyCode::Tab => state.next(), KeyCode::Down | KeyCode::Right | KeyCode::Tab => focus.next(state),
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny, KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
KeyCode::Char('o') | KeyCode::Char('O') => { KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match state.focus { let terms_type = match focus {
Focus::Eula => Type::EULA, Focus::Eula => Type::EULA,
Focus::Tos => Type::TOS, Focus::Tos => Type::TOS,
Focus::Pp => Type::PP, Focus::Pp => Type::PP,
@ -412,13 +282,14 @@ async fn run_consent_ui() -> UserChoice {
} else { } else {
terminal = FileViewer::new( terminal = FileViewer::new(
terms_type.to_string(), terms_type.to_string(),
"### A loading error occured", "### A loading error occured\
\nTry reloading this site or retry in a moment.",
) )
.force_popup(terminal); .force_popup(terminal);
} }
} }
KeyCode::Char('l') | KeyCode::Char('L') => { KeyCode::Char('l') | KeyCode::Char('L') => {
let terms_type = match state.focus { let terms_type = match focus {
Focus::Eula => Type::EULA, Focus::Eula => Type::EULA,
Focus::Tos => Type::TOS, Focus::Tos => Type::TOS,
Focus::Pp => Type::PP, Focus::Pp => Type::PP,
@ -427,7 +298,7 @@ async fn run_consent_ui() -> UserChoice {
let _ = open::that(get_link(terms_type)); let _ = open::that(get_link(terms_type));
} }
KeyCode::Char(' ') => match state.focus { KeyCode::Char(' ') => match focus {
Focus::Eula => { Focus::Eula => {
state.eula = !state.eula; state.eula = !state.eula;
state.tos = false; state.tos = false;
@ -445,7 +316,7 @@ async fn run_consent_ui() -> UserChoice {
} }
_ => {} _ => {}
}, },
KeyCode::Enter => match state.focus { KeyCode::Enter => match focus {
Focus::Cancel => break UserChoice::Deny, Focus::Cancel => break UserChoice::Deny,
Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA, Focus::Continue if state.can_continue() => break UserChoice::AcceptEULA,
Focus::ContinueAll if state.can_continue_all() => { Focus::ContinueAll if state.can_continue_all() => {
@ -526,7 +397,7 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentUiState) {
let style = match focus { let style = match focus {
Focus::Cancel => { Focus::Cancel => {
if state.focus == Focus::Cancel { if focus == &Focus::Cancel {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Red) .bg(Color::Red)
@ -536,7 +407,7 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentUiState) {
} }
} }
Focus::Continue => { Focus::Continue => {
if state.focus == Focus::Continue && state.can_continue() { if focus == &Focus::Continue && state.can_continue() {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Green) .bg(Color::Green)
@ -548,7 +419,7 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentUiState) {
} }
} }
Focus::ContinueAll => { Focus::ContinueAll => {
if state.focus == Focus::ContinueAll && state.can_continue_all() { if focus == &Focus::ContinueAll && state.can_continue_all() {
Style::default() Style::default()
.fg(Color::Black) .fg(Color::Black)
.bg(Color::Green) .bg(Color::Green)