From 5dc5ffdad86f71a1b75d515b8d5e26419f09da96 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:20:06 +0200 Subject: [PATCH 1/6] Migrated left overs from github --- Cargo.lock | 128 +++++---- README.md | 3 +- src/gui/elements/console_card.rs | 235 ++++++++++++++++- src/gui/elements/log_card.rs | 418 ++++++++++++++++++++++++++---- src/gui/input_handler.rs | 60 ++--- src/omikron/omikron_connection.rs | 310 +++++++++++++++------- src/util/chat_files.rs | 318 +++++++++++------------ src/util/chats_util.rs | 212 +++++++-------- src/util/communities_util.rs | 90 +++++++ src/util/db.rs | 185 +++++++++++++ src/util/logger.rs | 108 +++++--- src/util/mod.rs | 2 + 12 files changed, 1495 insertions(+), 574 deletions(-) create mode 100644 src/util/communities_util.rs create mode 100644 src/util/db.rs diff --git a/Cargo.lock b/Cargo.lock index aa15ccf..cd56dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1471,9 +1471,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1486,7 +1486,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1559,12 +1558,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1572,9 +1572,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1585,9 +1585,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1599,15 +1599,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1619,15 +1619,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1784,9 +1784,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "cfg-if", "futures-util", @@ -1941,9 +1941,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libsqlite3-sys" @@ -1972,9 +1972,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "litrs" @@ -2509,12 +2509,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.32" @@ -2632,9 +2626,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3695,9 +3689,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3879,7 +3873,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ttp-core" version = "0.1.0" -source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743" +source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" dependencies = [ "base64", "byteorder", @@ -3891,7 +3885,7 @@ dependencies = [ [[package]] name = "ttp-native" version = "0.1.0" -source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743" +source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" dependencies = [ "quinn", "rustls", @@ -4123,9 +4117,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -4136,9 +4130,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ "js-sys", "wasm-bindgen", @@ -4146,9 +4140,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4156,9 +4150,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -4169,9 +4163,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -4212,9 +4206,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -4872,9 +4866,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4883,9 +4877,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -4895,18 +4889,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -4915,18 +4909,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -4956,9 +4950,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -4967,9 +4961,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -4978,9 +4972,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/README.md b/README.md index 0ea942b..f7bc9c8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -# Iota -Iota is the selfhost server for establishing a connection between clients. It uses its counterpart Omikron to allow a connection between clients. +# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️ diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs index a9c1d72..b189bd4 100644 --- a/src/gui/elements/console_card.rs +++ b/src/gui/elements/console_card.rs @@ -17,20 +17,30 @@ use crate::{ ui::FPS, util::borders::draw_block_joins, }, - log, log_cv, + log, log_command, log_cv, omikron::omikron_connection::OMIKRON_CONNECTION, users::{user_manager, user_profile::UserProfile}, util::file_util, }; -use std::{any::Any, time::Duration}; +use std::{ + any::Any, + sync::{Arc, Mutex}, + time::Duration, +}; +use tokio::time::Instant; pub struct ConsoleCard { focused: bool, pub title: String, pub content: String, + pub cursor_position: usize, borders: Borders, joins: Borders, + + cursor: Arc>, + last_swap: Arc>, + tab_index: usize, } impl ConsoleCard { @@ -39,10 +49,164 @@ impl ConsoleCard { focused: false, title: title.to_string(), content: content.to_string(), + cursor_position: content.chars().count(), borders: Borders::ALL, joins: Borders::NONE, + cursor: Arc::new(Mutex::new(true)), + last_swap: Arc::new(Mutex::new(Instant::now())), + tab_index: 0, } } + + fn byte_index(&self) -> usize { + self.content + .char_indices() + .nth(self.cursor_position) + .map(|(i, _)| i) + .unwrap_or(self.content.len()) + } + + fn cursor_visible(&self) -> bool { + if !self.focused { + return false; + } + + let mut visible = self.cursor.lock().unwrap(); + let mut last = self.last_swap.lock().unwrap(); + let now = Instant::now(); + + if now.duration_since(*last) >= Duration::from_millis(500) { + *visible = !*visible; + *last = now; + } + + *visible + } + + fn current_prefix(&self) -> Option<&str> { + if self.content.starts_with('/') { + Some("/") + } else { + None + } + } + + fn cursor_spans(&self) -> Vec> { + let cursor_visible = self.cursor_visible(); + let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); + let mut spans = Vec::new(); + + if self.content.is_empty() { + if self.focused { + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } else { + spans.push(Span::styled(" ", Style::default().fg(Color::White))); + } + spans.push(Span::styled( + "send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } else { + spans.push(Span::styled( + " send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } + return spans; + } + + let byte_index = self.byte_index(); + let before = self.content[..byte_index].to_string(); + let after = self.content[byte_index..].to_string(); + + let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0); + + if prefix_len > 0 && before.len() >= prefix_len { + let prefix = &before[..prefix_len]; + let rest = &before[prefix_len..]; + spans.push(Span::styled( + prefix.to_string(), + Self::style_for_part(true, false, false), + )); + if !rest.is_empty() { + spans.push(Span::styled( + rest.to_string(), + Style::default().fg(Color::White), + )); + } + } else if !before.is_empty() { + spans.push(Span::styled( + before.clone(), + Style::default().fg(Color::White), + )); + } + + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } + + if !after.is_empty() { + spans.push(Span::styled(after, Style::default().fg(Color::White))); + } + + spans + } + + fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style { + if is_error { + return Style::default().fg(Color::Red); + } + + if is_hint { + return Style::default().fg(Color::DarkGray); + } + + if is_prefix { + return Style::default().fg(Color::DarkGray); + } + + Style::default().fg(Color::White) + } + + fn render_cursor_spans(&self) -> Vec> { + self.cursor_spans() + } + + fn move_cursor_left(&mut self) { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + + fn move_cursor_right(&mut self) { + let len = self.content.chars().count(); + if self.cursor_position < len { + self.cursor_position += 1; + } + } + + fn delete_at_cursor(&mut self) { + if self.content.is_empty() || self.cursor_position == 0 { + return; + } + + let start = self + .content + .char_indices() + .nth(self.cursor_position.saturating_sub(1)) + .map(|(i, _)| i) + .unwrap_or(0); + let end = self.byte_index(); + self.content.replace_range(start..end, ""); + self.cursor_position -= 1; + } + + fn insert_at_cursor(&mut self, c: char) { + let idx = self.byte_index(); + self.content.insert(idx, c); + self.cursor_position += 1; + } } impl Element for ConsoleCard { @@ -70,7 +234,8 @@ impl Element for ConsoleCard { Style::default() }); - let par = Paragraph::new(Line::from(Span::from(self.content.clone()))) + let spans = self.render_cursor_spans(); + let par = Paragraph::new(Line::from(spans)) .block(block) .scroll((0, 0)); f.render_widget(par, r); @@ -87,11 +252,11 @@ impl JoinableElement for ConsoleCard { self } - fn as_element(&self) -> &dyn Element { + fn as_element(&self) -> &(dyn Element + 'static) { self } - fn as_element_mut(&mut self) -> &mut dyn Element { + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { self } @@ -113,11 +278,11 @@ impl InteractableElement for ConsoleCard { self } - fn as_element(&self) -> &dyn Element { + fn as_element(&self) -> &(dyn Element + 'static) { self } - fn as_element_mut(&mut self) -> &mut dyn Element { + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { self } @@ -128,26 +293,71 @@ impl InteractableElement for ConsoleCard { log!(""); return InteractionResult::Handled; } + let command = self.content.clone(); let id = Uuid::new_v4(); let id = id.to_string(); let id = id.split_at(8).0; let task_id = format!("command_{}_{}", command, id); ACTIVE_TASKS.insert(task_id.clone()); + + log_command!("{}", command); + tokio::spawn(async move { run_command(&command).await; ACTIVE_TASKS.remove(&task_id); }); - self.content = "".to_string(); + + self.content.clear(); + self.cursor_position = 0; + self.tab_index = 0; InteractionResult::Handled } KeyCode::Backspace => { - self.content.pop(); + self.delete_at_cursor(); + InteractionResult::Handled + } + KeyCode::Delete => { + let len = self.content.chars().count(); + if self.cursor_position < len { + let start = self.byte_index(); + let end = self + .content + .char_indices() + .nth(self.cursor_position + 1) + .map(|(i, _)| i) + .unwrap_or(self.content.len()); + self.content.replace_range(start..end, ""); + } + InteractionResult::Handled + } + KeyCode::Left => { + self.move_cursor_left(); + InteractionResult::Handled + } + KeyCode::Right => { + self.move_cursor_right(); + InteractionResult::Handled + } + KeyCode::Home => { + self.cursor_position = 0; + InteractionResult::Handled + } + KeyCode::End => { + self.cursor_position = self.content.chars().count(); + InteractionResult::Handled + } + KeyCode::Tab => { + if let Some(prefix) = self.current_prefix() { + if prefix == "/" { + self.tab_index = self.tab_index.saturating_add(1); + } + } InteractionResult::Handled } _ => { if let Some(c) = key.code.as_char() { - self.content.push(c); + self.insert_at_cursor(c); InteractionResult::Handled } else { InteractionResult::Unhandled @@ -168,9 +378,8 @@ impl InteractableElement for ConsoleCard { self.focused = f; } } -pub async fn run_command(command: &str) { - log!(":{}", command); +pub async fn run_command(command: &str) { let parts = command.split(" ").collect::>(); match parts.as_slice() { @@ -267,8 +476,6 @@ pub async fn run_command(command: &str) { } pub async fn ping(time: u64) { - let time = time; - let conn = OMIKRON_CONNECTION.clone(); let response_cv = conn diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs index fe125c8..e2d4ba1 100755 --- a/src/gui/elements/log_card.rs +++ b/src/gui/elements/log_card.rs @@ -1,27 +1,76 @@ -use crate::gui::elements::elements::{InteractableElement, JoinableElement}; +use crate::APP_STATE; +use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; use crate::gui::interaction_result::InteractionResult; use crate::gui::util::borders::draw_block_joins; -use crate::{APP_STATE, gui::elements::elements::Element}; +use crate::util::logger::PrintType; use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ Frame, layout::Rect, style::{Color, Style}, text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, + widgets::{Block, Borders, Paragraph}, }; use std::any::Any; +use std::time::{SystemTime, UNIX_EPOCH}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct UiLogEntry { - pub line: String, - pub color: Color, + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl UiLogEntry { + pub fn format_timestamp(&self) -> String { + let secs = (self.timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("{:02}:{:02}:{:02}", hours, minutes, seconds) + } +} + +impl From for UiLogEntry { + fn from(entry: LogEntry) -> Self { + Self { + timestamp_ms: entry.timestamp_ms, + sender: entry.sender, + message: entry.message, + is_error: entry.is_error, + } + } +} + +#[derive(Clone, Debug)] +pub struct LogEntry { + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl LogEntry { + pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { + Self { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + sender, + message, + is_error, + } + } } pub struct LogCard { focused: bool, - scroll: u16, - + selected: bool, + scroll_offset: usize, + last_total_lines: usize, + last_visible_height: usize, pub borders: Borders, pub joins: Borders, } @@ -30,12 +79,213 @@ impl LogCard { pub fn new() -> Self { Self { focused: false, - scroll: 1, + selected: false, + scroll_offset: 0, + last_total_lines: 0, + last_visible_height: 10, borders: Borders::ALL, joins: Borders::NONE, } } + + fn get_logs(&self) -> Vec { + let state = APP_STATE.lock().unwrap(); + state.get_logs().iter().cloned().collect() + } + + fn find_split_point(s: &str, max_width: usize) -> usize { + if max_width == 0 { + return s.len(); + } + + let mut current_width = 0usize; + let mut last_boundary = 0usize; + + for (idx, ch) in s.char_indices() { + let char_width = if ch.is_ascii() { 1 } else { 2 }; + if current_width + char_width > max_width { + if last_boundary == 0 { + return idx + ch.len_utf8(); + } + return last_boundary; + } + current_width += char_width; + last_boundary = idx + ch.len_utf8(); + } + + s.len() + } + + fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { + let mut result = Vec::new(); + + let timestamp = entry.format_timestamp(); + let first_prefix = "┌ "; + let default_prefix = "│ "; + let last_prefix = "└ "; + + let prefix_width = 2; + let first_line_width = available_width.saturating_sub(prefix_width + 1); + let continuation_width = available_width.saturating_sub(prefix_width); + + let segments: Vec<&str> = entry.message.split('\n').collect(); + + let mut raw_lines = Vec::new(); + + for segment in segments { + let mut remaining = segment; + if remaining.is_empty() { + raw_lines.push(String::new()); + continue; + } + + let mut is_first_part = true; + while !remaining.is_empty() { + let current_width = if is_first_part { + first_line_width + } else { + continuation_width + }; + + let split_point = Self::find_split_point(remaining, current_width); + let line_content = remaining[..split_point].to_string(); + raw_lines.push(line_content); + remaining = &remaining[split_point..]; + is_first_part = false; + } + } + + if raw_lines.is_empty() { + raw_lines.push(String::new()); + } + + for (idx, content) in raw_lines.iter().enumerate() { + let is_last = idx + 1 == raw_lines.len(); + let is_single = raw_lines.len() == 1; + let is_first = idx == 0; + let prefix = if is_first { + first_prefix + } else if is_last && !is_single { + last_prefix + } else { + default_prefix + }; + + let mut line = String::from(prefix); + line.push_str(content); + + if is_last && !timestamp.is_empty() { + line.push(' '); + line.push_str(×tamp); + } + + result.push((line, entry.sender.prefix_color(), entry.is_error)); + } + + result + } + + fn build_all_lines( + &self, + entries: Vec, + width: usize, + ) -> Vec<(String, Color, bool)> { + let mut lines = Vec::new(); + + for entry in entries { + let wrapped = Self::wrap_entry(&entry, width); + lines.extend(wrapped); + } + + lines + } + + fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) { + if total_lines <= visible_height { + return (0, total_lines); + } + + let max_offset = total_lines - visible_height; + let clamped_offset = self.scroll_offset.min(max_offset); + + let end = total_lines - clamped_offset; + let start = end.saturating_sub(visible_height); + + (start, end) + } + + fn get_title_hints(&self) -> (bool, bool) { + if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height { + return (false, false); + } + + let max_offset = self.last_total_lines - self.last_visible_height; + let can_scroll_up = self.scroll_offset < max_offset; + let can_scroll_down = self.scroll_offset > 0; + + (can_scroll_up, can_scroll_down) + } + + fn build_title(&self) -> String { + if !self.focused { + return "Logs".to_string(); + } + + let (can_up, can_down) = self.get_title_hints(); + + if !can_up && !can_down { + return "Logs".to_string(); + } + + let nav_symbol = if self.selected { "↑" } else { "j" }; + let down_symbol = if self.selected { "↓" } else { "k" }; + + match (can_up, can_down) { + (true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol), + (true, false) => format!("Logs ({} older)", nav_symbol), + (false, true) => format!("Logs ({} newer)", down_symbol), + (false, false) => "Logs".to_string(), + } + } + + fn scroll_up(&mut self) { + let max_offset = self + .last_total_lines + .saturating_sub(self.last_visible_height); + self.scroll_offset = (self.scroll_offset + 1).min(max_offset); + } + + fn scroll_down(&mut self) { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + } + + fn split_line_prefix(line: &str) -> (&str, &str) { + if let Some(rest) = line.strip_prefix("└ ") { + ("└ ", rest) + } else if let Some(rest) = line.strip_prefix("│ ") { + ("│ ", rest) + } else if let Some(rest) = line.strip_prefix("┌ ") { + ("┌ ", rest) + } else { + ("", line) + } + } + + fn split_timestamp_suffix(line: &str) -> (&str, &str) { + if let Some(idx) = line.rfind(' ') { + let possible_timestamp = &line[idx + 1..]; + if possible_timestamp.len() == 8 + && possible_timestamp.as_bytes()[2] == b':' + && possible_timestamp.as_bytes()[5] == b':' + { + let (content, timestamp_with_space) = line.split_at(idx); + return (content.trim_end(), timestamp_with_space); + } + } + (line, "") + } } + impl Element for LogCard { fn as_any(&self) -> &dyn Any { self @@ -46,22 +296,10 @@ impl Element for LogCard { } fn render(&self, f: &mut Frame, area: Rect) { - let state = APP_STATE.lock().unwrap(); - let logs = state.get_logs(); - - let lines: Vec = logs - .iter() - .map(|log| { - Line::from(Span::raw(log.line.clone())).style(Style::default().fg(log.color)) - }) - .collect(); + let entries = self.get_logs(); let block = Block::default() - .title(if self.focused { - "Logs J/K to scroll" - } else { - "Logs" - }) + .title(self.build_title()) .borders(self.borders) .border_style(if self.focused { Style::default().fg(Color::Yellow) @@ -69,22 +307,70 @@ impl Element for LogCard { Style::default() }); - let inner_height = area.height.saturating_sub(2) as usize; + let inner_area = block.inner(area); + f.render_widget(block, area); - let total_lines = lines.len(); - let base_scroll = total_lines.saturating_sub(inner_height) as u16; + if inner_area.width == 0 || inner_area.height == 0 { + draw_block_joins(f, area, self.borders, self.joins); + return; + } - let scroll = base_scroll.saturating_sub(self.scroll); + let all_lines = self.build_all_lines(entries, inner_area.width as usize); + let total_lines = all_lines.len(); + let visible_height = inner_area.height as usize; - let paragraph = Paragraph::new(lines) - .block(block) - .wrap(Wrap { trim: false }) - .scroll((scroll, 0)); + let (start, end) = self.calculate_view_window(total_lines, visible_height); + let visible_lines = &all_lines[start..end]; + + let rendered_lines: Vec = visible_lines + .iter() + .map(|(line, prefix_color, is_error)| { + let mut spans = Vec::new(); + + let (prefix, rest) = Self::split_line_prefix(line); + + if !prefix.is_empty() { + spans.push(Span::styled( + prefix.to_string(), + Style::default().fg(*prefix_color), + )); + } + + let (content, timestamp) = Self::split_timestamp_suffix(rest); + let text_color = if *is_error { Color::Red } else { Color::White }; + + if !content.is_empty() { + spans.push(Span::styled( + content.to_string(), + Style::default().fg(text_color), + )); + } + + if !timestamp.is_empty() { + spans.push(Span::styled( + timestamp.to_string(), + Style::default().fg(Color::DarkGray), + )); + } + + Line::from(spans) + }) + .collect(); + + for (idx, line) in rendered_lines.iter().enumerate() { + let line_area = Rect { + x: inner_area.x, + y: inner_area.y + idx as u16, + width: inner_area.width, + height: 1, + }; + f.render_widget(Paragraph::new(line.clone()), line_area); + } - f.render_widget(paragraph, area); draw_block_joins(f, area, self.borders, self.joins); } } + impl JoinableElement for LogCard { fn as_any(&self) -> &dyn Any { self @@ -94,12 +380,12 @@ impl JoinableElement for LogCard { self } - fn as_element(&self) -> &dyn Element { - self + fn as_element(&self) -> &(dyn Element + 'static) { + &*self } - fn as_element_mut(&mut self) -> &mut dyn Element { - self + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self } fn set_borders(&mut self, borders: Borders) { @@ -110,6 +396,7 @@ impl JoinableElement for LogCard { self.joins = joins; } } + impl InteractableElement for LogCard { fn as_any(&self) -> &dyn Any { self @@ -119,30 +406,65 @@ impl InteractableElement for LogCard { self } - fn as_element(&self) -> &dyn Element { - self + fn as_element(&self) -> &(dyn Element + 'static) { + &*self } - fn as_element_mut(&mut self) -> &mut dyn Element { - self + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self } fn interact(&mut self, key: KeyEvent) -> InteractionResult { + let entries = self.get_logs(); + let estimated_width = 80usize; + let all_lines = self.build_all_lines(entries, estimated_width); + + self.last_total_lines = all_lines.len(); + let visible_height = self.last_visible_height.max(1); + match key.code { - KeyCode::Char('J') | KeyCode::Char('j') => { - let state = APP_STATE.lock().unwrap(); - let logs = state.get_logs(); - if self.scroll < logs.len() as u16 { - self.scroll += 1; + KeyCode::Enter | KeyCode::Char(' ') => { + self.selected = !self.selected; + InteractionResult::Handled + } + KeyCode::Char('j') | KeyCode::Char('J') => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); } InteractionResult::Handled } - KeyCode::Char('K') | KeyCode::Char('k') => { - if self.scroll > 1 { - self.scroll -= 1; + KeyCode::Char('k') | KeyCode::Char('K') => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); } InteractionResult::Handled } + KeyCode::Up if self.selected => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); + } + InteractionResult::Handled + } + KeyCode::Down if self.selected => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); + } + InteractionResult::Handled + } + KeyCode::Home => { + if self.last_total_lines > visible_height { + self.scroll_offset = self.last_total_lines - visible_height; + } + InteractionResult::Handled + } + KeyCode::End => { + self.scroll_offset = 0; + InteractionResult::Handled + } _ => InteractionResult::Unhandled, } } diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs index d4e0d45..b2557c5 100644 --- a/src/gui/input_handler.rs +++ b/src/gui/input_handler.rs @@ -1,59 +1,51 @@ -use crate::ACTIVE_TASKS; use crate::gui::ui::{UI, UNIQUE}; use crate::{RELOAD, SHUTDOWN}; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; +use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; use std::sync::Arc; use std::sync::atomic::Ordering; use std::time::Duration; pub fn setup_input_handler(ui: Arc) { tokio::spawn(async move { - ACTIVE_TASKS.insert("Input Handler".to_string()); - loop { - { - let should_shutdown = *SHUTDOWN.read().await; - if should_shutdown { - break; - } + if *SHUTDOWN.read().await { + break; } - let has_event = match poll(Duration::from_millis(100)) { - Ok(true) => true, - Ok(false) => false, - Err(_) => false, - }; + let event_result = tokio::task::spawn_blocking(|| { + if let Ok(true) = poll(Duration::from_millis(100)) { + read().ok().and_then(|ev| match ev { + Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), + _ => None, + }) + } else { + None + } + }) + .await; - if has_event { - match read() { - Ok(event) => { - if let Event::Key(key_event) = event { - if key_event.kind == KeyEventKind::Press { - let uic = ui.clone(); - handle_input(key_event, uic).await; - UNIQUE.store(true, Ordering::Relaxed); - } - } - } - Err(_) => (), + match event_result { + Ok(Some(key_event)) => { + handle_input(key_event, ui.clone()).await; + UNIQUE.store(true, Ordering::Relaxed); + } + Ok(_) => {} + Err(e) => { + eprintln!("Input task error: {}", e); + tokio::time::sleep(Duration::from_millis(10)).await; } } } - { - ACTIVE_TASKS.remove("Input Handler"); - } }); } pub async fn handle_input(key: KeyEvent, ui: Arc) { match (key.code, key.modifiers) { - (KeyCode::Char('q'), KeyModifiers::CONTROL) => { + (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) + | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { *SHUTDOWN.write().await = true; } - (KeyCode::Char('c'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (KeyCode::Char('r'), KeyModifiers::CONTROL) => { + (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { *RELOAD.write().await = true; *SHUTDOWN.write().await = true; } diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 3d60510..7e92269 100755 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -1,7 +1,7 @@ use crate::users::contact::Contact; -use crate::users::user_community_util::UserCommunityUtil; use crate::util::chat_files::{MessageState, change_message_state}; use crate::util::chats_util::{get_user, mod_user}; +use crate::util::communities_util::CommunitiesUtil; use crate::util::crypto_util::{DataFormat, SecurePayload}; use crate::util::file_util::{get_children, load_file, save_file}; use crate::util::{chat_files, chats_util}; @@ -283,11 +283,16 @@ impl OmikronConnection { let key_pair = crypto_helper::generate_keypair(); let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); - let private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); + let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); let mut conf_write = CONFIG.write().await; - /*conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); - conf_write.change("private_key", DataValue::Str(private_key_base64));*/ + // NOTE: + // Intentionally not storing the generated private/public keys directly into the + // config file here to avoid persisting sensitive material in plaintext. If you + // want to persist them, uncomment the two lines below and accept the security + // implications (they will be saved by `conf_write.update()`). + // conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); + // conf_write.change("private_key", DataValue::Str(private_key_base64)); conf_write.update(); drop(conf_write); @@ -418,7 +423,7 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::identification_response) { - if let Some(accepted) = cv.get_data(DataTypes::accepted).as_bool() { + if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() { let mut state = self.state.write().await; if let ConnectionState::Connected { identified: _ } = *state { *state = ConnectionState::Connected { identified: true }; @@ -430,6 +435,7 @@ impl OmikronConnection { // ************************************************ // // Direct messages // // ************************************************ // + if cv.is_type(CommunicationType::message_state) { let sender_id = &cv.get_sender(); let receiver_id = &cv.get_receiver(); @@ -458,6 +464,160 @@ impl OmikronConnection { ); } + // Incoming stored message: store for the recipient, attempt local delivery, notify sender. + if cv.is_type(CommunicationType::message_send) { + let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse receiver_id (the storage owner for this incoming message) + let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() + { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse send_time robustly (number or string), fallback to now + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp_i64 = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + let timestamp_u128 = timestamp_i64 as u128; + + // content may be missing; default to empty string + let content = cv + .get_data(DataTypes::content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + + // persist message for the receiver (storage_owner = receiver_id) + chat_files::add_message( + timestamp_u128, + false, + receiver_id as i64, + sender_id as i64, + &content, + height, + ); + + // persist message for the sender (storage_owner = sender_id) + chat_files::add_message( + timestamp_u128, + true, + sender_id as i64, + receiver_id as i64, + &content, + height, + ); + + // send confirmation back to sender + let conf_msg = CommunicationValue::new(CommunicationType::message_send) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64); + self.send_message(&conf_msg).await; + + // Build a live-delivery message for the local client (recipient) + let user_forward = CommunicationValue::new(CommunicationType::message_live) + .with_id(cv.get_id()) + .with_receiver(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); + + // Attempt delivery and await a response from the local client + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataTypes::message_state) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + // update stored message state for receiver + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + ms.clone(), + ); + + // update stored message state for sender + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + ms.clone(), + ); + + // notify original sender about the delivered/read state + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery failed or timed out; mark as Sent + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + MessageState::Sent, + ); + + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + MessageState::Sent, + ); + + // notify sender + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + if cv.is_type(CommunicationType::message_other_iota) { let sender_id = &cv.get_sender(); let receiver_id = &cv.get_receiver(); @@ -483,26 +643,25 @@ impl OmikronConnection { .unwrap_or("") .to_string(); + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + chat_files::add_message( timestamp as u128, false, *receiver_id as i64, *sender_id as i64, &content, + height, ); + // Build user_forward using the parsed numeric timestamp and safe content string let user_forward = CommunicationValue::new(CommunicationType::message_live) .with_id(cv.get_id()) .with_receiver(*receiver_id) - .add_data( - DataTypes::send_time, - cv.get_data(DataTypes::send_time).clone(), - ) - .add_data(DataTypes::message, cv.get_data(DataTypes::content).clone()) - .add_data( - DataTypes::sender_id, - DataValue::Number(cv.get_sender() as i64), - ); + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); let user_resp = self .clone() @@ -528,10 +687,7 @@ impl OmikronConnection { .with_id(cv.get_id()) .with_receiver(*sender_id) .with_sender(*receiver_id) - .add_data( - DataTypes::send_time, - cv.get_data(DataTypes::send_time).clone(), - ) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) .add_data( DataTypes::message_state, DataValue::Str(ms.as_str().to_string()), @@ -539,15 +695,20 @@ impl OmikronConnection { ) .await; } else { + // Delivery timed out/failed — update stored state and notify sender with numeric timestamp + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + self.send_message( &CommunicationValue::new(CommunicationType::message_state) .with_id(cv.get_id()) .with_receiver(*sender_id) .with_sender(*receiver_id) - .add_data( - DataTypes::send_time, - cv.get_data(DataTypes::send_time).clone(), - ) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) .add_data( DataTypes::message_state, DataValue::Str(MessageState::Sent.as_str().to_string()), @@ -558,90 +719,21 @@ impl OmikronConnection { return; } - if cv.is_type(CommunicationType::message_send) { - let my_id = cv.get_sender(); - - // parse other id robustly (number or string) - let other_id = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; - - let now_ms_u128 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u128; - // derive an i64 timestamp for protocol fields; fall back to current time if out of range - let now_ms_i64: i64 = match i64::try_from(now_ms_u128) { - Ok(v) => v, - Err(_) => SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64, - }; - - // safe content extraction - let content = cv - .get_data(DataTypes::content) - .as_str() - .unwrap_or("") - .to_string(); - - chat_files::add_message(now_ms_u128, true, my_id as i64, other_id, &content); - - let ack = CommunicationValue::new(CommunicationType::success) - .with_id(cv.get_id()) - .with_receiver(my_id); - self.send_message(&ack).await; - - let forward = CommunicationValue::new(CommunicationType::message_other_iota) - .with_id(cv.get_id()) - .with_receiver(other_id as u64) - .add_data(DataTypes::receiver_id, DataValue::Number(other_id)) - .with_sender(my_id) - .add_data(DataTypes::send_time, DataValue::Number(now_ms_i64)) - .add_data(DataTypes::sender_id, DataValue::Number(my_id as i64)) - .add_data(DataTypes::content, DataValue::Str(content)); - if let Err(err) = self.send_message_result(&forward).await { - // sending failed - record via existing logging path - log_t!("send_message_failed", err); - } else { - // forwarding succeeded -> update stored message state to Sent - let _ = chat_files::change_message_state( - now_ms_i64, - my_id as i64, - other_id, - MessageState::Sent, - ); - } - return; - } - if cv.is_type(CommunicationType::messages_get) { let my_id = cv.get_sender(); let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); - // retrieve raw JSON messages let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); - // convert JSON array -> protocol Array of Containers (send_time, content, sender_id, message_state) let mut msg_array: Vec = Vec::new(); for m in messages.members() { - // extract fields defensively let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); let content: String = m["content"].as_str().unwrap_or("").to_string(); let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false); - // determine sender id: - // - if sent_by_self => sender is the requester (my_id) - // - otherwise prefer an explicit chat_partner_id if present on the request, - // fallback to the partner_id parameter + let height: i64 = m["height"].as_i64().unwrap_or(0); let sender_id: i64 = if sent_by_self { my_id as i64 } else { - // check for chat_partner_id in the incoming request (accept number or string) if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() { n as i64 } else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { @@ -654,9 +746,11 @@ impl OmikronConnection { let mut container = Vec::new(); container.push((DataTypes::send_time, DataValue::Number(message_time))); - container.push((DataTypes::message, DataValue::Str(content))); + container.push((DataTypes::content, DataValue::Str(content))); container.push((DataTypes::sender_id, DataValue::Number(sender_id))); container.push((DataTypes::message_state, DataValue::Str(message_state))); + container.push((DataTypes::height, DataValue::Number(height))); + container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self))); msg_array.push(DataValue::Container(container)); } @@ -724,7 +818,7 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::add_community) { - UserCommunityUtil::add_community( + CommunitiesUtil::add_community( cv.get_sender() as i64, cv.get_data(DataTypes::community_address) .as_str() @@ -747,19 +841,37 @@ impl OmikronConnection { } if cv.is_type(CommunicationType::get_communities) { + let mut comm_array = Vec::new(); + for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { + let mut container: Vec<(DataTypes, DataValue)> = Vec::new(); + if let Some(address) = c["address"].as_str() { + container.push(( + DataTypes::community_address, + DataValue::Str(address.to_string()), + )); + } + if let Some(title) = c["title"].as_str() { + container.push(( + DataTypes::community_title, + DataValue::Str(title.to_string()), + )); + } + if let Some(position) = c["position"].as_str() { + container.push((DataTypes::position, DataValue::Str(position.to_string()))); + } + comm_array.push(DataValue::Container(container)); + } + let resp = CommunicationValue::new(CommunicationType::get_communities) .with_id(cv.get_id()) .with_receiver(cv.get_sender()) - /*.add_data( - DataTypes::communities, - DataValue::Array(UserCommunityUtil::get_communities(cv.get_sender() as i64)), - ) */; + .add_data(DataTypes::communities, DataValue::Array(comm_array)); self.send_message(&resp).await; return; } if cv.is_type(CommunicationType::remove_community) { - UserCommunityUtil::remove_community( + CommunitiesUtil::remove_community( cv.get_sender() as i64, cv.get_data(DataTypes::community_address) .as_str() diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs index bfdc184..e54fe29 100644 --- a/src/util/chat_files.rs +++ b/src/util/chat_files.rs @@ -1,9 +1,9 @@ use crate::log; -use crate::util::file_util::get_directory; +use crate::util::db; use json::{JsonValue, array, object}; -use rusqlite::{Connection, params}; +use rusqlite::params; use std::io; -use std::sync::{LazyLock, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; #[derive(PartialEq, Debug, Clone)] pub enum MessageState { @@ -45,30 +45,10 @@ impl MessageState { } } -static DB_CONN: LazyLock> = LazyLock::new(|| { - let conn = Connection::open(format!("{}/messages.sqlite3", get_directory())) - .expect("Failed to open messages sqlite DB"); - conn.execute_batch( - r#" - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - external_user INTEGER NOT NULL, - message_time INTEGER NOT NULL, - content TEXT NOT NULL, - sent_by_self INTEGER NOT NULL, - message_state TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_messages_lookup - ON messages (storage_owner, external_user, message_time DESC); - "#, - ) - .expect("Failed to initialize messages DB"); - Mutex::new(conn) +// Shared DB created via helper. +// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist. +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") }); pub fn add_message( @@ -77,6 +57,7 @@ pub fn add_message( storage_owner: i64, external_user: i64, message: &str, + height: i64, ) { let message_time = match i64::try_from(send_time) { Ok(v) => v, @@ -86,40 +67,48 @@ pub fn add_message( } }; - let conn = match DB_CONN.lock() { - Ok(g) => g, - Err(e) => { - log!("Failed to lock messages DB mutex for add_message: {:?}", e); - return; - } - }; + // Insert the message into the DB + let insert_result = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO messages ( + storage_owner, + external_user, + message_time, + content, + sent_by_self, + message_state, + height + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + "#, + params![ + storage_owner, + external_user, + message_time, + message, + if storage_owner_is_sender { + 1_i64 + } else { + 0_i64 + }, + MessageState::Sending.as_str(), + height, + ], + )?; + Ok(()) + }); - if let Err(e) = conn.execute( - r#" - INSERT INTO messages ( - storage_owner, - external_user, - message_time, - content, - sent_by_self, - message_state - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6) - "#, - params![ - storage_owner, - external_user, - message_time, - message, - if storage_owner_is_sender { - 1_i64 - } else { - 0_i64 - }, - MessageState::Sending.as_str(), - ], - ) { + if let Err(e) = insert_result { log!("Failed to insert message into sqlite: {}", e); + return; } + + // Update contacts table to reflect that this conversation exists and has a recent message. + // Use the Contact helper to set last_message_at to the message timestamp. + let mut contact = crate::users::contact::Contact::new(external_user); + contact.set_last_message_at(message_time); + // This will insert or update the contact for the storage owner. + crate::util::chats_util::mod_user(storage_owner, &contact); } pub fn change_message_state( @@ -128,56 +117,58 @@ pub fn change_message_state( external_user: i64, new_state: MessageState, ) -> io::Result<()> { - let conn = DB_CONN - .lock() - .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex lock error: {:?}", e)))?; - - let current: Option = match conn.query_row( - r#" - SELECT message_state - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - AND message_time = ?3 - ORDER BY id DESC - LIMIT 1 - "#, - params![storage_owner, external_user, timestamp], - |row| row.get(0), - ) { - Ok(state) => Some(state), - Err(rusqlite::Error::QueryReturnedNoRows) => None, - Err(e) => return Err(io::Error::new(io::ErrorKind::Other, e.to_string())), - }; - - let Some(current_state_raw) = current else { - return Ok(()); - }; - - let upgraded = MessageState::from_str(¤t_state_raw) - .upgrade(new_state) - .as_str() - .to_string(); - - conn.execute( - r#" - UPDATE messages - SET message_state = ?1 - WHERE id = ( - SELECT id + // Run the SELECT and UPDATE inside with_conn to centralize connection access. + let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| { + let current: Option = match conn.query_row( + r#" + SELECT message_state FROM messages - WHERE storage_owner = ?2 - AND external_user = ?3 - AND message_time = ?4 + WHERE storage_owner = ?1 + AND external_user = ?2 + AND message_time = ?3 ORDER BY id DESC LIMIT 1 - ) - "#, - params![upgraded, storage_owner, external_user, timestamp], - ) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + "#, + params![storage_owner, external_user, timestamp], + |row| row.get(0), + ) { + Ok(state) => Some(state), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => return Err(e), + }; - Ok(()) + let Some(current_state_raw) = current else { + return Ok(()); + }; + + let upgraded = MessageState::from_str(¤t_state_raw) + .upgrade(new_state) + .as_str() + .to_string(); + + conn.execute( + r#" + UPDATE messages + SET message_state = ?1 + WHERE id = ( + SELECT id + FROM messages + WHERE storage_owner = ?2 + AND external_user = ?3 + AND message_time = ?4 + ORDER BY id DESC + LIMIT 1 + ) + "#, + params![upgraded, storage_owner, external_user, timestamp], + )?; + Ok(()) + }); + + match res { + Ok(_) => Ok(()), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), + } } pub fn get_messages( @@ -186,80 +177,73 @@ pub fn get_messages( loaded_messages: i64, amount: i64, ) -> JsonValue { - let mut messages = array![]; + let messages = array![]; if amount <= 0 || loaded_messages < 0 { return messages; } - let conn = match DB_CONN.lock() { - Ok(g) => g, - Err(e) => { - log!("Failed to lock messages DB mutex for get_messages: {:?}", e); - return messages; - } - }; + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT + message_time, + content, + sent_by_self, + message_state, + height + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + ORDER BY message_time DESC, id DESC + LIMIT ?3 OFFSET ?4 + "#, + )?; - let mut stmt = match conn.prepare( - r#" - SELECT - message_time, - content, - sent_by_self, - message_state - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - ORDER BY message_time DESC, id DESC - LIMIT ?3 OFFSET ?4 - "#, - ) { - Ok(s) => s, - Err(e) => { - log!("Failed to prepare get_messages query: {}", e); - return messages; - } - }; + let rows = stmt.query_map( + params![storage_owner, external_user, amount, loaded_messages], + |row| { + let message_time: i64 = row.get(0)?; + let content: String = row.get(1)?; + let sent_by_self: i64 = row.get(2)?; + let message_state: String = row.get(3)?; + let height: i64 = row.get(4).unwrap_or(0); + Ok((message_time, content, sent_by_self, message_state, height)) + }, + )?; - let rows = stmt.query_map( - params![storage_owner, external_user, amount, loaded_messages], - |row| { - let message_time: i64 = row.get(0)?; - let content: String = row.get(1)?; - let sent_by_self: i64 = row.get(2)?; - let message_state: String = row.get(3)?; - Ok((message_time, content, sent_by_self, message_state)) - }, - ); - - let Ok(rows) = rows else { - if let Err(e) = rows { - log!("Failed to query messages: {}", e); - } - return messages; - }; - - for row in rows { - match row { - Ok((message_time, content, sent_by_self, message_state)) => { - let msg = object! { - "message_time" => message_time, - "content" => content, - "sent_by_self" => (sent_by_self != 0), - "message_state" => message_state - }; - - if let Err(e) = messages.push(msg) { - log!("Failed to append message to output array: {}", e); + let mut out = array![]; + for row in rows { + match row { + Ok((message_time, content, sent_by_self, message_state, height)) => { + let msg = object! { + "message_time" => message_time, + "content" => content, + "sent_by_self" => (sent_by_self != 0), + "message_state" => message_state, + "height" => height + }; + if let Err(e) = out.push(msg) { + // out.push returns a JsonError; log it instead of using `?` to avoid + // incompatible error conversions inside the DB closure. + log!("Failed to append message to output array: {:?}", e); + } + } + Err(e) => { + log!("Failed to read row from sqlite: {}", e); } } - Err(e) => { - log!("Failed to read row from sqlite: {}", e); - } + } + Ok(out) + }); + + match res { + Ok(v) => v, + Err(e) => { + log!("Failed to query messages: {}", e); + messages } } - - messages } #[cfg(test)] diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs index 0677b19..b8e0ad2 100644 --- a/src/util/chats_util.rs +++ b/src/util/chats_util.rs @@ -1,69 +1,96 @@ use crate::users::contact::Contact; -use crate::util::file_util::get_directory; -use rusqlite::{Connection, params}; -use std::sync::{LazyLock, Mutex}; +use crate::util::db; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; -static DB_CONN: LazyLock> = LazyLock::new(|| { - let conn = Connection::open(format!("{}/messages.sqlite3", get_directory())) - .expect("Failed to open DB"); - conn.execute_batch( - r#" - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - - CREATE TABLE IF NOT EXISTS contacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - user_id INTEGER NOT NULL, - user_name TEXT, - last_message_at INTEGER, - UNIQUE(storage_owner, user_id) - ); - - CREATE INDEX IF NOT EXISTS idx_contacts_owner - ON contacts (storage_owner, last_message_at DESC, user_id ASC); - "#, - ) - .expect("Failed to initialize DB"); - Mutex::new(conn) +/// Shared DB connection for contacts/messages (created by db helper). +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") }); +/// Insert or update a contact for the given storage owner. pub fn mod_user(storage_owner: i64, contact: &Contact) { - let conn = DB_CONN.lock().unwrap(); - - let _ = conn.execute( - r#" - INSERT INTO contacts ( - storage_owner, - user_id, - user_name, - last_message_at - ) VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(storage_owner, user_id) DO UPDATE SET - user_name = excluded.user_name, - last_message_at = excluded.last_message_at - "#, - params![ - storage_owner, - contact.user_id, - contact.user_name.clone(), - contact.last_message_at - ], - ); + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO contacts ( + storage_owner, + user_id, + user_name, + last_message_at + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, user_id) DO UPDATE SET + user_name = excluded.user_name, + last_message_at = excluded.last_message_at + "#, + params![ + storage_owner, + contact.user_id, + contact.user_name.clone(), + contact.last_message_at + ], + )?; + Ok(()) + }) { + eprintln!("Failed to mod_user: {}", e); + } } +/// Retrieve a single contact for storage_owner/user_id. pub fn get_user(storage_owner: i64, user_id: i64) -> Option { - let conn = DB_CONN.lock().unwrap(); + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + match conn.query_row( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 AND user_id = ?2 + LIMIT 1 + "#, + params![storage_owner, user_id], + |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + }, + ) { + Ok(c) => Ok(Some(c)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + }); - let row = conn.query_row( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 AND user_id = ?2 - LIMIT 1 - "#, - params![storage_owner, user_id], - |r| { + match res { + Ok(opt) => opt, + Err(e) => { + eprintln!("Error querying user in get_user: {}", e); + None + } + } +} + +/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc. +pub fn get_users(storage_owner: i64) -> Vec { + let contacts_out = Vec::new(); + + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 + ORDER BY + CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, + last_message_at DESC, + user_id ASC + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { let user_id: i64 = r.get(0)?; let user_name: Option = r.get(1)?; let last_message_at: Option = r.get(2)?; @@ -72,64 +99,23 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option { user_name, last_message_at, }) - }, - ); + })?; - match row { - Ok(contact) => Some(contact), - Err(rusqlite::Error::QueryReturnedNoRows) => None, + let mut out = Vec::new(); + for row in rows { + match row { + Ok(contact) => out.push(contact), + Err(e) => eprintln!("Failed to read contact row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(v) => v, Err(e) => { - eprintln!("Error querying user in get_user: {}", e); - None + eprintln!("Failed to query contacts in get_users: {}", e); + contacts_out } } } - -pub fn get_users(storage_owner: i64) -> Vec { - let mut contacts_out = Vec::new(); - - let conn = DB_CONN.lock().unwrap(); - - let mut stmt = match conn.prepare( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 - ORDER BY - CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, - last_message_at DESC, - user_id ASC - "#, - ) { - Ok(s) => s, - Err(e) => { - eprintln!("Failed to prepare statement in get_users: {}", e); - return contacts_out; - } - }; - - let rows = match stmt.query_map(params![storage_owner], |r| { - let user_id: i64 = r.get(0)?; - let user_name: Option = r.get(1)?; - let last_message_at: Option = r.get(2)?; - Ok(Contact { - user_id, - user_name, - last_message_at, - }) - }) { - Ok(r) => r, - Err(e) => { - eprintln!("Failed to query map in get_users: {}", e); - return contacts_out; - } - }; - - for row in rows { - if let Ok(contact) = row { - contacts_out.push(contact); - } - } - - contacts_out -} diff --git a/src/util/communities_util.rs b/src/util/communities_util.rs new file mode 100644 index 0000000..ed8b7ae --- /dev/null +++ b/src/util/communities_util.rs @@ -0,0 +1,90 @@ +use crate::util::db; +use json::Array; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; + +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +pub struct CommunitiesUtil; + +impl CommunitiesUtil { + pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO communities ( + storage_owner, + address, + title, + position + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, address) DO UPDATE SET + title = excluded.title, + position = excluded.position + "#, + params![storage_owner, address, title, position], + )?; + Ok(()) + }) { + eprintln!("Failed to add_community: {}", e); + } + } + + pub fn remove_community(storage_owner: i64, community_address: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", + params![storage_owner, community_address], + )?; + Ok(()) + }) { + eprintln!("Failed to remove_community: {}", e); + } + } + + pub fn get_communities(storage_owner: i64) -> Array { + let communities_out = Array::new(); + + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT address, title, position + FROM communities + WHERE storage_owner = ?1 + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { + let address: String = r.get(0)?; + let title: String = r.get(1)?; + let position: String = r.get(2)?; + Ok((address, title, position)) + })?; + + let mut out = Array::new(); + for row in rows { + match row { + Ok((address, title, position)) => { + let mut community = json::JsonValue::new_object(); + community["title"] = json::JsonValue::String(title); + community["address"] = json::JsonValue::String(address); + community["position"] = json::JsonValue::String(position); + out.push(community); + } + Err(e) => eprintln!("Failed to read community row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(arr) => arr, + Err(e) => { + eprintln!("Failed to query communities in get_communities: {}", e); + communities_out + } + } + } +} diff --git a/src/util/db.rs b/src/util/db.rs new file mode 100644 index 0000000..9fb40a0 --- /dev/null +++ b/src/util/db.rs @@ -0,0 +1,185 @@ +//! Database helper utilities. +//! +//! This module provides small helpers to open/init sqlite databases and to +//! create a shared (Arc>) connection wrapper callers can +//! reuse. The goal is to centralize the "open and initialize" logic and +//! provide small convenience helpers used by other util modules. + +use crate::util::file_util::get_directory; +use rusqlite::{Connection, Error as RusqliteError}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Returns the file path for a named DB inside the application's data directory. +/// +/// Arguments: +/// - `db_name` : name of the DB (without extension). Example: `"messages"`. +pub fn db_file_path(db_name: &str) -> String { + let mut p = PathBuf::from(get_directory()); + p.push(format!("{db_name}.sqlite3")); + p.to_string_lossy().to_string() +} + +/// Open a sqlite connection to the named DB file (no initialization). +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +pub fn open_connection(db_name: &str) -> Result { + let path = db_file_path(db_name); + Connection::open(path) +} + +/// Open a connection and immediately run `init_sql` via `execute_batch`. +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple). +pub fn open_and_init(db_name: &str, init_sql: &str) -> Result { + let conn = open_connection(db_name)?; + conn.execute_batch(init_sql)?; + Ok(conn) +} + +/// Create a shared, Arc> initialized with the given SQL. +/// +/// This is a convenience wrapper that returns an owned Arc> +/// so caller modules can store it in a `static` or pass it around. +/// +/// Arguments: +/// - `db_name`: DB name (without extension). +/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements). +pub fn create_shared_connection( + db_name: &str, + init_sql: &str, +) -> Result>, String> { + match open_and_init(db_name, init_sql) { + Ok(conn) => { + // Configure some sensible defaults for concurrency + // Attempt to set a busy timeout to reduce SQLITE_BUSY failures. + let _ = conn.busy_timeout(Duration::from_millis(250)); + Ok(Arc::new(Mutex::new(conn))) + } + Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)), + } +} + +/// Acquire the Connection from an Arc> and run the provided +/// closure. Converts rusqlite::Error into a String on error. +/// +/// Arguments: +/// - `shared`: Arc> +/// - `f`: closure that receives &Connection and returns Result +/// +/// Returns Ok(T) or Err(String). +pub fn with_conn(shared: &Arc>, f: F) -> Result +where + F: FnOnce(&Connection) -> Result, +{ + // When invoked from within an async runtime (such as Tokio), taking a blocking + // std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent + // awaits. Detect whether we're running inside a Tokio runtime and, if so, + // execute the blocking lock + database closure using Tokio's blocking helper. + // + // The blocking section returns Result so we can propagate errors + // in the same form as before. + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(|| { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + }) + } else { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + } +} + +/// Initialize a general-purpose messages+contacts DB and return a shared +/// connection. This helper creates a single DB file that can contain multiple +/// tables (messages, contacts, ...). The SQL here is conservative and intended +/// to be safe if called multiple times. +/// +/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)` +/// directly, but this convenience is useful for code that expects both tables. +pub fn create_general_messages_db() -> Result>, String> { + // Keep PRAGMA and schema in one multi-statement string so callers only + // need to call a single execute_batch. + const INIT_SQL: &str = r#" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + external_user INTEGER NOT NULL, + message_time INTEGER NOT NULL, + content TEXT NOT NULL, + sent_by_self INTEGER NOT NULL, + message_state TEXT NOT NULL, + height INTEGER NOT NULL DEFAULT 0 + ); + + CREATE INDEX IF NOT EXISTS idx_messages_lookup + ON messages (storage_owner, external_user, message_time DESC); + + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + user_id INTEGER NOT NULL, + user_name TEXT, + last_message_at INTEGER, + UNIQUE(storage_owner, user_id) + ); + + CREATE INDEX IF NOT EXISTS idx_contacts_owner + ON contacts (storage_owner, last_message_at DESC, user_id ASC); + + CREATE TABLE IF NOT EXISTS communities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + address TEXT NOT NULL, + title TEXT NOT NULL, + position TEXT NOT NULL, + UNIQUE(storage_owner, address) + ); + + CREATE INDEX IF NOT EXISTS idx_communities_owner + ON communities (storage_owner); + "#; + + match create_shared_connection("messages", INIT_SQL) { + Ok(shared_conn) => { + // Attempt to add the height column for backwards compatibility. + // This will fail if the column already exists, which is expected. + let _ = with_conn(&shared_conn, |conn| { + let _ = conn.execute( + "ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0", + [], + ); + Ok(()) + }); + Ok(shared_conn) + } + Err(e) => Err(e), + } +} + +/* +Example usage: + +// In some util module (at init time, e.g. lazy_static or LazyLock) +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + create_general_messages_db().expect("failed to create messages DB") +}); + +// Later, to run a query: +let res: Result, String> = with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare("SELECT ...")?; + let rows = stmt.query_map(...)?; + // collect and return Ok(...) +}); +*/ diff --git a/src/util/logger.rs b/src/util/logger.rs index 2c09c33..a5feb02 100755 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -13,13 +13,13 @@ use ttp_core::{CommunicationValue, DataTypes, DataValue}; use crate::{ APP_STATE, - gui::{elements::log_card::UiLogEntry, ui::UNIQUE}, + gui::{elements::log_card::LogEntry, ui::UNIQUE}, langu::language_manager, }; static LOGGER: OnceLock> = OnceLock::new(); -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(unused)] pub enum PrintType { Call, @@ -28,6 +28,20 @@ pub enum PrintType { Omikron, Omega, General, + Command, +} +impl PrintType { + pub fn prefix_color(self) -> Color { + match self { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } + } } struct LogMessage { @@ -35,10 +49,8 @@ struct LogMessage { prefix: String, kind: PrintType, is_error: bool, - translation_key: Option, format_args: Vec, - message: Option, } @@ -70,27 +82,49 @@ pub fn startup() { msg.message.unwrap_or_default() }; - let ts = fixed_box(&msg.timestamp_ms.to_string(), 13); + let timestamp = format_timestamp_inline(msg.timestamp_ms); + let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); - let line = format!("{} {}", msg.prefix, resolved_message); - - let _ = writeln!(file, "{} {}", ts, line); - - let color = colorize(msg.kind, msg.is_error); - - let ui_entry = UiLogEntry { - line: line.clone(), - color, + let prefix = if msg.prefix.is_empty() { + String::new() + } else { + format!("{} ", msg.prefix) }; - { - let mut state = APP_STATE.lock().unwrap(); - state.push_log(ui_entry); - } + let _ = writeln!( + file, + "{} {}{}", + fixed_box(&msg.timestamp_ms.to_string(), 13), + prefix, + entry.message + ); + + let _ = writeln!(file, " {}", timestamp); + + let mut state = APP_STATE.lock().unwrap(); + state.push_log(entry.into()); } }); } +fn format_timestamp_inline(timestamp_ms: u128) -> String { + let secs = (timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) +} + +fn fixed_box(content: &str, width: usize) -> String { + let s: String = content.chars().take(width).collect(); + let len = s.chars().count(); + if len < width { + format!("[{}{}]", " ".repeat(width - len), s) + } else { + s + } +} + fn colorize(kind: PrintType, is_error: bool) -> Color { if is_error { return Color::Red; @@ -102,17 +136,8 @@ fn colorize(kind: PrintType, is_error: bool) -> Color { PrintType::Iota => Color::Yellow, PrintType::Omikron => Color::Blue, PrintType::Omega => Color::Cyan, - PrintType::General => Color::White, - } -} - -fn fixed_box(content: &str, width: usize) -> String { - let s: String = content.chars().take(width).collect(); - let len = s.chars().count(); - if len < width { - format!("[{}{}]", " ".repeat(width - len), s) - } else { - s + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, } } @@ -180,6 +205,7 @@ macro_rules! log_t { ) }; } + #[macro_export] macro_rules! log_t_err { ($key:expr) => { @@ -203,13 +229,32 @@ macro_rules! log_t_err { }; } +/// Log a command message. +#[macro_export] +macro_rules! log_command { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::Command, + "".to_string(), + false, + format!($($arg)*) + ) + }; +} + /// Log a general informational message. #[macro_export] macro_rules! log { ($($arg:tt)*) => { - $crate::util::logger::log_internal($crate::util::logger::PrintType::General, "".to_string(), false, format!($($arg)*)) + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + format!($($arg)*) + ) }; } + /// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { @@ -222,6 +267,7 @@ macro_rules! log_in { ) }; } + /// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { @@ -234,6 +280,7 @@ macro_rules! log_out { ) }; } + /// Log an error message (`>>`). #[macro_export] macro_rules! log_err { @@ -373,6 +420,7 @@ macro_rules! log_cv_in { $crate::util::logger::log_cv_internal("> ", &$cv, None) }; } + #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => { diff --git a/src/util/mod.rs b/src/util/mod.rs index 8a1382b..47901f9 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,7 +1,9 @@ pub mod chat_files; pub mod chats_util; +pub mod communities_util; pub mod config_util; pub mod crypto_helper; pub mod crypto_util; +pub mod db; pub mod file_util; pub mod logger; From b9c04475dd4e47cf622f7fde6b525fa00581facc Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:21:13 +0200 Subject: [PATCH 2/6] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7bc9c8..cd711b2 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️ +# Iota From 32d44eb4253ef6244feff25e5011266c27bf9678 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:25:20 +0200 Subject: [PATCH 3/6] Sync --- .cargo_toml_backup | 76 - .github/workflows/build.yml | 23 - .gitignore | 21 +- Cargo.lock | 3083 ++++++----------- Cargo.toml | 61 +- README.md | 2 +- dockerfile | 20 - .../anonymous_client_connection.rs | 519 +++ src/anonymous_clients/anonymous_manager.rs | 53 + src/anonymous_clients/mod.rs | 2 + src/auth/auth_user.rs | 18 - src/auth/local_auth.rs | 18 - src/auth/mod.rs | 2 - src/calls/call_group.rs | 106 + src/calls/call_manager.rs | 97 + src/calls/call_util.rs | 151 + src/calls/caller.rs | 49 + src/calls/mod.rs | 4 + src/communities/community.rs | 356 -- src/communities/community_connection.rs | 396 --- src/communities/community_manager.rs | 59 - src/communities/interactables/category.rs | 121 - src/communities/interactables/interactable.rs | 35 - src/communities/interactables/registry.rs | 75 - src/communities/interactables/text_chat.rs | 261 -- src/communities/interactables/voice_chat.rs | 187 - src/communities/mod.rs | 13 - src/communities/perms/permission.rs | 27 - src/data/mod.rs | 1 + src/data/user.rs | 30 + src/gui/app_state.rs | 201 -- src/gui/elements/console_card.rs | 491 --- src/gui/elements/elements.rs | 49 - src/gui/elements/graph_card.rs | 215 -- src/gui/elements/log_card.rs | 483 --- src/gui/input_handler.rs | 56 - src/gui/interaction_result.rs | 49 - src/gui/mod.rs | 20 - src/gui/screens/main_screen.rs | 242 -- src/gui/screens/md_viewer.rs | 493 --- src/gui/screens/screens.rs | 25 - src/gui/screens/terms_checker.rs | 351 -- src/gui/screens/terms_updater.rs | 691 ---- src/gui/tui.rs | 0 src/gui/ui.rs | 167 - src/gui/util/borders.rs | 63 - src/langu/language_creator.rs | 80 - src/langu/language_manager.rs | 105 - src/langu/mod.rs | 2 - src/main.rs | 214 +- src/omega/mod.rs | 1 + src/omega/omega_connection.rs | 741 ++++ src/omega/ping_pong_task.rs | 41 + src/omikron/mod.rs | 2 - src/omikron/omikron_connection.rs | 1159 ------- src/omikron/ping_pong_task.rs | 38 - src/rho/client_connection.rs | 583 ++++ src/rho/connection.rs | 358 ++ src/rho/iota_connection.rs | 461 +++ src/rho/mod.rs | 6 + src/rho/rho_connection.rs | 199 ++ src/rho/rho_manager.rs | 83 + src/rho/server.rs | 25 + src/server/api.rs | 190 - src/server/mod.rs | 3 - src/server/server.rs | 169 - src/server/web_path_parser.rs | 77 - src/terms/buttons.rs | 175 - src/terms/consent_state.rs | 491 --- src/terms/doc.rs | 73 - src/terms/focus.rs | 25 - src/terms/mod.rs | 5 - src/terms/terms_getter.rs | 105 - src/users/contact.rs | 61 - src/users/mod.rs | 4 - src/users/user_community_util.rs | 67 - src/users/user_manager.rs | 197 -- src/users/user_profile.rs | 126 - src/util/chat_files.rs | 276 -- src/util/chats_util.rs | 121 - src/util/communities_util.rs | 90 - src/util/config_util.rs | 60 - src/util/crypto_helper.rs | 282 +- src/util/db.rs | 185 - src/util/file_util.rs | 227 +- src/util/logger.rs | 242 +- src/util/mod.rs | 5 - static/web/404.html | 4 - 88 files changed, 4781 insertions(+), 12039 deletions(-) delete mode 100644 .cargo_toml_backup delete mode 100644 .github/workflows/build.yml delete mode 100644 dockerfile create mode 100644 src/anonymous_clients/anonymous_client_connection.rs create mode 100644 src/anonymous_clients/anonymous_manager.rs create mode 100644 src/anonymous_clients/mod.rs delete mode 100644 src/auth/auth_user.rs delete mode 100644 src/auth/local_auth.rs delete mode 100644 src/auth/mod.rs create mode 100644 src/calls/call_group.rs create mode 100644 src/calls/call_manager.rs create mode 100644 src/calls/call_util.rs create mode 100644 src/calls/caller.rs create mode 100644 src/calls/mod.rs delete mode 100644 src/communities/community.rs delete mode 100644 src/communities/community_connection.rs delete mode 100644 src/communities/community_manager.rs delete mode 100644 src/communities/interactables/category.rs delete mode 100644 src/communities/interactables/interactable.rs delete mode 100644 src/communities/interactables/registry.rs delete mode 100644 src/communities/interactables/text_chat.rs delete mode 100644 src/communities/interactables/voice_chat.rs delete mode 100644 src/communities/mod.rs delete mode 100644 src/communities/perms/permission.rs create mode 100644 src/data/mod.rs create mode 100644 src/data/user.rs delete mode 100755 src/gui/app_state.rs delete mode 100644 src/gui/elements/console_card.rs delete mode 100644 src/gui/elements/elements.rs delete mode 100644 src/gui/elements/graph_card.rs delete mode 100755 src/gui/elements/log_card.rs delete mode 100644 src/gui/input_handler.rs delete mode 100644 src/gui/interaction_result.rs delete mode 100644 src/gui/mod.rs delete mode 100644 src/gui/screens/main_screen.rs delete mode 100644 src/gui/screens/md_viewer.rs delete mode 100644 src/gui/screens/screens.rs delete mode 100644 src/gui/screens/terms_checker.rs delete mode 100644 src/gui/screens/terms_updater.rs delete mode 100644 src/gui/tui.rs delete mode 100644 src/gui/ui.rs delete mode 100644 src/gui/util/borders.rs delete mode 100644 src/langu/language_creator.rs delete mode 100644 src/langu/language_manager.rs delete mode 100644 src/langu/mod.rs create mode 100644 src/omega/mod.rs create mode 100644 src/omega/omega_connection.rs create mode 100644 src/omega/ping_pong_task.rs delete mode 100644 src/omikron/mod.rs delete mode 100755 src/omikron/omikron_connection.rs delete mode 100644 src/omikron/ping_pong_task.rs create mode 100644 src/rho/client_connection.rs create mode 100755 src/rho/connection.rs create mode 100755 src/rho/iota_connection.rs create mode 100644 src/rho/mod.rs create mode 100644 src/rho/rho_connection.rs create mode 100644 src/rho/rho_manager.rs create mode 100644 src/rho/server.rs delete mode 100755 src/server/api.rs delete mode 100644 src/server/mod.rs delete mode 100644 src/server/server.rs delete mode 100755 src/server/web_path_parser.rs delete mode 100644 src/terms/buttons.rs delete mode 100644 src/terms/consent_state.rs delete mode 100644 src/terms/doc.rs delete mode 100644 src/terms/focus.rs delete mode 100644 src/terms/mod.rs delete mode 100755 src/terms/terms_getter.rs delete mode 100644 src/users/contact.rs delete mode 100644 src/users/mod.rs delete mode 100644 src/users/user_community_util.rs delete mode 100644 src/users/user_manager.rs delete mode 100644 src/users/user_profile.rs delete mode 100644 src/util/chat_files.rs delete mode 100644 src/util/chats_util.rs delete mode 100644 src/util/communities_util.rs delete mode 100644 src/util/config_util.rs mode change 100644 => 100755 src/util/crypto_helper.rs delete mode 100644 src/util/db.rs mode change 100755 => 100644 src/util/file_util.rs mode change 100755 => 100644 src/util/logger.rs delete mode 100644 static/web/404.html diff --git a/.cargo_toml_backup b/.cargo_toml_backup deleted file mode 100644 index f7a9016..0000000 --- a/.cargo_toml_backup +++ /dev/null @@ -1,76 +0,0 @@ -[package] -name = "iota" -version = "0.1.0" -edition = "2024" - -[dependencies] -actix = "0.13.5" -actix-rt = "2.11.0" -actix-web = { version = "4.12.1", features = ["rustls-0_23"] } -actix-web-actors = "4.3.1" -aes-gcm = "*" -axum = { version = "0.8.8", features = ["ws", "http2"] } -base64 = "0.22.1" -bytes = "*" -cmake = "*" -color-eyre = "*" -crossterm = "*" -der = "*" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "*" -hmac = "*" -hyper-util = { version = "*" } -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -http-body-util = "*" -json = "*" -native-tls = { version = "*", default-features = false } -once_cell = "1.21.3" -pkcs8 = { version = "*", features = ["alloc"] } -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -reactive-rs = "*" -aws-lc-rs = "*" -reqwest = "0.12.23" -rustls = { version = "*", features = ["aws-lc-rs"] } -serde = { version = "1.0.219", features = ["derive"] } -sha2 = "*" -sys-info = "*" -sysinfo = "0.30" -tokio = { version = "1.48.0", features = ["full"] } -tokio-util = { version = "*", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tower = "*" -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -x509 = "*" -tokio-rustls = "0.26.4" -rustls-pemfile = "2.2.0" -async-trait = "0.1.89" -sha1 = "0.10.6" -async-tungstenite = "0.32.0" -zip = "6.0.0" -tui-textarea = "0.7.0" -tui-input = "0.14.0" -pnet = "0.35.0" -dashmap = "6.1.0" -strum = "0.27.2" -strum_macros = "0.27.2" -ratatui = "0.30.0" -ratatui_input = "0.1.3" -open = "5.3.3" -chrono = "0.4.43" -axum-server = { version = "0.8.0", features = ["tls-rustls"] } -serde_json = "1.0.149" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 4265c56..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Build & Publish Docker Image - -on: - workflow_dispatch: - -jobs: - build: - name: Build & Publish Docker Image - runs-on: ubuntu-latest - steps: - - name: Set up repository - uses: actions/checkout@v4 - - - name: Login - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PASSWD }} - - - name: Build & Push - run: | - docker build -t tensamin/iota:latest . - docker push tensamin/iota:latest diff --git a/.gitignore b/.gitignore index 0000aa1..1baef92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,19 @@ -target -logs +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb +*.env +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/W + + +# Added by cargo +/target +/logs diff --git a/Cargo.lock b/Cargo.lock index cd56dfb..41cef35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,256 +3,35 @@ version = 4 [[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +name = "Omikron" +version = "0.1.0" dependencies = [ - "actix-rt", - "bitflags 2.11.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", + "aes-gcm", + "ansi_term", + "base64 0.22.1", + "dashmap", + "dotenv", + "futures", + "hex", + "hkdf", + "livekit-api", + "livekit-protocol", "log", "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", + "rand 0.8.5", + "rand_core 0.6.4", + "rustls", + "sha2", + "strum", + "strum_macros", + "thiserror 2.0.18", "tokio", - "tokio-util", + "ttp-core", + "ttp-native", + "uuid", + "x448", ] -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64", - "bitflags 2.11.0", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.1.5", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash 0.1.5", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.3", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.3.1+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.5.2" @@ -297,27 +76,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -327,21 +85,21 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "asn1-rs" version = "0.7.1" @@ -366,7 +124,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -378,27 +136,147 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] -name = "async-trait" -version = "0.1.89" +name = "async-channel" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", ] [[package]] -name = "atomic" -version = "0.6.1" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ - "bytemuck", + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-native-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" +dependencies = [ + "futures-util", + "native-tls", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-tungstenite" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +dependencies = [ + "async-native-tls", + "async-std", + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "tungstenite 0.21.0", ] [[package]] @@ -436,6 +314,18 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -443,25 +333,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bit-set" -version = "0.5.3" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -479,24 +354,25 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "8.0.2" +name = "block2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", + "objc2", ] [[package]] -name = "brotli-decompressor" -version = "5.0.0" +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] @@ -505,12 +381,6 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - [[package]] name = "byteorder" version = "1.5.0" @@ -523,33 +393,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.58" @@ -562,12 +405,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -586,11 +423,7 @@ version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "iana-time-zone", - "js-sys", "num-traits", - "wasm-bindgen", - "windows-link", ] [[package]] @@ -613,64 +446,19 @@ dependencies = [ ] [[package]] -name = "combine" -version = "4.6.7" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "bytes", - "memchr", + "crossbeam-utils", ] [[package]] -name = "compact_str" -version = "0.9.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -697,39 +485,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -737,30 +492,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crossterm" -version = "0.29.0" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] @@ -774,16 +514,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - [[package]] name = "ctr" version = "0.9.2" @@ -794,37 +524,30 @@ dependencies = [ ] [[package]] -name = "darling" -version = "0.23.0" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "darling_core", - "darling_macro", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "darling_core" -version = "0.23.0" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -848,16 +571,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] -name = "deflate64" -version = "0.1.12" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] [[package]] name = "der-parser" @@ -882,40 +604,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - [[package]] name = "digest" version = "0.10.7" @@ -923,10 +611,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -935,17 +634,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] -name = "document-features" -version = "0.2.12" +name = "dotenv" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dunce" @@ -953,13 +649,51 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "ed448-goldilocks" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" dependencies = [ - "fiat-crypto", + "fiat-crypto 0.1.20", "hex", "subtle", ] @@ -971,12 +705,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "cfg-if", + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] @@ -996,34 +742,30 @@ dependencies = [ ] [[package]] -name = "euclid" -version = "0.22.14" +name = "event-listener" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "num-traits", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "bit-set", - "regex", + "event-listener 5.4.1", + "pin-project-lite", ] [[package]] @@ -1032,6 +774,16 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.1.20" @@ -1039,15 +791,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" [[package]] -name = "filedescriptor" -version = "0.8.3" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" @@ -1055,29 +802,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", - "zlib-rs", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1090,12 +820,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "foreign-types" version = "0.3.2" @@ -1174,6 +898,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -1182,7 +919,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -1222,6 +959,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1275,47 +1013,26 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.3" +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ - "bytes", - "fnv", + "futures-channel", "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "h2" -version = "0.4.13" +name = "group" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -1330,7 +1047,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash 0.1.5", + "foldhash", ] [[package]] @@ -1338,44 +1055,12 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] [[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "headers" +name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http 1.4.0", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http 1.4.0", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "heck" @@ -1383,6 +1068,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1463,12 +1154,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" version = "1.9.0" @@ -1479,11 +1164,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.13", "http 1.4.0", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1492,18 +1175,18 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.27.7" +name = "hyper-tls" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "http 1.4.0", + "bytes", + "http-body-util", "hyper", "hyper-util", - "rustls", - "rustls-pki-types", + "native-tls", "tokio", - "tokio-rustls", + "tokio-native-tls", "tower-service", ] @@ -1513,7 +1196,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1525,35 +1208,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.3", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", ] [[package]] @@ -1644,12 +1301,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -1671,12 +1322,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "2.13.0" @@ -1689,15 +1334,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inout" version = "0.1.4" @@ -1707,81 +1343,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "iota" -version = "0.1.0" -dependencies = [ - "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64", - "chrono", - "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf", - "hyper", - "hyper-util", - "json", - "lazy_static", - "once_cell", - "open", - "pnet", - "rand 0.8.5", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", - "serde_json", - "sha2", - "strum 0.27.2", - "strum_macros 0.27.2", - "sysinfo", - "tokio", - "tokio-tungstenite", - "ttp-core", - "ttp-native", - "tungstenite", - "uuid", - "walkdir", - "warp", - "x448", - "zip", -] - [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "ipnetwork" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - [[package]] name = "iri-string" version = "0.7.12" @@ -1793,29 +1360,19 @@ dependencies = [ ] [[package]] -name = "is-docker" -version = "0.2.0" +name = "itertools" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", + "either", ] [[package]] name = "itertools" -version = "0.14.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1826,50 +1383,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - [[package]] name = "jobserver" version = "0.1.34" @@ -1893,39 +1406,43 @@ dependencies = [ ] [[package]] -name = "json" -version = "0.12.4" +name = "jsonwebtoken" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", ] [[package]] -name = "lab" -version = "0.11.0" +name = "kv-log-macro" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1933,12 +1450,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - [[package]] name = "libc" version = "0.2.184" @@ -1946,23 +1457,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] -name = "libsqlite3-sys" -version = "0.37.0" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.11.0", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" @@ -1977,27 +1475,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] -name = "litrs" -version = "1.0.0" +name = "livekit-api" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +checksum = "020c7eaab7cec16b69a058659f9acff32067e0e2d4f81e80639f3ffa29464b09" dependencies = [ - "futures-core", - "futures-sink", - "local-waker", + "async-tungstenite", + "base64 0.21.7", + "http 1.4.0", + "jsonwebtoken", + "livekit-protocol", + "log", + "os_info", + "parking_lot", + "pbjson-types", + "prost", + "rand 0.9.2", + "reqwest", + "scopeguard", + "serde", + "serde_json", + "sha2", + "thiserror 1.0.69", + "tokio-tungstenite", + "url", ] [[package]] -name = "local-waker" -version = "0.1.4" +name = "livekit-protocol" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" +checksum = "c21d56b4dc2598b1296ca891abb225bf1c05ea007d6f43bc4fd48c31be64bf59" +dependencies = [ + "futures-util", + "livekit-runtime", + "parking_lot", + "pbjson", + "pbjson-types", + "prost", + "serde", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "livekit-runtime" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" +dependencies = [ + "tokio", + "tokio-stream", +] [[package]] name = "lock_api" @@ -2013,14 +1542,8 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.16.1", + "value-bag", ] [[package]] @@ -2029,79 +1552,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" -dependencies = [ - "crc", - "sha2", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix", - "winapi", -] - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "1.2.0" @@ -2109,11 +1571,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "native-tls" version = "0.2.18" @@ -2133,23 +1600,16 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", - "memoffset", ] -[[package]] -name = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" - [[package]] name = "nom" version = "7.1.3" @@ -2160,15 +1620,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -2179,23 +1630,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -2205,6 +1661,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2212,15 +1679,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] -name = "num_threads" -version = "0.1.7" +name = "objc2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ - "libc", + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", ] [[package]] @@ -2229,17 +1718,127 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.0", + "bitflags", + "dispatch2", + "objc2", ] [[package]] -name = "objc2-io-kit" +name = "objc2-core-graphics" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "libc", + "bitflags", + "dispatch2", + "objc2", "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", ] [[package]] @@ -2269,24 +1868,13 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - [[package]] name = "openssl" version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.11.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -2303,7 +1891,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -2325,14 +1913,51 @@ dependencies = [ ] [[package]] -name = "ordered-float" -version = "4.6.0" +name = "os_info" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" dependencies = [ - "num-traits", + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2357,19 +1982,40 @@ dependencies = [ ] [[package]] -name = "pathdiff" -version = "0.2.3" +name = "pbjson" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" +dependencies = [ + "base64 0.21.7", + "serde", +] [[package]] -name = "pbkdf2" -version = "0.12.2" +name = "pbjson-build" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" dependencies = [ - "digest", - "hmac", + "heck 0.4.1", + "itertools 0.11.0", + "prost", + "prost-types", +] + +[[package]] +name = "pbjson-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" +dependencies = [ + "bytes", + "chrono", + "pbjson", + "pbjson-build", + "prost", + "prost-build", + "serde", ] [[package]] @@ -2378,10 +2024,19 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64", + "base64 0.22.1", "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2389,118 +2044,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "pest" -version = "2.8.6" +name = "petgraph" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "fixedbitset", + "indexmap", ] [[package]] @@ -2509,6 +2059,44 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2516,94 +2104,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "pnet" -version = "0.35.0" +name = "polling" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "ipnetwork", - "pnet_base", - "pnet_datalink", - "pnet_packet", - "pnet_sys", - "pnet_transport", -] - -[[package]] -name = "pnet_base" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" -dependencies = [ - "no-std-net", -] - -[[package]] -name = "pnet_datalink" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" -dependencies = [ - "ipnetwork", - "libc", - "pnet_base", - "pnet_sys", - "winapi", -] - -[[package]] -name = "pnet_macros" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.117", -] - -[[package]] -name = "pnet_macros_support" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - -[[package]] -name = "pnet_sys" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "pnet_transport" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" -dependencies = [ - "libc", - "pnet_base", - "pnet_packet", - "pnet_sys", + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2618,12 +2129,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "potential_utf" version = "0.1.5" @@ -2639,12 +2144,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2661,7 +2160,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", ] [[package]] @@ -2673,6 +2181,59 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools 0.12.1", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2815,91 +2376,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools", - "kasuari", - "lru", - "strum 0.27.2", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools", - "line-clipping", - "ratatui-core", - "strum 0.27.2", - "time", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "rcgen" version = "0.14.7" @@ -2919,7 +2395,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.0", + "bitflags", ] [[package]] @@ -2945,12 +2421,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "regex-syntax" version = "0.8.10" @@ -2959,33 +2429,31 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", - "h2 0.4.13", "http 1.4.0", "http-body", "http-body-util", "hyper", - "hyper-rustls", + "hyper-tls", "hyper-util", "js-sys", "log", - "mime", + "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", "rustls-pki-types", - "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-native-tls", "tower", "tower-http", "tower-service", @@ -2995,6 +2463,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -3010,28 +2488,23 @@ dependencies = [ ] [[package]] -name = "rsqlite-vfs" -version = "0.1.0" +name = "rsa" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - -[[package]] -name = "rusqlite" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" -dependencies = [ - "bitflags 2.11.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -3064,7 +2537,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -3099,15 +2572,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -3118,33 +2582,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.103.10" @@ -3169,15 +2606,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" version = "0.1.29" @@ -3187,26 +2615,34 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", + "bitflags", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3255,7 +2691,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3311,27 +2747,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -3343,16 +2758,14 @@ dependencies = [ ] [[package]] -name = "simd-adler32" -version = "0.3.9" +name = "signature" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] [[package]] name = "slab" @@ -3387,15 +2800,19 @@ dependencies = [ ] [[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", + "base64ct", + "der", ] [[package]] @@ -3404,55 +2821,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "strum_macros" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3461,17 +2845,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.117" @@ -3500,42 +2873,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", + "syn", ] [[package]] @@ -3551,69 +2889,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom", - "phf", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.11.0", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -3640,7 +2915,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3651,7 +2926,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3662,9 +2937,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde_core", "time-core", @@ -3737,7 +3010,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3751,40 +3024,28 @@ dependencies = [ ] [[package]] -name = "tokio-rustls" -version = "0.26.4" +name = "tokio-stream" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ - "rustls", + "futures-core", + "pin-project-lite", "tokio", ] [[package]] name = "tokio-tungstenite" -version = "0.29.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", "native-tls", "tokio", "tokio-native-tls", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", + "tungstenite 0.20.1", ] [[package]] @@ -3808,7 +3069,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.0", + "bitflags", "bytes", "futures-util", "http 1.4.0", @@ -3838,7 +3099,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3852,7 +3112,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -3875,11 +3135,11 @@ name = "ttp-core" version = "0.1.0" source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" dependencies = [ - "base64", + "base64 0.22.1", "byteorder", "rand 0.8.5", - "strum 0.28.0", - "strum_macros 0.28.0", + "strum", + "strum_macros", ] [[package]] @@ -3898,19 +3158,42 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.29.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", "bytes", "data-encoding", "http 1.4.0", "httparse", "log", "native-tls", - "rand 0.9.2", + "rand 0.8.5", "sha1", - "thiserror 2.0.18", + "thiserror 1.0.69", + "url", + "utf-8", ] [[package]] @@ -3919,47 +3202,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "unicode-xid" version = "0.2.6" @@ -4000,30 +3248,35 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "atomic", "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4036,25 +3289,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -4064,33 +3298,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" -dependencies = [ - "bytes", - "futures-util", - "headers", - "http 1.4.0", - "http-body", - "http-body-util", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -4157,7 +3364,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wasm-bindgen-shared", ] @@ -4198,7 +3405,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -4224,87 +3431,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - [[package]] name = "winapi" version = "0.3.9" @@ -4321,142 +3447,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4484,21 +3486,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4532,21 +3519,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -4559,12 +3531,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4577,12 +3543,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4607,12 +3567,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4625,12 +3579,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4643,12 +3591,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4661,12 +3603,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4695,7 +3631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "wit-parser", ] @@ -4706,10 +3642,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck", + "heck 0.5.0", "indexmap", "prettyplease", - "syn 2.0.117", + "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4725,7 +3661,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4737,7 +3673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags", "indexmap", "log", "serde", @@ -4883,7 +3819,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -4904,7 +3840,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] [[package]] @@ -4924,7 +3860,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", "synstructure", ] @@ -4933,20 +3869,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] [[package]] name = "zerotrie" @@ -4978,84 +3900,11 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn", ] -[[package]] -name = "zip" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.3.4", - "hmac", - "indexmap", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1", - "time", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 8767b56..3127b12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "iota" +name = "Omikron" version = "0.1.0" edition = "2024" @@ -7,50 +7,31 @@ edition = "2024" ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" } ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" } -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" +ansi_term = "*" +uuid = { version = "*", features = ["v4"] } + base64 = "0.22.1" -crossterm = "*" +dashmap = "*" futures = "*" -futures-util = "*" hex = "*" -hkdf = "0.12.4" -hyper-util = { version = "*" } -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -json = "*" once_cell = "1.21.3" rand = "0.8" rand_core = { version = "0.6", features = ["getrandom", "std"] } -reqwest = "0.13.2" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } +rustls = { version = "0.23.37", default-features = false, features = [ + "std", + "tls12", + "aws-lc-rs", + "prefer-post-quantum", +] } +aes-gcm = "0.10.3" sha2 = "0.10.9" -sysinfo = "0.38.3" -tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" +tokio = { version = "*", features = ["full"] } x448 = { version = "*" } -rustls-pemfile = "2.2.0" -async-trait = "0.1.89" -zip = "6.0.0" -pnet = "0.35.0" -dashmap = "6.1.0" -strum = "0.27.2" -strum_macros = "0.27.2" -ratatui = "0.30.0" -open = "5.3.3" -chrono = "0.4.43" -serde_json = "1.0.149" -rusqlite = "0.39.0" -lazy_static = "1.5.0" +log = "0.4" +dotenv = "0.15.0" +hkdf = "0.12.4" +strum = "0.28.0" +strum_macros = "0.28.0" +livekit-api = { version = "0.4.14", features = ["native-tls"] } +livekit-protocol = "0.7.1" +thiserror = "2.0.18" diff --git a/README.md b/README.md index cd711b2..06c1cdb 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# Iota +# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/omikron) ⚠️ diff --git a/dockerfile b/dockerfile deleted file mode 100644 index ee00add..0000000 --- a/dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build stage -FROM rust:latest AS builder - -WORKDIR /app -COPY . . - -RUN cargo build --release - -# Runtime stage -FROM debian:sid - -WORKDIR /app - -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/iota . - -EXPOSE 1984 - -CMD ["./iota"] \ No newline at end of file diff --git a/src/anonymous_clients/anonymous_client_connection.rs b/src/anonymous_clients/anonymous_client_connection.rs new file mode 100644 index 0000000..8c57490 --- /dev/null +++ b/src/anonymous_clients/anonymous_client_connection.rs @@ -0,0 +1,519 @@ +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::RwLock; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use ttp_native::{Receiver, Sender}; +use uuid::Uuid; + +use crate::anonymous_clients::anonymous_manager::{self, generate_username}; +use crate::calls::call_manager; +use crate::omega::omega_connection::get_omega_connection; +use crate::rho::connection::GeneralConnection; +use crate::rho::rho_manager; +use crate::util::logger::PrintType; +use crate::{log_cv_in, log_cv_out, log_out}; + +pub struct AnonymousClientConnection { + user_id: u64, + + pub sender: Arc, + pub receiver: Arc, + pub ping: Arc>, + pub interested_users: Arc>>, + is_open: Arc>, + pub user_name: Arc>, + pub display_name: Arc>, + pub avatar: Arc>, +} + +impl AnonymousClientConnection { + pub async fn from_general(general: Arc, user_id: u64) -> Arc { + let username: String = generate_username(); + Arc::new(Self { + user_id: user_id, + + ping: Arc::new(RwLock::new(0)), + interested_users: Arc::new(RwLock::new(Vec::new())), + is_open: Arc::new(RwLock::new(true)), + sender: general.sender.clone(), + receiver: general.receiver.clone(), + user_name: Arc::new(RwLock::new(username.to_lowercase())), + display_name: Arc::new(RwLock::new(username)), + avatar: Arc::new(RwLock::new(String::new())), + }) + } + pub fn start(self: Arc) { + let self_clone = self.clone(); + tokio::spawn(async move { + while let Ok(cv) = self_clone.receiver.receive().await { + self_clone.clone().handle_message(cv).await; + } + self_clone.handle_close().await; + }); + } + + /// Get the user ID + pub fn get_user_id(&self) -> u64 { + self.user_id + } + + /// Get the user name + pub async fn get_user_name(&self) -> String { + self.user_name.read().await.clone() + } + + /// Get the display name + pub async fn get_display_name(&self) -> String { + self.display_name.read().await.clone() + } + + pub async fn set_display_name(&self, display: String) { + *self.display_name.write().await = display; + } + + /// Get the avatar + pub async fn get_avatar(&self) -> String { + self.avatar.read().await.clone() + } + + /// Send a CommunicationValue to the client + pub async fn send_message(self: Arc, cv: &CommunicationValue) { + if !*self.is_open.read().await { + log_out!( + self.user_id as i64, + PrintType::Client, + "Attempted to send message to a closed connection." + ); + return; + } + if !cv.is_type(CommunicationType::pong) { + log_cv_out!(PrintType::Client, &cv); + } + if let Err(e) = self.sender.send(&cv).await { + log_out!( + self.user_id as i64, + PrintType::Client, + "Send failed: {:?}", + e + ); + } + } + + /// Handle incoming message from client + pub async fn handle_message(self: Arc, cv: CommunicationValue) { + tokio::spawn(async move { + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + log_cv_in!(PrintType::Client, &cv); + + if cv.is_type(CommunicationType::identification) { + let call_id = + Uuid::parse_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")) + .unwrap_or(Uuid::new_v4()); + + let call = if let Some(call) = call_manager::get_call(call_id).await { + if call.is_anonymous().await { + call + } else { + self.send_error_response( + &cv.get_id(), + CommunicationType::error_not_authenticated, + ) + .await; + return; + } + } else { + self.send_error_response( + &cv.get_id(), + CommunicationType::error_not_authenticated, + ) + .await; + return; + }; + + let mut invited = Vec::new(); + for call_invitee in call.members.read().await.clone() { + let call_invitee_cv = get_omega_connection() + .await_response( + &CommunicationValue::new(CommunicationType::get_user_data).add_data( + DataTypes::user_id, + DataValue::Number(call_invitee.user_id as i64), + ), + Some(Duration::from_secs(2)), + ) + .await + .unwrap(); + let mut json_invitee = Vec::new(); + let _ = json_invitee.push(( + DataTypes::user_id, + call_invitee_cv.get_data(DataTypes::user_id).clone(), + )); + let _ = json_invitee.push(( + DataTypes::username, + call_invitee_cv.get_data(DataTypes::username).clone(), + )); + let _ = json_invitee.push(( + DataTypes::display, + call_invitee_cv.get_data(DataTypes::display).clone(), + )); + let _ = json_invitee.push(( + DataTypes::avatar, + call_invitee_cv.get_data(DataTypes::avatar).clone(), + )); + + let _ = invited.push(DataValue::Container(json_invitee)); + } + + let token = call.create_anonymous_token(self.get_user_id()).await; + + let mut serialized = Vec::new(); + let _ = serialized.push((DataTypes::call_id, DataValue::Str(call_id.to_string()))); + let _ = + serialized.push((DataTypes::call_invited, DataValue::Array(invited.clone()))); + let _ = serialized.push((DataTypes::call_members, DataValue::Array(invited))); + let _ = serialized.push((DataTypes::call_token, DataValue::Str(token.unwrap()))); + self.clone() + .send_message( + &&CommunicationValue::new(CommunicationType::identification_response) + .with_id(cv.get_id()) + .add_data(DataTypes::user_id, DataValue::Number(self.user_id as i64)) + .add_data( + DataTypes::username, + DataValue::Str(self.clone().get_user_name().await), + ) + .add_data( + DataTypes::display, + DataValue::Str(self.get_display_name().await), + ) + .add_data(DataTypes::avatar, DataValue::Str(self.get_avatar().await)) + .add_data(DataTypes::call_state, DataValue::Container(serialized)), + ) + .await; + } + + // Handle ping + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + // Handle client status changes + if cv.is_type(CommunicationType::client_changed) { + self.handle_client_changed(cv).await; + return; + } + + // Handle call invites + if cv.is_type(CommunicationType::call_invite) { + self.handle_call_invite(cv).await; + return; + } + + // Handle get call requests + if cv.is_type(CommunicationType::call_token) { + self.handle_get_call(cv).await; + return; + } + + if cv.is_type(CommunicationType::call_disconnect_user) { + self.handle_call_disconnect_user(cv).await; + return; + } + + if cv.is_type(CommunicationType::call_timeout_user) { + self.handle_call_timeout_user(cv).await; + return; + } + + if cv.is_type(CommunicationType::change_user_data) { + if let Some(display_name) = cv.get_data(DataTypes::display).as_str() { + let _ = self.set_display_name(display_name.to_string()).await; + } + + return; + } + + if cv.is_type(CommunicationType::get_user_data) { + if let Some(anonymous) = { + if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() { + anonymous_manager::get_anonymous_user(user_id as u64).await + } else if let Some(username) = cv.get_data(DataTypes::username).as_str() { + anonymous_manager::get_anonymous_user_by_name(username.to_string()).await + } else { + None + } + } { + let response = CommunicationValue::new(CommunicationType::get_user_data) + .with_id(cv.get_id()) + .add_data( + DataTypes::username, + DataValue::Str(anonymous.get_user_name().await), + ) + .add_data( + DataTypes::user_id, + DataValue::Number(anonymous.user_id as i64), + ) + .add_data( + DataTypes::display, + DataValue::Str(anonymous.get_display_name().await), + ) + .add_data(DataTypes::user_state, DataValue::Str("online".to_string())) + .add_data( + DataTypes::avatar, + DataValue::Str(anonymous.get_avatar().await), + ); + + self.send_message(&response).await; + + return; + } + } + + if cv.is_type(CommunicationType::get_user_data) + || cv.is_type(CommunicationType::get_iota_data) + || cv.is_type(CommunicationType::delete_user) + { + self.handle_omega_forward(cv).await; + return; + } + }); + } + async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { + let client_for_closure = self.clone(); + tokio::spawn(async move { + let response_cv = get_omega_connection() + .await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20))) + .await; + if let Ok(response_cv) = response_cv { + client_for_closure.send_message(&response_cv).await; + } + }); + } + + /// Handle ping message + async fn handle_ping(self: Arc, cv: CommunicationValue) { + // Update our ping if provided + if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { + if let Ok(ping_val) = last_ping.to_string().parse::() { + let mut ping_guard = self.ping.write().await; + *ping_guard = ping_val; + } + } + + // Send pong response + let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); + + self.send_message(&response).await; + } + + /// Handle client status change + async fn handle_client_changed(self: Arc, _cv: CommunicationValue) { + /*let user_id = self.get_user_id().await; + if let Some(_status_str) = cv.get_data(DataTypes::user_state) { + let user_status = UserStatus::online; + }*/ + } + + /// Handle call invite + async fn handle_call_invite(self: Arc, cv: CommunicationValue) { + let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0); + if receiver_id == 0 { + self.send_error_response(&cv.get_id(), CommunicationType::error_no_user_id) + .await; + return; + } + + let call_id = match cv.get_data(DataTypes::call_id) { + DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) { + Ok(id) => id, + Err(_) => { + self.send_error_response( + &cv.get_id(), + CommunicationType::error_invalid_call_id, + ) + .await; + return; + } + }, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error_no_call_id) + .await; + return; + } + }; + + let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await; + if !invited { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_call_id) + .await; + return; + } + + // Find target RhoConnection + let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await { + Some(rho) => rho, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + // Get sender user ID + let sender_id = self.get_user_id(); + + // Create and send call distribution message + let forward = CommunicationValue::new(CommunicationType::call_invite) + .with_receiver(receiver_id as u64) + .with_sender(sender_id) + .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) + .add_data( + DataTypes::receiver_id, + DataValue::Str(receiver_id.to_string()), + ) + .add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string())); + + target_rho.message_to_client(forward).await; + + let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id()); + self.send_message(&response).await; + } + + /// Handle get call request + async fn handle_get_call(self: Arc, cv: CommunicationValue) { + let user_id = self.get_user_id(); + + let call_id = match cv.get_data(DataTypes::call_id) { + DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) { + Ok(id) => id, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + if let Some(token) = call_manager::get_call_token(user_id, call_id).await { + let response = CommunicationValue::new(CommunicationType::call_token) + .with_id(cv.get_id()) + .with_receiver(user_id) + .add_data(DataTypes::call_token, DataValue::Str(token.to_string())); + self.send_message(&response).await; + } else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + } + async fn handle_call_timeout_user(self: Arc, cv: CommunicationValue) { + let call_id = + Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); + let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0); + + let call = call_manager::get_call(call_id).await; + if let Some(call) = call { + if call + .get_caller(self.get_user_id()) + .await + .unwrap() + .has_admin() + { + call.get_caller(user_id as u64) + .await + .unwrap() + .set_timeout(untill) + .await; + } + } + } + async fn handle_call_disconnect_user(self: Arc, cv: CommunicationValue) { + let call_id = + Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); + let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + + let call = call_manager::get_call(call_id).await; + if let Some(call) = call { + if call + .get_caller(self.get_user_id()) + .await + .unwrap() + .has_admin() + { + call.remove_caller(user_id as u64).await; + } + } + } + + /// Send error response + async fn send_error_response(self: Arc, message_id: &u32, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(*message_id); + self.send_message(&error).await; + } + /// Close the connection + pub async fn close(&self) { + let mut is_open_guard = self.is_open.write().await; + if !*is_open_guard { + return; + } + *is_open_guard = false; + + let _ = self.sender.close(); + } + + #[allow(dead_code)] + /// Set interested users list + pub async fn set_interested_users(self: Arc, interested_ids: Vec) { + let mut interested_guard = self.interested_users.write().await; + *interested_guard = interested_ids; + } + #[allow(dead_code)] + pub async fn get_interested_users(self: Arc) -> Vec { + let interested_guard = self.interested_users.read().await; + interested_guard.clone() + } + + #[allow(dead_code)] + /// Check if interested in a user and send notification + pub async fn are_you_interested(self: Arc, user_id: i64) { + let interested_guard = self.clone().get_interested_users().await; + if interested_guard.contains(&user_id) { + let notification = CommunicationValue::new(CommunicationType::client_changed) + .add_data(DataTypes::user_id, DataValue::Str(user_id.to_string())) + .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); + + self.send_message(¬ification).await; + } + } + + /// Handle connection close + pub async fn handle_close(&self) { + + // TODO delete temp user + } +} + +// Implement Clone to make it easier to work with Arc +impl Clone for AnonymousClientConnection { + fn clone(&self) -> Self { + Self { + sender: Arc::clone(&self.sender), + receiver: Arc::clone(&self.receiver), + user_id: self.user_id, + ping: Arc::clone(&self.ping), + interested_users: Arc::clone(&self.interested_users), + is_open: Arc::clone(&self.is_open), + user_name: Arc::clone(&self.user_name), + display_name: Arc::clone(&self.display_name), + avatar: Arc::clone(&self.avatar), + } + } +} diff --git a/src/anonymous_clients/anonymous_manager.rs b/src/anonymous_clients/anonymous_manager.rs new file mode 100644 index 0000000..edfefde --- /dev/null +++ b/src/anonymous_clients/anonymous_manager.rs @@ -0,0 +1,53 @@ +use dashmap::DashMap; +use once_cell::sync::Lazy; +use rand::Rng; +use rand::seq::SliceRandom; +use std::sync::Arc; + +use crate::anonymous_clients::anonymous_client_connection::AnonymousClientConnection; + +static ANONYMOUS_USERS: Lazy>> = + Lazy::new(|| DashMap::new()); + +#[allow(dead_code)] +pub async fn add_anonymous_user(connection: Arc) { + ANONYMOUS_USERS.insert(connection.get_user_id(), connection); +} + +#[allow(dead_code)] +pub async fn remove_anonymous_user(user_id: u64) { + ANONYMOUS_USERS.remove(&user_id); +} + +pub async fn get_anonymous_user(user_id: u64) -> Option> { + ANONYMOUS_USERS.get(&user_id).map(|c| c.clone()) +} + +pub async fn get_anonymous_user_by_name( + username: String, +) -> Option> { + for user_conn in ANONYMOUS_USERS + .iter() + .map(|ref_multi| ref_multi.value().clone()) + { + if user_conn.get_user_name().await == username { + return Some(user_conn); + } + } + + return None; +} + +// TODO: implement check if taken +pub fn generate_username() -> String { + let adjectives = ["Swift", "Clever", "Brave", "Sneaky", "Fierce"]; + let nouns = ["Tiger", "Eagle", "Shark", "Wolf", "Dragon"]; + + let mut rng = rand::thread_rng(); + let adj = adjectives.choose(&mut rng).unwrap(); + let noun = nouns.choose(&mut rng).unwrap(); + + let number: u16 = rng.gen_range(0..10000); + + format!("{}{}{}", adj, noun, number) +} diff --git a/src/anonymous_clients/mod.rs b/src/anonymous_clients/mod.rs new file mode 100644 index 0000000..3edd0b4 --- /dev/null +++ b/src/anonymous_clients/mod.rs @@ -0,0 +1,2 @@ +pub mod anonymous_client_connection; +pub mod anonymous_manager; diff --git a/src/auth/auth_user.rs b/src/auth/auth_user.rs deleted file mode 100644 index 8f763b6..0000000 --- a/src/auth/auth_user.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct AuthUser { - pub id: i64, - pub username: String, - pub password_hash: String, - pub public_key: String, -} - -impl AuthUser { - pub fn new(id: i64, username: String, password_hash: String, public_key: String) -> Self { - AuthUser { - id, - username, - password_hash, - public_key, - } - } -} diff --git a/src/auth/local_auth.rs b/src/auth/local_auth.rs deleted file mode 100644 index 112ee2b..0000000 --- a/src/auth/local_auth.rs +++ /dev/null @@ -1,18 +0,0 @@ -use json::JsonValue; - -use crate::util::file_util::load_file; -// NOT USED AT MOMENT -pub fn is_private_key_valid(user_id: &i64, key_hash: &str) -> bool { - let file_contents = load_file("", "users.json"); - - let users = json::parse(&file_contents).unwrap(); - if let JsonValue::Array(users_array) = users { - for user in users_array { - if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash { - return true; - } - } - } - - false -} diff --git a/src/auth/mod.rs b/src/auth/mod.rs deleted file mode 100644 index 73d6442..0000000 --- a/src/auth/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod auth_user; -pub mod local_auth; diff --git a/src/calls/call_group.rs b/src/calls/call_group.rs new file mode 100644 index 0000000..91e85cf --- /dev/null +++ b/src/calls/call_group.rs @@ -0,0 +1,106 @@ +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; + +use std::{env, sync::Arc, time::Duration}; +use tokio::sync::RwLock; +use uuid::Uuid; + +use crate::{ + calls::{call_util, caller::Caller}, + omega::omega_connection::get_omega_connection, +}; + +pub struct CallGroup { + pub call_id: Uuid, + pub members: RwLock>>, + pub show: RwLock, + pub anonymous_joining: RwLock, + pub short_link: RwLock>, +} + +impl CallGroup { + pub fn new(call_id: Uuid, user: Arc) -> Self { + CallGroup { + call_id, + members: RwLock::new(vec![user]), + show: RwLock::new(true), + anonymous_joining: RwLock::new(false), + short_link: RwLock::new(None), + } + } + + pub async fn get_caller(&self, user_id: u64) -> Option> { + self.members + .read() + .await + .iter() + .find(|caller| caller.user_id == user_id) + .cloned() + } + + pub async fn is_anonymous(&self) -> bool { + *self.anonymous_joining.read().await + } + + pub async fn set_anonymous_joining(&self, enable: bool) { + *self.anonymous_joining.write().await = enable; + + let _ = call_util::set_room_metadata( + self.call_id, + format!("{{\"anonymous_joining\": \"{}\"}}", enable), + ) + .await; + + if self.short_link.read().await.is_none() { + let long_link = format!( + "https://app.tensamin.net/call/anonymous?call_id={}&omikron_id={}", + self.call_id, + env::var("ID") + .unwrap_or("0".to_string()) + .parse::() + .unwrap_or(0), + ); + let response_cv = get_omega_connection() + .await_response( + &CommunicationValue::new(CommunicationType::shorten_link) + .add_data(DataTypes::link, DataValue::Str(long_link)), + Some(Duration::from_secs(20)), + ) + .await; + if let Ok(response) = response_cv { + *self.short_link.write().await = Some( + response + .get_data(DataTypes::link) + .as_str() + .unwrap() + .to_string(), + ); + log::info!( + "Shortened link for call {} is {}", + self.call_id, + self.short_link.read().await.as_ref().unwrap() + ); + } + } + } + + pub async fn create_anonymous_token(&self, user_id: u64) -> Option { + if self.is_anonymous().await { + if let Ok(token) = call_util::create_token(user_id, self.call_id, false) { + return Some(token); + } + } + None + } + + pub async fn remove_caller(&self, user_id: u64) { + let _ = call_util::remove_participant(self.call_id, user_id).await; + self.members + .write() + .await + .retain(|caller| caller.user_id != user_id); + } + + pub async fn get_short_link(self: Arc) -> Option { + self.short_link.read().await.clone() + } +} diff --git a/src/calls/call_manager.rs b/src/calls/call_manager.rs new file mode 100644 index 0000000..a17f347 --- /dev/null +++ b/src/calls/call_manager.rs @@ -0,0 +1,97 @@ +use dashmap::DashMap; +use once_cell::sync::Lazy; +use std::sync::Arc; +use uuid::Uuid; + +use crate::calls::{call_group::CallGroup, caller::Caller}; + +pub static CALL_GROUPS: Lazy>> = Lazy::new(|| DashMap::new()); +#[allow(dead_code)] +pub async fn get_call_invites(user_id: u64) -> Vec> { + let mut callers = Vec::new(); + for (_, cg) in CALL_GROUPS.clone().into_iter() { + let members = cg.members.read().await; + for member in members.iter() { + if member.user_id == user_id { + callers.push(member.clone()); + } + } + } + callers +} + +pub async fn get_call(call_id: Uuid) -> Option> { + if let Some(b) = CALL_GROUPS.get(&call_id) { + Some(b.clone()) + } else { + None + } +} + +pub async fn get_call_groups(user_id: u64) -> Vec> { + let mut call_groups = Vec::new(); + for (_, cg) in CALL_GROUPS.clone().into_iter() { + let is_member = { + let members = cg.members.read().await; + members.iter().any(|m| m.user_id == user_id) + }; + + if is_member { + call_groups.push(cg.clone()); + } + } + call_groups +} + +pub async fn get_call_token(user_id: u64, call_id: Uuid) -> Option { + if let Some(cg) = CALL_GROUPS.get(&call_id) { + let mut members = cg.members.write().await; + + if let Some(member) = members.iter().find(|m| m.user_id == user_id) { + return Some(member.create_token()); + } + + let new_caller = Arc::new(Caller::new(user_id, call_id, false)); + let token = new_caller.create_token(); + + members.push(new_caller); + + return Some(token); + } + + if let Some(cg) = CALL_GROUPS.get(&call_id) { + let cg_clone = cg.clone(); + + let mut members = cg_clone.members.write().await; + if let Some(member) = members.iter().find(|m| m.user_id == user_id) { + return Some(member.create_token()); + } + let new_caller = Arc::new(Caller::new(user_id, call_id, false)); + let token = new_caller.create_token(); + members.push(new_caller); + return Some(token); + } + + let caller = Arc::new(Caller::new(user_id, call_id, true)); + let call_group = CallGroup::new(call_id, caller.clone()); + + CALL_GROUPS.insert(call_id, Arc::new(call_group)); + + Some(caller.create_token()) +} + +pub async fn add_invite(call_id: Uuid, inviter_id: u64, invitee_id: u64) -> bool { + if let Some(cg) = CALL_GROUPS.get(&call_id) { + let mut members = cg.members.write().await; + + let is_inviter_member = members.iter().any(|m| m.user_id == inviter_id); + + if is_inviter_member { + if !members.iter().any(|m| m.user_id == invitee_id) { + members.push(Arc::new(Caller::new(invitee_id, call_id, false))); + } + return true; + } + } + false +} diff --git a/src/calls/call_util.rs b/src/calls/call_util.rs new file mode 100644 index 0000000..57df005 --- /dev/null +++ b/src/calls/call_util.rs @@ -0,0 +1,151 @@ +use livekit_api::{ + access_token::{self}, + services::room::RoomClient, +}; +use livekit_protocol::Room; +use std::env; +use std::str::FromStr; +use std::time::Duration; +use uuid::Uuid; + +use crate::{calls::call_manager::CALL_GROUPS, log, log_err, util::logger::PrintType}; + +pub fn get_livekit() -> Result<(String, String, String), ()> { + let hostname = match env::var("LIVEKI_HOSTNAME") { + Ok(secret) => secret, + Err(_) => { + log_err!(0, PrintType::General, "LIVEKI_HOSTNAME not set!"); + return Err(()); + } + }; + let api_key = match env::var("LIVEKIT_API_KEY") { + Ok(key) => key, + Err(_) => { + log_err!(0, PrintType::General, "LIVEKIT_API_KEY not set!"); + return Err(()); + } + }; + let api_secret = match env::var("LIVEKIT_API_SECRET") { + Ok(secret) => secret, + Err(_) => { + log_err!(0, PrintType::General, "LIVEKIT_API_SECRET not set!"); + return Err(()); + } + }; + Ok((hostname, api_key, api_secret)) +} + +pub fn create_token(user_id: u64, call_id: Uuid, has_admin: bool) -> Result { + let (_, api_key, api_secret) = get_livekit()?; + + let token = access_token::AccessToken::with_api_key(&api_key, &api_secret) + .with_identity(&user_id.to_string()) + .with_grants(access_token::VideoGrants { + room_join: true, + room_admin: has_admin, + room: call_id.to_string(), + ..Default::default() + }) + .with_metadata(&format!("{{\"isAdmin\":{}}}", has_admin)) + .to_jwt(); + if let Ok(token) = token { + Ok(token) + } else { + Err(()) + } +} +#[allow(dead_code)] +pub async fn get_room(call_id: Uuid) -> Result<(RoomClient, Room), ()> { + if let Ok((hostname, api_key, api_secret)) = get_livekit() { + let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); + let rooms = room_service.list_rooms(Vec::new()).await; + if let Ok(rooms) = rooms { + for room in rooms { + if room.name == call_id.to_string() { + return Ok((room_service, room)); + } + } + } + } + return Err(()); +} + +pub async fn remove_participant(call_id: Uuid, user_id: u64) -> Result<(), ()> { + if let Ok((hostname, api_key, api_secret)) = get_livekit() { + let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); + if let Ok(_) = room_service + .remove_participant(&call_id.to_string(), &user_id.to_string()) + .await + { + return Ok(()); + } + } + return Err(()); +} + +#[allow(dead_code)] +pub async fn get_room_metadata(call_id: Uuid) -> Result { + if let Ok((_, room)) = get_room(call_id).await { + Ok(room.metadata) + } else { + Err(()) + } +} + +pub async fn set_room_metadata(call_id: Uuid, metadata: String) -> Result<(), ()> { + if let Ok((hostname, api_key, api_secret)) = get_livekit() { + let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); + if let Ok(_) = room_service + .update_room_metadata(&call_id.to_string(), &metadata) + .await + { + return Ok(()); + } + } + return Err(()); +} + +pub fn garbage_collect_calls() { + tokio::spawn(async move { + loop { + if let Ok((hostname, api_key, api_secret)) = get_livekit() { + let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); + clean_calls(room_service).await; + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + }); +} +pub async fn clean_calls(room_service: RoomClient) { + let rooms = room_service.list_rooms(Vec::new()).await.unwrap(); + let mut call_ids: Vec = Vec::new(); + let mut no_users: Vec = Vec::new(); + for room in rooms { + if let Ok(id) = Uuid::from_str(&room.name) { + if room.num_participants == 0 { + no_users.push(id); + } + call_ids.push(id); + } + } + let size_pre = CALL_GROUPS.len(); + for (id, _) in CALL_GROUPS.clone().into_iter() { + if !call_ids.contains(&id) { + CALL_GROUPS.remove(&id); + } + } + for (_, cg) in CALL_GROUPS.clone().into_iter() { + *cg.show.write().await = !no_users.contains(&cg.call_id); + } + + let size_post = CALL_GROUPS.len(); + if size_pre - size_post != 0 { + log!( + 0, + PrintType::Call, + "Cleaned {} calls, {} remaining", + size_pre - size_post, + size_post + ); + } +} diff --git a/src/calls/caller.rs b/src/calls/caller.rs new file mode 100644 index 0000000..74e0c3b --- /dev/null +++ b/src/calls/caller.rs @@ -0,0 +1,49 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use tokio::sync::RwLock; +use uuid::Uuid; + +use crate::calls::call_util; + +pub struct Caller { + pub user_id: u64, + pub call_id: Uuid, + pub has_admin: bool, + pub timeout: RwLock, +} + +impl Caller { + pub fn new(user_id: u64, call_id: Uuid, has_admin: bool) -> Self { + Caller { + user_id, + call_id, + has_admin, + timeout: RwLock::new(0), + } + } + #[allow(dead_code)] + pub fn set_admin(&mut self, has_admin: bool) { + self.has_admin = has_admin; + } + pub fn has_admin(&self) -> bool { + self.has_admin + } + #[allow(dead_code)] + pub async fn is_timeouted(&self) -> bool { + *self.timeout.read().await + > SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64 + } + pub async fn set_timeout(&self, timeout: i64) { + *self.timeout.write().await = timeout; + } + pub fn create_token(&self) -> String { + if let Ok(token) = call_util::create_token(self.user_id, self.call_id, self.has_admin()) { + token + } else { + String::new() + } + } +} diff --git a/src/calls/mod.rs b/src/calls/mod.rs new file mode 100644 index 0000000..8413354 --- /dev/null +++ b/src/calls/mod.rs @@ -0,0 +1,4 @@ +pub mod call_group; +pub mod call_manager; +pub mod call_util; +pub mod caller; diff --git a/src/communities/community.rs b/src/communities/community.rs deleted file mode 100644 index 835c88a..0000000 --- a/src/communities/community.rs +++ /dev/null @@ -1,356 +0,0 @@ -use crate::communities::interactables::category::Category; -use crate::communities::interactables::registry; -use crate::communities::perms::permission::Permission; -use crate::communities::{ - community_connection::CommunityConnection, interactables::interactable::Interactable, -}; -use crate::util::file_util; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use json::JsonValue; -use json::number::Number; -use json::object::Object; -use rand::RngCore; -use rand_core::OsRng; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use x448::{PublicKey, Secret}; -/// Permissions -// uuid -> interactable/path/like/this/interactable_name:permission -// -> role - -/// Roles -// rolename -> interactable/path/like/this/interactable_name:permission -// -> other/path/like/this/interactable_name:permission - -pub struct Community { - name: String, - owner_id: Arc>, - members: Vec, - permissions: HashMap>, - roles: HashMap>, - private_key: Secret, - public_key: PublicKey, - pub interactables: Arc>>>>, - pub connections: Arc>>>>, -} - -impl Community { - pub fn new() -> Self { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - Community { - name: String::new(), - owner_id: Arc::new(RwLock::new(0)), - members: Vec::new(), - permissions: HashMap::new(), - roles: HashMap::new(), - private_key, - public_key, - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - } - } - pub async fn create(name: String, owner_id: i64) -> Self { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - let c = Community { - name, - owner_id: Arc::new(RwLock::new(owner_id)), - members: Vec::new(), - permissions: HashMap::new(), - roles: HashMap::new(), - private_key, - public_key, - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - }; - c.save().await; - c - } - - pub async fn to_json(&self) -> JsonValue { - let mut json = JsonValue::new_object(); - json["name"] = self.name.clone().into(); - json["owner_id"] = (*self.owner_id.read().await as i64).into(); - json["members"] = self - .members - .clone() - .iter() - .map(|f| f.to_string()) - .collect::>() - .into(); - json["private_key"] = self.private_key.as_bytes().to_vec().into(); - json["public_key"] = self.public_key.as_bytes().to_vec().into(); - json["connections"] = self.connections.read().await.clone().len().into(); - json - } - - pub async fn frontend(&self) -> JsonValue { - let mut json = JsonValue::new_object(); - json["name"] = self.name.clone().into(); - json["owner_id"] = (*self.owner_id.read().await as i64).into(); - json["members"] = self - .members - .clone() - .iter() - .map(|f| f.to_string()) - .collect::>() - .into(); - json["public_key"] = self.public_key.as_bytes().to_vec().into(); - json["connections"] = self.connections.read().await.clone().len().into(); - json - } - - pub fn add_member(&mut self, member_id: i64) { - self.members.push(member_id); - } - - pub fn remove_member(&mut self, member_id: i64) { - self.members.retain(|id| *id != member_id); - } - pub fn get_name(&self) -> &str { - &self.name - } - pub async fn get_owner_id(&self) -> i64 { - *self.owner_id.read().await - } - pub fn get_members(&self) -> Vec { - self.members.clone() - } - pub fn get_private_key(&self) -> Secret { - Secret::from_bytes(self.private_key.as_bytes()).unwrap() - } - pub fn get_public_key(&self) -> &PublicKey { - &self.public_key - } - pub async fn set_owner(self: Arc, owner_id: i64) { - *self.owner_id.write().await = owner_id; - } - pub async fn add_connection(self: &Arc, other: Arc) { - let mut vec = self - .connections - .read() - .await - .get(&other.get_user_id().await) - .cloned() - .unwrap_or_default(); - vec.push(other.clone()); - self.connections - .write() - .await - .insert(other.get_user_id().await, vec); - } - pub async fn remove_connection(self: &Arc, other: Arc) { - let mut vec = self - .connections - .read() - .await - .get(&other.get_user_id().await) - .cloned() - .unwrap_or_default(); - vec.retain(|conn| !Arc::ptr_eq(conn, &other)); - self.connections - .write() - .await - .insert(other.get_user_id().await, vec); - } - pub async fn get_connections(&self) -> HashMap>> { - self.connections.read().await.clone() - } - pub async fn get_connections_for_user(&self, user_id: i64) -> Vec> { - self.connections - .read() - .await - .get(&user_id) - .cloned() - .unwrap_or_default() - } - pub async fn get_interactables( - &self, - _user_id: i64, - ) -> Vec>> { - self.interactables.read().await.clone() - } - pub async fn add_interactable(self: &mut Arc, interactable: Arc>) { - self.interactables.write().await.push(interactable); - } - pub async fn remove_interactable( - self: &mut Arc, - interactable: Arc>, - ) { - self.interactables - .write() - .await - .retain(|i| !Arc::ptr_eq(i, &interactable)); - } - pub async fn broadcast_message(&self, message: &CommunicationValue) { - for (_, conns) in self.connections.read().await.clone().iter() { - for user in conns.iter() { - user.send_message(message).await; - } - } - } - pub async fn run_function( - self: &mut Arc, - _user_id: i64, - name: &str, - path: &str, - _function: &str, - cv: &CommunicationValue, - ) -> CommunicationValue { - if path.is_empty() { - let target_interactables = &self.interactables.read().await.clone(); - for interactable in target_interactables.iter() { - if interactable.get_name() == name { - if interactable.get_codec() == "category" { - return CommunicationValue::new(CommunicationType::error); - } else { - // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) - return interactable.run_function(cv.clone()).await; - } - } - } - } else { - let target_interactables = &self.interactables.read().await.clone(); - for interactable in target_interactables.iter() { - if interactable.get_name() == name { - if interactable.get_codec() == "category" { - let category: &Category = - interactable.as_any().downcast_ref::().unwrap(); - // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) - return category - .get_child(path.to_string(), name.to_string()) - .unwrap() - .run_function(cv.clone()) - .await; - } else { - return CommunicationValue::new(CommunicationType::error); - } - } - } - } - CommunicationValue::new(CommunicationType::add_conversation) - } - - pub async fn save(&self) { - let mut json = Object::new(); - json.insert("name", JsonValue::String(self.name.clone())); - json.insert( - "owner_id", - JsonValue::Number(Number::from(*self.owner_id.read().await as i64)), - ); - - json.insert( - "private_key", - JsonValue::String(STANDARD.encode(&self.private_key.as_bytes())), - ); - json.insert( - "public_key", - JsonValue::String(STANDARD.encode(&self.public_key.as_bytes())), - ); - - file_util::save_file( - &format!("communities/{}/", self.name), - "config.json", - &json.dump(), - ); - - let mut user_data = Object::new(); - for user in self.members.iter() { - let mut data = JsonValue::new_object(); - - let mut permissions = JsonValue::new_array(); - for perm in self.permissions.get(user).unwrap() { - if let Ok(_) = permissions.push(perm.to_string()) {} - } - - if let Ok(_) = data.insert("permissions", permissions) {} - user_data.insert(&user.to_string(), data); - } - file_util::save_file( - &format!("communities/{}/", self.name), - "users.json", - &user_data.dump(), - ); - - for interactable in self.interactables.read().await.clone().iter() { - registry::save(interactable).await; - } - } -} -pub async fn load(name: &String) -> Option> { - let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json"); - let json_content = json::parse(&file_contents).unwrap(); - - let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json"); - let user_json: JsonValue = json::parse(&user_data).unwrap(); - let mut users = Vec::new(); - let mut permissions: HashMap> = HashMap::new(); - - for user in user_json.entries() { - let (id, json): (&str, &JsonValue) = user; - let perms_j = &json["permissions"]; - let perms = Vec::new(); - for _ in perms_j.entries() { - // let perm_j = i.as_str().unwrap(); - // perms.push(perm_j.to_string()); - } - - let user_id: i64 = id.parse().unwrap_or(0); - - users.push(user_id); - permissions.insert(user_id, perms); - } - - let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json"); - let roles: HashMap> = HashMap::new(); - if let Ok(_) = json::parse(&role_data) { - // Fill roles - } else { - return None; - }; - - let community = Community { - name: json_content["name"].as_str().unwrap().to_string(), - owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))), - members: users, - roles, - permissions, - private_key: Secret::from_bytes( - &STANDARD - .decode(json_content["private_key"].as_str().unwrap()) - .unwrap(), - ) - .unwrap(), - public_key: PublicKey::from( - &Secret::from_bytes( - &STANDARD - .decode(json_content["private_key"].as_str().unwrap()) - .unwrap(), - ) - .unwrap(), - ), - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - }; - let mut comarc = Arc::new(community); - - let interactable_files: Vec = - file_util::get_children(&format!("communities/{}/interactables/", name)); - for file in interactable_files { - if file.contains(".json") { - let name = file.split('.').next().unwrap().to_string(); - let interactable: Box = - registry::load(comarc.clone(), String::new(), name).await; - comarc.add_interactable(Arc::new(interactable)).await; - } - } - Some(comarc) -} diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs deleted file mode 100644 index 62df251..0000000 --- a/src/communities/community_connection.rs +++ /dev/null @@ -1,396 +0,0 @@ -use crate::auth::auth_user::AuthUser; -use crate::communities::community::Community; -use crate::communities::interactables::interactable::Interactable; -use crate::users::user_manager::get_user; -use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use futures::SinkExt; -use futures::stream::SplitSink; -use futures::stream::SplitStream; -use hkdf::Hkdf; -use hyper::upgrade::Upgraded; -use hyper_util::rt::TokioIo; -use json::JsonValue; -use json::number::Number; -use rand::{Rng, distributions::Alphanumeric}; -use sha2::Sha256; -use std::sync::Arc; -use tokio::sync::RwLock; -use tokio_tungstenite::WebSocketStream; -use tungstenite::Message; -use tungstenite::Utf8Bytes; -use uuid::Uuid; -use x448::PublicKey; -pub struct CommunityConnection { - pub sender: Arc>, Message>>>, - pub receiver: Arc>>>>, - pub user_id: Arc>, - pub community: Arc>>>, - identified: Arc>, - challenged: Arc>, - challenge: Arc>, - auth: Arc>>, - pub ping: Arc>, -} -impl CommunityConnection { - pub fn new( - sender: SplitSink>, Message>, - receiver: SplitStream>>, - community: Arc, - ) -> Arc { - Arc::new(Self { - sender: Arc::new(RwLock::new(sender)), - receiver: Arc::new(RwLock::new(receiver)), - user_id: Arc::new(RwLock::new(0)), - community: Arc::new(RwLock::new(Some(community))), - identified: Arc::new(RwLock::new(false)), - challenged: Arc::new(RwLock::new(false)), - challenge: Arc::new(RwLock::new(String::new())), - auth: Arc::new(RwLock::new(None)), - ping: Arc::new(RwLock::new(-1)), - }) - } - pub async fn send_message(&self, message: &CommunicationValue) { - let mut sender = self.sender.write().await; // Access the SplitSink - let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string())); - sender.send(message_text).await.unwrap(); // Send the message via the SplitSink - } - pub async fn get_community(&self) -> Option> { - self.community.read().await.clone() - } - pub async fn get_user_id(&self) -> i64 { - *self.user_id.read().await - } - pub async fn is_identified(&self) -> bool { - *self.identified.read().await && *self.challenged.read().await - } - - pub async fn handle_message(self: Arc, message: String) { - let mut cv = CommunicationValue::from_json(&message); - let user_id = self.get_user_id().await; - cv = cv.with_sender(user_id); - - if cv.is_type(CommunicationType::identification) && !self.is_identified().await { - self.handle_identification(cv).await; - return; - } - - if cv.is_type(CommunicationType::challenge_response) && !self.is_identified().await { - self.handle_challenge_response(cv).await; - return; - } - - if !self.is_identified().await { - return; - } - - if cv.is_type(CommunicationType::ping) { - self.handle_ping(cv).await; - return; - } - - if cv.is_type(CommunicationType::client_changed) { - //self.handle_client_changed(cv).await; - return; - } - - if cv.is_type(CommunicationType::function) { - self.handle_function(cv).await; - return; - } - } - async fn handle_function(&self, cv: CommunicationValue) { - let name = cv.get_data(DataTypes::name).unwrap().as_str().unwrap(); - let path = cv.get_data(DataTypes::path).unwrap().as_str().unwrap(); - let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); - - let result = self - .get_community() - .await - .unwrap() - .run_function(self.get_user_id().await, name, path, function, &cv) - .await; - - self.send_message(&result).await; - } - async fn handle_identification(&self, cv: CommunicationValue) { - let user_id = cv - .get_data(DataTypes::user_id) - .unwrap_or(&JsonValue::Number(Number::from(0))) - .as_i64() - .unwrap_or(0); - - let Some(user) = get_user(user_id) else { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - }; - - { - let mut auth_guard = self.auth.write().await; - //*auth_guard = Some(user.clone()); - - let mut user_id_guard = self.user_id.write().await; - *user_id_guard = user_id; - - let mut identified_guard = self.identified.write().await; - *identified_guard = true; - } - - let challenge_str: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect(); - - { - let mut challenge_guard = self.challenge.write().await; - *challenge_guard = challenge_str.clone(); - } - - let user_public_key_bytes = match STANDARD.decode(&user.public_key) { - Ok(bytes) => bytes, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - } - }; - - let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { - Some(key) => key, - __ => { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - } - }; - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let community_private_key = community.get_private_key(); - let community_public_key = community.get_public_key(); - - let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { - Some(secret) => secret, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let aes_key = { - let hk = Hkdf::::new(None, shared_secret.as_bytes()); - let mut key_bytes = [0u8; 32]; - hk.expand(b"challenge", &mut key_bytes) - .expect("HKDF failure"); - key_bytes - }; - - let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); - - let nonce_bytes: [u8; 12] = rand::random(); - let nonce = Nonce::from_slice(&nonce_bytes); - - let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { - Ok(data) => data, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let mut encrypted_out = nonce_bytes.to_vec(); - encrypted_out.extend(encrypted_challenge); - - let response = CommunicationValue::new(CommunicationType::challenge) - .add_data_str( - DataTypes::public_key, - STANDARD.encode(community_public_key.as_bytes()), - ) - .add_data_str(DataTypes::challenge, STANDARD.encode(&encrypted_out)) - .with_id(cv.get_id()); - - self.send_message(&response).await; - } - async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { - let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { - Some(data) => data.to_string(), - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { - Ok(bytes) => bytes, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - if challenge_response_bytes.len() < 12 { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - - let Some(user) = self.auth.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let community_private_key = community.get_private_key(); - - let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { - Some(secret) => secret, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let aes_key = { - use hkdf::Hkdf; - use sha2::Sha256; - - let hk = Hkdf::::new(None, shared_secret.as_bytes()); - let mut key_bytes = [0u8; 32]; - hk.expand(b"challenge", &mut key_bytes) - .expect("HKDF failure"); - key_bytes - }; - - let (nonce_bytes, ciphertext) = challenge_response_bytes.split_at(12); - let nonce = Nonce::from_slice(nonce_bytes); - let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); - - let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { - Ok(pt) => pt, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let client_response = match String::from_utf8(decrypted_bytes) { - Ok(str) => str, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let expected_challenge = self.challenge.read().await.clone(); - - if client_response != expected_challenge { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - self.close().await; - return; - } - - { - let mut authenticated_guard = self.challenged.write().await; - *authenticated_guard = true; - } - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - let arc = Arc::new(community); - - let user_id = self.get_user_id().await; - if user_id == 0 { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - - arc.add_connection(self.clone()).await; - - let response = CommunicationValue::new(CommunicationType::identification_response) - .add_data(DataTypes::interactables, { - let a: Vec>> = arc.get_interactables(user_id).await; - let mut c: JsonValue = JsonValue::new_object(); - for b in a { - let mut subject = JsonValue::new_object(); - subject["codec"] = JsonValue::String(b.get_codec()); - subject["data"] = b.get_data(); - c[b.get_name()] = subject; - } - c - }) - .with_id(cv.get_id()); - - self.send_message(&response).await; - } - - async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(*message_id); - self.send_message(&error).await; - } - pub async fn close(&self) { - let mut sender = self.sender.write().await; - let _ = sender.close().await; - } - pub async fn handle_close(self: Arc) { - if self.is_identified().await { - if self.get_user_id().await != 0 { - self.community - .read() - .await - .as_ref() - .unwrap() - .remove_connection(self.clone()) - .await; - } - } - } - - async fn handle_ping(&self, cv: CommunicationValue) { - if let Some(last_ping) = cv.get_data(DataTypes::last_ping) { - if let Ok(ping_val) = last_ping.to_string().parse::() { - let mut ping_guard = self.ping.write().await; - *ping_guard = ping_val; - } - } - - let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); - - self.send_message(&response).await; - } -} diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs deleted file mode 100644 index 8369f5a..0000000 --- a/src/communities/community_manager.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::communities::community::{self, Community}; -use crate::log; -use crate::util::file_util; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; - -pub static COMMUNITY_REGISTRY: Lazy>>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); - -pub async fn add_community(community: Arc) { - COMMUNITY_REGISTRY - .lock() - .await - .insert(community.get_name().to_string(), community); -} -pub async fn remove_community(name: &str) { - COMMUNITY_REGISTRY.lock().await.remove(name); -} -pub async fn clear() { - COMMUNITY_REGISTRY.lock().await.clear(); -} -pub async fn get_community(name: &str) -> Option> { - if let Some(c) = COMMUNITY_REGISTRY.lock().await.get(name) { - Some(c.clone()) - } else { - None - } -} - -pub async fn load_communities() { - let community_names = file_util::get_children("communities"); - for name in community_names { - if let Some(community) = community::load(&name).await { - add_community(community).await; - } else { - log!("failed to load the {} community", &name); - } - } -} -pub async fn save_communities() { - for community in COMMUNITY_REGISTRY.lock().await.values() { - community.save().await; - } -} - -pub async fn get_communities() -> Vec> { - COMMUNITY_REGISTRY.lock().await.values().cloned().collect() -} - -pub async fn rename_community(old_name: &str, new_name: &str) { - if let Some(community) = COMMUNITY_REGISTRY.lock().await.remove(old_name) { - COMMUNITY_REGISTRY - .lock() - .await - .insert(new_name.to_string(), community); - } -} diff --git a/src/communities/interactables/category.rs b/src/communities/interactables/category.rs deleted file mode 100644 index 6b6415f..0000000 --- a/src/communities/interactables/category.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::communities::{community::Community, interactables::interactable::Interactable}; -use async_trait::async_trait; -use json::JsonValue; -use std::any::Any; -use std::sync::Arc; -use ttp_core::CommunicationValue; -use uuid::Uuid; - -pub struct Category { - id: Uuid, - name: String, - path: String, - community: Arc, - children: Vec>>, -} -impl Category { - pub fn new() -> Category { - Category { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - children: Vec::new(), - } - } - pub fn get_child(&self, path: String, name: String) -> Option>> { - if path.is_empty() { - self.children - .iter() - .find(|child| child.get_name() == &name) - .cloned() - } else { - let sub_module = path.split("/").next().unwrap(); - let next = self - .children - .iter() - .find(|child| child.get_name() == sub_module) - .unwrap(); - if next.get_codec() == "category" { - let next_cat = next.as_any().downcast_ref::().unwrap(); - next_cat.get_child(path, name) - } else { - Some(next.clone()) - } - } - } - pub fn get_children(&self) -> Vec>> { - self.children.iter().map(|child| child.clone()).collect() - } -} - -#[async_trait] -impl Interactable for Category { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "category".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - let mut v = JsonValue::new_object(); - for child in &self.children { - let mut subject = JsonValue::new_object(); - subject["codec"] = JsonValue::String(child.get_codec()); - subject["data"] = child.get_data(); - v[child.get_name()] = subject; - } - v - } - async fn run_function(&self, _cv: CommunicationValue) -> CommunicationValue { - CommunicationValue::new(CommunicationType::error) - } - fn to_json(&self) -> JsonValue { - let mut v = JsonValue::new_object(); - v["children"] = JsonValue::new_array(); - for child in &self.children { - let _ = v["children"].push(child.to_json()); - } - v - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/interactables/interactable.rs b/src/communities/interactables/interactable.rs deleted file mode 100644 index 0521de6..0000000 --- a/src/communities/interactables/interactable.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::communities::community::Community; -use async_trait::async_trait; -use json::JsonValue; -use std::any::Any; -use std::sync::Arc; -use ttp_core::CommunicationValue; -use uuid::Uuid; - -pub type InteractableFactory = fn() -> Box; - -#[async_trait] -pub trait Interactable: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn get_codec(&self) -> String; - fn get_name(&self) -> &String; - fn get_path(&self) -> &String; - fn get_total_path(&self) -> String; - fn set_name(&mut self, name: String); - fn set_path(&mut self, path: String); - fn get_community(&self) -> &Arc; - fn set_community(&mut self, community: Arc); - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue; - fn get_data(&self) -> JsonValue; - fn get_id(&self) -> &Uuid; - fn to_json(&self) -> JsonValue; - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - json: &JsonValue, - ); -} diff --git a/src/communities/interactables/registry.rs b/src/communities/interactables/registry.rs deleted file mode 100644 index 2f57005..0000000 --- a/src/communities/interactables/registry.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::communities::community::Community; -use crate::communities::interactables::category::Category; -use crate::communities::interactables::interactable::{Interactable, InteractableFactory}; -use crate::communities::interactables::text_chat::TextChat; -use crate::communities::interactables::voice_chat::VoiceChat; -use crate::util::file_util; -use json::JsonValue; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; -use uuid::Uuid; - -pub static INTERACTABLE_REGISTRY: Lazy>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); -pub async fn load_interactables() { - INTERACTABLE_REGISTRY - .lock() - .await - .insert(TextChat::new().get_codec(), || { - Box::new(TextChat::new()) as Box - }); - INTERACTABLE_REGISTRY - .lock() - .await - .insert(VoiceChat::new().get_codec(), || { - Box::new(VoiceChat::new()) as Box - }); - INTERACTABLE_REGISTRY - .lock() - .await - .insert(Category::new().get_codec(), || { - Box::new(Category::new()) as Box - }); -} -pub async fn register_interactable(name: String, interactable: InteractableFactory) { - INTERACTABLE_REGISTRY - .lock() - .await - .insert(name.to_string(), interactable) - .unwrap(); -} -pub async fn get_interactable(name: &str) -> Box { - INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()() -} -pub async fn save(interactable: &Arc>) { - let mut json_object: JsonValue = interactable.to_json().clone(); - json_object["codec"] = JsonValue::String(interactable.get_codec()); - json_object["id"] = JsonValue::String(interactable.get_id().to_string()); - file_util::save_file( - &format!( - "communities/{}/interactables/{}", - interactable.get_community().get_name(), - interactable.get_path() - ), - &format!("{}.json", interactable.get_name()), - &json_object.to_string(), - ); -} -pub async fn load( - c: Arc, - path: String, - name: String, -) -> Box { - let s = file_util::load_file( - &format!("communities/{}/interactables/{}", c.get_name(), path), - &format!("{}.json", name), - ); - let json_object: JsonValue = json::parse(&s).unwrap(); - let codec: String = json_object["codec"].as_str().unwrap().to_string(); - let id: String = json_object["id"].as_str().unwrap().to_string(); - let mut interactable = get_interactable(&codec).await; - interactable.load(c, Uuid::parse_str(&id).unwrap(), path, name, &json_object); - interactable -} diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs deleted file mode 100644 index 88e1545..0000000 --- a/src/communities/interactables/text_chat.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::{ - communities::{ - community::Community, community_connection::CommunityConnection, - interactables::interactable::Interactable, - }, - log, - util::file_util::{get_children, load_file, save_file}, -}; -use async_trait::async_trait; -use json::{JsonValue, array, object}; -use std::fs; -use std::sync::Arc; -use std::{any::Any, collections::HashMap}; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes}; -use uuid::Uuid; -pub struct TextChat { - id: Uuid, - name: String, - path: String, - community: Arc, -} -impl TextChat { - pub fn new() -> TextChat { - TextChat { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - } - } - pub fn add_message(&self, send_time: u128, sender: i64, message: &str) { - let user_dir = &format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - ); - - if let Err(e) = fs::create_dir_all(user_dir) { - log!("Failed to create chat directory: {}", e); - return; - } - - let mut chunk_index = 0; - let mut message_chunk = array![]; - - // find latest chunk not full (max 800 msgs) - loop { - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file(&user_dir, &file_name); - - if !file_content.is_empty() { - if let Ok(current_chunk) = json::parse(&file_content) { - if current_chunk.is_array() && current_chunk.len() < 800 { - message_chunk = current_chunk; - break; - } - } else { - log!("Failed to parse existing JSON file: {}", file_name); - } - } else { - break; - } - - chunk_index += 1; - if chunk_index > 1000 { - log!("Too many message chunks. Aborting add."); - return; - } - } - - let json_obj = object! { - "timestamp" => send_time as i64, - "content" => message, - "sender" => sender.to_string(), - }; - - if let Err(e) = message_chunk.push(json_obj) { - log!("Failed to push new message into JSON array: {}", e); - return; - } - - let file_name = format!("msgs_{}.json", chunk_index); - log!("Saving message to {}/{}", user_dir, file_name); - save_file(&user_dir, &file_name, &message_chunk.dump()); - } - pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { - let mut messages = array![]; - - let mut latest_chunk_index: i32 = -1; - let files = get_children(&format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - )); - - for entry in files { - if let Some(num) = { - entry - .strip_prefix("msgs_") - .and_then(|s| s.strip_suffix(".json")) - } { - if let Ok(index) = num.parse::() { - if index > latest_chunk_index { - latest_chunk_index = index; - } - } - } - } - - if latest_chunk_index == -1 { - return messages; - } - - let mut to_skip = loaded_messages; - let mut needed = amount; - - for chunk_index in (0..=latest_chunk_index).rev() { - if needed == 0 { - break; - } - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file( - &format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - ), - &file_name, - ); - if file_content.is_empty() { - continue; - } - if let Ok(chunk) = json::parse(&file_content) { - for i in (0..chunk.len()).rev() { - if needed == 0 { - break; - } - if to_skip > 0 { - to_skip -= 1; - continue; - } - messages.push(chunk[i].clone()).unwrap(); - needed -= 1; - } - } - } - - messages - } -} -#[async_trait] -impl Interactable for TextChat { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "text".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - JsonValue::new_object() - } - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { - let payload = cv.get_data(DataTypes::payload).as_container().unwrap(); - if cv.get_data(DataTypes::function).as_str().unwrap() == "get_messages" { - let amount = payload.get(DataTypes::amount).as_i64().unwrap(); - let loaded_messages = payload["loaded_messages"].as_i64().unwrap(); - let messages = self.get_messages(loaded_messages, amount).clone(); - let mut payload = JsonValue::new_object(); - payload["messages"] = messages; - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_chunk".to_string()) - .add_data(DataTypes::payload, payload); - } - if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "send_message" { - let message = payload["message"].as_str().unwrap(); - let milliseconds_timestamp: u128 = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(); - self.add_message(milliseconds_timestamp, cv.get_sender(), message); - - let mut distribution_payload = JsonValue::new_object(); - distribution_payload["message"] = JsonValue::String(message.to_string()); - distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string()); - distribution_payload["send_time"] = - JsonValue::String(milliseconds_timestamp.to_string()); - let distribution = CommunicationValue::new(CommunicationType::update) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_live".to_string()) - .add_data(DataTypes::payload, distribution_payload); - - let connections: HashMap>> = - self.get_community().get_connections().await.clone(); - - for con in connections.values() { - for c in con { - let cd: &Arc = c; - cd.send_message(&distribution).await; - } - } - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_received".to_string()) - .add_data(DataTypes::payload, JsonValue::new_object()); - } - CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) - } - fn to_json(&self) -> JsonValue { - JsonValue::new_object() - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/interactables/voice_chat.rs b/src/communities/interactables/voice_chat.rs deleted file mode 100644 index 2a2e3a2..0000000 --- a/src/communities/interactables/voice_chat.rs +++ /dev/null @@ -1,187 +0,0 @@ -use crate::communities::{community::Community, interactables::interactable::Interactable}; -use async_trait::async_trait; -use json::JsonValue; -use std::sync::Arc; -use std::{any::Any, sync::RwLock}; -use uuid::Uuid; -pub enum CallUserState { - Active, - Muted, - Deafed, -} -impl CallUserState { - pub fn parse(state: &str) -> CallUserState { - match state { - "active" => CallUserState::Active, - "muted" => CallUserState::Muted, - "deafed" => CallUserState::Deafed, - _ => CallUserState::Active, - } - } - pub fn to_string(&self) -> String { - match self { - CallUserState::Active => "active".to_string(), - CallUserState::Muted => "muted".to_string(), - CallUserState::Deafed => "deafed".to_string(), - } - } -} - -pub struct CallUser { - pub user_id: Uuid, - pub user_state: CallUserState, - pub streaming: bool, -} - -pub struct VoiceChat { - id: Uuid, - name: String, - path: String, - community: Arc, - users: RwLock>, -} -impl VoiceChat { - pub fn new() -> VoiceChat { - VoiceChat { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - users: RwLock::new(Vec::new()), - } - } - pub fn update_user_state( - self: Arc, - user_id: Uuid, - state: CallUserState, - streaming: bool, - ) { - if let Some(user) = self - .users - .write() - .unwrap() - .iter_mut() - .find(|u| u.user_id == user_id) - { - user.user_state = state; - user.streaming = streaming; - } - } -} -#[async_trait] -impl Interactable for VoiceChat { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "voice".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - let mut data = JsonValue::new_object(); - let mut active_users = JsonValue::new_object(); - for user in self.users.read().unwrap().iter() { - let mut user_data = JsonValue::new_object(); - let _ = user_data.insert("state", JsonValue::String(user.user_state.to_string())); - let _ = user_data.insert("streaming", JsonValue::Boolean(user.streaming)); - let _ = active_users.insert(&user.user_id.to_string(), user_data); - } - let _ = data.insert("active_users", active_users); - data - } - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { - let payload = cv.get_data(DataTypes::payload).unwrap(); - let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); - - if function == "get_call" { - let sender_id = payload["sender_id"].as_str().unwrap(); - let message_id = payload["message"].as_str().unwrap(); - let send_time = payload["send_time"].as_str().unwrap(); - - let mut response_payload = JsonValue::new_object(); - response_payload["sender_id"] = JsonValue::String(sender_id.to_string()); - response_payload["message"] = JsonValue::String(message_id.to_string()); - response_payload["send_time"] = JsonValue::String(send_time.to_string()); - - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "getting_call".to_string()) - .add_data(DataTypes::payload, response_payload); - } - - if function == "update_user_state" { - let user_id = payload["user_id"].as_str().unwrap(); - let state = payload["state"].as_str().unwrap(); - let streaming = payload["streaming"].as_bool().unwrap(); - - if let Some(user) = self - .users - .write() - .unwrap() - .iter_mut() - .find(|u| u.user_id == Uuid::parse_str(user_id).unwrap()) - { - user.user_state = CallUserState::parse(state); - user.streaming = streaming; - } - let mut response_payload = JsonValue::new_object(); - response_payload["user_id"] = JsonValue::String(user_id.to_string()); - response_payload["state"] = JsonValue::String(state.to_string()); - response_payload["streaming"] = JsonValue::Boolean(streaming); - - return CommunicationValue::new(CommunicationType::update) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "user_changed".to_string()) - .add_data(DataTypes::payload, response_payload); - } - CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) - } - - fn to_json(&self) -> JsonValue { - let v = JsonValue::new_object(); - v - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/mod.rs b/src/communities/mod.rs deleted file mode 100644 index ecad116..0000000 --- a/src/communities/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod community_manager; -pub mod interactables { - pub mod category; - pub mod interactable; - pub mod registry; - pub mod text_chat; - pub mod voice_chat; -} -pub mod community; -pub mod community_connection; -pub mod perms { - pub mod permission; -} diff --git a/src/communities/perms/permission.rs b/src/communities/perms/permission.rs deleted file mode 100644 index 32a37ee..0000000 --- a/src/communities/perms/permission.rs +++ /dev/null @@ -1,27 +0,0 @@ -use json::JsonValue; -use uuid::Uuid; - -pub struct Permission { - pub id: Uuid, - pub name: String, -} -impl Permission { - pub fn new(id: Uuid, name: String) -> Self { - Permission { id, name } - } - pub fn to_json(&self) -> JsonValue { - json::object! { - "id" => self.id.to_string(), - "name" => self.name.clone() - } - } - pub fn from_json(json: JsonValue) -> Self { - Permission { - id: Uuid::parse_str(json["id"].as_str().unwrap()).unwrap(), - name: json["name"].as_str().unwrap().to_string(), - } - } - pub fn to_string(&self) -> String { - self.to_json().as_str().unwrap().to_string() - } -} diff --git a/src/data/mod.rs b/src/data/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/src/data/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/src/data/user.rs b/src/data/user.rs new file mode 100644 index 0000000..a175e6d --- /dev/null +++ b/src/data/user.rs @@ -0,0 +1,30 @@ +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +#[derive(Debug, Clone, PartialEq, EnumIter, Eq)] +#[allow(unused, non_camel_case_types)] +pub enum UserStatus { + user_offline, + user_online, + user_dnd, + user_idle, + user_wc, + user_borked, + iota_offline, + iota_online, + iota_borked, +} +#[allow(unused)] +impl UserStatus { + pub fn to_string(&self) -> String { + format!("{:?}", self) + } + pub fn from_str(s: &str) -> Option { + for sel in UserStatus::iter() { + if &sel.to_string() == s { + return Some(sel); + } + } + None + } +} diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs deleted file mode 100755 index a161226..0000000 --- a/src/gui/app_state.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry}; -use json::{JsonValue, object}; -use std::{collections::VecDeque, thread, time::Duration}; -use sysinfo::{RefreshKind, System}; - -#[derive(Clone)] -pub struct AppState { - pub logs: VecDeque, - pub cpu: Vec<(f64, f64)>, - pub ram: Vec<(f64, f64)>, - pub ping: Vec<(f64, f64)>, - pub net_up: Vec<(f64, f64)>, - pub net_down: Vec<(f64, f64)>, - pub sys_info: String, -} -const MAX_POINTS: usize = 1000; -const MAX_LOGS: usize = 100; - -impl AppState { - pub fn new() -> Self { - Self { - logs: VecDeque::new(), - cpu: Vec::new(), - ram: Vec::new(), - ping: Vec::new(), - net_up: Vec::new(), - net_down: Vec::new(), - sys_info: String::from("Loading..."), - } - } - - pub fn push_log(&mut self, msg: UiLogEntry) { - if self.logs.len() >= MAX_LOGS { - self.logs.pop_front(); - } - self.logs.push_back(msg); - } - - pub fn get_logs(&self) -> &VecDeque { - &self.logs - } - - pub fn push_cpu(&mut self, pt: (f64, f64)) { - self.cpu.push(pt); - if self.cpu.len() > MAX_POINTS { - self.cpu.remove(0); - } - } - - pub fn push_ram(&mut self, pt: (f64, f64)) { - self.ram.push(pt); - if self.ram.len() > MAX_POINTS { - self.ram.remove(0); - } - } - - pub fn push_ping_val(&mut self, pt: f64) { - self.ping.push((self.ping.len() as f64, pt)); - if self.ping.len() > MAX_POINTS { - self.ping.remove(0); - } - } - - pub fn push_net_up(&mut self, pt: (f64, f64)) { - self.net_up.push(pt); - if self.net_up.len() > MAX_POINTS { - self.net_up.remove(0); - } - } - - pub fn push_net_down(&mut self, pt: (f64, f64)) { - self.net_down.push(pt); - if self.net_down.len() > MAX_POINTS { - self.net_down.remove(0); - } - } - pub fn to_json(&self) -> JsonValue { - let json = object! { - "cpu" => self.cpu - .iter() - .map(|(_, y)| *y) - .collect::>(), - "ram" => self.ram - .iter() - .map(|(_, y)| *y) - .collect::>(), - "ping" => self - .ping - .iter() - .map(|(_, y)| *y) - .collect::>(), - "net_up" => self - .net_up - .iter() - .map(|(_, y)| *y) - .collect::>(), - "net_down" => self - .net_down - .iter() - .map(|(_, y)| *y) - .collect::>(), - }; - json - } - pub fn with_width(&self, width: u16) -> Self { - let mut new = self.clone(); - new.cpu = Self::downsample_to_fit_width(&new.cpu, width); - new.ram = Self::downsample_to_fit_width(&new.ram, width); - new.ping = Self::downsample_to_fit_width(&new.ping, width); - new.net_up = Self::downsample_to_fit_width(&new.net_up, width); - new.net_down = Self::downsample_to_fit_width(&new.net_down, width); - new - } - - fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { - let width_usize = (width as usize) * 2; - let len = data.len(); - - if len >= width_usize { - data[len - width_usize..].to_vec() - } else { - let mut result = Vec::with_capacity(width_usize); - - let dx = 1.0; - let pad_len = width_usize - len; - - let start_x = data - .first() - .map(|(x, _)| x - (dx * pad_len as f64)) - .unwrap_or(0.0); - let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0); - - for i in 0..pad_len { - result.push((start_x + i as f64 * dx, -1 as f64)); - } - - result.extend_from_slice(data); - result - } - } -} - -pub fn setup() { - ACTIVE_TASKS.insert("System info loader".to_string()); - tokio::spawn(async move { - let mut sys = System::new_with_specifics(RefreshKind::everything()); - let mut last_total_received = 0u64; - let mut last_total_transmitted = 0u64; - let mut counter = 0.0; - loop { - if *SHUTDOWN.read().await { - break; - } - sys.refresh_all(); - - let mut tcpu = 0; - for cpu in sys.cpus() { - tcpu += cpu.cpu_usage() as i64; - tcpu /= 2; - } - let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; - - let total_received = 0u64; - let total_transmitted = 0u64; - - let delta_received = if last_total_received == 0 { - 0 - } else { - total_received.saturating_sub(last_total_received) - }; - let delta_transmitted = if last_total_transmitted == 0 { - 0 - } else { - total_transmitted.saturating_sub(last_total_transmitted) - }; - last_total_received = total_received; - last_total_transmitted = total_transmitted; - - let net_down = delta_received as f64; - let net_up = delta_transmitted as f64; - - { - let mut st = APP_STATE.lock().unwrap(); - st.push_cpu((counter, tcpu as f64)); - st.push_ram((counter, ram)); - st.push_net_down((counter, net_down)); - st.push_net_up((counter, net_up)); - - st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); - } - - counter += 1.0; - if counter > 30.0 { - thread::sleep(Duration::from_millis(500)); - } else { - thread::sleep(Duration::from_millis(5)); - } - } - ACTIVE_TASKS.remove("System info loader"); - }); -} diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs deleted file mode 100644 index b189bd4..0000000 --- a/src/gui/elements/console_card.rs +++ /dev/null @@ -1,491 +0,0 @@ -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; -use ttp_core::{CommunicationType, CommunicationValue}; -use uuid::Uuid; - -use crate::{ - ACTIVE_TASKS, RELOAD, SHUTDOWN, - gui::{ - elements::elements::{Element, InteractableElement, JoinableElement}, - interaction_result::InteractionResult, - ui::FPS, - util::borders::draw_block_joins, - }, - log, log_command, log_cv, - omikron::omikron_connection::OMIKRON_CONNECTION, - users::{user_manager, user_profile::UserProfile}, - util::file_util, -}; -use std::{ - any::Any, - sync::{Arc, Mutex}, - time::Duration, -}; -use tokio::time::Instant; - -pub struct ConsoleCard { - focused: bool, - pub title: String, - pub content: String, - pub cursor_position: usize, - - borders: Borders, - joins: Borders, - - cursor: Arc>, - last_swap: Arc>, - tab_index: usize, -} - -impl ConsoleCard { - pub fn new(title: &str, content: &str) -> Self { - ConsoleCard { - focused: false, - title: title.to_string(), - content: content.to_string(), - cursor_position: content.chars().count(), - borders: Borders::ALL, - joins: Borders::NONE, - cursor: Arc::new(Mutex::new(true)), - last_swap: Arc::new(Mutex::new(Instant::now())), - tab_index: 0, - } - } - - fn byte_index(&self) -> usize { - self.content - .char_indices() - .nth(self.cursor_position) - .map(|(i, _)| i) - .unwrap_or(self.content.len()) - } - - fn cursor_visible(&self) -> bool { - if !self.focused { - return false; - } - - let mut visible = self.cursor.lock().unwrap(); - let mut last = self.last_swap.lock().unwrap(); - let now = Instant::now(); - - if now.duration_since(*last) >= Duration::from_millis(500) { - *visible = !*visible; - *last = now; - } - - *visible - } - - fn current_prefix(&self) -> Option<&str> { - if self.content.starts_with('/') { - Some("/") - } else { - None - } - } - - fn cursor_spans(&self) -> Vec> { - let cursor_visible = self.cursor_visible(); - let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); - let mut spans = Vec::new(); - - if self.content.is_empty() { - if self.focused { - if cursor_visible { - spans.push(Span::styled(" ", cursor_style)); - } else { - spans.push(Span::styled(" ", Style::default().fg(Color::White))); - } - spans.push(Span::styled( - "send command ( for info)", - Style::default().fg(Color::DarkGray), - )); - } else { - spans.push(Span::styled( - " send command ( for info)", - Style::default().fg(Color::DarkGray), - )); - } - return spans; - } - - let byte_index = self.byte_index(); - let before = self.content[..byte_index].to_string(); - let after = self.content[byte_index..].to_string(); - - let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0); - - if prefix_len > 0 && before.len() >= prefix_len { - let prefix = &before[..prefix_len]; - let rest = &before[prefix_len..]; - spans.push(Span::styled( - prefix.to_string(), - Self::style_for_part(true, false, false), - )); - if !rest.is_empty() { - spans.push(Span::styled( - rest.to_string(), - Style::default().fg(Color::White), - )); - } - } else if !before.is_empty() { - spans.push(Span::styled( - before.clone(), - Style::default().fg(Color::White), - )); - } - - if cursor_visible { - spans.push(Span::styled(" ", cursor_style)); - } - - if !after.is_empty() { - spans.push(Span::styled(after, Style::default().fg(Color::White))); - } - - spans - } - - fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style { - if is_error { - return Style::default().fg(Color::Red); - } - - if is_hint { - return Style::default().fg(Color::DarkGray); - } - - if is_prefix { - return Style::default().fg(Color::DarkGray); - } - - Style::default().fg(Color::White) - } - - fn render_cursor_spans(&self) -> Vec> { - self.cursor_spans() - } - - fn move_cursor_left(&mut self) { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - - fn move_cursor_right(&mut self) { - let len = self.content.chars().count(); - if self.cursor_position < len { - self.cursor_position += 1; - } - } - - fn delete_at_cursor(&mut self) { - if self.content.is_empty() || self.cursor_position == 0 { - return; - } - - let start = self - .content - .char_indices() - .nth(self.cursor_position.saturating_sub(1)) - .map(|(i, _)| i) - .unwrap_or(0); - let end = self.byte_index(); - self.content.replace_range(start..end, ""); - self.cursor_position -= 1; - } - - fn insert_at_cursor(&mut self, c: char) { - let idx = self.byte_index(); - self.content.insert(idx, c); - self.cursor_position += 1; - } -} - -impl Element for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, r: Rect) { - let block = Block::default() - .borders(self.borders) - .title(self.title.clone()) - .title_style(Style::default().fg(Color::White)) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }) - .style(if self.focused { - Style::default().fg(Color::White) - } else { - Style::default() - }); - - let spans = self.render_cursor_spans(); - let par = Paragraph::new(Line::from(spans)) - .block(block) - .scroll((0, 0)); - f.render_widget(par, r); - draw_block_joins(f, r, self.borders, self.joins); - } -} - -impl JoinableElement for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self - } - - fn interact(&mut self, key: KeyEvent) -> InteractionResult { - match key.code { - KeyCode::Enter => { - if self.content.is_empty() { - log!(""); - return InteractionResult::Handled; - } - - let command = self.content.clone(); - let id = Uuid::new_v4(); - let id = id.to_string(); - let id = id.split_at(8).0; - let task_id = format!("command_{}_{}", command, id); - ACTIVE_TASKS.insert(task_id.clone()); - - log_command!("{}", command); - - tokio::spawn(async move { - run_command(&command).await; - ACTIVE_TASKS.remove(&task_id); - }); - - self.content.clear(); - self.cursor_position = 0; - self.tab_index = 0; - InteractionResult::Handled - } - KeyCode::Backspace => { - self.delete_at_cursor(); - InteractionResult::Handled - } - KeyCode::Delete => { - let len = self.content.chars().count(); - if self.cursor_position < len { - let start = self.byte_index(); - let end = self - .content - .char_indices() - .nth(self.cursor_position + 1) - .map(|(i, _)| i) - .unwrap_or(self.content.len()); - self.content.replace_range(start..end, ""); - } - InteractionResult::Handled - } - KeyCode::Left => { - self.move_cursor_left(); - InteractionResult::Handled - } - KeyCode::Right => { - self.move_cursor_right(); - InteractionResult::Handled - } - KeyCode::Home => { - self.cursor_position = 0; - InteractionResult::Handled - } - KeyCode::End => { - self.cursor_position = self.content.chars().count(); - InteractionResult::Handled - } - KeyCode::Tab => { - if let Some(prefix) = self.current_prefix() { - if prefix == "/" { - self.tab_index = self.tab_index.saturating_add(1); - } - } - InteractionResult::Handled - } - _ => { - if let Some(c) = key.code.as_char() { - self.insert_at_cursor(c); - InteractionResult::Handled - } else { - InteractionResult::Unhandled - } - } - } - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} - -pub async fn run_command(command: &str) { - let parts = command.split(" ").collect::>(); - - match parts.as_slice() { - ["tasks"] => { - let active_tasks: Vec = - ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect(); - let info = if *SHUTDOWN.read().await && *RELOAD.read().await { - "Rebooting, " - } else if *SHUTDOWN.read().await { - "Shutting , " - } else { - "" - }; - log!("{}Active tasks: {:?}", info, active_tasks); - } - ["fps"] => { - let (fps, skips) = *FPS.read().await; - log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips); - } - - ["help"] => { - log!("Available commands: tasks, fps, ping, user"); - } - - ["help", "tasks"] => { - log!("Tasks command usage: tasks"); - } - ["help", "fps"] => { - log!("FPS command usage: fps"); - } - ["help", "ping"] => { - log!("Ping command usage: ping [time]"); - } - ["help", "user"] => { - log!("User command usage: user add | user remove | user list"); - } - - ["ping"] => { - ping(20).await; - } - ["ping", time] => { - let time = time.parse::().unwrap_or(20); - ping(time).await; - } - ["user", "add", username] => { - if let (Some(user), Some(_)) = user_manager::create_user(username).await { - log!("Created user {}", user.user_id); - } else { - log!("Failed to create user"); - } - } - ["user", "remove", username] => { - if let Some(user) = user_manager::get_user_by_username(username) { - user_manager::remove_user(user.user_id); - log!("Removed user {}", user.user_id); - } else { - log!("Failed to find user"); - } - } - ["user", "list"] => { - let users: Vec = user_manager::get_users(); - for user in users { - let storage = file_util::get_designed_storage(user.user_id); - log!( - "> Username: {}, ID: {}, created at: {}, storage: {}", - user.username, - user.user_id, - user.created_at, - storage - ); - } - } - ["user", "info", username] => { - if let Some(user) = user_manager::get_user_by_username(username) { - user_manager::remove_user(user.user_id); - log!("Removed user {}", user.user_id); - } else { - log!("Failed to find user"); - } - } - ["reload"] | ["restart"] => { - log!("Restarting"); - *RELOAD.write().await = true; - *SHUTDOWN.write().await = true; - } - ["shutdown"] | ["stop"] => { - log!("Shutting down"); - *SHUTDOWN.write().await = true; - } - _ => { - log!("Unknown command"); - } - } -} - -pub async fn ping(time: u64) { - let conn = OMIKRON_CONNECTION.clone(); - - let response_cv = conn - .await_response( - &CommunicationValue::new(CommunicationType::ping), - Some(Duration::from_secs(time)), - ) - .await; - match response_cv { - Ok(response) => log_cv!(response), - Err(err) => log!("Ping error: {:?}", err), - } -} diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs deleted file mode 100644 index 3429e52..0000000 --- a/src/gui/elements/elements.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::any::Any; - -use crossterm::event::KeyEvent; -use ratatui::{Frame, layout::Rect, widgets::Borders}; - -use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; - -#[allow(unused)] -pub trait Element: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - - fn render(&self, f: &mut Frame, r: Rect); -} - -#[allow(unused)] -pub trait JoinableElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn set_borders(&mut self, borders: Borders); - fn set_joins(&mut self, joins: Borders); -} - -#[allow(unused)] -pub trait InfoElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn get_info_screen(&self) -> Box; -} - -#[allow(unused)] -pub trait InteractableElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn interact(&mut self, key: KeyEvent) -> InteractionResult; - - fn can_focus(&self) -> bool; - fn is_focused(&self) -> bool; - fn focus(&mut self, f: bool); -} diff --git a/src/gui/elements/graph_card.rs b/src/gui/elements/graph_card.rs deleted file mode 100644 index d95f937..0000000 --- a/src/gui/elements/graph_card.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{any::Any, sync::Arc}; - -use crossterm::event::KeyEvent; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - widgets::{ - Block, Borders, - canvas::{Canvas, Line}, - }, -}; - -use crate::{ - APP_STATE, - gui::{ - elements::elements::{Element, InteractableElement, JoinableElement}, - interaction_result::InteractionResult, - ui::UI, - util::borders::draw_block_joins, - }, -}; - -pub enum GRAPHS { - Ram, - Cpu, - Ping, -} - -impl GRAPHS { - pub fn get_color(&self) -> Color { - match self { - GRAPHS::Ram => Color::Blue, - GRAPHS::Cpu => Color::Red, - GRAPHS::Ping => Color::Green, - } - } - - pub fn get_graph(&self) -> Vec<(f64, f64)> { - match self { - GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(), - GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(), - GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(), - } - } - - pub fn get_unit(&self) -> String { - match self { - GRAPHS::Ram => "MB".to_string(), - GRAPHS::Cpu => "%".to_string(), - GRAPHS::Ping => "ms".to_string(), - } - } -} - -#[allow(unused)] -pub struct GraphCard { - ui: Arc, - graph_type: GRAPHS, - - focused: bool, - pub title: String, - - borders: Borders, - joins: Borders, - - open: bool, -} - -impl GraphCard { - pub fn new(ui: Arc, graph_type: GRAPHS, title: String) -> Self { - Self { - ui, - graph_type, - focused: false, - title, - borders: Borders::ALL, - joins: Borders::NONE, - open: true, - } - } - - pub fn set_open(&mut self, open: bool) { - self.open = open; - } -} -impl Element for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, r: Rect) { - if self.open { - let graph = self.graph_type.get_graph(); - let unit = self.graph_type.get_unit(); - let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0); - let min_y = graph - .iter() - .map(|(_, y)| *y) - .filter(|y| *y > 0.0) - .min_by(|a, b| a.total_cmp(b)) - .unwrap_or(0.0); - let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max); - - let block = Block::default() - .title(format!( - "{}:─{}{}─{}min/{}max", - self.title, - graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, - unit, - min_y as i64, - max_y as i64, - )) - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - - let canvas = Canvas::default() - .block(block) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, 100.0]) - .paint(|ctx| { - for (x, y) in &graph { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color: self.graph_type.get_color(), - }); - } - }); - f.render_widget(canvas, r); - } else { - let block = Block::default() - .title("") - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - f.render_widget(block, r); - } - draw_block_joins(f, r, self.borders, self.joins); - } -} - -impl JoinableElement for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &dyn Element { - self - } - - fn as_element_mut(&mut self) -> &mut dyn Element { - self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &dyn Element { - self - } - - fn as_element_mut(&mut self) -> &mut dyn Element { - self - } - - fn interact(&mut self, _key: KeyEvent) -> InteractionResult { - InteractionResult::Handled - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs deleted file mode 100755 index e2d4ba1..0000000 --- a/src/gui/elements/log_card.rs +++ /dev/null @@ -1,483 +0,0 @@ -use crate::APP_STATE; -use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; -use crate::gui::interaction_result::InteractionResult; -use crate::gui::util::borders::draw_block_joins; -use crate::util::logger::PrintType; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; -use std::any::Any; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[derive(Clone, Debug)] -pub struct UiLogEntry { - pub timestamp_ms: u128, - pub sender: PrintType, - pub message: String, - pub is_error: bool, -} - -impl UiLogEntry { - pub fn format_timestamp(&self) -> String { - let secs = (self.timestamp_ms / 1000) as i64; - let hours = (secs / 3600) % 24; - let minutes = (secs / 60) % 60; - let seconds = secs % 60; - format!("{:02}:{:02}:{:02}", hours, minutes, seconds) - } -} - -impl From for UiLogEntry { - fn from(entry: LogEntry) -> Self { - Self { - timestamp_ms: entry.timestamp_ms, - sender: entry.sender, - message: entry.message, - is_error: entry.is_error, - } - } -} - -#[derive(Clone, Debug)] -pub struct LogEntry { - pub timestamp_ms: u128, - pub sender: PrintType, - pub message: String, - pub is_error: bool, -} - -impl LogEntry { - pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { - Self { - timestamp_ms: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - sender, - message, - is_error, - } - } -} - -pub struct LogCard { - focused: bool, - selected: bool, - scroll_offset: usize, - last_total_lines: usize, - last_visible_height: usize, - pub borders: Borders, - pub joins: Borders, -} - -impl LogCard { - pub fn new() -> Self { - Self { - focused: false, - selected: false, - scroll_offset: 0, - last_total_lines: 0, - last_visible_height: 10, - borders: Borders::ALL, - joins: Borders::NONE, - } - } - - fn get_logs(&self) -> Vec { - let state = APP_STATE.lock().unwrap(); - state.get_logs().iter().cloned().collect() - } - - fn find_split_point(s: &str, max_width: usize) -> usize { - if max_width == 0 { - return s.len(); - } - - let mut current_width = 0usize; - let mut last_boundary = 0usize; - - for (idx, ch) in s.char_indices() { - let char_width = if ch.is_ascii() { 1 } else { 2 }; - if current_width + char_width > max_width { - if last_boundary == 0 { - return idx + ch.len_utf8(); - } - return last_boundary; - } - current_width += char_width; - last_boundary = idx + ch.len_utf8(); - } - - s.len() - } - - fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { - let mut result = Vec::new(); - - let timestamp = entry.format_timestamp(); - let first_prefix = "┌ "; - let default_prefix = "│ "; - let last_prefix = "└ "; - - let prefix_width = 2; - let first_line_width = available_width.saturating_sub(prefix_width + 1); - let continuation_width = available_width.saturating_sub(prefix_width); - - let segments: Vec<&str> = entry.message.split('\n').collect(); - - let mut raw_lines = Vec::new(); - - for segment in segments { - let mut remaining = segment; - if remaining.is_empty() { - raw_lines.push(String::new()); - continue; - } - - let mut is_first_part = true; - while !remaining.is_empty() { - let current_width = if is_first_part { - first_line_width - } else { - continuation_width - }; - - let split_point = Self::find_split_point(remaining, current_width); - let line_content = remaining[..split_point].to_string(); - raw_lines.push(line_content); - remaining = &remaining[split_point..]; - is_first_part = false; - } - } - - if raw_lines.is_empty() { - raw_lines.push(String::new()); - } - - for (idx, content) in raw_lines.iter().enumerate() { - let is_last = idx + 1 == raw_lines.len(); - let is_single = raw_lines.len() == 1; - let is_first = idx == 0; - let prefix = if is_first { - first_prefix - } else if is_last && !is_single { - last_prefix - } else { - default_prefix - }; - - let mut line = String::from(prefix); - line.push_str(content); - - if is_last && !timestamp.is_empty() { - line.push(' '); - line.push_str(×tamp); - } - - result.push((line, entry.sender.prefix_color(), entry.is_error)); - } - - result - } - - fn build_all_lines( - &self, - entries: Vec, - width: usize, - ) -> Vec<(String, Color, bool)> { - let mut lines = Vec::new(); - - for entry in entries { - let wrapped = Self::wrap_entry(&entry, width); - lines.extend(wrapped); - } - - lines - } - - fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) { - if total_lines <= visible_height { - return (0, total_lines); - } - - let max_offset = total_lines - visible_height; - let clamped_offset = self.scroll_offset.min(max_offset); - - let end = total_lines - clamped_offset; - let start = end.saturating_sub(visible_height); - - (start, end) - } - - fn get_title_hints(&self) -> (bool, bool) { - if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height { - return (false, false); - } - - let max_offset = self.last_total_lines - self.last_visible_height; - let can_scroll_up = self.scroll_offset < max_offset; - let can_scroll_down = self.scroll_offset > 0; - - (can_scroll_up, can_scroll_down) - } - - fn build_title(&self) -> String { - if !self.focused { - return "Logs".to_string(); - } - - let (can_up, can_down) = self.get_title_hints(); - - if !can_up && !can_down { - return "Logs".to_string(); - } - - let nav_symbol = if self.selected { "↑" } else { "j" }; - let down_symbol = if self.selected { "↓" } else { "k" }; - - match (can_up, can_down) { - (true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol), - (true, false) => format!("Logs ({} older)", nav_symbol), - (false, true) => format!("Logs ({} newer)", down_symbol), - (false, false) => "Logs".to_string(), - } - } - - fn scroll_up(&mut self) { - let max_offset = self - .last_total_lines - .saturating_sub(self.last_visible_height); - self.scroll_offset = (self.scroll_offset + 1).min(max_offset); - } - - fn scroll_down(&mut self) { - self.scroll_offset = self.scroll_offset.saturating_sub(1); - } - - fn split_line_prefix(line: &str) -> (&str, &str) { - if let Some(rest) = line.strip_prefix("└ ") { - ("└ ", rest) - } else if let Some(rest) = line.strip_prefix("│ ") { - ("│ ", rest) - } else if let Some(rest) = line.strip_prefix("┌ ") { - ("┌ ", rest) - } else { - ("", line) - } - } - - fn split_timestamp_suffix(line: &str) -> (&str, &str) { - if let Some(idx) = line.rfind(' ') { - let possible_timestamp = &line[idx + 1..]; - if possible_timestamp.len() == 8 - && possible_timestamp.as_bytes()[2] == b':' - && possible_timestamp.as_bytes()[5] == b':' - { - let (content, timestamp_with_space) = line.split_at(idx); - return (content.trim_end(), timestamp_with_space); - } - } - (line, "") - } -} - -impl Element for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, area: Rect) { - let entries = self.get_logs(); - - let block = Block::default() - .title(self.build_title()) - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - - let inner_area = block.inner(area); - f.render_widget(block, area); - - if inner_area.width == 0 || inner_area.height == 0 { - draw_block_joins(f, area, self.borders, self.joins); - return; - } - - let all_lines = self.build_all_lines(entries, inner_area.width as usize); - let total_lines = all_lines.len(); - let visible_height = inner_area.height as usize; - - let (start, end) = self.calculate_view_window(total_lines, visible_height); - let visible_lines = &all_lines[start..end]; - - let rendered_lines: Vec = visible_lines - .iter() - .map(|(line, prefix_color, is_error)| { - let mut spans = Vec::new(); - - let (prefix, rest) = Self::split_line_prefix(line); - - if !prefix.is_empty() { - spans.push(Span::styled( - prefix.to_string(), - Style::default().fg(*prefix_color), - )); - } - - let (content, timestamp) = Self::split_timestamp_suffix(rest); - let text_color = if *is_error { Color::Red } else { Color::White }; - - if !content.is_empty() { - spans.push(Span::styled( - content.to_string(), - Style::default().fg(text_color), - )); - } - - if !timestamp.is_empty() { - spans.push(Span::styled( - timestamp.to_string(), - Style::default().fg(Color::DarkGray), - )); - } - - Line::from(spans) - }) - .collect(); - - for (idx, line) in rendered_lines.iter().enumerate() { - let line_area = Rect { - x: inner_area.x, - y: inner_area.y + idx as u16, - width: inner_area.width, - height: 1, - }; - f.render_widget(Paragraph::new(line.clone()), line_area); - } - - draw_block_joins(f, area, self.borders, self.joins); - } -} - -impl JoinableElement for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - &*self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - &mut *self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - &*self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - &mut *self - } - - fn interact(&mut self, key: KeyEvent) -> InteractionResult { - let entries = self.get_logs(); - let estimated_width = 80usize; - let all_lines = self.build_all_lines(entries, estimated_width); - - self.last_total_lines = all_lines.len(); - let visible_height = self.last_visible_height.max(1); - - match key.code { - KeyCode::Enter | KeyCode::Char(' ') => { - self.selected = !self.selected; - InteractionResult::Handled - } - KeyCode::Char('j') | KeyCode::Char('J') => { - let (can_up, _) = self.get_title_hints(); - if can_up { - self.scroll_up(); - } - InteractionResult::Handled - } - KeyCode::Char('k') | KeyCode::Char('K') => { - let (_, can_down) = self.get_title_hints(); - if can_down { - self.scroll_down(); - } - InteractionResult::Handled - } - KeyCode::Up if self.selected => { - let (can_up, _) = self.get_title_hints(); - if can_up { - self.scroll_up(); - } - InteractionResult::Handled - } - KeyCode::Down if self.selected => { - let (_, can_down) = self.get_title_hints(); - if can_down { - self.scroll_down(); - } - InteractionResult::Handled - } - KeyCode::Home => { - if self.last_total_lines > visible_height { - self.scroll_offset = self.last_total_lines - visible_height; - } - InteractionResult::Handled - } - KeyCode::End => { - self.scroll_offset = 0; - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - } - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs deleted file mode 100644 index b2557c5..0000000 --- a/src/gui/input_handler.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::gui::ui::{UI, UNIQUE}; -use crate::{RELOAD, SHUTDOWN}; -use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; -use std::sync::Arc; -use std::sync::atomic::Ordering; -use std::time::Duration; - -pub fn setup_input_handler(ui: Arc) { - tokio::spawn(async move { - loop { - if *SHUTDOWN.read().await { - break; - } - - let event_result = tokio::task::spawn_blocking(|| { - if let Ok(true) = poll(Duration::from_millis(100)) { - read().ok().and_then(|ev| match ev { - Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), - _ => None, - }) - } else { - None - } - }) - .await; - - match event_result { - Ok(Some(key_event)) => { - handle_input(key_event, ui.clone()).await; - UNIQUE.store(true, Ordering::Relaxed); - } - Ok(_) => {} - Err(e) => { - eprintln!("Input task error: {}", e); - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - } - }); -} - -pub async fn handle_input(key: KeyEvent, ui: Arc) { - match (key.code, key.modifiers) { - (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) - | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { - *RELOAD.write().await = true; - *SHUTDOWN.write().await = true; - } - _ => { - ui.handle_input(key).await; - } - } -} diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs deleted file mode 100644 index 462149a..0000000 --- a/src/gui/interaction_result.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::pin::Pin; - -use crate::gui::screens::screens::Screen; - -#[allow(unused)] -pub enum InteractionResult { - CloseScreen, - OpenScreen { - screen: Box, - }, - OpenFutureScreen { - screen: Pin> + Send>>, - }, - Handled, - Unhandled, -} - -impl Debug for InteractionResult { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"), - InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"), - InteractionResult::CloseScreen => write!(f, "CloseScreen"), - InteractionResult::Handled => write!(f, "Handled"), - InteractionResult::Unhandled => write!(f, "Unhandled"), - } - } -} - -impl PartialEq for InteractionResult { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - InteractionResult::OpenScreen { screen: _ }, - InteractionResult::OpenScreen { screen: _ }, - ) => true, - ( - InteractionResult::OpenFutureScreen { screen: _ }, - InteractionResult::OpenFutureScreen { screen: _ }, - ) => true, - (InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true, - (InteractionResult::Handled, InteractionResult::Handled) => true, - (InteractionResult::Unhandled, InteractionResult::Unhandled) => true, - _ => false, - } - } -} diff --git a/src/gui/mod.rs b/src/gui/mod.rs deleted file mode 100644 index b355046..0000000 --- a/src/gui/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod elements { - pub mod console_card; - pub mod elements; - pub mod graph_card; - pub mod log_card; -} -pub mod screens { - pub mod main_screen; - pub mod md_viewer; - pub mod screens; - pub mod terms_checker; - pub mod terms_updater; -} -pub mod util { - pub mod borders; -} -pub mod app_state; -pub mod input_handler; -pub mod interaction_result; -pub mod ui; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs deleted file mode 100644 index 85dbbaa..0000000 --- a/src/gui/screens/main_screen.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::gui::{ - elements::{ - console_card::ConsoleCard, - elements::{InteractableElement, JoinableElement}, - graph_card::{GRAPHS, GraphCard}, - log_card::LogCard, - }, - interaction_result::InteractionResult, - screens::screens::{NavDirection, Screen}, - ui::UI, -}; - -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Constraint, Layout, Margin, Rect}, - widgets::{Block, Borders}, -}; - -use std::{any::Any, sync::Arc}; - -pub struct MainScreen { - elements: Vec>, - nav_grid: Vec>>, - selected_coords: (usize, usize), - graphs_open: bool, -} - -impl MainScreen { - pub async fn new(ui: Arc) -> Self { - let mut elements: Vec> = Vec::new(); - - let nav_grid = vec![ - vec![Some(0), Some(2)], - vec![Some(0), Some(3)], - vec![Some(1), Some(4)], - ]; - - let mut log_card = LogCard::new(); - log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); - let mut console_card = ConsoleCard::new("Console", ""); - console_card.set_joins(Borders::TOP); - - elements.push(Box::new(log_card)); - elements.push(Box::new(console_card)); - - let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into()); - ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); - elements.push(Box::new(ram_graph)); - let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into()); - cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); - cpu_graph.set_joins(Borders::TOP); - elements.push(Box::new(cpu_graph)); - let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into()); - ping_graph.set_joins(Borders::TOP); - elements.push(Box::new(ping_graph)); - - let graphs_open = true; - - let mut screen = MainScreen { - elements, - nav_grid, - selected_coords: (1, 0), - graphs_open, - }; - screen.focus_current(); - screen - } - - fn focus_current(&mut self) { - let (y, x) = self.selected_coords; - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { - if let Some(element) = self.elements.get_mut(*index) { - if element.can_focus() { - element.focus(true); - } - } - } - } - - fn unfocus_current(&mut self, y: usize, x: usize) { - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { - if let Some(element) = self.elements.get_mut(*index) { - element.focus(false); - } - } - } - - fn navigate(&mut self, direction: NavDirection) { - let (current_row, current_col) = self.selected_coords; - let current_element = self.nav_grid[current_row][current_col]; - - self.unfocus_current(current_row, current_col); - - let (delta_row, delta_col) = match direction { - NavDirection::Up => (-1isize, 0), - NavDirection::Down => (1, 0), - NavDirection::Left => (0, -1), - NavDirection::Right => (0, 1), - _ => (0, 0), - }; - - let mut next_row = current_row as isize; - let mut next_col = current_col as isize; - - loop { - next_row += delta_row; - next_col += delta_col; - - if next_row < 0 || next_col < 0 { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - let next_row_u = next_row as usize; - let next_col_u = next_col as usize; - - if next_row_u >= self.nav_grid.len() { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - - if let Some(row) = self.nav_grid.get(next_row_u) { - if next_col_u >= row.len() { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - - if let Some(next_element) = row[next_col_u] { - if Some(next_element) != current_element { - self.selected_coords = (next_row_u, next_col_u); - self.focus_current(); - return; - } - } - } - } - - self.focus_current(); - } -} - -impl Screen for MainScreen { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, rect: Rect) { - let main_block = Block::default().title("Main").borders(Borders::ALL); - f.render_widget(main_block, rect); - - let inner = rect.inner(Margin { - vertical: 1, - horizontal: 1, - }); - - let graphs_width = if self.graphs_open { 30 } else { 2 }; - let main_width = inner.width.saturating_sub(graphs_width); - - let horizontal_chunks = Layout::default() - .direction(ratatui::layout::Direction::Horizontal) - .constraints([ - Constraint::Length(main_width), - Constraint::Length(graphs_width), - ]) - .split(inner); - - let left_area = horizontal_chunks[0]; - let right_area = horizontal_chunks[1]; - - let left_rows = - Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area); - - if let Some(log) = self.elements.get(0) { - log.as_element().render(f, left_rows[0]); - } - - if let Some(console) = self.elements.get(1) { - console.as_element().render(f, left_rows[1]); - } - - let graph_elements: Vec<_> = self - .elements - .iter() - .filter(|el| el.as_any().is::()) - .collect(); - - if !graph_elements.is_empty() { - let graph_chunks = Layout::vertical( - graph_elements - .iter() - .map(|_| Constraint::Ratio(1, graph_elements.len() as u32)) - .collect::>(), - ) - .split(right_area); - - for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { - el.as_element().render(f, *area); - } - } - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - match event.code { - KeyCode::Up => self.navigate(NavDirection::Up), - KeyCode::Down => self.navigate(NavDirection::Down), - KeyCode::Left => self.navigate(NavDirection::Left), - KeyCode::Right => self.navigate(NavDirection::Right), - KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => { - self.graphs_open = !self.graphs_open; - for element in self.elements.iter_mut() { - if let Some(graph) = element.as_any_mut().downcast_mut::() { - graph.set_open(self.graphs_open); - } - } - return InteractionResult::Handled; - } - _ => { - let (y, x) = self.selected_coords; - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { - if let Some(el) = self.elements.get_mut(*index) { - return el.interact(event); - } - } - } - } - - InteractionResult::Handled - } -} diff --git a/src/gui/screens/md_viewer.rs b/src/gui/screens/md_viewer.rs deleted file mode 100644 index 10eabce..0000000 --- a/src/gui/screens/md_viewer.rs +++ /dev/null @@ -1,493 +0,0 @@ -use crossterm::event::{self, Event, KeyCode, KeyEvent}; -use ratatui::{ - DefaultTerminal, - prelude::*, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, -}; -use std::{any::Any, time::Duration}; - -use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; - -pub struct FileViewer { - title: String, - text: Vec, - scroll: u16, - scroll_x: u16, -} - -impl Screen for FileViewer { - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, rect: Rect) { - self.draw(f, rect); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - match event.code { - KeyCode::Char('q') | KeyCode::Esc => { - return InteractionResult::CloseScreen; - } - - KeyCode::Down => self.scroll = self.scroll.saturating_add(1), - KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), - KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), - KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), - KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), - KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), - - _ => {} - } - - InteractionResult::Unhandled - } -} - -impl FileViewer { - pub fn new(title: String, content: &str) -> Self { - Self { - title, - text: parse_document(content.to_owned()), - scroll: 0, - scroll_x: 0, - } - } - pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal { - loop { - terminal - .draw(|f| { - let area = f.area(); - self.draw(f, area); - }) - .unwrap(); - - if event::poll(Duration::from_millis(100)).unwrap() { - let ev = event::read().unwrap(); - self.handle_event(&ev); - - if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) { - break; - } - } - } - terminal - } - fn draw(&self, f: &mut Frame, area: Rect) { - use ratatui::text::Text; - - let mut rendered_lines = Vec::new(); - - for display_line in &self.text { - if display_line.scrollable { - let content: String = display_line - .line - .spans - .iter() - .map(|s| s.content.clone()) - .collect(); - - let start = self.scroll_x as usize; - let width = area.width as usize - 2; - - let visible = if start < content.chars().count() { - content.chars().skip(start).take(width).collect() - } else { - String::new() - }; - - let mut chars: Vec = visible.chars().collect(); - - if start > 0 && !chars.is_empty() { - chars[0] = '<'; - } - - if start + width < content.chars().count() && !chars.is_empty() { - let last = chars.len() - 1; - chars[last] = '>'; - } - - let visible: String = chars.into_iter().collect(); - - rendered_lines.push(Line::from(Span::styled( - visible, - display_line - .line - .spans - .first() - .map(|s| s.style) - .unwrap_or_default(), - ))); - } else { - rendered_lines.push(display_line.line.clone()); - } - } - - let paragraph = Paragraph::new(Text::from(rendered_lines)) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("{} - [Q to close]", self.title.as_str(),)), - ) - .wrap(Wrap { trim: false }) - .scroll((self.scroll, 0)); - - f.render_widget(paragraph, area); - } - - pub fn handle_event(&mut self, event: &Event) { - if let Event::Key(key) = event { - match key.code { - KeyCode::Down => self.scroll = self.scroll.saturating_add(1), - KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), - KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), - KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), - KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), - KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), - _ => {} - } - } - } -} -fn parse_document(input: String) -> Vec { - let mut lines_vec = Vec::new(); - let mut in_code_block = false; - let liness: Vec = input.lines().map(String::from).collect(); - let mut i = 0; - - while i < liness.len() { - let raw = &liness[i]; - - if raw.trim().starts_with("```") { - in_code_block = !in_code_block; - let code: String = if raw.trim().replace("```", "").is_empty() { - "──".to_string() - } else { - raw.trim().replace("```", "") - }; - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - format!("────────{}────────", code), - Style::default().fg(Color::DarkGray), - )), - scrollable: false, - }); - i += 1; - continue; - } - - if in_code_block { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.to_string(), - Style::default().fg(Color::Yellow), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("### ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("### ").to_string(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("## ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("## ").to_string(), - Style::default() - .fg(Color::LightCyan) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("# ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("# ").to_string(), - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - - if raw.trim_start().starts_with("- ") { - let indent = raw.chars().take_while(|c| *c == ' ').count(); - lines_vec.push(DisplayLine { - line: Line::from(Span::raw(format!( - "{}• {}", - " ".repeat(indent), - raw.trim_start_matches("- ") - ))), - scrollable: false, - }); - i += 1; - continue; - } - - if raw.trim().starts_with('|') && raw.contains('|') { - let mut table_lines = vec![raw.clone()]; - let mut j = i + 1; - while j < liness.len() && liness[j].trim().starts_with('|') { - table_lines.push(liness[j].clone()); - j += 1; - } - - let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::>()); - lines_vec.extend(table_to_lines(table)); - i = j; - continue; - } - - lines_vec.push(DisplayLine { - line: Line::from(parse_inline(raw.as_str())), - scrollable: false, - }); - i += 1; - } - - lines_vec -} - -fn parse_inline(input: &str) -> Vec> { - let mut spans = Vec::new(); - let mut buf = String::new(); - - let mut bold = false; - let mut underline = false; - let mut code = false; - - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - let toggle = match c { - '*' if chars.peek() == Some(&'*') => { - chars.next(); - Some("bold") - } - '_' if chars.peek() == Some(&'_') => { - chars.next(); - Some("underline") - } - '`' => Some("code"), - _ => None, - }; - - if let Some(kind) = toggle { - flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); - - match kind { - "bold" => bold = !bold, - "underline" => underline = !underline, - "code" => code = !code, - _ => {} - } - continue; - } - - buf.push(c); - } - - flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); - spans -} - -fn current_style(bold: bool, underline: bool, code: bool) -> Style { - let mut style = Style::default(); - - if bold { - style = style.add_modifier(Modifier::BOLD); - } - if underline { - style = style.add_modifier(Modifier::UNDERLINED); - } - if code { - style = style.fg(Color::Yellow); - } - - style -} -#[derive(Clone)] -pub struct DisplayLine { - line: Line<'static>, - scrollable: bool, -} - -fn table_to_lines(table: Vec>) -> Vec { - if table.len() < 2 { - return vec![]; - } - - 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 = header - .iter() - .enumerate() - .map(|(i, h)| { - let h_len = h.chars().count().max(1); - if i == 0 { - table - .iter() - .map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0)) - .max() - .unwrap_or(h_len) - } else { - let max = (3 * h_len) as usize; - max.max(h_len) - } - }) - .collect(); - - let mut lines = Vec::new(); - - for (row_idx, row) in table.iter().enumerate() { - if row_idx == 1 { - let divider = widths - .iter() - .map(|w| "─".repeat(*w)) - .collect::>() - .join("─┼─"); - - lines.push(DisplayLine { - line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))), - scrollable: true, - }); - continue; - } - - let wrapped_cells: Vec> = row - .iter() - .enumerate() - .map(|(i, cell)| wrap_cell(cell, widths[i])) - .collect(); - - let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1); - - for line_idx in 0..row_height { - let mut line = String::new(); - - for (i, cell) in wrapped_cells.iter().enumerate() { - let content = cell.get(line_idx).map(String::as_str).unwrap_or(""); - line.push_str(&format!("{:width$}", content, width = widths[i])); - if i < wrapped_cells.len() - 1 { - line.push_str(" │ "); - } - } - - let style = if row_idx == 0 { - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Green) - }; - - lines.push(DisplayLine { - line: Line::from(Span::styled(line, style)), - scrollable: true, - }); - } - } - - lines -} -fn wrap_cell(cell: &str, width: usize) -> Vec { - if width == 0 { - return vec![String::new()]; - } - - let mut lines = Vec::new(); - let mut current = String::new(); - - 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::>().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::>().chunks(width) { - lines.push(chunk.iter().collect()); - } - } - } - } - - if !current.is_empty() { - lines.push(current); - } - - if lines.is_empty() { - lines.push(String::new()); - } - - lines -} - -fn flush_span(spans: &mut Vec, buf: &mut String, style: Style) { - if !buf.is_empty() { - spans.push(Span::styled(buf.clone(), style)); - buf.clear(); - } -} - -fn parse_table(lines: &[&str]) -> Vec> { - let mut table = Vec::new(); - - for &line in lines { - if !line.starts_with('|') || !line.contains('|') { - break; - } - let row: Vec = line - .trim_matches('|') - .split('|') - .map(|s| s.trim().to_string()) - .collect(); - table.push(row); - } - - table -} diff --git a/src/gui/screens/screens.rs b/src/gui/screens/screens.rs deleted file mode 100644 index 9676fbb..0000000 --- a/src/gui/screens/screens.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::any::Any; - -use crossterm::event::KeyEvent; -use ratatui::{Frame, layout::Rect}; - -use crate::gui::interaction_result::InteractionResult; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NavDirection { - Up, - Down, - Left, - Right, - - Next, - Prev, -} - -pub trait Screen: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - - fn render(&self, f: &mut Frame, rect: Rect); - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; -} diff --git a/src/gui/screens/terms_checker.rs b/src/gui/screens/terms_checker.rs deleted file mode 100644 index 26bb7c6..0000000 --- a/src/gui/screens/terms_checker.rs +++ /dev/null @@ -1,351 +0,0 @@ -use crate::{ - gui::{ - interaction_result::InteractionResult, - screens::{md_viewer::FileViewer, screens::Screen}, - ui::UI, - }, - terms::{ - buttons::{checkbox, draw_buttons}, - consent_state::UserChoice, - focus::Focus, - terms_getter::{Type, get_link, get_terms}, - }, -}; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph}, -}; -use std::{any::Any, pin::Pin, sync::Arc}; -use tokio::sync::oneshot; - -pub struct TermsCheckerScreen { - ui: Arc, - sender: Option>, - - eula: bool, - tos: bool, - pp: bool, - - focus: Focus, -} - -impl TermsCheckerScreen { - pub fn new(ui: Arc, sender: Option>) -> Self { - Self { - ui, - sender, - eula: false, - tos: false, - pp: false, - focus: Focus::Eula, - } - } -} - -impl Screen for TermsCheckerScreen { - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, size: Rect) { - let mut needed_height = 5; - - if size.height < 6 || size.width < 27 { - f.render_widget( - Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), - size, - ); - return; - } - - let max_width = 150; - let max_height = 26; - - let content_width = if max_width < size.width { - max_width - } else { - size.width - }; - let content_height = if max_height < size.height { - max_height - } else { - size.height - }; - - let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; - let vertical_margin = (size.height.saturating_sub(content_height)) / 2; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(7), Constraint::Length(3)]) - .split(Rect { - x: horizontal_margin, - y: vertical_margin, - width: content_width, - height: content_height, - }); - let eula_text = if size.width < 70 { - "EULA ¹ (https://legal.tensamin.net/eula/)" - } 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/)" - } 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/)" - } else { - "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" - }; - - let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = if size.width > 143 { - ( - vec![8, 3, 8, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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 > 92 { - ( - vec![9, 3, 9, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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![9, 3, 9, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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.", - ], - ) - } else { - ( - vec![10, 3, 11, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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.", - ], - ) - }; - let mut text_lines = vec![ - checkbox(eula_text, self.eula, self.focus == Focus::Eula, true), - checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula), - checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula), - Line::from(""), - Line::from("¹ Necessary– required to run the program"), - Line::from("² Optional – required only for Tensamin services"), - ]; - for line in agree_lines { - text_lines.insert(text_lines.len(), Line::from(line)); - } - - while size.height - 5 < text_lines.len() as u16 { - if optional_lines.len() == 0 { - break; - } - text_lines.remove(optional_lines[0] as usize); - optional_lines.remove(0); - } - needed_height += text_lines.len(); - - if size.width < 60 || size.height < needed_height as u16 { - let width_style = if size.width > 76 { - Style::default().fg(Color::Green) - } else if size.width >= 60 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let height_style = if size.height > 19 { - Style::default().fg(Color::Green) - } else if size.height >= 13 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let warning_text = Text::from(vec![ - Line::from(vec![ - Span::raw("Width: "), - Span::styled(format!("{}", size.width), width_style), - Span::raw(" / 60"), - ]), - Line::from(vec![ - Span::raw("Height: "), - Span::styled(format!("{}", size.height), height_style), - Span::raw(format!(" / 13")), - ]), - ]); - - let warning = Paragraph::new(warning_text) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("UI Too Small [Q to Quit]")), - ); - f.render_widget(warning, size); - return; - } - - let consent_block = Paragraph::new(Text::from(text_lines)).block( - Block::default() - .title(format!(" Tensamin User Consent [Q to Quit] ",)) - .borders(Borders::ALL), - ); - f.render_widget(consent_block, chunks[0]); - - draw_buttons( - f, - chunks[1], - self.focus, - (self.eula, self.tos && self.pp), - true, - false, - true, - ); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel]; - - if self.eula { - possible_states.push(Focus::Continue); - if self.tos && self.pp { - possible_states.push(Focus::ContinueAll); - } - } - - match event.code { - KeyCode::Esc => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - KeyCode::Up | KeyCode::Left => { - self.focus.prev(&possible_states); - InteractionResult::Handled - } - KeyCode::Down | KeyCode::Right | KeyCode::Tab => { - self.focus.next(&possible_states); - InteractionResult::Handled - } - KeyCode::Char('q') | KeyCode::Char('Q') => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - KeyCode::Char('o') | KeyCode::Char('O') => { - let terms_type = match self.focus { - Focus::Eula => Some(Type::EULA), - Focus::Tos => Some(Type::TOS), - Focus::Pp => Some(Type::PP), - _ => None, - }; - if let Some(terms_type) = terms_type { - let fut: Pin> + Send>> = - Box::pin(async move { - let content = get_terms(terms_type.clone()).await.unwrap(); - let screen: FileViewer = - FileViewer::new(terms_type.to_string(), &content); - Box::new(screen) as Box - }); - InteractionResult::OpenFutureScreen { screen: fut } - } else { - InteractionResult::Unhandled - } - } - KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { - Focus::Eula => { - let _ = open::that(get_link(Type::EULA)); - InteractionResult::Handled - } - Focus::Tos => { - let _ = open::that(get_link(Type::TOS)); - InteractionResult::Handled - } - Focus::Pp => { - let _ = open::that(get_link(Type::PP)); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Enter | KeyCode::Char(' ') => match self.focus { - Focus::Eula => { - self.eula = !self.eula; - self.tos = false; - self.pp = false; - InteractionResult::Handled - } - Focus::Tos if self.eula => { - self.tos = !self.tos; - InteractionResult::Handled - } - Focus::Pp if self.eula => { - self.pp = !self.pp; - InteractionResult::Handled - } - Focus::Cancel => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - Focus::Continue if self.eula => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptEULA); - } - InteractionResult::CloseScreen - } - Focus::ContinueAll if self.eula && self.tos && self.pp => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptAll); - } - InteractionResult::CloseScreen - } - _ => InteractionResult::Unhandled, - }, - - _ => InteractionResult::Unhandled, - } - } -} diff --git a/src/gui/screens/terms_updater.rs b/src/gui/screens/terms_updater.rs deleted file mode 100644 index 35da364..0000000 --- a/src/gui/screens/terms_updater.rs +++ /dev/null @@ -1,691 +0,0 @@ -use crate::{ - gui::{ - interaction_result::InteractionResult, - screens::{md_viewer::FileViewer, screens::Screen}, - }, - terms::{ - buttons::{checkbox, draw_buttons}, - consent_state::{UpdateDecision, UserChoice}, - doc::Doc, - focus::Focus, - terms_getter::{Type, get_newest_link, get_terms}, - }, -}; -use chrono::{Local, TimeZone, Utc}; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph}, -}; -use std::any::Any; -use tokio::sync::oneshot; - -pub struct TermsUpdaterScreen { - sender: Option>, - - eula_needed: bool, - tos_needed: bool, - pp_needed: bool, - - eula_future: bool, - tos_future: bool, - pp_future: bool, - - eula_for_future: Option, - tos_for_future: Option, - pp_for_future: Option, - - update_needed: bool, - - eula: bool, - tos: bool, - pp: bool, - - focus: Focus, -} -impl TermsUpdaterScreen { - pub fn new( - consent_eula: UpdateDecision, - consent_tos: UpdateDecision, - consent_pp: UpdateDecision, - sender: Option>, - ) -> Self { - let (eula_needed, eula_future, eula_for_future): (bool, bool, Option) = - match consent_eula { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - let (tos_needed, tos_future, tos_for_future): (bool, bool, Option) = match consent_tos - { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - let (pp_needed, pp_future, pp_for_future): (bool, bool, Option) = match consent_pp { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - - let focus = if eula_needed { - Focus::Eula - } else if tos_needed { - Focus::Tos - } else if pp_needed { - Focus::Pp - } else { - Focus::Cancel - }; - - let update_needed = (eula_needed && !eula_future) - || (tos_needed && !tos_future) - || (pp_needed && !pp_future); - - Self { - sender, - - eula_needed, - tos_needed, - pp_needed, - - eula_for_future, - tos_for_future, - pp_for_future, - - eula_future, - tos_future, - pp_future, - - update_needed, - - eula: !eula_needed, - tos: !tos_needed, - pp: !pp_needed, - - focus, - } - } -} -impl Screen for TermsUpdaterScreen { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, size: Rect) { - let mut needed_height = 5; - - if size.height < 6 || size.width < 27 { - f.render_widget( - Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), - size, - ); - return; - } - - let max_width = 150; - let max_height = 30; - - let content_width = if max_width < size.width { - max_width - } else { - size.width - }; - let content_height = if max_height < size.height { - max_height - } else { - size.height - }; - - let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; - let vertical_margin = (size.height.saturating_sub(content_height)) / 2; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(7), Constraint::Length(3)]) - .split(Rect { - x: horizontal_margin, - y: vertical_margin, - width: content_width, - height: content_height, - }); - - let mut text_lines: Vec = Vec::new(); - let mut header_lines = 0; - let separator = if size.width > 72 { - header_lines += 3; - text_lines.push(Line::from( - "You previously accepted earlier versions of Tensamin’s legal documents.", - )); - text_lines.push(Line::from( - "Some of them have been updated and are listed below for your review.", - )); - text_lines.push(Line::from("")); - 2 - } else { - header_lines += 5; - text_lines.push(Line::from("You previously accepted earlier ")); - text_lines.push(Line::from("versions of Tensamin’s legal documents.")); - text_lines.push(Line::from("Some of them have been updated and")); - text_lines.push(Line::from("are listed below for your review.")); - text_lines.push(Line::from("")); - 4 - }; - - if self.eula_needed { - header_lines += 1; - if self.eula_future { - if size.width < 80 { - text_lines.push(checkbox( - "EULA ¹³ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - - header_lines += 1; - let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - - header_lines += 1; - let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "EULA ¹ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - } else { - text_lines.push(checkbox( - "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - } - } - } - - if self.tos_needed { - header_lines += 1; - if self.tos_future { - if size.width < 80 { - text_lines.push(checkbox( - "ToS ²³ (https://legal.tensamin.net/tos/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "ToS ² (https://legal.tensamin.net/tos/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - } else { - text_lines.push(checkbox( - "Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - } - } - } - - if self.pp_needed { - header_lines += 1; - if self.pp_future { - if size.width < 80 { - text_lines.push(checkbox( - "PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "PP ² (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - } else { - text_lines.push(checkbox( - "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - } - } - } - - text_lines.push(Line::from("")); - text_lines.push(Line::from("¹ Necessary to run the program")); - text_lines.push(Line::from("² Optional - only for Tensamin services")); - if size.width < 100 { - text_lines.push(Line::from( - "³ Future version - consent stored now, takes effect later", - )); - } else { - text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply.")); - } - - text_lines.push(Line::from("")); - - let mut optional_lines: Vec = if size.width > 143 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 6, - header_lines, - header_lines + 6, - separator, - header_lines + 2, - ] - } - } else if size.width > 92 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you agree to the End User", - )); - text_lines.push(Line::from( - "License Agreement and applicable Terms of Service.", - )); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you agree to the End User", - )); - text_lines.push(Line::from( - "License Agreement and applicable Terms of Service.", - )); - } - - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 8, - header_lines, - header_lines + 8, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } - } else if size.width > 73 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you read, understood and", - )); - text_lines.push(Line::from( - "agree to the End User License Agreement and applicable Terms of Service.", - )); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you read, understood and", - )); - text_lines.push(Line::from( - "agree to the End User License Agreement and applicable Terms of Service.", - )); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the ToS and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "While having a document selected press O to view in this UI or press L", - )); - text_lines.push(Line::from("to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 8, - header_lines, - header_lines + 8, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } - } else { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from("By selecting Continue, you confirm that you")); - text_lines.push(Line::from("agree to the End User License")); - text_lines.push(Line::from("Agreement and applicable Terms of Service.")); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate", - )); - text_lines.push(Line::from("once these changes apply.")); - } else { - text_lines.push(Line::from("By selecting Continue, you confirm that you")); - text_lines.push(Line::from("agree to the End User License")); - text_lines.push(Line::from("Agreement and applicable Terms of Service.")); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from("Tensamin services require acceptance of the")); - text_lines.push(Line::from("Terms of Service and Privacy Policy.")); - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "While having a document selected press O to view", - )); - text_lines.push(Line::from("in this UI or press L to open as a link.")); - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 10, - header_lines, - header_lines + 11, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 8, - header_lines, - header_lines + 9, - separator, - header_lines + 2, - ] - } - }; - - while size.height - 5 < text_lines.len() as u16 { - if optional_lines.len() == 0 { - break; - } - text_lines.remove(optional_lines[0] as usize); - optional_lines.remove(0); - } - needed_height += text_lines.len(); - - let q_informer = if self.update_needed { - "Q to Exit" - } else { - "Q to Cancel" - }; - if size.width < 60 || size.height < needed_height as u16 { - let width_style = if size.width > 76 { - Style::default().fg(Color::Green) - } else if size.width >= 60 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let height_style = if size.height > 20 { - Style::default().fg(Color::Green) - } else if size.height >= (header_lines as u16 + 10) { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let warning_text = Text::from(vec![ - Line::from(vec![ - Span::raw("Width: "), - Span::styled(format!("{}", size.width), width_style), - Span::raw(" / 60"), - ]), - Line::from(vec![ - Span::raw("Height: "), - Span::styled(format!("{}", size.height), height_style), - Span::raw(format!(" / {}", header_lines + 10)), - ]), - ]); - - let warning = Paragraph::new(warning_text) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("UI Too Small [{}]", q_informer)), - ); - - f.render_widget(warning, size); - return; - } - - let consent_block = Paragraph::new(Text::from(text_lines)).block( - Block::default() - .title(format!(" Update Tensamin User Consent [{}] ", q_informer)) - .borders(Borders::ALL), - ); - f.render_widget(consent_block, chunks[0]); - - let downgrade_scenario = self.tos_needed || self.pp_needed; - draw_buttons( - f, - chunks[1], - self.focus, - (self.eula, self.tos && self.pp), - self.update_needed, - downgrade_scenario, - self.pp_needed || self.tos_needed, - ); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - let mut possible_states = Vec::new(); - - if self.eula_needed { - possible_states.push(Focus::Eula); - } - if self.tos_needed { - possible_states.push(Focus::Tos); - } - if self.pp_needed { - possible_states.push(Focus::Pp); - } - - possible_states.push(Focus::Cancel); - - if self.eula { - possible_states.push(Focus::Continue); - if self.tos && self.pp { - possible_states.push(Focus::ContinueAll); - } - } - - match event.code { - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - return InteractionResult::CloseScreen; - } - KeyCode::Enter | KeyCode::Char(' ') => match self.focus { - Focus::Eula => { - self.eula = !self.eula; - self.tos = !self.tos_needed; - self.pp = !self.pp_needed; - InteractionResult::Handled - } - Focus::Tos if self.eula => { - self.tos = !self.tos; - InteractionResult::Handled - } - Focus::Pp if self.eula => { - self.pp = !self.pp; - InteractionResult::Handled - } - Focus::Cancel => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - Focus::Continue if self.eula => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptEULA); - } - InteractionResult::CloseScreen - } - Focus::ContinueAll if self.eula && self.tos && self.pp => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptAll); - } - InteractionResult::CloseScreen - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Char('o') | KeyCode::Char('O') => { - let terms_type = match self.focus { - Focus::Eula => Some(Type::EULA), - Focus::Tos => Some(Type::TOS), - Focus::Pp => Some(Type::PP), - _ => None, - }; - - if let Some(terms_type) = terms_type { - let fut = Box::pin(async move { - let content = get_terms(terms_type.clone()).await.unwrap(); - Box::new(FileViewer::new(terms_type.to_string(), &content)) - as Box - }); - - return InteractionResult::OpenFutureScreen { screen: fut }; - } else { - InteractionResult::Unhandled - } - } - KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { - Focus::Eula => { - let _ = open::that(get_newest_link(Type::EULA)); - InteractionResult::Handled - } - Focus::Tos => { - let _ = open::that(get_newest_link(Type::TOS)); - InteractionResult::Handled - } - Focus::Pp => { - let _ = open::that(get_newest_link(Type::PP)); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Up | KeyCode::Left => { - self.focus.prev(&possible_states); - InteractionResult::Handled - } - KeyCode::Down | KeyCode::Right | KeyCode::Tab => { - self.focus.next(&possible_states); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - } - } -} diff --git a/src/gui/tui.rs b/src/gui/tui.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/gui/ui.rs b/src/gui/ui.rs deleted file mode 100644 index 5903a4b..0000000 --- a/src/gui/ui.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - ACTIVE_TASKS, SHUTDOWN, - gui::{ - input_handler::setup_input_handler, interaction_result::InteractionResult, - screens::screens::Screen, - }, -}; -use crossterm::event::KeyEvent; -use once_cell::sync::Lazy; -use ratatui::{Terminal, backend::CrosstermBackend, init}; -use std::{ - collections::VecDeque, - io::Stdout, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; -use tokio::{sync::RwLock, time::Instant}; - -/// UI state and rendering -pub static UNIQUE: AtomicBool = AtomicBool::new(true); - -pub static FPS: Lazy> = Lazy::new(|| RwLock::new((0.0, 0.0))); - -pub struct UI { - pub terminal: Arc>>>, - screen_stack: Arc>>>, -} - -pub fn start_tui() -> Arc { - let ui = Arc::new(UI::new()); - let uic = ui.clone(); - ACTIVE_TASKS.insert("UI Renderer".to_string()); - tokio::spawn(async move { - let mut last_render = Instant::now(); - - let mut fps_samples: VecDeque = VecDeque::with_capacity(20); - let mut skip_samples: VecDeque = VecDeque::with_capacity(20); - - let mut fps_sum = 0.0; - let mut skip_sum: u32 = 0; - - let mut skipped = 0; - - loop { - if *SHUTDOWN.read().await { - break; - } - - if skipped > 5 || UNIQUE.load(Ordering::Relaxed) { - uic.render().await; - - skip_samples.push_back(skipped); - skip_sum += skipped as u32; - - if skip_samples.len() > 20 { - if let Some(old) = skip_samples.pop_front() { - skip_sum -= old as u32; - } - } - - skipped = 0; - - let elapsed = last_render.elapsed().as_secs_f64(); - if elapsed > 0.0 { - let fps = 1.0 / elapsed; - - fps_samples.push_back(fps); - fps_sum += fps; - - if fps_samples.len() > 20 { - if let Some(old) = fps_samples.pop_front() { - fps_sum -= old; - } - } - } - - let avg_fps = if !fps_samples.is_empty() { - fps_sum / fps_samples.len() as f64 - } else { - 0.0 - }; - - let avg_skips_percentage = if !skip_samples.is_empty() { - let avg_skipped = skip_sum as f64 / skip_samples.len() as f64; - let total_iterations = avg_skipped + 1.0; - (avg_skipped / total_iterations) * 100.0 - } else { - 0.0 - }; - - *FPS.write().await = (avg_fps, avg_skips_percentage); - - last_render = Instant::now(); - UNIQUE.store(false, Ordering::Relaxed); - } else { - skipped += 1; - } - tokio::time::sleep(Duration::from_millis(16)).await; - } - ACTIVE_TASKS.remove("UI Renderer"); - ratatui::restore(); - }); - setup_input_handler(ui.clone()); - ui -} -impl UI { - pub fn new() -> Self { - let terminal = init(); - Self { - terminal: Arc::new(Mutex::new(terminal)), - screen_stack: Arc::new(RwLock::new(Vec::new())), - } - } - - pub async fn set_screen(&self, screen: Box) { - self.screen_stack.write().await.push(screen); - } - pub async fn replace_screen(&self, screen: Box) { - let mut stack = self.screen_stack.write().await; - stack.pop(); - stack.push(screen); - } - pub async fn handle_input(self: Arc, key_event: KeyEvent) { - let result = { - let mut stack = self.screen_stack.write().await; - if let Some(screen) = stack.last_mut() { - screen.handle_input(key_event) - } else { - return; - } - }; - match result { - InteractionResult::OpenScreen { screen } => { - self.set_screen(screen).await; - } - InteractionResult::OpenFutureScreen { screen: fut } => { - let ui = self.clone(); - let screen = fut.await; - ui.set_screen(screen).await; - } - InteractionResult::CloseScreen => { - let mut stack = self.screen_stack.write().await; - stack.pop(); - - if stack.is_empty() { - *SHUTDOWN.write().await = true; - } - } - InteractionResult::Handled => {} - InteractionResult::Unhandled => {} - } - } - - pub async fn render(&self) { - if let Some(screen) = self.screen_stack.read().await.last() { - let mut terminal = self.terminal.lock().unwrap(); - terminal - .draw(|f| { - screen.render(f, f.area()); - }) - .unwrap(); - } - } -} diff --git a/src/gui/util/borders.rs b/src/gui/util/borders.rs deleted file mode 100644 index 34460bc..0000000 --- a/src/gui/util/borders.rs +++ /dev/null @@ -1,63 +0,0 @@ -use ratatui::layout::Rect; -use ratatui::prelude::*; -use ratatui::style::Style; -use ratatui::widgets::Borders; - -fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { - frame - .buffer_mut() - .set_string(x, y, c.to_string(), Style::default()); -} - -pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { - let x0 = area.x; - let y0 = area.y; - let x1 = area.x + area.width - 1; - let y1 = area.y + area.height - 1; - - if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { - let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { - (true, true) => '┼', - (true, false) => '├', - (false, true) => '┬', - (false, false) => '┌', - }; - set_join_char(frame, x0, y0, top_left); - } - - if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { - let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { - (true, true) => '┼', - (true, false) => '┤', - (false, true) => '┬', - (false, false) => '┐', - }; - set_join_char(frame, x1, y0, top_right); - } - - if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { - let bottom_left = match ( - joins.contains(Borders::BOTTOM), - joins.contains(Borders::LEFT), - ) { - (true, true) => '┼', - (true, false) => '├', - (false, true) => '┴', - (false, false) => '└', - }; - set_join_char(frame, x0, y1, bottom_left); - } - - if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { - let bottom_right = match ( - joins.contains(Borders::BOTTOM), - joins.contains(Borders::RIGHT), - ) { - (true, true) => '┼', - (true, false) => '┤', - (false, true) => '┴', - (false, false) => '┘', - }; - set_join_char(frame, x1, y1, bottom_right); - } -} diff --git a/src/langu/language_creator.rs b/src/langu/language_creator.rs deleted file mode 100644 index fc6d1c5..0000000 --- a/src/langu/language_creator.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::util::file_util::save_file; -use json::{self, JsonError, JsonValue}; - -pub fn create_languages() -> Result<(), JsonError> { - let mut frontend_messages = JsonValue::new_object(); - let mut omikron_messages = JsonValue::new_object(); - let mut button_texts = JsonValue::new_object(); - let mut general_texts = JsonValue::new_object(); - let mut debug_messages = JsonValue::new_object(); - - frontend_messages.insert("error", "An error occurred")?; - // FRONTEND - frontend_messages.insert("get_chats", "User {} is loading conversations")?; - frontend_messages.insert("message_get", "User {} is loading messages")?; - frontend_messages.insert("get_communities", "User {} is loading communities")?; - frontend_messages.insert("client_connected", "Client {} connected")?; - frontend_messages.insert("add_conversation", "User {} added {}")?; - frontend_messages.insert("message_send", "User {} sent a message")?; - - // OMIKRON - omikron_messages.insert( - "identification_response", - "IOTA identified on Omikron, {} users!", - )?; - omikron_messages.insert( - "send_message_failed", - "Failed to send message to Omikron: {}", - )?; - omikron_messages.insert("connection_failed", "Failed to connect to Omikron: {}")?; - - // BUTTONS - button_texts.insert("exit", "Exit")?; - - // GENERAL - general_texts.insert("iota_id", "IOTA ID: {}-####-####-####-############")?; - general_texts.insert("user_id", "USER ID: {}")?; - general_texts.insert("user_ids", "USER IDS: {}")?; - general_texts.insert("user_load_failed", "Failed to load user data")?; - general_texts.insert("setup_completed", "Launched")?; - general_texts.insert( - "community_active", - "Communities active on ws://{}:{}/community/...", - )?; - general_texts.insert( - "community_start_error", - "Failed to start community socket on port {}!", - )?; - general_texts.insert( - "community_start_error_admin", - "Failed to start community socket on port {}! Run with admin privileges", - )?; - // DEBUG - debug_messages.insert("", "")?; - save_file( - "languages/en_INT", - "frontend.json", - &frontend_messages.to_string(), - ); - save_file( - "languages/en_INT", - "omikron.json", - &omikron_messages.to_string(), - ); - save_file( - "languages/en_INT", - "buttons.json", - &button_texts.to_string(), - ); - save_file( - "languages/en_INT", - "debug.json", - &debug_messages.to_string(), - ); - save_file( - "languages/en_INT", - "general.json", - &general_texts.to_string(), - ); - Ok(()) -} diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs deleted file mode 100644 index c903886..0000000 --- a/src/langu/language_manager.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::util::file_util::{self}; -use json::parse; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Mutex; - -#[derive(Clone)] -pub struct LanguagePack { - language: HashMap, -} - -pub static LANGUAGE_PACK: Lazy> = - 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 { - 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); -} - -pub fn from_key(key: &str) -> String { - LANGUAGE_PACK - .lock() - .unwrap() - .get_translation(key) - .to_string() -} - -pub fn format(key: &str, args: &[&str]) -> String { - let message = from_key(key); - let mut formatted = String::new(); - let parts = message.split("{}"); - for (i, part) in parts.enumerate() { - formatted.push_str(part); - if i < args.len() { - formatted.push_str(args[i]); - } - } - formatted -} - -impl LanguagePack { - pub fn new(language: &str) -> Self { - let mut pack = LanguagePack { - language: HashMap::new(), - }; - pack.load_language(language); - pack - } - - pub fn load_language(&mut self, language: &str) { - let path = format!("languages/{}/", language); - - let frontend_messages = file_util::load_file(&path, "frontend.json"); - let frontend_messages = parse(&frontend_messages).unwrap(); - for (key, value) in frontend_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let omikron_messages = file_util::load_file(&path, "omikron.json"); - let omikron_messages = parse(&omikron_messages).unwrap(); - for (key, value) in omikron_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let button_texts = file_util::load_file(&path, "buttons.json"); - let button_texts = parse(&button_texts).unwrap(); - for (key, value) in button_texts.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let debug_messages = file_util::load_file(&path, "debug.json"); - let debug_messages = parse(&debug_messages).unwrap(); - for (key, value) in debug_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let general_messages = file_util::load_file(&path, "general.json"); - let general_messages = parse(&general_messages).unwrap(); - for (key, value) in general_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - } - pub fn get_translation(&self, key: &str) -> String { - match self.language.get(key) { - Some(v) if !v.is_empty() => v.clone(), - _ => key.to_uppercase(), - } - } -} diff --git a/src/langu/mod.rs b/src/langu/mod.rs deleted file mode 100644 index 352d639..0000000 --- a/src/langu/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod language_creator; -pub mod language_manager; diff --git a/src/main.rs b/src/main.rs index 767106f..fa511ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,183 +1,51 @@ -use dashmap::DashSet; -use once_cell::sync::Lazy; -use pnet::datalink::NetworkInterface; -use std::sync::Arc; -use std::sync::LazyLock; -use std::sync::Mutex; -use tokio::sync::RwLock; -use tokio::time::{Duration, sleep}; - -mod auth; -mod gui; -mod langu; -mod omikron; -mod terms; -mod users; +mod anonymous_clients; +mod calls; +mod data; +mod omega; +mod rho; mod util; -use crate::gui::app_state; -use crate::gui::app_state::AppState; -use crate::gui::screens::main_screen::MainScreen; -use crate::gui::ui::start_tui; -use crate::langu::language_creator; -use crate::omikron::omikron_connection::OmikronConnection; -use crate::terms::consent_state; -use crate::users::user_manager; -use crate::util::config_util::CONFIG; -use crate::util::file_util::download_and_extract_zip; -use crate::util::file_util::has_dir; -use crate::util::logger; +use std::env; -pub static APP_STATE: LazyLock>> = - LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); +use dotenv::dotenv; +use once_cell::sync::Lazy; +use rustls::crypto::aws_lc_rs::default_provider; -pub static SHUTDOWN: Lazy> = Lazy::new(|| RwLock::new(false)); -pub static RELOAD: Lazy> = Lazy::new(|| RwLock::new(true)); -pub static ACTIVE_TASKS: Lazy> = Lazy::new(|| DashSet::new()); +use crate::{ + calls::call_util::garbage_collect_calls, + omega::omega_connection::get_omega_connection, + rho::server::start, + util::{ + crypto_helper::{load_public_key, load_secret_key}, + logger::startup, + }, +}; -#[tokio::main(flavor = "multi_thread", worker_threads = 16)] -#[allow(unused_must_use, dead_code)] +static PRIVATE_KEY: Lazy = Lazy::new(|| env::var("PRIVATE_KEY").unwrap()); +pub fn get_private_key() -> x448::Secret { + load_secret_key(&*PRIVATE_KEY).unwrap() +} +static PUBLIC_KEY: Lazy = Lazy::new(|| env::var("PUBLIC_KEY").unwrap()); +pub fn get_public_key() -> x448::PublicKey { + load_public_key(&*PUBLIC_KEY).unwrap() +} + +#[tokio::main] async fn main() { - while *RELOAD.read().await { - *RELOAD.write().await = false; - *SHUTDOWN.write().await = false; - - let ui = start_tui(); - - let (eula, tos_pp) = consent_state::check(ui.clone()).await; - - if !eula { - *SHUTDOWN.write().await = true; - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_millis(100)).await; - } - println!("You need to accept our End User Licence Agreement before launching!"); - println!("You can find this at 'agreements'!"); - return; - } - if !tos_pp { - *SHUTDOWN.write().await = true; - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_millis(100)).await; - } - println!( - "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" - ); - println!("In future releases this will be optional!"); - println!("You can find this at 'agreements'!"); - return; - } - app_state::setup(); - - let main_screen = MainScreen::new(ui.clone()).await; - ui.set_screen(Box::new(main_screen)).await; - - // LANGUAGE PACK - if let Err(e) = language_creator::create_languages() { - println!("Language pack creation failed: {}", e); - return; - } - - // UI - logger::startup(); - - // BASIC CONFIGURATION - &CONFIG.write().await.load(); - - // USER MANAGEMENT - if let Err(_) = user_manager::load_users().await { - log_t!("user_load_failed"); - } + if let Err(_) = default_provider().install_default() { + println!("Error loading Provider"); + return; + } + dotenv().ok(); + startup(); - let mut sb = "".to_string(); - - for up in user_manager::get_users() { - sb = sb + "," + &up.user_id.to_string().as_str(); - } - - if !sb.is_empty() { - {} - sb.remove(0); - sb = sb + ","; + get_omega_connection(); + tokio::spawn(async move { + if let Err(e) = start(959).await { + log_err!(0, util::logger::PrintType::General, "{}", e); } - log!( - "IOTA ID: {}", - CONFIG.read().await.get_iota_id().to_string() - ); - log!("User IDS: {}", sb); + }); + garbage_collect_calls(); - // COMMUNITY MANAGEMENT - /* registry::load_interactables().await; - community_manager::load_communities().await; - community_manager::save_communities().await; - let mut sb1 = "".to_string(); - for cp in community_manager::get_communities().await { - sb1 = sb1 + "," + &cp.get_name().to_string().as_str(); - } - - if !sb1.is_empty() { - sb1.remove(0); - sb1 = sb1 + ","; - } - log!("Community IDS: {}", sb1); */ - let port = CONFIG.read().await.get_port(); - let mut ip = "0.0.0.0".to_string(); - for iface in pnet::datalink::interfaces() { - let iface: NetworkInterface = iface; - if iface.ips.len() > 0 { - let ipsv = format!("{}", iface.ips[0]); - let ips: &str = ipsv.split('/').next().unwrap_or(""); - if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") { - ip = ips.to_string(); - } - } - } - /* - if start(port).await { - log_t!("community_active", ip, port.to_string()); - } else { - if port < 1024 { - log_t!("community_start_error_admin", port.to_string()); - } else { - log_t!("community_start_error", port.to_string()); - } - } */ - if !has_dir("web") { - download_and_extract_zip( - "https://omega.tensamin.net/api/download/iota_frontend", - "web", - ) - .await; - } - let _ = omikron::omikron_connection::get_omikron_connection().await; - - log_t!("setup_completed"); - loop { - if *SHUTDOWN.read().await { - break; - } - - sleep(Duration::from_millis(100)).await; - } - if *RELOAD.read().await { - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_secs(1)).await; - } - &CONFIG.write().await.clear(); - user_manager::clear(); - /*community_manager::clear();*/ - *APP_STATE.lock().unwrap() = AppState::new(); - } - ui.terminal.lock().unwrap().clear(); - ui.terminal.lock().unwrap().flush(); - } + tokio::signal::ctrl_c().await.unwrap(); } diff --git a/src/omega/mod.rs b/src/omega/mod.rs new file mode 100644 index 0000000..cbe77d9 --- /dev/null +++ b/src/omega/mod.rs @@ -0,0 +1 @@ +pub mod omega_connection; diff --git a/src/omega/omega_connection.rs b/src/omega/omega_connection.rs new file mode 100644 index 0000000..ab84eb2 --- /dev/null +++ b/src/omega/omega_connection.rs @@ -0,0 +1,741 @@ +use crate::{ + data::user::UserStatus, + get_private_key, log, log_cv_in, log_cv_out, log_err, log_in, + rho::rho_manager::{self, RHO_CONNECTIONS, connection_count}, + util::{ + crypto_helper::{decrypt_b64, secret_key_to_base64}, + file_util::load_file_vec, + logger::PrintType, + }, +}; +use dashmap::DashMap; +use once_cell::sync::Lazy; +use std::{collections::HashMap, env, sync::Arc, time::Duration}; +use tokio::{ + sync::{Mutex, RwLock, mpsc, watch}, + task::JoinHandle, + time::{Instant, sleep}, +}; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; +use ttp_native::{Receiver, Sender}; +use uuid::Uuid; + +// ============================================================================ +// Configuration +// ============================================================================ + +const OMEGA_HOST_DEFAULT: &str = "methanium.net"; +const OMEGA_PORT_DEFAULT: u16 = 9187; +const RECONNECT_DELAY: Duration = Duration::from_secs(5); +const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); +const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); +const TASK_MAX_AGE: Duration = Duration::from_secs(60); + +// ============================================================================ +// Waiting Task System +// ============================================================================ + +pub struct WaitingTask { + pub task: Box, CommunicationValue) -> bool + Send + Sync>, + pub inserted_at: Instant, +} + +pub static WAITING_TASKS: Lazy> = Lazy::new(DashMap::new); + +pub fn start_task_cleanup_loop() { + tokio::spawn(async { + loop { + sleep(TASK_CLEANUP_INTERVAL).await; + WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); + } + }); +} + +// ============================================================================ +// Connection State +// ============================================================================ + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ConnectionState { + Disconnected, + Connecting, + Connected { identified: bool }, +} + +#[allow(unused_variables)] +impl ConnectionState { + pub fn is_connected(&self) -> bool { + match self { + ConnectionState::Connected { identified } => true, + _ => false, + } + } + + #[allow(dead_code)] + pub fn is_identified(&self) -> bool { + match self { + ConnectionState::Connected { identified: true } => true, + _ => false, + } + } +} + +// ============================================================================ +// Omega Connection (Client-side with auto-reconnect) +// ============================================================================ +#[allow(dead_code)] +pub struct OmegaConnection { + state: Arc>, + sender: Arc>>>, + connection_loop_handle: Arc>>>, + host: String, + port: u16, + server_cert: Vec, + last_ping: Arc>, + heartbeat_handle: Arc>>>, + message_send_times: Arc>>, + pub connection_id: Uuid, + shutdown_tx: Arc>>>, + // Track if we should reconnect on close + reconnect_on_close: Arc>, +} + +impl OmegaConnection { + pub fn new() -> Self { + Self::with_host(OMEGA_HOST_DEFAULT, OMEGA_PORT_DEFAULT) + } + + pub fn with_host(host: &str, port: u16) -> Self { + // Load server certificate from default location + let server_cert = + load_file_vec("certs", "cert.pem").expect("Failed to load server certificate"); + + Self::with_host_and_cert(host, port, server_cert) + } + + // New constructor that accepts certificate directly + pub fn with_host_and_cert(host: &str, port: u16, server_cert: Vec) -> Self { + let (shutdown_tx, _) = watch::channel(false); + + OmegaConnection { + state: Arc::new(RwLock::new(ConnectionState::Disconnected)), + sender: Arc::new(RwLock::new(None)), + connection_loop_handle: Arc::new(Mutex::new(None)), + host: host.to_string(), + port, + server_cert, + last_ping: Arc::new(Mutex::new(-1)), + heartbeat_handle: Arc::new(Mutex::new(None)), + message_send_times: Arc::new(Mutex::new(HashMap::new())), + connection_id: Uuid::new_v4(), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + reconnect_on_close: Arc::new(RwLock::new(true)), + } + } + + // ------------------------------------------------------------------------- + // Connection Management + // ------------------------------------------------------------------------- + + pub async fn start(self: Arc) { + // Cancel any existing connection loop + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + // Set reconnect flag + *self.reconnect_on_close.write().await = true; + + let self_clone = self.clone(); + let handle = tokio::spawn(async move { + self_clone.connection_loop().await; + }); + + *self.connection_loop_handle.lock().await = Some(handle); + } + + #[allow(dead_code)] + pub async fn stop(&self) { + // Disable reconnection + *self.reconnect_on_close.write().await = false; + + if let Some(tx) = self.shutdown_tx.lock().await.take() { + let _ = tx.send(true); + } + + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + // Close sender if connected + if let Some(sender) = self.sender.read().await.as_ref() { + sender.close(); + } + + *self.state.write().await = ConnectionState::Disconnected; + *self.sender.write().await = None; + } + + async fn connection_loop(self: Arc) { + let mut reconnect_delay = RECONNECT_DELAY; + let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); + let mut shutdown_rx = shutdown_rx; + + loop { + if *shutdown_rx.borrow() { + log_in!(0, PrintType::Omega, "Connection loop shutting down"); + break; + } + + // Check if reconnection is enabled + if !*self.reconnect_on_close.read().await { + log_in!(0, PrintType::Omega, "Reconnection disabled, exiting loop"); + break; + } + + match self.clone().connect_once().await { + Ok(()) => { + // Connection closed gracefully, check if we should reconnect + if *self.reconnect_on_close.read().await { + log_err!( + 0, + PrintType::Omega, + "Connection lost, reconnecting in {:?}...", + reconnect_delay + ); + } else { + log_in!(0, PrintType::Omega, "Connection closed, not reconnecting"); + break; + } + } + Err(e) => { + log_err!( + 0, + PrintType::Omega, + "Connection failed: {}, retrying in {:?}...", + e, + reconnect_delay + ); + } + } + + tokio::select! { + _ = sleep(reconnect_delay) => {} + _ = shutdown_rx.changed() => { + if *shutdown_rx.borrow() { + break; + } + } + } + + reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); + } + } + + async fn connect_once(self: Arc) -> Result<(), String> { + *self.state.write().await = ConnectionState::Connecting; + + let addr_str = format!("https://{}:{}", self.host, self.port); + + let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) + .await + .map_err(|e| format!("Connection failed: {}", e))?; + + log_in!( + 0, + PrintType::Omega, + "QUIC connection established to {}", + addr_str + ); + + // Store sender + let sender_arc = Arc::new(sender); + *self.sender.write().await = Some(sender_arc.clone()); + *self.state.write().await = ConnectionState::Connected { identified: false }; + + // Get handle for close monitoring + let sender_handle = sender_arc.handle().clone(); + + // Start read loop + let read_self = self.clone(); + let read_handle = tokio::spawn(async move { + read_self.read_loop(&mut receiver, sender_handle).await; + }); + + // Send identification + self.send_identification().await; + + // Start heartbeat + let heartbeat_self = self.clone(); + let heartbeat_handle = tokio::spawn(async move { + heartbeat_self.heartbeat_loop().await; + }); + *self.heartbeat_handle.lock().await = Some(heartbeat_handle); + + // Wait for read loop to complete (connection closed) + let result = read_handle.await; + + // Cleanup + *self.sender.write().await = None; + *self.state.write().await = ConnectionState::Disconnected; + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + match result { + Ok(()) => { + // Check if we should reconnect + if *self.reconnect_on_close.read().await { + Err("Connection closed, will reconnect".to_string()) + } else { + Ok(()) + } + } + Err(e) => Err(format!("Read loop error: {}", e)), + } + } + + // ------------------------------------------------------------------------- + // Identification Handshake + // ------------------------------------------------------------------------- + + async fn send_identification(&self) { + let id = rand_u32(); + + let omikron_id = env::var("ID") + .unwrap_or("0".to_string()) + .parse::() + .unwrap_or(0); + + let identify_msg = CommunicationValue::new(CommunicationType::identification) + .with_id(id) + .add_data(DataTypes::omikron_id, DataValue::Number(omikron_id)); + + WAITING_TASKS.insert( + id, + WaitingTask { + task: Box::new(|selfc, cv| { + if cv.is_type(CommunicationType::error_not_found) { + log_err!( + 0, + PrintType::Omega, + "Identification failed: Omikron ID not found" + ); + return false; + } + if !cv.is_type(CommunicationType::challenge) { + return false; + } + + tokio::spawn(async move { + if let Err(e) = selfc.handle_challenge(cv).await { + log_err!(0, PrintType::Omega, "Challenge handling failed: {}", e); + } + }); + true + }), + inserted_at: Instant::now(), + }, + ); + + self.send_message(&identify_msg).await; + } + + async fn handle_challenge(&self, cv: CommunicationValue) -> Result<(), String> { + let challenge = cv + .get_data(DataTypes::challenge) + .as_str() + .ok_or("Challenge not found")?; + + let server_pub_key = cv + .get_data(DataTypes::public_key) + .as_str() + .ok_or("Public key not found")?; + + let decrypted_challenge = decrypt_b64( + &secret_key_to_base64(&get_private_key()), + server_pub_key, + challenge, + ) + .map_err(|e| format!("Decryption failed: {:?}", e))?; + + let response_msg = CommunicationValue::new(CommunicationType::challenge_response) + .with_id(cv.get_id()) + .add_data(DataTypes::challenge, DataValue::Str(decrypted_challenge)); + + let response_id = response_msg.get_id(); + + WAITING_TASKS.insert( + response_id, + WaitingTask { + task: Box::new(|selfc, final_cv| { + if !final_cv.is_type(CommunicationType::identification_response) { + log_err!(0, PrintType::Omega, "Expected identification_response"); + return false; + } + + let accepted = final_cv + .get_data(DataTypes::accepted) + .as_bool() + .unwrap_or(false); + + if !accepted { + log_err!(0, PrintType::Omega, "Omega did not accept identification"); + return false; + } + + tokio::spawn(async move { + let mut state = selfc.state.write().await; + if let ConnectionState::Connected { identified: _ } = *state { + *state = ConnectionState::Connected { identified: true }; + } + drop(state); + + selfc.sync_client_iota_status().await; + }); + + log!(0, PrintType::Omega, "Successfully identified with Omega"); + true + }), + inserted_at: Instant::now(), + }, + ); + + self.send_message(&response_msg).await; + Ok(()) + } + + async fn sync_client_iota_status(self: Arc) { + let mut connected_iota_ids: Vec = Vec::new(); + let mut connected_user_ids: Vec = Vec::new(); + + let rho_connections_reader = RHO_CONNECTIONS.read().await; + + for iota_id in rho_connections_reader.keys() { + connected_iota_ids.push(DataValue::Number(*iota_id)); + } + + for rho in rho_connections_reader.values() { + for client_conn in rho.get_client_connections().await { + connected_user_ids.push(DataValue::Number(client_conn.get_user_id().await as i64)); + } + } + + drop(rho_connections_reader); + + let sync_msg = CommunicationValue::new(CommunicationType::sync_client_iota_status) + .add_data(DataTypes::iota_ids, DataValue::Array(connected_iota_ids)) + .add_data(DataTypes::user_ids, DataValue::Array(connected_user_ids)) + .add_data( + DataTypes::rho_connections, + DataValue::Number(connection_count().await as i64), + ); + + self.send_message(&sync_msg).await; + } + + // ------------------------------------------------------------------------- + // Read Loop & Heartbeat + // ------------------------------------------------------------------------- + + async fn read_loop( + self: Arc, + receiver: &mut Receiver, + sender_handle: Arc, + ) { + // Monitor both receiver and sender handle for close + let mut close_rx = sender_handle.subscribe_close(); + + loop { + tokio::select! { + result = receiver.receive() => { + match result { + Ok(cv) => { + if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { + log_cv_in!(PrintType::Omega, &cv); + } + + if cv.is_type(CommunicationType::pong) || cv.is_type(CommunicationType::ping) { + self.handle_pong(&cv).await; + continue; + } + + let msg_id = cv.get_id(); + if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { + if (task.task)(self.clone(), cv.clone()) { + continue; + } + } + + if cv.is_type(CommunicationType::iota_user_data) { + if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) { + let mut user_ids: Vec = Vec::new(); + for value in users { + if let DataValue::Number(user_id) = value { + user_ids.push(*user_id as u64); + } + } + let connections = crate::rho::rho_manager::RHO_CONNECTIONS.read().await; + if let Some(iota_id) = cv.get_data(DataTypes::iota_id).as_number() { + if let Some(rho) = connections.get(&iota_id) { + rho.get_iota_connection().set_user_ids(user_ids).await; + } + } else { + for rho in connections.values() { + rho.get_iota_connection().set_user_ids(user_ids.clone()).await; + } + } + } + } + } + Err(e) => { + log_err!(0, PrintType::Omega, "Receive error: {}", e); + break; + } + } + } + _ = close_rx.changed() => { + // Connection was closed by either side + if let Some(reason) = close_rx.borrow().clone() { + log_err!(0, PrintType::Omega, "Connection closed: {:?}", reason); + } else { + log_in!(0, PrintType::Omega, "Connection closed cleanly"); + } + break; + } + } + } + } + + async fn heartbeat_loop(self: Arc) { + loop { + sleep(HEARTBEAT_INTERVAL).await; + + // Check if still connected + if !self.state.read().await.is_connected() { + break; + } + + // Check if sender is closed + if let Some(sender) = self.sender.read().await.as_ref() { + if sender.is_closed() { + log_err!(0, PrintType::Omega, "Sender closed, stopping heartbeat"); + break; + } + } else { + break; + } + + self.send_ping().await; + } + } + + async fn send_ping(&self) { + let ping = CommunicationValue::new(CommunicationType::ping).add_data( + DataTypes::send_time, + DataValue::Number( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64, + ), + ); + self.send_message(&ping).await; + } + + async fn handle_pong(&self, cv: &CommunicationValue) { + let timestamp = cv + .get_data(DataTypes::send_time) + .as_number() + .unwrap_or_else(|| { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 + }); + + *self.last_ping.lock().await = timestamp; + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + pub async fn send_message(&self, cv: &CommunicationValue) { + if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { + log_cv_out!(PrintType::Omega, &cv); + } + + let sender_guard = self.sender.read().await; + if let Some(sender) = sender_guard.as_ref() { + // Check if closed before sending + if sender.is_closed() { + log_err!(0, PrintType::Omega, "Cannot send: connection closed"); + drop(sender_guard); + // Trigger reconnection by closing the connection state + if let Some(sender) = self.sender.write().await.take() { + sender.close(); + } + return; + } + + let sender_clone = Arc::clone(sender); + drop(sender_guard); + + if let Err(e) = sender_clone.send(cv).await { + log_err!(0, PrintType::Omega, "Send failed: {}", e); + } + } else { + log_err!(0, PrintType::Omega, "Cannot send: not connected"); + } + } + + pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { + if self.state.read().await.is_connected() { + return Ok(()); + } + + let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); + let start = Instant::now(); + + loop { + if self.state.read().await.is_connected() { + return Ok(()); + } + + if start.elapsed() >= timeout { + return Err(format!( + "Connection not established within {} seconds", + timeout.as_secs() + )); + } + + sleep(Duration::from_millis(100)).await; + } + } + + pub async fn await_response( + &self, + cv: &CommunicationValue, + timeout_duration: Option, + ) -> Result { + self.await_connection(timeout_duration).await?; + + let (tx, mut rx) = mpsc::channel(1); + let msg_id = cv.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new(move |_, response_cv| { + let inner_tx = tx.clone(); + tokio::spawn(async move { + let _ = inner_tx.send(response_cv).await; + }); + true + }), + inserted_at: Instant::now(), + }, + ); + + self.send_message(cv).await; + + let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); + + match tokio::time::timeout(timeout, rx.recv()).await { + Ok(Some(response_cv)) => Ok(response_cv), + Ok(_) => Err("Channel closed".to_string()), + Err(_) => { + WAITING_TASKS.remove(&msg_id); + Err("Request timed out".to_string()) + } + } + } + + #[allow(dead_code)] + pub async fn is_connected(&self) -> bool { + self.state.read().await.is_connected() + } + #[allow(dead_code)] + pub async fn is_identified(&self) -> bool { + self.state.read().await.is_identified() + } + + #[allow(dead_code)] + pub async fn close_iota(iota_id: i64) { + let cv = CommunicationValue::new(CommunicationType::iota_disconnected) + .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); + OMEGA_CONNECTION.send_message(&cv).await; + } + + pub async fn client_changed(_iota_id: i64, user_id: i64, state: UserStatus) { + let msg_type = match state { + UserStatus::iota_offline => CommunicationType::user_disconnected, + UserStatus::user_offline => CommunicationType::user_disconnected, + _ => CommunicationType::user_connected, + }; + + let cv = CommunicationValue::new(msg_type) + .add_data(DataTypes::user_id, DataValue::Number(user_id)); + OMEGA_CONNECTION.send_message(&cv).await; + } + + pub async fn user_states(user_id: i64, user_ids: Vec) { + let user_ids = user_ids.iter().map(|v| DataValue::Number(*v)).collect(); + + let cv = CommunicationValue::new(CommunicationType::get_states) + .add_data(DataTypes::user_ids, DataValue::Array(user_ids)); + let msg_id = cv.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new( + move |_: Arc, response: CommunicationValue| { + tokio::spawn(async move { + let rho = rho_manager::get_rho_con_for_user(user_id).await; + if let Some(rho) = rho { + for client in rho.get_client_connections_for_user(user_id).await { + client.send_message(&response).await; + } + } + }); + true + }, + ), + inserted_at: Instant::now(), + }, + ); + + OMEGA_CONNECTION.send_message(&cv).await; + } +} + +// ============================================================================ +// Global Instance +// ============================================================================ + +static OMEGA_CONNECTION: Lazy> = Lazy::new(|| { + let conn = Arc::new(OmegaConnection::new()); + + // Start the connection manager immediately + let conn_clone = conn.clone(); + tokio::spawn(async move { + conn_clone.start().await; + }); + + start_task_cleanup_loop(); + + conn +}); + +pub fn get_omega_connection() -> Arc { + OMEGA_CONNECTION.clone() +} diff --git a/src/omega/ping_pong_task.rs b/src/omega/ping_pong_task.rs new file mode 100644 index 0000000..c7fc0b9 --- /dev/null +++ b/src/omega/ping_pong_task.rs @@ -0,0 +1,41 @@ +use std::time::Duration; +use tokio::time::Instant; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; + +use crate::omega::omega_connection::OmegaConnection; + +const PING_TIMEOUT: Duration = Duration::from_secs(30); + +impl OmegaConnection { + pub async fn send_ping(&self) { + let id = rand_u32(); + let send_time = Instant::now(); + + let mut message_send_times = self.message_send_times.lock().await; + message_send_times.retain(|_uuid, time| time.elapsed() < PING_TIMEOUT); + message_send_times.insert(id as i64, send_time); + + self.send_ping_message(id).await; + } + + pub async fn send_ping_message(&self, id: u32) { + let ping_message = CommunicationValue::new(CommunicationType::ping) + .with_id(id) + .add_data( + DataTypes::last_ping, + DataValue::Number(self.last_ping.lock().await.unwrap()), + ); + + self.send_message(&ping_message).await; + } + + /// Handles incoming pong and calculates latency + pub async fn handle_pong(&self, cv: &CommunicationValue, _log: bool) { + let id = cv.get_id(); + let mut message_send_times = self.message_send_times.lock().await; + if let Some(send_time) = message_send_times.remove(&(id as i64)) { + let ping = Instant::now().duration_since(send_time).as_millis() as i64; + *self.last_ping.lock().await = ping; + } + } +} diff --git a/src/omikron/mod.rs b/src/omikron/mod.rs deleted file mode 100644 index acb0f59..0000000 --- a/src/omikron/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod omikron_connection; -pub mod ping_pong_task; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs deleted file mode 100755 index 7e92269..0000000 --- a/src/omikron/omikron_connection.rs +++ /dev/null @@ -1,1159 +0,0 @@ -use crate::users::contact::Contact; -use crate::util::chat_files::{MessageState, change_message_state}; -use crate::util::chats_util::{get_user, mod_user}; -use crate::util::communities_util::CommunitiesUtil; -use crate::util::crypto_util::{DataFormat, SecurePayload}; -use crate::util::file_util::{get_children, load_file, save_file}; -use crate::util::{chat_files, chats_util}; -use crate::util::{config_util::CONFIG, crypto_helper}; -use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv_in, log_cv_out, log_t}; -use dashmap::DashMap; -use json::JsonValue; -use std::collections::HashMap; -use std::sync::{Arc, LazyLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, RwLock, mpsc, watch}; -use tokio::task::JoinHandle; -use tokio::time::sleep; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use ttp_native::{Receiver, Sender}; -use uuid::Uuid; - -// ============================================================================ -// Configuration -// ============================================================================ - -const OMIKRON_HOST_DEFAULT: &str = "methanium.net"; -const OMIKRON_PORT_DEFAULT: u16 = 959; -const RECONNECT_DELAY: Duration = Duration::from_secs(5); -const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); -const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); -const TASK_MAX_AGE: Duration = Duration::from_secs(60); - -// ============================================================================ -// Waiting Task System -// ============================================================================ - -pub struct WaitingTask { - pub task: Box, CommunicationValue) -> bool + Send + Sync>, - pub inserted_at: Instant, -} - -pub static WAITING_TASKS: LazyLock> = LazyLock::new(|| DashMap::new()); - -pub fn start_task_cleanup_loop() { - tokio::spawn(async { - loop { - sleep(TASK_CLEANUP_INTERVAL).await; - WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); - } - }); -} - -// ============================================================================ -// Connection State -// ============================================================================ - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected { identified: bool }, -} - -impl ConnectionState { - pub fn is_connected(&self) -> bool { - matches!(self, ConnectionState::Connected { .. }) - } - - pub fn is_identified(&self) -> bool { - matches!(self, ConnectionState::Connected { identified: true }) - } -} - -// ============================================================================ -// Omikron Connection (Client-side with auto-reconnect) -// ============================================================================ - -pub struct OmikronConnection { - state: Arc>, - sender: Arc>>>, - connection_loop_handle: Arc>>>, - host: String, - port: u16, - pub last_ping: Arc>, - heartbeat_handle: Arc>>>, - message_send_times: Arc>>, - pub connection_id: Uuid, - shutdown_tx: Arc>>>, - reconnect_on_close: Arc>, -} - -impl OmikronConnection { - pub fn new() -> Self { - Self::with_host(OMIKRON_HOST_DEFAULT, OMIKRON_PORT_DEFAULT) - } - - pub fn with_host(host: &str, port: u16) -> Self { - let (shutdown_tx, _) = watch::channel(false); - - OmikronConnection { - state: Arc::new(RwLock::new(ConnectionState::Disconnected)), - sender: Arc::new(RwLock::new(None)), - connection_loop_handle: Arc::new(Mutex::new(None)), - host: host.to_string(), - port, - last_ping: Arc::new(Mutex::new(-1)), - heartbeat_handle: Arc::new(Mutex::new(None)), - message_send_times: Arc::new(Mutex::new(HashMap::new())), - connection_id: Uuid::new_v4(), - shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), - reconnect_on_close: Arc::new(RwLock::new(true)), - } - } - - // ------------------------------------------------------------------------- - // Connection Management - // ------------------------------------------------------------------------- - - pub async fn connect(self: &Arc) { - if self.connection_loop_handle.lock().await.is_none() { - self.clone().start().await; - } - } - - pub async fn start(self: Arc) { - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - *self.reconnect_on_close.write().await = true; - - let self_clone = self.clone(); - let handle = tokio::spawn(async move { - self_clone.connection_loop().await; - }); - - *self.connection_loop_handle.lock().await = Some(handle); - } - - pub async fn stop(&self) { - *self.reconnect_on_close.write().await = false; - - if let Some(tx) = self.shutdown_tx.lock().await.take() { - let _ = tx.send(true); - } - - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - if let Some(sender) = self.sender.read().await.as_ref() { - sender.close(); - } - - *self.state.write().await = ConnectionState::Disconnected; - *self.sender.write().await = None; - } - - async fn connection_loop(self: Arc) { - let mut reconnect_delay = RECONNECT_DELAY; - let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); - let mut shutdown_rx = shutdown_rx; - - loop { - if *shutdown_rx.borrow() || *SHUTDOWN.read().await { - log_t!("omikron_connection_loop_shutdown"); - break; - } - - if !*self.reconnect_on_close.read().await { - break; - } - - match self.clone().connect_once().await { - Ok(()) => { - if *self.reconnect_on_close.read().await { - log!("Connection lost, reconnecting in {:?}...", reconnect_delay); - } else { - break; - } - } - Err(e) => { - log!( - "Connection failed: {}, retrying in {:?}...", - e, - reconnect_delay - ); - } - } - - tokio::select! { - _ = sleep(reconnect_delay) => {} - _ = shutdown_rx.changed() => { - if *shutdown_rx.borrow() { - break; - } - } - } - - reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); - } - } - - async fn connect_once(self: Arc) -> Result<(), String> { - *self.state.write().await = ConnectionState::Connecting; - log_t!("omikron_connecting"); - - let addr_str = format!("https://{}:{}/ws/iota/", self.host, self.port); - - let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) - .await - .map_err(|e| format!("Connection failed: {}", e))?; - - log_t!("omikron_connection_success"); - - let sender_arc = Arc::new(sender); - *self.sender.write().await = Some(sender_arc.clone()); - *self.state.write().await = ConnectionState::Connected { identified: false }; - - // Handle registration/identification - self.handle_authentication().await; - - // Start read loop - let read_self = self.clone(); - let read_handle = tokio::spawn(async move { - read_self.read_loop(&mut receiver).await; - }); - - // Start heartbeat - let heartbeat_self = self.clone(); - let heartbeat_handle = tokio::spawn(async move { - heartbeat_self.heartbeat_loop().await; - }); - *self.heartbeat_handle.lock().await = Some(heartbeat_handle); - - { - ACTIVE_TASKS.insert("Omikron Listener".to_string()); - } - - // Wait for read loop to complete - let result = read_handle.await; - *self.sender.write().await = None; - *self.state.write().await = ConnectionState::Disconnected; - { - ACTIVE_TASKS.remove("Omikron Listener"); - } - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - match result { - Ok(()) => { - if *self.reconnect_on_close.read().await { - Err("Connection closed, will reconnect".to_string()) - } else { - Ok(()) - } - } - Err(e) => Err(format!("Read loop error: {}", e)), - } - } - - // ------------------------------------------------------------------------- - // Authentication (Registration/Identification) - // ------------------------------------------------------------------------- - - async fn handle_authentication(&self) { - let conf = CONFIG.read().await; - let iota_id = conf.get_iota_id(); - let public_key = conf.get_public_key(); - let private_key = conf.get_private_key(); - drop(conf); - - if iota_id == 0 || public_key.is_none() || private_key.is_none() { - log_t!("iota_register_new"); - - let key_pair = crypto_helper::generate_keypair(); - let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); - let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); - - let mut conf_write = CONFIG.write().await; - // NOTE: - // Intentionally not storing the generated private/public keys directly into the - // config file here to avoid persisting sensitive material in plaintext. If you - // want to persist them, uncomment the two lines below and accept the security - // implications (they will be saved by `conf_write.update()`). - // conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); - // conf_write.change("private_key", DataValue::Str(private_key_base64)); - conf_write.update(); - drop(conf_write); - - let register_msg = CommunicationValue::new(CommunicationType::register_iota) - .add_data(DataTypes::public_key, DataValue::Str(public_key_base64)); - - let msg_id = register_msg.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new(|selfc, cv| { - if !cv.is_type(CommunicationType::success) { - return false; - } - - let iota_value = cv.get_data(DataTypes::register_id); - let iota_id = iota_value.as_number().unwrap_or(0); - - if iota_id != 0 { - tokio::spawn(async move { - let mut conf_write = CONFIG.write().await; - conf_write.change("iota_id", JsonValue::from(iota_id)); - conf_write.update(); - drop(conf_write); - log!("Registered with Iota-ID: {}", iota_id); - - // Send identification after registration - let identify_msg = - CommunicationValue::new(CommunicationType::identification) - .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); - selfc.send_message(&identify_msg).await; - }); - } else { - log!("Iota registration failed."); - } - true - }), - inserted_at: Instant::now(), - }, - ); - - self.send_message(®ister_msg).await; - } else { - let identify_msg = CommunicationValue::new(CommunicationType::identification) - .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); - self.send_message(&identify_msg).await; - } - } - - // ------------------------------------------------------------------------- - // Read Loop & Heartbeat - // ------------------------------------------------------------------------- - - async fn read_loop(self: Arc, receiver: &mut Receiver) { - loop { - let result = receiver.receive().await; - match result { - Ok(cv) => { - self.clone().handle_message(cv).await; - } - Err(e) => { - self.fail_all_waiting_tasks(format!( - "Connection receive error: {} (connection_id={})", - e, self.connection_id - )) - .await; - break; - } - } - if !receiver.is_open() { - self.fail_all_waiting_tasks(format!( - "Connection closed (connection_id={}, receiver_open=false)", - self.connection_id - )) - .await; - break; - } - } - } - - async fn heartbeat_loop(self: Arc) { - loop { - sleep(HEARTBEAT_INTERVAL).await; - - if !self.state.read().await.is_connected() { - break; - } - - if let Some(sender) = self.sender.read().await.as_ref() { - if !sender.is_open() { - break; - } - } else { - break; - } - - self.send_ping().await; - } - } - - // ------------------------------------------------------------------------- - // Message Handling (Preserved from original) - // ------------------------------------------------------------------------- - - pub async fn handle_message(self: Arc, cv: CommunicationValue) { - if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { - log_cv_in!(&cv); - } - - let msg_id = cv.get_id(); - - // Dispatch waiting task for this message id - if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { - if (task.task)(self.clone(), cv.clone()) { - return; - } - } - - if cv.is_type(CommunicationType::pong) { - self.handle_pong(&cv).await; - return; - } - - if cv.is_type(CommunicationType::challenge) { - self.handle_challenge(&cv).await; - return; - } - - if cv.is_type(CommunicationType::identification_response) { - if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() { - let mut state = self.state.write().await; - if let ConnectionState::Connected { identified: _ } = *state { - *state = ConnectionState::Connected { identified: true }; - } - } - return; - } - - // ************************************************ // - // Direct messages // - // ************************************************ // - - if cv.is_type(CommunicationType::message_state) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); - - // Parse send_time robustly: accept numeric or string, fallback to current time - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp_i64 = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - - let _ = chat_files::change_message_state( - timestamp_i64, - *receiver_id as i64, - *sender_id as i64, - MessageState::from_str( - cv.get_data(DataTypes::message_state).as_str().unwrap_or(""), - ), - ); - } - - // Incoming stored message: store for the recipient, attempt local delivery, notify sender. - if cv.is_type(CommunicationType::message_send) { - let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; - - // parse receiver_id (the storage owner for this incoming message) - let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() - { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; - - // parse send_time robustly (number or string), fallback to now - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp_i64 = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - let timestamp_u128 = timestamp_i64 as u128; - - // content may be missing; default to empty string - let content = cv - .get_data(DataTypes::content) - .as_str() - .unwrap_or("") - .to_string(); - - let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; - - // persist message for the receiver (storage_owner = receiver_id) - chat_files::add_message( - timestamp_u128, - false, - receiver_id as i64, - sender_id as i64, - &content, - height, - ); - - // persist message for the sender (storage_owner = sender_id) - chat_files::add_message( - timestamp_u128, - true, - sender_id as i64, - receiver_id as i64, - &content, - height, - ); - - // send confirmation back to sender - let conf_msg = CommunicationValue::new(CommunicationType::message_send) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&conf_msg).await; - - // Build a live-delivery message for the local client (recipient) - let user_forward = CommunicationValue::new(CommunicationType::message_live) - .with_id(cv.get_id()) - .with_receiver(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data(DataTypes::content, DataValue::Str(content.clone())) - .add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64)) - .add_data(DataTypes::height, DataValue::Number(height)); - - // Attempt delivery and await a response from the local client - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(10))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataTypes::message_state) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - - // update stored message state for receiver - let _ = chat_files::change_message_state( - timestamp_i64, - receiver_id as i64, - sender_id as i64, - ms.clone(), - ); - - // update stored message state for sender - let _ = chat_files::change_message_state( - timestamp_i64, - sender_id as i64, - receiver_id as i64, - ms.clone(), - ); - - // notify original sender about the delivered/read state - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data( - DataTypes::message_state, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; - } else { - // Delivery failed or timed out; mark as Sent - let _ = chat_files::change_message_state( - timestamp_i64, - receiver_id as i64, - sender_id as i64, - MessageState::Sent, - ); - - let _ = chat_files::change_message_state( - timestamp_i64, - sender_id as i64, - receiver_id as i64, - MessageState::Sent, - ); - - // notify sender - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data( - DataTypes::message_state, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; - } - return; - } - - if cv.is_type(CommunicationType::message_other_iota) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); - - // parse send_time safely (number or string), fallback to now - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - - // content may be missing or non-string; default to empty string - let content = cv - .get_data(DataTypes::content) - .as_str() - .unwrap_or("") - .to_string(); - - let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; - - chat_files::add_message( - timestamp as u128, - false, - *receiver_id as i64, - *sender_id as i64, - &content, - height, - ); - - // Build user_forward using the parsed numeric timestamp and safe content string - let user_forward = CommunicationValue::new(CommunicationType::message_live) - .with_id(cv.get_id()) - .with_receiver(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data(DataTypes::content, DataValue::Str(content.clone())) - .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64)) - .add_data(DataTypes::height, DataValue::Number(height)); - - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(10))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataTypes::message_state) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - - let _ = change_message_state( - timestamp, - *receiver_id as i64, - *sender_id as i64, - ms.clone(), - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(*sender_id) - .with_sender(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data( - DataTypes::message_state, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; - } else { - // Delivery timed out/failed — update stored state and notify sender with numeric timestamp - let _ = chat_files::change_message_state( - timestamp, - *receiver_id as i64, - *sender_id as i64, - MessageState::Sent, - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(*sender_id) - .with_sender(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data( - DataTypes::message_state, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; - } - return; - } - - if cv.is_type(CommunicationType::messages_get) { - let my_id = cv.get_sender(); - let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); - let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); - let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); - let mut msg_array: Vec = Vec::new(); - for m in messages.members() { - let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); - let content: String = m["content"].as_str().unwrap_or("").to_string(); - let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false); - let height: i64 = m["height"].as_i64().unwrap_or(0); - let sender_id: i64 = if sent_by_self { - my_id as i64 - } else { - if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { - s.parse::().unwrap_or(partner_id as i64) - } else { - partner_id as i64 - } - }; - let message_state: String = m["message_state"].as_str().unwrap_or("").to_string(); - - let mut container = Vec::new(); - container.push((DataTypes::send_time, DataValue::Number(message_time))); - container.push((DataTypes::content, DataValue::Str(content))); - container.push((DataTypes::sender_id, DataValue::Number(sender_id))); - container.push((DataTypes::message_state, DataValue::Str(message_state))); - container.push((DataTypes::height, DataValue::Number(height))); - container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self))); - msg_array.push(DataValue::Container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::messages_get) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::messages, DataValue::Array(msg_array)); - - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::get_chats) { - let user_id = cv.get_sender(); - let users = chats_util::get_users(user_id as i64); - let mut user_array = Vec::new(); - for user in users { - let mut container = Vec::new(); - container.push((DataTypes::user_id, DataValue::Number(user.user_id))); - if let Some(name) = user.user_name { - container.push((DataTypes::username, DataValue::Str(name))); - } - if let Some(ts) = user.last_message_at { - container.push((DataTypes::last_message_at, DataValue::Number(ts))); - } - user_array.push(DataValue::Container(container)); - } - let resp = CommunicationValue::new(CommunicationType::get_chats) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_data(DataTypes::user_ids, DataValue::Array(user_array)); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::add_conversation) { - let user_id = cv.get_sender(); - let other_id = match cv.get_data(DataTypes::chat_partner_id).as_number() { - Some(n) => n as i64, - None => cv - .get_data(DataTypes::chat_partner_id) - .as_str() - .unwrap_or("0") - .parse() - .unwrap_or(0), - }; - let mut contact = get_user(user_id as i64, other_id).unwrap_or(Contact::new(other_id)); - - if let Some(name) = cv.get_data(DataTypes::chat_partner_name).as_str() { - contact.user_name = Some(name.to_string()); - } - - contact.set_last_message_at( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64, - ); - mod_user(user_id as i64, &contact); - let resp = CommunicationValue::new(CommunicationType::add_conversation) - .with_id(cv.get_id()) - .with_receiver(user_id); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::add_community) { - CommunitiesUtil::add_community( - cv.get_sender() as i64, - cv.get_data(DataTypes::community_address) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataTypes::community_title) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataTypes::position) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::add_community) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::get_communities) { - let mut comm_array = Vec::new(); - for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { - let mut container: Vec<(DataTypes, DataValue)> = Vec::new(); - if let Some(address) = c["address"].as_str() { - container.push(( - DataTypes::community_address, - DataValue::Str(address.to_string()), - )); - } - if let Some(title) = c["title"].as_str() { - container.push(( - DataTypes::community_title, - DataValue::Str(title.to_string()), - )); - } - if let Some(position) = c["position"].as_str() { - container.push((DataTypes::position, DataValue::Str(position.to_string()))); - } - comm_array.push(DataValue::Container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::get_communities) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_data(DataTypes::communities, DataValue::Array(comm_array)); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::remove_community) { - CommunitiesUtil::remove_community( - cv.get_sender() as i64, - cv.get_data(DataTypes::community_address) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::remove_community) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::settings_save) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataTypes::settings_name).as_str().unwrap(); - let settings_value = cv.get_data(DataTypes::payload).as_str().unwrap(); - - save_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - &settings_value, - ); - - let response = CommunicationValue::new(CommunicationType::settings_save) - .with_receiver(my_id) - .with_id(cv.get_id()); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::settings_load) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataTypes::settings_name).as_string().unwrap(); - let settings_value_str = load_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - ); - let response = CommunicationValue::new(CommunicationType::settings_load) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::payload, DataValue::Str(settings_value_str)) - .add_data(DataTypes::settings_name, DataValue::Str(settings_name)); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::settings_list) { - let my_id = cv.get_sender(); - let settings = get_children(&format!("users/{}/settings/", my_id)); - let mut settings_json = Vec::new(); - for s in settings { - let s = s.replace(".settings", ""); - if s.is_empty() { - continue; - } - let _ = settings_json.push(DataValue::Str(s)); - } - let response = CommunicationValue::new(CommunicationType::settings_list) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::settings, DataValue::Array(settings_json)); - - self.send_message(&response).await; - return; - } - } - - async fn handle_challenge(&self, cv: &CommunicationValue) { - let conf = CONFIG.read().await; - let private_key = conf.get_private_key().unwrap(); - drop(conf); - - let omikron_public_key = cv.get_data(DataTypes::public_key).as_str().unwrap(); - let encrypted_challenge = cv.get_data(DataTypes::challenge).as_str().unwrap(); - - let solved_challenge = { - if let Ok(decrypted) = SecurePayload::new( - encrypted_challenge, - DataFormat::Base64, - crypto_helper::load_secret_key(&private_key).unwrap(), - ) { - if let Ok(decrypted) = decrypted - .decrypt_x448(crypto_helper::load_public_key(omikron_public_key).unwrap()) - { - Some(decrypted) - } else { - None - } - } else { - None - } - }; - - if let Some(decrypted) = solved_challenge { - let solved = decrypted.export(DataFormat::Raw); - - let response = CommunicationValue::new(CommunicationType::challenge_response) - .with_id(cv.get_id()) - .add_data(DataTypes::challenge, DataValue::Str(solved)); - - self.send_message(&response).await; - } - } - - // ------------------------------------------------------------------------- - // Public API - // ------------------------------------------------------------------------- - - pub async fn send_message(&self, cv: &CommunicationValue) { - if let Err(err) = self.send_message_result(cv).await { - log_t!("send_message_failed", err); - } - } - - async fn send_message_result(&self, cv: &CommunicationValue) -> Result<(), String> { - let sender_guard = self.sender.read().await; - if let Some(sender) = sender_guard.as_ref() { - if !sender.is_open() { - drop(sender_guard); - if let Some(sender) = self.sender.write().await.take() { - sender.close(); - } - self.fail_all_waiting_tasks(format!( - "Send failed: connection closed (connection_id={})", - self.connection_id - )) - .await; - return Err("connection closed".to_string()); - } - - let sender_clone = Arc::clone(sender); - drop(sender_guard); - - if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { - log_cv_out!(&cv); - } - - if let Err(e) = sender_clone.send(cv).await { - self.fail_all_waiting_tasks(format!( - "Send failed: {} (connection_id={})", - e, self.connection_id - )) - .await; - return Err(e.to_string()); - } - - Ok(()) - } else { - Err("not connected".to_string()) - } - } - - async fn fail_all_waiting_tasks(&self, reason: String) { - let keys: Vec = WAITING_TASKS.iter().map(|entry| *entry.key()).collect(); - - for key in keys { - if let Some((_, waiting_task)) = WAITING_TASKS.remove(&key) { - let response = CommunicationValue::new(CommunicationType::error) - .with_id(key) - .add_data(DataTypes::message, DataValue::Str(reason.clone())); - let _ = (waiting_task.task)(OMIKRON_CONNECTION.clone(), response); - } - } - } - - pub async fn is_connected(&self) -> bool { - self.state.read().await.is_connected() - } - - pub async fn is_identified(&self) -> bool { - self.state.read().await.is_identified() - } - - pub async fn await_response( - &self, - cv: &CommunicationValue, - timeout_duration: Option, - ) -> Result { - let (tx, mut rx) = mpsc::channel(1); - let msg_id = cv.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new(move |_, response_cv| { - let inner_tx = tx.clone(); - tokio::spawn(async move { - let _ = inner_tx.send(response_cv).await; - }); - true - }), - inserted_at: Instant::now(), - }, - ); - - if let Err(send_err) = self.send_message_result(cv).await { - WAITING_TASKS.remove(&msg_id); - return Err(format!( - "Request send failed (msg_id={}, reason={})", - msg_id, send_err - )); - } - - let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); - - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response_cv)) => { - if response_cv.is_type(CommunicationType::error) { - let reason = response_cv - .get_data(DataTypes::message) - .as_str() - .unwrap_or("connection error") - .to_string(); - Err(format!( - "Request failed due to disconnect (msg_id={}, reason={})", - msg_id, reason - )) - } else { - Ok(response_cv) - } - } - Ok(_) => { - WAITING_TASKS.remove(&msg_id); - Err("Channel closed while awaiting response".to_string()) - } - Err(_) => { - let waiting_tasks_len = WAITING_TASKS.len(); - WAITING_TASKS.remove(&msg_id); - Err(format!( - "Request timed out (msg_id={}, timeout={}s, connected={}, waiting_tasks={})", - msg_id, - timeout.as_secs(), - self.is_connected().await, - waiting_tasks_len - )) - } - } - } - - pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { - if self.state.read().await.is_connected() { - return Ok(()); - } - - let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); - let start = Instant::now(); - - loop { - if self.state.read().await.is_connected() { - return Ok(()); - } - - if start.elapsed() >= timeout { - return Err(format!( - "Connection not established within {} seconds", - timeout.as_secs() - )); - } - - sleep(Duration::from_millis(100)).await; - } - } -} - -// ============================================================================ -// Global Instance -// ============================================================================ - -pub static OMIKRON_CONNECTION: LazyLock> = LazyLock::new(|| { - let conn = Arc::new(OmikronConnection::new()); - - start_task_cleanup_loop(); - - conn -}); - -pub async fn get_omikron_connection() -> Arc { - let conn = OMIKRON_CONNECTION.clone(); - - conn.connect().await; - conn -} diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs deleted file mode 100644 index f1a891f..0000000 --- a/src/omikron/ping_pong_task.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::omikron::omikron_connection::OmikronConnection; -use crate::{APP_STATE, log}; -use dashmap::DashMap; -use std::sync::LazyLock; -use std::time::Instant; -use tokio::time::Duration; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; - -static PING_TIMES: LazyLock> = LazyLock::new(|| DashMap::new()); - -impl OmikronConnection { - pub async fn send_ping(&self) { - let id = rand_u32(); - - PING_TIMES.insert(id, Instant::now()); - - PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30)); - - let ping_message = CommunicationValue::new(CommunicationType::ping) - .with_id(id) - .add_data( - DataTypes::last_ping, - DataValue::Array(vec![DataValue::Number(*self.last_ping.lock().await)]), - ); - - self.send_message(&ping_message).await; - } - - pub async fn handle_pong(&self, cv: &CommunicationValue) { - let id = cv.get_id(); - - if let Some((_, send_time)) = PING_TIMES.remove(&id) { - let ping_ms = Instant::now().duration_since(send_time).as_millis() as i64; - *self.last_ping.lock().await = ping_ms; - APP_STATE.lock().unwrap().push_ping_val(ping_ms as f64); - } - } -} diff --git a/src/rho/client_connection.rs b/src/rho/client_connection.rs new file mode 100644 index 0000000..ebaabf2 --- /dev/null +++ b/src/rho/client_connection.rs @@ -0,0 +1,583 @@ +use crate::anonymous_clients::anonymous_manager; +use crate::calls::{call_manager, call_util}; +use crate::omega::omega_connection::get_omega_connection; +use crate::rho::connection::GeneralConnection; +use crate::rho::{rho_connection::RhoConnection, rho_manager}; +use crate::util::logger::PrintType; +use crate::{data::user::UserStatus, omega::omega_connection::OmegaConnection}; +use crate::{log_cv_in, log_cv_out, log_err, log_in, log_out}; +use std::str::FromStr; +use std::sync::Arc; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tokio::sync::RwLock; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use ttp_native::{Receiver, Sender}; +use uuid::Uuid; + +pub struct ClientConnection { + pub user_id: u64, + + pub sender: Arc, + pub receiver: Arc, + pub ping: Arc>, + pub_key: Arc>>>, + pub rho_connection: Arc>>>, + pub interested_users: Arc>>, + is_open: Arc>, +} + +impl ClientConnection { + pub async fn from_general(general: Arc, user_id: u64) -> Arc { + Arc::new(Self { + ping: Arc::new(RwLock::new(0)), + pub_key: Arc::new(RwLock::new(None)), + rho_connection: general.rho_connection.clone(), + interested_users: Arc::new(RwLock::new(Vec::new())), + is_open: Arc::new(RwLock::new(true)), + sender: general.sender.clone(), + receiver: general.receiver.clone(), + user_id: user_id, + }) + } + pub fn start(self: Arc) { + let self_clone = self.clone(); + tokio::spawn(async move { + while let Ok(cv) = self_clone.receiver.receive().await { + self_clone.clone().handle_message(cv).await; + } + self_clone.handle_close().await; + }); + + let self_clone2 = self.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(50)).await; + if self_clone2.get_rho_connection().await.is_none() { + self_clone2 + .send_error_response(0, CommunicationType::error) + .await; + } + }); + } + + /// Get the user ID + pub async fn get_user_id(&self) -> u64 { + self.user_id + } + + /// Get current ping + pub async fn get_ping(&self) -> i64 { + *self.ping.read().await + } + + /// Get RhoConnection if available + pub async fn get_rho_connection(&self) -> Option> { + self.rho_connection.read().await.clone() + } + + /// Send a CommunicationValue to the client + pub async fn send_message(self: Arc, cv: &CommunicationValue) { + if !*self.is_open.read().await { + log_out!( + self.user_id as i64, + PrintType::Client, + "Attempted to send message to a closed connection." + ); + return; + } + if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { + log_cv_out!(PrintType::Client, &cv); + } + let _ = self.sender.send(&cv).await; + } + + /// Handle incoming message from client + pub async fn handle_message(self: Arc, cv: CommunicationValue) { + tokio::spawn(async move { + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + log_cv_in!(PrintType::Client, cv); + + // Handle client status changes + if cv.is_type(CommunicationType::client_changed) { + self.handle_client_changed(cv).await; + return; + } + + // Handle call invites + if cv.is_type(CommunicationType::call_invite) { + self.handle_call_invite(cv).await; + return; + } + + // Handle get call requests + if cv.is_type(CommunicationType::call_token) { + self.handle_get_call(cv).await; + return; + } + + if cv.is_type(CommunicationType::call_disconnect_user) { + self.handle_call_disconnect_user(cv).await; + return; + } + + if cv.is_type(CommunicationType::call_timeout_user) { + self.handle_call_timeout_user(cv).await; + return; + } + + if cv.is_type(CommunicationType::call_set_anonymous_joining) { + self.handle_call_set_anonymous_joining(cv).await; + return; + } + if cv.is_type(CommunicationType::get_user_data) { + if let Some(anonymous) = { + if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() { + anonymous_manager::get_anonymous_user(user_id as u64).await + } else if let Some(username) = cv.get_data(DataTypes::username).as_str() { + anonymous_manager::get_anonymous_user_by_name(username.to_string()).await + } else { + None + } + } { + let response = CommunicationValue::new(CommunicationType::get_user_data) + .with_id(cv.get_id()) + .add_data( + DataTypes::username, + DataValue::Str(anonymous.get_user_name().await), + ) + .add_data( + DataTypes::user_id, + DataValue::Number(anonymous.get_user_id() as i64), + ) + .add_data( + DataTypes::display, + DataValue::Str(anonymous.get_display_name().await), + ) + .add_data( + DataTypes::avatar, + DataValue::Str(anonymous.get_avatar().await), + ) + .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); + + self.send_message(&response).await; + + return; + } + } + + if cv.is_type(CommunicationType::change_user_data) + || cv.is_type(CommunicationType::read_notification) + || cv.is_type(CommunicationType::get_notifications) + || cv.is_type(CommunicationType::get_user_data) + || cv.is_type(CommunicationType::get_iota_data) + || cv.is_type(CommunicationType::delete_user) + { + let sender = self.get_user_id().await; + self.handle_omega_forward(cv.with_sender(sender as u64)) + .await; + return; + } + // Forward other messages to Iota + self.forward_to_iota(cv).await; + }); + } + async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { + let client_for_closure = self.clone(); + tokio::spawn(async move { + let response_cv = get_omega_connection() + .await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20))) + .await; + if let Ok(response_cv) = response_cv { + client_for_closure.send_message(&response_cv).await; + } + }); + } + + /// Handle ping message + async fn handle_ping(self: Arc, cv: CommunicationValue) { + // Update our ping if provided + if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { + let current = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(); + let mut ping_guard = self.ping.write().await; + *ping_guard = current as i64 - last_ping; + } + + // Get Iota ping from RhoConnection + let iota_ping = if let Some(rho_conn) = self.get_rho_connection().await { + rho_conn.get_iota_connection().get_ping().await + } else { + -1 + }; + + // Send pong response + let response = CommunicationValue::new(CommunicationType::pong) + .with_id(cv.get_id()) + .add_data(DataTypes::ping_iota, DataValue::Number(iota_ping)); + + self.send_message(&response).await; + } + + /// Handle client status change + async fn handle_client_changed(self: Arc, cv: CommunicationValue) { + let user_id = self.get_user_id().await; + if let DataValue::Str(_status_str) = cv.get_data(DataTypes::user_state) { + let user_status = UserStatus::user_online; + if let Some(rho_conn) = self.get_rho_connection().await { + OmegaConnection::client_changed( + rho_conn.get_iota_id().await as i64, + user_id as i64, + user_status, + ) + .await; + } + } + } + + /// Handle call invite + async fn handle_call_invite(self: Arc, cv: CommunicationValue) { + let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0); + if receiver_id == 0 { + self.send_error_response(cv.get_id(), CommunicationType::error_no_user_id) + .await; + return; + } + + let call_id = match cv.get_data(DataTypes::call_id) { + DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) { + Ok(id) => id, + Err(_) => { + self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id) + .await; + return; + } + }, + _ => { + self.send_error_response(cv.get_id(), CommunicationType::error_no_call_id) + .await; + return; + } + }; + + let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await; + if !invited { + self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id) + .await; + return; + } + + // Find target RhoConnection + let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await { + Some(rho) => rho, + _ => { + self.send_error_response(cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + // Get sender user ID + let sender_id = self.get_user_id().await; + + // Create and send call distribution message + let forward = CommunicationValue::new(CommunicationType::call_invite) + .with_receiver(receiver_id as u64) + .with_sender(sender_id as u64) + .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) + .add_data( + DataTypes::receiver_id, + DataValue::Str(receiver_id.to_string()), + ) + .add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string())); + + target_rho.message_to_client(forward).await; + + let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id()); + self.send_message(&response).await; + } + + /// Handle get call request + async fn handle_get_call(self: Arc, cv: CommunicationValue) { + let user_id = self.get_user_id().await; + + let call_id = match cv.get_data(DataTypes::call_id) { + DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) { + Ok(id) => id, + Err(_) => { + self.send_error_response(cv.get_id(), CommunicationType::error) + .await; + return; + } + }, + _ => { + self.send_error_response(cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + if let Some(token) = call_manager::get_call_token(user_id, call_id).await { + let response = CommunicationValue::new(CommunicationType::call_token) + .with_id(cv.get_id()) + .with_receiver(user_id as u64) + .add_data(DataTypes::call_token, DataValue::Str(token)); + self.send_message(&response).await; + } else { + self.send_error_response(cv.get_id(), CommunicationType::error) + .await; + return; + } + } + async fn handle_call_timeout_user(self: Arc, cv: CommunicationValue) { + let call_id = + Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); + let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0); + + let call = call_manager::get_call(call_id).await; + if let Some(call) = call { + if call + .get_caller(self.get_user_id().await) + .await + .unwrap() + .has_admin() + { + let _ = call_util::remove_participant(call_id, user_id as u64).await; + call.get_caller(user_id as u64) + .await + .unwrap() + .set_timeout(untill) + .await; + } + } + } + async fn handle_call_disconnect_user(self: Arc, cv: CommunicationValue) { + let call_id = + Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); + let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + + let call = call_manager::get_call(call_id).await; + if let Some(call) = call { + if call + .get_caller(self.get_user_id().await) + .await + .unwrap() + .has_admin() + { + call.remove_caller(user_id as u64).await; + } + } + } + async fn handle_call_set_anonymous_joining(self: Arc, cv: CommunicationValue) { + let call_id = + Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); + let enable = cv.get_data(DataTypes::enabled).as_bool().unwrap_or(true); + + let call = call_manager::get_call(call_id).await; + + let mut short_link = None; + if let Some(call) = call { + if call + .get_caller(self.get_user_id().await) + .await + .unwrap() + .has_admin() + { + call.set_anonymous_joining(enable).await; + } + short_link = call.get_short_link().await; + } + let mut response_cv = + CommunicationValue::new(CommunicationType::call_set_anonymous_joining) + .with_id(cv.get_id()) + .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) + .add_data(DataTypes::enabled, DataValue::Bool(enable)); + if let Some(short_link) = short_link { + response_cv = response_cv.add_data(DataTypes::link, DataValue::Str(short_link)); + } + self.send_message(&response_cv).await; + } + + /// Forward message to Iota + async fn forward_to_iota(self: Arc, cv: CommunicationValue) { + let sender_user_id = self.get_user_id().await; + let msg_id = cv.get_id(); + let msg_type = cv.get_type(); + + log_in!( + sender_user_id as i64, + PrintType::Client, + "Forwarding client->iota: sender={} type={:?} id={} receiver={}", + sender_user_id, + msg_type, + msg_id, + cv.get_receiver() + ); + + if cv.is_type(CommunicationType::add_conversation) + && cv + .get_data(DataTypes::chat_partner_id) + .as_number() + .is_none() + { + let chat_partner_name = cv + .get_data(DataTypes::chat_partner_name) + .as_str() + .unwrap_or("") + .to_string(); + + if anonymous_manager::get_anonymous_user_by_name(chat_partner_name.to_string()) + .await + .is_some() + { + self.send_error_response(cv.get_id(), CommunicationType::error_anonymous) + .await; + return; + } + + let load_uuid_response = get_omega_connection() + .await_response( + &CommunicationValue::new(CommunicationType::get_user_data) + .with_id(cv.clone().get_id()) + .add_data( + DataTypes::username, + DataValue::Str(chat_partner_name.clone()), + ), + Some(Duration::from_secs(20)), + ) + .await; + let chat_partner_id = { + if let Ok(load_uuid_response) = load_uuid_response { + load_uuid_response.get_data(DataTypes::user_id).clone() + } else { + DataValue::Null + } + }; + + if let Some(rho_conn) = self.get_rho_connection().await { + let iota_id = rho_conn.get_iota_id().await; + log_in!( + sender_user_id as i64, + PrintType::Client, + "Resolved rho for add_conversation: sender={} -> iota_id={} id={}", + sender_user_id, + iota_id, + msg_id + ); + + let updated_cv = cv + .with_sender(sender_user_id as u64) + .add_data(DataTypes::chat_partner_id, chat_partner_id); + rho_conn.message_to_iota(updated_cv).await; + } else { + log_err!( + sender_user_id as i64, + PrintType::Client, + "No rho/iota mapping found for add_conversation sender={} type={:?} id={}", + sender_user_id, + msg_type, + msg_id + ); + } + return; + } + + if let Some(rho_conn) = self.get_rho_connection().await { + let iota_id = rho_conn.get_iota_id().await; + log_in!( + sender_user_id as i64, + PrintType::Client, + "Resolved rho for forward: sender={} -> iota_id={} type={:?} id={}", + sender_user_id, + iota_id, + msg_type, + msg_id + ); + + let updated_cv = cv.with_sender(sender_user_id as u64); + rho_conn.message_to_iota(updated_cv).await; + } else { + log_err!( + sender_user_id as i64, + PrintType::Client, + "No rho/iota mapping found for sender={} type={:?} id={}", + sender_user_id, + msg_type, + msg_id + ); + self.send_error_response(msg_id, CommunicationType::error) + .await; + } + } + + /// Send error response + async fn send_error_response(self: Arc, message_id: u32, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(message_id); + self.send_message(&error).await; + } + + /// Close the connection + pub async fn close(&self) { + let mut is_open_guard = self.is_open.write().await; + if !*is_open_guard { + return; + } + *is_open_guard = false; + + let _ = self.sender.close(); + } + + /// Set interested users list + pub async fn set_interested_users(self: Arc, interested_ids: Vec) { + let mut interested_guard = self.interested_users.write().await; + *interested_guard = interested_ids; + } + #[allow(dead_code)] + pub async fn get_interested_users(self: Arc) -> Vec { + let interested_guard = self.interested_users.read().await; + interested_guard.clone() + } + + /// Check if interested in a user and send notification + #[allow(dead_code)] + pub async fn are_you_interested(self: Arc, user_id: i64) { + let interested_guard = self.clone().get_interested_users().await; + if interested_guard.contains(&user_id) { + let notification = CommunicationValue::new(CommunicationType::client_changed) + .add_data(DataTypes::user_id, DataValue::Str(user_id.to_string())) + .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); + + self.send_message(¬ification).await; + } + } + + /// Handle connection close + pub async fn handle_close(&self) { + let user_id = self.get_user_id().await; + if let Some(rho_conn) = rho_manager::get_rho_con_for_user(user_id as i64).await { + rho_conn + .close_client_connection(Arc::new(self.clone())) + .await; + } + } +} + +// Implement Clone to make it easier to work with Arc +impl Clone for ClientConnection { + fn clone(&self) -> Self { + Self { + sender: Arc::clone(&self.sender), + receiver: Arc::clone(&self.receiver), + user_id: self.user_id, + ping: Arc::clone(&self.ping), + pub_key: Arc::clone(&self.pub_key), + rho_connection: Arc::clone(&self.rho_connection), + interested_users: Arc::clone(&self.interested_users), + is_open: Arc::clone(&self.is_open), + } + } +} diff --git a/src/rho/connection.rs b/src/rho/connection.rs new file mode 100755 index 0000000..ca746f0 --- /dev/null +++ b/src/rho/connection.rs @@ -0,0 +1,358 @@ +use rand::{Rng, distributions::Alphanumeric}; +use std::{sync::Arc, time::Duration}; +use tokio::sync::RwLock; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use ttp_native::{Receiver, Sender}; + +use crate::{ + anonymous_clients::anonymous_client_connection::AnonymousClientConnection, + get_private_key, get_public_key, log_cv_in, log_cv_out, log_err, log_in, log_out, + omega::omega_connection::get_omega_connection, + rho::{ + client_connection::ClientConnection, iota_connection::IotaConnection, + rho_connection::RhoConnection, rho_manager, + }, + util::{ + crypto_helper::{load_public_key, public_key_to_base64}, + crypto_util::{DataFormat, SecurePayload}, + logger::PrintType, + }, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum ConnectionKind { + Client, + Iota, + AnonymousClient, + Phi, +} + +pub struct GeneralConnection { + pub sender: Arc, + pub receiver: Arc, + + identified: Arc>, + challenged: Arc>, + challenge: Arc>, + + connection_kind: Arc>>, + pub rho_connection: Arc>>>, + id: Arc>, + + pub_key: Arc>>>, +} +impl GeneralConnection { + pub fn new(sender: Sender, receiver: Receiver) -> Arc { + Arc::new(Self { + sender: Arc::new(sender), + receiver: Arc::new(receiver), + identified: Arc::new(RwLock::new(false)), + challenged: Arc::new(RwLock::new(false)), + challenge: Arc::new(RwLock::new(String::new())), + connection_kind: Arc::new(RwLock::new(None)), + rho_connection: Arc::new(RwLock::new(None)), + id: Arc::new(RwLock::new(0)), + pub_key: Arc::new(RwLock::new(None)), + }) + } +} +impl GeneralConnection { + pub async fn handle(self: Arc) { + log_in!(0, PrintType::General, "General connection handler started"); + + loop { + let cv = match self.receiver.receive().await { + Ok(v) => v, + Err(_) => { + break; + } + }; + + log_cv_in!(cv); + + if !*self.identified.read().await { + self.handle_identification(cv).await; + continue; + } + + if !*self.challenged.read().await { + self.handle_challenge_response(cv).await; + } + + if *self.challenged.read().await { + let self_clone = self.clone(); + tokio::spawn(async move { + self_clone.migrate().await; + }); + break; + } + } + + log_out!(0, PrintType::General, "General connection handler stopped"); + } + async fn handle_identification(self: &Arc, cv: CommunicationValue) { + if !cv.is_type(CommunicationType::identification) { + return; + } + + if let DataValue::Number(iota_id) = cv.get_data(DataTypes::iota_id) { + *self.id.write().await = *iota_id as u64; + *self.connection_kind.write().await = Some(ConnectionKind::Iota); + + let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_iota_data) + .add_data(DataTypes::iota_id, DataValue::Number(*iota_id)); + + let response_cv = get_omega_connection() + .await_response(&get_pub_key_msg, Some(Duration::from_secs(20))) + .await; + + let response_cv = match response_cv { + Ok(r) => r, + Err(_) => { + return; + } + }; + + let base64_pub = response_cv + .get_data(DataTypes::public_key) + .as_str() + .unwrap_or(""); + + let pub_key = match load_public_key(base64_pub) { + Some(pk) => pk, + None => { + return; + } + }; + + *self.pub_key.write().await = Some(pub_key.as_bytes().to_vec()); + + let challenge: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + *self.challenge.write().await = challenge.clone(); + *self.identified.write().await = true; + + let encrypted_challenge = + SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key()) + .unwrap() + .encrypt_x448(pub_key) + .unwrap() + .export(DataFormat::Base64); + + let response = CommunicationValue::new(CommunicationType::challenge) + .with_id(cv.get_id()) + .add_data( + DataTypes::public_key, + DataValue::Str(public_key_to_base64(&get_public_key())), + ) + .add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge)); + + log_cv_out!(response); + let _ = self.sender.send(&response).await; + } else if let DataValue::Number(user_id) = cv.get_data(DataTypes::user_id) { + *self.id.write().await = *user_id as u64; + *self.connection_kind.write().await = Some(ConnectionKind::Client); + + let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_user_data) + .add_data(DataTypes::user_id, DataValue::Number(*user_id)); + + let response_cv = get_omega_connection() + .await_response(&get_pub_key_msg, Some(Duration::from_secs(20))) + .await; + + let response_cv = match response_cv { + Ok(r) => r, + Err(_) => { + return; + } + }; + + let base64_pub = response_cv + .get_data(DataTypes::public_key) + .as_str() + .unwrap_or(""); + + let pub_key = match load_public_key(base64_pub) { + Some(pk) => pk, + None => { + return; + } + }; + + *self.pub_key.write().await = Some(pub_key.as_bytes().to_vec()); + + let challenge: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + *self.challenge.write().await = challenge.clone(); + *self.identified.write().await = true; + + let encrypted_challenge = + SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key()) + .unwrap() + .encrypt_x448(pub_key) + .unwrap() + .export(DataFormat::Base64); + + let response = CommunicationValue::new(CommunicationType::challenge) + .with_id(cv.get_id()) + .add_data( + DataTypes::public_key, + DataValue::Str(public_key_to_base64(&get_public_key())), + ) + .add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge)); + + log_cv_out!(response); + let _ = self.sender.send(&response).await; + } + } + async fn handle_challenge_response(self: &Arc, cv: CommunicationValue) { + let id = *self.id.read().await as i64; + + if !cv.is_type(CommunicationType::challenge_response) { + return; + } + + if let DataValue::Str(response) = cv.get_data(DataTypes::challenge) { + let expected = self.challenge.read().await.clone(); + + if *response == expected { + *self.challenged.write().await = true; + + let response = CommunicationValue::new(CommunicationType::identification_response) + .with_id(cv.get_id()) + .add_data(DataTypes::accepted, DataValue::Bool(true)); + + log_cv_out!(response); + if let Err(_) = self.sender.send(&response).await { + return; + } + } else { + log_err!( + id, + PrintType::Iota, + "Challenge response mismatch expected={} actual={}", + expected, + response + ); + } + } else { + log_err!( + id, + PrintType::Iota, + "Challenge response missing challenge payload" + ); + } + } + + async fn migrate(self: &Arc) -> bool { + let kind = match *self.connection_kind.read().await { + Some(kind) => kind, + None => { + return false; + } + }; + let id = *self.id.read().await; + + match kind { + ConnectionKind::Client => { + let notify = CommunicationValue::new(CommunicationType::user_connected) + .add_data(DataTypes::user_id, DataValue::Number(id as i64)); + get_omega_connection().send_message(¬ify).await; + + let user_id = id as i64; + + let client = ClientConnection::from_general(self.clone(), id).await; + + let mut rho = rho_manager::get_rho_con_for_user(user_id).await; + + if rho.is_none() { + let get_user_msg = CommunicationValue::new(CommunicationType::get_user_data) + .add_data(DataTypes::user_id, DataValue::Number(user_id)); + + if let Ok(user_data_cv) = get_omega_connection() + .await_response(&get_user_msg, Some(Duration::from_secs(20))) + .await + { + if let DataValue::Number(iota_id) = + user_data_cv.get_data(DataTypes::iota_id) + { + if let Some(bound_rho) = + rho_manager::bind_user_to_iota(user_id, *iota_id).await + { + bound_rho.bind_user_id(user_id).await; + rho = Some(bound_rho); + } + } + } + } + + *self.rho_connection.write().await = rho.clone(); + + if let Some(rho_conn) = rho { + rho_conn.bind_user_id(user_id).await; + rho_conn.add_client_connection(client.clone()).await; + } else { + log_err!( + user_id, + PrintType::Client, + "No RhoConnection found for user {}, client not attached to iota", + id + ); + } + + client.start(); + } + ConnectionKind::Iota => { + let notify = CommunicationValue::new(CommunicationType::iota_connected) + .add_data(DataTypes::iota_id, DataValue::Number(id as i64)); + get_omega_connection().send_message(¬ify).await; + + let iota = IotaConnection::from_general(self.clone(), id).await; + + let rho = Arc::new(RhoConnection::new(iota.clone(), Vec::new()).await); + + iota.set_rho_connection(rho.clone()).await; + + rho_manager::add_rho(rho).await; + + let get_iota_msg = CommunicationValue::new(CommunicationType::get_iota_data) + .add_data(DataTypes::iota_id, DataValue::Number(id as i64)); + + if let Ok(iota_data_cv) = get_omega_connection() + .await_response(&get_iota_msg, Some(Duration::from_secs(20))) + .await + { + if let DataValue::Array(users) = iota_data_cv.get_data(DataTypes::user_ids) { + let mut user_ids: Vec = Vec::new(); + for value in users { + if let DataValue::Number(user_id) = value { + user_ids.push(*user_id as u64); + } + } + iota.set_user_ids(user_ids).await; + } + } + + iota.clone().start(); + } + ConnectionKind::AnonymousClient => { + let client = AnonymousClientConnection::from_general(self.clone(), id).await; + client.start(); + } + ConnectionKind::Phi => { + let phi = ClientConnection::from_general(self.clone(), id).await; + phi.start(); + } + } + true + } +} diff --git a/src/rho/iota_connection.rs b/src/rho/iota_connection.rs new file mode 100755 index 0000000..41336ea --- /dev/null +++ b/src/rho/iota_connection.rs @@ -0,0 +1,461 @@ +use crate::calls::call_group::CallGroup; +use crate::calls::call_manager; +use crate::log_cv_in; +use crate::log_cv_out; +use crate::log_err; +use crate::log_in; +use crate::omega::omega_connection::get_omega_connection; +use crate::rho::connection::GeneralConnection; +use crate::util::logger::PrintType; +use dashmap::DashMap; +use std::collections::BTreeMap; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::sync::RwLock; +use tokio::sync::mpsc; +use ttp_core::CommunicationType; +use ttp_core::CommunicationValue; +use ttp_core::DataTypes; +use ttp_core::DataValue; +use ttp_native::Receiver; +use ttp_native::Sender; +use x448::PublicKey; + +use super::{rho_connection::RhoConnection, rho_manager}; +use crate::omega::omega_connection::OmegaConnection; + +#[allow(dead_code)] +pub struct IotaConnection { + pub iota_id: u64, + pub sender: Arc, + pub receiver: Arc, + pub user_ids: Arc>>, + pub ping: Arc>, + pub_key: Arc>>>, + pub waiting_tasks: + DashMap, CommunicationValue) -> bool + Send + Sync>>, + pub rho_connection: Arc>>>, +} + +impl IotaConnection { + pub async fn from_general(general: Arc, iota_id: u64) -> Arc { + Arc::new(Self { + ping: Arc::new(RwLock::new(0)), + pub_key: Arc::new(RwLock::new(None)), + rho_connection: general.rho_connection.clone(), + user_ids: Arc::new(RwLock::new(Vec::new())), + sender: general.sender.clone(), + receiver: general.receiver.clone(), + iota_id: iota_id, + waiting_tasks: DashMap::new(), + }) + } + pub fn start(self: Arc) { + let self_clone = self.clone(); + tokio::spawn(async move { + loop { + match self_clone.receiver.receive().await { + Ok(cv) => { + self_clone.clone().handle_message(cv).await; + } + Err(_) => { + break; + } + } + } + self_clone.handle_close().await; + }); + } + + /// Get the Iota ID + pub async fn get_iota_id(&self) -> u64 { + self.iota_id + } + + #[allow(dead_code)] + pub async fn get_public_key(&self) -> Option { + if let Some(public_key) = self.pub_key.read().await.clone() { + PublicKey::from_bytes(&public_key) + } else { + None + } + } + + /// Get the user IDs + pub async fn get_user_ids(&self) -> Vec { + self.user_ids.read().await.clone() + } + + /// Replace all users linked to this iota and synchronize the attached rho mapping. + pub async fn set_user_ids(&self, user_ids: Vec) { + { + let mut guard = self.user_ids.write().await; + *guard = user_ids.clone(); + } + + if let Some(rho_conn) = self.get_rho_connection().await { + let user_ids_i64: Vec = user_ids.into_iter().map(|u| u as i64).collect(); + rho_conn.set_user_ids(user_ids_i64).await; + } + } + + pub async fn add_user_id(&self, user_id: u64) { + let mut should_sync = false; + { + let mut guard = self.user_ids.write().await; + if !guard.contains(&user_id) { + guard.push(user_id); + should_sync = true; + } + } + + if should_sync { + if let Some(rho_conn) = self.get_rho_connection().await { + rho_conn.add_user_id(user_id as i64).await; + } + } + } + + /// Get current ping + pub async fn get_ping(&self) -> i64 { + *self.ping.read().await + } + + /// Set the RhoConnection reference + pub async fn set_rho_connection(&self, rho_connection: Arc) { + let mut rho_ref = self.rho_connection.write().await; + *rho_ref = Some(rho_connection); + } + + /// Get RhoConnection if available + pub async fn get_rho_connection(&self) -> Option> { + let rho_ref = self.rho_connection.read().await; + if let Some(weak_ref) = rho_ref.as_ref() { + Some(weak_ref.clone()) + } else { + None + } + } + + /// Send a CommunicationValue to the Iota + pub async fn send_message(&self, cv: &CommunicationValue) { + if !cv.is_type(CommunicationType::pong) { + log_cv_out!(PrintType::Iota, cv); + } + if let Err(e) = self.sender.send(&cv).await { + log_err!( + self.iota_id as i64, + PrintType::Iota, + "Failed to send message: {:?}", + e + ); + } + } + + /// Handle incoming message from Iota + pub async fn handle_message(self: Arc, cv: CommunicationValue) { + let msg_id = cv.get_id(); + if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) { + if (task)(self.clone(), cv.clone()) { + return; + } + } + + // Handle ping + if cv.is_type(CommunicationType::ping) || cv.is_type(CommunicationType::pong) { + self.handle_ping(cv).await; + return; + } + + log_cv_in!(PrintType::Iota, cv); + + // Handle GET_CHATS + if cv.is_type(CommunicationType::get_chats) { + self.handle_get_chats(cv).await; + return; + } + + // Handle forwarding to other Iotas or clients + let receiver_id = cv.get_receiver(); + if (receiver_id != 0 && !self.get_user_ids().await.contains(&(receiver_id as u64))) + || cv.is_type(CommunicationType::message_other_iota) + || cv.is_type(CommunicationType::send_chat) + { + self.handle_forward_message(cv).await; + return; + } + + if cv.is_type(CommunicationType::change_iota_data) + || cv.is_type(CommunicationType::push_notification) + || cv.is_type(CommunicationType::get_user_data) + || cv.is_type(CommunicationType::get_iota_data) + || cv.is_type(CommunicationType::get_register) + || cv.is_type(CommunicationType::complete_register_user) + || cv.is_type(CommunicationType::delete_iota) + { + let sender = self.get_iota_id().await; + self.handle_omega_forward(cv.with_sender(sender as u64)) + .await; + return; + } + self.forward_to_client(cv).await; + } + + #[allow(dead_code)] + async fn send_error_response(&self, message_id: u32, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(message_id); + self.send_message(&error).await; + } + + #[allow(dead_code)] + async fn close(&self) { + let _ = self.sender.close(); + } + + async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { + let iota_for_closure = self.clone(); + tokio::spawn(async move { + let response_cv = get_omega_connection() + .await_response(&cv.with_sender(self.iota_id), Some(Duration::from_secs(20))) + .await; + if let Ok(response_cv) = response_cv { + iota_for_closure.send_message(&response_cv).await; + } + }); + } + /// Handle ping message + async fn handle_ping(&self, cv: CommunicationValue) { + if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { + if let Ok(ping_val) = last_ping.to_string().parse::() { + let mut ping_guard = self.ping.write().await; + *ping_guard = ping_val; + } + } + + let client_pings = if let Some(rho_conn) = self.get_rho_connection().await { + rho_conn.get_client_pings().await + } else { + HashMap::new() + }; + + let pings: Vec<(DataTypes, DataValue)> = client_pings + .into_iter() + .map(|(k, v)| (DataTypes::parse(k), DataValue::Number(v))) + .collect(); + let response = CommunicationValue::new(CommunicationType::pong) + .with_id(cv.get_id()) + .add_data(DataTypes::ping_clients, DataValue::Container(pings)); + + self.send_message(&response).await; + } + + /// Handle message forwarding to other Iotas + async fn handle_forward_message(&self, cv: CommunicationValue) { + let receiver_id = cv.get_receiver(); + let sender_id = cv.get_sender(); + let my_user_ids = self.get_user_ids().await; + + log_in!( + self.iota_id as i64, + PrintType::Iota, + "Authority check: sender_id={} receiver_id={} iota_user_ids={:?} msg_type={:?} msg_id={}", + sender_id, + receiver_id, + my_user_ids, + cv.get_type(), + cv.get_id() + ); + + if my_user_ids.contains(&(sender_id as u64)) { + if let Some(target_rho) = rho_manager::get_rho_con_for_user(receiver_id as i64).await { + target_rho.message_to_iota(cv).await; + } else { + let error = CommunicationValue::new(CommunicationType::error_no_iota) + .with_id(cv.get_id()) + .with_sender(cv.get_sender()); + self.send_message(&error).await; + } + } else { + log_err!( + self.iota_id as i64, + PrintType::Iota, + "Rejected client->iota forward: sender_id={} is not authorized for this iota. Known users={:?}", + sender_id, + my_user_ids + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::error_invalid_user_id).add_data( + DataTypes::error_type, + DataValue::Str( + "You are sending to another User without authority.".to_string(), + ), + ), + ) + .await; + } + } + + /// Handle GET_CHATS message + async fn handle_get_chats(&self, cv: CommunicationValue) { + let receiver_id = cv.get_receiver(); + let mut interested_ids: Vec = Vec::new(); + + // ============================ + // Load Calls + // ============================ + let calls: Vec> = call_manager::get_call_groups(receiver_id).await; + + let mut invites: HashMap> = HashMap::new(); + let empty = calls.is_empty(); + + for call in calls { + for inviter in call.members.read().await.iter() { + let call_self = call.get_caller(receiver_id).await.unwrap(); + + let inviter_id = inviter.user_id; + let timeout = *call_self.timeout.read().await; + let admin = call_self.has_admin(); + + // Build call container + let mut call_map: BTreeMap = BTreeMap::new(); + + call_map.insert(DataTypes::call_id, DataValue::Str(call.call_id.to_string())); + + if timeout > 0 { + call_map.insert(DataTypes::timeout, DataValue::Number(timeout as i64)); + } + + if admin { + call_map.insert(DataTypes::has_admin, DataValue::Bool(true)); + } + + let call_container = DataValue::container_from_map(&call_map); + + invites + .entry(inviter_id as i64) + .or_insert_with(Vec::new) + .push(call_container); + } + } + + // ============================ + // Enrich Contacts + // ============================ + let enriched_contacts = if empty { + match cv.get_data(DataTypes::user_ids) { + DataValue::Array(arr) => DataValue::Array(arr.clone()), + _ => DataValue::Array(vec![]), + } + } else { + let mut enriched: Vec = Vec::new(); + + if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) { + for user_val in users { + if let DataValue::Container(entries) = user_val { + let mut user_map: BTreeMap = + entries.iter().cloned().collect(); + + // extract user_id + if let Some(DataValue::Number(user_id)) = user_map.get(&DataTypes::user_id) + { + interested_ids.push(*user_id); + + // attach calls if exists + if let Some(call_list) = invites.get(user_id) { + user_map + .insert(DataTypes::calls, DataValue::Array(call_list.clone())); + } + } + + enriched.push(DataValue::container_from_map(&user_map)); + } + } + } + + DataValue::Array(enriched) + }; + + // ============================ + // Notify Omega + // ============================ + OmegaConnection::user_states(receiver_id as i64, interested_ids.clone()).await; + + // ============================ + // Notify Rho + // ============================ + if let Some(rho_conn) = self.get_rho_connection().await { + rho_conn + .set_interested(receiver_id as i64, interested_ids) + .await; + } + + // ============================ + // Forward to client + // ============================ + self.forward_to_client(cv.add_data(DataTypes::user_ids, enriched_contacts)) + .await; + } + + /// Forward message to client + async fn forward_to_client(&self, cv: CommunicationValue) { + if let Some(rho_conn) = self.get_rho_connection().await { + let updated_cv = cv.with_sender(self.get_iota_id().await); + rho_conn.message_to_client(updated_cv).await; + } else { + } + } + + pub async fn handle_close(&self) { + if let Some(rho_conn) = self.get_rho_connection().await { + rho_conn.close_iota_connection().await; + } + } + + #[allow(dead_code)] + pub async fn await_response( + self: Arc, + cv: &CommunicationValue, + timeout_duration: Option, + ) -> Result { + let (tx, mut rx) = mpsc::channel(1); + let msg_id = cv.get_id(); + + let task_tx = tx.clone(); + self.waiting_tasks.insert( + msg_id, + Box::new(move |_, response_cv| { + let inner_tx = task_tx.clone(); + tokio::spawn(async move { + let _ = inner_tx.send(response_cv).await; + }); + true + }), + ); + + self.send_message(cv).await; + + let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); + + match tokio::time::timeout(timeout, rx.recv()).await { + Ok(Some(response_cv)) => Ok(response_cv), + Ok(_) => Err("Failed to receive response, channel was closed.".to_string()), + Err(_) => { + self.waiting_tasks.remove(&msg_id); + Err(format!( + "Request timed out after {} seconds.", + timeout.as_secs() + )) + } + } + } +} + +impl std::fmt::Debug for IotaConnection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IotaConnection") + .field("iota_id", &"[async]") + .field("identified", &"[async]") + .field("ping", &"[async]") + .finish() + } +} diff --git a/src/rho/mod.rs b/src/rho/mod.rs new file mode 100644 index 0000000..ac498f3 --- /dev/null +++ b/src/rho/mod.rs @@ -0,0 +1,6 @@ +pub mod client_connection; +pub mod connection; +pub mod iota_connection; +pub mod rho_connection; +pub mod rho_manager; +pub mod server; diff --git a/src/rho/rho_connection.rs b/src/rho/rho_connection.rs new file mode 100644 index 0000000..6a740ff --- /dev/null +++ b/src/rho/rho_connection.rs @@ -0,0 +1,199 @@ +use super::{client_connection::ClientConnection, iota_connection::IotaConnection, rho_manager}; + +use crate::data::user::UserStatus; +use crate::omega::omega_connection::OmegaConnection; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; + +pub struct RhoConnection { + iota_connection: Arc, + user_ids: Arc>>, + client_connections: Arc>>>, +} + +impl RhoConnection { + /// Create a new RhoConnection + pub async fn new(iota_connection: Arc, user_ids: Vec) -> Self { + let rho_connection = Self { + iota_connection, + user_ids: Arc::new(RwLock::new(user_ids.clone())), + client_connections: Arc::new(RwLock::new(Vec::new())), + }; + + rho_connection + } + + pub async fn get_iota_id(&self) -> u64 { + self.iota_connection.iota_id + } + + pub async fn get_user_ids(&self) -> Vec { + self.user_ids.read().await.clone() + } + + pub async fn set_user_ids(&self, user_ids: Vec) { + let mut guard = self.user_ids.write().await; + *guard = user_ids; + } + + pub async fn add_user_id(&self, user_id: i64) { + let mut guard = self.user_ids.write().await; + if !guard.contains(&user_id) { + guard.push(user_id); + } + } + + pub async fn bind_user_id(&self, user_id: i64) { + self.add_user_id(user_id).await; + self.iota_connection.add_user_id(user_id as u64).await; + } + + pub fn get_iota_connection(&self) -> &Arc { + &self.iota_connection + } + + pub async fn get_client_connections(&self) -> Vec> { + let connections = self.client_connections.read().await; + connections.clone() + } + + /// Get client connections for a specific user + pub async fn get_client_connections_for_user( + &self, + user_id: i64, + ) -> Vec> { + let connections = self.client_connections.read().await; + let mut collections = Vec::new(); + for con in connections.iter() { + if con.get_user_id().await == user_id as u64 { + collections.push(con.clone()); + } + } + collections + } + + /// Add a client connection + #[allow(dead_code)] + pub async fn add_client_connection(&self, connection: Arc) { + let notification = CommunicationValue::new(CommunicationType::client_connected).add_data( + DataTypes::user_id, + DataValue::Number(connection.get_user_id().await as i64), + ); + + self.iota_connection.send_message(¬ification).await; + + { + let mut connections = self.client_connections.write().await; + connections.push(Arc::clone(&connection)); + } + + OmegaConnection::client_changed( + self.get_iota_id().await as i64, + connection.get_user_id().await as i64, + UserStatus::user_online, + ) + .await; + } + + /// Remove a client connection + pub async fn close_client_connection(&self, connection: Arc) { + let target_user_id = connection.get_user_id().await; + { + let mut connections = self.client_connections.write().await; + connections.retain(|con| { + futures::executor::block_on(async { con.get_user_id().await != target_user_id }) + }); + } + + // Notify OmegaConnection + OmegaConnection::client_changed( + self.get_iota_id().await as i64, + connection.get_user_id().await as i64, + UserStatus::user_offline, + ) + .await; + } + + /// Close the Iota connection and all associated client connections + pub async fn close_iota_connection(&self) { + // Close all client connections + let connections = self.get_client_connections().await; + for connection in connections { + connection.close().await; + } + + // Remove from manager + rho_manager::remove_rho(self.get_iota_id().await as i64).await; + + // Notify OmegaConnection + OmegaConnection::close_iota(self.get_iota_id().await as i64).await; + } + + /// Send message from Iota to specific client + pub async fn message_to_client(&self, cv: CommunicationValue) { + let connections = self.client_connections.read().await; + let receiver_id = cv.get_receiver(); + for connection in connections.iter() { + if connection.get_user_id().await == receiver_id { + connection.clone().send_message(&cv).await; + } + } + } + + /// Send message to Iota + pub async fn message_to_iota(&self, cv: CommunicationValue) { + self.iota_connection.send_message(&cv).await; + } + + /// Set interested users for a specific client + pub async fn set_interested(&self, user_id: i64, interested_ids: Vec) { + let connections = self.client_connections.read().await; + for connection in connections.iter() { + let conn_user_id = connection.get_user_id().await; + if conn_user_id == user_id as u64 { + connection + .clone() + .set_interested_users(interested_ids.clone()) + .await; + break; + } + } + } + + /// Check if clients are interested in a user + #[allow(dead_code)] + pub async fn are_they_interested(&self, user_id: i64) { + let connections = self.client_connections.read().await; + for connection in connections.iter() { + connection.clone().are_you_interested(user_id).await; + } + } + + /// Get ping information for all clients + pub async fn get_client_pings(&self) -> HashMap { + let connections = self.client_connections.read().await; + let mut pings = HashMap::new(); + + for connection in connections.iter() { + let user_id = connection.get_user_id().await; + pings.insert(user_id.to_string(), connection.get_ping().await); + } + + pings + } + + /// Check if this RhoConnection contains a specific user ID + #[allow(dead_code)] + pub async fn contains_user(&self, user_id: &i64) -> bool { + self.user_ids.read().await.contains(user_id) + } + + /// Get count of active client connections + #[allow(dead_code)] + pub async fn client_count(&self) -> usize { + let connections = self.client_connections.read().await; + connections.len() + } +} diff --git a/src/rho/rho_manager.rs b/src/rho/rho_manager.rs new file mode 100644 index 0000000..e823a04 --- /dev/null +++ b/src/rho/rho_manager.rs @@ -0,0 +1,83 @@ +use super::rho_connection::RhoConnection; +use crate::log_in; +use crate::util::logger::PrintType; +use std::{ + collections::HashMap, + sync::{Arc, LazyLock}, +}; +use tokio::sync::RwLock; + +pub static RHO_CONNECTIONS: LazyLock>>>> = + LazyLock::new(|| Arc::new(RwLock::new(HashMap::new()))); + +pub async fn get_rho_con_for_user(user_id: i64) -> Option> { + let connections = RHO_CONNECTIONS.read().await; + for rho_connection in connections.values() { + let rho_user_ids = rho_connection.get_user_ids().await; + log_in!( + user_id, + PrintType::Client, + "Comparing user IDs: {:?}", + rho_user_ids + ); + if rho_user_ids.contains(&user_id) { + return Some(Arc::clone(rho_connection)); + } + } + None +} + +#[allow(dead_code)] +pub async fn contains_iota(iota_id: i64) -> bool { + let connections = RHO_CONNECTIONS.read().await; + connections.contains_key(&iota_id) +} + +/// Bind a user ID to an already tracked iota/rho connection. +pub async fn bind_user_to_iota(user_id: i64, iota_id: i64) -> Option> { + let connections = RHO_CONNECTIONS.read().await; + if let Some(rho_connection) = connections.get(&iota_id) { + let rho = Arc::clone(rho_connection); + drop(connections); + + rho.add_user_id(user_id).await; + + log_in!( + user_id, + PrintType::Client, + "Bound user {} to iota {}", + user_id, + iota_id + ); + + Some(rho) + } else { + None + } +} + +/// Remove a RhoConnection by Iota ID +pub async fn remove_rho(iota_id: i64) -> Option> { + let mut connections = RHO_CONNECTIONS.write().await; + connections.remove(&iota_id) +} + +/// Add a RhoConnection to the manager +pub async fn add_rho(rho_connection: Arc) { + let mut connections = RHO_CONNECTIONS.write().await; + let iota_id = rho_connection.get_iota_id().await; + connections.insert(iota_id as i64, rho_connection); +} + +/// Get a RhoConnection by Iota ID directly +#[allow(dead_code)] +pub async fn get_rho_by_iota(iota_id: i64) -> Option> { + let connections = RHO_CONNECTIONS.read().await; + connections.get(&iota_id).map(Arc::clone) +} + +/// Get the count of active connections +pub async fn connection_count() -> usize { + let connections = RHO_CONNECTIONS.read().await; + connections.len() +} diff --git a/src/rho/server.rs b/src/rho/server.rs new file mode 100644 index 0000000..9e3946d --- /dev/null +++ b/src/rho/server.rs @@ -0,0 +1,25 @@ +use crate::{ + log, + rho::connection::GeneralConnection, + util::{file_util::load_file_vec, logger::PrintType}, +}; +use ttp_native::Host; + +pub async fn start(port: u16) -> Result<(), Box> { + let cert_pem = load_file_vec("certs", "cert.pem").expect("Error loading Pemfile"); + + let key_pem = load_file_vec("certs", "key.pem").expect("Error loading Keyfile"); + + let mut host: Host = ttp_native::host(port, cert_pem, key_pem).await?; + log!(0, PrintType::General, "Server listening on port {}", port); + + while let Some((sender, receiver)) = host.next().await { + tokio::spawn(async move { + let conn = GeneralConnection::new(sender, receiver); + conn.handle().await; + }); + } + log!(0, PrintType::General, "Server stopped"); + + Ok(()) +} diff --git a/src/server/api.rs b/src/server/api.rs deleted file mode 100755 index f9a87a4..0000000 --- a/src/server/api.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::server::server::is_local_network; -use crate::util::config_util::CONFIG; -use actix_web::{HttpRequest, HttpResponse, Responder, web}; -use serde_json::{Value, json}; -use std::net::SocketAddr; -use std::sync::Arc; - -pub fn api_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/api") - .route("/shutdown/", web::post().to(shutdown)) - .route("/reload/", web::post().to(reload)) - .route("/users/add/", web::post().to(users_add)) - .route("/users/remove/", web::post().to(users_remove)) - .route("/users/get/", web::get().to(users_get)) - .route("/communities/add/", web::post().to(communities_add)) - .route("/communities/get/", web::get().to(communities_get)) - .route("/settings/set/", web::post().to(settings_set)) - .route("/settings/get/", web::get().to(settings_get)), - ); -} - -async fn settings_set(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let key = req.headers().get("key").and_then(|v| v.to_str().ok()); - let value = req.headers().get("value").and_then(|v| v.to_str().ok()); - - match (key, value) { - (Some(k), Some(v)) => { - let _ = CONFIG - .write() - .await - .config - .insert(&k.to_string(), v.to_string()); - - success() - } - _ => error(), - } -} - -async fn settings_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - let config = CONFIG.read().await.config.clone(); - let serde_config: Value = serde_json::to_value(config.to_string()).unwrap(); - HttpResponse::Ok().json(serde_config) -} - -async fn communities_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let communities = crate::communities::community_manager::get_communities().await; - - let mut list = Vec::new(); - - for c in communities { - let val = c.frontend().await; - let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); - list.push(s_val); - } - HttpResponse::Ok().json(list) -} - -async fn communities_add( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let name = payload["name"].as_str().unwrap_or("").to_string(); - let owner = payload["owner"].as_i64().unwrap_or(0); - - let community = Arc::new(crate::communities::community::Community::create(name, owner).await); - - crate::communities::community_manager::add_community(community).await; - - success() -} - -async fn users_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let users = crate::users::user_manager::get_users(); - - let list: Vec<_> = users - .into_iter() - .map(|u| { - let val = u.frontend(); - serde_json::to_value(val.to_string()).unwrap() - }) - .collect(); - - HttpResponse::Ok().json(list) -} - -async fn users_remove( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0); - - crate::users::user_manager::remove_user(uuid); - crate::users::user_manager::save_users(); - - success() -} - -async fn users_add( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let username = match payload.get("username").and_then(|v| v.as_str()) { - Some(u) => u, - _ => return error(), - }; - - if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { - let val = user.frontend(); - let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); - HttpResponse::Ok().json(s_val) - } else { - error() - } -} - -async fn shutdown(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - *crate::SHUTDOWN.write().await = true; - success() -} - -async fn reload(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - *crate::SHUTDOWN.write().await = true; - *crate::RELOAD.write().await = true; - - success() -} - -fn forbidden() -> HttpResponse { - HttpResponse::Forbidden().body("403 Forbidden") -} - -fn success() -> HttpResponse { - HttpResponse::Ok().json(json!({ "type": "success" })) -} - -fn error() -> HttpResponse { - HttpResponse::Ok().json(json!({ "type": "error" })) -} - -fn is_allowed(addr: SocketAddr, ssl: bool) -> bool { - is_local_network(addr.ip()) || ssl -} - -fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool { - if let Some(addr) = req.peer_addr() { - is_allowed(addr, ssl) - } else { - false - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index de2a79a..0000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod api; -pub mod server; -pub mod web_path_parser; diff --git a/src/server/server.rs b/src/server/server.rs deleted file mode 100644 index b55a50f..0000000 --- a/src/server/server.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::log; -use crate::server::api::api_config; -use crate::server::web_path_parser; -use crate::util::file_util::load_file_buf; -use crate::{ACTIVE_TASKS, SHUTDOWN}; -use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; -use actix_web_actors::ws; -use rustls::ServerConfig; -use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -use std::{ - error::Error as StdError, - io::{self, BufReader, ErrorKind}, - net::IpAddr, - sync::Arc, - time::Duration, -}; - -async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result { - let path = req.path().to_string(); - log!("WS connection from {:?}", req.peer_addr()); - let session = WsSession::new(path); - ws::start(session, &req, stream) -} - -use tokio::sync::oneshot; - -pub async fn start(port: u16) -> bool { - let (tx, rx) = oneshot::channel::(); - - let _ = tokio::spawn(async move { - let server = match load_tls_config() { - Ok(Some(tls_config)) => { - log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); - let _config = (*tls_config).clone(); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(true)) - .configure(api_config) - .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) - .default_service(web::to(web_path_parser::handle)) - }) - .bind(("0.0.0.0", port)) - .unwrap() - .run() - } - Ok(_) => { - log!("HTTP Server running on 0.0.0.0:{}", port); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(false)) - .configure(api_config) - .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) - .default_service(web::to(web_path_parser::handle)) - }) - .bind(("0.0.0.0", port)) - .unwrap() - .run() - } - Err(e) => { - log!("TLS config error: {}", e); - return; - } - }; - - let server_handle = server.handle(); - tx.send(server_handle).unwrap(); - - ACTIVE_TASKS.insert("WebServer".into()); - server.await.unwrap(); - ACTIVE_TASKS.remove("WebServer"); - log!("Web Server shutdown complete."); - }); - - if let Ok(server_handle) = rx.await { - tokio::spawn(async move { - wait_for_shutdown(server_handle).await; - }); - true - } else { - false - } -} - -async fn wait_for_shutdown(server_handle: ServerHandle) { - loop { - if *SHUTDOWN.read().await { - log!("Shutdown signal received."); - server_handle.stop(true).await; - break; - } - tokio::time::sleep(Duration::from_millis(100)).await; - } -} - -fn load_tls_config() -> Result>, Box> { - let cert_file_res = load_file_buf("certs", "cert.pem"); - let key_file_res = load_file_buf("certs", "cert.key"); - - let cert_file_buf = match cert_file_res { - Ok(b) => b, - Err(e) if e.kind() == ErrorKind::NotFound => { - log!("TLS certificate 'certs/cert.pem' not found."); - return Ok(None); - } - Err(e) => return Err(e.into()), // Other IO error - }; - - let key_file_buf = match key_file_res { - Ok(b) => b, - Err(e) if e.kind() == ErrorKind::NotFound => { - log!("TLS key 'certs/cert.key' not found."); - return Ok(None); - } - Err(e) => return Err(e.into()), // Other IO error - }; - - let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf)) - .collect::, _>>()?; - - // PKCS8 - let mut key_reader = BufReader::new(key_file_buf); - let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - - if key_ders.is_empty() { - // RSA - key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file - key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - } - - if key_ders.is_empty() { - // EC - key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file - key_ders = rustls_pemfile::ec_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - } - - if key_ders.is_empty() { - return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into()); - } - - let config = ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(cert_chain, key_ders.remove(0)) - .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; - - Ok(Some(Arc::new(config))) -} - -pub fn is_local_network(addr: IpAddr) -> bool { - match addr { - IpAddr::V4(v4) => { - let o = v4.octets(); - o[0] == 10 - || (o[0] == 172 && (16..=31).contains(&o[1])) - || (o[0] == 192 && o[1] == 168) - || o[0] == 127 - || (o[0] == 169 && o[1] == 254) - } - IpAddr::V6(v6) => { - let s = v6.segments(); - (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback() - } - } -} diff --git a/src/server/web_path_parser.rs b/src/server/web_path_parser.rs deleted file mode 100755 index a82c8db..0000000 --- a/src/server/web_path_parser.rs +++ /dev/null @@ -1,77 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse}; -use std::path::{Path, PathBuf}; - -use crate::util::file_util::load_file_vec; - -fn codec_for_ext(ext: &str) -> &'static str { - match ext { - "html" => "text/html; charset=utf-8", - "css" => "text/css", - "js" => "application/javascript", - "json" => "application/json", - "png" => "image/png", - "ico" => "image/x-icon", - "woff2" => "font/woff2", - _ => "application/octet-stream", - } -} - -pub async fn handle(req: HttpRequest) -> HttpResponse { - let req_path = req.path().trim_start_matches('/'); - - let mut fs_path = PathBuf::from("web"); - - if req_path.is_empty() { - fs_path.push("index.html"); - } else { - fs_path.extend(req_path.split('/')); - } - - if fs_path.is_dir() { - fs_path.push("index.html"); - } - - let ext_opt = fs_path.extension().and_then(|e| e.to_str()); - let mut final_name = fs_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("") - .to_string(); - - if ext_opt.is_none() { - if final_name.is_empty() { - final_name = "index.html".to_string(); - } else { - final_name.push_str(".html"); - } - } - - let content_type = codec_for_ext( - fs_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("html"), - ); - - let dir = fs_path.parent().unwrap_or(Path::new("web")); - - match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) { - Ok(content) => HttpResponse::Ok().content_type(content_type).body(content), - - Err(_) => { - let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if matches!(ext, "js" | "css" | "woff2") { - return HttpResponse::NotFound() - .content_type("text/plain") - .body("Not found"); - } - - let fallback = load_file_vec("web", "404.html") - .unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec()); - - HttpResponse::NotFound() - .content_type("text/html; charset=utf-8") - .body(fallback) - } - } -} diff --git a/src/terms/buttons.rs b/src/terms/buttons.rs deleted file mode 100644 index c545a4c..0000000 --- a/src/terms/buttons.rs +++ /dev/null @@ -1,175 +0,0 @@ -use ratatui::{ - layout::{Alignment, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; - -use crate::terms::focus::Focus; - -#[allow(mismatched_lifetime_syntaxes)] -pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line { - let box_char = if checked { "[x]" } else { "[ ]" }; - let (box_style, text_style) = if active { - if allowed { - ( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - } else { - ( - Style::default().fg(Color::Gray), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ) - } - } else { - (Style::default(), Style::default()) - }; - Line::from(vec![ - Span::styled(box_char, box_style), - Span::raw(" "), - Span::styled(label, text_style), - ]) -} - -pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { - let p = Paragraph::new(Span::styled(label, style)) - .alignment(Alignment::Center) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(p, area); -} -pub fn draw_buttons( - f: &mut ratatui::Frame, - area: Rect, - current_focus: Focus, - state: (bool, bool), - update_needed: bool, - downgrade_scenario: bool, - tos_or_privacy: bool, -) { - let cancel_text = if update_needed { - "[Q] Quit" - } else { - "[Q] Not now" - }; - let continue_text = if downgrade_scenario { - "Downgrade" - } else { - "Continue" - }; - let mut buttons = vec![ - (cancel_text, Focus::Cancel), - (continue_text, Focus::Continue), - ]; - if tos_or_privacy { - buttons.push(("Continue with Tensamin Services", Focus::ContinueAll)); - } - - let padding = 2; - let min_widths: Vec = buttons - .iter() - .map(|(label, _)| label.len() as u16 + padding) - .collect(); - - let widths = compute_widths(area.width, &min_widths); - - let mut x = area.x; - - for ((label, focus), width) in buttons.iter().zip(widths) { - let chunk = Rect { - x, - y: area.y, - width, - height: area.height, - }; - x += width; - - let is_focused = current_focus == *focus; - - let style = match focus { - Focus::Cancel => { - if is_focused { - Style::default() - .fg(Color::Black) - .bg(Color::Red) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Red) - } - } - - Focus::Continue => { - if is_focused && state.0 { - Style::default() - .fg(Color::Black) - .bg(Color::Green) - .add_modifier(Modifier::BOLD) - } else if state.0 { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::DarkGray) - } - } - - Focus::ContinueAll => { - if is_focused && state.1 { - Style::default() - .fg(Color::Black) - .bg(Color::Green) - .add_modifier(Modifier::BOLD) - } else if state.1 { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::DarkGray) - } - } - - _ => Style::default().fg(Color::DarkGray), - }; - - draw_button(f, chunk, label, style); - } -} -pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec { - let mut widths = vec![0; min_widths.len()]; - let mut remaining: Vec = (0..min_widths.len()).collect(); - - let mut remaining_width = area_width; - - while !remaining.is_empty() { - let count = remaining.len() as u16; - let equal = remaining_width / count; - - let mut clamped = Vec::new(); - - for &i in &remaining { - if min_widths[i] > equal { - widths[i] = min_widths[i]; - remaining_width -= min_widths[i]; - clamped.push(i); - } - } - - if clamped.is_empty() { - let mut remainder = remaining_width % count; - for &i in &remaining { - widths[i] = equal - + if remainder > 0 { - remainder -= 1; - 1 - } else { - 0 - }; - } - break; - } - - remaining.retain(|i| !clamped.contains(i)); - } - - widths -} diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs deleted file mode 100644 index 1cc6e0a..0000000 --- a/src/terms/consent_state.rs +++ /dev/null @@ -1,491 +0,0 @@ -use tokio::sync::oneshot; - -use crate::{ - gui::{ - screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen}, - ui::UI, - }, - terms::{ - doc::Doc, - terms_getter::{Type, get_current_docs, get_newest_docs}, - }, - util::file_util::{load_file, save_file}, -}; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub async fn check(ui: Arc) -> (bool, bool) { - let mut state = ConsentState::load_state(); - - if ensure_initial_consent(ui.clone(), &mut state) - .await - .is_err() - { - return (false, false); - } - if ensure_updates(ui, &mut state).await.is_err() { - return (false, false); - }; - - state = state.sanitize(); - state.save_state(); - - (state.accepted_eula, state.accepted_tos && state.accepted_pp) -} - -async fn ensure_initial_consent(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { - if state.accepted_eula { - return Ok(()); - } - - let (tx, rx) = oneshot::channel(); - - ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx)))) - .await; - - let result = rx.await.unwrap_or(UserChoice::Deny); - - match result { - UserChoice::AcceptEULA | UserChoice::AcceptAll => { - if let Some((eula, tos, privacy)) = get_current_docs().await { - state.accepted_eula = true; - state.eula = Some(eula); - - if matches!(result, UserChoice::AcceptAll) { - state.accepted_tos = true; - state.accepted_pp = true; - state.tos = Some(tos); - state.privacy = Some(privacy); - } - } - - let _ = &state.save_state(); - Ok(()) - } - UserChoice::Deny => Err(()), - } -} -async fn ensure_updates(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { - let Some((eula_update, tos_update, privacy_update)) = get_updates().await else { - return Ok(()); - }; - - let is_forced = matches!(eula_update, UpdateDecision::Forced(_)) - || matches!(tos_update, UpdateDecision::Forced(_)) - || matches!(privacy_update, UpdateDecision::Forced(_)); - - let (tx, rx) = oneshot::channel(); - - ui.set_screen(Box::new(TermsUpdaterScreen::new( - eula_update.clone(), - tos_update.clone(), - privacy_update.clone(), - Some(tx), - ))) - .await; - - let result = rx.await.unwrap_or(UserChoice::Deny); - - if is_forced { - match result { - UserChoice::AcceptAll => { - state.accepted_eula = true; - state.accepted_tos = true; - state.accepted_pp = true; - } - UserChoice::AcceptEULA => { - state.accepted_eula = true; - } - UserChoice::Deny => return Err(()), - } - } else { - apply_future_updates(state, result, eula_update, tos_update, privacy_update); - } - - state.save_state(); - Ok(()) -} -fn apply_future_updates( - state: &mut ConsentState, - result: UserChoice, - eula_update: UpdateDecision, - tos_update: UpdateDecision, - privacy_update: UpdateDecision, -) { - match result { - UserChoice::AcceptAll => { - if let UpdateDecision::Future { newest } = eula_update { - state.future_eula = Some(newest); - } - if let UpdateDecision::Future { newest } = tos_update { - state.future_tos = Some(newest); - } - if let UpdateDecision::Future { newest } = privacy_update { - state.future_privacy = Some(newest); - } - } - UserChoice::AcceptEULA => { - if let UpdateDecision::Future { newest } = eula_update { - state.future_eula = Some(newest); - } - } - UserChoice::Deny => {} - } -} - -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 - UpdateDecision, - UpdateDecision, - UpdateDecision, -)> { - if let ( - Some((current_eula, current_tos, current_privacy)), - Some((newest_eula, newest_tos, newest_privacy)), - ) = (get_current_docs().await, get_newest_docs().await) - { - let file = load_file("", "agreements"); - let accepted_state = ConsentState::from_str(&file).sanitize(); - save_file("", "agreements", &accepted_state.to_string()); - - let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) { - if current_eula.equals(&newest_eula) { - UpdateDecision::NoChange - } else { - if newest_eula.equals_some(&accepted_state.future_eula) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { - newest: newest_eula, - } - } - } - } else if newest_eula.equals_some(&accepted_state.eula) { - UpdateDecision::NoChange - } else { - UpdateDecision::Forced(current_eula) - }; - - let tos_update: UpdateDecision = - if !accepted_state.accepted_tos || newest_tos.equals_some(&accepted_state.tos) { - UpdateDecision::NoChange - } else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) { - if current_tos.equals(&newest_tos) { - UpdateDecision::NoChange - } else { - if newest_tos.equals_some(&accepted_state.future_tos) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { newest: newest_tos } - } - } - } else { - UpdateDecision::Forced(current_tos) - }; - - let privacy_update: UpdateDecision = if !accepted_state.accepted_pp - || newest_privacy.equals_some(&accepted_state.privacy) - { - UpdateDecision::NoChange - } else if accepted_state.accepted_pp && current_privacy.equals_some(&accepted_state.privacy) - { - if current_privacy.equals(&newest_privacy) { - UpdateDecision::NoChange - } else { - if newest_privacy.equals_some(&accepted_state.future_privacy) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { - newest: newest_privacy, - } - } - } - } else { - UpdateDecision::Forced(current_privacy) - }; - match (&eula_update, &tos_update, &privacy_update) { - (&UpdateDecision::NoChange, &UpdateDecision::NoChange, &UpdateDecision::NoChange) => { - None - } - _ => Some((eula_update, tos_update, privacy_update)), - } - } else { - None - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum UserChoice { - Deny, - AcceptEULA, - AcceptAll, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum UpdateDecision { - NoChange, - Future { newest: Doc }, - Forced(Doc), -} - -#[derive(Debug, Clone)] -pub struct ConsentState { - pub eula: Option, - pub accepted_eula: bool, - pub future_eula: Option, - - pub tos: Option, - pub accepted_tos: bool, - pub future_tos: Option, - - pub privacy: Option, - pub accepted_pp: bool, - pub future_privacy: Option, -} - -impl ConsentState { - fn sanitize(mut self) -> Self { - let current_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - if let Some(future_eula) = self.future_eula.clone() { - if future_eula.get_time() < current_secs { - self.eula = Some(future_eula); - self.future_eula = None; - } - } - if let Some(future_tos) = self.future_tos.clone() { - if future_tos.get_time() < current_secs { - self.tos = Some(future_tos); - self.future_tos = None; - } - } - if let Some(future_privacy) = self.future_privacy.clone() { - if future_privacy.get_time() < current_secs { - self.privacy = Some(future_privacy); - self.future_privacy = None; - } - } - - if !self.accepted_eula { - self.accepted_tos = false; - self.accepted_pp = false; - } - self - } - - pub fn load_state() -> ConsentState { - let file = load_file("", "agreements"); - ConsentState::from_str(&file).sanitize() - } - - pub fn save_state(&self) { - save_file("", "agreements", &self.to_string()); - } - - fn to_string(&self) -> String { - let current_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let mut file_out: String = format!( - "This 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={}", - current_secs - ); - - if let Some(eula) = &self.eula { - 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={}\ - \nEULA-VERSION={}\ - \nEULA-HASH={}\ - ", self.accepted_eula, eula.get_version(), eula.get_hash())); - - if self.accepted_tos - && let Some(tos) = &self.tos - { - file_out.push_str(&format!("\ - \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.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/\ - \nPrivacy-Policy={}\ - \nPrivacy-Policy-VERSION={}\ - \nPrivacy-Policy-HASH={}\ - ", self.accepted_pp, pp.get_version(), pp.get_hash())); - } - } else { - file_out.push_str("\ - \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\ - \nEULA=false\ - "); - } - - if let Some(eula) = &self.future_eula { - file_out.push_str(&format!( - "\ - \nFUTURE-EULA-VERSION={}\ - \nFUTURE-EULA-HASH={}\ - \nFUTURE-EULA-TIME={}\ - ", - eula.get_version(), - eula.get_hash(), - eula.get_time() - )); - } - if let Some(tos) = &self.future_tos { - file_out.push_str(&format!( - "\ - \nFUTURE-Terms-of-Service-VERSION={}\ - \nFUTURE-Terms-of-Service-HASH={}\ - \nFUTURE-Terms-of-Service-TIME={}\ - ", - tos.get_version(), - tos.get_hash(), - tos.get_time() - )); - } - if let Some(pp) = &self.future_privacy { - file_out.push_str(&format!( - "\ - \nFUTURE-Privacy-Policy-VERSION={}\ - \nFUTURE-Privacy-Policy-HASH={}\ - \nFUTURE-Privacy-Policy-TIME={}\ - ", - pp.get_version(), - pp.get_hash(), - pp.get_time() - )); - } - - file_out - } - - fn from_str(s: &str) -> Self { - let mut eula = false; - let mut eula_version = String::new(); - let mut eula_hash = String::new(); - let mut pp = false; - let mut pp_version = String::new(); - let mut pp_hash = String::new(); - let mut tos = false; - let mut tos_version = String::new(); - let mut tos_hash = String::new(); - - let mut future_eula_version = String::new(); - let mut future_eula_hash = String::new(); - let mut future_eula_time = String::new(); - - let mut future_tos_version = String::new(); - let mut future_tos_hash = String::new(); - let mut future_tos_time = String::new(); - - let mut future_pp_version = String::new(); - let mut future_pp_hash = String::new(); - let mut future_pp_time = String::new(); - - let mut unix = String::new(); - - for line in s.lines() { - if let Some(v) = line.strip_prefix("EULA=") { - eula = v == "true"; - } else if let Some(v) = line.strip_prefix("EULA-VERSION=") { - 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("Terms-of-Service=") { - tos = v == "true"; - } 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("Terms-of-Service-HASH=") { - tos_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("Privacy-Policy=") { - pp = v == "true"; - } else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") { - pp_version = v.to_string(); - } 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-SECOND=") { - unix = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") { - future_eula_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-HASH=") { - future_eula_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-TIME=") { - future_eula_time = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-VERSION=") { - future_tos_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-HASH=") { - future_tos_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-TIME=") { - future_tos_time = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-VERSION=") { - future_pp_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-HASH=") { - future_pp_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-TIME=") { - future_pp_time = v.to_string(); - } - } - let unix: u64 = unix.parse::().unwrap_or(0); - let future_eula_time = future_eula_time.parse::().unwrap_or(0); - let future_tos_time = future_tos_time.parse::().unwrap_or(0); - let future_pp_time = future_pp_time.parse::().unwrap_or(0); - let state = Self { - accepted_eula: eula, - eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), - accepted_pp: pp, - 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)), - future_eula: if !future_eula_version.is_empty() { - Some(Doc::new( - future_eula_version, - future_eula_hash, - Type::EULA, - future_eula_time, - )) - } else { - None - }, - future_tos: if !future_tos_version.is_empty() { - Some(Doc::new( - future_tos_version, - future_tos_hash, - Type::TOS, - future_tos_time, - )) - } else { - None - }, - future_privacy: if !future_pp_version.is_empty() { - Some(Doc::new( - future_pp_version, - future_pp_hash, - Type::PP, - future_pp_time, - )) - } else { - None - }, - } - .sanitize(); - - state - } -} diff --git a/src/terms/doc.rs b/src/terms/doc.rs deleted file mode 100644 index 2365ba3..0000000 --- a/src/terms/doc.rs +++ /dev/null @@ -1,73 +0,0 @@ -use json::{JsonValue, object::Object}; - -use crate::{terms::terms_getter::Type, util::file_util::load_file}; - -#[derive(Clone, Debug, PartialEq, Eq)] -#[allow(unused)] -pub struct Doc { - version: String, - hash: String, - pub doc_type: Type, - timestamp: u64, -} - -#[allow(dead_code)] -impl Doc { - pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc { - Doc { - version, - hash, - doc_type, - timestamp, - } - } - - pub fn equals_some(&self, other: &Option) -> bool { - if let Some(other) = other { - self.get_version() == other.get_version() && self.get_hash() == other.get_hash() - } else { - false - } - } - pub fn equals(&self, other: &Self) -> bool { - self.get_version() == other.get_version() && self.get_hash() == other.get_hash() - } - - 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 { - 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, - }) - } -} diff --git a/src/terms/focus.rs b/src/terms/focus.rs deleted file mode 100644 index 669e936..0000000 --- a/src/terms/focus.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - Eula, - Tos, - Pp, - Cancel, - Continue, - ContinueAll, -} - -impl Focus { - pub fn next(&mut self, order: &[Focus]) { - if let Some(pos) = order.iter().position(|&f| f == *self) { - let next_pos = (pos + 1) % order.len(); - *self = order[next_pos]; - } - } - - pub fn prev(&mut self, order: &[Focus]) { - if let Some(pos) = order.iter().position(|&f| f == *self) { - let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 }; - *self = order[prev_pos]; - } - } -} diff --git a/src/terms/mod.rs b/src/terms/mod.rs deleted file mode 100644 index 4a11008..0000000 --- a/src/terms/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod buttons; -pub mod consent_state; -pub mod doc; -pub mod focus; -pub mod terms_getter; diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs deleted file mode 100755 index a0fa53b..0000000 --- a/src/terms/terms_getter.rs +++ /dev/null @@ -1,105 +0,0 @@ -use json::JsonValue::Object; - -use crate::terms::doc::Doc; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Type { - EULA, - TOS, - PP, -} - -impl Type { - pub fn to_str(&self) -> &str { - match self { - Self::EULA => "eula", - Self::TOS => "tos", - Self::PP => "privacy", - } - } - pub fn to_string(&self) -> String { - match self { - Self::EULA => "End User License Agreement".to_string(), - Self::TOS => "Terms of Service".to_string(), - Self::PP => "Privacy Policy".to_string(), - } - } -} - -pub fn get_link(terms_type: Type) -> String { - format!("https://legal.tensamin.net/{}/", terms_type.to_str()) -} -pub fn get_newest_link(terms_type: Type) -> String { - format!("https://legal.tensamin.net/{}/newest/", 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_newest_docs() -> Option<(Doc, Doc, Doc)> { - let body = reqwest::get("https://legal.tensamin.net/api/newest/") - .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 { - let body = reqwest::get(format!( - "https://legal.tensamin.net/api/text/{}/", - terms_type.to_str() - )) - .await - .ok()? - .text() - .await - .ok()?; - - Some(body) -} diff --git a/src/users/contact.rs b/src/users/contact.rs deleted file mode 100644 index 632cea3..0000000 --- a/src/users/contact.rs +++ /dev/null @@ -1,61 +0,0 @@ -use json::{self, JsonValue, number::Number}; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[derive(Debug, Clone)] -pub struct Contact { - pub user_id: i64, - pub user_name: Option, - pub last_message_at: Option, -} - -impl Default for Contact { - fn default() -> Self { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64; - Contact { - user_id: 0, - user_name: None, - last_message_at: Some(now), - } - } -} - -impl Contact { - pub fn new(user_id: i64) -> Self { - Contact { - user_id: user_id, - user_name: None, - last_message_at: None, - } - } - pub fn set_last_message_at(&mut self, p0: i64) { - self.last_message_at = Option::from(p0); - } - - pub fn to_json(&self) -> JsonValue { - let mut obj = JsonValue::new_object(); - obj["user_id"] = JsonValue::Number(Number::from(self.user_id)); - if let Some(name) = &self.user_name { - obj["user_name"] = JsonValue::from(name.as_str()); - } - if let Some(ts) = &self.last_message_at { - obj["last_message_at"] = JsonValue::Number(Number::from(*ts)); - } - obj - } - pub fn from_json(o: &JsonValue) -> Contact { - let user_id = o["user_id"].as_i64().unwrap_or(0); - - let user_name = o["user_name"].as_str().map(|s| s.to_string()); - - let last_message_at = o["last_message_at"].as_i64(); - - Contact { - user_id, - user_name, - last_message_at, - } - } -} diff --git a/src/users/mod.rs b/src/users/mod.rs deleted file mode 100644 index aef5a02..0000000 --- a/src/users/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod contact; -pub mod user_community_util; -pub mod user_manager; -pub mod user_profile; diff --git a/src/users/user_community_util.rs b/src/users/user_community_util.rs deleted file mode 100644 index 45b036c..0000000 --- a/src/users/user_community_util.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::util::file_util::save_file; -use json::{self, Array, JsonValue}; -use std::fs; -use std::path::Path; - -pub struct UserCommunityUtil; - -impl UserCommunityUtil { - pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { - let file_path = format!("users/{}/", storage_owner); - let mut communities = Self::load_array(&file_path); - - let mut community = JsonValue::new_object(); - community["title"] = JsonValue::String(title); - community["address"] = JsonValue::String(address); - community["position"] = JsonValue::String(position); - - communities.push(community); - - save_file( - &file_path, - "communities.json", - &JsonValue::Array(communities).to_string(), - ); - } - - pub fn remove_community(storage_owner: i64, community_address: String) { - let file_path = format!("users/{}/", storage_owner); - let communities = Self::load_array(&file_path); - - let filtered: Array = communities - .iter() - .filter(|entry| entry["address"].as_str() != Some(&community_address)) - .cloned() - .collect(); - save_file( - &file_path, - "communities.json", - &JsonValue::Array(filtered).to_string(), - ); - } - - pub fn get_communities(storage_owner: i64) -> Array { - let file_path = format!("users/{}/communities.json", storage_owner); - Self::load_array(&file_path) - } - - fn load_array(file_path: &str) -> Array { - if !Path::new(file_path).exists() { - return Array::new(); - } - - match fs::read_to_string(file_path) { - Ok(content) => { - let parsed = json::parse(&content); - match parsed { - Ok(JsonValue::Array(arr)) => arr, - _ => Array::new(), - } - } - Err(err) => { - eprintln!("Failed to read file {}: {}", file_path, err); - Array::new() - } - } - } -} diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs deleted file mode 100644 index bcb5e26..0000000 --- a/src/users/user_manager.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::omikron::omikron_connection::OMIKRON_CONNECTION; -use crate::users::user_profile::UserProfile; -use crate::util::crypto_helper::{self, public_key_to_base64}; -use crate::util::file_util::{load_file, save_file}; -use crate::util::logger::PrintType; -use crate::{RELOAD, SHUTDOWN}; -use crate::{log, log_cv}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use hex::{self}; -use json::JsonValue; -use once_cell::sync::Lazy; -use rand::Rng; -use rand_core::OsRng; -use rand_core::RngCore; -use sha2::{Digest, Sha256}; -use std::io::{self}; -use std::sync::Mutex; -use std::time::Duration; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use x448::{PublicKey, Secret}; - -static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); -static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); - -#[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::>(); - let uuid = segments[0].parse::().unwrap_or(0); - let b64_private_key = segments[1]; - - let secret: Secret = crypto_helper::load_secret_key(b64_private_key).unwrap(); - let public_key = PublicKey::from(&secret); - - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let reset_token = STANDARD.encode(&bytes); - - let user_profile = UserProfile::new( - uuid, - username.to_string(), - Some(username.to_string()), - crypto_helper::public_key_to_base64(&public_key), - crypto_helper::hex_hash(b64_private_key), - reset_token, - ); - USERS.lock().unwrap().push(user_profile); - Ok(()) -} - -pub async fn create_user(username: &str) -> (Option, Option) { - let register_cv = CommunicationValue::new(CommunicationType::get_register); - - let conn = OMIKRON_CONNECTION.clone(); - - let response_cv = match conn - .await_response(®ister_cv, Some(Duration::from_secs(20))) - .await - { - Ok(cv) => cv, - Err(_) => return (None, None), - }; - log_cv!(PrintType::Omega, response_cv); - - let user_id = match response_cv.get_data(DataTypes::user_id).as_number() { - Some(id) => id, - None => return (None, None), - }; - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - - let mut hasher = Sha256::new(); - hasher.update(&STANDARD.encode(&private_key.as_bytes()).as_bytes()); - let result = hasher.finalize(); - let private_key_hash = hex::encode(result); - - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let reset_token = STANDARD.encode(&bytes); - - let up = UserProfile::new( - user_id, - username.to_string(), - None, - STANDARD.encode(&public_key.as_bytes()), - private_key_hash, - reset_token.clone(), - ); - - let cv = CommunicationValue::new(CommunicationType::complete_register_user) - .add_data(DataTypes::user_id, DataValue::Number(user_id)) - .add_data(DataTypes::username, DataValue::Str(username.to_string())) - .add_data( - DataTypes::public_key, - DataValue::Str(public_key_to_base64(&public_key)), - ) - .add_data(DataTypes::iota_id, DataValue::Number(user_id)) - .add_data(DataTypes::reset_token, DataValue::Str(reset_token)); - - let response_cv = conn - .await_response(&cv, Some(Duration::from_secs(20))) - .await; - - if let Ok(resp) = response_cv { - log_cv!(PrintType::Omega, resp); - if !resp.is_type(CommunicationType::success) { - return (None, None); - } - } else { - return (None, None); - } - *SHUTDOWN.write().await = true; - *RELOAD.write().await = true; - log!("Created User"); - save_file( - "", - &format!("{}.tu", username), - &format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())), - ); - - USERS.lock().unwrap().push(up.clone()); - save_users(); - (Some(up), Some(STANDARD.encode(&private_key.as_bytes()))) -} - -pub fn get_user_by_username(username: &str) -> Option { - USERS - .lock() - .unwrap() - .iter() - .cloned() - .find(|u| u.username == username) -} - -pub fn get_user(user_id: i64) -> Option { - USERS - .lock() - .unwrap() - .iter() - .cloned() - .find(|u| u.user_id == user_id) -} - -pub fn get_users() -> Vec { - USERS.lock().unwrap().clone() -} - -pub fn remove_user(user_id: i64) { - let mut users = USERS.lock().unwrap(); - users.retain(|u| u.user_id != user_id); - *UNIQUE.lock().unwrap() = true; -} - -pub fn save_users() { - *UNIQUE.lock().unwrap() = false; - let users = USERS.lock().unwrap(); - let arr: Vec = users.iter().map(|u| u.to_json()).collect(); - let json_str = JsonValue::Array(arr).dump(); - - save_file("", "users.json", &json_str); -} - -pub fn clear() { - let mut users = USERS.lock().unwrap(); - users.clear(); - *UNIQUE.lock().unwrap() = true; -} - -pub async fn load_users() -> io::Result<()> { - let content = load_file("", "users.json"); - if content.trim().is_empty() { - return Ok(()); - } - - let parsed = - json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - if let JsonValue::Array(arr) = parsed { - let mut users = USERS.lock().unwrap(); - for j in arr.iter() { - if let Some(up) = UserProfile::from_json(j).await { - users.push(up); - } - } - } - if *UNIQUE.lock().unwrap() { - save_users(); - } - Ok(()) -} - -#[allow(dead_code)] -pub fn set_unique(val: bool) { - *UNIQUE.lock().unwrap() = val; -} diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs deleted file mode 100644 index 5d60277..0000000 --- a/src/users/user_profile.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::util::file_util::{has_file, load_file, used_dir_space}; -use base64::{Engine as _, engine::general_purpose}; -use json::{JsonValue, object}; -use rand::Rng; -use rand::rngs::OsRng; - -// --- UserProfile --- -#[derive(Clone, Debug)] -pub struct UserProfile { - pub user_id: i64, - pub username: String, - pub public_key: String, - pub private_key_hash: String, - pub reset_token: String, - pub created_at: i64, - pub display_name: Option, -} - -impl UserProfile { - pub fn new( - user_id: i64, - username: String, - display_name: Option, - public_key: String, - private_key_hash: String, - reset_token: String, - ) -> Self { - Self { - user_id, - username, - display_name, - public_key, - private_key_hash, - created_at: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64, - reset_token, - } - } - - pub fn to_json(&self) -> JsonValue { - let mut obj = object! { - "uuid" => self.user_id, - "username" => self.username.clone(), - "public_key" => self.public_key.clone(), - "private_key_hash" => self.private_key_hash.clone(), - "created_at" => self.created_at, - "reset_token" => self.reset_token.clone() - }; - if let Some(d) = &self.display_name { - obj["display_name"] = d.clone().into(); - } - obj - } - pub fn frontend(&self) -> JsonValue { - let mut obj = object! { - "uuid" => self.user_id, - "username" => self.username.clone(), - "public_key" => self.public_key.clone(), - "private_key_hash" => self.private_key_hash.clone(), - "created_at" => self.created_at, - "storage" => used_dir_space(&format!("users/{}", self.user_id.to_string())), - }; - if let Some(d) = &self.display_name { - obj["display_name"] = d.clone().into(); - } - if has_file("", &format!("{}.tu", self.username.clone())) { - obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into(); - } - - obj - } - pub async fn from_json(j: &JsonValue) -> Option { - let user_id = j["uuid"].as_i64()?; - let username = j["username"].as_str()?.to_string(); - let public_key = j["public_key"].as_str()?.to_string(); - let private_key_hash = j["private_key_hash"].as_str()?.to_string(); - let reset_token = j["reset_token"].as_str()?.to_string(); - let created_at = j["created_at"].as_i64()?; - let display_name = j["display_name"].as_str().map(|s| s.to_string()); - - let up = UserProfile { - user_id, - username, - display_name, - public_key, - private_key_hash, - created_at, - reset_token, - }; - - // TODO: Migrate to Omikron / Wss - /* if j.has_key("migrate") - || j.has_key("migrating") - || j.has_key("changing") - || j.has_key("move") - || j.has_key("moving") - { - if auth_connector::migrate_user(&mut up).await { - log_message(format!("[INFO] Migration triggered for {}", up.username)); - user_manager::set_unique(true); - } - } */ - - Some(up) - } - - #[allow(dead_code)] - pub fn randomize_reset_token(&mut self) -> String { - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let new_token = general_purpose::STANDARD.encode(&bytes); - self.reset_token = new_token.clone(); - new_token - } - - #[allow(dead_code)] - pub fn get_display_name(&self) -> String { - self.display_name - .clone() - .unwrap_or_else(|| self.username.clone()) - } -} diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs deleted file mode 100644 index e54fe29..0000000 --- a/src/util/chat_files.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::log; -use crate::util::db; -use json::{JsonValue, array, object}; -use rusqlite::params; -use std::io; -use std::sync::{Arc, LazyLock, Mutex}; - -#[derive(PartialEq, Debug, Clone)] -pub enum MessageState { - Read, - Received, - Sent, - Sending, -} - -impl MessageState { - pub fn as_str(&self) -> &'static str { - match self { - MessageState::Read => "read", - MessageState::Received => "received", - MessageState::Sent => "sent", - MessageState::Sending => "sending", - } - } - - pub fn from_str(value: &str) -> Self { - match value.to_lowercase().as_str() { - "read" => MessageState::Read, - "received" => MessageState::Received, - "sent" => MessageState::Sent, - _ => MessageState::Sending, - } - } - - pub fn upgrade(self, other: Self) -> Self { - if other == Self::Read || self == Self::Read { - Self::Read - } else if other == Self::Received || self == Self::Received { - Self::Received - } else if other == Self::Sent || self == Self::Sent { - Self::Sent - } else { - Self::Sending - } - } -} - -// Shared DB created via helper. -// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist. -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -pub fn add_message( - send_time: u128, - storage_owner_is_sender: bool, - storage_owner: i64, - external_user: i64, - message: &str, - height: i64, -) { - let message_time = match i64::try_from(send_time) { - Ok(v) => v, - Err(_) => { - log!("Failed to store message: send_time out of range for i64 ({send_time})"); - return; - } - }; - - // Insert the message into the DB - let insert_result = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO messages ( - storage_owner, - external_user, - message_time, - content, - sent_by_self, - message_state, - height - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - "#, - params![ - storage_owner, - external_user, - message_time, - message, - if storage_owner_is_sender { - 1_i64 - } else { - 0_i64 - }, - MessageState::Sending.as_str(), - height, - ], - )?; - Ok(()) - }); - - if let Err(e) = insert_result { - log!("Failed to insert message into sqlite: {}", e); - return; - } - - // Update contacts table to reflect that this conversation exists and has a recent message. - // Use the Contact helper to set last_message_at to the message timestamp. - let mut contact = crate::users::contact::Contact::new(external_user); - contact.set_last_message_at(message_time); - // This will insert or update the contact for the storage owner. - crate::util::chats_util::mod_user(storage_owner, &contact); -} - -pub fn change_message_state( - timestamp: i64, - storage_owner: i64, - external_user: i64, - new_state: MessageState, -) -> io::Result<()> { - // Run the SELECT and UPDATE inside with_conn to centralize connection access. - let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| { - let current: Option = match conn.query_row( - r#" - SELECT message_state - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - AND message_time = ?3 - ORDER BY id DESC - LIMIT 1 - "#, - params![storage_owner, external_user, timestamp], - |row| row.get(0), - ) { - Ok(state) => Some(state), - Err(rusqlite::Error::QueryReturnedNoRows) => None, - Err(e) => return Err(e), - }; - - let Some(current_state_raw) = current else { - return Ok(()); - }; - - let upgraded = MessageState::from_str(¤t_state_raw) - .upgrade(new_state) - .as_str() - .to_string(); - - conn.execute( - r#" - UPDATE messages - SET message_state = ?1 - WHERE id = ( - SELECT id - FROM messages - WHERE storage_owner = ?2 - AND external_user = ?3 - AND message_time = ?4 - ORDER BY id DESC - LIMIT 1 - ) - "#, - params![upgraded, storage_owner, external_user, timestamp], - )?; - Ok(()) - }); - - match res { - Ok(_) => Ok(()), - Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), - } -} - -pub fn get_messages( - storage_owner: i64, - external_user: i64, - loaded_messages: i64, - amount: i64, -) -> JsonValue { - let messages = array![]; - - if amount <= 0 || loaded_messages < 0 { - return messages; - } - - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT - message_time, - content, - sent_by_self, - message_state, - height - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - ORDER BY message_time DESC, id DESC - LIMIT ?3 OFFSET ?4 - "#, - )?; - - let rows = stmt.query_map( - params![storage_owner, external_user, amount, loaded_messages], - |row| { - let message_time: i64 = row.get(0)?; - let content: String = row.get(1)?; - let sent_by_self: i64 = row.get(2)?; - let message_state: String = row.get(3)?; - let height: i64 = row.get(4).unwrap_or(0); - Ok((message_time, content, sent_by_self, message_state, height)) - }, - )?; - - let mut out = array![]; - for row in rows { - match row { - Ok((message_time, content, sent_by_self, message_state, height)) => { - let msg = object! { - "message_time" => message_time, - "content" => content, - "sent_by_self" => (sent_by_self != 0), - "message_state" => message_state, - "height" => height - }; - if let Err(e) = out.push(msg) { - // out.push returns a JsonError; log it instead of using `?` to avoid - // incompatible error conversions inside the DB closure. - log!("Failed to append message to output array: {:?}", e); - } - } - Err(e) => { - log!("Failed to read row from sqlite: {}", e); - } - } - } - Ok(out) - }); - - match res { - Ok(v) => v, - Err(e) => { - log!("Failed to query messages: {}", e); - messages - } - } -} - -#[cfg(test)] -mod tests { - use super::MessageState; - - #[test] - fn upgrade_prefers_highest_state() { - assert_eq!( - MessageState::Sending.upgrade(MessageState::Sent), - MessageState::Sent - ); - assert_eq!( - MessageState::Sent.upgrade(MessageState::Received), - MessageState::Received - ); - assert_eq!( - MessageState::Received.upgrade(MessageState::Read), - MessageState::Read - ); - } - - #[test] - fn from_str_is_case_insensitive() { - assert_eq!(MessageState::from_str("READ"), MessageState::Read); - assert_eq!(MessageState::from_str("received"), MessageState::Received); - assert_eq!(MessageState::from_str("Sent"), MessageState::Sent); - assert_eq!(MessageState::from_str("unknown"), MessageState::Sending); - } -} diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs deleted file mode 100644 index b8e0ad2..0000000 --- a/src/util/chats_util.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::users::contact::Contact; -use crate::util::db; -use rusqlite::params; -use std::sync::{Arc, LazyLock, Mutex}; - -/// Shared DB connection for contacts/messages (created by db helper). -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -/// Insert or update a contact for the given storage owner. -pub fn mod_user(storage_owner: i64, contact: &Contact) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO contacts ( - storage_owner, - user_id, - user_name, - last_message_at - ) VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(storage_owner, user_id) DO UPDATE SET - user_name = excluded.user_name, - last_message_at = excluded.last_message_at - "#, - params![ - storage_owner, - contact.user_id, - contact.user_name.clone(), - contact.last_message_at - ], - )?; - Ok(()) - }) { - eprintln!("Failed to mod_user: {}", e); - } -} - -/// Retrieve a single contact for storage_owner/user_id. -pub fn get_user(storage_owner: i64, user_id: i64) -> Option { - let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { - match conn.query_row( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 AND user_id = ?2 - LIMIT 1 - "#, - params![storage_owner, user_id], - |r| { - let user_id: i64 = r.get(0)?; - let user_name: Option = r.get(1)?; - let last_message_at: Option = r.get(2)?; - Ok(Contact { - user_id, - user_name, - last_message_at, - }) - }, - ) { - Ok(c) => Ok(Some(c)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e), - } - }); - - match res { - Ok(opt) => opt, - Err(e) => { - eprintln!("Error querying user in get_user: {}", e); - None - } - } -} - -/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc. -pub fn get_users(storage_owner: i64) -> Vec { - let contacts_out = Vec::new(); - - let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 - ORDER BY - CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, - last_message_at DESC, - user_id ASC - "#, - )?; - - let rows = stmt.query_map(params![storage_owner], |r| { - let user_id: i64 = r.get(0)?; - let user_name: Option = r.get(1)?; - let last_message_at: Option = r.get(2)?; - Ok(Contact { - user_id, - user_name, - last_message_at, - }) - })?; - - let mut out = Vec::new(); - for row in rows { - match row { - Ok(contact) => out.push(contact), - Err(e) => eprintln!("Failed to read contact row: {}", e), - } - } - Ok(out) - }); - - match res { - Ok(v) => v, - Err(e) => { - eprintln!("Failed to query contacts in get_users: {}", e); - contacts_out - } - } -} diff --git a/src/util/communities_util.rs b/src/util/communities_util.rs deleted file mode 100644 index ed8b7ae..0000000 --- a/src/util/communities_util.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::util::db; -use json::Array; -use rusqlite::params; -use std::sync::{Arc, LazyLock, Mutex}; - -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -pub struct CommunitiesUtil; - -impl CommunitiesUtil { - pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO communities ( - storage_owner, - address, - title, - position - ) VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(storage_owner, address) DO UPDATE SET - title = excluded.title, - position = excluded.position - "#, - params![storage_owner, address, title, position], - )?; - Ok(()) - }) { - eprintln!("Failed to add_community: {}", e); - } - } - - pub fn remove_community(storage_owner: i64, community_address: String) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", - params![storage_owner, community_address], - )?; - Ok(()) - }) { - eprintln!("Failed to remove_community: {}", e); - } - } - - pub fn get_communities(storage_owner: i64) -> Array { - let communities_out = Array::new(); - - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT address, title, position - FROM communities - WHERE storage_owner = ?1 - "#, - )?; - - let rows = stmt.query_map(params![storage_owner], |r| { - let address: String = r.get(0)?; - let title: String = r.get(1)?; - let position: String = r.get(2)?; - Ok((address, title, position)) - })?; - - let mut out = Array::new(); - for row in rows { - match row { - Ok((address, title, position)) => { - let mut community = json::JsonValue::new_object(); - community["title"] = json::JsonValue::String(title); - community["address"] = json::JsonValue::String(address); - community["position"] = json::JsonValue::String(position); - out.push(community); - } - Err(e) => eprintln!("Failed to read community row: {}", e), - } - } - Ok(out) - }); - - match res { - Ok(arr) => arr, - Err(e) => { - eprintln!("Failed to query communities in get_communities: {}", e); - communities_out - } - } - } -} diff --git a/src/util/config_util.rs b/src/util/config_util.rs deleted file mode 100644 index 6330b59..0000000 --- a/src/util/config_util.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::util::file_util::{load_file, save_file}; -use json::JsonValue; -use once_cell::sync::Lazy; -use tokio::sync::RwLock; - -pub static CONFIG: Lazy> = Lazy::new(|| RwLock::new(ConfigUtil::new())); - -pub struct ConfigUtil { - pub config: JsonValue, - pub unique: bool, -} - -impl ConfigUtil { - pub fn new() -> Self { - Self { - config: JsonValue::new_object(), - unique: false, - } - } - pub fn clear(&mut self) { - self.config = JsonValue::new_object(); - } - pub fn load(&mut self) { - let s = load_file("", "config.json"); - if !s.is_empty() { - self.config = json::parse(&s).unwrap_or(JsonValue::new_object()); - } - } - - pub fn get_iota_id(&self) -> i64 { - self.config["iota_id"].as_i64().unwrap_or(0) - } - - pub fn get_port(&self) -> u16 { - self.config["port"].as_u16().unwrap_or(1984) - } - - pub fn get_public_key(&self) -> Option { - self.config["public_key"].as_str().map(String::from) - } - - pub fn get_private_key(&self) -> Option { - self.config["private_key"].as_str().map(String::from) - } - - pub fn get(&self, key: &str) -> &JsonValue { - &self.config[key] - } - - pub fn change(&mut self, key: &str, value: JsonValue) { - self.config[key] = value; - self.unique = true; - } - - pub fn update(&mut self) { - if self.unique { - save_file("", "config.json", &self.config.to_string()); - } - } -} diff --git a/src/util/crypto_helper.rs b/src/util/crypto_helper.rs old mode 100644 new mode 100755 index 4a7fd76..cb3ae6a --- a/src/util/crypto_helper.rs +++ b/src/util/crypto_helper.rs @@ -1,133 +1,149 @@ -use aes_gcm::{ - Aes256Gcm, Nonce, - aead::{Aead, KeyInit, OsRng}, -}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use rand_core::RngCore; -use sha2::{Digest, Sha256}; -use x448::{PublicKey, Secret, SharedSecret}; - -/// Errors for crypto opertions -#[derive(Debug)] -#[allow(dead_code)] -pub enum CryptoError { - Base64Decode(base64::DecodeError), - InvalidKey, - AgreementError, - EncryptionError(aes_gcm::Error), - DecryptionError(aes_gcm::Error), -} - -impl From for CryptoError { - fn from(err: base64::DecodeError) -> Self { - CryptoError::Base64Decode(err) - } -} - -pub struct KeyPair { - pub secret: Secret, - pub public: PublicKey, -} - -pub fn generate_keypair() -> KeyPair { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let secret = Secret::from_bytes(&buf).unwrap(); - let public = PublicKey::from(&secret); - KeyPair { secret, public } -} - -pub fn public_key_to_base64(pubkey: &PublicKey) -> String { - STANDARD.encode(pubkey.as_bytes().as_ref()) -} - -pub fn secret_key_to_base64(secret: &Secret) -> String { - STANDARD.encode(secret.as_bytes().as_ref()) -} - -pub fn load_public_key(base64_pub: &str) -> Option { - let bytes = STANDARD.decode(base64_pub).unwrap(); - PublicKey::from_bytes(&bytes) -} - -pub fn load_secret_key(base64_secret: &str) -> Option { - let bytes = STANDARD.decode(base64_secret).unwrap(); - 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()); - let result = hasher.finalize(); - let mut key = [0u8; 32]; - key.copy_from_slice(&result[..32]); - key -} - -#[allow(dead_code)] -pub fn encrypt( - base64_secret: &str, - base64_peer_pub: &str, - plaintext: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - let ciphertext = cipher - .encrypt(nonce, plaintext.as_bytes()) - .map_err(CryptoError::EncryptionError)?; - // prefix nonce to ciphertext - let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); - out.extend_from_slice(&nonce_bytes); - out.extend_from_slice(&ciphertext); - Ok(STANDARD.encode(&out)) -} - -#[allow(dead_code)] -pub fn decrypt( - base64_secret: &str, - base64_peer_pub: &str, - encrypted_base64: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - - let encrypted = STANDARD.decode(encrypted_base64)?; - if encrypted.len() < 12 { - return Err(CryptoError::DecryptionError(aes_gcm::Error)); - } - let nonce_bytes = &encrypted[..12]; - let ciphertext = &encrypted[12..]; - let nonce = Nonce::from_slice(nonce_bytes); - let plaintext_bytes = cipher - .decrypt(nonce, ciphertext) - .map_err(CryptoError::DecryptionError)?; - let plaintext = String::from_utf8(plaintext_bytes) - .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; - Ok(plaintext) -} - -pub fn hash_it(input: &str) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - hasher.finalize().to_vec() -} - -pub fn hex_hash(input: &str) -> String { - let digest = hash_it(input); - digest.iter().map(|b| format!("{:02x}", b)).collect() -} +use aes_gcm::{ + Aes256Gcm, Nonce, + aead::{Aead, KeyInit, OsRng}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use x448::{PublicKey, Secret, SharedSecret}; + +/// Errors for crypto operations +#[allow(dead_code)] +#[derive(Debug)] +pub enum CryptoError { + Base64Decode(base64::DecodeError), + InvalidKey, + AgreementError, + EncryptionError(aes_gcm::Error), + DecryptionError(aes_gcm::Error), +} + +impl From for CryptoError { + fn from(err: base64::DecodeError) -> Self { + CryptoError::Base64Decode(err) + } +} + +#[allow(dead_code)] +pub struct KeyPair { + pub secret: Secret, + pub public: PublicKey, +} + +#[allow(dead_code)] +pub fn generate_keypair() -> KeyPair { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let secret = Secret::from_bytes(&buf).unwrap(); + let public = PublicKey::from(&secret); + KeyPair { secret, public } +} + +pub fn public_key_to_base64(pubkey: &PublicKey) -> String { + STANDARD.encode(pubkey.as_bytes().as_ref()) +} + +pub fn secret_key_to_base64(secret: &Secret) -> String { + STANDARD.encode(secret.as_bytes().as_ref()) +} + +pub fn load_public_key(base64_pub: &str) -> Option { + let bytes = STANDARD.decode(base64_pub).unwrap(); + PublicKey::from_bytes(&bytes) +} + +pub fn load_secret_key(base64_secret: &str) -> Option { + let bytes = STANDARD.decode(base64_secret).unwrap(); + Secret::from_bytes(&bytes) +} + +fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(shared.as_bytes()); + let result = hasher.finalize(); + let mut key = [0u8; 32]; + key.copy_from_slice(&result[..32]); + key +} + +#[allow(dead_code)] +pub fn encrypt_b64( + base64_secret: &str, + base64_peer_pub: &str, + plaintext: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + encrypt(secret, peer_pub, plaintext) +} +pub fn encrypt( + secret: Secret, + peer_pub: PublicKey, + plaintext: &str, +) -> Result { + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(CryptoError::EncryptionError)?; + // prefix nonce to ciphertext + let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + Ok(STANDARD.encode(&out)) +} + +pub fn decrypt_b64( + base64_secret: &str, + base64_peer_pub: &str, + encrypted_base64: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + decrypt(secret, peer_pub, encrypted_base64) +} +pub fn decrypt( + secret: Secret, + peer_pub: PublicKey, + encrypted_base64: &str, +) -> Result { + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + + let encrypted = STANDARD.decode(encrypted_base64)?; + if encrypted.len() < 12 { + return Err(CryptoError::DecryptionError(aes_gcm::Error)); + } + let nonce_bytes = &encrypted[..12]; + let ciphertext = &encrypted[12..]; + let nonce = Nonce::from_slice(nonce_bytes); + let plaintext_bytes = cipher + .decrypt(nonce, ciphertext) + .map_err(CryptoError::DecryptionError)?; + let plaintext = String::from_utf8(plaintext_bytes) + .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; + Ok(plaintext) +} + +#[allow(dead_code)] +pub fn hash_it(input: &str) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + hasher.finalize().to_vec() +} + +#[allow(dead_code)] +pub fn hex_hash(input: &str) -> String { + let digest = hash_it(input); + digest.iter().map(|b| format!("{:02x}", b)).collect() +} diff --git a/src/util/db.rs b/src/util/db.rs deleted file mode 100644 index 9fb40a0..0000000 --- a/src/util/db.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Database helper utilities. -//! -//! This module provides small helpers to open/init sqlite databases and to -//! create a shared (Arc>) connection wrapper callers can -//! reuse. The goal is to centralize the "open and initialize" logic and -//! provide small convenience helpers used by other util modules. - -use crate::util::file_util::get_directory; -use rusqlite::{Connection, Error as RusqliteError}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -/// Returns the file path for a named DB inside the application's data directory. -/// -/// Arguments: -/// - `db_name` : name of the DB (without extension). Example: `"messages"`. -pub fn db_file_path(db_name: &str) -> String { - let mut p = PathBuf::from(get_directory()); - p.push(format!("{db_name}.sqlite3")); - p.to_string_lossy().to_string() -} - -/// Open a sqlite connection to the named DB file (no initialization). -/// -/// Arguments: -/// - `db_name`: name of the DB (without extension). -pub fn open_connection(db_name: &str) -> Result { - let path = db_file_path(db_name); - Connection::open(path) -} - -/// Open a connection and immediately run `init_sql` via `execute_batch`. -/// -/// Arguments: -/// - `db_name`: name of the DB (without extension). -/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple). -pub fn open_and_init(db_name: &str, init_sql: &str) -> Result { - let conn = open_connection(db_name)?; - conn.execute_batch(init_sql)?; - Ok(conn) -} - -/// Create a shared, Arc> initialized with the given SQL. -/// -/// This is a convenience wrapper that returns an owned Arc> -/// so caller modules can store it in a `static` or pass it around. -/// -/// Arguments: -/// - `db_name`: DB name (without extension). -/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements). -pub fn create_shared_connection( - db_name: &str, - init_sql: &str, -) -> Result>, String> { - match open_and_init(db_name, init_sql) { - Ok(conn) => { - // Configure some sensible defaults for concurrency - // Attempt to set a busy timeout to reduce SQLITE_BUSY failures. - let _ = conn.busy_timeout(Duration::from_millis(250)); - Ok(Arc::new(Mutex::new(conn))) - } - Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)), - } -} - -/// Acquire the Connection from an Arc> and run the provided -/// closure. Converts rusqlite::Error into a String on error. -/// -/// Arguments: -/// - `shared`: Arc> -/// - `f`: closure that receives &Connection and returns Result -/// -/// Returns Ok(T) or Err(String). -pub fn with_conn(shared: &Arc>, f: F) -> Result -where - F: FnOnce(&Connection) -> Result, -{ - // When invoked from within an async runtime (such as Tokio), taking a blocking - // std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent - // awaits. Detect whether we're running inside a Tokio runtime and, if so, - // execute the blocking lock + database closure using Tokio's blocking helper. - // - // The blocking section returns Result so we can propagate errors - // in the same form as before. - if tokio::runtime::Handle::try_current().is_ok() { - tokio::task::block_in_place(|| { - let guard = shared - .lock() - .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; - f(&*guard).map_err(|e| e.to_string()) - }) - } else { - let guard = shared - .lock() - .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; - f(&*guard).map_err(|e| e.to_string()) - } -} - -/// Initialize a general-purpose messages+contacts DB and return a shared -/// connection. This helper creates a single DB file that can contain multiple -/// tables (messages, contacts, ...). The SQL here is conservative and intended -/// to be safe if called multiple times. -/// -/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)` -/// directly, but this convenience is useful for code that expects both tables. -pub fn create_general_messages_db() -> Result>, String> { - // Keep PRAGMA and schema in one multi-statement string so callers only - // need to call a single execute_batch. - const INIT_SQL: &str = r#" - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - external_user INTEGER NOT NULL, - message_time INTEGER NOT NULL, - content TEXT NOT NULL, - sent_by_self INTEGER NOT NULL, - message_state TEXT NOT NULL, - height INTEGER NOT NULL DEFAULT 0 - ); - - CREATE INDEX IF NOT EXISTS idx_messages_lookup - ON messages (storage_owner, external_user, message_time DESC); - - CREATE TABLE IF NOT EXISTS contacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - user_id INTEGER NOT NULL, - user_name TEXT, - last_message_at INTEGER, - UNIQUE(storage_owner, user_id) - ); - - CREATE INDEX IF NOT EXISTS idx_contacts_owner - ON contacts (storage_owner, last_message_at DESC, user_id ASC); - - CREATE TABLE IF NOT EXISTS communities ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - address TEXT NOT NULL, - title TEXT NOT NULL, - position TEXT NOT NULL, - UNIQUE(storage_owner, address) - ); - - CREATE INDEX IF NOT EXISTS idx_communities_owner - ON communities (storage_owner); - "#; - - match create_shared_connection("messages", INIT_SQL) { - Ok(shared_conn) => { - // Attempt to add the height column for backwards compatibility. - // This will fail if the column already exists, which is expected. - let _ = with_conn(&shared_conn, |conn| { - let _ = conn.execute( - "ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0", - [], - ); - Ok(()) - }); - Ok(shared_conn) - } - Err(e) => Err(e), - } -} - -/* -Example usage: - -// In some util module (at init time, e.g. lazy_static or LazyLock) -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - create_general_messages_db().expect("failed to create messages DB") -}); - -// Later, to run a query: -let res: Result, String> = with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare("SELECT ...")?; - let rows = stmt.query_map(...)?; - // collect and return Ok(...) -}); -*/ diff --git a/src/util/file_util.rs b/src/util/file_util.rs old mode 100755 new mode 100644 index 2d00718..cd214b1 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -1,15 +1,9 @@ -use reqwest::Client; -use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, BufReader, Read}; use std::path::{Path, PathBuf}; -use sysinfo::System; -use tokio::io::AsyncWriteExt; -use uuid::Uuid; -use walkdir::WalkDir; -use zip::ZipArchive; use crate::log; +use crate::util::logger::PrintType; #[allow(dead_code)] pub fn delete_directory(path: &str) -> bool { @@ -24,6 +18,8 @@ fn delete_dir_recursive(directory: &Path) -> bool { } if let Err(e) = fs::remove_dir_all(directory) { log!( + 0, + PrintType::General, "[IMPORTANT] Couldn't delete directory {}: {}", directory.display(), e, @@ -41,6 +37,7 @@ pub fn delete_user_directory(user_id: i64) { let _ = delete_dir_recursive(&user_dir); } +#[allow(dead_code)] pub fn load_file_buf(path: &str, name: &str) -> io::Result> { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); @@ -67,6 +64,7 @@ pub fn load_file_buf(path: &str, name: &str) -> io::Result> { let file = File::open(&file_path)?; Ok(BufReader::new(file)) } +#[allow(dead_code)] pub fn has_file(path: &str, name: &str) -> bool { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); @@ -81,6 +79,7 @@ pub fn has_file(path: &str, name: &str) -> bool { true } +#[allow(dead_code)] pub fn has_dir(path: &str) -> bool { let dir = Path::new(&get_directory()).join(path); @@ -91,13 +90,19 @@ pub fn has_dir(path: &str) -> bool { true } +#[allow(dead_code)] pub fn load_file(path: &str, name: &str) -> String { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); + log!( + 0, + PrintType::General, + "[IMPORTANT] Couldn't create directories: {}", + e + ); return String::new(); } return String::new(); @@ -105,7 +110,12 @@ pub fn load_file(path: &str, name: &str) -> String { if !file_path.exists() { if let Err(e) = File::create(&file_path) { - log!("[IMPORTANT] Couldn't create file: {}", e); + log!( + 0, + PrintType::General, + "[IMPORTANT] Couldn't create file: {}", + e + ); } return String::new(); } @@ -124,19 +134,27 @@ pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> std::fs::read(file_path) } +#[allow(dead_code)] pub fn save_file(path: &str, name: &str, value: &str) { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); + log!( + 0, + PrintType::General, + "[IMPORTANT] Couldn't create directories: {}", + e + ); return; } } if let Err(e) = fs::write(&file_path, value) { log!( + 0, + PrintType::General, "[IMPORTANT] Couldn't write file {}: {}", file_path.display(), e @@ -144,6 +162,7 @@ pub fn save_file(path: &str, name: &str, value: &str) { } } +#[allow(dead_code)] pub fn get_children(path: &str) -> Vec { let dir = Path::new(&get_directory()).join(path); let mut children = Vec::new(); @@ -164,191 +183,3 @@ pub fn get_directory() -> String { .to_string_lossy() .to_string() } - -// Helper to download the zip file content to a file on disk -#[allow(dead_code)] -pub fn used_space() -> u64 { - get_directory_size(&PathBuf::from(get_directory())) -} -pub fn used_dir_space(path: &str) -> u64 { - get_directory_size(&PathBuf::from(format!("{}/{}", get_directory(), path))) -} - -pub fn get_directory_size(directory: &Path) -> u64 { - let mut size = 0; - for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() { - if let Ok(metadata) = path.metadata() { - size += path.file_name().unwrap_or(OsStr::new("")).len() as u64; - size += metadata.len(); - } - } - } - size -} - -#[allow(dead_code)] -pub fn get_designed_storage(user_id: i64) -> String { - let user_dir = Path::new(&get_directory()) - .join("users") - .join(user_id.to_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; - let m = k / 1024.0; - let g = m / 1024.0; - let t = g / 1024.0; - - if t >= 1.0 { - hr_size = format!("{:.2}TB", t); - } else if g >= 1.0 { - hr_size = format!("{:.2}GB", g); - } else if m >= 1.0 { - hr_size = format!("{:.2}MB", m); - } else if k >= 1.0 { - hr_size = format!("{:.2}KB", k); - } - hr_size -} - -#[allow(dead_code)] -pub fn get_used_ram() -> String { - let mut sys = System::new_all(); - sys.refresh_all(); - let used = sys.used_memory() * 1024; // kB to bytes - let total = sys.total_memory() * 1024; - format!("{}/{}", design_byte(used), design_byte(total)) -} - -pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box> { - let client = Client::new(); - let mut response = client.get(url).send().await?; - - if !response.status().is_success() { - let err_msg = format!("Failed to download file: Status {}", response.status()); - log!("{}", err_msg.clone()); - return Err(err_msg.into()); - } - - let mut zip_file = tokio::fs::File::create(zip_path).await?; - while let Some(chunk) = response.chunk().await? { - zip_file.write_all(&chunk).await?; - } - - zip_file.flush().await?; - Ok(()) -} - -#[allow(dead_code, deprecated)] -fn extract_zip_contents_to_folder( - zip_path: &Path, - target_dir: &Path, -) -> Result<(), Box> { - let file = File::open(zip_path)?; - let mut archive = ZipArchive::new(file)?; - - let staging_dir = target_dir.with_extension("staging"); - - let _ = fs::remove_dir_all(&staging_dir); - fs::create_dir_all(&staging_dir)?; - - let mut first_item_name: Option = None; - - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let entry_path = staging_dir.join(file.sanitized_name()); - - if i == 0 { - if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 { - first_item_name = Some(file.sanitized_name()); - } - } - - if file.name().ends_with('/') { - fs::create_dir_all(&entry_path)?; - } else { - if let Some(parent) = entry_path.parent() { - fs::create_dir_all(parent)?; - } - let mut out_file = File::create(entry_path)?; - io::copy(&mut file, &mut out_file)?; - } - } - - if let Some(root_path) = first_item_name { - let root_dir = staging_dir.join(&root_path); - - if root_dir.is_dir() { - let root_contents_count = fs::read_dir(&staging_dir)?.count(); - - if root_contents_count == 1 - || (root_contents_count > 1 && fs::metadata(&root_dir).is_ok()) - { - let _ = fs::remove_dir_all(target_dir); - fs::create_dir_all(target_dir)?; - - for entry in fs::read_dir(root_dir)? { - let entry = entry?; - let src = entry.path(); - let dest = target_dir.join(entry.file_name()); - - if let Err(_) = fs::rename(&src, &dest) { - if src.is_file() { - fs::copy(&src, &dest)?; - } else { - if entry.path().is_dir() { - fs::rename(&src, &dest)?; - } - } - } - } - - let _ = fs::remove_dir_all(&staging_dir); - return Ok(()); - } - } - } - - log!("Extracting directly (no single root folder detected)."); - let _ = fs::remove_dir_all(target_dir); - fs::rename(&staging_dir, target_dir)?; - - Ok(()) -} - -#[allow(dead_code)] -pub async fn download_and_extract_zip(url: &str, as_name: &str) { - log!("Downloading ZIP file..."); - let base_dir = PathBuf::from(get_directory()); - let zip_filename = format!("{}.zip", Uuid::new_v4()); - let zip_path = base_dir.join(&zip_filename); - let target_dir = base_dir.join(as_name); - - if let Err(e) = download_zip(url, &zip_path).await { - log!("Error downloading file: {}", e); - return; - } - - let zip_path_clone = zip_path.clone(); - let target_dir_clone = target_dir.clone(); - let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone); - - let successful = match extract_result { - Ok(()) => true, - Err(e) => { - log!("Error during ZIP extraction: {}", e); - false - } - }; - - if let Err(e) = tokio::fs::remove_file(&zip_path).await { - log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); - } else if successful { - log!("Downloaded and extracted ZIP file successfully."); - } -} diff --git a/src/util/logger.rs b/src/util/logger.rs old mode 100755 new mode 100644 index a5feb02..9e9a085 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -3,24 +3,18 @@ use std::{ fs::{self, OpenOptions}, io::Write, path::Path, - sync::{OnceLock, atomic::Ordering, mpsc}, + sync::{OnceLock, mpsc}, thread, time::{SystemTime, UNIX_EPOCH}, }; -use ratatui::style::Color; +use ansi_term::Color; use ttp_core::{CommunicationValue, DataTypes, DataValue}; -use crate::{ - APP_STATE, - gui::{elements::log_card::LogEntry, ui::UNIQUE}, - langu::language_manager, -}; - static LOGGER: OnceLock> = OnceLock::new(); -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[allow(unused)] +#[allow(dead_code)] +#[derive(Clone, Copy)] pub enum PrintType { Call, Client, @@ -28,30 +22,15 @@ pub enum PrintType { Omikron, Omega, General, - Command, -} -impl PrintType { - pub fn prefix_color(self) -> Color { - match self { - PrintType::Call => Color::Magenta, - PrintType::Client => Color::Green, - PrintType::Iota => Color::Yellow, - PrintType::Omikron => Color::Blue, - PrintType::Omega => Color::Cyan, - PrintType::General => Color::LightCyan, - PrintType::Command => Color::LightGreen, - } - } } struct LogMessage { timestamp_ms: u128, - prefix: String, + sender: Option, + prefix: &'static str, kind: PrintType, is_error: bool, - translation_key: Option, - format_args: Vec, - message: Option, + message: String, } pub fn startup() { @@ -75,44 +54,34 @@ pub fn startup() { .expect("Failed to open log file"); for msg in rx { - let resolved_message = if let Some(key) = msg.translation_key { - let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); - language_manager::format(&key, &args) - } else { - msg.message.unwrap_or_default() + let ts = fixed_box(&msg.timestamp_ms.to_string(), 13); + let sender = match msg.sender { + Some(id) => fixed_box(&id.to_string(), 19), + _ => fixed_box("", 19), }; - let timestamp = format_timestamp_inline(msg.timestamp_ms); - let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); + let line = format!("{} {} {} {}", ts, sender, msg.prefix, msg.message); - let prefix = if msg.prefix.is_empty() { - String::new() - } else { - format!("{} ", msg.prefix) - }; + println!("{}", colorize(msg.kind, msg.is_error).paint(&line)); - let _ = writeln!( - file, - "{} {}{}", - fixed_box(&msg.timestamp_ms.to_string(), 13), - prefix, - entry.message - ); - - let _ = writeln!(file, " {}", timestamp); - - let mut state = APP_STATE.lock().unwrap(); - state.push_log(entry.into()); + let _ = writeln!(file, "{}", line); } }); } -fn format_timestamp_inline(timestamp_ms: u128) -> String { - let secs = (timestamp_ms / 1000) as i64; - let hours = (secs / 3600) % 24; - let minutes = (secs / 60) % 60; - let seconds = secs % 60; - format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) +fn colorize(kind: PrintType, is_error: bool) -> Color { + if is_error { + return Color::Red; + } + + match kind { + PrintType::Call => Color::Purple, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::White, + } } fn fixed_box(content: &str, width: usize) -> String { @@ -125,172 +94,51 @@ fn fixed_box(content: &str, width: usize) -> String { } } -fn colorize(kind: PrintType, is_error: bool) -> Color { - if is_error { - return Color::Red; - } - - match kind { - PrintType::Call => Color::Magenta, - PrintType::Client => Color::Green, - PrintType::Iota => Color::Yellow, - PrintType::Omikron => Color::Blue, - PrintType::Omega => Color::Cyan, - PrintType::General => Color::LightCyan, - PrintType::Command => Color::LightGreen, - } -} - -pub fn log_internal_translated( +pub fn log_internal( + sender: i64, kind: PrintType, - prefix: String, + prefix: &'static str, is_error: bool, - key: &str, - args: Vec, + message: String, ) { + let sender = if sender == 0 { None } else { Some(sender) }; + if let Some(tx) = LOGGER.get() { - UNIQUE.store(true, Ordering::Relaxed); let _ = tx.send(LogMessage { timestamp_ms: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), + sender, prefix, kind, is_error, - translation_key: Some(key.to_string()), - format_args: args, - message: None, + message, }); } } - -pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { - if let Some(tx) = LOGGER.get() { - UNIQUE.store(true, Ordering::Relaxed); - let _ = tx.send(LogMessage { - timestamp_ms: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - prefix, - kind, - is_error, - translation_key: None, - format_args: Vec::new(), - message: Some(message), - }); - } -} - -#[macro_export] -macro_rules! log_t { - ($key:expr) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - $key, - vec![] - ) - }; - - ($key:expr, $($arg:expr),+) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - $key, - vec![$($arg),+] - ) - }; -} - -#[macro_export] -macro_rules! log_t_err { - ($key:expr) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - true, - $key, - vec![] - ) - }; - - ($key:expr, $($arg:expr),+) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - true, - $key, - vec![$($arg.to_string()),+] - ) - }; -} - -/// Log a command message. -#[macro_export] -macro_rules! log_command { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::Command, - "".to_string(), - false, - format!($($arg)*) - ) - }; -} - -/// Log a general informational message. #[macro_export] macro_rules! log { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - format!($($arg)*) - ) + ($sender: expr, $kind:expr, $($arg:tt)*) => { + $crate::util::logger::log_internal($sender, $kind, "", false, format!($($arg)*)) }; } - -/// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - ">".to_string(), - false, - format!($($arg)*) - ) + ($sender: expr, $kind:expr, $($arg:tt)*) => { + $crate::util::logger::log_internal($sender, $kind, ">", false, format!($($arg)*)) }; } - -/// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - "<".to_string(), - false, - format!($($arg)*) - ) + ($sender:expr, $kind:expr, $($arg:tt)*) => { + $crate::util::logger::log_internal($sender, $kind, "<", false, format!($($arg)*)) }; } - -/// Log an error message (`>>`). #[macro_export] macro_rules! log_err { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - ">>".to_string(), - true, - format!($($arg)*) - ) + ($sender:expr, $kind:expr, $($arg:tt)*) => { + $crate::util::logger::log_internal($sender, $kind, ">>", true, format!($($arg)*)) }; } @@ -303,8 +151,9 @@ pub fn log_cv_internal( let formatted = format_cv(cv); log_internal( + cv.get_sender() as i64, print_type.unwrap_or(PrintType::General), - prefix.to_string(), + prefix, false, formatted, ); @@ -420,7 +269,6 @@ macro_rules! log_cv_in { $crate::util::logger::log_cv_internal("> ", &$cv, None) }; } - #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => { diff --git a/src/util/mod.rs b/src/util/mod.rs index 47901f9..1897223 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,9 +1,4 @@ -pub mod chat_files; -pub mod chats_util; -pub mod communities_util; -pub mod config_util; pub mod crypto_helper; pub mod crypto_util; -pub mod db; pub mod file_util; pub mod logger; diff --git a/static/web/404.html b/static/web/404.html deleted file mode 100644 index cb6efe1..0000000 --- a/static/web/404.html +++ /dev/null @@ -1,4 +0,0 @@ - - -

404

- From 60a0d47df5a59e5e31ab597b9378c262de32bbb2 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:25:57 +0200 Subject: [PATCH 4/6] Sync --- .cargo_toml_backup | 76 + .github/workflows/build.yml | 23 + .gitignore | 21 +- Cargo.lock | 3081 +++++++++++------ Cargo.toml | 61 +- README.md | 2 +- dockerfile | 20 + .../anonymous_client_connection.rs | 519 --- src/anonymous_clients/anonymous_manager.rs | 53 - src/anonymous_clients/mod.rs | 2 - src/auth/auth_user.rs | 18 + src/auth/local_auth.rs | 18 + src/auth/mod.rs | 2 + src/calls/call_group.rs | 106 - src/calls/call_manager.rs | 97 - src/calls/call_util.rs | 151 - src/calls/caller.rs | 49 - src/calls/mod.rs | 4 - src/communities/community.rs | 356 ++ src/communities/community_connection.rs | 396 +++ src/communities/community_manager.rs | 59 + src/communities/interactables/category.rs | 121 + src/communities/interactables/interactable.rs | 35 + src/communities/interactables/registry.rs | 75 + src/communities/interactables/text_chat.rs | 261 ++ src/communities/interactables/voice_chat.rs | 187 + src/communities/mod.rs | 13 + src/communities/perms/permission.rs | 27 + src/data/mod.rs | 1 - src/data/user.rs | 30 - src/gui/app_state.rs | 201 ++ src/gui/elements/console_card.rs | 491 +++ src/gui/elements/elements.rs | 49 + src/gui/elements/graph_card.rs | 215 ++ src/gui/elements/log_card.rs | 483 +++ src/gui/input_handler.rs | 56 + src/gui/interaction_result.rs | 49 + src/gui/mod.rs | 20 + src/gui/screens/main_screen.rs | 242 ++ src/gui/screens/md_viewer.rs | 493 +++ src/gui/screens/screens.rs | 25 + src/gui/screens/terms_checker.rs | 351 ++ src/gui/screens/terms_updater.rs | 691 ++++ src/gui/tui.rs | 0 src/gui/ui.rs | 167 + src/gui/util/borders.rs | 63 + src/langu/language_creator.rs | 80 + src/langu/language_manager.rs | 105 + src/langu/mod.rs | 2 + src/main.rs | 214 +- src/omega/mod.rs | 1 - src/omega/omega_connection.rs | 741 ---- src/omega/ping_pong_task.rs | 41 - src/omikron/mod.rs | 2 + src/omikron/omikron_connection.rs | 1159 +++++++ src/omikron/ping_pong_task.rs | 38 + src/rho/client_connection.rs | 583 ---- src/rho/connection.rs | 358 -- src/rho/iota_connection.rs | 461 --- src/rho/mod.rs | 6 - src/rho/rho_connection.rs | 199 -- src/rho/rho_manager.rs | 83 - src/rho/server.rs | 25 - src/server/api.rs | 190 + src/server/mod.rs | 3 + src/server/server.rs | 169 + src/server/web_path_parser.rs | 77 + src/terms/buttons.rs | 175 + src/terms/consent_state.rs | 491 +++ src/terms/doc.rs | 73 + src/terms/focus.rs | 25 + src/terms/mod.rs | 5 + src/terms/terms_getter.rs | 105 + src/users/contact.rs | 61 + src/users/mod.rs | 4 + src/users/user_community_util.rs | 67 + src/users/user_manager.rs | 197 ++ src/users/user_profile.rs | 126 + src/util/chat_files.rs | 276 ++ src/util/chats_util.rs | 121 + src/util/communities_util.rs | 90 + src/util/config_util.rs | 60 + src/util/crypto_helper.rs | 282 +- src/util/db.rs | 185 + src/util/file_util.rs | 227 +- src/util/logger.rs | 246 +- src/util/mod.rs | 5 + static/web/404.html | 4 + 88 files changed, 12040 insertions(+), 4782 deletions(-) create mode 100644 .cargo_toml_backup create mode 100644 .github/workflows/build.yml create mode 100644 dockerfile delete mode 100644 src/anonymous_clients/anonymous_client_connection.rs delete mode 100644 src/anonymous_clients/anonymous_manager.rs delete mode 100644 src/anonymous_clients/mod.rs create mode 100644 src/auth/auth_user.rs create mode 100644 src/auth/local_auth.rs create mode 100644 src/auth/mod.rs delete mode 100644 src/calls/call_group.rs delete mode 100644 src/calls/call_manager.rs delete mode 100644 src/calls/call_util.rs delete mode 100644 src/calls/caller.rs delete mode 100644 src/calls/mod.rs create mode 100644 src/communities/community.rs create mode 100644 src/communities/community_connection.rs create mode 100644 src/communities/community_manager.rs create mode 100644 src/communities/interactables/category.rs create mode 100644 src/communities/interactables/interactable.rs create mode 100644 src/communities/interactables/registry.rs create mode 100644 src/communities/interactables/text_chat.rs create mode 100644 src/communities/interactables/voice_chat.rs create mode 100644 src/communities/mod.rs create mode 100644 src/communities/perms/permission.rs delete mode 100644 src/data/mod.rs delete mode 100644 src/data/user.rs create mode 100755 src/gui/app_state.rs create mode 100644 src/gui/elements/console_card.rs create mode 100644 src/gui/elements/elements.rs create mode 100644 src/gui/elements/graph_card.rs create mode 100755 src/gui/elements/log_card.rs create mode 100644 src/gui/input_handler.rs create mode 100644 src/gui/interaction_result.rs create mode 100644 src/gui/mod.rs create mode 100644 src/gui/screens/main_screen.rs create mode 100644 src/gui/screens/md_viewer.rs create mode 100644 src/gui/screens/screens.rs create mode 100644 src/gui/screens/terms_checker.rs create mode 100644 src/gui/screens/terms_updater.rs create mode 100644 src/gui/tui.rs create mode 100644 src/gui/ui.rs create mode 100644 src/gui/util/borders.rs create mode 100644 src/langu/language_creator.rs create mode 100644 src/langu/language_manager.rs create mode 100644 src/langu/mod.rs delete mode 100644 src/omega/mod.rs delete mode 100644 src/omega/omega_connection.rs delete mode 100644 src/omega/ping_pong_task.rs create mode 100644 src/omikron/mod.rs create mode 100755 src/omikron/omikron_connection.rs create mode 100644 src/omikron/ping_pong_task.rs delete mode 100644 src/rho/client_connection.rs delete mode 100755 src/rho/connection.rs delete mode 100755 src/rho/iota_connection.rs delete mode 100644 src/rho/mod.rs delete mode 100644 src/rho/rho_connection.rs delete mode 100644 src/rho/rho_manager.rs delete mode 100644 src/rho/server.rs create mode 100755 src/server/api.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/server.rs create mode 100755 src/server/web_path_parser.rs create mode 100644 src/terms/buttons.rs create mode 100644 src/terms/consent_state.rs create mode 100644 src/terms/doc.rs create mode 100644 src/terms/focus.rs create mode 100644 src/terms/mod.rs create mode 100755 src/terms/terms_getter.rs create mode 100644 src/users/contact.rs create mode 100644 src/users/mod.rs create mode 100644 src/users/user_community_util.rs create mode 100644 src/users/user_manager.rs create mode 100644 src/users/user_profile.rs create mode 100644 src/util/chat_files.rs create mode 100644 src/util/chats_util.rs create mode 100644 src/util/communities_util.rs create mode 100644 src/util/config_util.rs mode change 100755 => 100644 src/util/crypto_helper.rs create mode 100644 src/util/db.rs mode change 100644 => 100755 src/util/file_util.rs mode change 100644 => 100755 src/util/logger.rs create mode 100644 static/web/404.html diff --git a/.cargo_toml_backup b/.cargo_toml_backup new file mode 100644 index 0000000..f7a9016 --- /dev/null +++ b/.cargo_toml_backup @@ -0,0 +1,76 @@ +[package] +name = "iota" +version = "0.1.0" +edition = "2024" + +[dependencies] +actix = "0.13.5" +actix-rt = "2.11.0" +actix-web = { version = "4.12.1", features = ["rustls-0_23"] } +actix-web-actors = "4.3.1" +aes-gcm = "*" +axum = { version = "0.8.8", features = ["ws", "http2"] } +base64 = "0.22.1" +bytes = "*" +cmake = "*" +color-eyre = "*" +crossterm = "*" +der = "*" +futures = "*" +futures-util = "*" +hex = "*" +hkdf = "*" +hmac = "*" +hyper-util = { version = "*" } +hyper = { version = "1.8.1", features = [ + "capi", + "client", + "full", + "http1", + "http2", + "nightly", + "server", +] } +http-body-util = "*" +json = "*" +native-tls = { version = "*", default-features = false } +once_cell = "1.21.3" +pkcs8 = { version = "*", features = ["alloc"] } +rand = "0.8" +rand_core = { version = "0.6", features = ["getrandom", "std"] } +reactive-rs = "*" +aws-lc-rs = "*" +reqwest = "0.12.23" +rustls = { version = "*", features = ["aws-lc-rs"] } +serde = { version = "1.0.219", features = ["derive"] } +sha2 = "*" +sys-info = "*" +sysinfo = "0.30" +tokio = { version = "1.48.0", features = ["full"] } +tokio-util = { version = "*", features = ["full"] } +tokio-tungstenite = { version = "*", features = ["native-tls"] } +tower = "*" +tungstenite = "*" +uuid = { version = "*", features = ["v4"] } +walkdir = "2.5.0" +warp = "*" +x448 = { version = "*" } +x509 = "*" +tokio-rustls = "0.26.4" +rustls-pemfile = "2.2.0" +async-trait = "0.1.89" +sha1 = "0.10.6" +async-tungstenite = "0.32.0" +zip = "6.0.0" +tui-textarea = "0.7.0" +tui-input = "0.14.0" +pnet = "0.35.0" +dashmap = "6.1.0" +strum = "0.27.2" +strum_macros = "0.27.2" +ratatui = "0.30.0" +ratatui_input = "0.1.3" +open = "5.3.3" +chrono = "0.4.43" +axum-server = { version = "0.8.0", features = ["tls-rustls"] } +serde_json = "1.0.149" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4265c56 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build & Publish Docker Image + +on: + workflow_dispatch: + +jobs: + build: + name: Build & Publish Docker Image + runs-on: ubuntu-latest + steps: + - name: Set up repository + uses: actions/checkout@v4 + + - name: Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWD }} + + - name: Build & Push + run: | + docker build -t tensamin/iota:latest . + docker push tensamin/iota:latest diff --git a/.gitignore b/.gitignore index 1baef92..0000aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,2 @@ -# Generated by Cargo -# will have compiled files and executables -debug -target - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb -*.env -# Generated by cargo mutants -# Contains mutation testing data -**/mutants.out*/W - - -# Added by cargo -/target -/logs +target +logs diff --git a/Cargo.lock b/Cargo.lock index 41cef35..cd56dfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,35 +3,256 @@ version = 4 [[package]] -name = "Omikron" -version = "0.1.0" +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" dependencies = [ - "aes-gcm", - "ansi_term", - "base64 0.22.1", - "dashmap", - "dotenv", - "futures", - "hex", - "hkdf", - "livekit-api", - "livekit-protocol", + "actix-rt", + "bitflags 2.11.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", "log", "once_cell", - "rand 0.8.5", - "rand_core 0.6.4", - "rustls", - "sha2", - "strum", - "strum_macros", - "thiserror 2.0.18", + "parking_lot", + "pin-project-lite", + "smallvec", "tokio", - "ttp-core", - "ttp-native", - "uuid", - "x448", + "tokio-util", ] +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64", + "bitflags 2.11.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.1.5", + "futures-core", + "h2 0.3.27", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.1.5", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.3", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -76,6 +297,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -85,21 +327,21 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -124,7 +366,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -136,147 +378,27 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "async-channel" -version = "1.9.0" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "async-channel" -version = "2.5.0" +name = "atomic" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.5.0", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener 5.4.1", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-native-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec" -dependencies = [ - "futures-util", - "native-tls", - "thiserror 1.0.69", - "url", -] - -[[package]] -name = "async-std" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-tungstenite" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" -dependencies = [ - "async-native-tls", - "async-std", - "futures-io", - "futures-util", - "log", - "pin-project-lite", - "tungstenite 0.21.0", + "bytemuck", ] [[package]] @@ -314,18 +436,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -333,10 +443,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64ct" -version = "1.8.3" +name = "bit-set" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" @@ -354,25 +479,24 @@ dependencies = [ ] [[package]] -name = "block2" -version = "0.6.2" +name = "brotli" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ - "objc2", + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", ] [[package]] -name = "blocking" -version = "1.6.2" +name = "brotli-decompressor" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ - "async-channel 2.5.0", - "async-task", - "futures-io", - "futures-lite", - "piper", + "alloc-no-stdlib", + "alloc-stdlib", ] [[package]] @@ -381,6 +505,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -393,6 +523,33 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.58" @@ -405,6 +562,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -423,7 +586,11 @@ version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-link", ] [[package]] @@ -446,19 +613,64 @@ dependencies = [ ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "crossbeam-utils", + "bytes", + "memchr", ] [[package]] -name = "const-oid" -version = "0.9.6" +name = "compact_str" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "core-foundation" @@ -485,6 +697,39 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -492,15 +737,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crypto-bigint" -version = "0.5.5" +name = "crossterm" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", ] [[package]] @@ -514,6 +774,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "ctr" version = "0.9.2" @@ -524,30 +794,37 @@ dependencies = [ ] [[package]] -name = "curve25519-dalek" -version = "4.1.3" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto 0.2.9", - "rustc_version", - "subtle", - "zeroize", + "darling_core", + "darling_macro", ] [[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ + "ident_case", "proc-macro2", "quote", - "syn", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", ] [[package]] @@ -571,15 +848,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] -name = "der" -version = "0.7.10" +name = "deflate64" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" [[package]] name = "der-parser" @@ -604,6 +882,40 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -611,21 +923,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags", - "objc2", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -634,14 +935,17 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "dotenv" -version = "0.15.0" +name = "document-features" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] [[package]] name = "dunce" @@ -649,51 +953,13 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - [[package]] name = "ed448-goldilocks" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" dependencies = [ - "fiat-crypto 0.1.20", + "fiat-crypto", "hex", "subtle", ] @@ -705,24 +971,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] -name = "elliptic-curve" -version = "0.13.8" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", + "cfg-if", ] [[package]] @@ -742,30 +996,34 @@ dependencies = [ ] [[package]] -name = "event-listener" -version = "2.5.3" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "num-traits", ] [[package]] -name = "event-listener-strategy" -version = "0.5.4" +name = "fallible-iterator" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ - "event-listener 5.4.1", - "pin-project-lite", + "bit-set", + "regex", ] [[package]] @@ -774,16 +1032,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "fiat-crypto" version = "0.1.20" @@ -791,10 +1039,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" [[package]] -name = "fiat-crypto" -version = "0.2.9" +name = "filedescriptor" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] [[package]] name = "find-msvc-tools" @@ -802,12 +1055,29 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "fnv" version = "1.0.7" @@ -820,6 +1090,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -898,19 +1174,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.32" @@ -919,7 +1182,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -959,7 +1222,6 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", - "zeroize", ] [[package]] @@ -1013,26 +1275,47 @@ dependencies = [ ] [[package]] -name = "gloo-timers" -version = "0.3.0" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ - "futures-channel", + "bytes", + "fnv", "futures-core", - "js-sys", - "wasm-bindgen", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "group" -version = "0.13.0" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] @@ -1047,7 +1330,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1055,12 +1338,44 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] -name = "heck" +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "headers" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 1.4.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.4.0", +] [[package]] name = "heck" @@ -1068,12 +1383,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hex" version = "0.4.3" @@ -1154,6 +1463,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.9.0" @@ -1164,9 +1479,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.13", "http 1.4.0", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1175,18 +1492,18 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.6.0" +name = "hyper-rustls" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "bytes", - "http-body-util", + "http 1.4.0", "hyper", "hyper-util", - "native-tls", + "rustls", + "rustls-pki-types", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", ] @@ -1196,7 +1513,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -1208,9 +1525,35 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.3", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] @@ -1301,6 +1644,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1322,6 +1671,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + [[package]] name = "indexmap" version = "2.13.0" @@ -1334,6 +1689,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + [[package]] name = "inout" version = "0.1.4" @@ -1343,12 +1707,81 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "iota" +version = "0.1.0" +dependencies = [ + "actix-web", + "actix-web-actors", + "aes-gcm", + "async-trait", + "base64", + "chrono", + "crossterm", + "dashmap", + "futures", + "futures-util", + "hex", + "hkdf", + "hyper", + "hyper-util", + "json", + "lazy_static", + "once_cell", + "open", + "pnet", + "rand 0.8.5", + "rand_core 0.6.4", + "ratatui", + "reqwest", + "rusqlite", + "rustls", + "rustls-pemfile", + "serde_json", + "sha2", + "strum 0.27.2", + "strum_macros 0.27.2", + "sysinfo", + "tokio", + "tokio-tungstenite", + "ttp-core", + "ttp-native", + "tungstenite", + "uuid", + "walkdir", + "warp", + "x448", + "zip", +] + [[package]] name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + [[package]] name = "iri-string" version = "0.7.12" @@ -1360,19 +1793,29 @@ dependencies = [ ] [[package]] -name = "itertools" -version = "0.11.0" +name = "is-docker" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" dependencies = [ - "either", + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", ] [[package]] name = "itertools" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -1383,6 +1826,50 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1406,43 +1893,39 @@ dependencies = [ ] [[package]] -name = "jsonwebtoken" -version = "10.3.0" +name = "json" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ - "base64 0.22.1", - "ed25519-dalek", - "getrandom 0.2.17", - "hmac", - "js-sys", - "p256", - "p384", - "rand 0.8.5", - "rsa", - "serde", - "serde_json", - "sha2", - "signature", + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", ] [[package]] -name = "kv-log-macro" -version = "1.0.7" +name = "lab" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -1450,6 +1933,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.184" @@ -1457,10 +1946,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] -name = "libm" -version = "0.2.16" +name = "libsqlite3-sys" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "linux-raw-sys" @@ -1475,58 +1977,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] -name = "livekit-api" -version = "0.4.16" +name = "litrs" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020c7eaab7cec16b69a058659f9acff32067e0e2d4f81e80639f3ffa29464b09" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ - "async-tungstenite", - "base64 0.21.7", - "http 1.4.0", - "jsonwebtoken", - "livekit-protocol", - "log", - "os_info", - "parking_lot", - "pbjson-types", - "prost", - "rand 0.9.2", - "reqwest", - "scopeguard", - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", - "tokio-tungstenite", - "url", + "futures-core", + "futures-sink", + "local-waker", ] [[package]] -name = "livekit-protocol" -version = "0.7.2" +name = "local-waker" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c21d56b4dc2598b1296ca891abb225bf1c05ea007d6f43bc4fd48c31be64bf59" -dependencies = [ - "futures-util", - "livekit-runtime", - "parking_lot", - "pbjson", - "pbjson-types", - "prost", - "serde", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "livekit-runtime" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532e84c6cdc5fe774f2b5d9912597b5f3bea561927a48296d03e24549d21c3f6" -dependencies = [ - "tokio", - "tokio-stream", -] +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" @@ -1542,8 +2013,14 @@ name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "value-bag", + "hashbrown 0.16.1", ] [[package]] @@ -1552,18 +2029,79 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.0" @@ -1571,16 +2109,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "native-tls" version = "0.2.18" @@ -1600,16 +2133,23 @@ dependencies = [ [[package]] name = "nix" -version = "0.30.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", + "memoffset", ] +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + [[package]] name = "nom" version = "7.1.3" @@ -1620,6 +2160,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1630,28 +2179,23 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1661,17 +2205,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1679,37 +2212,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] -name = "objc2" -version = "0.6.4" +name = "num_threads" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", + "libc", ] [[package]] @@ -1718,129 +2229,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", - "dispatch2", - "objc2", + "bitflags 2.11.0", ] [[package]] -name = "objc2-core-graphics" +name = "objc2-io-kit" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ - "bitflags", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags", - "block2", "libc", - "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "octets" version = "0.3.5" @@ -1868,13 +2269,24 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl" version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -1891,7 +2303,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1913,51 +2325,14 @@ dependencies = [ ] [[package]] -name = "os_info" -version = "3.14.0" +name = "ordered-float" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ - "android_system_properties", - "log", - "nix", - "objc2", - "objc2-foundation", - "objc2-ui-kit", - "serde", - "windows-sys 0.61.2", + "num-traits", ] -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1982,40 +2357,19 @@ dependencies = [ ] [[package]] -name = "pbjson" -version = "0.6.0" +name = "pathdiff" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" -dependencies = [ - "base64 0.21.7", - "serde", -] +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] -name = "pbjson-build" -version = "0.6.2" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "heck 0.4.1", - "itertools 0.11.0", - "prost", - "prost-types", -] - -[[package]] -name = "pbjson-types" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" -dependencies = [ - "bytes", - "chrono", - "pbjson", - "pbjson-build", - "prost", - "prost-build", - "serde", + "digest", + "hmac", ] [[package]] @@ -2024,19 +2378,10 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -2044,13 +2389,118 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "petgraph" -version = "0.6.5" +name = "pest" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ - "fixedbitset", - "indexmap", + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2059,44 +2509,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -2104,17 +2516,94 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "polling" -version = "3.11.0" +name = "pnet" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix", - "windows-sys 0.61.2", + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", ] [[package]] @@ -2129,6 +2618,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.5" @@ -2144,6 +2639,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2160,16 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", + "syn 2.0.117", ] [[package]] @@ -2181,59 +2673,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost", -] - [[package]] name = "quinn" version = "0.11.9" @@ -2376,6 +2815,91 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum 0.27.2", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", + "unicode-segmentation", + "unicode-width", +] + [[package]] name = "rcgen" version = "0.14.7" @@ -2395,7 +2919,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.11.0", ] [[package]] @@ -2421,6 +2945,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.10" @@ -2429,31 +2959,33 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.28" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ - "base64 0.22.1", + "base64", "bytes", + "encoding_rs", "futures-core", + "h2 0.4.13", "http 1.4.0", "http-body", "http-body-util", "hyper", - "hyper-tls", + "hyper-rustls", "hyper-util", "js-sys", "log", - "native-tls", + "mime", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", + "rustls-platform-verifier", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -2463,16 +2995,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - [[package]] name = "ring" version = "0.17.14" @@ -2488,23 +3010,28 @@ dependencies = [ ] [[package]] -name = "rsa" -version = "0.9.10" +name = "rsqlite-vfs" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -2537,7 +3064,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2572,6 +3099,15 @@ dependencies = [ "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2582,6 +3118,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.10" @@ -2606,6 +3169,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -2615,34 +3187,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - [[package]] name = "security-framework" version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2691,7 +3255,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2747,6 +3311,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2758,14 +3343,16 @@ dependencies = [ ] [[package]] -name = "signature" -version = "2.2.0" +name = "simd-adler32" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" @@ -2800,19 +3387,15 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.9.8" +name = "sqlite-wasm-rs" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" dependencies = [ - "base64ct", - "der", + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", ] [[package]] @@ -2821,22 +3404,55 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + [[package]] name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "strum_macros" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2845,6 +3461,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -2873,7 +3500,42 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2889,6 +3551,69 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2915,7 +3640,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2926,7 +3651,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2937,7 +3662,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -3010,7 +3737,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3024,28 +3751,40 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.18" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "futures-core", - "pin-project-lite", + "rustls", "tokio", ] [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" dependencies = [ "futures-util", "log", "native-tls", "tokio", "tokio-native-tls", - "tungstenite 0.20.1", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", ] [[package]] @@ -3069,7 +3808,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", @@ -3099,6 +3838,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3112,7 +3852,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3135,11 +3875,11 @@ name = "ttp-core" version = "0.1.0" source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" dependencies = [ - "base64 0.22.1", + "base64", "byteorder", "rand 0.8.5", - "strum", - "strum_macros", + "strum 0.28.0", + "strum_macros 0.28.0", ] [[package]] @@ -3158,42 +3898,19 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 0.2.12", - "httparse", - "log", - "native-tls", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", "bytes", "data-encoding", "http 1.4.0", "httparse", "log", "native-tls", - "rand 0.8.5", + "rand 0.9.2", "sha1", - "thiserror 1.0.69", - "url", - "utf-8", + "thiserror 2.0.18", ] [[package]] @@ -3202,12 +3919,47 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3248,35 +4000,30 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ + "atomic", "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] -[[package]] -name = "value-bag" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3289,6 +4036,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -3298,6 +4064,33 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http 1.4.0", + "http-body", + "http-body-util", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3364,7 +4157,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -3405,7 +4198,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -3431,6 +4224,87 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3447,18 +4321,142 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3486,6 +4484,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3519,6 +4532,21 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3531,6 +4559,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3543,6 +4577,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3567,6 +4607,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3579,6 +4625,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3591,6 +4643,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3603,6 +4661,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3631,7 +4695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "wit-parser", ] @@ -3642,10 +4706,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", - "heck 0.5.0", + "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3661,7 +4725,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -3673,7 +4737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -3819,7 +4883,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3840,7 +4904,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -3860,7 +4924,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -3869,6 +4933,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "zerotrie" @@ -3900,11 +4978,84 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 3127b12..8767b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "Omikron" +name = "iota" version = "0.1.0" edition = "2024" @@ -7,31 +7,50 @@ edition = "2024" ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" } ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" } -ansi_term = "*" -uuid = { version = "*", features = ["v4"] } - +actix-web = { version = "4", features = ["rustls-0_23"] } +actix-web-actors = "4" +aes-gcm = "0.10.3" base64 = "0.22.1" -dashmap = "*" +crossterm = "*" futures = "*" +futures-util = "*" hex = "*" +hkdf = "0.12.4" +hyper-util = { version = "*" } +hyper = { version = "1.8.1", features = [ + "capi", + "client", + "full", + "http1", + "http2", + "nightly", + "server", +] } +json = "*" once_cell = "1.21.3" rand = "0.8" rand_core = { version = "0.6", features = ["getrandom", "std"] } -rustls = { version = "0.23.37", default-features = false, features = [ - "std", - "tls12", - "aws-lc-rs", - "prefer-post-quantum", -] } -aes-gcm = "0.10.3" +reqwest = "0.13.2" +rustls = { version = "0.23.37", features = ["aws-lc-rs"] } sha2 = "0.10.9" -tokio = { version = "*", features = ["full"] } +sysinfo = "0.38.3" +tokio = { version = "1.50.0", features = ["full"] } +tokio-tungstenite = { version = "*", features = ["native-tls"] } +tungstenite = "*" +uuid = { version = "*", features = ["v4"] } +walkdir = "2.5.0" +warp = "*" x448 = { version = "*" } -log = "0.4" -dotenv = "0.15.0" -hkdf = "0.12.4" -strum = "0.28.0" -strum_macros = "0.28.0" -livekit-api = { version = "0.4.14", features = ["native-tls"] } -livekit-protocol = "0.7.1" -thiserror = "2.0.18" +rustls-pemfile = "2.2.0" +async-trait = "0.1.89" +zip = "6.0.0" +pnet = "0.35.0" +dashmap = "6.1.0" +strum = "0.27.2" +strum_macros = "0.27.2" +ratatui = "0.30.0" +open = "5.3.3" +chrono = "0.4.43" +serde_json = "1.0.149" +rusqlite = "0.39.0" +lazy_static = "1.5.0" diff --git a/README.md b/README.md index 06c1cdb..f7bc9c8 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/omikron) ⚠️ +# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️ diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..ee00add --- /dev/null +++ b/dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM rust:latest AS builder + +WORKDIR /app +COPY . . + +RUN cargo build --release + +# Runtime stage +FROM debian:sid + +WORKDIR /app + +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/iota . + +EXPOSE 1984 + +CMD ["./iota"] \ No newline at end of file diff --git a/src/anonymous_clients/anonymous_client_connection.rs b/src/anonymous_clients/anonymous_client_connection.rs deleted file mode 100644 index 8c57490..0000000 --- a/src/anonymous_clients/anonymous_client_connection.rs +++ /dev/null @@ -1,519 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::RwLock; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use ttp_native::{Receiver, Sender}; -use uuid::Uuid; - -use crate::anonymous_clients::anonymous_manager::{self, generate_username}; -use crate::calls::call_manager; -use crate::omega::omega_connection::get_omega_connection; -use crate::rho::connection::GeneralConnection; -use crate::rho::rho_manager; -use crate::util::logger::PrintType; -use crate::{log_cv_in, log_cv_out, log_out}; - -pub struct AnonymousClientConnection { - user_id: u64, - - pub sender: Arc, - pub receiver: Arc, - pub ping: Arc>, - pub interested_users: Arc>>, - is_open: Arc>, - pub user_name: Arc>, - pub display_name: Arc>, - pub avatar: Arc>, -} - -impl AnonymousClientConnection { - pub async fn from_general(general: Arc, user_id: u64) -> Arc { - let username: String = generate_username(); - Arc::new(Self { - user_id: user_id, - - ping: Arc::new(RwLock::new(0)), - interested_users: Arc::new(RwLock::new(Vec::new())), - is_open: Arc::new(RwLock::new(true)), - sender: general.sender.clone(), - receiver: general.receiver.clone(), - user_name: Arc::new(RwLock::new(username.to_lowercase())), - display_name: Arc::new(RwLock::new(username)), - avatar: Arc::new(RwLock::new(String::new())), - }) - } - pub fn start(self: Arc) { - let self_clone = self.clone(); - tokio::spawn(async move { - while let Ok(cv) = self_clone.receiver.receive().await { - self_clone.clone().handle_message(cv).await; - } - self_clone.handle_close().await; - }); - } - - /// Get the user ID - pub fn get_user_id(&self) -> u64 { - self.user_id - } - - /// Get the user name - pub async fn get_user_name(&self) -> String { - self.user_name.read().await.clone() - } - - /// Get the display name - pub async fn get_display_name(&self) -> String { - self.display_name.read().await.clone() - } - - pub async fn set_display_name(&self, display: String) { - *self.display_name.write().await = display; - } - - /// Get the avatar - pub async fn get_avatar(&self) -> String { - self.avatar.read().await.clone() - } - - /// Send a CommunicationValue to the client - pub async fn send_message(self: Arc, cv: &CommunicationValue) { - if !*self.is_open.read().await { - log_out!( - self.user_id as i64, - PrintType::Client, - "Attempted to send message to a closed connection." - ); - return; - } - if !cv.is_type(CommunicationType::pong) { - log_cv_out!(PrintType::Client, &cv); - } - if let Err(e) = self.sender.send(&cv).await { - log_out!( - self.user_id as i64, - PrintType::Client, - "Send failed: {:?}", - e - ); - } - } - - /// Handle incoming message from client - pub async fn handle_message(self: Arc, cv: CommunicationValue) { - tokio::spawn(async move { - if cv.is_type(CommunicationType::ping) { - self.handle_ping(cv).await; - return; - } - log_cv_in!(PrintType::Client, &cv); - - if cv.is_type(CommunicationType::identification) { - let call_id = - Uuid::parse_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")) - .unwrap_or(Uuid::new_v4()); - - let call = if let Some(call) = call_manager::get_call(call_id).await { - if call.is_anonymous().await { - call - } else { - self.send_error_response( - &cv.get_id(), - CommunicationType::error_not_authenticated, - ) - .await; - return; - } - } else { - self.send_error_response( - &cv.get_id(), - CommunicationType::error_not_authenticated, - ) - .await; - return; - }; - - let mut invited = Vec::new(); - for call_invitee in call.members.read().await.clone() { - let call_invitee_cv = get_omega_connection() - .await_response( - &CommunicationValue::new(CommunicationType::get_user_data).add_data( - DataTypes::user_id, - DataValue::Number(call_invitee.user_id as i64), - ), - Some(Duration::from_secs(2)), - ) - .await - .unwrap(); - let mut json_invitee = Vec::new(); - let _ = json_invitee.push(( - DataTypes::user_id, - call_invitee_cv.get_data(DataTypes::user_id).clone(), - )); - let _ = json_invitee.push(( - DataTypes::username, - call_invitee_cv.get_data(DataTypes::username).clone(), - )); - let _ = json_invitee.push(( - DataTypes::display, - call_invitee_cv.get_data(DataTypes::display).clone(), - )); - let _ = json_invitee.push(( - DataTypes::avatar, - call_invitee_cv.get_data(DataTypes::avatar).clone(), - )); - - let _ = invited.push(DataValue::Container(json_invitee)); - } - - let token = call.create_anonymous_token(self.get_user_id()).await; - - let mut serialized = Vec::new(); - let _ = serialized.push((DataTypes::call_id, DataValue::Str(call_id.to_string()))); - let _ = - serialized.push((DataTypes::call_invited, DataValue::Array(invited.clone()))); - let _ = serialized.push((DataTypes::call_members, DataValue::Array(invited))); - let _ = serialized.push((DataTypes::call_token, DataValue::Str(token.unwrap()))); - self.clone() - .send_message( - &&CommunicationValue::new(CommunicationType::identification_response) - .with_id(cv.get_id()) - .add_data(DataTypes::user_id, DataValue::Number(self.user_id as i64)) - .add_data( - DataTypes::username, - DataValue::Str(self.clone().get_user_name().await), - ) - .add_data( - DataTypes::display, - DataValue::Str(self.get_display_name().await), - ) - .add_data(DataTypes::avatar, DataValue::Str(self.get_avatar().await)) - .add_data(DataTypes::call_state, DataValue::Container(serialized)), - ) - .await; - } - - // Handle ping - if cv.is_type(CommunicationType::ping) { - self.handle_ping(cv).await; - return; - } - // Handle client status changes - if cv.is_type(CommunicationType::client_changed) { - self.handle_client_changed(cv).await; - return; - } - - // Handle call invites - if cv.is_type(CommunicationType::call_invite) { - self.handle_call_invite(cv).await; - return; - } - - // Handle get call requests - if cv.is_type(CommunicationType::call_token) { - self.handle_get_call(cv).await; - return; - } - - if cv.is_type(CommunicationType::call_disconnect_user) { - self.handle_call_disconnect_user(cv).await; - return; - } - - if cv.is_type(CommunicationType::call_timeout_user) { - self.handle_call_timeout_user(cv).await; - return; - } - - if cv.is_type(CommunicationType::change_user_data) { - if let Some(display_name) = cv.get_data(DataTypes::display).as_str() { - let _ = self.set_display_name(display_name.to_string()).await; - } - - return; - } - - if cv.is_type(CommunicationType::get_user_data) { - if let Some(anonymous) = { - if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() { - anonymous_manager::get_anonymous_user(user_id as u64).await - } else if let Some(username) = cv.get_data(DataTypes::username).as_str() { - anonymous_manager::get_anonymous_user_by_name(username.to_string()).await - } else { - None - } - } { - let response = CommunicationValue::new(CommunicationType::get_user_data) - .with_id(cv.get_id()) - .add_data( - DataTypes::username, - DataValue::Str(anonymous.get_user_name().await), - ) - .add_data( - DataTypes::user_id, - DataValue::Number(anonymous.user_id as i64), - ) - .add_data( - DataTypes::display, - DataValue::Str(anonymous.get_display_name().await), - ) - .add_data(DataTypes::user_state, DataValue::Str("online".to_string())) - .add_data( - DataTypes::avatar, - DataValue::Str(anonymous.get_avatar().await), - ); - - self.send_message(&response).await; - - return; - } - } - - if cv.is_type(CommunicationType::get_user_data) - || cv.is_type(CommunicationType::get_iota_data) - || cv.is_type(CommunicationType::delete_user) - { - self.handle_omega_forward(cv).await; - return; - } - }); - } - async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { - let client_for_closure = self.clone(); - tokio::spawn(async move { - let response_cv = get_omega_connection() - .await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20))) - .await; - if let Ok(response_cv) = response_cv { - client_for_closure.send_message(&response_cv).await; - } - }); - } - - /// Handle ping message - async fn handle_ping(self: Arc, cv: CommunicationValue) { - // Update our ping if provided - if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { - if let Ok(ping_val) = last_ping.to_string().parse::() { - let mut ping_guard = self.ping.write().await; - *ping_guard = ping_val; - } - } - - // Send pong response - let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); - - self.send_message(&response).await; - } - - /// Handle client status change - async fn handle_client_changed(self: Arc, _cv: CommunicationValue) { - /*let user_id = self.get_user_id().await; - if let Some(_status_str) = cv.get_data(DataTypes::user_state) { - let user_status = UserStatus::online; - }*/ - } - - /// Handle call invite - async fn handle_call_invite(self: Arc, cv: CommunicationValue) { - let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0); - if receiver_id == 0 { - self.send_error_response(&cv.get_id(), CommunicationType::error_no_user_id) - .await; - return; - } - - let call_id = match cv.get_data(DataTypes::call_id) { - DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) { - Ok(id) => id, - Err(_) => { - self.send_error_response( - &cv.get_id(), - CommunicationType::error_invalid_call_id, - ) - .await; - return; - } - }, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error_no_call_id) - .await; - return; - } - }; - - let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await; - if !invited { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_call_id) - .await; - return; - } - - // Find target RhoConnection - let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await { - Some(rho) => rho, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - // Get sender user ID - let sender_id = self.get_user_id(); - - // Create and send call distribution message - let forward = CommunicationValue::new(CommunicationType::call_invite) - .with_receiver(receiver_id as u64) - .with_sender(sender_id) - .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) - .add_data( - DataTypes::receiver_id, - DataValue::Str(receiver_id.to_string()), - ) - .add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string())); - - target_rho.message_to_client(forward).await; - - let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id()); - self.send_message(&response).await; - } - - /// Handle get call request - async fn handle_get_call(self: Arc, cv: CommunicationValue) { - let user_id = self.get_user_id(); - - let call_id = match cv.get_data(DataTypes::call_id) { - DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) { - Ok(id) => id, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - if let Some(token) = call_manager::get_call_token(user_id, call_id).await { - let response = CommunicationValue::new(CommunicationType::call_token) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_data(DataTypes::call_token, DataValue::Str(token.to_string())); - self.send_message(&response).await; - } else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - } - async fn handle_call_timeout_user(self: Arc, cv: CommunicationValue) { - let call_id = - Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); - let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0); - - let call = call_manager::get_call(call_id).await; - if let Some(call) = call { - if call - .get_caller(self.get_user_id()) - .await - .unwrap() - .has_admin() - { - call.get_caller(user_id as u64) - .await - .unwrap() - .set_timeout(untill) - .await; - } - } - } - async fn handle_call_disconnect_user(self: Arc, cv: CommunicationValue) { - let call_id = - Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); - let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - - let call = call_manager::get_call(call_id).await; - if let Some(call) = call { - if call - .get_caller(self.get_user_id()) - .await - .unwrap() - .has_admin() - { - call.remove_caller(user_id as u64).await; - } - } - } - - /// Send error response - async fn send_error_response(self: Arc, message_id: &u32, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(*message_id); - self.send_message(&error).await; - } - /// Close the connection - pub async fn close(&self) { - let mut is_open_guard = self.is_open.write().await; - if !*is_open_guard { - return; - } - *is_open_guard = false; - - let _ = self.sender.close(); - } - - #[allow(dead_code)] - /// Set interested users list - pub async fn set_interested_users(self: Arc, interested_ids: Vec) { - let mut interested_guard = self.interested_users.write().await; - *interested_guard = interested_ids; - } - #[allow(dead_code)] - pub async fn get_interested_users(self: Arc) -> Vec { - let interested_guard = self.interested_users.read().await; - interested_guard.clone() - } - - #[allow(dead_code)] - /// Check if interested in a user and send notification - pub async fn are_you_interested(self: Arc, user_id: i64) { - let interested_guard = self.clone().get_interested_users().await; - if interested_guard.contains(&user_id) { - let notification = CommunicationValue::new(CommunicationType::client_changed) - .add_data(DataTypes::user_id, DataValue::Str(user_id.to_string())) - .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); - - self.send_message(¬ification).await; - } - } - - /// Handle connection close - pub async fn handle_close(&self) { - - // TODO delete temp user - } -} - -// Implement Clone to make it easier to work with Arc -impl Clone for AnonymousClientConnection { - fn clone(&self) -> Self { - Self { - sender: Arc::clone(&self.sender), - receiver: Arc::clone(&self.receiver), - user_id: self.user_id, - ping: Arc::clone(&self.ping), - interested_users: Arc::clone(&self.interested_users), - is_open: Arc::clone(&self.is_open), - user_name: Arc::clone(&self.user_name), - display_name: Arc::clone(&self.display_name), - avatar: Arc::clone(&self.avatar), - } - } -} diff --git a/src/anonymous_clients/anonymous_manager.rs b/src/anonymous_clients/anonymous_manager.rs deleted file mode 100644 index edfefde..0000000 --- a/src/anonymous_clients/anonymous_manager.rs +++ /dev/null @@ -1,53 +0,0 @@ -use dashmap::DashMap; -use once_cell::sync::Lazy; -use rand::Rng; -use rand::seq::SliceRandom; -use std::sync::Arc; - -use crate::anonymous_clients::anonymous_client_connection::AnonymousClientConnection; - -static ANONYMOUS_USERS: Lazy>> = - Lazy::new(|| DashMap::new()); - -#[allow(dead_code)] -pub async fn add_anonymous_user(connection: Arc) { - ANONYMOUS_USERS.insert(connection.get_user_id(), connection); -} - -#[allow(dead_code)] -pub async fn remove_anonymous_user(user_id: u64) { - ANONYMOUS_USERS.remove(&user_id); -} - -pub async fn get_anonymous_user(user_id: u64) -> Option> { - ANONYMOUS_USERS.get(&user_id).map(|c| c.clone()) -} - -pub async fn get_anonymous_user_by_name( - username: String, -) -> Option> { - for user_conn in ANONYMOUS_USERS - .iter() - .map(|ref_multi| ref_multi.value().clone()) - { - if user_conn.get_user_name().await == username { - return Some(user_conn); - } - } - - return None; -} - -// TODO: implement check if taken -pub fn generate_username() -> String { - let adjectives = ["Swift", "Clever", "Brave", "Sneaky", "Fierce"]; - let nouns = ["Tiger", "Eagle", "Shark", "Wolf", "Dragon"]; - - let mut rng = rand::thread_rng(); - let adj = adjectives.choose(&mut rng).unwrap(); - let noun = nouns.choose(&mut rng).unwrap(); - - let number: u16 = rng.gen_range(0..10000); - - format!("{}{}{}", adj, noun, number) -} diff --git a/src/anonymous_clients/mod.rs b/src/anonymous_clients/mod.rs deleted file mode 100644 index 3edd0b4..0000000 --- a/src/anonymous_clients/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod anonymous_client_connection; -pub mod anonymous_manager; diff --git a/src/auth/auth_user.rs b/src/auth/auth_user.rs new file mode 100644 index 0000000..8f763b6 --- /dev/null +++ b/src/auth/auth_user.rs @@ -0,0 +1,18 @@ +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct AuthUser { + pub id: i64, + pub username: String, + pub password_hash: String, + pub public_key: String, +} + +impl AuthUser { + pub fn new(id: i64, username: String, password_hash: String, public_key: String) -> Self { + AuthUser { + id, + username, + password_hash, + public_key, + } + } +} diff --git a/src/auth/local_auth.rs b/src/auth/local_auth.rs new file mode 100644 index 0000000..112ee2b --- /dev/null +++ b/src/auth/local_auth.rs @@ -0,0 +1,18 @@ +use json::JsonValue; + +use crate::util::file_util::load_file; +// NOT USED AT MOMENT +pub fn is_private_key_valid(user_id: &i64, key_hash: &str) -> bool { + let file_contents = load_file("", "users.json"); + + let users = json::parse(&file_contents).unwrap(); + if let JsonValue::Array(users_array) = users { + for user in users_array { + if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash { + return true; + } + } + } + + false +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..73d6442 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod auth_user; +pub mod local_auth; diff --git a/src/calls/call_group.rs b/src/calls/call_group.rs deleted file mode 100644 index 91e85cf..0000000 --- a/src/calls/call_group.rs +++ /dev/null @@ -1,106 +0,0 @@ -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; - -use std::{env, sync::Arc, time::Duration}; -use tokio::sync::RwLock; -use uuid::Uuid; - -use crate::{ - calls::{call_util, caller::Caller}, - omega::omega_connection::get_omega_connection, -}; - -pub struct CallGroup { - pub call_id: Uuid, - pub members: RwLock>>, - pub show: RwLock, - pub anonymous_joining: RwLock, - pub short_link: RwLock>, -} - -impl CallGroup { - pub fn new(call_id: Uuid, user: Arc) -> Self { - CallGroup { - call_id, - members: RwLock::new(vec![user]), - show: RwLock::new(true), - anonymous_joining: RwLock::new(false), - short_link: RwLock::new(None), - } - } - - pub async fn get_caller(&self, user_id: u64) -> Option> { - self.members - .read() - .await - .iter() - .find(|caller| caller.user_id == user_id) - .cloned() - } - - pub async fn is_anonymous(&self) -> bool { - *self.anonymous_joining.read().await - } - - pub async fn set_anonymous_joining(&self, enable: bool) { - *self.anonymous_joining.write().await = enable; - - let _ = call_util::set_room_metadata( - self.call_id, - format!("{{\"anonymous_joining\": \"{}\"}}", enable), - ) - .await; - - if self.short_link.read().await.is_none() { - let long_link = format!( - "https://app.tensamin.net/call/anonymous?call_id={}&omikron_id={}", - self.call_id, - env::var("ID") - .unwrap_or("0".to_string()) - .parse::() - .unwrap_or(0), - ); - let response_cv = get_omega_connection() - .await_response( - &CommunicationValue::new(CommunicationType::shorten_link) - .add_data(DataTypes::link, DataValue::Str(long_link)), - Some(Duration::from_secs(20)), - ) - .await; - if let Ok(response) = response_cv { - *self.short_link.write().await = Some( - response - .get_data(DataTypes::link) - .as_str() - .unwrap() - .to_string(), - ); - log::info!( - "Shortened link for call {} is {}", - self.call_id, - self.short_link.read().await.as_ref().unwrap() - ); - } - } - } - - pub async fn create_anonymous_token(&self, user_id: u64) -> Option { - if self.is_anonymous().await { - if let Ok(token) = call_util::create_token(user_id, self.call_id, false) { - return Some(token); - } - } - None - } - - pub async fn remove_caller(&self, user_id: u64) { - let _ = call_util::remove_participant(self.call_id, user_id).await; - self.members - .write() - .await - .retain(|caller| caller.user_id != user_id); - } - - pub async fn get_short_link(self: Arc) -> Option { - self.short_link.read().await.clone() - } -} diff --git a/src/calls/call_manager.rs b/src/calls/call_manager.rs deleted file mode 100644 index a17f347..0000000 --- a/src/calls/call_manager.rs +++ /dev/null @@ -1,97 +0,0 @@ -use dashmap::DashMap; -use once_cell::sync::Lazy; -use std::sync::Arc; -use uuid::Uuid; - -use crate::calls::{call_group::CallGroup, caller::Caller}; - -pub static CALL_GROUPS: Lazy>> = Lazy::new(|| DashMap::new()); -#[allow(dead_code)] -pub async fn get_call_invites(user_id: u64) -> Vec> { - let mut callers = Vec::new(); - for (_, cg) in CALL_GROUPS.clone().into_iter() { - let members = cg.members.read().await; - for member in members.iter() { - if member.user_id == user_id { - callers.push(member.clone()); - } - } - } - callers -} - -pub async fn get_call(call_id: Uuid) -> Option> { - if let Some(b) = CALL_GROUPS.get(&call_id) { - Some(b.clone()) - } else { - None - } -} - -pub async fn get_call_groups(user_id: u64) -> Vec> { - let mut call_groups = Vec::new(); - for (_, cg) in CALL_GROUPS.clone().into_iter() { - let is_member = { - let members = cg.members.read().await; - members.iter().any(|m| m.user_id == user_id) - }; - - if is_member { - call_groups.push(cg.clone()); - } - } - call_groups -} - -pub async fn get_call_token(user_id: u64, call_id: Uuid) -> Option { - if let Some(cg) = CALL_GROUPS.get(&call_id) { - let mut members = cg.members.write().await; - - if let Some(member) = members.iter().find(|m| m.user_id == user_id) { - return Some(member.create_token()); - } - - let new_caller = Arc::new(Caller::new(user_id, call_id, false)); - let token = new_caller.create_token(); - - members.push(new_caller); - - return Some(token); - } - - if let Some(cg) = CALL_GROUPS.get(&call_id) { - let cg_clone = cg.clone(); - - let mut members = cg_clone.members.write().await; - if let Some(member) = members.iter().find(|m| m.user_id == user_id) { - return Some(member.create_token()); - } - let new_caller = Arc::new(Caller::new(user_id, call_id, false)); - let token = new_caller.create_token(); - members.push(new_caller); - return Some(token); - } - - let caller = Arc::new(Caller::new(user_id, call_id, true)); - let call_group = CallGroup::new(call_id, caller.clone()); - - CALL_GROUPS.insert(call_id, Arc::new(call_group)); - - Some(caller.create_token()) -} - -pub async fn add_invite(call_id: Uuid, inviter_id: u64, invitee_id: u64) -> bool { - if let Some(cg) = CALL_GROUPS.get(&call_id) { - let mut members = cg.members.write().await; - - let is_inviter_member = members.iter().any(|m| m.user_id == inviter_id); - - if is_inviter_member { - if !members.iter().any(|m| m.user_id == invitee_id) { - members.push(Arc::new(Caller::new(invitee_id, call_id, false))); - } - return true; - } - } - false -} diff --git a/src/calls/call_util.rs b/src/calls/call_util.rs deleted file mode 100644 index 57df005..0000000 --- a/src/calls/call_util.rs +++ /dev/null @@ -1,151 +0,0 @@ -use livekit_api::{ - access_token::{self}, - services::room::RoomClient, -}; -use livekit_protocol::Room; -use std::env; -use std::str::FromStr; -use std::time::Duration; -use uuid::Uuid; - -use crate::{calls::call_manager::CALL_GROUPS, log, log_err, util::logger::PrintType}; - -pub fn get_livekit() -> Result<(String, String, String), ()> { - let hostname = match env::var("LIVEKI_HOSTNAME") { - Ok(secret) => secret, - Err(_) => { - log_err!(0, PrintType::General, "LIVEKI_HOSTNAME not set!"); - return Err(()); - } - }; - let api_key = match env::var("LIVEKIT_API_KEY") { - Ok(key) => key, - Err(_) => { - log_err!(0, PrintType::General, "LIVEKIT_API_KEY not set!"); - return Err(()); - } - }; - let api_secret = match env::var("LIVEKIT_API_SECRET") { - Ok(secret) => secret, - Err(_) => { - log_err!(0, PrintType::General, "LIVEKIT_API_SECRET not set!"); - return Err(()); - } - }; - Ok((hostname, api_key, api_secret)) -} - -pub fn create_token(user_id: u64, call_id: Uuid, has_admin: bool) -> Result { - let (_, api_key, api_secret) = get_livekit()?; - - let token = access_token::AccessToken::with_api_key(&api_key, &api_secret) - .with_identity(&user_id.to_string()) - .with_grants(access_token::VideoGrants { - room_join: true, - room_admin: has_admin, - room: call_id.to_string(), - ..Default::default() - }) - .with_metadata(&format!("{{\"isAdmin\":{}}}", has_admin)) - .to_jwt(); - if let Ok(token) = token { - Ok(token) - } else { - Err(()) - } -} -#[allow(dead_code)] -pub async fn get_room(call_id: Uuid) -> Result<(RoomClient, Room), ()> { - if let Ok((hostname, api_key, api_secret)) = get_livekit() { - let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); - let rooms = room_service.list_rooms(Vec::new()).await; - if let Ok(rooms) = rooms { - for room in rooms { - if room.name == call_id.to_string() { - return Ok((room_service, room)); - } - } - } - } - return Err(()); -} - -pub async fn remove_participant(call_id: Uuid, user_id: u64) -> Result<(), ()> { - if let Ok((hostname, api_key, api_secret)) = get_livekit() { - let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); - if let Ok(_) = room_service - .remove_participant(&call_id.to_string(), &user_id.to_string()) - .await - { - return Ok(()); - } - } - return Err(()); -} - -#[allow(dead_code)] -pub async fn get_room_metadata(call_id: Uuid) -> Result { - if let Ok((_, room)) = get_room(call_id).await { - Ok(room.metadata) - } else { - Err(()) - } -} - -pub async fn set_room_metadata(call_id: Uuid, metadata: String) -> Result<(), ()> { - if let Ok((hostname, api_key, api_secret)) = get_livekit() { - let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); - if let Ok(_) = room_service - .update_room_metadata(&call_id.to_string(), &metadata) - .await - { - return Ok(()); - } - } - return Err(()); -} - -pub fn garbage_collect_calls() { - tokio::spawn(async move { - loop { - if let Ok((hostname, api_key, api_secret)) = get_livekit() { - let room_service = RoomClient::with_api_key(&hostname, &api_key, &api_secret); - clean_calls(room_service).await; - } - tokio::time::sleep(Duration::from_secs(2)).await; - } - }); -} -pub async fn clean_calls(room_service: RoomClient) { - let rooms = room_service.list_rooms(Vec::new()).await.unwrap(); - let mut call_ids: Vec = Vec::new(); - let mut no_users: Vec = Vec::new(); - for room in rooms { - if let Ok(id) = Uuid::from_str(&room.name) { - if room.num_participants == 0 { - no_users.push(id); - } - call_ids.push(id); - } - } - let size_pre = CALL_GROUPS.len(); - for (id, _) in CALL_GROUPS.clone().into_iter() { - if !call_ids.contains(&id) { - CALL_GROUPS.remove(&id); - } - } - for (_, cg) in CALL_GROUPS.clone().into_iter() { - *cg.show.write().await = !no_users.contains(&cg.call_id); - } - - let size_post = CALL_GROUPS.len(); - if size_pre - size_post != 0 { - log!( - 0, - PrintType::Call, - "Cleaned {} calls, {} remaining", - size_pre - size_post, - size_post - ); - } -} diff --git a/src/calls/caller.rs b/src/calls/caller.rs deleted file mode 100644 index 74e0c3b..0000000 --- a/src/calls/caller.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use tokio::sync::RwLock; -use uuid::Uuid; - -use crate::calls::call_util; - -pub struct Caller { - pub user_id: u64, - pub call_id: Uuid, - pub has_admin: bool, - pub timeout: RwLock, -} - -impl Caller { - pub fn new(user_id: u64, call_id: Uuid, has_admin: bool) -> Self { - Caller { - user_id, - call_id, - has_admin, - timeout: RwLock::new(0), - } - } - #[allow(dead_code)] - pub fn set_admin(&mut self, has_admin: bool) { - self.has_admin = has_admin; - } - pub fn has_admin(&self) -> bool { - self.has_admin - } - #[allow(dead_code)] - pub async fn is_timeouted(&self) -> bool { - *self.timeout.read().await - > SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64 - } - pub async fn set_timeout(&self, timeout: i64) { - *self.timeout.write().await = timeout; - } - pub fn create_token(&self) -> String { - if let Ok(token) = call_util::create_token(self.user_id, self.call_id, self.has_admin()) { - token - } else { - String::new() - } - } -} diff --git a/src/calls/mod.rs b/src/calls/mod.rs deleted file mode 100644 index 8413354..0000000 --- a/src/calls/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod call_group; -pub mod call_manager; -pub mod call_util; -pub mod caller; diff --git a/src/communities/community.rs b/src/communities/community.rs new file mode 100644 index 0000000..835c88a --- /dev/null +++ b/src/communities/community.rs @@ -0,0 +1,356 @@ +use crate::communities::interactables::category::Category; +use crate::communities::interactables::registry; +use crate::communities::perms::permission::Permission; +use crate::communities::{ + community_connection::CommunityConnection, interactables::interactable::Interactable, +}; +use crate::util::file_util; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use json::JsonValue; +use json::number::Number; +use json::object::Object; +use rand::RngCore; +use rand_core::OsRng; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use x448::{PublicKey, Secret}; +/// Permissions +// uuid -> interactable/path/like/this/interactable_name:permission +// -> role + +/// Roles +// rolename -> interactable/path/like/this/interactable_name:permission +// -> other/path/like/this/interactable_name:permission + +pub struct Community { + name: String, + owner_id: Arc>, + members: Vec, + permissions: HashMap>, + roles: HashMap>, + private_key: Secret, + public_key: PublicKey, + pub interactables: Arc>>>>, + pub connections: Arc>>>>, +} + +impl Community { + pub fn new() -> Self { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + Community { + name: String::new(), + owner_id: Arc::new(RwLock::new(0)), + members: Vec::new(), + permissions: HashMap::new(), + roles: HashMap::new(), + private_key, + public_key, + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + } + } + pub async fn create(name: String, owner_id: i64) -> Self { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + let c = Community { + name, + owner_id: Arc::new(RwLock::new(owner_id)), + members: Vec::new(), + permissions: HashMap::new(), + roles: HashMap::new(), + private_key, + public_key, + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + }; + c.save().await; + c + } + + pub async fn to_json(&self) -> JsonValue { + let mut json = JsonValue::new_object(); + json["name"] = self.name.clone().into(); + json["owner_id"] = (*self.owner_id.read().await as i64).into(); + json["members"] = self + .members + .clone() + .iter() + .map(|f| f.to_string()) + .collect::>() + .into(); + json["private_key"] = self.private_key.as_bytes().to_vec().into(); + json["public_key"] = self.public_key.as_bytes().to_vec().into(); + json["connections"] = self.connections.read().await.clone().len().into(); + json + } + + pub async fn frontend(&self) -> JsonValue { + let mut json = JsonValue::new_object(); + json["name"] = self.name.clone().into(); + json["owner_id"] = (*self.owner_id.read().await as i64).into(); + json["members"] = self + .members + .clone() + .iter() + .map(|f| f.to_string()) + .collect::>() + .into(); + json["public_key"] = self.public_key.as_bytes().to_vec().into(); + json["connections"] = self.connections.read().await.clone().len().into(); + json + } + + pub fn add_member(&mut self, member_id: i64) { + self.members.push(member_id); + } + + pub fn remove_member(&mut self, member_id: i64) { + self.members.retain(|id| *id != member_id); + } + pub fn get_name(&self) -> &str { + &self.name + } + pub async fn get_owner_id(&self) -> i64 { + *self.owner_id.read().await + } + pub fn get_members(&self) -> Vec { + self.members.clone() + } + pub fn get_private_key(&self) -> Secret { + Secret::from_bytes(self.private_key.as_bytes()).unwrap() + } + pub fn get_public_key(&self) -> &PublicKey { + &self.public_key + } + pub async fn set_owner(self: Arc, owner_id: i64) { + *self.owner_id.write().await = owner_id; + } + pub async fn add_connection(self: &Arc, other: Arc) { + let mut vec = self + .connections + .read() + .await + .get(&other.get_user_id().await) + .cloned() + .unwrap_or_default(); + vec.push(other.clone()); + self.connections + .write() + .await + .insert(other.get_user_id().await, vec); + } + pub async fn remove_connection(self: &Arc, other: Arc) { + let mut vec = self + .connections + .read() + .await + .get(&other.get_user_id().await) + .cloned() + .unwrap_or_default(); + vec.retain(|conn| !Arc::ptr_eq(conn, &other)); + self.connections + .write() + .await + .insert(other.get_user_id().await, vec); + } + pub async fn get_connections(&self) -> HashMap>> { + self.connections.read().await.clone() + } + pub async fn get_connections_for_user(&self, user_id: i64) -> Vec> { + self.connections + .read() + .await + .get(&user_id) + .cloned() + .unwrap_or_default() + } + pub async fn get_interactables( + &self, + _user_id: i64, + ) -> Vec>> { + self.interactables.read().await.clone() + } + pub async fn add_interactable(self: &mut Arc, interactable: Arc>) { + self.interactables.write().await.push(interactable); + } + pub async fn remove_interactable( + self: &mut Arc, + interactable: Arc>, + ) { + self.interactables + .write() + .await + .retain(|i| !Arc::ptr_eq(i, &interactable)); + } + pub async fn broadcast_message(&self, message: &CommunicationValue) { + for (_, conns) in self.connections.read().await.clone().iter() { + for user in conns.iter() { + user.send_message(message).await; + } + } + } + pub async fn run_function( + self: &mut Arc, + _user_id: i64, + name: &str, + path: &str, + _function: &str, + cv: &CommunicationValue, + ) -> CommunicationValue { + if path.is_empty() { + let target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + return CommunicationValue::new(CommunicationType::error); + } else { + // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) + return interactable.run_function(cv.clone()).await; + } + } + } + } else { + let target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + let category: &Category = + interactable.as_any().downcast_ref::().unwrap(); + // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) + return category + .get_child(path.to_string(), name.to_string()) + .unwrap() + .run_function(cv.clone()) + .await; + } else { + return CommunicationValue::new(CommunicationType::error); + } + } + } + } + CommunicationValue::new(CommunicationType::add_conversation) + } + + pub async fn save(&self) { + let mut json = Object::new(); + json.insert("name", JsonValue::String(self.name.clone())); + json.insert( + "owner_id", + JsonValue::Number(Number::from(*self.owner_id.read().await as i64)), + ); + + json.insert( + "private_key", + JsonValue::String(STANDARD.encode(&self.private_key.as_bytes())), + ); + json.insert( + "public_key", + JsonValue::String(STANDARD.encode(&self.public_key.as_bytes())), + ); + + file_util::save_file( + &format!("communities/{}/", self.name), + "config.json", + &json.dump(), + ); + + let mut user_data = Object::new(); + for user in self.members.iter() { + let mut data = JsonValue::new_object(); + + let mut permissions = JsonValue::new_array(); + for perm in self.permissions.get(user).unwrap() { + if let Ok(_) = permissions.push(perm.to_string()) {} + } + + if let Ok(_) = data.insert("permissions", permissions) {} + user_data.insert(&user.to_string(), data); + } + file_util::save_file( + &format!("communities/{}/", self.name), + "users.json", + &user_data.dump(), + ); + + for interactable in self.interactables.read().await.clone().iter() { + registry::save(interactable).await; + } + } +} +pub async fn load(name: &String) -> Option> { + let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json"); + let json_content = json::parse(&file_contents).unwrap(); + + let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json"); + let user_json: JsonValue = json::parse(&user_data).unwrap(); + let mut users = Vec::new(); + let mut permissions: HashMap> = HashMap::new(); + + for user in user_json.entries() { + let (id, json): (&str, &JsonValue) = user; + let perms_j = &json["permissions"]; + let perms = Vec::new(); + for _ in perms_j.entries() { + // let perm_j = i.as_str().unwrap(); + // perms.push(perm_j.to_string()); + } + + let user_id: i64 = id.parse().unwrap_or(0); + + users.push(user_id); + permissions.insert(user_id, perms); + } + + let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json"); + let roles: HashMap> = HashMap::new(); + if let Ok(_) = json::parse(&role_data) { + // Fill roles + } else { + return None; + }; + + let community = Community { + name: json_content["name"].as_str().unwrap().to_string(), + owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))), + members: users, + roles, + permissions, + private_key: Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + public_key: PublicKey::from( + &Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + ), + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + }; + let mut comarc = Arc::new(community); + + let interactable_files: Vec = + file_util::get_children(&format!("communities/{}/interactables/", name)); + for file in interactable_files { + if file.contains(".json") { + let name = file.split('.').next().unwrap().to_string(); + let interactable: Box = + registry::load(comarc.clone(), String::new(), name).await; + comarc.add_interactable(Arc::new(interactable)).await; + } + } + Some(comarc) +} diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs new file mode 100644 index 0000000..62df251 --- /dev/null +++ b/src/communities/community_connection.rs @@ -0,0 +1,396 @@ +use crate::auth::auth_user::AuthUser; +use crate::communities::community::Community; +use crate::communities::interactables::interactable::Interactable; +use crate::users::user_manager::get_user; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use futures::SinkExt; +use futures::stream::SplitSink; +use futures::stream::SplitStream; +use hkdf::Hkdf; +use hyper::upgrade::Upgraded; +use hyper_util::rt::TokioIo; +use json::JsonValue; +use json::number::Number; +use rand::{Rng, distributions::Alphanumeric}; +use sha2::Sha256; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio_tungstenite::WebSocketStream; +use tungstenite::Message; +use tungstenite::Utf8Bytes; +use uuid::Uuid; +use x448::PublicKey; +pub struct CommunityConnection { + pub sender: Arc>, Message>>>, + pub receiver: Arc>>>>, + pub user_id: Arc>, + pub community: Arc>>>, + identified: Arc>, + challenged: Arc>, + challenge: Arc>, + auth: Arc>>, + pub ping: Arc>, +} +impl CommunityConnection { + pub fn new( + sender: SplitSink>, Message>, + receiver: SplitStream>>, + community: Arc, + ) -> Arc { + Arc::new(Self { + sender: Arc::new(RwLock::new(sender)), + receiver: Arc::new(RwLock::new(receiver)), + user_id: Arc::new(RwLock::new(0)), + community: Arc::new(RwLock::new(Some(community))), + identified: Arc::new(RwLock::new(false)), + challenged: Arc::new(RwLock::new(false)), + challenge: Arc::new(RwLock::new(String::new())), + auth: Arc::new(RwLock::new(None)), + ping: Arc::new(RwLock::new(-1)), + }) + } + pub async fn send_message(&self, message: &CommunicationValue) { + let mut sender = self.sender.write().await; // Access the SplitSink + let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string())); + sender.send(message_text).await.unwrap(); // Send the message via the SplitSink + } + pub async fn get_community(&self) -> Option> { + self.community.read().await.clone() + } + pub async fn get_user_id(&self) -> i64 { + *self.user_id.read().await + } + pub async fn is_identified(&self) -> bool { + *self.identified.read().await && *self.challenged.read().await + } + + pub async fn handle_message(self: Arc, message: String) { + let mut cv = CommunicationValue::from_json(&message); + let user_id = self.get_user_id().await; + cv = cv.with_sender(user_id); + + if cv.is_type(CommunicationType::identification) && !self.is_identified().await { + self.handle_identification(cv).await; + return; + } + + if cv.is_type(CommunicationType::challenge_response) && !self.is_identified().await { + self.handle_challenge_response(cv).await; + return; + } + + if !self.is_identified().await { + return; + } + + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + + if cv.is_type(CommunicationType::client_changed) { + //self.handle_client_changed(cv).await; + return; + } + + if cv.is_type(CommunicationType::function) { + self.handle_function(cv).await; + return; + } + } + async fn handle_function(&self, cv: CommunicationValue) { + let name = cv.get_data(DataTypes::name).unwrap().as_str().unwrap(); + let path = cv.get_data(DataTypes::path).unwrap().as_str().unwrap(); + let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); + + let result = self + .get_community() + .await + .unwrap() + .run_function(self.get_user_id().await, name, path, function, &cv) + .await; + + self.send_message(&result).await; + } + async fn handle_identification(&self, cv: CommunicationValue) { + let user_id = cv + .get_data(DataTypes::user_id) + .unwrap_or(&JsonValue::Number(Number::from(0))) + .as_i64() + .unwrap_or(0); + + let Some(user) = get_user(user_id) else { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + }; + + { + let mut auth_guard = self.auth.write().await; + //*auth_guard = Some(user.clone()); + + let mut user_id_guard = self.user_id.write().await; + *user_id_guard = user_id; + + let mut identified_guard = self.identified.write().await; + *identified_guard = true; + } + + let challenge_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + { + let mut challenge_guard = self.challenge.write().await; + *challenge_guard = challenge_str.clone(); + } + + let user_public_key_bytes = match STANDARD.decode(&user.public_key) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { + Some(key) => key, + __ => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + let community_public_key = community.get_public_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let nonce_bytes: [u8; 12] = rand::random(); + let nonce = Nonce::from_slice(&nonce_bytes); + + let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { + Ok(data) => data, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let mut encrypted_out = nonce_bytes.to_vec(); + encrypted_out.extend(encrypted_challenge); + + let response = CommunicationValue::new(CommunicationType::challenge) + .add_data_str( + DataTypes::public_key, + STANDARD.encode(community_public_key.as_bytes()), + ) + .add_data_str(DataTypes::challenge, STANDARD.encode(&encrypted_out)) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { + let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { + Some(data) => data.to_string(), + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + if challenge_response_bytes.len() < 12 { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + + let Some(user) = self.auth.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + use hkdf::Hkdf; + use sha2::Sha256; + + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let (nonce_bytes, ciphertext) = challenge_response_bytes.split_at(12); + let nonce = Nonce::from_slice(nonce_bytes); + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { + Ok(pt) => pt, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let client_response = match String::from_utf8(decrypted_bytes) { + Ok(str) => str, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let expected_challenge = self.challenge.read().await.clone(); + + if client_response != expected_challenge { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + self.close().await; + return; + } + + { + let mut authenticated_guard = self.challenged.write().await; + *authenticated_guard = true; + } + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + let arc = Arc::new(community); + + let user_id = self.get_user_id().await; + if user_id == 0 { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + + arc.add_connection(self.clone()).await; + + let response = CommunicationValue::new(CommunicationType::identification_response) + .add_data(DataTypes::interactables, { + let a: Vec>> = arc.get_interactables(user_id).await; + let mut c: JsonValue = JsonValue::new_object(); + for b in a { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(b.get_codec()); + subject["data"] = b.get_data(); + c[b.get_name()] = subject; + } + c + }) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + + async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(*message_id); + self.send_message(&error).await; + } + pub async fn close(&self) { + let mut sender = self.sender.write().await; + let _ = sender.close().await; + } + pub async fn handle_close(self: Arc) { + if self.is_identified().await { + if self.get_user_id().await != 0 { + self.community + .read() + .await + .as_ref() + .unwrap() + .remove_connection(self.clone()) + .await; + } + } + } + + async fn handle_ping(&self, cv: CommunicationValue) { + if let Some(last_ping) = cv.get_data(DataTypes::last_ping) { + if let Ok(ping_val) = last_ping.to_string().parse::() { + let mut ping_guard = self.ping.write().await; + *ping_guard = ping_val; + } + } + + let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); + + self.send_message(&response).await; + } +} diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs new file mode 100644 index 0000000..8369f5a --- /dev/null +++ b/src/communities/community_manager.rs @@ -0,0 +1,59 @@ +use crate::communities::community::{self, Community}; +use crate::log; +use crate::util::file_util; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub static COMMUNITY_REGISTRY: Lazy>>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +pub async fn add_community(community: Arc) { + COMMUNITY_REGISTRY + .lock() + .await + .insert(community.get_name().to_string(), community); +} +pub async fn remove_community(name: &str) { + COMMUNITY_REGISTRY.lock().await.remove(name); +} +pub async fn clear() { + COMMUNITY_REGISTRY.lock().await.clear(); +} +pub async fn get_community(name: &str) -> Option> { + if let Some(c) = COMMUNITY_REGISTRY.lock().await.get(name) { + Some(c.clone()) + } else { + None + } +} + +pub async fn load_communities() { + let community_names = file_util::get_children("communities"); + for name in community_names { + if let Some(community) = community::load(&name).await { + add_community(community).await; + } else { + log!("failed to load the {} community", &name); + } + } +} +pub async fn save_communities() { + for community in COMMUNITY_REGISTRY.lock().await.values() { + community.save().await; + } +} + +pub async fn get_communities() -> Vec> { + COMMUNITY_REGISTRY.lock().await.values().cloned().collect() +} + +pub async fn rename_community(old_name: &str, new_name: &str) { + if let Some(community) = COMMUNITY_REGISTRY.lock().await.remove(old_name) { + COMMUNITY_REGISTRY + .lock() + .await + .insert(new_name.to_string(), community); + } +} diff --git a/src/communities/interactables/category.rs b/src/communities/interactables/category.rs new file mode 100644 index 0000000..6b6415f --- /dev/null +++ b/src/communities/interactables/category.rs @@ -0,0 +1,121 @@ +use crate::communities::{community::Community, interactables::interactable::Interactable}; +use async_trait::async_trait; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; +use ttp_core::CommunicationValue; +use uuid::Uuid; + +pub struct Category { + id: Uuid, + name: String, + path: String, + community: Arc, + children: Vec>>, +} +impl Category { + pub fn new() -> Category { + Category { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + children: Vec::new(), + } + } + pub fn get_child(&self, path: String, name: String) -> Option>> { + if path.is_empty() { + self.children + .iter() + .find(|child| child.get_name() == &name) + .cloned() + } else { + let sub_module = path.split("/").next().unwrap(); + let next = self + .children + .iter() + .find(|child| child.get_name() == sub_module) + .unwrap(); + if next.get_codec() == "category" { + let next_cat = next.as_any().downcast_ref::().unwrap(); + next_cat.get_child(path, name) + } else { + Some(next.clone()) + } + } + } + pub fn get_children(&self) -> Vec>> { + self.children.iter().map(|child| child.clone()).collect() + } +} + +#[async_trait] +impl Interactable for Category { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "category".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + for child in &self.children { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(child.get_codec()); + subject["data"] = child.get_data(); + v[child.get_name()] = subject; + } + v + } + async fn run_function(&self, _cv: CommunicationValue) -> CommunicationValue { + CommunicationValue::new(CommunicationType::error) + } + fn to_json(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + v["children"] = JsonValue::new_array(); + for child in &self.children { + let _ = v["children"].push(child.to_json()); + } + v + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/interactable.rs b/src/communities/interactables/interactable.rs new file mode 100644 index 0000000..0521de6 --- /dev/null +++ b/src/communities/interactables/interactable.rs @@ -0,0 +1,35 @@ +use crate::communities::community::Community; +use async_trait::async_trait; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; +use ttp_core::CommunicationValue; +use uuid::Uuid; + +pub type InteractableFactory = fn() -> Box; + +#[async_trait] +pub trait Interactable: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn get_codec(&self) -> String; + fn get_name(&self) -> &String; + fn get_path(&self) -> &String; + fn get_total_path(&self) -> String; + fn set_name(&mut self, name: String); + fn set_path(&mut self, path: String); + fn get_community(&self) -> &Arc; + fn set_community(&mut self, community: Arc); + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue; + fn get_data(&self) -> JsonValue; + fn get_id(&self) -> &Uuid; + fn to_json(&self) -> JsonValue; + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + json: &JsonValue, + ); +} diff --git a/src/communities/interactables/registry.rs b/src/communities/interactables/registry.rs new file mode 100644 index 0000000..2f57005 --- /dev/null +++ b/src/communities/interactables/registry.rs @@ -0,0 +1,75 @@ +use crate::communities::community::Community; +use crate::communities::interactables::category::Category; +use crate::communities::interactables::interactable::{Interactable, InteractableFactory}; +use crate::communities::interactables::text_chat::TextChat; +use crate::communities::interactables::voice_chat::VoiceChat; +use crate::util::file_util; +use json::JsonValue; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +pub static INTERACTABLE_REGISTRY: Lazy>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); +pub async fn load_interactables() { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(TextChat::new().get_codec(), || { + Box::new(TextChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(VoiceChat::new().get_codec(), || { + Box::new(VoiceChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(Category::new().get_codec(), || { + Box::new(Category::new()) as Box + }); +} +pub async fn register_interactable(name: String, interactable: InteractableFactory) { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(name.to_string(), interactable) + .unwrap(); +} +pub async fn get_interactable(name: &str) -> Box { + INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()() +} +pub async fn save(interactable: &Arc>) { + let mut json_object: JsonValue = interactable.to_json().clone(); + json_object["codec"] = JsonValue::String(interactable.get_codec()); + json_object["id"] = JsonValue::String(interactable.get_id().to_string()); + file_util::save_file( + &format!( + "communities/{}/interactables/{}", + interactable.get_community().get_name(), + interactable.get_path() + ), + &format!("{}.json", interactable.get_name()), + &json_object.to_string(), + ); +} +pub async fn load( + c: Arc, + path: String, + name: String, +) -> Box { + let s = file_util::load_file( + &format!("communities/{}/interactables/{}", c.get_name(), path), + &format!("{}.json", name), + ); + let json_object: JsonValue = json::parse(&s).unwrap(); + let codec: String = json_object["codec"].as_str().unwrap().to_string(); + let id: String = json_object["id"].as_str().unwrap().to_string(); + let mut interactable = get_interactable(&codec).await; + interactable.load(c, Uuid::parse_str(&id).unwrap(), path, name, &json_object); + interactable +} diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs new file mode 100644 index 0000000..88e1545 --- /dev/null +++ b/src/communities/interactables/text_chat.rs @@ -0,0 +1,261 @@ +use crate::{ + communities::{ + community::Community, community_connection::CommunityConnection, + interactables::interactable::Interactable, + }, + log, + util::file_util::{get_children, load_file, save_file}, +}; +use async_trait::async_trait; +use json::{JsonValue, array, object}; +use std::fs; +use std::sync::Arc; +use std::{any::Any, collections::HashMap}; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes}; +use uuid::Uuid; +pub struct TextChat { + id: Uuid, + name: String, + path: String, + community: Arc, +} +impl TextChat { + pub fn new() -> TextChat { + TextChat { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + } + } + pub fn add_message(&self, send_time: u128, sender: i64, message: &str) { + let user_dir = &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ); + + if let Err(e) = fs::create_dir_all(user_dir) { + log!("Failed to create chat directory: {}", e); + return; + } + + let mut chunk_index = 0; + let mut message_chunk = array![]; + + // find latest chunk not full (max 800 msgs) + loop { + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file(&user_dir, &file_name); + + if !file_content.is_empty() { + if let Ok(current_chunk) = json::parse(&file_content) { + if current_chunk.is_array() && current_chunk.len() < 800 { + message_chunk = current_chunk; + break; + } + } else { + log!("Failed to parse existing JSON file: {}", file_name); + } + } else { + break; + } + + chunk_index += 1; + if chunk_index > 1000 { + log!("Too many message chunks. Aborting add."); + return; + } + } + + let json_obj = object! { + "timestamp" => send_time as i64, + "content" => message, + "sender" => sender.to_string(), + }; + + if let Err(e) = message_chunk.push(json_obj) { + log!("Failed to push new message into JSON array: {}", e); + return; + } + + let file_name = format!("msgs_{}.json", chunk_index); + log!("Saving message to {}/{}", user_dir, file_name); + save_file(&user_dir, &file_name, &message_chunk.dump()); + } + pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { + let mut messages = array![]; + + let mut latest_chunk_index: i32 = -1; + let files = get_children(&format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + )); + + for entry in files { + if let Some(num) = { + entry + .strip_prefix("msgs_") + .and_then(|s| s.strip_suffix(".json")) + } { + if let Ok(index) = num.parse::() { + if index > latest_chunk_index { + latest_chunk_index = index; + } + } + } + } + + if latest_chunk_index == -1 { + return messages; + } + + let mut to_skip = loaded_messages; + let mut needed = amount; + + for chunk_index in (0..=latest_chunk_index).rev() { + if needed == 0 { + break; + } + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file( + &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ), + &file_name, + ); + if file_content.is_empty() { + continue; + } + if let Ok(chunk) = json::parse(&file_content) { + for i in (0..chunk.len()).rev() { + if needed == 0 { + break; + } + if to_skip > 0 { + to_skip -= 1; + continue; + } + messages.push(chunk[i].clone()).unwrap(); + needed -= 1; + } + } + } + + messages + } +} +#[async_trait] +impl Interactable for TextChat { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "text".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + JsonValue::new_object() + } + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + let payload = cv.get_data(DataTypes::payload).as_container().unwrap(); + if cv.get_data(DataTypes::function).as_str().unwrap() == "get_messages" { + let amount = payload.get(DataTypes::amount).as_i64().unwrap(); + let loaded_messages = payload["loaded_messages"].as_i64().unwrap(); + let messages = self.get_messages(loaded_messages, amount).clone(); + let mut payload = JsonValue::new_object(); + payload["messages"] = messages; + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_chunk".to_string()) + .add_data(DataTypes::payload, payload); + } + if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "send_message" { + let message = payload["message"].as_str().unwrap(); + let milliseconds_timestamp: u128 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + self.add_message(milliseconds_timestamp, cv.get_sender(), message); + + let mut distribution_payload = JsonValue::new_object(); + distribution_payload["message"] = JsonValue::String(message.to_string()); + distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string()); + distribution_payload["send_time"] = + JsonValue::String(milliseconds_timestamp.to_string()); + let distribution = CommunicationValue::new(CommunicationType::update) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_live".to_string()) + .add_data(DataTypes::payload, distribution_payload); + + let connections: HashMap>> = + self.get_community().get_connections().await.clone(); + + for con in connections.values() { + for c in con { + let cd: &Arc = c; + cd.send_message(&distribution).await; + } + } + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_received".to_string()) + .add_data(DataTypes::payload, JsonValue::new_object()); + } + CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) + } + fn to_json(&self) -> JsonValue { + JsonValue::new_object() + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/voice_chat.rs b/src/communities/interactables/voice_chat.rs new file mode 100644 index 0000000..2a2e3a2 --- /dev/null +++ b/src/communities/interactables/voice_chat.rs @@ -0,0 +1,187 @@ +use crate::communities::{community::Community, interactables::interactable::Interactable}; +use async_trait::async_trait; +use json::JsonValue; +use std::sync::Arc; +use std::{any::Any, sync::RwLock}; +use uuid::Uuid; +pub enum CallUserState { + Active, + Muted, + Deafed, +} +impl CallUserState { + pub fn parse(state: &str) -> CallUserState { + match state { + "active" => CallUserState::Active, + "muted" => CallUserState::Muted, + "deafed" => CallUserState::Deafed, + _ => CallUserState::Active, + } + } + pub fn to_string(&self) -> String { + match self { + CallUserState::Active => "active".to_string(), + CallUserState::Muted => "muted".to_string(), + CallUserState::Deafed => "deafed".to_string(), + } + } +} + +pub struct CallUser { + pub user_id: Uuid, + pub user_state: CallUserState, + pub streaming: bool, +} + +pub struct VoiceChat { + id: Uuid, + name: String, + path: String, + community: Arc, + users: RwLock>, +} +impl VoiceChat { + pub fn new() -> VoiceChat { + VoiceChat { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + users: RwLock::new(Vec::new()), + } + } + pub fn update_user_state( + self: Arc, + user_id: Uuid, + state: CallUserState, + streaming: bool, + ) { + if let Some(user) = self + .users + .write() + .unwrap() + .iter_mut() + .find(|u| u.user_id == user_id) + { + user.user_state = state; + user.streaming = streaming; + } + } +} +#[async_trait] +impl Interactable for VoiceChat { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "voice".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + let mut data = JsonValue::new_object(); + let mut active_users = JsonValue::new_object(); + for user in self.users.read().unwrap().iter() { + let mut user_data = JsonValue::new_object(); + let _ = user_data.insert("state", JsonValue::String(user.user_state.to_string())); + let _ = user_data.insert("streaming", JsonValue::Boolean(user.streaming)); + let _ = active_users.insert(&user.user_id.to_string(), user_data); + } + let _ = data.insert("active_users", active_users); + data + } + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + let payload = cv.get_data(DataTypes::payload).unwrap(); + let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); + + if function == "get_call" { + let sender_id = payload["sender_id"].as_str().unwrap(); + let message_id = payload["message"].as_str().unwrap(); + let send_time = payload["send_time"].as_str().unwrap(); + + let mut response_payload = JsonValue::new_object(); + response_payload["sender_id"] = JsonValue::String(sender_id.to_string()); + response_payload["message"] = JsonValue::String(message_id.to_string()); + response_payload["send_time"] = JsonValue::String(send_time.to_string()); + + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "getting_call".to_string()) + .add_data(DataTypes::payload, response_payload); + } + + if function == "update_user_state" { + let user_id = payload["user_id"].as_str().unwrap(); + let state = payload["state"].as_str().unwrap(); + let streaming = payload["streaming"].as_bool().unwrap(); + + if let Some(user) = self + .users + .write() + .unwrap() + .iter_mut() + .find(|u| u.user_id == Uuid::parse_str(user_id).unwrap()) + { + user.user_state = CallUserState::parse(state); + user.streaming = streaming; + } + let mut response_payload = JsonValue::new_object(); + response_payload["user_id"] = JsonValue::String(user_id.to_string()); + response_payload["state"] = JsonValue::String(state.to_string()); + response_payload["streaming"] = JsonValue::Boolean(streaming); + + return CommunicationValue::new(CommunicationType::update) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "user_changed".to_string()) + .add_data(DataTypes::payload, response_payload); + } + CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) + } + + fn to_json(&self) -> JsonValue { + let v = JsonValue::new_object(); + v + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/mod.rs b/src/communities/mod.rs new file mode 100644 index 0000000..ecad116 --- /dev/null +++ b/src/communities/mod.rs @@ -0,0 +1,13 @@ +pub mod community_manager; +pub mod interactables { + pub mod category; + pub mod interactable; + pub mod registry; + pub mod text_chat; + pub mod voice_chat; +} +pub mod community; +pub mod community_connection; +pub mod perms { + pub mod permission; +} diff --git a/src/communities/perms/permission.rs b/src/communities/perms/permission.rs new file mode 100644 index 0000000..32a37ee --- /dev/null +++ b/src/communities/perms/permission.rs @@ -0,0 +1,27 @@ +use json::JsonValue; +use uuid::Uuid; + +pub struct Permission { + pub id: Uuid, + pub name: String, +} +impl Permission { + pub fn new(id: Uuid, name: String) -> Self { + Permission { id, name } + } + pub fn to_json(&self) -> JsonValue { + json::object! { + "id" => self.id.to_string(), + "name" => self.name.clone() + } + } + pub fn from_json(json: JsonValue) -> Self { + Permission { + id: Uuid::parse_str(json["id"].as_str().unwrap()).unwrap(), + name: json["name"].as_str().unwrap().to_string(), + } + } + pub fn to_string(&self) -> String { + self.to_json().as_str().unwrap().to_string() + } +} diff --git a/src/data/mod.rs b/src/data/mod.rs deleted file mode 100644 index 22d12a3..0000000 --- a/src/data/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod user; diff --git a/src/data/user.rs b/src/data/user.rs deleted file mode 100644 index a175e6d..0000000 --- a/src/data/user.rs +++ /dev/null @@ -1,30 +0,0 @@ -use strum::IntoEnumIterator; -use strum_macros::EnumIter; - -#[derive(Debug, Clone, PartialEq, EnumIter, Eq)] -#[allow(unused, non_camel_case_types)] -pub enum UserStatus { - user_offline, - user_online, - user_dnd, - user_idle, - user_wc, - user_borked, - iota_offline, - iota_online, - iota_borked, -} -#[allow(unused)] -impl UserStatus { - pub fn to_string(&self) -> String { - format!("{:?}", self) - } - pub fn from_str(s: &str) -> Option { - for sel in UserStatus::iter() { - if &sel.to_string() == s { - return Some(sel); - } - } - None - } -} diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs new file mode 100755 index 0000000..a161226 --- /dev/null +++ b/src/gui/app_state.rs @@ -0,0 +1,201 @@ +use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry}; +use json::{JsonValue, object}; +use std::{collections::VecDeque, thread, time::Duration}; +use sysinfo::{RefreshKind, System}; + +#[derive(Clone)] +pub struct AppState { + pub logs: VecDeque, + pub cpu: Vec<(f64, f64)>, + pub ram: Vec<(f64, f64)>, + pub ping: Vec<(f64, f64)>, + pub net_up: Vec<(f64, f64)>, + pub net_down: Vec<(f64, f64)>, + pub sys_info: String, +} +const MAX_POINTS: usize = 1000; +const MAX_LOGS: usize = 100; + +impl AppState { + pub fn new() -> Self { + Self { + logs: VecDeque::new(), + cpu: Vec::new(), + ram: Vec::new(), + ping: Vec::new(), + net_up: Vec::new(), + net_down: Vec::new(), + sys_info: String::from("Loading..."), + } + } + + pub fn push_log(&mut self, msg: UiLogEntry) { + if self.logs.len() >= MAX_LOGS { + self.logs.pop_front(); + } + self.logs.push_back(msg); + } + + pub fn get_logs(&self) -> &VecDeque { + &self.logs + } + + pub fn push_cpu(&mut self, pt: (f64, f64)) { + self.cpu.push(pt); + if self.cpu.len() > MAX_POINTS { + self.cpu.remove(0); + } + } + + pub fn push_ram(&mut self, pt: (f64, f64)) { + self.ram.push(pt); + if self.ram.len() > MAX_POINTS { + self.ram.remove(0); + } + } + + pub fn push_ping_val(&mut self, pt: f64) { + self.ping.push((self.ping.len() as f64, pt)); + if self.ping.len() > MAX_POINTS { + self.ping.remove(0); + } + } + + pub fn push_net_up(&mut self, pt: (f64, f64)) { + self.net_up.push(pt); + if self.net_up.len() > MAX_POINTS { + self.net_up.remove(0); + } + } + + pub fn push_net_down(&mut self, pt: (f64, f64)) { + self.net_down.push(pt); + if self.net_down.len() > MAX_POINTS { + self.net_down.remove(0); + } + } + pub fn to_json(&self) -> JsonValue { + let json = object! { + "cpu" => self.cpu + .iter() + .map(|(_, y)| *y) + .collect::>(), + "ram" => self.ram + .iter() + .map(|(_, y)| *y) + .collect::>(), + "ping" => self + .ping + .iter() + .map(|(_, y)| *y) + .collect::>(), + "net_up" => self + .net_up + .iter() + .map(|(_, y)| *y) + .collect::>(), + "net_down" => self + .net_down + .iter() + .map(|(_, y)| *y) + .collect::>(), + }; + json + } + pub fn with_width(&self, width: u16) -> Self { + let mut new = self.clone(); + new.cpu = Self::downsample_to_fit_width(&new.cpu, width); + new.ram = Self::downsample_to_fit_width(&new.ram, width); + new.ping = Self::downsample_to_fit_width(&new.ping, width); + new.net_up = Self::downsample_to_fit_width(&new.net_up, width); + new.net_down = Self::downsample_to_fit_width(&new.net_down, width); + new + } + + fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { + let width_usize = (width as usize) * 2; + let len = data.len(); + + if len >= width_usize { + data[len - width_usize..].to_vec() + } else { + let mut result = Vec::with_capacity(width_usize); + + let dx = 1.0; + let pad_len = width_usize - len; + + let start_x = data + .first() + .map(|(x, _)| x - (dx * pad_len as f64)) + .unwrap_or(0.0); + let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0); + + for i in 0..pad_len { + result.push((start_x + i as f64 * dx, -1 as f64)); + } + + result.extend_from_slice(data); + result + } + } +} + +pub fn setup() { + ACTIVE_TASKS.insert("System info loader".to_string()); + tokio::spawn(async move { + let mut sys = System::new_with_specifics(RefreshKind::everything()); + let mut last_total_received = 0u64; + let mut last_total_transmitted = 0u64; + let mut counter = 0.0; + loop { + if *SHUTDOWN.read().await { + break; + } + sys.refresh_all(); + + let mut tcpu = 0; + for cpu in sys.cpus() { + tcpu += cpu.cpu_usage() as i64; + tcpu /= 2; + } + let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; + + let total_received = 0u64; + let total_transmitted = 0u64; + + let delta_received = if last_total_received == 0 { + 0 + } else { + total_received.saturating_sub(last_total_received) + }; + let delta_transmitted = if last_total_transmitted == 0 { + 0 + } else { + total_transmitted.saturating_sub(last_total_transmitted) + }; + last_total_received = total_received; + last_total_transmitted = total_transmitted; + + let net_down = delta_received as f64; + let net_up = delta_transmitted as f64; + + { + let mut st = APP_STATE.lock().unwrap(); + st.push_cpu((counter, tcpu as f64)); + st.push_ram((counter, ram)); + st.push_net_down((counter, net_down)); + st.push_net_up((counter, net_up)); + + st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); + } + + counter += 1.0; + if counter > 30.0 { + thread::sleep(Duration::from_millis(500)); + } else { + thread::sleep(Duration::from_millis(5)); + } + } + ACTIVE_TASKS.remove("System info loader"); + }); +} diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs new file mode 100644 index 0000000..b189bd4 --- /dev/null +++ b/src/gui/elements/console_card.rs @@ -0,0 +1,491 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; +use ttp_core::{CommunicationType, CommunicationValue}; +use uuid::Uuid; + +use crate::{ + ACTIVE_TASKS, RELOAD, SHUTDOWN, + gui::{ + elements::elements::{Element, InteractableElement, JoinableElement}, + interaction_result::InteractionResult, + ui::FPS, + util::borders::draw_block_joins, + }, + log, log_command, log_cv, + omikron::omikron_connection::OMIKRON_CONNECTION, + users::{user_manager, user_profile::UserProfile}, + util::file_util, +}; +use std::{ + any::Any, + sync::{Arc, Mutex}, + time::Duration, +}; +use tokio::time::Instant; + +pub struct ConsoleCard { + focused: bool, + pub title: String, + pub content: String, + pub cursor_position: usize, + + borders: Borders, + joins: Borders, + + cursor: Arc>, + last_swap: Arc>, + tab_index: usize, +} + +impl ConsoleCard { + pub fn new(title: &str, content: &str) -> Self { + ConsoleCard { + focused: false, + title: title.to_string(), + content: content.to_string(), + cursor_position: content.chars().count(), + borders: Borders::ALL, + joins: Borders::NONE, + cursor: Arc::new(Mutex::new(true)), + last_swap: Arc::new(Mutex::new(Instant::now())), + tab_index: 0, + } + } + + fn byte_index(&self) -> usize { + self.content + .char_indices() + .nth(self.cursor_position) + .map(|(i, _)| i) + .unwrap_or(self.content.len()) + } + + fn cursor_visible(&self) -> bool { + if !self.focused { + return false; + } + + let mut visible = self.cursor.lock().unwrap(); + let mut last = self.last_swap.lock().unwrap(); + let now = Instant::now(); + + if now.duration_since(*last) >= Duration::from_millis(500) { + *visible = !*visible; + *last = now; + } + + *visible + } + + fn current_prefix(&self) -> Option<&str> { + if self.content.starts_with('/') { + Some("/") + } else { + None + } + } + + fn cursor_spans(&self) -> Vec> { + let cursor_visible = self.cursor_visible(); + let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); + let mut spans = Vec::new(); + + if self.content.is_empty() { + if self.focused { + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } else { + spans.push(Span::styled(" ", Style::default().fg(Color::White))); + } + spans.push(Span::styled( + "send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } else { + spans.push(Span::styled( + " send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } + return spans; + } + + let byte_index = self.byte_index(); + let before = self.content[..byte_index].to_string(); + let after = self.content[byte_index..].to_string(); + + let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0); + + if prefix_len > 0 && before.len() >= prefix_len { + let prefix = &before[..prefix_len]; + let rest = &before[prefix_len..]; + spans.push(Span::styled( + prefix.to_string(), + Self::style_for_part(true, false, false), + )); + if !rest.is_empty() { + spans.push(Span::styled( + rest.to_string(), + Style::default().fg(Color::White), + )); + } + } else if !before.is_empty() { + spans.push(Span::styled( + before.clone(), + Style::default().fg(Color::White), + )); + } + + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } + + if !after.is_empty() { + spans.push(Span::styled(after, Style::default().fg(Color::White))); + } + + spans + } + + fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style { + if is_error { + return Style::default().fg(Color::Red); + } + + if is_hint { + return Style::default().fg(Color::DarkGray); + } + + if is_prefix { + return Style::default().fg(Color::DarkGray); + } + + Style::default().fg(Color::White) + } + + fn render_cursor_spans(&self) -> Vec> { + self.cursor_spans() + } + + fn move_cursor_left(&mut self) { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + + fn move_cursor_right(&mut self) { + let len = self.content.chars().count(); + if self.cursor_position < len { + self.cursor_position += 1; + } + } + + fn delete_at_cursor(&mut self) { + if self.content.is_empty() || self.cursor_position == 0 { + return; + } + + let start = self + .content + .char_indices() + .nth(self.cursor_position.saturating_sub(1)) + .map(|(i, _)| i) + .unwrap_or(0); + let end = self.byte_index(); + self.content.replace_range(start..end, ""); + self.cursor_position -= 1; + } + + fn insert_at_cursor(&mut self, c: char) { + let idx = self.byte_index(); + self.content.insert(idx, c); + self.cursor_position += 1; + } +} + +impl Element for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, r: Rect) { + let block = Block::default() + .borders(self.borders) + .title(self.title.clone()) + .title_style(Style::default().fg(Color::White)) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }) + .style(if self.focused { + Style::default().fg(Color::White) + } else { + Style::default() + }); + + let spans = self.render_cursor_spans(); + let par = Paragraph::new(Line::from(spans)) + .block(block) + .scroll((0, 0)); + f.render_widget(par, r); + draw_block_joins(f, r, self.borders, self.joins); + } +} + +impl JoinableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + match key.code { + KeyCode::Enter => { + if self.content.is_empty() { + log!(""); + return InteractionResult::Handled; + } + + let command = self.content.clone(); + let id = Uuid::new_v4(); + let id = id.to_string(); + let id = id.split_at(8).0; + let task_id = format!("command_{}_{}", command, id); + ACTIVE_TASKS.insert(task_id.clone()); + + log_command!("{}", command); + + tokio::spawn(async move { + run_command(&command).await; + ACTIVE_TASKS.remove(&task_id); + }); + + self.content.clear(); + self.cursor_position = 0; + self.tab_index = 0; + InteractionResult::Handled + } + KeyCode::Backspace => { + self.delete_at_cursor(); + InteractionResult::Handled + } + KeyCode::Delete => { + let len = self.content.chars().count(); + if self.cursor_position < len { + let start = self.byte_index(); + let end = self + .content + .char_indices() + .nth(self.cursor_position + 1) + .map(|(i, _)| i) + .unwrap_or(self.content.len()); + self.content.replace_range(start..end, ""); + } + InteractionResult::Handled + } + KeyCode::Left => { + self.move_cursor_left(); + InteractionResult::Handled + } + KeyCode::Right => { + self.move_cursor_right(); + InteractionResult::Handled + } + KeyCode::Home => { + self.cursor_position = 0; + InteractionResult::Handled + } + KeyCode::End => { + self.cursor_position = self.content.chars().count(); + InteractionResult::Handled + } + KeyCode::Tab => { + if let Some(prefix) = self.current_prefix() { + if prefix == "/" { + self.tab_index = self.tab_index.saturating_add(1); + } + } + InteractionResult::Handled + } + _ => { + if let Some(c) = key.code.as_char() { + self.insert_at_cursor(c); + InteractionResult::Handled + } else { + InteractionResult::Unhandled + } + } + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} + +pub async fn run_command(command: &str) { + let parts = command.split(" ").collect::>(); + + match parts.as_slice() { + ["tasks"] => { + let active_tasks: Vec = + ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect(); + let info = if *SHUTDOWN.read().await && *RELOAD.read().await { + "Rebooting, " + } else if *SHUTDOWN.read().await { + "Shutting , " + } else { + "" + }; + log!("{}Active tasks: {:?}", info, active_tasks); + } + ["fps"] => { + let (fps, skips) = *FPS.read().await; + log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips); + } + + ["help"] => { + log!("Available commands: tasks, fps, ping, user"); + } + + ["help", "tasks"] => { + log!("Tasks command usage: tasks"); + } + ["help", "fps"] => { + log!("FPS command usage: fps"); + } + ["help", "ping"] => { + log!("Ping command usage: ping [time]"); + } + ["help", "user"] => { + log!("User command usage: user add | user remove | user list"); + } + + ["ping"] => { + ping(20).await; + } + ["ping", time] => { + let time = time.parse::().unwrap_or(20); + ping(time).await; + } + ["user", "add", username] => { + if let (Some(user), Some(_)) = user_manager::create_user(username).await { + log!("Created user {}", user.user_id); + } else { + log!("Failed to create user"); + } + } + ["user", "remove", username] => { + if let Some(user) = user_manager::get_user_by_username(username) { + user_manager::remove_user(user.user_id); + log!("Removed user {}", user.user_id); + } else { + log!("Failed to find user"); + } + } + ["user", "list"] => { + let users: Vec = user_manager::get_users(); + for user in users { + let storage = file_util::get_designed_storage(user.user_id); + log!( + "> Username: {}, ID: {}, created at: {}, storage: {}", + user.username, + user.user_id, + user.created_at, + storage + ); + } + } + ["user", "info", username] => { + if let Some(user) = user_manager::get_user_by_username(username) { + user_manager::remove_user(user.user_id); + log!("Removed user {}", user.user_id); + } else { + log!("Failed to find user"); + } + } + ["reload"] | ["restart"] => { + log!("Restarting"); + *RELOAD.write().await = true; + *SHUTDOWN.write().await = true; + } + ["shutdown"] | ["stop"] => { + log!("Shutting down"); + *SHUTDOWN.write().await = true; + } + _ => { + log!("Unknown command"); + } + } +} + +pub async fn ping(time: u64) { + let conn = OMIKRON_CONNECTION.clone(); + + let response_cv = conn + .await_response( + &CommunicationValue::new(CommunicationType::ping), + Some(Duration::from_secs(time)), + ) + .await; + match response_cv { + Ok(response) => log_cv!(response), + Err(err) => log!("Ping error: {:?}", err), + } +} diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs new file mode 100644 index 0000000..3429e52 --- /dev/null +++ b/src/gui/elements/elements.rs @@ -0,0 +1,49 @@ +use std::any::Any; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect, widgets::Borders}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +#[allow(unused)] +pub trait Element: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn render(&self, f: &mut Frame, r: Rect); +} + +#[allow(unused)] +pub trait JoinableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn set_borders(&mut self, borders: Borders); + fn set_joins(&mut self, joins: Borders); +} + +#[allow(unused)] +pub trait InfoElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn get_info_screen(&self) -> Box; +} + +#[allow(unused)] +pub trait InteractableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn interact(&mut self, key: KeyEvent) -> InteractionResult; + + fn can_focus(&self) -> bool; + fn is_focused(&self) -> bool; + fn focus(&mut self, f: bool); +} diff --git a/src/gui/elements/graph_card.rs b/src/gui/elements/graph_card.rs new file mode 100644 index 0000000..d95f937 --- /dev/null +++ b/src/gui/elements/graph_card.rs @@ -0,0 +1,215 @@ +use std::{any::Any, sync::Arc}; + +use crossterm::event::KeyEvent; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + widgets::{ + Block, Borders, + canvas::{Canvas, Line}, + }, +}; + +use crate::{ + APP_STATE, + gui::{ + elements::elements::{Element, InteractableElement, JoinableElement}, + interaction_result::InteractionResult, + ui::UI, + util::borders::draw_block_joins, + }, +}; + +pub enum GRAPHS { + Ram, + Cpu, + Ping, +} + +impl GRAPHS { + pub fn get_color(&self) -> Color { + match self { + GRAPHS::Ram => Color::Blue, + GRAPHS::Cpu => Color::Red, + GRAPHS::Ping => Color::Green, + } + } + + pub fn get_graph(&self) -> Vec<(f64, f64)> { + match self { + GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(), + GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(), + GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(), + } + } + + pub fn get_unit(&self) -> String { + match self { + GRAPHS::Ram => "MB".to_string(), + GRAPHS::Cpu => "%".to_string(), + GRAPHS::Ping => "ms".to_string(), + } + } +} + +#[allow(unused)] +pub struct GraphCard { + ui: Arc, + graph_type: GRAPHS, + + focused: bool, + pub title: String, + + borders: Borders, + joins: Borders, + + open: bool, +} + +impl GraphCard { + pub fn new(ui: Arc, graph_type: GRAPHS, title: String) -> Self { + Self { + ui, + graph_type, + focused: false, + title, + borders: Borders::ALL, + joins: Borders::NONE, + open: true, + } + } + + pub fn set_open(&mut self, open: bool) { + self.open = open; + } +} +impl Element for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, r: Rect) { + if self.open { + let graph = self.graph_type.get_graph(); + let unit = self.graph_type.get_unit(); + let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0); + let min_y = graph + .iter() + .map(|(_, y)| *y) + .filter(|y| *y > 0.0) + .min_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0); + let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max); + + let block = Block::default() + .title(format!( + "{}:─{}{}─{}min/{}max", + self.title, + graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, + unit, + min_y as i64, + max_y as i64, + )) + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let canvas = Canvas::default() + .block(block) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, 100.0]) + .paint(|ctx| { + for (x, y) in &graph { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *y, + color: self.graph_type.get_color(), + }); + } + }); + f.render_widget(canvas, r); + } else { + let block = Block::default() + .title("") + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + f.render_widget(block, r); + } + draw_block_joins(f, r, self.borders, self.joins); + } +} + +impl JoinableElement for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn interact(&mut self, _key: KeyEvent) -> InteractionResult { + InteractionResult::Handled + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs new file mode 100755 index 0000000..e2d4ba1 --- /dev/null +++ b/src/gui/elements/log_card.rs @@ -0,0 +1,483 @@ +use crate::APP_STATE; +use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; +use crate::gui::interaction_result::InteractionResult; +use crate::gui::util::borders::draw_block_joins; +use crate::util::logger::PrintType; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; +use std::any::Any; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Clone, Debug)] +pub struct UiLogEntry { + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl UiLogEntry { + pub fn format_timestamp(&self) -> String { + let secs = (self.timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("{:02}:{:02}:{:02}", hours, minutes, seconds) + } +} + +impl From for UiLogEntry { + fn from(entry: LogEntry) -> Self { + Self { + timestamp_ms: entry.timestamp_ms, + sender: entry.sender, + message: entry.message, + is_error: entry.is_error, + } + } +} + +#[derive(Clone, Debug)] +pub struct LogEntry { + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl LogEntry { + pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { + Self { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + sender, + message, + is_error, + } + } +} + +pub struct LogCard { + focused: bool, + selected: bool, + scroll_offset: usize, + last_total_lines: usize, + last_visible_height: usize, + pub borders: Borders, + pub joins: Borders, +} + +impl LogCard { + pub fn new() -> Self { + Self { + focused: false, + selected: false, + scroll_offset: 0, + last_total_lines: 0, + last_visible_height: 10, + borders: Borders::ALL, + joins: Borders::NONE, + } + } + + fn get_logs(&self) -> Vec { + let state = APP_STATE.lock().unwrap(); + state.get_logs().iter().cloned().collect() + } + + fn find_split_point(s: &str, max_width: usize) -> usize { + if max_width == 0 { + return s.len(); + } + + let mut current_width = 0usize; + let mut last_boundary = 0usize; + + for (idx, ch) in s.char_indices() { + let char_width = if ch.is_ascii() { 1 } else { 2 }; + if current_width + char_width > max_width { + if last_boundary == 0 { + return idx + ch.len_utf8(); + } + return last_boundary; + } + current_width += char_width; + last_boundary = idx + ch.len_utf8(); + } + + s.len() + } + + fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { + let mut result = Vec::new(); + + let timestamp = entry.format_timestamp(); + let first_prefix = "┌ "; + let default_prefix = "│ "; + let last_prefix = "└ "; + + let prefix_width = 2; + let first_line_width = available_width.saturating_sub(prefix_width + 1); + let continuation_width = available_width.saturating_sub(prefix_width); + + let segments: Vec<&str> = entry.message.split('\n').collect(); + + let mut raw_lines = Vec::new(); + + for segment in segments { + let mut remaining = segment; + if remaining.is_empty() { + raw_lines.push(String::new()); + continue; + } + + let mut is_first_part = true; + while !remaining.is_empty() { + let current_width = if is_first_part { + first_line_width + } else { + continuation_width + }; + + let split_point = Self::find_split_point(remaining, current_width); + let line_content = remaining[..split_point].to_string(); + raw_lines.push(line_content); + remaining = &remaining[split_point..]; + is_first_part = false; + } + } + + if raw_lines.is_empty() { + raw_lines.push(String::new()); + } + + for (idx, content) in raw_lines.iter().enumerate() { + let is_last = idx + 1 == raw_lines.len(); + let is_single = raw_lines.len() == 1; + let is_first = idx == 0; + let prefix = if is_first { + first_prefix + } else if is_last && !is_single { + last_prefix + } else { + default_prefix + }; + + let mut line = String::from(prefix); + line.push_str(content); + + if is_last && !timestamp.is_empty() { + line.push(' '); + line.push_str(×tamp); + } + + result.push((line, entry.sender.prefix_color(), entry.is_error)); + } + + result + } + + fn build_all_lines( + &self, + entries: Vec, + width: usize, + ) -> Vec<(String, Color, bool)> { + let mut lines = Vec::new(); + + for entry in entries { + let wrapped = Self::wrap_entry(&entry, width); + lines.extend(wrapped); + } + + lines + } + + fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) { + if total_lines <= visible_height { + return (0, total_lines); + } + + let max_offset = total_lines - visible_height; + let clamped_offset = self.scroll_offset.min(max_offset); + + let end = total_lines - clamped_offset; + let start = end.saturating_sub(visible_height); + + (start, end) + } + + fn get_title_hints(&self) -> (bool, bool) { + if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height { + return (false, false); + } + + let max_offset = self.last_total_lines - self.last_visible_height; + let can_scroll_up = self.scroll_offset < max_offset; + let can_scroll_down = self.scroll_offset > 0; + + (can_scroll_up, can_scroll_down) + } + + fn build_title(&self) -> String { + if !self.focused { + return "Logs".to_string(); + } + + let (can_up, can_down) = self.get_title_hints(); + + if !can_up && !can_down { + return "Logs".to_string(); + } + + let nav_symbol = if self.selected { "↑" } else { "j" }; + let down_symbol = if self.selected { "↓" } else { "k" }; + + match (can_up, can_down) { + (true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol), + (true, false) => format!("Logs ({} older)", nav_symbol), + (false, true) => format!("Logs ({} newer)", down_symbol), + (false, false) => "Logs".to_string(), + } + } + + fn scroll_up(&mut self) { + let max_offset = self + .last_total_lines + .saturating_sub(self.last_visible_height); + self.scroll_offset = (self.scroll_offset + 1).min(max_offset); + } + + fn scroll_down(&mut self) { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + } + + fn split_line_prefix(line: &str) -> (&str, &str) { + if let Some(rest) = line.strip_prefix("└ ") { + ("└ ", rest) + } else if let Some(rest) = line.strip_prefix("│ ") { + ("│ ", rest) + } else if let Some(rest) = line.strip_prefix("┌ ") { + ("┌ ", rest) + } else { + ("", line) + } + } + + fn split_timestamp_suffix(line: &str) -> (&str, &str) { + if let Some(idx) = line.rfind(' ') { + let possible_timestamp = &line[idx + 1..]; + if possible_timestamp.len() == 8 + && possible_timestamp.as_bytes()[2] == b':' + && possible_timestamp.as_bytes()[5] == b':' + { + let (content, timestamp_with_space) = line.split_at(idx); + return (content.trim_end(), timestamp_with_space); + } + } + (line, "") + } +} + +impl Element for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, area: Rect) { + let entries = self.get_logs(); + + let block = Block::default() + .title(self.build_title()) + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let inner_area = block.inner(area); + f.render_widget(block, area); + + if inner_area.width == 0 || inner_area.height == 0 { + draw_block_joins(f, area, self.borders, self.joins); + return; + } + + let all_lines = self.build_all_lines(entries, inner_area.width as usize); + let total_lines = all_lines.len(); + let visible_height = inner_area.height as usize; + + let (start, end) = self.calculate_view_window(total_lines, visible_height); + let visible_lines = &all_lines[start..end]; + + let rendered_lines: Vec = visible_lines + .iter() + .map(|(line, prefix_color, is_error)| { + let mut spans = Vec::new(); + + let (prefix, rest) = Self::split_line_prefix(line); + + if !prefix.is_empty() { + spans.push(Span::styled( + prefix.to_string(), + Style::default().fg(*prefix_color), + )); + } + + let (content, timestamp) = Self::split_timestamp_suffix(rest); + let text_color = if *is_error { Color::Red } else { Color::White }; + + if !content.is_empty() { + spans.push(Span::styled( + content.to_string(), + Style::default().fg(text_color), + )); + } + + if !timestamp.is_empty() { + spans.push(Span::styled( + timestamp.to_string(), + Style::default().fg(Color::DarkGray), + )); + } + + Line::from(spans) + }) + .collect(); + + for (idx, line) in rendered_lines.iter().enumerate() { + let line_area = Rect { + x: inner_area.x, + y: inner_area.y + idx as u16, + width: inner_area.width, + height: 1, + }; + f.render_widget(Paragraph::new(line.clone()), line_area); + } + + draw_block_joins(f, area, self.borders, self.joins); + } +} + +impl JoinableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + &*self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + &*self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + let entries = self.get_logs(); + let estimated_width = 80usize; + let all_lines = self.build_all_lines(entries, estimated_width); + + self.last_total_lines = all_lines.len(); + let visible_height = self.last_visible_height.max(1); + + match key.code { + KeyCode::Enter | KeyCode::Char(' ') => { + self.selected = !self.selected; + InteractionResult::Handled + } + KeyCode::Char('j') | KeyCode::Char('J') => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); + } + InteractionResult::Handled + } + KeyCode::Char('k') | KeyCode::Char('K') => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); + } + InteractionResult::Handled + } + KeyCode::Up if self.selected => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); + } + InteractionResult::Handled + } + KeyCode::Down if self.selected => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); + } + InteractionResult::Handled + } + KeyCode::Home => { + if self.last_total_lines > visible_height { + self.scroll_offset = self.last_total_lines - visible_height; + } + InteractionResult::Handled + } + KeyCode::End => { + self.scroll_offset = 0; + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs new file mode 100644 index 0000000..b2557c5 --- /dev/null +++ b/src/gui/input_handler.rs @@ -0,0 +1,56 @@ +use crate::gui::ui::{UI, UNIQUE}; +use crate::{RELOAD, SHUTDOWN}; +use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Duration; + +pub fn setup_input_handler(ui: Arc) { + tokio::spawn(async move { + loop { + if *SHUTDOWN.read().await { + break; + } + + let event_result = tokio::task::spawn_blocking(|| { + if let Ok(true) = poll(Duration::from_millis(100)) { + read().ok().and_then(|ev| match ev { + Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), + _ => None, + }) + } else { + None + } + }) + .await; + + match event_result { + Ok(Some(key_event)) => { + handle_input(key_event, ui.clone()).await; + UNIQUE.store(true, Ordering::Relaxed); + } + Ok(_) => {} + Err(e) => { + eprintln!("Input task error: {}", e); + tokio::time::sleep(Duration::from_millis(10)).await; + } + } + } + }); +} + +pub async fn handle_input(key: KeyEvent, ui: Arc) { + match (key.code, key.modifiers) { + (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) + | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { + *SHUTDOWN.write().await = true; + } + (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { + *RELOAD.write().await = true; + *SHUTDOWN.write().await = true; + } + _ => { + ui.handle_input(key).await; + } + } +} diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs new file mode 100644 index 0000000..462149a --- /dev/null +++ b/src/gui/interaction_result.rs @@ -0,0 +1,49 @@ +use std::fmt::{Debug, Formatter}; +use std::future::Future; +use std::pin::Pin; + +use crate::gui::screens::screens::Screen; + +#[allow(unused)] +pub enum InteractionResult { + CloseScreen, + OpenScreen { + screen: Box, + }, + OpenFutureScreen { + screen: Pin> + Send>>, + }, + Handled, + Unhandled, +} + +impl Debug for InteractionResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"), + InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"), + InteractionResult::CloseScreen => write!(f, "CloseScreen"), + InteractionResult::Handled => write!(f, "Handled"), + InteractionResult::Unhandled => write!(f, "Unhandled"), + } + } +} + +impl PartialEq for InteractionResult { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + InteractionResult::OpenScreen { screen: _ }, + InteractionResult::OpenScreen { screen: _ }, + ) => true, + ( + InteractionResult::OpenFutureScreen { screen: _ }, + InteractionResult::OpenFutureScreen { screen: _ }, + ) => true, + (InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true, + (InteractionResult::Handled, InteractionResult::Handled) => true, + (InteractionResult::Unhandled, InteractionResult::Unhandled) => true, + _ => false, + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..b355046 --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,20 @@ +pub mod elements { + pub mod console_card; + pub mod elements; + pub mod graph_card; + pub mod log_card; +} +pub mod screens { + pub mod main_screen; + pub mod md_viewer; + pub mod screens; + pub mod terms_checker; + pub mod terms_updater; +} +pub mod util { + pub mod borders; +} +pub mod app_state; +pub mod input_handler; +pub mod interaction_result; +pub mod ui; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs new file mode 100644 index 0000000..85dbbaa --- /dev/null +++ b/src/gui/screens/main_screen.rs @@ -0,0 +1,242 @@ +use crate::gui::{ + elements::{ + console_card::ConsoleCard, + elements::{InteractableElement, JoinableElement}, + graph_card::{GRAPHS, GraphCard}, + log_card::LogCard, + }, + interaction_result::InteractionResult, + screens::screens::{NavDirection, Screen}, + ui::UI, +}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Layout, Margin, Rect}, + widgets::{Block, Borders}, +}; + +use std::{any::Any, sync::Arc}; + +pub struct MainScreen { + elements: Vec>, + nav_grid: Vec>>, + selected_coords: (usize, usize), + graphs_open: bool, +} + +impl MainScreen { + pub async fn new(ui: Arc) -> Self { + let mut elements: Vec> = Vec::new(); + + let nav_grid = vec![ + vec![Some(0), Some(2)], + vec![Some(0), Some(3)], + vec![Some(1), Some(4)], + ]; + + let mut log_card = LogCard::new(); + log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); + let mut console_card = ConsoleCard::new("Console", ""); + console_card.set_joins(Borders::TOP); + + elements.push(Box::new(log_card)); + elements.push(Box::new(console_card)); + + let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into()); + ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); + elements.push(Box::new(ram_graph)); + let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into()); + cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); + cpu_graph.set_joins(Borders::TOP); + elements.push(Box::new(cpu_graph)); + let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into()); + ping_graph.set_joins(Borders::TOP); + elements.push(Box::new(ping_graph)); + + let graphs_open = true; + + let mut screen = MainScreen { + elements, + nav_grid, + selected_coords: (1, 0), + graphs_open, + }; + screen.focus_current(); + screen + } + + fn focus_current(&mut self) { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + if element.can_focus() { + element.focus(true); + } + } + } + } + + fn unfocus_current(&mut self, y: usize, x: usize) { + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + element.focus(false); + } + } + } + + fn navigate(&mut self, direction: NavDirection) { + let (current_row, current_col) = self.selected_coords; + let current_element = self.nav_grid[current_row][current_col]; + + self.unfocus_current(current_row, current_col); + + let (delta_row, delta_col) = match direction { + NavDirection::Up => (-1isize, 0), + NavDirection::Down => (1, 0), + NavDirection::Left => (0, -1), + NavDirection::Right => (0, 1), + _ => (0, 0), + }; + + let mut next_row = current_row as isize; + let mut next_col = current_col as isize; + + loop { + next_row += delta_row; + next_col += delta_col; + + if next_row < 0 || next_col < 0 { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + let next_row_u = next_row as usize; + let next_col_u = next_col as usize; + + if next_row_u >= self.nav_grid.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + + if let Some(row) = self.nav_grid.get(next_row_u) { + if next_col_u >= row.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + + if let Some(next_element) = row[next_col_u] { + if Some(next_element) != current_element { + self.selected_coords = (next_row_u, next_col_u); + self.focus_current(); + return; + } + } + } + } + + self.focus_current(); + } +} + +impl Screen for MainScreen { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, rect: Rect) { + let main_block = Block::default().title("Main").borders(Borders::ALL); + f.render_widget(main_block, rect); + + let inner = rect.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + let graphs_width = if self.graphs_open { 30 } else { 2 }; + let main_width = inner.width.saturating_sub(graphs_width); + + let horizontal_chunks = Layout::default() + .direction(ratatui::layout::Direction::Horizontal) + .constraints([ + Constraint::Length(main_width), + Constraint::Length(graphs_width), + ]) + .split(inner); + + let left_area = horizontal_chunks[0]; + let right_area = horizontal_chunks[1]; + + let left_rows = + Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area); + + if let Some(log) = self.elements.get(0) { + log.as_element().render(f, left_rows[0]); + } + + if let Some(console) = self.elements.get(1) { + console.as_element().render(f, left_rows[1]); + } + + let graph_elements: Vec<_> = self + .elements + .iter() + .filter(|el| el.as_any().is::()) + .collect(); + + if !graph_elements.is_empty() { + let graph_chunks = Layout::vertical( + graph_elements + .iter() + .map(|_| Constraint::Ratio(1, graph_elements.len() as u32)) + .collect::>(), + ) + .split(right_area); + + for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { + el.as_element().render(f, *area); + } + } + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + match event.code { + KeyCode::Up => self.navigate(NavDirection::Up), + KeyCode::Down => self.navigate(NavDirection::Down), + KeyCode::Left => self.navigate(NavDirection::Left), + KeyCode::Right => self.navigate(NavDirection::Right), + KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => { + self.graphs_open = !self.graphs_open; + for element in self.elements.iter_mut() { + if let Some(graph) = element.as_any_mut().downcast_mut::() { + graph.set_open(self.graphs_open); + } + } + return InteractionResult::Handled; + } + _ => { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { + if let Some(el) = self.elements.get_mut(*index) { + return el.interact(event); + } + } + } + } + + InteractionResult::Handled + } +} diff --git a/src/gui/screens/md_viewer.rs b/src/gui/screens/md_viewer.rs new file mode 100644 index 0000000..10eabce --- /dev/null +++ b/src/gui/screens/md_viewer.rs @@ -0,0 +1,493 @@ +use crossterm::event::{self, Event, KeyCode, KeyEvent}; +use ratatui::{ + DefaultTerminal, + prelude::*, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, +}; +use std::{any::Any, time::Duration}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +pub struct FileViewer { + title: String, + text: Vec, + scroll: u16, + scroll_x: u16, +} + +impl Screen for FileViewer { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, rect: Rect) { + self.draw(f, rect); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + match event.code { + KeyCode::Char('q') | KeyCode::Esc => { + return InteractionResult::CloseScreen; + } + + KeyCode::Down => self.scroll = self.scroll.saturating_add(1), + KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), + KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), + KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), + KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), + KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), + + _ => {} + } + + InteractionResult::Unhandled + } +} + +impl FileViewer { + pub fn new(title: String, content: &str) -> Self { + Self { + title, + text: parse_document(content.to_owned()), + scroll: 0, + scroll_x: 0, + } + } + pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal { + loop { + terminal + .draw(|f| { + let area = f.area(); + self.draw(f, area); + }) + .unwrap(); + + if event::poll(Duration::from_millis(100)).unwrap() { + let ev = event::read().unwrap(); + self.handle_event(&ev); + + if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) { + break; + } + } + } + terminal + } + fn draw(&self, f: &mut Frame, area: Rect) { + use ratatui::text::Text; + + let mut rendered_lines = Vec::new(); + + for display_line in &self.text { + if display_line.scrollable { + let content: String = display_line + .line + .spans + .iter() + .map(|s| s.content.clone()) + .collect(); + + let start = self.scroll_x as usize; + let width = area.width as usize - 2; + + let visible = if start < content.chars().count() { + content.chars().skip(start).take(width).collect() + } else { + String::new() + }; + + let mut chars: Vec = visible.chars().collect(); + + if start > 0 && !chars.is_empty() { + chars[0] = '<'; + } + + if start + width < content.chars().count() && !chars.is_empty() { + let last = chars.len() - 1; + chars[last] = '>'; + } + + let visible: String = chars.into_iter().collect(); + + rendered_lines.push(Line::from(Span::styled( + visible, + display_line + .line + .spans + .first() + .map(|s| s.style) + .unwrap_or_default(), + ))); + } else { + rendered_lines.push(display_line.line.clone()); + } + } + + let paragraph = Paragraph::new(Text::from(rendered_lines)) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("{} - [Q to close]", self.title.as_str(),)), + ) + .wrap(Wrap { trim: false }) + .scroll((self.scroll, 0)); + + f.render_widget(paragraph, area); + } + + pub fn handle_event(&mut self, event: &Event) { + if let Event::Key(key) = event { + match key.code { + KeyCode::Down => self.scroll = self.scroll.saturating_add(1), + KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), + KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), + KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), + KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), + KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), + _ => {} + } + } + } +} +fn parse_document(input: String) -> Vec { + let mut lines_vec = Vec::new(); + let mut in_code_block = false; + let liness: Vec = input.lines().map(String::from).collect(); + let mut i = 0; + + while i < liness.len() { + let raw = &liness[i]; + + if raw.trim().starts_with("```") { + in_code_block = !in_code_block; + let code: String = if raw.trim().replace("```", "").is_empty() { + "──".to_string() + } else { + raw.trim().replace("```", "") + }; + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + format!("────────{}────────", code), + Style::default().fg(Color::DarkGray), + )), + scrollable: false, + }); + i += 1; + continue; + } + + if in_code_block { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.to_string(), + Style::default().fg(Color::Yellow), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("### ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("### ").to_string(), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("## ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("## ").to_string(), + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("# ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("# ").to_string(), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + + if raw.trim_start().starts_with("- ") { + let indent = raw.chars().take_while(|c| *c == ' ').count(); + lines_vec.push(DisplayLine { + line: Line::from(Span::raw(format!( + "{}• {}", + " ".repeat(indent), + raw.trim_start_matches("- ") + ))), + scrollable: false, + }); + i += 1; + continue; + } + + if raw.trim().starts_with('|') && raw.contains('|') { + let mut table_lines = vec![raw.clone()]; + let mut j = i + 1; + while j < liness.len() && liness[j].trim().starts_with('|') { + table_lines.push(liness[j].clone()); + j += 1; + } + + let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::>()); + lines_vec.extend(table_to_lines(table)); + i = j; + continue; + } + + lines_vec.push(DisplayLine { + line: Line::from(parse_inline(raw.as_str())), + scrollable: false, + }); + i += 1; + } + + lines_vec +} + +fn parse_inline(input: &str) -> Vec> { + let mut spans = Vec::new(); + let mut buf = String::new(); + + let mut bold = false; + let mut underline = false; + let mut code = false; + + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + let toggle = match c { + '*' if chars.peek() == Some(&'*') => { + chars.next(); + Some("bold") + } + '_' if chars.peek() == Some(&'_') => { + chars.next(); + Some("underline") + } + '`' => Some("code"), + _ => None, + }; + + if let Some(kind) = toggle { + flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); + + match kind { + "bold" => bold = !bold, + "underline" => underline = !underline, + "code" => code = !code, + _ => {} + } + continue; + } + + buf.push(c); + } + + flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); + spans +} + +fn current_style(bold: bool, underline: bool, code: bool) -> Style { + let mut style = Style::default(); + + if bold { + style = style.add_modifier(Modifier::BOLD); + } + if underline { + style = style.add_modifier(Modifier::UNDERLINED); + } + if code { + style = style.fg(Color::Yellow); + } + + style +} +#[derive(Clone)] +pub struct DisplayLine { + line: Line<'static>, + scrollable: bool, +} + +fn table_to_lines(table: Vec>) -> Vec { + if table.len() < 2 { + return vec![]; + } + + 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 = header + .iter() + .enumerate() + .map(|(i, h)| { + let h_len = h.chars().count().max(1); + if i == 0 { + table + .iter() + .map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0)) + .max() + .unwrap_or(h_len) + } else { + let max = (3 * h_len) as usize; + max.max(h_len) + } + }) + .collect(); + + let mut lines = Vec::new(); + + for (row_idx, row) in table.iter().enumerate() { + if row_idx == 1 { + let divider = widths + .iter() + .map(|w| "─".repeat(*w)) + .collect::>() + .join("─┼─"); + + lines.push(DisplayLine { + line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))), + scrollable: true, + }); + continue; + } + + let wrapped_cells: Vec> = row + .iter() + .enumerate() + .map(|(i, cell)| wrap_cell(cell, widths[i])) + .collect(); + + let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1); + + for line_idx in 0..row_height { + let mut line = String::new(); + + for (i, cell) in wrapped_cells.iter().enumerate() { + let content = cell.get(line_idx).map(String::as_str).unwrap_or(""); + line.push_str(&format!("{:width$}", content, width = widths[i])); + if i < wrapped_cells.len() - 1 { + line.push_str(" │ "); + } + } + + let style = if row_idx == 0 { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Green) + }; + + lines.push(DisplayLine { + line: Line::from(Span::styled(line, style)), + scrollable: true, + }); + } + } + + lines +} +fn wrap_cell(cell: &str, width: usize) -> Vec { + if width == 0 { + return vec![String::new()]; + } + + let mut lines = Vec::new(); + let mut current = String::new(); + + 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::>().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::>().chunks(width) { + lines.push(chunk.iter().collect()); + } + } + } + } + + if !current.is_empty() { + lines.push(current); + } + + if lines.is_empty() { + lines.push(String::new()); + } + + lines +} + +fn flush_span(spans: &mut Vec, buf: &mut String, style: Style) { + if !buf.is_empty() { + spans.push(Span::styled(buf.clone(), style)); + buf.clear(); + } +} + +fn parse_table(lines: &[&str]) -> Vec> { + let mut table = Vec::new(); + + for &line in lines { + if !line.starts_with('|') || !line.contains('|') { + break; + } + let row: Vec = line + .trim_matches('|') + .split('|') + .map(|s| s.trim().to_string()) + .collect(); + table.push(row); + } + + table +} diff --git a/src/gui/screens/screens.rs b/src/gui/screens/screens.rs new file mode 100644 index 0000000..9676fbb --- /dev/null +++ b/src/gui/screens/screens.rs @@ -0,0 +1,25 @@ +use std::any::Any; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect}; + +use crate::gui::interaction_result::InteractionResult; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NavDirection { + Up, + Down, + Left, + Right, + + Next, + Prev, +} + +pub trait Screen: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn render(&self, f: &mut Frame, rect: Rect); + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; +} diff --git a/src/gui/screens/terms_checker.rs b/src/gui/screens/terms_checker.rs new file mode 100644 index 0000000..26bb7c6 --- /dev/null +++ b/src/gui/screens/terms_checker.rs @@ -0,0 +1,351 @@ +use crate::{ + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + ui::UI, + }, + terms::{ + buttons::{checkbox, draw_buttons}, + consent_state::UserChoice, + focus::Focus, + terms_getter::{Type, get_link, get_terms}, + }, +}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::{any::Any, pin::Pin, sync::Arc}; +use tokio::sync::oneshot; + +pub struct TermsCheckerScreen { + ui: Arc, + sender: Option>, + + eula: bool, + tos: bool, + pp: bool, + + focus: Focus, +} + +impl TermsCheckerScreen { + pub fn new(ui: Arc, sender: Option>) -> Self { + Self { + ui, + sender, + eula: false, + tos: false, + pp: false, + focus: Focus::Eula, + } + } +} + +impl Screen for TermsCheckerScreen { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, size: Rect) { + let mut needed_height = 5; + + if size.height < 6 || size.width < 27 { + f.render_widget( + Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), + size, + ); + return; + } + + let max_width = 150; + let max_height = 26; + + let content_width = if max_width < size.width { + max_width + } else { + size.width + }; + let content_height = if max_height < size.height { + max_height + } else { + size.height + }; + + let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; + let vertical_margin = (size.height.saturating_sub(content_height)) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .split(Rect { + x: horizontal_margin, + y: vertical_margin, + width: content_width, + height: content_height, + }); + let eula_text = if size.width < 70 { + "EULA ¹ (https://legal.tensamin.net/eula/)" + } 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/)" + } 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/)" + } else { + "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" + }; + + let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = if size.width > 143 { + ( + vec![8, 3, 8, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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 > 92 { + ( + vec![9, 3, 9, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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![9, 3, 9, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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.", + ], + ) + } else { + ( + vec![10, 3, 11, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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.", + ], + ) + }; + let mut text_lines = vec![ + checkbox(eula_text, self.eula, self.focus == Focus::Eula, true), + checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula), + checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula), + Line::from(""), + Line::from("¹ Necessary– required to run the program"), + Line::from("² Optional – required only for Tensamin services"), + ]; + for line in agree_lines { + text_lines.insert(text_lines.len(), Line::from(line)); + } + + while size.height - 5 < text_lines.len() as u16 { + if optional_lines.len() == 0 { + break; + } + text_lines.remove(optional_lines[0] as usize); + optional_lines.remove(0); + } + needed_height += text_lines.len(); + + if size.width < 60 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 60 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height > 19 { + Style::default().fg(Color::Green) + } else if size.height >= 13 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 60"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / 13")), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("UI Too Small [Q to Quit]")), + ); + f.render_widget(warning, size); + return; + } + + let consent_block = Paragraph::new(Text::from(text_lines)).block( + Block::default() + .title(format!(" Tensamin User Consent [Q to Quit] ",)) + .borders(Borders::ALL), + ); + f.render_widget(consent_block, chunks[0]); + + draw_buttons( + f, + chunks[1], + self.focus, + (self.eula, self.tos && self.pp), + true, + false, + true, + ); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel]; + + if self.eula { + possible_states.push(Focus::Continue); + if self.tos && self.pp { + possible_states.push(Focus::ContinueAll); + } + } + + match event.code { + KeyCode::Esc => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + KeyCode::Up | KeyCode::Left => { + self.focus.prev(&possible_states); + InteractionResult::Handled + } + KeyCode::Down | KeyCode::Right | KeyCode::Tab => { + self.focus.next(&possible_states); + InteractionResult::Handled + } + KeyCode::Char('q') | KeyCode::Char('Q') => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + KeyCode::Char('o') | KeyCode::Char('O') => { + let terms_type = match self.focus { + Focus::Eula => Some(Type::EULA), + Focus::Tos => Some(Type::TOS), + Focus::Pp => Some(Type::PP), + _ => None, + }; + if let Some(terms_type) = terms_type { + let fut: Pin> + Send>> = + Box::pin(async move { + let content = get_terms(terms_type.clone()).await.unwrap(); + let screen: FileViewer = + FileViewer::new(terms_type.to_string(), &content); + Box::new(screen) as Box + }); + InteractionResult::OpenFutureScreen { screen: fut } + } else { + InteractionResult::Unhandled + } + } + KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { + Focus::Eula => { + let _ = open::that(get_link(Type::EULA)); + InteractionResult::Handled + } + Focus::Tos => { + let _ = open::that(get_link(Type::TOS)); + InteractionResult::Handled + } + Focus::Pp => { + let _ = open::that(get_link(Type::PP)); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Enter | KeyCode::Char(' ') => match self.focus { + Focus::Eula => { + self.eula = !self.eula; + self.tos = false; + self.pp = false; + InteractionResult::Handled + } + Focus::Tos if self.eula => { + self.tos = !self.tos; + InteractionResult::Handled + } + Focus::Pp if self.eula => { + self.pp = !self.pp; + InteractionResult::Handled + } + Focus::Cancel => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + Focus::Continue if self.eula => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptEULA); + } + InteractionResult::CloseScreen + } + Focus::ContinueAll if self.eula && self.tos && self.pp => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptAll); + } + InteractionResult::CloseScreen + } + _ => InteractionResult::Unhandled, + }, + + _ => InteractionResult::Unhandled, + } + } +} diff --git a/src/gui/screens/terms_updater.rs b/src/gui/screens/terms_updater.rs new file mode 100644 index 0000000..35da364 --- /dev/null +++ b/src/gui/screens/terms_updater.rs @@ -0,0 +1,691 @@ +use crate::{ + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + }, + terms::{ + buttons::{checkbox, draw_buttons}, + consent_state::{UpdateDecision, UserChoice}, + doc::Doc, + focus::Focus, + terms_getter::{Type, get_newest_link, get_terms}, + }, +}; +use chrono::{Local, TimeZone, Utc}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::any::Any; +use tokio::sync::oneshot; + +pub struct TermsUpdaterScreen { + sender: Option>, + + eula_needed: bool, + tos_needed: bool, + pp_needed: bool, + + eula_future: bool, + tos_future: bool, + pp_future: bool, + + eula_for_future: Option, + tos_for_future: Option, + pp_for_future: Option, + + update_needed: bool, + + eula: bool, + tos: bool, + pp: bool, + + focus: Focus, +} +impl TermsUpdaterScreen { + pub fn new( + consent_eula: UpdateDecision, + consent_tos: UpdateDecision, + consent_pp: UpdateDecision, + sender: Option>, + ) -> Self { + let (eula_needed, eula_future, eula_for_future): (bool, bool, Option) = + match consent_eula { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + let (tos_needed, tos_future, tos_for_future): (bool, bool, Option) = match consent_tos + { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + let (pp_needed, pp_future, pp_for_future): (bool, bool, Option) = match consent_pp { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + + let focus = if eula_needed { + Focus::Eula + } else if tos_needed { + Focus::Tos + } else if pp_needed { + Focus::Pp + } else { + Focus::Cancel + }; + + let update_needed = (eula_needed && !eula_future) + || (tos_needed && !tos_future) + || (pp_needed && !pp_future); + + Self { + sender, + + eula_needed, + tos_needed, + pp_needed, + + eula_for_future, + tos_for_future, + pp_for_future, + + eula_future, + tos_future, + pp_future, + + update_needed, + + eula: !eula_needed, + tos: !tos_needed, + pp: !pp_needed, + + focus, + } + } +} +impl Screen for TermsUpdaterScreen { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, size: Rect) { + let mut needed_height = 5; + + if size.height < 6 || size.width < 27 { + f.render_widget( + Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), + size, + ); + return; + } + + let max_width = 150; + let max_height = 30; + + let content_width = if max_width < size.width { + max_width + } else { + size.width + }; + let content_height = if max_height < size.height { + max_height + } else { + size.height + }; + + let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; + let vertical_margin = (size.height.saturating_sub(content_height)) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .split(Rect { + x: horizontal_margin, + y: vertical_margin, + width: content_width, + height: content_height, + }); + + let mut text_lines: Vec = Vec::new(); + let mut header_lines = 0; + let separator = if size.width > 72 { + header_lines += 3; + text_lines.push(Line::from( + "You previously accepted earlier versions of Tensamin’s legal documents.", + )); + text_lines.push(Line::from( + "Some of them have been updated and are listed below for your review.", + )); + text_lines.push(Line::from("")); + 2 + } else { + header_lines += 5; + text_lines.push(Line::from("You previously accepted earlier ")); + text_lines.push(Line::from("versions of Tensamin’s legal documents.")); + text_lines.push(Line::from("Some of them have been updated and")); + text_lines.push(Line::from("are listed below for your review.")); + text_lines.push(Line::from("")); + 4 + }; + + if self.eula_needed { + header_lines += 1; + if self.eula_future { + if size.width < 80 { + text_lines.push(checkbox( + "EULA ¹³ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + + header_lines += 1; + let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + + header_lines += 1; + let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "EULA ¹ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + } else { + text_lines.push(checkbox( + "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + } + } + } + + if self.tos_needed { + header_lines += 1; + if self.tos_future { + if size.width < 80 { + text_lines.push(checkbox( + "ToS ²³ (https://legal.tensamin.net/tos/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "ToS ² (https://legal.tensamin.net/tos/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + } else { + text_lines.push(checkbox( + "Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + } + } + } + + if self.pp_needed { + header_lines += 1; + if self.pp_future { + if size.width < 80 { + text_lines.push(checkbox( + "PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "PP ² (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + } else { + text_lines.push(checkbox( + "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + } + } + } + + text_lines.push(Line::from("")); + text_lines.push(Line::from("¹ Necessary to run the program")); + text_lines.push(Line::from("² Optional - only for Tensamin services")); + if size.width < 100 { + text_lines.push(Line::from( + "³ Future version - consent stored now, takes effect later", + )); + } else { + text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply.")); + } + + text_lines.push(Line::from("")); + + let mut optional_lines: Vec = if size.width > 143 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 6, + header_lines, + header_lines + 6, + separator, + header_lines + 2, + ] + } + } else if size.width > 92 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you agree to the End User", + )); + text_lines.push(Line::from( + "License Agreement and applicable Terms of Service.", + )); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you agree to the End User", + )); + text_lines.push(Line::from( + "License Agreement and applicable Terms of Service.", + )); + } + + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 8, + header_lines, + header_lines + 8, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } + } else if size.width > 73 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you read, understood and", + )); + text_lines.push(Line::from( + "agree to the End User License Agreement and applicable Terms of Service.", + )); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you read, understood and", + )); + text_lines.push(Line::from( + "agree to the End User License Agreement and applicable Terms of Service.", + )); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the ToS and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "While having a document selected press O to view in this UI or press L", + )); + text_lines.push(Line::from("to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 8, + header_lines, + header_lines + 8, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } + } else { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from("By selecting Continue, you confirm that you")); + text_lines.push(Line::from("agree to the End User License")); + text_lines.push(Line::from("Agreement and applicable Terms of Service.")); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate", + )); + text_lines.push(Line::from("once these changes apply.")); + } else { + text_lines.push(Line::from("By selecting Continue, you confirm that you")); + text_lines.push(Line::from("agree to the End User License")); + text_lines.push(Line::from("Agreement and applicable Terms of Service.")); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from("Tensamin services require acceptance of the")); + text_lines.push(Line::from("Terms of Service and Privacy Policy.")); + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "While having a document selected press O to view", + )); + text_lines.push(Line::from("in this UI or press L to open as a link.")); + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 10, + header_lines, + header_lines + 11, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 8, + header_lines, + header_lines + 9, + separator, + header_lines + 2, + ] + } + }; + + while size.height - 5 < text_lines.len() as u16 { + if optional_lines.len() == 0 { + break; + } + text_lines.remove(optional_lines[0] as usize); + optional_lines.remove(0); + } + needed_height += text_lines.len(); + + let q_informer = if self.update_needed { + "Q to Exit" + } else { + "Q to Cancel" + }; + if size.width < 60 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 60 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height > 20 { + Style::default().fg(Color::Green) + } else if size.height >= (header_lines as u16 + 10) { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 60"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / {}", header_lines + 10)), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("UI Too Small [{}]", q_informer)), + ); + + f.render_widget(warning, size); + return; + } + + let consent_block = Paragraph::new(Text::from(text_lines)).block( + Block::default() + .title(format!(" Update Tensamin User Consent [{}] ", q_informer)) + .borders(Borders::ALL), + ); + f.render_widget(consent_block, chunks[0]); + + let downgrade_scenario = self.tos_needed || self.pp_needed; + draw_buttons( + f, + chunks[1], + self.focus, + (self.eula, self.tos && self.pp), + self.update_needed, + downgrade_scenario, + self.pp_needed || self.tos_needed, + ); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + let mut possible_states = Vec::new(); + + if self.eula_needed { + possible_states.push(Focus::Eula); + } + if self.tos_needed { + possible_states.push(Focus::Tos); + } + if self.pp_needed { + possible_states.push(Focus::Pp); + } + + possible_states.push(Focus::Cancel); + + if self.eula { + possible_states.push(Focus::Continue); + if self.tos && self.pp { + possible_states.push(Focus::ContinueAll); + } + } + + match event.code { + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + return InteractionResult::CloseScreen; + } + KeyCode::Enter | KeyCode::Char(' ') => match self.focus { + Focus::Eula => { + self.eula = !self.eula; + self.tos = !self.tos_needed; + self.pp = !self.pp_needed; + InteractionResult::Handled + } + Focus::Tos if self.eula => { + self.tos = !self.tos; + InteractionResult::Handled + } + Focus::Pp if self.eula => { + self.pp = !self.pp; + InteractionResult::Handled + } + Focus::Cancel => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + Focus::Continue if self.eula => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptEULA); + } + InteractionResult::CloseScreen + } + Focus::ContinueAll if self.eula && self.tos && self.pp => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptAll); + } + InteractionResult::CloseScreen + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Char('o') | KeyCode::Char('O') => { + let terms_type = match self.focus { + Focus::Eula => Some(Type::EULA), + Focus::Tos => Some(Type::TOS), + Focus::Pp => Some(Type::PP), + _ => None, + }; + + if let Some(terms_type) = terms_type { + let fut = Box::pin(async move { + let content = get_terms(terms_type.clone()).await.unwrap(); + Box::new(FileViewer::new(terms_type.to_string(), &content)) + as Box + }); + + return InteractionResult::OpenFutureScreen { screen: fut }; + } else { + InteractionResult::Unhandled + } + } + KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { + Focus::Eula => { + let _ = open::that(get_newest_link(Type::EULA)); + InteractionResult::Handled + } + Focus::Tos => { + let _ = open::that(get_newest_link(Type::TOS)); + InteractionResult::Handled + } + Focus::Pp => { + let _ = open::that(get_newest_link(Type::PP)); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Up | KeyCode::Left => { + self.focus.prev(&possible_states); + InteractionResult::Handled + } + KeyCode::Down | KeyCode::Right | KeyCode::Tab => { + self.focus.next(&possible_states); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + } + } +} diff --git a/src/gui/tui.rs b/src/gui/tui.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/gui/ui.rs b/src/gui/ui.rs new file mode 100644 index 0000000..5903a4b --- /dev/null +++ b/src/gui/ui.rs @@ -0,0 +1,167 @@ +use crate::{ + ACTIVE_TASKS, SHUTDOWN, + gui::{ + input_handler::setup_input_handler, interaction_result::InteractionResult, + screens::screens::Screen, + }, +}; +use crossterm::event::KeyEvent; +use once_cell::sync::Lazy; +use ratatui::{Terminal, backend::CrosstermBackend, init}; +use std::{ + collections::VecDeque, + io::Stdout, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; +use tokio::{sync::RwLock, time::Instant}; + +/// UI state and rendering +pub static UNIQUE: AtomicBool = AtomicBool::new(true); + +pub static FPS: Lazy> = Lazy::new(|| RwLock::new((0.0, 0.0))); + +pub struct UI { + pub terminal: Arc>>>, + screen_stack: Arc>>>, +} + +pub fn start_tui() -> Arc { + let ui = Arc::new(UI::new()); + let uic = ui.clone(); + ACTIVE_TASKS.insert("UI Renderer".to_string()); + tokio::spawn(async move { + let mut last_render = Instant::now(); + + let mut fps_samples: VecDeque = VecDeque::with_capacity(20); + let mut skip_samples: VecDeque = VecDeque::with_capacity(20); + + let mut fps_sum = 0.0; + let mut skip_sum: u32 = 0; + + let mut skipped = 0; + + loop { + if *SHUTDOWN.read().await { + break; + } + + if skipped > 5 || UNIQUE.load(Ordering::Relaxed) { + uic.render().await; + + skip_samples.push_back(skipped); + skip_sum += skipped as u32; + + if skip_samples.len() > 20 { + if let Some(old) = skip_samples.pop_front() { + skip_sum -= old as u32; + } + } + + skipped = 0; + + let elapsed = last_render.elapsed().as_secs_f64(); + if elapsed > 0.0 { + let fps = 1.0 / elapsed; + + fps_samples.push_back(fps); + fps_sum += fps; + + if fps_samples.len() > 20 { + if let Some(old) = fps_samples.pop_front() { + fps_sum -= old; + } + } + } + + let avg_fps = if !fps_samples.is_empty() { + fps_sum / fps_samples.len() as f64 + } else { + 0.0 + }; + + let avg_skips_percentage = if !skip_samples.is_empty() { + let avg_skipped = skip_sum as f64 / skip_samples.len() as f64; + let total_iterations = avg_skipped + 1.0; + (avg_skipped / total_iterations) * 100.0 + } else { + 0.0 + }; + + *FPS.write().await = (avg_fps, avg_skips_percentage); + + last_render = Instant::now(); + UNIQUE.store(false, Ordering::Relaxed); + } else { + skipped += 1; + } + tokio::time::sleep(Duration::from_millis(16)).await; + } + ACTIVE_TASKS.remove("UI Renderer"); + ratatui::restore(); + }); + setup_input_handler(ui.clone()); + ui +} +impl UI { + pub fn new() -> Self { + let terminal = init(); + Self { + terminal: Arc::new(Mutex::new(terminal)), + screen_stack: Arc::new(RwLock::new(Vec::new())), + } + } + + pub async fn set_screen(&self, screen: Box) { + self.screen_stack.write().await.push(screen); + } + pub async fn replace_screen(&self, screen: Box) { + let mut stack = self.screen_stack.write().await; + stack.pop(); + stack.push(screen); + } + pub async fn handle_input(self: Arc, key_event: KeyEvent) { + let result = { + let mut stack = self.screen_stack.write().await; + if let Some(screen) = stack.last_mut() { + screen.handle_input(key_event) + } else { + return; + } + }; + match result { + InteractionResult::OpenScreen { screen } => { + self.set_screen(screen).await; + } + InteractionResult::OpenFutureScreen { screen: fut } => { + let ui = self.clone(); + let screen = fut.await; + ui.set_screen(screen).await; + } + InteractionResult::CloseScreen => { + let mut stack = self.screen_stack.write().await; + stack.pop(); + + if stack.is_empty() { + *SHUTDOWN.write().await = true; + } + } + InteractionResult::Handled => {} + InteractionResult::Unhandled => {} + } + } + + pub async fn render(&self) { + if let Some(screen) = self.screen_stack.read().await.last() { + let mut terminal = self.terminal.lock().unwrap(); + terminal + .draw(|f| { + screen.render(f, f.area()); + }) + .unwrap(); + } + } +} diff --git a/src/gui/util/borders.rs b/src/gui/util/borders.rs new file mode 100644 index 0000000..34460bc --- /dev/null +++ b/src/gui/util/borders.rs @@ -0,0 +1,63 @@ +use ratatui::layout::Rect; +use ratatui::prelude::*; +use ratatui::style::Style; +use ratatui::widgets::Borders; + +fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { + frame + .buffer_mut() + .set_string(x, y, c.to_string(), Style::default()); +} + +pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { + let x0 = area.x; + let y0 = area.y; + let x1 = area.x + area.width - 1; + let y1 = area.y + area.height - 1; + + if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { + let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┬', + (false, false) => '┌', + }; + set_join_char(frame, x0, y0, top_left); + } + + if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { + let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┬', + (false, false) => '┐', + }; + set_join_char(frame, x1, y0, top_right); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { + let bottom_left = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::LEFT), + ) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┴', + (false, false) => '└', + }; + set_join_char(frame, x0, y1, bottom_left); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { + let bottom_right = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::RIGHT), + ) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┴', + (false, false) => '┘', + }; + set_join_char(frame, x1, y1, bottom_right); + } +} diff --git a/src/langu/language_creator.rs b/src/langu/language_creator.rs new file mode 100644 index 0000000..fc6d1c5 --- /dev/null +++ b/src/langu/language_creator.rs @@ -0,0 +1,80 @@ +use crate::util::file_util::save_file; +use json::{self, JsonError, JsonValue}; + +pub fn create_languages() -> Result<(), JsonError> { + let mut frontend_messages = JsonValue::new_object(); + let mut omikron_messages = JsonValue::new_object(); + let mut button_texts = JsonValue::new_object(); + let mut general_texts = JsonValue::new_object(); + let mut debug_messages = JsonValue::new_object(); + + frontend_messages.insert("error", "An error occurred")?; + // FRONTEND + frontend_messages.insert("get_chats", "User {} is loading conversations")?; + frontend_messages.insert("message_get", "User {} is loading messages")?; + frontend_messages.insert("get_communities", "User {} is loading communities")?; + frontend_messages.insert("client_connected", "Client {} connected")?; + frontend_messages.insert("add_conversation", "User {} added {}")?; + frontend_messages.insert("message_send", "User {} sent a message")?; + + // OMIKRON + omikron_messages.insert( + "identification_response", + "IOTA identified on Omikron, {} users!", + )?; + omikron_messages.insert( + "send_message_failed", + "Failed to send message to Omikron: {}", + )?; + omikron_messages.insert("connection_failed", "Failed to connect to Omikron: {}")?; + + // BUTTONS + button_texts.insert("exit", "Exit")?; + + // GENERAL + general_texts.insert("iota_id", "IOTA ID: {}-####-####-####-############")?; + general_texts.insert("user_id", "USER ID: {}")?; + general_texts.insert("user_ids", "USER IDS: {}")?; + general_texts.insert("user_load_failed", "Failed to load user data")?; + general_texts.insert("setup_completed", "Launched")?; + general_texts.insert( + "community_active", + "Communities active on ws://{}:{}/community/...", + )?; + general_texts.insert( + "community_start_error", + "Failed to start community socket on port {}!", + )?; + general_texts.insert( + "community_start_error_admin", + "Failed to start community socket on port {}! Run with admin privileges", + )?; + // DEBUG + debug_messages.insert("", "")?; + save_file( + "languages/en_INT", + "frontend.json", + &frontend_messages.to_string(), + ); + save_file( + "languages/en_INT", + "omikron.json", + &omikron_messages.to_string(), + ); + save_file( + "languages/en_INT", + "buttons.json", + &button_texts.to_string(), + ); + save_file( + "languages/en_INT", + "debug.json", + &debug_messages.to_string(), + ); + save_file( + "languages/en_INT", + "general.json", + &general_texts.to_string(), + ); + Ok(()) +} diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs new file mode 100644 index 0000000..c903886 --- /dev/null +++ b/src/langu/language_manager.rs @@ -0,0 +1,105 @@ +use crate::util::file_util::{self}; +use json::parse; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +#[derive(Clone)] +pub struct LanguagePack { + language: HashMap, +} + +pub static LANGUAGE_PACK: Lazy> = + 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 { + 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); +} + +pub fn from_key(key: &str) -> String { + LANGUAGE_PACK + .lock() + .unwrap() + .get_translation(key) + .to_string() +} + +pub fn format(key: &str, args: &[&str]) -> String { + let message = from_key(key); + let mut formatted = String::new(); + let parts = message.split("{}"); + for (i, part) in parts.enumerate() { + formatted.push_str(part); + if i < args.len() { + formatted.push_str(args[i]); + } + } + formatted +} + +impl LanguagePack { + pub fn new(language: &str) -> Self { + let mut pack = LanguagePack { + language: HashMap::new(), + }; + pack.load_language(language); + pack + } + + pub fn load_language(&mut self, language: &str) { + let path = format!("languages/{}/", language); + + let frontend_messages = file_util::load_file(&path, "frontend.json"); + let frontend_messages = parse(&frontend_messages).unwrap(); + for (key, value) in frontend_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let omikron_messages = file_util::load_file(&path, "omikron.json"); + let omikron_messages = parse(&omikron_messages).unwrap(); + for (key, value) in omikron_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let button_texts = file_util::load_file(&path, "buttons.json"); + let button_texts = parse(&button_texts).unwrap(); + for (key, value) in button_texts.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let debug_messages = file_util::load_file(&path, "debug.json"); + let debug_messages = parse(&debug_messages).unwrap(); + for (key, value) in debug_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let general_messages = file_util::load_file(&path, "general.json"); + let general_messages = parse(&general_messages).unwrap(); + for (key, value) in general_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + } + pub fn get_translation(&self, key: &str) -> String { + match self.language.get(key) { + Some(v) if !v.is_empty() => v.clone(), + _ => key.to_uppercase(), + } + } +} diff --git a/src/langu/mod.rs b/src/langu/mod.rs new file mode 100644 index 0000000..352d639 --- /dev/null +++ b/src/langu/mod.rs @@ -0,0 +1,2 @@ +pub mod language_creator; +pub mod language_manager; diff --git a/src/main.rs b/src/main.rs index fa511ea..767106f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,51 +1,183 @@ -mod anonymous_clients; -mod calls; -mod data; -mod omega; -mod rho; +use dashmap::DashSet; +use once_cell::sync::Lazy; +use pnet::datalink::NetworkInterface; +use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::Mutex; +use tokio::sync::RwLock; +use tokio::time::{Duration, sleep}; + +mod auth; +mod gui; +mod langu; +mod omikron; +mod terms; +mod users; mod util; -use std::env; +use crate::gui::app_state; +use crate::gui::app_state::AppState; +use crate::gui::screens::main_screen::MainScreen; +use crate::gui::ui::start_tui; +use crate::langu::language_creator; +use crate::omikron::omikron_connection::OmikronConnection; +use crate::terms::consent_state; +use crate::users::user_manager; +use crate::util::config_util::CONFIG; +use crate::util::file_util::download_and_extract_zip; +use crate::util::file_util::has_dir; +use crate::util::logger; -use dotenv::dotenv; -use once_cell::sync::Lazy; -use rustls::crypto::aws_lc_rs::default_provider; +pub static APP_STATE: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); -use crate::{ - calls::call_util::garbage_collect_calls, - omega::omega_connection::get_omega_connection, - rho::server::start, - util::{ - crypto_helper::{load_public_key, load_secret_key}, - logger::startup, - }, -}; +pub static SHUTDOWN: Lazy> = Lazy::new(|| RwLock::new(false)); +pub static RELOAD: Lazy> = Lazy::new(|| RwLock::new(true)); +pub static ACTIVE_TASKS: Lazy> = Lazy::new(|| DashSet::new()); -static PRIVATE_KEY: Lazy = Lazy::new(|| env::var("PRIVATE_KEY").unwrap()); -pub fn get_private_key() -> x448::Secret { - load_secret_key(&*PRIVATE_KEY).unwrap() -} -static PUBLIC_KEY: Lazy = Lazy::new(|| env::var("PUBLIC_KEY").unwrap()); -pub fn get_public_key() -> x448::PublicKey { - load_public_key(&*PUBLIC_KEY).unwrap() -} - -#[tokio::main] +#[tokio::main(flavor = "multi_thread", worker_threads = 16)] +#[allow(unused_must_use, dead_code)] async fn main() { - if let Err(_) = default_provider().install_default() { - println!("Error loading Provider"); - return; - } - dotenv().ok(); - startup(); + while *RELOAD.read().await { + *RELOAD.write().await = false; + *SHUTDOWN.write().await = false; - get_omega_connection(); - tokio::spawn(async move { - if let Err(e) = start(959).await { - log_err!(0, util::logger::PrintType::General, "{}", e); + let ui = start_tui(); + + let (eula, tos_pp) = consent_state::check(ui.clone()).await; + + if !eula { + *SHUTDOWN.write().await = true; + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_millis(100)).await; + } + println!("You need to accept our End User Licence Agreement before launching!"); + println!("You can find this at 'agreements'!"); + return; } - }); - garbage_collect_calls(); + if !tos_pp { + *SHUTDOWN.write().await = true; + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_millis(100)).await; + } + println!( + "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" + ); + println!("In future releases this will be optional!"); + println!("You can find this at 'agreements'!"); + return; + } + app_state::setup(); - tokio::signal::ctrl_c().await.unwrap(); + let main_screen = MainScreen::new(ui.clone()).await; + ui.set_screen(Box::new(main_screen)).await; + + // LANGUAGE PACK + if let Err(e) = language_creator::create_languages() { + println!("Language pack creation failed: {}", e); + return; + } + + // UI + logger::startup(); + + // BASIC CONFIGURATION + &CONFIG.write().await.load(); + + // USER MANAGEMENT + if let Err(_) = user_manager::load_users().await { + log_t!("user_load_failed"); + } + + let mut sb = "".to_string(); + + for up in user_manager::get_users() { + sb = sb + "," + &up.user_id.to_string().as_str(); + } + + if !sb.is_empty() { + {} + sb.remove(0); + sb = sb + ","; + } + log!( + "IOTA ID: {}", + CONFIG.read().await.get_iota_id().to_string() + ); + log!("User IDS: {}", sb); + + // COMMUNITY MANAGEMENT + /* registry::load_interactables().await; + community_manager::load_communities().await; + community_manager::save_communities().await; + let mut sb1 = "".to_string(); + for cp in community_manager::get_communities().await { + sb1 = sb1 + "," + &cp.get_name().to_string().as_str(); + } + + if !sb1.is_empty() { + sb1.remove(0); + sb1 = sb1 + ","; + } + log!("Community IDS: {}", sb1); */ + let port = CONFIG.read().await.get_port(); + let mut ip = "0.0.0.0".to_string(); + for iface in pnet::datalink::interfaces() { + let iface: NetworkInterface = iface; + if iface.ips.len() > 0 { + let ipsv = format!("{}", iface.ips[0]); + let ips: &str = ipsv.split('/').next().unwrap_or(""); + if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") { + ip = ips.to_string(); + } + } + } + /* + if start(port).await { + log_t!("community_active", ip, port.to_string()); + } else { + if port < 1024 { + log_t!("community_start_error_admin", port.to_string()); + } else { + log_t!("community_start_error", port.to_string()); + } + } */ + if !has_dir("web") { + download_and_extract_zip( + "https://omega.tensamin.net/api/download/iota_frontend", + "web", + ) + .await; + } + let _ = omikron::omikron_connection::get_omikron_connection().await; + + log_t!("setup_completed"); + loop { + if *SHUTDOWN.read().await { + break; + } + + sleep(Duration::from_millis(100)).await; + } + if *RELOAD.read().await { + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_secs(1)).await; + } + &CONFIG.write().await.clear(); + user_manager::clear(); + /*community_manager::clear();*/ + *APP_STATE.lock().unwrap() = AppState::new(); + } + ui.terminal.lock().unwrap().clear(); + ui.terminal.lock().unwrap().flush(); + } } diff --git a/src/omega/mod.rs b/src/omega/mod.rs deleted file mode 100644 index cbe77d9..0000000 --- a/src/omega/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod omega_connection; diff --git a/src/omega/omega_connection.rs b/src/omega/omega_connection.rs deleted file mode 100644 index ab84eb2..0000000 --- a/src/omega/omega_connection.rs +++ /dev/null @@ -1,741 +0,0 @@ -use crate::{ - data::user::UserStatus, - get_private_key, log, log_cv_in, log_cv_out, log_err, log_in, - rho::rho_manager::{self, RHO_CONNECTIONS, connection_count}, - util::{ - crypto_helper::{decrypt_b64, secret_key_to_base64}, - file_util::load_file_vec, - logger::PrintType, - }, -}; -use dashmap::DashMap; -use once_cell::sync::Lazy; -use std::{collections::HashMap, env, sync::Arc, time::Duration}; -use tokio::{ - sync::{Mutex, RwLock, mpsc, watch}, - task::JoinHandle, - time::{Instant, sleep}, -}; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; -use ttp_native::{Receiver, Sender}; -use uuid::Uuid; - -// ============================================================================ -// Configuration -// ============================================================================ - -const OMEGA_HOST_DEFAULT: &str = "methanium.net"; -const OMEGA_PORT_DEFAULT: u16 = 9187; -const RECONNECT_DELAY: Duration = Duration::from_secs(5); -const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); -const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); -const TASK_MAX_AGE: Duration = Duration::from_secs(60); - -// ============================================================================ -// Waiting Task System -// ============================================================================ - -pub struct WaitingTask { - pub task: Box, CommunicationValue) -> bool + Send + Sync>, - pub inserted_at: Instant, -} - -pub static WAITING_TASKS: Lazy> = Lazy::new(DashMap::new); - -pub fn start_task_cleanup_loop() { - tokio::spawn(async { - loop { - sleep(TASK_CLEANUP_INTERVAL).await; - WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); - } - }); -} - -// ============================================================================ -// Connection State -// ============================================================================ - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected { identified: bool }, -} - -#[allow(unused_variables)] -impl ConnectionState { - pub fn is_connected(&self) -> bool { - match self { - ConnectionState::Connected { identified } => true, - _ => false, - } - } - - #[allow(dead_code)] - pub fn is_identified(&self) -> bool { - match self { - ConnectionState::Connected { identified: true } => true, - _ => false, - } - } -} - -// ============================================================================ -// Omega Connection (Client-side with auto-reconnect) -// ============================================================================ -#[allow(dead_code)] -pub struct OmegaConnection { - state: Arc>, - sender: Arc>>>, - connection_loop_handle: Arc>>>, - host: String, - port: u16, - server_cert: Vec, - last_ping: Arc>, - heartbeat_handle: Arc>>>, - message_send_times: Arc>>, - pub connection_id: Uuid, - shutdown_tx: Arc>>>, - // Track if we should reconnect on close - reconnect_on_close: Arc>, -} - -impl OmegaConnection { - pub fn new() -> Self { - Self::with_host(OMEGA_HOST_DEFAULT, OMEGA_PORT_DEFAULT) - } - - pub fn with_host(host: &str, port: u16) -> Self { - // Load server certificate from default location - let server_cert = - load_file_vec("certs", "cert.pem").expect("Failed to load server certificate"); - - Self::with_host_and_cert(host, port, server_cert) - } - - // New constructor that accepts certificate directly - pub fn with_host_and_cert(host: &str, port: u16, server_cert: Vec) -> Self { - let (shutdown_tx, _) = watch::channel(false); - - OmegaConnection { - state: Arc::new(RwLock::new(ConnectionState::Disconnected)), - sender: Arc::new(RwLock::new(None)), - connection_loop_handle: Arc::new(Mutex::new(None)), - host: host.to_string(), - port, - server_cert, - last_ping: Arc::new(Mutex::new(-1)), - heartbeat_handle: Arc::new(Mutex::new(None)), - message_send_times: Arc::new(Mutex::new(HashMap::new())), - connection_id: Uuid::new_v4(), - shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), - reconnect_on_close: Arc::new(RwLock::new(true)), - } - } - - // ------------------------------------------------------------------------- - // Connection Management - // ------------------------------------------------------------------------- - - pub async fn start(self: Arc) { - // Cancel any existing connection loop - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - // Set reconnect flag - *self.reconnect_on_close.write().await = true; - - let self_clone = self.clone(); - let handle = tokio::spawn(async move { - self_clone.connection_loop().await; - }); - - *self.connection_loop_handle.lock().await = Some(handle); - } - - #[allow(dead_code)] - pub async fn stop(&self) { - // Disable reconnection - *self.reconnect_on_close.write().await = false; - - if let Some(tx) = self.shutdown_tx.lock().await.take() { - let _ = tx.send(true); - } - - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - // Close sender if connected - if let Some(sender) = self.sender.read().await.as_ref() { - sender.close(); - } - - *self.state.write().await = ConnectionState::Disconnected; - *self.sender.write().await = None; - } - - async fn connection_loop(self: Arc) { - let mut reconnect_delay = RECONNECT_DELAY; - let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); - let mut shutdown_rx = shutdown_rx; - - loop { - if *shutdown_rx.borrow() { - log_in!(0, PrintType::Omega, "Connection loop shutting down"); - break; - } - - // Check if reconnection is enabled - if !*self.reconnect_on_close.read().await { - log_in!(0, PrintType::Omega, "Reconnection disabled, exiting loop"); - break; - } - - match self.clone().connect_once().await { - Ok(()) => { - // Connection closed gracefully, check if we should reconnect - if *self.reconnect_on_close.read().await { - log_err!( - 0, - PrintType::Omega, - "Connection lost, reconnecting in {:?}...", - reconnect_delay - ); - } else { - log_in!(0, PrintType::Omega, "Connection closed, not reconnecting"); - break; - } - } - Err(e) => { - log_err!( - 0, - PrintType::Omega, - "Connection failed: {}, retrying in {:?}...", - e, - reconnect_delay - ); - } - } - - tokio::select! { - _ = sleep(reconnect_delay) => {} - _ = shutdown_rx.changed() => { - if *shutdown_rx.borrow() { - break; - } - } - } - - reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); - } - } - - async fn connect_once(self: Arc) -> Result<(), String> { - *self.state.write().await = ConnectionState::Connecting; - - let addr_str = format!("https://{}:{}", self.host, self.port); - - let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) - .await - .map_err(|e| format!("Connection failed: {}", e))?; - - log_in!( - 0, - PrintType::Omega, - "QUIC connection established to {}", - addr_str - ); - - // Store sender - let sender_arc = Arc::new(sender); - *self.sender.write().await = Some(sender_arc.clone()); - *self.state.write().await = ConnectionState::Connected { identified: false }; - - // Get handle for close monitoring - let sender_handle = sender_arc.handle().clone(); - - // Start read loop - let read_self = self.clone(); - let read_handle = tokio::spawn(async move { - read_self.read_loop(&mut receiver, sender_handle).await; - }); - - // Send identification - self.send_identification().await; - - // Start heartbeat - let heartbeat_self = self.clone(); - let heartbeat_handle = tokio::spawn(async move { - heartbeat_self.heartbeat_loop().await; - }); - *self.heartbeat_handle.lock().await = Some(heartbeat_handle); - - // Wait for read loop to complete (connection closed) - let result = read_handle.await; - - // Cleanup - *self.sender.write().await = None; - *self.state.write().await = ConnectionState::Disconnected; - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - match result { - Ok(()) => { - // Check if we should reconnect - if *self.reconnect_on_close.read().await { - Err("Connection closed, will reconnect".to_string()) - } else { - Ok(()) - } - } - Err(e) => Err(format!("Read loop error: {}", e)), - } - } - - // ------------------------------------------------------------------------- - // Identification Handshake - // ------------------------------------------------------------------------- - - async fn send_identification(&self) { - let id = rand_u32(); - - let omikron_id = env::var("ID") - .unwrap_or("0".to_string()) - .parse::() - .unwrap_or(0); - - let identify_msg = CommunicationValue::new(CommunicationType::identification) - .with_id(id) - .add_data(DataTypes::omikron_id, DataValue::Number(omikron_id)); - - WAITING_TASKS.insert( - id, - WaitingTask { - task: Box::new(|selfc, cv| { - if cv.is_type(CommunicationType::error_not_found) { - log_err!( - 0, - PrintType::Omega, - "Identification failed: Omikron ID not found" - ); - return false; - } - if !cv.is_type(CommunicationType::challenge) { - return false; - } - - tokio::spawn(async move { - if let Err(e) = selfc.handle_challenge(cv).await { - log_err!(0, PrintType::Omega, "Challenge handling failed: {}", e); - } - }); - true - }), - inserted_at: Instant::now(), - }, - ); - - self.send_message(&identify_msg).await; - } - - async fn handle_challenge(&self, cv: CommunicationValue) -> Result<(), String> { - let challenge = cv - .get_data(DataTypes::challenge) - .as_str() - .ok_or("Challenge not found")?; - - let server_pub_key = cv - .get_data(DataTypes::public_key) - .as_str() - .ok_or("Public key not found")?; - - let decrypted_challenge = decrypt_b64( - &secret_key_to_base64(&get_private_key()), - server_pub_key, - challenge, - ) - .map_err(|e| format!("Decryption failed: {:?}", e))?; - - let response_msg = CommunicationValue::new(CommunicationType::challenge_response) - .with_id(cv.get_id()) - .add_data(DataTypes::challenge, DataValue::Str(decrypted_challenge)); - - let response_id = response_msg.get_id(); - - WAITING_TASKS.insert( - response_id, - WaitingTask { - task: Box::new(|selfc, final_cv| { - if !final_cv.is_type(CommunicationType::identification_response) { - log_err!(0, PrintType::Omega, "Expected identification_response"); - return false; - } - - let accepted = final_cv - .get_data(DataTypes::accepted) - .as_bool() - .unwrap_or(false); - - if !accepted { - log_err!(0, PrintType::Omega, "Omega did not accept identification"); - return false; - } - - tokio::spawn(async move { - let mut state = selfc.state.write().await; - if let ConnectionState::Connected { identified: _ } = *state { - *state = ConnectionState::Connected { identified: true }; - } - drop(state); - - selfc.sync_client_iota_status().await; - }); - - log!(0, PrintType::Omega, "Successfully identified with Omega"); - true - }), - inserted_at: Instant::now(), - }, - ); - - self.send_message(&response_msg).await; - Ok(()) - } - - async fn sync_client_iota_status(self: Arc) { - let mut connected_iota_ids: Vec = Vec::new(); - let mut connected_user_ids: Vec = Vec::new(); - - let rho_connections_reader = RHO_CONNECTIONS.read().await; - - for iota_id in rho_connections_reader.keys() { - connected_iota_ids.push(DataValue::Number(*iota_id)); - } - - for rho in rho_connections_reader.values() { - for client_conn in rho.get_client_connections().await { - connected_user_ids.push(DataValue::Number(client_conn.get_user_id().await as i64)); - } - } - - drop(rho_connections_reader); - - let sync_msg = CommunicationValue::new(CommunicationType::sync_client_iota_status) - .add_data(DataTypes::iota_ids, DataValue::Array(connected_iota_ids)) - .add_data(DataTypes::user_ids, DataValue::Array(connected_user_ids)) - .add_data( - DataTypes::rho_connections, - DataValue::Number(connection_count().await as i64), - ); - - self.send_message(&sync_msg).await; - } - - // ------------------------------------------------------------------------- - // Read Loop & Heartbeat - // ------------------------------------------------------------------------- - - async fn read_loop( - self: Arc, - receiver: &mut Receiver, - sender_handle: Arc, - ) { - // Monitor both receiver and sender handle for close - let mut close_rx = sender_handle.subscribe_close(); - - loop { - tokio::select! { - result = receiver.receive() => { - match result { - Ok(cv) => { - if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { - log_cv_in!(PrintType::Omega, &cv); - } - - if cv.is_type(CommunicationType::pong) || cv.is_type(CommunicationType::ping) { - self.handle_pong(&cv).await; - continue; - } - - let msg_id = cv.get_id(); - if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { - if (task.task)(self.clone(), cv.clone()) { - continue; - } - } - - if cv.is_type(CommunicationType::iota_user_data) { - if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) { - let mut user_ids: Vec = Vec::new(); - for value in users { - if let DataValue::Number(user_id) = value { - user_ids.push(*user_id as u64); - } - } - let connections = crate::rho::rho_manager::RHO_CONNECTIONS.read().await; - if let Some(iota_id) = cv.get_data(DataTypes::iota_id).as_number() { - if let Some(rho) = connections.get(&iota_id) { - rho.get_iota_connection().set_user_ids(user_ids).await; - } - } else { - for rho in connections.values() { - rho.get_iota_connection().set_user_ids(user_ids.clone()).await; - } - } - } - } - } - Err(e) => { - log_err!(0, PrintType::Omega, "Receive error: {}", e); - break; - } - } - } - _ = close_rx.changed() => { - // Connection was closed by either side - if let Some(reason) = close_rx.borrow().clone() { - log_err!(0, PrintType::Omega, "Connection closed: {:?}", reason); - } else { - log_in!(0, PrintType::Omega, "Connection closed cleanly"); - } - break; - } - } - } - } - - async fn heartbeat_loop(self: Arc) { - loop { - sleep(HEARTBEAT_INTERVAL).await; - - // Check if still connected - if !self.state.read().await.is_connected() { - break; - } - - // Check if sender is closed - if let Some(sender) = self.sender.read().await.as_ref() { - if sender.is_closed() { - log_err!(0, PrintType::Omega, "Sender closed, stopping heartbeat"); - break; - } - } else { - break; - } - - self.send_ping().await; - } - } - - async fn send_ping(&self) { - let ping = CommunicationValue::new(CommunicationType::ping).add_data( - DataTypes::send_time, - DataValue::Number( - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64, - ), - ); - self.send_message(&ping).await; - } - - async fn handle_pong(&self, cv: &CommunicationValue) { - let timestamp = cv - .get_data(DataTypes::send_time) - .as_number() - .unwrap_or_else(|| { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64 - }); - - *self.last_ping.lock().await = timestamp; - } - - // ------------------------------------------------------------------------- - // Public API - // ------------------------------------------------------------------------- - - pub async fn send_message(&self, cv: &CommunicationValue) { - if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { - log_cv_out!(PrintType::Omega, &cv); - } - - let sender_guard = self.sender.read().await; - if let Some(sender) = sender_guard.as_ref() { - // Check if closed before sending - if sender.is_closed() { - log_err!(0, PrintType::Omega, "Cannot send: connection closed"); - drop(sender_guard); - // Trigger reconnection by closing the connection state - if let Some(sender) = self.sender.write().await.take() { - sender.close(); - } - return; - } - - let sender_clone = Arc::clone(sender); - drop(sender_guard); - - if let Err(e) = sender_clone.send(cv).await { - log_err!(0, PrintType::Omega, "Send failed: {}", e); - } - } else { - log_err!(0, PrintType::Omega, "Cannot send: not connected"); - } - } - - pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { - if self.state.read().await.is_connected() { - return Ok(()); - } - - let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); - let start = Instant::now(); - - loop { - if self.state.read().await.is_connected() { - return Ok(()); - } - - if start.elapsed() >= timeout { - return Err(format!( - "Connection not established within {} seconds", - timeout.as_secs() - )); - } - - sleep(Duration::from_millis(100)).await; - } - } - - pub async fn await_response( - &self, - cv: &CommunicationValue, - timeout_duration: Option, - ) -> Result { - self.await_connection(timeout_duration).await?; - - let (tx, mut rx) = mpsc::channel(1); - let msg_id = cv.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new(move |_, response_cv| { - let inner_tx = tx.clone(); - tokio::spawn(async move { - let _ = inner_tx.send(response_cv).await; - }); - true - }), - inserted_at: Instant::now(), - }, - ); - - self.send_message(cv).await; - - let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); - - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response_cv)) => Ok(response_cv), - Ok(_) => Err("Channel closed".to_string()), - Err(_) => { - WAITING_TASKS.remove(&msg_id); - Err("Request timed out".to_string()) - } - } - } - - #[allow(dead_code)] - pub async fn is_connected(&self) -> bool { - self.state.read().await.is_connected() - } - #[allow(dead_code)] - pub async fn is_identified(&self) -> bool { - self.state.read().await.is_identified() - } - - #[allow(dead_code)] - pub async fn close_iota(iota_id: i64) { - let cv = CommunicationValue::new(CommunicationType::iota_disconnected) - .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); - OMEGA_CONNECTION.send_message(&cv).await; - } - - pub async fn client_changed(_iota_id: i64, user_id: i64, state: UserStatus) { - let msg_type = match state { - UserStatus::iota_offline => CommunicationType::user_disconnected, - UserStatus::user_offline => CommunicationType::user_disconnected, - _ => CommunicationType::user_connected, - }; - - let cv = CommunicationValue::new(msg_type) - .add_data(DataTypes::user_id, DataValue::Number(user_id)); - OMEGA_CONNECTION.send_message(&cv).await; - } - - pub async fn user_states(user_id: i64, user_ids: Vec) { - let user_ids = user_ids.iter().map(|v| DataValue::Number(*v)).collect(); - - let cv = CommunicationValue::new(CommunicationType::get_states) - .add_data(DataTypes::user_ids, DataValue::Array(user_ids)); - let msg_id = cv.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new( - move |_: Arc, response: CommunicationValue| { - tokio::spawn(async move { - let rho = rho_manager::get_rho_con_for_user(user_id).await; - if let Some(rho) = rho { - for client in rho.get_client_connections_for_user(user_id).await { - client.send_message(&response).await; - } - } - }); - true - }, - ), - inserted_at: Instant::now(), - }, - ); - - OMEGA_CONNECTION.send_message(&cv).await; - } -} - -// ============================================================================ -// Global Instance -// ============================================================================ - -static OMEGA_CONNECTION: Lazy> = Lazy::new(|| { - let conn = Arc::new(OmegaConnection::new()); - - // Start the connection manager immediately - let conn_clone = conn.clone(); - tokio::spawn(async move { - conn_clone.start().await; - }); - - start_task_cleanup_loop(); - - conn -}); - -pub fn get_omega_connection() -> Arc { - OMEGA_CONNECTION.clone() -} diff --git a/src/omega/ping_pong_task.rs b/src/omega/ping_pong_task.rs deleted file mode 100644 index c7fc0b9..0000000 --- a/src/omega/ping_pong_task.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::time::Duration; -use tokio::time::Instant; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; - -use crate::omega::omega_connection::OmegaConnection; - -const PING_TIMEOUT: Duration = Duration::from_secs(30); - -impl OmegaConnection { - pub async fn send_ping(&self) { - let id = rand_u32(); - let send_time = Instant::now(); - - let mut message_send_times = self.message_send_times.lock().await; - message_send_times.retain(|_uuid, time| time.elapsed() < PING_TIMEOUT); - message_send_times.insert(id as i64, send_time); - - self.send_ping_message(id).await; - } - - pub async fn send_ping_message(&self, id: u32) { - let ping_message = CommunicationValue::new(CommunicationType::ping) - .with_id(id) - .add_data( - DataTypes::last_ping, - DataValue::Number(self.last_ping.lock().await.unwrap()), - ); - - self.send_message(&ping_message).await; - } - - /// Handles incoming pong and calculates latency - pub async fn handle_pong(&self, cv: &CommunicationValue, _log: bool) { - let id = cv.get_id(); - let mut message_send_times = self.message_send_times.lock().await; - if let Some(send_time) = message_send_times.remove(&(id as i64)) { - let ping = Instant::now().duration_since(send_time).as_millis() as i64; - *self.last_ping.lock().await = ping; - } - } -} diff --git a/src/omikron/mod.rs b/src/omikron/mod.rs new file mode 100644 index 0000000..acb0f59 --- /dev/null +++ b/src/omikron/mod.rs @@ -0,0 +1,2 @@ +pub mod omikron_connection; +pub mod ping_pong_task; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs new file mode 100755 index 0000000..7e92269 --- /dev/null +++ b/src/omikron/omikron_connection.rs @@ -0,0 +1,1159 @@ +use crate::users::contact::Contact; +use crate::util::chat_files::{MessageState, change_message_state}; +use crate::util::chats_util::{get_user, mod_user}; +use crate::util::communities_util::CommunitiesUtil; +use crate::util::crypto_util::{DataFormat, SecurePayload}; +use crate::util::file_util::{get_children, load_file, save_file}; +use crate::util::{chat_files, chats_util}; +use crate::util::{config_util::CONFIG, crypto_helper}; +use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv_in, log_cv_out, log_t}; +use dashmap::DashMap; +use json::JsonValue; +use std::collections::HashMap; +use std::sync::{Arc, LazyLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use tokio::sync::{Mutex, RwLock, mpsc, watch}; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use ttp_native::{Receiver, Sender}; +use uuid::Uuid; + +// ============================================================================ +// Configuration +// ============================================================================ + +const OMIKRON_HOST_DEFAULT: &str = "methanium.net"; +const OMIKRON_PORT_DEFAULT: u16 = 959; +const RECONNECT_DELAY: Duration = Duration::from_secs(5); +const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); +const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); +const TASK_MAX_AGE: Duration = Duration::from_secs(60); + +// ============================================================================ +// Waiting Task System +// ============================================================================ + +pub struct WaitingTask { + pub task: Box, CommunicationValue) -> bool + Send + Sync>, + pub inserted_at: Instant, +} + +pub static WAITING_TASKS: LazyLock> = LazyLock::new(|| DashMap::new()); + +pub fn start_task_cleanup_loop() { + tokio::spawn(async { + loop { + sleep(TASK_CLEANUP_INTERVAL).await; + WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); + } + }); +} + +// ============================================================================ +// Connection State +// ============================================================================ + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ConnectionState { + Disconnected, + Connecting, + Connected { identified: bool }, +} + +impl ConnectionState { + pub fn is_connected(&self) -> bool { + matches!(self, ConnectionState::Connected { .. }) + } + + pub fn is_identified(&self) -> bool { + matches!(self, ConnectionState::Connected { identified: true }) + } +} + +// ============================================================================ +// Omikron Connection (Client-side with auto-reconnect) +// ============================================================================ + +pub struct OmikronConnection { + state: Arc>, + sender: Arc>>>, + connection_loop_handle: Arc>>>, + host: String, + port: u16, + pub last_ping: Arc>, + heartbeat_handle: Arc>>>, + message_send_times: Arc>>, + pub connection_id: Uuid, + shutdown_tx: Arc>>>, + reconnect_on_close: Arc>, +} + +impl OmikronConnection { + pub fn new() -> Self { + Self::with_host(OMIKRON_HOST_DEFAULT, OMIKRON_PORT_DEFAULT) + } + + pub fn with_host(host: &str, port: u16) -> Self { + let (shutdown_tx, _) = watch::channel(false); + + OmikronConnection { + state: Arc::new(RwLock::new(ConnectionState::Disconnected)), + sender: Arc::new(RwLock::new(None)), + connection_loop_handle: Arc::new(Mutex::new(None)), + host: host.to_string(), + port, + last_ping: Arc::new(Mutex::new(-1)), + heartbeat_handle: Arc::new(Mutex::new(None)), + message_send_times: Arc::new(Mutex::new(HashMap::new())), + connection_id: Uuid::new_v4(), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + reconnect_on_close: Arc::new(RwLock::new(true)), + } + } + + // ------------------------------------------------------------------------- + // Connection Management + // ------------------------------------------------------------------------- + + pub async fn connect(self: &Arc) { + if self.connection_loop_handle.lock().await.is_none() { + self.clone().start().await; + } + } + + pub async fn start(self: Arc) { + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + *self.reconnect_on_close.write().await = true; + + let self_clone = self.clone(); + let handle = tokio::spawn(async move { + self_clone.connection_loop().await; + }); + + *self.connection_loop_handle.lock().await = Some(handle); + } + + pub async fn stop(&self) { + *self.reconnect_on_close.write().await = false; + + if let Some(tx) = self.shutdown_tx.lock().await.take() { + let _ = tx.send(true); + } + + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + if let Some(sender) = self.sender.read().await.as_ref() { + sender.close(); + } + + *self.state.write().await = ConnectionState::Disconnected; + *self.sender.write().await = None; + } + + async fn connection_loop(self: Arc) { + let mut reconnect_delay = RECONNECT_DELAY; + let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); + let mut shutdown_rx = shutdown_rx; + + loop { + if *shutdown_rx.borrow() || *SHUTDOWN.read().await { + log_t!("omikron_connection_loop_shutdown"); + break; + } + + if !*self.reconnect_on_close.read().await { + break; + } + + match self.clone().connect_once().await { + Ok(()) => { + if *self.reconnect_on_close.read().await { + log!("Connection lost, reconnecting in {:?}...", reconnect_delay); + } else { + break; + } + } + Err(e) => { + log!( + "Connection failed: {}, retrying in {:?}...", + e, + reconnect_delay + ); + } + } + + tokio::select! { + _ = sleep(reconnect_delay) => {} + _ = shutdown_rx.changed() => { + if *shutdown_rx.borrow() { + break; + } + } + } + + reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); + } + } + + async fn connect_once(self: Arc) -> Result<(), String> { + *self.state.write().await = ConnectionState::Connecting; + log_t!("omikron_connecting"); + + let addr_str = format!("https://{}:{}/ws/iota/", self.host, self.port); + + let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) + .await + .map_err(|e| format!("Connection failed: {}", e))?; + + log_t!("omikron_connection_success"); + + let sender_arc = Arc::new(sender); + *self.sender.write().await = Some(sender_arc.clone()); + *self.state.write().await = ConnectionState::Connected { identified: false }; + + // Handle registration/identification + self.handle_authentication().await; + + // Start read loop + let read_self = self.clone(); + let read_handle = tokio::spawn(async move { + read_self.read_loop(&mut receiver).await; + }); + + // Start heartbeat + let heartbeat_self = self.clone(); + let heartbeat_handle = tokio::spawn(async move { + heartbeat_self.heartbeat_loop().await; + }); + *self.heartbeat_handle.lock().await = Some(heartbeat_handle); + + { + ACTIVE_TASKS.insert("Omikron Listener".to_string()); + } + + // Wait for read loop to complete + let result = read_handle.await; + *self.sender.write().await = None; + *self.state.write().await = ConnectionState::Disconnected; + { + ACTIVE_TASKS.remove("Omikron Listener"); + } + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + match result { + Ok(()) => { + if *self.reconnect_on_close.read().await { + Err("Connection closed, will reconnect".to_string()) + } else { + Ok(()) + } + } + Err(e) => Err(format!("Read loop error: {}", e)), + } + } + + // ------------------------------------------------------------------------- + // Authentication (Registration/Identification) + // ------------------------------------------------------------------------- + + async fn handle_authentication(&self) { + let conf = CONFIG.read().await; + let iota_id = conf.get_iota_id(); + let public_key = conf.get_public_key(); + let private_key = conf.get_private_key(); + drop(conf); + + if iota_id == 0 || public_key.is_none() || private_key.is_none() { + log_t!("iota_register_new"); + + let key_pair = crypto_helper::generate_keypair(); + let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); + let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); + + let mut conf_write = CONFIG.write().await; + // NOTE: + // Intentionally not storing the generated private/public keys directly into the + // config file here to avoid persisting sensitive material in plaintext. If you + // want to persist them, uncomment the two lines below and accept the security + // implications (they will be saved by `conf_write.update()`). + // conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); + // conf_write.change("private_key", DataValue::Str(private_key_base64)); + conf_write.update(); + drop(conf_write); + + let register_msg = CommunicationValue::new(CommunicationType::register_iota) + .add_data(DataTypes::public_key, DataValue::Str(public_key_base64)); + + let msg_id = register_msg.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new(|selfc, cv| { + if !cv.is_type(CommunicationType::success) { + return false; + } + + let iota_value = cv.get_data(DataTypes::register_id); + let iota_id = iota_value.as_number().unwrap_or(0); + + if iota_id != 0 { + tokio::spawn(async move { + let mut conf_write = CONFIG.write().await; + conf_write.change("iota_id", JsonValue::from(iota_id)); + conf_write.update(); + drop(conf_write); + log!("Registered with Iota-ID: {}", iota_id); + + // Send identification after registration + let identify_msg = + CommunicationValue::new(CommunicationType::identification) + .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); + selfc.send_message(&identify_msg).await; + }); + } else { + log!("Iota registration failed."); + } + true + }), + inserted_at: Instant::now(), + }, + ); + + self.send_message(®ister_msg).await; + } else { + let identify_msg = CommunicationValue::new(CommunicationType::identification) + .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); + self.send_message(&identify_msg).await; + } + } + + // ------------------------------------------------------------------------- + // Read Loop & Heartbeat + // ------------------------------------------------------------------------- + + async fn read_loop(self: Arc, receiver: &mut Receiver) { + loop { + let result = receiver.receive().await; + match result { + Ok(cv) => { + self.clone().handle_message(cv).await; + } + Err(e) => { + self.fail_all_waiting_tasks(format!( + "Connection receive error: {} (connection_id={})", + e, self.connection_id + )) + .await; + break; + } + } + if !receiver.is_open() { + self.fail_all_waiting_tasks(format!( + "Connection closed (connection_id={}, receiver_open=false)", + self.connection_id + )) + .await; + break; + } + } + } + + async fn heartbeat_loop(self: Arc) { + loop { + sleep(HEARTBEAT_INTERVAL).await; + + if !self.state.read().await.is_connected() { + break; + } + + if let Some(sender) = self.sender.read().await.as_ref() { + if !sender.is_open() { + break; + } + } else { + break; + } + + self.send_ping().await; + } + } + + // ------------------------------------------------------------------------- + // Message Handling (Preserved from original) + // ------------------------------------------------------------------------- + + pub async fn handle_message(self: Arc, cv: CommunicationValue) { + if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { + log_cv_in!(&cv); + } + + let msg_id = cv.get_id(); + + // Dispatch waiting task for this message id + if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { + if (task.task)(self.clone(), cv.clone()) { + return; + } + } + + if cv.is_type(CommunicationType::pong) { + self.handle_pong(&cv).await; + return; + } + + if cv.is_type(CommunicationType::challenge) { + self.handle_challenge(&cv).await; + return; + } + + if cv.is_type(CommunicationType::identification_response) { + if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() { + let mut state = self.state.write().await; + if let ConnectionState::Connected { identified: _ } = *state { + *state = ConnectionState::Connected { identified: true }; + } + } + return; + } + + // ************************************************ // + // Direct messages // + // ************************************************ // + + if cv.is_type(CommunicationType::message_state) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + // Parse send_time robustly: accept numeric or string, fallback to current time + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp_i64 = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + + let _ = chat_files::change_message_state( + timestamp_i64, + *receiver_id as i64, + *sender_id as i64, + MessageState::from_str( + cv.get_data(DataTypes::message_state).as_str().unwrap_or(""), + ), + ); + } + + // Incoming stored message: store for the recipient, attempt local delivery, notify sender. + if cv.is_type(CommunicationType::message_send) { + let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse receiver_id (the storage owner for this incoming message) + let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() + { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse send_time robustly (number or string), fallback to now + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp_i64 = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + let timestamp_u128 = timestamp_i64 as u128; + + // content may be missing; default to empty string + let content = cv + .get_data(DataTypes::content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + + // persist message for the receiver (storage_owner = receiver_id) + chat_files::add_message( + timestamp_u128, + false, + receiver_id as i64, + sender_id as i64, + &content, + height, + ); + + // persist message for the sender (storage_owner = sender_id) + chat_files::add_message( + timestamp_u128, + true, + sender_id as i64, + receiver_id as i64, + &content, + height, + ); + + // send confirmation back to sender + let conf_msg = CommunicationValue::new(CommunicationType::message_send) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64); + self.send_message(&conf_msg).await; + + // Build a live-delivery message for the local client (recipient) + let user_forward = CommunicationValue::new(CommunicationType::message_live) + .with_id(cv.get_id()) + .with_receiver(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); + + // Attempt delivery and await a response from the local client + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataTypes::message_state) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + // update stored message state for receiver + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + ms.clone(), + ); + + // update stored message state for sender + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + ms.clone(), + ); + + // notify original sender about the delivered/read state + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery failed or timed out; mark as Sent + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + MessageState::Sent, + ); + + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + MessageState::Sent, + ); + + // notify sender + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + + if cv.is_type(CommunicationType::message_other_iota) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + // parse send_time safely (number or string), fallback to now + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + + // content may be missing or non-string; default to empty string + let content = cv + .get_data(DataTypes::content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + + chat_files::add_message( + timestamp as u128, + false, + *receiver_id as i64, + *sender_id as i64, + &content, + height, + ); + + // Build user_forward using the parsed numeric timestamp and safe content string + let user_forward = CommunicationValue::new(CommunicationType::message_live) + .with_id(cv.get_id()) + .with_receiver(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); + + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataTypes::message_state) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + let _ = change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + ms.clone(), + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data( + DataTypes::message_state, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery timed out/failed — update stored state and notify sender with numeric timestamp + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data( + DataTypes::message_state, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + + if cv.is_type(CommunicationType::messages_get) { + let my_id = cv.get_sender(); + let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); + let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); + let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); + let mut msg_array: Vec = Vec::new(); + for m in messages.members() { + let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); + let content: String = m["content"].as_str().unwrap_or("").to_string(); + let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false); + let height: i64 = m["height"].as_i64().unwrap_or(0); + let sender_id: i64 = if sent_by_self { + my_id as i64 + } else { + if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { + s.parse::().unwrap_or(partner_id as i64) + } else { + partner_id as i64 + } + }; + let message_state: String = m["message_state"].as_str().unwrap_or("").to_string(); + + let mut container = Vec::new(); + container.push((DataTypes::send_time, DataValue::Number(message_time))); + container.push((DataTypes::content, DataValue::Str(content))); + container.push((DataTypes::sender_id, DataValue::Number(sender_id))); + container.push((DataTypes::message_state, DataValue::Str(message_state))); + container.push((DataTypes::height, DataValue::Number(height))); + container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self))); + msg_array.push(DataValue::Container(container)); + } + + let resp = CommunicationValue::new(CommunicationType::messages_get) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::messages, DataValue::Array(msg_array)); + + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::get_chats) { + let user_id = cv.get_sender(); + let users = chats_util::get_users(user_id as i64); + let mut user_array = Vec::new(); + for user in users { + let mut container = Vec::new(); + container.push((DataTypes::user_id, DataValue::Number(user.user_id))); + if let Some(name) = user.user_name { + container.push((DataTypes::username, DataValue::Str(name))); + } + if let Some(ts) = user.last_message_at { + container.push((DataTypes::last_message_at, DataValue::Number(ts))); + } + user_array.push(DataValue::Container(container)); + } + let resp = CommunicationValue::new(CommunicationType::get_chats) + .with_id(cv.get_id()) + .with_receiver(user_id) + .add_data(DataTypes::user_ids, DataValue::Array(user_array)); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::add_conversation) { + let user_id = cv.get_sender(); + let other_id = match cv.get_data(DataTypes::chat_partner_id).as_number() { + Some(n) => n as i64, + None => cv + .get_data(DataTypes::chat_partner_id) + .as_str() + .unwrap_or("0") + .parse() + .unwrap_or(0), + }; + let mut contact = get_user(user_id as i64, other_id).unwrap_or(Contact::new(other_id)); + + if let Some(name) = cv.get_data(DataTypes::chat_partner_name).as_str() { + contact.user_name = Some(name.to_string()); + } + + contact.set_last_message_at( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + ); + mod_user(user_id as i64, &contact); + let resp = CommunicationValue::new(CommunicationType::add_conversation) + .with_id(cv.get_id()) + .with_receiver(user_id); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::add_community) { + CommunitiesUtil::add_community( + cv.get_sender() as i64, + cv.get_data(DataTypes::community_address) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataTypes::community_title) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataTypes::position) + .as_str() + .unwrap() + .to_string(), + ); + let resp = CommunicationValue::new(CommunicationType::add_community) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::get_communities) { + let mut comm_array = Vec::new(); + for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { + let mut container: Vec<(DataTypes, DataValue)> = Vec::new(); + if let Some(address) = c["address"].as_str() { + container.push(( + DataTypes::community_address, + DataValue::Str(address.to_string()), + )); + } + if let Some(title) = c["title"].as_str() { + container.push(( + DataTypes::community_title, + DataValue::Str(title.to_string()), + )); + } + if let Some(position) = c["position"].as_str() { + container.push((DataTypes::position, DataValue::Str(position.to_string()))); + } + comm_array.push(DataValue::Container(container)); + } + + let resp = CommunicationValue::new(CommunicationType::get_communities) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) + .add_data(DataTypes::communities, DataValue::Array(comm_array)); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::remove_community) { + CommunitiesUtil::remove_community( + cv.get_sender() as i64, + cv.get_data(DataTypes::community_address) + .as_str() + .unwrap() + .to_string(), + ); + let resp = CommunicationValue::new(CommunicationType::remove_community) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::settings_save) { + let my_id = cv.get_sender(); + let settings_name = cv.get_data(DataTypes::settings_name).as_str().unwrap(); + let settings_value = cv.get_data(DataTypes::payload).as_str().unwrap(); + + save_file( + &format!("users/{}/settings/", my_id), + &format!("{}.settings", settings_name), + &settings_value, + ); + + let response = CommunicationValue::new(CommunicationType::settings_save) + .with_receiver(my_id) + .with_id(cv.get_id()); + + self.send_message(&response).await; + return; + } + + if cv.is_type(CommunicationType::settings_load) { + let my_id = cv.get_sender(); + let settings_name = cv.get_data(DataTypes::settings_name).as_string().unwrap(); + let settings_value_str = load_file( + &format!("users/{}/settings/", my_id), + &format!("{}.settings", settings_name), + ); + let response = CommunicationValue::new(CommunicationType::settings_load) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::payload, DataValue::Str(settings_value_str)) + .add_data(DataTypes::settings_name, DataValue::Str(settings_name)); + + self.send_message(&response).await; + return; + } + + if cv.is_type(CommunicationType::settings_list) { + let my_id = cv.get_sender(); + let settings = get_children(&format!("users/{}/settings/", my_id)); + let mut settings_json = Vec::new(); + for s in settings { + let s = s.replace(".settings", ""); + if s.is_empty() { + continue; + } + let _ = settings_json.push(DataValue::Str(s)); + } + let response = CommunicationValue::new(CommunicationType::settings_list) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::settings, DataValue::Array(settings_json)); + + self.send_message(&response).await; + return; + } + } + + async fn handle_challenge(&self, cv: &CommunicationValue) { + let conf = CONFIG.read().await; + let private_key = conf.get_private_key().unwrap(); + drop(conf); + + let omikron_public_key = cv.get_data(DataTypes::public_key).as_str().unwrap(); + let encrypted_challenge = cv.get_data(DataTypes::challenge).as_str().unwrap(); + + let solved_challenge = { + if let Ok(decrypted) = SecurePayload::new( + encrypted_challenge, + DataFormat::Base64, + crypto_helper::load_secret_key(&private_key).unwrap(), + ) { + if let Ok(decrypted) = decrypted + .decrypt_x448(crypto_helper::load_public_key(omikron_public_key).unwrap()) + { + Some(decrypted) + } else { + None + } + } else { + None + } + }; + + if let Some(decrypted) = solved_challenge { + let solved = decrypted.export(DataFormat::Raw); + + let response = CommunicationValue::new(CommunicationType::challenge_response) + .with_id(cv.get_id()) + .add_data(DataTypes::challenge, DataValue::Str(solved)); + + self.send_message(&response).await; + } + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + pub async fn send_message(&self, cv: &CommunicationValue) { + if let Err(err) = self.send_message_result(cv).await { + log_t!("send_message_failed", err); + } + } + + async fn send_message_result(&self, cv: &CommunicationValue) -> Result<(), String> { + let sender_guard = self.sender.read().await; + if let Some(sender) = sender_guard.as_ref() { + if !sender.is_open() { + drop(sender_guard); + if let Some(sender) = self.sender.write().await.take() { + sender.close(); + } + self.fail_all_waiting_tasks(format!( + "Send failed: connection closed (connection_id={})", + self.connection_id + )) + .await; + return Err("connection closed".to_string()); + } + + let sender_clone = Arc::clone(sender); + drop(sender_guard); + + if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { + log_cv_out!(&cv); + } + + if let Err(e) = sender_clone.send(cv).await { + self.fail_all_waiting_tasks(format!( + "Send failed: {} (connection_id={})", + e, self.connection_id + )) + .await; + return Err(e.to_string()); + } + + Ok(()) + } else { + Err("not connected".to_string()) + } + } + + async fn fail_all_waiting_tasks(&self, reason: String) { + let keys: Vec = WAITING_TASKS.iter().map(|entry| *entry.key()).collect(); + + for key in keys { + if let Some((_, waiting_task)) = WAITING_TASKS.remove(&key) { + let response = CommunicationValue::new(CommunicationType::error) + .with_id(key) + .add_data(DataTypes::message, DataValue::Str(reason.clone())); + let _ = (waiting_task.task)(OMIKRON_CONNECTION.clone(), response); + } + } + } + + pub async fn is_connected(&self) -> bool { + self.state.read().await.is_connected() + } + + pub async fn is_identified(&self) -> bool { + self.state.read().await.is_identified() + } + + pub async fn await_response( + &self, + cv: &CommunicationValue, + timeout_duration: Option, + ) -> Result { + let (tx, mut rx) = mpsc::channel(1); + let msg_id = cv.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new(move |_, response_cv| { + let inner_tx = tx.clone(); + tokio::spawn(async move { + let _ = inner_tx.send(response_cv).await; + }); + true + }), + inserted_at: Instant::now(), + }, + ); + + if let Err(send_err) = self.send_message_result(cv).await { + WAITING_TASKS.remove(&msg_id); + return Err(format!( + "Request send failed (msg_id={}, reason={})", + msg_id, send_err + )); + } + + let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); + + match tokio::time::timeout(timeout, rx.recv()).await { + Ok(Some(response_cv)) => { + if response_cv.is_type(CommunicationType::error) { + let reason = response_cv + .get_data(DataTypes::message) + .as_str() + .unwrap_or("connection error") + .to_string(); + Err(format!( + "Request failed due to disconnect (msg_id={}, reason={})", + msg_id, reason + )) + } else { + Ok(response_cv) + } + } + Ok(_) => { + WAITING_TASKS.remove(&msg_id); + Err("Channel closed while awaiting response".to_string()) + } + Err(_) => { + let waiting_tasks_len = WAITING_TASKS.len(); + WAITING_TASKS.remove(&msg_id); + Err(format!( + "Request timed out (msg_id={}, timeout={}s, connected={}, waiting_tasks={})", + msg_id, + timeout.as_secs(), + self.is_connected().await, + waiting_tasks_len + )) + } + } + } + + pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { + if self.state.read().await.is_connected() { + return Ok(()); + } + + let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); + let start = Instant::now(); + + loop { + if self.state.read().await.is_connected() { + return Ok(()); + } + + if start.elapsed() >= timeout { + return Err(format!( + "Connection not established within {} seconds", + timeout.as_secs() + )); + } + + sleep(Duration::from_millis(100)).await; + } + } +} + +// ============================================================================ +// Global Instance +// ============================================================================ + +pub static OMIKRON_CONNECTION: LazyLock> = LazyLock::new(|| { + let conn = Arc::new(OmikronConnection::new()); + + start_task_cleanup_loop(); + + conn +}); + +pub async fn get_omikron_connection() -> Arc { + let conn = OMIKRON_CONNECTION.clone(); + + conn.connect().await; + conn +} diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs new file mode 100644 index 0000000..f1a891f --- /dev/null +++ b/src/omikron/ping_pong_task.rs @@ -0,0 +1,38 @@ +use crate::omikron::omikron_connection::OmikronConnection; +use crate::{APP_STATE, log}; +use dashmap::DashMap; +use std::sync::LazyLock; +use std::time::Instant; +use tokio::time::Duration; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; + +static PING_TIMES: LazyLock> = LazyLock::new(|| DashMap::new()); + +impl OmikronConnection { + pub async fn send_ping(&self) { + let id = rand_u32(); + + PING_TIMES.insert(id, Instant::now()); + + PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30)); + + let ping_message = CommunicationValue::new(CommunicationType::ping) + .with_id(id) + .add_data( + DataTypes::last_ping, + DataValue::Array(vec![DataValue::Number(*self.last_ping.lock().await)]), + ); + + self.send_message(&ping_message).await; + } + + pub async fn handle_pong(&self, cv: &CommunicationValue) { + let id = cv.get_id(); + + if let Some((_, send_time)) = PING_TIMES.remove(&id) { + let ping_ms = Instant::now().duration_since(send_time).as_millis() as i64; + *self.last_ping.lock().await = ping_ms; + APP_STATE.lock().unwrap().push_ping_val(ping_ms as f64); + } + } +} diff --git a/src/rho/client_connection.rs b/src/rho/client_connection.rs deleted file mode 100644 index ebaabf2..0000000 --- a/src/rho/client_connection.rs +++ /dev/null @@ -1,583 +0,0 @@ -use crate::anonymous_clients::anonymous_manager; -use crate::calls::{call_manager, call_util}; -use crate::omega::omega_connection::get_omega_connection; -use crate::rho::connection::GeneralConnection; -use crate::rho::{rho_connection::RhoConnection, rho_manager}; -use crate::util::logger::PrintType; -use crate::{data::user::UserStatus, omega::omega_connection::OmegaConnection}; -use crate::{log_cv_in, log_cv_out, log_err, log_in, log_out}; -use std::str::FromStr; -use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tokio::sync::RwLock; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use ttp_native::{Receiver, Sender}; -use uuid::Uuid; - -pub struct ClientConnection { - pub user_id: u64, - - pub sender: Arc, - pub receiver: Arc, - pub ping: Arc>, - pub_key: Arc>>>, - pub rho_connection: Arc>>>, - pub interested_users: Arc>>, - is_open: Arc>, -} - -impl ClientConnection { - pub async fn from_general(general: Arc, user_id: u64) -> Arc { - Arc::new(Self { - ping: Arc::new(RwLock::new(0)), - pub_key: Arc::new(RwLock::new(None)), - rho_connection: general.rho_connection.clone(), - interested_users: Arc::new(RwLock::new(Vec::new())), - is_open: Arc::new(RwLock::new(true)), - sender: general.sender.clone(), - receiver: general.receiver.clone(), - user_id: user_id, - }) - } - pub fn start(self: Arc) { - let self_clone = self.clone(); - tokio::spawn(async move { - while let Ok(cv) = self_clone.receiver.receive().await { - self_clone.clone().handle_message(cv).await; - } - self_clone.handle_close().await; - }); - - let self_clone2 = self.clone(); - tokio::spawn(async move { - tokio::time::sleep(Duration::from_millis(50)).await; - if self_clone2.get_rho_connection().await.is_none() { - self_clone2 - .send_error_response(0, CommunicationType::error) - .await; - } - }); - } - - /// Get the user ID - pub async fn get_user_id(&self) -> u64 { - self.user_id - } - - /// Get current ping - pub async fn get_ping(&self) -> i64 { - *self.ping.read().await - } - - /// Get RhoConnection if available - pub async fn get_rho_connection(&self) -> Option> { - self.rho_connection.read().await.clone() - } - - /// Send a CommunicationValue to the client - pub async fn send_message(self: Arc, cv: &CommunicationValue) { - if !*self.is_open.read().await { - log_out!( - self.user_id as i64, - PrintType::Client, - "Attempted to send message to a closed connection." - ); - return; - } - if !cv.is_type(CommunicationType::pong) && !cv.is_type(CommunicationType::ping) { - log_cv_out!(PrintType::Client, &cv); - } - let _ = self.sender.send(&cv).await; - } - - /// Handle incoming message from client - pub async fn handle_message(self: Arc, cv: CommunicationValue) { - tokio::spawn(async move { - if cv.is_type(CommunicationType::ping) { - self.handle_ping(cv).await; - return; - } - log_cv_in!(PrintType::Client, cv); - - // Handle client status changes - if cv.is_type(CommunicationType::client_changed) { - self.handle_client_changed(cv).await; - return; - } - - // Handle call invites - if cv.is_type(CommunicationType::call_invite) { - self.handle_call_invite(cv).await; - return; - } - - // Handle get call requests - if cv.is_type(CommunicationType::call_token) { - self.handle_get_call(cv).await; - return; - } - - if cv.is_type(CommunicationType::call_disconnect_user) { - self.handle_call_disconnect_user(cv).await; - return; - } - - if cv.is_type(CommunicationType::call_timeout_user) { - self.handle_call_timeout_user(cv).await; - return; - } - - if cv.is_type(CommunicationType::call_set_anonymous_joining) { - self.handle_call_set_anonymous_joining(cv).await; - return; - } - if cv.is_type(CommunicationType::get_user_data) { - if let Some(anonymous) = { - if let Some(user_id) = cv.get_data(DataTypes::user_id).as_number() { - anonymous_manager::get_anonymous_user(user_id as u64).await - } else if let Some(username) = cv.get_data(DataTypes::username).as_str() { - anonymous_manager::get_anonymous_user_by_name(username.to_string()).await - } else { - None - } - } { - let response = CommunicationValue::new(CommunicationType::get_user_data) - .with_id(cv.get_id()) - .add_data( - DataTypes::username, - DataValue::Str(anonymous.get_user_name().await), - ) - .add_data( - DataTypes::user_id, - DataValue::Number(anonymous.get_user_id() as i64), - ) - .add_data( - DataTypes::display, - DataValue::Str(anonymous.get_display_name().await), - ) - .add_data( - DataTypes::avatar, - DataValue::Str(anonymous.get_avatar().await), - ) - .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); - - self.send_message(&response).await; - - return; - } - } - - if cv.is_type(CommunicationType::change_user_data) - || cv.is_type(CommunicationType::read_notification) - || cv.is_type(CommunicationType::get_notifications) - || cv.is_type(CommunicationType::get_user_data) - || cv.is_type(CommunicationType::get_iota_data) - || cv.is_type(CommunicationType::delete_user) - { - let sender = self.get_user_id().await; - self.handle_omega_forward(cv.with_sender(sender as u64)) - .await; - return; - } - // Forward other messages to Iota - self.forward_to_iota(cv).await; - }); - } - async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { - let client_for_closure = self.clone(); - tokio::spawn(async move { - let response_cv = get_omega_connection() - .await_response(&cv.with_sender(self.user_id), Some(Duration::from_secs(20))) - .await; - if let Ok(response_cv) = response_cv { - client_for_closure.send_message(&response_cv).await; - } - }); - } - - /// Handle ping message - async fn handle_ping(self: Arc, cv: CommunicationValue) { - // Update our ping if provided - if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { - let current = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - let mut ping_guard = self.ping.write().await; - *ping_guard = current as i64 - last_ping; - } - - // Get Iota ping from RhoConnection - let iota_ping = if let Some(rho_conn) = self.get_rho_connection().await { - rho_conn.get_iota_connection().get_ping().await - } else { - -1 - }; - - // Send pong response - let response = CommunicationValue::new(CommunicationType::pong) - .with_id(cv.get_id()) - .add_data(DataTypes::ping_iota, DataValue::Number(iota_ping)); - - self.send_message(&response).await; - } - - /// Handle client status change - async fn handle_client_changed(self: Arc, cv: CommunicationValue) { - let user_id = self.get_user_id().await; - if let DataValue::Str(_status_str) = cv.get_data(DataTypes::user_state) { - let user_status = UserStatus::user_online; - if let Some(rho_conn) = self.get_rho_connection().await { - OmegaConnection::client_changed( - rho_conn.get_iota_id().await as i64, - user_id as i64, - user_status, - ) - .await; - } - } - } - - /// Handle call invite - async fn handle_call_invite(self: Arc, cv: CommunicationValue) { - let receiver_id: i64 = cv.get_data(DataTypes::receiver_id).as_number().unwrap_or(0); - if receiver_id == 0 { - self.send_error_response(cv.get_id(), CommunicationType::error_no_user_id) - .await; - return; - } - - let call_id = match cv.get_data(DataTypes::call_id) { - DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) { - Ok(id) => id, - Err(_) => { - self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id) - .await; - return; - } - }, - _ => { - self.send_error_response(cv.get_id(), CommunicationType::error_no_call_id) - .await; - return; - } - }; - - let invited = call_manager::add_invite(call_id, self.user_id, receiver_id as u64).await; - if !invited { - self.send_error_response(cv.get_id(), CommunicationType::error_invalid_call_id) - .await; - return; - } - - // Find target RhoConnection - let target_rho = match rho_manager::get_rho_con_for_user(receiver_id).await { - Some(rho) => rho, - _ => { - self.send_error_response(cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - // Get sender user ID - let sender_id = self.get_user_id().await; - - // Create and send call distribution message - let forward = CommunicationValue::new(CommunicationType::call_invite) - .with_receiver(receiver_id as u64) - .with_sender(sender_id as u64) - .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) - .add_data( - DataTypes::receiver_id, - DataValue::Str(receiver_id.to_string()), - ) - .add_data(DataTypes::sender_id, DataValue::Str(sender_id.to_string())); - - target_rho.message_to_client(forward).await; - - let response = CommunicationValue::new(CommunicationType::success).with_id(cv.get_id()); - self.send_message(&response).await; - } - - /// Handle get call request - async fn handle_get_call(self: Arc, cv: CommunicationValue) { - let user_id = self.get_user_id().await; - - let call_id = match cv.get_data(DataTypes::call_id) { - DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) { - Ok(id) => id, - Err(_) => { - self.send_error_response(cv.get_id(), CommunicationType::error) - .await; - return; - } - }, - _ => { - self.send_error_response(cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - if let Some(token) = call_manager::get_call_token(user_id, call_id).await { - let response = CommunicationValue::new(CommunicationType::call_token) - .with_id(cv.get_id()) - .with_receiver(user_id as u64) - .add_data(DataTypes::call_token, DataValue::Str(token)); - self.send_message(&response).await; - } else { - self.send_error_response(cv.get_id(), CommunicationType::error) - .await; - return; - } - } - async fn handle_call_timeout_user(self: Arc, cv: CommunicationValue) { - let call_id = - Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); - let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - let untill = cv.get_data(DataTypes::untill).as_number().unwrap_or(0); - - let call = call_manager::get_call(call_id).await; - if let Some(call) = call { - if call - .get_caller(self.get_user_id().await) - .await - .unwrap() - .has_admin() - { - let _ = call_util::remove_participant(call_id, user_id as u64).await; - call.get_caller(user_id as u64) - .await - .unwrap() - .set_timeout(untill) - .await; - } - } - } - async fn handle_call_disconnect_user(self: Arc, cv: CommunicationValue) { - let call_id = - Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); - let user_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - - let call = call_manager::get_call(call_id).await; - if let Some(call) = call { - if call - .get_caller(self.get_user_id().await) - .await - .unwrap() - .has_admin() - { - call.remove_caller(user_id as u64).await; - } - } - } - async fn handle_call_set_anonymous_joining(self: Arc, cv: CommunicationValue) { - let call_id = - Uuid::from_str(cv.get_data(DataTypes::call_id).as_str().unwrap_or("")).unwrap(); - let enable = cv.get_data(DataTypes::enabled).as_bool().unwrap_or(true); - - let call = call_manager::get_call(call_id).await; - - let mut short_link = None; - if let Some(call) = call { - if call - .get_caller(self.get_user_id().await) - .await - .unwrap() - .has_admin() - { - call.set_anonymous_joining(enable).await; - } - short_link = call.get_short_link().await; - } - let mut response_cv = - CommunicationValue::new(CommunicationType::call_set_anonymous_joining) - .with_id(cv.get_id()) - .add_data(DataTypes::call_id, DataValue::Str(call_id.to_string())) - .add_data(DataTypes::enabled, DataValue::Bool(enable)); - if let Some(short_link) = short_link { - response_cv = response_cv.add_data(DataTypes::link, DataValue::Str(short_link)); - } - self.send_message(&response_cv).await; - } - - /// Forward message to Iota - async fn forward_to_iota(self: Arc, cv: CommunicationValue) { - let sender_user_id = self.get_user_id().await; - let msg_id = cv.get_id(); - let msg_type = cv.get_type(); - - log_in!( - sender_user_id as i64, - PrintType::Client, - "Forwarding client->iota: sender={} type={:?} id={} receiver={}", - sender_user_id, - msg_type, - msg_id, - cv.get_receiver() - ); - - if cv.is_type(CommunicationType::add_conversation) - && cv - .get_data(DataTypes::chat_partner_id) - .as_number() - .is_none() - { - let chat_partner_name = cv - .get_data(DataTypes::chat_partner_name) - .as_str() - .unwrap_or("") - .to_string(); - - if anonymous_manager::get_anonymous_user_by_name(chat_partner_name.to_string()) - .await - .is_some() - { - self.send_error_response(cv.get_id(), CommunicationType::error_anonymous) - .await; - return; - } - - let load_uuid_response = get_omega_connection() - .await_response( - &CommunicationValue::new(CommunicationType::get_user_data) - .with_id(cv.clone().get_id()) - .add_data( - DataTypes::username, - DataValue::Str(chat_partner_name.clone()), - ), - Some(Duration::from_secs(20)), - ) - .await; - let chat_partner_id = { - if let Ok(load_uuid_response) = load_uuid_response { - load_uuid_response.get_data(DataTypes::user_id).clone() - } else { - DataValue::Null - } - }; - - if let Some(rho_conn) = self.get_rho_connection().await { - let iota_id = rho_conn.get_iota_id().await; - log_in!( - sender_user_id as i64, - PrintType::Client, - "Resolved rho for add_conversation: sender={} -> iota_id={} id={}", - sender_user_id, - iota_id, - msg_id - ); - - let updated_cv = cv - .with_sender(sender_user_id as u64) - .add_data(DataTypes::chat_partner_id, chat_partner_id); - rho_conn.message_to_iota(updated_cv).await; - } else { - log_err!( - sender_user_id as i64, - PrintType::Client, - "No rho/iota mapping found for add_conversation sender={} type={:?} id={}", - sender_user_id, - msg_type, - msg_id - ); - } - return; - } - - if let Some(rho_conn) = self.get_rho_connection().await { - let iota_id = rho_conn.get_iota_id().await; - log_in!( - sender_user_id as i64, - PrintType::Client, - "Resolved rho for forward: sender={} -> iota_id={} type={:?} id={}", - sender_user_id, - iota_id, - msg_type, - msg_id - ); - - let updated_cv = cv.with_sender(sender_user_id as u64); - rho_conn.message_to_iota(updated_cv).await; - } else { - log_err!( - sender_user_id as i64, - PrintType::Client, - "No rho/iota mapping found for sender={} type={:?} id={}", - sender_user_id, - msg_type, - msg_id - ); - self.send_error_response(msg_id, CommunicationType::error) - .await; - } - } - - /// Send error response - async fn send_error_response(self: Arc, message_id: u32, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(message_id); - self.send_message(&error).await; - } - - /// Close the connection - pub async fn close(&self) { - let mut is_open_guard = self.is_open.write().await; - if !*is_open_guard { - return; - } - *is_open_guard = false; - - let _ = self.sender.close(); - } - - /// Set interested users list - pub async fn set_interested_users(self: Arc, interested_ids: Vec) { - let mut interested_guard = self.interested_users.write().await; - *interested_guard = interested_ids; - } - #[allow(dead_code)] - pub async fn get_interested_users(self: Arc) -> Vec { - let interested_guard = self.interested_users.read().await; - interested_guard.clone() - } - - /// Check if interested in a user and send notification - #[allow(dead_code)] - pub async fn are_you_interested(self: Arc, user_id: i64) { - let interested_guard = self.clone().get_interested_users().await; - if interested_guard.contains(&user_id) { - let notification = CommunicationValue::new(CommunicationType::client_changed) - .add_data(DataTypes::user_id, DataValue::Str(user_id.to_string())) - .add_data(DataTypes::user_state, DataValue::Str("online".to_string())); - - self.send_message(¬ification).await; - } - } - - /// Handle connection close - pub async fn handle_close(&self) { - let user_id = self.get_user_id().await; - if let Some(rho_conn) = rho_manager::get_rho_con_for_user(user_id as i64).await { - rho_conn - .close_client_connection(Arc::new(self.clone())) - .await; - } - } -} - -// Implement Clone to make it easier to work with Arc -impl Clone for ClientConnection { - fn clone(&self) -> Self { - Self { - sender: Arc::clone(&self.sender), - receiver: Arc::clone(&self.receiver), - user_id: self.user_id, - ping: Arc::clone(&self.ping), - pub_key: Arc::clone(&self.pub_key), - rho_connection: Arc::clone(&self.rho_connection), - interested_users: Arc::clone(&self.interested_users), - is_open: Arc::clone(&self.is_open), - } - } -} diff --git a/src/rho/connection.rs b/src/rho/connection.rs deleted file mode 100755 index ca746f0..0000000 --- a/src/rho/connection.rs +++ /dev/null @@ -1,358 +0,0 @@ -use rand::{Rng, distributions::Alphanumeric}; -use std::{sync::Arc, time::Duration}; -use tokio::sync::RwLock; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use ttp_native::{Receiver, Sender}; - -use crate::{ - anonymous_clients::anonymous_client_connection::AnonymousClientConnection, - get_private_key, get_public_key, log_cv_in, log_cv_out, log_err, log_in, log_out, - omega::omega_connection::get_omega_connection, - rho::{ - client_connection::ClientConnection, iota_connection::IotaConnection, - rho_connection::RhoConnection, rho_manager, - }, - util::{ - crypto_helper::{load_public_key, public_key_to_base64}, - crypto_util::{DataFormat, SecurePayload}, - logger::PrintType, - }, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] -pub enum ConnectionKind { - Client, - Iota, - AnonymousClient, - Phi, -} - -pub struct GeneralConnection { - pub sender: Arc, - pub receiver: Arc, - - identified: Arc>, - challenged: Arc>, - challenge: Arc>, - - connection_kind: Arc>>, - pub rho_connection: Arc>>>, - id: Arc>, - - pub_key: Arc>>>, -} -impl GeneralConnection { - pub fn new(sender: Sender, receiver: Receiver) -> Arc { - Arc::new(Self { - sender: Arc::new(sender), - receiver: Arc::new(receiver), - identified: Arc::new(RwLock::new(false)), - challenged: Arc::new(RwLock::new(false)), - challenge: Arc::new(RwLock::new(String::new())), - connection_kind: Arc::new(RwLock::new(None)), - rho_connection: Arc::new(RwLock::new(None)), - id: Arc::new(RwLock::new(0)), - pub_key: Arc::new(RwLock::new(None)), - }) - } -} -impl GeneralConnection { - pub async fn handle(self: Arc) { - log_in!(0, PrintType::General, "General connection handler started"); - - loop { - let cv = match self.receiver.receive().await { - Ok(v) => v, - Err(_) => { - break; - } - }; - - log_cv_in!(cv); - - if !*self.identified.read().await { - self.handle_identification(cv).await; - continue; - } - - if !*self.challenged.read().await { - self.handle_challenge_response(cv).await; - } - - if *self.challenged.read().await { - let self_clone = self.clone(); - tokio::spawn(async move { - self_clone.migrate().await; - }); - break; - } - } - - log_out!(0, PrintType::General, "General connection handler stopped"); - } - async fn handle_identification(self: &Arc, cv: CommunicationValue) { - if !cv.is_type(CommunicationType::identification) { - return; - } - - if let DataValue::Number(iota_id) = cv.get_data(DataTypes::iota_id) { - *self.id.write().await = *iota_id as u64; - *self.connection_kind.write().await = Some(ConnectionKind::Iota); - - let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_iota_data) - .add_data(DataTypes::iota_id, DataValue::Number(*iota_id)); - - let response_cv = get_omega_connection() - .await_response(&get_pub_key_msg, Some(Duration::from_secs(20))) - .await; - - let response_cv = match response_cv { - Ok(r) => r, - Err(_) => { - return; - } - }; - - let base64_pub = response_cv - .get_data(DataTypes::public_key) - .as_str() - .unwrap_or(""); - - let pub_key = match load_public_key(base64_pub) { - Some(pk) => pk, - None => { - return; - } - }; - - *self.pub_key.write().await = Some(pub_key.as_bytes().to_vec()); - - let challenge: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect(); - - *self.challenge.write().await = challenge.clone(); - *self.identified.write().await = true; - - let encrypted_challenge = - SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key()) - .unwrap() - .encrypt_x448(pub_key) - .unwrap() - .export(DataFormat::Base64); - - let response = CommunicationValue::new(CommunicationType::challenge) - .with_id(cv.get_id()) - .add_data( - DataTypes::public_key, - DataValue::Str(public_key_to_base64(&get_public_key())), - ) - .add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge)); - - log_cv_out!(response); - let _ = self.sender.send(&response).await; - } else if let DataValue::Number(user_id) = cv.get_data(DataTypes::user_id) { - *self.id.write().await = *user_id as u64; - *self.connection_kind.write().await = Some(ConnectionKind::Client); - - let get_pub_key_msg = CommunicationValue::new(CommunicationType::get_user_data) - .add_data(DataTypes::user_id, DataValue::Number(*user_id)); - - let response_cv = get_omega_connection() - .await_response(&get_pub_key_msg, Some(Duration::from_secs(20))) - .await; - - let response_cv = match response_cv { - Ok(r) => r, - Err(_) => { - return; - } - }; - - let base64_pub = response_cv - .get_data(DataTypes::public_key) - .as_str() - .unwrap_or(""); - - let pub_key = match load_public_key(base64_pub) { - Some(pk) => pk, - None => { - return; - } - }; - - *self.pub_key.write().await = Some(pub_key.as_bytes().to_vec()); - - let challenge: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect(); - - *self.challenge.write().await = challenge.clone(); - *self.identified.write().await = true; - - let encrypted_challenge = - SecurePayload::new(challenge.as_bytes(), DataFormat::Raw, get_private_key()) - .unwrap() - .encrypt_x448(pub_key) - .unwrap() - .export(DataFormat::Base64); - - let response = CommunicationValue::new(CommunicationType::challenge) - .with_id(cv.get_id()) - .add_data( - DataTypes::public_key, - DataValue::Str(public_key_to_base64(&get_public_key())), - ) - .add_data(DataTypes::challenge, DataValue::Str(encrypted_challenge)); - - log_cv_out!(response); - let _ = self.sender.send(&response).await; - } - } - async fn handle_challenge_response(self: &Arc, cv: CommunicationValue) { - let id = *self.id.read().await as i64; - - if !cv.is_type(CommunicationType::challenge_response) { - return; - } - - if let DataValue::Str(response) = cv.get_data(DataTypes::challenge) { - let expected = self.challenge.read().await.clone(); - - if *response == expected { - *self.challenged.write().await = true; - - let response = CommunicationValue::new(CommunicationType::identification_response) - .with_id(cv.get_id()) - .add_data(DataTypes::accepted, DataValue::Bool(true)); - - log_cv_out!(response); - if let Err(_) = self.sender.send(&response).await { - return; - } - } else { - log_err!( - id, - PrintType::Iota, - "Challenge response mismatch expected={} actual={}", - expected, - response - ); - } - } else { - log_err!( - id, - PrintType::Iota, - "Challenge response missing challenge payload" - ); - } - } - - async fn migrate(self: &Arc) -> bool { - let kind = match *self.connection_kind.read().await { - Some(kind) => kind, - None => { - return false; - } - }; - let id = *self.id.read().await; - - match kind { - ConnectionKind::Client => { - let notify = CommunicationValue::new(CommunicationType::user_connected) - .add_data(DataTypes::user_id, DataValue::Number(id as i64)); - get_omega_connection().send_message(¬ify).await; - - let user_id = id as i64; - - let client = ClientConnection::from_general(self.clone(), id).await; - - let mut rho = rho_manager::get_rho_con_for_user(user_id).await; - - if rho.is_none() { - let get_user_msg = CommunicationValue::new(CommunicationType::get_user_data) - .add_data(DataTypes::user_id, DataValue::Number(user_id)); - - if let Ok(user_data_cv) = get_omega_connection() - .await_response(&get_user_msg, Some(Duration::from_secs(20))) - .await - { - if let DataValue::Number(iota_id) = - user_data_cv.get_data(DataTypes::iota_id) - { - if let Some(bound_rho) = - rho_manager::bind_user_to_iota(user_id, *iota_id).await - { - bound_rho.bind_user_id(user_id).await; - rho = Some(bound_rho); - } - } - } - } - - *self.rho_connection.write().await = rho.clone(); - - if let Some(rho_conn) = rho { - rho_conn.bind_user_id(user_id).await; - rho_conn.add_client_connection(client.clone()).await; - } else { - log_err!( - user_id, - PrintType::Client, - "No RhoConnection found for user {}, client not attached to iota", - id - ); - } - - client.start(); - } - ConnectionKind::Iota => { - let notify = CommunicationValue::new(CommunicationType::iota_connected) - .add_data(DataTypes::iota_id, DataValue::Number(id as i64)); - get_omega_connection().send_message(¬ify).await; - - let iota = IotaConnection::from_general(self.clone(), id).await; - - let rho = Arc::new(RhoConnection::new(iota.clone(), Vec::new()).await); - - iota.set_rho_connection(rho.clone()).await; - - rho_manager::add_rho(rho).await; - - let get_iota_msg = CommunicationValue::new(CommunicationType::get_iota_data) - .add_data(DataTypes::iota_id, DataValue::Number(id as i64)); - - if let Ok(iota_data_cv) = get_omega_connection() - .await_response(&get_iota_msg, Some(Duration::from_secs(20))) - .await - { - if let DataValue::Array(users) = iota_data_cv.get_data(DataTypes::user_ids) { - let mut user_ids: Vec = Vec::new(); - for value in users { - if let DataValue::Number(user_id) = value { - user_ids.push(*user_id as u64); - } - } - iota.set_user_ids(user_ids).await; - } - } - - iota.clone().start(); - } - ConnectionKind::AnonymousClient => { - let client = AnonymousClientConnection::from_general(self.clone(), id).await; - client.start(); - } - ConnectionKind::Phi => { - let phi = ClientConnection::from_general(self.clone(), id).await; - phi.start(); - } - } - true - } -} diff --git a/src/rho/iota_connection.rs b/src/rho/iota_connection.rs deleted file mode 100755 index 41336ea..0000000 --- a/src/rho/iota_connection.rs +++ /dev/null @@ -1,461 +0,0 @@ -use crate::calls::call_group::CallGroup; -use crate::calls::call_manager; -use crate::log_cv_in; -use crate::log_cv_out; -use crate::log_err; -use crate::log_in; -use crate::omega::omega_connection::get_omega_connection; -use crate::rho::connection::GeneralConnection; -use crate::util::logger::PrintType; -use dashmap::DashMap; -use std::collections::BTreeMap; -use std::{collections::HashMap, sync::Arc, time::Duration}; -use tokio::sync::RwLock; -use tokio::sync::mpsc; -use ttp_core::CommunicationType; -use ttp_core::CommunicationValue; -use ttp_core::DataTypes; -use ttp_core::DataValue; -use ttp_native::Receiver; -use ttp_native::Sender; -use x448::PublicKey; - -use super::{rho_connection::RhoConnection, rho_manager}; -use crate::omega::omega_connection::OmegaConnection; - -#[allow(dead_code)] -pub struct IotaConnection { - pub iota_id: u64, - pub sender: Arc, - pub receiver: Arc, - pub user_ids: Arc>>, - pub ping: Arc>, - pub_key: Arc>>>, - pub waiting_tasks: - DashMap, CommunicationValue) -> bool + Send + Sync>>, - pub rho_connection: Arc>>>, -} - -impl IotaConnection { - pub async fn from_general(general: Arc, iota_id: u64) -> Arc { - Arc::new(Self { - ping: Arc::new(RwLock::new(0)), - pub_key: Arc::new(RwLock::new(None)), - rho_connection: general.rho_connection.clone(), - user_ids: Arc::new(RwLock::new(Vec::new())), - sender: general.sender.clone(), - receiver: general.receiver.clone(), - iota_id: iota_id, - waiting_tasks: DashMap::new(), - }) - } - pub fn start(self: Arc) { - let self_clone = self.clone(); - tokio::spawn(async move { - loop { - match self_clone.receiver.receive().await { - Ok(cv) => { - self_clone.clone().handle_message(cv).await; - } - Err(_) => { - break; - } - } - } - self_clone.handle_close().await; - }); - } - - /// Get the Iota ID - pub async fn get_iota_id(&self) -> u64 { - self.iota_id - } - - #[allow(dead_code)] - pub async fn get_public_key(&self) -> Option { - if let Some(public_key) = self.pub_key.read().await.clone() { - PublicKey::from_bytes(&public_key) - } else { - None - } - } - - /// Get the user IDs - pub async fn get_user_ids(&self) -> Vec { - self.user_ids.read().await.clone() - } - - /// Replace all users linked to this iota and synchronize the attached rho mapping. - pub async fn set_user_ids(&self, user_ids: Vec) { - { - let mut guard = self.user_ids.write().await; - *guard = user_ids.clone(); - } - - if let Some(rho_conn) = self.get_rho_connection().await { - let user_ids_i64: Vec = user_ids.into_iter().map(|u| u as i64).collect(); - rho_conn.set_user_ids(user_ids_i64).await; - } - } - - pub async fn add_user_id(&self, user_id: u64) { - let mut should_sync = false; - { - let mut guard = self.user_ids.write().await; - if !guard.contains(&user_id) { - guard.push(user_id); - should_sync = true; - } - } - - if should_sync { - if let Some(rho_conn) = self.get_rho_connection().await { - rho_conn.add_user_id(user_id as i64).await; - } - } - } - - /// Get current ping - pub async fn get_ping(&self) -> i64 { - *self.ping.read().await - } - - /// Set the RhoConnection reference - pub async fn set_rho_connection(&self, rho_connection: Arc) { - let mut rho_ref = self.rho_connection.write().await; - *rho_ref = Some(rho_connection); - } - - /// Get RhoConnection if available - pub async fn get_rho_connection(&self) -> Option> { - let rho_ref = self.rho_connection.read().await; - if let Some(weak_ref) = rho_ref.as_ref() { - Some(weak_ref.clone()) - } else { - None - } - } - - /// Send a CommunicationValue to the Iota - pub async fn send_message(&self, cv: &CommunicationValue) { - if !cv.is_type(CommunicationType::pong) { - log_cv_out!(PrintType::Iota, cv); - } - if let Err(e) = self.sender.send(&cv).await { - log_err!( - self.iota_id as i64, - PrintType::Iota, - "Failed to send message: {:?}", - e - ); - } - } - - /// Handle incoming message from Iota - pub async fn handle_message(self: Arc, cv: CommunicationValue) { - let msg_id = cv.get_id(); - if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) { - if (task)(self.clone(), cv.clone()) { - return; - } - } - - // Handle ping - if cv.is_type(CommunicationType::ping) || cv.is_type(CommunicationType::pong) { - self.handle_ping(cv).await; - return; - } - - log_cv_in!(PrintType::Iota, cv); - - // Handle GET_CHATS - if cv.is_type(CommunicationType::get_chats) { - self.handle_get_chats(cv).await; - return; - } - - // Handle forwarding to other Iotas or clients - let receiver_id = cv.get_receiver(); - if (receiver_id != 0 && !self.get_user_ids().await.contains(&(receiver_id as u64))) - || cv.is_type(CommunicationType::message_other_iota) - || cv.is_type(CommunicationType::send_chat) - { - self.handle_forward_message(cv).await; - return; - } - - if cv.is_type(CommunicationType::change_iota_data) - || cv.is_type(CommunicationType::push_notification) - || cv.is_type(CommunicationType::get_user_data) - || cv.is_type(CommunicationType::get_iota_data) - || cv.is_type(CommunicationType::get_register) - || cv.is_type(CommunicationType::complete_register_user) - || cv.is_type(CommunicationType::delete_iota) - { - let sender = self.get_iota_id().await; - self.handle_omega_forward(cv.with_sender(sender as u64)) - .await; - return; - } - self.forward_to_client(cv).await; - } - - #[allow(dead_code)] - async fn send_error_response(&self, message_id: u32, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(message_id); - self.send_message(&error).await; - } - - #[allow(dead_code)] - async fn close(&self) { - let _ = self.sender.close(); - } - - async fn handle_omega_forward(self: Arc, cv: CommunicationValue) { - let iota_for_closure = self.clone(); - tokio::spawn(async move { - let response_cv = get_omega_connection() - .await_response(&cv.with_sender(self.iota_id), Some(Duration::from_secs(20))) - .await; - if let Ok(response_cv) = response_cv { - iota_for_closure.send_message(&response_cv).await; - } - }); - } - /// Handle ping message - async fn handle_ping(&self, cv: CommunicationValue) { - if let DataValue::Number(last_ping) = cv.get_data(DataTypes::last_ping) { - if let Ok(ping_val) = last_ping.to_string().parse::() { - let mut ping_guard = self.ping.write().await; - *ping_guard = ping_val; - } - } - - let client_pings = if let Some(rho_conn) = self.get_rho_connection().await { - rho_conn.get_client_pings().await - } else { - HashMap::new() - }; - - let pings: Vec<(DataTypes, DataValue)> = client_pings - .into_iter() - .map(|(k, v)| (DataTypes::parse(k), DataValue::Number(v))) - .collect(); - let response = CommunicationValue::new(CommunicationType::pong) - .with_id(cv.get_id()) - .add_data(DataTypes::ping_clients, DataValue::Container(pings)); - - self.send_message(&response).await; - } - - /// Handle message forwarding to other Iotas - async fn handle_forward_message(&self, cv: CommunicationValue) { - let receiver_id = cv.get_receiver(); - let sender_id = cv.get_sender(); - let my_user_ids = self.get_user_ids().await; - - log_in!( - self.iota_id as i64, - PrintType::Iota, - "Authority check: sender_id={} receiver_id={} iota_user_ids={:?} msg_type={:?} msg_id={}", - sender_id, - receiver_id, - my_user_ids, - cv.get_type(), - cv.get_id() - ); - - if my_user_ids.contains(&(sender_id as u64)) { - if let Some(target_rho) = rho_manager::get_rho_con_for_user(receiver_id as i64).await { - target_rho.message_to_iota(cv).await; - } else { - let error = CommunicationValue::new(CommunicationType::error_no_iota) - .with_id(cv.get_id()) - .with_sender(cv.get_sender()); - self.send_message(&error).await; - } - } else { - log_err!( - self.iota_id as i64, - PrintType::Iota, - "Rejected client->iota forward: sender_id={} is not authorized for this iota. Known users={:?}", - sender_id, - my_user_ids - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::error_invalid_user_id).add_data( - DataTypes::error_type, - DataValue::Str( - "You are sending to another User without authority.".to_string(), - ), - ), - ) - .await; - } - } - - /// Handle GET_CHATS message - async fn handle_get_chats(&self, cv: CommunicationValue) { - let receiver_id = cv.get_receiver(); - let mut interested_ids: Vec = Vec::new(); - - // ============================ - // Load Calls - // ============================ - let calls: Vec> = call_manager::get_call_groups(receiver_id).await; - - let mut invites: HashMap> = HashMap::new(); - let empty = calls.is_empty(); - - for call in calls { - for inviter in call.members.read().await.iter() { - let call_self = call.get_caller(receiver_id).await.unwrap(); - - let inviter_id = inviter.user_id; - let timeout = *call_self.timeout.read().await; - let admin = call_self.has_admin(); - - // Build call container - let mut call_map: BTreeMap = BTreeMap::new(); - - call_map.insert(DataTypes::call_id, DataValue::Str(call.call_id.to_string())); - - if timeout > 0 { - call_map.insert(DataTypes::timeout, DataValue::Number(timeout as i64)); - } - - if admin { - call_map.insert(DataTypes::has_admin, DataValue::Bool(true)); - } - - let call_container = DataValue::container_from_map(&call_map); - - invites - .entry(inviter_id as i64) - .or_insert_with(Vec::new) - .push(call_container); - } - } - - // ============================ - // Enrich Contacts - // ============================ - let enriched_contacts = if empty { - match cv.get_data(DataTypes::user_ids) { - DataValue::Array(arr) => DataValue::Array(arr.clone()), - _ => DataValue::Array(vec![]), - } - } else { - let mut enriched: Vec = Vec::new(); - - if let DataValue::Array(users) = cv.get_data(DataTypes::user_ids) { - for user_val in users { - if let DataValue::Container(entries) = user_val { - let mut user_map: BTreeMap = - entries.iter().cloned().collect(); - - // extract user_id - if let Some(DataValue::Number(user_id)) = user_map.get(&DataTypes::user_id) - { - interested_ids.push(*user_id); - - // attach calls if exists - if let Some(call_list) = invites.get(user_id) { - user_map - .insert(DataTypes::calls, DataValue::Array(call_list.clone())); - } - } - - enriched.push(DataValue::container_from_map(&user_map)); - } - } - } - - DataValue::Array(enriched) - }; - - // ============================ - // Notify Omega - // ============================ - OmegaConnection::user_states(receiver_id as i64, interested_ids.clone()).await; - - // ============================ - // Notify Rho - // ============================ - if let Some(rho_conn) = self.get_rho_connection().await { - rho_conn - .set_interested(receiver_id as i64, interested_ids) - .await; - } - - // ============================ - // Forward to client - // ============================ - self.forward_to_client(cv.add_data(DataTypes::user_ids, enriched_contacts)) - .await; - } - - /// Forward message to client - async fn forward_to_client(&self, cv: CommunicationValue) { - if let Some(rho_conn) = self.get_rho_connection().await { - let updated_cv = cv.with_sender(self.get_iota_id().await); - rho_conn.message_to_client(updated_cv).await; - } else { - } - } - - pub async fn handle_close(&self) { - if let Some(rho_conn) = self.get_rho_connection().await { - rho_conn.close_iota_connection().await; - } - } - - #[allow(dead_code)] - pub async fn await_response( - self: Arc, - cv: &CommunicationValue, - timeout_duration: Option, - ) -> Result { - let (tx, mut rx) = mpsc::channel(1); - let msg_id = cv.get_id(); - - let task_tx = tx.clone(); - self.waiting_tasks.insert( - msg_id, - Box::new(move |_, response_cv| { - let inner_tx = task_tx.clone(); - tokio::spawn(async move { - let _ = inner_tx.send(response_cv).await; - }); - true - }), - ); - - self.send_message(cv).await; - - let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); - - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response_cv)) => Ok(response_cv), - Ok(_) => Err("Failed to receive response, channel was closed.".to_string()), - Err(_) => { - self.waiting_tasks.remove(&msg_id); - Err(format!( - "Request timed out after {} seconds.", - timeout.as_secs() - )) - } - } - } -} - -impl std::fmt::Debug for IotaConnection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("IotaConnection") - .field("iota_id", &"[async]") - .field("identified", &"[async]") - .field("ping", &"[async]") - .finish() - } -} diff --git a/src/rho/mod.rs b/src/rho/mod.rs deleted file mode 100644 index ac498f3..0000000 --- a/src/rho/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod client_connection; -pub mod connection; -pub mod iota_connection; -pub mod rho_connection; -pub mod rho_manager; -pub mod server; diff --git a/src/rho/rho_connection.rs b/src/rho/rho_connection.rs deleted file mode 100644 index 6a740ff..0000000 --- a/src/rho/rho_connection.rs +++ /dev/null @@ -1,199 +0,0 @@ -use super::{client_connection::ClientConnection, iota_connection::IotaConnection, rho_manager}; - -use crate::data::user::UserStatus; -use crate::omega::omega_connection::OmegaConnection; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; - -pub struct RhoConnection { - iota_connection: Arc, - user_ids: Arc>>, - client_connections: Arc>>>, -} - -impl RhoConnection { - /// Create a new RhoConnection - pub async fn new(iota_connection: Arc, user_ids: Vec) -> Self { - let rho_connection = Self { - iota_connection, - user_ids: Arc::new(RwLock::new(user_ids.clone())), - client_connections: Arc::new(RwLock::new(Vec::new())), - }; - - rho_connection - } - - pub async fn get_iota_id(&self) -> u64 { - self.iota_connection.iota_id - } - - pub async fn get_user_ids(&self) -> Vec { - self.user_ids.read().await.clone() - } - - pub async fn set_user_ids(&self, user_ids: Vec) { - let mut guard = self.user_ids.write().await; - *guard = user_ids; - } - - pub async fn add_user_id(&self, user_id: i64) { - let mut guard = self.user_ids.write().await; - if !guard.contains(&user_id) { - guard.push(user_id); - } - } - - pub async fn bind_user_id(&self, user_id: i64) { - self.add_user_id(user_id).await; - self.iota_connection.add_user_id(user_id as u64).await; - } - - pub fn get_iota_connection(&self) -> &Arc { - &self.iota_connection - } - - pub async fn get_client_connections(&self) -> Vec> { - let connections = self.client_connections.read().await; - connections.clone() - } - - /// Get client connections for a specific user - pub async fn get_client_connections_for_user( - &self, - user_id: i64, - ) -> Vec> { - let connections = self.client_connections.read().await; - let mut collections = Vec::new(); - for con in connections.iter() { - if con.get_user_id().await == user_id as u64 { - collections.push(con.clone()); - } - } - collections - } - - /// Add a client connection - #[allow(dead_code)] - pub async fn add_client_connection(&self, connection: Arc) { - let notification = CommunicationValue::new(CommunicationType::client_connected).add_data( - DataTypes::user_id, - DataValue::Number(connection.get_user_id().await as i64), - ); - - self.iota_connection.send_message(¬ification).await; - - { - let mut connections = self.client_connections.write().await; - connections.push(Arc::clone(&connection)); - } - - OmegaConnection::client_changed( - self.get_iota_id().await as i64, - connection.get_user_id().await as i64, - UserStatus::user_online, - ) - .await; - } - - /// Remove a client connection - pub async fn close_client_connection(&self, connection: Arc) { - let target_user_id = connection.get_user_id().await; - { - let mut connections = self.client_connections.write().await; - connections.retain(|con| { - futures::executor::block_on(async { con.get_user_id().await != target_user_id }) - }); - } - - // Notify OmegaConnection - OmegaConnection::client_changed( - self.get_iota_id().await as i64, - connection.get_user_id().await as i64, - UserStatus::user_offline, - ) - .await; - } - - /// Close the Iota connection and all associated client connections - pub async fn close_iota_connection(&self) { - // Close all client connections - let connections = self.get_client_connections().await; - for connection in connections { - connection.close().await; - } - - // Remove from manager - rho_manager::remove_rho(self.get_iota_id().await as i64).await; - - // Notify OmegaConnection - OmegaConnection::close_iota(self.get_iota_id().await as i64).await; - } - - /// Send message from Iota to specific client - pub async fn message_to_client(&self, cv: CommunicationValue) { - let connections = self.client_connections.read().await; - let receiver_id = cv.get_receiver(); - for connection in connections.iter() { - if connection.get_user_id().await == receiver_id { - connection.clone().send_message(&cv).await; - } - } - } - - /// Send message to Iota - pub async fn message_to_iota(&self, cv: CommunicationValue) { - self.iota_connection.send_message(&cv).await; - } - - /// Set interested users for a specific client - pub async fn set_interested(&self, user_id: i64, interested_ids: Vec) { - let connections = self.client_connections.read().await; - for connection in connections.iter() { - let conn_user_id = connection.get_user_id().await; - if conn_user_id == user_id as u64 { - connection - .clone() - .set_interested_users(interested_ids.clone()) - .await; - break; - } - } - } - - /// Check if clients are interested in a user - #[allow(dead_code)] - pub async fn are_they_interested(&self, user_id: i64) { - let connections = self.client_connections.read().await; - for connection in connections.iter() { - connection.clone().are_you_interested(user_id).await; - } - } - - /// Get ping information for all clients - pub async fn get_client_pings(&self) -> HashMap { - let connections = self.client_connections.read().await; - let mut pings = HashMap::new(); - - for connection in connections.iter() { - let user_id = connection.get_user_id().await; - pings.insert(user_id.to_string(), connection.get_ping().await); - } - - pings - } - - /// Check if this RhoConnection contains a specific user ID - #[allow(dead_code)] - pub async fn contains_user(&self, user_id: &i64) -> bool { - self.user_ids.read().await.contains(user_id) - } - - /// Get count of active client connections - #[allow(dead_code)] - pub async fn client_count(&self) -> usize { - let connections = self.client_connections.read().await; - connections.len() - } -} diff --git a/src/rho/rho_manager.rs b/src/rho/rho_manager.rs deleted file mode 100644 index e823a04..0000000 --- a/src/rho/rho_manager.rs +++ /dev/null @@ -1,83 +0,0 @@ -use super::rho_connection::RhoConnection; -use crate::log_in; -use crate::util::logger::PrintType; -use std::{ - collections::HashMap, - sync::{Arc, LazyLock}, -}; -use tokio::sync::RwLock; - -pub static RHO_CONNECTIONS: LazyLock>>>> = - LazyLock::new(|| Arc::new(RwLock::new(HashMap::new()))); - -pub async fn get_rho_con_for_user(user_id: i64) -> Option> { - let connections = RHO_CONNECTIONS.read().await; - for rho_connection in connections.values() { - let rho_user_ids = rho_connection.get_user_ids().await; - log_in!( - user_id, - PrintType::Client, - "Comparing user IDs: {:?}", - rho_user_ids - ); - if rho_user_ids.contains(&user_id) { - return Some(Arc::clone(rho_connection)); - } - } - None -} - -#[allow(dead_code)] -pub async fn contains_iota(iota_id: i64) -> bool { - let connections = RHO_CONNECTIONS.read().await; - connections.contains_key(&iota_id) -} - -/// Bind a user ID to an already tracked iota/rho connection. -pub async fn bind_user_to_iota(user_id: i64, iota_id: i64) -> Option> { - let connections = RHO_CONNECTIONS.read().await; - if let Some(rho_connection) = connections.get(&iota_id) { - let rho = Arc::clone(rho_connection); - drop(connections); - - rho.add_user_id(user_id).await; - - log_in!( - user_id, - PrintType::Client, - "Bound user {} to iota {}", - user_id, - iota_id - ); - - Some(rho) - } else { - None - } -} - -/// Remove a RhoConnection by Iota ID -pub async fn remove_rho(iota_id: i64) -> Option> { - let mut connections = RHO_CONNECTIONS.write().await; - connections.remove(&iota_id) -} - -/// Add a RhoConnection to the manager -pub async fn add_rho(rho_connection: Arc) { - let mut connections = RHO_CONNECTIONS.write().await; - let iota_id = rho_connection.get_iota_id().await; - connections.insert(iota_id as i64, rho_connection); -} - -/// Get a RhoConnection by Iota ID directly -#[allow(dead_code)] -pub async fn get_rho_by_iota(iota_id: i64) -> Option> { - let connections = RHO_CONNECTIONS.read().await; - connections.get(&iota_id).map(Arc::clone) -} - -/// Get the count of active connections -pub async fn connection_count() -> usize { - let connections = RHO_CONNECTIONS.read().await; - connections.len() -} diff --git a/src/rho/server.rs b/src/rho/server.rs deleted file mode 100644 index 9e3946d..0000000 --- a/src/rho/server.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ - log, - rho::connection::GeneralConnection, - util::{file_util::load_file_vec, logger::PrintType}, -}; -use ttp_native::Host; - -pub async fn start(port: u16) -> Result<(), Box> { - let cert_pem = load_file_vec("certs", "cert.pem").expect("Error loading Pemfile"); - - let key_pem = load_file_vec("certs", "key.pem").expect("Error loading Keyfile"); - - let mut host: Host = ttp_native::host(port, cert_pem, key_pem).await?; - log!(0, PrintType::General, "Server listening on port {}", port); - - while let Some((sender, receiver)) = host.next().await { - tokio::spawn(async move { - let conn = GeneralConnection::new(sender, receiver); - conn.handle().await; - }); - } - log!(0, PrintType::General, "Server stopped"); - - Ok(()) -} diff --git a/src/server/api.rs b/src/server/api.rs new file mode 100755 index 0000000..f9a87a4 --- /dev/null +++ b/src/server/api.rs @@ -0,0 +1,190 @@ +use crate::server::server::is_local_network; +use crate::util::config_util::CONFIG; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use serde_json::{Value, json}; +use std::net::SocketAddr; +use std::sync::Arc; + +pub fn api_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/api") + .route("/shutdown/", web::post().to(shutdown)) + .route("/reload/", web::post().to(reload)) + .route("/users/add/", web::post().to(users_add)) + .route("/users/remove/", web::post().to(users_remove)) + .route("/users/get/", web::get().to(users_get)) + .route("/communities/add/", web::post().to(communities_add)) + .route("/communities/get/", web::get().to(communities_get)) + .route("/settings/set/", web::post().to(settings_set)) + .route("/settings/get/", web::get().to(settings_get)), + ); +} + +async fn settings_set(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let key = req.headers().get("key").and_then(|v| v.to_str().ok()); + let value = req.headers().get("value").and_then(|v| v.to_str().ok()); + + match (key, value) { + (Some(k), Some(v)) => { + let _ = CONFIG + .write() + .await + .config + .insert(&k.to_string(), v.to_string()); + + success() + } + _ => error(), + } +} + +async fn settings_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + let config = CONFIG.read().await.config.clone(); + let serde_config: Value = serde_json::to_value(config.to_string()).unwrap(); + HttpResponse::Ok().json(serde_config) +} + +async fn communities_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let communities = crate::communities::community_manager::get_communities().await; + + let mut list = Vec::new(); + + for c in communities { + let val = c.frontend().await; + let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); + list.push(s_val); + } + HttpResponse::Ok().json(list) +} + +async fn communities_add( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let name = payload["name"].as_str().unwrap_or("").to_string(); + let owner = payload["owner"].as_i64().unwrap_or(0); + + let community = Arc::new(crate::communities::community::Community::create(name, owner).await); + + crate::communities::community_manager::add_community(community).await; + + success() +} + +async fn users_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let users = crate::users::user_manager::get_users(); + + let list: Vec<_> = users + .into_iter() + .map(|u| { + let val = u.frontend(); + serde_json::to_value(val.to_string()).unwrap() + }) + .collect(); + + HttpResponse::Ok().json(list) +} + +async fn users_remove( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0); + + crate::users::user_manager::remove_user(uuid); + crate::users::user_manager::save_users(); + + success() +} + +async fn users_add( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let username = match payload.get("username").and_then(|v| v.as_str()) { + Some(u) => u, + _ => return error(), + }; + + if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { + let val = user.frontend(); + let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); + HttpResponse::Ok().json(s_val) + } else { + error() + } +} + +async fn shutdown(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + *crate::SHUTDOWN.write().await = true; + success() +} + +async fn reload(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + *crate::SHUTDOWN.write().await = true; + *crate::RELOAD.write().await = true; + + success() +} + +fn forbidden() -> HttpResponse { + HttpResponse::Forbidden().body("403 Forbidden") +} + +fn success() -> HttpResponse { + HttpResponse::Ok().json(json!({ "type": "success" })) +} + +fn error() -> HttpResponse { + HttpResponse::Ok().json(json!({ "type": "error" })) +} + +fn is_allowed(addr: SocketAddr, ssl: bool) -> bool { + is_local_network(addr.ip()) || ssl +} + +fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool { + if let Some(addr) = req.peer_addr() { + is_allowed(addr, ssl) + } else { + false + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..de2a79a --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod server; +pub mod web_path_parser; diff --git a/src/server/server.rs b/src/server/server.rs new file mode 100644 index 0000000..b55a50f --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,169 @@ +use crate::log; +use crate::server::api::api_config; +use crate::server::web_path_parser; +use crate::util::file_util::load_file_buf; +use crate::{ACTIVE_TASKS, SHUTDOWN}; +use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; +use actix_web_actors::ws; +use rustls::ServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use std::{ + error::Error as StdError, + io::{self, BufReader, ErrorKind}, + net::IpAddr, + sync::Arc, + time::Duration, +}; + +async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result { + let path = req.path().to_string(); + log!("WS connection from {:?}", req.peer_addr()); + let session = WsSession::new(path); + ws::start(session, &req, stream) +} + +use tokio::sync::oneshot; + +pub async fn start(port: u16) -> bool { + let (tx, rx) = oneshot::channel::(); + + let _ = tokio::spawn(async move { + let server = match load_tls_config() { + Ok(Some(tls_config)) => { + log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); + let _config = (*tls_config).clone(); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(true)) + .configure(api_config) + .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) + .default_service(web::to(web_path_parser::handle)) + }) + .bind(("0.0.0.0", port)) + .unwrap() + .run() + } + Ok(_) => { + log!("HTTP Server running on 0.0.0.0:{}", port); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(false)) + .configure(api_config) + .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) + .default_service(web::to(web_path_parser::handle)) + }) + .bind(("0.0.0.0", port)) + .unwrap() + .run() + } + Err(e) => { + log!("TLS config error: {}", e); + return; + } + }; + + let server_handle = server.handle(); + tx.send(server_handle).unwrap(); + + ACTIVE_TASKS.insert("WebServer".into()); + server.await.unwrap(); + ACTIVE_TASKS.remove("WebServer"); + log!("Web Server shutdown complete."); + }); + + if let Ok(server_handle) = rx.await { + tokio::spawn(async move { + wait_for_shutdown(server_handle).await; + }); + true + } else { + false + } +} + +async fn wait_for_shutdown(server_handle: ServerHandle) { + loop { + if *SHUTDOWN.read().await { + log!("Shutdown signal received."); + server_handle.stop(true).await; + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +fn load_tls_config() -> Result>, Box> { + let cert_file_res = load_file_buf("certs", "cert.pem"); + let key_file_res = load_file_buf("certs", "cert.key"); + + let cert_file_buf = match cert_file_res { + Ok(b) => b, + Err(e) if e.kind() == ErrorKind::NotFound => { + log!("TLS certificate 'certs/cert.pem' not found."); + return Ok(None); + } + Err(e) => return Err(e.into()), // Other IO error + }; + + let key_file_buf = match key_file_res { + Ok(b) => b, + Err(e) if e.kind() == ErrorKind::NotFound => { + log!("TLS key 'certs/cert.key' not found."); + return Ok(None); + } + Err(e) => return Err(e.into()), // Other IO error + }; + + let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf)) + .collect::, _>>()?; + + // PKCS8 + let mut key_reader = BufReader::new(key_file_buf); + let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + + if key_ders.is_empty() { + // RSA + key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file + key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + } + + if key_ders.is_empty() { + // EC + key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file + key_ders = rustls_pemfile::ec_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + } + + if key_ders.is_empty() { + return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into()); + } + + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, key_ders.remove(0)) + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; + + Ok(Some(Arc::new(config))) +} + +pub fn is_local_network(addr: IpAddr) -> bool { + match addr { + IpAddr::V4(v4) => { + let o = v4.octets(); + o[0] == 10 + || (o[0] == 172 && (16..=31).contains(&o[1])) + || (o[0] == 192 && o[1] == 168) + || o[0] == 127 + || (o[0] == 169 && o[1] == 254) + } + IpAddr::V6(v6) => { + let s = v6.segments(); + (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback() + } + } +} diff --git a/src/server/web_path_parser.rs b/src/server/web_path_parser.rs new file mode 100755 index 0000000..a82c8db --- /dev/null +++ b/src/server/web_path_parser.rs @@ -0,0 +1,77 @@ +use actix_web::{HttpRequest, HttpResponse}; +use std::path::{Path, PathBuf}; + +use crate::util::file_util::load_file_vec; + +fn codec_for_ext(ext: &str) -> &'static str { + match ext { + "html" => "text/html; charset=utf-8", + "css" => "text/css", + "js" => "application/javascript", + "json" => "application/json", + "png" => "image/png", + "ico" => "image/x-icon", + "woff2" => "font/woff2", + _ => "application/octet-stream", + } +} + +pub async fn handle(req: HttpRequest) -> HttpResponse { + let req_path = req.path().trim_start_matches('/'); + + let mut fs_path = PathBuf::from("web"); + + if req_path.is_empty() { + fs_path.push("index.html"); + } else { + fs_path.extend(req_path.split('/')); + } + + if fs_path.is_dir() { + fs_path.push("index.html"); + } + + let ext_opt = fs_path.extension().and_then(|e| e.to_str()); + let mut final_name = fs_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + if ext_opt.is_none() { + if final_name.is_empty() { + final_name = "index.html".to_string(); + } else { + final_name.push_str(".html"); + } + } + + let content_type = codec_for_ext( + fs_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("html"), + ); + + let dir = fs_path.parent().unwrap_or(Path::new("web")); + + match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) { + Ok(content) => HttpResponse::Ok().content_type(content_type).body(content), + + Err(_) => { + let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if matches!(ext, "js" | "css" | "woff2") { + return HttpResponse::NotFound() + .content_type("text/plain") + .body("Not found"); + } + + let fallback = load_file_vec("web", "404.html") + .unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec()); + + HttpResponse::NotFound() + .content_type("text/html; charset=utf-8") + .body(fallback) + } + } +} diff --git a/src/terms/buttons.rs b/src/terms/buttons.rs new file mode 100644 index 0000000..c545a4c --- /dev/null +++ b/src/terms/buttons.rs @@ -0,0 +1,175 @@ +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::terms::focus::Focus; + +#[allow(mismatched_lifetime_syntaxes)] +pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line { + let box_char = if checked { "[x]" } else { "[ ]" }; + let (box_style, text_style) = if active { + if allowed { + ( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + ( + Style::default().fg(Color::Gray), + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ) + } + } else { + (Style::default(), Style::default()) + }; + Line::from(vec![ + Span::styled(box_char, box_style), + Span::raw(" "), + Span::styled(label, text_style), + ]) +} + +pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { + let p = Paragraph::new(Span::styled(label, style)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(p, area); +} +pub fn draw_buttons( + f: &mut ratatui::Frame, + area: Rect, + current_focus: Focus, + state: (bool, bool), + update_needed: bool, + downgrade_scenario: bool, + tos_or_privacy: bool, +) { + let cancel_text = if update_needed { + "[Q] Quit" + } else { + "[Q] Not now" + }; + let continue_text = if downgrade_scenario { + "Downgrade" + } else { + "Continue" + }; + let mut buttons = vec![ + (cancel_text, Focus::Cancel), + (continue_text, Focus::Continue), + ]; + if tos_or_privacy { + buttons.push(("Continue with Tensamin Services", Focus::ContinueAll)); + } + + let padding = 2; + let min_widths: Vec = buttons + .iter() + .map(|(label, _)| label.len() as u16 + padding) + .collect(); + + let widths = compute_widths(area.width, &min_widths); + + let mut x = area.x; + + for ((label, focus), width) in buttons.iter().zip(widths) { + let chunk = Rect { + x, + y: area.y, + width, + height: area.height, + }; + x += width; + + let is_focused = current_focus == *focus; + + let style = match focus { + Focus::Cancel => { + if is_focused { + Style::default() + .fg(Color::Black) + .bg(Color::Red) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Red) + } + } + + Focus::Continue => { + if is_focused && state.0 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.0 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + + Focus::ContinueAll => { + if is_focused && state.1 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.1 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + + _ => Style::default().fg(Color::DarkGray), + }; + + draw_button(f, chunk, label, style); + } +} +pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec { + let mut widths = vec![0; min_widths.len()]; + let mut remaining: Vec = (0..min_widths.len()).collect(); + + let mut remaining_width = area_width; + + while !remaining.is_empty() { + let count = remaining.len() as u16; + let equal = remaining_width / count; + + let mut clamped = Vec::new(); + + for &i in &remaining { + if min_widths[i] > equal { + widths[i] = min_widths[i]; + remaining_width -= min_widths[i]; + clamped.push(i); + } + } + + if clamped.is_empty() { + let mut remainder = remaining_width % count; + for &i in &remaining { + widths[i] = equal + + if remainder > 0 { + remainder -= 1; + 1 + } else { + 0 + }; + } + break; + } + + remaining.retain(|i| !clamped.contains(i)); + } + + widths +} diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs new file mode 100644 index 0000000..1cc6e0a --- /dev/null +++ b/src/terms/consent_state.rs @@ -0,0 +1,491 @@ +use tokio::sync::oneshot; + +use crate::{ + gui::{ + screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen}, + ui::UI, + }, + terms::{ + doc::Doc, + terms_getter::{Type, get_current_docs, get_newest_docs}, + }, + util::file_util::{load_file, save_file}, +}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub async fn check(ui: Arc) -> (bool, bool) { + let mut state = ConsentState::load_state(); + + if ensure_initial_consent(ui.clone(), &mut state) + .await + .is_err() + { + return (false, false); + } + if ensure_updates(ui, &mut state).await.is_err() { + return (false, false); + }; + + state = state.sanitize(); + state.save_state(); + + (state.accepted_eula, state.accepted_tos && state.accepted_pp) +} + +async fn ensure_initial_consent(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { + if state.accepted_eula { + return Ok(()); + } + + let (tx, rx) = oneshot::channel(); + + ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx)))) + .await; + + let result = rx.await.unwrap_or(UserChoice::Deny); + + match result { + UserChoice::AcceptEULA | UserChoice::AcceptAll => { + if let Some((eula, tos, privacy)) = get_current_docs().await { + state.accepted_eula = true; + state.eula = Some(eula); + + if matches!(result, UserChoice::AcceptAll) { + state.accepted_tos = true; + state.accepted_pp = true; + state.tos = Some(tos); + state.privacy = Some(privacy); + } + } + + let _ = &state.save_state(); + Ok(()) + } + UserChoice::Deny => Err(()), + } +} +async fn ensure_updates(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { + let Some((eula_update, tos_update, privacy_update)) = get_updates().await else { + return Ok(()); + }; + + let is_forced = matches!(eula_update, UpdateDecision::Forced(_)) + || matches!(tos_update, UpdateDecision::Forced(_)) + || matches!(privacy_update, UpdateDecision::Forced(_)); + + let (tx, rx) = oneshot::channel(); + + ui.set_screen(Box::new(TermsUpdaterScreen::new( + eula_update.clone(), + tos_update.clone(), + privacy_update.clone(), + Some(tx), + ))) + .await; + + let result = rx.await.unwrap_or(UserChoice::Deny); + + if is_forced { + match result { + UserChoice::AcceptAll => { + state.accepted_eula = true; + state.accepted_tos = true; + state.accepted_pp = true; + } + UserChoice::AcceptEULA => { + state.accepted_eula = true; + } + UserChoice::Deny => return Err(()), + } + } else { + apply_future_updates(state, result, eula_update, tos_update, privacy_update); + } + + state.save_state(); + Ok(()) +} +fn apply_future_updates( + state: &mut ConsentState, + result: UserChoice, + eula_update: UpdateDecision, + tos_update: UpdateDecision, + privacy_update: UpdateDecision, +) { + match result { + UserChoice::AcceptAll => { + if let UpdateDecision::Future { newest } = eula_update { + state.future_eula = Some(newest); + } + if let UpdateDecision::Future { newest } = tos_update { + state.future_tos = Some(newest); + } + if let UpdateDecision::Future { newest } = privacy_update { + state.future_privacy = Some(newest); + } + } + UserChoice::AcceptEULA => { + if let UpdateDecision::Future { newest } = eula_update { + state.future_eula = Some(newest); + } + } + UserChoice::Deny => {} + } +} + +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 + UpdateDecision, + UpdateDecision, + UpdateDecision, +)> { + if let ( + Some((current_eula, current_tos, current_privacy)), + Some((newest_eula, newest_tos, newest_privacy)), + ) = (get_current_docs().await, get_newest_docs().await) + { + let file = load_file("", "agreements"); + let accepted_state = ConsentState::from_str(&file).sanitize(); + save_file("", "agreements", &accepted_state.to_string()); + + let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) { + if current_eula.equals(&newest_eula) { + UpdateDecision::NoChange + } else { + if newest_eula.equals_some(&accepted_state.future_eula) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { + newest: newest_eula, + } + } + } + } else if newest_eula.equals_some(&accepted_state.eula) { + UpdateDecision::NoChange + } else { + UpdateDecision::Forced(current_eula) + }; + + let tos_update: UpdateDecision = + if !accepted_state.accepted_tos || newest_tos.equals_some(&accepted_state.tos) { + UpdateDecision::NoChange + } else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) { + if current_tos.equals(&newest_tos) { + UpdateDecision::NoChange + } else { + if newest_tos.equals_some(&accepted_state.future_tos) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { newest: newest_tos } + } + } + } else { + UpdateDecision::Forced(current_tos) + }; + + let privacy_update: UpdateDecision = if !accepted_state.accepted_pp + || newest_privacy.equals_some(&accepted_state.privacy) + { + UpdateDecision::NoChange + } else if accepted_state.accepted_pp && current_privacy.equals_some(&accepted_state.privacy) + { + if current_privacy.equals(&newest_privacy) { + UpdateDecision::NoChange + } else { + if newest_privacy.equals_some(&accepted_state.future_privacy) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { + newest: newest_privacy, + } + } + } + } else { + UpdateDecision::Forced(current_privacy) + }; + match (&eula_update, &tos_update, &privacy_update) { + (&UpdateDecision::NoChange, &UpdateDecision::NoChange, &UpdateDecision::NoChange) => { + None + } + _ => Some((eula_update, tos_update, privacy_update)), + } + } else { + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UserChoice { + Deny, + AcceptEULA, + AcceptAll, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UpdateDecision { + NoChange, + Future { newest: Doc }, + Forced(Doc), +} + +#[derive(Debug, Clone)] +pub struct ConsentState { + pub eula: Option, + pub accepted_eula: bool, + pub future_eula: Option, + + pub tos: Option, + pub accepted_tos: bool, + pub future_tos: Option, + + pub privacy: Option, + pub accepted_pp: bool, + pub future_privacy: Option, +} + +impl ConsentState { + fn sanitize(mut self) -> Self { + let current_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(future_eula) = self.future_eula.clone() { + if future_eula.get_time() < current_secs { + self.eula = Some(future_eula); + self.future_eula = None; + } + } + if let Some(future_tos) = self.future_tos.clone() { + if future_tos.get_time() < current_secs { + self.tos = Some(future_tos); + self.future_tos = None; + } + } + if let Some(future_privacy) = self.future_privacy.clone() { + if future_privacy.get_time() < current_secs { + self.privacy = Some(future_privacy); + self.future_privacy = None; + } + } + + if !self.accepted_eula { + self.accepted_tos = false; + self.accepted_pp = false; + } + self + } + + pub fn load_state() -> ConsentState { + let file = load_file("", "agreements"); + ConsentState::from_str(&file).sanitize() + } + + pub fn save_state(&self) { + save_file("", "agreements", &self.to_string()); + } + + fn to_string(&self) -> String { + let current_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let mut file_out: String = format!( + "This 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={}", + current_secs + ); + + if let Some(eula) = &self.eula { + 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={}\ + \nEULA-VERSION={}\ + \nEULA-HASH={}\ + ", self.accepted_eula, eula.get_version(), eula.get_hash())); + + if self.accepted_tos + && let Some(tos) = &self.tos + { + file_out.push_str(&format!("\ + \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.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/\ + \nPrivacy-Policy={}\ + \nPrivacy-Policy-VERSION={}\ + \nPrivacy-Policy-HASH={}\ + ", self.accepted_pp, pp.get_version(), pp.get_hash())); + } + } else { + file_out.push_str("\ + \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\ + \nEULA=false\ + "); + } + + if let Some(eula) = &self.future_eula { + file_out.push_str(&format!( + "\ + \nFUTURE-EULA-VERSION={}\ + \nFUTURE-EULA-HASH={}\ + \nFUTURE-EULA-TIME={}\ + ", + eula.get_version(), + eula.get_hash(), + eula.get_time() + )); + } + if let Some(tos) = &self.future_tos { + file_out.push_str(&format!( + "\ + \nFUTURE-Terms-of-Service-VERSION={}\ + \nFUTURE-Terms-of-Service-HASH={}\ + \nFUTURE-Terms-of-Service-TIME={}\ + ", + tos.get_version(), + tos.get_hash(), + tos.get_time() + )); + } + if let Some(pp) = &self.future_privacy { + file_out.push_str(&format!( + "\ + \nFUTURE-Privacy-Policy-VERSION={}\ + \nFUTURE-Privacy-Policy-HASH={}\ + \nFUTURE-Privacy-Policy-TIME={}\ + ", + pp.get_version(), + pp.get_hash(), + pp.get_time() + )); + } + + file_out + } + + fn from_str(s: &str) -> Self { + let mut eula = false; + let mut eula_version = String::new(); + let mut eula_hash = String::new(); + let mut pp = false; + let mut pp_version = String::new(); + let mut pp_hash = String::new(); + let mut tos = false; + let mut tos_version = String::new(); + let mut tos_hash = String::new(); + + let mut future_eula_version = String::new(); + let mut future_eula_hash = String::new(); + let mut future_eula_time = String::new(); + + let mut future_tos_version = String::new(); + let mut future_tos_hash = String::new(); + let mut future_tos_time = String::new(); + + let mut future_pp_version = String::new(); + let mut future_pp_hash = String::new(); + let mut future_pp_time = String::new(); + + let mut unix = String::new(); + + for line in s.lines() { + if let Some(v) = line.strip_prefix("EULA=") { + eula = v == "true"; + } else if let Some(v) = line.strip_prefix("EULA-VERSION=") { + 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("Terms-of-Service=") { + tos = v == "true"; + } 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("Terms-of-Service-HASH=") { + tos_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("Privacy-Policy=") { + pp = v == "true"; + } else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") { + pp_version = v.to_string(); + } 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-SECOND=") { + unix = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") { + future_eula_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-HASH=") { + future_eula_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-TIME=") { + future_eula_time = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-VERSION=") { + future_tos_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-HASH=") { + future_tos_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-TIME=") { + future_tos_time = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-VERSION=") { + future_pp_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-HASH=") { + future_pp_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-TIME=") { + future_pp_time = v.to_string(); + } + } + let unix: u64 = unix.parse::().unwrap_or(0); + let future_eula_time = future_eula_time.parse::().unwrap_or(0); + let future_tos_time = future_tos_time.parse::().unwrap_or(0); + let future_pp_time = future_pp_time.parse::().unwrap_or(0); + let state = Self { + accepted_eula: eula, + eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), + accepted_pp: pp, + 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)), + future_eula: if !future_eula_version.is_empty() { + Some(Doc::new( + future_eula_version, + future_eula_hash, + Type::EULA, + future_eula_time, + )) + } else { + None + }, + future_tos: if !future_tos_version.is_empty() { + Some(Doc::new( + future_tos_version, + future_tos_hash, + Type::TOS, + future_tos_time, + )) + } else { + None + }, + future_privacy: if !future_pp_version.is_empty() { + Some(Doc::new( + future_pp_version, + future_pp_hash, + Type::PP, + future_pp_time, + )) + } else { + None + }, + } + .sanitize(); + + state + } +} diff --git a/src/terms/doc.rs b/src/terms/doc.rs new file mode 100644 index 0000000..2365ba3 --- /dev/null +++ b/src/terms/doc.rs @@ -0,0 +1,73 @@ +use json::{JsonValue, object::Object}; + +use crate::{terms::terms_getter::Type, util::file_util::load_file}; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[allow(unused)] +pub struct Doc { + version: String, + hash: String, + pub doc_type: Type, + timestamp: u64, +} + +#[allow(dead_code)] +impl Doc { + pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc { + Doc { + version, + hash, + doc_type, + timestamp, + } + } + + pub fn equals_some(&self, other: &Option) -> bool { + if let Some(other) = other { + self.get_version() == other.get_version() && self.get_hash() == other.get_hash() + } else { + false + } + } + pub fn equals(&self, other: &Self) -> bool { + self.get_version() == other.get_version() && self.get_hash() == other.get_hash() + } + + 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 { + 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, + }) + } +} diff --git a/src/terms/focus.rs b/src/terms/focus.rs new file mode 100644 index 0000000..669e936 --- /dev/null +++ b/src/terms/focus.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Focus { + Eula, + Tos, + Pp, + Cancel, + Continue, + ContinueAll, +} + +impl Focus { + pub fn next(&mut self, order: &[Focus]) { + if let Some(pos) = order.iter().position(|&f| f == *self) { + let next_pos = (pos + 1) % order.len(); + *self = order[next_pos]; + } + } + + pub fn prev(&mut self, order: &[Focus]) { + if let Some(pos) = order.iter().position(|&f| f == *self) { + let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 }; + *self = order[prev_pos]; + } + } +} diff --git a/src/terms/mod.rs b/src/terms/mod.rs new file mode 100644 index 0000000..4a11008 --- /dev/null +++ b/src/terms/mod.rs @@ -0,0 +1,5 @@ +pub mod buttons; +pub mod consent_state; +pub mod doc; +pub mod focus; +pub mod terms_getter; diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs new file mode 100755 index 0000000..a0fa53b --- /dev/null +++ b/src/terms/terms_getter.rs @@ -0,0 +1,105 @@ +use json::JsonValue::Object; + +use crate::terms::doc::Doc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Type { + EULA, + TOS, + PP, +} + +impl Type { + pub fn to_str(&self) -> &str { + match self { + Self::EULA => "eula", + Self::TOS => "tos", + Self::PP => "privacy", + } + } + pub fn to_string(&self) -> String { + match self { + Self::EULA => "End User License Agreement".to_string(), + Self::TOS => "Terms of Service".to_string(), + Self::PP => "Privacy Policy".to_string(), + } + } +} + +pub fn get_link(terms_type: Type) -> String { + format!("https://legal.tensamin.net/{}/", terms_type.to_str()) +} +pub fn get_newest_link(terms_type: Type) -> String { + format!("https://legal.tensamin.net/{}/newest/", 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_newest_docs() -> Option<(Doc, Doc, Doc)> { + let body = reqwest::get("https://legal.tensamin.net/api/newest/") + .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 { + let body = reqwest::get(format!( + "https://legal.tensamin.net/api/text/{}/", + terms_type.to_str() + )) + .await + .ok()? + .text() + .await + .ok()?; + + Some(body) +} diff --git a/src/users/contact.rs b/src/users/contact.rs new file mode 100644 index 0000000..632cea3 --- /dev/null +++ b/src/users/contact.rs @@ -0,0 +1,61 @@ +use json::{self, JsonValue, number::Number}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone)] +pub struct Contact { + pub user_id: i64, + pub user_name: Option, + pub last_message_at: Option, +} + +impl Default for Contact { + fn default() -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + Contact { + user_id: 0, + user_name: None, + last_message_at: Some(now), + } + } +} + +impl Contact { + pub fn new(user_id: i64) -> Self { + Contact { + user_id: user_id, + user_name: None, + last_message_at: None, + } + } + pub fn set_last_message_at(&mut self, p0: i64) { + self.last_message_at = Option::from(p0); + } + + pub fn to_json(&self) -> JsonValue { + let mut obj = JsonValue::new_object(); + obj["user_id"] = JsonValue::Number(Number::from(self.user_id)); + if let Some(name) = &self.user_name { + obj["user_name"] = JsonValue::from(name.as_str()); + } + if let Some(ts) = &self.last_message_at { + obj["last_message_at"] = JsonValue::Number(Number::from(*ts)); + } + obj + } + pub fn from_json(o: &JsonValue) -> Contact { + let user_id = o["user_id"].as_i64().unwrap_or(0); + + let user_name = o["user_name"].as_str().map(|s| s.to_string()); + + let last_message_at = o["last_message_at"].as_i64(); + + Contact { + user_id, + user_name, + last_message_at, + } + } +} diff --git a/src/users/mod.rs b/src/users/mod.rs new file mode 100644 index 0000000..aef5a02 --- /dev/null +++ b/src/users/mod.rs @@ -0,0 +1,4 @@ +pub mod contact; +pub mod user_community_util; +pub mod user_manager; +pub mod user_profile; diff --git a/src/users/user_community_util.rs b/src/users/user_community_util.rs new file mode 100644 index 0000000..45b036c --- /dev/null +++ b/src/users/user_community_util.rs @@ -0,0 +1,67 @@ +use crate::util::file_util::save_file; +use json::{self, Array, JsonValue}; +use std::fs; +use std::path::Path; + +pub struct UserCommunityUtil; + +impl UserCommunityUtil { + pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { + let file_path = format!("users/{}/", storage_owner); + let mut communities = Self::load_array(&file_path); + + let mut community = JsonValue::new_object(); + community["title"] = JsonValue::String(title); + community["address"] = JsonValue::String(address); + community["position"] = JsonValue::String(position); + + communities.push(community); + + save_file( + &file_path, + "communities.json", + &JsonValue::Array(communities).to_string(), + ); + } + + pub fn remove_community(storage_owner: i64, community_address: String) { + let file_path = format!("users/{}/", storage_owner); + let communities = Self::load_array(&file_path); + + let filtered: Array = communities + .iter() + .filter(|entry| entry["address"].as_str() != Some(&community_address)) + .cloned() + .collect(); + save_file( + &file_path, + "communities.json", + &JsonValue::Array(filtered).to_string(), + ); + } + + pub fn get_communities(storage_owner: i64) -> Array { + let file_path = format!("users/{}/communities.json", storage_owner); + Self::load_array(&file_path) + } + + fn load_array(file_path: &str) -> Array { + if !Path::new(file_path).exists() { + return Array::new(); + } + + match fs::read_to_string(file_path) { + Ok(content) => { + let parsed = json::parse(&content); + match parsed { + Ok(JsonValue::Array(arr)) => arr, + _ => Array::new(), + } + } + Err(err) => { + eprintln!("Failed to read file {}: {}", file_path, err); + Array::new() + } + } + } +} diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs new file mode 100644 index 0000000..bcb5e26 --- /dev/null +++ b/src/users/user_manager.rs @@ -0,0 +1,197 @@ +use crate::omikron::omikron_connection::OMIKRON_CONNECTION; +use crate::users::user_profile::UserProfile; +use crate::util::crypto_helper::{self, public_key_to_base64}; +use crate::util::file_util::{load_file, save_file}; +use crate::util::logger::PrintType; +use crate::{RELOAD, SHUTDOWN}; +use crate::{log, log_cv}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use hex::{self}; +use json::JsonValue; +use once_cell::sync::Lazy; +use rand::Rng; +use rand_core::OsRng; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use std::io::{self}; +use std::sync::Mutex; +use std::time::Duration; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use x448::{PublicKey, Secret}; + +static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); +static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); + +#[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::>(); + let uuid = segments[0].parse::().unwrap_or(0); + let b64_private_key = segments[1]; + + let secret: Secret = crypto_helper::load_secret_key(b64_private_key).unwrap(); + let public_key = PublicKey::from(&secret); + + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let reset_token = STANDARD.encode(&bytes); + + let user_profile = UserProfile::new( + uuid, + username.to_string(), + Some(username.to_string()), + crypto_helper::public_key_to_base64(&public_key), + crypto_helper::hex_hash(b64_private_key), + reset_token, + ); + USERS.lock().unwrap().push(user_profile); + Ok(()) +} + +pub async fn create_user(username: &str) -> (Option, Option) { + let register_cv = CommunicationValue::new(CommunicationType::get_register); + + let conn = OMIKRON_CONNECTION.clone(); + + let response_cv = match conn + .await_response(®ister_cv, Some(Duration::from_secs(20))) + .await + { + Ok(cv) => cv, + Err(_) => return (None, None), + }; + log_cv!(PrintType::Omega, response_cv); + + let user_id = match response_cv.get_data(DataTypes::user_id).as_number() { + Some(id) => id, + None => return (None, None), + }; + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + + let mut hasher = Sha256::new(); + hasher.update(&STANDARD.encode(&private_key.as_bytes()).as_bytes()); + let result = hasher.finalize(); + let private_key_hash = hex::encode(result); + + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let reset_token = STANDARD.encode(&bytes); + + let up = UserProfile::new( + user_id, + username.to_string(), + None, + STANDARD.encode(&public_key.as_bytes()), + private_key_hash, + reset_token.clone(), + ); + + let cv = CommunicationValue::new(CommunicationType::complete_register_user) + .add_data(DataTypes::user_id, DataValue::Number(user_id)) + .add_data(DataTypes::username, DataValue::Str(username.to_string())) + .add_data( + DataTypes::public_key, + DataValue::Str(public_key_to_base64(&public_key)), + ) + .add_data(DataTypes::iota_id, DataValue::Number(user_id)) + .add_data(DataTypes::reset_token, DataValue::Str(reset_token)); + + let response_cv = conn + .await_response(&cv, Some(Duration::from_secs(20))) + .await; + + if let Ok(resp) = response_cv { + log_cv!(PrintType::Omega, resp); + if !resp.is_type(CommunicationType::success) { + return (None, None); + } + } else { + return (None, None); + } + *SHUTDOWN.write().await = true; + *RELOAD.write().await = true; + log!("Created User"); + save_file( + "", + &format!("{}.tu", username), + &format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())), + ); + + USERS.lock().unwrap().push(up.clone()); + save_users(); + (Some(up), Some(STANDARD.encode(&private_key.as_bytes()))) +} + +pub fn get_user_by_username(username: &str) -> Option { + USERS + .lock() + .unwrap() + .iter() + .cloned() + .find(|u| u.username == username) +} + +pub fn get_user(user_id: i64) -> Option { + USERS + .lock() + .unwrap() + .iter() + .cloned() + .find(|u| u.user_id == user_id) +} + +pub fn get_users() -> Vec { + USERS.lock().unwrap().clone() +} + +pub fn remove_user(user_id: i64) { + let mut users = USERS.lock().unwrap(); + users.retain(|u| u.user_id != user_id); + *UNIQUE.lock().unwrap() = true; +} + +pub fn save_users() { + *UNIQUE.lock().unwrap() = false; + let users = USERS.lock().unwrap(); + let arr: Vec = users.iter().map(|u| u.to_json()).collect(); + let json_str = JsonValue::Array(arr).dump(); + + save_file("", "users.json", &json_str); +} + +pub fn clear() { + let mut users = USERS.lock().unwrap(); + users.clear(); + *UNIQUE.lock().unwrap() = true; +} + +pub async fn load_users() -> io::Result<()> { + let content = load_file("", "users.json"); + if content.trim().is_empty() { + return Ok(()); + } + + let parsed = + json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + if let JsonValue::Array(arr) = parsed { + let mut users = USERS.lock().unwrap(); + for j in arr.iter() { + if let Some(up) = UserProfile::from_json(j).await { + users.push(up); + } + } + } + if *UNIQUE.lock().unwrap() { + save_users(); + } + Ok(()) +} + +#[allow(dead_code)] +pub fn set_unique(val: bool) { + *UNIQUE.lock().unwrap() = val; +} diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs new file mode 100644 index 0000000..5d60277 --- /dev/null +++ b/src/users/user_profile.rs @@ -0,0 +1,126 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::util::file_util::{has_file, load_file, used_dir_space}; +use base64::{Engine as _, engine::general_purpose}; +use json::{JsonValue, object}; +use rand::Rng; +use rand::rngs::OsRng; + +// --- UserProfile --- +#[derive(Clone, Debug)] +pub struct UserProfile { + pub user_id: i64, + pub username: String, + pub public_key: String, + pub private_key_hash: String, + pub reset_token: String, + pub created_at: i64, + pub display_name: Option, +} + +impl UserProfile { + pub fn new( + user_id: i64, + username: String, + display_name: Option, + public_key: String, + private_key_hash: String, + reset_token: String, + ) -> Self { + Self { + user_id, + username, + display_name, + public_key, + private_key_hash, + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + reset_token, + } + } + + pub fn to_json(&self) -> JsonValue { + let mut obj = object! { + "uuid" => self.user_id, + "username" => self.username.clone(), + "public_key" => self.public_key.clone(), + "private_key_hash" => self.private_key_hash.clone(), + "created_at" => self.created_at, + "reset_token" => self.reset_token.clone() + }; + if let Some(d) = &self.display_name { + obj["display_name"] = d.clone().into(); + } + obj + } + pub fn frontend(&self) -> JsonValue { + let mut obj = object! { + "uuid" => self.user_id, + "username" => self.username.clone(), + "public_key" => self.public_key.clone(), + "private_key_hash" => self.private_key_hash.clone(), + "created_at" => self.created_at, + "storage" => used_dir_space(&format!("users/{}", self.user_id.to_string())), + }; + if let Some(d) = &self.display_name { + obj["display_name"] = d.clone().into(); + } + if has_file("", &format!("{}.tu", self.username.clone())) { + obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into(); + } + + obj + } + pub async fn from_json(j: &JsonValue) -> Option { + let user_id = j["uuid"].as_i64()?; + let username = j["username"].as_str()?.to_string(); + let public_key = j["public_key"].as_str()?.to_string(); + let private_key_hash = j["private_key_hash"].as_str()?.to_string(); + let reset_token = j["reset_token"].as_str()?.to_string(); + let created_at = j["created_at"].as_i64()?; + let display_name = j["display_name"].as_str().map(|s| s.to_string()); + + let up = UserProfile { + user_id, + username, + display_name, + public_key, + private_key_hash, + created_at, + reset_token, + }; + + // TODO: Migrate to Omikron / Wss + /* if j.has_key("migrate") + || j.has_key("migrating") + || j.has_key("changing") + || j.has_key("move") + || j.has_key("moving") + { + if auth_connector::migrate_user(&mut up).await { + log_message(format!("[INFO] Migration triggered for {}", up.username)); + user_manager::set_unique(true); + } + } */ + + Some(up) + } + + #[allow(dead_code)] + pub fn randomize_reset_token(&mut self) -> String { + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let new_token = general_purpose::STANDARD.encode(&bytes); + self.reset_token = new_token.clone(); + new_token + } + + #[allow(dead_code)] + pub fn get_display_name(&self) -> String { + self.display_name + .clone() + .unwrap_or_else(|| self.username.clone()) + } +} diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs new file mode 100644 index 0000000..e54fe29 --- /dev/null +++ b/src/util/chat_files.rs @@ -0,0 +1,276 @@ +use crate::log; +use crate::util::db; +use json::{JsonValue, array, object}; +use rusqlite::params; +use std::io; +use std::sync::{Arc, LazyLock, Mutex}; + +#[derive(PartialEq, Debug, Clone)] +pub enum MessageState { + Read, + Received, + Sent, + Sending, +} + +impl MessageState { + pub fn as_str(&self) -> &'static str { + match self { + MessageState::Read => "read", + MessageState::Received => "received", + MessageState::Sent => "sent", + MessageState::Sending => "sending", + } + } + + pub fn from_str(value: &str) -> Self { + match value.to_lowercase().as_str() { + "read" => MessageState::Read, + "received" => MessageState::Received, + "sent" => MessageState::Sent, + _ => MessageState::Sending, + } + } + + pub fn upgrade(self, other: Self) -> Self { + if other == Self::Read || self == Self::Read { + Self::Read + } else if other == Self::Received || self == Self::Received { + Self::Received + } else if other == Self::Sent || self == Self::Sent { + Self::Sent + } else { + Self::Sending + } + } +} + +// Shared DB created via helper. +// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist. +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +pub fn add_message( + send_time: u128, + storage_owner_is_sender: bool, + storage_owner: i64, + external_user: i64, + message: &str, + height: i64, +) { + let message_time = match i64::try_from(send_time) { + Ok(v) => v, + Err(_) => { + log!("Failed to store message: send_time out of range for i64 ({send_time})"); + return; + } + }; + + // Insert the message into the DB + let insert_result = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO messages ( + storage_owner, + external_user, + message_time, + content, + sent_by_self, + message_state, + height + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + "#, + params![ + storage_owner, + external_user, + message_time, + message, + if storage_owner_is_sender { + 1_i64 + } else { + 0_i64 + }, + MessageState::Sending.as_str(), + height, + ], + )?; + Ok(()) + }); + + if let Err(e) = insert_result { + log!("Failed to insert message into sqlite: {}", e); + return; + } + + // Update contacts table to reflect that this conversation exists and has a recent message. + // Use the Contact helper to set last_message_at to the message timestamp. + let mut contact = crate::users::contact::Contact::new(external_user); + contact.set_last_message_at(message_time); + // This will insert or update the contact for the storage owner. + crate::util::chats_util::mod_user(storage_owner, &contact); +} + +pub fn change_message_state( + timestamp: i64, + storage_owner: i64, + external_user: i64, + new_state: MessageState, +) -> io::Result<()> { + // Run the SELECT and UPDATE inside with_conn to centralize connection access. + let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| { + let current: Option = match conn.query_row( + r#" + SELECT message_state + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + AND message_time = ?3 + ORDER BY id DESC + LIMIT 1 + "#, + params![storage_owner, external_user, timestamp], + |row| row.get(0), + ) { + Ok(state) => Some(state), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => return Err(e), + }; + + let Some(current_state_raw) = current else { + return Ok(()); + }; + + let upgraded = MessageState::from_str(¤t_state_raw) + .upgrade(new_state) + .as_str() + .to_string(); + + conn.execute( + r#" + UPDATE messages + SET message_state = ?1 + WHERE id = ( + SELECT id + FROM messages + WHERE storage_owner = ?2 + AND external_user = ?3 + AND message_time = ?4 + ORDER BY id DESC + LIMIT 1 + ) + "#, + params![upgraded, storage_owner, external_user, timestamp], + )?; + Ok(()) + }); + + match res { + Ok(_) => Ok(()), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), + } +} + +pub fn get_messages( + storage_owner: i64, + external_user: i64, + loaded_messages: i64, + amount: i64, +) -> JsonValue { + let messages = array![]; + + if amount <= 0 || loaded_messages < 0 { + return messages; + } + + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT + message_time, + content, + sent_by_self, + message_state, + height + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + ORDER BY message_time DESC, id DESC + LIMIT ?3 OFFSET ?4 + "#, + )?; + + let rows = stmt.query_map( + params![storage_owner, external_user, amount, loaded_messages], + |row| { + let message_time: i64 = row.get(0)?; + let content: String = row.get(1)?; + let sent_by_self: i64 = row.get(2)?; + let message_state: String = row.get(3)?; + let height: i64 = row.get(4).unwrap_or(0); + Ok((message_time, content, sent_by_self, message_state, height)) + }, + )?; + + let mut out = array![]; + for row in rows { + match row { + Ok((message_time, content, sent_by_self, message_state, height)) => { + let msg = object! { + "message_time" => message_time, + "content" => content, + "sent_by_self" => (sent_by_self != 0), + "message_state" => message_state, + "height" => height + }; + if let Err(e) = out.push(msg) { + // out.push returns a JsonError; log it instead of using `?` to avoid + // incompatible error conversions inside the DB closure. + log!("Failed to append message to output array: {:?}", e); + } + } + Err(e) => { + log!("Failed to read row from sqlite: {}", e); + } + } + } + Ok(out) + }); + + match res { + Ok(v) => v, + Err(e) => { + log!("Failed to query messages: {}", e); + messages + } + } +} + +#[cfg(test)] +mod tests { + use super::MessageState; + + #[test] + fn upgrade_prefers_highest_state() { + assert_eq!( + MessageState::Sending.upgrade(MessageState::Sent), + MessageState::Sent + ); + assert_eq!( + MessageState::Sent.upgrade(MessageState::Received), + MessageState::Received + ); + assert_eq!( + MessageState::Received.upgrade(MessageState::Read), + MessageState::Read + ); + } + + #[test] + fn from_str_is_case_insensitive() { + assert_eq!(MessageState::from_str("READ"), MessageState::Read); + assert_eq!(MessageState::from_str("received"), MessageState::Received); + assert_eq!(MessageState::from_str("Sent"), MessageState::Sent); + assert_eq!(MessageState::from_str("unknown"), MessageState::Sending); + } +} diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs new file mode 100644 index 0000000..b8e0ad2 --- /dev/null +++ b/src/util/chats_util.rs @@ -0,0 +1,121 @@ +use crate::users::contact::Contact; +use crate::util::db; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; + +/// Shared DB connection for contacts/messages (created by db helper). +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +/// Insert or update a contact for the given storage owner. +pub fn mod_user(storage_owner: i64, contact: &Contact) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO contacts ( + storage_owner, + user_id, + user_name, + last_message_at + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, user_id) DO UPDATE SET + user_name = excluded.user_name, + last_message_at = excluded.last_message_at + "#, + params![ + storage_owner, + contact.user_id, + contact.user_name.clone(), + contact.last_message_at + ], + )?; + Ok(()) + }) { + eprintln!("Failed to mod_user: {}", e); + } +} + +/// Retrieve a single contact for storage_owner/user_id. +pub fn get_user(storage_owner: i64, user_id: i64) -> Option { + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + match conn.query_row( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 AND user_id = ?2 + LIMIT 1 + "#, + params![storage_owner, user_id], + |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + }, + ) { + Ok(c) => Ok(Some(c)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + }); + + match res { + Ok(opt) => opt, + Err(e) => { + eprintln!("Error querying user in get_user: {}", e); + None + } + } +} + +/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc. +pub fn get_users(storage_owner: i64) -> Vec { + let contacts_out = Vec::new(); + + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 + ORDER BY + CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, + last_message_at DESC, + user_id ASC + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + })?; + + let mut out = Vec::new(); + for row in rows { + match row { + Ok(contact) => out.push(contact), + Err(e) => eprintln!("Failed to read contact row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to query contacts in get_users: {}", e); + contacts_out + } + } +} diff --git a/src/util/communities_util.rs b/src/util/communities_util.rs new file mode 100644 index 0000000..ed8b7ae --- /dev/null +++ b/src/util/communities_util.rs @@ -0,0 +1,90 @@ +use crate::util::db; +use json::Array; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; + +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +pub struct CommunitiesUtil; + +impl CommunitiesUtil { + pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO communities ( + storage_owner, + address, + title, + position + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, address) DO UPDATE SET + title = excluded.title, + position = excluded.position + "#, + params![storage_owner, address, title, position], + )?; + Ok(()) + }) { + eprintln!("Failed to add_community: {}", e); + } + } + + pub fn remove_community(storage_owner: i64, community_address: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", + params![storage_owner, community_address], + )?; + Ok(()) + }) { + eprintln!("Failed to remove_community: {}", e); + } + } + + pub fn get_communities(storage_owner: i64) -> Array { + let communities_out = Array::new(); + + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT address, title, position + FROM communities + WHERE storage_owner = ?1 + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { + let address: String = r.get(0)?; + let title: String = r.get(1)?; + let position: String = r.get(2)?; + Ok((address, title, position)) + })?; + + let mut out = Array::new(); + for row in rows { + match row { + Ok((address, title, position)) => { + let mut community = json::JsonValue::new_object(); + community["title"] = json::JsonValue::String(title); + community["address"] = json::JsonValue::String(address); + community["position"] = json::JsonValue::String(position); + out.push(community); + } + Err(e) => eprintln!("Failed to read community row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(arr) => arr, + Err(e) => { + eprintln!("Failed to query communities in get_communities: {}", e); + communities_out + } + } + } +} diff --git a/src/util/config_util.rs b/src/util/config_util.rs new file mode 100644 index 0000000..6330b59 --- /dev/null +++ b/src/util/config_util.rs @@ -0,0 +1,60 @@ +use crate::util::file_util::{load_file, save_file}; +use json::JsonValue; +use once_cell::sync::Lazy; +use tokio::sync::RwLock; + +pub static CONFIG: Lazy> = Lazy::new(|| RwLock::new(ConfigUtil::new())); + +pub struct ConfigUtil { + pub config: JsonValue, + pub unique: bool, +} + +impl ConfigUtil { + pub fn new() -> Self { + Self { + config: JsonValue::new_object(), + unique: false, + } + } + pub fn clear(&mut self) { + self.config = JsonValue::new_object(); + } + pub fn load(&mut self) { + let s = load_file("", "config.json"); + if !s.is_empty() { + self.config = json::parse(&s).unwrap_or(JsonValue::new_object()); + } + } + + pub fn get_iota_id(&self) -> i64 { + self.config["iota_id"].as_i64().unwrap_or(0) + } + + pub fn get_port(&self) -> u16 { + self.config["port"].as_u16().unwrap_or(1984) + } + + pub fn get_public_key(&self) -> Option { + self.config["public_key"].as_str().map(String::from) + } + + pub fn get_private_key(&self) -> Option { + self.config["private_key"].as_str().map(String::from) + } + + pub fn get(&self, key: &str) -> &JsonValue { + &self.config[key] + } + + pub fn change(&mut self, key: &str, value: JsonValue) { + self.config[key] = value; + self.unique = true; + } + + pub fn update(&mut self) { + if self.unique { + save_file("", "config.json", &self.config.to_string()); + } + } +} diff --git a/src/util/crypto_helper.rs b/src/util/crypto_helper.rs old mode 100755 new mode 100644 index cb3ae6a..4a7fd76 --- a/src/util/crypto_helper.rs +++ b/src/util/crypto_helper.rs @@ -1,149 +1,133 @@ -use aes_gcm::{ - Aes256Gcm, Nonce, - aead::{Aead, KeyInit, OsRng}, -}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use rand_core::RngCore; -use sha2::{Digest, Sha256}; -use x448::{PublicKey, Secret, SharedSecret}; - -/// Errors for crypto operations -#[allow(dead_code)] -#[derive(Debug)] -pub enum CryptoError { - Base64Decode(base64::DecodeError), - InvalidKey, - AgreementError, - EncryptionError(aes_gcm::Error), - DecryptionError(aes_gcm::Error), -} - -impl From for CryptoError { - fn from(err: base64::DecodeError) -> Self { - CryptoError::Base64Decode(err) - } -} - -#[allow(dead_code)] -pub struct KeyPair { - pub secret: Secret, - pub public: PublicKey, -} - -#[allow(dead_code)] -pub fn generate_keypair() -> KeyPair { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let secret = Secret::from_bytes(&buf).unwrap(); - let public = PublicKey::from(&secret); - KeyPair { secret, public } -} - -pub fn public_key_to_base64(pubkey: &PublicKey) -> String { - STANDARD.encode(pubkey.as_bytes().as_ref()) -} - -pub fn secret_key_to_base64(secret: &Secret) -> String { - STANDARD.encode(secret.as_bytes().as_ref()) -} - -pub fn load_public_key(base64_pub: &str) -> Option { - let bytes = STANDARD.decode(base64_pub).unwrap(); - PublicKey::from_bytes(&bytes) -} - -pub fn load_secret_key(base64_secret: &str) -> Option { - let bytes = STANDARD.decode(base64_secret).unwrap(); - Secret::from_bytes(&bytes) -} - -fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(shared.as_bytes()); - let result = hasher.finalize(); - let mut key = [0u8; 32]; - key.copy_from_slice(&result[..32]); - key -} - -#[allow(dead_code)] -pub fn encrypt_b64( - base64_secret: &str, - base64_peer_pub: &str, - plaintext: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - encrypt(secret, peer_pub, plaintext) -} -pub fn encrypt( - secret: Secret, - peer_pub: PublicKey, - plaintext: &str, -) -> Result { - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - let ciphertext = cipher - .encrypt(nonce, plaintext.as_bytes()) - .map_err(CryptoError::EncryptionError)?; - // prefix nonce to ciphertext - let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); - out.extend_from_slice(&nonce_bytes); - out.extend_from_slice(&ciphertext); - Ok(STANDARD.encode(&out)) -} - -pub fn decrypt_b64( - base64_secret: &str, - base64_peer_pub: &str, - encrypted_base64: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - decrypt(secret, peer_pub, encrypted_base64) -} -pub fn decrypt( - secret: Secret, - peer_pub: PublicKey, - encrypted_base64: &str, -) -> Result { - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - - let encrypted = STANDARD.decode(encrypted_base64)?; - if encrypted.len() < 12 { - return Err(CryptoError::DecryptionError(aes_gcm::Error)); - } - let nonce_bytes = &encrypted[..12]; - let ciphertext = &encrypted[12..]; - let nonce = Nonce::from_slice(nonce_bytes); - let plaintext_bytes = cipher - .decrypt(nonce, ciphertext) - .map_err(CryptoError::DecryptionError)?; - let plaintext = String::from_utf8(plaintext_bytes) - .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; - Ok(plaintext) -} - -#[allow(dead_code)] -pub fn hash_it(input: &str) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - hasher.finalize().to_vec() -} - -#[allow(dead_code)] -pub fn hex_hash(input: &str) -> String { - let digest = hash_it(input); - digest.iter().map(|b| format!("{:02x}", b)).collect() -} +use aes_gcm::{ + Aes256Gcm, Nonce, + aead::{Aead, KeyInit, OsRng}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use x448::{PublicKey, Secret, SharedSecret}; + +/// Errors for crypto opertions +#[derive(Debug)] +#[allow(dead_code)] +pub enum CryptoError { + Base64Decode(base64::DecodeError), + InvalidKey, + AgreementError, + EncryptionError(aes_gcm::Error), + DecryptionError(aes_gcm::Error), +} + +impl From for CryptoError { + fn from(err: base64::DecodeError) -> Self { + CryptoError::Base64Decode(err) + } +} + +pub struct KeyPair { + pub secret: Secret, + pub public: PublicKey, +} + +pub fn generate_keypair() -> KeyPair { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let secret = Secret::from_bytes(&buf).unwrap(); + let public = PublicKey::from(&secret); + KeyPair { secret, public } +} + +pub fn public_key_to_base64(pubkey: &PublicKey) -> String { + STANDARD.encode(pubkey.as_bytes().as_ref()) +} + +pub fn secret_key_to_base64(secret: &Secret) -> String { + STANDARD.encode(secret.as_bytes().as_ref()) +} + +pub fn load_public_key(base64_pub: &str) -> Option { + let bytes = STANDARD.decode(base64_pub).unwrap(); + PublicKey::from_bytes(&bytes) +} + +pub fn load_secret_key(base64_secret: &str) -> Option { + let bytes = STANDARD.decode(base64_secret).unwrap(); + 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()); + let result = hasher.finalize(); + let mut key = [0u8; 32]; + key.copy_from_slice(&result[..32]); + key +} + +#[allow(dead_code)] +pub fn encrypt( + base64_secret: &str, + base64_peer_pub: &str, + plaintext: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(CryptoError::EncryptionError)?; + // prefix nonce to ciphertext + let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + Ok(STANDARD.encode(&out)) +} + +#[allow(dead_code)] +pub fn decrypt( + base64_secret: &str, + base64_peer_pub: &str, + encrypted_base64: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + + let encrypted = STANDARD.decode(encrypted_base64)?; + if encrypted.len() < 12 { + return Err(CryptoError::DecryptionError(aes_gcm::Error)); + } + let nonce_bytes = &encrypted[..12]; + let ciphertext = &encrypted[12..]; + let nonce = Nonce::from_slice(nonce_bytes); + let plaintext_bytes = cipher + .decrypt(nonce, ciphertext) + .map_err(CryptoError::DecryptionError)?; + let plaintext = String::from_utf8(plaintext_bytes) + .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; + Ok(plaintext) +} + +pub fn hash_it(input: &str) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + hasher.finalize().to_vec() +} + +pub fn hex_hash(input: &str) -> String { + let digest = hash_it(input); + digest.iter().map(|b| format!("{:02x}", b)).collect() +} diff --git a/src/util/db.rs b/src/util/db.rs new file mode 100644 index 0000000..9fb40a0 --- /dev/null +++ b/src/util/db.rs @@ -0,0 +1,185 @@ +//! Database helper utilities. +//! +//! This module provides small helpers to open/init sqlite databases and to +//! create a shared (Arc>) connection wrapper callers can +//! reuse. The goal is to centralize the "open and initialize" logic and +//! provide small convenience helpers used by other util modules. + +use crate::util::file_util::get_directory; +use rusqlite::{Connection, Error as RusqliteError}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Returns the file path for a named DB inside the application's data directory. +/// +/// Arguments: +/// - `db_name` : name of the DB (without extension). Example: `"messages"`. +pub fn db_file_path(db_name: &str) -> String { + let mut p = PathBuf::from(get_directory()); + p.push(format!("{db_name}.sqlite3")); + p.to_string_lossy().to_string() +} + +/// Open a sqlite connection to the named DB file (no initialization). +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +pub fn open_connection(db_name: &str) -> Result { + let path = db_file_path(db_name); + Connection::open(path) +} + +/// Open a connection and immediately run `init_sql` via `execute_batch`. +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple). +pub fn open_and_init(db_name: &str, init_sql: &str) -> Result { + let conn = open_connection(db_name)?; + conn.execute_batch(init_sql)?; + Ok(conn) +} + +/// Create a shared, Arc> initialized with the given SQL. +/// +/// This is a convenience wrapper that returns an owned Arc> +/// so caller modules can store it in a `static` or pass it around. +/// +/// Arguments: +/// - `db_name`: DB name (without extension). +/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements). +pub fn create_shared_connection( + db_name: &str, + init_sql: &str, +) -> Result>, String> { + match open_and_init(db_name, init_sql) { + Ok(conn) => { + // Configure some sensible defaults for concurrency + // Attempt to set a busy timeout to reduce SQLITE_BUSY failures. + let _ = conn.busy_timeout(Duration::from_millis(250)); + Ok(Arc::new(Mutex::new(conn))) + } + Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)), + } +} + +/// Acquire the Connection from an Arc> and run the provided +/// closure. Converts rusqlite::Error into a String on error. +/// +/// Arguments: +/// - `shared`: Arc> +/// - `f`: closure that receives &Connection and returns Result +/// +/// Returns Ok(T) or Err(String). +pub fn with_conn(shared: &Arc>, f: F) -> Result +where + F: FnOnce(&Connection) -> Result, +{ + // When invoked from within an async runtime (such as Tokio), taking a blocking + // std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent + // awaits. Detect whether we're running inside a Tokio runtime and, if so, + // execute the blocking lock + database closure using Tokio's blocking helper. + // + // The blocking section returns Result so we can propagate errors + // in the same form as before. + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(|| { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + }) + } else { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + } +} + +/// Initialize a general-purpose messages+contacts DB and return a shared +/// connection. This helper creates a single DB file that can contain multiple +/// tables (messages, contacts, ...). The SQL here is conservative and intended +/// to be safe if called multiple times. +/// +/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)` +/// directly, but this convenience is useful for code that expects both tables. +pub fn create_general_messages_db() -> Result>, String> { + // Keep PRAGMA and schema in one multi-statement string so callers only + // need to call a single execute_batch. + const INIT_SQL: &str = r#" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + external_user INTEGER NOT NULL, + message_time INTEGER NOT NULL, + content TEXT NOT NULL, + sent_by_self INTEGER NOT NULL, + message_state TEXT NOT NULL, + height INTEGER NOT NULL DEFAULT 0 + ); + + CREATE INDEX IF NOT EXISTS idx_messages_lookup + ON messages (storage_owner, external_user, message_time DESC); + + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + user_id INTEGER NOT NULL, + user_name TEXT, + last_message_at INTEGER, + UNIQUE(storage_owner, user_id) + ); + + CREATE INDEX IF NOT EXISTS idx_contacts_owner + ON contacts (storage_owner, last_message_at DESC, user_id ASC); + + CREATE TABLE IF NOT EXISTS communities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + address TEXT NOT NULL, + title TEXT NOT NULL, + position TEXT NOT NULL, + UNIQUE(storage_owner, address) + ); + + CREATE INDEX IF NOT EXISTS idx_communities_owner + ON communities (storage_owner); + "#; + + match create_shared_connection("messages", INIT_SQL) { + Ok(shared_conn) => { + // Attempt to add the height column for backwards compatibility. + // This will fail if the column already exists, which is expected. + let _ = with_conn(&shared_conn, |conn| { + let _ = conn.execute( + "ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0", + [], + ); + Ok(()) + }); + Ok(shared_conn) + } + Err(e) => Err(e), + } +} + +/* +Example usage: + +// In some util module (at init time, e.g. lazy_static or LazyLock) +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + create_general_messages_db().expect("failed to create messages DB") +}); + +// Later, to run a query: +let res: Result, String> = with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare("SELECT ...")?; + let rows = stmt.query_map(...)?; + // collect and return Ok(...) +}); +*/ diff --git a/src/util/file_util.rs b/src/util/file_util.rs old mode 100644 new mode 100755 index cd214b1..2d00718 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -1,9 +1,15 @@ +use reqwest::Client; +use std::ffi::OsStr; use std::fs::{self, File}; use std::io::{self, BufReader, Read}; use std::path::{Path, PathBuf}; +use sysinfo::System; +use tokio::io::AsyncWriteExt; +use uuid::Uuid; +use walkdir::WalkDir; +use zip::ZipArchive; use crate::log; -use crate::util::logger::PrintType; #[allow(dead_code)] pub fn delete_directory(path: &str) -> bool { @@ -18,8 +24,6 @@ fn delete_dir_recursive(directory: &Path) -> bool { } if let Err(e) = fs::remove_dir_all(directory) { log!( - 0, - PrintType::General, "[IMPORTANT] Couldn't delete directory {}: {}", directory.display(), e, @@ -37,7 +41,6 @@ pub fn delete_user_directory(user_id: i64) { let _ = delete_dir_recursive(&user_dir); } -#[allow(dead_code)] pub fn load_file_buf(path: &str, name: &str) -> io::Result> { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); @@ -64,7 +67,6 @@ pub fn load_file_buf(path: &str, name: &str) -> io::Result> { let file = File::open(&file_path)?; Ok(BufReader::new(file)) } -#[allow(dead_code)] pub fn has_file(path: &str, name: &str) -> bool { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); @@ -79,7 +81,6 @@ pub fn has_file(path: &str, name: &str) -> bool { true } -#[allow(dead_code)] pub fn has_dir(path: &str) -> bool { let dir = Path::new(&get_directory()).join(path); @@ -90,19 +91,13 @@ pub fn has_dir(path: &str) -> bool { true } -#[allow(dead_code)] pub fn load_file(path: &str, name: &str) -> String { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log!( - 0, - PrintType::General, - "[IMPORTANT] Couldn't create directories: {}", - e - ); + log!("[IMPORTANT] Couldn't create directories: {}", e); return String::new(); } return String::new(); @@ -110,12 +105,7 @@ pub fn load_file(path: &str, name: &str) -> String { if !file_path.exists() { if let Err(e) = File::create(&file_path) { - log!( - 0, - PrintType::General, - "[IMPORTANT] Couldn't create file: {}", - e - ); + log!("[IMPORTANT] Couldn't create file: {}", e); } return String::new(); } @@ -134,27 +124,19 @@ pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> std::fs::read(file_path) } -#[allow(dead_code)] pub fn save_file(path: &str, name: &str, value: &str) { let dir = Path::new(&get_directory()).join(path); let file_path = dir.join(name); if !dir.exists() { if let Err(e) = fs::create_dir_all(&dir) { - log!( - 0, - PrintType::General, - "[IMPORTANT] Couldn't create directories: {}", - e - ); + log!("[IMPORTANT] Couldn't create directories: {}", e); return; } } if let Err(e) = fs::write(&file_path, value) { log!( - 0, - PrintType::General, "[IMPORTANT] Couldn't write file {}: {}", file_path.display(), e @@ -162,7 +144,6 @@ pub fn save_file(path: &str, name: &str, value: &str) { } } -#[allow(dead_code)] pub fn get_children(path: &str) -> Vec { let dir = Path::new(&get_directory()).join(path); let mut children = Vec::new(); @@ -183,3 +164,191 @@ pub fn get_directory() -> String { .to_string_lossy() .to_string() } + +// Helper to download the zip file content to a file on disk +#[allow(dead_code)] +pub fn used_space() -> u64 { + get_directory_size(&PathBuf::from(get_directory())) +} +pub fn used_dir_space(path: &str) -> u64 { + get_directory_size(&PathBuf::from(format!("{}/{}", get_directory(), path))) +} + +pub fn get_directory_size(directory: &Path) -> u64 { + let mut size = 0; + for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + if path.is_file() { + if let Ok(metadata) = path.metadata() { + size += path.file_name().unwrap_or(OsStr::new("")).len() as u64; + size += metadata.len(); + } + } + } + size +} + +#[allow(dead_code)] +pub fn get_designed_storage(user_id: i64) -> String { + let user_dir = Path::new(&get_directory()) + .join("users") + .join(user_id.to_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; + let m = k / 1024.0; + let g = m / 1024.0; + let t = g / 1024.0; + + if t >= 1.0 { + hr_size = format!("{:.2}TB", t); + } else if g >= 1.0 { + hr_size = format!("{:.2}GB", g); + } else if m >= 1.0 { + hr_size = format!("{:.2}MB", m); + } else if k >= 1.0 { + hr_size = format!("{:.2}KB", k); + } + hr_size +} + +#[allow(dead_code)] +pub fn get_used_ram() -> String { + let mut sys = System::new_all(); + sys.refresh_all(); + let used = sys.used_memory() * 1024; // kB to bytes + let total = sys.total_memory() * 1024; + format!("{}/{}", design_byte(used), design_byte(total)) +} + +pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box> { + let client = Client::new(); + let mut response = client.get(url).send().await?; + + if !response.status().is_success() { + let err_msg = format!("Failed to download file: Status {}", response.status()); + log!("{}", err_msg.clone()); + return Err(err_msg.into()); + } + + let mut zip_file = tokio::fs::File::create(zip_path).await?; + while let Some(chunk) = response.chunk().await? { + zip_file.write_all(&chunk).await?; + } + + zip_file.flush().await?; + Ok(()) +} + +#[allow(dead_code, deprecated)] +fn extract_zip_contents_to_folder( + zip_path: &Path, + target_dir: &Path, +) -> Result<(), Box> { + let file = File::open(zip_path)?; + let mut archive = ZipArchive::new(file)?; + + let staging_dir = target_dir.with_extension("staging"); + + let _ = fs::remove_dir_all(&staging_dir); + fs::create_dir_all(&staging_dir)?; + + let mut first_item_name: Option = None; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let entry_path = staging_dir.join(file.sanitized_name()); + + if i == 0 { + if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 { + first_item_name = Some(file.sanitized_name()); + } + } + + if file.name().ends_with('/') { + fs::create_dir_all(&entry_path)?; + } else { + if let Some(parent) = entry_path.parent() { + fs::create_dir_all(parent)?; + } + let mut out_file = File::create(entry_path)?; + io::copy(&mut file, &mut out_file)?; + } + } + + if let Some(root_path) = first_item_name { + let root_dir = staging_dir.join(&root_path); + + if root_dir.is_dir() { + let root_contents_count = fs::read_dir(&staging_dir)?.count(); + + if root_contents_count == 1 + || (root_contents_count > 1 && fs::metadata(&root_dir).is_ok()) + { + let _ = fs::remove_dir_all(target_dir); + fs::create_dir_all(target_dir)?; + + for entry in fs::read_dir(root_dir)? { + let entry = entry?; + let src = entry.path(); + let dest = target_dir.join(entry.file_name()); + + if let Err(_) = fs::rename(&src, &dest) { + if src.is_file() { + fs::copy(&src, &dest)?; + } else { + if entry.path().is_dir() { + fs::rename(&src, &dest)?; + } + } + } + } + + let _ = fs::remove_dir_all(&staging_dir); + return Ok(()); + } + } + } + + log!("Extracting directly (no single root folder detected)."); + let _ = fs::remove_dir_all(target_dir); + fs::rename(&staging_dir, target_dir)?; + + Ok(()) +} + +#[allow(dead_code)] +pub async fn download_and_extract_zip(url: &str, as_name: &str) { + log!("Downloading ZIP file..."); + let base_dir = PathBuf::from(get_directory()); + let zip_filename = format!("{}.zip", Uuid::new_v4()); + let zip_path = base_dir.join(&zip_filename); + let target_dir = base_dir.join(as_name); + + if let Err(e) = download_zip(url, &zip_path).await { + log!("Error downloading file: {}", e); + return; + } + + let zip_path_clone = zip_path.clone(); + let target_dir_clone = target_dir.clone(); + let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone); + + let successful = match extract_result { + Ok(()) => true, + Err(e) => { + log!("Error during ZIP extraction: {}", e); + false + } + }; + + if let Err(e) = tokio::fs::remove_file(&zip_path).await { + log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); + } else if successful { + log!("Downloaded and extracted ZIP file successfully."); + } +} diff --git a/src/util/logger.rs b/src/util/logger.rs old mode 100644 new mode 100755 index 9e9a085..a5feb02 --- a/src/util/logger.rs +++ b/src/util/logger.rs @@ -3,18 +3,24 @@ use std::{ fs::{self, OpenOptions}, io::Write, path::Path, - sync::{OnceLock, mpsc}, + sync::{OnceLock, atomic::Ordering, mpsc}, thread, time::{SystemTime, UNIX_EPOCH}, }; -use ansi_term::Color; +use ratatui::style::Color; use ttp_core::{CommunicationValue, DataTypes, DataValue}; +use crate::{ + APP_STATE, + gui::{elements::log_card::LogEntry, ui::UNIQUE}, + langu::language_manager, +}; + static LOGGER: OnceLock> = OnceLock::new(); -#[allow(dead_code)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[allow(unused)] pub enum PrintType { Call, Client, @@ -22,15 +28,30 @@ pub enum PrintType { Omikron, Omega, General, + Command, +} +impl PrintType { + pub fn prefix_color(self) -> Color { + match self { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } + } } struct LogMessage { timestamp_ms: u128, - sender: Option, - prefix: &'static str, + prefix: String, kind: PrintType, is_error: bool, - message: String, + translation_key: Option, + format_args: Vec, + message: Option, } pub fn startup() { @@ -54,34 +75,44 @@ pub fn startup() { .expect("Failed to open log file"); for msg in rx { - let ts = fixed_box(&msg.timestamp_ms.to_string(), 13); - let sender = match msg.sender { - Some(id) => fixed_box(&id.to_string(), 19), - _ => fixed_box("", 19), + let resolved_message = if let Some(key) = msg.translation_key { + let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); + language_manager::format(&key, &args) + } else { + msg.message.unwrap_or_default() }; - let line = format!("{} {} {} {}", ts, sender, msg.prefix, msg.message); + let timestamp = format_timestamp_inline(msg.timestamp_ms); + let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); - println!("{}", colorize(msg.kind, msg.is_error).paint(&line)); + let prefix = if msg.prefix.is_empty() { + String::new() + } else { + format!("{} ", msg.prefix) + }; - let _ = writeln!(file, "{}", line); + let _ = writeln!( + file, + "{} {}{}", + fixed_box(&msg.timestamp_ms.to_string(), 13), + prefix, + entry.message + ); + + let _ = writeln!(file, " {}", timestamp); + + let mut state = APP_STATE.lock().unwrap(); + state.push_log(entry.into()); } }); } -fn colorize(kind: PrintType, is_error: bool) -> Color { - if is_error { - return Color::Red; - } - - match kind { - PrintType::Call => Color::Purple, - PrintType::Client => Color::Green, - PrintType::Iota => Color::Yellow, - PrintType::Omikron => Color::Blue, - PrintType::Omega => Color::Cyan, - PrintType::General => Color::White, - } +fn format_timestamp_inline(timestamp_ms: u128) -> String { + let secs = (timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) } fn fixed_box(content: &str, width: usize) -> String { @@ -94,51 +125,172 @@ fn fixed_box(content: &str, width: usize) -> String { } } -pub fn log_internal( - sender: i64, - kind: PrintType, - prefix: &'static str, - is_error: bool, - message: String, -) { - let sender = if sender == 0 { None } else { Some(sender) }; +fn colorize(kind: PrintType, is_error: bool) -> Color { + if is_error { + return Color::Red; + } + match kind { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } +} + +pub fn log_internal_translated( + kind: PrintType, + prefix: String, + is_error: bool, + key: &str, + args: Vec, +) { if let Some(tx) = LOGGER.get() { + UNIQUE.store(true, Ordering::Relaxed); let _ = tx.send(LogMessage { timestamp_ms: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), - sender, prefix, kind, is_error, - message, + translation_key: Some(key.to_string()), + format_args: args, + message: None, }); } } + +pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { + if let Some(tx) = LOGGER.get() { + UNIQUE.store(true, Ordering::Relaxed); + let _ = tx.send(LogMessage { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + prefix, + kind, + is_error, + translation_key: None, + format_args: Vec::new(), + message: Some(message), + }); + } +} + +#[macro_export] +macro_rules! log_t { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + $key, + vec![$($arg),+] + ) + }; +} + +#[macro_export] +macro_rules! log_t_err { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + true, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + true, + $key, + vec![$($arg.to_string()),+] + ) + }; +} + +/// Log a command message. +#[macro_export] +macro_rules! log_command { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::Command, + "".to_string(), + false, + format!($($arg)*) + ) + }; +} + +/// Log a general informational message. #[macro_export] macro_rules! log { - ($sender: expr, $kind:expr, $($arg:tt)*) => { - $crate::util::logger::log_internal($sender, $kind, "", false, format!($($arg)*)) + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + format!($($arg)*) + ) }; } + +/// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { - ($sender: expr, $kind:expr, $($arg:tt)*) => { - $crate::util::logger::log_internal($sender, $kind, ">", false, format!($($arg)*)) + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">".to_string(), + false, + format!($($arg)*) + ) }; } + +/// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { - ($sender:expr, $kind:expr, $($arg:tt)*) => { - $crate::util::logger::log_internal($sender, $kind, "<", false, format!($($arg)*)) + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "<".to_string(), + false, + format!($($arg)*) + ) }; } + +/// Log an error message (`>>`). #[macro_export] macro_rules! log_err { - ($sender:expr, $kind:expr, $($arg:tt)*) => { - $crate::util::logger::log_internal($sender, $kind, ">>", true, format!($($arg)*)) + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">>".to_string(), + true, + format!($($arg)*) + ) }; } @@ -151,9 +303,8 @@ pub fn log_cv_internal( let formatted = format_cv(cv); log_internal( - cv.get_sender() as i64, print_type.unwrap_or(PrintType::General), - prefix, + prefix.to_string(), false, formatted, ); @@ -269,6 +420,7 @@ macro_rules! log_cv_in { $crate::util::logger::log_cv_internal("> ", &$cv, None) }; } + #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => { diff --git a/src/util/mod.rs b/src/util/mod.rs index 1897223..47901f9 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,9 @@ +pub mod chat_files; +pub mod chats_util; +pub mod communities_util; +pub mod config_util; pub mod crypto_helper; pub mod crypto_util; +pub mod db; pub mod file_util; pub mod logger; diff --git a/static/web/404.html b/static/web/404.html new file mode 100644 index 0000000..cb6efe1 --- /dev/null +++ b/static/web/404.html @@ -0,0 +1,4 @@ + + +

404

+ From 679a3db00c1db2f923d9ccdab75086f16f7ab830 Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:27:27 +0200 Subject: [PATCH 5/6] Nuke repo --- .cargo_toml_backup | 76 - .github/workflows/build.yml | 23 - .gitignore | 2 - Cargo.lock | 5061 ----------------- Cargo.toml | 56 - LICENSE | 16 - README.md | 1 - dockerfile | 20 - src/auth/auth_user.rs | 18 - src/auth/local_auth.rs | 18 - src/auth/mod.rs | 2 - src/communities/community.rs | 356 -- src/communities/community_connection.rs | 396 -- src/communities/community_manager.rs | 59 - src/communities/interactables/category.rs | 121 - src/communities/interactables/interactable.rs | 35 - src/communities/interactables/registry.rs | 75 - src/communities/interactables/text_chat.rs | 261 - src/communities/interactables/voice_chat.rs | 187 - src/communities/mod.rs | 13 - src/communities/perms/permission.rs | 27 - src/gui/app_state.rs | 201 - src/gui/elements/console_card.rs | 491 -- src/gui/elements/elements.rs | 49 - src/gui/elements/graph_card.rs | 215 - src/gui/elements/log_card.rs | 483 -- src/gui/input_handler.rs | 56 - src/gui/interaction_result.rs | 49 - src/gui/mod.rs | 20 - src/gui/screens/main_screen.rs | 242 - src/gui/screens/md_viewer.rs | 493 -- src/gui/screens/screens.rs | 25 - src/gui/screens/terms_checker.rs | 351 -- src/gui/screens/terms_updater.rs | 691 --- src/gui/tui.rs | 0 src/gui/ui.rs | 167 - src/gui/util/borders.rs | 63 - src/langu/language_creator.rs | 80 - src/langu/language_manager.rs | 105 - src/langu/mod.rs | 2 - src/main.rs | 183 - src/omikron/mod.rs | 2 - src/omikron/omikron_connection.rs | 1159 ---- src/omikron/ping_pong_task.rs | 38 - src/server/api.rs | 190 - src/server/mod.rs | 3 - src/server/server.rs | 169 - src/server/web_path_parser.rs | 77 - src/terms/buttons.rs | 175 - src/terms/consent_state.rs | 491 -- src/terms/doc.rs | 73 - src/terms/focus.rs | 25 - src/terms/mod.rs | 5 - src/terms/terms_getter.rs | 105 - src/users/contact.rs | 61 - src/users/mod.rs | 4 - src/users/user_community_util.rs | 67 - src/users/user_manager.rs | 197 - src/users/user_profile.rs | 126 - src/util/chat_files.rs | 276 - src/util/chats_util.rs | 121 - src/util/communities_util.rs | 90 - src/util/config_util.rs | 60 - src/util/crypto_helper.rs | 133 - src/util/crypto_util.rs | 178 - src/util/db.rs | 185 - src/util/file_util.rs | 354 -- src/util/logger.rs | 432 -- src/util/mod.rs | 9 - static/web/404.html | 4 - 70 files changed, 15598 deletions(-) delete mode 100644 .cargo_toml_backup delete mode 100644 .github/workflows/build.yml delete mode 100644 .gitignore delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 dockerfile delete mode 100644 src/auth/auth_user.rs delete mode 100644 src/auth/local_auth.rs delete mode 100644 src/auth/mod.rs delete mode 100644 src/communities/community.rs delete mode 100644 src/communities/community_connection.rs delete mode 100644 src/communities/community_manager.rs delete mode 100644 src/communities/interactables/category.rs delete mode 100644 src/communities/interactables/interactable.rs delete mode 100644 src/communities/interactables/registry.rs delete mode 100644 src/communities/interactables/text_chat.rs delete mode 100644 src/communities/interactables/voice_chat.rs delete mode 100644 src/communities/mod.rs delete mode 100644 src/communities/perms/permission.rs delete mode 100755 src/gui/app_state.rs delete mode 100644 src/gui/elements/console_card.rs delete mode 100644 src/gui/elements/elements.rs delete mode 100644 src/gui/elements/graph_card.rs delete mode 100755 src/gui/elements/log_card.rs delete mode 100644 src/gui/input_handler.rs delete mode 100644 src/gui/interaction_result.rs delete mode 100644 src/gui/mod.rs delete mode 100644 src/gui/screens/main_screen.rs delete mode 100644 src/gui/screens/md_viewer.rs delete mode 100644 src/gui/screens/screens.rs delete mode 100644 src/gui/screens/terms_checker.rs delete mode 100644 src/gui/screens/terms_updater.rs delete mode 100644 src/gui/tui.rs delete mode 100644 src/gui/ui.rs delete mode 100644 src/gui/util/borders.rs delete mode 100644 src/langu/language_creator.rs delete mode 100644 src/langu/language_manager.rs delete mode 100644 src/langu/mod.rs delete mode 100644 src/main.rs delete mode 100644 src/omikron/mod.rs delete mode 100755 src/omikron/omikron_connection.rs delete mode 100644 src/omikron/ping_pong_task.rs delete mode 100755 src/server/api.rs delete mode 100644 src/server/mod.rs delete mode 100644 src/server/server.rs delete mode 100755 src/server/web_path_parser.rs delete mode 100644 src/terms/buttons.rs delete mode 100644 src/terms/consent_state.rs delete mode 100644 src/terms/doc.rs delete mode 100644 src/terms/focus.rs delete mode 100644 src/terms/mod.rs delete mode 100755 src/terms/terms_getter.rs delete mode 100644 src/users/contact.rs delete mode 100644 src/users/mod.rs delete mode 100644 src/users/user_community_util.rs delete mode 100644 src/users/user_manager.rs delete mode 100644 src/users/user_profile.rs delete mode 100644 src/util/chat_files.rs delete mode 100644 src/util/chats_util.rs delete mode 100644 src/util/communities_util.rs delete mode 100644 src/util/config_util.rs delete mode 100644 src/util/crypto_helper.rs delete mode 100644 src/util/crypto_util.rs delete mode 100644 src/util/db.rs delete mode 100755 src/util/file_util.rs delete mode 100755 src/util/logger.rs delete mode 100644 src/util/mod.rs delete mode 100644 static/web/404.html diff --git a/.cargo_toml_backup b/.cargo_toml_backup deleted file mode 100644 index f7a9016..0000000 --- a/.cargo_toml_backup +++ /dev/null @@ -1,76 +0,0 @@ -[package] -name = "iota" -version = "0.1.0" -edition = "2024" - -[dependencies] -actix = "0.13.5" -actix-rt = "2.11.0" -actix-web = { version = "4.12.1", features = ["rustls-0_23"] } -actix-web-actors = "4.3.1" -aes-gcm = "*" -axum = { version = "0.8.8", features = ["ws", "http2"] } -base64 = "0.22.1" -bytes = "*" -cmake = "*" -color-eyre = "*" -crossterm = "*" -der = "*" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "*" -hmac = "*" -hyper-util = { version = "*" } -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -http-body-util = "*" -json = "*" -native-tls = { version = "*", default-features = false } -once_cell = "1.21.3" -pkcs8 = { version = "*", features = ["alloc"] } -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -reactive-rs = "*" -aws-lc-rs = "*" -reqwest = "0.12.23" -rustls = { version = "*", features = ["aws-lc-rs"] } -serde = { version = "1.0.219", features = ["derive"] } -sha2 = "*" -sys-info = "*" -sysinfo = "0.30" -tokio = { version = "1.48.0", features = ["full"] } -tokio-util = { version = "*", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tower = "*" -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -x509 = "*" -tokio-rustls = "0.26.4" -rustls-pemfile = "2.2.0" -async-trait = "0.1.89" -sha1 = "0.10.6" -async-tungstenite = "0.32.0" -zip = "6.0.0" -tui-textarea = "0.7.0" -tui-input = "0.14.0" -pnet = "0.35.0" -dashmap = "6.1.0" -strum = "0.27.2" -strum_macros = "0.27.2" -ratatui = "0.30.0" -ratatui_input = "0.1.3" -open = "5.3.3" -chrono = "0.4.43" -axum-server = { version = "0.8.0", features = ["tls-rustls"] } -serde_json = "1.0.149" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 4265c56..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Build & Publish Docker Image - -on: - workflow_dispatch: - -jobs: - build: - name: Build & Publish Docker Image - runs-on: ubuntu-latest - steps: - - name: Set up repository - uses: actions/checkout@v4 - - - name: Login - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PASSWD }} - - - name: Build & Push - run: | - docker build -t tensamin/iota:latest . - docker push tensamin/iota:latest diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0000aa1..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -target -logs diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index cd56dfb..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,5061 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" -dependencies = [ - "actix-rt", - "bitflags 2.11.0", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64", - "bitflags 2.11.0", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash 0.1.5", - "futures-core", - "h2 0.3.27", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash 0.1.5", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.3", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-actors" -version = "4.3.1+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" -dependencies = [ - "actix", - "actix-codec", - "actix-http", - "actix-web", - "bytes", - "bytestring", - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "asn1-rs" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compact_str" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csscolorparser" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" -dependencies = [ - "lab", - "phf", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "deflate64" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" - -[[package]] -name = "deltae" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" - -[[package]] -name = "der-parser" -version = "10.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "ed448-goldilocks" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" -dependencies = [ - "fiat-crypto", - "hex", - "subtle", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "fancy-regex" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" -dependencies = [ - "bit-set", - "regex", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "filedescriptor" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" -dependencies = [ - "libc", - "thiserror 1.0.69", - "winapi", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "finl_unicode" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", - "zlib-rs", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http 1.4.0", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http 1.4.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "httlib-huffman" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.4.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http 1.4.0", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.3", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling", - "indoc", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "iota" -version = "0.1.0" -dependencies = [ - "actix-web", - "actix-web-actors", - "aes-gcm", - "async-trait", - "base64", - "chrono", - "crossterm", - "dashmap", - "futures", - "futures-util", - "hex", - "hkdf", - "hyper", - "hyper-util", - "json", - "lazy_static", - "once_cell", - "open", - "pnet", - "rand 0.8.5", - "rand_core 0.6.4", - "ratatui", - "reqwest", - "rusqlite", - "rustls", - "rustls-pemfile", - "serde_json", - "sha2", - "strum 0.27.2", - "strum_macros 0.27.2", - "sysinfo", - "tokio", - "tokio-tungstenite", - "ttp-core", - "ttp-native", - "tungstenite", - "uuid", - "walkdir", - "warp", - "x448", - "zip", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "ipnetwork" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" -dependencies = [ - "serde", -] - -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[package]] -name = "kasuari" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" -dependencies = [ - "hashbrown 0.16.1", - "portable-atomic", - "thiserror 2.0.18", -] - -[[package]] -name = "lab" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - -[[package]] -name = "libc" -version = "0.2.184" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" - -[[package]] -name = "libsqlite3-sys" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "line-clipping" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lzma-rust2" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" -dependencies = [ - "crc", - "sha2", -] - -[[package]] -name = "mac_address" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" -dependencies = [ - "nix", - "winapi", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmem" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", - "memoffset", -] - -[[package]] -name = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "octets" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8311fa8ab7a57759b4ff1f851a3048d9ef0effaa0130726426b742d26d8a88e7" - -[[package]] -name = "oid-registry" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" -dependencies = [ - "asn1-rs", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openssl" -version = "0.10.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros", - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "pnet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" -dependencies = [ - "ipnetwork", - "pnet_base", - "pnet_datalink", - "pnet_packet", - "pnet_sys", - "pnet_transport", -] - -[[package]] -name = "pnet_base" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" -dependencies = [ - "no-std-net", -] - -[[package]] -name = "pnet_datalink" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" -dependencies = [ - "ipnetwork", - "libc", - "pnet_base", - "pnet_sys", - "winapi", -] - -[[package]] -name = "pnet_macros" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.117", -] - -[[package]] -name = "pnet_macros_support" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - -[[package]] -name = "pnet_sys" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "pnet_transport" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" -dependencies = [ - "libc", - "pnet_base", - "pnet_packet", - "pnet_sys", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppmd-rust" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.3", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.3", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "ratatui" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" -dependencies = [ - "instability", - "ratatui-core", - "ratatui-crossterm", - "ratatui-macros", - "ratatui-termwiz", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" -dependencies = [ - "bitflags 2.11.0", - "compact_str", - "hashbrown 0.16.1", - "indoc", - "itertools", - "kasuari", - "lru", - "strum 0.27.2", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-truncate", - "unicode-width", -] - -[[package]] -name = "ratatui-crossterm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" -dependencies = [ - "cfg-if", - "crossterm", - "instability", - "ratatui-core", -] - -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - -[[package]] -name = "ratatui-termwiz" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" -dependencies = [ - "ratatui-core", - "termwiz", -] - -[[package]] -name = "ratatui-widgets" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.16.1", - "indoc", - "instability", - "itertools", - "line-clipping", - "ratatui-core", - "strum 0.27.2", - "time", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "rcgen" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" -dependencies = [ - "aws-lc-rs", - "rustls-pki-types", - "time", - "x509-parser 0.18.1", - "yasna", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "h2 0.4.13", - "http 1.4.0", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "rsqlite-vfs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - -[[package]] -name = "rusqlite" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" -dependencies = [ - "bitflags 2.11.0", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", -] - -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom", - "phf", - "phf_codegen", -] - -[[package]] -name = "termios" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" -dependencies = [ - "libc", -] - -[[package]] -name = "termwiz" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" -dependencies = [ - "anyhow", - "base64", - "bitflags 2.11.0", - "fancy-regex", - "filedescriptor", - "finl_unicode", - "fixedbitset", - "hex", - "lazy_static", - "libc", - "log", - "memmem", - "nix", - "num-derive", - "num-traits", - "ordered-float", - "pest", - "pest_derive", - "phf", - "sha2", - "signal-hook", - "siphasher", - "terminfo", - "termios", - "thiserror 1.0.69", - "ucd-trie", - "unicode-segmentation", - "vtparse", - "wezterm-bidi", - "wezterm-blob-leases", - "wezterm-color-types", - "wezterm-dynamic", - "wezterm-input-types", - "winapi", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.3", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" -dependencies = [ - "futures-util", - "log", - "native-tls", - "tokio", - "tokio-native-tls", - "tungstenite", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.11.0", - "bytes", - "futures-util", - "http 1.4.0", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "ttp-core" -version = "0.1.0" -source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" -dependencies = [ - "base64", - "byteorder", - "rand 0.8.5", - "strum 0.28.0", - "strum_macros 0.28.0", -] - -[[package]] -name = "ttp-native" -version = "0.1.0" -source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" -dependencies = [ - "quinn", - "rustls", - "rustls-native-certs", - "thiserror 2.0.18", - "tokio", - "ttp-core", - "wtransport", -] - -[[package]] -name = "tungstenite" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" -dependencies = [ - "bytes", - "data-encoding", - "http 1.4.0", - "httparse", - "log", - "native-tls", - "rand 0.9.2", - "sha1", - "thiserror 2.0.18", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-truncate" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" -dependencies = [ - "itertools", - "unicode-segmentation", - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" -dependencies = [ - "atomic", - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vtparse" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "warp" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" -dependencies = [ - "bytes", - "futures-util", - "headers", - "http 1.4.0", - "http-body", - "http-body-util", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-util", - "tower-service", - "tracing", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "wezterm-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" -dependencies = [ - "log", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-blob-leases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" -dependencies = [ - "getrandom 0.3.4", - "mac_address", - "sha2", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "wezterm-color-types" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" -dependencies = [ - "csscolorparser", - "deltae", - "lazy_static", - "wezterm-dynamic", -] - -[[package]] -name = "wezterm-dynamic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" -dependencies = [ - "log", - "ordered-float", - "strsim", - "thiserror 1.0.69", - "wezterm-dynamic-derive", -] - -[[package]] -name = "wezterm-dynamic-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "wezterm-input-types" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" -dependencies = [ - "bitflags 1.3.2", - "euclid", - "lazy_static", - "serde", - "wezterm-dynamic", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wtransport" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56b611f195638f3790e4e5a41e9d777643a6c324ae4ffd1f0f53f2738e7678c" -dependencies = [ - "bytes", - "pem", - "quinn", - "rcgen", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "sha2", - "socket2 0.5.10", - "thiserror 2.0.18", - "time", - "tokio", - "tracing", - "url", - "wtransport-proto", - "x509-parser 0.17.0", -] - -[[package]] -name = "wtransport-proto" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1627c5b59450278e9771aab35275d72bfa2788128c197c5af1e4a820c8737ef4" -dependencies = [ - "httlib-huffman", - "octets", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "x448" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" -dependencies = [ - "ed448-goldilocks", - "hex", - "rand_core 0.5.1", -] - -[[package]] -name = "x509-parser" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" -dependencies = [ - "asn1-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "x509-parser" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" -dependencies = [ - "asn1-rs", - "aws-lc-rs", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "yasna" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" -dependencies = [ - "time", -] - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zip" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq", - "crc32fast", - "deflate64", - "flate2", - "getrandom 0.3.4", - "hmac", - "indexmap", - "lzma-rust2", - "memchr", - "pbkdf2", - "ppmd-rust", - "sha1", - "time", - "zeroize", - "zopfli", - "zstd", -] - -[[package]] -name = "zlib-rs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 8767b56..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "iota" -version = "0.1.0" -edition = "2024" - -[dependencies] -ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" } -ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" } - -actix-web = { version = "4", features = ["rustls-0_23"] } -actix-web-actors = "4" -aes-gcm = "0.10.3" -base64 = "0.22.1" -crossterm = "*" -futures = "*" -futures-util = "*" -hex = "*" -hkdf = "0.12.4" -hyper-util = { version = "*" } -hyper = { version = "1.8.1", features = [ - "capi", - "client", - "full", - "http1", - "http2", - "nightly", - "server", -] } -json = "*" -once_cell = "1.21.3" -rand = "0.8" -rand_core = { version = "0.6", features = ["getrandom", "std"] } -reqwest = "0.13.2" -rustls = { version = "0.23.37", features = ["aws-lc-rs"] } -sha2 = "0.10.9" -sysinfo = "0.38.3" -tokio = { version = "1.50.0", features = ["full"] } -tokio-tungstenite = { version = "*", features = ["native-tls"] } -tungstenite = "*" -uuid = { version = "*", features = ["v4"] } -walkdir = "2.5.0" -warp = "*" -x448 = { version = "*" } -rustls-pemfile = "2.2.0" -async-trait = "0.1.89" -zip = "6.0.0" -pnet = "0.35.0" -dashmap = "6.1.0" -strum = "0.27.2" -strum_macros = "0.27.2" -ratatui = "0.30.0" -open = "5.3.3" -chrono = "0.4.43" -serde_json = "1.0.149" -rusqlite = "0.39.0" -lazy_static = "1.5.0" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d218eff..0000000 --- a/LICENSE +++ /dev/null @@ -1,16 +0,0 @@ -Copyright (c) [2025] [Methanium] -All rights reserved. - -This software is protected by copyright. Copying, editing, -distributing, publicly performing, or any other use of this software -or its components, in source or binary form, is strictly prohibited without the express -written permission of the copyright holder. - -FUTURE LICENSE ACCEPTANCE: -It is the copyright holder's intention to release this software in the future -under a license yet to be defined, which will, among other things, -allow private, non-commercial use. This statement does not constitute -a current license grant and does not alter the above -prohibition on use, copying, or modification. Until the formal -publication of such a future license, all rights remain -reserved. diff --git a/README.md b/README.md deleted file mode 100644 index f7bc9c8..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️ diff --git a/dockerfile b/dockerfile deleted file mode 100644 index ee00add..0000000 --- a/dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -# Build stage -FROM rust:latest AS builder - -WORKDIR /app -COPY . . - -RUN cargo build --release - -# Runtime stage -FROM debian:sid - -WORKDIR /app - -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/iota . - -EXPOSE 1984 - -CMD ["./iota"] \ No newline at end of file diff --git a/src/auth/auth_user.rs b/src/auth/auth_user.rs deleted file mode 100644 index 8f763b6..0000000 --- a/src/auth/auth_user.rs +++ /dev/null @@ -1,18 +0,0 @@ -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct AuthUser { - pub id: i64, - pub username: String, - pub password_hash: String, - pub public_key: String, -} - -impl AuthUser { - pub fn new(id: i64, username: String, password_hash: String, public_key: String) -> Self { - AuthUser { - id, - username, - password_hash, - public_key, - } - } -} diff --git a/src/auth/local_auth.rs b/src/auth/local_auth.rs deleted file mode 100644 index 112ee2b..0000000 --- a/src/auth/local_auth.rs +++ /dev/null @@ -1,18 +0,0 @@ -use json::JsonValue; - -use crate::util::file_util::load_file; -// NOT USED AT MOMENT -pub fn is_private_key_valid(user_id: &i64, key_hash: &str) -> bool { - let file_contents = load_file("", "users.json"); - - let users = json::parse(&file_contents).unwrap(); - if let JsonValue::Array(users_array) = users { - for user in users_array { - if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash { - return true; - } - } - } - - false -} diff --git a/src/auth/mod.rs b/src/auth/mod.rs deleted file mode 100644 index 73d6442..0000000 --- a/src/auth/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod auth_user; -pub mod local_auth; diff --git a/src/communities/community.rs b/src/communities/community.rs deleted file mode 100644 index 835c88a..0000000 --- a/src/communities/community.rs +++ /dev/null @@ -1,356 +0,0 @@ -use crate::communities::interactables::category::Category; -use crate::communities::interactables::registry; -use crate::communities::perms::permission::Permission; -use crate::communities::{ - community_connection::CommunityConnection, interactables::interactable::Interactable, -}; -use crate::util::file_util; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use json::JsonValue; -use json::number::Number; -use json::object::Object; -use rand::RngCore; -use rand_core::OsRng; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use x448::{PublicKey, Secret}; -/// Permissions -// uuid -> interactable/path/like/this/interactable_name:permission -// -> role - -/// Roles -// rolename -> interactable/path/like/this/interactable_name:permission -// -> other/path/like/this/interactable_name:permission - -pub struct Community { - name: String, - owner_id: Arc>, - members: Vec, - permissions: HashMap>, - roles: HashMap>, - private_key: Secret, - public_key: PublicKey, - pub interactables: Arc>>>>, - pub connections: Arc>>>>, -} - -impl Community { - pub fn new() -> Self { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - Community { - name: String::new(), - owner_id: Arc::new(RwLock::new(0)), - members: Vec::new(), - permissions: HashMap::new(), - roles: HashMap::new(), - private_key, - public_key, - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - } - } - pub async fn create(name: String, owner_id: i64) -> Self { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - let c = Community { - name, - owner_id: Arc::new(RwLock::new(owner_id)), - members: Vec::new(), - permissions: HashMap::new(), - roles: HashMap::new(), - private_key, - public_key, - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - }; - c.save().await; - c - } - - pub async fn to_json(&self) -> JsonValue { - let mut json = JsonValue::new_object(); - json["name"] = self.name.clone().into(); - json["owner_id"] = (*self.owner_id.read().await as i64).into(); - json["members"] = self - .members - .clone() - .iter() - .map(|f| f.to_string()) - .collect::>() - .into(); - json["private_key"] = self.private_key.as_bytes().to_vec().into(); - json["public_key"] = self.public_key.as_bytes().to_vec().into(); - json["connections"] = self.connections.read().await.clone().len().into(); - json - } - - pub async fn frontend(&self) -> JsonValue { - let mut json = JsonValue::new_object(); - json["name"] = self.name.clone().into(); - json["owner_id"] = (*self.owner_id.read().await as i64).into(); - json["members"] = self - .members - .clone() - .iter() - .map(|f| f.to_string()) - .collect::>() - .into(); - json["public_key"] = self.public_key.as_bytes().to_vec().into(); - json["connections"] = self.connections.read().await.clone().len().into(); - json - } - - pub fn add_member(&mut self, member_id: i64) { - self.members.push(member_id); - } - - pub fn remove_member(&mut self, member_id: i64) { - self.members.retain(|id| *id != member_id); - } - pub fn get_name(&self) -> &str { - &self.name - } - pub async fn get_owner_id(&self) -> i64 { - *self.owner_id.read().await - } - pub fn get_members(&self) -> Vec { - self.members.clone() - } - pub fn get_private_key(&self) -> Secret { - Secret::from_bytes(self.private_key.as_bytes()).unwrap() - } - pub fn get_public_key(&self) -> &PublicKey { - &self.public_key - } - pub async fn set_owner(self: Arc, owner_id: i64) { - *self.owner_id.write().await = owner_id; - } - pub async fn add_connection(self: &Arc, other: Arc) { - let mut vec = self - .connections - .read() - .await - .get(&other.get_user_id().await) - .cloned() - .unwrap_or_default(); - vec.push(other.clone()); - self.connections - .write() - .await - .insert(other.get_user_id().await, vec); - } - pub async fn remove_connection(self: &Arc, other: Arc) { - let mut vec = self - .connections - .read() - .await - .get(&other.get_user_id().await) - .cloned() - .unwrap_or_default(); - vec.retain(|conn| !Arc::ptr_eq(conn, &other)); - self.connections - .write() - .await - .insert(other.get_user_id().await, vec); - } - pub async fn get_connections(&self) -> HashMap>> { - self.connections.read().await.clone() - } - pub async fn get_connections_for_user(&self, user_id: i64) -> Vec> { - self.connections - .read() - .await - .get(&user_id) - .cloned() - .unwrap_or_default() - } - pub async fn get_interactables( - &self, - _user_id: i64, - ) -> Vec>> { - self.interactables.read().await.clone() - } - pub async fn add_interactable(self: &mut Arc, interactable: Arc>) { - self.interactables.write().await.push(interactable); - } - pub async fn remove_interactable( - self: &mut Arc, - interactable: Arc>, - ) { - self.interactables - .write() - .await - .retain(|i| !Arc::ptr_eq(i, &interactable)); - } - pub async fn broadcast_message(&self, message: &CommunicationValue) { - for (_, conns) in self.connections.read().await.clone().iter() { - for user in conns.iter() { - user.send_message(message).await; - } - } - } - pub async fn run_function( - self: &mut Arc, - _user_id: i64, - name: &str, - path: &str, - _function: &str, - cv: &CommunicationValue, - ) -> CommunicationValue { - if path.is_empty() { - let target_interactables = &self.interactables.read().await.clone(); - for interactable in target_interactables.iter() { - if interactable.get_name() == name { - if interactable.get_codec() == "category" { - return CommunicationValue::new(CommunicationType::error); - } else { - // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) - return interactable.run_function(cv.clone()).await; - } - } - } - } else { - let target_interactables = &self.interactables.read().await.clone(); - for interactable in target_interactables.iter() { - if interactable.get_name() == name { - if interactable.get_codec() == "category" { - let category: &Category = - interactable.as_any().downcast_ref::().unwrap(); - // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) - return category - .get_child(path.to_string(), name.to_string()) - .unwrap() - .run_function(cv.clone()) - .await; - } else { - return CommunicationValue::new(CommunicationType::error); - } - } - } - } - CommunicationValue::new(CommunicationType::add_conversation) - } - - pub async fn save(&self) { - let mut json = Object::new(); - json.insert("name", JsonValue::String(self.name.clone())); - json.insert( - "owner_id", - JsonValue::Number(Number::from(*self.owner_id.read().await as i64)), - ); - - json.insert( - "private_key", - JsonValue::String(STANDARD.encode(&self.private_key.as_bytes())), - ); - json.insert( - "public_key", - JsonValue::String(STANDARD.encode(&self.public_key.as_bytes())), - ); - - file_util::save_file( - &format!("communities/{}/", self.name), - "config.json", - &json.dump(), - ); - - let mut user_data = Object::new(); - for user in self.members.iter() { - let mut data = JsonValue::new_object(); - - let mut permissions = JsonValue::new_array(); - for perm in self.permissions.get(user).unwrap() { - if let Ok(_) = permissions.push(perm.to_string()) {} - } - - if let Ok(_) = data.insert("permissions", permissions) {} - user_data.insert(&user.to_string(), data); - } - file_util::save_file( - &format!("communities/{}/", self.name), - "users.json", - &user_data.dump(), - ); - - for interactable in self.interactables.read().await.clone().iter() { - registry::save(interactable).await; - } - } -} -pub async fn load(name: &String) -> Option> { - let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json"); - let json_content = json::parse(&file_contents).unwrap(); - - let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json"); - let user_json: JsonValue = json::parse(&user_data).unwrap(); - let mut users = Vec::new(); - let mut permissions: HashMap> = HashMap::new(); - - for user in user_json.entries() { - let (id, json): (&str, &JsonValue) = user; - let perms_j = &json["permissions"]; - let perms = Vec::new(); - for _ in perms_j.entries() { - // let perm_j = i.as_str().unwrap(); - // perms.push(perm_j.to_string()); - } - - let user_id: i64 = id.parse().unwrap_or(0); - - users.push(user_id); - permissions.insert(user_id, perms); - } - - let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json"); - let roles: HashMap> = HashMap::new(); - if let Ok(_) = json::parse(&role_data) { - // Fill roles - } else { - return None; - }; - - let community = Community { - name: json_content["name"].as_str().unwrap().to_string(), - owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))), - members: users, - roles, - permissions, - private_key: Secret::from_bytes( - &STANDARD - .decode(json_content["private_key"].as_str().unwrap()) - .unwrap(), - ) - .unwrap(), - public_key: PublicKey::from( - &Secret::from_bytes( - &STANDARD - .decode(json_content["private_key"].as_str().unwrap()) - .unwrap(), - ) - .unwrap(), - ), - interactables: Arc::new(RwLock::new(Vec::new())), - connections: Arc::new(RwLock::new(HashMap::new())), - }; - let mut comarc = Arc::new(community); - - let interactable_files: Vec = - file_util::get_children(&format!("communities/{}/interactables/", name)); - for file in interactable_files { - if file.contains(".json") { - let name = file.split('.').next().unwrap().to_string(); - let interactable: Box = - registry::load(comarc.clone(), String::new(), name).await; - comarc.add_interactable(Arc::new(interactable)).await; - } - } - Some(comarc) -} diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs deleted file mode 100644 index 62df251..0000000 --- a/src/communities/community_connection.rs +++ /dev/null @@ -1,396 +0,0 @@ -use crate::auth::auth_user::AuthUser; -use crate::communities::community::Community; -use crate::communities::interactables::interactable::Interactable; -use crate::users::user_manager::get_user; -use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use futures::SinkExt; -use futures::stream::SplitSink; -use futures::stream::SplitStream; -use hkdf::Hkdf; -use hyper::upgrade::Upgraded; -use hyper_util::rt::TokioIo; -use json::JsonValue; -use json::number::Number; -use rand::{Rng, distributions::Alphanumeric}; -use sha2::Sha256; -use std::sync::Arc; -use tokio::sync::RwLock; -use tokio_tungstenite::WebSocketStream; -use tungstenite::Message; -use tungstenite::Utf8Bytes; -use uuid::Uuid; -use x448::PublicKey; -pub struct CommunityConnection { - pub sender: Arc>, Message>>>, - pub receiver: Arc>>>>, - pub user_id: Arc>, - pub community: Arc>>>, - identified: Arc>, - challenged: Arc>, - challenge: Arc>, - auth: Arc>>, - pub ping: Arc>, -} -impl CommunityConnection { - pub fn new( - sender: SplitSink>, Message>, - receiver: SplitStream>>, - community: Arc, - ) -> Arc { - Arc::new(Self { - sender: Arc::new(RwLock::new(sender)), - receiver: Arc::new(RwLock::new(receiver)), - user_id: Arc::new(RwLock::new(0)), - community: Arc::new(RwLock::new(Some(community))), - identified: Arc::new(RwLock::new(false)), - challenged: Arc::new(RwLock::new(false)), - challenge: Arc::new(RwLock::new(String::new())), - auth: Arc::new(RwLock::new(None)), - ping: Arc::new(RwLock::new(-1)), - }) - } - pub async fn send_message(&self, message: &CommunicationValue) { - let mut sender = self.sender.write().await; // Access the SplitSink - let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string())); - sender.send(message_text).await.unwrap(); // Send the message via the SplitSink - } - pub async fn get_community(&self) -> Option> { - self.community.read().await.clone() - } - pub async fn get_user_id(&self) -> i64 { - *self.user_id.read().await - } - pub async fn is_identified(&self) -> bool { - *self.identified.read().await && *self.challenged.read().await - } - - pub async fn handle_message(self: Arc, message: String) { - let mut cv = CommunicationValue::from_json(&message); - let user_id = self.get_user_id().await; - cv = cv.with_sender(user_id); - - if cv.is_type(CommunicationType::identification) && !self.is_identified().await { - self.handle_identification(cv).await; - return; - } - - if cv.is_type(CommunicationType::challenge_response) && !self.is_identified().await { - self.handle_challenge_response(cv).await; - return; - } - - if !self.is_identified().await { - return; - } - - if cv.is_type(CommunicationType::ping) { - self.handle_ping(cv).await; - return; - } - - if cv.is_type(CommunicationType::client_changed) { - //self.handle_client_changed(cv).await; - return; - } - - if cv.is_type(CommunicationType::function) { - self.handle_function(cv).await; - return; - } - } - async fn handle_function(&self, cv: CommunicationValue) { - let name = cv.get_data(DataTypes::name).unwrap().as_str().unwrap(); - let path = cv.get_data(DataTypes::path).unwrap().as_str().unwrap(); - let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); - - let result = self - .get_community() - .await - .unwrap() - .run_function(self.get_user_id().await, name, path, function, &cv) - .await; - - self.send_message(&result).await; - } - async fn handle_identification(&self, cv: CommunicationValue) { - let user_id = cv - .get_data(DataTypes::user_id) - .unwrap_or(&JsonValue::Number(Number::from(0))) - .as_i64() - .unwrap_or(0); - - let Some(user) = get_user(user_id) else { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - }; - - { - let mut auth_guard = self.auth.write().await; - //*auth_guard = Some(user.clone()); - - let mut user_id_guard = self.user_id.write().await; - *user_id_guard = user_id; - - let mut identified_guard = self.identified.write().await; - *identified_guard = true; - } - - let challenge_str: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect(); - - { - let mut challenge_guard = self.challenge.write().await; - *challenge_guard = challenge_str.clone(); - } - - let user_public_key_bytes = match STANDARD.decode(&user.public_key) { - Ok(bytes) => bytes, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - } - }; - - let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { - Some(key) => key, - __ => { - self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) - .await; - return; - } - }; - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let community_private_key = community.get_private_key(); - let community_public_key = community.get_public_key(); - - let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { - Some(secret) => secret, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let aes_key = { - let hk = Hkdf::::new(None, shared_secret.as_bytes()); - let mut key_bytes = [0u8; 32]; - hk.expand(b"challenge", &mut key_bytes) - .expect("HKDF failure"); - key_bytes - }; - - let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); - - let nonce_bytes: [u8; 12] = rand::random(); - let nonce = Nonce::from_slice(&nonce_bytes); - - let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { - Ok(data) => data, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let mut encrypted_out = nonce_bytes.to_vec(); - encrypted_out.extend(encrypted_challenge); - - let response = CommunicationValue::new(CommunicationType::challenge) - .add_data_str( - DataTypes::public_key, - STANDARD.encode(community_public_key.as_bytes()), - ) - .add_data_str(DataTypes::challenge, STANDARD.encode(&encrypted_out)) - .with_id(cv.get_id()); - - self.send_message(&response).await; - } - async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { - let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { - Some(data) => data.to_string(), - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { - Ok(bytes) => bytes, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - if challenge_response_bytes.len() < 12 { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - - let Some(user) = self.auth.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - - let community_private_key = community.get_private_key(); - - let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { - Some(secret) => secret, - _ => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let aes_key = { - use hkdf::Hkdf; - use sha2::Sha256; - - let hk = Hkdf::::new(None, shared_secret.as_bytes()); - let mut key_bytes = [0u8; 32]; - hk.expand(b"challenge", &mut key_bytes) - .expect("HKDF failure"); - key_bytes - }; - - let (nonce_bytes, ciphertext) = challenge_response_bytes.split_at(12); - let nonce = Nonce::from_slice(nonce_bytes); - let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); - - let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { - Ok(pt) => pt, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let client_response = match String::from_utf8(decrypted_bytes) { - Ok(str) => str, - Err(_) => { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - }; - - let expected_challenge = self.challenge.read().await.clone(); - - if client_response != expected_challenge { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - self.close().await; - return; - } - - { - let mut authenticated_guard = self.challenged.write().await; - *authenticated_guard = true; - } - - let Some(community) = self.community.read().await.clone() else { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - }; - let arc = Arc::new(community); - - let user_id = self.get_user_id().await; - if user_id == 0 { - self.send_error_response(&cv.get_id(), CommunicationType::error) - .await; - return; - } - - arc.add_connection(self.clone()).await; - - let response = CommunicationValue::new(CommunicationType::identification_response) - .add_data(DataTypes::interactables, { - let a: Vec>> = arc.get_interactables(user_id).await; - let mut c: JsonValue = JsonValue::new_object(); - for b in a { - let mut subject = JsonValue::new_object(); - subject["codec"] = JsonValue::String(b.get_codec()); - subject["data"] = b.get_data(); - c[b.get_name()] = subject; - } - c - }) - .with_id(cv.get_id()); - - self.send_message(&response).await; - } - - async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { - let error = CommunicationValue::new(error_type).with_id(*message_id); - self.send_message(&error).await; - } - pub async fn close(&self) { - let mut sender = self.sender.write().await; - let _ = sender.close().await; - } - pub async fn handle_close(self: Arc) { - if self.is_identified().await { - if self.get_user_id().await != 0 { - self.community - .read() - .await - .as_ref() - .unwrap() - .remove_connection(self.clone()) - .await; - } - } - } - - async fn handle_ping(&self, cv: CommunicationValue) { - if let Some(last_ping) = cv.get_data(DataTypes::last_ping) { - if let Ok(ping_val) = last_ping.to_string().parse::() { - let mut ping_guard = self.ping.write().await; - *ping_guard = ping_val; - } - } - - let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); - - self.send_message(&response).await; - } -} diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs deleted file mode 100644 index 8369f5a..0000000 --- a/src/communities/community_manager.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::communities::community::{self, Community}; -use crate::log; -use crate::util::file_util; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; - -pub static COMMUNITY_REGISTRY: Lazy>>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); - -pub async fn add_community(community: Arc) { - COMMUNITY_REGISTRY - .lock() - .await - .insert(community.get_name().to_string(), community); -} -pub async fn remove_community(name: &str) { - COMMUNITY_REGISTRY.lock().await.remove(name); -} -pub async fn clear() { - COMMUNITY_REGISTRY.lock().await.clear(); -} -pub async fn get_community(name: &str) -> Option> { - if let Some(c) = COMMUNITY_REGISTRY.lock().await.get(name) { - Some(c.clone()) - } else { - None - } -} - -pub async fn load_communities() { - let community_names = file_util::get_children("communities"); - for name in community_names { - if let Some(community) = community::load(&name).await { - add_community(community).await; - } else { - log!("failed to load the {} community", &name); - } - } -} -pub async fn save_communities() { - for community in COMMUNITY_REGISTRY.lock().await.values() { - community.save().await; - } -} - -pub async fn get_communities() -> Vec> { - COMMUNITY_REGISTRY.lock().await.values().cloned().collect() -} - -pub async fn rename_community(old_name: &str, new_name: &str) { - if let Some(community) = COMMUNITY_REGISTRY.lock().await.remove(old_name) { - COMMUNITY_REGISTRY - .lock() - .await - .insert(new_name.to_string(), community); - } -} diff --git a/src/communities/interactables/category.rs b/src/communities/interactables/category.rs deleted file mode 100644 index 6b6415f..0000000 --- a/src/communities/interactables/category.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::communities::{community::Community, interactables::interactable::Interactable}; -use async_trait::async_trait; -use json::JsonValue; -use std::any::Any; -use std::sync::Arc; -use ttp_core::CommunicationValue; -use uuid::Uuid; - -pub struct Category { - id: Uuid, - name: String, - path: String, - community: Arc, - children: Vec>>, -} -impl Category { - pub fn new() -> Category { - Category { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - children: Vec::new(), - } - } - pub fn get_child(&self, path: String, name: String) -> Option>> { - if path.is_empty() { - self.children - .iter() - .find(|child| child.get_name() == &name) - .cloned() - } else { - let sub_module = path.split("/").next().unwrap(); - let next = self - .children - .iter() - .find(|child| child.get_name() == sub_module) - .unwrap(); - if next.get_codec() == "category" { - let next_cat = next.as_any().downcast_ref::().unwrap(); - next_cat.get_child(path, name) - } else { - Some(next.clone()) - } - } - } - pub fn get_children(&self) -> Vec>> { - self.children.iter().map(|child| child.clone()).collect() - } -} - -#[async_trait] -impl Interactable for Category { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "category".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - let mut v = JsonValue::new_object(); - for child in &self.children { - let mut subject = JsonValue::new_object(); - subject["codec"] = JsonValue::String(child.get_codec()); - subject["data"] = child.get_data(); - v[child.get_name()] = subject; - } - v - } - async fn run_function(&self, _cv: CommunicationValue) -> CommunicationValue { - CommunicationValue::new(CommunicationType::error) - } - fn to_json(&self) -> JsonValue { - let mut v = JsonValue::new_object(); - v["children"] = JsonValue::new_array(); - for child in &self.children { - let _ = v["children"].push(child.to_json()); - } - v - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/interactables/interactable.rs b/src/communities/interactables/interactable.rs deleted file mode 100644 index 0521de6..0000000 --- a/src/communities/interactables/interactable.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::communities::community::Community; -use async_trait::async_trait; -use json::JsonValue; -use std::any::Any; -use std::sync::Arc; -use ttp_core::CommunicationValue; -use uuid::Uuid; - -pub type InteractableFactory = fn() -> Box; - -#[async_trait] -pub trait Interactable: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn get_codec(&self) -> String; - fn get_name(&self) -> &String; - fn get_path(&self) -> &String; - fn get_total_path(&self) -> String; - fn set_name(&mut self, name: String); - fn set_path(&mut self, path: String); - fn get_community(&self) -> &Arc; - fn set_community(&mut self, community: Arc); - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue; - fn get_data(&self) -> JsonValue; - fn get_id(&self) -> &Uuid; - fn to_json(&self) -> JsonValue; - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - json: &JsonValue, - ); -} diff --git a/src/communities/interactables/registry.rs b/src/communities/interactables/registry.rs deleted file mode 100644 index 2f57005..0000000 --- a/src/communities/interactables/registry.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::communities::community::Community; -use crate::communities::interactables::category::Category; -use crate::communities::interactables::interactable::{Interactable, InteractableFactory}; -use crate::communities::interactables::text_chat::TextChat; -use crate::communities::interactables::voice_chat::VoiceChat; -use crate::util::file_util; -use json::JsonValue; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; -use uuid::Uuid; - -pub static INTERACTABLE_REGISTRY: Lazy>>> = - Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); -pub async fn load_interactables() { - INTERACTABLE_REGISTRY - .lock() - .await - .insert(TextChat::new().get_codec(), || { - Box::new(TextChat::new()) as Box - }); - INTERACTABLE_REGISTRY - .lock() - .await - .insert(VoiceChat::new().get_codec(), || { - Box::new(VoiceChat::new()) as Box - }); - INTERACTABLE_REGISTRY - .lock() - .await - .insert(Category::new().get_codec(), || { - Box::new(Category::new()) as Box - }); -} -pub async fn register_interactable(name: String, interactable: InteractableFactory) { - INTERACTABLE_REGISTRY - .lock() - .await - .insert(name.to_string(), interactable) - .unwrap(); -} -pub async fn get_interactable(name: &str) -> Box { - INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()() -} -pub async fn save(interactable: &Arc>) { - let mut json_object: JsonValue = interactable.to_json().clone(); - json_object["codec"] = JsonValue::String(interactable.get_codec()); - json_object["id"] = JsonValue::String(interactable.get_id().to_string()); - file_util::save_file( - &format!( - "communities/{}/interactables/{}", - interactable.get_community().get_name(), - interactable.get_path() - ), - &format!("{}.json", interactable.get_name()), - &json_object.to_string(), - ); -} -pub async fn load( - c: Arc, - path: String, - name: String, -) -> Box { - let s = file_util::load_file( - &format!("communities/{}/interactables/{}", c.get_name(), path), - &format!("{}.json", name), - ); - let json_object: JsonValue = json::parse(&s).unwrap(); - let codec: String = json_object["codec"].as_str().unwrap().to_string(); - let id: String = json_object["id"].as_str().unwrap().to_string(); - let mut interactable = get_interactable(&codec).await; - interactable.load(c, Uuid::parse_str(&id).unwrap(), path, name, &json_object); - interactable -} diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs deleted file mode 100644 index 88e1545..0000000 --- a/src/communities/interactables/text_chat.rs +++ /dev/null @@ -1,261 +0,0 @@ -use crate::{ - communities::{ - community::Community, community_connection::CommunityConnection, - interactables::interactable::Interactable, - }, - log, - util::file_util::{get_children, load_file, save_file}, -}; -use async_trait::async_trait; -use json::{JsonValue, array, object}; -use std::fs; -use std::sync::Arc; -use std::{any::Any, collections::HashMap}; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes}; -use uuid::Uuid; -pub struct TextChat { - id: Uuid, - name: String, - path: String, - community: Arc, -} -impl TextChat { - pub fn new() -> TextChat { - TextChat { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - } - } - pub fn add_message(&self, send_time: u128, sender: i64, message: &str) { - let user_dir = &format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - ); - - if let Err(e) = fs::create_dir_all(user_dir) { - log!("Failed to create chat directory: {}", e); - return; - } - - let mut chunk_index = 0; - let mut message_chunk = array![]; - - // find latest chunk not full (max 800 msgs) - loop { - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file(&user_dir, &file_name); - - if !file_content.is_empty() { - if let Ok(current_chunk) = json::parse(&file_content) { - if current_chunk.is_array() && current_chunk.len() < 800 { - message_chunk = current_chunk; - break; - } - } else { - log!("Failed to parse existing JSON file: {}", file_name); - } - } else { - break; - } - - chunk_index += 1; - if chunk_index > 1000 { - log!("Too many message chunks. Aborting add."); - return; - } - } - - let json_obj = object! { - "timestamp" => send_time as i64, - "content" => message, - "sender" => sender.to_string(), - }; - - if let Err(e) = message_chunk.push(json_obj) { - log!("Failed to push new message into JSON array: {}", e); - return; - } - - let file_name = format!("msgs_{}.json", chunk_index); - log!("Saving message to {}/{}", user_dir, file_name); - save_file(&user_dir, &file_name, &message_chunk.dump()); - } - pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { - let mut messages = array![]; - - let mut latest_chunk_index: i32 = -1; - let files = get_children(&format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - )); - - for entry in files { - if let Some(num) = { - entry - .strip_prefix("msgs_") - .and_then(|s| s.strip_suffix(".json")) - } { - if let Ok(index) = num.parse::() { - if index > latest_chunk_index { - latest_chunk_index = index; - } - } - } - } - - if latest_chunk_index == -1 { - return messages; - } - - let mut to_skip = loaded_messages; - let mut needed = amount; - - for chunk_index in (0..=latest_chunk_index).rev() { - if needed == 0 { - break; - } - let file_name = format!("msgs_{}.json", chunk_index); - let file_content = load_file( - &format!( - "communities/{}/interactables/{}/{}", - self.get_community().get_name(), - self.get_path(), - self.get_name() - ), - &file_name, - ); - if file_content.is_empty() { - continue; - } - if let Ok(chunk) = json::parse(&file_content) { - for i in (0..chunk.len()).rev() { - if needed == 0 { - break; - } - if to_skip > 0 { - to_skip -= 1; - continue; - } - messages.push(chunk[i].clone()).unwrap(); - needed -= 1; - } - } - } - - messages - } -} -#[async_trait] -impl Interactable for TextChat { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "text".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - JsonValue::new_object() - } - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { - let payload = cv.get_data(DataTypes::payload).as_container().unwrap(); - if cv.get_data(DataTypes::function).as_str().unwrap() == "get_messages" { - let amount = payload.get(DataTypes::amount).as_i64().unwrap(); - let loaded_messages = payload["loaded_messages"].as_i64().unwrap(); - let messages = self.get_messages(loaded_messages, amount).clone(); - let mut payload = JsonValue::new_object(); - payload["messages"] = messages; - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_chunk".to_string()) - .add_data(DataTypes::payload, payload); - } - if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "send_message" { - let message = payload["message"].as_str().unwrap(); - let milliseconds_timestamp: u128 = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(); - self.add_message(milliseconds_timestamp, cv.get_sender(), message); - - let mut distribution_payload = JsonValue::new_object(); - distribution_payload["message"] = JsonValue::String(message.to_string()); - distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string()); - distribution_payload["send_time"] = - JsonValue::String(milliseconds_timestamp.to_string()); - let distribution = CommunicationValue::new(CommunicationType::update) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_live".to_string()) - .add_data(DataTypes::payload, distribution_payload); - - let connections: HashMap>> = - self.get_community().get_connections().await.clone(); - - for con in connections.values() { - for c in con { - let cd: &Arc = c; - cd.send_message(&distribution).await; - } - } - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "message_received".to_string()) - .add_data(DataTypes::payload, JsonValue::new_object()); - } - CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) - } - fn to_json(&self) -> JsonValue { - JsonValue::new_object() - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/interactables/voice_chat.rs b/src/communities/interactables/voice_chat.rs deleted file mode 100644 index 2a2e3a2..0000000 --- a/src/communities/interactables/voice_chat.rs +++ /dev/null @@ -1,187 +0,0 @@ -use crate::communities::{community::Community, interactables::interactable::Interactable}; -use async_trait::async_trait; -use json::JsonValue; -use std::sync::Arc; -use std::{any::Any, sync::RwLock}; -use uuid::Uuid; -pub enum CallUserState { - Active, - Muted, - Deafed, -} -impl CallUserState { - pub fn parse(state: &str) -> CallUserState { - match state { - "active" => CallUserState::Active, - "muted" => CallUserState::Muted, - "deafed" => CallUserState::Deafed, - _ => CallUserState::Active, - } - } - pub fn to_string(&self) -> String { - match self { - CallUserState::Active => "active".to_string(), - CallUserState::Muted => "muted".to_string(), - CallUserState::Deafed => "deafed".to_string(), - } - } -} - -pub struct CallUser { - pub user_id: Uuid, - pub user_state: CallUserState, - pub streaming: bool, -} - -pub struct VoiceChat { - id: Uuid, - name: String, - path: String, - community: Arc, - users: RwLock>, -} -impl VoiceChat { - pub fn new() -> VoiceChat { - VoiceChat { - id: Uuid::new_v4(), - name: String::new(), - path: String::new(), - community: Arc::new(Community::new()), - users: RwLock::new(Vec::new()), - } - } - pub fn update_user_state( - self: Arc, - user_id: Uuid, - state: CallUserState, - streaming: bool, - ) { - if let Some(user) = self - .users - .write() - .unwrap() - .iter_mut() - .find(|u| u.user_id == user_id) - { - user.user_state = state; - user.streaming = streaming; - } - } -} -#[async_trait] -impl Interactable for VoiceChat { - fn get_id(&self) -> &Uuid { - &self.id - } - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - fn get_codec(&self) -> String { - "voice".to_string() - } - fn set_name(&mut self, name: String) { - self.name = name; - } - fn set_path(&mut self, path: String) { - self.path = path; - } - fn get_community(&self) -> &Arc { - &self.community - } - fn set_community(&mut self, community: Arc) { - self.community = community; - } - fn get_name(&self) -> &String { - &self.name - } - fn get_path(&self) -> &String { - &self.path - } - fn get_total_path(&self) -> String { - String::new() + &self.path + "/" + &self.name - } - fn get_data(&self) -> JsonValue { - let mut data = JsonValue::new_object(); - let mut active_users = JsonValue::new_object(); - for user in self.users.read().unwrap().iter() { - let mut user_data = JsonValue::new_object(); - let _ = user_data.insert("state", JsonValue::String(user.user_state.to_string())); - let _ = user_data.insert("streaming", JsonValue::Boolean(user.streaming)); - let _ = active_users.insert(&user.user_id.to_string(), user_data); - } - let _ = data.insert("active_users", active_users); - data - } - async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { - let payload = cv.get_data(DataTypes::payload).unwrap(); - let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); - - if function == "get_call" { - let sender_id = payload["sender_id"].as_str().unwrap(); - let message_id = payload["message"].as_str().unwrap(); - let send_time = payload["send_time"].as_str().unwrap(); - - let mut response_payload = JsonValue::new_object(); - response_payload["sender_id"] = JsonValue::String(sender_id.to_string()); - response_payload["message"] = JsonValue::String(message_id.to_string()); - response_payload["send_time"] = JsonValue::String(send_time.to_string()); - - return CommunicationValue::new(CommunicationType::function) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "getting_call".to_string()) - .add_data(DataTypes::payload, response_payload); - } - - if function == "update_user_state" { - let user_id = payload["user_id"].as_str().unwrap(); - let state = payload["state"].as_str().unwrap(); - let streaming = payload["streaming"].as_bool().unwrap(); - - if let Some(user) = self - .users - .write() - .unwrap() - .iter_mut() - .find(|u| u.user_id == Uuid::parse_str(user_id).unwrap()) - { - user.user_state = CallUserState::parse(state); - user.streaming = streaming; - } - let mut response_payload = JsonValue::new_object(); - response_payload["user_id"] = JsonValue::String(user_id.to_string()); - response_payload["state"] = JsonValue::String(state.to_string()); - response_payload["streaming"] = JsonValue::Boolean(streaming); - - return CommunicationValue::new(CommunicationType::update) - .with_id(cv.get_id()) - .add_data_str(DataTypes::name, self.name.clone()) - .add_data_str(DataTypes::path, self.path.clone()) - .add_data_str(DataTypes::result, "user_changed".to_string()) - .add_data(DataTypes::payload, response_payload); - } - CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) - } - - fn to_json(&self) -> JsonValue { - let v = JsonValue::new_object(); - v - } - fn load( - &mut self, - community: Arc, - id: Uuid, - path: String, - name: String, - _json: &JsonValue, - ) { - self.community = community; - self.id = id; - self.name = name; - self.path = path; - } -} diff --git a/src/communities/mod.rs b/src/communities/mod.rs deleted file mode 100644 index ecad116..0000000 --- a/src/communities/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod community_manager; -pub mod interactables { - pub mod category; - pub mod interactable; - pub mod registry; - pub mod text_chat; - pub mod voice_chat; -} -pub mod community; -pub mod community_connection; -pub mod perms { - pub mod permission; -} diff --git a/src/communities/perms/permission.rs b/src/communities/perms/permission.rs deleted file mode 100644 index 32a37ee..0000000 --- a/src/communities/perms/permission.rs +++ /dev/null @@ -1,27 +0,0 @@ -use json::JsonValue; -use uuid::Uuid; - -pub struct Permission { - pub id: Uuid, - pub name: String, -} -impl Permission { - pub fn new(id: Uuid, name: String) -> Self { - Permission { id, name } - } - pub fn to_json(&self) -> JsonValue { - json::object! { - "id" => self.id.to_string(), - "name" => self.name.clone() - } - } - pub fn from_json(json: JsonValue) -> Self { - Permission { - id: Uuid::parse_str(json["id"].as_str().unwrap()).unwrap(), - name: json["name"].as_str().unwrap().to_string(), - } - } - pub fn to_string(&self) -> String { - self.to_json().as_str().unwrap().to_string() - } -} diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs deleted file mode 100755 index a161226..0000000 --- a/src/gui/app_state.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry}; -use json::{JsonValue, object}; -use std::{collections::VecDeque, thread, time::Duration}; -use sysinfo::{RefreshKind, System}; - -#[derive(Clone)] -pub struct AppState { - pub logs: VecDeque, - pub cpu: Vec<(f64, f64)>, - pub ram: Vec<(f64, f64)>, - pub ping: Vec<(f64, f64)>, - pub net_up: Vec<(f64, f64)>, - pub net_down: Vec<(f64, f64)>, - pub sys_info: String, -} -const MAX_POINTS: usize = 1000; -const MAX_LOGS: usize = 100; - -impl AppState { - pub fn new() -> Self { - Self { - logs: VecDeque::new(), - cpu: Vec::new(), - ram: Vec::new(), - ping: Vec::new(), - net_up: Vec::new(), - net_down: Vec::new(), - sys_info: String::from("Loading..."), - } - } - - pub fn push_log(&mut self, msg: UiLogEntry) { - if self.logs.len() >= MAX_LOGS { - self.logs.pop_front(); - } - self.logs.push_back(msg); - } - - pub fn get_logs(&self) -> &VecDeque { - &self.logs - } - - pub fn push_cpu(&mut self, pt: (f64, f64)) { - self.cpu.push(pt); - if self.cpu.len() > MAX_POINTS { - self.cpu.remove(0); - } - } - - pub fn push_ram(&mut self, pt: (f64, f64)) { - self.ram.push(pt); - if self.ram.len() > MAX_POINTS { - self.ram.remove(0); - } - } - - pub fn push_ping_val(&mut self, pt: f64) { - self.ping.push((self.ping.len() as f64, pt)); - if self.ping.len() > MAX_POINTS { - self.ping.remove(0); - } - } - - pub fn push_net_up(&mut self, pt: (f64, f64)) { - self.net_up.push(pt); - if self.net_up.len() > MAX_POINTS { - self.net_up.remove(0); - } - } - - pub fn push_net_down(&mut self, pt: (f64, f64)) { - self.net_down.push(pt); - if self.net_down.len() > MAX_POINTS { - self.net_down.remove(0); - } - } - pub fn to_json(&self) -> JsonValue { - let json = object! { - "cpu" => self.cpu - .iter() - .map(|(_, y)| *y) - .collect::>(), - "ram" => self.ram - .iter() - .map(|(_, y)| *y) - .collect::>(), - "ping" => self - .ping - .iter() - .map(|(_, y)| *y) - .collect::>(), - "net_up" => self - .net_up - .iter() - .map(|(_, y)| *y) - .collect::>(), - "net_down" => self - .net_down - .iter() - .map(|(_, y)| *y) - .collect::>(), - }; - json - } - pub fn with_width(&self, width: u16) -> Self { - let mut new = self.clone(); - new.cpu = Self::downsample_to_fit_width(&new.cpu, width); - new.ram = Self::downsample_to_fit_width(&new.ram, width); - new.ping = Self::downsample_to_fit_width(&new.ping, width); - new.net_up = Self::downsample_to_fit_width(&new.net_up, width); - new.net_down = Self::downsample_to_fit_width(&new.net_down, width); - new - } - - fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { - let width_usize = (width as usize) * 2; - let len = data.len(); - - if len >= width_usize { - data[len - width_usize..].to_vec() - } else { - let mut result = Vec::with_capacity(width_usize); - - let dx = 1.0; - let pad_len = width_usize - len; - - let start_x = data - .first() - .map(|(x, _)| x - (dx * pad_len as f64)) - .unwrap_or(0.0); - let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0); - - for i in 0..pad_len { - result.push((start_x + i as f64 * dx, -1 as f64)); - } - - result.extend_from_slice(data); - result - } - } -} - -pub fn setup() { - ACTIVE_TASKS.insert("System info loader".to_string()); - tokio::spawn(async move { - let mut sys = System::new_with_specifics(RefreshKind::everything()); - let mut last_total_received = 0u64; - let mut last_total_transmitted = 0u64; - let mut counter = 0.0; - loop { - if *SHUTDOWN.read().await { - break; - } - sys.refresh_all(); - - let mut tcpu = 0; - for cpu in sys.cpus() { - tcpu += cpu.cpu_usage() as i64; - tcpu /= 2; - } - let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; - - let total_received = 0u64; - let total_transmitted = 0u64; - - let delta_received = if last_total_received == 0 { - 0 - } else { - total_received.saturating_sub(last_total_received) - }; - let delta_transmitted = if last_total_transmitted == 0 { - 0 - } else { - total_transmitted.saturating_sub(last_total_transmitted) - }; - last_total_received = total_received; - last_total_transmitted = total_transmitted; - - let net_down = delta_received as f64; - let net_up = delta_transmitted as f64; - - { - let mut st = APP_STATE.lock().unwrap(); - st.push_cpu((counter, tcpu as f64)); - st.push_ram((counter, ram)); - st.push_net_down((counter, net_down)); - st.push_net_up((counter, net_up)); - - st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); - } - - counter += 1.0; - if counter > 30.0 { - thread::sleep(Duration::from_millis(500)); - } else { - thread::sleep(Duration::from_millis(5)); - } - } - ACTIVE_TASKS.remove("System info loader"); - }); -} diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs deleted file mode 100644 index b189bd4..0000000 --- a/src/gui/elements/console_card.rs +++ /dev/null @@ -1,491 +0,0 @@ -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; -use ttp_core::{CommunicationType, CommunicationValue}; -use uuid::Uuid; - -use crate::{ - ACTIVE_TASKS, RELOAD, SHUTDOWN, - gui::{ - elements::elements::{Element, InteractableElement, JoinableElement}, - interaction_result::InteractionResult, - ui::FPS, - util::borders::draw_block_joins, - }, - log, log_command, log_cv, - omikron::omikron_connection::OMIKRON_CONNECTION, - users::{user_manager, user_profile::UserProfile}, - util::file_util, -}; -use std::{ - any::Any, - sync::{Arc, Mutex}, - time::Duration, -}; -use tokio::time::Instant; - -pub struct ConsoleCard { - focused: bool, - pub title: String, - pub content: String, - pub cursor_position: usize, - - borders: Borders, - joins: Borders, - - cursor: Arc>, - last_swap: Arc>, - tab_index: usize, -} - -impl ConsoleCard { - pub fn new(title: &str, content: &str) -> Self { - ConsoleCard { - focused: false, - title: title.to_string(), - content: content.to_string(), - cursor_position: content.chars().count(), - borders: Borders::ALL, - joins: Borders::NONE, - cursor: Arc::new(Mutex::new(true)), - last_swap: Arc::new(Mutex::new(Instant::now())), - tab_index: 0, - } - } - - fn byte_index(&self) -> usize { - self.content - .char_indices() - .nth(self.cursor_position) - .map(|(i, _)| i) - .unwrap_or(self.content.len()) - } - - fn cursor_visible(&self) -> bool { - if !self.focused { - return false; - } - - let mut visible = self.cursor.lock().unwrap(); - let mut last = self.last_swap.lock().unwrap(); - let now = Instant::now(); - - if now.duration_since(*last) >= Duration::from_millis(500) { - *visible = !*visible; - *last = now; - } - - *visible - } - - fn current_prefix(&self) -> Option<&str> { - if self.content.starts_with('/') { - Some("/") - } else { - None - } - } - - fn cursor_spans(&self) -> Vec> { - let cursor_visible = self.cursor_visible(); - let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); - let mut spans = Vec::new(); - - if self.content.is_empty() { - if self.focused { - if cursor_visible { - spans.push(Span::styled(" ", cursor_style)); - } else { - spans.push(Span::styled(" ", Style::default().fg(Color::White))); - } - spans.push(Span::styled( - "send command ( for info)", - Style::default().fg(Color::DarkGray), - )); - } else { - spans.push(Span::styled( - " send command ( for info)", - Style::default().fg(Color::DarkGray), - )); - } - return spans; - } - - let byte_index = self.byte_index(); - let before = self.content[..byte_index].to_string(); - let after = self.content[byte_index..].to_string(); - - let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0); - - if prefix_len > 0 && before.len() >= prefix_len { - let prefix = &before[..prefix_len]; - let rest = &before[prefix_len..]; - spans.push(Span::styled( - prefix.to_string(), - Self::style_for_part(true, false, false), - )); - if !rest.is_empty() { - spans.push(Span::styled( - rest.to_string(), - Style::default().fg(Color::White), - )); - } - } else if !before.is_empty() { - spans.push(Span::styled( - before.clone(), - Style::default().fg(Color::White), - )); - } - - if cursor_visible { - spans.push(Span::styled(" ", cursor_style)); - } - - if !after.is_empty() { - spans.push(Span::styled(after, Style::default().fg(Color::White))); - } - - spans - } - - fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style { - if is_error { - return Style::default().fg(Color::Red); - } - - if is_hint { - return Style::default().fg(Color::DarkGray); - } - - if is_prefix { - return Style::default().fg(Color::DarkGray); - } - - Style::default().fg(Color::White) - } - - fn render_cursor_spans(&self) -> Vec> { - self.cursor_spans() - } - - fn move_cursor_left(&mut self) { - if self.cursor_position > 0 { - self.cursor_position -= 1; - } - } - - fn move_cursor_right(&mut self) { - let len = self.content.chars().count(); - if self.cursor_position < len { - self.cursor_position += 1; - } - } - - fn delete_at_cursor(&mut self) { - if self.content.is_empty() || self.cursor_position == 0 { - return; - } - - let start = self - .content - .char_indices() - .nth(self.cursor_position.saturating_sub(1)) - .map(|(i, _)| i) - .unwrap_or(0); - let end = self.byte_index(); - self.content.replace_range(start..end, ""); - self.cursor_position -= 1; - } - - fn insert_at_cursor(&mut self, c: char) { - let idx = self.byte_index(); - self.content.insert(idx, c); - self.cursor_position += 1; - } -} - -impl Element for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, r: Rect) { - let block = Block::default() - .borders(self.borders) - .title(self.title.clone()) - .title_style(Style::default().fg(Color::White)) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }) - .style(if self.focused { - Style::default().fg(Color::White) - } else { - Style::default() - }); - - let spans = self.render_cursor_spans(); - let par = Paragraph::new(Line::from(spans)) - .block(block) - .scroll((0, 0)); - f.render_widget(par, r); - draw_block_joins(f, r, self.borders, self.joins); - } -} - -impl JoinableElement for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for ConsoleCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - self - } - - fn interact(&mut self, key: KeyEvent) -> InteractionResult { - match key.code { - KeyCode::Enter => { - if self.content.is_empty() { - log!(""); - return InteractionResult::Handled; - } - - let command = self.content.clone(); - let id = Uuid::new_v4(); - let id = id.to_string(); - let id = id.split_at(8).0; - let task_id = format!("command_{}_{}", command, id); - ACTIVE_TASKS.insert(task_id.clone()); - - log_command!("{}", command); - - tokio::spawn(async move { - run_command(&command).await; - ACTIVE_TASKS.remove(&task_id); - }); - - self.content.clear(); - self.cursor_position = 0; - self.tab_index = 0; - InteractionResult::Handled - } - KeyCode::Backspace => { - self.delete_at_cursor(); - InteractionResult::Handled - } - KeyCode::Delete => { - let len = self.content.chars().count(); - if self.cursor_position < len { - let start = self.byte_index(); - let end = self - .content - .char_indices() - .nth(self.cursor_position + 1) - .map(|(i, _)| i) - .unwrap_or(self.content.len()); - self.content.replace_range(start..end, ""); - } - InteractionResult::Handled - } - KeyCode::Left => { - self.move_cursor_left(); - InteractionResult::Handled - } - KeyCode::Right => { - self.move_cursor_right(); - InteractionResult::Handled - } - KeyCode::Home => { - self.cursor_position = 0; - InteractionResult::Handled - } - KeyCode::End => { - self.cursor_position = self.content.chars().count(); - InteractionResult::Handled - } - KeyCode::Tab => { - if let Some(prefix) = self.current_prefix() { - if prefix == "/" { - self.tab_index = self.tab_index.saturating_add(1); - } - } - InteractionResult::Handled - } - _ => { - if let Some(c) = key.code.as_char() { - self.insert_at_cursor(c); - InteractionResult::Handled - } else { - InteractionResult::Unhandled - } - } - } - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} - -pub async fn run_command(command: &str) { - let parts = command.split(" ").collect::>(); - - match parts.as_slice() { - ["tasks"] => { - let active_tasks: Vec = - ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect(); - let info = if *SHUTDOWN.read().await && *RELOAD.read().await { - "Rebooting, " - } else if *SHUTDOWN.read().await { - "Shutting , " - } else { - "" - }; - log!("{}Active tasks: {:?}", info, active_tasks); - } - ["fps"] => { - let (fps, skips) = *FPS.read().await; - log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips); - } - - ["help"] => { - log!("Available commands: tasks, fps, ping, user"); - } - - ["help", "tasks"] => { - log!("Tasks command usage: tasks"); - } - ["help", "fps"] => { - log!("FPS command usage: fps"); - } - ["help", "ping"] => { - log!("Ping command usage: ping [time]"); - } - ["help", "user"] => { - log!("User command usage: user add | user remove | user list"); - } - - ["ping"] => { - ping(20).await; - } - ["ping", time] => { - let time = time.parse::().unwrap_or(20); - ping(time).await; - } - ["user", "add", username] => { - if let (Some(user), Some(_)) = user_manager::create_user(username).await { - log!("Created user {}", user.user_id); - } else { - log!("Failed to create user"); - } - } - ["user", "remove", username] => { - if let Some(user) = user_manager::get_user_by_username(username) { - user_manager::remove_user(user.user_id); - log!("Removed user {}", user.user_id); - } else { - log!("Failed to find user"); - } - } - ["user", "list"] => { - let users: Vec = user_manager::get_users(); - for user in users { - let storage = file_util::get_designed_storage(user.user_id); - log!( - "> Username: {}, ID: {}, created at: {}, storage: {}", - user.username, - user.user_id, - user.created_at, - storage - ); - } - } - ["user", "info", username] => { - if let Some(user) = user_manager::get_user_by_username(username) { - user_manager::remove_user(user.user_id); - log!("Removed user {}", user.user_id); - } else { - log!("Failed to find user"); - } - } - ["reload"] | ["restart"] => { - log!("Restarting"); - *RELOAD.write().await = true; - *SHUTDOWN.write().await = true; - } - ["shutdown"] | ["stop"] => { - log!("Shutting down"); - *SHUTDOWN.write().await = true; - } - _ => { - log!("Unknown command"); - } - } -} - -pub async fn ping(time: u64) { - let conn = OMIKRON_CONNECTION.clone(); - - let response_cv = conn - .await_response( - &CommunicationValue::new(CommunicationType::ping), - Some(Duration::from_secs(time)), - ) - .await; - match response_cv { - Ok(response) => log_cv!(response), - Err(err) => log!("Ping error: {:?}", err), - } -} diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs deleted file mode 100644 index 3429e52..0000000 --- a/src/gui/elements/elements.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::any::Any; - -use crossterm::event::KeyEvent; -use ratatui::{Frame, layout::Rect, widgets::Borders}; - -use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; - -#[allow(unused)] -pub trait Element: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - - fn render(&self, f: &mut Frame, r: Rect); -} - -#[allow(unused)] -pub trait JoinableElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn set_borders(&mut self, borders: Borders); - fn set_joins(&mut self, joins: Borders); -} - -#[allow(unused)] -pub trait InfoElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn get_info_screen(&self) -> Box; -} - -#[allow(unused)] -pub trait InteractableElement: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - fn as_element(&self) -> &dyn Element; - fn as_element_mut(&mut self) -> &mut dyn Element; - - fn interact(&mut self, key: KeyEvent) -> InteractionResult; - - fn can_focus(&self) -> bool; - fn is_focused(&self) -> bool; - fn focus(&mut self, f: bool); -} diff --git a/src/gui/elements/graph_card.rs b/src/gui/elements/graph_card.rs deleted file mode 100644 index d95f937..0000000 --- a/src/gui/elements/graph_card.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::{any::Any, sync::Arc}; - -use crossterm::event::KeyEvent; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - widgets::{ - Block, Borders, - canvas::{Canvas, Line}, - }, -}; - -use crate::{ - APP_STATE, - gui::{ - elements::elements::{Element, InteractableElement, JoinableElement}, - interaction_result::InteractionResult, - ui::UI, - util::borders::draw_block_joins, - }, -}; - -pub enum GRAPHS { - Ram, - Cpu, - Ping, -} - -impl GRAPHS { - pub fn get_color(&self) -> Color { - match self { - GRAPHS::Ram => Color::Blue, - GRAPHS::Cpu => Color::Red, - GRAPHS::Ping => Color::Green, - } - } - - pub fn get_graph(&self) -> Vec<(f64, f64)> { - match self { - GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(), - GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(), - GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(), - } - } - - pub fn get_unit(&self) -> String { - match self { - GRAPHS::Ram => "MB".to_string(), - GRAPHS::Cpu => "%".to_string(), - GRAPHS::Ping => "ms".to_string(), - } - } -} - -#[allow(unused)] -pub struct GraphCard { - ui: Arc, - graph_type: GRAPHS, - - focused: bool, - pub title: String, - - borders: Borders, - joins: Borders, - - open: bool, -} - -impl GraphCard { - pub fn new(ui: Arc, graph_type: GRAPHS, title: String) -> Self { - Self { - ui, - graph_type, - focused: false, - title, - borders: Borders::ALL, - joins: Borders::NONE, - open: true, - } - } - - pub fn set_open(&mut self, open: bool) { - self.open = open; - } -} -impl Element for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, r: Rect) { - if self.open { - let graph = self.graph_type.get_graph(); - let unit = self.graph_type.get_unit(); - let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0); - let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0); - let min_y = graph - .iter() - .map(|(_, y)| *y) - .filter(|y| *y > 0.0) - .min_by(|a, b| a.total_cmp(b)) - .unwrap_or(0.0); - let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max); - - let block = Block::default() - .title(format!( - "{}:─{}{}─{}min/{}max", - self.title, - graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, - unit, - min_y as i64, - max_y as i64, - )) - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - - let canvas = Canvas::default() - .block(block) - .x_bounds([min_x, max_x]) - .y_bounds([0.0, 100.0]) - .paint(|ctx| { - for (x, y) in &graph { - ctx.draw(&Line { - x1: *x, - y1: 0.0, - x2: *x, - y2: *y, - color: self.graph_type.get_color(), - }); - } - }); - f.render_widget(canvas, r); - } else { - let block = Block::default() - .title("") - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - f.render_widget(block, r); - } - draw_block_joins(f, r, self.borders, self.joins); - } -} - -impl JoinableElement for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &dyn Element { - self - } - - fn as_element_mut(&mut self) -> &mut dyn Element { - self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for GraphCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &dyn Element { - self - } - - fn as_element_mut(&mut self) -> &mut dyn Element { - self - } - - fn interact(&mut self, _key: KeyEvent) -> InteractionResult { - InteractionResult::Handled - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs deleted file mode 100755 index e2d4ba1..0000000 --- a/src/gui/elements/log_card.rs +++ /dev/null @@ -1,483 +0,0 @@ -use crate::APP_STATE; -use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; -use crate::gui::interaction_result::InteractionResult; -use crate::gui::util::borders::draw_block_joins; -use crate::util::logger::PrintType; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::Rect, - style::{Color, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; -use std::any::Any; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[derive(Clone, Debug)] -pub struct UiLogEntry { - pub timestamp_ms: u128, - pub sender: PrintType, - pub message: String, - pub is_error: bool, -} - -impl UiLogEntry { - pub fn format_timestamp(&self) -> String { - let secs = (self.timestamp_ms / 1000) as i64; - let hours = (secs / 3600) % 24; - let minutes = (secs / 60) % 60; - let seconds = secs % 60; - format!("{:02}:{:02}:{:02}", hours, minutes, seconds) - } -} - -impl From for UiLogEntry { - fn from(entry: LogEntry) -> Self { - Self { - timestamp_ms: entry.timestamp_ms, - sender: entry.sender, - message: entry.message, - is_error: entry.is_error, - } - } -} - -#[derive(Clone, Debug)] -pub struct LogEntry { - pub timestamp_ms: u128, - pub sender: PrintType, - pub message: String, - pub is_error: bool, -} - -impl LogEntry { - pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { - Self { - timestamp_ms: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - sender, - message, - is_error, - } - } -} - -pub struct LogCard { - focused: bool, - selected: bool, - scroll_offset: usize, - last_total_lines: usize, - last_visible_height: usize, - pub borders: Borders, - pub joins: Borders, -} - -impl LogCard { - pub fn new() -> Self { - Self { - focused: false, - selected: false, - scroll_offset: 0, - last_total_lines: 0, - last_visible_height: 10, - borders: Borders::ALL, - joins: Borders::NONE, - } - } - - fn get_logs(&self) -> Vec { - let state = APP_STATE.lock().unwrap(); - state.get_logs().iter().cloned().collect() - } - - fn find_split_point(s: &str, max_width: usize) -> usize { - if max_width == 0 { - return s.len(); - } - - let mut current_width = 0usize; - let mut last_boundary = 0usize; - - for (idx, ch) in s.char_indices() { - let char_width = if ch.is_ascii() { 1 } else { 2 }; - if current_width + char_width > max_width { - if last_boundary == 0 { - return idx + ch.len_utf8(); - } - return last_boundary; - } - current_width += char_width; - last_boundary = idx + ch.len_utf8(); - } - - s.len() - } - - fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { - let mut result = Vec::new(); - - let timestamp = entry.format_timestamp(); - let first_prefix = "┌ "; - let default_prefix = "│ "; - let last_prefix = "└ "; - - let prefix_width = 2; - let first_line_width = available_width.saturating_sub(prefix_width + 1); - let continuation_width = available_width.saturating_sub(prefix_width); - - let segments: Vec<&str> = entry.message.split('\n').collect(); - - let mut raw_lines = Vec::new(); - - for segment in segments { - let mut remaining = segment; - if remaining.is_empty() { - raw_lines.push(String::new()); - continue; - } - - let mut is_first_part = true; - while !remaining.is_empty() { - let current_width = if is_first_part { - first_line_width - } else { - continuation_width - }; - - let split_point = Self::find_split_point(remaining, current_width); - let line_content = remaining[..split_point].to_string(); - raw_lines.push(line_content); - remaining = &remaining[split_point..]; - is_first_part = false; - } - } - - if raw_lines.is_empty() { - raw_lines.push(String::new()); - } - - for (idx, content) in raw_lines.iter().enumerate() { - let is_last = idx + 1 == raw_lines.len(); - let is_single = raw_lines.len() == 1; - let is_first = idx == 0; - let prefix = if is_first { - first_prefix - } else if is_last && !is_single { - last_prefix - } else { - default_prefix - }; - - let mut line = String::from(prefix); - line.push_str(content); - - if is_last && !timestamp.is_empty() { - line.push(' '); - line.push_str(×tamp); - } - - result.push((line, entry.sender.prefix_color(), entry.is_error)); - } - - result - } - - fn build_all_lines( - &self, - entries: Vec, - width: usize, - ) -> Vec<(String, Color, bool)> { - let mut lines = Vec::new(); - - for entry in entries { - let wrapped = Self::wrap_entry(&entry, width); - lines.extend(wrapped); - } - - lines - } - - fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) { - if total_lines <= visible_height { - return (0, total_lines); - } - - let max_offset = total_lines - visible_height; - let clamped_offset = self.scroll_offset.min(max_offset); - - let end = total_lines - clamped_offset; - let start = end.saturating_sub(visible_height); - - (start, end) - } - - fn get_title_hints(&self) -> (bool, bool) { - if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height { - return (false, false); - } - - let max_offset = self.last_total_lines - self.last_visible_height; - let can_scroll_up = self.scroll_offset < max_offset; - let can_scroll_down = self.scroll_offset > 0; - - (can_scroll_up, can_scroll_down) - } - - fn build_title(&self) -> String { - if !self.focused { - return "Logs".to_string(); - } - - let (can_up, can_down) = self.get_title_hints(); - - if !can_up && !can_down { - return "Logs".to_string(); - } - - let nav_symbol = if self.selected { "↑" } else { "j" }; - let down_symbol = if self.selected { "↓" } else { "k" }; - - match (can_up, can_down) { - (true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol), - (true, false) => format!("Logs ({} older)", nav_symbol), - (false, true) => format!("Logs ({} newer)", down_symbol), - (false, false) => "Logs".to_string(), - } - } - - fn scroll_up(&mut self) { - let max_offset = self - .last_total_lines - .saturating_sub(self.last_visible_height); - self.scroll_offset = (self.scroll_offset + 1).min(max_offset); - } - - fn scroll_down(&mut self) { - self.scroll_offset = self.scroll_offset.saturating_sub(1); - } - - fn split_line_prefix(line: &str) -> (&str, &str) { - if let Some(rest) = line.strip_prefix("└ ") { - ("└ ", rest) - } else if let Some(rest) = line.strip_prefix("│ ") { - ("│ ", rest) - } else if let Some(rest) = line.strip_prefix("┌ ") { - ("┌ ", rest) - } else { - ("", line) - } - } - - fn split_timestamp_suffix(line: &str) -> (&str, &str) { - if let Some(idx) = line.rfind(' ') { - let possible_timestamp = &line[idx + 1..]; - if possible_timestamp.len() == 8 - && possible_timestamp.as_bytes()[2] == b':' - && possible_timestamp.as_bytes()[5] == b':' - { - let (content, timestamp_with_space) = line.split_at(idx); - return (content.trim_end(), timestamp_with_space); - } - } - (line, "") - } -} - -impl Element for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, area: Rect) { - let entries = self.get_logs(); - - let block = Block::default() - .title(self.build_title()) - .borders(self.borders) - .border_style(if self.focused { - Style::default().fg(Color::Yellow) - } else { - Style::default() - }); - - let inner_area = block.inner(area); - f.render_widget(block, area); - - if inner_area.width == 0 || inner_area.height == 0 { - draw_block_joins(f, area, self.borders, self.joins); - return; - } - - let all_lines = self.build_all_lines(entries, inner_area.width as usize); - let total_lines = all_lines.len(); - let visible_height = inner_area.height as usize; - - let (start, end) = self.calculate_view_window(total_lines, visible_height); - let visible_lines = &all_lines[start..end]; - - let rendered_lines: Vec = visible_lines - .iter() - .map(|(line, prefix_color, is_error)| { - let mut spans = Vec::new(); - - let (prefix, rest) = Self::split_line_prefix(line); - - if !prefix.is_empty() { - spans.push(Span::styled( - prefix.to_string(), - Style::default().fg(*prefix_color), - )); - } - - let (content, timestamp) = Self::split_timestamp_suffix(rest); - let text_color = if *is_error { Color::Red } else { Color::White }; - - if !content.is_empty() { - spans.push(Span::styled( - content.to_string(), - Style::default().fg(text_color), - )); - } - - if !timestamp.is_empty() { - spans.push(Span::styled( - timestamp.to_string(), - Style::default().fg(Color::DarkGray), - )); - } - - Line::from(spans) - }) - .collect(); - - for (idx, line) in rendered_lines.iter().enumerate() { - let line_area = Rect { - x: inner_area.x, - y: inner_area.y + idx as u16, - width: inner_area.width, - height: 1, - }; - f.render_widget(Paragraph::new(line.clone()), line_area); - } - - draw_block_joins(f, area, self.borders, self.joins); - } -} - -impl JoinableElement for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - &*self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - &mut *self - } - - fn set_borders(&mut self, borders: Borders) { - self.borders = borders; - } - - fn set_joins(&mut self, joins: Borders) { - self.joins = joins; - } -} - -impl InteractableElement for LogCard { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn as_element(&self) -> &(dyn Element + 'static) { - &*self - } - - fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { - &mut *self - } - - fn interact(&mut self, key: KeyEvent) -> InteractionResult { - let entries = self.get_logs(); - let estimated_width = 80usize; - let all_lines = self.build_all_lines(entries, estimated_width); - - self.last_total_lines = all_lines.len(); - let visible_height = self.last_visible_height.max(1); - - match key.code { - KeyCode::Enter | KeyCode::Char(' ') => { - self.selected = !self.selected; - InteractionResult::Handled - } - KeyCode::Char('j') | KeyCode::Char('J') => { - let (can_up, _) = self.get_title_hints(); - if can_up { - self.scroll_up(); - } - InteractionResult::Handled - } - KeyCode::Char('k') | KeyCode::Char('K') => { - let (_, can_down) = self.get_title_hints(); - if can_down { - self.scroll_down(); - } - InteractionResult::Handled - } - KeyCode::Up if self.selected => { - let (can_up, _) = self.get_title_hints(); - if can_up { - self.scroll_up(); - } - InteractionResult::Handled - } - KeyCode::Down if self.selected => { - let (_, can_down) = self.get_title_hints(); - if can_down { - self.scroll_down(); - } - InteractionResult::Handled - } - KeyCode::Home => { - if self.last_total_lines > visible_height { - self.scroll_offset = self.last_total_lines - visible_height; - } - InteractionResult::Handled - } - KeyCode::End => { - self.scroll_offset = 0; - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - } - } - - fn can_focus(&self) -> bool { - true - } - - fn is_focused(&self) -> bool { - self.focused - } - - fn focus(&mut self, f: bool) { - self.focused = f; - } -} diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs deleted file mode 100644 index b2557c5..0000000 --- a/src/gui/input_handler.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::gui::ui::{UI, UNIQUE}; -use crate::{RELOAD, SHUTDOWN}; -use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; -use std::sync::Arc; -use std::sync::atomic::Ordering; -use std::time::Duration; - -pub fn setup_input_handler(ui: Arc) { - tokio::spawn(async move { - loop { - if *SHUTDOWN.read().await { - break; - } - - let event_result = tokio::task::spawn_blocking(|| { - if let Ok(true) = poll(Duration::from_millis(100)) { - read().ok().and_then(|ev| match ev { - Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), - _ => None, - }) - } else { - None - } - }) - .await; - - match event_result { - Ok(Some(key_event)) => { - handle_input(key_event, ui.clone()).await; - UNIQUE.store(true, Ordering::Relaxed); - } - Ok(_) => {} - Err(e) => { - eprintln!("Input task error: {}", e); - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - } - }); -} - -pub async fn handle_input(key: KeyEvent, ui: Arc) { - match (key.code, key.modifiers) { - (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) - | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { - *SHUTDOWN.write().await = true; - } - (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { - *RELOAD.write().await = true; - *SHUTDOWN.write().await = true; - } - _ => { - ui.handle_input(key).await; - } - } -} diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs deleted file mode 100644 index 462149a..0000000 --- a/src/gui/interaction_result.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::pin::Pin; - -use crate::gui::screens::screens::Screen; - -#[allow(unused)] -pub enum InteractionResult { - CloseScreen, - OpenScreen { - screen: Box, - }, - OpenFutureScreen { - screen: Pin> + Send>>, - }, - Handled, - Unhandled, -} - -impl Debug for InteractionResult { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"), - InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"), - InteractionResult::CloseScreen => write!(f, "CloseScreen"), - InteractionResult::Handled => write!(f, "Handled"), - InteractionResult::Unhandled => write!(f, "Unhandled"), - } - } -} - -impl PartialEq for InteractionResult { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - ( - InteractionResult::OpenScreen { screen: _ }, - InteractionResult::OpenScreen { screen: _ }, - ) => true, - ( - InteractionResult::OpenFutureScreen { screen: _ }, - InteractionResult::OpenFutureScreen { screen: _ }, - ) => true, - (InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true, - (InteractionResult::Handled, InteractionResult::Handled) => true, - (InteractionResult::Unhandled, InteractionResult::Unhandled) => true, - _ => false, - } - } -} diff --git a/src/gui/mod.rs b/src/gui/mod.rs deleted file mode 100644 index b355046..0000000 --- a/src/gui/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod elements { - pub mod console_card; - pub mod elements; - pub mod graph_card; - pub mod log_card; -} -pub mod screens { - pub mod main_screen; - pub mod md_viewer; - pub mod screens; - pub mod terms_checker; - pub mod terms_updater; -} -pub mod util { - pub mod borders; -} -pub mod app_state; -pub mod input_handler; -pub mod interaction_result; -pub mod ui; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs deleted file mode 100644 index 85dbbaa..0000000 --- a/src/gui/screens/main_screen.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::gui::{ - elements::{ - console_card::ConsoleCard, - elements::{InteractableElement, JoinableElement}, - graph_card::{GRAPHS, GraphCard}, - log_card::LogCard, - }, - interaction_result::InteractionResult, - screens::screens::{NavDirection, Screen}, - ui::UI, -}; - -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Constraint, Layout, Margin, Rect}, - widgets::{Block, Borders}, -}; - -use std::{any::Any, sync::Arc}; - -pub struct MainScreen { - elements: Vec>, - nav_grid: Vec>>, - selected_coords: (usize, usize), - graphs_open: bool, -} - -impl MainScreen { - pub async fn new(ui: Arc) -> Self { - let mut elements: Vec> = Vec::new(); - - let nav_grid = vec![ - vec![Some(0), Some(2)], - vec![Some(0), Some(3)], - vec![Some(1), Some(4)], - ]; - - let mut log_card = LogCard::new(); - log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); - let mut console_card = ConsoleCard::new("Console", ""); - console_card.set_joins(Borders::TOP); - - elements.push(Box::new(log_card)); - elements.push(Box::new(console_card)); - - let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into()); - ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); - elements.push(Box::new(ram_graph)); - let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into()); - cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); - cpu_graph.set_joins(Borders::TOP); - elements.push(Box::new(cpu_graph)); - let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into()); - ping_graph.set_joins(Borders::TOP); - elements.push(Box::new(ping_graph)); - - let graphs_open = true; - - let mut screen = MainScreen { - elements, - nav_grid, - selected_coords: (1, 0), - graphs_open, - }; - screen.focus_current(); - screen - } - - fn focus_current(&mut self) { - let (y, x) = self.selected_coords; - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { - if let Some(element) = self.elements.get_mut(*index) { - if element.can_focus() { - element.focus(true); - } - } - } - } - - fn unfocus_current(&mut self, y: usize, x: usize) { - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { - if let Some(element) = self.elements.get_mut(*index) { - element.focus(false); - } - } - } - - fn navigate(&mut self, direction: NavDirection) { - let (current_row, current_col) = self.selected_coords; - let current_element = self.nav_grid[current_row][current_col]; - - self.unfocus_current(current_row, current_col); - - let (delta_row, delta_col) = match direction { - NavDirection::Up => (-1isize, 0), - NavDirection::Down => (1, 0), - NavDirection::Left => (0, -1), - NavDirection::Right => (0, 1), - _ => (0, 0), - }; - - let mut next_row = current_row as isize; - let mut next_col = current_col as isize; - - loop { - next_row += delta_row; - next_col += delta_col; - - if next_row < 0 || next_col < 0 { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - let next_row_u = next_row as usize; - let next_col_u = next_col as usize; - - if next_row_u >= self.nav_grid.len() { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - - if let Some(row) = self.nav_grid.get(next_row_u) { - if next_col_u >= row.len() { - self.selected_coords = ( - (next_row - delta_row) as usize, - (next_col - delta_col) as usize, - ); - break; - } - - if let Some(next_element) = row[next_col_u] { - if Some(next_element) != current_element { - self.selected_coords = (next_row_u, next_col_u); - self.focus_current(); - return; - } - } - } - } - - self.focus_current(); - } -} - -impl Screen for MainScreen { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, rect: Rect) { - let main_block = Block::default().title("Main").borders(Borders::ALL); - f.render_widget(main_block, rect); - - let inner = rect.inner(Margin { - vertical: 1, - horizontal: 1, - }); - - let graphs_width = if self.graphs_open { 30 } else { 2 }; - let main_width = inner.width.saturating_sub(graphs_width); - - let horizontal_chunks = Layout::default() - .direction(ratatui::layout::Direction::Horizontal) - .constraints([ - Constraint::Length(main_width), - Constraint::Length(graphs_width), - ]) - .split(inner); - - let left_area = horizontal_chunks[0]; - let right_area = horizontal_chunks[1]; - - let left_rows = - Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area); - - if let Some(log) = self.elements.get(0) { - log.as_element().render(f, left_rows[0]); - } - - if let Some(console) = self.elements.get(1) { - console.as_element().render(f, left_rows[1]); - } - - let graph_elements: Vec<_> = self - .elements - .iter() - .filter(|el| el.as_any().is::()) - .collect(); - - if !graph_elements.is_empty() { - let graph_chunks = Layout::vertical( - graph_elements - .iter() - .map(|_| Constraint::Ratio(1, graph_elements.len() as u32)) - .collect::>(), - ) - .split(right_area); - - for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { - el.as_element().render(f, *area); - } - } - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - match event.code { - KeyCode::Up => self.navigate(NavDirection::Up), - KeyCode::Down => self.navigate(NavDirection::Down), - KeyCode::Left => self.navigate(NavDirection::Left), - KeyCode::Right => self.navigate(NavDirection::Right), - KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => { - self.graphs_open = !self.graphs_open; - for element in self.elements.iter_mut() { - if let Some(graph) = element.as_any_mut().downcast_mut::() { - graph.set_open(self.graphs_open); - } - } - return InteractionResult::Handled; - } - _ => { - let (y, x) = self.selected_coords; - if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { - if let Some(el) = self.elements.get_mut(*index) { - return el.interact(event); - } - } - } - } - - InteractionResult::Handled - } -} diff --git a/src/gui/screens/md_viewer.rs b/src/gui/screens/md_viewer.rs deleted file mode 100644 index 10eabce..0000000 --- a/src/gui/screens/md_viewer.rs +++ /dev/null @@ -1,493 +0,0 @@ -use crossterm::event::{self, Event, KeyCode, KeyEvent}; -use ratatui::{ - DefaultTerminal, - prelude::*, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Wrap}, -}; -use std::{any::Any, time::Duration}; - -use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; - -pub struct FileViewer { - title: String, - text: Vec, - scroll: u16, - scroll_x: u16, -} - -impl Screen for FileViewer { - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, rect: Rect) { - self.draw(f, rect); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - match event.code { - KeyCode::Char('q') | KeyCode::Esc => { - return InteractionResult::CloseScreen; - } - - KeyCode::Down => self.scroll = self.scroll.saturating_add(1), - KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), - KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), - KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), - KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), - KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), - - _ => {} - } - - InteractionResult::Unhandled - } -} - -impl FileViewer { - pub fn new(title: String, content: &str) -> Self { - Self { - title, - text: parse_document(content.to_owned()), - scroll: 0, - scroll_x: 0, - } - } - pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal { - loop { - terminal - .draw(|f| { - let area = f.area(); - self.draw(f, area); - }) - .unwrap(); - - if event::poll(Duration::from_millis(100)).unwrap() { - let ev = event::read().unwrap(); - self.handle_event(&ev); - - if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) { - break; - } - } - } - terminal - } - fn draw(&self, f: &mut Frame, area: Rect) { - use ratatui::text::Text; - - let mut rendered_lines = Vec::new(); - - for display_line in &self.text { - if display_line.scrollable { - let content: String = display_line - .line - .spans - .iter() - .map(|s| s.content.clone()) - .collect(); - - let start = self.scroll_x as usize; - let width = area.width as usize - 2; - - let visible = if start < content.chars().count() { - content.chars().skip(start).take(width).collect() - } else { - String::new() - }; - - let mut chars: Vec = visible.chars().collect(); - - if start > 0 && !chars.is_empty() { - chars[0] = '<'; - } - - if start + width < content.chars().count() && !chars.is_empty() { - let last = chars.len() - 1; - chars[last] = '>'; - } - - let visible: String = chars.into_iter().collect(); - - rendered_lines.push(Line::from(Span::styled( - visible, - display_line - .line - .spans - .first() - .map(|s| s.style) - .unwrap_or_default(), - ))); - } else { - rendered_lines.push(display_line.line.clone()); - } - } - - let paragraph = Paragraph::new(Text::from(rendered_lines)) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("{} - [Q to close]", self.title.as_str(),)), - ) - .wrap(Wrap { trim: false }) - .scroll((self.scroll, 0)); - - f.render_widget(paragraph, area); - } - - pub fn handle_event(&mut self, event: &Event) { - if let Event::Key(key) = event { - match key.code { - KeyCode::Down => self.scroll = self.scroll.saturating_add(1), - KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), - KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), - KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), - KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), - KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), - _ => {} - } - } - } -} -fn parse_document(input: String) -> Vec { - let mut lines_vec = Vec::new(); - let mut in_code_block = false; - let liness: Vec = input.lines().map(String::from).collect(); - let mut i = 0; - - while i < liness.len() { - let raw = &liness[i]; - - if raw.trim().starts_with("```") { - in_code_block = !in_code_block; - let code: String = if raw.trim().replace("```", "").is_empty() { - "──".to_string() - } else { - raw.trim().replace("```", "") - }; - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - format!("────────{}────────", code), - Style::default().fg(Color::DarkGray), - )), - scrollable: false, - }); - i += 1; - continue; - } - - if in_code_block { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.to_string(), - Style::default().fg(Color::Yellow), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("### ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("### ").to_string(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("## ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("## ").to_string(), - Style::default() - .fg(Color::LightCyan) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - if raw.starts_with("# ") { - lines_vec.push(DisplayLine { - line: Line::from(Span::styled( - raw.trim_start_matches("# ").to_string(), - Style::default() - .fg(Color::Gray) - .add_modifier(Modifier::BOLD), - )), - scrollable: false, - }); - i += 1; - continue; - } - - if raw.trim_start().starts_with("- ") { - let indent = raw.chars().take_while(|c| *c == ' ').count(); - lines_vec.push(DisplayLine { - line: Line::from(Span::raw(format!( - "{}• {}", - " ".repeat(indent), - raw.trim_start_matches("- ") - ))), - scrollable: false, - }); - i += 1; - continue; - } - - if raw.trim().starts_with('|') && raw.contains('|') { - let mut table_lines = vec![raw.clone()]; - let mut j = i + 1; - while j < liness.len() && liness[j].trim().starts_with('|') { - table_lines.push(liness[j].clone()); - j += 1; - } - - let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::>()); - lines_vec.extend(table_to_lines(table)); - i = j; - continue; - } - - lines_vec.push(DisplayLine { - line: Line::from(parse_inline(raw.as_str())), - scrollable: false, - }); - i += 1; - } - - lines_vec -} - -fn parse_inline(input: &str) -> Vec> { - let mut spans = Vec::new(); - let mut buf = String::new(); - - let mut bold = false; - let mut underline = false; - let mut code = false; - - let mut chars = input.chars().peekable(); - - while let Some(c) = chars.next() { - let toggle = match c { - '*' if chars.peek() == Some(&'*') => { - chars.next(); - Some("bold") - } - '_' if chars.peek() == Some(&'_') => { - chars.next(); - Some("underline") - } - '`' => Some("code"), - _ => None, - }; - - if let Some(kind) = toggle { - flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); - - match kind { - "bold" => bold = !bold, - "underline" => underline = !underline, - "code" => code = !code, - _ => {} - } - continue; - } - - buf.push(c); - } - - flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); - spans -} - -fn current_style(bold: bool, underline: bool, code: bool) -> Style { - let mut style = Style::default(); - - if bold { - style = style.add_modifier(Modifier::BOLD); - } - if underline { - style = style.add_modifier(Modifier::UNDERLINED); - } - if code { - style = style.fg(Color::Yellow); - } - - style -} -#[derive(Clone)] -pub struct DisplayLine { - line: Line<'static>, - scrollable: bool, -} - -fn table_to_lines(table: Vec>) -> Vec { - if table.len() < 2 { - return vec![]; - } - - 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 = header - .iter() - .enumerate() - .map(|(i, h)| { - let h_len = h.chars().count().max(1); - if i == 0 { - table - .iter() - .map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0)) - .max() - .unwrap_or(h_len) - } else { - let max = (3 * h_len) as usize; - max.max(h_len) - } - }) - .collect(); - - let mut lines = Vec::new(); - - for (row_idx, row) in table.iter().enumerate() { - if row_idx == 1 { - let divider = widths - .iter() - .map(|w| "─".repeat(*w)) - .collect::>() - .join("─┼─"); - - lines.push(DisplayLine { - line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))), - scrollable: true, - }); - continue; - } - - let wrapped_cells: Vec> = row - .iter() - .enumerate() - .map(|(i, cell)| wrap_cell(cell, widths[i])) - .collect(); - - let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1); - - for line_idx in 0..row_height { - let mut line = String::new(); - - for (i, cell) in wrapped_cells.iter().enumerate() { - let content = cell.get(line_idx).map(String::as_str).unwrap_or(""); - line.push_str(&format!("{:width$}", content, width = widths[i])); - if i < wrapped_cells.len() - 1 { - line.push_str(" │ "); - } - } - - let style = if row_idx == 0 { - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Green) - }; - - lines.push(DisplayLine { - line: Line::from(Span::styled(line, style)), - scrollable: true, - }); - } - } - - lines -} -fn wrap_cell(cell: &str, width: usize) -> Vec { - if width == 0 { - return vec![String::new()]; - } - - let mut lines = Vec::new(); - let mut current = String::new(); - - 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::>().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::>().chunks(width) { - lines.push(chunk.iter().collect()); - } - } - } - } - - if !current.is_empty() { - lines.push(current); - } - - if lines.is_empty() { - lines.push(String::new()); - } - - lines -} - -fn flush_span(spans: &mut Vec, buf: &mut String, style: Style) { - if !buf.is_empty() { - spans.push(Span::styled(buf.clone(), style)); - buf.clear(); - } -} - -fn parse_table(lines: &[&str]) -> Vec> { - let mut table = Vec::new(); - - for &line in lines { - if !line.starts_with('|') || !line.contains('|') { - break; - } - let row: Vec = line - .trim_matches('|') - .split('|') - .map(|s| s.trim().to_string()) - .collect(); - table.push(row); - } - - table -} diff --git a/src/gui/screens/screens.rs b/src/gui/screens/screens.rs deleted file mode 100644 index 9676fbb..0000000 --- a/src/gui/screens/screens.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::any::Any; - -use crossterm::event::KeyEvent; -use ratatui::{Frame, layout::Rect}; - -use crate::gui::interaction_result::InteractionResult; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NavDirection { - Up, - Down, - Left, - Right, - - Next, - Prev, -} - -pub trait Screen: Send + Sync + Any { - fn as_any(&self) -> &dyn Any; - fn as_any_mut(&mut self) -> &mut dyn Any; - - fn render(&self, f: &mut Frame, rect: Rect); - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; -} diff --git a/src/gui/screens/terms_checker.rs b/src/gui/screens/terms_checker.rs deleted file mode 100644 index 26bb7c6..0000000 --- a/src/gui/screens/terms_checker.rs +++ /dev/null @@ -1,351 +0,0 @@ -use crate::{ - gui::{ - interaction_result::InteractionResult, - screens::{md_viewer::FileViewer, screens::Screen}, - ui::UI, - }, - terms::{ - buttons::{checkbox, draw_buttons}, - consent_state::UserChoice, - focus::Focus, - terms_getter::{Type, get_link, get_terms}, - }, -}; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph}, -}; -use std::{any::Any, pin::Pin, sync::Arc}; -use tokio::sync::oneshot; - -pub struct TermsCheckerScreen { - ui: Arc, - sender: Option>, - - eula: bool, - tos: bool, - pp: bool, - - focus: Focus, -} - -impl TermsCheckerScreen { - pub fn new(ui: Arc, sender: Option>) -> Self { - Self { - ui, - sender, - eula: false, - tos: false, - pp: false, - focus: Focus::Eula, - } - } -} - -impl Screen for TermsCheckerScreen { - fn as_any(&self) -> &dyn Any { - self - } - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, size: Rect) { - let mut needed_height = 5; - - if size.height < 6 || size.width < 27 { - f.render_widget( - Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), - size, - ); - return; - } - - let max_width = 150; - let max_height = 26; - - let content_width = if max_width < size.width { - max_width - } else { - size.width - }; - let content_height = if max_height < size.height { - max_height - } else { - size.height - }; - - let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; - let vertical_margin = (size.height.saturating_sub(content_height)) / 2; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(7), Constraint::Length(3)]) - .split(Rect { - x: horizontal_margin, - y: vertical_margin, - width: content_width, - height: content_height, - }); - let eula_text = if size.width < 70 { - "EULA ¹ (https://legal.tensamin.net/eula/)" - } 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/)" - } 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/)" - } else { - "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" - }; - - let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = if size.width > 143 { - ( - vec![8, 3, 8, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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 > 92 { - ( - vec![9, 3, 9, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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![9, 3, 9, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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.", - ], - ) - } else { - ( - vec![10, 3, 11, 5], - vec![ - "", - "By selecting Continue, you confirm that you 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.", - ], - ) - }; - let mut text_lines = vec![ - checkbox(eula_text, self.eula, self.focus == Focus::Eula, true), - checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula), - checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula), - Line::from(""), - Line::from("¹ Necessary– required to run the program"), - Line::from("² Optional – required only for Tensamin services"), - ]; - for line in agree_lines { - text_lines.insert(text_lines.len(), Line::from(line)); - } - - while size.height - 5 < text_lines.len() as u16 { - if optional_lines.len() == 0 { - break; - } - text_lines.remove(optional_lines[0] as usize); - optional_lines.remove(0); - } - needed_height += text_lines.len(); - - if size.width < 60 || size.height < needed_height as u16 { - let width_style = if size.width > 76 { - Style::default().fg(Color::Green) - } else if size.width >= 60 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let height_style = if size.height > 19 { - Style::default().fg(Color::Green) - } else if size.height >= 13 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let warning_text = Text::from(vec![ - Line::from(vec![ - Span::raw("Width: "), - Span::styled(format!("{}", size.width), width_style), - Span::raw(" / 60"), - ]), - Line::from(vec![ - Span::raw("Height: "), - Span::styled(format!("{}", size.height), height_style), - Span::raw(format!(" / 13")), - ]), - ]); - - let warning = Paragraph::new(warning_text) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("UI Too Small [Q to Quit]")), - ); - f.render_widget(warning, size); - return; - } - - let consent_block = Paragraph::new(Text::from(text_lines)).block( - Block::default() - .title(format!(" Tensamin User Consent [Q to Quit] ",)) - .borders(Borders::ALL), - ); - f.render_widget(consent_block, chunks[0]); - - draw_buttons( - f, - chunks[1], - self.focus, - (self.eula, self.tos && self.pp), - true, - false, - true, - ); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel]; - - if self.eula { - possible_states.push(Focus::Continue); - if self.tos && self.pp { - possible_states.push(Focus::ContinueAll); - } - } - - match event.code { - KeyCode::Esc => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - KeyCode::Up | KeyCode::Left => { - self.focus.prev(&possible_states); - InteractionResult::Handled - } - KeyCode::Down | KeyCode::Right | KeyCode::Tab => { - self.focus.next(&possible_states); - InteractionResult::Handled - } - KeyCode::Char('q') | KeyCode::Char('Q') => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - KeyCode::Char('o') | KeyCode::Char('O') => { - let terms_type = match self.focus { - Focus::Eula => Some(Type::EULA), - Focus::Tos => Some(Type::TOS), - Focus::Pp => Some(Type::PP), - _ => None, - }; - if let Some(terms_type) = terms_type { - let fut: Pin> + Send>> = - Box::pin(async move { - let content = get_terms(terms_type.clone()).await.unwrap(); - let screen: FileViewer = - FileViewer::new(terms_type.to_string(), &content); - Box::new(screen) as Box - }); - InteractionResult::OpenFutureScreen { screen: fut } - } else { - InteractionResult::Unhandled - } - } - KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { - Focus::Eula => { - let _ = open::that(get_link(Type::EULA)); - InteractionResult::Handled - } - Focus::Tos => { - let _ = open::that(get_link(Type::TOS)); - InteractionResult::Handled - } - Focus::Pp => { - let _ = open::that(get_link(Type::PP)); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Enter | KeyCode::Char(' ') => match self.focus { - Focus::Eula => { - self.eula = !self.eula; - self.tos = false; - self.pp = false; - InteractionResult::Handled - } - Focus::Tos if self.eula => { - self.tos = !self.tos; - InteractionResult::Handled - } - Focus::Pp if self.eula => { - self.pp = !self.pp; - InteractionResult::Handled - } - Focus::Cancel => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - Focus::Continue if self.eula => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptEULA); - } - InteractionResult::CloseScreen - } - Focus::ContinueAll if self.eula && self.tos && self.pp => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptAll); - } - InteractionResult::CloseScreen - } - _ => InteractionResult::Unhandled, - }, - - _ => InteractionResult::Unhandled, - } - } -} diff --git a/src/gui/screens/terms_updater.rs b/src/gui/screens/terms_updater.rs deleted file mode 100644 index 35da364..0000000 --- a/src/gui/screens/terms_updater.rs +++ /dev/null @@ -1,691 +0,0 @@ -use crate::{ - gui::{ - interaction_result::InteractionResult, - screens::{md_viewer::FileViewer, screens::Screen}, - }, - terms::{ - buttons::{checkbox, draw_buttons}, - consent_state::{UpdateDecision, UserChoice}, - doc::Doc, - focus::Focus, - terms_getter::{Type, get_newest_link, get_terms}, - }, -}; -use chrono::{Local, TimeZone, Utc}; -use crossterm::event::{KeyCode, KeyEvent}; -use ratatui::{ - Frame, - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph}, -}; -use std::any::Any; -use tokio::sync::oneshot; - -pub struct TermsUpdaterScreen { - sender: Option>, - - eula_needed: bool, - tos_needed: bool, - pp_needed: bool, - - eula_future: bool, - tos_future: bool, - pp_future: bool, - - eula_for_future: Option, - tos_for_future: Option, - pp_for_future: Option, - - update_needed: bool, - - eula: bool, - tos: bool, - pp: bool, - - focus: Focus, -} -impl TermsUpdaterScreen { - pub fn new( - consent_eula: UpdateDecision, - consent_tos: UpdateDecision, - consent_pp: UpdateDecision, - sender: Option>, - ) -> Self { - let (eula_needed, eula_future, eula_for_future): (bool, bool, Option) = - match consent_eula { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - let (tos_needed, tos_future, tos_for_future): (bool, bool, Option) = match consent_tos - { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - let (pp_needed, pp_future, pp_for_future): (bool, bool, Option) = match consent_pp { - UpdateDecision::NoChange => (false, false, None), - UpdateDecision::Future { newest } => (true, true, Some(newest)), - UpdateDecision::Forced(doc) => (true, false, Some(doc)), - }; - - let focus = if eula_needed { - Focus::Eula - } else if tos_needed { - Focus::Tos - } else if pp_needed { - Focus::Pp - } else { - Focus::Cancel - }; - - let update_needed = (eula_needed && !eula_future) - || (tos_needed && !tos_future) - || (pp_needed && !pp_future); - - Self { - sender, - - eula_needed, - tos_needed, - pp_needed, - - eula_for_future, - tos_for_future, - pp_for_future, - - eula_future, - tos_future, - pp_future, - - update_needed, - - eula: !eula_needed, - tos: !tos_needed, - pp: !pp_needed, - - focus, - } - } -} -impl Screen for TermsUpdaterScreen { - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn render(&self, f: &mut Frame, size: Rect) { - let mut needed_height = 5; - - if size.height < 6 || size.width < 27 { - f.render_widget( - Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), - size, - ); - return; - } - - let max_width = 150; - let max_height = 30; - - let content_width = if max_width < size.width { - max_width - } else { - size.width - }; - let content_height = if max_height < size.height { - max_height - } else { - size.height - }; - - let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; - let vertical_margin = (size.height.saturating_sub(content_height)) / 2; - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(7), Constraint::Length(3)]) - .split(Rect { - x: horizontal_margin, - y: vertical_margin, - width: content_width, - height: content_height, - }); - - let mut text_lines: Vec = Vec::new(); - let mut header_lines = 0; - let separator = if size.width > 72 { - header_lines += 3; - text_lines.push(Line::from( - "You previously accepted earlier versions of Tensamin’s legal documents.", - )); - text_lines.push(Line::from( - "Some of them have been updated and are listed below for your review.", - )); - text_lines.push(Line::from("")); - 2 - } else { - header_lines += 5; - text_lines.push(Line::from("You previously accepted earlier ")); - text_lines.push(Line::from("versions of Tensamin’s legal documents.")); - text_lines.push(Line::from("Some of them have been updated and")); - text_lines.push(Line::from("are listed below for your review.")); - text_lines.push(Line::from("")); - 4 - }; - - if self.eula_needed { - header_lines += 1; - if self.eula_future { - if size.width < 80 { - text_lines.push(checkbox( - "EULA ¹³ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - - header_lines += 1; - let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - - header_lines += 1; - let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "EULA ¹ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - } else { - text_lines.push(checkbox( - "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", - self.eula, - self.focus == Focus::Eula, - true, - )); - } - } - } - - if self.tos_needed { - header_lines += 1; - if self.tos_future { - if size.width < 80 { - text_lines.push(checkbox( - "ToS ²³ (https://legal.tensamin.net/tos/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "ToS ² (https://legal.tensamin.net/tos/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - } else { - text_lines.push(checkbox( - "Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", - self.tos, - self.focus == Focus::Tos, - self.eula, - )); - } - } - } - - if self.pp_needed { - header_lines += 1; - if self.pp_future { - if size.width < 80 { - text_lines.push(checkbox( - "PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } else { - text_lines.push(checkbox( - "Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - - header_lines += 1; - let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; - let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); - let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); - text_lines.push(Line::from(format!(" Goes into effect on {}", date))); - } - } else { - if size.width < 80 { - text_lines.push(checkbox( - "PP ² (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - } else { - text_lines.push(checkbox( - "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", - self.pp, - self.focus == Focus::Pp, - self.eula, - )); - } - } - } - - text_lines.push(Line::from("")); - text_lines.push(Line::from("¹ Necessary to run the program")); - text_lines.push(Line::from("² Optional - only for Tensamin services")); - if size.width < 100 { - text_lines.push(Line::from( - "³ Future version - consent stored now, takes effect later", - )); - } else { - text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply.")); - } - - text_lines.push(Line::from("")); - - let mut optional_lines: Vec = if size.width > 143 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 6, - header_lines, - header_lines + 6, - separator, - header_lines + 2, - ] - } - } else if size.width > 92 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you agree to the End User", - )); - text_lines.push(Line::from( - "License Agreement and applicable Terms of Service.", - )); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you agree to the End User", - )); - text_lines.push(Line::from( - "License Agreement and applicable Terms of Service.", - )); - } - - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 8, - header_lines, - header_lines + 8, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } - } else if size.width > 73 { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you read, understood and", - )); - text_lines.push(Line::from( - "agree to the End User License Agreement and applicable Terms of Service.", - )); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate once these changes apply.", - )); - } else { - text_lines.push(Line::from( - "By selecting Continue, you confirm that you read, understood and", - )); - text_lines.push(Line::from( - "agree to the End User License Agreement and applicable Terms of Service.", - )); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "Tensamin services require acceptance of the ToS and Privacy Policy.", - )); - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "While having a document selected press O to view in this UI or press L", - )); - text_lines.push(Line::from("to open as a link.")); - - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 8, - header_lines, - header_lines + 8, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 7, - header_lines, - header_lines + 7, - separator, - header_lines + 2, - ] - } - } else { - if self.tos_needed || self.pp_needed { - text_lines.push(Line::from("By selecting Continue, you confirm that you")); - text_lines.push(Line::from("agree to the End User License")); - text_lines.push(Line::from("Agreement and applicable Terms of Service.")); - text_lines.push(Line::from( - "On Downgrade: Tensamin Services will deactivate", - )); - text_lines.push(Line::from("once these changes apply.")); - } else { - text_lines.push(Line::from("By selecting Continue, you confirm that you")); - text_lines.push(Line::from("agree to the End User License")); - text_lines.push(Line::from("Agreement and applicable Terms of Service.")); - } - text_lines.push(Line::from("")); - text_lines.push(Line::from("Tensamin services require acceptance of the")); - text_lines.push(Line::from("Terms of Service and Privacy Policy.")); - text_lines.push(Line::from("")); - text_lines.push(Line::from( - "While having a document selected press O to view", - )); - text_lines.push(Line::from("in this UI or press L to open as a link.")); - if self.tos_needed || self.pp_needed { - vec![ - header_lines + 10, - header_lines, - header_lines + 11, - separator, - header_lines + 2, - ] - } else { - vec![ - header_lines + 8, - header_lines, - header_lines + 9, - separator, - header_lines + 2, - ] - } - }; - - while size.height - 5 < text_lines.len() as u16 { - if optional_lines.len() == 0 { - break; - } - text_lines.remove(optional_lines[0] as usize); - optional_lines.remove(0); - } - needed_height += text_lines.len(); - - let q_informer = if self.update_needed { - "Q to Exit" - } else { - "Q to Cancel" - }; - if size.width < 60 || size.height < needed_height as u16 { - let width_style = if size.width > 76 { - Style::default().fg(Color::Green) - } else if size.width >= 60 { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let height_style = if size.height > 20 { - Style::default().fg(Color::Green) - } else if size.height >= (header_lines as u16 + 10) { - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Red) - }; - - let warning_text = Text::from(vec![ - Line::from(vec![ - Span::raw("Width: "), - Span::styled(format!("{}", size.width), width_style), - Span::raw(" / 60"), - ]), - Line::from(vec![ - Span::raw("Height: "), - Span::styled(format!("{}", size.height), height_style), - Span::raw(format!(" / {}", header_lines + 10)), - ]), - ]); - - let warning = Paragraph::new(warning_text) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("UI Too Small [{}]", q_informer)), - ); - - f.render_widget(warning, size); - return; - } - - let consent_block = Paragraph::new(Text::from(text_lines)).block( - Block::default() - .title(format!(" Update Tensamin User Consent [{}] ", q_informer)) - .borders(Borders::ALL), - ); - f.render_widget(consent_block, chunks[0]); - - let downgrade_scenario = self.tos_needed || self.pp_needed; - draw_buttons( - f, - chunks[1], - self.focus, - (self.eula, self.tos && self.pp), - self.update_needed, - downgrade_scenario, - self.pp_needed || self.tos_needed, - ); - } - - fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { - let mut possible_states = Vec::new(); - - if self.eula_needed { - possible_states.push(Focus::Eula); - } - if self.tos_needed { - possible_states.push(Focus::Tos); - } - if self.pp_needed { - possible_states.push(Focus::Pp); - } - - possible_states.push(Focus::Cancel); - - if self.eula { - possible_states.push(Focus::Continue); - if self.tos && self.pp { - possible_states.push(Focus::ContinueAll); - } - } - - match event.code { - KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - return InteractionResult::CloseScreen; - } - KeyCode::Enter | KeyCode::Char(' ') => match self.focus { - Focus::Eula => { - self.eula = !self.eula; - self.tos = !self.tos_needed; - self.pp = !self.pp_needed; - InteractionResult::Handled - } - Focus::Tos if self.eula => { - self.tos = !self.tos; - InteractionResult::Handled - } - Focus::Pp if self.eula => { - self.pp = !self.pp; - InteractionResult::Handled - } - Focus::Cancel => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::Deny); - } - InteractionResult::CloseScreen - } - Focus::Continue if self.eula => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptEULA); - } - InteractionResult::CloseScreen - } - Focus::ContinueAll if self.eula && self.tos && self.pp => { - if let Some(sender) = self.sender.take() { - let _ = sender.send(UserChoice::AcceptAll); - } - InteractionResult::CloseScreen - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Char('o') | KeyCode::Char('O') => { - let terms_type = match self.focus { - Focus::Eula => Some(Type::EULA), - Focus::Tos => Some(Type::TOS), - Focus::Pp => Some(Type::PP), - _ => None, - }; - - if let Some(terms_type) = terms_type { - let fut = Box::pin(async move { - let content = get_terms(terms_type.clone()).await.unwrap(); - Box::new(FileViewer::new(terms_type.to_string(), &content)) - as Box - }); - - return InteractionResult::OpenFutureScreen { screen: fut }; - } else { - InteractionResult::Unhandled - } - } - KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { - Focus::Eula => { - let _ = open::that(get_newest_link(Type::EULA)); - InteractionResult::Handled - } - Focus::Tos => { - let _ = open::that(get_newest_link(Type::TOS)); - InteractionResult::Handled - } - Focus::Pp => { - let _ = open::that(get_newest_link(Type::PP)); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - }, - KeyCode::Up | KeyCode::Left => { - self.focus.prev(&possible_states); - InteractionResult::Handled - } - KeyCode::Down | KeyCode::Right | KeyCode::Tab => { - self.focus.next(&possible_states); - InteractionResult::Handled - } - _ => InteractionResult::Unhandled, - } - } -} diff --git a/src/gui/tui.rs b/src/gui/tui.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/gui/ui.rs b/src/gui/ui.rs deleted file mode 100644 index 5903a4b..0000000 --- a/src/gui/ui.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::{ - ACTIVE_TASKS, SHUTDOWN, - gui::{ - input_handler::setup_input_handler, interaction_result::InteractionResult, - screens::screens::Screen, - }, -}; -use crossterm::event::KeyEvent; -use once_cell::sync::Lazy; -use ratatui::{Terminal, backend::CrosstermBackend, init}; -use std::{ - collections::VecDeque, - io::Stdout, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; -use tokio::{sync::RwLock, time::Instant}; - -/// UI state and rendering -pub static UNIQUE: AtomicBool = AtomicBool::new(true); - -pub static FPS: Lazy> = Lazy::new(|| RwLock::new((0.0, 0.0))); - -pub struct UI { - pub terminal: Arc>>>, - screen_stack: Arc>>>, -} - -pub fn start_tui() -> Arc { - let ui = Arc::new(UI::new()); - let uic = ui.clone(); - ACTIVE_TASKS.insert("UI Renderer".to_string()); - tokio::spawn(async move { - let mut last_render = Instant::now(); - - let mut fps_samples: VecDeque = VecDeque::with_capacity(20); - let mut skip_samples: VecDeque = VecDeque::with_capacity(20); - - let mut fps_sum = 0.0; - let mut skip_sum: u32 = 0; - - let mut skipped = 0; - - loop { - if *SHUTDOWN.read().await { - break; - } - - if skipped > 5 || UNIQUE.load(Ordering::Relaxed) { - uic.render().await; - - skip_samples.push_back(skipped); - skip_sum += skipped as u32; - - if skip_samples.len() > 20 { - if let Some(old) = skip_samples.pop_front() { - skip_sum -= old as u32; - } - } - - skipped = 0; - - let elapsed = last_render.elapsed().as_secs_f64(); - if elapsed > 0.0 { - let fps = 1.0 / elapsed; - - fps_samples.push_back(fps); - fps_sum += fps; - - if fps_samples.len() > 20 { - if let Some(old) = fps_samples.pop_front() { - fps_sum -= old; - } - } - } - - let avg_fps = if !fps_samples.is_empty() { - fps_sum / fps_samples.len() as f64 - } else { - 0.0 - }; - - let avg_skips_percentage = if !skip_samples.is_empty() { - let avg_skipped = skip_sum as f64 / skip_samples.len() as f64; - let total_iterations = avg_skipped + 1.0; - (avg_skipped / total_iterations) * 100.0 - } else { - 0.0 - }; - - *FPS.write().await = (avg_fps, avg_skips_percentage); - - last_render = Instant::now(); - UNIQUE.store(false, Ordering::Relaxed); - } else { - skipped += 1; - } - tokio::time::sleep(Duration::from_millis(16)).await; - } - ACTIVE_TASKS.remove("UI Renderer"); - ratatui::restore(); - }); - setup_input_handler(ui.clone()); - ui -} -impl UI { - pub fn new() -> Self { - let terminal = init(); - Self { - terminal: Arc::new(Mutex::new(terminal)), - screen_stack: Arc::new(RwLock::new(Vec::new())), - } - } - - pub async fn set_screen(&self, screen: Box) { - self.screen_stack.write().await.push(screen); - } - pub async fn replace_screen(&self, screen: Box) { - let mut stack = self.screen_stack.write().await; - stack.pop(); - stack.push(screen); - } - pub async fn handle_input(self: Arc, key_event: KeyEvent) { - let result = { - let mut stack = self.screen_stack.write().await; - if let Some(screen) = stack.last_mut() { - screen.handle_input(key_event) - } else { - return; - } - }; - match result { - InteractionResult::OpenScreen { screen } => { - self.set_screen(screen).await; - } - InteractionResult::OpenFutureScreen { screen: fut } => { - let ui = self.clone(); - let screen = fut.await; - ui.set_screen(screen).await; - } - InteractionResult::CloseScreen => { - let mut stack = self.screen_stack.write().await; - stack.pop(); - - if stack.is_empty() { - *SHUTDOWN.write().await = true; - } - } - InteractionResult::Handled => {} - InteractionResult::Unhandled => {} - } - } - - pub async fn render(&self) { - if let Some(screen) = self.screen_stack.read().await.last() { - let mut terminal = self.terminal.lock().unwrap(); - terminal - .draw(|f| { - screen.render(f, f.area()); - }) - .unwrap(); - } - } -} diff --git a/src/gui/util/borders.rs b/src/gui/util/borders.rs deleted file mode 100644 index 34460bc..0000000 --- a/src/gui/util/borders.rs +++ /dev/null @@ -1,63 +0,0 @@ -use ratatui::layout::Rect; -use ratatui::prelude::*; -use ratatui::style::Style; -use ratatui::widgets::Borders; - -fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { - frame - .buffer_mut() - .set_string(x, y, c.to_string(), Style::default()); -} - -pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { - let x0 = area.x; - let y0 = area.y; - let x1 = area.x + area.width - 1; - let y1 = area.y + area.height - 1; - - if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { - let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { - (true, true) => '┼', - (true, false) => '├', - (false, true) => '┬', - (false, false) => '┌', - }; - set_join_char(frame, x0, y0, top_left); - } - - if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { - let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { - (true, true) => '┼', - (true, false) => '┤', - (false, true) => '┬', - (false, false) => '┐', - }; - set_join_char(frame, x1, y0, top_right); - } - - if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { - let bottom_left = match ( - joins.contains(Borders::BOTTOM), - joins.contains(Borders::LEFT), - ) { - (true, true) => '┼', - (true, false) => '├', - (false, true) => '┴', - (false, false) => '└', - }; - set_join_char(frame, x0, y1, bottom_left); - } - - if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { - let bottom_right = match ( - joins.contains(Borders::BOTTOM), - joins.contains(Borders::RIGHT), - ) { - (true, true) => '┼', - (true, false) => '┤', - (false, true) => '┴', - (false, false) => '┘', - }; - set_join_char(frame, x1, y1, bottom_right); - } -} diff --git a/src/langu/language_creator.rs b/src/langu/language_creator.rs deleted file mode 100644 index fc6d1c5..0000000 --- a/src/langu/language_creator.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::util::file_util::save_file; -use json::{self, JsonError, JsonValue}; - -pub fn create_languages() -> Result<(), JsonError> { - let mut frontend_messages = JsonValue::new_object(); - let mut omikron_messages = JsonValue::new_object(); - let mut button_texts = JsonValue::new_object(); - let mut general_texts = JsonValue::new_object(); - let mut debug_messages = JsonValue::new_object(); - - frontend_messages.insert("error", "An error occurred")?; - // FRONTEND - frontend_messages.insert("get_chats", "User {} is loading conversations")?; - frontend_messages.insert("message_get", "User {} is loading messages")?; - frontend_messages.insert("get_communities", "User {} is loading communities")?; - frontend_messages.insert("client_connected", "Client {} connected")?; - frontend_messages.insert("add_conversation", "User {} added {}")?; - frontend_messages.insert("message_send", "User {} sent a message")?; - - // OMIKRON - omikron_messages.insert( - "identification_response", - "IOTA identified on Omikron, {} users!", - )?; - omikron_messages.insert( - "send_message_failed", - "Failed to send message to Omikron: {}", - )?; - omikron_messages.insert("connection_failed", "Failed to connect to Omikron: {}")?; - - // BUTTONS - button_texts.insert("exit", "Exit")?; - - // GENERAL - general_texts.insert("iota_id", "IOTA ID: {}-####-####-####-############")?; - general_texts.insert("user_id", "USER ID: {}")?; - general_texts.insert("user_ids", "USER IDS: {}")?; - general_texts.insert("user_load_failed", "Failed to load user data")?; - general_texts.insert("setup_completed", "Launched")?; - general_texts.insert( - "community_active", - "Communities active on ws://{}:{}/community/...", - )?; - general_texts.insert( - "community_start_error", - "Failed to start community socket on port {}!", - )?; - general_texts.insert( - "community_start_error_admin", - "Failed to start community socket on port {}! Run with admin privileges", - )?; - // DEBUG - debug_messages.insert("", "")?; - save_file( - "languages/en_INT", - "frontend.json", - &frontend_messages.to_string(), - ); - save_file( - "languages/en_INT", - "omikron.json", - &omikron_messages.to_string(), - ); - save_file( - "languages/en_INT", - "buttons.json", - &button_texts.to_string(), - ); - save_file( - "languages/en_INT", - "debug.json", - &debug_messages.to_string(), - ); - save_file( - "languages/en_INT", - "general.json", - &general_texts.to_string(), - ); - Ok(()) -} diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs deleted file mode 100644 index c903886..0000000 --- a/src/langu/language_manager.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::util::file_util::{self}; -use json::parse; -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::Mutex; - -#[derive(Clone)] -pub struct LanguagePack { - language: HashMap, -} - -pub static LANGUAGE_PACK: Lazy> = - 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 { - 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); -} - -pub fn from_key(key: &str) -> String { - LANGUAGE_PACK - .lock() - .unwrap() - .get_translation(key) - .to_string() -} - -pub fn format(key: &str, args: &[&str]) -> String { - let message = from_key(key); - let mut formatted = String::new(); - let parts = message.split("{}"); - for (i, part) in parts.enumerate() { - formatted.push_str(part); - if i < args.len() { - formatted.push_str(args[i]); - } - } - formatted -} - -impl LanguagePack { - pub fn new(language: &str) -> Self { - let mut pack = LanguagePack { - language: HashMap::new(), - }; - pack.load_language(language); - pack - } - - pub fn load_language(&mut self, language: &str) { - let path = format!("languages/{}/", language); - - let frontend_messages = file_util::load_file(&path, "frontend.json"); - let frontend_messages = parse(&frontend_messages).unwrap(); - for (key, value) in frontend_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let omikron_messages = file_util::load_file(&path, "omikron.json"); - let omikron_messages = parse(&omikron_messages).unwrap(); - for (key, value) in omikron_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let button_texts = file_util::load_file(&path, "buttons.json"); - let button_texts = parse(&button_texts).unwrap(); - for (key, value) in button_texts.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let debug_messages = file_util::load_file(&path, "debug.json"); - let debug_messages = parse(&debug_messages).unwrap(); - for (key, value) in debug_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - - let general_messages = file_util::load_file(&path, "general.json"); - let general_messages = parse(&general_messages).unwrap(); - for (key, value) in general_messages.entries() { - self.language - .insert(key.to_string(), value.as_str().unwrap().to_string()); - } - } - pub fn get_translation(&self, key: &str) -> String { - match self.language.get(key) { - Some(v) if !v.is_empty() => v.clone(), - _ => key.to_uppercase(), - } - } -} diff --git a/src/langu/mod.rs b/src/langu/mod.rs deleted file mode 100644 index 352d639..0000000 --- a/src/langu/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod language_creator; -pub mod language_manager; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 767106f..0000000 --- a/src/main.rs +++ /dev/null @@ -1,183 +0,0 @@ -use dashmap::DashSet; -use once_cell::sync::Lazy; -use pnet::datalink::NetworkInterface; -use std::sync::Arc; -use std::sync::LazyLock; -use std::sync::Mutex; -use tokio::sync::RwLock; -use tokio::time::{Duration, sleep}; - -mod auth; -mod gui; -mod langu; -mod omikron; -mod terms; -mod users; -mod util; - -use crate::gui::app_state; -use crate::gui::app_state::AppState; -use crate::gui::screens::main_screen::MainScreen; -use crate::gui::ui::start_tui; -use crate::langu::language_creator; -use crate::omikron::omikron_connection::OmikronConnection; -use crate::terms::consent_state; -use crate::users::user_manager; -use crate::util::config_util::CONFIG; -use crate::util::file_util::download_and_extract_zip; -use crate::util::file_util::has_dir; -use crate::util::logger; - -pub static APP_STATE: LazyLock>> = - LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); - -pub static SHUTDOWN: Lazy> = Lazy::new(|| RwLock::new(false)); -pub static RELOAD: Lazy> = Lazy::new(|| RwLock::new(true)); -pub static ACTIVE_TASKS: Lazy> = Lazy::new(|| DashSet::new()); - -#[tokio::main(flavor = "multi_thread", worker_threads = 16)] -#[allow(unused_must_use, dead_code)] -async fn main() { - while *RELOAD.read().await { - *RELOAD.write().await = false; - *SHUTDOWN.write().await = false; - - let ui = start_tui(); - - let (eula, tos_pp) = consent_state::check(ui.clone()).await; - - if !eula { - *SHUTDOWN.write().await = true; - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_millis(100)).await; - } - println!("You need to accept our End User Licence Agreement before launching!"); - println!("You can find this at 'agreements'!"); - return; - } - if !tos_pp { - *SHUTDOWN.write().await = true; - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_millis(100)).await; - } - println!( - "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" - ); - println!("In future releases this will be optional!"); - println!("You can find this at 'agreements'!"); - return; - } - app_state::setup(); - - let main_screen = MainScreen::new(ui.clone()).await; - ui.set_screen(Box::new(main_screen)).await; - - // LANGUAGE PACK - if let Err(e) = language_creator::create_languages() { - println!("Language pack creation failed: {}", e); - return; - } - - // UI - logger::startup(); - - // BASIC CONFIGURATION - &CONFIG.write().await.load(); - - // USER MANAGEMENT - if let Err(_) = user_manager::load_users().await { - log_t!("user_load_failed"); - } - - let mut sb = "".to_string(); - - for up in user_manager::get_users() { - sb = sb + "," + &up.user_id.to_string().as_str(); - } - - if !sb.is_empty() { - {} - sb.remove(0); - sb = sb + ","; - } - log!( - "IOTA ID: {}", - CONFIG.read().await.get_iota_id().to_string() - ); - log!("User IDS: {}", sb); - - // COMMUNITY MANAGEMENT - /* registry::load_interactables().await; - community_manager::load_communities().await; - community_manager::save_communities().await; - let mut sb1 = "".to_string(); - for cp in community_manager::get_communities().await { - sb1 = sb1 + "," + &cp.get_name().to_string().as_str(); - } - - if !sb1.is_empty() { - sb1.remove(0); - sb1 = sb1 + ","; - } - log!("Community IDS: {}", sb1); */ - let port = CONFIG.read().await.get_port(); - let mut ip = "0.0.0.0".to_string(); - for iface in pnet::datalink::interfaces() { - let iface: NetworkInterface = iface; - if iface.ips.len() > 0 { - let ipsv = format!("{}", iface.ips[0]); - let ips: &str = ipsv.split('/').next().unwrap_or(""); - if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") { - ip = ips.to_string(); - } - } - } - /* - if start(port).await { - log_t!("community_active", ip, port.to_string()); - } else { - if port < 1024 { - log_t!("community_start_error_admin", port.to_string()); - } else { - log_t!("community_start_error", port.to_string()); - } - } */ - if !has_dir("web") { - download_and_extract_zip( - "https://omega.tensamin.net/api/download/iota_frontend", - "web", - ) - .await; - } - let _ = omikron::omikron_connection::get_omikron_connection().await; - - log_t!("setup_completed"); - loop { - if *SHUTDOWN.read().await { - break; - } - - sleep(Duration::from_millis(100)).await; - } - if *RELOAD.read().await { - loop { - if ACTIVE_TASKS.is_empty() { - break; - } - sleep(Duration::from_secs(1)).await; - } - &CONFIG.write().await.clear(); - user_manager::clear(); - /*community_manager::clear();*/ - *APP_STATE.lock().unwrap() = AppState::new(); - } - ui.terminal.lock().unwrap().clear(); - ui.terminal.lock().unwrap().flush(); - } -} diff --git a/src/omikron/mod.rs b/src/omikron/mod.rs deleted file mode 100644 index acb0f59..0000000 --- a/src/omikron/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod omikron_connection; -pub mod ping_pong_task; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs deleted file mode 100755 index 7e92269..0000000 --- a/src/omikron/omikron_connection.rs +++ /dev/null @@ -1,1159 +0,0 @@ -use crate::users::contact::Contact; -use crate::util::chat_files::{MessageState, change_message_state}; -use crate::util::chats_util::{get_user, mod_user}; -use crate::util::communities_util::CommunitiesUtil; -use crate::util::crypto_util::{DataFormat, SecurePayload}; -use crate::util::file_util::{get_children, load_file, save_file}; -use crate::util::{chat_files, chats_util}; -use crate::util::{config_util::CONFIG, crypto_helper}; -use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv_in, log_cv_out, log_t}; -use dashmap::DashMap; -use json::JsonValue; -use std::collections::HashMap; -use std::sync::{Arc, LazyLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, RwLock, mpsc, watch}; -use tokio::task::JoinHandle; -use tokio::time::sleep; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use ttp_native::{Receiver, Sender}; -use uuid::Uuid; - -// ============================================================================ -// Configuration -// ============================================================================ - -const OMIKRON_HOST_DEFAULT: &str = "methanium.net"; -const OMIKRON_PORT_DEFAULT: u16 = 959; -const RECONNECT_DELAY: Duration = Duration::from_secs(5); -const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); -const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); -const TASK_MAX_AGE: Duration = Duration::from_secs(60); - -// ============================================================================ -// Waiting Task System -// ============================================================================ - -pub struct WaitingTask { - pub task: Box, CommunicationValue) -> bool + Send + Sync>, - pub inserted_at: Instant, -} - -pub static WAITING_TASKS: LazyLock> = LazyLock::new(|| DashMap::new()); - -pub fn start_task_cleanup_loop() { - tokio::spawn(async { - loop { - sleep(TASK_CLEANUP_INTERVAL).await; - WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); - } - }); -} - -// ============================================================================ -// Connection State -// ============================================================================ - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum ConnectionState { - Disconnected, - Connecting, - Connected { identified: bool }, -} - -impl ConnectionState { - pub fn is_connected(&self) -> bool { - matches!(self, ConnectionState::Connected { .. }) - } - - pub fn is_identified(&self) -> bool { - matches!(self, ConnectionState::Connected { identified: true }) - } -} - -// ============================================================================ -// Omikron Connection (Client-side with auto-reconnect) -// ============================================================================ - -pub struct OmikronConnection { - state: Arc>, - sender: Arc>>>, - connection_loop_handle: Arc>>>, - host: String, - port: u16, - pub last_ping: Arc>, - heartbeat_handle: Arc>>>, - message_send_times: Arc>>, - pub connection_id: Uuid, - shutdown_tx: Arc>>>, - reconnect_on_close: Arc>, -} - -impl OmikronConnection { - pub fn new() -> Self { - Self::with_host(OMIKRON_HOST_DEFAULT, OMIKRON_PORT_DEFAULT) - } - - pub fn with_host(host: &str, port: u16) -> Self { - let (shutdown_tx, _) = watch::channel(false); - - OmikronConnection { - state: Arc::new(RwLock::new(ConnectionState::Disconnected)), - sender: Arc::new(RwLock::new(None)), - connection_loop_handle: Arc::new(Mutex::new(None)), - host: host.to_string(), - port, - last_ping: Arc::new(Mutex::new(-1)), - heartbeat_handle: Arc::new(Mutex::new(None)), - message_send_times: Arc::new(Mutex::new(HashMap::new())), - connection_id: Uuid::new_v4(), - shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), - reconnect_on_close: Arc::new(RwLock::new(true)), - } - } - - // ------------------------------------------------------------------------- - // Connection Management - // ------------------------------------------------------------------------- - - pub async fn connect(self: &Arc) { - if self.connection_loop_handle.lock().await.is_none() { - self.clone().start().await; - } - } - - pub async fn start(self: Arc) { - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - *self.reconnect_on_close.write().await = true; - - let self_clone = self.clone(); - let handle = tokio::spawn(async move { - self_clone.connection_loop().await; - }); - - *self.connection_loop_handle.lock().await = Some(handle); - } - - pub async fn stop(&self) { - *self.reconnect_on_close.write().await = false; - - if let Some(tx) = self.shutdown_tx.lock().await.take() { - let _ = tx.send(true); - } - - if let Some(handle) = self.connection_loop_handle.lock().await.take() { - handle.abort(); - } - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - if let Some(sender) = self.sender.read().await.as_ref() { - sender.close(); - } - - *self.state.write().await = ConnectionState::Disconnected; - *self.sender.write().await = None; - } - - async fn connection_loop(self: Arc) { - let mut reconnect_delay = RECONNECT_DELAY; - let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); - let mut shutdown_rx = shutdown_rx; - - loop { - if *shutdown_rx.borrow() || *SHUTDOWN.read().await { - log_t!("omikron_connection_loop_shutdown"); - break; - } - - if !*self.reconnect_on_close.read().await { - break; - } - - match self.clone().connect_once().await { - Ok(()) => { - if *self.reconnect_on_close.read().await { - log!("Connection lost, reconnecting in {:?}...", reconnect_delay); - } else { - break; - } - } - Err(e) => { - log!( - "Connection failed: {}, retrying in {:?}...", - e, - reconnect_delay - ); - } - } - - tokio::select! { - _ = sleep(reconnect_delay) => {} - _ = shutdown_rx.changed() => { - if *shutdown_rx.borrow() { - break; - } - } - } - - reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); - } - } - - async fn connect_once(self: Arc) -> Result<(), String> { - *self.state.write().await = ConnectionState::Connecting; - log_t!("omikron_connecting"); - - let addr_str = format!("https://{}:{}/ws/iota/", self.host, self.port); - - let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) - .await - .map_err(|e| format!("Connection failed: {}", e))?; - - log_t!("omikron_connection_success"); - - let sender_arc = Arc::new(sender); - *self.sender.write().await = Some(sender_arc.clone()); - *self.state.write().await = ConnectionState::Connected { identified: false }; - - // Handle registration/identification - self.handle_authentication().await; - - // Start read loop - let read_self = self.clone(); - let read_handle = tokio::spawn(async move { - read_self.read_loop(&mut receiver).await; - }); - - // Start heartbeat - let heartbeat_self = self.clone(); - let heartbeat_handle = tokio::spawn(async move { - heartbeat_self.heartbeat_loop().await; - }); - *self.heartbeat_handle.lock().await = Some(heartbeat_handle); - - { - ACTIVE_TASKS.insert("Omikron Listener".to_string()); - } - - // Wait for read loop to complete - let result = read_handle.await; - *self.sender.write().await = None; - *self.state.write().await = ConnectionState::Disconnected; - { - ACTIVE_TASKS.remove("Omikron Listener"); - } - - if let Some(handle) = self.heartbeat_handle.lock().await.take() { - handle.abort(); - } - - match result { - Ok(()) => { - if *self.reconnect_on_close.read().await { - Err("Connection closed, will reconnect".to_string()) - } else { - Ok(()) - } - } - Err(e) => Err(format!("Read loop error: {}", e)), - } - } - - // ------------------------------------------------------------------------- - // Authentication (Registration/Identification) - // ------------------------------------------------------------------------- - - async fn handle_authentication(&self) { - let conf = CONFIG.read().await; - let iota_id = conf.get_iota_id(); - let public_key = conf.get_public_key(); - let private_key = conf.get_private_key(); - drop(conf); - - if iota_id == 0 || public_key.is_none() || private_key.is_none() { - log_t!("iota_register_new"); - - let key_pair = crypto_helper::generate_keypair(); - let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); - let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); - - let mut conf_write = CONFIG.write().await; - // NOTE: - // Intentionally not storing the generated private/public keys directly into the - // config file here to avoid persisting sensitive material in plaintext. If you - // want to persist them, uncomment the two lines below and accept the security - // implications (they will be saved by `conf_write.update()`). - // conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); - // conf_write.change("private_key", DataValue::Str(private_key_base64)); - conf_write.update(); - drop(conf_write); - - let register_msg = CommunicationValue::new(CommunicationType::register_iota) - .add_data(DataTypes::public_key, DataValue::Str(public_key_base64)); - - let msg_id = register_msg.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new(|selfc, cv| { - if !cv.is_type(CommunicationType::success) { - return false; - } - - let iota_value = cv.get_data(DataTypes::register_id); - let iota_id = iota_value.as_number().unwrap_or(0); - - if iota_id != 0 { - tokio::spawn(async move { - let mut conf_write = CONFIG.write().await; - conf_write.change("iota_id", JsonValue::from(iota_id)); - conf_write.update(); - drop(conf_write); - log!("Registered with Iota-ID: {}", iota_id); - - // Send identification after registration - let identify_msg = - CommunicationValue::new(CommunicationType::identification) - .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); - selfc.send_message(&identify_msg).await; - }); - } else { - log!("Iota registration failed."); - } - true - }), - inserted_at: Instant::now(), - }, - ); - - self.send_message(®ister_msg).await; - } else { - let identify_msg = CommunicationValue::new(CommunicationType::identification) - .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); - self.send_message(&identify_msg).await; - } - } - - // ------------------------------------------------------------------------- - // Read Loop & Heartbeat - // ------------------------------------------------------------------------- - - async fn read_loop(self: Arc, receiver: &mut Receiver) { - loop { - let result = receiver.receive().await; - match result { - Ok(cv) => { - self.clone().handle_message(cv).await; - } - Err(e) => { - self.fail_all_waiting_tasks(format!( - "Connection receive error: {} (connection_id={})", - e, self.connection_id - )) - .await; - break; - } - } - if !receiver.is_open() { - self.fail_all_waiting_tasks(format!( - "Connection closed (connection_id={}, receiver_open=false)", - self.connection_id - )) - .await; - break; - } - } - } - - async fn heartbeat_loop(self: Arc) { - loop { - sleep(HEARTBEAT_INTERVAL).await; - - if !self.state.read().await.is_connected() { - break; - } - - if let Some(sender) = self.sender.read().await.as_ref() { - if !sender.is_open() { - break; - } - } else { - break; - } - - self.send_ping().await; - } - } - - // ------------------------------------------------------------------------- - // Message Handling (Preserved from original) - // ------------------------------------------------------------------------- - - pub async fn handle_message(self: Arc, cv: CommunicationValue) { - if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { - log_cv_in!(&cv); - } - - let msg_id = cv.get_id(); - - // Dispatch waiting task for this message id - if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { - if (task.task)(self.clone(), cv.clone()) { - return; - } - } - - if cv.is_type(CommunicationType::pong) { - self.handle_pong(&cv).await; - return; - } - - if cv.is_type(CommunicationType::challenge) { - self.handle_challenge(&cv).await; - return; - } - - if cv.is_type(CommunicationType::identification_response) { - if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() { - let mut state = self.state.write().await; - if let ConnectionState::Connected { identified: _ } = *state { - *state = ConnectionState::Connected { identified: true }; - } - } - return; - } - - // ************************************************ // - // Direct messages // - // ************************************************ // - - if cv.is_type(CommunicationType::message_state) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); - - // Parse send_time robustly: accept numeric or string, fallback to current time - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp_i64 = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - - let _ = chat_files::change_message_state( - timestamp_i64, - *receiver_id as i64, - *sender_id as i64, - MessageState::from_str( - cv.get_data(DataTypes::message_state).as_str().unwrap_or(""), - ), - ); - } - - // Incoming stored message: store for the recipient, attempt local delivery, notify sender. - if cv.is_type(CommunicationType::message_send) { - let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; - - // parse receiver_id (the storage owner for this incoming message) - let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() - { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; - - // parse send_time robustly (number or string), fallback to now - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp_i64 = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - let timestamp_u128 = timestamp_i64 as u128; - - // content may be missing; default to empty string - let content = cv - .get_data(DataTypes::content) - .as_str() - .unwrap_or("") - .to_string(); - - let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; - - // persist message for the receiver (storage_owner = receiver_id) - chat_files::add_message( - timestamp_u128, - false, - receiver_id as i64, - sender_id as i64, - &content, - height, - ); - - // persist message for the sender (storage_owner = sender_id) - chat_files::add_message( - timestamp_u128, - true, - sender_id as i64, - receiver_id as i64, - &content, - height, - ); - - // send confirmation back to sender - let conf_msg = CommunicationValue::new(CommunicationType::message_send) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&conf_msg).await; - - // Build a live-delivery message for the local client (recipient) - let user_forward = CommunicationValue::new(CommunicationType::message_live) - .with_id(cv.get_id()) - .with_receiver(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data(DataTypes::content, DataValue::Str(content.clone())) - .add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64)) - .add_data(DataTypes::height, DataValue::Number(height)); - - // Attempt delivery and await a response from the local client - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(10))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataTypes::message_state) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - - // update stored message state for receiver - let _ = chat_files::change_message_state( - timestamp_i64, - receiver_id as i64, - sender_id as i64, - ms.clone(), - ); - - // update stored message state for sender - let _ = chat_files::change_message_state( - timestamp_i64, - sender_id as i64, - receiver_id as i64, - ms.clone(), - ); - - // notify original sender about the delivered/read state - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data( - DataTypes::message_state, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; - } else { - // Delivery failed or timed out; mark as Sent - let _ = chat_files::change_message_state( - timestamp_i64, - receiver_id as i64, - sender_id as i64, - MessageState::Sent, - ); - - let _ = chat_files::change_message_state( - timestamp_i64, - sender_id as i64, - receiver_id as i64, - MessageState::Sent, - ); - - // notify sender - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) - .add_data( - DataTypes::message_state, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; - } - return; - } - - if cv.is_type(CommunicationType::message_other_iota) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); - - // parse send_time safely (number or string), fallback to now - let send_time_val = cv.get_data(DataTypes::send_time); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let timestamp = if let Some(n) = send_time_val.as_number() { - n as i64 - } else if let Some(s) = send_time_val.as_str() { - s.parse::().unwrap_or(now_i64) - } else { - now_i64 - }; - - // content may be missing or non-string; default to empty string - let content = cv - .get_data(DataTypes::content) - .as_str() - .unwrap_or("") - .to_string(); - - let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; - - chat_files::add_message( - timestamp as u128, - false, - *receiver_id as i64, - *sender_id as i64, - &content, - height, - ); - - // Build user_forward using the parsed numeric timestamp and safe content string - let user_forward = CommunicationValue::new(CommunicationType::message_live) - .with_id(cv.get_id()) - .with_receiver(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data(DataTypes::content, DataValue::Str(content.clone())) - .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64)) - .add_data(DataTypes::height, DataValue::Number(height)); - - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(10))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataTypes::message_state) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - - let _ = change_message_state( - timestamp, - *receiver_id as i64, - *sender_id as i64, - ms.clone(), - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(*sender_id) - .with_sender(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data( - DataTypes::message_state, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; - } else { - // Delivery timed out/failed — update stored state and notify sender with numeric timestamp - let _ = chat_files::change_message_state( - timestamp, - *receiver_id as i64, - *sender_id as i64, - MessageState::Sent, - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::message_state) - .with_id(cv.get_id()) - .with_receiver(*sender_id) - .with_sender(*receiver_id) - .add_data(DataTypes::send_time, DataValue::Number(timestamp)) - .add_data( - DataTypes::message_state, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; - } - return; - } - - if cv.is_type(CommunicationType::messages_get) { - let my_id = cv.get_sender(); - let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); - let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); - let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); - let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); - let mut msg_array: Vec = Vec::new(); - for m in messages.members() { - let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); - let content: String = m["content"].as_str().unwrap_or("").to_string(); - let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false); - let height: i64 = m["height"].as_i64().unwrap_or(0); - let sender_id: i64 = if sent_by_self { - my_id as i64 - } else { - if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { - s.parse::().unwrap_or(partner_id as i64) - } else { - partner_id as i64 - } - }; - let message_state: String = m["message_state"].as_str().unwrap_or("").to_string(); - - let mut container = Vec::new(); - container.push((DataTypes::send_time, DataValue::Number(message_time))); - container.push((DataTypes::content, DataValue::Str(content))); - container.push((DataTypes::sender_id, DataValue::Number(sender_id))); - container.push((DataTypes::message_state, DataValue::Str(message_state))); - container.push((DataTypes::height, DataValue::Number(height))); - container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self))); - msg_array.push(DataValue::Container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::messages_get) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::messages, DataValue::Array(msg_array)); - - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::get_chats) { - let user_id = cv.get_sender(); - let users = chats_util::get_users(user_id as i64); - let mut user_array = Vec::new(); - for user in users { - let mut container = Vec::new(); - container.push((DataTypes::user_id, DataValue::Number(user.user_id))); - if let Some(name) = user.user_name { - container.push((DataTypes::username, DataValue::Str(name))); - } - if let Some(ts) = user.last_message_at { - container.push((DataTypes::last_message_at, DataValue::Number(ts))); - } - user_array.push(DataValue::Container(container)); - } - let resp = CommunicationValue::new(CommunicationType::get_chats) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_data(DataTypes::user_ids, DataValue::Array(user_array)); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::add_conversation) { - let user_id = cv.get_sender(); - let other_id = match cv.get_data(DataTypes::chat_partner_id).as_number() { - Some(n) => n as i64, - None => cv - .get_data(DataTypes::chat_partner_id) - .as_str() - .unwrap_or("0") - .parse() - .unwrap_or(0), - }; - let mut contact = get_user(user_id as i64, other_id).unwrap_or(Contact::new(other_id)); - - if let Some(name) = cv.get_data(DataTypes::chat_partner_name).as_str() { - contact.user_name = Some(name.to_string()); - } - - contact.set_last_message_at( - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64, - ); - mod_user(user_id as i64, &contact); - let resp = CommunicationValue::new(CommunicationType::add_conversation) - .with_id(cv.get_id()) - .with_receiver(user_id); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::add_community) { - CommunitiesUtil::add_community( - cv.get_sender() as i64, - cv.get_data(DataTypes::community_address) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataTypes::community_title) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataTypes::position) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::add_community) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::get_communities) { - let mut comm_array = Vec::new(); - for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { - let mut container: Vec<(DataTypes, DataValue)> = Vec::new(); - if let Some(address) = c["address"].as_str() { - container.push(( - DataTypes::community_address, - DataValue::Str(address.to_string()), - )); - } - if let Some(title) = c["title"].as_str() { - container.push(( - DataTypes::community_title, - DataValue::Str(title.to_string()), - )); - } - if let Some(position) = c["position"].as_str() { - container.push((DataTypes::position, DataValue::Str(position.to_string()))); - } - comm_array.push(DataValue::Container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::get_communities) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_data(DataTypes::communities, DataValue::Array(comm_array)); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::remove_community) { - CommunitiesUtil::remove_community( - cv.get_sender() as i64, - cv.get_data(DataTypes::community_address) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::remove_community) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::settings_save) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataTypes::settings_name).as_str().unwrap(); - let settings_value = cv.get_data(DataTypes::payload).as_str().unwrap(); - - save_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - &settings_value, - ); - - let response = CommunicationValue::new(CommunicationType::settings_save) - .with_receiver(my_id) - .with_id(cv.get_id()); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::settings_load) { - let my_id = cv.get_sender(); - let settings_name = cv.get_data(DataTypes::settings_name).as_string().unwrap(); - let settings_value_str = load_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - ); - let response = CommunicationValue::new(CommunicationType::settings_load) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::payload, DataValue::Str(settings_value_str)) - .add_data(DataTypes::settings_name, DataValue::Str(settings_name)); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::settings_list) { - let my_id = cv.get_sender(); - let settings = get_children(&format!("users/{}/settings/", my_id)); - let mut settings_json = Vec::new(); - for s in settings { - let s = s.replace(".settings", ""); - if s.is_empty() { - continue; - } - let _ = settings_json.push(DataValue::Str(s)); - } - let response = CommunicationValue::new(CommunicationType::settings_list) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_data(DataTypes::settings, DataValue::Array(settings_json)); - - self.send_message(&response).await; - return; - } - } - - async fn handle_challenge(&self, cv: &CommunicationValue) { - let conf = CONFIG.read().await; - let private_key = conf.get_private_key().unwrap(); - drop(conf); - - let omikron_public_key = cv.get_data(DataTypes::public_key).as_str().unwrap(); - let encrypted_challenge = cv.get_data(DataTypes::challenge).as_str().unwrap(); - - let solved_challenge = { - if let Ok(decrypted) = SecurePayload::new( - encrypted_challenge, - DataFormat::Base64, - crypto_helper::load_secret_key(&private_key).unwrap(), - ) { - if let Ok(decrypted) = decrypted - .decrypt_x448(crypto_helper::load_public_key(omikron_public_key).unwrap()) - { - Some(decrypted) - } else { - None - } - } else { - None - } - }; - - if let Some(decrypted) = solved_challenge { - let solved = decrypted.export(DataFormat::Raw); - - let response = CommunicationValue::new(CommunicationType::challenge_response) - .with_id(cv.get_id()) - .add_data(DataTypes::challenge, DataValue::Str(solved)); - - self.send_message(&response).await; - } - } - - // ------------------------------------------------------------------------- - // Public API - // ------------------------------------------------------------------------- - - pub async fn send_message(&self, cv: &CommunicationValue) { - if let Err(err) = self.send_message_result(cv).await { - log_t!("send_message_failed", err); - } - } - - async fn send_message_result(&self, cv: &CommunicationValue) -> Result<(), String> { - let sender_guard = self.sender.read().await; - if let Some(sender) = sender_guard.as_ref() { - if !sender.is_open() { - drop(sender_guard); - if let Some(sender) = self.sender.write().await.take() { - sender.close(); - } - self.fail_all_waiting_tasks(format!( - "Send failed: connection closed (connection_id={})", - self.connection_id - )) - .await; - return Err("connection closed".to_string()); - } - - let sender_clone = Arc::clone(sender); - drop(sender_guard); - - if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { - log_cv_out!(&cv); - } - - if let Err(e) = sender_clone.send(cv).await { - self.fail_all_waiting_tasks(format!( - "Send failed: {} (connection_id={})", - e, self.connection_id - )) - .await; - return Err(e.to_string()); - } - - Ok(()) - } else { - Err("not connected".to_string()) - } - } - - async fn fail_all_waiting_tasks(&self, reason: String) { - let keys: Vec = WAITING_TASKS.iter().map(|entry| *entry.key()).collect(); - - for key in keys { - if let Some((_, waiting_task)) = WAITING_TASKS.remove(&key) { - let response = CommunicationValue::new(CommunicationType::error) - .with_id(key) - .add_data(DataTypes::message, DataValue::Str(reason.clone())); - let _ = (waiting_task.task)(OMIKRON_CONNECTION.clone(), response); - } - } - } - - pub async fn is_connected(&self) -> bool { - self.state.read().await.is_connected() - } - - pub async fn is_identified(&self) -> bool { - self.state.read().await.is_identified() - } - - pub async fn await_response( - &self, - cv: &CommunicationValue, - timeout_duration: Option, - ) -> Result { - let (tx, mut rx) = mpsc::channel(1); - let msg_id = cv.get_id(); - - WAITING_TASKS.insert( - msg_id, - WaitingTask { - task: Box::new(move |_, response_cv| { - let inner_tx = tx.clone(); - tokio::spawn(async move { - let _ = inner_tx.send(response_cv).await; - }); - true - }), - inserted_at: Instant::now(), - }, - ); - - if let Err(send_err) = self.send_message_result(cv).await { - WAITING_TASKS.remove(&msg_id); - return Err(format!( - "Request send failed (msg_id={}, reason={})", - msg_id, send_err - )); - } - - let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); - - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response_cv)) => { - if response_cv.is_type(CommunicationType::error) { - let reason = response_cv - .get_data(DataTypes::message) - .as_str() - .unwrap_or("connection error") - .to_string(); - Err(format!( - "Request failed due to disconnect (msg_id={}, reason={})", - msg_id, reason - )) - } else { - Ok(response_cv) - } - } - Ok(_) => { - WAITING_TASKS.remove(&msg_id); - Err("Channel closed while awaiting response".to_string()) - } - Err(_) => { - let waiting_tasks_len = WAITING_TASKS.len(); - WAITING_TASKS.remove(&msg_id); - Err(format!( - "Request timed out (msg_id={}, timeout={}s, connected={}, waiting_tasks={})", - msg_id, - timeout.as_secs(), - self.is_connected().await, - waiting_tasks_len - )) - } - } - } - - pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { - if self.state.read().await.is_connected() { - return Ok(()); - } - - let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); - let start = Instant::now(); - - loop { - if self.state.read().await.is_connected() { - return Ok(()); - } - - if start.elapsed() >= timeout { - return Err(format!( - "Connection not established within {} seconds", - timeout.as_secs() - )); - } - - sleep(Duration::from_millis(100)).await; - } - } -} - -// ============================================================================ -// Global Instance -// ============================================================================ - -pub static OMIKRON_CONNECTION: LazyLock> = LazyLock::new(|| { - let conn = Arc::new(OmikronConnection::new()); - - start_task_cleanup_loop(); - - conn -}); - -pub async fn get_omikron_connection() -> Arc { - let conn = OMIKRON_CONNECTION.clone(); - - conn.connect().await; - conn -} diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs deleted file mode 100644 index f1a891f..0000000 --- a/src/omikron/ping_pong_task.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::omikron::omikron_connection::OmikronConnection; -use crate::{APP_STATE, log}; -use dashmap::DashMap; -use std::sync::LazyLock; -use std::time::Instant; -use tokio::time::Duration; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; - -static PING_TIMES: LazyLock> = LazyLock::new(|| DashMap::new()); - -impl OmikronConnection { - pub async fn send_ping(&self) { - let id = rand_u32(); - - PING_TIMES.insert(id, Instant::now()); - - PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30)); - - let ping_message = CommunicationValue::new(CommunicationType::ping) - .with_id(id) - .add_data( - DataTypes::last_ping, - DataValue::Array(vec![DataValue::Number(*self.last_ping.lock().await)]), - ); - - self.send_message(&ping_message).await; - } - - pub async fn handle_pong(&self, cv: &CommunicationValue) { - let id = cv.get_id(); - - if let Some((_, send_time)) = PING_TIMES.remove(&id) { - let ping_ms = Instant::now().duration_since(send_time).as_millis() as i64; - *self.last_ping.lock().await = ping_ms; - APP_STATE.lock().unwrap().push_ping_val(ping_ms as f64); - } - } -} diff --git a/src/server/api.rs b/src/server/api.rs deleted file mode 100755 index f9a87a4..0000000 --- a/src/server/api.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::server::server::is_local_network; -use crate::util::config_util::CONFIG; -use actix_web::{HttpRequest, HttpResponse, Responder, web}; -use serde_json::{Value, json}; -use std::net::SocketAddr; -use std::sync::Arc; - -pub fn api_config(cfg: &mut web::ServiceConfig) { - cfg.service( - web::scope("/api") - .route("/shutdown/", web::post().to(shutdown)) - .route("/reload/", web::post().to(reload)) - .route("/users/add/", web::post().to(users_add)) - .route("/users/remove/", web::post().to(users_remove)) - .route("/users/get/", web::get().to(users_get)) - .route("/communities/add/", web::post().to(communities_add)) - .route("/communities/get/", web::get().to(communities_get)) - .route("/settings/set/", web::post().to(settings_set)) - .route("/settings/get/", web::get().to(settings_get)), - ); -} - -async fn settings_set(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let key = req.headers().get("key").and_then(|v| v.to_str().ok()); - let value = req.headers().get("value").and_then(|v| v.to_str().ok()); - - match (key, value) { - (Some(k), Some(v)) => { - let _ = CONFIG - .write() - .await - .config - .insert(&k.to_string(), v.to_string()); - - success() - } - _ => error(), - } -} - -async fn settings_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - let config = CONFIG.read().await.config.clone(); - let serde_config: Value = serde_json::to_value(config.to_string()).unwrap(); - HttpResponse::Ok().json(serde_config) -} - -async fn communities_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let communities = crate::communities::community_manager::get_communities().await; - - let mut list = Vec::new(); - - for c in communities { - let val = c.frontend().await; - let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); - list.push(s_val); - } - HttpResponse::Ok().json(list) -} - -async fn communities_add( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let name = payload["name"].as_str().unwrap_or("").to_string(); - let owner = payload["owner"].as_i64().unwrap_or(0); - - let community = Arc::new(crate::communities::community::Community::create(name, owner).await); - - crate::communities::community_manager::add_community(community).await; - - success() -} - -async fn users_get(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let users = crate::users::user_manager::get_users(); - - let list: Vec<_> = users - .into_iter() - .map(|u| { - let val = u.frontend(); - serde_json::to_value(val.to_string()).unwrap() - }) - .collect(); - - HttpResponse::Ok().json(list) -} - -async fn users_remove( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0); - - crate::users::user_manager::remove_user(uuid); - crate::users::user_manager::save_users(); - - success() -} - -async fn users_add( - req: HttpRequest, - ssl: web::Data, - payload: web::Json, -) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - let username = match payload.get("username").and_then(|v| v.as_str()) { - Some(u) => u, - _ => return error(), - }; - - if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { - let val = user.frontend(); - let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); - HttpResponse::Ok().json(s_val) - } else { - error() - } -} - -async fn shutdown(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - *crate::SHUTDOWN.write().await = true; - success() -} - -async fn reload(req: HttpRequest, ssl: web::Data) -> impl Responder { - if !is_allowed_req(&req, *ssl.get_ref()) { - return forbidden(); - } - - *crate::SHUTDOWN.write().await = true; - *crate::RELOAD.write().await = true; - - success() -} - -fn forbidden() -> HttpResponse { - HttpResponse::Forbidden().body("403 Forbidden") -} - -fn success() -> HttpResponse { - HttpResponse::Ok().json(json!({ "type": "success" })) -} - -fn error() -> HttpResponse { - HttpResponse::Ok().json(json!({ "type": "error" })) -} - -fn is_allowed(addr: SocketAddr, ssl: bool) -> bool { - is_local_network(addr.ip()) || ssl -} - -fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool { - if let Some(addr) = req.peer_addr() { - is_allowed(addr, ssl) - } else { - false - } -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index de2a79a..0000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod api; -pub mod server; -pub mod web_path_parser; diff --git a/src/server/server.rs b/src/server/server.rs deleted file mode 100644 index b55a50f..0000000 --- a/src/server/server.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::log; -use crate::server::api::api_config; -use crate::server::web_path_parser; -use crate::util::file_util::load_file_buf; -use crate::{ACTIVE_TASKS, SHUTDOWN}; -use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; -use actix_web_actors::ws; -use rustls::ServerConfig; -use rustls::pki_types::{CertificateDer, PrivateKeyDer}; -use std::{ - error::Error as StdError, - io::{self, BufReader, ErrorKind}, - net::IpAddr, - sync::Arc, - time::Duration, -}; - -async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result { - let path = req.path().to_string(); - log!("WS connection from {:?}", req.peer_addr()); - let session = WsSession::new(path); - ws::start(session, &req, stream) -} - -use tokio::sync::oneshot; - -pub async fn start(port: u16) -> bool { - let (tx, rx) = oneshot::channel::(); - - let _ = tokio::spawn(async move { - let server = match load_tls_config() { - Ok(Some(tls_config)) => { - log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); - let _config = (*tls_config).clone(); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(true)) - .configure(api_config) - .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) - .default_service(web::to(web_path_parser::handle)) - }) - .bind(("0.0.0.0", port)) - .unwrap() - .run() - } - Ok(_) => { - log!("HTTP Server running on 0.0.0.0:{}", port); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(false)) - .configure(api_config) - .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) - .default_service(web::to(web_path_parser::handle)) - }) - .bind(("0.0.0.0", port)) - .unwrap() - .run() - } - Err(e) => { - log!("TLS config error: {}", e); - return; - } - }; - - let server_handle = server.handle(); - tx.send(server_handle).unwrap(); - - ACTIVE_TASKS.insert("WebServer".into()); - server.await.unwrap(); - ACTIVE_TASKS.remove("WebServer"); - log!("Web Server shutdown complete."); - }); - - if let Ok(server_handle) = rx.await { - tokio::spawn(async move { - wait_for_shutdown(server_handle).await; - }); - true - } else { - false - } -} - -async fn wait_for_shutdown(server_handle: ServerHandle) { - loop { - if *SHUTDOWN.read().await { - log!("Shutdown signal received."); - server_handle.stop(true).await; - break; - } - tokio::time::sleep(Duration::from_millis(100)).await; - } -} - -fn load_tls_config() -> Result>, Box> { - let cert_file_res = load_file_buf("certs", "cert.pem"); - let key_file_res = load_file_buf("certs", "cert.key"); - - let cert_file_buf = match cert_file_res { - Ok(b) => b, - Err(e) if e.kind() == ErrorKind::NotFound => { - log!("TLS certificate 'certs/cert.pem' not found."); - return Ok(None); - } - Err(e) => return Err(e.into()), // Other IO error - }; - - let key_file_buf = match key_file_res { - Ok(b) => b, - Err(e) if e.kind() == ErrorKind::NotFound => { - log!("TLS key 'certs/cert.key' not found."); - return Ok(None); - } - Err(e) => return Err(e.into()), // Other IO error - }; - - let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf)) - .collect::, _>>()?; - - // PKCS8 - let mut key_reader = BufReader::new(key_file_buf); - let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - - if key_ders.is_empty() { - // RSA - key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file - key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - } - - if key_ders.is_empty() { - // EC - key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file - key_ders = rustls_pemfile::ec_private_keys(&mut key_reader) - .map(|r| r.map(Into::into)) - .collect::, _>>()?; - } - - if key_ders.is_empty() { - return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into()); - } - - let config = ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(cert_chain, key_ders.remove(0)) - .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; - - Ok(Some(Arc::new(config))) -} - -pub fn is_local_network(addr: IpAddr) -> bool { - match addr { - IpAddr::V4(v4) => { - let o = v4.octets(); - o[0] == 10 - || (o[0] == 172 && (16..=31).contains(&o[1])) - || (o[0] == 192 && o[1] == 168) - || o[0] == 127 - || (o[0] == 169 && o[1] == 254) - } - IpAddr::V6(v6) => { - let s = v6.segments(); - (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback() - } - } -} diff --git a/src/server/web_path_parser.rs b/src/server/web_path_parser.rs deleted file mode 100755 index a82c8db..0000000 --- a/src/server/web_path_parser.rs +++ /dev/null @@ -1,77 +0,0 @@ -use actix_web::{HttpRequest, HttpResponse}; -use std::path::{Path, PathBuf}; - -use crate::util::file_util::load_file_vec; - -fn codec_for_ext(ext: &str) -> &'static str { - match ext { - "html" => "text/html; charset=utf-8", - "css" => "text/css", - "js" => "application/javascript", - "json" => "application/json", - "png" => "image/png", - "ico" => "image/x-icon", - "woff2" => "font/woff2", - _ => "application/octet-stream", - } -} - -pub async fn handle(req: HttpRequest) -> HttpResponse { - let req_path = req.path().trim_start_matches('/'); - - let mut fs_path = PathBuf::from("web"); - - if req_path.is_empty() { - fs_path.push("index.html"); - } else { - fs_path.extend(req_path.split('/')); - } - - if fs_path.is_dir() { - fs_path.push("index.html"); - } - - let ext_opt = fs_path.extension().and_then(|e| e.to_str()); - let mut final_name = fs_path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("") - .to_string(); - - if ext_opt.is_none() { - if final_name.is_empty() { - final_name = "index.html".to_string(); - } else { - final_name.push_str(".html"); - } - } - - let content_type = codec_for_ext( - fs_path - .extension() - .and_then(|e| e.to_str()) - .unwrap_or("html"), - ); - - let dir = fs_path.parent().unwrap_or(Path::new("web")); - - match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) { - Ok(content) => HttpResponse::Ok().content_type(content_type).body(content), - - Err(_) => { - let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if matches!(ext, "js" | "css" | "woff2") { - return HttpResponse::NotFound() - .content_type("text/plain") - .body("Not found"); - } - - let fallback = load_file_vec("web", "404.html") - .unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec()); - - HttpResponse::NotFound() - .content_type("text/html; charset=utf-8") - .body(fallback) - } - } -} diff --git a/src/terms/buttons.rs b/src/terms/buttons.rs deleted file mode 100644 index c545a4c..0000000 --- a/src/terms/buttons.rs +++ /dev/null @@ -1,175 +0,0 @@ -use ratatui::{ - layout::{Alignment, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph}, -}; - -use crate::terms::focus::Focus; - -#[allow(mismatched_lifetime_syntaxes)] -pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line { - let box_char = if checked { "[x]" } else { "[ ]" }; - let (box_style, text_style) = if active { - if allowed { - ( - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD), - ) - } else { - ( - Style::default().fg(Color::Gray), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ) - } - } else { - (Style::default(), Style::default()) - }; - Line::from(vec![ - Span::styled(box_char, box_style), - Span::raw(" "), - Span::styled(label, text_style), - ]) -} - -pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { - let p = Paragraph::new(Span::styled(label, style)) - .alignment(Alignment::Center) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(p, area); -} -pub fn draw_buttons( - f: &mut ratatui::Frame, - area: Rect, - current_focus: Focus, - state: (bool, bool), - update_needed: bool, - downgrade_scenario: bool, - tos_or_privacy: bool, -) { - let cancel_text = if update_needed { - "[Q] Quit" - } else { - "[Q] Not now" - }; - let continue_text = if downgrade_scenario { - "Downgrade" - } else { - "Continue" - }; - let mut buttons = vec![ - (cancel_text, Focus::Cancel), - (continue_text, Focus::Continue), - ]; - if tos_or_privacy { - buttons.push(("Continue with Tensamin Services", Focus::ContinueAll)); - } - - let padding = 2; - let min_widths: Vec = buttons - .iter() - .map(|(label, _)| label.len() as u16 + padding) - .collect(); - - let widths = compute_widths(area.width, &min_widths); - - let mut x = area.x; - - for ((label, focus), width) in buttons.iter().zip(widths) { - let chunk = Rect { - x, - y: area.y, - width, - height: area.height, - }; - x += width; - - let is_focused = current_focus == *focus; - - let style = match focus { - Focus::Cancel => { - if is_focused { - Style::default() - .fg(Color::Black) - .bg(Color::Red) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::Red) - } - } - - Focus::Continue => { - if is_focused && state.0 { - Style::default() - .fg(Color::Black) - .bg(Color::Green) - .add_modifier(Modifier::BOLD) - } else if state.0 { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::DarkGray) - } - } - - Focus::ContinueAll => { - if is_focused && state.1 { - Style::default() - .fg(Color::Black) - .bg(Color::Green) - .add_modifier(Modifier::BOLD) - } else if state.1 { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::DarkGray) - } - } - - _ => Style::default().fg(Color::DarkGray), - }; - - draw_button(f, chunk, label, style); - } -} -pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec { - let mut widths = vec![0; min_widths.len()]; - let mut remaining: Vec = (0..min_widths.len()).collect(); - - let mut remaining_width = area_width; - - while !remaining.is_empty() { - let count = remaining.len() as u16; - let equal = remaining_width / count; - - let mut clamped = Vec::new(); - - for &i in &remaining { - if min_widths[i] > equal { - widths[i] = min_widths[i]; - remaining_width -= min_widths[i]; - clamped.push(i); - } - } - - if clamped.is_empty() { - let mut remainder = remaining_width % count; - for &i in &remaining { - widths[i] = equal - + if remainder > 0 { - remainder -= 1; - 1 - } else { - 0 - }; - } - break; - } - - remaining.retain(|i| !clamped.contains(i)); - } - - widths -} diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs deleted file mode 100644 index 1cc6e0a..0000000 --- a/src/terms/consent_state.rs +++ /dev/null @@ -1,491 +0,0 @@ -use tokio::sync::oneshot; - -use crate::{ - gui::{ - screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen}, - ui::UI, - }, - terms::{ - doc::Doc, - terms_getter::{Type, get_current_docs, get_newest_docs}, - }, - util::file_util::{load_file, save_file}, -}; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; - -pub async fn check(ui: Arc) -> (bool, bool) { - let mut state = ConsentState::load_state(); - - if ensure_initial_consent(ui.clone(), &mut state) - .await - .is_err() - { - return (false, false); - } - if ensure_updates(ui, &mut state).await.is_err() { - return (false, false); - }; - - state = state.sanitize(); - state.save_state(); - - (state.accepted_eula, state.accepted_tos && state.accepted_pp) -} - -async fn ensure_initial_consent(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { - if state.accepted_eula { - return Ok(()); - } - - let (tx, rx) = oneshot::channel(); - - ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx)))) - .await; - - let result = rx.await.unwrap_or(UserChoice::Deny); - - match result { - UserChoice::AcceptEULA | UserChoice::AcceptAll => { - if let Some((eula, tos, privacy)) = get_current_docs().await { - state.accepted_eula = true; - state.eula = Some(eula); - - if matches!(result, UserChoice::AcceptAll) { - state.accepted_tos = true; - state.accepted_pp = true; - state.tos = Some(tos); - state.privacy = Some(privacy); - } - } - - let _ = &state.save_state(); - Ok(()) - } - UserChoice::Deny => Err(()), - } -} -async fn ensure_updates(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { - let Some((eula_update, tos_update, privacy_update)) = get_updates().await else { - return Ok(()); - }; - - let is_forced = matches!(eula_update, UpdateDecision::Forced(_)) - || matches!(tos_update, UpdateDecision::Forced(_)) - || matches!(privacy_update, UpdateDecision::Forced(_)); - - let (tx, rx) = oneshot::channel(); - - ui.set_screen(Box::new(TermsUpdaterScreen::new( - eula_update.clone(), - tos_update.clone(), - privacy_update.clone(), - Some(tx), - ))) - .await; - - let result = rx.await.unwrap_or(UserChoice::Deny); - - if is_forced { - match result { - UserChoice::AcceptAll => { - state.accepted_eula = true; - state.accepted_tos = true; - state.accepted_pp = true; - } - UserChoice::AcceptEULA => { - state.accepted_eula = true; - } - UserChoice::Deny => return Err(()), - } - } else { - apply_future_updates(state, result, eula_update, tos_update, privacy_update); - } - - state.save_state(); - Ok(()) -} -fn apply_future_updates( - state: &mut ConsentState, - result: UserChoice, - eula_update: UpdateDecision, - tos_update: UpdateDecision, - privacy_update: UpdateDecision, -) { - match result { - UserChoice::AcceptAll => { - if let UpdateDecision::Future { newest } = eula_update { - state.future_eula = Some(newest); - } - if let UpdateDecision::Future { newest } = tos_update { - state.future_tos = Some(newest); - } - if let UpdateDecision::Future { newest } = privacy_update { - state.future_privacy = Some(newest); - } - } - UserChoice::AcceptEULA => { - if let UpdateDecision::Future { newest } = eula_update { - state.future_eula = Some(newest); - } - } - UserChoice::Deny => {} - } -} - -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 - UpdateDecision, - UpdateDecision, - UpdateDecision, -)> { - if let ( - Some((current_eula, current_tos, current_privacy)), - Some((newest_eula, newest_tos, newest_privacy)), - ) = (get_current_docs().await, get_newest_docs().await) - { - let file = load_file("", "agreements"); - let accepted_state = ConsentState::from_str(&file).sanitize(); - save_file("", "agreements", &accepted_state.to_string()); - - let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) { - if current_eula.equals(&newest_eula) { - UpdateDecision::NoChange - } else { - if newest_eula.equals_some(&accepted_state.future_eula) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { - newest: newest_eula, - } - } - } - } else if newest_eula.equals_some(&accepted_state.eula) { - UpdateDecision::NoChange - } else { - UpdateDecision::Forced(current_eula) - }; - - let tos_update: UpdateDecision = - if !accepted_state.accepted_tos || newest_tos.equals_some(&accepted_state.tos) { - UpdateDecision::NoChange - } else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) { - if current_tos.equals(&newest_tos) { - UpdateDecision::NoChange - } else { - if newest_tos.equals_some(&accepted_state.future_tos) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { newest: newest_tos } - } - } - } else { - UpdateDecision::Forced(current_tos) - }; - - let privacy_update: UpdateDecision = if !accepted_state.accepted_pp - || newest_privacy.equals_some(&accepted_state.privacy) - { - UpdateDecision::NoChange - } else if accepted_state.accepted_pp && current_privacy.equals_some(&accepted_state.privacy) - { - if current_privacy.equals(&newest_privacy) { - UpdateDecision::NoChange - } else { - if newest_privacy.equals_some(&accepted_state.future_privacy) { - UpdateDecision::NoChange - } else { - UpdateDecision::Future { - newest: newest_privacy, - } - } - } - } else { - UpdateDecision::Forced(current_privacy) - }; - match (&eula_update, &tos_update, &privacy_update) { - (&UpdateDecision::NoChange, &UpdateDecision::NoChange, &UpdateDecision::NoChange) => { - None - } - _ => Some((eula_update, tos_update, privacy_update)), - } - } else { - None - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum UserChoice { - Deny, - AcceptEULA, - AcceptAll, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum UpdateDecision { - NoChange, - Future { newest: Doc }, - Forced(Doc), -} - -#[derive(Debug, Clone)] -pub struct ConsentState { - pub eula: Option, - pub accepted_eula: bool, - pub future_eula: Option, - - pub tos: Option, - pub accepted_tos: bool, - pub future_tos: Option, - - pub privacy: Option, - pub accepted_pp: bool, - pub future_privacy: Option, -} - -impl ConsentState { - fn sanitize(mut self) -> Self { - let current_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - if let Some(future_eula) = self.future_eula.clone() { - if future_eula.get_time() < current_secs { - self.eula = Some(future_eula); - self.future_eula = None; - } - } - if let Some(future_tos) = self.future_tos.clone() { - if future_tos.get_time() < current_secs { - self.tos = Some(future_tos); - self.future_tos = None; - } - } - if let Some(future_privacy) = self.future_privacy.clone() { - if future_privacy.get_time() < current_secs { - self.privacy = Some(future_privacy); - self.future_privacy = None; - } - } - - if !self.accepted_eula { - self.accepted_tos = false; - self.accepted_pp = false; - } - self - } - - pub fn load_state() -> ConsentState { - let file = load_file("", "agreements"); - ConsentState::from_str(&file).sanitize() - } - - pub fn save_state(&self) { - save_file("", "agreements", &self.to_string()); - } - - fn to_string(&self) -> String { - let current_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let mut file_out: String = format!( - "This 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={}", - current_secs - ); - - if let Some(eula) = &self.eula { - 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={}\ - \nEULA-VERSION={}\ - \nEULA-HASH={}\ - ", self.accepted_eula, eula.get_version(), eula.get_hash())); - - if self.accepted_tos - && let Some(tos) = &self.tos - { - file_out.push_str(&format!("\ - \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.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/\ - \nPrivacy-Policy={}\ - \nPrivacy-Policy-VERSION={}\ - \nPrivacy-Policy-HASH={}\ - ", self.accepted_pp, pp.get_version(), pp.get_hash())); - } - } else { - file_out.push_str("\ - \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\ - \nEULA=false\ - "); - } - - if let Some(eula) = &self.future_eula { - file_out.push_str(&format!( - "\ - \nFUTURE-EULA-VERSION={}\ - \nFUTURE-EULA-HASH={}\ - \nFUTURE-EULA-TIME={}\ - ", - eula.get_version(), - eula.get_hash(), - eula.get_time() - )); - } - if let Some(tos) = &self.future_tos { - file_out.push_str(&format!( - "\ - \nFUTURE-Terms-of-Service-VERSION={}\ - \nFUTURE-Terms-of-Service-HASH={}\ - \nFUTURE-Terms-of-Service-TIME={}\ - ", - tos.get_version(), - tos.get_hash(), - tos.get_time() - )); - } - if let Some(pp) = &self.future_privacy { - file_out.push_str(&format!( - "\ - \nFUTURE-Privacy-Policy-VERSION={}\ - \nFUTURE-Privacy-Policy-HASH={}\ - \nFUTURE-Privacy-Policy-TIME={}\ - ", - pp.get_version(), - pp.get_hash(), - pp.get_time() - )); - } - - file_out - } - - fn from_str(s: &str) -> Self { - let mut eula = false; - let mut eula_version = String::new(); - let mut eula_hash = String::new(); - let mut pp = false; - let mut pp_version = String::new(); - let mut pp_hash = String::new(); - let mut tos = false; - let mut tos_version = String::new(); - let mut tos_hash = String::new(); - - let mut future_eula_version = String::new(); - let mut future_eula_hash = String::new(); - let mut future_eula_time = String::new(); - - let mut future_tos_version = String::new(); - let mut future_tos_hash = String::new(); - let mut future_tos_time = String::new(); - - let mut future_pp_version = String::new(); - let mut future_pp_hash = String::new(); - let mut future_pp_time = String::new(); - - let mut unix = String::new(); - - for line in s.lines() { - if let Some(v) = line.strip_prefix("EULA=") { - eula = v == "true"; - } else if let Some(v) = line.strip_prefix("EULA-VERSION=") { - 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("Terms-of-Service=") { - tos = v == "true"; - } 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("Terms-of-Service-HASH=") { - tos_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("Privacy-Policy=") { - pp = v == "true"; - } else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") { - pp_version = v.to_string(); - } 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-SECOND=") { - unix = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") { - future_eula_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-HASH=") { - future_eula_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-EULA-TIME=") { - future_eula_time = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-VERSION=") { - future_tos_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-HASH=") { - future_tos_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-TIME=") { - future_tos_time = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-VERSION=") { - future_pp_version = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-HASH=") { - future_pp_hash = v.to_string(); - } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-TIME=") { - future_pp_time = v.to_string(); - } - } - let unix: u64 = unix.parse::().unwrap_or(0); - let future_eula_time = future_eula_time.parse::().unwrap_or(0); - let future_tos_time = future_tos_time.parse::().unwrap_or(0); - let future_pp_time = future_pp_time.parse::().unwrap_or(0); - let state = Self { - accepted_eula: eula, - eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), - accepted_pp: pp, - 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)), - future_eula: if !future_eula_version.is_empty() { - Some(Doc::new( - future_eula_version, - future_eula_hash, - Type::EULA, - future_eula_time, - )) - } else { - None - }, - future_tos: if !future_tos_version.is_empty() { - Some(Doc::new( - future_tos_version, - future_tos_hash, - Type::TOS, - future_tos_time, - )) - } else { - None - }, - future_privacy: if !future_pp_version.is_empty() { - Some(Doc::new( - future_pp_version, - future_pp_hash, - Type::PP, - future_pp_time, - )) - } else { - None - }, - } - .sanitize(); - - state - } -} diff --git a/src/terms/doc.rs b/src/terms/doc.rs deleted file mode 100644 index 2365ba3..0000000 --- a/src/terms/doc.rs +++ /dev/null @@ -1,73 +0,0 @@ -use json::{JsonValue, object::Object}; - -use crate::{terms::terms_getter::Type, util::file_util::load_file}; - -#[derive(Clone, Debug, PartialEq, Eq)] -#[allow(unused)] -pub struct Doc { - version: String, - hash: String, - pub doc_type: Type, - timestamp: u64, -} - -#[allow(dead_code)] -impl Doc { - pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc { - Doc { - version, - hash, - doc_type, - timestamp, - } - } - - pub fn equals_some(&self, other: &Option) -> bool { - if let Some(other) = other { - self.get_version() == other.get_version() && self.get_hash() == other.get_hash() - } else { - false - } - } - pub fn equals(&self, other: &Self) -> bool { - self.get_version() == other.get_version() && self.get_hash() == other.get_hash() - } - - 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 { - 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, - }) - } -} diff --git a/src/terms/focus.rs b/src/terms/focus.rs deleted file mode 100644 index 669e936..0000000 --- a/src/terms/focus.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Focus { - Eula, - Tos, - Pp, - Cancel, - Continue, - ContinueAll, -} - -impl Focus { - pub fn next(&mut self, order: &[Focus]) { - if let Some(pos) = order.iter().position(|&f| f == *self) { - let next_pos = (pos + 1) % order.len(); - *self = order[next_pos]; - } - } - - pub fn prev(&mut self, order: &[Focus]) { - if let Some(pos) = order.iter().position(|&f| f == *self) { - let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 }; - *self = order[prev_pos]; - } - } -} diff --git a/src/terms/mod.rs b/src/terms/mod.rs deleted file mode 100644 index 4a11008..0000000 --- a/src/terms/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod buttons; -pub mod consent_state; -pub mod doc; -pub mod focus; -pub mod terms_getter; diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs deleted file mode 100755 index a0fa53b..0000000 --- a/src/terms/terms_getter.rs +++ /dev/null @@ -1,105 +0,0 @@ -use json::JsonValue::Object; - -use crate::terms::doc::Doc; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Type { - EULA, - TOS, - PP, -} - -impl Type { - pub fn to_str(&self) -> &str { - match self { - Self::EULA => "eula", - Self::TOS => "tos", - Self::PP => "privacy", - } - } - pub fn to_string(&self) -> String { - match self { - Self::EULA => "End User License Agreement".to_string(), - Self::TOS => "Terms of Service".to_string(), - Self::PP => "Privacy Policy".to_string(), - } - } -} - -pub fn get_link(terms_type: Type) -> String { - format!("https://legal.tensamin.net/{}/", terms_type.to_str()) -} -pub fn get_newest_link(terms_type: Type) -> String { - format!("https://legal.tensamin.net/{}/newest/", 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_newest_docs() -> Option<(Doc, Doc, Doc)> { - let body = reqwest::get("https://legal.tensamin.net/api/newest/") - .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 { - let body = reqwest::get(format!( - "https://legal.tensamin.net/api/text/{}/", - terms_type.to_str() - )) - .await - .ok()? - .text() - .await - .ok()?; - - Some(body) -} diff --git a/src/users/contact.rs b/src/users/contact.rs deleted file mode 100644 index 632cea3..0000000 --- a/src/users/contact.rs +++ /dev/null @@ -1,61 +0,0 @@ -use json::{self, JsonValue, number::Number}; -use std::time::{SystemTime, UNIX_EPOCH}; - -#[derive(Debug, Clone)] -pub struct Contact { - pub user_id: i64, - pub user_name: Option, - pub last_message_at: Option, -} - -impl Default for Contact { - fn default() -> Self { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64; - Contact { - user_id: 0, - user_name: None, - last_message_at: Some(now), - } - } -} - -impl Contact { - pub fn new(user_id: i64) -> Self { - Contact { - user_id: user_id, - user_name: None, - last_message_at: None, - } - } - pub fn set_last_message_at(&mut self, p0: i64) { - self.last_message_at = Option::from(p0); - } - - pub fn to_json(&self) -> JsonValue { - let mut obj = JsonValue::new_object(); - obj["user_id"] = JsonValue::Number(Number::from(self.user_id)); - if let Some(name) = &self.user_name { - obj["user_name"] = JsonValue::from(name.as_str()); - } - if let Some(ts) = &self.last_message_at { - obj["last_message_at"] = JsonValue::Number(Number::from(*ts)); - } - obj - } - pub fn from_json(o: &JsonValue) -> Contact { - let user_id = o["user_id"].as_i64().unwrap_or(0); - - let user_name = o["user_name"].as_str().map(|s| s.to_string()); - - let last_message_at = o["last_message_at"].as_i64(); - - Contact { - user_id, - user_name, - last_message_at, - } - } -} diff --git a/src/users/mod.rs b/src/users/mod.rs deleted file mode 100644 index aef5a02..0000000 --- a/src/users/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod contact; -pub mod user_community_util; -pub mod user_manager; -pub mod user_profile; diff --git a/src/users/user_community_util.rs b/src/users/user_community_util.rs deleted file mode 100644 index 45b036c..0000000 --- a/src/users/user_community_util.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::util::file_util::save_file; -use json::{self, Array, JsonValue}; -use std::fs; -use std::path::Path; - -pub struct UserCommunityUtil; - -impl UserCommunityUtil { - pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { - let file_path = format!("users/{}/", storage_owner); - let mut communities = Self::load_array(&file_path); - - let mut community = JsonValue::new_object(); - community["title"] = JsonValue::String(title); - community["address"] = JsonValue::String(address); - community["position"] = JsonValue::String(position); - - communities.push(community); - - save_file( - &file_path, - "communities.json", - &JsonValue::Array(communities).to_string(), - ); - } - - pub fn remove_community(storage_owner: i64, community_address: String) { - let file_path = format!("users/{}/", storage_owner); - let communities = Self::load_array(&file_path); - - let filtered: Array = communities - .iter() - .filter(|entry| entry["address"].as_str() != Some(&community_address)) - .cloned() - .collect(); - save_file( - &file_path, - "communities.json", - &JsonValue::Array(filtered).to_string(), - ); - } - - pub fn get_communities(storage_owner: i64) -> Array { - let file_path = format!("users/{}/communities.json", storage_owner); - Self::load_array(&file_path) - } - - fn load_array(file_path: &str) -> Array { - if !Path::new(file_path).exists() { - return Array::new(); - } - - match fs::read_to_string(file_path) { - Ok(content) => { - let parsed = json::parse(&content); - match parsed { - Ok(JsonValue::Array(arr)) => arr, - _ => Array::new(), - } - } - Err(err) => { - eprintln!("Failed to read file {}: {}", file_path, err); - Array::new() - } - } - } -} diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs deleted file mode 100644 index bcb5e26..0000000 --- a/src/users/user_manager.rs +++ /dev/null @@ -1,197 +0,0 @@ -use crate::omikron::omikron_connection::OMIKRON_CONNECTION; -use crate::users::user_profile::UserProfile; -use crate::util::crypto_helper::{self, public_key_to_base64}; -use crate::util::file_util::{load_file, save_file}; -use crate::util::logger::PrintType; -use crate::{RELOAD, SHUTDOWN}; -use crate::{log, log_cv}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use hex::{self}; -use json::JsonValue; -use once_cell::sync::Lazy; -use rand::Rng; -use rand_core::OsRng; -use rand_core::RngCore; -use sha2::{Digest, Sha256}; -use std::io::{self}; -use std::sync::Mutex; -use std::time::Duration; -use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; -use x448::{PublicKey, Secret}; - -static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); -static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); - -#[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::>(); - let uuid = segments[0].parse::().unwrap_or(0); - let b64_private_key = segments[1]; - - let secret: Secret = crypto_helper::load_secret_key(b64_private_key).unwrap(); - let public_key = PublicKey::from(&secret); - - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let reset_token = STANDARD.encode(&bytes); - - let user_profile = UserProfile::new( - uuid, - username.to_string(), - Some(username.to_string()), - crypto_helper::public_key_to_base64(&public_key), - crypto_helper::hex_hash(b64_private_key), - reset_token, - ); - USERS.lock().unwrap().push(user_profile); - Ok(()) -} - -pub async fn create_user(username: &str) -> (Option, Option) { - let register_cv = CommunicationValue::new(CommunicationType::get_register); - - let conn = OMIKRON_CONNECTION.clone(); - - let response_cv = match conn - .await_response(®ister_cv, Some(Duration::from_secs(20))) - .await - { - Ok(cv) => cv, - Err(_) => return (None, None), - }; - log_cv!(PrintType::Omega, response_cv); - - let user_id = match response_cv.get_data(DataTypes::user_id).as_number() { - Some(id) => id, - None => return (None, None), - }; - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let private_key = Secret::from_bytes(&buf).unwrap(); - let public_key = PublicKey::from(&private_key); - - let mut hasher = Sha256::new(); - hasher.update(&STANDARD.encode(&private_key.as_bytes()).as_bytes()); - let result = hasher.finalize(); - let private_key_hash = hex::encode(result); - - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let reset_token = STANDARD.encode(&bytes); - - let up = UserProfile::new( - user_id, - username.to_string(), - None, - STANDARD.encode(&public_key.as_bytes()), - private_key_hash, - reset_token.clone(), - ); - - let cv = CommunicationValue::new(CommunicationType::complete_register_user) - .add_data(DataTypes::user_id, DataValue::Number(user_id)) - .add_data(DataTypes::username, DataValue::Str(username.to_string())) - .add_data( - DataTypes::public_key, - DataValue::Str(public_key_to_base64(&public_key)), - ) - .add_data(DataTypes::iota_id, DataValue::Number(user_id)) - .add_data(DataTypes::reset_token, DataValue::Str(reset_token)); - - let response_cv = conn - .await_response(&cv, Some(Duration::from_secs(20))) - .await; - - if let Ok(resp) = response_cv { - log_cv!(PrintType::Omega, resp); - if !resp.is_type(CommunicationType::success) { - return (None, None); - } - } else { - return (None, None); - } - *SHUTDOWN.write().await = true; - *RELOAD.write().await = true; - log!("Created User"); - save_file( - "", - &format!("{}.tu", username), - &format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())), - ); - - USERS.lock().unwrap().push(up.clone()); - save_users(); - (Some(up), Some(STANDARD.encode(&private_key.as_bytes()))) -} - -pub fn get_user_by_username(username: &str) -> Option { - USERS - .lock() - .unwrap() - .iter() - .cloned() - .find(|u| u.username == username) -} - -pub fn get_user(user_id: i64) -> Option { - USERS - .lock() - .unwrap() - .iter() - .cloned() - .find(|u| u.user_id == user_id) -} - -pub fn get_users() -> Vec { - USERS.lock().unwrap().clone() -} - -pub fn remove_user(user_id: i64) { - let mut users = USERS.lock().unwrap(); - users.retain(|u| u.user_id != user_id); - *UNIQUE.lock().unwrap() = true; -} - -pub fn save_users() { - *UNIQUE.lock().unwrap() = false; - let users = USERS.lock().unwrap(); - let arr: Vec = users.iter().map(|u| u.to_json()).collect(); - let json_str = JsonValue::Array(arr).dump(); - - save_file("", "users.json", &json_str); -} - -pub fn clear() { - let mut users = USERS.lock().unwrap(); - users.clear(); - *UNIQUE.lock().unwrap() = true; -} - -pub async fn load_users() -> io::Result<()> { - let content = load_file("", "users.json"); - if content.trim().is_empty() { - return Ok(()); - } - - let parsed = - json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; - if let JsonValue::Array(arr) = parsed { - let mut users = USERS.lock().unwrap(); - for j in arr.iter() { - if let Some(up) = UserProfile::from_json(j).await { - users.push(up); - } - } - } - if *UNIQUE.lock().unwrap() { - save_users(); - } - Ok(()) -} - -#[allow(dead_code)] -pub fn set_unique(val: bool) { - *UNIQUE.lock().unwrap() = val; -} diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs deleted file mode 100644 index 5d60277..0000000 --- a/src/users/user_profile.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::util::file_util::{has_file, load_file, used_dir_space}; -use base64::{Engine as _, engine::general_purpose}; -use json::{JsonValue, object}; -use rand::Rng; -use rand::rngs::OsRng; - -// --- UserProfile --- -#[derive(Clone, Debug)] -pub struct UserProfile { - pub user_id: i64, - pub username: String, - pub public_key: String, - pub private_key_hash: String, - pub reset_token: String, - pub created_at: i64, - pub display_name: Option, -} - -impl UserProfile { - pub fn new( - user_id: i64, - username: String, - display_name: Option, - public_key: String, - private_key_hash: String, - reset_token: String, - ) -> Self { - Self { - user_id, - username, - display_name, - public_key, - private_key_hash, - created_at: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as i64, - reset_token, - } - } - - pub fn to_json(&self) -> JsonValue { - let mut obj = object! { - "uuid" => self.user_id, - "username" => self.username.clone(), - "public_key" => self.public_key.clone(), - "private_key_hash" => self.private_key_hash.clone(), - "created_at" => self.created_at, - "reset_token" => self.reset_token.clone() - }; - if let Some(d) = &self.display_name { - obj["display_name"] = d.clone().into(); - } - obj - } - pub fn frontend(&self) -> JsonValue { - let mut obj = object! { - "uuid" => self.user_id, - "username" => self.username.clone(), - "public_key" => self.public_key.clone(), - "private_key_hash" => self.private_key_hash.clone(), - "created_at" => self.created_at, - "storage" => used_dir_space(&format!("users/{}", self.user_id.to_string())), - }; - if let Some(d) = &self.display_name { - obj["display_name"] = d.clone().into(); - } - if has_file("", &format!("{}.tu", self.username.clone())) { - obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into(); - } - - obj - } - pub async fn from_json(j: &JsonValue) -> Option { - let user_id = j["uuid"].as_i64()?; - let username = j["username"].as_str()?.to_string(); - let public_key = j["public_key"].as_str()?.to_string(); - let private_key_hash = j["private_key_hash"].as_str()?.to_string(); - let reset_token = j["reset_token"].as_str()?.to_string(); - let created_at = j["created_at"].as_i64()?; - let display_name = j["display_name"].as_str().map(|s| s.to_string()); - - let up = UserProfile { - user_id, - username, - display_name, - public_key, - private_key_hash, - created_at, - reset_token, - }; - - // TODO: Migrate to Omikron / Wss - /* if j.has_key("migrate") - || j.has_key("migrating") - || j.has_key("changing") - || j.has_key("move") - || j.has_key("moving") - { - if auth_connector::migrate_user(&mut up).await { - log_message(format!("[INFO] Migration triggered for {}", up.username)); - user_manager::set_unique(true); - } - } */ - - Some(up) - } - - #[allow(dead_code)] - pub fn randomize_reset_token(&mut self) -> String { - let mut bytes = [0u8; 192]; - OsRng.fill(bytes.as_mut()); - let new_token = general_purpose::STANDARD.encode(&bytes); - self.reset_token = new_token.clone(); - new_token - } - - #[allow(dead_code)] - pub fn get_display_name(&self) -> String { - self.display_name - .clone() - .unwrap_or_else(|| self.username.clone()) - } -} diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs deleted file mode 100644 index e54fe29..0000000 --- a/src/util/chat_files.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::log; -use crate::util::db; -use json::{JsonValue, array, object}; -use rusqlite::params; -use std::io; -use std::sync::{Arc, LazyLock, Mutex}; - -#[derive(PartialEq, Debug, Clone)] -pub enum MessageState { - Read, - Received, - Sent, - Sending, -} - -impl MessageState { - pub fn as_str(&self) -> &'static str { - match self { - MessageState::Read => "read", - MessageState::Received => "received", - MessageState::Sent => "sent", - MessageState::Sending => "sending", - } - } - - pub fn from_str(value: &str) -> Self { - match value.to_lowercase().as_str() { - "read" => MessageState::Read, - "received" => MessageState::Received, - "sent" => MessageState::Sent, - _ => MessageState::Sending, - } - } - - pub fn upgrade(self, other: Self) -> Self { - if other == Self::Read || self == Self::Read { - Self::Read - } else if other == Self::Received || self == Self::Received { - Self::Received - } else if other == Self::Sent || self == Self::Sent { - Self::Sent - } else { - Self::Sending - } - } -} - -// Shared DB created via helper. -// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist. -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -pub fn add_message( - send_time: u128, - storage_owner_is_sender: bool, - storage_owner: i64, - external_user: i64, - message: &str, - height: i64, -) { - let message_time = match i64::try_from(send_time) { - Ok(v) => v, - Err(_) => { - log!("Failed to store message: send_time out of range for i64 ({send_time})"); - return; - } - }; - - // Insert the message into the DB - let insert_result = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO messages ( - storage_owner, - external_user, - message_time, - content, - sent_by_self, - message_state, - height - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - "#, - params![ - storage_owner, - external_user, - message_time, - message, - if storage_owner_is_sender { - 1_i64 - } else { - 0_i64 - }, - MessageState::Sending.as_str(), - height, - ], - )?; - Ok(()) - }); - - if let Err(e) = insert_result { - log!("Failed to insert message into sqlite: {}", e); - return; - } - - // Update contacts table to reflect that this conversation exists and has a recent message. - // Use the Contact helper to set last_message_at to the message timestamp. - let mut contact = crate::users::contact::Contact::new(external_user); - contact.set_last_message_at(message_time); - // This will insert or update the contact for the storage owner. - crate::util::chats_util::mod_user(storage_owner, &contact); -} - -pub fn change_message_state( - timestamp: i64, - storage_owner: i64, - external_user: i64, - new_state: MessageState, -) -> io::Result<()> { - // Run the SELECT and UPDATE inside with_conn to centralize connection access. - let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| { - let current: Option = match conn.query_row( - r#" - SELECT message_state - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - AND message_time = ?3 - ORDER BY id DESC - LIMIT 1 - "#, - params![storage_owner, external_user, timestamp], - |row| row.get(0), - ) { - Ok(state) => Some(state), - Err(rusqlite::Error::QueryReturnedNoRows) => None, - Err(e) => return Err(e), - }; - - let Some(current_state_raw) = current else { - return Ok(()); - }; - - let upgraded = MessageState::from_str(¤t_state_raw) - .upgrade(new_state) - .as_str() - .to_string(); - - conn.execute( - r#" - UPDATE messages - SET message_state = ?1 - WHERE id = ( - SELECT id - FROM messages - WHERE storage_owner = ?2 - AND external_user = ?3 - AND message_time = ?4 - ORDER BY id DESC - LIMIT 1 - ) - "#, - params![upgraded, storage_owner, external_user, timestamp], - )?; - Ok(()) - }); - - match res { - Ok(_) => Ok(()), - Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), - } -} - -pub fn get_messages( - storage_owner: i64, - external_user: i64, - loaded_messages: i64, - amount: i64, -) -> JsonValue { - let messages = array![]; - - if amount <= 0 || loaded_messages < 0 { - return messages; - } - - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT - message_time, - content, - sent_by_self, - message_state, - height - FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 - ORDER BY message_time DESC, id DESC - LIMIT ?3 OFFSET ?4 - "#, - )?; - - let rows = stmt.query_map( - params![storage_owner, external_user, amount, loaded_messages], - |row| { - let message_time: i64 = row.get(0)?; - let content: String = row.get(1)?; - let sent_by_self: i64 = row.get(2)?; - let message_state: String = row.get(3)?; - let height: i64 = row.get(4).unwrap_or(0); - Ok((message_time, content, sent_by_self, message_state, height)) - }, - )?; - - let mut out = array![]; - for row in rows { - match row { - Ok((message_time, content, sent_by_self, message_state, height)) => { - let msg = object! { - "message_time" => message_time, - "content" => content, - "sent_by_self" => (sent_by_self != 0), - "message_state" => message_state, - "height" => height - }; - if let Err(e) = out.push(msg) { - // out.push returns a JsonError; log it instead of using `?` to avoid - // incompatible error conversions inside the DB closure. - log!("Failed to append message to output array: {:?}", e); - } - } - Err(e) => { - log!("Failed to read row from sqlite: {}", e); - } - } - } - Ok(out) - }); - - match res { - Ok(v) => v, - Err(e) => { - log!("Failed to query messages: {}", e); - messages - } - } -} - -#[cfg(test)] -mod tests { - use super::MessageState; - - #[test] - fn upgrade_prefers_highest_state() { - assert_eq!( - MessageState::Sending.upgrade(MessageState::Sent), - MessageState::Sent - ); - assert_eq!( - MessageState::Sent.upgrade(MessageState::Received), - MessageState::Received - ); - assert_eq!( - MessageState::Received.upgrade(MessageState::Read), - MessageState::Read - ); - } - - #[test] - fn from_str_is_case_insensitive() { - assert_eq!(MessageState::from_str("READ"), MessageState::Read); - assert_eq!(MessageState::from_str("received"), MessageState::Received); - assert_eq!(MessageState::from_str("Sent"), MessageState::Sent); - assert_eq!(MessageState::from_str("unknown"), MessageState::Sending); - } -} diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs deleted file mode 100644 index b8e0ad2..0000000 --- a/src/util/chats_util.rs +++ /dev/null @@ -1,121 +0,0 @@ -use crate::users::contact::Contact; -use crate::util::db; -use rusqlite::params; -use std::sync::{Arc, LazyLock, Mutex}; - -/// Shared DB connection for contacts/messages (created by db helper). -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -/// Insert or update a contact for the given storage owner. -pub fn mod_user(storage_owner: i64, contact: &Contact) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO contacts ( - storage_owner, - user_id, - user_name, - last_message_at - ) VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(storage_owner, user_id) DO UPDATE SET - user_name = excluded.user_name, - last_message_at = excluded.last_message_at - "#, - params![ - storage_owner, - contact.user_id, - contact.user_name.clone(), - contact.last_message_at - ], - )?; - Ok(()) - }) { - eprintln!("Failed to mod_user: {}", e); - } -} - -/// Retrieve a single contact for storage_owner/user_id. -pub fn get_user(storage_owner: i64, user_id: i64) -> Option { - let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { - match conn.query_row( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 AND user_id = ?2 - LIMIT 1 - "#, - params![storage_owner, user_id], - |r| { - let user_id: i64 = r.get(0)?; - let user_name: Option = r.get(1)?; - let last_message_at: Option = r.get(2)?; - Ok(Contact { - user_id, - user_name, - last_message_at, - }) - }, - ) { - Ok(c) => Ok(Some(c)), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e), - } - }); - - match res { - Ok(opt) => opt, - Err(e) => { - eprintln!("Error querying user in get_user: {}", e); - None - } - } -} - -/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc. -pub fn get_users(storage_owner: i64) -> Vec { - let contacts_out = Vec::new(); - - let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT user_id, user_name, last_message_at - FROM contacts - WHERE storage_owner = ?1 - ORDER BY - CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, - last_message_at DESC, - user_id ASC - "#, - )?; - - let rows = stmt.query_map(params![storage_owner], |r| { - let user_id: i64 = r.get(0)?; - let user_name: Option = r.get(1)?; - let last_message_at: Option = r.get(2)?; - Ok(Contact { - user_id, - user_name, - last_message_at, - }) - })?; - - let mut out = Vec::new(); - for row in rows { - match row { - Ok(contact) => out.push(contact), - Err(e) => eprintln!("Failed to read contact row: {}", e), - } - } - Ok(out) - }); - - match res { - Ok(v) => v, - Err(e) => { - eprintln!("Failed to query contacts in get_users: {}", e); - contacts_out - } - } -} diff --git a/src/util/communities_util.rs b/src/util/communities_util.rs deleted file mode 100644 index ed8b7ae..0000000 --- a/src/util/communities_util.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::util::db; -use json::Array; -use rusqlite::params; -use std::sync::{Arc, LazyLock, Mutex}; - -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - db::create_general_messages_db().expect("Failed to create or initialize general messages DB") -}); - -pub struct CommunitiesUtil; - -impl CommunitiesUtil { - pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - r#" - INSERT INTO communities ( - storage_owner, - address, - title, - position - ) VALUES (?1, ?2, ?3, ?4) - ON CONFLICT(storage_owner, address) DO UPDATE SET - title = excluded.title, - position = excluded.position - "#, - params![storage_owner, address, title, position], - )?; - Ok(()) - }) { - eprintln!("Failed to add_community: {}", e); - } - } - - pub fn remove_community(storage_owner: i64, community_address: String) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { - conn.execute( - "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", - params![storage_owner, community_address], - )?; - Ok(()) - }) { - eprintln!("Failed to remove_community: {}", e); - } - } - - pub fn get_communities(storage_owner: i64) -> Array { - let communities_out = Array::new(); - - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare( - r#" - SELECT address, title, position - FROM communities - WHERE storage_owner = ?1 - "#, - )?; - - let rows = stmt.query_map(params![storage_owner], |r| { - let address: String = r.get(0)?; - let title: String = r.get(1)?; - let position: String = r.get(2)?; - Ok((address, title, position)) - })?; - - let mut out = Array::new(); - for row in rows { - match row { - Ok((address, title, position)) => { - let mut community = json::JsonValue::new_object(); - community["title"] = json::JsonValue::String(title); - community["address"] = json::JsonValue::String(address); - community["position"] = json::JsonValue::String(position); - out.push(community); - } - Err(e) => eprintln!("Failed to read community row: {}", e), - } - } - Ok(out) - }); - - match res { - Ok(arr) => arr, - Err(e) => { - eprintln!("Failed to query communities in get_communities: {}", e); - communities_out - } - } - } -} diff --git a/src/util/config_util.rs b/src/util/config_util.rs deleted file mode 100644 index 6330b59..0000000 --- a/src/util/config_util.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::util::file_util::{load_file, save_file}; -use json::JsonValue; -use once_cell::sync::Lazy; -use tokio::sync::RwLock; - -pub static CONFIG: Lazy> = Lazy::new(|| RwLock::new(ConfigUtil::new())); - -pub struct ConfigUtil { - pub config: JsonValue, - pub unique: bool, -} - -impl ConfigUtil { - pub fn new() -> Self { - Self { - config: JsonValue::new_object(), - unique: false, - } - } - pub fn clear(&mut self) { - self.config = JsonValue::new_object(); - } - pub fn load(&mut self) { - let s = load_file("", "config.json"); - if !s.is_empty() { - self.config = json::parse(&s).unwrap_or(JsonValue::new_object()); - } - } - - pub fn get_iota_id(&self) -> i64 { - self.config["iota_id"].as_i64().unwrap_or(0) - } - - pub fn get_port(&self) -> u16 { - self.config["port"].as_u16().unwrap_or(1984) - } - - pub fn get_public_key(&self) -> Option { - self.config["public_key"].as_str().map(String::from) - } - - pub fn get_private_key(&self) -> Option { - self.config["private_key"].as_str().map(String::from) - } - - pub fn get(&self, key: &str) -> &JsonValue { - &self.config[key] - } - - pub fn change(&mut self, key: &str, value: JsonValue) { - self.config[key] = value; - self.unique = true; - } - - pub fn update(&mut self) { - if self.unique { - save_file("", "config.json", &self.config.to_string()); - } - } -} diff --git a/src/util/crypto_helper.rs b/src/util/crypto_helper.rs deleted file mode 100644 index 4a7fd76..0000000 --- a/src/util/crypto_helper.rs +++ /dev/null @@ -1,133 +0,0 @@ -use aes_gcm::{ - Aes256Gcm, Nonce, - aead::{Aead, KeyInit, OsRng}, -}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; -use rand_core::RngCore; -use sha2::{Digest, Sha256}; -use x448::{PublicKey, Secret, SharedSecret}; - -/// Errors for crypto opertions -#[derive(Debug)] -#[allow(dead_code)] -pub enum CryptoError { - Base64Decode(base64::DecodeError), - InvalidKey, - AgreementError, - EncryptionError(aes_gcm::Error), - DecryptionError(aes_gcm::Error), -} - -impl From for CryptoError { - fn from(err: base64::DecodeError) -> Self { - CryptoError::Base64Decode(err) - } -} - -pub struct KeyPair { - pub secret: Secret, - pub public: PublicKey, -} - -pub fn generate_keypair() -> KeyPair { - let mut buf = [0u8; 56]; - let mut rng = OsRng; - rng.fill_bytes(&mut buf); - let secret = Secret::from_bytes(&buf).unwrap(); - let public = PublicKey::from(&secret); - KeyPair { secret, public } -} - -pub fn public_key_to_base64(pubkey: &PublicKey) -> String { - STANDARD.encode(pubkey.as_bytes().as_ref()) -} - -pub fn secret_key_to_base64(secret: &Secret) -> String { - STANDARD.encode(secret.as_bytes().as_ref()) -} - -pub fn load_public_key(base64_pub: &str) -> Option { - let bytes = STANDARD.decode(base64_pub).unwrap(); - PublicKey::from_bytes(&bytes) -} - -pub fn load_secret_key(base64_secret: &str) -> Option { - let bytes = STANDARD.decode(base64_secret).unwrap(); - 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()); - let result = hasher.finalize(); - let mut key = [0u8; 32]; - key.copy_from_slice(&result[..32]); - key -} - -#[allow(dead_code)] -pub fn encrypt( - base64_secret: &str, - base64_peer_pub: &str, - plaintext: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - let ciphertext = cipher - .encrypt(nonce, plaintext.as_bytes()) - .map_err(CryptoError::EncryptionError)?; - // prefix nonce to ciphertext - let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); - out.extend_from_slice(&nonce_bytes); - out.extend_from_slice(&ciphertext); - Ok(STANDARD.encode(&out)) -} - -#[allow(dead_code)] -pub fn decrypt( - base64_secret: &str, - base64_peer_pub: &str, - encrypted_base64: &str, -) -> Result { - let secret = load_secret_key(base64_secret).unwrap(); - let peer_pub = load_public_key(base64_peer_pub).unwrap(); - let shared = secret - .to_diffie_hellman(&peer_pub) - .ok_or(CryptoError::AgreementError)?; - let key_bytes = derive_aes_key(&shared); - let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); - - let encrypted = STANDARD.decode(encrypted_base64)?; - if encrypted.len() < 12 { - return Err(CryptoError::DecryptionError(aes_gcm::Error)); - } - let nonce_bytes = &encrypted[..12]; - let ciphertext = &encrypted[12..]; - let nonce = Nonce::from_slice(nonce_bytes); - let plaintext_bytes = cipher - .decrypt(nonce, ciphertext) - .map_err(CryptoError::DecryptionError)?; - let plaintext = String::from_utf8(plaintext_bytes) - .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; - Ok(plaintext) -} - -pub fn hash_it(input: &str) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(input.as_bytes()); - hasher.finalize().to_vec() -} - -pub fn hex_hash(input: &str) -> String { - let digest = hash_it(input); - digest.iter().map(|b| format!("{:02x}", b)).collect() -} diff --git a/src/util/crypto_util.rs b/src/util/crypto_util.rs deleted file mode 100644 index bb784e8..0000000 --- a/src/util/crypto_util.rs +++ /dev/null @@ -1,178 +0,0 @@ -use aes_gcm::{ - Aes256Gcm, Nonce, - aead::{Aead, KeyInit, Payload}, -}; -use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STD}; -use hkdf::Hkdf; -type HkdfSha256 = sha2::Sha256; -use sha2::{Digest, Sha256 as HashSha256}; -use x448::{PublicKey, Secret}; - -#[derive(Debug)] -#[allow(dead_code)] -pub enum SecurePayloadError { - InvalidBase64, - InvalidHex, - EncryptionError, - DecryptionError, - InvalidKeyLength, -} - -#[derive(Clone, Copy, Debug)] -#[allow(dead_code)] -pub enum DataFormat { - Raw, - Base64, - Hex, -} - -pub struct SecurePayload { - inner_data: Vec, - private_key: Secret, -} - -impl Clone for SecurePayload { - fn clone(&self) -> Self { - Self { - inner_data: self.inner_data.clone(), - private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), - } - } -} - -#[allow(dead_code)] -impl SecurePayload { - pub fn new>( - data: T, - format: DataFormat, - private_key: S, - ) -> Result - where - S: Into, - { - let raw_data = match format { - DataFormat::Raw => data.as_ref().to_vec(), - DataFormat::Base64 => BASE64_STD - .decode(data.as_ref()) - .map_err(|_| SecurePayloadError::InvalidBase64)?, - DataFormat::Hex => { - hex::decode(data.as_ref()).map_err(|_| SecurePayloadError::InvalidHex)? - } - }; - - Ok(Self { - inner_data: raw_data, - private_key: private_key.into(), - }) - } - - pub fn get_public_key(&self) -> [u8; 56] { - *PublicKey::from(&self.private_key).as_bytes() - } - - pub fn export(&self, format: DataFormat) -> String { - match format.into() { - DataFormat::Raw => String::from_utf8_lossy(&self.inner_data).to_string(), - DataFormat::Base64 => BASE64_STD.encode(&self.inner_data), - DataFormat::Hex => hex::encode(&self.inner_data), - } - } - - pub fn get_bytes(&self) -> &[u8] { - &self.inner_data - } - - pub fn get_hash(&self, format: DataFormat) -> String { - let mut hasher = HashSha256::new(); - hasher.update(&self.inner_data); - let result = hasher.finalize(); - - match format { - DataFormat::Raw => String::from_utf8_lossy(&result).to_string(), - DataFormat::Base64 => BASE64_STD.encode(result), - DataFormat::Hex => hex::encode(result), - } - } - - pub fn encrypt_x448(&self, public_key: S) -> Result - where - S: Into, - { - let peer_pub = public_key.into(); - let shared_secret = self.private_key.as_diffie_hellman(&peer_pub).unwrap(); - - let hkdf = Hkdf::::new(None, shared_secret.as_bytes()); - let mut okm = [0u8; 44]; - - hkdf.expand(b"x448-aes-gcm-no-overhead", &mut okm) - .map_err(|_| SecurePayloadError::EncryptionError)?; - - let key = &okm[..32]; - let nonce_bytes = &okm[32..]; - - let cipher = Aes256Gcm::new(key.into()); - let nonce = Nonce::from_slice(nonce_bytes); - - let ciphertext = cipher - .encrypt( - nonce, - Payload { - msg: &self.inner_data, - aad: &[], - }, - ) - .map_err(|_| SecurePayloadError::EncryptionError)?; - - Ok(SecurePayload { - inner_data: ciphertext, - private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), - }) - } - - pub fn decrypt_to_format( - &self, - peer_public_key_bytes: &[u8; 56], - output_format: DataFormat, - ) -> Result { - let decrypted_instance = - self.decrypt_x448(PublicKey::from_bytes(peer_public_key_bytes).unwrap())?; - Ok(decrypted_instance.export(output_format)) - } - - pub fn decrypt_x448( - &self, - peer_public_key_bytes: S, - ) -> Result - where - S: Into, - { - let peer_pub = peer_public_key_bytes.into(); - let shared_secret = self.private_key.as_diffie_hellman(&peer_pub).unwrap(); - - let hkdf = Hkdf::::new(None, shared_secret.as_bytes()); - let mut okm = [0u8; 44]; - hkdf.expand(b"x448-aes-gcm-no-overhead", &mut okm) - .map_err(|_| SecurePayloadError::DecryptionError)?; - - let key = &okm[..32]; - let nonce_bytes = &okm[32..]; - - let cipher = Aes256Gcm::new(key.into()); - let nonce = Nonce::from_slice(nonce_bytes); - - let plaintext = cipher - .decrypt( - nonce, - Payload { - msg: &self.inner_data, - aad: &[], - }, - ) - .map_err(|_| SecurePayloadError::DecryptionError)?; - - Ok(SecurePayload { - inner_data: plaintext, - private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), - }) - } -} diff --git a/src/util/db.rs b/src/util/db.rs deleted file mode 100644 index 9fb40a0..0000000 --- a/src/util/db.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! Database helper utilities. -//! -//! This module provides small helpers to open/init sqlite databases and to -//! create a shared (Arc>) connection wrapper callers can -//! reuse. The goal is to centralize the "open and initialize" logic and -//! provide small convenience helpers used by other util modules. - -use crate::util::file_util::get_directory; -use rusqlite::{Connection, Error as RusqliteError}; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -/// Returns the file path for a named DB inside the application's data directory. -/// -/// Arguments: -/// - `db_name` : name of the DB (without extension). Example: `"messages"`. -pub fn db_file_path(db_name: &str) -> String { - let mut p = PathBuf::from(get_directory()); - p.push(format!("{db_name}.sqlite3")); - p.to_string_lossy().to_string() -} - -/// Open a sqlite connection to the named DB file (no initialization). -/// -/// Arguments: -/// - `db_name`: name of the DB (without extension). -pub fn open_connection(db_name: &str) -> Result { - let path = db_file_path(db_name); - Connection::open(path) -} - -/// Open a connection and immediately run `init_sql` via `execute_batch`. -/// -/// Arguments: -/// - `db_name`: name of the DB (without extension). -/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple). -pub fn open_and_init(db_name: &str, init_sql: &str) -> Result { - let conn = open_connection(db_name)?; - conn.execute_batch(init_sql)?; - Ok(conn) -} - -/// Create a shared, Arc> initialized with the given SQL. -/// -/// This is a convenience wrapper that returns an owned Arc> -/// so caller modules can store it in a `static` or pass it around. -/// -/// Arguments: -/// - `db_name`: DB name (without extension). -/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements). -pub fn create_shared_connection( - db_name: &str, - init_sql: &str, -) -> Result>, String> { - match open_and_init(db_name, init_sql) { - Ok(conn) => { - // Configure some sensible defaults for concurrency - // Attempt to set a busy timeout to reduce SQLITE_BUSY failures. - let _ = conn.busy_timeout(Duration::from_millis(250)); - Ok(Arc::new(Mutex::new(conn))) - } - Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)), - } -} - -/// Acquire the Connection from an Arc> and run the provided -/// closure. Converts rusqlite::Error into a String on error. -/// -/// Arguments: -/// - `shared`: Arc> -/// - `f`: closure that receives &Connection and returns Result -/// -/// Returns Ok(T) or Err(String). -pub fn with_conn(shared: &Arc>, f: F) -> Result -where - F: FnOnce(&Connection) -> Result, -{ - // When invoked from within an async runtime (such as Tokio), taking a blocking - // std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent - // awaits. Detect whether we're running inside a Tokio runtime and, if so, - // execute the blocking lock + database closure using Tokio's blocking helper. - // - // The blocking section returns Result so we can propagate errors - // in the same form as before. - if tokio::runtime::Handle::try_current().is_ok() { - tokio::task::block_in_place(|| { - let guard = shared - .lock() - .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; - f(&*guard).map_err(|e| e.to_string()) - }) - } else { - let guard = shared - .lock() - .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; - f(&*guard).map_err(|e| e.to_string()) - } -} - -/// Initialize a general-purpose messages+contacts DB and return a shared -/// connection. This helper creates a single DB file that can contain multiple -/// tables (messages, contacts, ...). The SQL here is conservative and intended -/// to be safe if called multiple times. -/// -/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)` -/// directly, but this convenience is useful for code that expects both tables. -pub fn create_general_messages_db() -> Result>, String> { - // Keep PRAGMA and schema in one multi-statement string so callers only - // need to call a single execute_batch. - const INIT_SQL: &str = r#" - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - - CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - external_user INTEGER NOT NULL, - message_time INTEGER NOT NULL, - content TEXT NOT NULL, - sent_by_self INTEGER NOT NULL, - message_state TEXT NOT NULL, - height INTEGER NOT NULL DEFAULT 0 - ); - - CREATE INDEX IF NOT EXISTS idx_messages_lookup - ON messages (storage_owner, external_user, message_time DESC); - - CREATE TABLE IF NOT EXISTS contacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - user_id INTEGER NOT NULL, - user_name TEXT, - last_message_at INTEGER, - UNIQUE(storage_owner, user_id) - ); - - CREATE INDEX IF NOT EXISTS idx_contacts_owner - ON contacts (storage_owner, last_message_at DESC, user_id ASC); - - CREATE TABLE IF NOT EXISTS communities ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - storage_owner INTEGER NOT NULL, - address TEXT NOT NULL, - title TEXT NOT NULL, - position TEXT NOT NULL, - UNIQUE(storage_owner, address) - ); - - CREATE INDEX IF NOT EXISTS idx_communities_owner - ON communities (storage_owner); - "#; - - match create_shared_connection("messages", INIT_SQL) { - Ok(shared_conn) => { - // Attempt to add the height column for backwards compatibility. - // This will fail if the column already exists, which is expected. - let _ = with_conn(&shared_conn, |conn| { - let _ = conn.execute( - "ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0", - [], - ); - Ok(()) - }); - Ok(shared_conn) - } - Err(e) => Err(e), - } -} - -/* -Example usage: - -// In some util module (at init time, e.g. lazy_static or LazyLock) -static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { - create_general_messages_db().expect("failed to create messages DB") -}); - -// Later, to run a query: -let res: Result, String> = with_conn(&MESSAGES_DB, |conn| { - let mut stmt = conn.prepare("SELECT ...")?; - let rows = stmt.query_map(...)?; - // collect and return Ok(...) -}); -*/ diff --git a/src/util/file_util.rs b/src/util/file_util.rs deleted file mode 100755 index 2d00718..0000000 --- a/src/util/file_util.rs +++ /dev/null @@ -1,354 +0,0 @@ -use reqwest::Client; -use std::ffi::OsStr; -use std::fs::{self, File}; -use std::io::{self, BufReader, Read}; -use std::path::{Path, PathBuf}; -use sysinfo::System; -use tokio::io::AsyncWriteExt; -use uuid::Uuid; -use walkdir::WalkDir; -use zip::ZipArchive; - -use crate::log; - -#[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; - } - if let Err(e) = fs::remove_dir_all(directory) { - log!( - "[IMPORTANT] Couldn't delete directory {}: {}", - directory.display(), - e, - ); - return false; - } - true -} - -#[allow(dead_code)] -pub fn delete_user_directory(user_id: i64) { - let user_dir = Path::new(&get_directory()) - .join("users") - .join(user_id.to_string()); - let _ = delete_dir_recursive(&user_dir); -} - -pub fn load_file_buf(path: &str, name: &str) -> io::Result> { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - // Ensure the directory exists, create if necessary - if !dir.exists() { - if let Err(_) = fs::create_dir_all(&dir) { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "Directory creation failed", - )); - } - } - - // Create the file if it doesn't exist - if !file_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "File creation failed", - )); - } - - // Open the file and return a BufReader for efficient reading - let file = File::open(&file_path)?; - Ok(BufReader::new(file)) -} -pub fn has_file(path: &str, name: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - return false; - } - - if !file_path.exists() { - return false; - } - - true -} -pub fn has_dir(path: &str) -> bool { - let dir = Path::new(&get_directory()).join(path); - - if !dir.exists() { - return false; - } - - true -} - -pub fn load_file(path: &str, name: &str) -> String { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); - return String::new(); - } - return String::new(); - } - - if !file_path.exists() { - if let Err(e) = File::create(&file_path) { - log!("[IMPORTANT] Couldn't create file: {}", e); - } - return String::new(); - } - - let mut content = String::new(); - if let Ok(mut f) = File::open(&file_path) { - let _ = f.read_to_string(&mut content); - } - content -} - -pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - std::fs::read(file_path) -} - -pub fn save_file(path: &str, name: &str, value: &str) { - let dir = Path::new(&get_directory()).join(path); - let file_path = dir.join(name); - - if !dir.exists() { - if let Err(e) = fs::create_dir_all(&dir) { - log!("[IMPORTANT] Couldn't create directories: {}", e); - return; - } - } - - if let Err(e) = fs::write(&file_path, value) { - log!( - "[IMPORTANT] Couldn't write file {}: {}", - file_path.display(), - e - ); - } -} - -pub fn get_children(path: &str) -> Vec { - let dir = Path::new(&get_directory()).join(path); - let mut children = Vec::new(); - if let Ok(entries) = fs::read_dir(&dir) { - for entry in entries { - if let Ok(entry) = entry { - children.push(entry.file_name().to_string_lossy().to_string()); - } - } - } - children -} - -pub fn get_directory() -> String { - let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(".")); - exe.parent() - .unwrap_or(Path::new(".")) - .to_string_lossy() - .to_string() -} - -// Helper to download the zip file content to a file on disk -#[allow(dead_code)] -pub fn used_space() -> u64 { - get_directory_size(&PathBuf::from(get_directory())) -} -pub fn used_dir_space(path: &str) -> u64 { - get_directory_size(&PathBuf::from(format!("{}/{}", get_directory(), path))) -} - -pub fn get_directory_size(directory: &Path) -> u64 { - let mut size = 0; - for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - if path.is_file() { - if let Ok(metadata) = path.metadata() { - size += path.file_name().unwrap_or(OsStr::new("")).len() as u64; - size += metadata.len(); - } - } - } - size -} - -#[allow(dead_code)] -pub fn get_designed_storage(user_id: i64) -> String { - let user_dir = Path::new(&get_directory()) - .join("users") - .join(user_id.to_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; - let m = k / 1024.0; - let g = m / 1024.0; - let t = g / 1024.0; - - if t >= 1.0 { - hr_size = format!("{:.2}TB", t); - } else if g >= 1.0 { - hr_size = format!("{:.2}GB", g); - } else if m >= 1.0 { - hr_size = format!("{:.2}MB", m); - } else if k >= 1.0 { - hr_size = format!("{:.2}KB", k); - } - hr_size -} - -#[allow(dead_code)] -pub fn get_used_ram() -> String { - let mut sys = System::new_all(); - sys.refresh_all(); - let used = sys.used_memory() * 1024; // kB to bytes - let total = sys.total_memory() * 1024; - format!("{}/{}", design_byte(used), design_byte(total)) -} - -pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box> { - let client = Client::new(); - let mut response = client.get(url).send().await?; - - if !response.status().is_success() { - let err_msg = format!("Failed to download file: Status {}", response.status()); - log!("{}", err_msg.clone()); - return Err(err_msg.into()); - } - - let mut zip_file = tokio::fs::File::create(zip_path).await?; - while let Some(chunk) = response.chunk().await? { - zip_file.write_all(&chunk).await?; - } - - zip_file.flush().await?; - Ok(()) -} - -#[allow(dead_code, deprecated)] -fn extract_zip_contents_to_folder( - zip_path: &Path, - target_dir: &Path, -) -> Result<(), Box> { - let file = File::open(zip_path)?; - let mut archive = ZipArchive::new(file)?; - - let staging_dir = target_dir.with_extension("staging"); - - let _ = fs::remove_dir_all(&staging_dir); - fs::create_dir_all(&staging_dir)?; - - let mut first_item_name: Option = None; - - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let entry_path = staging_dir.join(file.sanitized_name()); - - if i == 0 { - if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 { - first_item_name = Some(file.sanitized_name()); - } - } - - if file.name().ends_with('/') { - fs::create_dir_all(&entry_path)?; - } else { - if let Some(parent) = entry_path.parent() { - fs::create_dir_all(parent)?; - } - let mut out_file = File::create(entry_path)?; - io::copy(&mut file, &mut out_file)?; - } - } - - if let Some(root_path) = first_item_name { - let root_dir = staging_dir.join(&root_path); - - if root_dir.is_dir() { - let root_contents_count = fs::read_dir(&staging_dir)?.count(); - - if root_contents_count == 1 - || (root_contents_count > 1 && fs::metadata(&root_dir).is_ok()) - { - let _ = fs::remove_dir_all(target_dir); - fs::create_dir_all(target_dir)?; - - for entry in fs::read_dir(root_dir)? { - let entry = entry?; - let src = entry.path(); - let dest = target_dir.join(entry.file_name()); - - if let Err(_) = fs::rename(&src, &dest) { - if src.is_file() { - fs::copy(&src, &dest)?; - } else { - if entry.path().is_dir() { - fs::rename(&src, &dest)?; - } - } - } - } - - let _ = fs::remove_dir_all(&staging_dir); - return Ok(()); - } - } - } - - log!("Extracting directly (no single root folder detected)."); - let _ = fs::remove_dir_all(target_dir); - fs::rename(&staging_dir, target_dir)?; - - Ok(()) -} - -#[allow(dead_code)] -pub async fn download_and_extract_zip(url: &str, as_name: &str) { - log!("Downloading ZIP file..."); - let base_dir = PathBuf::from(get_directory()); - let zip_filename = format!("{}.zip", Uuid::new_v4()); - let zip_path = base_dir.join(&zip_filename); - let target_dir = base_dir.join(as_name); - - if let Err(e) = download_zip(url, &zip_path).await { - log!("Error downloading file: {}", e); - return; - } - - let zip_path_clone = zip_path.clone(); - let target_dir_clone = target_dir.clone(); - let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone); - - let successful = match extract_result { - Ok(()) => true, - Err(e) => { - log!("Error during ZIP extraction: {}", e); - false - } - }; - - if let Err(e) = tokio::fs::remove_file(&zip_path).await { - log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); - } else if successful { - log!("Downloaded and extracted ZIP file successfully."); - } -} diff --git a/src/util/logger.rs b/src/util/logger.rs deleted file mode 100755 index a5feb02..0000000 --- a/src/util/logger.rs +++ /dev/null @@ -1,432 +0,0 @@ -use std::{ - collections::BTreeMap, - fs::{self, OpenOptions}, - io::Write, - path::Path, - sync::{OnceLock, atomic::Ordering, mpsc}, - thread, - time::{SystemTime, UNIX_EPOCH}, -}; - -use ratatui::style::Color; -use ttp_core::{CommunicationValue, DataTypes, DataValue}; - -use crate::{ - APP_STATE, - gui::{elements::log_card::LogEntry, ui::UNIQUE}, - langu::language_manager, -}; - -static LOGGER: OnceLock> = OnceLock::new(); - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[allow(unused)] -pub enum PrintType { - Call, - Client, - Iota, - Omikron, - Omega, - General, - Command, -} -impl PrintType { - pub fn prefix_color(self) -> Color { - match self { - PrintType::Call => Color::Magenta, - PrintType::Client => Color::Green, - PrintType::Iota => Color::Yellow, - PrintType::Omikron => Color::Blue, - PrintType::Omega => Color::Cyan, - PrintType::General => Color::LightCyan, - PrintType::Command => Color::LightGreen, - } - } -} - -struct LogMessage { - timestamp_ms: u128, - prefix: String, - kind: PrintType, - is_error: bool, - translation_key: Option, - format_args: Vec, - message: Option, -} - -pub fn startup() { - let (tx, rx) = mpsc::channel::(); - LOGGER.set(tx).expect("Logger already initialized"); - - thread::spawn(move || { - let log_dir = Path::new("logs"); - fs::create_dir_all(log_dir).expect("Failed to create log directory"); - - let start_ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let path = log_dir.join(format!("log_{}.txt", start_ts)); - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(path) - .expect("Failed to open log file"); - - for msg in rx { - let resolved_message = if let Some(key) = msg.translation_key { - let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); - language_manager::format(&key, &args) - } else { - msg.message.unwrap_or_default() - }; - - let timestamp = format_timestamp_inline(msg.timestamp_ms); - let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); - - let prefix = if msg.prefix.is_empty() { - String::new() - } else { - format!("{} ", msg.prefix) - }; - - let _ = writeln!( - file, - "{} {}{}", - fixed_box(&msg.timestamp_ms.to_string(), 13), - prefix, - entry.message - ); - - let _ = writeln!(file, " {}", timestamp); - - let mut state = APP_STATE.lock().unwrap(); - state.push_log(entry.into()); - } - }); -} - -fn format_timestamp_inline(timestamp_ms: u128) -> String { - let secs = (timestamp_ms / 1000) as i64; - let hours = (secs / 3600) % 24; - let minutes = (secs / 60) % 60; - let seconds = secs % 60; - format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) -} - -fn fixed_box(content: &str, width: usize) -> String { - let s: String = content.chars().take(width).collect(); - let len = s.chars().count(); - if len < width { - format!("[{}{}]", " ".repeat(width - len), s) - } else { - s - } -} - -fn colorize(kind: PrintType, is_error: bool) -> Color { - if is_error { - return Color::Red; - } - - match kind { - PrintType::Call => Color::Magenta, - PrintType::Client => Color::Green, - PrintType::Iota => Color::Yellow, - PrintType::Omikron => Color::Blue, - PrintType::Omega => Color::Cyan, - PrintType::General => Color::LightCyan, - PrintType::Command => Color::LightGreen, - } -} - -pub fn log_internal_translated( - kind: PrintType, - prefix: String, - is_error: bool, - key: &str, - args: Vec, -) { - if let Some(tx) = LOGGER.get() { - UNIQUE.store(true, Ordering::Relaxed); - let _ = tx.send(LogMessage { - timestamp_ms: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - prefix, - kind, - is_error, - translation_key: Some(key.to_string()), - format_args: args, - message: None, - }); - } -} - -pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { - if let Some(tx) = LOGGER.get() { - UNIQUE.store(true, Ordering::Relaxed); - let _ = tx.send(LogMessage { - timestamp_ms: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(), - prefix, - kind, - is_error, - translation_key: None, - format_args: Vec::new(), - message: Some(message), - }); - } -} - -#[macro_export] -macro_rules! log_t { - ($key:expr) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - $key, - vec![] - ) - }; - - ($key:expr, $($arg:expr),+) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - $key, - vec![$($arg),+] - ) - }; -} - -#[macro_export] -macro_rules! log_t_err { - ($key:expr) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - true, - $key, - vec![] - ) - }; - - ($key:expr, $($arg:expr),+) => { - $crate::util::logger::log_internal_translated( - $crate::util::logger::PrintType::General, - "".to_string(), - true, - $key, - vec![$($arg.to_string()),+] - ) - }; -} - -/// Log a command message. -#[macro_export] -macro_rules! log_command { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::Command, - "".to_string(), - false, - format!($($arg)*) - ) - }; -} - -/// Log a general informational message. -#[macro_export] -macro_rules! log { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - "".to_string(), - false, - format!($($arg)*) - ) - }; -} - -/// Log an inbound message (`>`). -#[macro_export] -macro_rules! log_in { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - ">".to_string(), - false, - format!($($arg)*) - ) - }; -} - -/// Log an outbound message (`<`). -#[macro_export] -macro_rules! log_out { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - "<".to_string(), - false, - format!($($arg)*) - ) - }; -} - -/// Log an error message (`>>`). -#[macro_export] -macro_rules! log_err { - ($($arg:tt)*) => { - $crate::util::logger::log_internal( - $crate::util::logger::PrintType::General, - ">>".to_string(), - true, - format!($($arg)*) - ) - }; -} - -// ******** COMMUNICATION VALUES ******** -pub fn log_cv_internal( - prefix: &'static str, - cv: &CommunicationValue, - print_type: Option, -) { - let formatted = format_cv(cv); - - log_internal( - print_type.unwrap_or(PrintType::General), - prefix.to_string(), - false, - formatted, - ); -} - -pub fn format_cv(cv: &CommunicationValue) -> String { - let mut parts = Vec::new(); - - let sender = cv.get_sender(); - let receiver = cv.get_receiver(); - - if sender > 0 && receiver > 0 { - parts.push(format!("{} > {}", sender, receiver)); - } else if sender > 0 { - parts.push(format!("{}", sender)); - } else if receiver > 0 { - parts.push(format!("> {}", receiver)); - } - - let comm_type = cv.get_type().to_string(); - parts.push(format!("{}", comm_type)); - - let data: &BTreeMap = cv.get_data_container(); - - let formated_data = - format_data_container(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); - - parts.push(format!("{}", formated_data)); - - parts.join(": ") -} - -fn format_data_container(data: Vec<(DataTypes, DataValue)>) -> String { - let parts: Vec = data - .into_iter() - .map(|(key, value)| { - let key_str = key.to_string(); - - match value { - DataValue::Str(s) => format!("{}=\"{}\"", key_str, s), - - DataValue::Container(inner) => { - let inner_formatted = format_data_container(inner); - format!("{}={{ {} }}", key_str, inner_formatted) - } - - DataValue::Array(arr) => { - let arr_formatted = format_array(arr); - format!("{}=[{}]", key_str, arr_formatted) - } - - DataValue::Bool(b) => format!("{}={}", key_str, b), - - DataValue::BoolTrue => format!("{}=true", key_str), - DataValue::BoolFalse => format!("{}=false", key_str), - - DataValue::Number(num) => format!("{}={}", key_str, num), - - _ => "".to_string(), - } - }) - .collect(); - - parts.join(", ") -} - -fn format_array(arr: Vec) -> String { - let parts: Vec = arr - .into_iter() - .map(|value| match value { - DataValue::Str(s) => format!("\"{}\"", s), - - DataValue::Container(inner) => { - let inner_formatted = format_data_container(inner); - format!("{{ {} }}", inner_formatted) - } - - DataValue::Array(inner_arr) => { - let formatted = format_array(inner_arr); - format!("[{}]", formatted) - } - - DataValue::Bool(b) => b.to_string(), - - DataValue::BoolTrue => "true".to_string(), - DataValue::BoolFalse => "false".to_string(), - - DataValue::Number(num) => num.to_string(), - - _ => String::new(), - }) - .collect(); - - parts.join(", ") -} - -#[macro_export] -macro_rules! log_cv { - ($kind:expr, $cv:expr) => { - $crate::util::logger::log_cv_internal("", &$cv, Some($kind)) - }; - ($cv:expr) => { - $crate::util::logger::log_cv_internal("", &$cv, None) - }; -} - -#[macro_export] -macro_rules! log_cv_in { - ($kind:expr, $cv:expr) => { - $crate::util::logger::log_cv_internal("> ", &$cv, Some($kind)) - }; - ($cv:expr) => { - $crate::util::logger::log_cv_internal("> ", &$cv, None) - }; -} - -#[macro_export] -macro_rules! log_cv_out { - ($kind:expr, $cv:expr) => { - $crate::util::logger::log_cv_internal("< ", &$cv, Some($kind)) - }; - ($cv:expr) => { - $crate::util::logger::log_cv_internal("< ", &$cv, None) - }; -} diff --git a/src/util/mod.rs b/src/util/mod.rs deleted file mode 100644 index 47901f9..0000000 --- a/src/util/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod chat_files; -pub mod chats_util; -pub mod communities_util; -pub mod config_util; -pub mod crypto_helper; -pub mod crypto_util; -pub mod db; -pub mod file_util; -pub mod logger; diff --git a/static/web/404.html b/static/web/404.html deleted file mode 100644 index cb6efe1..0000000 --- a/static/web/404.html +++ /dev/null @@ -1,4 +0,0 @@ - - -

404

- From f2f7ff657f98cd44d20e6d6825ce52942d45a81c Mon Sep 17 00:00:00 2001 From: Alois Date: Thu, 2 Apr 2026 22:28:25 +0200 Subject: [PATCH 6/6] Sync --- .cargo_toml_backup | 76 + .github/workflows/build.yml | 23 + .gitignore | 2 + Cargo.lock | 5061 +++++++++++++++++ Cargo.toml | 56 + LICENSE | 16 + README.md | 1 + dockerfile | 20 + src/auth/auth_user.rs | 18 + src/auth/local_auth.rs | 18 + src/auth/mod.rs | 2 + src/communities/community.rs | 356 ++ src/communities/community_connection.rs | 396 ++ src/communities/community_manager.rs | 59 + src/communities/interactables/category.rs | 121 + src/communities/interactables/interactable.rs | 35 + src/communities/interactables/registry.rs | 75 + src/communities/interactables/text_chat.rs | 261 + src/communities/interactables/voice_chat.rs | 187 + src/communities/mod.rs | 13 + src/communities/perms/permission.rs | 27 + src/gui/app_state.rs | 201 + src/gui/elements/console_card.rs | 491 ++ src/gui/elements/elements.rs | 49 + src/gui/elements/graph_card.rs | 215 + src/gui/elements/log_card.rs | 483 ++ src/gui/input_handler.rs | 56 + src/gui/interaction_result.rs | 49 + src/gui/mod.rs | 20 + src/gui/screens/main_screen.rs | 242 + src/gui/screens/md_viewer.rs | 493 ++ src/gui/screens/screens.rs | 25 + src/gui/screens/terms_checker.rs | 351 ++ src/gui/screens/terms_updater.rs | 691 +++ src/gui/tui.rs | 0 src/gui/ui.rs | 167 + src/gui/util/borders.rs | 63 + src/langu/language_creator.rs | 80 + src/langu/language_manager.rs | 105 + src/langu/mod.rs | 2 + src/main.rs | 183 + src/omikron/mod.rs | 2 + src/omikron/omikron_connection.rs | 1159 ++++ src/omikron/ping_pong_task.rs | 38 + src/server/api.rs | 190 + src/server/mod.rs | 3 + src/server/server.rs | 169 + src/server/web_path_parser.rs | 77 + src/terms/buttons.rs | 175 + src/terms/consent_state.rs | 491 ++ src/terms/doc.rs | 73 + src/terms/focus.rs | 25 + src/terms/mod.rs | 5 + src/terms/terms_getter.rs | 105 + src/users/contact.rs | 61 + src/users/mod.rs | 4 + src/users/user_community_util.rs | 67 + src/users/user_manager.rs | 197 + src/users/user_profile.rs | 126 + src/util/chat_files.rs | 276 + src/util/chats_util.rs | 121 + src/util/communities_util.rs | 90 + src/util/config_util.rs | 60 + src/util/crypto_helper.rs | 133 + src/util/crypto_util.rs | 178 + src/util/db.rs | 185 + src/util/file_util.rs | 354 ++ src/util/logger.rs | 432 ++ src/util/mod.rs | 9 + static/web/404.html | 4 + 70 files changed, 15598 insertions(+) create mode 100644 .cargo_toml_backup create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dockerfile create mode 100644 src/auth/auth_user.rs create mode 100644 src/auth/local_auth.rs create mode 100644 src/auth/mod.rs create mode 100644 src/communities/community.rs create mode 100644 src/communities/community_connection.rs create mode 100644 src/communities/community_manager.rs create mode 100644 src/communities/interactables/category.rs create mode 100644 src/communities/interactables/interactable.rs create mode 100644 src/communities/interactables/registry.rs create mode 100644 src/communities/interactables/text_chat.rs create mode 100644 src/communities/interactables/voice_chat.rs create mode 100644 src/communities/mod.rs create mode 100644 src/communities/perms/permission.rs create mode 100755 src/gui/app_state.rs create mode 100644 src/gui/elements/console_card.rs create mode 100644 src/gui/elements/elements.rs create mode 100644 src/gui/elements/graph_card.rs create mode 100755 src/gui/elements/log_card.rs create mode 100644 src/gui/input_handler.rs create mode 100644 src/gui/interaction_result.rs create mode 100644 src/gui/mod.rs create mode 100644 src/gui/screens/main_screen.rs create mode 100644 src/gui/screens/md_viewer.rs create mode 100644 src/gui/screens/screens.rs create mode 100644 src/gui/screens/terms_checker.rs create mode 100644 src/gui/screens/terms_updater.rs create mode 100644 src/gui/tui.rs create mode 100644 src/gui/ui.rs create mode 100644 src/gui/util/borders.rs create mode 100644 src/langu/language_creator.rs create mode 100644 src/langu/language_manager.rs create mode 100644 src/langu/mod.rs create mode 100644 src/main.rs create mode 100644 src/omikron/mod.rs create mode 100755 src/omikron/omikron_connection.rs create mode 100644 src/omikron/ping_pong_task.rs create mode 100755 src/server/api.rs create mode 100644 src/server/mod.rs create mode 100644 src/server/server.rs create mode 100755 src/server/web_path_parser.rs create mode 100644 src/terms/buttons.rs create mode 100644 src/terms/consent_state.rs create mode 100644 src/terms/doc.rs create mode 100644 src/terms/focus.rs create mode 100644 src/terms/mod.rs create mode 100755 src/terms/terms_getter.rs create mode 100644 src/users/contact.rs create mode 100644 src/users/mod.rs create mode 100644 src/users/user_community_util.rs create mode 100644 src/users/user_manager.rs create mode 100644 src/users/user_profile.rs create mode 100644 src/util/chat_files.rs create mode 100644 src/util/chats_util.rs create mode 100644 src/util/communities_util.rs create mode 100644 src/util/config_util.rs create mode 100644 src/util/crypto_helper.rs create mode 100644 src/util/crypto_util.rs create mode 100644 src/util/db.rs create mode 100755 src/util/file_util.rs create mode 100755 src/util/logger.rs create mode 100644 src/util/mod.rs create mode 100644 static/web/404.html diff --git a/.cargo_toml_backup b/.cargo_toml_backup new file mode 100644 index 0000000..f7a9016 --- /dev/null +++ b/.cargo_toml_backup @@ -0,0 +1,76 @@ +[package] +name = "iota" +version = "0.1.0" +edition = "2024" + +[dependencies] +actix = "0.13.5" +actix-rt = "2.11.0" +actix-web = { version = "4.12.1", features = ["rustls-0_23"] } +actix-web-actors = "4.3.1" +aes-gcm = "*" +axum = { version = "0.8.8", features = ["ws", "http2"] } +base64 = "0.22.1" +bytes = "*" +cmake = "*" +color-eyre = "*" +crossterm = "*" +der = "*" +futures = "*" +futures-util = "*" +hex = "*" +hkdf = "*" +hmac = "*" +hyper-util = { version = "*" } +hyper = { version = "1.8.1", features = [ + "capi", + "client", + "full", + "http1", + "http2", + "nightly", + "server", +] } +http-body-util = "*" +json = "*" +native-tls = { version = "*", default-features = false } +once_cell = "1.21.3" +pkcs8 = { version = "*", features = ["alloc"] } +rand = "0.8" +rand_core = { version = "0.6", features = ["getrandom", "std"] } +reactive-rs = "*" +aws-lc-rs = "*" +reqwest = "0.12.23" +rustls = { version = "*", features = ["aws-lc-rs"] } +serde = { version = "1.0.219", features = ["derive"] } +sha2 = "*" +sys-info = "*" +sysinfo = "0.30" +tokio = { version = "1.48.0", features = ["full"] } +tokio-util = { version = "*", features = ["full"] } +tokio-tungstenite = { version = "*", features = ["native-tls"] } +tower = "*" +tungstenite = "*" +uuid = { version = "*", features = ["v4"] } +walkdir = "2.5.0" +warp = "*" +x448 = { version = "*" } +x509 = "*" +tokio-rustls = "0.26.4" +rustls-pemfile = "2.2.0" +async-trait = "0.1.89" +sha1 = "0.10.6" +async-tungstenite = "0.32.0" +zip = "6.0.0" +tui-textarea = "0.7.0" +tui-input = "0.14.0" +pnet = "0.35.0" +dashmap = "6.1.0" +strum = "0.27.2" +strum_macros = "0.27.2" +ratatui = "0.30.0" +ratatui_input = "0.1.3" +open = "5.3.3" +chrono = "0.4.43" +axum-server = { version = "0.8.0", features = ["tls-rustls"] } +serde_json = "1.0.149" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4265c56 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build & Publish Docker Image + +on: + workflow_dispatch: + +jobs: + build: + name: Build & Publish Docker Image + runs-on: ubuntu-latest + steps: + - name: Set up repository + uses: actions/checkout@v4 + + - name: Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWD }} + + - name: Build & Push + run: | + docker build -t tensamin/iota:latest . + docker push tensamin/iota:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0000aa1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +logs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cd56dfb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5061 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" +dependencies = [ + "actix-rt", + "bitflags 2.11.0", + "bytes", + "crossbeam-channel", + "futures-core", + "futures-sink", + "futures-task", + "futures-util", + "log", + "once_cell", + "parking_lot", + "pin-project-lite", + "smallvec", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "base64", + "bitflags 2.11.0", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash 0.1.5", + "futures-core", + "h2 0.3.27", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "actix-router" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6176099de3f58fbddac916a7f8c6db297e021d706e7a6b99947785fee14abe9f" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "pin-project-lite", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash 0.1.5", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.6.3", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-actors" +version = "4.3.1+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c5300b38fd004fe7d2a964f9a90813fdbe8a81fed500587e78b1b71c6f980" +dependencies = [ + "actix", + "actix-codec", + "actix-http", + "actix-web", + "bytes", + "bytestring", + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed448-goldilocks" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" +dependencies = [ + "fiat-crypto", + "hex", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http 1.4.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.4.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "httlib-huffman" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9fcbcc408c5526c3ab80d534e5c86e7967c1fb7aa0a8c76abd1edc27deb877" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "iota" +version = "0.1.0" +dependencies = [ + "actix-web", + "actix-web-actors", + "aes-gcm", + "async-trait", + "base64", + "chrono", + "crossterm", + "dashmap", + "futures", + "futures-util", + "hex", + "hkdf", + "hyper", + "hyper-util", + "json", + "lazy_static", + "once_cell", + "open", + "pnet", + "rand 0.8.5", + "rand_core 0.6.4", + "ratatui", + "reqwest", + "rusqlite", + "rustls", + "rustls-pemfile", + "serde_json", + "sha2", + "strum 0.27.2", + "strum_macros 0.27.2", + "sysinfo", + "tokio", + "tokio-tungstenite", + "ttp-core", + "ttp-native", + "tungstenite", + "uuid", + "walkdir", + "warp", + "x448", + "zip", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "octets" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8311fa8ab7a57759b4ff1f851a3048d9ef0effaa0130726426b742d26d8a88e7" + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.117", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.11.0", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum 0.27.2", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum 0.27.2", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "rustls-pki-types", + "time", + "x509-parser 0.18.1", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.11.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttp-core" +version = "0.1.0" +source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" +dependencies = [ + "base64", + "byteorder", + "rand 0.8.5", + "strum 0.28.0", + "strum_macros 0.28.0", +] + +[[package]] +name = "ttp-native" +version = "0.1.0" +source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167" +dependencies = [ + "quinn", + "rustls", + "rustls-native-certs", + "thiserror 2.0.18", + "tokio", + "ttp-core", + "wtransport", +] + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http 1.4.0", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.18", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d06d9202adc1f15d709c4f4a2069be5428aa912cc025d6f268ac441ab066b0" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http 1.4.0", + "http-body", + "http-body-util", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wtransport" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e56b611f195638f3790e4e5a41e9d777643a6c324ae4ffd1f0f53f2738e7678c" +dependencies = [ + "bytes", + "pem", + "quinn", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha2", + "socket2 0.5.10", + "thiserror 2.0.18", + "time", + "tokio", + "tracing", + "url", + "wtransport-proto", + "x509-parser 0.17.0", +] + +[[package]] +name = "wtransport-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1627c5b59450278e9771aab35275d72bfa2788128c197c5af1e4a820c8737ef4" +dependencies = [ + "httlib-huffman", + "octets", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "x448" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" +dependencies = [ + "ed448-goldilocks", + "hex", + "rand_core 0.5.1", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8767b56 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = "iota" +version = "0.1.0" +edition = "2024" + +[dependencies] +ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" } +ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" } + +actix-web = { version = "4", features = ["rustls-0_23"] } +actix-web-actors = "4" +aes-gcm = "0.10.3" +base64 = "0.22.1" +crossterm = "*" +futures = "*" +futures-util = "*" +hex = "*" +hkdf = "0.12.4" +hyper-util = { version = "*" } +hyper = { version = "1.8.1", features = [ + "capi", + "client", + "full", + "http1", + "http2", + "nightly", + "server", +] } +json = "*" +once_cell = "1.21.3" +rand = "0.8" +rand_core = { version = "0.6", features = ["getrandom", "std"] } +reqwest = "0.13.2" +rustls = { version = "0.23.37", features = ["aws-lc-rs"] } +sha2 = "0.10.9" +sysinfo = "0.38.3" +tokio = { version = "1.50.0", features = ["full"] } +tokio-tungstenite = { version = "*", features = ["native-tls"] } +tungstenite = "*" +uuid = { version = "*", features = ["v4"] } +walkdir = "2.5.0" +warp = "*" +x448 = { version = "*" } +rustls-pemfile = "2.2.0" +async-trait = "0.1.89" +zip = "6.0.0" +pnet = "0.35.0" +dashmap = "6.1.0" +strum = "0.27.2" +strum_macros = "0.27.2" +ratatui = "0.30.0" +open = "5.3.3" +chrono = "0.4.43" +serde_json = "1.0.149" +rusqlite = "0.39.0" +lazy_static = "1.5.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d218eff --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright (c) [2025] [Methanium] +All rights reserved. + +This software is protected by copyright. Copying, editing, +distributing, publicly performing, or any other use of this software +or its components, in source or binary form, is strictly prohibited without the express +written permission of the copyright holder. + +FUTURE LICENSE ACCEPTANCE: +It is the copyright holder's intention to release this software in the future +under a license yet to be defined, which will, among other things, +allow private, non-commercial use. This statement does not constitute +a current license grant and does not alter the above +prohibition on use, copying, or modification. Until the formal +publication of such a future license, all rights remain +reserved. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7bc9c8 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️ diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..ee00add --- /dev/null +++ b/dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM rust:latest AS builder + +WORKDIR /app +COPY . . + +RUN cargo build --release + +# Runtime stage +FROM debian:sid + +WORKDIR /app + +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/target/release/iota . + +EXPOSE 1984 + +CMD ["./iota"] \ No newline at end of file diff --git a/src/auth/auth_user.rs b/src/auth/auth_user.rs new file mode 100644 index 0000000..8f763b6 --- /dev/null +++ b/src/auth/auth_user.rs @@ -0,0 +1,18 @@ +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct AuthUser { + pub id: i64, + pub username: String, + pub password_hash: String, + pub public_key: String, +} + +impl AuthUser { + pub fn new(id: i64, username: String, password_hash: String, public_key: String) -> Self { + AuthUser { + id, + username, + password_hash, + public_key, + } + } +} diff --git a/src/auth/local_auth.rs b/src/auth/local_auth.rs new file mode 100644 index 0000000..112ee2b --- /dev/null +++ b/src/auth/local_auth.rs @@ -0,0 +1,18 @@ +use json::JsonValue; + +use crate::util::file_util::load_file; +// NOT USED AT MOMENT +pub fn is_private_key_valid(user_id: &i64, key_hash: &str) -> bool { + let file_contents = load_file("", "users.json"); + + let users = json::parse(&file_contents).unwrap(); + if let JsonValue::Array(users_array) = users { + for user in users_array { + if user["uuid"] == user_id.to_string() && user["private_key_hash"] == key_hash { + return true; + } + } + } + + false +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..73d6442 --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod auth_user; +pub mod local_auth; diff --git a/src/communities/community.rs b/src/communities/community.rs new file mode 100644 index 0000000..835c88a --- /dev/null +++ b/src/communities/community.rs @@ -0,0 +1,356 @@ +use crate::communities::interactables::category::Category; +use crate::communities::interactables::registry; +use crate::communities::perms::permission::Permission; +use crate::communities::{ + community_connection::CommunityConnection, interactables::interactable::Interactable, +}; +use crate::util::file_util; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use json::JsonValue; +use json::number::Number; +use json::object::Object; +use rand::RngCore; +use rand_core::OsRng; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use x448::{PublicKey, Secret}; +/// Permissions +// uuid -> interactable/path/like/this/interactable_name:permission +// -> role + +/// Roles +// rolename -> interactable/path/like/this/interactable_name:permission +// -> other/path/like/this/interactable_name:permission + +pub struct Community { + name: String, + owner_id: Arc>, + members: Vec, + permissions: HashMap>, + roles: HashMap>, + private_key: Secret, + public_key: PublicKey, + pub interactables: Arc>>>>, + pub connections: Arc>>>>, +} + +impl Community { + pub fn new() -> Self { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + Community { + name: String::new(), + owner_id: Arc::new(RwLock::new(0)), + members: Vec::new(), + permissions: HashMap::new(), + roles: HashMap::new(), + private_key, + public_key, + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + } + } + pub async fn create(name: String, owner_id: i64) -> Self { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + let c = Community { + name, + owner_id: Arc::new(RwLock::new(owner_id)), + members: Vec::new(), + permissions: HashMap::new(), + roles: HashMap::new(), + private_key, + public_key, + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + }; + c.save().await; + c + } + + pub async fn to_json(&self) -> JsonValue { + let mut json = JsonValue::new_object(); + json["name"] = self.name.clone().into(); + json["owner_id"] = (*self.owner_id.read().await as i64).into(); + json["members"] = self + .members + .clone() + .iter() + .map(|f| f.to_string()) + .collect::>() + .into(); + json["private_key"] = self.private_key.as_bytes().to_vec().into(); + json["public_key"] = self.public_key.as_bytes().to_vec().into(); + json["connections"] = self.connections.read().await.clone().len().into(); + json + } + + pub async fn frontend(&self) -> JsonValue { + let mut json = JsonValue::new_object(); + json["name"] = self.name.clone().into(); + json["owner_id"] = (*self.owner_id.read().await as i64).into(); + json["members"] = self + .members + .clone() + .iter() + .map(|f| f.to_string()) + .collect::>() + .into(); + json["public_key"] = self.public_key.as_bytes().to_vec().into(); + json["connections"] = self.connections.read().await.clone().len().into(); + json + } + + pub fn add_member(&mut self, member_id: i64) { + self.members.push(member_id); + } + + pub fn remove_member(&mut self, member_id: i64) { + self.members.retain(|id| *id != member_id); + } + pub fn get_name(&self) -> &str { + &self.name + } + pub async fn get_owner_id(&self) -> i64 { + *self.owner_id.read().await + } + pub fn get_members(&self) -> Vec { + self.members.clone() + } + pub fn get_private_key(&self) -> Secret { + Secret::from_bytes(self.private_key.as_bytes()).unwrap() + } + pub fn get_public_key(&self) -> &PublicKey { + &self.public_key + } + pub async fn set_owner(self: Arc, owner_id: i64) { + *self.owner_id.write().await = owner_id; + } + pub async fn add_connection(self: &Arc, other: Arc) { + let mut vec = self + .connections + .read() + .await + .get(&other.get_user_id().await) + .cloned() + .unwrap_or_default(); + vec.push(other.clone()); + self.connections + .write() + .await + .insert(other.get_user_id().await, vec); + } + pub async fn remove_connection(self: &Arc, other: Arc) { + let mut vec = self + .connections + .read() + .await + .get(&other.get_user_id().await) + .cloned() + .unwrap_or_default(); + vec.retain(|conn| !Arc::ptr_eq(conn, &other)); + self.connections + .write() + .await + .insert(other.get_user_id().await, vec); + } + pub async fn get_connections(&self) -> HashMap>> { + self.connections.read().await.clone() + } + pub async fn get_connections_for_user(&self, user_id: i64) -> Vec> { + self.connections + .read() + .await + .get(&user_id) + .cloned() + .unwrap_or_default() + } + pub async fn get_interactables( + &self, + _user_id: i64, + ) -> Vec>> { + self.interactables.read().await.clone() + } + pub async fn add_interactable(self: &mut Arc, interactable: Arc>) { + self.interactables.write().await.push(interactable); + } + pub async fn remove_interactable( + self: &mut Arc, + interactable: Arc>, + ) { + self.interactables + .write() + .await + .retain(|i| !Arc::ptr_eq(i, &interactable)); + } + pub async fn broadcast_message(&self, message: &CommunicationValue) { + for (_, conns) in self.connections.read().await.clone().iter() { + for user in conns.iter() { + user.send_message(message).await; + } + } + } + pub async fn run_function( + self: &mut Arc, + _user_id: i64, + name: &str, + path: &str, + _function: &str, + cv: &CommunicationValue, + ) -> CommunicationValue { + if path.is_empty() { + let target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + return CommunicationValue::new(CommunicationType::error); + } else { + // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) + return interactable.run_function(cv.clone()).await; + } + } + } + } else { + let target_interactables = &self.interactables.read().await.clone(); + for interactable in target_interactables.iter() { + if interactable.get_name() == name { + if interactable.get_codec() == "category" { + let category: &Category = + interactable.as_any().downcast_ref::().unwrap(); + // cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161) + return category + .get_child(path.to_string(), name.to_string()) + .unwrap() + .run_function(cv.clone()) + .await; + } else { + return CommunicationValue::new(CommunicationType::error); + } + } + } + } + CommunicationValue::new(CommunicationType::add_conversation) + } + + pub async fn save(&self) { + let mut json = Object::new(); + json.insert("name", JsonValue::String(self.name.clone())); + json.insert( + "owner_id", + JsonValue::Number(Number::from(*self.owner_id.read().await as i64)), + ); + + json.insert( + "private_key", + JsonValue::String(STANDARD.encode(&self.private_key.as_bytes())), + ); + json.insert( + "public_key", + JsonValue::String(STANDARD.encode(&self.public_key.as_bytes())), + ); + + file_util::save_file( + &format!("communities/{}/", self.name), + "config.json", + &json.dump(), + ); + + let mut user_data = Object::new(); + for user in self.members.iter() { + let mut data = JsonValue::new_object(); + + let mut permissions = JsonValue::new_array(); + for perm in self.permissions.get(user).unwrap() { + if let Ok(_) = permissions.push(perm.to_string()) {} + } + + if let Ok(_) = data.insert("permissions", permissions) {} + user_data.insert(&user.to_string(), data); + } + file_util::save_file( + &format!("communities/{}/", self.name), + "users.json", + &user_data.dump(), + ); + + for interactable in self.interactables.read().await.clone().iter() { + registry::save(interactable).await; + } + } +} +pub async fn load(name: &String) -> Option> { + let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json"); + let json_content = json::parse(&file_contents).unwrap(); + + let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json"); + let user_json: JsonValue = json::parse(&user_data).unwrap(); + let mut users = Vec::new(); + let mut permissions: HashMap> = HashMap::new(); + + for user in user_json.entries() { + let (id, json): (&str, &JsonValue) = user; + let perms_j = &json["permissions"]; + let perms = Vec::new(); + for _ in perms_j.entries() { + // let perm_j = i.as_str().unwrap(); + // perms.push(perm_j.to_string()); + } + + let user_id: i64 = id.parse().unwrap_or(0); + + users.push(user_id); + permissions.insert(user_id, perms); + } + + let role_data = file_util::load_file(&format!("communities/{}/", name), "roles.json"); + let roles: HashMap> = HashMap::new(); + if let Ok(_) = json::parse(&role_data) { + // Fill roles + } else { + return None; + }; + + let community = Community { + name: json_content["name"].as_str().unwrap().to_string(), + owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))), + members: users, + roles, + permissions, + private_key: Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + public_key: PublicKey::from( + &Secret::from_bytes( + &STANDARD + .decode(json_content["private_key"].as_str().unwrap()) + .unwrap(), + ) + .unwrap(), + ), + interactables: Arc::new(RwLock::new(Vec::new())), + connections: Arc::new(RwLock::new(HashMap::new())), + }; + let mut comarc = Arc::new(community); + + let interactable_files: Vec = + file_util::get_children(&format!("communities/{}/interactables/", name)); + for file in interactable_files { + if file.contains(".json") { + let name = file.split('.').next().unwrap().to_string(); + let interactable: Box = + registry::load(comarc.clone(), String::new(), name).await; + comarc.add_interactable(Arc::new(interactable)).await; + } + } + Some(comarc) +} diff --git a/src/communities/community_connection.rs b/src/communities/community_connection.rs new file mode 100644 index 0000000..62df251 --- /dev/null +++ b/src/communities/community_connection.rs @@ -0,0 +1,396 @@ +use crate::auth::auth_user::AuthUser; +use crate::communities::community::Community; +use crate::communities::interactables::interactable::Interactable; +use crate::users::user_manager::get_user; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use futures::SinkExt; +use futures::stream::SplitSink; +use futures::stream::SplitStream; +use hkdf::Hkdf; +use hyper::upgrade::Upgraded; +use hyper_util::rt::TokioIo; +use json::JsonValue; +use json::number::Number; +use rand::{Rng, distributions::Alphanumeric}; +use sha2::Sha256; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio_tungstenite::WebSocketStream; +use tungstenite::Message; +use tungstenite::Utf8Bytes; +use uuid::Uuid; +use x448::PublicKey; +pub struct CommunityConnection { + pub sender: Arc>, Message>>>, + pub receiver: Arc>>>>, + pub user_id: Arc>, + pub community: Arc>>>, + identified: Arc>, + challenged: Arc>, + challenge: Arc>, + auth: Arc>>, + pub ping: Arc>, +} +impl CommunityConnection { + pub fn new( + sender: SplitSink>, Message>, + receiver: SplitStream>>, + community: Arc, + ) -> Arc { + Arc::new(Self { + sender: Arc::new(RwLock::new(sender)), + receiver: Arc::new(RwLock::new(receiver)), + user_id: Arc::new(RwLock::new(0)), + community: Arc::new(RwLock::new(Some(community))), + identified: Arc::new(RwLock::new(false)), + challenged: Arc::new(RwLock::new(false)), + challenge: Arc::new(RwLock::new(String::new())), + auth: Arc::new(RwLock::new(None)), + ping: Arc::new(RwLock::new(-1)), + }) + } + pub async fn send_message(&self, message: &CommunicationValue) { + let mut sender = self.sender.write().await; // Access the SplitSink + let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string())); + sender.send(message_text).await.unwrap(); // Send the message via the SplitSink + } + pub async fn get_community(&self) -> Option> { + self.community.read().await.clone() + } + pub async fn get_user_id(&self) -> i64 { + *self.user_id.read().await + } + pub async fn is_identified(&self) -> bool { + *self.identified.read().await && *self.challenged.read().await + } + + pub async fn handle_message(self: Arc, message: String) { + let mut cv = CommunicationValue::from_json(&message); + let user_id = self.get_user_id().await; + cv = cv.with_sender(user_id); + + if cv.is_type(CommunicationType::identification) && !self.is_identified().await { + self.handle_identification(cv).await; + return; + } + + if cv.is_type(CommunicationType::challenge_response) && !self.is_identified().await { + self.handle_challenge_response(cv).await; + return; + } + + if !self.is_identified().await { + return; + } + + if cv.is_type(CommunicationType::ping) { + self.handle_ping(cv).await; + return; + } + + if cv.is_type(CommunicationType::client_changed) { + //self.handle_client_changed(cv).await; + return; + } + + if cv.is_type(CommunicationType::function) { + self.handle_function(cv).await; + return; + } + } + async fn handle_function(&self, cv: CommunicationValue) { + let name = cv.get_data(DataTypes::name).unwrap().as_str().unwrap(); + let path = cv.get_data(DataTypes::path).unwrap().as_str().unwrap(); + let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); + + let result = self + .get_community() + .await + .unwrap() + .run_function(self.get_user_id().await, name, path, function, &cv) + .await; + + self.send_message(&result).await; + } + async fn handle_identification(&self, cv: CommunicationValue) { + let user_id = cv + .get_data(DataTypes::user_id) + .unwrap_or(&JsonValue::Number(Number::from(0))) + .as_i64() + .unwrap_or(0); + + let Some(user) = get_user(user_id) else { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + }; + + { + let mut auth_guard = self.auth.write().await; + //*auth_guard = Some(user.clone()); + + let mut user_id_guard = self.user_id.write().await; + *user_id_guard = user_id; + + let mut identified_guard = self.identified.write().await; + *identified_guard = true; + } + + let challenge_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + { + let mut challenge_guard = self.challenge.write().await; + *challenge_guard = challenge_str.clone(); + } + + let user_public_key_bytes = match STANDARD.decode(&user.public_key) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let user_pub_key: PublicKey = match PublicKey::from_bytes(&user_public_key_bytes) { + Some(key) => key, + __ => { + self.send_error_response(&cv.get_id(), CommunicationType::error_invalid_user_id) + .await; + return; + } + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + let community_public_key = community.get_public_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let nonce_bytes: [u8; 12] = rand::random(); + let nonce = Nonce::from_slice(&nonce_bytes); + + let encrypted_challenge = match cipher.encrypt(nonce, challenge_str.as_bytes()) { + Ok(data) => data, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let mut encrypted_out = nonce_bytes.to_vec(); + encrypted_out.extend(encrypted_challenge); + + let response = CommunicationValue::new(CommunicationType::challenge) + .add_data_str( + DataTypes::public_key, + STANDARD.encode(community_public_key.as_bytes()), + ) + .add_data_str(DataTypes::challenge, STANDARD.encode(&encrypted_out)) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + async fn handle_challenge_response(self: Arc, cv: CommunicationValue) { + let client_challenge_response_b64 = match cv.get_data(DataTypes::challenge) { + Some(data) => data.to_string(), + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let challenge_response_bytes = match STANDARD.decode(&client_challenge_response_b64) { + Ok(bytes) => bytes, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + if challenge_response_bytes.len() < 12 { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + + let Some(user) = self.auth.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_bytes) = STANDARD.decode(&user.public_key).ok() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(user_pub_key) = PublicKey::from_bytes(&user_pub_bytes) else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + + let community_private_key = community.get_private_key(); + + let shared_secret = match community_private_key.to_diffie_hellman(&user_pub_key) { + Some(secret) => secret, + _ => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let aes_key = { + use hkdf::Hkdf; + use sha2::Sha256; + + let hk = Hkdf::::new(None, shared_secret.as_bytes()); + let mut key_bytes = [0u8; 32]; + hk.expand(b"challenge", &mut key_bytes) + .expect("HKDF failure"); + key_bytes + }; + + let (nonce_bytes, ciphertext) = challenge_response_bytes.split_at(12); + let nonce = Nonce::from_slice(nonce_bytes); + let cipher = Aes256Gcm::new_from_slice(&aes_key).expect("AES init failed"); + + let decrypted_bytes = match cipher.decrypt(nonce, ciphertext) { + Ok(pt) => pt, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let client_response = match String::from_utf8(decrypted_bytes) { + Ok(str) => str, + Err(_) => { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + }; + + let expected_challenge = self.challenge.read().await.clone(); + + if client_response != expected_challenge { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + self.close().await; + return; + } + + { + let mut authenticated_guard = self.challenged.write().await; + *authenticated_guard = true; + } + + let Some(community) = self.community.read().await.clone() else { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + }; + let arc = Arc::new(community); + + let user_id = self.get_user_id().await; + if user_id == 0 { + self.send_error_response(&cv.get_id(), CommunicationType::error) + .await; + return; + } + + arc.add_connection(self.clone()).await; + + let response = CommunicationValue::new(CommunicationType::identification_response) + .add_data(DataTypes::interactables, { + let a: Vec>> = arc.get_interactables(user_id).await; + let mut c: JsonValue = JsonValue::new_object(); + for b in a { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(b.get_codec()); + subject["data"] = b.get_data(); + c[b.get_name()] = subject; + } + c + }) + .with_id(cv.get_id()); + + self.send_message(&response).await; + } + + async fn send_error_response(&self, message_id: &Uuid, error_type: CommunicationType) { + let error = CommunicationValue::new(error_type).with_id(*message_id); + self.send_message(&error).await; + } + pub async fn close(&self) { + let mut sender = self.sender.write().await; + let _ = sender.close().await; + } + pub async fn handle_close(self: Arc) { + if self.is_identified().await { + if self.get_user_id().await != 0 { + self.community + .read() + .await + .as_ref() + .unwrap() + .remove_connection(self.clone()) + .await; + } + } + } + + async fn handle_ping(&self, cv: CommunicationValue) { + if let Some(last_ping) = cv.get_data(DataTypes::last_ping) { + if let Ok(ping_val) = last_ping.to_string().parse::() { + let mut ping_guard = self.ping.write().await; + *ping_guard = ping_val; + } + } + + let response = CommunicationValue::new(CommunicationType::pong).with_id(cv.get_id()); + + self.send_message(&response).await; + } +} diff --git a/src/communities/community_manager.rs b/src/communities/community_manager.rs new file mode 100644 index 0000000..8369f5a --- /dev/null +++ b/src/communities/community_manager.rs @@ -0,0 +1,59 @@ +use crate::communities::community::{self, Community}; +use crate::log; +use crate::util::file_util; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub static COMMUNITY_REGISTRY: Lazy>>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +pub async fn add_community(community: Arc) { + COMMUNITY_REGISTRY + .lock() + .await + .insert(community.get_name().to_string(), community); +} +pub async fn remove_community(name: &str) { + COMMUNITY_REGISTRY.lock().await.remove(name); +} +pub async fn clear() { + COMMUNITY_REGISTRY.lock().await.clear(); +} +pub async fn get_community(name: &str) -> Option> { + if let Some(c) = COMMUNITY_REGISTRY.lock().await.get(name) { + Some(c.clone()) + } else { + None + } +} + +pub async fn load_communities() { + let community_names = file_util::get_children("communities"); + for name in community_names { + if let Some(community) = community::load(&name).await { + add_community(community).await; + } else { + log!("failed to load the {} community", &name); + } + } +} +pub async fn save_communities() { + for community in COMMUNITY_REGISTRY.lock().await.values() { + community.save().await; + } +} + +pub async fn get_communities() -> Vec> { + COMMUNITY_REGISTRY.lock().await.values().cloned().collect() +} + +pub async fn rename_community(old_name: &str, new_name: &str) { + if let Some(community) = COMMUNITY_REGISTRY.lock().await.remove(old_name) { + COMMUNITY_REGISTRY + .lock() + .await + .insert(new_name.to_string(), community); + } +} diff --git a/src/communities/interactables/category.rs b/src/communities/interactables/category.rs new file mode 100644 index 0000000..6b6415f --- /dev/null +++ b/src/communities/interactables/category.rs @@ -0,0 +1,121 @@ +use crate::communities::{community::Community, interactables::interactable::Interactable}; +use async_trait::async_trait; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; +use ttp_core::CommunicationValue; +use uuid::Uuid; + +pub struct Category { + id: Uuid, + name: String, + path: String, + community: Arc, + children: Vec>>, +} +impl Category { + pub fn new() -> Category { + Category { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + children: Vec::new(), + } + } + pub fn get_child(&self, path: String, name: String) -> Option>> { + if path.is_empty() { + self.children + .iter() + .find(|child| child.get_name() == &name) + .cloned() + } else { + let sub_module = path.split("/").next().unwrap(); + let next = self + .children + .iter() + .find(|child| child.get_name() == sub_module) + .unwrap(); + if next.get_codec() == "category" { + let next_cat = next.as_any().downcast_ref::().unwrap(); + next_cat.get_child(path, name) + } else { + Some(next.clone()) + } + } + } + pub fn get_children(&self) -> Vec>> { + self.children.iter().map(|child| child.clone()).collect() + } +} + +#[async_trait] +impl Interactable for Category { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "category".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + for child in &self.children { + let mut subject = JsonValue::new_object(); + subject["codec"] = JsonValue::String(child.get_codec()); + subject["data"] = child.get_data(); + v[child.get_name()] = subject; + } + v + } + async fn run_function(&self, _cv: CommunicationValue) -> CommunicationValue { + CommunicationValue::new(CommunicationType::error) + } + fn to_json(&self) -> JsonValue { + let mut v = JsonValue::new_object(); + v["children"] = JsonValue::new_array(); + for child in &self.children { + let _ = v["children"].push(child.to_json()); + } + v + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/interactable.rs b/src/communities/interactables/interactable.rs new file mode 100644 index 0000000..0521de6 --- /dev/null +++ b/src/communities/interactables/interactable.rs @@ -0,0 +1,35 @@ +use crate::communities::community::Community; +use async_trait::async_trait; +use json::JsonValue; +use std::any::Any; +use std::sync::Arc; +use ttp_core::CommunicationValue; +use uuid::Uuid; + +pub type InteractableFactory = fn() -> Box; + +#[async_trait] +pub trait Interactable: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn get_codec(&self) -> String; + fn get_name(&self) -> &String; + fn get_path(&self) -> &String; + fn get_total_path(&self) -> String; + fn set_name(&mut self, name: String); + fn set_path(&mut self, path: String); + fn get_community(&self) -> &Arc; + fn set_community(&mut self, community: Arc); + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue; + fn get_data(&self) -> JsonValue; + fn get_id(&self) -> &Uuid; + fn to_json(&self) -> JsonValue; + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + json: &JsonValue, + ); +} diff --git a/src/communities/interactables/registry.rs b/src/communities/interactables/registry.rs new file mode 100644 index 0000000..2f57005 --- /dev/null +++ b/src/communities/interactables/registry.rs @@ -0,0 +1,75 @@ +use crate::communities::community::Community; +use crate::communities::interactables::category::Category; +use crate::communities::interactables::interactable::{Interactable, InteractableFactory}; +use crate::communities::interactables::text_chat::TextChat; +use crate::communities::interactables::voice_chat::VoiceChat; +use crate::util::file_util; +use json::JsonValue; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +pub static INTERACTABLE_REGISTRY: Lazy>>> = + Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); +pub async fn load_interactables() { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(TextChat::new().get_codec(), || { + Box::new(TextChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(VoiceChat::new().get_codec(), || { + Box::new(VoiceChat::new()) as Box + }); + INTERACTABLE_REGISTRY + .lock() + .await + .insert(Category::new().get_codec(), || { + Box::new(Category::new()) as Box + }); +} +pub async fn register_interactable(name: String, interactable: InteractableFactory) { + INTERACTABLE_REGISTRY + .lock() + .await + .insert(name.to_string(), interactable) + .unwrap(); +} +pub async fn get_interactable(name: &str) -> Box { + INTERACTABLE_REGISTRY.lock().await.get(name).unwrap()() +} +pub async fn save(interactable: &Arc>) { + let mut json_object: JsonValue = interactable.to_json().clone(); + json_object["codec"] = JsonValue::String(interactable.get_codec()); + json_object["id"] = JsonValue::String(interactable.get_id().to_string()); + file_util::save_file( + &format!( + "communities/{}/interactables/{}", + interactable.get_community().get_name(), + interactable.get_path() + ), + &format!("{}.json", interactable.get_name()), + &json_object.to_string(), + ); +} +pub async fn load( + c: Arc, + path: String, + name: String, +) -> Box { + let s = file_util::load_file( + &format!("communities/{}/interactables/{}", c.get_name(), path), + &format!("{}.json", name), + ); + let json_object: JsonValue = json::parse(&s).unwrap(); + let codec: String = json_object["codec"].as_str().unwrap().to_string(); + let id: String = json_object["id"].as_str().unwrap().to_string(); + let mut interactable = get_interactable(&codec).await; + interactable.load(c, Uuid::parse_str(&id).unwrap(), path, name, &json_object); + interactable +} diff --git a/src/communities/interactables/text_chat.rs b/src/communities/interactables/text_chat.rs new file mode 100644 index 0000000..88e1545 --- /dev/null +++ b/src/communities/interactables/text_chat.rs @@ -0,0 +1,261 @@ +use crate::{ + communities::{ + community::Community, community_connection::CommunityConnection, + interactables::interactable::Interactable, + }, + log, + util::file_util::{get_children, load_file, save_file}, +}; +use async_trait::async_trait; +use json::{JsonValue, array, object}; +use std::fs; +use std::sync::Arc; +use std::{any::Any, collections::HashMap}; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes}; +use uuid::Uuid; +pub struct TextChat { + id: Uuid, + name: String, + path: String, + community: Arc, +} +impl TextChat { + pub fn new() -> TextChat { + TextChat { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + } + } + pub fn add_message(&self, send_time: u128, sender: i64, message: &str) { + let user_dir = &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ); + + if let Err(e) = fs::create_dir_all(user_dir) { + log!("Failed to create chat directory: {}", e); + return; + } + + let mut chunk_index = 0; + let mut message_chunk = array![]; + + // find latest chunk not full (max 800 msgs) + loop { + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file(&user_dir, &file_name); + + if !file_content.is_empty() { + if let Ok(current_chunk) = json::parse(&file_content) { + if current_chunk.is_array() && current_chunk.len() < 800 { + message_chunk = current_chunk; + break; + } + } else { + log!("Failed to parse existing JSON file: {}", file_name); + } + } else { + break; + } + + chunk_index += 1; + if chunk_index > 1000 { + log!("Too many message chunks. Aborting add."); + return; + } + } + + let json_obj = object! { + "timestamp" => send_time as i64, + "content" => message, + "sender" => sender.to_string(), + }; + + if let Err(e) = message_chunk.push(json_obj) { + log!("Failed to push new message into JSON array: {}", e); + return; + } + + let file_name = format!("msgs_{}.json", chunk_index); + log!("Saving message to {}/{}", user_dir, file_name); + save_file(&user_dir, &file_name, &message_chunk.dump()); + } + pub fn get_messages(&self, loaded_messages: i64, amount: i64) -> JsonValue { + let mut messages = array![]; + + let mut latest_chunk_index: i32 = -1; + let files = get_children(&format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + )); + + for entry in files { + if let Some(num) = { + entry + .strip_prefix("msgs_") + .and_then(|s| s.strip_suffix(".json")) + } { + if let Ok(index) = num.parse::() { + if index > latest_chunk_index { + latest_chunk_index = index; + } + } + } + } + + if latest_chunk_index == -1 { + return messages; + } + + let mut to_skip = loaded_messages; + let mut needed = amount; + + for chunk_index in (0..=latest_chunk_index).rev() { + if needed == 0 { + break; + } + let file_name = format!("msgs_{}.json", chunk_index); + let file_content = load_file( + &format!( + "communities/{}/interactables/{}/{}", + self.get_community().get_name(), + self.get_path(), + self.get_name() + ), + &file_name, + ); + if file_content.is_empty() { + continue; + } + if let Ok(chunk) = json::parse(&file_content) { + for i in (0..chunk.len()).rev() { + if needed == 0 { + break; + } + if to_skip > 0 { + to_skip -= 1; + continue; + } + messages.push(chunk[i].clone()).unwrap(); + needed -= 1; + } + } + } + + messages + } +} +#[async_trait] +impl Interactable for TextChat { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "text".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + JsonValue::new_object() + } + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + let payload = cv.get_data(DataTypes::payload).as_container().unwrap(); + if cv.get_data(DataTypes::function).as_str().unwrap() == "get_messages" { + let amount = payload.get(DataTypes::amount).as_i64().unwrap(); + let loaded_messages = payload["loaded_messages"].as_i64().unwrap(); + let messages = self.get_messages(loaded_messages, amount).clone(); + let mut payload = JsonValue::new_object(); + payload["messages"] = messages; + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_chunk".to_string()) + .add_data(DataTypes::payload, payload); + } + if cv.get_data(DataTypes::function).unwrap().as_str().unwrap() == "send_message" { + let message = payload["message"].as_str().unwrap(); + let milliseconds_timestamp: u128 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis(); + self.add_message(milliseconds_timestamp, cv.get_sender(), message); + + let mut distribution_payload = JsonValue::new_object(); + distribution_payload["message"] = JsonValue::String(message.to_string()); + distribution_payload["sender_id"] = JsonValue::String(cv.get_sender().to_string()); + distribution_payload["send_time"] = + JsonValue::String(milliseconds_timestamp.to_string()); + let distribution = CommunicationValue::new(CommunicationType::update) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_live".to_string()) + .add_data(DataTypes::payload, distribution_payload); + + let connections: HashMap>> = + self.get_community().get_connections().await.clone(); + + for con in connections.values() { + for c in con { + let cd: &Arc = c; + cd.send_message(&distribution).await; + } + } + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "message_received".to_string()) + .add_data(DataTypes::payload, JsonValue::new_object()); + } + CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) + } + fn to_json(&self) -> JsonValue { + JsonValue::new_object() + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/interactables/voice_chat.rs b/src/communities/interactables/voice_chat.rs new file mode 100644 index 0000000..2a2e3a2 --- /dev/null +++ b/src/communities/interactables/voice_chat.rs @@ -0,0 +1,187 @@ +use crate::communities::{community::Community, interactables::interactable::Interactable}; +use async_trait::async_trait; +use json::JsonValue; +use std::sync::Arc; +use std::{any::Any, sync::RwLock}; +use uuid::Uuid; +pub enum CallUserState { + Active, + Muted, + Deafed, +} +impl CallUserState { + pub fn parse(state: &str) -> CallUserState { + match state { + "active" => CallUserState::Active, + "muted" => CallUserState::Muted, + "deafed" => CallUserState::Deafed, + _ => CallUserState::Active, + } + } + pub fn to_string(&self) -> String { + match self { + CallUserState::Active => "active".to_string(), + CallUserState::Muted => "muted".to_string(), + CallUserState::Deafed => "deafed".to_string(), + } + } +} + +pub struct CallUser { + pub user_id: Uuid, + pub user_state: CallUserState, + pub streaming: bool, +} + +pub struct VoiceChat { + id: Uuid, + name: String, + path: String, + community: Arc, + users: RwLock>, +} +impl VoiceChat { + pub fn new() -> VoiceChat { + VoiceChat { + id: Uuid::new_v4(), + name: String::new(), + path: String::new(), + community: Arc::new(Community::new()), + users: RwLock::new(Vec::new()), + } + } + pub fn update_user_state( + self: Arc, + user_id: Uuid, + state: CallUserState, + streaming: bool, + ) { + if let Some(user) = self + .users + .write() + .unwrap() + .iter_mut() + .find(|u| u.user_id == user_id) + { + user.user_state = state; + user.streaming = streaming; + } + } +} +#[async_trait] +impl Interactable for VoiceChat { + fn get_id(&self) -> &Uuid { + &self.id + } + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn get_codec(&self) -> String { + "voice".to_string() + } + fn set_name(&mut self, name: String) { + self.name = name; + } + fn set_path(&mut self, path: String) { + self.path = path; + } + fn get_community(&self) -> &Arc { + &self.community + } + fn set_community(&mut self, community: Arc) { + self.community = community; + } + fn get_name(&self) -> &String { + &self.name + } + fn get_path(&self) -> &String { + &self.path + } + fn get_total_path(&self) -> String { + String::new() + &self.path + "/" + &self.name + } + fn get_data(&self) -> JsonValue { + let mut data = JsonValue::new_object(); + let mut active_users = JsonValue::new_object(); + for user in self.users.read().unwrap().iter() { + let mut user_data = JsonValue::new_object(); + let _ = user_data.insert("state", JsonValue::String(user.user_state.to_string())); + let _ = user_data.insert("streaming", JsonValue::Boolean(user.streaming)); + let _ = active_users.insert(&user.user_id.to_string(), user_data); + } + let _ = data.insert("active_users", active_users); + data + } + async fn run_function(&self, cv: CommunicationValue) -> CommunicationValue { + let payload = cv.get_data(DataTypes::payload).unwrap(); + let function = cv.get_data(DataTypes::function).unwrap().as_str().unwrap(); + + if function == "get_call" { + let sender_id = payload["sender_id"].as_str().unwrap(); + let message_id = payload["message"].as_str().unwrap(); + let send_time = payload["send_time"].as_str().unwrap(); + + let mut response_payload = JsonValue::new_object(); + response_payload["sender_id"] = JsonValue::String(sender_id.to_string()); + response_payload["message"] = JsonValue::String(message_id.to_string()); + response_payload["send_time"] = JsonValue::String(send_time.to_string()); + + return CommunicationValue::new(CommunicationType::function) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "getting_call".to_string()) + .add_data(DataTypes::payload, response_payload); + } + + if function == "update_user_state" { + let user_id = payload["user_id"].as_str().unwrap(); + let state = payload["state"].as_str().unwrap(); + let streaming = payload["streaming"].as_bool().unwrap(); + + if let Some(user) = self + .users + .write() + .unwrap() + .iter_mut() + .find(|u| u.user_id == Uuid::parse_str(user_id).unwrap()) + { + user.user_state = CallUserState::parse(state); + user.streaming = streaming; + } + let mut response_payload = JsonValue::new_object(); + response_payload["user_id"] = JsonValue::String(user_id.to_string()); + response_payload["state"] = JsonValue::String(state.to_string()); + response_payload["streaming"] = JsonValue::Boolean(streaming); + + return CommunicationValue::new(CommunicationType::update) + .with_id(cv.get_id()) + .add_data_str(DataTypes::name, self.name.clone()) + .add_data_str(DataTypes::path, self.path.clone()) + .add_data_str(DataTypes::result, "user_changed".to_string()) + .add_data(DataTypes::payload, response_payload); + } + CommunicationValue::new(CommunicationType::error).with_id(cv.get_id()) + } + + fn to_json(&self) -> JsonValue { + let v = JsonValue::new_object(); + v + } + fn load( + &mut self, + community: Arc, + id: Uuid, + path: String, + name: String, + _json: &JsonValue, + ) { + self.community = community; + self.id = id; + self.name = name; + self.path = path; + } +} diff --git a/src/communities/mod.rs b/src/communities/mod.rs new file mode 100644 index 0000000..ecad116 --- /dev/null +++ b/src/communities/mod.rs @@ -0,0 +1,13 @@ +pub mod community_manager; +pub mod interactables { + pub mod category; + pub mod interactable; + pub mod registry; + pub mod text_chat; + pub mod voice_chat; +} +pub mod community; +pub mod community_connection; +pub mod perms { + pub mod permission; +} diff --git a/src/communities/perms/permission.rs b/src/communities/perms/permission.rs new file mode 100644 index 0000000..32a37ee --- /dev/null +++ b/src/communities/perms/permission.rs @@ -0,0 +1,27 @@ +use json::JsonValue; +use uuid::Uuid; + +pub struct Permission { + pub id: Uuid, + pub name: String, +} +impl Permission { + pub fn new(id: Uuid, name: String) -> Self { + Permission { id, name } + } + pub fn to_json(&self) -> JsonValue { + json::object! { + "id" => self.id.to_string(), + "name" => self.name.clone() + } + } + pub fn from_json(json: JsonValue) -> Self { + Permission { + id: Uuid::parse_str(json["id"].as_str().unwrap()).unwrap(), + name: json["name"].as_str().unwrap().to_string(), + } + } + pub fn to_string(&self) -> String { + self.to_json().as_str().unwrap().to_string() + } +} diff --git a/src/gui/app_state.rs b/src/gui/app_state.rs new file mode 100755 index 0000000..a161226 --- /dev/null +++ b/src/gui/app_state.rs @@ -0,0 +1,201 @@ +use crate::{ACTIVE_TASKS, APP_STATE, SHUTDOWN, gui::elements::log_card::UiLogEntry}; +use json::{JsonValue, object}; +use std::{collections::VecDeque, thread, time::Duration}; +use sysinfo::{RefreshKind, System}; + +#[derive(Clone)] +pub struct AppState { + pub logs: VecDeque, + pub cpu: Vec<(f64, f64)>, + pub ram: Vec<(f64, f64)>, + pub ping: Vec<(f64, f64)>, + pub net_up: Vec<(f64, f64)>, + pub net_down: Vec<(f64, f64)>, + pub sys_info: String, +} +const MAX_POINTS: usize = 1000; +const MAX_LOGS: usize = 100; + +impl AppState { + pub fn new() -> Self { + Self { + logs: VecDeque::new(), + cpu: Vec::new(), + ram: Vec::new(), + ping: Vec::new(), + net_up: Vec::new(), + net_down: Vec::new(), + sys_info: String::from("Loading..."), + } + } + + pub fn push_log(&mut self, msg: UiLogEntry) { + if self.logs.len() >= MAX_LOGS { + self.logs.pop_front(); + } + self.logs.push_back(msg); + } + + pub fn get_logs(&self) -> &VecDeque { + &self.logs + } + + pub fn push_cpu(&mut self, pt: (f64, f64)) { + self.cpu.push(pt); + if self.cpu.len() > MAX_POINTS { + self.cpu.remove(0); + } + } + + pub fn push_ram(&mut self, pt: (f64, f64)) { + self.ram.push(pt); + if self.ram.len() > MAX_POINTS { + self.ram.remove(0); + } + } + + pub fn push_ping_val(&mut self, pt: f64) { + self.ping.push((self.ping.len() as f64, pt)); + if self.ping.len() > MAX_POINTS { + self.ping.remove(0); + } + } + + pub fn push_net_up(&mut self, pt: (f64, f64)) { + self.net_up.push(pt); + if self.net_up.len() > MAX_POINTS { + self.net_up.remove(0); + } + } + + pub fn push_net_down(&mut self, pt: (f64, f64)) { + self.net_down.push(pt); + if self.net_down.len() > MAX_POINTS { + self.net_down.remove(0); + } + } + pub fn to_json(&self) -> JsonValue { + let json = object! { + "cpu" => self.cpu + .iter() + .map(|(_, y)| *y) + .collect::>(), + "ram" => self.ram + .iter() + .map(|(_, y)| *y) + .collect::>(), + "ping" => self + .ping + .iter() + .map(|(_, y)| *y) + .collect::>(), + "net_up" => self + .net_up + .iter() + .map(|(_, y)| *y) + .collect::>(), + "net_down" => self + .net_down + .iter() + .map(|(_, y)| *y) + .collect::>(), + }; + json + } + pub fn with_width(&self, width: u16) -> Self { + let mut new = self.clone(); + new.cpu = Self::downsample_to_fit_width(&new.cpu, width); + new.ram = Self::downsample_to_fit_width(&new.ram, width); + new.ping = Self::downsample_to_fit_width(&new.ping, width); + new.net_up = Self::downsample_to_fit_width(&new.net_up, width); + new.net_down = Self::downsample_to_fit_width(&new.net_down, width); + new + } + + fn downsample_to_fit_width(data: &[(f64, f64)], width: u16) -> Vec<(f64, f64)> { + let width_usize = (width as usize) * 2; + let len = data.len(); + + if len >= width_usize { + data[len - width_usize..].to_vec() + } else { + let mut result = Vec::with_capacity(width_usize); + + let dx = 1.0; + let pad_len = width_usize - len; + + let start_x = data + .first() + .map(|(x, _)| x - (dx * pad_len as f64)) + .unwrap_or(0.0); + let _ = data.first().map(|(_, y)| *y).unwrap_or(0.0); + + for i in 0..pad_len { + result.push((start_x + i as f64 * dx, -1 as f64)); + } + + result.extend_from_slice(data); + result + } + } +} + +pub fn setup() { + ACTIVE_TASKS.insert("System info loader".to_string()); + tokio::spawn(async move { + let mut sys = System::new_with_specifics(RefreshKind::everything()); + let mut last_total_received = 0u64; + let mut last_total_transmitted = 0u64; + let mut counter = 0.0; + loop { + if *SHUTDOWN.read().await { + break; + } + sys.refresh_all(); + + let mut tcpu = 0; + for cpu in sys.cpus() { + tcpu += cpu.cpu_usage() as i64; + tcpu /= 2; + } + let ram = (sys.used_memory() as f64 / sys.total_memory() as f64) * 100.0; + + let total_received = 0u64; + let total_transmitted = 0u64; + + let delta_received = if last_total_received == 0 { + 0 + } else { + total_received.saturating_sub(last_total_received) + }; + let delta_transmitted = if last_total_transmitted == 0 { + 0 + } else { + total_transmitted.saturating_sub(last_total_transmitted) + }; + last_total_received = total_received; + last_total_transmitted = total_transmitted; + + let net_down = delta_received as f64; + let net_up = delta_transmitted as f64; + + { + let mut st = APP_STATE.lock().unwrap(); + st.push_cpu((counter, tcpu as f64)); + st.push_ram((counter, ram)); + st.push_net_down((counter, net_down)); + st.push_net_up((counter, net_up)); + + st.sys_info = format!("NetDown: {} NetUp: {}", delta_received, delta_transmitted); + } + + counter += 1.0; + if counter > 30.0 { + thread::sleep(Duration::from_millis(500)); + } else { + thread::sleep(Duration::from_millis(5)); + } + } + ACTIVE_TASKS.remove("System info loader"); + }); +} diff --git a/src/gui/elements/console_card.rs b/src/gui/elements/console_card.rs new file mode 100644 index 0000000..b189bd4 --- /dev/null +++ b/src/gui/elements/console_card.rs @@ -0,0 +1,491 @@ +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; +use ttp_core::{CommunicationType, CommunicationValue}; +use uuid::Uuid; + +use crate::{ + ACTIVE_TASKS, RELOAD, SHUTDOWN, + gui::{ + elements::elements::{Element, InteractableElement, JoinableElement}, + interaction_result::InteractionResult, + ui::FPS, + util::borders::draw_block_joins, + }, + log, log_command, log_cv, + omikron::omikron_connection::OMIKRON_CONNECTION, + users::{user_manager, user_profile::UserProfile}, + util::file_util, +}; +use std::{ + any::Any, + sync::{Arc, Mutex}, + time::Duration, +}; +use tokio::time::Instant; + +pub struct ConsoleCard { + focused: bool, + pub title: String, + pub content: String, + pub cursor_position: usize, + + borders: Borders, + joins: Borders, + + cursor: Arc>, + last_swap: Arc>, + tab_index: usize, +} + +impl ConsoleCard { + pub fn new(title: &str, content: &str) -> Self { + ConsoleCard { + focused: false, + title: title.to_string(), + content: content.to_string(), + cursor_position: content.chars().count(), + borders: Borders::ALL, + joins: Borders::NONE, + cursor: Arc::new(Mutex::new(true)), + last_swap: Arc::new(Mutex::new(Instant::now())), + tab_index: 0, + } + } + + fn byte_index(&self) -> usize { + self.content + .char_indices() + .nth(self.cursor_position) + .map(|(i, _)| i) + .unwrap_or(self.content.len()) + } + + fn cursor_visible(&self) -> bool { + if !self.focused { + return false; + } + + let mut visible = self.cursor.lock().unwrap(); + let mut last = self.last_swap.lock().unwrap(); + let now = Instant::now(); + + if now.duration_since(*last) >= Duration::from_millis(500) { + *visible = !*visible; + *last = now; + } + + *visible + } + + fn current_prefix(&self) -> Option<&str> { + if self.content.starts_with('/') { + Some("/") + } else { + None + } + } + + fn cursor_spans(&self) -> Vec> { + let cursor_visible = self.cursor_visible(); + let cursor_style = Style::default().fg(Color::White).bg(Color::DarkGray); + let mut spans = Vec::new(); + + if self.content.is_empty() { + if self.focused { + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } else { + spans.push(Span::styled(" ", Style::default().fg(Color::White))); + } + spans.push(Span::styled( + "send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } else { + spans.push(Span::styled( + " send command ( for info)", + Style::default().fg(Color::DarkGray), + )); + } + return spans; + } + + let byte_index = self.byte_index(); + let before = self.content[..byte_index].to_string(); + let after = self.content[byte_index..].to_string(); + + let prefix_len = self.current_prefix().map(|s| s.len()).unwrap_or(0); + + if prefix_len > 0 && before.len() >= prefix_len { + let prefix = &before[..prefix_len]; + let rest = &before[prefix_len..]; + spans.push(Span::styled( + prefix.to_string(), + Self::style_for_part(true, false, false), + )); + if !rest.is_empty() { + spans.push(Span::styled( + rest.to_string(), + Style::default().fg(Color::White), + )); + } + } else if !before.is_empty() { + spans.push(Span::styled( + before.clone(), + Style::default().fg(Color::White), + )); + } + + if cursor_visible { + spans.push(Span::styled(" ", cursor_style)); + } + + if !after.is_empty() { + spans.push(Span::styled(after, Style::default().fg(Color::White))); + } + + spans + } + + fn style_for_part(is_prefix: bool, is_hint: bool, is_error: bool) -> Style { + if is_error { + return Style::default().fg(Color::Red); + } + + if is_hint { + return Style::default().fg(Color::DarkGray); + } + + if is_prefix { + return Style::default().fg(Color::DarkGray); + } + + Style::default().fg(Color::White) + } + + fn render_cursor_spans(&self) -> Vec> { + self.cursor_spans() + } + + fn move_cursor_left(&mut self) { + if self.cursor_position > 0 { + self.cursor_position -= 1; + } + } + + fn move_cursor_right(&mut self) { + let len = self.content.chars().count(); + if self.cursor_position < len { + self.cursor_position += 1; + } + } + + fn delete_at_cursor(&mut self) { + if self.content.is_empty() || self.cursor_position == 0 { + return; + } + + let start = self + .content + .char_indices() + .nth(self.cursor_position.saturating_sub(1)) + .map(|(i, _)| i) + .unwrap_or(0); + let end = self.byte_index(); + self.content.replace_range(start..end, ""); + self.cursor_position -= 1; + } + + fn insert_at_cursor(&mut self, c: char) { + let idx = self.byte_index(); + self.content.insert(idx, c); + self.cursor_position += 1; + } +} + +impl Element for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, r: Rect) { + let block = Block::default() + .borders(self.borders) + .title(self.title.clone()) + .title_style(Style::default().fg(Color::White)) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }) + .style(if self.focused { + Style::default().fg(Color::White) + } else { + Style::default() + }); + + let spans = self.render_cursor_spans(); + let par = Paragraph::new(Line::from(spans)) + .block(block) + .scroll((0, 0)); + f.render_widget(par, r); + draw_block_joins(f, r, self.borders, self.joins); + } +} + +impl JoinableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for ConsoleCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + match key.code { + KeyCode::Enter => { + if self.content.is_empty() { + log!(""); + return InteractionResult::Handled; + } + + let command = self.content.clone(); + let id = Uuid::new_v4(); + let id = id.to_string(); + let id = id.split_at(8).0; + let task_id = format!("command_{}_{}", command, id); + ACTIVE_TASKS.insert(task_id.clone()); + + log_command!("{}", command); + + tokio::spawn(async move { + run_command(&command).await; + ACTIVE_TASKS.remove(&task_id); + }); + + self.content.clear(); + self.cursor_position = 0; + self.tab_index = 0; + InteractionResult::Handled + } + KeyCode::Backspace => { + self.delete_at_cursor(); + InteractionResult::Handled + } + KeyCode::Delete => { + let len = self.content.chars().count(); + if self.cursor_position < len { + let start = self.byte_index(); + let end = self + .content + .char_indices() + .nth(self.cursor_position + 1) + .map(|(i, _)| i) + .unwrap_or(self.content.len()); + self.content.replace_range(start..end, ""); + } + InteractionResult::Handled + } + KeyCode::Left => { + self.move_cursor_left(); + InteractionResult::Handled + } + KeyCode::Right => { + self.move_cursor_right(); + InteractionResult::Handled + } + KeyCode::Home => { + self.cursor_position = 0; + InteractionResult::Handled + } + KeyCode::End => { + self.cursor_position = self.content.chars().count(); + InteractionResult::Handled + } + KeyCode::Tab => { + if let Some(prefix) = self.current_prefix() { + if prefix == "/" { + self.tab_index = self.tab_index.saturating_add(1); + } + } + InteractionResult::Handled + } + _ => { + if let Some(c) = key.code.as_char() { + self.insert_at_cursor(c); + InteractionResult::Handled + } else { + InteractionResult::Unhandled + } + } + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} + +pub async fn run_command(command: &str) { + let parts = command.split(" ").collect::>(); + + match parts.as_slice() { + ["tasks"] => { + let active_tasks: Vec = + ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect(); + let info = if *SHUTDOWN.read().await && *RELOAD.read().await { + "Rebooting, " + } else if *SHUTDOWN.read().await { + "Shutting , " + } else { + "" + }; + log!("{}Active tasks: {:?}", info, active_tasks); + } + ["fps"] => { + let (fps, skips) = *FPS.read().await; + log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips); + } + + ["help"] => { + log!("Available commands: tasks, fps, ping, user"); + } + + ["help", "tasks"] => { + log!("Tasks command usage: tasks"); + } + ["help", "fps"] => { + log!("FPS command usage: fps"); + } + ["help", "ping"] => { + log!("Ping command usage: ping [time]"); + } + ["help", "user"] => { + log!("User command usage: user add | user remove | user list"); + } + + ["ping"] => { + ping(20).await; + } + ["ping", time] => { + let time = time.parse::().unwrap_or(20); + ping(time).await; + } + ["user", "add", username] => { + if let (Some(user), Some(_)) = user_manager::create_user(username).await { + log!("Created user {}", user.user_id); + } else { + log!("Failed to create user"); + } + } + ["user", "remove", username] => { + if let Some(user) = user_manager::get_user_by_username(username) { + user_manager::remove_user(user.user_id); + log!("Removed user {}", user.user_id); + } else { + log!("Failed to find user"); + } + } + ["user", "list"] => { + let users: Vec = user_manager::get_users(); + for user in users { + let storage = file_util::get_designed_storage(user.user_id); + log!( + "> Username: {}, ID: {}, created at: {}, storage: {}", + user.username, + user.user_id, + user.created_at, + storage + ); + } + } + ["user", "info", username] => { + if let Some(user) = user_manager::get_user_by_username(username) { + user_manager::remove_user(user.user_id); + log!("Removed user {}", user.user_id); + } else { + log!("Failed to find user"); + } + } + ["reload"] | ["restart"] => { + log!("Restarting"); + *RELOAD.write().await = true; + *SHUTDOWN.write().await = true; + } + ["shutdown"] | ["stop"] => { + log!("Shutting down"); + *SHUTDOWN.write().await = true; + } + _ => { + log!("Unknown command"); + } + } +} + +pub async fn ping(time: u64) { + let conn = OMIKRON_CONNECTION.clone(); + + let response_cv = conn + .await_response( + &CommunicationValue::new(CommunicationType::ping), + Some(Duration::from_secs(time)), + ) + .await; + match response_cv { + Ok(response) => log_cv!(response), + Err(err) => log!("Ping error: {:?}", err), + } +} diff --git a/src/gui/elements/elements.rs b/src/gui/elements/elements.rs new file mode 100644 index 0000000..3429e52 --- /dev/null +++ b/src/gui/elements/elements.rs @@ -0,0 +1,49 @@ +use std::any::Any; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect, widgets::Borders}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +#[allow(unused)] +pub trait Element: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn render(&self, f: &mut Frame, r: Rect); +} + +#[allow(unused)] +pub trait JoinableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn set_borders(&mut self, borders: Borders); + fn set_joins(&mut self, joins: Borders); +} + +#[allow(unused)] +pub trait InfoElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn get_info_screen(&self) -> Box; +} + +#[allow(unused)] +pub trait InteractableElement: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + fn as_element(&self) -> &dyn Element; + fn as_element_mut(&mut self) -> &mut dyn Element; + + fn interact(&mut self, key: KeyEvent) -> InteractionResult; + + fn can_focus(&self) -> bool; + fn is_focused(&self) -> bool; + fn focus(&mut self, f: bool); +} diff --git a/src/gui/elements/graph_card.rs b/src/gui/elements/graph_card.rs new file mode 100644 index 0000000..d95f937 --- /dev/null +++ b/src/gui/elements/graph_card.rs @@ -0,0 +1,215 @@ +use std::{any::Any, sync::Arc}; + +use crossterm::event::KeyEvent; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + widgets::{ + Block, Borders, + canvas::{Canvas, Line}, + }, +}; + +use crate::{ + APP_STATE, + gui::{ + elements::elements::{Element, InteractableElement, JoinableElement}, + interaction_result::InteractionResult, + ui::UI, + util::borders::draw_block_joins, + }, +}; + +pub enum GRAPHS { + Ram, + Cpu, + Ping, +} + +impl GRAPHS { + pub fn get_color(&self) -> Color { + match self { + GRAPHS::Ram => Color::Blue, + GRAPHS::Cpu => Color::Red, + GRAPHS::Ping => Color::Green, + } + } + + pub fn get_graph(&self) -> Vec<(f64, f64)> { + match self { + GRAPHS::Ram => APP_STATE.lock().unwrap().with_width(28).ram.clone(), + GRAPHS::Cpu => APP_STATE.lock().unwrap().with_width(28).cpu.clone(), + GRAPHS::Ping => APP_STATE.lock().unwrap().with_width(28).ping.clone(), + } + } + + pub fn get_unit(&self) -> String { + match self { + GRAPHS::Ram => "MB".to_string(), + GRAPHS::Cpu => "%".to_string(), + GRAPHS::Ping => "ms".to_string(), + } + } +} + +#[allow(unused)] +pub struct GraphCard { + ui: Arc, + graph_type: GRAPHS, + + focused: bool, + pub title: String, + + borders: Borders, + joins: Borders, + + open: bool, +} + +impl GraphCard { + pub fn new(ui: Arc, graph_type: GRAPHS, title: String) -> Self { + Self { + ui, + graph_type, + focused: false, + title, + borders: Borders::ALL, + joins: Borders::NONE, + open: true, + } + } + + pub fn set_open(&mut self, open: bool) { + self.open = open; + } +} +impl Element for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, r: Rect) { + if self.open { + let graph = self.graph_type.get_graph(); + let unit = self.graph_type.get_unit(); + let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0); + let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0); + let min_y = graph + .iter() + .map(|(_, y)| *y) + .filter(|y| *y > 0.0) + .min_by(|a, b| a.total_cmp(b)) + .unwrap_or(0.0); + let max_y = graph.iter().map(|(_, y)| *y).fold(-1.0, f64::max); + + let block = Block::default() + .title(format!( + "{}:─{}{}─{}min/{}max", + self.title, + graph.last().unwrap_or(&(0.0, 0.0)).1 as i64, + unit, + min_y as i64, + max_y as i64, + )) + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let canvas = Canvas::default() + .block(block) + .x_bounds([min_x, max_x]) + .y_bounds([0.0, 100.0]) + .paint(|ctx| { + for (x, y) in &graph { + ctx.draw(&Line { + x1: *x, + y1: 0.0, + x2: *x, + y2: *y, + color: self.graph_type.get_color(), + }); + } + }); + f.render_widget(canvas, r); + } else { + let block = Block::default() + .title("") + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + f.render_widget(block, r); + } + draw_block_joins(f, r, self.borders, self.joins); + } +} + +impl JoinableElement for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for GraphCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &dyn Element { + self + } + + fn as_element_mut(&mut self) -> &mut dyn Element { + self + } + + fn interact(&mut self, _key: KeyEvent) -> InteractionResult { + InteractionResult::Handled + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/elements/log_card.rs b/src/gui/elements/log_card.rs new file mode 100755 index 0000000..e2d4ba1 --- /dev/null +++ b/src/gui/elements/log_card.rs @@ -0,0 +1,483 @@ +use crate::APP_STATE; +use crate::gui::elements::elements::{Element, InteractableElement, JoinableElement}; +use crate::gui::interaction_result::InteractionResult; +use crate::gui::util::borders::draw_block_joins; +use crate::util::logger::PrintType; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::Rect, + style::{Color, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; +use std::any::Any; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Clone, Debug)] +pub struct UiLogEntry { + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl UiLogEntry { + pub fn format_timestamp(&self) -> String { + let secs = (self.timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("{:02}:{:02}:{:02}", hours, minutes, seconds) + } +} + +impl From for UiLogEntry { + fn from(entry: LogEntry) -> Self { + Self { + timestamp_ms: entry.timestamp_ms, + sender: entry.sender, + message: entry.message, + is_error: entry.is_error, + } + } +} + +#[derive(Clone, Debug)] +pub struct LogEntry { + pub timestamp_ms: u128, + pub sender: PrintType, + pub message: String, + pub is_error: bool, +} + +impl LogEntry { + pub fn new(sender: PrintType, message: String, is_error: bool) -> Self { + Self { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + sender, + message, + is_error, + } + } +} + +pub struct LogCard { + focused: bool, + selected: bool, + scroll_offset: usize, + last_total_lines: usize, + last_visible_height: usize, + pub borders: Borders, + pub joins: Borders, +} + +impl LogCard { + pub fn new() -> Self { + Self { + focused: false, + selected: false, + scroll_offset: 0, + last_total_lines: 0, + last_visible_height: 10, + borders: Borders::ALL, + joins: Borders::NONE, + } + } + + fn get_logs(&self) -> Vec { + let state = APP_STATE.lock().unwrap(); + state.get_logs().iter().cloned().collect() + } + + fn find_split_point(s: &str, max_width: usize) -> usize { + if max_width == 0 { + return s.len(); + } + + let mut current_width = 0usize; + let mut last_boundary = 0usize; + + for (idx, ch) in s.char_indices() { + let char_width = if ch.is_ascii() { 1 } else { 2 }; + if current_width + char_width > max_width { + if last_boundary == 0 { + return idx + ch.len_utf8(); + } + return last_boundary; + } + current_width += char_width; + last_boundary = idx + ch.len_utf8(); + } + + s.len() + } + + fn wrap_entry(entry: &UiLogEntry, available_width: usize) -> Vec<(String, Color, bool)> { + let mut result = Vec::new(); + + let timestamp = entry.format_timestamp(); + let first_prefix = "┌ "; + let default_prefix = "│ "; + let last_prefix = "└ "; + + let prefix_width = 2; + let first_line_width = available_width.saturating_sub(prefix_width + 1); + let continuation_width = available_width.saturating_sub(prefix_width); + + let segments: Vec<&str> = entry.message.split('\n').collect(); + + let mut raw_lines = Vec::new(); + + for segment in segments { + let mut remaining = segment; + if remaining.is_empty() { + raw_lines.push(String::new()); + continue; + } + + let mut is_first_part = true; + while !remaining.is_empty() { + let current_width = if is_first_part { + first_line_width + } else { + continuation_width + }; + + let split_point = Self::find_split_point(remaining, current_width); + let line_content = remaining[..split_point].to_string(); + raw_lines.push(line_content); + remaining = &remaining[split_point..]; + is_first_part = false; + } + } + + if raw_lines.is_empty() { + raw_lines.push(String::new()); + } + + for (idx, content) in raw_lines.iter().enumerate() { + let is_last = idx + 1 == raw_lines.len(); + let is_single = raw_lines.len() == 1; + let is_first = idx == 0; + let prefix = if is_first { + first_prefix + } else if is_last && !is_single { + last_prefix + } else { + default_prefix + }; + + let mut line = String::from(prefix); + line.push_str(content); + + if is_last && !timestamp.is_empty() { + line.push(' '); + line.push_str(×tamp); + } + + result.push((line, entry.sender.prefix_color(), entry.is_error)); + } + + result + } + + fn build_all_lines( + &self, + entries: Vec, + width: usize, + ) -> Vec<(String, Color, bool)> { + let mut lines = Vec::new(); + + for entry in entries { + let wrapped = Self::wrap_entry(&entry, width); + lines.extend(wrapped); + } + + lines + } + + fn calculate_view_window(&self, total_lines: usize, visible_height: usize) -> (usize, usize) { + if total_lines <= visible_height { + return (0, total_lines); + } + + let max_offset = total_lines - visible_height; + let clamped_offset = self.scroll_offset.min(max_offset); + + let end = total_lines - clamped_offset; + let start = end.saturating_sub(visible_height); + + (start, end) + } + + fn get_title_hints(&self) -> (bool, bool) { + if self.last_total_lines == 0 || self.last_total_lines <= self.last_visible_height { + return (false, false); + } + + let max_offset = self.last_total_lines - self.last_visible_height; + let can_scroll_up = self.scroll_offset < max_offset; + let can_scroll_down = self.scroll_offset > 0; + + (can_scroll_up, can_scroll_down) + } + + fn build_title(&self) -> String { + if !self.focused { + return "Logs".to_string(); + } + + let (can_up, can_down) = self.get_title_hints(); + + if !can_up && !can_down { + return "Logs".to_string(); + } + + let nav_symbol = if self.selected { "↑" } else { "j" }; + let down_symbol = if self.selected { "↓" } else { "k" }; + + match (can_up, can_down) { + (true, true) => format!("Logs ({} older {} newer)", nav_symbol, down_symbol), + (true, false) => format!("Logs ({} older)", nav_symbol), + (false, true) => format!("Logs ({} newer)", down_symbol), + (false, false) => "Logs".to_string(), + } + } + + fn scroll_up(&mut self) { + let max_offset = self + .last_total_lines + .saturating_sub(self.last_visible_height); + self.scroll_offset = (self.scroll_offset + 1).min(max_offset); + } + + fn scroll_down(&mut self) { + self.scroll_offset = self.scroll_offset.saturating_sub(1); + } + + fn split_line_prefix(line: &str) -> (&str, &str) { + if let Some(rest) = line.strip_prefix("└ ") { + ("└ ", rest) + } else if let Some(rest) = line.strip_prefix("│ ") { + ("│ ", rest) + } else if let Some(rest) = line.strip_prefix("┌ ") { + ("┌ ", rest) + } else { + ("", line) + } + } + + fn split_timestamp_suffix(line: &str) -> (&str, &str) { + if let Some(idx) = line.rfind(' ') { + let possible_timestamp = &line[idx + 1..]; + if possible_timestamp.len() == 8 + && possible_timestamp.as_bytes()[2] == b':' + && possible_timestamp.as_bytes()[5] == b':' + { + let (content, timestamp_with_space) = line.split_at(idx); + return (content.trim_end(), timestamp_with_space); + } + } + (line, "") + } +} + +impl Element for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, area: Rect) { + let entries = self.get_logs(); + + let block = Block::default() + .title(self.build_title()) + .borders(self.borders) + .border_style(if self.focused { + Style::default().fg(Color::Yellow) + } else { + Style::default() + }); + + let inner_area = block.inner(area); + f.render_widget(block, area); + + if inner_area.width == 0 || inner_area.height == 0 { + draw_block_joins(f, area, self.borders, self.joins); + return; + } + + let all_lines = self.build_all_lines(entries, inner_area.width as usize); + let total_lines = all_lines.len(); + let visible_height = inner_area.height as usize; + + let (start, end) = self.calculate_view_window(total_lines, visible_height); + let visible_lines = &all_lines[start..end]; + + let rendered_lines: Vec = visible_lines + .iter() + .map(|(line, prefix_color, is_error)| { + let mut spans = Vec::new(); + + let (prefix, rest) = Self::split_line_prefix(line); + + if !prefix.is_empty() { + spans.push(Span::styled( + prefix.to_string(), + Style::default().fg(*prefix_color), + )); + } + + let (content, timestamp) = Self::split_timestamp_suffix(rest); + let text_color = if *is_error { Color::Red } else { Color::White }; + + if !content.is_empty() { + spans.push(Span::styled( + content.to_string(), + Style::default().fg(text_color), + )); + } + + if !timestamp.is_empty() { + spans.push(Span::styled( + timestamp.to_string(), + Style::default().fg(Color::DarkGray), + )); + } + + Line::from(spans) + }) + .collect(); + + for (idx, line) in rendered_lines.iter().enumerate() { + let line_area = Rect { + x: inner_area.x, + y: inner_area.y + idx as u16, + width: inner_area.width, + height: 1, + }; + f.render_widget(Paragraph::new(line.clone()), line_area); + } + + draw_block_joins(f, area, self.borders, self.joins); + } +} + +impl JoinableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + &*self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self + } + + fn set_borders(&mut self, borders: Borders) { + self.borders = borders; + } + + fn set_joins(&mut self, joins: Borders) { + self.joins = joins; + } +} + +impl InteractableElement for LogCard { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn as_element(&self) -> &(dyn Element + 'static) { + &*self + } + + fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) { + &mut *self + } + + fn interact(&mut self, key: KeyEvent) -> InteractionResult { + let entries = self.get_logs(); + let estimated_width = 80usize; + let all_lines = self.build_all_lines(entries, estimated_width); + + self.last_total_lines = all_lines.len(); + let visible_height = self.last_visible_height.max(1); + + match key.code { + KeyCode::Enter | KeyCode::Char(' ') => { + self.selected = !self.selected; + InteractionResult::Handled + } + KeyCode::Char('j') | KeyCode::Char('J') => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); + } + InteractionResult::Handled + } + KeyCode::Char('k') | KeyCode::Char('K') => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); + } + InteractionResult::Handled + } + KeyCode::Up if self.selected => { + let (can_up, _) = self.get_title_hints(); + if can_up { + self.scroll_up(); + } + InteractionResult::Handled + } + KeyCode::Down if self.selected => { + let (_, can_down) = self.get_title_hints(); + if can_down { + self.scroll_down(); + } + InteractionResult::Handled + } + KeyCode::Home => { + if self.last_total_lines > visible_height { + self.scroll_offset = self.last_total_lines - visible_height; + } + InteractionResult::Handled + } + KeyCode::End => { + self.scroll_offset = 0; + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + } + } + + fn can_focus(&self) -> bool { + true + } + + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, f: bool) { + self.focused = f; + } +} diff --git a/src/gui/input_handler.rs b/src/gui/input_handler.rs new file mode 100644 index 0000000..b2557c5 --- /dev/null +++ b/src/gui/input_handler.rs @@ -0,0 +1,56 @@ +use crate::gui::ui::{UI, UNIQUE}; +use crate::{RELOAD, SHUTDOWN}; +use crossterm::event::{Event, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Duration; + +pub fn setup_input_handler(ui: Arc) { + tokio::spawn(async move { + loop { + if *SHUTDOWN.read().await { + break; + } + + let event_result = tokio::task::spawn_blocking(|| { + if let Ok(true) = poll(Duration::from_millis(100)) { + read().ok().and_then(|ev| match ev { + Event::Key(key) if key.kind == KeyEventKind::Press => Some(key), + _ => None, + }) + } else { + None + } + }) + .await; + + match event_result { + Ok(Some(key_event)) => { + handle_input(key_event, ui.clone()).await; + UNIQUE.store(true, Ordering::Relaxed); + } + Ok(_) => {} + Err(e) => { + eprintln!("Input task error: {}", e); + tokio::time::sleep(Duration::from_millis(10)).await; + } + } + } + }); +} + +pub async fn handle_input(key: KeyEvent, ui: Arc) { + match (key.code, key.modifiers) { + (crossterm::event::KeyCode::Char('q'), KeyModifiers::CONTROL) + | (crossterm::event::KeyCode::Char('c'), KeyModifiers::CONTROL) => { + *SHUTDOWN.write().await = true; + } + (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => { + *RELOAD.write().await = true; + *SHUTDOWN.write().await = true; + } + _ => { + ui.handle_input(key).await; + } + } +} diff --git a/src/gui/interaction_result.rs b/src/gui/interaction_result.rs new file mode 100644 index 0000000..462149a --- /dev/null +++ b/src/gui/interaction_result.rs @@ -0,0 +1,49 @@ +use std::fmt::{Debug, Formatter}; +use std::future::Future; +use std::pin::Pin; + +use crate::gui::screens::screens::Screen; + +#[allow(unused)] +pub enum InteractionResult { + CloseScreen, + OpenScreen { + screen: Box, + }, + OpenFutureScreen { + screen: Pin> + Send>>, + }, + Handled, + Unhandled, +} + +impl Debug for InteractionResult { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + InteractionResult::OpenScreen { screen: _ } => write!(f, "OpenScreen"), + InteractionResult::OpenFutureScreen { screen: _ } => write!(f, "OpenFutureScreen"), + InteractionResult::CloseScreen => write!(f, "CloseScreen"), + InteractionResult::Handled => write!(f, "Handled"), + InteractionResult::Unhandled => write!(f, "Unhandled"), + } + } +} + +impl PartialEq for InteractionResult { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + InteractionResult::OpenScreen { screen: _ }, + InteractionResult::OpenScreen { screen: _ }, + ) => true, + ( + InteractionResult::OpenFutureScreen { screen: _ }, + InteractionResult::OpenFutureScreen { screen: _ }, + ) => true, + (InteractionResult::CloseScreen, InteractionResult::CloseScreen) => true, + (InteractionResult::Handled, InteractionResult::Handled) => true, + (InteractionResult::Unhandled, InteractionResult::Unhandled) => true, + _ => false, + } + } +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..b355046 --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,20 @@ +pub mod elements { + pub mod console_card; + pub mod elements; + pub mod graph_card; + pub mod log_card; +} +pub mod screens { + pub mod main_screen; + pub mod md_viewer; + pub mod screens; + pub mod terms_checker; + pub mod terms_updater; +} +pub mod util { + pub mod borders; +} +pub mod app_state; +pub mod input_handler; +pub mod interaction_result; +pub mod ui; diff --git a/src/gui/screens/main_screen.rs b/src/gui/screens/main_screen.rs new file mode 100644 index 0000000..85dbbaa --- /dev/null +++ b/src/gui/screens/main_screen.rs @@ -0,0 +1,242 @@ +use crate::gui::{ + elements::{ + console_card::ConsoleCard, + elements::{InteractableElement, JoinableElement}, + graph_card::{GRAPHS, GraphCard}, + log_card::LogCard, + }, + interaction_result::InteractionResult, + screens::screens::{NavDirection, Screen}, + ui::UI, +}; + +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Constraint, Layout, Margin, Rect}, + widgets::{Block, Borders}, +}; + +use std::{any::Any, sync::Arc}; + +pub struct MainScreen { + elements: Vec>, + nav_grid: Vec>>, + selected_coords: (usize, usize), + graphs_open: bool, +} + +impl MainScreen { + pub async fn new(ui: Arc) -> Self { + let mut elements: Vec> = Vec::new(); + + let nav_grid = vec![ + vec![Some(0), Some(2)], + vec![Some(0), Some(3)], + vec![Some(1), Some(4)], + ]; + + let mut log_card = LogCard::new(); + log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); + let mut console_card = ConsoleCard::new("Console", ""); + console_card.set_joins(Borders::TOP); + + elements.push(Box::new(log_card)); + elements.push(Box::new(console_card)); + + let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM".into()); + ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); + elements.push(Box::new(ram_graph)); + let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU".into()); + cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT)); + cpu_graph.set_joins(Borders::TOP); + elements.push(Box::new(cpu_graph)); + let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into()); + ping_graph.set_joins(Borders::TOP); + elements.push(Box::new(ping_graph)); + + let graphs_open = true; + + let mut screen = MainScreen { + elements, + nav_grid, + selected_coords: (1, 0), + graphs_open, + }; + screen.focus_current(); + screen + } + + fn focus_current(&mut self) { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + if element.can_focus() { + element.focus(true); + } + } + } + } + + fn unfocus_current(&mut self, y: usize, x: usize) { + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|row| row.get(x)) { + if let Some(element) = self.elements.get_mut(*index) { + element.focus(false); + } + } + } + + fn navigate(&mut self, direction: NavDirection) { + let (current_row, current_col) = self.selected_coords; + let current_element = self.nav_grid[current_row][current_col]; + + self.unfocus_current(current_row, current_col); + + let (delta_row, delta_col) = match direction { + NavDirection::Up => (-1isize, 0), + NavDirection::Down => (1, 0), + NavDirection::Left => (0, -1), + NavDirection::Right => (0, 1), + _ => (0, 0), + }; + + let mut next_row = current_row as isize; + let mut next_col = current_col as isize; + + loop { + next_row += delta_row; + next_col += delta_col; + + if next_row < 0 || next_col < 0 { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + let next_row_u = next_row as usize; + let next_col_u = next_col as usize; + + if next_row_u >= self.nav_grid.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + + if let Some(row) = self.nav_grid.get(next_row_u) { + if next_col_u >= row.len() { + self.selected_coords = ( + (next_row - delta_row) as usize, + (next_col - delta_col) as usize, + ); + break; + } + + if let Some(next_element) = row[next_col_u] { + if Some(next_element) != current_element { + self.selected_coords = (next_row_u, next_col_u); + self.focus_current(); + return; + } + } + } + } + + self.focus_current(); + } +} + +impl Screen for MainScreen { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, rect: Rect) { + let main_block = Block::default().title("Main").borders(Borders::ALL); + f.render_widget(main_block, rect); + + let inner = rect.inner(Margin { + vertical: 1, + horizontal: 1, + }); + + let graphs_width = if self.graphs_open { 30 } else { 2 }; + let main_width = inner.width.saturating_sub(graphs_width); + + let horizontal_chunks = Layout::default() + .direction(ratatui::layout::Direction::Horizontal) + .constraints([ + Constraint::Length(main_width), + Constraint::Length(graphs_width), + ]) + .split(inner); + + let left_area = horizontal_chunks[0]; + let right_area = horizontal_chunks[1]; + + let left_rows = + Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(left_area); + + if let Some(log) = self.elements.get(0) { + log.as_element().render(f, left_rows[0]); + } + + if let Some(console) = self.elements.get(1) { + console.as_element().render(f, left_rows[1]); + } + + let graph_elements: Vec<_> = self + .elements + .iter() + .filter(|el| el.as_any().is::()) + .collect(); + + if !graph_elements.is_empty() { + let graph_chunks = Layout::vertical( + graph_elements + .iter() + .map(|_| Constraint::Ratio(1, graph_elements.len() as u32)) + .collect::>(), + ) + .split(right_area); + + for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) { + el.as_element().render(f, *area); + } + } + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + match event.code { + KeyCode::Up => self.navigate(NavDirection::Up), + KeyCode::Down => self.navigate(NavDirection::Down), + KeyCode::Left => self.navigate(NavDirection::Left), + KeyCode::Right => self.navigate(NavDirection::Right), + KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => { + self.graphs_open = !self.graphs_open; + for element in self.elements.iter_mut() { + if let Some(graph) = element.as_any_mut().downcast_mut::() { + graph.set_open(self.graphs_open); + } + } + return InteractionResult::Handled; + } + _ => { + let (y, x) = self.selected_coords; + if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { + if let Some(el) = self.elements.get_mut(*index) { + return el.interact(event); + } + } + } + } + + InteractionResult::Handled + } +} diff --git a/src/gui/screens/md_viewer.rs b/src/gui/screens/md_viewer.rs new file mode 100644 index 0000000..10eabce --- /dev/null +++ b/src/gui/screens/md_viewer.rs @@ -0,0 +1,493 @@ +use crossterm::event::{self, Event, KeyCode, KeyEvent}; +use ratatui::{ + DefaultTerminal, + prelude::*, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph, Wrap}, +}; +use std::{any::Any, time::Duration}; + +use crate::gui::{interaction_result::InteractionResult, screens::screens::Screen}; + +pub struct FileViewer { + title: String, + text: Vec, + scroll: u16, + scroll_x: u16, +} + +impl Screen for FileViewer { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, rect: Rect) { + self.draw(f, rect); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + match event.code { + KeyCode::Char('q') | KeyCode::Esc => { + return InteractionResult::CloseScreen; + } + + KeyCode::Down => self.scroll = self.scroll.saturating_add(1), + KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), + KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), + KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), + KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), + KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), + + _ => {} + } + + InteractionResult::Unhandled + } +} + +impl FileViewer { + pub fn new(title: String, content: &str) -> Self { + Self { + title, + text: parse_document(content.to_owned()), + scroll: 0, + scroll_x: 0, + } + } + pub fn force_popup(mut self, mut terminal: DefaultTerminal) -> DefaultTerminal { + loop { + terminal + .draw(|f| { + let area = f.area(); + self.draw(f, area); + }) + .unwrap(); + + if event::poll(Duration::from_millis(100)).unwrap() { + let ev = event::read().unwrap(); + self.handle_event(&ev); + + if matches!(ev, Event::Key(k) if k.code == KeyCode::Char('q')) { + break; + } + } + } + terminal + } + fn draw(&self, f: &mut Frame, area: Rect) { + use ratatui::text::Text; + + let mut rendered_lines = Vec::new(); + + for display_line in &self.text { + if display_line.scrollable { + let content: String = display_line + .line + .spans + .iter() + .map(|s| s.content.clone()) + .collect(); + + let start = self.scroll_x as usize; + let width = area.width as usize - 2; + + let visible = if start < content.chars().count() { + content.chars().skip(start).take(width).collect() + } else { + String::new() + }; + + let mut chars: Vec = visible.chars().collect(); + + if start > 0 && !chars.is_empty() { + chars[0] = '<'; + } + + if start + width < content.chars().count() && !chars.is_empty() { + let last = chars.len() - 1; + chars[last] = '>'; + } + + let visible: String = chars.into_iter().collect(); + + rendered_lines.push(Line::from(Span::styled( + visible, + display_line + .line + .spans + .first() + .map(|s| s.style) + .unwrap_or_default(), + ))); + } else { + rendered_lines.push(display_line.line.clone()); + } + } + + let paragraph = Paragraph::new(Text::from(rendered_lines)) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("{} - [Q to close]", self.title.as_str(),)), + ) + .wrap(Wrap { trim: false }) + .scroll((self.scroll, 0)); + + f.render_widget(paragraph, area); + } + + pub fn handle_event(&mut self, event: &Event) { + if let Event::Key(key) = event { + match key.code { + KeyCode::Down => self.scroll = self.scroll.saturating_add(1), + KeyCode::Up => self.scroll = self.scroll.saturating_sub(1), + KeyCode::PageDown => self.scroll = self.scroll.saturating_add(10), + KeyCode::PageUp => self.scroll = self.scroll.saturating_sub(10), + KeyCode::Right => self.scroll_x = self.scroll_x.saturating_add(2), + KeyCode::Left => self.scroll_x = self.scroll_x.saturating_sub(2), + _ => {} + } + } + } +} +fn parse_document(input: String) -> Vec { + let mut lines_vec = Vec::new(); + let mut in_code_block = false; + let liness: Vec = input.lines().map(String::from).collect(); + let mut i = 0; + + while i < liness.len() { + let raw = &liness[i]; + + if raw.trim().starts_with("```") { + in_code_block = !in_code_block; + let code: String = if raw.trim().replace("```", "").is_empty() { + "──".to_string() + } else { + raw.trim().replace("```", "") + }; + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + format!("────────{}────────", code), + Style::default().fg(Color::DarkGray), + )), + scrollable: false, + }); + i += 1; + continue; + } + + if in_code_block { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.to_string(), + Style::default().fg(Color::Yellow), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("### ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("### ").to_string(), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("## ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("## ").to_string(), + Style::default() + .fg(Color::LightCyan) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + if raw.starts_with("# ") { + lines_vec.push(DisplayLine { + line: Line::from(Span::styled( + raw.trim_start_matches("# ").to_string(), + Style::default() + .fg(Color::Gray) + .add_modifier(Modifier::BOLD), + )), + scrollable: false, + }); + i += 1; + continue; + } + + if raw.trim_start().starts_with("- ") { + let indent = raw.chars().take_while(|c| *c == ' ').count(); + lines_vec.push(DisplayLine { + line: Line::from(Span::raw(format!( + "{}• {}", + " ".repeat(indent), + raw.trim_start_matches("- ") + ))), + scrollable: false, + }); + i += 1; + continue; + } + + if raw.trim().starts_with('|') && raw.contains('|') { + let mut table_lines = vec![raw.clone()]; + let mut j = i + 1; + while j < liness.len() && liness[j].trim().starts_with('|') { + table_lines.push(liness[j].clone()); + j += 1; + } + + let table = parse_table(&table_lines.iter().map(|s| s.as_str()).collect::>()); + lines_vec.extend(table_to_lines(table)); + i = j; + continue; + } + + lines_vec.push(DisplayLine { + line: Line::from(parse_inline(raw.as_str())), + scrollable: false, + }); + i += 1; + } + + lines_vec +} + +fn parse_inline(input: &str) -> Vec> { + let mut spans = Vec::new(); + let mut buf = String::new(); + + let mut bold = false; + let mut underline = false; + let mut code = false; + + let mut chars = input.chars().peekable(); + + while let Some(c) = chars.next() { + let toggle = match c { + '*' if chars.peek() == Some(&'*') => { + chars.next(); + Some("bold") + } + '_' if chars.peek() == Some(&'_') => { + chars.next(); + Some("underline") + } + '`' => Some("code"), + _ => None, + }; + + if let Some(kind) = toggle { + flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); + + match kind { + "bold" => bold = !bold, + "underline" => underline = !underline, + "code" => code = !code, + _ => {} + } + continue; + } + + buf.push(c); + } + + flush_span(&mut spans, &mut buf, current_style(bold, underline, code)); + spans +} + +fn current_style(bold: bool, underline: bool, code: bool) -> Style { + let mut style = Style::default(); + + if bold { + style = style.add_modifier(Modifier::BOLD); + } + if underline { + style = style.add_modifier(Modifier::UNDERLINED); + } + if code { + style = style.fg(Color::Yellow); + } + + style +} +#[derive(Clone)] +pub struct DisplayLine { + line: Line<'static>, + scrollable: bool, +} + +fn table_to_lines(table: Vec>) -> Vec { + if table.len() < 2 { + return vec![]; + } + + 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 = header + .iter() + .enumerate() + .map(|(i, h)| { + let h_len = h.chars().count().max(1); + if i == 0 { + table + .iter() + .map(|row| row.get(i).map(|c| c.chars().count()).unwrap_or(0)) + .max() + .unwrap_or(h_len) + } else { + let max = (3 * h_len) as usize; + max.max(h_len) + } + }) + .collect(); + + let mut lines = Vec::new(); + + for (row_idx, row) in table.iter().enumerate() { + if row_idx == 1 { + let divider = widths + .iter() + .map(|w| "─".repeat(*w)) + .collect::>() + .join("─┼─"); + + lines.push(DisplayLine { + line: Line::from(Span::styled(divider, Style::default().fg(Color::DarkGray))), + scrollable: true, + }); + continue; + } + + let wrapped_cells: Vec> = row + .iter() + .enumerate() + .map(|(i, cell)| wrap_cell(cell, widths[i])) + .collect(); + + let row_height = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1); + + for line_idx in 0..row_height { + let mut line = String::new(); + + for (i, cell) in wrapped_cells.iter().enumerate() { + let content = cell.get(line_idx).map(String::as_str).unwrap_or(""); + line.push_str(&format!("{:width$}", content, width = widths[i])); + if i < wrapped_cells.len() - 1 { + line.push_str(" │ "); + } + } + + let style = if row_idx == 0 { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Green) + }; + + lines.push(DisplayLine { + line: Line::from(Span::styled(line, style)), + scrollable: true, + }); + } + } + + lines +} +fn wrap_cell(cell: &str, width: usize) -> Vec { + if width == 0 { + return vec![String::new()]; + } + + let mut lines = Vec::new(); + let mut current = String::new(); + + 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::>().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::>().chunks(width) { + lines.push(chunk.iter().collect()); + } + } + } + } + + if !current.is_empty() { + lines.push(current); + } + + if lines.is_empty() { + lines.push(String::new()); + } + + lines +} + +fn flush_span(spans: &mut Vec, buf: &mut String, style: Style) { + if !buf.is_empty() { + spans.push(Span::styled(buf.clone(), style)); + buf.clear(); + } +} + +fn parse_table(lines: &[&str]) -> Vec> { + let mut table = Vec::new(); + + for &line in lines { + if !line.starts_with('|') || !line.contains('|') { + break; + } + let row: Vec = line + .trim_matches('|') + .split('|') + .map(|s| s.trim().to_string()) + .collect(); + table.push(row); + } + + table +} diff --git a/src/gui/screens/screens.rs b/src/gui/screens/screens.rs new file mode 100644 index 0000000..9676fbb --- /dev/null +++ b/src/gui/screens/screens.rs @@ -0,0 +1,25 @@ +use std::any::Any; + +use crossterm::event::KeyEvent; +use ratatui::{Frame, layout::Rect}; + +use crate::gui::interaction_result::InteractionResult; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NavDirection { + Up, + Down, + Left, + Right, + + Next, + Prev, +} + +pub trait Screen: Send + Sync + Any { + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; + + fn render(&self, f: &mut Frame, rect: Rect); + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; +} diff --git a/src/gui/screens/terms_checker.rs b/src/gui/screens/terms_checker.rs new file mode 100644 index 0000000..26bb7c6 --- /dev/null +++ b/src/gui/screens/terms_checker.rs @@ -0,0 +1,351 @@ +use crate::{ + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + ui::UI, + }, + terms::{ + buttons::{checkbox, draw_buttons}, + consent_state::UserChoice, + focus::Focus, + terms_getter::{Type, get_link, get_terms}, + }, +}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::{any::Any, pin::Pin, sync::Arc}; +use tokio::sync::oneshot; + +pub struct TermsCheckerScreen { + ui: Arc, + sender: Option>, + + eula: bool, + tos: bool, + pp: bool, + + focus: Focus, +} + +impl TermsCheckerScreen { + pub fn new(ui: Arc, sender: Option>) -> Self { + Self { + ui, + sender, + eula: false, + tos: false, + pp: false, + focus: Focus::Eula, + } + } +} + +impl Screen for TermsCheckerScreen { + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, size: Rect) { + let mut needed_height = 5; + + if size.height < 6 || size.width < 27 { + f.render_widget( + Line::from(format!("too small {}/63 by {}/13", size.width, size.height)), + size, + ); + return; + } + + let max_width = 150; + let max_height = 26; + + let content_width = if max_width < size.width { + max_width + } else { + size.width + }; + let content_height = if max_height < size.height { + max_height + } else { + size.height + }; + + let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; + let vertical_margin = (size.height.saturating_sub(content_height)) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .split(Rect { + x: horizontal_margin, + y: vertical_margin, + width: content_width, + height: content_height, + }); + let eula_text = if size.width < 70 { + "EULA ¹ (https://legal.tensamin.net/eula/)" + } 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/)" + } 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/)" + } else { + "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)" + }; + + let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = if size.width > 143 { + ( + vec![8, 3, 8, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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 > 92 { + ( + vec![9, 3, 9, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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![9, 3, 9, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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.", + ], + ) + } else { + ( + vec![10, 3, 11, 5], + vec![ + "", + "By selecting Continue, you confirm that you 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.", + ], + ) + }; + let mut text_lines = vec![ + checkbox(eula_text, self.eula, self.focus == Focus::Eula, true), + checkbox(tos_text, self.tos, self.focus == Focus::Tos, self.eula), + checkbox(pp_text, self.pp, self.focus == Focus::Pp, self.eula), + Line::from(""), + Line::from("¹ Necessary– required to run the program"), + Line::from("² Optional – required only for Tensamin services"), + ]; + for line in agree_lines { + text_lines.insert(text_lines.len(), Line::from(line)); + } + + while size.height - 5 < text_lines.len() as u16 { + if optional_lines.len() == 0 { + break; + } + text_lines.remove(optional_lines[0] as usize); + optional_lines.remove(0); + } + needed_height += text_lines.len(); + + if size.width < 60 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 60 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height > 19 { + Style::default().fg(Color::Green) + } else if size.height >= 13 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 60"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / 13")), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("UI Too Small [Q to Quit]")), + ); + f.render_widget(warning, size); + return; + } + + let consent_block = Paragraph::new(Text::from(text_lines)).block( + Block::default() + .title(format!(" Tensamin User Consent [Q to Quit] ",)) + .borders(Borders::ALL), + ); + f.render_widget(consent_block, chunks[0]); + + draw_buttons( + f, + chunks[1], + self.focus, + (self.eula, self.tos && self.pp), + true, + false, + true, + ); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + let mut possible_states = vec![Focus::Eula, Focus::Tos, Focus::Pp, Focus::Cancel]; + + if self.eula { + possible_states.push(Focus::Continue); + if self.tos && self.pp { + possible_states.push(Focus::ContinueAll); + } + } + + match event.code { + KeyCode::Esc => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + KeyCode::Up | KeyCode::Left => { + self.focus.prev(&possible_states); + InteractionResult::Handled + } + KeyCode::Down | KeyCode::Right | KeyCode::Tab => { + self.focus.next(&possible_states); + InteractionResult::Handled + } + KeyCode::Char('q') | KeyCode::Char('Q') => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + KeyCode::Char('o') | KeyCode::Char('O') => { + let terms_type = match self.focus { + Focus::Eula => Some(Type::EULA), + Focus::Tos => Some(Type::TOS), + Focus::Pp => Some(Type::PP), + _ => None, + }; + if let Some(terms_type) = terms_type { + let fut: Pin> + Send>> = + Box::pin(async move { + let content = get_terms(terms_type.clone()).await.unwrap(); + let screen: FileViewer = + FileViewer::new(terms_type.to_string(), &content); + Box::new(screen) as Box + }); + InteractionResult::OpenFutureScreen { screen: fut } + } else { + InteractionResult::Unhandled + } + } + KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { + Focus::Eula => { + let _ = open::that(get_link(Type::EULA)); + InteractionResult::Handled + } + Focus::Tos => { + let _ = open::that(get_link(Type::TOS)); + InteractionResult::Handled + } + Focus::Pp => { + let _ = open::that(get_link(Type::PP)); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Enter | KeyCode::Char(' ') => match self.focus { + Focus::Eula => { + self.eula = !self.eula; + self.tos = false; + self.pp = false; + InteractionResult::Handled + } + Focus::Tos if self.eula => { + self.tos = !self.tos; + InteractionResult::Handled + } + Focus::Pp if self.eula => { + self.pp = !self.pp; + InteractionResult::Handled + } + Focus::Cancel => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + Focus::Continue if self.eula => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptEULA); + } + InteractionResult::CloseScreen + } + Focus::ContinueAll if self.eula && self.tos && self.pp => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptAll); + } + InteractionResult::CloseScreen + } + _ => InteractionResult::Unhandled, + }, + + _ => InteractionResult::Unhandled, + } + } +} diff --git a/src/gui/screens/terms_updater.rs b/src/gui/screens/terms_updater.rs new file mode 100644 index 0000000..35da364 --- /dev/null +++ b/src/gui/screens/terms_updater.rs @@ -0,0 +1,691 @@ +use crate::{ + gui::{ + interaction_result::InteractionResult, + screens::{md_viewer::FileViewer, screens::Screen}, + }, + terms::{ + buttons::{checkbox, draw_buttons}, + consent_state::{UpdateDecision, UserChoice}, + doc::Doc, + focus::Focus, + terms_getter::{Type, get_newest_link, get_terms}, + }, +}; +use chrono::{Local, TimeZone, Utc}; +use crossterm::event::{KeyCode, KeyEvent}; +use ratatui::{ + Frame, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph}, +}; +use std::any::Any; +use tokio::sync::oneshot; + +pub struct TermsUpdaterScreen { + sender: Option>, + + eula_needed: bool, + tos_needed: bool, + pp_needed: bool, + + eula_future: bool, + tos_future: bool, + pp_future: bool, + + eula_for_future: Option, + tos_for_future: Option, + pp_for_future: Option, + + update_needed: bool, + + eula: bool, + tos: bool, + pp: bool, + + focus: Focus, +} +impl TermsUpdaterScreen { + pub fn new( + consent_eula: UpdateDecision, + consent_tos: UpdateDecision, + consent_pp: UpdateDecision, + sender: Option>, + ) -> Self { + let (eula_needed, eula_future, eula_for_future): (bool, bool, Option) = + match consent_eula { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + let (tos_needed, tos_future, tos_for_future): (bool, bool, Option) = match consent_tos + { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + let (pp_needed, pp_future, pp_for_future): (bool, bool, Option) = match consent_pp { + UpdateDecision::NoChange => (false, false, None), + UpdateDecision::Future { newest } => (true, true, Some(newest)), + UpdateDecision::Forced(doc) => (true, false, Some(doc)), + }; + + let focus = if eula_needed { + Focus::Eula + } else if tos_needed { + Focus::Tos + } else if pp_needed { + Focus::Pp + } else { + Focus::Cancel + }; + + let update_needed = (eula_needed && !eula_future) + || (tos_needed && !tos_future) + || (pp_needed && !pp_future); + + Self { + sender, + + eula_needed, + tos_needed, + pp_needed, + + eula_for_future, + tos_for_future, + pp_for_future, + + eula_future, + tos_future, + pp_future, + + update_needed, + + eula: !eula_needed, + tos: !tos_needed, + pp: !pp_needed, + + focus, + } + } +} +impl Screen for TermsUpdaterScreen { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn render(&self, f: &mut Frame, size: Rect) { + let mut needed_height = 5; + + if size.height < 6 || size.width < 27 { + f.render_widget( + Line::from(format!("too small {}/63 by {}/15", size.width, size.height)), + size, + ); + return; + } + + let max_width = 150; + let max_height = 30; + + let content_width = if max_width < size.width { + max_width + } else { + size.width + }; + let content_height = if max_height < size.height { + max_height + } else { + size.height + }; + + let horizontal_margin = (size.width.saturating_sub(content_width)) / 2; + let vertical_margin = (size.height.saturating_sub(content_height)) / 2; + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(7), Constraint::Length(3)]) + .split(Rect { + x: horizontal_margin, + y: vertical_margin, + width: content_width, + height: content_height, + }); + + let mut text_lines: Vec = Vec::new(); + let mut header_lines = 0; + let separator = if size.width > 72 { + header_lines += 3; + text_lines.push(Line::from( + "You previously accepted earlier versions of Tensamin’s legal documents.", + )); + text_lines.push(Line::from( + "Some of them have been updated and are listed below for your review.", + )); + text_lines.push(Line::from("")); + 2 + } else { + header_lines += 5; + text_lines.push(Line::from("You previously accepted earlier ")); + text_lines.push(Line::from("versions of Tensamin’s legal documents.")); + text_lines.push(Line::from("Some of them have been updated and")); + text_lines.push(Line::from("are listed below for your review.")); + text_lines.push(Line::from("")); + 4 + }; + + if self.eula_needed { + header_lines += 1; + if self.eula_future { + if size.width < 80 { + text_lines.push(checkbox( + "EULA ¹³ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + + header_lines += 1; + let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + + header_lines += 1; + let unix_timestamp = self.eula_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "EULA ¹ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + } else { + text_lines.push(checkbox( + "End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)", + self.eula, + self.focus == Focus::Eula, + true, + )); + } + } + } + + if self.tos_needed { + header_lines += 1; + if self.tos_future { + if size.width < 80 { + text_lines.push(checkbox( + "ToS ²³ (https://legal.tensamin.net/tos/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.tos_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "ToS ² (https://legal.tensamin.net/tos/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + } else { + text_lines.push(checkbox( + "Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)", + self.tos, + self.focus == Focus::Tos, + self.eula, + )); + } + } + } + + if self.pp_needed { + header_lines += 1; + if self.pp_future { + if size.width < 80 { + text_lines.push(checkbox( + "PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } else { + text_lines.push(checkbox( + "Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + + header_lines += 1; + let unix_timestamp = self.pp_for_future.clone().unwrap().get_time() as i64; + let datetime = Utc.timestamp_opt(unix_timestamp, 0).single().unwrap(); + let date = datetime.with_timezone(&Local).format("%Y-%m-%d at %H:%M"); + text_lines.push(Line::from(format!(" Goes into effect on {}", date))); + } + } else { + if size.width < 80 { + text_lines.push(checkbox( + "PP ² (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + } else { + text_lines.push(checkbox( + "Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)", + self.pp, + self.focus == Focus::Pp, + self.eula, + )); + } + } + } + + text_lines.push(Line::from("")); + text_lines.push(Line::from("¹ Necessary to run the program")); + text_lines.push(Line::from("² Optional - only for Tensamin services")); + if size.width < 100 { + text_lines.push(Line::from( + "³ Future version - consent stored now, takes effect later", + )); + } else { + text_lines.push(Line::from("³ Future version - You’ll continue using this version, automatically updated when changes apply.")); + } + + text_lines.push(Line::from("")); + + let mut optional_lines: Vec = if size.width > 143 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from("By selecting Downgrade, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from("By selecting Continue, you confirm that you agree to the End User License Agreement and applicable Terms of Service.")); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 6, + header_lines, + header_lines + 6, + separator, + header_lines + 2, + ] + } + } else if size.width > 92 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you agree to the End User", + )); + text_lines.push(Line::from( + "License Agreement and applicable Terms of Service.", + )); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you agree to the End User", + )); + text_lines.push(Line::from( + "License Agreement and applicable Terms of Service.", + )); + } + + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from("While having a document selected press O to view in this UI or press L to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 8, + header_lines, + header_lines + 8, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } + } else if size.width > 73 { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you read, understood and", + )); + text_lines.push(Line::from( + "agree to the End User License Agreement and applicable Terms of Service.", + )); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate once these changes apply.", + )); + } else { + text_lines.push(Line::from( + "By selecting Continue, you confirm that you read, understood and", + )); + text_lines.push(Line::from( + "agree to the End User License Agreement and applicable Terms of Service.", + )); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "Tensamin services require acceptance of the ToS and Privacy Policy.", + )); + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "While having a document selected press O to view in this UI or press L", + )); + text_lines.push(Line::from("to open as a link.")); + + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 8, + header_lines, + header_lines + 8, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 7, + header_lines, + header_lines + 7, + separator, + header_lines + 2, + ] + } + } else { + if self.tos_needed || self.pp_needed { + text_lines.push(Line::from("By selecting Continue, you confirm that you")); + text_lines.push(Line::from("agree to the End User License")); + text_lines.push(Line::from("Agreement and applicable Terms of Service.")); + text_lines.push(Line::from( + "On Downgrade: Tensamin Services will deactivate", + )); + text_lines.push(Line::from("once these changes apply.")); + } else { + text_lines.push(Line::from("By selecting Continue, you confirm that you")); + text_lines.push(Line::from("agree to the End User License")); + text_lines.push(Line::from("Agreement and applicable Terms of Service.")); + } + text_lines.push(Line::from("")); + text_lines.push(Line::from("Tensamin services require acceptance of the")); + text_lines.push(Line::from("Terms of Service and Privacy Policy.")); + text_lines.push(Line::from("")); + text_lines.push(Line::from( + "While having a document selected press O to view", + )); + text_lines.push(Line::from("in this UI or press L to open as a link.")); + if self.tos_needed || self.pp_needed { + vec![ + header_lines + 10, + header_lines, + header_lines + 11, + separator, + header_lines + 2, + ] + } else { + vec![ + header_lines + 8, + header_lines, + header_lines + 9, + separator, + header_lines + 2, + ] + } + }; + + while size.height - 5 < text_lines.len() as u16 { + if optional_lines.len() == 0 { + break; + } + text_lines.remove(optional_lines[0] as usize); + optional_lines.remove(0); + } + needed_height += text_lines.len(); + + let q_informer = if self.update_needed { + "Q to Exit" + } else { + "Q to Cancel" + }; + if size.width < 60 || size.height < needed_height as u16 { + let width_style = if size.width > 76 { + Style::default().fg(Color::Green) + } else if size.width >= 60 { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let height_style = if size.height > 20 { + Style::default().fg(Color::Green) + } else if size.height >= (header_lines as u16 + 10) { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Red) + }; + + let warning_text = Text::from(vec![ + Line::from(vec![ + Span::raw("Width: "), + Span::styled(format!("{}", size.width), width_style), + Span::raw(" / 60"), + ]), + Line::from(vec![ + Span::raw("Height: "), + Span::styled(format!("{}", size.height), height_style), + Span::raw(format!(" / {}", header_lines + 10)), + ]), + ]); + + let warning = Paragraph::new(warning_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(format!("UI Too Small [{}]", q_informer)), + ); + + f.render_widget(warning, size); + return; + } + + let consent_block = Paragraph::new(Text::from(text_lines)).block( + Block::default() + .title(format!(" Update Tensamin User Consent [{}] ", q_informer)) + .borders(Borders::ALL), + ); + f.render_widget(consent_block, chunks[0]); + + let downgrade_scenario = self.tos_needed || self.pp_needed; + draw_buttons( + f, + chunks[1], + self.focus, + (self.eula, self.tos && self.pp), + self.update_needed, + downgrade_scenario, + self.pp_needed || self.tos_needed, + ); + } + + fn handle_input(&mut self, event: KeyEvent) -> InteractionResult { + let mut possible_states = Vec::new(); + + if self.eula_needed { + possible_states.push(Focus::Eula); + } + if self.tos_needed { + possible_states.push(Focus::Tos); + } + if self.pp_needed { + possible_states.push(Focus::Pp); + } + + possible_states.push(Focus::Cancel); + + if self.eula { + possible_states.push(Focus::Continue); + if self.tos && self.pp { + possible_states.push(Focus::ContinueAll); + } + } + + match event.code { + KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + return InteractionResult::CloseScreen; + } + KeyCode::Enter | KeyCode::Char(' ') => match self.focus { + Focus::Eula => { + self.eula = !self.eula; + self.tos = !self.tos_needed; + self.pp = !self.pp_needed; + InteractionResult::Handled + } + Focus::Tos if self.eula => { + self.tos = !self.tos; + InteractionResult::Handled + } + Focus::Pp if self.eula => { + self.pp = !self.pp; + InteractionResult::Handled + } + Focus::Cancel => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::Deny); + } + InteractionResult::CloseScreen + } + Focus::Continue if self.eula => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptEULA); + } + InteractionResult::CloseScreen + } + Focus::ContinueAll if self.eula && self.tos && self.pp => { + if let Some(sender) = self.sender.take() { + let _ = sender.send(UserChoice::AcceptAll); + } + InteractionResult::CloseScreen + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Char('o') | KeyCode::Char('O') => { + let terms_type = match self.focus { + Focus::Eula => Some(Type::EULA), + Focus::Tos => Some(Type::TOS), + Focus::Pp => Some(Type::PP), + _ => None, + }; + + if let Some(terms_type) = terms_type { + let fut = Box::pin(async move { + let content = get_terms(terms_type.clone()).await.unwrap(); + Box::new(FileViewer::new(terms_type.to_string(), &content)) + as Box + }); + + return InteractionResult::OpenFutureScreen { screen: fut }; + } else { + InteractionResult::Unhandled + } + } + KeyCode::Char('l') | KeyCode::Char('L') => match self.focus { + Focus::Eula => { + let _ = open::that(get_newest_link(Type::EULA)); + InteractionResult::Handled + } + Focus::Tos => { + let _ = open::that(get_newest_link(Type::TOS)); + InteractionResult::Handled + } + Focus::Pp => { + let _ = open::that(get_newest_link(Type::PP)); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + }, + KeyCode::Up | KeyCode::Left => { + self.focus.prev(&possible_states); + InteractionResult::Handled + } + KeyCode::Down | KeyCode::Right | KeyCode::Tab => { + self.focus.next(&possible_states); + InteractionResult::Handled + } + _ => InteractionResult::Unhandled, + } + } +} diff --git a/src/gui/tui.rs b/src/gui/tui.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/gui/ui.rs b/src/gui/ui.rs new file mode 100644 index 0000000..5903a4b --- /dev/null +++ b/src/gui/ui.rs @@ -0,0 +1,167 @@ +use crate::{ + ACTIVE_TASKS, SHUTDOWN, + gui::{ + input_handler::setup_input_handler, interaction_result::InteractionResult, + screens::screens::Screen, + }, +}; +use crossterm::event::KeyEvent; +use once_cell::sync::Lazy; +use ratatui::{Terminal, backend::CrosstermBackend, init}; +use std::{ + collections::VecDeque, + io::Stdout, + sync::{ + Arc, Mutex, + atomic::{AtomicBool, Ordering}, + }, + time::Duration, +}; +use tokio::{sync::RwLock, time::Instant}; + +/// UI state and rendering +pub static UNIQUE: AtomicBool = AtomicBool::new(true); + +pub static FPS: Lazy> = Lazy::new(|| RwLock::new((0.0, 0.0))); + +pub struct UI { + pub terminal: Arc>>>, + screen_stack: Arc>>>, +} + +pub fn start_tui() -> Arc { + let ui = Arc::new(UI::new()); + let uic = ui.clone(); + ACTIVE_TASKS.insert("UI Renderer".to_string()); + tokio::spawn(async move { + let mut last_render = Instant::now(); + + let mut fps_samples: VecDeque = VecDeque::with_capacity(20); + let mut skip_samples: VecDeque = VecDeque::with_capacity(20); + + let mut fps_sum = 0.0; + let mut skip_sum: u32 = 0; + + let mut skipped = 0; + + loop { + if *SHUTDOWN.read().await { + break; + } + + if skipped > 5 || UNIQUE.load(Ordering::Relaxed) { + uic.render().await; + + skip_samples.push_back(skipped); + skip_sum += skipped as u32; + + if skip_samples.len() > 20 { + if let Some(old) = skip_samples.pop_front() { + skip_sum -= old as u32; + } + } + + skipped = 0; + + let elapsed = last_render.elapsed().as_secs_f64(); + if elapsed > 0.0 { + let fps = 1.0 / elapsed; + + fps_samples.push_back(fps); + fps_sum += fps; + + if fps_samples.len() > 20 { + if let Some(old) = fps_samples.pop_front() { + fps_sum -= old; + } + } + } + + let avg_fps = if !fps_samples.is_empty() { + fps_sum / fps_samples.len() as f64 + } else { + 0.0 + }; + + let avg_skips_percentage = if !skip_samples.is_empty() { + let avg_skipped = skip_sum as f64 / skip_samples.len() as f64; + let total_iterations = avg_skipped + 1.0; + (avg_skipped / total_iterations) * 100.0 + } else { + 0.0 + }; + + *FPS.write().await = (avg_fps, avg_skips_percentage); + + last_render = Instant::now(); + UNIQUE.store(false, Ordering::Relaxed); + } else { + skipped += 1; + } + tokio::time::sleep(Duration::from_millis(16)).await; + } + ACTIVE_TASKS.remove("UI Renderer"); + ratatui::restore(); + }); + setup_input_handler(ui.clone()); + ui +} +impl UI { + pub fn new() -> Self { + let terminal = init(); + Self { + terminal: Arc::new(Mutex::new(terminal)), + screen_stack: Arc::new(RwLock::new(Vec::new())), + } + } + + pub async fn set_screen(&self, screen: Box) { + self.screen_stack.write().await.push(screen); + } + pub async fn replace_screen(&self, screen: Box) { + let mut stack = self.screen_stack.write().await; + stack.pop(); + stack.push(screen); + } + pub async fn handle_input(self: Arc, key_event: KeyEvent) { + let result = { + let mut stack = self.screen_stack.write().await; + if let Some(screen) = stack.last_mut() { + screen.handle_input(key_event) + } else { + return; + } + }; + match result { + InteractionResult::OpenScreen { screen } => { + self.set_screen(screen).await; + } + InteractionResult::OpenFutureScreen { screen: fut } => { + let ui = self.clone(); + let screen = fut.await; + ui.set_screen(screen).await; + } + InteractionResult::CloseScreen => { + let mut stack = self.screen_stack.write().await; + stack.pop(); + + if stack.is_empty() { + *SHUTDOWN.write().await = true; + } + } + InteractionResult::Handled => {} + InteractionResult::Unhandled => {} + } + } + + pub async fn render(&self) { + if let Some(screen) = self.screen_stack.read().await.last() { + let mut terminal = self.terminal.lock().unwrap(); + terminal + .draw(|f| { + screen.render(f, f.area()); + }) + .unwrap(); + } + } +} diff --git a/src/gui/util/borders.rs b/src/gui/util/borders.rs new file mode 100644 index 0000000..34460bc --- /dev/null +++ b/src/gui/util/borders.rs @@ -0,0 +1,63 @@ +use ratatui::layout::Rect; +use ratatui::prelude::*; +use ratatui::style::Style; +use ratatui::widgets::Borders; + +fn set_join_char(frame: &mut Frame, x: u16, y: u16, c: char) { + frame + .buffer_mut() + .set_string(x, y, c.to_string(), Style::default()); +} + +pub fn draw_block_joins(frame: &mut Frame, area: Rect, borders: Borders, joins: Borders) { + let x0 = area.x; + let y0 = area.y; + let x1 = area.x + area.width - 1; + let y1 = area.y + area.height - 1; + + if borders.contains(Borders::TOP) && borders.contains(Borders::LEFT) { + let top_left = match (joins.contains(Borders::TOP), joins.contains(Borders::LEFT)) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┬', + (false, false) => '┌', + }; + set_join_char(frame, x0, y0, top_left); + } + + if borders.contains(Borders::TOP) && borders.contains(Borders::RIGHT) { + let top_right = match (joins.contains(Borders::TOP), joins.contains(Borders::RIGHT)) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┬', + (false, false) => '┐', + }; + set_join_char(frame, x1, y0, top_right); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::LEFT) { + let bottom_left = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::LEFT), + ) { + (true, true) => '┼', + (true, false) => '├', + (false, true) => '┴', + (false, false) => '└', + }; + set_join_char(frame, x0, y1, bottom_left); + } + + if borders.contains(Borders::BOTTOM) && borders.contains(Borders::RIGHT) { + let bottom_right = match ( + joins.contains(Borders::BOTTOM), + joins.contains(Borders::RIGHT), + ) { + (true, true) => '┼', + (true, false) => '┤', + (false, true) => '┴', + (false, false) => '┘', + }; + set_join_char(frame, x1, y1, bottom_right); + } +} diff --git a/src/langu/language_creator.rs b/src/langu/language_creator.rs new file mode 100644 index 0000000..fc6d1c5 --- /dev/null +++ b/src/langu/language_creator.rs @@ -0,0 +1,80 @@ +use crate::util::file_util::save_file; +use json::{self, JsonError, JsonValue}; + +pub fn create_languages() -> Result<(), JsonError> { + let mut frontend_messages = JsonValue::new_object(); + let mut omikron_messages = JsonValue::new_object(); + let mut button_texts = JsonValue::new_object(); + let mut general_texts = JsonValue::new_object(); + let mut debug_messages = JsonValue::new_object(); + + frontend_messages.insert("error", "An error occurred")?; + // FRONTEND + frontend_messages.insert("get_chats", "User {} is loading conversations")?; + frontend_messages.insert("message_get", "User {} is loading messages")?; + frontend_messages.insert("get_communities", "User {} is loading communities")?; + frontend_messages.insert("client_connected", "Client {} connected")?; + frontend_messages.insert("add_conversation", "User {} added {}")?; + frontend_messages.insert("message_send", "User {} sent a message")?; + + // OMIKRON + omikron_messages.insert( + "identification_response", + "IOTA identified on Omikron, {} users!", + )?; + omikron_messages.insert( + "send_message_failed", + "Failed to send message to Omikron: {}", + )?; + omikron_messages.insert("connection_failed", "Failed to connect to Omikron: {}")?; + + // BUTTONS + button_texts.insert("exit", "Exit")?; + + // GENERAL + general_texts.insert("iota_id", "IOTA ID: {}-####-####-####-############")?; + general_texts.insert("user_id", "USER ID: {}")?; + general_texts.insert("user_ids", "USER IDS: {}")?; + general_texts.insert("user_load_failed", "Failed to load user data")?; + general_texts.insert("setup_completed", "Launched")?; + general_texts.insert( + "community_active", + "Communities active on ws://{}:{}/community/...", + )?; + general_texts.insert( + "community_start_error", + "Failed to start community socket on port {}!", + )?; + general_texts.insert( + "community_start_error_admin", + "Failed to start community socket on port {}! Run with admin privileges", + )?; + // DEBUG + debug_messages.insert("", "")?; + save_file( + "languages/en_INT", + "frontend.json", + &frontend_messages.to_string(), + ); + save_file( + "languages/en_INT", + "omikron.json", + &omikron_messages.to_string(), + ); + save_file( + "languages/en_INT", + "buttons.json", + &button_texts.to_string(), + ); + save_file( + "languages/en_INT", + "debug.json", + &debug_messages.to_string(), + ); + save_file( + "languages/en_INT", + "general.json", + &general_texts.to_string(), + ); + Ok(()) +} diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs new file mode 100644 index 0000000..c903886 --- /dev/null +++ b/src/langu/language_manager.rs @@ -0,0 +1,105 @@ +use crate::util::file_util::{self}; +use json::parse; +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +#[derive(Clone)] +pub struct LanguagePack { + language: HashMap, +} + +pub static LANGUAGE_PACK: Lazy> = + 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 { + 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); +} + +pub fn from_key(key: &str) -> String { + LANGUAGE_PACK + .lock() + .unwrap() + .get_translation(key) + .to_string() +} + +pub fn format(key: &str, args: &[&str]) -> String { + let message = from_key(key); + let mut formatted = String::new(); + let parts = message.split("{}"); + for (i, part) in parts.enumerate() { + formatted.push_str(part); + if i < args.len() { + formatted.push_str(args[i]); + } + } + formatted +} + +impl LanguagePack { + pub fn new(language: &str) -> Self { + let mut pack = LanguagePack { + language: HashMap::new(), + }; + pack.load_language(language); + pack + } + + pub fn load_language(&mut self, language: &str) { + let path = format!("languages/{}/", language); + + let frontend_messages = file_util::load_file(&path, "frontend.json"); + let frontend_messages = parse(&frontend_messages).unwrap(); + for (key, value) in frontend_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let omikron_messages = file_util::load_file(&path, "omikron.json"); + let omikron_messages = parse(&omikron_messages).unwrap(); + for (key, value) in omikron_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let button_texts = file_util::load_file(&path, "buttons.json"); + let button_texts = parse(&button_texts).unwrap(); + for (key, value) in button_texts.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let debug_messages = file_util::load_file(&path, "debug.json"); + let debug_messages = parse(&debug_messages).unwrap(); + for (key, value) in debug_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + + let general_messages = file_util::load_file(&path, "general.json"); + let general_messages = parse(&general_messages).unwrap(); + for (key, value) in general_messages.entries() { + self.language + .insert(key.to_string(), value.as_str().unwrap().to_string()); + } + } + pub fn get_translation(&self, key: &str) -> String { + match self.language.get(key) { + Some(v) if !v.is_empty() => v.clone(), + _ => key.to_uppercase(), + } + } +} diff --git a/src/langu/mod.rs b/src/langu/mod.rs new file mode 100644 index 0000000..352d639 --- /dev/null +++ b/src/langu/mod.rs @@ -0,0 +1,2 @@ +pub mod language_creator; +pub mod language_manager; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..767106f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,183 @@ +use dashmap::DashSet; +use once_cell::sync::Lazy; +use pnet::datalink::NetworkInterface; +use std::sync::Arc; +use std::sync::LazyLock; +use std::sync::Mutex; +use tokio::sync::RwLock; +use tokio::time::{Duration, sleep}; + +mod auth; +mod gui; +mod langu; +mod omikron; +mod terms; +mod users; +mod util; + +use crate::gui::app_state; +use crate::gui::app_state::AppState; +use crate::gui::screens::main_screen::MainScreen; +use crate::gui::ui::start_tui; +use crate::langu::language_creator; +use crate::omikron::omikron_connection::OmikronConnection; +use crate::terms::consent_state; +use crate::users::user_manager; +use crate::util::config_util::CONFIG; +use crate::util::file_util::download_and_extract_zip; +use crate::util::file_util::has_dir; +use crate::util::logger; + +pub static APP_STATE: LazyLock>> = + LazyLock::new(|| Arc::new(Mutex::new(AppState::new()))); + +pub static SHUTDOWN: Lazy> = Lazy::new(|| RwLock::new(false)); +pub static RELOAD: Lazy> = Lazy::new(|| RwLock::new(true)); +pub static ACTIVE_TASKS: Lazy> = Lazy::new(|| DashSet::new()); + +#[tokio::main(flavor = "multi_thread", worker_threads = 16)] +#[allow(unused_must_use, dead_code)] +async fn main() { + while *RELOAD.read().await { + *RELOAD.write().await = false; + *SHUTDOWN.write().await = false; + + let ui = start_tui(); + + let (eula, tos_pp) = consent_state::check(ui.clone()).await; + + if !eula { + *SHUTDOWN.write().await = true; + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_millis(100)).await; + } + println!("You need to accept our End User Licence Agreement before launching!"); + println!("You can find this at 'agreements'!"); + return; + } + if !tos_pp { + *SHUTDOWN.write().await = true; + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_millis(100)).await; + } + println!( + "Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!" + ); + println!("In future releases this will be optional!"); + println!("You can find this at 'agreements'!"); + return; + } + app_state::setup(); + + let main_screen = MainScreen::new(ui.clone()).await; + ui.set_screen(Box::new(main_screen)).await; + + // LANGUAGE PACK + if let Err(e) = language_creator::create_languages() { + println!("Language pack creation failed: {}", e); + return; + } + + // UI + logger::startup(); + + // BASIC CONFIGURATION + &CONFIG.write().await.load(); + + // USER MANAGEMENT + if let Err(_) = user_manager::load_users().await { + log_t!("user_load_failed"); + } + + let mut sb = "".to_string(); + + for up in user_manager::get_users() { + sb = sb + "," + &up.user_id.to_string().as_str(); + } + + if !sb.is_empty() { + {} + sb.remove(0); + sb = sb + ","; + } + log!( + "IOTA ID: {}", + CONFIG.read().await.get_iota_id().to_string() + ); + log!("User IDS: {}", sb); + + // COMMUNITY MANAGEMENT + /* registry::load_interactables().await; + community_manager::load_communities().await; + community_manager::save_communities().await; + let mut sb1 = "".to_string(); + for cp in community_manager::get_communities().await { + sb1 = sb1 + "," + &cp.get_name().to_string().as_str(); + } + + if !sb1.is_empty() { + sb1.remove(0); + sb1 = sb1 + ","; + } + log!("Community IDS: {}", sb1); */ + let port = CONFIG.read().await.get_port(); + let mut ip = "0.0.0.0".to_string(); + for iface in pnet::datalink::interfaces() { + let iface: NetworkInterface = iface; + if iface.ips.len() > 0 { + let ipsv = format!("{}", iface.ips[0]); + let ips: &str = ipsv.split('/').next().unwrap_or(""); + if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") { + ip = ips.to_string(); + } + } + } + /* + if start(port).await { + log_t!("community_active", ip, port.to_string()); + } else { + if port < 1024 { + log_t!("community_start_error_admin", port.to_string()); + } else { + log_t!("community_start_error", port.to_string()); + } + } */ + if !has_dir("web") { + download_and_extract_zip( + "https://omega.tensamin.net/api/download/iota_frontend", + "web", + ) + .await; + } + let _ = omikron::omikron_connection::get_omikron_connection().await; + + log_t!("setup_completed"); + loop { + if *SHUTDOWN.read().await { + break; + } + + sleep(Duration::from_millis(100)).await; + } + if *RELOAD.read().await { + loop { + if ACTIVE_TASKS.is_empty() { + break; + } + sleep(Duration::from_secs(1)).await; + } + &CONFIG.write().await.clear(); + user_manager::clear(); + /*community_manager::clear();*/ + *APP_STATE.lock().unwrap() = AppState::new(); + } + ui.terminal.lock().unwrap().clear(); + ui.terminal.lock().unwrap().flush(); + } +} diff --git a/src/omikron/mod.rs b/src/omikron/mod.rs new file mode 100644 index 0000000..acb0f59 --- /dev/null +++ b/src/omikron/mod.rs @@ -0,0 +1,2 @@ +pub mod omikron_connection; +pub mod ping_pong_task; diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs new file mode 100755 index 0000000..7e92269 --- /dev/null +++ b/src/omikron/omikron_connection.rs @@ -0,0 +1,1159 @@ +use crate::users::contact::Contact; +use crate::util::chat_files::{MessageState, change_message_state}; +use crate::util::chats_util::{get_user, mod_user}; +use crate::util::communities_util::CommunitiesUtil; +use crate::util::crypto_util::{DataFormat, SecurePayload}; +use crate::util::file_util::{get_children, load_file, save_file}; +use crate::util::{chat_files, chats_util}; +use crate::util::{config_util::CONFIG, crypto_helper}; +use crate::{ACTIVE_TASKS, SHUTDOWN, log, log_cv_in, log_cv_out, log_t}; +use dashmap::DashMap; +use json::JsonValue; +use std::collections::HashMap; +use std::sync::{Arc, LazyLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use tokio::sync::{Mutex, RwLock, mpsc, watch}; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use ttp_native::{Receiver, Sender}; +use uuid::Uuid; + +// ============================================================================ +// Configuration +// ============================================================================ + +const OMIKRON_HOST_DEFAULT: &str = "methanium.net"; +const OMIKRON_PORT_DEFAULT: u16 = 959; +const RECONNECT_DELAY: Duration = Duration::from_secs(5); +const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300); +const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10); +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); +const TASK_MAX_AGE: Duration = Duration::from_secs(60); + +// ============================================================================ +// Waiting Task System +// ============================================================================ + +pub struct WaitingTask { + pub task: Box, CommunicationValue) -> bool + Send + Sync>, + pub inserted_at: Instant, +} + +pub static WAITING_TASKS: LazyLock> = LazyLock::new(|| DashMap::new()); + +pub fn start_task_cleanup_loop() { + tokio::spawn(async { + loop { + sleep(TASK_CLEANUP_INTERVAL).await; + WAITING_TASKS.retain(|_, v| v.inserted_at.elapsed() < TASK_MAX_AGE); + } + }); +} + +// ============================================================================ +// Connection State +// ============================================================================ + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ConnectionState { + Disconnected, + Connecting, + Connected { identified: bool }, +} + +impl ConnectionState { + pub fn is_connected(&self) -> bool { + matches!(self, ConnectionState::Connected { .. }) + } + + pub fn is_identified(&self) -> bool { + matches!(self, ConnectionState::Connected { identified: true }) + } +} + +// ============================================================================ +// Omikron Connection (Client-side with auto-reconnect) +// ============================================================================ + +pub struct OmikronConnection { + state: Arc>, + sender: Arc>>>, + connection_loop_handle: Arc>>>, + host: String, + port: u16, + pub last_ping: Arc>, + heartbeat_handle: Arc>>>, + message_send_times: Arc>>, + pub connection_id: Uuid, + shutdown_tx: Arc>>>, + reconnect_on_close: Arc>, +} + +impl OmikronConnection { + pub fn new() -> Self { + Self::with_host(OMIKRON_HOST_DEFAULT, OMIKRON_PORT_DEFAULT) + } + + pub fn with_host(host: &str, port: u16) -> Self { + let (shutdown_tx, _) = watch::channel(false); + + OmikronConnection { + state: Arc::new(RwLock::new(ConnectionState::Disconnected)), + sender: Arc::new(RwLock::new(None)), + connection_loop_handle: Arc::new(Mutex::new(None)), + host: host.to_string(), + port, + last_ping: Arc::new(Mutex::new(-1)), + heartbeat_handle: Arc::new(Mutex::new(None)), + message_send_times: Arc::new(Mutex::new(HashMap::new())), + connection_id: Uuid::new_v4(), + shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), + reconnect_on_close: Arc::new(RwLock::new(true)), + } + } + + // ------------------------------------------------------------------------- + // Connection Management + // ------------------------------------------------------------------------- + + pub async fn connect(self: &Arc) { + if self.connection_loop_handle.lock().await.is_none() { + self.clone().start().await; + } + } + + pub async fn start(self: Arc) { + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + *self.reconnect_on_close.write().await = true; + + let self_clone = self.clone(); + let handle = tokio::spawn(async move { + self_clone.connection_loop().await; + }); + + *self.connection_loop_handle.lock().await = Some(handle); + } + + pub async fn stop(&self) { + *self.reconnect_on_close.write().await = false; + + if let Some(tx) = self.shutdown_tx.lock().await.take() { + let _ = tx.send(true); + } + + if let Some(handle) = self.connection_loop_handle.lock().await.take() { + handle.abort(); + } + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + if let Some(sender) = self.sender.read().await.as_ref() { + sender.close(); + } + + *self.state.write().await = ConnectionState::Disconnected; + *self.sender.write().await = None; + } + + async fn connection_loop(self: Arc) { + let mut reconnect_delay = RECONNECT_DELAY; + let shutdown_rx = self.shutdown_tx.lock().await.as_ref().unwrap().subscribe(); + let mut shutdown_rx = shutdown_rx; + + loop { + if *shutdown_rx.borrow() || *SHUTDOWN.read().await { + log_t!("omikron_connection_loop_shutdown"); + break; + } + + if !*self.reconnect_on_close.read().await { + break; + } + + match self.clone().connect_once().await { + Ok(()) => { + if *self.reconnect_on_close.read().await { + log!("Connection lost, reconnecting in {:?}...", reconnect_delay); + } else { + break; + } + } + Err(e) => { + log!( + "Connection failed: {}, retrying in {:?}...", + e, + reconnect_delay + ); + } + } + + tokio::select! { + _ = sleep(reconnect_delay) => {} + _ = shutdown_rx.changed() => { + if *shutdown_rx.borrow() { + break; + } + } + } + + reconnect_delay = std::cmp::min(reconnect_delay * 2, MAX_RECONNECT_DELAY); + } + } + + async fn connect_once(self: Arc) -> Result<(), String> { + *self.state.write().await = ConnectionState::Connecting; + log_t!("omikron_connecting"); + + let addr_str = format!("https://{}:{}/ws/iota/", self.host, self.port); + + let (sender, mut receiver) = ttp_native::client::connect(&addr_str, None) + .await + .map_err(|e| format!("Connection failed: {}", e))?; + + log_t!("omikron_connection_success"); + + let sender_arc = Arc::new(sender); + *self.sender.write().await = Some(sender_arc.clone()); + *self.state.write().await = ConnectionState::Connected { identified: false }; + + // Handle registration/identification + self.handle_authentication().await; + + // Start read loop + let read_self = self.clone(); + let read_handle = tokio::spawn(async move { + read_self.read_loop(&mut receiver).await; + }); + + // Start heartbeat + let heartbeat_self = self.clone(); + let heartbeat_handle = tokio::spawn(async move { + heartbeat_self.heartbeat_loop().await; + }); + *self.heartbeat_handle.lock().await = Some(heartbeat_handle); + + { + ACTIVE_TASKS.insert("Omikron Listener".to_string()); + } + + // Wait for read loop to complete + let result = read_handle.await; + *self.sender.write().await = None; + *self.state.write().await = ConnectionState::Disconnected; + { + ACTIVE_TASKS.remove("Omikron Listener"); + } + + if let Some(handle) = self.heartbeat_handle.lock().await.take() { + handle.abort(); + } + + match result { + Ok(()) => { + if *self.reconnect_on_close.read().await { + Err("Connection closed, will reconnect".to_string()) + } else { + Ok(()) + } + } + Err(e) => Err(format!("Read loop error: {}", e)), + } + } + + // ------------------------------------------------------------------------- + // Authentication (Registration/Identification) + // ------------------------------------------------------------------------- + + async fn handle_authentication(&self) { + let conf = CONFIG.read().await; + let iota_id = conf.get_iota_id(); + let public_key = conf.get_public_key(); + let private_key = conf.get_private_key(); + drop(conf); + + if iota_id == 0 || public_key.is_none() || private_key.is_none() { + log_t!("iota_register_new"); + + let key_pair = crypto_helper::generate_keypair(); + let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); + let _private_key_base64 = crypto_helper::secret_key_to_base64(&key_pair.secret); + + let mut conf_write = CONFIG.write().await; + // NOTE: + // Intentionally not storing the generated private/public keys directly into the + // config file here to avoid persisting sensitive material in plaintext. If you + // want to persist them, uncomment the two lines below and accept the security + // implications (they will be saved by `conf_write.update()`). + // conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); + // conf_write.change("private_key", DataValue::Str(private_key_base64)); + conf_write.update(); + drop(conf_write); + + let register_msg = CommunicationValue::new(CommunicationType::register_iota) + .add_data(DataTypes::public_key, DataValue::Str(public_key_base64)); + + let msg_id = register_msg.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new(|selfc, cv| { + if !cv.is_type(CommunicationType::success) { + return false; + } + + let iota_value = cv.get_data(DataTypes::register_id); + let iota_id = iota_value.as_number().unwrap_or(0); + + if iota_id != 0 { + tokio::spawn(async move { + let mut conf_write = CONFIG.write().await; + conf_write.change("iota_id", JsonValue::from(iota_id)); + conf_write.update(); + drop(conf_write); + log!("Registered with Iota-ID: {}", iota_id); + + // Send identification after registration + let identify_msg = + CommunicationValue::new(CommunicationType::identification) + .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); + selfc.send_message(&identify_msg).await; + }); + } else { + log!("Iota registration failed."); + } + true + }), + inserted_at: Instant::now(), + }, + ); + + self.send_message(®ister_msg).await; + } else { + let identify_msg = CommunicationValue::new(CommunicationType::identification) + .add_data(DataTypes::iota_id, DataValue::Number(iota_id)); + self.send_message(&identify_msg).await; + } + } + + // ------------------------------------------------------------------------- + // Read Loop & Heartbeat + // ------------------------------------------------------------------------- + + async fn read_loop(self: Arc, receiver: &mut Receiver) { + loop { + let result = receiver.receive().await; + match result { + Ok(cv) => { + self.clone().handle_message(cv).await; + } + Err(e) => { + self.fail_all_waiting_tasks(format!( + "Connection receive error: {} (connection_id={})", + e, self.connection_id + )) + .await; + break; + } + } + if !receiver.is_open() { + self.fail_all_waiting_tasks(format!( + "Connection closed (connection_id={}, receiver_open=false)", + self.connection_id + )) + .await; + break; + } + } + } + + async fn heartbeat_loop(self: Arc) { + loop { + sleep(HEARTBEAT_INTERVAL).await; + + if !self.state.read().await.is_connected() { + break; + } + + if let Some(sender) = self.sender.read().await.as_ref() { + if !sender.is_open() { + break; + } + } else { + break; + } + + self.send_ping().await; + } + } + + // ------------------------------------------------------------------------- + // Message Handling (Preserved from original) + // ------------------------------------------------------------------------- + + pub async fn handle_message(self: Arc, cv: CommunicationValue) { + if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { + log_cv_in!(&cv); + } + + let msg_id = cv.get_id(); + + // Dispatch waiting task for this message id + if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) { + if (task.task)(self.clone(), cv.clone()) { + return; + } + } + + if cv.is_type(CommunicationType::pong) { + self.handle_pong(&cv).await; + return; + } + + if cv.is_type(CommunicationType::challenge) { + self.handle_challenge(&cv).await; + return; + } + + if cv.is_type(CommunicationType::identification_response) { + if let Some(_accepted) = cv.get_data(DataTypes::accepted).as_bool() { + let mut state = self.state.write().await; + if let ConnectionState::Connected { identified: _ } = *state { + *state = ConnectionState::Connected { identified: true }; + } + } + return; + } + + // ************************************************ // + // Direct messages // + // ************************************************ // + + if cv.is_type(CommunicationType::message_state) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + // Parse send_time robustly: accept numeric or string, fallback to current time + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp_i64 = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + + let _ = chat_files::change_message_state( + timestamp_i64, + *receiver_id as i64, + *sender_id as i64, + MessageState::from_str( + cv.get_data(DataTypes::message_state).as_str().unwrap_or(""), + ), + ); + } + + // Incoming stored message: store for the recipient, attempt local delivery, notify sender. + if cv.is_type(CommunicationType::message_send) { + let sender_id: i64 = if let Some(n) = cv.get_data(DataTypes::sender_id).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::sender_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse receiver_id (the storage owner for this incoming message) + let receiver_id: i64 = if let Some(n) = cv.get_data(DataTypes::receiver_id).as_number() + { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::receiver_id).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; + + // parse send_time robustly (number or string), fallback to now + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp_i64 = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + let timestamp_u128 = timestamp_i64 as u128; + + // content may be missing; default to empty string + let content = cv + .get_data(DataTypes::content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + + // persist message for the receiver (storage_owner = receiver_id) + chat_files::add_message( + timestamp_u128, + false, + receiver_id as i64, + sender_id as i64, + &content, + height, + ); + + // persist message for the sender (storage_owner = sender_id) + chat_files::add_message( + timestamp_u128, + true, + sender_id as i64, + receiver_id as i64, + &content, + height, + ); + + // send confirmation back to sender + let conf_msg = CommunicationValue::new(CommunicationType::message_send) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64); + self.send_message(&conf_msg).await; + + // Build a live-delivery message for the local client (recipient) + let user_forward = CommunicationValue::new(CommunicationType::message_live) + .with_id(cv.get_id()) + .with_receiver(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); + + // Attempt delivery and await a response from the local client + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataTypes::message_state) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + // update stored message state for receiver + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + ms.clone(), + ); + + // update stored message state for sender + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + ms.clone(), + ); + + // notify original sender about the delivered/read state + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery failed or timed out; mark as Sent + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + sender_id as i64, + MessageState::Sent, + ); + + let _ = chat_files::change_message_state( + timestamp_i64, + sender_id as i64, + receiver_id as i64, + MessageState::Sent, + ); + + // notify sender + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_data(DataTypes::send_time, DataValue::Number(timestamp_i64)) + .add_data( + DataTypes::message_state, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + + if cv.is_type(CommunicationType::message_other_iota) { + let sender_id = &cv.get_sender(); + let receiver_id = &cv.get_receiver(); + + // parse send_time safely (number or string), fallback to now + let send_time_val = cv.get_data(DataTypes::send_time); + let now_i64 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64; + let timestamp = if let Some(n) = send_time_val.as_number() { + n as i64 + } else if let Some(s) = send_time_val.as_str() { + s.parse::().unwrap_or(now_i64) + } else { + now_i64 + }; + + // content may be missing or non-string; default to empty string + let content = cv + .get_data(DataTypes::content) + .as_str() + .unwrap_or("") + .to_string(); + + let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64; + + chat_files::add_message( + timestamp as u128, + false, + *receiver_id as i64, + *sender_id as i64, + &content, + height, + ); + + // Build user_forward using the parsed numeric timestamp and safe content string + let user_forward = CommunicationValue::new(CommunicationType::message_live) + .with_id(cv.get_id()) + .with_receiver(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data(DataTypes::content, DataValue::Str(content.clone())) + .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64)) + .add_data(DataTypes::height, DataValue::Number(height)); + + let user_resp = self + .clone() + .await_response(&user_forward, Some(Duration::from_secs(10))) + .await; + + if let Ok(user_resp) = user_resp { + let ms_raw = user_resp + .get_data(DataTypes::message_state) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + let _ = change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + ms.clone(), + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data( + DataTypes::message_state, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } else { + // Delivery timed out/failed — update stored state and notify sender with numeric timestamp + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + + self.send_message( + &CommunicationValue::new(CommunicationType::message_state) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_data(DataTypes::send_time, DataValue::Number(timestamp)) + .add_data( + DataTypes::message_state, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + return; + } + + if cv.is_type(CommunicationType::messages_get) { + let my_id = cv.get_sender(); + let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); + let offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0); + let amount = cv.get_data(DataTypes::amount).as_number().unwrap_or(0); + let messages = chat_files::get_messages(my_id as i64, partner_id, offset, amount); + let mut msg_array: Vec = Vec::new(); + for m in messages.members() { + let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); + let content: String = m["content"].as_str().unwrap_or("").to_string(); + let sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false); + let height: i64 = m["height"].as_i64().unwrap_or(0); + let sender_id: i64 = if sent_by_self { + my_id as i64 + } else { + if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { + s.parse::().unwrap_or(partner_id as i64) + } else { + partner_id as i64 + } + }; + let message_state: String = m["message_state"].as_str().unwrap_or("").to_string(); + + let mut container = Vec::new(); + container.push((DataTypes::send_time, DataValue::Number(message_time))); + container.push((DataTypes::content, DataValue::Str(content))); + container.push((DataTypes::sender_id, DataValue::Number(sender_id))); + container.push((DataTypes::message_state, DataValue::Str(message_state))); + container.push((DataTypes::height, DataValue::Number(height))); + container.push((DataTypes::sent_by_self, DataValue::Bool(sent_by_self))); + msg_array.push(DataValue::Container(container)); + } + + let resp = CommunicationValue::new(CommunicationType::messages_get) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::messages, DataValue::Array(msg_array)); + + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::get_chats) { + let user_id = cv.get_sender(); + let users = chats_util::get_users(user_id as i64); + let mut user_array = Vec::new(); + for user in users { + let mut container = Vec::new(); + container.push((DataTypes::user_id, DataValue::Number(user.user_id))); + if let Some(name) = user.user_name { + container.push((DataTypes::username, DataValue::Str(name))); + } + if let Some(ts) = user.last_message_at { + container.push((DataTypes::last_message_at, DataValue::Number(ts))); + } + user_array.push(DataValue::Container(container)); + } + let resp = CommunicationValue::new(CommunicationType::get_chats) + .with_id(cv.get_id()) + .with_receiver(user_id) + .add_data(DataTypes::user_ids, DataValue::Array(user_array)); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::add_conversation) { + let user_id = cv.get_sender(); + let other_id = match cv.get_data(DataTypes::chat_partner_id).as_number() { + Some(n) => n as i64, + None => cv + .get_data(DataTypes::chat_partner_id) + .as_str() + .unwrap_or("0") + .parse() + .unwrap_or(0), + }; + let mut contact = get_user(user_id as i64, other_id).unwrap_or(Contact::new(other_id)); + + if let Some(name) = cv.get_data(DataTypes::chat_partner_name).as_str() { + contact.user_name = Some(name.to_string()); + } + + contact.set_last_message_at( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + ); + mod_user(user_id as i64, &contact); + let resp = CommunicationValue::new(CommunicationType::add_conversation) + .with_id(cv.get_id()) + .with_receiver(user_id); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::add_community) { + CommunitiesUtil::add_community( + cv.get_sender() as i64, + cv.get_data(DataTypes::community_address) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataTypes::community_title) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataTypes::position) + .as_str() + .unwrap() + .to_string(), + ); + let resp = CommunicationValue::new(CommunicationType::add_community) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::get_communities) { + let mut comm_array = Vec::new(); + for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { + let mut container: Vec<(DataTypes, DataValue)> = Vec::new(); + if let Some(address) = c["address"].as_str() { + container.push(( + DataTypes::community_address, + DataValue::Str(address.to_string()), + )); + } + if let Some(title) = c["title"].as_str() { + container.push(( + DataTypes::community_title, + DataValue::Str(title.to_string()), + )); + } + if let Some(position) = c["position"].as_str() { + container.push((DataTypes::position, DataValue::Str(position.to_string()))); + } + comm_array.push(DataValue::Container(container)); + } + + let resp = CommunicationValue::new(CommunicationType::get_communities) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) + .add_data(DataTypes::communities, DataValue::Array(comm_array)); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::remove_community) { + CommunitiesUtil::remove_community( + cv.get_sender() as i64, + cv.get_data(DataTypes::community_address) + .as_str() + .unwrap() + .to_string(), + ); + let resp = CommunicationValue::new(CommunicationType::remove_community) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()); + self.send_message(&resp).await; + return; + } + + if cv.is_type(CommunicationType::settings_save) { + let my_id = cv.get_sender(); + let settings_name = cv.get_data(DataTypes::settings_name).as_str().unwrap(); + let settings_value = cv.get_data(DataTypes::payload).as_str().unwrap(); + + save_file( + &format!("users/{}/settings/", my_id), + &format!("{}.settings", settings_name), + &settings_value, + ); + + let response = CommunicationValue::new(CommunicationType::settings_save) + .with_receiver(my_id) + .with_id(cv.get_id()); + + self.send_message(&response).await; + return; + } + + if cv.is_type(CommunicationType::settings_load) { + let my_id = cv.get_sender(); + let settings_name = cv.get_data(DataTypes::settings_name).as_string().unwrap(); + let settings_value_str = load_file( + &format!("users/{}/settings/", my_id), + &format!("{}.settings", settings_name), + ); + let response = CommunicationValue::new(CommunicationType::settings_load) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::payload, DataValue::Str(settings_value_str)) + .add_data(DataTypes::settings_name, DataValue::Str(settings_name)); + + self.send_message(&response).await; + return; + } + + if cv.is_type(CommunicationType::settings_list) { + let my_id = cv.get_sender(); + let settings = get_children(&format!("users/{}/settings/", my_id)); + let mut settings_json = Vec::new(); + for s in settings { + let s = s.replace(".settings", ""); + if s.is_empty() { + continue; + } + let _ = settings_json.push(DataValue::Str(s)); + } + let response = CommunicationValue::new(CommunicationType::settings_list) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_data(DataTypes::settings, DataValue::Array(settings_json)); + + self.send_message(&response).await; + return; + } + } + + async fn handle_challenge(&self, cv: &CommunicationValue) { + let conf = CONFIG.read().await; + let private_key = conf.get_private_key().unwrap(); + drop(conf); + + let omikron_public_key = cv.get_data(DataTypes::public_key).as_str().unwrap(); + let encrypted_challenge = cv.get_data(DataTypes::challenge).as_str().unwrap(); + + let solved_challenge = { + if let Ok(decrypted) = SecurePayload::new( + encrypted_challenge, + DataFormat::Base64, + crypto_helper::load_secret_key(&private_key).unwrap(), + ) { + if let Ok(decrypted) = decrypted + .decrypt_x448(crypto_helper::load_public_key(omikron_public_key).unwrap()) + { + Some(decrypted) + } else { + None + } + } else { + None + } + }; + + if let Some(decrypted) = solved_challenge { + let solved = decrypted.export(DataFormat::Raw); + + let response = CommunicationValue::new(CommunicationType::challenge_response) + .with_id(cv.get_id()) + .add_data(DataTypes::challenge, DataValue::Str(solved)); + + self.send_message(&response).await; + } + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + pub async fn send_message(&self, cv: &CommunicationValue) { + if let Err(err) = self.send_message_result(cv).await { + log_t!("send_message_failed", err); + } + } + + async fn send_message_result(&self, cv: &CommunicationValue) -> Result<(), String> { + let sender_guard = self.sender.read().await; + if let Some(sender) = sender_guard.as_ref() { + if !sender.is_open() { + drop(sender_guard); + if let Some(sender) = self.sender.write().await.take() { + sender.close(); + } + self.fail_all_waiting_tasks(format!( + "Send failed: connection closed (connection_id={})", + self.connection_id + )) + .await; + return Err("connection closed".to_string()); + } + + let sender_clone = Arc::clone(sender); + drop(sender_guard); + + if !cv.is_type(CommunicationType::ping) && !cv.is_type(CommunicationType::pong) { + log_cv_out!(&cv); + } + + if let Err(e) = sender_clone.send(cv).await { + self.fail_all_waiting_tasks(format!( + "Send failed: {} (connection_id={})", + e, self.connection_id + )) + .await; + return Err(e.to_string()); + } + + Ok(()) + } else { + Err("not connected".to_string()) + } + } + + async fn fail_all_waiting_tasks(&self, reason: String) { + let keys: Vec = WAITING_TASKS.iter().map(|entry| *entry.key()).collect(); + + for key in keys { + if let Some((_, waiting_task)) = WAITING_TASKS.remove(&key) { + let response = CommunicationValue::new(CommunicationType::error) + .with_id(key) + .add_data(DataTypes::message, DataValue::Str(reason.clone())); + let _ = (waiting_task.task)(OMIKRON_CONNECTION.clone(), response); + } + } + } + + pub async fn is_connected(&self) -> bool { + self.state.read().await.is_connected() + } + + pub async fn is_identified(&self) -> bool { + self.state.read().await.is_identified() + } + + pub async fn await_response( + &self, + cv: &CommunicationValue, + timeout_duration: Option, + ) -> Result { + let (tx, mut rx) = mpsc::channel(1); + let msg_id = cv.get_id(); + + WAITING_TASKS.insert( + msg_id, + WaitingTask { + task: Box::new(move |_, response_cv| { + let inner_tx = tx.clone(); + tokio::spawn(async move { + let _ = inner_tx.send(response_cv).await; + }); + true + }), + inserted_at: Instant::now(), + }, + ); + + if let Err(send_err) = self.send_message_result(cv).await { + WAITING_TASKS.remove(&msg_id); + return Err(format!( + "Request send failed (msg_id={}, reason={})", + msg_id, send_err + )); + } + + let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); + + match tokio::time::timeout(timeout, rx.recv()).await { + Ok(Some(response_cv)) => { + if response_cv.is_type(CommunicationType::error) { + let reason = response_cv + .get_data(DataTypes::message) + .as_str() + .unwrap_or("connection error") + .to_string(); + Err(format!( + "Request failed due to disconnect (msg_id={}, reason={})", + msg_id, reason + )) + } else { + Ok(response_cv) + } + } + Ok(_) => { + WAITING_TASKS.remove(&msg_id); + Err("Channel closed while awaiting response".to_string()) + } + Err(_) => { + let waiting_tasks_len = WAITING_TASKS.len(); + WAITING_TASKS.remove(&msg_id); + Err(format!( + "Request timed out (msg_id={}, timeout={}s, connected={}, waiting_tasks={})", + msg_id, + timeout.as_secs(), + self.is_connected().await, + waiting_tasks_len + )) + } + } + } + + pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { + if self.state.read().await.is_connected() { + return Ok(()); + } + + let timeout = timeout_duration.unwrap_or(CONNECTION_TIMEOUT); + let start = Instant::now(); + + loop { + if self.state.read().await.is_connected() { + return Ok(()); + } + + if start.elapsed() >= timeout { + return Err(format!( + "Connection not established within {} seconds", + timeout.as_secs() + )); + } + + sleep(Duration::from_millis(100)).await; + } + } +} + +// ============================================================================ +// Global Instance +// ============================================================================ + +pub static OMIKRON_CONNECTION: LazyLock> = LazyLock::new(|| { + let conn = Arc::new(OmikronConnection::new()); + + start_task_cleanup_loop(); + + conn +}); + +pub async fn get_omikron_connection() -> Arc { + let conn = OMIKRON_CONNECTION.clone(); + + conn.connect().await; + conn +} diff --git a/src/omikron/ping_pong_task.rs b/src/omikron/ping_pong_task.rs new file mode 100644 index 0000000..f1a891f --- /dev/null +++ b/src/omikron/ping_pong_task.rs @@ -0,0 +1,38 @@ +use crate::omikron::omikron_connection::OmikronConnection; +use crate::{APP_STATE, log}; +use dashmap::DashMap; +use std::sync::LazyLock; +use std::time::Instant; +use tokio::time::Duration; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue, rand_u32}; + +static PING_TIMES: LazyLock> = LazyLock::new(|| DashMap::new()); + +impl OmikronConnection { + pub async fn send_ping(&self) { + let id = rand_u32(); + + PING_TIMES.insert(id, Instant::now()); + + PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30)); + + let ping_message = CommunicationValue::new(CommunicationType::ping) + .with_id(id) + .add_data( + DataTypes::last_ping, + DataValue::Array(vec![DataValue::Number(*self.last_ping.lock().await)]), + ); + + self.send_message(&ping_message).await; + } + + pub async fn handle_pong(&self, cv: &CommunicationValue) { + let id = cv.get_id(); + + if let Some((_, send_time)) = PING_TIMES.remove(&id) { + let ping_ms = Instant::now().duration_since(send_time).as_millis() as i64; + *self.last_ping.lock().await = ping_ms; + APP_STATE.lock().unwrap().push_ping_val(ping_ms as f64); + } + } +} diff --git a/src/server/api.rs b/src/server/api.rs new file mode 100755 index 0000000..f9a87a4 --- /dev/null +++ b/src/server/api.rs @@ -0,0 +1,190 @@ +use crate::server::server::is_local_network; +use crate::util::config_util::CONFIG; +use actix_web::{HttpRequest, HttpResponse, Responder, web}; +use serde_json::{Value, json}; +use std::net::SocketAddr; +use std::sync::Arc; + +pub fn api_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/api") + .route("/shutdown/", web::post().to(shutdown)) + .route("/reload/", web::post().to(reload)) + .route("/users/add/", web::post().to(users_add)) + .route("/users/remove/", web::post().to(users_remove)) + .route("/users/get/", web::get().to(users_get)) + .route("/communities/add/", web::post().to(communities_add)) + .route("/communities/get/", web::get().to(communities_get)) + .route("/settings/set/", web::post().to(settings_set)) + .route("/settings/get/", web::get().to(settings_get)), + ); +} + +async fn settings_set(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let key = req.headers().get("key").and_then(|v| v.to_str().ok()); + let value = req.headers().get("value").and_then(|v| v.to_str().ok()); + + match (key, value) { + (Some(k), Some(v)) => { + let _ = CONFIG + .write() + .await + .config + .insert(&k.to_string(), v.to_string()); + + success() + } + _ => error(), + } +} + +async fn settings_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + let config = CONFIG.read().await.config.clone(); + let serde_config: Value = serde_json::to_value(config.to_string()).unwrap(); + HttpResponse::Ok().json(serde_config) +} + +async fn communities_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let communities = crate::communities::community_manager::get_communities().await; + + let mut list = Vec::new(); + + for c in communities { + let val = c.frontend().await; + let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); + list.push(s_val); + } + HttpResponse::Ok().json(list) +} + +async fn communities_add( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let name = payload["name"].as_str().unwrap_or("").to_string(); + let owner = payload["owner"].as_i64().unwrap_or(0); + + let community = Arc::new(crate::communities::community::Community::create(name, owner).await); + + crate::communities::community_manager::add_community(community).await; + + success() +} + +async fn users_get(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let users = crate::users::user_manager::get_users(); + + let list: Vec<_> = users + .into_iter() + .map(|u| { + let val = u.frontend(); + serde_json::to_value(val.to_string()).unwrap() + }) + .collect(); + + HttpResponse::Ok().json(list) +} + +async fn users_remove( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0); + + crate::users::user_manager::remove_user(uuid); + crate::users::user_manager::save_users(); + + success() +} + +async fn users_add( + req: HttpRequest, + ssl: web::Data, + payload: web::Json, +) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + let username = match payload.get("username").and_then(|v| v.as_str()) { + Some(u) => u, + _ => return error(), + }; + + if let (Some(user), Some(_)) = crate::users::user_manager::create_user(username).await { + let val = user.frontend(); + let s_val: Value = serde_json::to_value(val.to_string()).unwrap(); + HttpResponse::Ok().json(s_val) + } else { + error() + } +} + +async fn shutdown(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + *crate::SHUTDOWN.write().await = true; + success() +} + +async fn reload(req: HttpRequest, ssl: web::Data) -> impl Responder { + if !is_allowed_req(&req, *ssl.get_ref()) { + return forbidden(); + } + + *crate::SHUTDOWN.write().await = true; + *crate::RELOAD.write().await = true; + + success() +} + +fn forbidden() -> HttpResponse { + HttpResponse::Forbidden().body("403 Forbidden") +} + +fn success() -> HttpResponse { + HttpResponse::Ok().json(json!({ "type": "success" })) +} + +fn error() -> HttpResponse { + HttpResponse::Ok().json(json!({ "type": "error" })) +} + +fn is_allowed(addr: SocketAddr, ssl: bool) -> bool { + is_local_network(addr.ip()) || ssl +} + +fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool { + if let Some(addr) = req.peer_addr() { + is_allowed(addr, ssl) + } else { + false + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..de2a79a --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +pub mod server; +pub mod web_path_parser; diff --git a/src/server/server.rs b/src/server/server.rs new file mode 100644 index 0000000..b55a50f --- /dev/null +++ b/src/server/server.rs @@ -0,0 +1,169 @@ +use crate::log; +use crate::server::api::api_config; +use crate::server::web_path_parser; +use crate::util::file_util::load_file_buf; +use crate::{ACTIVE_TASKS, SHUTDOWN}; +use actix_web::{App, Error, HttpRequest, HttpServer, Responder, dev::ServerHandle, web}; +use actix_web_actors::ws; +use rustls::ServerConfig; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use std::{ + error::Error as StdError, + io::{self, BufReader, ErrorKind}, + net::IpAddr, + sync::Arc, + time::Duration, +}; + +async fn ws_handler(req: HttpRequest, stream: web::Payload) -> Result { + let path = req.path().to_string(); + log!("WS connection from {:?}", req.peer_addr()); + let session = WsSession::new(path); + ws::start(session, &req, stream) +} + +use tokio::sync::oneshot; + +pub async fn start(port: u16) -> bool { + let (tx, rx) = oneshot::channel::(); + + let _ = tokio::spawn(async move { + let server = match load_tls_config() { + Ok(Some(tls_config)) => { + log!("HTTPS (HTTP/2) Server running on 0.0.0.0:{}", port); + let _config = (*tls_config).clone(); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(true)) + .configure(api_config) + .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) + .default_service(web::to(web_path_parser::handle)) + }) + .bind(("0.0.0.0", port)) + .unwrap() + .run() + } + Ok(_) => { + log!("HTTP Server running on 0.0.0.0:{}", port); + HttpServer::new(move || { + App::new() + .app_data(web::Data::new(false)) + .configure(api_config) + .service(web::resource("/ws/{path:.*}").route(web::get().to(ws_handler))) + .default_service(web::to(web_path_parser::handle)) + }) + .bind(("0.0.0.0", port)) + .unwrap() + .run() + } + Err(e) => { + log!("TLS config error: {}", e); + return; + } + }; + + let server_handle = server.handle(); + tx.send(server_handle).unwrap(); + + ACTIVE_TASKS.insert("WebServer".into()); + server.await.unwrap(); + ACTIVE_TASKS.remove("WebServer"); + log!("Web Server shutdown complete."); + }); + + if let Ok(server_handle) = rx.await { + tokio::spawn(async move { + wait_for_shutdown(server_handle).await; + }); + true + } else { + false + } +} + +async fn wait_for_shutdown(server_handle: ServerHandle) { + loop { + if *SHUTDOWN.read().await { + log!("Shutdown signal received."); + server_handle.stop(true).await; + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +fn load_tls_config() -> Result>, Box> { + let cert_file_res = load_file_buf("certs", "cert.pem"); + let key_file_res = load_file_buf("certs", "cert.key"); + + let cert_file_buf = match cert_file_res { + Ok(b) => b, + Err(e) if e.kind() == ErrorKind::NotFound => { + log!("TLS certificate 'certs/cert.pem' not found."); + return Ok(None); + } + Err(e) => return Err(e.into()), // Other IO error + }; + + let key_file_buf = match key_file_res { + Ok(b) => b, + Err(e) if e.kind() == ErrorKind::NotFound => { + log!("TLS key 'certs/cert.key' not found."); + return Ok(None); + } + Err(e) => return Err(e.into()), // Other IO error + }; + + let cert_chain = rustls_pemfile::certs(&mut BufReader::new(cert_file_buf)) + .collect::, _>>()?; + + // PKCS8 + let mut key_reader = BufReader::new(key_file_buf); + let mut key_ders = rustls_pemfile::pkcs8_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + + if key_ders.is_empty() { + // RSA + key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file + key_ders = rustls_pemfile::rsa_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + } + + if key_ders.is_empty() { + // EC + key_reader = BufReader::new(load_file_buf("certs", "cert.key")?); // Re-read key file + key_ders = rustls_pemfile::ec_private_keys(&mut key_reader) + .map(|r| r.map(Into::into)) + .collect::, _>>()?; + } + + if key_ders.is_empty() { + return Err("No private keys found in key file. (Tried PKCS8, RSA, and EC)".into()); + } + + let config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cert_chain, key_ders.remove(0)) + .map_err(|e| io::Error::new(ErrorKind::Other, e.to_string()))?; + + Ok(Some(Arc::new(config))) +} + +pub fn is_local_network(addr: IpAddr) -> bool { + match addr { + IpAddr::V4(v4) => { + let o = v4.octets(); + o[0] == 10 + || (o[0] == 172 && (16..=31).contains(&o[1])) + || (o[0] == 192 && o[1] == 168) + || o[0] == 127 + || (o[0] == 169 && o[1] == 254) + } + IpAddr::V6(v6) => { + let s = v6.segments(); + (s[0] & 0xfe00) == 0xfc00 || (s[0] & 0xffc0) == 0xfe80 || v6.is_loopback() + } + } +} diff --git a/src/server/web_path_parser.rs b/src/server/web_path_parser.rs new file mode 100755 index 0000000..a82c8db --- /dev/null +++ b/src/server/web_path_parser.rs @@ -0,0 +1,77 @@ +use actix_web::{HttpRequest, HttpResponse}; +use std::path::{Path, PathBuf}; + +use crate::util::file_util::load_file_vec; + +fn codec_for_ext(ext: &str) -> &'static str { + match ext { + "html" => "text/html; charset=utf-8", + "css" => "text/css", + "js" => "application/javascript", + "json" => "application/json", + "png" => "image/png", + "ico" => "image/x-icon", + "woff2" => "font/woff2", + _ => "application/octet-stream", + } +} + +pub async fn handle(req: HttpRequest) -> HttpResponse { + let req_path = req.path().trim_start_matches('/'); + + let mut fs_path = PathBuf::from("web"); + + if req_path.is_empty() { + fs_path.push("index.html"); + } else { + fs_path.extend(req_path.split('/')); + } + + if fs_path.is_dir() { + fs_path.push("index.html"); + } + + let ext_opt = fs_path.extension().and_then(|e| e.to_str()); + let mut final_name = fs_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + if ext_opt.is_none() { + if final_name.is_empty() { + final_name = "index.html".to_string(); + } else { + final_name.push_str(".html"); + } + } + + let content_type = codec_for_ext( + fs_path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("html"), + ); + + let dir = fs_path.parent().unwrap_or(Path::new("web")); + + match load_file_vec(dir.to_str().unwrap_or("web"), &final_name) { + Ok(content) => HttpResponse::Ok().content_type(content_type).body(content), + + Err(_) => { + let ext = fs_path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if matches!(ext, "js" | "css" | "woff2") { + return HttpResponse::NotFound() + .content_type("text/plain") + .body("Not found"); + } + + let fallback = load_file_vec("web", "404.html") + .unwrap_or_else(|_| include_bytes!("../../static/web/404.html").to_vec()); + + HttpResponse::NotFound() + .content_type("text/html; charset=utf-8") + .body(fallback) + } + } +} diff --git a/src/terms/buttons.rs b/src/terms/buttons.rs new file mode 100644 index 0000000..c545a4c --- /dev/null +++ b/src/terms/buttons.rs @@ -0,0 +1,175 @@ +use ratatui::{ + layout::{Alignment, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, +}; + +use crate::terms::focus::Focus; + +#[allow(mismatched_lifetime_syntaxes)] +pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line { + let box_char = if checked { "[x]" } else { "[ ]" }; + let (box_style, text_style) = if active { + if allowed { + ( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) + } else { + ( + Style::default().fg(Color::Gray), + Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + ) + } + } else { + (Style::default(), Style::default()) + }; + Line::from(vec![ + Span::styled(box_char, box_style), + Span::raw(" "), + Span::styled(label, text_style), + ]) +} + +pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) { + let p = Paragraph::new(Span::styled(label, style)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(p, area); +} +pub fn draw_buttons( + f: &mut ratatui::Frame, + area: Rect, + current_focus: Focus, + state: (bool, bool), + update_needed: bool, + downgrade_scenario: bool, + tos_or_privacy: bool, +) { + let cancel_text = if update_needed { + "[Q] Quit" + } else { + "[Q] Not now" + }; + let continue_text = if downgrade_scenario { + "Downgrade" + } else { + "Continue" + }; + let mut buttons = vec![ + (cancel_text, Focus::Cancel), + (continue_text, Focus::Continue), + ]; + if tos_or_privacy { + buttons.push(("Continue with Tensamin Services", Focus::ContinueAll)); + } + + let padding = 2; + let min_widths: Vec = buttons + .iter() + .map(|(label, _)| label.len() as u16 + padding) + .collect(); + + let widths = compute_widths(area.width, &min_widths); + + let mut x = area.x; + + for ((label, focus), width) in buttons.iter().zip(widths) { + let chunk = Rect { + x, + y: area.y, + width, + height: area.height, + }; + x += width; + + let is_focused = current_focus == *focus; + + let style = match focus { + Focus::Cancel => { + if is_focused { + Style::default() + .fg(Color::Black) + .bg(Color::Red) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Red) + } + } + + Focus::Continue => { + if is_focused && state.0 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.0 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + + Focus::ContinueAll => { + if is_focused && state.1 { + Style::default() + .fg(Color::Black) + .bg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if state.1 { + Style::default().fg(Color::Green) + } else { + Style::default().fg(Color::DarkGray) + } + } + + _ => Style::default().fg(Color::DarkGray), + }; + + draw_button(f, chunk, label, style); + } +} +pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec { + let mut widths = vec![0; min_widths.len()]; + let mut remaining: Vec = (0..min_widths.len()).collect(); + + let mut remaining_width = area_width; + + while !remaining.is_empty() { + let count = remaining.len() as u16; + let equal = remaining_width / count; + + let mut clamped = Vec::new(); + + for &i in &remaining { + if min_widths[i] > equal { + widths[i] = min_widths[i]; + remaining_width -= min_widths[i]; + clamped.push(i); + } + } + + if clamped.is_empty() { + let mut remainder = remaining_width % count; + for &i in &remaining { + widths[i] = equal + + if remainder > 0 { + remainder -= 1; + 1 + } else { + 0 + }; + } + break; + } + + remaining.retain(|i| !clamped.contains(i)); + } + + widths +} diff --git a/src/terms/consent_state.rs b/src/terms/consent_state.rs new file mode 100644 index 0000000..1cc6e0a --- /dev/null +++ b/src/terms/consent_state.rs @@ -0,0 +1,491 @@ +use tokio::sync::oneshot; + +use crate::{ + gui::{ + screens::{terms_checker::TermsCheckerScreen, terms_updater::TermsUpdaterScreen}, + ui::UI, + }, + terms::{ + doc::Doc, + terms_getter::{Type, get_current_docs, get_newest_docs}, + }, + util::file_util::{load_file, save_file}, +}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub async fn check(ui: Arc) -> (bool, bool) { + let mut state = ConsentState::load_state(); + + if ensure_initial_consent(ui.clone(), &mut state) + .await + .is_err() + { + return (false, false); + } + if ensure_updates(ui, &mut state).await.is_err() { + return (false, false); + }; + + state = state.sanitize(); + state.save_state(); + + (state.accepted_eula, state.accepted_tos && state.accepted_pp) +} + +async fn ensure_initial_consent(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { + if state.accepted_eula { + return Ok(()); + } + + let (tx, rx) = oneshot::channel(); + + ui.set_screen(Box::new(TermsCheckerScreen::new(ui.clone(), Some(tx)))) + .await; + + let result = rx.await.unwrap_or(UserChoice::Deny); + + match result { + UserChoice::AcceptEULA | UserChoice::AcceptAll => { + if let Some((eula, tos, privacy)) = get_current_docs().await { + state.accepted_eula = true; + state.eula = Some(eula); + + if matches!(result, UserChoice::AcceptAll) { + state.accepted_tos = true; + state.accepted_pp = true; + state.tos = Some(tos); + state.privacy = Some(privacy); + } + } + + let _ = &state.save_state(); + Ok(()) + } + UserChoice::Deny => Err(()), + } +} +async fn ensure_updates(ui: Arc, state: &mut ConsentState) -> Result<(), ()> { + let Some((eula_update, tos_update, privacy_update)) = get_updates().await else { + return Ok(()); + }; + + let is_forced = matches!(eula_update, UpdateDecision::Forced(_)) + || matches!(tos_update, UpdateDecision::Forced(_)) + || matches!(privacy_update, UpdateDecision::Forced(_)); + + let (tx, rx) = oneshot::channel(); + + ui.set_screen(Box::new(TermsUpdaterScreen::new( + eula_update.clone(), + tos_update.clone(), + privacy_update.clone(), + Some(tx), + ))) + .await; + + let result = rx.await.unwrap_or(UserChoice::Deny); + + if is_forced { + match result { + UserChoice::AcceptAll => { + state.accepted_eula = true; + state.accepted_tos = true; + state.accepted_pp = true; + } + UserChoice::AcceptEULA => { + state.accepted_eula = true; + } + UserChoice::Deny => return Err(()), + } + } else { + apply_future_updates(state, result, eula_update, tos_update, privacy_update); + } + + state.save_state(); + Ok(()) +} +fn apply_future_updates( + state: &mut ConsentState, + result: UserChoice, + eula_update: UpdateDecision, + tos_update: UpdateDecision, + privacy_update: UpdateDecision, +) { + match result { + UserChoice::AcceptAll => { + if let UpdateDecision::Future { newest } = eula_update { + state.future_eula = Some(newest); + } + if let UpdateDecision::Future { newest } = tos_update { + state.future_tos = Some(newest); + } + if let UpdateDecision::Future { newest } = privacy_update { + state.future_privacy = Some(newest); + } + } + UserChoice::AcceptEULA => { + if let UpdateDecision::Future { newest } = eula_update { + state.future_eula = Some(newest); + } + } + UserChoice::Deny => {} + } +} + +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 + UpdateDecision, + UpdateDecision, + UpdateDecision, +)> { + if let ( + Some((current_eula, current_tos, current_privacy)), + Some((newest_eula, newest_tos, newest_privacy)), + ) = (get_current_docs().await, get_newest_docs().await) + { + let file = load_file("", "agreements"); + let accepted_state = ConsentState::from_str(&file).sanitize(); + save_file("", "agreements", &accepted_state.to_string()); + + let eula_update: UpdateDecision = if current_eula.equals_some(&accepted_state.eula) { + if current_eula.equals(&newest_eula) { + UpdateDecision::NoChange + } else { + if newest_eula.equals_some(&accepted_state.future_eula) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { + newest: newest_eula, + } + } + } + } else if newest_eula.equals_some(&accepted_state.eula) { + UpdateDecision::NoChange + } else { + UpdateDecision::Forced(current_eula) + }; + + let tos_update: UpdateDecision = + if !accepted_state.accepted_tos || newest_tos.equals_some(&accepted_state.tos) { + UpdateDecision::NoChange + } else if accepted_state.accepted_tos && current_tos.equals_some(&accepted_state.tos) { + if current_tos.equals(&newest_tos) { + UpdateDecision::NoChange + } else { + if newest_tos.equals_some(&accepted_state.future_tos) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { newest: newest_tos } + } + } + } else { + UpdateDecision::Forced(current_tos) + }; + + let privacy_update: UpdateDecision = if !accepted_state.accepted_pp + || newest_privacy.equals_some(&accepted_state.privacy) + { + UpdateDecision::NoChange + } else if accepted_state.accepted_pp && current_privacy.equals_some(&accepted_state.privacy) + { + if current_privacy.equals(&newest_privacy) { + UpdateDecision::NoChange + } else { + if newest_privacy.equals_some(&accepted_state.future_privacy) { + UpdateDecision::NoChange + } else { + UpdateDecision::Future { + newest: newest_privacy, + } + } + } + } else { + UpdateDecision::Forced(current_privacy) + }; + match (&eula_update, &tos_update, &privacy_update) { + (&UpdateDecision::NoChange, &UpdateDecision::NoChange, &UpdateDecision::NoChange) => { + None + } + _ => Some((eula_update, tos_update, privacy_update)), + } + } else { + None + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UserChoice { + Deny, + AcceptEULA, + AcceptAll, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum UpdateDecision { + NoChange, + Future { newest: Doc }, + Forced(Doc), +} + +#[derive(Debug, Clone)] +pub struct ConsentState { + pub eula: Option, + pub accepted_eula: bool, + pub future_eula: Option, + + pub tos: Option, + pub accepted_tos: bool, + pub future_tos: Option, + + pub privacy: Option, + pub accepted_pp: bool, + pub future_privacy: Option, +} + +impl ConsentState { + fn sanitize(mut self) -> Self { + let current_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(future_eula) = self.future_eula.clone() { + if future_eula.get_time() < current_secs { + self.eula = Some(future_eula); + self.future_eula = None; + } + } + if let Some(future_tos) = self.future_tos.clone() { + if future_tos.get_time() < current_secs { + self.tos = Some(future_tos); + self.future_tos = None; + } + } + if let Some(future_privacy) = self.future_privacy.clone() { + if future_privacy.get_time() < current_secs { + self.privacy = Some(future_privacy); + self.future_privacy = None; + } + } + + if !self.accepted_eula { + self.accepted_tos = false; + self.accepted_pp = false; + } + self + } + + pub fn load_state() -> ConsentState { + let file = load_file("", "agreements"); + ConsentState::from_str(&file).sanitize() + } + + pub fn save_state(&self) { + save_file("", "agreements", &self.to_string()); + } + + fn to_string(&self) -> String { + let current_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let mut file_out: String = format!( + "This 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={}", + current_secs + ); + + if let Some(eula) = &self.eula { + 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={}\ + \nEULA-VERSION={}\ + \nEULA-HASH={}\ + ", self.accepted_eula, eula.get_version(), eula.get_hash())); + + if self.accepted_tos + && let Some(tos) = &self.tos + { + file_out.push_str(&format!("\ + \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.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/\ + \nPrivacy-Policy={}\ + \nPrivacy-Policy-VERSION={}\ + \nPrivacy-Policy-HASH={}\ + ", self.accepted_pp, pp.get_version(), pp.get_hash())); + } + } else { + file_out.push_str("\ + \n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\ + \nEULA=false\ + "); + } + + if let Some(eula) = &self.future_eula { + file_out.push_str(&format!( + "\ + \nFUTURE-EULA-VERSION={}\ + \nFUTURE-EULA-HASH={}\ + \nFUTURE-EULA-TIME={}\ + ", + eula.get_version(), + eula.get_hash(), + eula.get_time() + )); + } + if let Some(tos) = &self.future_tos { + file_out.push_str(&format!( + "\ + \nFUTURE-Terms-of-Service-VERSION={}\ + \nFUTURE-Terms-of-Service-HASH={}\ + \nFUTURE-Terms-of-Service-TIME={}\ + ", + tos.get_version(), + tos.get_hash(), + tos.get_time() + )); + } + if let Some(pp) = &self.future_privacy { + file_out.push_str(&format!( + "\ + \nFUTURE-Privacy-Policy-VERSION={}\ + \nFUTURE-Privacy-Policy-HASH={}\ + \nFUTURE-Privacy-Policy-TIME={}\ + ", + pp.get_version(), + pp.get_hash(), + pp.get_time() + )); + } + + file_out + } + + fn from_str(s: &str) -> Self { + let mut eula = false; + let mut eula_version = String::new(); + let mut eula_hash = String::new(); + let mut pp = false; + let mut pp_version = String::new(); + let mut pp_hash = String::new(); + let mut tos = false; + let mut tos_version = String::new(); + let mut tos_hash = String::new(); + + let mut future_eula_version = String::new(); + let mut future_eula_hash = String::new(); + let mut future_eula_time = String::new(); + + let mut future_tos_version = String::new(); + let mut future_tos_hash = String::new(); + let mut future_tos_time = String::new(); + + let mut future_pp_version = String::new(); + let mut future_pp_hash = String::new(); + let mut future_pp_time = String::new(); + + let mut unix = String::new(); + + for line in s.lines() { + if let Some(v) = line.strip_prefix("EULA=") { + eula = v == "true"; + } else if let Some(v) = line.strip_prefix("EULA-VERSION=") { + 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("Terms-of-Service=") { + tos = v == "true"; + } 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("Terms-of-Service-HASH=") { + tos_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("Privacy-Policy=") { + pp = v == "true"; + } else if let Some(v) = line.strip_prefix("Privacy-Policy-VERSION=") { + pp_version = v.to_string(); + } 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-SECOND=") { + unix = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-VERSION=") { + future_eula_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-HASH=") { + future_eula_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-EULA-TIME=") { + future_eula_time = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-VERSION=") { + future_tos_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-HASH=") { + future_tos_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Terms-of-Service-TIME=") { + future_tos_time = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-VERSION=") { + future_pp_version = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-HASH=") { + future_pp_hash = v.to_string(); + } else if let Some(v) = line.strip_prefix("FUTURE-Privacy-Policy-TIME=") { + future_pp_time = v.to_string(); + } + } + let unix: u64 = unix.parse::().unwrap_or(0); + let future_eula_time = future_eula_time.parse::().unwrap_or(0); + let future_tos_time = future_tos_time.parse::().unwrap_or(0); + let future_pp_time = future_pp_time.parse::().unwrap_or(0); + let state = Self { + accepted_eula: eula, + eula: Some(Doc::new(eula_version, eula_hash, Type::EULA, unix)), + accepted_pp: pp, + 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)), + future_eula: if !future_eula_version.is_empty() { + Some(Doc::new( + future_eula_version, + future_eula_hash, + Type::EULA, + future_eula_time, + )) + } else { + None + }, + future_tos: if !future_tos_version.is_empty() { + Some(Doc::new( + future_tos_version, + future_tos_hash, + Type::TOS, + future_tos_time, + )) + } else { + None + }, + future_privacy: if !future_pp_version.is_empty() { + Some(Doc::new( + future_pp_version, + future_pp_hash, + Type::PP, + future_pp_time, + )) + } else { + None + }, + } + .sanitize(); + + state + } +} diff --git a/src/terms/doc.rs b/src/terms/doc.rs new file mode 100644 index 0000000..2365ba3 --- /dev/null +++ b/src/terms/doc.rs @@ -0,0 +1,73 @@ +use json::{JsonValue, object::Object}; + +use crate::{terms::terms_getter::Type, util::file_util::load_file}; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[allow(unused)] +pub struct Doc { + version: String, + hash: String, + pub doc_type: Type, + timestamp: u64, +} + +#[allow(dead_code)] +impl Doc { + pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc { + Doc { + version, + hash, + doc_type, + timestamp, + } + } + + pub fn equals_some(&self, other: &Option) -> bool { + if let Some(other) = other { + self.get_version() == other.get_version() && self.get_hash() == other.get_hash() + } else { + false + } + } + pub fn equals(&self, other: &Self) -> bool { + self.get_version() == other.get_version() && self.get_hash() == other.get_hash() + } + + 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 { + 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, + }) + } +} diff --git a/src/terms/focus.rs b/src/terms/focus.rs new file mode 100644 index 0000000..669e936 --- /dev/null +++ b/src/terms/focus.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Focus { + Eula, + Tos, + Pp, + Cancel, + Continue, + ContinueAll, +} + +impl Focus { + pub fn next(&mut self, order: &[Focus]) { + if let Some(pos) = order.iter().position(|&f| f == *self) { + let next_pos = (pos + 1) % order.len(); + *self = order[next_pos]; + } + } + + pub fn prev(&mut self, order: &[Focus]) { + if let Some(pos) = order.iter().position(|&f| f == *self) { + let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 }; + *self = order[prev_pos]; + } + } +} diff --git a/src/terms/mod.rs b/src/terms/mod.rs new file mode 100644 index 0000000..4a11008 --- /dev/null +++ b/src/terms/mod.rs @@ -0,0 +1,5 @@ +pub mod buttons; +pub mod consent_state; +pub mod doc; +pub mod focus; +pub mod terms_getter; diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs new file mode 100755 index 0000000..a0fa53b --- /dev/null +++ b/src/terms/terms_getter.rs @@ -0,0 +1,105 @@ +use json::JsonValue::Object; + +use crate::terms::doc::Doc; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Type { + EULA, + TOS, + PP, +} + +impl Type { + pub fn to_str(&self) -> &str { + match self { + Self::EULA => "eula", + Self::TOS => "tos", + Self::PP => "privacy", + } + } + pub fn to_string(&self) -> String { + match self { + Self::EULA => "End User License Agreement".to_string(), + Self::TOS => "Terms of Service".to_string(), + Self::PP => "Privacy Policy".to_string(), + } + } +} + +pub fn get_link(terms_type: Type) -> String { + format!("https://legal.tensamin.net/{}/", terms_type.to_str()) +} +pub fn get_newest_link(terms_type: Type) -> String { + format!("https://legal.tensamin.net/{}/newest/", 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_newest_docs() -> Option<(Doc, Doc, Doc)> { + let body = reqwest::get("https://legal.tensamin.net/api/newest/") + .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 { + let body = reqwest::get(format!( + "https://legal.tensamin.net/api/text/{}/", + terms_type.to_str() + )) + .await + .ok()? + .text() + .await + .ok()?; + + Some(body) +} diff --git a/src/users/contact.rs b/src/users/contact.rs new file mode 100644 index 0000000..632cea3 --- /dev/null +++ b/src/users/contact.rs @@ -0,0 +1,61 @@ +use json::{self, JsonValue, number::Number}; +use std::time::{SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone)] +pub struct Contact { + pub user_id: i64, + pub user_name: Option, + pub last_message_at: Option, +} + +impl Default for Contact { + fn default() -> Self { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + Contact { + user_id: 0, + user_name: None, + last_message_at: Some(now), + } + } +} + +impl Contact { + pub fn new(user_id: i64) -> Self { + Contact { + user_id: user_id, + user_name: None, + last_message_at: None, + } + } + pub fn set_last_message_at(&mut self, p0: i64) { + self.last_message_at = Option::from(p0); + } + + pub fn to_json(&self) -> JsonValue { + let mut obj = JsonValue::new_object(); + obj["user_id"] = JsonValue::Number(Number::from(self.user_id)); + if let Some(name) = &self.user_name { + obj["user_name"] = JsonValue::from(name.as_str()); + } + if let Some(ts) = &self.last_message_at { + obj["last_message_at"] = JsonValue::Number(Number::from(*ts)); + } + obj + } + pub fn from_json(o: &JsonValue) -> Contact { + let user_id = o["user_id"].as_i64().unwrap_or(0); + + let user_name = o["user_name"].as_str().map(|s| s.to_string()); + + let last_message_at = o["last_message_at"].as_i64(); + + Contact { + user_id, + user_name, + last_message_at, + } + } +} diff --git a/src/users/mod.rs b/src/users/mod.rs new file mode 100644 index 0000000..aef5a02 --- /dev/null +++ b/src/users/mod.rs @@ -0,0 +1,4 @@ +pub mod contact; +pub mod user_community_util; +pub mod user_manager; +pub mod user_profile; diff --git a/src/users/user_community_util.rs b/src/users/user_community_util.rs new file mode 100644 index 0000000..45b036c --- /dev/null +++ b/src/users/user_community_util.rs @@ -0,0 +1,67 @@ +use crate::util::file_util::save_file; +use json::{self, Array, JsonValue}; +use std::fs; +use std::path::Path; + +pub struct UserCommunityUtil; + +impl UserCommunityUtil { + pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { + let file_path = format!("users/{}/", storage_owner); + let mut communities = Self::load_array(&file_path); + + let mut community = JsonValue::new_object(); + community["title"] = JsonValue::String(title); + community["address"] = JsonValue::String(address); + community["position"] = JsonValue::String(position); + + communities.push(community); + + save_file( + &file_path, + "communities.json", + &JsonValue::Array(communities).to_string(), + ); + } + + pub fn remove_community(storage_owner: i64, community_address: String) { + let file_path = format!("users/{}/", storage_owner); + let communities = Self::load_array(&file_path); + + let filtered: Array = communities + .iter() + .filter(|entry| entry["address"].as_str() != Some(&community_address)) + .cloned() + .collect(); + save_file( + &file_path, + "communities.json", + &JsonValue::Array(filtered).to_string(), + ); + } + + pub fn get_communities(storage_owner: i64) -> Array { + let file_path = format!("users/{}/communities.json", storage_owner); + Self::load_array(&file_path) + } + + fn load_array(file_path: &str) -> Array { + if !Path::new(file_path).exists() { + return Array::new(); + } + + match fs::read_to_string(file_path) { + Ok(content) => { + let parsed = json::parse(&content); + match parsed { + Ok(JsonValue::Array(arr)) => arr, + _ => Array::new(), + } + } + Err(err) => { + eprintln!("Failed to read file {}: {}", file_path, err); + Array::new() + } + } + } +} diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs new file mode 100644 index 0000000..bcb5e26 --- /dev/null +++ b/src/users/user_manager.rs @@ -0,0 +1,197 @@ +use crate::omikron::omikron_connection::OMIKRON_CONNECTION; +use crate::users::user_profile::UserProfile; +use crate::util::crypto_helper::{self, public_key_to_base64}; +use crate::util::file_util::{load_file, save_file}; +use crate::util::logger::PrintType; +use crate::{RELOAD, SHUTDOWN}; +use crate::{log, log_cv}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use hex::{self}; +use json::JsonValue; +use once_cell::sync::Lazy; +use rand::Rng; +use rand_core::OsRng; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use std::io::{self}; +use std::sync::Mutex; +use std::time::Duration; +use ttp_core::{CommunicationType, CommunicationValue, DataTypes, DataValue}; +use x448::{PublicKey, Secret}; + +static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); +static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); + +#[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::>(); + let uuid = segments[0].parse::().unwrap_or(0); + let b64_private_key = segments[1]; + + let secret: Secret = crypto_helper::load_secret_key(b64_private_key).unwrap(); + let public_key = PublicKey::from(&secret); + + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let reset_token = STANDARD.encode(&bytes); + + let user_profile = UserProfile::new( + uuid, + username.to_string(), + Some(username.to_string()), + crypto_helper::public_key_to_base64(&public_key), + crypto_helper::hex_hash(b64_private_key), + reset_token, + ); + USERS.lock().unwrap().push(user_profile); + Ok(()) +} + +pub async fn create_user(username: &str) -> (Option, Option) { + let register_cv = CommunicationValue::new(CommunicationType::get_register); + + let conn = OMIKRON_CONNECTION.clone(); + + let response_cv = match conn + .await_response(®ister_cv, Some(Duration::from_secs(20))) + .await + { + Ok(cv) => cv, + Err(_) => return (None, None), + }; + log_cv!(PrintType::Omega, response_cv); + + let user_id = match response_cv.get_data(DataTypes::user_id).as_number() { + Some(id) => id, + None => return (None, None), + }; + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let private_key = Secret::from_bytes(&buf).unwrap(); + let public_key = PublicKey::from(&private_key); + + let mut hasher = Sha256::new(); + hasher.update(&STANDARD.encode(&private_key.as_bytes()).as_bytes()); + let result = hasher.finalize(); + let private_key_hash = hex::encode(result); + + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let reset_token = STANDARD.encode(&bytes); + + let up = UserProfile::new( + user_id, + username.to_string(), + None, + STANDARD.encode(&public_key.as_bytes()), + private_key_hash, + reset_token.clone(), + ); + + let cv = CommunicationValue::new(CommunicationType::complete_register_user) + .add_data(DataTypes::user_id, DataValue::Number(user_id)) + .add_data(DataTypes::username, DataValue::Str(username.to_string())) + .add_data( + DataTypes::public_key, + DataValue::Str(public_key_to_base64(&public_key)), + ) + .add_data(DataTypes::iota_id, DataValue::Number(user_id)) + .add_data(DataTypes::reset_token, DataValue::Str(reset_token)); + + let response_cv = conn + .await_response(&cv, Some(Duration::from_secs(20))) + .await; + + if let Ok(resp) = response_cv { + log_cv!(PrintType::Omega, resp); + if !resp.is_type(CommunicationType::success) { + return (None, None); + } + } else { + return (None, None); + } + *SHUTDOWN.write().await = true; + *RELOAD.write().await = true; + log!("Created User"); + save_file( + "", + &format!("{}.tu", username), + &format!("{}::{}", user_id, STANDARD.encode(&private_key.as_bytes())), + ); + + USERS.lock().unwrap().push(up.clone()); + save_users(); + (Some(up), Some(STANDARD.encode(&private_key.as_bytes()))) +} + +pub fn get_user_by_username(username: &str) -> Option { + USERS + .lock() + .unwrap() + .iter() + .cloned() + .find(|u| u.username == username) +} + +pub fn get_user(user_id: i64) -> Option { + USERS + .lock() + .unwrap() + .iter() + .cloned() + .find(|u| u.user_id == user_id) +} + +pub fn get_users() -> Vec { + USERS.lock().unwrap().clone() +} + +pub fn remove_user(user_id: i64) { + let mut users = USERS.lock().unwrap(); + users.retain(|u| u.user_id != user_id); + *UNIQUE.lock().unwrap() = true; +} + +pub fn save_users() { + *UNIQUE.lock().unwrap() = false; + let users = USERS.lock().unwrap(); + let arr: Vec = users.iter().map(|u| u.to_json()).collect(); + let json_str = JsonValue::Array(arr).dump(); + + save_file("", "users.json", &json_str); +} + +pub fn clear() { + let mut users = USERS.lock().unwrap(); + users.clear(); + *UNIQUE.lock().unwrap() = true; +} + +pub async fn load_users() -> io::Result<()> { + let content = load_file("", "users.json"); + if content.trim().is_empty() { + return Ok(()); + } + + let parsed = + json::parse(&content).map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + if let JsonValue::Array(arr) = parsed { + let mut users = USERS.lock().unwrap(); + for j in arr.iter() { + if let Some(up) = UserProfile::from_json(j).await { + users.push(up); + } + } + } + if *UNIQUE.lock().unwrap() { + save_users(); + } + Ok(()) +} + +#[allow(dead_code)] +pub fn set_unique(val: bool) { + *UNIQUE.lock().unwrap() = val; +} diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs new file mode 100644 index 0000000..5d60277 --- /dev/null +++ b/src/users/user_profile.rs @@ -0,0 +1,126 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::util::file_util::{has_file, load_file, used_dir_space}; +use base64::{Engine as _, engine::general_purpose}; +use json::{JsonValue, object}; +use rand::Rng; +use rand::rngs::OsRng; + +// --- UserProfile --- +#[derive(Clone, Debug)] +pub struct UserProfile { + pub user_id: i64, + pub username: String, + pub public_key: String, + pub private_key_hash: String, + pub reset_token: String, + pub created_at: i64, + pub display_name: Option, +} + +impl UserProfile { + pub fn new( + user_id: i64, + username: String, + display_name: Option, + public_key: String, + private_key_hash: String, + reset_token: String, + ) -> Self { + Self { + user_id, + username, + display_name, + public_key, + private_key_hash, + created_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as i64, + reset_token, + } + } + + pub fn to_json(&self) -> JsonValue { + let mut obj = object! { + "uuid" => self.user_id, + "username" => self.username.clone(), + "public_key" => self.public_key.clone(), + "private_key_hash" => self.private_key_hash.clone(), + "created_at" => self.created_at, + "reset_token" => self.reset_token.clone() + }; + if let Some(d) = &self.display_name { + obj["display_name"] = d.clone().into(); + } + obj + } + pub fn frontend(&self) -> JsonValue { + let mut obj = object! { + "uuid" => self.user_id, + "username" => self.username.clone(), + "public_key" => self.public_key.clone(), + "private_key_hash" => self.private_key_hash.clone(), + "created_at" => self.created_at, + "storage" => used_dir_space(&format!("users/{}", self.user_id.to_string())), + }; + if let Some(d) = &self.display_name { + obj["display_name"] = d.clone().into(); + } + if has_file("", &format!("{}.tu", self.username.clone())) { + obj["tu"] = load_file("", &format!("{}.tu", self.username.clone())).into(); + } + + obj + } + pub async fn from_json(j: &JsonValue) -> Option { + let user_id = j["uuid"].as_i64()?; + let username = j["username"].as_str()?.to_string(); + let public_key = j["public_key"].as_str()?.to_string(); + let private_key_hash = j["private_key_hash"].as_str()?.to_string(); + let reset_token = j["reset_token"].as_str()?.to_string(); + let created_at = j["created_at"].as_i64()?; + let display_name = j["display_name"].as_str().map(|s| s.to_string()); + + let up = UserProfile { + user_id, + username, + display_name, + public_key, + private_key_hash, + created_at, + reset_token, + }; + + // TODO: Migrate to Omikron / Wss + /* if j.has_key("migrate") + || j.has_key("migrating") + || j.has_key("changing") + || j.has_key("move") + || j.has_key("moving") + { + if auth_connector::migrate_user(&mut up).await { + log_message(format!("[INFO] Migration triggered for {}", up.username)); + user_manager::set_unique(true); + } + } */ + + Some(up) + } + + #[allow(dead_code)] + pub fn randomize_reset_token(&mut self) -> String { + let mut bytes = [0u8; 192]; + OsRng.fill(bytes.as_mut()); + let new_token = general_purpose::STANDARD.encode(&bytes); + self.reset_token = new_token.clone(); + new_token + } + + #[allow(dead_code)] + pub fn get_display_name(&self) -> String { + self.display_name + .clone() + .unwrap_or_else(|| self.username.clone()) + } +} diff --git a/src/util/chat_files.rs b/src/util/chat_files.rs new file mode 100644 index 0000000..e54fe29 --- /dev/null +++ b/src/util/chat_files.rs @@ -0,0 +1,276 @@ +use crate::log; +use crate::util::db; +use json::{JsonValue, array, object}; +use rusqlite::params; +use std::io; +use std::sync::{Arc, LazyLock, Mutex}; + +#[derive(PartialEq, Debug, Clone)] +pub enum MessageState { + Read, + Received, + Sent, + Sending, +} + +impl MessageState { + pub fn as_str(&self) -> &'static str { + match self { + MessageState::Read => "read", + MessageState::Received => "received", + MessageState::Sent => "sent", + MessageState::Sending => "sending", + } + } + + pub fn from_str(value: &str) -> Self { + match value.to_lowercase().as_str() { + "read" => MessageState::Read, + "received" => MessageState::Received, + "sent" => MessageState::Sent, + _ => MessageState::Sending, + } + } + + pub fn upgrade(self, other: Self) -> Self { + if other == Self::Read || self == Self::Read { + Self::Read + } else if other == Self::Received || self == Self::Received { + Self::Received + } else if other == Self::Sent || self == Self::Sent { + Self::Sent + } else { + Self::Sending + } + } +} + +// Shared DB created via helper. +// The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist. +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +pub fn add_message( + send_time: u128, + storage_owner_is_sender: bool, + storage_owner: i64, + external_user: i64, + message: &str, + height: i64, +) { + let message_time = match i64::try_from(send_time) { + Ok(v) => v, + Err(_) => { + log!("Failed to store message: send_time out of range for i64 ({send_time})"); + return; + } + }; + + // Insert the message into the DB + let insert_result = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO messages ( + storage_owner, + external_user, + message_time, + content, + sent_by_self, + message_state, + height + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + "#, + params![ + storage_owner, + external_user, + message_time, + message, + if storage_owner_is_sender { + 1_i64 + } else { + 0_i64 + }, + MessageState::Sending.as_str(), + height, + ], + )?; + Ok(()) + }); + + if let Err(e) = insert_result { + log!("Failed to insert message into sqlite: {}", e); + return; + } + + // Update contacts table to reflect that this conversation exists and has a recent message. + // Use the Contact helper to set last_message_at to the message timestamp. + let mut contact = crate::users::contact::Contact::new(external_user); + contact.set_last_message_at(message_time); + // This will insert or update the contact for the storage owner. + crate::util::chats_util::mod_user(storage_owner, &contact); +} + +pub fn change_message_state( + timestamp: i64, + storage_owner: i64, + external_user: i64, + new_state: MessageState, +) -> io::Result<()> { + // Run the SELECT and UPDATE inside with_conn to centralize connection access. + let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| { + let current: Option = match conn.query_row( + r#" + SELECT message_state + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + AND message_time = ?3 + ORDER BY id DESC + LIMIT 1 + "#, + params![storage_owner, external_user, timestamp], + |row| row.get(0), + ) { + Ok(state) => Some(state), + Err(rusqlite::Error::QueryReturnedNoRows) => None, + Err(e) => return Err(e), + }; + + let Some(current_state_raw) = current else { + return Ok(()); + }; + + let upgraded = MessageState::from_str(¤t_state_raw) + .upgrade(new_state) + .as_str() + .to_string(); + + conn.execute( + r#" + UPDATE messages + SET message_state = ?1 + WHERE id = ( + SELECT id + FROM messages + WHERE storage_owner = ?2 + AND external_user = ?3 + AND message_time = ?4 + ORDER BY id DESC + LIMIT 1 + ) + "#, + params![upgraded, storage_owner, external_user, timestamp], + )?; + Ok(()) + }); + + match res { + Ok(_) => Ok(()), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), + } +} + +pub fn get_messages( + storage_owner: i64, + external_user: i64, + loaded_messages: i64, + amount: i64, +) -> JsonValue { + let messages = array![]; + + if amount <= 0 || loaded_messages < 0 { + return messages; + } + + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT + message_time, + content, + sent_by_self, + message_state, + height + FROM messages + WHERE storage_owner = ?1 + AND external_user = ?2 + ORDER BY message_time DESC, id DESC + LIMIT ?3 OFFSET ?4 + "#, + )?; + + let rows = stmt.query_map( + params![storage_owner, external_user, amount, loaded_messages], + |row| { + let message_time: i64 = row.get(0)?; + let content: String = row.get(1)?; + let sent_by_self: i64 = row.get(2)?; + let message_state: String = row.get(3)?; + let height: i64 = row.get(4).unwrap_or(0); + Ok((message_time, content, sent_by_self, message_state, height)) + }, + )?; + + let mut out = array![]; + for row in rows { + match row { + Ok((message_time, content, sent_by_self, message_state, height)) => { + let msg = object! { + "message_time" => message_time, + "content" => content, + "sent_by_self" => (sent_by_self != 0), + "message_state" => message_state, + "height" => height + }; + if let Err(e) = out.push(msg) { + // out.push returns a JsonError; log it instead of using `?` to avoid + // incompatible error conversions inside the DB closure. + log!("Failed to append message to output array: {:?}", e); + } + } + Err(e) => { + log!("Failed to read row from sqlite: {}", e); + } + } + } + Ok(out) + }); + + match res { + Ok(v) => v, + Err(e) => { + log!("Failed to query messages: {}", e); + messages + } + } +} + +#[cfg(test)] +mod tests { + use super::MessageState; + + #[test] + fn upgrade_prefers_highest_state() { + assert_eq!( + MessageState::Sending.upgrade(MessageState::Sent), + MessageState::Sent + ); + assert_eq!( + MessageState::Sent.upgrade(MessageState::Received), + MessageState::Received + ); + assert_eq!( + MessageState::Received.upgrade(MessageState::Read), + MessageState::Read + ); + } + + #[test] + fn from_str_is_case_insensitive() { + assert_eq!(MessageState::from_str("READ"), MessageState::Read); + assert_eq!(MessageState::from_str("received"), MessageState::Received); + assert_eq!(MessageState::from_str("Sent"), MessageState::Sent); + assert_eq!(MessageState::from_str("unknown"), MessageState::Sending); + } +} diff --git a/src/util/chats_util.rs b/src/util/chats_util.rs new file mode 100644 index 0000000..b8e0ad2 --- /dev/null +++ b/src/util/chats_util.rs @@ -0,0 +1,121 @@ +use crate::users::contact::Contact; +use crate::util::db; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; + +/// Shared DB connection for contacts/messages (created by db helper). +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +/// Insert or update a contact for the given storage owner. +pub fn mod_user(storage_owner: i64, contact: &Contact) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO contacts ( + storage_owner, + user_id, + user_name, + last_message_at + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, user_id) DO UPDATE SET + user_name = excluded.user_name, + last_message_at = excluded.last_message_at + "#, + params![ + storage_owner, + contact.user_id, + contact.user_name.clone(), + contact.last_message_at + ], + )?; + Ok(()) + }) { + eprintln!("Failed to mod_user: {}", e); + } +} + +/// Retrieve a single contact for storage_owner/user_id. +pub fn get_user(storage_owner: i64, user_id: i64) -> Option { + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + match conn.query_row( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 AND user_id = ?2 + LIMIT 1 + "#, + params![storage_owner, user_id], + |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + }, + ) { + Ok(c) => Ok(Some(c)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + }); + + match res { + Ok(opt) => opt, + Err(e) => { + eprintln!("Error querying user in get_user: {}", e); + None + } + } +} + +/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc. +pub fn get_users(storage_owner: i64) -> Vec { + let contacts_out = Vec::new(); + + let res: Result, String> = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT user_id, user_name, last_message_at + FROM contacts + WHERE storage_owner = ?1 + ORDER BY + CASE WHEN last_message_at IS NULL THEN 1 ELSE 0 END, + last_message_at DESC, + user_id ASC + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { + let user_id: i64 = r.get(0)?; + let user_name: Option = r.get(1)?; + let last_message_at: Option = r.get(2)?; + Ok(Contact { + user_id, + user_name, + last_message_at, + }) + })?; + + let mut out = Vec::new(); + for row in rows { + match row { + Ok(contact) => out.push(contact), + Err(e) => eprintln!("Failed to read contact row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to query contacts in get_users: {}", e); + contacts_out + } + } +} diff --git a/src/util/communities_util.rs b/src/util/communities_util.rs new file mode 100644 index 0000000..ed8b7ae --- /dev/null +++ b/src/util/communities_util.rs @@ -0,0 +1,90 @@ +use crate::util::db; +use json::Array; +use rusqlite::params; +use std::sync::{Arc, LazyLock, Mutex}; + +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + db::create_general_messages_db().expect("Failed to create or initialize general messages DB") +}); + +pub struct CommunitiesUtil; + +impl CommunitiesUtil { + pub fn add_community(storage_owner: i64, address: String, title: String, position: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + r#" + INSERT INTO communities ( + storage_owner, + address, + title, + position + ) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(storage_owner, address) DO UPDATE SET + title = excluded.title, + position = excluded.position + "#, + params![storage_owner, address, title, position], + )?; + Ok(()) + }) { + eprintln!("Failed to add_community: {}", e); + } + } + + pub fn remove_community(storage_owner: i64, community_address: String) { + if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + conn.execute( + "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", + params![storage_owner, community_address], + )?; + Ok(()) + }) { + eprintln!("Failed to remove_community: {}", e); + } + } + + pub fn get_communities(storage_owner: i64) -> Array { + let communities_out = Array::new(); + + let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare( + r#" + SELECT address, title, position + FROM communities + WHERE storage_owner = ?1 + "#, + )?; + + let rows = stmt.query_map(params![storage_owner], |r| { + let address: String = r.get(0)?; + let title: String = r.get(1)?; + let position: String = r.get(2)?; + Ok((address, title, position)) + })?; + + let mut out = Array::new(); + for row in rows { + match row { + Ok((address, title, position)) => { + let mut community = json::JsonValue::new_object(); + community["title"] = json::JsonValue::String(title); + community["address"] = json::JsonValue::String(address); + community["position"] = json::JsonValue::String(position); + out.push(community); + } + Err(e) => eprintln!("Failed to read community row: {}", e), + } + } + Ok(out) + }); + + match res { + Ok(arr) => arr, + Err(e) => { + eprintln!("Failed to query communities in get_communities: {}", e); + communities_out + } + } + } +} diff --git a/src/util/config_util.rs b/src/util/config_util.rs new file mode 100644 index 0000000..6330b59 --- /dev/null +++ b/src/util/config_util.rs @@ -0,0 +1,60 @@ +use crate::util::file_util::{load_file, save_file}; +use json::JsonValue; +use once_cell::sync::Lazy; +use tokio::sync::RwLock; + +pub static CONFIG: Lazy> = Lazy::new(|| RwLock::new(ConfigUtil::new())); + +pub struct ConfigUtil { + pub config: JsonValue, + pub unique: bool, +} + +impl ConfigUtil { + pub fn new() -> Self { + Self { + config: JsonValue::new_object(), + unique: false, + } + } + pub fn clear(&mut self) { + self.config = JsonValue::new_object(); + } + pub fn load(&mut self) { + let s = load_file("", "config.json"); + if !s.is_empty() { + self.config = json::parse(&s).unwrap_or(JsonValue::new_object()); + } + } + + pub fn get_iota_id(&self) -> i64 { + self.config["iota_id"].as_i64().unwrap_or(0) + } + + pub fn get_port(&self) -> u16 { + self.config["port"].as_u16().unwrap_or(1984) + } + + pub fn get_public_key(&self) -> Option { + self.config["public_key"].as_str().map(String::from) + } + + pub fn get_private_key(&self) -> Option { + self.config["private_key"].as_str().map(String::from) + } + + pub fn get(&self, key: &str) -> &JsonValue { + &self.config[key] + } + + pub fn change(&mut self, key: &str, value: JsonValue) { + self.config[key] = value; + self.unique = true; + } + + pub fn update(&mut self) { + if self.unique { + save_file("", "config.json", &self.config.to_string()); + } + } +} diff --git a/src/util/crypto_helper.rs b/src/util/crypto_helper.rs new file mode 100644 index 0000000..4a7fd76 --- /dev/null +++ b/src/util/crypto_helper.rs @@ -0,0 +1,133 @@ +use aes_gcm::{ + Aes256Gcm, Nonce, + aead::{Aead, KeyInit, OsRng}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use rand_core::RngCore; +use sha2::{Digest, Sha256}; +use x448::{PublicKey, Secret, SharedSecret}; + +/// Errors for crypto opertions +#[derive(Debug)] +#[allow(dead_code)] +pub enum CryptoError { + Base64Decode(base64::DecodeError), + InvalidKey, + AgreementError, + EncryptionError(aes_gcm::Error), + DecryptionError(aes_gcm::Error), +} + +impl From for CryptoError { + fn from(err: base64::DecodeError) -> Self { + CryptoError::Base64Decode(err) + } +} + +pub struct KeyPair { + pub secret: Secret, + pub public: PublicKey, +} + +pub fn generate_keypair() -> KeyPair { + let mut buf = [0u8; 56]; + let mut rng = OsRng; + rng.fill_bytes(&mut buf); + let secret = Secret::from_bytes(&buf).unwrap(); + let public = PublicKey::from(&secret); + KeyPair { secret, public } +} + +pub fn public_key_to_base64(pubkey: &PublicKey) -> String { + STANDARD.encode(pubkey.as_bytes().as_ref()) +} + +pub fn secret_key_to_base64(secret: &Secret) -> String { + STANDARD.encode(secret.as_bytes().as_ref()) +} + +pub fn load_public_key(base64_pub: &str) -> Option { + let bytes = STANDARD.decode(base64_pub).unwrap(); + PublicKey::from_bytes(&bytes) +} + +pub fn load_secret_key(base64_secret: &str) -> Option { + let bytes = STANDARD.decode(base64_secret).unwrap(); + 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()); + let result = hasher.finalize(); + let mut key = [0u8; 32]; + key.copy_from_slice(&result[..32]); + key +} + +#[allow(dead_code)] +pub fn encrypt( + base64_secret: &str, + base64_peer_pub: &str, + plaintext: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + let ciphertext = cipher + .encrypt(nonce, plaintext.as_bytes()) + .map_err(CryptoError::EncryptionError)?; + // prefix nonce to ciphertext + let mut out = Vec::with_capacity(nonce_bytes.len() + ciphertext.len()); + out.extend_from_slice(&nonce_bytes); + out.extend_from_slice(&ciphertext); + Ok(STANDARD.encode(&out)) +} + +#[allow(dead_code)] +pub fn decrypt( + base64_secret: &str, + base64_peer_pub: &str, + encrypted_base64: &str, +) -> Result { + let secret = load_secret_key(base64_secret).unwrap(); + let peer_pub = load_public_key(base64_peer_pub).unwrap(); + let shared = secret + .to_diffie_hellman(&peer_pub) + .ok_or(CryptoError::AgreementError)?; + let key_bytes = derive_aes_key(&shared); + let cipher = Aes256Gcm::new_from_slice(&key_bytes).expect("Key length should be correct"); + + let encrypted = STANDARD.decode(encrypted_base64)?; + if encrypted.len() < 12 { + return Err(CryptoError::DecryptionError(aes_gcm::Error)); + } + let nonce_bytes = &encrypted[..12]; + let ciphertext = &encrypted[12..]; + let nonce = Nonce::from_slice(nonce_bytes); + let plaintext_bytes = cipher + .decrypt(nonce, ciphertext) + .map_err(CryptoError::DecryptionError)?; + let plaintext = String::from_utf8(plaintext_bytes) + .map_err(|_| CryptoError::DecryptionError(aes_gcm::Error))?; + Ok(plaintext) +} + +pub fn hash_it(input: &str) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(input.as_bytes()); + hasher.finalize().to_vec() +} + +pub fn hex_hash(input: &str) -> String { + let digest = hash_it(input); + digest.iter().map(|b| format!("{:02x}", b)).collect() +} diff --git a/src/util/crypto_util.rs b/src/util/crypto_util.rs new file mode 100644 index 0000000..bb784e8 --- /dev/null +++ b/src/util/crypto_util.rs @@ -0,0 +1,178 @@ +use aes_gcm::{ + Aes256Gcm, Nonce, + aead::{Aead, KeyInit, Payload}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STD}; +use hkdf::Hkdf; +type HkdfSha256 = sha2::Sha256; +use sha2::{Digest, Sha256 as HashSha256}; +use x448::{PublicKey, Secret}; + +#[derive(Debug)] +#[allow(dead_code)] +pub enum SecurePayloadError { + InvalidBase64, + InvalidHex, + EncryptionError, + DecryptionError, + InvalidKeyLength, +} + +#[derive(Clone, Copy, Debug)] +#[allow(dead_code)] +pub enum DataFormat { + Raw, + Base64, + Hex, +} + +pub struct SecurePayload { + inner_data: Vec, + private_key: Secret, +} + +impl Clone for SecurePayload { + fn clone(&self) -> Self { + Self { + inner_data: self.inner_data.clone(), + private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), + } + } +} + +#[allow(dead_code)] +impl SecurePayload { + pub fn new>( + data: T, + format: DataFormat, + private_key: S, + ) -> Result + where + S: Into, + { + let raw_data = match format { + DataFormat::Raw => data.as_ref().to_vec(), + DataFormat::Base64 => BASE64_STD + .decode(data.as_ref()) + .map_err(|_| SecurePayloadError::InvalidBase64)?, + DataFormat::Hex => { + hex::decode(data.as_ref()).map_err(|_| SecurePayloadError::InvalidHex)? + } + }; + + Ok(Self { + inner_data: raw_data, + private_key: private_key.into(), + }) + } + + pub fn get_public_key(&self) -> [u8; 56] { + *PublicKey::from(&self.private_key).as_bytes() + } + + pub fn export(&self, format: DataFormat) -> String { + match format.into() { + DataFormat::Raw => String::from_utf8_lossy(&self.inner_data).to_string(), + DataFormat::Base64 => BASE64_STD.encode(&self.inner_data), + DataFormat::Hex => hex::encode(&self.inner_data), + } + } + + pub fn get_bytes(&self) -> &[u8] { + &self.inner_data + } + + pub fn get_hash(&self, format: DataFormat) -> String { + let mut hasher = HashSha256::new(); + hasher.update(&self.inner_data); + let result = hasher.finalize(); + + match format { + DataFormat::Raw => String::from_utf8_lossy(&result).to_string(), + DataFormat::Base64 => BASE64_STD.encode(result), + DataFormat::Hex => hex::encode(result), + } + } + + pub fn encrypt_x448(&self, public_key: S) -> Result + where + S: Into, + { + let peer_pub = public_key.into(); + let shared_secret = self.private_key.as_diffie_hellman(&peer_pub).unwrap(); + + let hkdf = Hkdf::::new(None, shared_secret.as_bytes()); + let mut okm = [0u8; 44]; + + hkdf.expand(b"x448-aes-gcm-no-overhead", &mut okm) + .map_err(|_| SecurePayloadError::EncryptionError)?; + + let key = &okm[..32]; + let nonce_bytes = &okm[32..]; + + let cipher = Aes256Gcm::new(key.into()); + let nonce = Nonce::from_slice(nonce_bytes); + + let ciphertext = cipher + .encrypt( + nonce, + Payload { + msg: &self.inner_data, + aad: &[], + }, + ) + .map_err(|_| SecurePayloadError::EncryptionError)?; + + Ok(SecurePayload { + inner_data: ciphertext, + private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), + }) + } + + pub fn decrypt_to_format( + &self, + peer_public_key_bytes: &[u8; 56], + output_format: DataFormat, + ) -> Result { + let decrypted_instance = + self.decrypt_x448(PublicKey::from_bytes(peer_public_key_bytes).unwrap())?; + Ok(decrypted_instance.export(output_format)) + } + + pub fn decrypt_x448( + &self, + peer_public_key_bytes: S, + ) -> Result + where + S: Into, + { + let peer_pub = peer_public_key_bytes.into(); + let shared_secret = self.private_key.as_diffie_hellman(&peer_pub).unwrap(); + + let hkdf = Hkdf::::new(None, shared_secret.as_bytes()); + let mut okm = [0u8; 44]; + hkdf.expand(b"x448-aes-gcm-no-overhead", &mut okm) + .map_err(|_| SecurePayloadError::DecryptionError)?; + + let key = &okm[..32]; + let nonce_bytes = &okm[32..]; + + let cipher = Aes256Gcm::new(key.into()); + let nonce = Nonce::from_slice(nonce_bytes); + + let plaintext = cipher + .decrypt( + nonce, + Payload { + msg: &self.inner_data, + aad: &[], + }, + ) + .map_err(|_| SecurePayloadError::DecryptionError)?; + + Ok(SecurePayload { + inner_data: plaintext, + private_key: Secret::from_bytes(self.private_key.as_bytes()).unwrap(), + }) + } +} diff --git a/src/util/db.rs b/src/util/db.rs new file mode 100644 index 0000000..9fb40a0 --- /dev/null +++ b/src/util/db.rs @@ -0,0 +1,185 @@ +//! Database helper utilities. +//! +//! This module provides small helpers to open/init sqlite databases and to +//! create a shared (Arc>) connection wrapper callers can +//! reuse. The goal is to centralize the "open and initialize" logic and +//! provide small convenience helpers used by other util modules. + +use crate::util::file_util::get_directory; +use rusqlite::{Connection, Error as RusqliteError}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +/// Returns the file path for a named DB inside the application's data directory. +/// +/// Arguments: +/// - `db_name` : name of the DB (without extension). Example: `"messages"`. +pub fn db_file_path(db_name: &str) -> String { + let mut p = PathBuf::from(get_directory()); + p.push(format!("{db_name}.sqlite3")); + p.to_string_lossy().to_string() +} + +/// Open a sqlite connection to the named DB file (no initialization). +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +pub fn open_connection(db_name: &str) -> Result { + let path = db_file_path(db_name); + Connection::open(path) +} + +/// Open a connection and immediately run `init_sql` via `execute_batch`. +/// +/// Arguments: +/// - `db_name`: name of the DB (without extension). +/// - `init_sql`: SQL statements to initialize schema & PRAGMAs (can be multiple). +pub fn open_and_init(db_name: &str, init_sql: &str) -> Result { + let conn = open_connection(db_name)?; + conn.execute_batch(init_sql)?; + Ok(conn) +} + +/// Create a shared, Arc> initialized with the given SQL. +/// +/// This is a convenience wrapper that returns an owned Arc> +/// so caller modules can store it in a `static` or pass it around. +/// +/// Arguments: +/// - `db_name`: DB name (without extension). +/// - `init_sql`: init SQL (eg PRAGMA + CREATE TABLE statements). +pub fn create_shared_connection( + db_name: &str, + init_sql: &str, +) -> Result>, String> { + match open_and_init(db_name, init_sql) { + Ok(conn) => { + // Configure some sensible defaults for concurrency + // Attempt to set a busy timeout to reduce SQLITE_BUSY failures. + let _ = conn.busy_timeout(Duration::from_millis(250)); + Ok(Arc::new(Mutex::new(conn))) + } + Err(e) => Err(format!("Failed to open/init DB '{}': {}", db_name, e)), + } +} + +/// Acquire the Connection from an Arc> and run the provided +/// closure. Converts rusqlite::Error into a String on error. +/// +/// Arguments: +/// - `shared`: Arc> +/// - `f`: closure that receives &Connection and returns Result +/// +/// Returns Ok(T) or Err(String). +pub fn with_conn(shared: &Arc>, f: F) -> Result +where + F: FnOnce(&Connection) -> Result, +{ + // When invoked from within an async runtime (such as Tokio), taking a blocking + // std::sync::Mutex lock on the runtime thread can cause deadlocks or permanent + // awaits. Detect whether we're running inside a Tokio runtime and, if so, + // execute the blocking lock + database closure using Tokio's blocking helper. + // + // The blocking section returns Result so we can propagate errors + // in the same form as before. + if tokio::runtime::Handle::try_current().is_ok() { + tokio::task::block_in_place(|| { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + }) + } else { + let guard = shared + .lock() + .map_err(|e| format!("DB mutex poisoned: {:?}", e))?; + f(&*guard).map_err(|e| e.to_string()) + } +} + +/// Initialize a general-purpose messages+contacts DB and return a shared +/// connection. This helper creates a single DB file that can contain multiple +/// tables (messages, contacts, ...). The SQL here is conservative and intended +/// to be safe if called multiple times. +/// +/// Callers may prefer to call `create_shared_connection("messages", INIT_SQL)` +/// directly, but this convenience is useful for code that expects both tables. +pub fn create_general_messages_db() -> Result>, String> { + // Keep PRAGMA and schema in one multi-statement string so callers only + // need to call a single execute_batch. + const INIT_SQL: &str = r#" + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + external_user INTEGER NOT NULL, + message_time INTEGER NOT NULL, + content TEXT NOT NULL, + sent_by_self INTEGER NOT NULL, + message_state TEXT NOT NULL, + height INTEGER NOT NULL DEFAULT 0 + ); + + CREATE INDEX IF NOT EXISTS idx_messages_lookup + ON messages (storage_owner, external_user, message_time DESC); + + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + user_id INTEGER NOT NULL, + user_name TEXT, + last_message_at INTEGER, + UNIQUE(storage_owner, user_id) + ); + + CREATE INDEX IF NOT EXISTS idx_contacts_owner + ON contacts (storage_owner, last_message_at DESC, user_id ASC); + + CREATE TABLE IF NOT EXISTS communities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_owner INTEGER NOT NULL, + address TEXT NOT NULL, + title TEXT NOT NULL, + position TEXT NOT NULL, + UNIQUE(storage_owner, address) + ); + + CREATE INDEX IF NOT EXISTS idx_communities_owner + ON communities (storage_owner); + "#; + + match create_shared_connection("messages", INIT_SQL) { + Ok(shared_conn) => { + // Attempt to add the height column for backwards compatibility. + // This will fail if the column already exists, which is expected. + let _ = with_conn(&shared_conn, |conn| { + let _ = conn.execute( + "ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0", + [], + ); + Ok(()) + }); + Ok(shared_conn) + } + Err(e) => Err(e), + } +} + +/* +Example usage: + +// In some util module (at init time, e.g. lazy_static or LazyLock) +static MESSAGES_DB: LazyLock>> = LazyLock::new(|| { + create_general_messages_db().expect("failed to create messages DB") +}); + +// Later, to run a query: +let res: Result, String> = with_conn(&MESSAGES_DB, |conn| { + let mut stmt = conn.prepare("SELECT ...")?; + let rows = stmt.query_map(...)?; + // collect and return Ok(...) +}); +*/ diff --git a/src/util/file_util.rs b/src/util/file_util.rs new file mode 100755 index 0000000..2d00718 --- /dev/null +++ b/src/util/file_util.rs @@ -0,0 +1,354 @@ +use reqwest::Client; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::{self, BufReader, Read}; +use std::path::{Path, PathBuf}; +use sysinfo::System; +use tokio::io::AsyncWriteExt; +use uuid::Uuid; +use walkdir::WalkDir; +use zip::ZipArchive; + +use crate::log; + +#[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; + } + if let Err(e) = fs::remove_dir_all(directory) { + log!( + "[IMPORTANT] Couldn't delete directory {}: {}", + directory.display(), + e, + ); + return false; + } + true +} + +#[allow(dead_code)] +pub fn delete_user_directory(user_id: i64) { + let user_dir = Path::new(&get_directory()) + .join("users") + .join(user_id.to_string()); + let _ = delete_dir_recursive(&user_dir); +} + +pub fn load_file_buf(path: &str, name: &str) -> io::Result> { + let dir = Path::new(&get_directory()).join(path); + let file_path = dir.join(name); + + // Ensure the directory exists, create if necessary + if !dir.exists() { + if let Err(_) = fs::create_dir_all(&dir) { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "Directory creation failed", + )); + } + } + + // Create the file if it doesn't exist + if !file_path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "File creation failed", + )); + } + + // Open the file and return a BufReader for efficient reading + let file = File::open(&file_path)?; + Ok(BufReader::new(file)) +} +pub fn has_file(path: &str, name: &str) -> bool { + let dir = Path::new(&get_directory()).join(path); + let file_path = dir.join(name); + + if !dir.exists() { + return false; + } + + if !file_path.exists() { + return false; + } + + true +} +pub fn has_dir(path: &str) -> bool { + let dir = Path::new(&get_directory()).join(path); + + if !dir.exists() { + return false; + } + + true +} + +pub fn load_file(path: &str, name: &str) -> String { + let dir = Path::new(&get_directory()).join(path); + let file_path = dir.join(name); + + if !dir.exists() { + if let Err(e) = fs::create_dir_all(&dir) { + log!("[IMPORTANT] Couldn't create directories: {}", e); + return String::new(); + } + return String::new(); + } + + if !file_path.exists() { + if let Err(e) = File::create(&file_path) { + log!("[IMPORTANT] Couldn't create file: {}", e); + } + return String::new(); + } + + let mut content = String::new(); + if let Ok(mut f) = File::open(&file_path) { + let _ = f.read_to_string(&mut content); + } + content +} + +pub fn load_file_vec(path: &str, name: &str) -> Result, std::io::Error> { + let dir = Path::new(&get_directory()).join(path); + let file_path = dir.join(name); + + std::fs::read(file_path) +} + +pub fn save_file(path: &str, name: &str, value: &str) { + let dir = Path::new(&get_directory()).join(path); + let file_path = dir.join(name); + + if !dir.exists() { + if let Err(e) = fs::create_dir_all(&dir) { + log!("[IMPORTANT] Couldn't create directories: {}", e); + return; + } + } + + if let Err(e) = fs::write(&file_path, value) { + log!( + "[IMPORTANT] Couldn't write file {}: {}", + file_path.display(), + e + ); + } +} + +pub fn get_children(path: &str) -> Vec { + let dir = Path::new(&get_directory()).join(path); + let mut children = Vec::new(); + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries { + if let Ok(entry) = entry { + children.push(entry.file_name().to_string_lossy().to_string()); + } + } + } + children +} + +pub fn get_directory() -> String { + let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(".")); + exe.parent() + .unwrap_or(Path::new(".")) + .to_string_lossy() + .to_string() +} + +// Helper to download the zip file content to a file on disk +#[allow(dead_code)] +pub fn used_space() -> u64 { + get_directory_size(&PathBuf::from(get_directory())) +} +pub fn used_dir_space(path: &str) -> u64 { + get_directory_size(&PathBuf::from(format!("{}/{}", get_directory(), path))) +} + +pub fn get_directory_size(directory: &Path) -> u64 { + let mut size = 0; + for entry in WalkDir::new(directory).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + if path.is_file() { + if let Ok(metadata) = path.metadata() { + size += path.file_name().unwrap_or(OsStr::new("")).len() as u64; + size += metadata.len(); + } + } + } + size +} + +#[allow(dead_code)] +pub fn get_designed_storage(user_id: i64) -> String { + let user_dir = Path::new(&get_directory()) + .join("users") + .join(user_id.to_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; + let m = k / 1024.0; + let g = m / 1024.0; + let t = g / 1024.0; + + if t >= 1.0 { + hr_size = format!("{:.2}TB", t); + } else if g >= 1.0 { + hr_size = format!("{:.2}GB", g); + } else if m >= 1.0 { + hr_size = format!("{:.2}MB", m); + } else if k >= 1.0 { + hr_size = format!("{:.2}KB", k); + } + hr_size +} + +#[allow(dead_code)] +pub fn get_used_ram() -> String { + let mut sys = System::new_all(); + sys.refresh_all(); + let used = sys.used_memory() * 1024; // kB to bytes + let total = sys.total_memory() * 1024; + format!("{}/{}", design_byte(used), design_byte(total)) +} + +pub async fn download_zip(url: &str, zip_path: &Path) -> Result<(), Box> { + let client = Client::new(); + let mut response = client.get(url).send().await?; + + if !response.status().is_success() { + let err_msg = format!("Failed to download file: Status {}", response.status()); + log!("{}", err_msg.clone()); + return Err(err_msg.into()); + } + + let mut zip_file = tokio::fs::File::create(zip_path).await?; + while let Some(chunk) = response.chunk().await? { + zip_file.write_all(&chunk).await?; + } + + zip_file.flush().await?; + Ok(()) +} + +#[allow(dead_code, deprecated)] +fn extract_zip_contents_to_folder( + zip_path: &Path, + target_dir: &Path, +) -> Result<(), Box> { + let file = File::open(zip_path)?; + let mut archive = ZipArchive::new(file)?; + + let staging_dir = target_dir.with_extension("staging"); + + let _ = fs::remove_dir_all(&staging_dir); + fs::create_dir_all(&staging_dir)?; + + let mut first_item_name: Option = None; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let entry_path = staging_dir.join(file.sanitized_name()); + + if i == 0 { + if file.name().ends_with('/') || file.sanitized_name().components().count() == 1 { + first_item_name = Some(file.sanitized_name()); + } + } + + if file.name().ends_with('/') { + fs::create_dir_all(&entry_path)?; + } else { + if let Some(parent) = entry_path.parent() { + fs::create_dir_all(parent)?; + } + let mut out_file = File::create(entry_path)?; + io::copy(&mut file, &mut out_file)?; + } + } + + if let Some(root_path) = first_item_name { + let root_dir = staging_dir.join(&root_path); + + if root_dir.is_dir() { + let root_contents_count = fs::read_dir(&staging_dir)?.count(); + + if root_contents_count == 1 + || (root_contents_count > 1 && fs::metadata(&root_dir).is_ok()) + { + let _ = fs::remove_dir_all(target_dir); + fs::create_dir_all(target_dir)?; + + for entry in fs::read_dir(root_dir)? { + let entry = entry?; + let src = entry.path(); + let dest = target_dir.join(entry.file_name()); + + if let Err(_) = fs::rename(&src, &dest) { + if src.is_file() { + fs::copy(&src, &dest)?; + } else { + if entry.path().is_dir() { + fs::rename(&src, &dest)?; + } + } + } + } + + let _ = fs::remove_dir_all(&staging_dir); + return Ok(()); + } + } + } + + log!("Extracting directly (no single root folder detected)."); + let _ = fs::remove_dir_all(target_dir); + fs::rename(&staging_dir, target_dir)?; + + Ok(()) +} + +#[allow(dead_code)] +pub async fn download_and_extract_zip(url: &str, as_name: &str) { + log!("Downloading ZIP file..."); + let base_dir = PathBuf::from(get_directory()); + let zip_filename = format!("{}.zip", Uuid::new_v4()); + let zip_path = base_dir.join(&zip_filename); + let target_dir = base_dir.join(as_name); + + if let Err(e) = download_zip(url, &zip_path).await { + log!("Error downloading file: {}", e); + return; + } + + let zip_path_clone = zip_path.clone(); + let target_dir_clone = target_dir.clone(); + let extract_result = extract_zip_contents_to_folder(&zip_path_clone, &target_dir_clone); + + let successful = match extract_result { + Ok(()) => true, + Err(e) => { + log!("Error during ZIP extraction: {}", e); + false + } + }; + + if let Err(e) = tokio::fs::remove_file(&zip_path).await { + log!("Error cleaning up ZIP file {}: {}", zip_path.display(), e); + } else if successful { + log!("Downloaded and extracted ZIP file successfully."); + } +} diff --git a/src/util/logger.rs b/src/util/logger.rs new file mode 100755 index 0000000..a5feb02 --- /dev/null +++ b/src/util/logger.rs @@ -0,0 +1,432 @@ +use std::{ + collections::BTreeMap, + fs::{self, OpenOptions}, + io::Write, + path::Path, + sync::{OnceLock, atomic::Ordering, mpsc}, + thread, + time::{SystemTime, UNIX_EPOCH}, +}; + +use ratatui::style::Color; +use ttp_core::{CommunicationValue, DataTypes, DataValue}; + +use crate::{ + APP_STATE, + gui::{elements::log_card::LogEntry, ui::UNIQUE}, + langu::language_manager, +}; + +static LOGGER: OnceLock> = OnceLock::new(); + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[allow(unused)] +pub enum PrintType { + Call, + Client, + Iota, + Omikron, + Omega, + General, + Command, +} +impl PrintType { + pub fn prefix_color(self) -> Color { + match self { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } + } +} + +struct LogMessage { + timestamp_ms: u128, + prefix: String, + kind: PrintType, + is_error: bool, + translation_key: Option, + format_args: Vec, + message: Option, +} + +pub fn startup() { + let (tx, rx) = mpsc::channel::(); + LOGGER.set(tx).expect("Logger already initialized"); + + thread::spawn(move || { + let log_dir = Path::new("logs"); + fs::create_dir_all(log_dir).expect("Failed to create log directory"); + + let start_ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let path = log_dir.join(format!("log_{}.txt", start_ts)); + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(path) + .expect("Failed to open log file"); + + for msg in rx { + let resolved_message = if let Some(key) = msg.translation_key { + let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); + language_manager::format(&key, &args) + } else { + msg.message.unwrap_or_default() + }; + + let timestamp = format_timestamp_inline(msg.timestamp_ms); + let entry = LogEntry::new(msg.kind, resolved_message, msg.is_error); + + let prefix = if msg.prefix.is_empty() { + String::new() + } else { + format!("{} ", msg.prefix) + }; + + let _ = writeln!( + file, + "{} {}{}", + fixed_box(&msg.timestamp_ms.to_string(), 13), + prefix, + entry.message + ); + + let _ = writeln!(file, " {}", timestamp); + + let mut state = APP_STATE.lock().unwrap(); + state.push_log(entry.into()); + } + }); +} + +fn format_timestamp_inline(timestamp_ms: u128) -> String { + let secs = (timestamp_ms / 1000) as i64; + let hours = (secs / 3600) % 24; + let minutes = (secs / 60) % 60; + let seconds = secs % 60; + format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) +} + +fn fixed_box(content: &str, width: usize) -> String { + let s: String = content.chars().take(width).collect(); + let len = s.chars().count(); + if len < width { + format!("[{}{}]", " ".repeat(width - len), s) + } else { + s + } +} + +fn colorize(kind: PrintType, is_error: bool) -> Color { + if is_error { + return Color::Red; + } + + match kind { + PrintType::Call => Color::Magenta, + PrintType::Client => Color::Green, + PrintType::Iota => Color::Yellow, + PrintType::Omikron => Color::Blue, + PrintType::Omega => Color::Cyan, + PrintType::General => Color::LightCyan, + PrintType::Command => Color::LightGreen, + } +} + +pub fn log_internal_translated( + kind: PrintType, + prefix: String, + is_error: bool, + key: &str, + args: Vec, +) { + if let Some(tx) = LOGGER.get() { + UNIQUE.store(true, Ordering::Relaxed); + let _ = tx.send(LogMessage { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + prefix, + kind, + is_error, + translation_key: Some(key.to_string()), + format_args: args, + message: None, + }); + } +} + +pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { + if let Some(tx) = LOGGER.get() { + UNIQUE.store(true, Ordering::Relaxed); + let _ = tx.send(LogMessage { + timestamp_ms: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis(), + prefix, + kind, + is_error, + translation_key: None, + format_args: Vec::new(), + message: Some(message), + }); + } +} + +#[macro_export] +macro_rules! log_t { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + $key, + vec![$($arg),+] + ) + }; +} + +#[macro_export] +macro_rules! log_t_err { + ($key:expr) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + true, + $key, + vec![] + ) + }; + + ($key:expr, $($arg:expr),+) => { + $crate::util::logger::log_internal_translated( + $crate::util::logger::PrintType::General, + "".to_string(), + true, + $key, + vec![$($arg.to_string()),+] + ) + }; +} + +/// Log a command message. +#[macro_export] +macro_rules! log_command { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::Command, + "".to_string(), + false, + format!($($arg)*) + ) + }; +} + +/// Log a general informational message. +#[macro_export] +macro_rules! log { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "".to_string(), + false, + format!($($arg)*) + ) + }; +} + +/// Log an inbound message (`>`). +#[macro_export] +macro_rules! log_in { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">".to_string(), + false, + format!($($arg)*) + ) + }; +} + +/// Log an outbound message (`<`). +#[macro_export] +macro_rules! log_out { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + "<".to_string(), + false, + format!($($arg)*) + ) + }; +} + +/// Log an error message (`>>`). +#[macro_export] +macro_rules! log_err { + ($($arg:tt)*) => { + $crate::util::logger::log_internal( + $crate::util::logger::PrintType::General, + ">>".to_string(), + true, + format!($($arg)*) + ) + }; +} + +// ******** COMMUNICATION VALUES ******** +pub fn log_cv_internal( + prefix: &'static str, + cv: &CommunicationValue, + print_type: Option, +) { + let formatted = format_cv(cv); + + log_internal( + print_type.unwrap_or(PrintType::General), + prefix.to_string(), + false, + formatted, + ); +} + +pub fn format_cv(cv: &CommunicationValue) -> String { + let mut parts = Vec::new(); + + let sender = cv.get_sender(); + let receiver = cv.get_receiver(); + + if sender > 0 && receiver > 0 { + parts.push(format!("{} > {}", sender, receiver)); + } else if sender > 0 { + parts.push(format!("{}", sender)); + } else if receiver > 0 { + parts.push(format!("> {}", receiver)); + } + + let comm_type = cv.get_type().to_string(); + parts.push(format!("{}", comm_type)); + + let data: &BTreeMap = cv.get_data_container(); + + let formated_data = + format_data_container(data.iter().map(|(k, v)| (k.clone(), v.clone())).collect()); + + parts.push(format!("{}", formated_data)); + + parts.join(": ") +} + +fn format_data_container(data: Vec<(DataTypes, DataValue)>) -> String { + let parts: Vec = data + .into_iter() + .map(|(key, value)| { + let key_str = key.to_string(); + + match value { + DataValue::Str(s) => format!("{}=\"{}\"", key_str, s), + + DataValue::Container(inner) => { + let inner_formatted = format_data_container(inner); + format!("{}={{ {} }}", key_str, inner_formatted) + } + + DataValue::Array(arr) => { + let arr_formatted = format_array(arr); + format!("{}=[{}]", key_str, arr_formatted) + } + + DataValue::Bool(b) => format!("{}={}", key_str, b), + + DataValue::BoolTrue => format!("{}=true", key_str), + DataValue::BoolFalse => format!("{}=false", key_str), + + DataValue::Number(num) => format!("{}={}", key_str, num), + + _ => "".to_string(), + } + }) + .collect(); + + parts.join(", ") +} + +fn format_array(arr: Vec) -> String { + let parts: Vec = arr + .into_iter() + .map(|value| match value { + DataValue::Str(s) => format!("\"{}\"", s), + + DataValue::Container(inner) => { + let inner_formatted = format_data_container(inner); + format!("{{ {} }}", inner_formatted) + } + + DataValue::Array(inner_arr) => { + let formatted = format_array(inner_arr); + format!("[{}]", formatted) + } + + DataValue::Bool(b) => b.to_string(), + + DataValue::BoolTrue => "true".to_string(), + DataValue::BoolFalse => "false".to_string(), + + DataValue::Number(num) => num.to_string(), + + _ => String::new(), + }) + .collect(); + + parts.join(", ") +} + +#[macro_export] +macro_rules! log_cv { + ($kind:expr, $cv:expr) => { + $crate::util::logger::log_cv_internal("", &$cv, Some($kind)) + }; + ($cv:expr) => { + $crate::util::logger::log_cv_internal("", &$cv, None) + }; +} + +#[macro_export] +macro_rules! log_cv_in { + ($kind:expr, $cv:expr) => { + $crate::util::logger::log_cv_internal("> ", &$cv, Some($kind)) + }; + ($cv:expr) => { + $crate::util::logger::log_cv_internal("> ", &$cv, None) + }; +} + +#[macro_export] +macro_rules! log_cv_out { + ($kind:expr, $cv:expr) => { + $crate::util::logger::log_cv_internal("< ", &$cv, Some($kind)) + }; + ($cv:expr) => { + $crate::util::logger::log_cv_internal("< ", &$cv, None) + }; +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..47901f9 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,9 @@ +pub mod chat_files; +pub mod chats_util; +pub mod communities_util; +pub mod config_util; +pub mod crypto_helper; +pub mod crypto_util; +pub mod db; +pub mod file_util; +pub mod logger; diff --git a/static/web/404.html b/static/web/404.html new file mode 100644 index 0000000..cb6efe1 --- /dev/null +++ b/static/web/404.html @@ -0,0 +1,4 @@ + + +

404

+