Migrated left overs from github

This commit is contained in:
Alois 2026-04-02 22:20:06 +02:00
commit 5dc5ffdad8
12 changed files with 1487 additions and 566 deletions

128
Cargo.lock generated
View file

@ -1471,9 +1471,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.8.1" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@ -1486,7 +1486,6 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"smallvec", "smallvec",
"tokio", "tokio",
"want", "want",
@ -1559,12 +1558,13 @@ dependencies = [
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"potential_utf", "potential_utf",
"utf8_iter",
"yoke", "yoke",
"zerofrom", "zerofrom",
"zerovec", "zerovec",
@ -1572,9 +1572,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_locale_core" name = "icu_locale_core"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"litemap", "litemap",
@ -1585,9 +1585,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [ dependencies = [
"icu_collections", "icu_collections",
"icu_normalizer_data", "icu_normalizer_data",
@ -1599,15 +1599,15 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "2.1.2" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [ dependencies = [
"icu_collections", "icu_collections",
"icu_locale_core", "icu_locale_core",
@ -1619,15 +1619,15 @@ dependencies = [
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "2.1.2" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locale_core", "icu_locale_core",
@ -1784,9 +1784,9 @@ dependencies = [
[[package]] [[package]]
name = "iri-string" name = "iri-string"
version = "0.7.11" version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@ -1882,9 +1882,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.92" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@ -1941,9 +1941,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
@ -1972,9 +1972,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]] [[package]]
name = "litrs" name = "litrs"
@ -2509,12 +2509,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@ -2632,9 +2626,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@ -3695,9 +3689,9 @@ dependencies = [
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"zerovec", "zerovec",
@ -3879,7 +3873,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "ttp-core" name = "ttp-core"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743" source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167"
dependencies = [ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
@ -3891,7 +3885,7 @@ dependencies = [
[[package]] [[package]]
name = "ttp-native" name = "ttp-native"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Tensamin/TTP.git#23c9e68da6622a0cb3a773881f353ae4489c9743" source = "git+https://github.com/Tensamin/TTP.git#e246d1af6a42c71514d0ccc06fef94d71e4ad167"
dependencies = [ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
@ -4123,9 +4117,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -4136,9 +4130,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.65" version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -4146,9 +4140,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -4156,9 +4150,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@ -4169,9 +4163,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -4212,9 +4206,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.92" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@ -4872,9 +4866,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [ dependencies = [
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive",
@ -4883,9 +4877,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke-derive" name = "yoke-derive"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4895,18 +4889,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.47" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.47" version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4915,18 +4909,18 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]
[[package]] [[package]]
name = "zerofrom-derive" name = "zerofrom-derive"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4956,9 +4950,9 @@ dependencies = [
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"yoke", "yoke",
@ -4967,9 +4961,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.11.5" version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
@ -4978,9 +4972,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.11.2" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,2 +1 @@
# Iota # ⚠️ Moved to [git.methanium.net](https://git.methanium.net/tensamin/iota) ⚠️
Iota is the selfhost server for establishing a connection between clients. It uses its counterpart Omikron to allow a connection between clients.

View file

@ -17,20 +17,30 @@ use crate::{
ui::FPS, ui::FPS,
util::borders::draw_block_joins, util::borders::draw_block_joins,
}, },
log, log_cv, log, log_command, log_cv,
omikron::omikron_connection::OMIKRON_CONNECTION, omikron::omikron_connection::OMIKRON_CONNECTION,
users::{user_manager, user_profile::UserProfile}, users::{user_manager, user_profile::UserProfile},
util::file_util, 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 { pub struct ConsoleCard {
focused: bool, focused: bool,
pub title: String, pub title: String,
pub content: String, pub content: String,
pub cursor_position: usize,
borders: Borders, borders: Borders,
joins: Borders, joins: Borders,
cursor: Arc<Mutex<bool>>,
last_swap: Arc<Mutex<Instant>>,
tab_index: usize,
} }
impl ConsoleCard { impl ConsoleCard {
@ -39,10 +49,164 @@ impl ConsoleCard {
focused: false, focused: false,
title: title.to_string(), title: title.to_string(),
content: content.to_string(), content: content.to_string(),
cursor_position: content.chars().count(),
borders: Borders::ALL, borders: Borders::ALL,
joins: Borders::NONE, 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<Span<'static>> {
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 (<help> for info)",
Style::default().fg(Color::DarkGray),
));
} else {
spans.push(Span::styled(
" send command (<help> 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<Span<'static>> {
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 { impl Element for ConsoleCard {
@ -70,7 +234,8 @@ impl Element for ConsoleCard {
Style::default() 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) .block(block)
.scroll((0, 0)); .scroll((0, 0));
f.render_widget(par, r); f.render_widget(par, r);
@ -87,11 +252,11 @@ impl JoinableElement for ConsoleCard {
self self
} }
fn as_element(&self) -> &dyn Element { fn as_element(&self) -> &(dyn Element + 'static) {
self self
} }
fn as_element_mut(&mut self) -> &mut dyn Element { fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self self
} }
@ -113,11 +278,11 @@ impl InteractableElement for ConsoleCard {
self self
} }
fn as_element(&self) -> &dyn Element { fn as_element(&self) -> &(dyn Element + 'static) {
self self
} }
fn as_element_mut(&mut self) -> &mut dyn Element { fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self self
} }
@ -128,26 +293,71 @@ impl InteractableElement for ConsoleCard {
log!(""); log!("");
return InteractionResult::Handled; return InteractionResult::Handled;
} }
let command = self.content.clone(); let command = self.content.clone();
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let id = id.to_string(); let id = id.to_string();
let id = id.split_at(8).0; let id = id.split_at(8).0;
let task_id = format!("command_{}_{}", command, id); let task_id = format!("command_{}_{}", command, id);
ACTIVE_TASKS.insert(task_id.clone()); ACTIVE_TASKS.insert(task_id.clone());
log_command!("{}", command);
tokio::spawn(async move { tokio::spawn(async move {
run_command(&command).await; run_command(&command).await;
ACTIVE_TASKS.remove(&task_id); ACTIVE_TASKS.remove(&task_id);
}); });
self.content = "".to_string();
self.content.clear();
self.cursor_position = 0;
self.tab_index = 0;
InteractionResult::Handled InteractionResult::Handled
} }
KeyCode::Backspace => { 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 InteractionResult::Handled
} }
_ => { _ => {
if let Some(c) = key.code.as_char() { if let Some(c) = key.code.as_char() {
self.content.push(c); self.insert_at_cursor(c);
InteractionResult::Handled InteractionResult::Handled
} else { } else {
InteractionResult::Unhandled InteractionResult::Unhandled
@ -168,9 +378,8 @@ impl InteractableElement for ConsoleCard {
self.focused = f; self.focused = f;
} }
} }
pub async fn run_command(command: &str) {
log!(":{}", command);
pub async fn run_command(command: &str) {
let parts = command.split(" ").collect::<Vec<&str>>(); let parts = command.split(" ").collect::<Vec<&str>>();
match parts.as_slice() { match parts.as_slice() {
@ -267,8 +476,6 @@ pub async fn run_command(command: &str) {
} }
pub async fn ping(time: u64) { pub async fn ping(time: u64) {
let time = time;
let conn = OMIKRON_CONNECTION.clone(); let conn = OMIKRON_CONNECTION.clone();
let response_cv = conn let response_cv = conn

View file

@ -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::interaction_result::InteractionResult;
use crate::gui::util::borders::draw_block_joins; 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 crossterm::event::{KeyCode, KeyEvent};
use ratatui::{ use ratatui::{
Frame, Frame,
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap}, widgets::{Block, Borders, Paragraph},
}; };
use std::any::Any; use std::any::Any;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct UiLogEntry { pub struct UiLogEntry {
pub line: String, pub timestamp_ms: u128,
pub color: Color, 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<LogEntry> 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 { pub struct LogCard {
focused: bool, focused: bool,
scroll: u16, selected: bool,
scroll_offset: usize,
last_total_lines: usize,
last_visible_height: usize,
pub borders: Borders, pub borders: Borders,
pub joins: Borders, pub joins: Borders,
} }
@ -30,12 +79,213 @@ impl LogCard {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
focused: false, focused: false,
scroll: 1, selected: false,
scroll_offset: 0,
last_total_lines: 0,
last_visible_height: 10,
borders: Borders::ALL, borders: Borders::ALL,
joins: Borders::NONE, joins: Borders::NONE,
} }
} }
fn get_logs(&self) -> Vec<UiLogEntry> {
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(&timestamp);
}
result.push((line, entry.sender.prefix_color(), entry.is_error));
}
result
}
fn build_all_lines(
&self,
entries: Vec<UiLogEntry>,
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 { impl Element for LogCard {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
@ -46,22 +296,10 @@ impl Element for LogCard {
} }
fn render(&self, f: &mut Frame, area: Rect) { fn render(&self, f: &mut Frame, area: Rect) {
let state = APP_STATE.lock().unwrap(); let entries = self.get_logs();
let logs = state.get_logs();
let lines: Vec<Line> = logs
.iter()
.map(|log| {
Line::from(Span::raw(log.line.clone())).style(Style::default().fg(log.color))
})
.collect();
let block = Block::default() let block = Block::default()
.title(if self.focused { .title(self.build_title())
"Logs J/K to scroll"
} else {
"Logs"
})
.borders(self.borders) .borders(self.borders)
.border_style(if self.focused { .border_style(if self.focused {
Style::default().fg(Color::Yellow) Style::default().fg(Color::Yellow)
@ -69,22 +307,70 @@ impl Element for LogCard {
Style::default() 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(); if inner_area.width == 0 || inner_area.height == 0 {
let base_scroll = total_lines.saturating_sub(inner_height) as u16; 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) let (start, end) = self.calculate_view_window(total_lines, visible_height);
.block(block) let visible_lines = &all_lines[start..end];
.wrap(Wrap { trim: false })
.scroll((scroll, 0)); let rendered_lines: Vec<Line> = 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); draw_block_joins(f, area, self.borders, self.joins);
} }
} }
impl JoinableElement for LogCard { impl JoinableElement for LogCard {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
@ -94,12 +380,12 @@ impl JoinableElement for LogCard {
self self
} }
fn as_element(&self) -> &dyn Element { fn as_element(&self) -> &(dyn Element + 'static) {
self &*self
} }
fn as_element_mut(&mut self) -> &mut dyn Element { fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self &mut *self
} }
fn set_borders(&mut self, borders: Borders) { fn set_borders(&mut self, borders: Borders) {
@ -110,6 +396,7 @@ impl JoinableElement for LogCard {
self.joins = joins; self.joins = joins;
} }
} }
impl InteractableElement for LogCard { impl InteractableElement for LogCard {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
@ -119,30 +406,65 @@ impl InteractableElement for LogCard {
self self
} }
fn as_element(&self) -> &dyn Element { fn as_element(&self) -> &(dyn Element + 'static) {
self &*self
} }
fn as_element_mut(&mut self) -> &mut dyn Element { fn as_element_mut(&mut self) -> &mut (dyn Element + 'static) {
self &mut *self
} }
fn interact(&mut self, key: KeyEvent) -> InteractionResult { 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 { match key.code {
KeyCode::Char('J') | KeyCode::Char('j') => { KeyCode::Enter | KeyCode::Char(' ') => {
let state = APP_STATE.lock().unwrap(); self.selected = !self.selected;
let logs = state.get_logs(); InteractionResult::Handled
if self.scroll < logs.len() as u16 { }
self.scroll += 1; KeyCode::Char('j') | KeyCode::Char('J') => {
let (can_up, _) = self.get_title_hints();
if can_up {
self.scroll_up();
} }
InteractionResult::Handled InteractionResult::Handled
} }
KeyCode::Char('K') | KeyCode::Char('k') => { KeyCode::Char('k') | KeyCode::Char('K') => {
if self.scroll > 1 { let (_, can_down) = self.get_title_hints();
self.scroll -= 1; if can_down {
self.scroll_down();
} }
InteractionResult::Handled 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, _ => InteractionResult::Unhandled,
} }
} }

View file

@ -1,59 +1,51 @@
use crate::ACTIVE_TASKS;
use crate::gui::ui::{UI, UNIQUE}; use crate::gui::ui::{UI, UNIQUE};
use crate::{RELOAD, SHUTDOWN}; 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::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
pub fn setup_input_handler(ui: Arc<UI>) { pub fn setup_input_handler(ui: Arc<UI>) {
tokio::spawn(async move { tokio::spawn(async move {
ACTIVE_TASKS.insert("Input Handler".to_string());
loop { loop {
{ if *SHUTDOWN.read().await {
let should_shutdown = *SHUTDOWN.read().await; break;
if should_shutdown {
break;
}
} }
let has_event = match poll(Duration::from_millis(100)) { let event_result = tokio::task::spawn_blocking(|| {
Ok(true) => true, if let Ok(true) = poll(Duration::from_millis(100)) {
Ok(false) => false, read().ok().and_then(|ev| match ev {
Err(_) => false, Event::Key(key) if key.kind == KeyEventKind::Press => Some(key),
}; _ => None,
})
} else {
None
}
})
.await;
if has_event { match event_result {
match read() { Ok(Some(key_event)) => {
Ok(event) => { handle_input(key_event, ui.clone()).await;
if let Event::Key(key_event) = event { UNIQUE.store(true, Ordering::Relaxed);
if key_event.kind == KeyEventKind::Press { }
let uic = ui.clone(); Ok(_) => {}
handle_input(key_event, uic).await; Err(e) => {
UNIQUE.store(true, Ordering::Relaxed); eprintln!("Input task error: {}", e);
} tokio::time::sleep(Duration::from_millis(10)).await;
}
}
Err(_) => (),
} }
} }
} }
{
ACTIVE_TASKS.remove("Input Handler");
}
}); });
} }
pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) { pub async fn handle_input(key: KeyEvent, ui: Arc<UI>) {
match (key.code, key.modifiers) { 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; *SHUTDOWN.write().await = true;
} }
(KeyCode::Char('c'), KeyModifiers::CONTROL) => { (crossterm::event::KeyCode::Char('r'), KeyModifiers::CONTROL) => {
*SHUTDOWN.write().await = true;
}
(KeyCode::Char('r'), KeyModifiers::CONTROL) => {
*RELOAD.write().await = true; *RELOAD.write().await = true;
*SHUTDOWN.write().await = true; *SHUTDOWN.write().await = true;
} }

View file

@ -1,7 +1,7 @@
use crate::users::contact::Contact; use crate::users::contact::Contact;
use crate::users::user_community_util::UserCommunityUtil;
use crate::util::chat_files::{MessageState, change_message_state}; use crate::util::chat_files::{MessageState, change_message_state};
use crate::util::chats_util::{get_user, mod_user}; 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::crypto_util::{DataFormat, SecurePayload};
use crate::util::file_util::{get_children, load_file, save_file}; use crate::util::file_util::{get_children, load_file, save_file};
use crate::util::{chat_files, chats_util}; use crate::util::{chat_files, chats_util};
@ -283,11 +283,16 @@ impl OmikronConnection {
let key_pair = crypto_helper::generate_keypair(); let key_pair = crypto_helper::generate_keypair();
let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); 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; let mut conf_write = CONFIG.write().await;
/*conf_write.change("public_key", DataValue::Str(public_key_base64.clone())); // NOTE:
conf_write.change("private_key", DataValue::Str(private_key_base64));*/ // 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(); conf_write.update();
drop(conf_write); drop(conf_write);
@ -418,7 +423,7 @@ impl OmikronConnection {
} }
if cv.is_type(CommunicationType::identification_response) { 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; let mut state = self.state.write().await;
if let ConnectionState::Connected { identified: _ } = *state { if let ConnectionState::Connected { identified: _ } = *state {
*state = ConnectionState::Connected { identified: true }; *state = ConnectionState::Connected { identified: true };
@ -430,6 +435,7 @@ impl OmikronConnection {
// ************************************************ // // ************************************************ //
// Direct messages // // Direct messages //
// ************************************************ // // ************************************************ //
if cv.is_type(CommunicationType::message_state) { if cv.is_type(CommunicationType::message_state) {
let sender_id = &cv.get_sender(); let sender_id = &cv.get_sender();
let receiver_id = &cv.get_receiver(); 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::<i64>().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::<i64>().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::<i64>().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) { if cv.is_type(CommunicationType::message_other_iota) {
let sender_id = &cv.get_sender(); let sender_id = &cv.get_sender();
let receiver_id = &cv.get_receiver(); let receiver_id = &cv.get_receiver();
@ -483,26 +643,25 @@ impl OmikronConnection {
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let height = cv.get_data(DataTypes::height).as_number().unwrap_or(0) as i64;
chat_files::add_message( chat_files::add_message(
timestamp as u128, timestamp as u128,
false, false,
*receiver_id as i64, *receiver_id as i64,
*sender_id as i64, *sender_id as i64,
&content, &content,
height,
); );
// Build user_forward using the parsed numeric timestamp and safe content string
let user_forward = CommunicationValue::new(CommunicationType::message_live) let user_forward = CommunicationValue::new(CommunicationType::message_live)
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(*receiver_id) .with_receiver(*receiver_id)
.add_data( .add_data(DataTypes::send_time, DataValue::Number(timestamp))
DataTypes::send_time, .add_data(DataTypes::content, DataValue::Str(content.clone()))
cv.get_data(DataTypes::send_time).clone(), .add_data(DataTypes::sender_id, DataValue::Number(*sender_id as i64))
) .add_data(DataTypes::height, DataValue::Number(height));
.add_data(DataTypes::message, cv.get_data(DataTypes::content).clone())
.add_data(
DataTypes::sender_id,
DataValue::Number(cv.get_sender() as i64),
);
let user_resp = self let user_resp = self
.clone() .clone()
@ -528,10 +687,7 @@ impl OmikronConnection {
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(*sender_id) .with_receiver(*sender_id)
.with_sender(*receiver_id) .with_sender(*receiver_id)
.add_data( .add_data(DataTypes::send_time, DataValue::Number(timestamp))
DataTypes::send_time,
cv.get_data(DataTypes::send_time).clone(),
)
.add_data( .add_data(
DataTypes::message_state, DataTypes::message_state,
DataValue::Str(ms.as_str().to_string()), DataValue::Str(ms.as_str().to_string()),
@ -539,15 +695,20 @@ impl OmikronConnection {
) )
.await; .await;
} else { } 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( self.send_message(
&CommunicationValue::new(CommunicationType::message_state) &CommunicationValue::new(CommunicationType::message_state)
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(*sender_id) .with_receiver(*sender_id)
.with_sender(*receiver_id) .with_sender(*receiver_id)
.add_data( .add_data(DataTypes::send_time, DataValue::Number(timestamp))
DataTypes::send_time,
cv.get_data(DataTypes::send_time).clone(),
)
.add_data( .add_data(
DataTypes::message_state, DataTypes::message_state,
DataValue::Str(MessageState::Sent.as_str().to_string()), DataValue::Str(MessageState::Sent.as_str().to_string()),
@ -558,90 +719,21 @@ impl OmikronConnection {
return; 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::<i64>().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) { if cv.is_type(CommunicationType::messages_get) {
let my_id = cv.get_sender(); let my_id = cv.get_sender();
let partner_id = cv.get_data(DataTypes::user_id).as_number().unwrap_or(0); 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 offset = cv.get_data(DataTypes::offset).as_number().unwrap_or(0);
let amount = cv.get_data(DataTypes::amount).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); 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<DataValue> = Vec::new(); let mut msg_array: Vec<DataValue> = Vec::new();
for m in messages.members() { for m in messages.members() {
// extract fields defensively
let message_time: i64 = m["message_time"].as_i64().unwrap_or(0); let message_time: i64 = m["message_time"].as_i64().unwrap_or(0);
let content: String = m["content"].as_str().unwrap_or("").to_string(); 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 sent_by_self: bool = m["sent_by_self"].as_bool().unwrap_or(false);
// determine sender id: let height: i64 = m["height"].as_i64().unwrap_or(0);
// - 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 sender_id: i64 = if sent_by_self { let sender_id: i64 = if sent_by_self {
my_id as i64 my_id as i64
} else { } 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() { if let Some(n) = cv.get_data(DataTypes::chat_partner_id).as_number() {
n as i64 n as i64
} else if let Some(s) = cv.get_data(DataTypes::chat_partner_id).as_str() { } 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(); let mut container = Vec::new();
container.push((DataTypes::send_time, DataValue::Number(message_time))); 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::sender_id, DataValue::Number(sender_id)));
container.push((DataTypes::message_state, DataValue::Str(message_state))); 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)); msg_array.push(DataValue::Container(container));
} }
@ -724,7 +818,7 @@ impl OmikronConnection {
} }
if cv.is_type(CommunicationType::add_community) { if cv.is_type(CommunicationType::add_community) {
UserCommunityUtil::add_community( CommunitiesUtil::add_community(
cv.get_sender() as i64, cv.get_sender() as i64,
cv.get_data(DataTypes::community_address) cv.get_data(DataTypes::community_address)
.as_str() .as_str()
@ -747,19 +841,37 @@ impl OmikronConnection {
} }
if cv.is_type(CommunicationType::get_communities) { 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) let resp = CommunicationValue::new(CommunicationType::get_communities)
.with_id(cv.get_id()) .with_id(cv.get_id())
.with_receiver(cv.get_sender()) .with_receiver(cv.get_sender())
/*.add_data( .add_data(DataTypes::communities, DataValue::Array(comm_array));
DataTypes::communities,
DataValue::Array(UserCommunityUtil::get_communities(cv.get_sender() as i64)),
) */;
self.send_message(&resp).await; self.send_message(&resp).await;
return; return;
} }
if cv.is_type(CommunicationType::remove_community) { if cv.is_type(CommunicationType::remove_community) {
UserCommunityUtil::remove_community( CommunitiesUtil::remove_community(
cv.get_sender() as i64, cv.get_sender() as i64,
cv.get_data(DataTypes::community_address) cv.get_data(DataTypes::community_address)
.as_str() .as_str()

View file

@ -1,9 +1,9 @@
use crate::log; use crate::log;
use crate::util::file_util::get_directory; use crate::util::db;
use json::{JsonValue, array, object}; use json::{JsonValue, array, object};
use rusqlite::{Connection, params}; use rusqlite::params;
use std::io; use std::io;
use std::sync::{LazyLock, Mutex}; use std::sync::{Arc, LazyLock, Mutex};
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone)]
pub enum MessageState { pub enum MessageState {
@ -45,30 +45,10 @@ impl MessageState {
} }
} }
static DB_CONN: LazyLock<Mutex<Connection>> = LazyLock::new(|| { // Shared DB created via helper.
let conn = Connection::open(format!("{}/messages.sqlite3", get_directory())) // The db helper constructs the messages sqlite file and ensures PRAGMAs and schema exist.
.expect("Failed to open messages sqlite DB"); static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
conn.execute_batch( db::create_general_messages_db().expect("Failed to create or initialize general messages DB")
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)
}); });
pub fn add_message( pub fn add_message(
@ -77,6 +57,7 @@ pub fn add_message(
storage_owner: i64, storage_owner: i64,
external_user: i64, external_user: i64,
message: &str, message: &str,
height: i64,
) { ) {
let message_time = match i64::try_from(send_time) { let message_time = match i64::try_from(send_time) {
Ok(v) => v, Ok(v) => v,
@ -86,40 +67,48 @@ pub fn add_message(
} }
}; };
let conn = match DB_CONN.lock() { // Insert the message into the DB
Ok(g) => g, let insert_result = db::with_conn(&MESSAGES_DB, |conn| {
Err(e) => { conn.execute(
log!("Failed to lock messages DB mutex for add_message: {:?}", e); r#"
return; 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( if let Err(e) = insert_result {
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(),
],
) {
log!("Failed to insert message into sqlite: {}", e); 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( pub fn change_message_state(
@ -128,56 +117,58 @@ pub fn change_message_state(
external_user: i64, external_user: i64,
new_state: MessageState, new_state: MessageState,
) -> io::Result<()> { ) -> io::Result<()> {
let conn = DB_CONN // Run the SELECT and UPDATE inside with_conn to centralize connection access.
.lock() let res: Result<(), String> = db::with_conn(&MESSAGES_DB, |conn| {
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Mutex lock error: {:?}", e)))?; let current: Option<String> = match conn.query_row(
r#"
let current: Option<String> = match conn.query_row( SELECT message_state
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(&current_state_raw)
.upgrade(new_state)
.as_str()
.to_string();
conn.execute(
r#"
UPDATE messages
SET message_state = ?1
WHERE id = (
SELECT id
FROM messages FROM messages
WHERE storage_owner = ?2 WHERE storage_owner = ?1
AND external_user = ?3 AND external_user = ?2
AND message_time = ?4 AND message_time = ?3
ORDER BY id DESC ORDER BY id DESC
LIMIT 1 LIMIT 1
) "#,
"#, params![storage_owner, external_user, timestamp],
params![upgraded, storage_owner, external_user, timestamp], |row| row.get(0),
) ) {
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; 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(&current_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( pub fn get_messages(
@ -186,80 +177,73 @@ pub fn get_messages(
loaded_messages: i64, loaded_messages: i64,
amount: i64, amount: i64,
) -> JsonValue { ) -> JsonValue {
let mut messages = array![]; let messages = array![];
if amount <= 0 || loaded_messages < 0 { if amount <= 0 || loaded_messages < 0 {
return messages; return messages;
} }
let conn = match DB_CONN.lock() { let res: Result<JsonValue, String> = db::with_conn(&MESSAGES_DB, |conn| {
Ok(g) => g, let mut stmt = conn.prepare(
Err(e) => { r#"
log!("Failed to lock messages DB mutex for get_messages: {:?}", e); SELECT
return messages; 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( let rows = stmt.query_map(
r#" params![storage_owner, external_user, amount, loaded_messages],
SELECT |row| {
message_time, let message_time: i64 = row.get(0)?;
content, let content: String = row.get(1)?;
sent_by_self, let sent_by_self: i64 = row.get(2)?;
message_state let message_state: String = row.get(3)?;
FROM messages let height: i64 = row.get(4).unwrap_or(0);
WHERE storage_owner = ?1 Ok((message_time, content, sent_by_self, message_state, height))
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( let mut out = array![];
params![storage_owner, external_user, amount, loaded_messages], for row in rows {
|row| { match row {
let message_time: i64 = row.get(0)?; Ok((message_time, content, sent_by_self, message_state, height)) => {
let content: String = row.get(1)?; let msg = object! {
let sent_by_self: i64 = row.get(2)?; "message_time" => message_time,
let message_state: String = row.get(3)?; "content" => content,
Ok((message_time, content, sent_by_self, message_state)) "sent_by_self" => (sent_by_self != 0),
}, "message_state" => message_state,
); "height" => height
};
let Ok(rows) = rows else { if let Err(e) = out.push(msg) {
if let Err(e) = rows { // out.push returns a JsonError; log it instead of using `?` to avoid
log!("Failed to query messages: {}", e); // incompatible error conversions inside the DB closure.
} log!("Failed to append message to output array: {:?}", e);
return messages; }
}; }
Err(e) => {
for row in rows { log!("Failed to read row from sqlite: {}", e);
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);
} }
} }
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)] #[cfg(test)]

View file

@ -1,69 +1,96 @@
use crate::users::contact::Contact; use crate::users::contact::Contact;
use crate::util::file_util::get_directory; use crate::util::db;
use rusqlite::{Connection, params}; use rusqlite::params;
use std::sync::{LazyLock, Mutex}; use std::sync::{Arc, LazyLock, Mutex};
static DB_CONN: LazyLock<Mutex<Connection>> = LazyLock::new(|| { /// Shared DB connection for contacts/messages (created by db helper).
let conn = Connection::open(format!("{}/messages.sqlite3", get_directory())) static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = LazyLock::new(|| {
.expect("Failed to open DB"); db::create_general_messages_db().expect("Failed to create or initialize general messages 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)
}); });
/// Insert or update a contact for the given storage owner.
pub fn mod_user(storage_owner: i64, contact: &Contact) { pub fn mod_user(storage_owner: i64, contact: &Contact) {
let conn = DB_CONN.lock().unwrap(); if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| {
conn.execute(
let _ = conn.execute( r#"
r#" INSERT INTO contacts (
INSERT INTO contacts ( storage_owner,
storage_owner, user_id,
user_id, user_name,
user_name, last_message_at
last_message_at ) VALUES (?1, ?2, ?3, ?4)
) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(storage_owner, user_id) DO UPDATE SET
ON CONFLICT(storage_owner, user_id) DO UPDATE SET user_name = excluded.user_name,
user_name = excluded.user_name, last_message_at = excluded.last_message_at
last_message_at = excluded.last_message_at "#,
"#, params![
params![ storage_owner,
storage_owner, contact.user_id,
contact.user_id, contact.user_name.clone(),
contact.user_name.clone(), contact.last_message_at
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<Contact> { pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
let conn = DB_CONN.lock().unwrap(); let res: Result<Option<Contact>, 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<String> = r.get(1)?;
let last_message_at: Option<i64> = 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( match res {
r#" Ok(opt) => opt,
SELECT user_id, user_name, last_message_at Err(e) => {
FROM contacts eprintln!("Error querying user in get_user: {}", e);
WHERE storage_owner = ?1 AND user_id = ?2 None
LIMIT 1 }
"#, }
params![storage_owner, user_id], }
|r| {
/// Retrieve all contacts for a storage owner, ordered by last_message_at desc / user_id asc.
pub fn get_users(storage_owner: i64) -> Vec<Contact> {
let contacts_out = Vec::new();
let res: Result<Vec<Contact>, 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_id: i64 = r.get(0)?;
let user_name: Option<String> = r.get(1)?; let user_name: Option<String> = r.get(1)?;
let last_message_at: Option<i64> = r.get(2)?; let last_message_at: Option<i64> = r.get(2)?;
@ -72,64 +99,23 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option<Contact> {
user_name, user_name,
last_message_at, last_message_at,
}) })
}, })?;
);
match row { let mut out = Vec::new();
Ok(contact) => Some(contact), for row in rows {
Err(rusqlite::Error::QueryReturnedNoRows) => None, 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) => { Err(e) => {
eprintln!("Error querying user in get_user: {}", e); eprintln!("Failed to query contacts in get_users: {}", e);
None contacts_out
} }
} }
} }
pub fn get_users(storage_owner: i64) -> Vec<Contact> {
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<String> = r.get(1)?;
let last_message_at: Option<i64> = 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
}

View file

@ -0,0 +1,90 @@
use crate::util::db;
use json::Array;
use rusqlite::params;
use std::sync::{Arc, LazyLock, Mutex};
static MESSAGES_DB: LazyLock<Arc<Mutex<rusqlite::Connection>>> = 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<Array, String> = 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
}
}
}
}

185
src/util/db.rs Normal file
View file

@ -0,0 +1,185 @@
//! Database helper utilities.
//!
//! This module provides small helpers to open/init sqlite databases and to
//! create a shared (Arc<Mutex<Connection>>) 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<Connection, RusqliteError> {
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<Connection, RusqliteError> {
let conn = open_connection(db_name)?;
conn.execute_batch(init_sql)?;
Ok(conn)
}
/// Create a shared, Arc<Mutex<Connection>> initialized with the given SQL.
///
/// This is a convenience wrapper that returns an owned Arc<Mutex<Connection>>
/// 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<Arc<Mutex<Connection>>, 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<Mutex<Connection>> and run the provided
/// closure. Converts rusqlite::Error into a String on error.
///
/// Arguments:
/// - `shared`: Arc<Mutex<Connection>>
/// - `f`: closure that receives &Connection and returns Result<T, RusqliteError>
///
/// Returns Ok(T) or Err(String).
pub fn with_conn<T, F>(shared: &Arc<Mutex<Connection>>, f: F) -> Result<T, String>
where
F: FnOnce(&Connection) -> Result<T, RusqliteError>,
{
// 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<T, String> 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<Arc<Mutex<Connection>>, 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<Arc<Mutex<Connection>>> = LazyLock::new(|| {
create_general_messages_db().expect("failed to create messages DB")
});
// Later, to run a query:
let res: Result<Vec<MyRow>, String> = with_conn(&MESSAGES_DB, |conn| {
let mut stmt = conn.prepare("SELECT ...")?;
let rows = stmt.query_map(...)?;
// collect and return Ok(...)
});
*/

View file

@ -13,13 +13,13 @@ use ttp_core::{CommunicationValue, DataTypes, DataValue};
use crate::{ use crate::{
APP_STATE, APP_STATE,
gui::{elements::log_card::UiLogEntry, ui::UNIQUE}, gui::{elements::log_card::LogEntry, ui::UNIQUE},
langu::language_manager, langu::language_manager,
}; };
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new(); static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
#[derive(Clone, Copy)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[allow(unused)] #[allow(unused)]
pub enum PrintType { pub enum PrintType {
Call, Call,
@ -28,6 +28,20 @@ pub enum PrintType {
Omikron, Omikron,
Omega, Omega,
General, 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 { struct LogMessage {
@ -35,10 +49,8 @@ struct LogMessage {
prefix: String, prefix: String,
kind: PrintType, kind: PrintType,
is_error: bool, is_error: bool,
translation_key: Option<String>, translation_key: Option<String>,
format_args: Vec<String>, format_args: Vec<String>,
message: Option<String>, message: Option<String>,
} }
@ -70,27 +82,49 @@ pub fn startup() {
msg.message.unwrap_or_default() 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 prefix = if msg.prefix.is_empty() {
String::new()
let _ = writeln!(file, "{} {}", ts, line); } else {
format!("{} ", msg.prefix)
let color = colorize(msg.kind, msg.is_error);
let ui_entry = UiLogEntry {
line: line.clone(),
color,
}; };
{ let _ = writeln!(
let mut state = APP_STATE.lock().unwrap(); file,
state.push_log(ui_entry); "{} {}{}",
} 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 { fn colorize(kind: PrintType, is_error: bool) -> Color {
if is_error { if is_error {
return Color::Red; return Color::Red;
@ -102,17 +136,8 @@ fn colorize(kind: PrintType, is_error: bool) -> Color {
PrintType::Iota => Color::Yellow, PrintType::Iota => Color::Yellow,
PrintType::Omikron => Color::Blue, PrintType::Omikron => Color::Blue,
PrintType::Omega => Color::Cyan, PrintType::Omega => Color::Cyan,
PrintType::General => Color::White, PrintType::General => Color::LightCyan,
} PrintType::Command => Color::LightGreen,
}
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
} }
} }
@ -180,6 +205,7 @@ macro_rules! log_t {
) )
}; };
} }
#[macro_export] #[macro_export]
macro_rules! log_t_err { macro_rules! log_t_err {
($key:expr) => { ($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. /// Log a general informational message.
#[macro_export] #[macro_export]
macro_rules! log { macro_rules! log {
($($arg:tt)*) => { ($($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 (`>`). /// Log an inbound message (`>`).
#[macro_export] #[macro_export]
macro_rules! log_in { macro_rules! log_in {
@ -222,6 +267,7 @@ macro_rules! log_in {
) )
}; };
} }
/// Log an outbound message (`<`). /// Log an outbound message (`<`).
#[macro_export] #[macro_export]
macro_rules! log_out { macro_rules! log_out {
@ -234,6 +280,7 @@ macro_rules! log_out {
) )
}; };
} }
/// Log an error message (`>>`). /// Log an error message (`>>`).
#[macro_export] #[macro_export]
macro_rules! log_err { macro_rules! log_err {
@ -373,6 +420,7 @@ macro_rules! log_cv_in {
$crate::util::logger::log_cv_internal("> ", &$cv, None) $crate::util::logger::log_cv_internal("> ", &$cv, None)
}; };
} }
#[macro_export] #[macro_export]
macro_rules! log_cv_out { macro_rules! log_cv_out {
($kind:expr, $cv:expr) => { ($kind:expr, $cv:expr) => {

View file

@ -1,7 +1,9 @@
pub mod chat_files; pub mod chat_files;
pub mod chats_util; pub mod chats_util;
pub mod communities_util;
pub mod config_util; pub mod config_util;
pub mod crypto_helper; pub mod crypto_helper;
pub mod crypto_util; pub mod crypto_util;
pub mod db;
pub mod file_util; pub mod file_util;
pub mod logger; pub mod logger;