[Add] Accepted Document Version

This commit is contained in:
Alex-Emmet 2026-02-03 22:46:54 +01:00
commit 0812d85e43
14 changed files with 231 additions and 72 deletions

View file

@ -1,23 +1,13 @@
use std::{
io::{Stdout, Write, stdout},
sync::Arc,
time::Duration,
};
use std::{io::Stdout, sync::Arc, time::Duration};
use crate::{
ACTIVE_TASKS, APP_STATE, SHUTDOWN,
APP_STATE, SHUTDOWN,
gui::{settings_panel, widgets::betterblock::draw_block_joins},
util::config_util::CONFIG,
};
use crossterm::{
cursor::Show,
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode},
};
use once_cell::sync::Lazy;
use ratatui::{
Frame, Terminal,
crossterm::terminal::disable_raw_mode,
layout::{Constraint, Direction, Layout, Rect},
prelude::CrosstermBackend,
style::Color,

View file

@ -9,20 +9,20 @@ pub struct LanguagePack {
language: HashMap<String, String>,
}
// Language packs need to have formatting
// Variables need to be provided
pub static LANGUAGE_PACK: Lazy<Mutex<LanguagePack>> =
Lazy::new(|| Mutex::new(LanguagePack::new("en_INT")));
#[allow(dead_code)]
pub fn get_language() -> LanguagePack {
LANGUAGE_PACK.lock().unwrap().clone()
}
#[allow(dead_code)]
pub fn get_languages() -> Vec<String> {
file_util::get_children("languages")
}
#[allow(dead_code)]
pub fn set_language(language: &str) {
LANGUAGE_PACK.lock().unwrap().language.clear();
LANGUAGE_PACK.lock().unwrap().load_language(language);

View file

@ -146,7 +146,7 @@ async fn main() {
break;
}
let omikron: Arc<OmikronConnection> = Arc::new(OmikronConnection::new());
omikron.connect(sb.clone()).await;
omikron.connect().await;
let mut omikron_connection = OMIKRON_CONNECTION.write().await;
*omikron_connection = Some(omikron.clone());
log_message_trans("setup_completed");

View file

@ -14,15 +14,14 @@ use crate::{
util::{config_util::CONFIG, crypto_helper},
};
use dashmap::DashMap;
use futures::Stream;
use futures::stream::{SplitSink, SplitStream};
use futures::{FutureExt, Stream};
use futures_util::sink::Sink;
use futures_util::{SinkExt, StreamExt};
use hyper::upgrade::Upgraded;
use hyper_util::rt::TokioIo;
use json::JsonValue;
use json::number::Number;
use pkcs8::DecodePrivateKey;
use std::collections::HashMap;
use std::sync::{Arc, LazyLock};
use std::time::{SystemTime, UNIX_EPOCH};
@ -31,7 +30,6 @@ use tokio::time::{Duration, Instant, sleep};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tungstenite::Utf8Bytes;
use uuid::Uuid;
use warp::reply::Json;
pub static OMIKRON_CONNECTION: LazyLock<Arc<RwLock<Option<Arc<OmikronConnection>>>>> =
LazyLock::new(|| Arc::new(RwLock::new(None)));
@ -50,7 +48,6 @@ pub struct OmikronConnection {
pub(crate) writer:
Arc<Mutex<Option<Box<dyn Sink<Message, Error = tungstenite::Error> + Send + Unpin>>>>,
waiting: Arc<DashMap<Uuid, Box<dyn Fn(CommunicationValue) + Send + Sync>>>,
pingpong: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
pub last_ping: Arc<Mutex<i64>>,
pub message_send_times: Arc<Mutex<HashMap<Uuid, Instant>>>,
pub is_connected: Arc<Mutex<bool>>,
@ -63,7 +60,6 @@ impl OmikronConnection {
user_id: Arc::new(RwLock::new(0)),
writer: Arc::new(Mutex::new(None)),
waiting: Arc::new(DashMap::new()),
pingpong: Arc::new(Mutex::new(None)),
last_ping: Arc::new(Mutex::new(-1)),
message_send_times: Arc::new(Mutex::new(HashMap::new())),
is_connected: Arc::new(Mutex::new(false)),
@ -79,7 +75,6 @@ impl OmikronConnection {
writer: Arc::new(Mutex::new(Some(Box::new(writer)
as Box<dyn Sink<Message, Error = tungstenite::Error> + Send + Unpin>))),
waiting: Arc::new(DashMap::new()),
pingpong: Arc::new(Mutex::new(None)),
last_ping: Arc::new(Mutex::new(-1)),
message_send_times: Arc::new(Mutex::new(HashMap::new())),
is_connected: Arc::new(Mutex::new(true)),
@ -94,8 +89,8 @@ impl OmikronConnection {
pub async fn is_connected(&self) -> bool {
*self.is_connected.lock().await
}
/// Connect loop with retry
pub async fn connect(self: &Arc<Self>, user_ids: String) {
pub async fn connect(self: &Arc<Self>) {
if self.is_connected().await {
return;
}
@ -104,10 +99,9 @@ impl OmikronConnection {
let iota_id = conf.get_iota_id();
let public_key = conf.get_public_key();
let private_key = conf.get_private_key();
drop(conf); // release read lock
drop(conf);
if iota_id == 0 || public_key.is_none() || private_key.is_none() {
// Registration flow
log_message_trans("iota_register_new");
let key_pair = crypto_helper::generate_keypair();
let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public);
@ -120,7 +114,6 @@ impl OmikronConnection {
drop(conf_write);
if self.connect_internal().await {
// a new helper function to just connect
match self
.clone()
.await_response(
@ -148,7 +141,6 @@ impl OmikronConnection {
}
}
} else {
// Login flow
if self.connect_internal().await {
self.send_message(
&CommunicationValue::new(CommunicationType::identification).add_data(
@ -167,7 +159,6 @@ impl OmikronConnection {
}
log_message_trans("omikron_connecting");
// connect to omikron
let conf = CONFIG.read().await;
let addr = conf
.get("omikron_addr")
@ -223,7 +214,6 @@ impl OmikronConnection {
*self.user_id.write().await = user_id;
}
/// Listener for all incoming messages
async fn spawn_listener(
self: &Arc<Self>,
mut read_half: Box<dyn Stream<Item = Result<Message, tungstenite::Error>> + Send + Unpin>,
@ -444,7 +434,7 @@ impl OmikronConnection {
let sender_id = &cv.get_sender();
let receiver_id = &cv.get_receiver();
chat_files::change_message_state(
let _ = chat_files::change_message_state(
cv.get_data(DataTypes::send_time)
.unwrap_or(&JsonValue::new_object())
.as_i64()
@ -520,7 +510,8 @@ impl OmikronConnection {
DataTypes::message_state,
JsonValue::from(ms.as_str()),
),
);
)
.await;
} else {
self.send_message(
&CommunicationValue::new(CommunicationType::message_state)
@ -535,7 +526,8 @@ impl OmikronConnection {
DataTypes::message_state,
JsonValue::from(MessageState::Send.as_str()),
),
);
)
.await;
}
return;
}

53
src/terms/doc.rs Normal file
View file

@ -0,0 +1,53 @@
use json::{JsonValue, object::Object};
use crate::{terms::terms_getter::Type, util::file_util::load_file};
#[derive(Clone)]
#[allow(unused)]
pub struct Doc {
version: String,
hash: String,
doc_type: Type,
timestamp: u64,
}
#[allow(dead_code)]
impl Doc {
pub fn get_version(&self) -> String {
self.version.clone()
}
pub fn get_hash(&self) -> String {
self.hash.clone()
}
pub fn get_time(&self) -> u64 {
self.timestamp.clone()
}
pub fn get_content(&self) -> String {
load_file(
format!("docs/{}/", self.doc_type.to_str()).as_str(),
format!("{}.md", self.version).as_str(),
)
}
pub fn to_json(&self) -> JsonValue {
let mut json = JsonValue::new_object();
let _ = json.insert("version", self.version.clone());
let _ = json.insert("hash", self.hash.clone());
let _ = json.insert("unix", self.timestamp.clone());
json
}
pub fn from_json(doc_type: Type, json: Object) -> Option<Self> {
let hash = json.get("hash")?.as_str()?.to_string();
let version = json.get("version")?.as_str()?.to_string();
let timestamp = json.get("unix")?.as_u64()?;
Some(Doc {
version,
hash,
doc_type,
timestamp,
})
}
}

View file

@ -306,6 +306,14 @@ fn table_to_lines(table: Vec<Vec<String>>) -> Vec<DisplayLine> {
}
let header = &table[0];
let mut column_heights = vec![0; table[0].len()];
for row in table.iter().skip(1) {
for (i, cell) in row.iter().enumerate() {
let lines = cell.lines().count().max(1);
column_heights[i] += lines;
}
}
let widths: Vec<usize> = header
.iter()
@ -383,22 +391,47 @@ fn wrap_cell(cell: &str, width: usize) -> Vec<String> {
return vec![String::new()];
}
let mut out = Vec::new();
let mut chars = cell.chars();
let mut lines = Vec::new();
let mut current = String::new();
loop {
let line: String = chars.by_ref().take(width).collect();
if line.is_empty() {
break;
for word in cell.split_whitespace() {
let word_len = word.chars().count();
let current_len = current.chars().count();
if current_len == 0 {
if word_len <= width {
current.push_str(word);
} else {
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
lines.push(chunk.iter().collect());
}
}
} else if current_len + 1 + word_len <= width {
current.push(' ');
current.push_str(word);
} else {
lines.push(current);
current = String::new();
if word_len <= width {
current.push_str(word);
} else {
for chunk in word.chars().collect::<Vec<_>>().chunks(width) {
lines.push(chunk.iter().collect());
}
}
}
out.push(line);
}
if out.is_empty() {
out.push(String::new());
if !current.is_empty() {
lines.push(current);
}
out
if lines.is_empty() {
lines.push(String::new());
}
lines
}
fn flush_span(spans: &mut Vec<Span>, buf: &mut String, style: Style) {

View file

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

View file

@ -1,7 +1,7 @@
use crate::{
terms::{
md_viewer::FileViewer,
terms_getter::{Type, get_link, get_terms},
terms_getter::{Type, get_current_docs, get_link, get_terms},
},
util::file_util::{load_file, save_file},
};
@ -41,7 +41,9 @@ impl ConsentManager {
},
};
let state = state.sanitize();
save_file("", "agreements", &state.to_string());
if let Ok(string) = state.to_string().await {
save_file("", "agreements", &string);
}
state
};
@ -92,24 +94,46 @@ impl ConsentUiState {
self
}
fn to_string(self) -> String {
async fn to_string(self) -> Result<String, ()> {
let current_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!(
"\"EULA=true\" indicates that you read and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
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={}\
\n\"PrivacyPolicy=true\" indicates that you read and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\
\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={}\
\n\"ToS=true\" indicates that you read and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\
\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, self.pp, self.tos, current_secs
)
\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 {
@ -195,7 +219,7 @@ async fn run_consent_ui() -> UserChoice {
return;
}
let max_width = 132;
let max_width = 150;
let max_height = 20;
let content_width = if max_width < size.width { max_width } else { size.width} ;
@ -230,30 +254,43 @@ async fn run_consent_ui() -> UserChoice {
};
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) =
if size.width > 132 {
if size.width > 143 {
(
vec![7, 3, 7, 4],
vec![
"",
"By selecting Continue, you confirm that you have read and agree to the End User License Agreement and applicable Terms of Service.",
"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.",
"",
"While having a document selected press O to view in this UI or press L to open as a link.",
]
)
} else if size.width > 68 {
} else if size.width > 92 {
(
vec![8, 3, 8, 4],
vec![
"",
"By selecting Continue, you confirm that you have read and agree",
"to the End User License Agreement and applicable Terms of Service.",
"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.",
"",
"While having a document selected press O to view in this UI or press L to open as a link.",
]
)
} else if size.width > 73 {
(
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.",
"",
"Tensamin services require acceptance of the ToS and Privacy Policy.",
"",
"While having a document selected press O to view in this UI or",
"press L to open as a link.",
"While having a document selected press O to view in this UI or press L",
"to open as a link.",
]
)
} else {
@ -262,8 +299,8 @@ async fn run_consent_ui() -> UserChoice {
vec![
"",
"By selecting Continue, you confirm that you have",
"read and agree to the End User License Agreement",
"and applicable Terms of Service.",
"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.",
@ -340,7 +377,7 @@ async fn run_consent_ui() -> UserChoice {
}
let consent_block = Paragraph::new(Text::from(text_lines))
.block(Block::default().title(" Tensamin User Consent ").borders(Borders::ALL));
.block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL));
f.render_widget(consent_block, chunks[0]);
draw_buttons(f, chunks[1], &state);
@ -358,8 +395,8 @@ async fn run_consent_ui() -> UserChoice {
}
match key.code {
KeyCode::Esc => break UserChoice::Deny,
KeyCode::Up => state.prev(),
KeyCode::Down | KeyCode::Tab => state.next(),
KeyCode::Up | KeyCode::Left => state.prev(),
KeyCode::Down | KeyCode::Right | KeyCode::Tab => state.next(),
KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny,
KeyCode::Char('o') | KeyCode::Char('O') => {
let terms_type = match state.focus {
@ -516,7 +553,7 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentUiState) {
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.can_continue() && state.pp {
} else if state.can_continue_all() {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)

View file

@ -1,3 +1,7 @@
use json::JsonValue::Object;
use crate::terms::doc::Doc;
#[derive(Clone)]
pub enum Type {
EULA,
@ -10,7 +14,7 @@ impl Type {
match self {
Self::EULA => "eula",
Self::TOS => "tos",
Self::PP => "pp",
Self::PP => "privacy",
}
}
pub fn to_string(&self) -> String {
@ -21,9 +25,40 @@ impl Type {
}
}
}
pub fn get_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
}
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/current/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/",

View file

@ -22,7 +22,7 @@ use x448::{PublicKey, Secret};
static USERS: Lazy<Mutex<Vec<UserProfile>>> = Lazy::new(|| Mutex::new(Vec::new()));
static UNIQUE: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
// uuid::private_key
#[allow(dead_code)]
pub async fn load_from_tu(username: &str) -> Result<(), ()> {
let file_content = load_file("", &format!("{}.tu", username));
let segments = file_content.split("::").collect::<Vec<&str>>();
@ -181,6 +181,7 @@ pub async fn load_users() -> io::Result<()> {
Ok(())
}
#[allow(dead_code)]
pub fn set_unique(val: bool) {
*UNIQUE.lock().unwrap() = val;
}

View file

@ -1,7 +1,5 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::gui::log_panel::log_message;
use crate::users::user_manager;
use crate::util::file_util::{has_file, load_file, used_dir_space};
use base64::{Engine as _, engine::general_purpose};
use json::{JsonValue, object};
@ -84,7 +82,7 @@ impl UserProfile {
let created_at = j["created_at"].as_i64()?;
let display_name = j["display_name"].as_str().map(|s| s.to_string());
let mut up = UserProfile {
let up = UserProfile {
user_id,
username,
display_name,
@ -110,6 +108,7 @@ impl UserProfile {
Some(up)
}
#[allow(dead_code)]
pub fn randomize_reset_token(&mut self) -> String {
let mut bytes = [0u8; 192];
OsRng.fill(bytes.as_mut());
@ -118,6 +117,7 @@ impl UserProfile {
new_token
}
#[allow(dead_code)]
pub fn get_display_name(&self) -> String {
self.display_name
.clone()

View file

@ -9,6 +9,7 @@ use x448::{PublicKey, Secret, SharedSecret};
/// Errors for crypto opertions
#[derive(Debug)]
#[allow(dead_code)]
pub enum CryptoError {
Base64Decode(base64::DecodeError),
InvalidKey,
@ -55,6 +56,7 @@ pub fn load_secret_key(base64_secret: &str) -> Option<Secret> {
Secret::from_bytes(&bytes)
}
#[allow(dead_code)]
fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(shared.as_bytes());
@ -64,6 +66,7 @@ fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] {
key
}
#[allow(dead_code)]
pub fn encrypt(
base64_secret: &str,
base64_peer_pub: &str,
@ -89,6 +92,7 @@ pub fn encrypt(
Ok(STANDARD.encode(&out))
}
#[allow(dead_code)]
pub fn decrypt(
base64_secret: &str,
base64_peer_pub: &str,

View file

@ -9,6 +9,7 @@ use x448::{PublicKey, Secret};
// --- Custom Errors ---
#[derive(Debug)]
#[allow(dead_code)]
pub enum SecurePayloadError {
InvalidBase64,
InvalidHex,
@ -19,6 +20,7 @@ pub enum SecurePayloadError {
// --- Data Format Enum ---
#[derive(Clone, Copy, Debug)]
#[allow(dead_code)]
pub enum DataFormat {
Raw,
Base64,
@ -42,6 +44,7 @@ impl Clone for SecurePayload {
}
}
#[allow(dead_code)]
impl SecurePayload {
/// Clear Constructor: Takes data in any format and the user's private key.
pub fn new<S, T: AsRef<[u8]>>(

View file

@ -18,11 +18,13 @@ pub fn delete_file(path: &str, name: &str) -> bool {
fs::remove_file(file).is_ok()
}
#[allow(dead_code)]
pub fn delete_directory(path: &str) -> bool {
let dir = Path::new(&get_directory()).join(path);
delete_dir_recursive(&dir)
}
#[allow(dead_code)]
fn delete_dir_recursive(directory: &Path) -> bool {
if !directory.exists() {
return false;
@ -38,6 +40,7 @@ fn delete_dir_recursive(directory: &Path) -> bool {
true
}
#[allow(dead_code)]
pub fn delete_user_directory(user_id: Uuid) {
let user_dir = Path::new(&get_directory())
.join("users")
@ -187,6 +190,7 @@ pub fn get_directory() -> String {
.to_string()
}
#[allow(dead_code)]
pub fn used_space() -> u64 {
get_directory_size(&PathBuf::from(get_directory()))
}
@ -208,6 +212,7 @@ pub fn get_directory_size(directory: &Path) -> u64 {
size
}
#[allow(dead_code)]
pub fn get_designed_storage(user_id: Uuid) -> String {
let user_dir = Path::new(&get_directory())
.join("users")
@ -215,6 +220,7 @@ pub fn get_designed_storage(user_id: Uuid) -> String {
design_byte(get_directory_size(&user_dir))
}
#[allow(dead_code)]
pub fn design_byte(bytes: u64) -> String {
let mut hr_size = format!("{:.2}B", bytes as f64);
let k = bytes as f64 / 1024.0;
@ -234,6 +240,7 @@ pub fn design_byte(bytes: u64) -> String {
hr_size
}
#[allow(dead_code)]
pub fn get_used_ram() -> String {
let mut sys = System::new_all();
sys.refresh_all();
@ -243,6 +250,7 @@ pub fn get_used_ram() -> String {
}
// Helper to download the zip file content to a file on disk
#[allow(dead_code)]
async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::get(url).await?;
@ -258,6 +266,7 @@ async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box<dyn std::erro
Ok(())
}
#[allow(dead_code, deprecated)]
fn extract_zip_contents_to_folder(
zip_path: &Path,
target_dir: &Path,
@ -334,6 +343,7 @@ fn extract_zip_contents_to_folder(
Ok(())
}
#[allow(dead_code)]
pub async fn download_and_extract_zip(url: &str, as_name: &str) {
let base_dir = PathBuf::from(get_directory());
let zip_filename = format!("{}.zip", Uuid::new_v4()); // Use a unique name for the downloaded ZIP file