[WIP] Terms update support

This commit is contained in:
Alex-Emmet 2026-02-06 22:47:42 +01:00
commit b81276dd18
6 changed files with 251 additions and 91 deletions

View file

@ -3,41 +3,71 @@ use crate::{
doc::Doc,
terms_checker,
terms_getter::{Type, get_current_docs, get_newest_docs},
terms_updater,
},
util::file_util::load_file,
util::file_util::{load_file, save_file},
};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::task;
pub struct ConsentManager;
impl ConsentManager {
pub async fn check() -> (bool, bool) {
let file = load_file("", "agreements");
let file_state = ConsentState::from_str(&file).sanitize();
let accepted_state = if !&file_state.accepted_eula {
terms_checker::run_consent_ui(file_state).await
} else {
file_state
let mut file_state = ConsentState::from_str(&file).sanitize();
if !file_state.accepted_eula {
file_state = terms_checker::run_consent_ui(file_state).await;
println!("{}", &file_state.clone().sanitize().to_string());
println!("-----------------------");
save_file("", "agreements", &file_state.to_string());
}
if !file_state.accepted_eula {
return (false, false);
}
if let Some((eula_update, tos_update, privacy_update)) = Self::get_updates().await {
let is_forced = eula_update.is_err() || tos_update.is_err() || privacy_update.is_err();
let updater_logic = async move {
let state_to_update = terms_updater::run_consent_ui(
file_state,
eula_update,
tos_update,
privacy_update,
)
.await;
println!("{}", &state_to_update.clone().sanitize().to_string());
save_file("", "agreements", &state_to_update.sanitize().to_string());
};
if let Some((current_eula, current_tos, current_privacy)) = get_current_docs().await {
if current_eula.equals_some(&accepted_state.eula) {
if current_tos.equals_some(&accepted_state.tos)
&& current_privacy.equals_some(&accepted_state.pp)
{
(true, true)
if is_forced {
updater_logic.await;
} else {
(true, false)
}
} else {
(false, false)
}
} else {
println!("There was an error while loading our EULA, please retry later!");
(false, false)
task::spawn(updater_logic);
}
}
pub async fn check_updates() -> Option<(Option<Doc>, Option<Doc>, Option<Doc>)> {
let final_state = ConsentState::from_str(&load_file("", "agreements")).sanitize();
(
final_state.accepted_eula,
final_state.accepted_tos && final_state.accepted_pp,
)
}
async fn get_updates() -> Option<(
// Ok(None) indicates no update
// Ok(Some) Indicates a future update
// Err indicates a update that has to be accepted before the programm can continue
Result<Option<Doc>, Doc>,
Result<Option<Doc>, Doc>,
Result<Option<Doc>, Doc>,
)> {
let file = load_file("", "agreements");
let accepted_state = ConsentState::from_str(&file).sanitize();
@ -46,7 +76,48 @@ impl ConsentManager {
Some((newest_eula, newest_tos, newest_privacy)),
) = (get_current_docs().await, get_newest_docs().await)
{
None
let eula_update: Result<Option<Doc>, Doc> =
if current_eula.equals_some(&accepted_state.eula) {
if current_eula.equals(&newest_eula) {
Ok(None)
} else {
Ok(Some(newest_eula))
}
} else if newest_eula.equals_some(&accepted_state.eula) {
Ok(None)
} else {
Err(current_eula)
};
let tos_update: Result<Option<Doc>, Doc> = if newest_tos
.equals_some(&accepted_state.tos)
{
Ok(None)
} else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) {
if current_tos.equals(&newest_tos) {
Ok(None)
} else {
Ok(Some(newest_tos))
}
} else {
Err(current_tos)
};
let privacy_update: Result<Option<Doc>, Doc> =
if newest_privacy.equals_some(&accepted_state.privacy) {
Ok(None)
} else if accepted_state.accepted_pp
&& current_privacy.equals_some(&accepted_state.privacy)
{
if current_privacy.equals(&newest_privacy) {
Ok(None)
} else {
Ok(Some(newest_privacy))
}
} else {
Err(current_privacy)
};
Some((eula_update, tos_update, privacy_update))
} else {
None
}
@ -66,7 +137,7 @@ pub struct ConsentState {
pub accepted_eula: bool,
pub tos: Option<Doc>,
pub accepted_tos: bool,
pub pp: Option<Doc>,
pub privacy: Option<Doc>,
pub accepted_pp: bool,
}
@ -79,7 +150,7 @@ impl ConsentState {
self
}
async fn to_string(self) -> String {
pub fn to_string(&self) -> String {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@ -93,9 +164,13 @@ impl ConsentState {
current_secs
);
if self.accepted_eula
&& let Some(eula) = self.eula
{
if let Some(eula) = &self.eula {
println!(
"EULA: {}, {}, {}",
self.accepted_eula,
eula.get_version(),
eula.get_hash()
);
file_out.push_str(&format!("\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
\nEULA={}\
@ -104,17 +179,17 @@ impl ConsentState {
", self.accepted_eula, eula.get_version(), eula.get_hash()));
if self.accepted_tos
&& let Some(tos) = self.tos
&& let Some(tos) = &self.tos
{
file_out.push_str(&format!("\
\n\"ToS=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
\nToS={}\
\nToS-VERSION={}\
\nToS-HASH={}\
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
\nTerms-of-Service={}\
\nTerms-of-Service-VERSION={}\
\nTerms-of-Service-HASH={}\
", self.accepted_tos, tos.get_version(), tos.get_hash()));
}
if self.accepted_pp
&& let Some(pp) = self.pp
&& let Some(pp) = &self.privacy
{
file_out.push_str(&format!("\
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\
@ -152,17 +227,17 @@ impl ConsentState {
eula_version = v.to_string();
} else if let Some(v) = line.strip_prefix("EULA-HASH=") {
eula_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("ToS=") {
} else if let Some(v) = line.strip_prefix("Terms-of-Service=") {
tos = v == "true";
} else if let Some(v) = line.strip_prefix("ToS-VERSION=") {
} else if let Some(v) = line.strip_prefix("Terms-of-Service-VERSION=") {
tos_version = v.to_string();
} else if let Some(v) = line.strip_prefix("ToS-HASH=") {
} else if let Some(v) = line.strip_prefix("Terms-of-Service-HASH=") {
tos_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("PrivacyPolicy=") {
} else if let Some(v) = line.strip_prefix("Privacy-Policy=") {
pp = v == "true";
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-VERSION=") {
} else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") {
pp_version = v.to_string();
} else if let Some(v) = line.strip_prefix("PrivacyPolicy-HASH=") {
} else if let Some(v) = line.strip_prefix("Privacy-Policy-HASH=") {
pp_hash = v.to_string();
} else if let Some(v) = line.strip_prefix("UNIX=") {
unix = v.to_string();
@ -173,7 +248,7 @@ impl ConsentState {
accepted_eula: eula,
eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)),
accepted_pp: pp,
pp: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
privacy: Some(Doc::new(pp_version, pp_hash, Type::PP, unix)),
accepted_tos: tos,
tos: Some(Doc::new(tos_version, tos_hash, Type::TOS, unix)),
}

View file

@ -2,12 +2,12 @@ use json::{JsonValue, object::Object};
use crate::{terms::terms_getter::Type, util::file_util::load_file};
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(unused)]
pub struct Doc {
version: String,
hash: String,
doc_type: Type,
pub doc_type: Type,
timestamp: u64,
}

View file

@ -1,4 +1,4 @@
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Eula,
Tos,
@ -7,6 +7,7 @@ pub enum Focus {
Continue,
ContinueAll,
}
impl Focus {
pub fn next(&mut self, state: (bool, bool, bool)) {
*self = match self {

View file

@ -2,7 +2,7 @@ use crate::terms::{
consent_state::{ConsentState, UserChoice},
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms},
terms_getter::{Type, get_current_docs, get_link, get_terms},
};
use crossterm::event::{self, Event, KeyCode};
use ratatui::{
@ -52,19 +52,19 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
height: content_height,
});
let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)"
"EULA ¹ (https://legal.tensamin.net/eula/newest/)"
} else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)"
};
let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
"ToS ² (https://legal.tensamin.net/terms-of-service/newest/)"
} else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)"
};
let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)"
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)"
} else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)"
};
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
@ -275,6 +275,12 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
};
ratatui::restore();
if let Some((eula, tos, privacy)) = get_current_docs().await {
consent.eula = Some(eula);
consent.tos = Some(tos);
consent.privacy = Some(privacy);
}
match result {
UserChoice::AcceptAll => {
consent.accepted_eula = true;
@ -292,6 +298,7 @@ pub async fn run_consent_ui(mut consent: ConsentState) -> ConsentState {
consent.accepted_pp = false;
}
};
consent
}

3
src/terms/terms_getter.rs Normal file → Executable file
View file

@ -2,7 +2,7 @@ use json::JsonValue::Object;
use crate::terms::doc::Doc;
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Type {
EULA,
TOS,
@ -87,7 +87,6 @@ pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
None
}
}
pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/",

View file

@ -1,5 +1,6 @@
use crate::terms::{
consent_state::UserChoice,
consent_state::{ConsentState, UserChoice},
doc::Doc,
focus::Focus,
md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms},
@ -13,10 +14,29 @@ use ratatui::{
};
use std::time::Duration;
pub async fn run_consent_ui() -> UserChoice {
pub async fn run_consent_ui(
mut consent: ConsentState,
consent_eula: Result<Option<Doc>, Doc>,
consent_tos: Result<Option<Doc>, Doc>,
consent_pp: Result<Option<Doc>, Doc>,
) -> ConsentState {
let mut terminal = ratatui::init();
let (mut eula, mut tos, mut pp) = (false, false, false);
let (eula_needed, eula_future, future_eula) = match consent_eula {
Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (tos_needed, tos_future, future_tos) = match consent_tos {
Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (pp_needed, pp_future, future_privacy) = match consent_pp {
Ok(Some(future)) => (true, true, Some(future)),
Ok(None) => (false, false, None),
Err(future) => (true, false, Some(future)),
};
let (mut eula, mut tos, mut pp) = (!eula_needed, !tos_needed, !pp_needed);
let mut focus = Focus::Eula;
let result = loop {
@ -51,28 +71,68 @@ pub async fn run_consent_ui() -> UserChoice {
width: content_width,
height: content_height,
});
let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)"
let mut text_lines = Vec::new();
if eula_needed {
if eula_future {
if size.width < 70 {
text_lines.push(checkbox("EULA ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
} else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
};
let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
text_lines.push(checkbox("End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
}
} else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
};
let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)"
if size.width < 70 {
text_lines.push(checkbox("EULA ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
} else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
};
text_lines.push(checkbox("End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", eula, focus == Focus::Eula, true));
}
}
}
if tos_needed {
if tos_future {
if size.width < 70 {
text_lines.push(checkbox("ToS ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
} else {
text_lines.push(checkbox("Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
}
} else {
if size.width < 70 {
text_lines.push(checkbox("ToS ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
} else {
text_lines.push(checkbox("Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", tos, focus == Focus::Tos, eula));
}
}
}
if pp_needed {
if pp_future {
if size.width < 70 {
text_lines.push(checkbox("PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
} else {
text_lines.push(checkbox("Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
}
} else {
if size.width < 70 {
text_lines.push(checkbox("PP ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, pp));
} else {
text_lines.push(checkbox("Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", pp, focus == Focus::Pp, eula));
}
}
}
text_lines.push(Line::from(""));
text_lines.push(Line::from("¹ Necessary, ² Optional, ³ Future"));
text_lines.push(Line::from(""));
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
if size.width > 143 {
(
vec![7, 3, 7, 4],
vec![
"",
"By selecting Continue, you confirm that you have read, understood and agree to the End User License Agreement and applicable Terms of Service.",
"",
"Tensamin services require acceptance of the Terms of Service and Privacy Policy.",
@ -97,7 +157,6 @@ pub async fn run_consent_ui() -> UserChoice {
(
vec![8, 3, 8, 4],
vec![
"",
"By selecting Continue, you confirm that you have read, understood and",
"agree to the End User License Agreement and applicable Terms of Service.",
"",
@ -111,7 +170,6 @@ pub async fn run_consent_ui() -> UserChoice {
(
vec![9, 3, 10, 4],
vec![
"",
"By selecting Continue, you confirm that you have",
"read, understood and agree to the End User License",
"Agreement and applicable Terms of Service.",
@ -124,13 +182,8 @@ pub async fn run_consent_ui() -> UserChoice {
]
)
};
let mut text_lines = vec![
checkbox(eula_text, eula, focus == Focus::Eula, true),
checkbox(tos_text, tos, focus == Focus::Tos, eula),
checkbox(pp_text, pp, focus == Focus::Pp, eula),
Line::from(""),
Line::from("¹ Necessary, ² Optional"),
];
for line in agree_lines {
text_lines.insert(text_lines.len(), Line::from(line));
@ -191,10 +244,10 @@ pub async fn run_consent_ui() -> UserChoice {
}
let consent_block = Paragraph::new(Text::from(text_lines))
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
.block(Block::default().title(format!(" Update Tensamin User Consent [Q to Quit] {}, {}, {}", pp_needed, eula_needed, tos_needed)).borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]);
draw_buttons(f, chunks[1], (eula, tos, pp));
draw_buttons(f, chunks[1], focus, (eula, tos, pp));
})
.unwrap();
@ -275,7 +328,23 @@ pub async fn run_consent_ui() -> UserChoice {
};
ratatui::restore();
result
match result {
UserChoice::AcceptAll => {
consent.accepted_eula = true;
consent.accepted_tos = true;
consent.accepted_pp = true;
consent.eula = future_eula;
consent.tos = future_tos;
consent.privacy = future_privacy;
}
UserChoice::AcceptEULA => {
consent.accepted_eula = true;
consent.eula = future_eula;
}
UserChoice::Deny => {}
};
consent
}
#[allow(mismatched_lifetime_syntaxes)]
@ -313,7 +382,12 @@ fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
.block(Block::default().borders(Borders::ALL));
f.render_widget(p, area);
}
fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
fn draw_buttons(
f: &mut ratatui::Frame,
area: Rect,
current_focus: Focus,
state: (bool, bool, bool),
) {
let buttons = vec![
("[Q] Cancel", Focus::Cancel),
("Continue", Focus::Continue),
@ -339,9 +413,11 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
};
x += width;
let is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if focus == &Focus::Cancel {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
@ -350,20 +426,22 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if focus == &Focus::Continue && state.0 {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 && state.1 && state.2 {
Style::default().fg(Color::Green)
} else if state.0 {
Style::default().fg(Color::Green) // enabled but not focused
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if focus == &Focus::ContinueAll && state.0 && state.1 && state.2 {
if is_focused && state.0 && state.1 && state.2 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
@ -374,13 +452,13 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: (bool, bool, bool)) {
Style::default().fg(Color::DarkGray)
}
}
_ => Style::default().fg(Color::DarkGray),
};
draw_button(f, chunk, label, style);
}
}
fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (0..min_widths.len()).collect();