diff --git a/Cargo.lock b/Cargo.lock index cf4ad0b..04277be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b" dependencies = [ "actix-rt", - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "crossbeam-channel", "futures-core", @@ -31,7 +31,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-core", "futures-sink", @@ -54,7 +54,7 @@ dependencies = [ "actix-tls", "actix-utils", "base64", - "bitflags 2.13.0", + "bitflags 2.13.1", "brotli", "bytes", "bytestring", @@ -89,7 +89,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -211,7 +211,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.4", + "socket2 0.6.5", "time", "tracing", "url", @@ -244,7 +244,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "approx" @@ -372,7 +372,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -384,7 +384,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -396,18 +396,18 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.1", ] [[package]] @@ -433,9 +433,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -499,9 +499,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "block-buffer" @@ -556,9 +556,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "byteorder" @@ -568,9 +568,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytestring" @@ -601,9 +601,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -619,9 +619,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -701,6 +701,7 @@ dependencies = [ "hyper", "hyper-util", "iota-auth", + "iota-connection", "iota-logger", "iota-state", "iota-storage", @@ -711,7 +712,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -905,7 +906,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crossterm_winapi", "derive_more", "document-features", @@ -1000,7 +1001,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1023,7 +1024,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1034,7 +1035,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1082,9 +1083,9 @@ dependencies = [ [[package]] name = "der" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid 0.10.2", "zeroize", @@ -1118,7 +1119,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1140,7 +1141,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", "unicode-xid", ] @@ -1175,7 +1176,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1297,10 +1298,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] -name = "fastrand" -version = "2.4.1" +name = "fastbloom" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash", + "libm", + "portable-atomic", + "siphasher", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fiat-crypto" @@ -1398,9 +1411,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1413,9 +1426,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1423,15 +1436,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1440,38 +1453,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1587,6 +1600,62 @@ dependencies = [ "tracing", ] +[[package]] +name = "h3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be" +dependencies = [ + "bytes", + "fastrand", + "futures-util", + "http 1.4.2", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "h3-datagram" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2c9f77921668673721ae40f17c729fc48b9e38a663858097cea547484fdf0f" +dependencies = [ + "bytes", + "h3", + "pin-project-lite", +] + +[[package]] +name = "h3-quinn" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4" +dependencies = [ + "bytes", + "futures", + "h3", + "h3-datagram", + "quinn", + "tokio", + "tokio-util", +] + +[[package]] +name = "h3-webtransport" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d91a50fd582a5d67b1f756fba3cd9c66367ff4f23e1017c882f664d63b350a7" +dependencies = [ + "bytes", + "futures-util", + "h3", + "h3-datagram", + "http 1.4.2", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1636,7 +1705,7 @@ dependencies = [ "http 1.4.2", "httpdate", "mime", - "sha1 0.10.6", + "sha1 0.10.7", ] [[package]] @@ -1725,9 +1794,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http 1.4.2", @@ -1735,9 +1804,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1822,7 +1891,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -2013,7 +2082,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2042,7 +2111,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -2094,7 +2163,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -2116,6 +2185,15 @@ dependencies = [ "zip", ] +[[package]] +name = "iota-connection" +version = "0.1.0" +dependencies = [ + "iota-storage", + "iota-util", + "mtp", +] + [[package]] name = "iota-core" version = "0.1.0" @@ -2179,15 +2257,18 @@ dependencies = [ "json", "mtp", "once_cell", - "rand 0.8.6", + "r2d2", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", "rusqlite", "serde", "serde_json", + "serde_yaml", "sha2 0.10.9", "sysinfo", + "thiserror 2.0.19", "tokio", "uuid", "walkdir", @@ -2242,7 +2323,6 @@ dependencies = [ "base64", "hex", "mtp", - "mtp-crypto", "reqwest", "sysinfo", "tokio", @@ -2312,7 +2392,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2327,7 +2407,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2346,7 +2426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2384,7 +2464,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2458,7 +2538,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2513,9 +2593,9 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.1", ] @@ -2548,9 +2628,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmem" @@ -2601,9 +2681,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -2666,8 +2746,8 @@ dependencies = [ [[package]] name = "mtp" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "mtp-client", "mtp-codec", @@ -2675,50 +2755,52 @@ dependencies = [ "mtp-crypto", "mtp-files", "mtp-host", + "mtp-transport", "mtp-type-map", + "mtp-webserver", ] [[package]] name = "mtp-client" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.8.6", + "rand 0.10.2", "tokio", ] [[package]] name = "mtp-codec" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "base64", "byteorder", "mtp-common", "mtp-crypto", "mtp-type-map", - "rand 0.8.6", + "rand 0.10.2", ] [[package]] name = "mtp-common" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "quinn", "rustls", - "thiserror 2.0.18", + "thiserror 2.0.19", "wtransport", ] [[package]] name = "mtp-crypto" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "base64", "chacha20poly1305", @@ -2727,59 +2809,93 @@ dependencies = [ "hkdf 0.13.0", "ml-dsa", "mlkem-tls", - "rand_core 0.6.4", + "rand 0.10.2", + "rand_core 0.10.1", + "rustls", "serde", "sha2 0.11.0", "thiserror 1.0.69", + "tokio", "zeroize", ] [[package]] name = "mtp-files" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "mtp-crypto", + "rand 0.10.2", "thiserror 1.0.69", + "zeroize", ] [[package]] name = "mtp-host" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "mtp-codec", "mtp-common", "mtp-crypto", "mtp-transport", - "rand 0.8.6", + "rand 0.8.7", "tokio", + "tracing", + "wtransport", ] [[package]] name = "mtp-transport" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ - "log", + "async-trait", "mtp-codec", "mtp-common", + "mtp-crypto", "rcgen", "rustls", "rustls-native-certs", + "sha2 0.11.0", "tokio", + "tracing", "wtransport", ] [[package]] name = "mtp-type-map" -version = "0.1.0" -source = "git+https://git.methanium.net/Methanium/mtp.git#2126a142f44634ccfdcaa297633fdb63b1d9b46d" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" dependencies = [ "serde", "serde_yaml", ] +[[package]] +name = "mtp-webserver" +version = "0.2.0" +source = "git+https://git.methanium.net/Methanium/mtp.git#04760fd88d2bc3adf548a9ec532fa228227f0a49" +dependencies = [ + "async-trait", + "bytes", + "h3", + "h3-quinn", + "h3-webtransport", + "http 1.4.2", + "mtp-codec", + "mtp-common", + "mtp-crypto", + "mtp-host", + "mtp-transport", + "quinn", + "rand 0.10.2", + "rustls", + "thiserror 2.0.19", + "tokio", + "tracing", +] + [[package]] name = "native-tls" version = "0.2.18" @@ -2803,7 +2919,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -2859,7 +2975,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2895,7 +3011,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2910,9 +3026,9 @@ dependencies = [ [[package]] name = "octets" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8311fa8ab7a57759b4ff1f851a3048d9ef0effaa0130726426b742d26d8a88e7" +checksum = "866cb5af6f3aa3c1b44c3c2d79d22165fbb1b102e1b3fb499864bfe34736ec4b" [[package]] name = "oid-registry" @@ -2930,13 +3046,14 @@ dependencies = [ "base64", "dashmap", "hex", + "iota-connection", "iota-logger", "iota-state", "iota-storage", "iota-util", "json", "mtp", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "reqwest", "sha2 0.10.9", @@ -2959,9 +3076,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.6" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ "is-wsl", "libc", @@ -2973,7 +3090,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "foreign-types", "libc", @@ -2989,7 +3106,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3048,7 +3165,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -3091,7 +3208,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3182,7 +3299,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3221,7 +3338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -3234,7 +3351,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3263,7 +3380,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3288,7 +3405,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.0", + "der 0.8.1", "spki 0.8.0", ] @@ -3343,7 +3460,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3414,9 +3531,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "potential_utf" @@ -3450,9 +3567,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3465,13 +3582,14 @@ checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", + "futures-io", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.4", - "thiserror 2.0.18", + "socket2 0.6.5", + "thiserror 2.0.19", "tokio", "tracing", "web-time", @@ -3485,6 +3603,7 @@ checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", + "fastbloom", "getrandom 0.4.3", "lru-slab", "rand 0.10.2", @@ -3493,8 +3612,9 @@ dependencies = [ "rustc-hash", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "slab", - "thiserror 2.0.18", + "thiserror 2.0.19", "tinyvec", "tracing", "web-time", @@ -3509,16 +3629,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.6.5", "tracing", "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3536,24 +3656,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "rand" -version = "0.8.6" +name = "r2d2" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", + "log", + "parking_lot", + "scheduled-thread-pool", ] [[package]] name = "rand" -version = "0.9.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", + "libc", + "rand_chacha", + "rand_core 0.6.4", ] [[package]] @@ -3577,16 +3698,6 @@ dependencies = [ "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" @@ -3602,15 +3713,6 @@ 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 = "rand_core" version = "0.10.1" @@ -3648,7 +3750,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "compact_str", "critical-section", "hashbrown 0.17.1", @@ -3658,7 +3760,7 @@ dependencies = [ "palette", "serde", "strum 0.28.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -3713,7 +3815,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "hashbrown 0.17.1", "indoc", "instability", @@ -3748,14 +3850,14 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3765,9 +3867,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3845,7 +3947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ "hashbrown 0.16.1", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -3854,7 +3956,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3893,7 +3995,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -3902,9 +4004,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "log", @@ -4016,6 +4118,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -4034,7 +4145,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4070,9 +4181,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4080,22 +4191,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.1", ] [[package]] @@ -4138,9 +4249,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -4259,15 +4370,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -4309,9 +4420,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4334,7 +4445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der 0.8.1", ] [[package]] @@ -4397,7 +4508,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4409,7 +4520,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4431,9 +4542,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5edbec4ed188954a10c12c038215f8ce7606b2d5c973cd8dc43e8795065c5f2f" dependencies = [ "proc-macro2", "quote", @@ -4457,7 +4579,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4480,7 +4602,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4514,7 +4636,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "parking_lot", "rustix", "signal-hook", @@ -4550,7 +4672,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.13.0", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -4595,11 +4717,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.18", + "thiserror-impl 2.0.19", ] [[package]] @@ -4610,18 +4732,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.1", ] [[package]] @@ -4668,9 +4790,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4683,9 +4805,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -4693,20 +4815,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.4", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4731,9 +4853,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" dependencies = [ "futures-util", "log", @@ -4777,7 +4899,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytes", "futures-util", "http 1.4.2", @@ -4821,7 +4943,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4841,9 +4963,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "data-encoding", @@ -4851,9 +4973,9 @@ dependencies = [ "httparse", "log", "native-tls", - "rand 0.9.4", - "sha1 0.10.6", - "thiserror 2.0.18", + "rand 0.10.2", + "sha1 0.11.0", + "thiserror 2.0.19", ] [[package]] @@ -4963,9 +5085,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "atomic", "getrandom 0.4.3", @@ -5097,7 +5219,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5114,7 +5236,13 @@ dependencies = [ name = "web-server" version = "0.1.0" dependencies = [ + "bytes", + "http 1.4.2", + "iota-logger", + "iota-state", + "iota-util", "mtp", + "tokio", ] [[package]] @@ -5166,7 +5294,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -5190,9 +5318,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -5353,7 +5481,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5364,7 +5492,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5529,8 +5657,8 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "sha2 0.11.0", - "socket2 0.6.4", - "thiserror 2.0.18", + "socket2 0.6.5", + "thiserror 2.0.19", "time", "tokio", "tracing", @@ -5547,7 +5675,7 @@ checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" dependencies = [ "httlib-huffman", "octets", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", ] @@ -5589,7 +5717,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -5622,28 +5750,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5663,7 +5791,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5684,7 +5812,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5717,7 +5845,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5740,7 +5868,7 @@ dependencies = [ "memchr", "pbkdf2", "ppmd-rust", - "sha1 0.10.6", + "sha1 0.10.7", "time", "zeroize", "zopfli", @@ -5749,15 +5877,15 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5431d5661c32445236631278f27946e444ddafe4684cac70b185272d4f9c52d5" +checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index c62208e..ff17473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,19 @@ [workspace] -members = ["iota-storage", "client", "iota-auth", "other-iota", "iota-updater", "iota-terms", "iota-state", "iota-cli", "iota-core", "omikron-connector", "web-server", "web-ui", "iota-logger", "iota-util"] +members = [ + "iota-storage", + "iota-connection", + "client", + "iota-auth", + "other-iota", + "iota-updater", + "iota-terms", + "iota-state", + "iota-cli", + "iota-core", + "omikron-connector", + "web-server", + "web-ui", + "iota-logger", + "iota-util", +] resolver = "3" diff --git a/client/Cargo.toml b/client/Cargo.toml index c29f662..bdf08ad 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["client"] } +iota-connection = { path = "../iota-connection" } iota-logger = { path = "../iota-logger" } iota-util = { path = "../iota-util" } iota-storage = { path = "../iota-storage" } diff --git a/client/src/client_connection.rs b/client/src/client_connection.rs index 774119f..95c66cc 100644 --- a/client/src/client_connection.rs +++ b/client/src/client_connection.rs @@ -1,160 +1,22 @@ use dashmap::DashMap; +use iota_connection::message_common::*; +use iota_connection::message_handlers; use iota_logger::{log_cv_in, log_cv_out, log_t}; use iota_state::SHUTDOWN; -use iota_storage::users::contact::Contact; -use iota_storage::util::chat_files::{MessageState, change_message_state}; -use iota_storage::util::chats_util::{get_user, mod_user}; -use iota_storage::util::communities_util::CommunitiesUtil; +use iota_storage::util::chat_files::{self, MessageState, change_message_state}; use iota_storage::util::config_util::CONFIG; -use iota_storage::util::e2ee_storage::{self, ChatSecretQuery, StoredChatSecret}; -use iota_storage::util::{chat_files, chats_util}; +use iota_storage::util::e2ee_storage::{self, StoredChatSecret}; use iota_util::crypto_helper::keyring_from_base64; use iota_util::crypto_util::{self}; -use iota_util::file_util::{get_children, load_file, save_file}; use mtp::client::{Receiver, Sender}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; -use mtp::type_map::TypeMap; use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use tokio::sync::{Mutex, RwLock, mpsc, watch}; use tokio::task::JoinHandle; use uuid::Uuid; -fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { - use mtp::type_map::{DataTypeId, TypeMap}; - let tm = TypeMap::latest(); - DataValue::Container( - items - .into_iter() - .filter_map(|(dt, dv)| tm.data_id_enum(dt).map(|id| (DataTypeId(id), dv))) - .collect(), - ) -} - -fn data_string(cv: &CommunicationValue, dt: DataType) -> Option { - cv.get_data(dt) - .as_str() - .map(|s| s.to_string()) - .or_else(|| cv.get_data(dt).as_number().map(|n| n.to_string())) - .or_else(|| cv.get_data(dt).as_signed_number().map(|n| n.to_string())) -} - -fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option { - cv.get_data(dt) - .as_number() - .and_then(|n| i64::try_from(n).ok()) - .or_else(|| { - cv.get_data(dt) - .as_signed_number() - .and_then(|n| i64::try_from(n).ok()) - }) - .or_else(|| cv.get_data(dt).as_str().and_then(|s| s.parse::().ok())) -} - -#[derive(Debug, Clone)] -struct ChatSecretRecipient { - user_id: String, - encrypted_secret: Vec, - kem_ciphertext: Vec, -} - -fn recipient_from_value(value: &DataValue) -> Option { - let tm = TypeMap::latest(); - let user_id = value - .get_field(DataType::UserId.to_id(&tm))? - .as_str() - .map(|s| s.to_string()) - .or_else(|| { - value - .get_field(DataType::UserId.to_id(&tm))? - .as_number() - .map(|n| n.to_string()) - })?; - let encrypted_secret = value - .get_field(DataType::EncryptedSecret.to_id(&tm))? - .as_bytes()?; - let kem_ciphertext = value - .get_field(DataType::KemCiphertext.to_id(&tm))? - .as_bytes()?; - - Some(ChatSecretRecipient { - user_id, - encrypted_secret, - kem_ciphertext, - }) -} - -fn chat_secret_recipients(cv: &CommunicationValue) -> Option> { - let recipients = cv.get_data(DataType::Recipients).as_array()?; - let parsed = recipients - .iter() - .map(recipient_from_value) - .collect::>>()?; - - if parsed.is_empty() { - None - } else { - Some(parsed) - } -} - -fn set_chat_secret_cv_for_recipient( - source: &CommunicationValue, - recipient: &ChatSecretRecipient, -) -> CommunicationValue { - let recipient_value = typed_container(vec![ - (DataType::UserId, DataValue::Str(recipient.user_id.clone())), - ( - DataType::EncryptedSecret, - DataValue::Bytes(recipient.encrypted_secret.clone()), - ), - ( - DataType::KemCiphertext, - DataValue::Bytes(recipient.kem_ciphertext.clone()), - ), - ]); - - CommunicationValue::new(CommunicationType::SetChatSecret) - .with_id(source.get_id()) - .with_sender(source.get_sender()) - .with_receiver(recipient.user_id.parse::().unwrap_or(0)) - .add_typed_default(DataType::ChatId, source.get_data(DataType::ChatId).clone()) - .add_typed_default( - DataType::SecretId, - source.get_data(DataType::SecretId).clone(), - ) - .add_typed_default( - DataType::VersionNumber, - source.get_data(DataType::VersionNumber).clone(), - ) - .add_typed_default( - DataType::WrappingScheme, - source.get_data(DataType::WrappingScheme).clone(), - ) - .add_typed_default( - DataType::CreatedAt, - source.get_data(DataType::CreatedAt).clone(), - ) - .add_typed_default( - DataType::Recipients, - DataValue::Array(vec![recipient_value]), - ) -} - -fn now_millis_i64() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64 -} - -fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { - CommunicationValue::new(ty) - .with_id(request.get_id()) - .with_receiver(request.get_sender()) -} - // ============================================================================ // Waiting Task System // ============================================================================ @@ -223,7 +85,7 @@ impl ClientConnection { } if let Some(sender) = self.sender.read().await.as_ref() { - sender.close(); + sender.close().await; } *self.sender.write().await = None; @@ -235,12 +97,9 @@ impl ClientConnection { async fn handle_ping(self: Arc, cv: CommunicationValue) { // Update our ping if provided if let DataValue::SignedNumber(last_ping) = cv.get_data(DataType::LastPing) { - let current = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); + let current = now_millis_i64(); let mut ping_guard = self.ping.write().await; - *ping_guard = current as i64 - *last_ping as i64; + *ping_guard = current - *last_ping as i64; } // Send pong response @@ -324,70 +183,8 @@ impl ClientConnection { } if cv.is_type(CommunicationType::GetChatSecret) { - let Some(user_id) = data_string(&cv, DataType::UserId) else { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - }; - let sender_id = cv.get_sender().to_string(); - if user_id != sender_id { - self.send_message(&error_response(&cv, CommunicationType::ErrorNotFound)) - .await; - return; - } - let Some(chat_id) = data_string(&cv, DataType::ChatId) else { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - }; - - match e2ee_storage::get_chat_secret(ChatSecretQuery { - user_id, - chat_id, - secret_id: data_string(&cv, DataType::SecretId), - }) { - Ok(Some(record)) => { - let response = CommunicationValue::new(CommunicationType::ChatSecretResponse) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_typed_default(DataType::UserId, DataValue::Str(record.user_id)) - .add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id)) - .add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id)) - .add_typed_default( - DataType::VersionNumber, - DataValue::SignedNumber(record.version as i128), - ) - .add_typed_default( - DataType::EncryptedSecret, - DataValue::Bytes(record.encrypted_secret), - ) - .add_typed_default( - DataType::KemCiphertext, - DataValue::Bytes(record.kem_ciphertext), - ) - .add_typed_default( - DataType::WrappingScheme, - DataValue::Str(record.wrapping_scheme), - ) - .add_typed_default( - DataType::CreatedAt, - DataValue::SignedNumber(record.created_at as i128), - ) - .add_typed_default( - DataType::UpdatedAt, - DataValue::SignedNumber(record.updated_at as i128), - ); - self.send_message(&response).await; - } - Ok(None) => { - self.send_message(&error_response(&cv, CommunicationType::ErrorNotSet)) - .await; - } - Err(_) => { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - } - } + self.send_message(&message_handlers::handle_get_chat_secret(&cv)) + .await; return; } @@ -434,132 +231,20 @@ impl ClientConnection { } if cv.is_type(CommunicationType::CreateApp) { - let sender_id = cv.get_sender() as i64; - let app_identifier = cv - .get_data(DataType::AppIdentifier) - .as_str() - .unwrap_or("") - .to_string(); - let app_public_key = cv - .get_data(DataType::AppPublicKey) - .as_str() - .unwrap_or("") - .to_string(); - - if !app_identifier.is_empty() && !app_public_key.is_empty() { - if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { - if !user.trusted_apps.contains_key(&app_identifier) { - user.trusted_apps.insert(app_identifier, app_public_key); - iota_storage::users::user_manager::update_user(user); - } - } - } - - let res = CommunicationValue::new(CommunicationType::CreateApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&res).await; + self.send_message(&message_handlers::handle_create_app(&cv)) + .await; return; } if cv.is_type(CommunicationType::DeleteApp) { - let sender_id = cv.get_sender() as i64; - let app_identifier = cv - .get_data(DataType::AppIdentifier) - .as_str() - .unwrap_or("") - .to_string(); - - if !app_identifier.is_empty() { - if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { - if user.trusted_apps.contains_key(&app_identifier) { - user.trusted_apps.remove(&app_identifier); - iota_storage::users::user_manager::update_user(user); - } - } - } - - let res = CommunicationValue::new(CommunicationType::DeleteApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&res).await; + self.send_message(&message_handlers::handle_delete_app(&cv)) + .await; return; } if cv.is_type(CommunicationType::ClientConnected) { - let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; - let _session_id = cv.get_data(DataType::SessionId).as_number().unwrap_or(0) as i64; - - let contacts = chats_util::get_users(user_id); - let mut contacts_array = Vec::new(); - - for (i, contact) in contacts.iter().enumerate() { - let mut contact_container = Vec::new(); - contact_container.push(( - DataType::UserId, - DataValue::SignedNumber(contact.user_id as i128), - )); - contact_container.push(( - DataType::LastMessageAt, - DataValue::SignedNumber(contact.last_message_at.unwrap_or(0) as i128), - )); - - if let Some(ref name) = contact.user_name { - contact_container.push((DataType::Username, DataValue::Str(name.clone()))); - } - - let amount = if i < 10 { 20 } else { 1 }; - let messages = chat_files::get_messages(user_id, contact.user_id, 0, amount); - - let mut msg_array = Vec::new(); - for m in messages.members() { - let message_time = m["message_time"].as_i64().unwrap_or(0); - let content = m["content"].as_str().unwrap_or("").to_string(); - let sent_by_self = m["sent_by_self"].as_bool().unwrap_or(false); - let height = m["height"].as_i64().unwrap_or(0); - let message_state = m["message_state"].as_str().unwrap_or("").to_string(); - - let mut msg_container = Vec::new(); - msg_container.push(( - DataType::SendTime, - DataValue::SignedNumber(message_time as i128), - )); - msg_container.push((DataType::Content, DataValue::Str(content.clone()))); - msg_container.push((DataType::MessageState, DataValue::Str(message_state))); - msg_container.push((DataType::Height, DataValue::SignedNumber(height as i128))); - msg_container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if sent_by_self { - user_id as u128 - } else { - contact.user_id as u128 - }), - )); - msg_array.push(typed_container(msg_container)); - - if msg_array.len() == 1 { - let sender_id = if sent_by_self { - user_id - } else { - contact.user_id - }; - let mut last_msg = Vec::new(); - last_msg.push((DataType::Content, DataValue::Str(content))); - last_msg.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - contact_container.push((DataType::LastMessage, typed_container(last_msg))); - } - } - contact_container.push((DataType::Messages, DataValue::Array(msg_array))); - contacts_array.push(typed_container(contact_container)); - } - - let resp = CommunicationValue::new(CommunicationType::ClientConnected) - .with_id(cv.get_id()) - .add_typed_default(DataType::Contacts, DataValue::Array(contacts_array)); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_client_connected(&cv)) + .await; return; } @@ -568,32 +253,32 @@ impl ClientConnection { // ************************************************ // if cv.is_type(CommunicationType::MessageState) { - let sender_id = &cv.get_sender(); - let receiver_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(id) => id, - _ => return, - }; + message_handlers::handle_message_state(&cv); + return; + } - // Parse send_time robustly: accept numeric or string, fallback to current time - let send_time_val = cv.get_data(DataType::SendTime); - 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 - }; + if cv.is_type(CommunicationType::MessageEdit) { + self.send_message(&message_handlers::handle_message_edit(&cv)) + .await; + return; + } - let _ = chat_files::change_message_state( - timestamp_i64, - receiver_id as i64, - *sender_id as i64, - MessageState::from_str(cv.get_data(DataType::MessageState).as_str().unwrap_or("")), - ); + if cv.is_type(CommunicationType::MessageReactionAdd) { + self.send_message(&message_handlers::handle_message_reaction(&cv, true)) + .await; + return; + } + + if cv.is_type(CommunicationType::MessageReactionRemove) { + self.send_message(&message_handlers::handle_message_reaction(&cv, false)) + .await; + return; + } + + if cv.is_type(CommunicationType::MessageDeleteLive) { + self.send_message(&message_handlers::handle_message_delete(&cv)) + .await; + return; } // Incoming storsed message: store for the recipient, attempt local delivery, notify sender. @@ -609,10 +294,7 @@ impl ClientConnection { // parse send_time safely (number or string), fallback to now let send_time_val = cv.get_data(DataType::SendTime); - let now_i64 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; + let now_i64 = now_millis_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() { @@ -738,186 +420,38 @@ impl ClientConnection { } if cv.is_type(CommunicationType::MessagesGet) { - let my_id = cv.get_sender(); - let partner_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0); - let offset = cv.get_data(DataType::Offset).as_number().unwrap_or(0); - let amount = cv.get_data(DataType::Amount).as_number().unwrap_or(0); - let messages = chat_files::get_messages( - my_id as i64, - partner_id as i64, - offset as i64, - amount as i64, - ); - 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(DataType::ChatPartnerId).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataType::ChatPartnerId).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(( - DataType::SendTime, - DataValue::SignedNumber(message_time as i128), - )); - container.push((DataType::Content, DataValue::Str(content))); - container.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - container.push((DataType::MessageState, DataValue::Str(message_state))); - container.push((DataType::Height, DataValue::SignedNumber(height as i128))); - container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if sent_by_self { - my_id as u128 - } else { - partner_id as u128 - }), - )); - msg_array.push(typed_container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::MessagesGet) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Messages, DataValue::Array(msg_array)); - - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_messages_get(&cv)) + .await; return; } if cv.is_type(CommunicationType::GetChats) { - 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(( - DataType::UserId, - DataValue::SignedNumber(user.user_id as i128), - )); - if let Some(name) = user.user_name { - container.push((DataType::Username, DataValue::Str(name))); - } - if let Some(ts) = user.last_message_at { - container.push((DataType::LastMessageAt, DataValue::SignedNumber(ts as i128))); - } - user_array.push(typed_container(container)); - } - let resp = CommunicationValue::new(CommunicationType::GetChats) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_typed_default(DataType::UserIds, DataValue::Array(user_array)); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_get_chats(&cv)) + .await; return; } if cv.is_type(CommunicationType::AddConversation) { - let user_id = cv.get_sender(); - let other_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(n) => n as i64, - None => cv - .get_data(DataType::ChatPartnerId) - .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(DataType::ChatPartnerName).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::AddConversation) - .with_id(cv.get_id()) - .with_receiver(user_id); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_add_conversation(&cv)) + .await; return; } if cv.is_type(CommunicationType::AddCommunity) { - CommunitiesUtil::add_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::CommunityTitle) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::Position) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::AddCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_add_community(&cv)) + .await; return; } if cv.is_type(CommunicationType::GetCommunities) { - let mut comm_array = Vec::new(); - for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { - let mut container: Vec<(DataType, DataValue)> = Vec::new(); - if let Some(address) = c["address"].as_str() { - container.push(( - DataType::CommunityAddress, - DataValue::Str(address.to_string()), - )); - } - if let Some(title) = c["title"].as_str() { - container.push((DataType::CommunityTitle, DataValue::Str(title.to_string()))); - } - if let Some(position) = c["position"].as_str() { - container.push((DataType::Position, DataValue::Str(position.to_string()))); - } - comm_array.push(typed_container(container)); - } - - let resp = CommunicationValue::new(CommunicationType::GetCommunities) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_typed_default(DataType::Communities, DataValue::Array(comm_array)); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_get_communities(&cv)) + .await; return; } if cv.is_type(CommunicationType::RemoveCommunity) { - CommunitiesUtil::remove_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::RemoveCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; + self.send_message(&message_handlers::handle_remove_community(&cv)) + .await; return; } @@ -926,10 +460,11 @@ impl ClientConnection { let settings_name = cv.get_data(DataType::SettingsName).as_str().unwrap(); let settings_value = cv.get_data(DataType::Payload).as_str().unwrap(); - save_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - &settings_value, + let _ = iota_storage::util::settings::save( + my_id as i64, + iota_storage::util::settings::GLOBAL_SESSION_ID, + settings_name, + settings_value, ); let response = CommunicationValue::new(CommunicationType::SettingsSave) @@ -943,10 +478,14 @@ impl ClientConnection { if cv.is_type(CommunicationType::SettingsLoad) { let my_id = cv.get_sender(); let settings_name = cv.get_data(DataType::SettingsName).as_string().unwrap(); - let settings_value_str = load_file( - &format!("users/{}/settings/", my_id), - &format!("{}.settings", settings_name), - ); + let settings_value_str = iota_storage::util::settings::load( + my_id as i64, + iota_storage::util::settings::GLOBAL_SESSION_ID, + &settings_name, + ) + .ok() + .flatten() + .unwrap_or_default(); let response = CommunicationValue::new(CommunicationType::SettingsLoad) .with_id(cv.get_id()) .with_receiver(my_id) @@ -959,15 +498,12 @@ impl ClientConnection { if cv.is_type(CommunicationType::SettingsList) { 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 settings = iota_storage::util::settings::list( + my_id as i64, + iota_storage::util::settings::GLOBAL_SESSION_ID, + ) + .unwrap_or_default(); + let settings_json = settings.into_iter().map(DataValue::Str).collect(); let response = CommunicationValue::new(CommunicationType::SettingsList) .with_id(cv.get_id()) .with_receiver(my_id) @@ -1014,7 +550,7 @@ impl ClientConnection { if !sender.is_open() { drop(sender_guard); if let Some(sender) = self.sender.write().await.take() { - sender.close(); + sender.close().await; } return Err("connection closed".to_string()); } diff --git a/iota-cli/src/elements/console_card.rs b/iota-cli/src/elements/console_card.rs index 6e9a7a0..820ae1e 100644 --- a/iota-cli/src/elements/console_card.rs +++ b/iota-cli/src/elements/console_card.rs @@ -444,7 +444,7 @@ pub async fn run_command(command: &str) { if let Some(user) = user_manager::get_user_by_username(username) { let msg = CommunicationValue::new(CommunicationType::DeleteUser) .with_sender(user.user_id as u64); - OMIKRON_CONNECTION.send_message(&msg).await; + let _ = OMIKRON_CONNECTION.send_message(&msg).await; user_manager::remove_user(user.user_id); log!("Removed user {}", user.user_id); } else { diff --git a/iota-connection/Cargo.toml b/iota-connection/Cargo.toml new file mode 100644 index 0000000..65eafad --- /dev/null +++ b/iota-connection/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "iota-connection" +version = "0.1.0" +edition = "2024" + +[dependencies] +iota-storage = { path = "../iota-storage" } +iota-util = { path = "../iota-util" } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } diff --git a/iota-connection/src/connection_handler.rs b/iota-connection/src/connection_handler.rs new file mode 100644 index 0000000..6a20bb1 --- /dev/null +++ b/iota-connection/src/connection_handler.rs @@ -0,0 +1,36 @@ +use mtp::codec::CommunicationValue; +use std::future::Future; +use std::time::Duration; + +/// Unified interface for all connection types (Omikron, Direct, future modes). +/// +/// Provides the common messaging API that the rest of the codebase uses, +/// regardless of whether the connection goes through Omikron or is direct. +pub trait ConnectionHandler: Send + Sync { + /// Send a message to the remote end. + fn send_message( + &self, + cv: &CommunicationValue, + ) -> impl Future> + Send; + + /// Send a message and wait for a correlated response. + /// + /// The implementation correlates requests/responses by message ID and + /// enforces the given `timeout`. Returns an error on timeout or if the + /// connection drops while waiting. + fn await_response( + &self, + cv: &CommunicationValue, + timeout: Option, + ) -> impl Future> + Send; + + /// Returns `true` when the connection is alive and ready for traffic. + fn is_connected(&self) -> impl Future + Send; + + /// Returns `true` when the connection has completed identification / + /// registration and is fully operational. + fn is_identified(&self) -> impl Future + Send; + + /// Gracefully tear down the connection. + fn stop(&self) -> impl Future + Send; +} diff --git a/iota-connection/src/lib.rs b/iota-connection/src/lib.rs new file mode 100644 index 0000000..601ca7b --- /dev/null +++ b/iota-connection/src/lib.rs @@ -0,0 +1,3 @@ +pub mod connection_handler; +pub mod message_common; +pub mod message_handlers; diff --git a/iota-connection/src/message_common.rs b/iota-connection/src/message_common.rs new file mode 100644 index 0000000..2ab9733 --- /dev/null +++ b/iota-connection/src/message_common.rs @@ -0,0 +1,137 @@ +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use mtp::type_map::TypeMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +pub fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { + use mtp::type_map::{DataTypeId, TypeMap}; + let tm = TypeMap::latest(); + DataValue::Container( + items + .into_iter() + .filter_map(|(dt, dv)| tm.data_id_enum(dt).map(|id| (DataTypeId(id), dv))) + .collect(), + ) +} + +pub fn data_string(cv: &CommunicationValue, dt: DataType) -> Option { + cv.get_data(dt) + .as_str() + .map(|s| s.to_string()) + .or_else(|| cv.get_data(dt).as_number().map(|n| n.to_string())) + .or_else(|| cv.get_data(dt).as_signed_number().map(|n| n.to_string())) +} + +pub fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option { + cv.get_data(dt) + .as_number() + .and_then(|n| i64::try_from(n).ok()) + .or_else(|| { + cv.get_data(dt) + .as_signed_number() + .and_then(|n| i64::try_from(n).ok()) + }) + .or_else(|| cv.get_data(dt).as_str().and_then(|s| s.parse::().ok())) +} + +#[derive(Debug, Clone)] +pub struct ChatSecretRecipient { + pub user_id: String, + pub encrypted_secret: Vec, + pub kem_ciphertext: Vec, +} + +pub fn recipient_from_value(value: &DataValue) -> Option { + let tm = TypeMap::latest(); + let user_id = value + .get_field(DataType::UserId.try_to_id(&tm)?)? + .as_str() + .map(|s| s.to_string()) + .or_else(|| { + value + .get_field(DataType::UserId.try_to_id(&tm)?)? + .as_number() + .map(|n| n.to_string()) + })?; + let encrypted_secret = value + .get_field(DataType::EncryptedSecret.try_to_id(&tm)?)? + .as_bytes()?; + let kem_ciphertext = value + .get_field(DataType::KemCiphertext.try_to_id(&tm)?)? + .as_bytes()?; + + Some(ChatSecretRecipient { + user_id, + encrypted_secret, + kem_ciphertext, + }) +} + +pub fn chat_secret_recipients(cv: &CommunicationValue) -> Option> { + let recipients = cv.get_data(DataType::Recipients).as_array()?; + let parsed = recipients + .iter() + .map(recipient_from_value) + .collect::>>()?; + + if parsed.is_empty() { + None + } else { + Some(parsed) + } +} + +pub fn set_chat_secret_cv_for_recipient( + source: &CommunicationValue, + recipient: &ChatSecretRecipient, +) -> CommunicationValue { + let recipient_value = typed_container(vec![ + (DataType::UserId, DataValue::Str(recipient.user_id.clone())), + ( + DataType::EncryptedSecret, + DataValue::Bytes(recipient.encrypted_secret.clone()), + ), + ( + DataType::KemCiphertext, + DataValue::Bytes(recipient.kem_ciphertext.clone()), + ), + ]); + + CommunicationValue::new(CommunicationType::SetChatSecret) + .with_id(source.get_id()) + .with_sender(source.get_sender()) + .with_receiver(recipient.user_id.parse::().unwrap_or(0)) + .add_typed_default(DataType::ChatId, source.get_data(DataType::ChatId).clone()) + .add_typed_default( + DataType::SecretId, + source.get_data(DataType::SecretId).clone(), + ) + .add_typed_default( + DataType::VersionNumber, + source.get_data(DataType::VersionNumber).clone(), + ) + .add_typed_default( + DataType::WrappingScheme, + source.get_data(DataType::WrappingScheme).clone(), + ) + .add_typed_default( + DataType::CreatedAt, + source.get_data(DataType::CreatedAt).clone(), + ) + .add_typed_default( + DataType::Recipients, + DataValue::Array(vec![recipient_value]), + ) +} + +pub fn now_millis_i64() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +pub fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { + CommunicationValue::new(ty) + .with_id(request.get_id()) + .with_receiver(request.get_sender()) +} diff --git a/iota-connection/src/message_handlers.rs b/iota-connection/src/message_handlers.rs new file mode 100644 index 0000000..b3c08a9 --- /dev/null +++ b/iota-connection/src/message_handlers.rs @@ -0,0 +1,766 @@ +use crate::message_common::*; +use iota_storage::util::chat_files::{self, MessageState}; +use iota_storage::util::chats_util::{self, get_user, mod_user}; +use iota_storage::util::communities_util::CommunitiesUtil; +use iota_storage::util::e2ee_storage::{self, ChatSecretQuery}; +use iota_storage::util::settings; +use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; + +pub struct MessageMutation { + pub sender_id: i64, + pub partner_id: i64, + pub send_time: i64, +} + +pub fn message_mutation(cv: &CommunicationValue) -> Result { + let sender_id = i64::try_from(cv.get_sender()) + .map_err(|_| error_response(cv, CommunicationType::ErrorInvalidData))?; + let partner_id = data_i64(cv, DataType::ChatPartnerId) + .filter(|id| *id > 0) + .ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?; + let send_time = data_i64(cv, DataType::SendTime) + .filter(|time| *time > 0) + .ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?; + + Ok(MessageMutation { + sender_id, + partner_id, + send_time, + }) +} + +pub fn success_response(cv: &CommunicationValue) -> CommunicationValue { + error_response(cv, CommunicationType::Success) +} + +pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue { + let mutation = match message_mutation(cv) { + Ok(mutation) => mutation, + Err(response) => return response, + }; + let Some(content) = cv.get_data(DataType::Content).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + + match chat_files::edit_message( + mutation.sender_id, + mutation.partner_id, + mutation.send_time, + mutation.sender_id, + content, + ) { + Ok(()) => success_response(cv), + Err(_) => error_response(cv, CommunicationType::ErrorNotFound), + } +} + +pub fn handle_message_reaction(cv: &CommunicationValue, add: bool) -> CommunicationValue { + let mutation = match message_mutation(cv) { + Ok(mutation) => mutation, + Err(response) => return response, + }; + let Some(reaction) = cv.get_data(DataType::Reaction).as_str() else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + if reaction.is_empty() || reaction.len() > 64 { + return error_response(cv, CommunicationType::ErrorInvalidData); + } + + let result = if add { + chat_files::add_reaction( + mutation.sender_id, + mutation.partner_id, + mutation.send_time, + mutation.sender_id, + reaction, + ) + } else { + chat_files::remove_reaction( + mutation.sender_id, + mutation.partner_id, + mutation.send_time, + mutation.sender_id, + reaction, + ) + }; + + match result { + Ok(()) => success_response(cv), + Err(_) => error_response(cv, CommunicationType::ErrorNotFound), + } +} + +pub fn handle_message_delete(cv: &CommunicationValue) -> CommunicationValue { + let mutation = match message_mutation(cv) { + Ok(mutation) => mutation, + Err(response) => return response, + }; + + match chat_files::delete_message(mutation.sender_id, mutation.partner_id, mutation.send_time) { + Ok(()) => success_response(cv), + Err(_) => error_response(cv, CommunicationType::ErrorNotFound), + } +} + +fn stored_message_value( + message: &chat_files::StoredMessage, + storage_owner: i64, + partner_id: i64, +) -> DataValue { + let mut fields = vec![ + ( + DataType::SendTime, + DataValue::SignedNumber(message.message_time as i128), + ), + (DataType::Content, DataValue::Str(message.content.clone())), + ( + DataType::MessageState, + DataValue::Str(message.message_state.clone()), + ), + ( + DataType::Height, + DataValue::SignedNumber(message.height as i128), + ), + ( + DataType::SenderId, + DataValue::UnsignedNumber(if message.sent_by_self { + storage_owner as u128 + } else { + partner_id as u128 + }), + ), + ]; + if message.edited { + fields.push((DataType::Edited, DataValue::Bool(true))); + } + if let Some(reply_to) = message.reply_to { + fields.push(( + DataType::ReplyId, + DataValue::UnsignedNumber(reply_to as u64 as u128), + )); + } + if !message.reactions.is_empty() { + let reactions = message + .reactions + .iter() + .map(|reaction| { + typed_container(vec![ + ( + DataType::Reaction, + DataValue::Str(reaction.reaction.clone()), + ), + ( + DataType::SenderId, + DataValue::SignedNumber(reaction.user_id as i128), + ), + ]) + }) + .collect(); + fields.push((DataType::Reactions, DataValue::Array(reactions))); + } + typed_container(fields) +} + +pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue { + let Some(user_id) = data_string(cv, DataType::UserId) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + if user_id != cv.get_sender().to_string() { + return error_response(cv, CommunicationType::ErrorNotFound); + } + let Some(chat_id) = data_string(cv, DataType::ChatId) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + + match e2ee_storage::get_chat_secret(ChatSecretQuery { + user_id, + chat_id, + secret_id: data_string(cv, DataType::SecretId), + }) { + Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) + .add_typed_default(DataType::UserId, DataValue::Str(record.user_id)) + .add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id)) + .add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id)) + .add_typed_default( + DataType::VersionNumber, + DataValue::SignedNumber(record.version as i128), + ) + .add_typed_default( + DataType::EncryptedSecret, + DataValue::Bytes(record.encrypted_secret), + ) + .add_typed_default( + DataType::KemCiphertext, + DataValue::Bytes(record.kem_ciphertext), + ) + .add_typed_default( + DataType::WrappingScheme, + DataValue::Str(record.wrapping_scheme), + ) + .add_typed_default( + DataType::CreatedAt, + DataValue::SignedNumber(record.created_at as i128), + ) + .add_typed_default( + DataType::UpdatedAt, + DataValue::SignedNumber(record.updated_at as i128), + ), + Ok(None) => error_response(cv, CommunicationType::ErrorNotSet), + Err(_) => error_response(cv, CommunicationType::ErrorInvalidData), + } +} + +pub fn handle_create_app(cv: &CommunicationValue) -> CommunicationValue { + let sender_id = cv.get_sender() as i64; + let app_identifier = cv + .get_data(DataType::AppIdentifier) + .as_str() + .unwrap_or("") + .to_string(); + let app_public_key = cv + .get_data(DataType::AppPublicKey) + .as_str() + .unwrap_or("") + .to_string(); + + if !app_identifier.is_empty() && !app_public_key.is_empty() { + if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { + if !user.trusted_apps.contains_key(&app_identifier) { + user.trusted_apps.insert(app_identifier, app_public_key); + iota_storage::users::user_manager::update_user(user); + } + } + } + + CommunicationValue::new(CommunicationType::CreateApp) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) +} + +pub fn handle_delete_app(cv: &CommunicationValue) -> CommunicationValue { + let sender_id = cv.get_sender() as i64; + let app_identifier = cv + .get_data(DataType::AppIdentifier) + .as_str() + .unwrap_or("") + .to_string(); + + if !app_identifier.is_empty() { + if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { + if user.trusted_apps.contains_key(&app_identifier) { + user.trusted_apps.remove(&app_identifier); + iota_storage::users::user_manager::update_user(user); + } + } + } + + CommunicationValue::new(CommunicationType::DeleteApp) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) +} + +pub fn handle_client_connected(cv: &CommunicationValue) -> CommunicationValue { + let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; + + let contacts = chats_util::get_users(user_id); + let mut contacts_array = Vec::new(); + + for (i, contact) in contacts.iter().enumerate() { + let mut contact_container = Vec::new(); + contact_container.push(( + DataType::UserId, + DataValue::SignedNumber(contact.user_id as i128), + )); + contact_container.push(( + DataType::LastMessageAt, + DataValue::SignedNumber(contact.last_message_at.unwrap_or(0) as i128), + )); + + if let Some(ref name) = contact.user_name { + contact_container.push((DataType::Username, DataValue::Str(name.clone()))); + } + + let amount = if i < 10 { 20 } else { 1 }; + let messages = chat_files::get_messages(user_id, contact.user_id, 0, amount); + + let mut msg_array = Vec::new(); + for m in &messages { + msg_array.push(stored_message_value(m, user_id, contact.user_id)); + + if msg_array.len() == 1 { + let sender_id = if m.sent_by_self { + user_id + } else { + contact.user_id + }; + let mut last_msg = Vec::new(); + last_msg.push((DataType::Content, DataValue::Str(m.content.clone()))); + last_msg.push(( + DataType::SenderId, + DataValue::SignedNumber(sender_id as i128), + )); + contact_container.push((DataType::LastMessage, typed_container(last_msg))); + } + } + contact_container.push((DataType::Messages, DataValue::Array(msg_array))); + contacts_array.push(typed_container(contact_container)); + } + + CommunicationValue::new(CommunicationType::ClientConnected) + .with_id(cv.get_id()) + .add_typed_default(DataType::Contacts, DataValue::Array(contacts_array)) +} + +pub fn handle_message_state(cv: &CommunicationValue) { + let sender_id = &cv.get_sender(); + let receiver_id = match cv.get_data(DataType::ChatPartnerId).as_number() { + Some(id) => id, + _ => return, + }; + + let timestamp_i64 = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { + s.parse::().unwrap_or_else(|_| now_millis_i64()) + } else { + now_millis_i64() + }; + + let _ = chat_files::change_message_state( + timestamp_i64, + receiver_id as i64, + *sender_id as i64, + MessageState::from_str(cv.get_data(DataType::MessageState).as_str().unwrap_or("")), + ); +} + +pub fn handle_messages_get(cv: &CommunicationValue) -> CommunicationValue { + let my_id = cv.get_sender(); + let partner_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0); + let offset = cv.get_data(DataType::Offset).as_number().unwrap_or(0); + let amount = cv.get_data(DataType::Amount).as_number().unwrap_or(0); + let messages = chat_files::get_messages( + my_id as i64, + partner_id as i64, + offset as i64, + amount as i64, + ); + let mut msg_array: Vec = Vec::new(); + for m in &messages { + msg_array.push(stored_message_value(m, my_id as i64, partner_id as i64)); + } + + CommunicationValue::new(CommunicationType::MessagesGet) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default(DataType::Messages, DataValue::Array(msg_array)) +} + +pub fn handle_get_chats(cv: &CommunicationValue) -> CommunicationValue { + 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(( + DataType::UserId, + DataValue::SignedNumber(user.user_id as i128), + )); + if let Some(name) = user.user_name { + container.push((DataType::Username, DataValue::Str(name))); + } + if let Some(ts) = user.last_message_at { + container.push((DataType::LastMessageAt, DataValue::SignedNumber(ts as i128))); + } + user_array.push(typed_container(container)); + } + CommunicationValue::new(CommunicationType::GetChats) + .with_id(cv.get_id()) + .with_receiver(user_id) + .add_typed_default(DataType::UserIds, DataValue::Array(user_array)) +} + +pub fn handle_add_conversation(cv: &CommunicationValue) -> CommunicationValue { + let user_id = cv.get_sender(); + let other_id = match cv.get_data(DataType::ChatPartnerId).as_number() { + Some(n) => n as i64, + None => cv + .get_data(DataType::ChatPartnerId) + .as_str() + .unwrap_or("0") + .parse() + .unwrap_or(0), + }; + let mut contact = get_user(user_id as i64, other_id) + .unwrap_or(iota_storage::users::contact::Contact::new(other_id)); + + if let Some(name) = cv.get_data(DataType::ChatPartnerName).as_str() { + contact.user_name = Some(name.to_string()); + } + + contact.set_last_message_at(now_millis_i64()); + mod_user(user_id as i64, &contact); + CommunicationValue::new(CommunicationType::AddConversation) + .with_id(cv.get_id()) + .with_receiver(user_id) +} + +pub fn handle_add_community(cv: &CommunicationValue) -> CommunicationValue { + CommunitiesUtil::add_community( + cv.get_sender() as i64, + cv.get_data(DataType::CommunityAddress) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataType::CommunityTitle) + .as_str() + .unwrap() + .to_string(), + cv.get_data(DataType::Position) + .as_str() + .unwrap() + .to_string(), + ); + CommunicationValue::new(CommunicationType::AddCommunity) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) +} + +pub fn handle_get_communities(cv: &CommunicationValue) -> CommunicationValue { + let mut comm_array = Vec::new(); + for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { + let mut container: Vec<(DataType, DataValue)> = Vec::new(); + container.push(( + DataType::CommunityAddress, + DataValue::Str(c.address.clone()), + )); + container.push((DataType::CommunityTitle, DataValue::Str(c.title.clone()))); + container.push((DataType::Position, DataValue::Str(c.position.clone()))); + comm_array.push(typed_container(container)); + } + + CommunicationValue::new(CommunicationType::GetCommunities) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) + .add_typed_default(DataType::Communities, DataValue::Array(comm_array)) +} + +pub fn handle_remove_community(cv: &CommunicationValue) -> CommunicationValue { + CommunitiesUtil::remove_community( + cv.get_sender() as i64, + cv.get_data(DataType::CommunityAddress) + .as_str() + .unwrap() + .to_string(), + ); + CommunicationValue::new(CommunicationType::RemoveCommunity) + .with_id(cv.get_id()) + .with_receiver(cv.get_sender()) +} + +pub fn handle_global_settings_save(cv: &CommunicationValue) -> CommunicationValue { + let my_id = cv.get_sender(); + let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing settings payload".to_string()), + ); + }; + + if settings::save_global(my_id as i64, settings_value).is_err() { + return error_response(cv, CommunicationType::ErrorInvalidData); + } + + let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave) + .with_receiver(my_id) + .with_id(cv.get_id()); + + if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { + response = response.add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + response +} + +pub fn handle_global_settings_load(cv: &CommunicationValue) -> CommunicationValue { + let my_id = cv.get_sender(); + let Ok(settings_value) = settings::load_global(my_id as i64) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(settings_value_str) = settings_value else { + let mut response = CommunicationValue::new(CommunicationType::ErrorNotFound) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Path, + DataValue::Str("global.settings".to_string()), + ); + + if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { + response = response.add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + return response; + }; + + let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsLoad) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)); + + if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { + response = response.add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + response +} + +pub fn handle_settings_save( + cv: &CommunicationValue, + _expected_session_id: i128, +) -> CommunicationValue { + let my_id = cv.get_sender(); + let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing session_id".to_string()), + ); + }; + if session_id == 0 || session_id > 1_000_000 { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Invalid session_id".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + }; + let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing settings_name".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + }; + let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing settings payload".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + }; + + if !settings_name + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') + || settings_name.contains("..") + { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Invalid settings_name".to_string()), + ) + .add_typed_default( + DataType::SettingsName, + DataValue::Str(settings_name.to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + if settings::save( + my_id as i64, + session_id as i64, + settings_name, + settings_value, + ) + .is_err() + { + return error_response(cv, CommunicationType::ErrorInvalidData); + } + + CommunicationValue::new(CommunicationType::SettingsSave) + .with_receiver(my_id) + .with_id(cv.get_id()) + .add_typed_default( + DataType::SettingsName, + DataValue::Str(settings_name.to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ) +} + +pub fn handle_settings_load( + cv: &CommunicationValue, + _expected_session_id: i128, +) -> CommunicationValue { + let my_id = cv.get_sender(); + let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing session_id".to_string()), + ); + }; + if session_id == 0 || session_id > 1_000_000 { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Invalid session_id".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing settings_name".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + }; + + if !settings_name + .chars() + .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') + || settings_name.contains("..") + { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Invalid settings_name".to_string()), + ) + .add_typed_default( + DataType::SettingsName, + DataValue::Str(settings_name.to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + let Ok(settings_value) = settings::load(my_id as i64, session_id as i64, settings_name) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let Some(settings_value_str) = settings_value else { + return CommunicationValue::new(CommunicationType::ErrorNotFound) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::SettingsName, + DataValue::Str(settings_name.to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + }; + + CommunicationValue::new(CommunicationType::SettingsLoad) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)) + .add_typed_default( + DataType::SettingsName, + DataValue::Str(settings_name.to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ) +} + +pub fn handle_settings_list( + cv: &CommunicationValue, + _expected_session_id: i128, +) -> CommunicationValue { + let my_id = cv.get_sender(); + let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Missing session_id".to_string()), + ); + }; + if session_id == 0 || session_id > 1_000_000 { + return CommunicationValue::new(CommunicationType::ErrorInvalidData) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default( + DataType::Message, + DataValue::Str("Invalid session_id".to_string()), + ) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ); + } + + let Ok(settings) = settings::list(my_id as i64, session_id as i64) else { + return error_response(cv, CommunicationType::ErrorInvalidData); + }; + let settings_json = settings.into_iter().map(DataValue::Str).collect(); + CommunicationValue::new(CommunicationType::SettingsList) + .with_id(cv.get_id()) + .with_receiver(my_id) + .add_typed_default(DataType::Settings, DataValue::Array(settings_json)) + .add_typed_default( + DataType::SessionId, + DataValue::SignedNumber(session_id as i128), + ) +} diff --git a/iota-core/src/main.rs b/iota-core/src/main.rs index 2baf39b..fd794e6 100644 --- a/iota-core/src/main.rs +++ b/iota-core/src/main.rs @@ -88,6 +88,9 @@ async fn main() { if let Err(_) = user_manager::load_users().await { log_t!("user_load_failed"); } + if let Err(e) = iota_storage::util::settings::migrate_legacy_files() { + log!("Failed to migrate legacy settings: {}", e); + } let mut sb = "".to_string(); @@ -102,7 +105,11 @@ async fn main() { } log!( "IOTA ID: {}", - CONFIG.load().iota_id.map(|id| id.to_string()).unwrap_or_else(|| "N/A".to_string()) + CONFIG + .load() + .iota_id + .map(|id| id.to_string()) + .unwrap_or_else(|| "N/A".to_string()) ); log!("User IDS: {}", sb); @@ -121,7 +128,7 @@ async fn main() { sb1 = sb1 + ","; } log!("Community IDS: {}", sb1); */ - let _port = CONFIG.load().port; + let port = CONFIG.load().port; let mut _ip = "0.0.0.0".to_string(); for iface in pnet::datalink::interfaces() { let iface: NetworkInterface = iface; @@ -133,17 +140,6 @@ async fn main() { } } } - /* Community port activation is used for activating the port for communities. - * Code is currently commented because communities have not been implemented yet. - 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", @@ -151,6 +147,9 @@ async fn main() { ) .await; } + if !web_server::start(port).await { + log!("Failed to start the MTP web server on port {}", port); + } let _ = omikron::omikron_connection::get_omikron_connection().await; log_t!("setup_completed"); @@ -162,7 +161,9 @@ async fn main() { if OMIKRON_CONNECTION.has_auth_failure().await { if let Some(reason) = OMIKRON_CONNECTION.get_auth_failure().await { log!("Authentication failed: {}", reason); - log!("Use /reconnect to try again or /regenerate private-key to create a new key pair"); + log!( + "Use /reconnect to try again or /regenerate private-key to create a new key pair" + ); OMIKRON_CONNECTION.clear_auth_failure().await; } } diff --git a/iota-storage/Cargo.toml b/iota-storage/Cargo.toml index 78359be..24d7603 100644 --- a/iota-storage/Cargo.toml +++ b/iota-storage/Cargo.toml @@ -17,8 +17,11 @@ hkdf = "0.12.4" json = "*" arc-swap = "1" once_cell = "1.21.3" +r2d2 = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_yaml = "0.9" +thiserror = "2" rand = "0.8" rand_core = { version = "0.6", features = ["getrandom", "std"] } ratatui = "0.30.0" diff --git a/iota-storage/src/lib.rs b/iota-storage/src/lib.rs index 7f0e8ef..a0e5061 100644 --- a/iota-storage/src/lib.rs +++ b/iota-storage/src/lib.rs @@ -1,2 +1,3 @@ +pub mod storage_error; pub mod users; pub mod util; diff --git a/iota-storage/src/storage_error.rs b/iota-storage/src/storage_error.rs new file mode 100644 index 0000000..100bf08 --- /dev/null +++ b/iota-storage/src/storage_error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StorageError { + #[error("Database error: {0}")] + Db(#[from] rusqlite::Error), + #[error("Connection pool error: {0}")] + Pool(String), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("{0}")] + Other(String), +} diff --git a/iota-storage/src/users/contact.rs b/iota-storage/src/users/contact.rs index 632cea3..50d224d 100644 --- a/iota-storage/src/users/contact.rs +++ b/iota-storage/src/users/contact.rs @@ -1,4 +1,3 @@ -use json::{self, JsonValue, number::Number}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, Clone)] @@ -33,29 +32,4 @@ impl Contact { 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/iota-storage/src/users/mod.rs b/iota-storage/src/users/mod.rs index aef5a02..cd4fa45 100644 --- a/iota-storage/src/users/mod.rs +++ b/iota-storage/src/users/mod.rs @@ -1,4 +1,3 @@ pub mod contact; -pub mod user_community_util; pub mod user_manager; pub mod user_profile; diff --git a/iota-storage/src/users/user_community_util.rs b/iota-storage/src/users/user_community_util.rs deleted file mode 100644 index 89e0fb6..0000000 --- a/iota-storage/src/users/user_community_util.rs +++ /dev/null @@ -1,58 +0,0 @@ -use iota_util::file_util::{load_file, save_file}; -use json::{self, Array, JsonValue}; - -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, "communities.json"); - - 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, "communities.json"); - - 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/{}/", storage_owner); - Self::load_array(&file_path, "communities.json") - } - - fn load_array(dir: &str, name: &str) -> Array { - let content = load_file(dir, name); - if content.is_empty() { - return Array::new(); - } - - let parsed = json::parse(&content); - match parsed { - Ok(JsonValue::Array(arr)) => arr, - _ => Array::new(), - } - } -} diff --git a/iota-storage/src/users/user_manager.rs b/iota-storage/src/users/user_manager.rs index bf1dc00..67c4e6a 100644 --- a/iota-storage/src/users/user_manager.rs +++ b/iota-storage/src/users/user_manager.rs @@ -1,15 +1,214 @@ use crate::users::user_profile::UserProfile; +use crate::util::db; use base64::{Engine as _, engine::general_purpose::STANDARD}; use iota_util::crypto_helper::{self, hex_hash, keyring_from_base64, public_key_bundle_to_base64}; use iota_util::file_util::{load_file, save_file}; -use json::JsonValue; -use once_cell::sync::Lazy; use rand_core::{OsRng, RngCore}; -use std::io::{self}; -use std::sync::Mutex; +use rusqlite::params; -static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); -static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); +pub fn add_user(user: UserProfile) { + if let Err(e) = db::with_db(|conn| { + conn.execute( + r#" + INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT(user_id) DO UPDATE SET + username = excluded.username, + public_key = excluded.public_key, + private_key_hash = excluded.private_key_hash, + reset_token = excluded.reset_token, + display_name = excluded.display_name + "#, + params![ + user.user_id, + user.username, + user.public_key, + user.private_key_hash, + user.reset_token, + user.created_at, + user.display_name, + ], + )?; + + for (app_id, app_secret) in &user.trusted_apps { + conn.execute( + r#" + INSERT OR REPLACE INTO trusted_apps (user_id, app_id, app_secret) + VALUES (?1, ?2, ?3) + "#, + params![user.user_id, app_id, app_secret], + )?; + } + Ok(()) + }) { + eprintln!("Failed to add_user: {}", e); + } +} + +pub fn update_user(user: UserProfile) { + add_user(user); +} + +pub fn get_user_by_username(username: &str) -> Option { + match db::with_db(|conn| { + match conn.query_row( + "SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE username = ?1 LIMIT 1", + params![username], + |r| { + let user_id: i64 = r.get(0)?; + Ok(UserProfile { + user_id, + username: r.get(1)?, + display_name: r.get(6)?, + public_key: r.get(2)?, + private_key_hash: r.get(3)?, + created_at: r.get(5)?, + reset_token: r.get(4)?, + trusted_apps: load_trusted_apps(user_id), + }) + }, + ) { + Ok(user) => Ok(Some(user)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + }) { + Ok(opt) => opt, + Err(e) => { + eprintln!("Error querying user by username: {}", e); + None + } + } +} + +pub fn get_user(user_id: i64) -> Option { + match db::with_db(|conn| { + match conn.query_row( + "SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name FROM users WHERE user_id = ?1 LIMIT 1", + params![user_id], + |r| { + let user_id: i64 = r.get(0)?; + Ok(UserProfile { + user_id, + username: r.get(1)?, + display_name: r.get(6)?, + public_key: r.get(2)?, + private_key_hash: r.get(3)?, + created_at: r.get(5)?, + reset_token: r.get(4)?, + trusted_apps: load_trusted_apps(user_id), + }) + }, + ) { + Ok(user) => Ok(Some(user)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + }) { + Ok(opt) => opt, + Err(e) => { + eprintln!("Error querying user: {}", e); + None + } + } +} + +pub fn get_users() -> Vec { + match db::with_db(|conn| { + let mut stmt = conn.prepare( + r#" + SELECT user_id, username, public_key, private_key_hash, reset_token, created_at, display_name + FROM users + ORDER BY username + "#, + )?; + + let rows = stmt.query_map([], |r| { + let user_id: i64 = r.get(0)?; + let username: String = r.get(1)?; + let public_key: String = r.get(2)?; + let private_key_hash: String = r.get(3)?; + let reset_token: String = r.get(4)?; + let created_at: i64 = r.get(5)?; + let display_name: Option = r.get(6)?; + + Ok(UserProfile { + user_id, + username, + display_name, + public_key, + private_key_hash, + created_at, + reset_token, + trusted_apps: std::collections::HashMap::new(), + }) + })?; + + let mut out = Vec::new(); + for row in rows { + match row { + Ok(mut user) => { + user.trusted_apps = load_trusted_apps(user.user_id); + out.push(user); + } + Err(e) => eprintln!("Failed to read user row: {}", e), + } + } + Ok(out) + }) { + Ok(v) => v, + Err(e) => { + eprintln!("Failed to query users: {}", e); + Vec::new() + } + } +} + +fn load_trusted_apps(user_id: i64) -> std::collections::HashMap { + match db::with_db(|conn| { + let mut stmt = + conn.prepare("SELECT app_id, app_secret FROM trusted_apps WHERE user_id = ?1")?; + let rows = stmt.query_map(params![user_id], |r| { + Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)) + })?; + + let mut map = std::collections::HashMap::new(); + for row in rows { + if let Ok((k, v)) = row { + map.insert(k, v); + } + } + Ok(map) + }) { + Ok(m) => m, + Err(e) => { + eprintln!("Failed to load trusted apps: {}", e); + std::collections::HashMap::new() + } + } +} + +pub fn remove_user(user_id: i64) { + if let Err(e) = db::with_db(|conn| { + conn.execute( + "DELETE FROM trusted_apps WHERE user_id = ?1", + params![user_id], + )?; + conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; + Ok(()) + }) { + eprintln!("Failed to remove_user: {}", e); + } +} + +pub fn clear() { + if let Err(e) = db::with_db(|conn| { + conn.execute_batch("DELETE FROM trusted_apps; DELETE FROM users;")?; + Ok(()) + }) { + eprintln!("Failed to clear users: {}", e); + } +} #[allow(dead_code)] pub async fn load_from_tu(username: &str) -> Result<(), ()> { @@ -35,91 +234,38 @@ pub async fn load_from_tu(username: &str) -> Result<(), ()> { hex_hash(&keyring_b64), reset_token, ); - USERS.lock().unwrap().push(user_profile); + add_user(user_profile); Ok(()) } -pub fn add_user(user: UserProfile) { - USERS.lock().unwrap().push(user); -} - -pub fn update_user(user: UserProfile) { - let mut users = USERS.lock().unwrap(); - if let Some(pos) = users.iter().position(|u| u.user_id == user.user_id) { - users[pos] = user; - } - *UNIQUE.lock().unwrap() = true; -} -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); + // No-op: users are auto-saved via SQLite. } -pub fn clear() { - let mut users = USERS.lock().unwrap(); - users.clear(); - *UNIQUE.lock().unwrap() = true; -} - -pub async fn load_users() -> io::Result<()> { +pub async fn load_users() -> std::io::Result<()> { + // Users are loaded from SQLite on demand. This function is kept for API compat. + // If we need to migrate from a legacy users.json file, we can do so here. 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 let Ok(parsed) = json::parse(&content) { + if let json::JsonValue::Array(arr) = parsed { + for j in arr.iter() { + if let Some(up) = UserProfile::from_json(j) { + add_user(up); + } } } } - if *UNIQUE.lock().unwrap() { - save_users(); - } + // Rename the old file so we don't re-import + let _ = std::fs::rename( + std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json"), + std::path::PathBuf::from(iota_util::file_util::get_directory()).join("users.json.imported"), + ); Ok(()) } -#[allow(dead_code)] -pub fn set_unique(val: bool) { - *UNIQUE.lock().unwrap() = val; -} - pub fn save_app_data(user_id: i64, app_identifier: &str, data: &str) { let path = format!("users/{}/apps", user_id); let name = format!("{}.json", app_identifier); diff --git a/iota-storage/src/users/user_profile.rs b/iota-storage/src/users/user_profile.rs index c0ae20e..2ecb9cc 100644 --- a/iota-storage/src/users/user_profile.rs +++ b/iota-storage/src/users/user_profile.rs @@ -5,9 +5,9 @@ use iota_util::file_util::{has_file, load_file, used_dir_space}; use json::{JsonValue, object}; use rand::Rng; use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; -// --- UserProfile --- -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserProfile { pub user_id: i64, pub username: String, @@ -43,26 +43,6 @@ impl UserProfile { } } - pub fn to_json(&self) -> JsonValue { - let mut trusted_apps_obj = json::JsonValue::new_object(); - for (k, v) in &self.trusted_apps { - trusted_apps_obj[k] = v.clone().into(); - } - - 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(), - "trusted_apps" => trusted_apps_obj, - }; - 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, @@ -78,10 +58,11 @@ impl UserProfile { 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 { + + /// Legacy JSON import - used when migrating from users.json to SQLite. + pub 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(); @@ -99,7 +80,7 @@ impl UserProfile { } } - let up = UserProfile { + Some(UserProfile { user_id, username, display_name, @@ -108,22 +89,15 @@ impl UserProfile { created_at, reset_token, trusted_apps, - }; + }) + } - // 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); - } - } */ + pub fn from_yaml(s: &str) -> Result { + serde_yaml::from_str(s) + } - Some(up) + pub fn to_yaml(&self) -> Result { + serde_yaml::to_string(self) } #[allow(dead_code)] diff --git a/iota-storage/src/util/chat_files.rs b/iota-storage/src/util/chat_files.rs index a5b50fe..b09d750 100644 --- a/iota-storage/src/util/chat_files.rs +++ b/iota-storage/src/util/chat_files.rs @@ -1,9 +1,7 @@ +use crate::storage_error::StorageError; use crate::util::db; use iota_logger::log; -use json::{JsonValue, array, object}; use rusqlite::params; -use std::io; -use std::sync::{Arc, LazyLock, Mutex}; #[derive(PartialEq, Debug, Clone)] pub enum MessageState { @@ -45,11 +43,327 @@ impl MessageState { } } -// 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") -}); +#[derive(Debug, Clone)] +pub struct StoredMessage { + pub id: i64, + pub message_time: i64, + pub content: String, + pub edited: bool, + pub sent_by_self: bool, + pub message_state: String, + pub height: i64, + pub reply_to: Option, + pub reactions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StoredReaction { + pub reaction: String, + pub user_id: i64, +} + +/* + * Each edit is recorded in message_edits with the before/after content and a + * timestamp. Only the original sender (sent_by_self = 1) may edit. + */ +pub fn edit_message( + storage_owner: i64, + external_user: i64, + message_time: i64, + editor_id: i64, + new_content: &str, +) -> Result<(), StorageError> { + update_message_content( + storage_owner, + external_user, + message_time, + editor_id, + new_content, + true, + ) +} + +/* Applies an edit received from the message sender to the recipient's copy. */ +pub fn apply_remote_edit( + storage_owner: i64, + external_user: i64, + message_time: i64, + editor_id: i64, + new_content: &str, +) -> Result<(), StorageError> { + if editor_id != external_user { + return Err(StorageError::Other( + "Remote editor does not match chat partner".into(), + )); + } + + update_message_content( + storage_owner, + external_user, + message_time, + editor_id, + new_content, + false, + ) +} + +fn update_message_content( + storage_owner: i64, + external_user: i64, + message_time: i64, + editor_id: i64, + new_content: &str, + require_sent_by_self: bool, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let msg = conn.query_row( + r#" + SELECT id, content, sent_by_self + 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, message_time], + |row| { + Ok(( + row.get::<_, i64>(0)?, + row.get::<_, String>(1)?, + row.get::<_, i64>(2)?, + )) + }, + )?; + + let (msg_id, old_content, sent_by_self) = msg; + if require_sent_by_self && sent_by_self != 1 { + return Err(StorageError::Other( + "Only the original sender can edit this message".into(), + )); + } + if !require_sent_by_self && sent_by_self != 0 { + return Err(StorageError::Other( + "Remote edits may only update received messages".into(), + )); + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + conn.execute( + r#" + INSERT INTO message_edits (message_id, content_before, content_after, edited_at, edited_by) + VALUES (?1, ?2, ?3, ?4, ?5) + "#, + params![msg_id, old_content, new_content, now, editor_id], + )?; + + conn.execute( + r#" + UPDATE messages + SET content = ?1, edited_count = edited_count + 1 + WHERE id = ?2 + "#, + params![new_content, msg_id], + )?; + + Ok(()) + }) +} + +pub fn hard_delete_message( + storage_owner: i64, + external_user: i64, + message_time: i64, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let msg_id: i64 = conn.query_row( + r#" + SELECT id 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, message_time], + |row| row.get(0), + )?; + + conn.execute( + "DELETE FROM message_edits WHERE message_id = ?1", + params![msg_id], + )?; + conn.execute( + "DELETE FROM reactions WHERE message_id = ?1", + params![msg_id], + )?; + conn.execute("DELETE FROM messages WHERE id = ?1", params![msg_id])?; + Ok(()) + }) +} + +/* Deletes a message from the sender's local copy after checking ownership. */ +pub fn delete_message( + storage_owner: i64, + external_user: i64, + message_time: i64, +) -> Result<(), StorageError> { + ensure_message_direction(storage_owner, external_user, message_time, true)?; + hard_delete_message(storage_owner, external_user, message_time) +} + +/* Flags the recipient's local copy after validating its sender, preserving its history. */ +pub fn apply_remote_delete( + storage_owner: i64, + external_user: i64, + message_time: i64, + sender_id: i64, +) -> Result<(), StorageError> { + if sender_id != external_user { + return Err(StorageError::Other( + "Remote sender does not match chat partner".into(), + )); + } + ensure_message_direction(storage_owner, external_user, message_time, false)?; + flag_deleted_by_external(storage_owner, external_user, message_time) +} + +fn ensure_message_direction( + storage_owner: i64, + external_user: i64, + message_time: i64, + expected_sent_by_self: bool, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let sent_by_self: i64 = conn.query_row( + r#" + SELECT sent_by_self 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, message_time], + |row| row.get(0), + )?; + if (sent_by_self != 0) != expected_sent_by_self { + return Err(StorageError::Other( + "Message sender is not authorized".into(), + )); + } + Ok(()) + }) +} + +/* + * Marks a message as deleted by the external user rather than removing the row, + * so the storage owner still sees a tombstone in the UI. + */ +pub fn flag_deleted_by_external( + storage_owner: i64, + external_user: i64, + message_time: i64, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let affected = conn.execute( + r#" + UPDATE messages + SET deleted_by_external = 1 + WHERE storage_owner = ?1 AND external_user = ?2 AND message_time = ?3 + "#, + params![storage_owner, external_user, message_time], + )?; + if affected == 0 { + return Err(StorageError::Other("Message not found".into())); + } + Ok(()) + }) +} + +/* + * Removes the edit trail but keeps the message with edited_count > 0 so + * the UI still shows the "edited" indicator. Only the own user should + * call this. + */ +pub fn delete_edit_history( + storage_owner: i64, + external_user: i64, + message_time: i64, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let msg_id: i64 = conn.query_row( + r#" + SELECT id 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, message_time], + |row| row.get(0), + )?; + + conn.execute( + "DELETE FROM message_edits WHERE message_id = ?1", + params![msg_id], + )?; + Ok(()) + }) +} + +pub fn add_reaction( + storage_owner: i64, + external_user: i64, + message_time: i64, + user_id: i64, + reaction: &str, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let msg_id: i64 = conn.query_row( + r#" + SELECT id 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, message_time], + |row| row.get(0), + )?; + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as i64; + + conn.execute( + r#" + INSERT OR IGNORE INTO reactions (message_id, user_id, reaction, created_at) + VALUES (?1, ?2, ?3, ?4) + "#, + params![msg_id, user_id, reaction, now], + )?; + Ok(()) + }) +} + +pub fn remove_reaction( + storage_owner: i64, + external_user: i64, + message_time: i64, + user_id: i64, + reaction: &str, +) -> Result<(), StorageError> { + db::with_db(|conn| { + let msg_id: i64 = conn.query_row( + r#" + SELECT id 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, message_time], + |row| row.get(0), + )?; + + conn.execute( + "DELETE FROM reactions WHERE message_id = ?1 AND user_id = ?2 AND reaction = ?3", + params![msg_id, user_id, reaction], + )?; + Ok(()) + }) +} pub fn add_message( send_time: u128, @@ -68,19 +382,12 @@ pub fn add_message( } }; - // Insert the message into the DB - let insert_result = db::with_conn(&MESSAGES_DB, |conn| { + if let Err(e) = db::with_db(|conn| { conn.execute( r#" INSERT INTO messages ( - storage_owner, - external_user, - message_time, - content, - sent_by_self, - message_state, - height, - reply_to + storage_owner, external_user, message_time, content, + sent_by_self, message_state, height, reply_to ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) "#, params![ @@ -99,18 +406,13 @@ pub fn add_message( ], )?; 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); } @@ -119,25 +421,21 @@ pub fn change_message_state( 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| { +) -> std::io::Result<()> { + db::with_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 + 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), + Err(e) => return Err(e.into()), }; let Some(current_state_raw) = current else { @@ -154,24 +452,58 @@ pub fn change_message_state( 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 + 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(()) - }); + }) + .map_err(|e: StorageError| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) +} - match res { - Ok(_) => Ok(()), - Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), +fn load_reactions( + conn: &rusqlite::Connection, + msg_ids: &[i64], +) -> std::collections::HashMap> { + if msg_ids.is_empty() { + return std::collections::HashMap::new(); } + + let placeholders: Vec = msg_ids + .iter() + .enumerate() + .map(|(i, _)| format!("?{}", i + 1)) + .collect(); + let query = format!( + "SELECT message_id, reaction, user_id FROM reactions WHERE message_id IN ({}) ORDER BY created_at ASC, id ASC", + placeholders.join(", ") + ); + + let mut map: std::collections::HashMap> = + std::collections::HashMap::new(); + if let Ok(mut stmt) = conn.prepare(&query) { + let params: Vec<&dyn rusqlite::types::ToSql> = msg_ids + .iter() + .map(|id| id as &dyn rusqlite::types::ToSql) + .collect(); + if let Ok(rows) = stmt.query_map(params.as_slice(), |row| { + Ok(( + row.get::<_, i64>(0)?, + StoredReaction { + reaction: row.get(1)?, + user_id: row.get(2)?, + }, + )) + }) { + for row in rows.flatten() { + map.entry(row.0).or_default().push(row.1); + } + } + } + map } pub fn get_messages( @@ -179,26 +511,17 @@ pub fn get_messages( external_user: i64, loaded_messages: i64, amount: i64, -) -> JsonValue { - let messages = array![]; - +) -> Vec { if amount <= 0 || loaded_messages < 0 { - return messages; + return Vec::new(); } - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + match db::with_db(|conn| { let mut stmt = conn.prepare( r#" - SELECT - message_time, - content, - sent_by_self, - message_state, - height, - reply_to + SELECT id, message_time, content, sent_by_self, message_state, height, reply_to, edited_count FROM messages - WHERE storage_owner = ?1 - AND external_user = ?2 + WHERE storage_owner = ?1 AND external_user = ?2 AND deleted_by_external = 0 ORDER BY message_time DESC, id DESC LIMIT ?3 OFFSET ?4 "#, @@ -207,49 +530,40 @@ pub fn get_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); - let reply_to: Option = row.get(5).ok().flatten(); - Ok((message_time, content, sent_by_self, message_state, height, reply_to)) + Ok(StoredMessage { + id: row.get(0)?, + message_time: row.get(1)?, + content: row.get(2)?, + sent_by_self: row.get::<_, i64>(3)? != 0, + message_state: row.get(4)?, + height: row.get(5).unwrap_or(0), + reply_to: row.get(6).ok().flatten(), + edited: row.get::<_, i64>(7).unwrap_or(0) > 0, + reactions: Vec::new(), + }) }, )?; - let mut out = array![]; + let mut out = Vec::new(); for row in rows { match row { - Ok((message_time, content, sent_by_self, message_state, height, reply_to)) => { - let mut msg = object! { - "message_time" => message_time, - "content" => content, - "sent_by_self" => (sent_by_self != 0), - "message_state" => message_state, - "height" => height - }; - if let Some(rt) = reply_to { - let _ = msg.insert("reply_to", rt); - } - 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(msg) => out.push(msg), + Err(e) => log!("Failed to read row from sqlite: {}", e), } } - Ok(out) - }); - match res { + let msg_ids: Vec = out.iter().map(|m| m.id).collect(); + let reaction_map = load_reactions(conn, &msg_ids); + for msg in &mut out { + msg.reactions = reaction_map.get(&msg.id).cloned().unwrap_or_default(); + } + + Ok(out) + }) { Ok(v) => v, Err(e) => { log!("Failed to query messages: {}", e); - messages + Vec::new() } } } diff --git a/iota-storage/src/util/chats_util.rs b/iota-storage/src/util/chats_util.rs index b8e0ad2..eb8f069 100644 --- a/iota-storage/src/util/chats_util.rs +++ b/iota-storage/src/util/chats_util.rs @@ -1,24 +1,13 @@ 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| { + if let Err(e) = db::with_db(|conn| { conn.execute( r#" - INSERT INTO contacts ( - storage_owner, - user_id, - user_name, - last_message_at - ) VALUES (?1, ?2, ?3, ?4) + 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 @@ -27,7 +16,7 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) { storage_owner, contact.user_id, contact.user_name.clone(), - contact.last_message_at + contact.last_message_at, ], )?; Ok(()) @@ -36,9 +25,8 @@ pub fn mod_user(storage_owner: i64, contact: &Contact) { } } -/// 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 db::with_db(|conn| { match conn.query_row( r#" SELECT user_id, user_name, last_message_at @@ -48,23 +36,18 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option { "#, 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, + user_id: r.get(0)?, + user_name: r.get(1)?, + last_message_at: r.get(2)?, }) }, ) { Ok(c) => Ok(Some(c)), Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), - Err(e) => Err(e), + Err(e) => Err(e.into()), } - }); - - match res { + }) { Ok(opt) => opt, Err(e) => { eprintln!("Error querying user in get_user: {}", e); @@ -73,11 +56,8 @@ pub fn get_user(storage_owner: i64, user_id: i64) -> Option { } } -/// 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| { + match db::with_db(|conn| { let mut stmt = conn.prepare( r#" SELECT user_id, user_name, last_message_at @@ -91,13 +71,10 @@ pub fn get_users(storage_owner: i64) -> Vec { )?; 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, + user_id: r.get(0)?, + user_name: r.get(1)?, + last_message_at: r.get(2)?, }) })?; @@ -109,13 +86,11 @@ pub fn get_users(storage_owner: i64) -> Vec { } } Ok(out) - }); - - match res { + }) { Ok(v) => v, Err(e) => { eprintln!("Failed to query contacts in get_users: {}", e); - contacts_out + Vec::new() } } } diff --git a/iota-storage/src/util/communities_util.rs b/iota-storage/src/util/communities_util.rs index ed8b7ae..c560056 100644 --- a/iota-storage/src/util/communities_util.rs +++ b/iota-storage/src/util/communities_util.rs @@ -1,25 +1,22 @@ 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") -}); +#[derive(Debug, Clone)] +pub struct StoredCommunity { + pub address: String, + pub title: String, + pub position: String, +} 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| { + if let Err(e) = db::with_db(|conn| { conn.execute( r#" - INSERT INTO communities ( - storage_owner, - address, - title, - position - ) VALUES (?1, ?2, ?3, ?4) + 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 @@ -33,7 +30,7 @@ impl CommunitiesUtil { } pub fn remove_community(storage_owner: i64, community_address: String) { - if let Err(e) = db::with_conn(&MESSAGES_DB, |conn| { + if let Err(e) = db::with_db(|conn| { conn.execute( "DELETE FROM communities WHERE storage_owner = ?1 AND address = ?2", params![storage_owner, community_address], @@ -44,10 +41,8 @@ impl CommunitiesUtil { } } - pub fn get_communities(storage_owner: i64) -> Array { - let communities_out = Array::new(); - - let res: Result = db::with_conn(&MESSAGES_DB, |conn| { + pub fn get_communities(storage_owner: i64) -> Vec { + match db::with_db(|conn| { let mut stmt = conn.prepare( r#" SELECT address, title, position @@ -57,33 +52,26 @@ impl CommunitiesUtil { )?; 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)) + Ok(StoredCommunity { + address: r.get(0)?, + title: r.get(1)?, + position: r.get(2)?, + }) })?; - let mut out = Array::new(); + let mut out = Vec::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); - } + Ok(community) => out.push(community), Err(e) => eprintln!("Failed to read community row: {}", e), } } Ok(out) - }); - - match res { - Ok(arr) => arr, + }) { + Ok(v) => v, Err(e) => { eprintln!("Failed to query communities in get_communities: {}", e); - communities_out + Vec::new() } } } diff --git a/iota-storage/src/util/config_util.rs b/iota-storage/src/util/config_util.rs index 450ffb4..82fbbaa 100644 --- a/iota-storage/src/util/config_util.rs +++ b/iota-storage/src/util/config_util.rs @@ -51,17 +51,17 @@ impl Default for IotaConfig { } pub fn load_config() { - let s = load_file("", "config.json"); + let s = load_file("", "config.yaml"); if s.is_empty() { return; } - match serde_json::from_str::(&s) { + match serde_yaml::from_str::(&s) { Ok(parsed) => { CONFIG.store(Arc::new(parsed)); } Err(e) => { - eprintln!("Failed to parse config.json: {}. Content: '{}'", e, s); + eprintln!("Failed to parse config.yaml: {}. Content: '{}'", e, s); } } } @@ -72,8 +72,8 @@ pub fn clear_config() { } pub fn save_config() { - if let Ok(json) = serde_json::to_string(&**CONFIG.load()) { - save_file("", "config.json", &json); + if let Ok(yaml) = serde_yaml::to_string(&**CONFIG.load()) { + save_file("", "config.yaml", &yaml); } } diff --git a/iota-storage/src/util/db.rs b/iota-storage/src/util/db.rs index c39a4bf..dfe9d2d 100644 --- a/iota-storage/src/util/db.rs +++ b/iota-storage/src/util/db.rs @@ -1,88 +1,247 @@ -//! 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 iota_util::file_util::get_directory; -use rusqlite::{Connection, Error as RusqliteError}; +use once_cell::sync::Lazy; +use r2d2::ManageConnection; +use rusqlite::Connection; use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; 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 { +use crate::storage_error::StorageError; + +const DB_NAME: &str = "messages"; + +/// A simple r2d2 manager for rusqlite connections. +pub struct SqliteManager; + +impl ManageConnection for SqliteManager { + type Connection = Connection; + type Error = rusqlite::Error; + + fn connect(&self) -> Result { + let path = db_file_path(DB_NAME); + let conn = Connection::open(path)?; + conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?; + conn.busy_timeout(Duration::from_millis(250))?; + Ok(conn) + } + + fn is_valid(&self, conn: &mut Connection) -> Result<(), rusqlite::Error> { + conn.execute_batch("SELECT 1") + } + + fn has_broken(&self, _conn: &mut Connection) -> bool { + false + } +} + +static POOL: Lazy>> = Lazy::new(|| { + let manager = SqliteManager; + let pool = r2d2::Pool::builder() + .max_size(8) + .build(manager) + .expect("Failed to create database connection pool"); + run_migrations(&pool).expect("Failed to run database migrations"); + Arc::new(pool) +}); + +pub fn pool() -> Arc> { + POOL.clone() +} + +pub fn with_db(f: F) -> Result +where + F: FnOnce(&Connection) -> Result, +{ + let conn = POOL.get().map_err(|e| StorageError::Pool(e.to_string()))?; + f(&conn) +} + +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 { +fn run_migrations(pool: &r2d2::Pool) -> Result<(), StorageError> { + let conn = pool.get().map_err(|e| StorageError::Pool(e.to_string()))?; + run_migrations_on_connection(&conn) +} + +/* + * Older builds could apply a schema change without advancing user_version. + * Check each added column so those databases can resume upgrading. + */ +fn add_column_if_missing( + conn: &Connection, + column: &str, + definition: &str, +) -> Result<(), StorageError> { + let mut statement = + conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; + let exists = statement.exists([column])?; + + if !exists { + conn.execute_batch(&format!("ALTER TABLE messages ADD COLUMN {definition};"))?; + } + + Ok(()) +} + +fn run_migrations_on_connection(conn: &Connection) -> Result<(), StorageError> { + let current_version: i64 = conn + .pragma_query_value(None, "user_version", |r| r.get(0)) + .unwrap_or(0); + + if current_version < 1 { + conn.execute_batch( + r#" + 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); + + 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); + + CREATE TABLE IF NOT EXISTS users ( + user_id INTEGER PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + public_key TEXT NOT NULL, + private_key_hash TEXT NOT NULL, + reset_token TEXT NOT NULL, + created_at INTEGER NOT NULL, + display_name TEXT + ); + + CREATE TABLE IF NOT EXISTS trusted_apps ( + user_id INTEGER NOT NULL, + app_id TEXT NOT NULL, + app_secret TEXT NOT NULL, + PRIMARY KEY (user_id, app_id) + ); + + PRAGMA user_version = 1; + "#, + )?; + } + + if current_version < 2 { + add_column_if_missing(conn, "height", "height INTEGER NOT NULL DEFAULT 0")?; + conn.execute_batch("PRAGMA user_version = 2;")?; + } + + if current_version < 3 { + add_column_if_missing(conn, "reply_to", "reply_to INTEGER")?; + conn.execute_batch("PRAGMA user_version = 3;")?; + } + + if current_version < 4 { + add_column_if_missing( + conn, + "edited_count", + "edited_count INTEGER NOT NULL DEFAULT 0", + )?; + add_column_if_missing( + conn, + "deleted_by_external", + "deleted_by_external INTEGER NOT NULL DEFAULT 0", + )?; + conn.execute_batch( + r#" + CREATE TABLE IF NOT EXISTS message_edits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL REFERENCES messages(id), + content_before TEXT NOT NULL, + content_after TEXT NOT NULL, + edited_at INTEGER NOT NULL, + edited_by INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_message_edits_msg + ON message_edits (message_id, edited_at DESC); + + CREATE TABLE IF NOT EXISTS reactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL REFERENCES messages(id), + user_id INTEGER NOT NULL, + reaction TEXT NOT NULL, + created_at INTEGER NOT NULL, + UNIQUE(message_id, user_id, reaction) + ); + CREATE INDEX IF NOT EXISTS idx_reactions_msg + ON reactions (message_id, reaction); + + PRAGMA user_version = 4; + "#, + )?; + } + + if current_version < 5 { + conn.execute_batch( + r#" + CREATE TABLE IF NOT EXISTS settings ( + user_id INTEGER NOT NULL, + session_id INTEGER NOT NULL, + name TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (user_id, session_id, name) + ); + CREATE INDEX IF NOT EXISTS idx_settings_lookup + ON settings (user_id, session_id, name); + + PRAGMA user_version = 5; + "#, + )?; + } + + Ok(()) +} + +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)), - } +) -> Result>, String> { + let path = db_file_path(db_name); + let conn = Connection::open(path).map_err(|e| e.to_string())?; + conn.execute_batch(init_sql).map_err(|e| e.to_string())?; + let _ = conn.busy_timeout(Duration::from_millis(250)); + Ok(Arc::new(std::sync::Mutex::new(conn))) } -/// 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 +pub fn with_conn(shared: &Arc>, f: F) -> Result where - F: FnOnce(&Connection) -> Result, + 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 @@ -98,97 +257,44 @@ where } } -/// 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(()) - }); - // Attempt to add the reply_to 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 reply_to INTEGER", - [], - ); - Ok(()) - }); - Ok(shared_conn) - } - Err(e) => Err(e), - } +/// Legacy - kept for e2ee_storage which uses its own DB. +pub fn create_general_messages_db() -> Result>, String> { + create_shared_connection(DB_NAME, "") } -/* -Example usage: +#[cfg(test)] +mod tests { + use super::*; -// 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") -}); + #[test] + fn resumes_migration_when_height_exists_before_its_version() -> Result<(), StorageError> { + let conn = Connection::open_in_memory()?; + conn.execute_batch( + r#" + CREATE TABLE 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 + ); + ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0; + PRAGMA user_version = 1; + "#, + )?; -// 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(...) -}); -*/ + run_migrations_on_connection(&conn)?; + + let version: i64 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?; + assert_eq!(version, 5); + for column in ["height", "reply_to", "edited_count", "deleted_by_external"] { + let mut statement = + conn.prepare("SELECT 1 FROM pragma_table_info('messages') WHERE name = ?1")?; + assert!(statement.exists([column])?); + } + + Ok(()) + } +} diff --git a/iota-storage/src/util/e2ee_storage.rs b/iota-storage/src/util/e2ee_storage.rs index d26a092..ad12661 100644 --- a/iota-storage/src/util/e2ee_storage.rs +++ b/iota-storage/src/util/e2ee_storage.rs @@ -1,5 +1,5 @@ use crate::util::db; -use rusqlite::{params, OptionalExtension}; +use rusqlite::{OptionalExtension, params}; use std::sync::{Arc, LazyLock, Mutex}; pub type StorageError = String; @@ -226,9 +226,7 @@ fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result, -) -> rusqlite::Result { +fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(PendingChatSecretForward { recipient_user_id: row.get(0)?, chat_id: row.get(1)?, diff --git a/iota-storage/src/util/mod.rs b/iota-storage/src/util/mod.rs index d5b2e4d..2fa8220 100644 --- a/iota-storage/src/util/mod.rs +++ b/iota-storage/src/util/mod.rs @@ -4,3 +4,4 @@ pub mod communities_util; pub mod config_util; pub mod db; pub mod e2ee_storage; +pub mod settings; diff --git a/iota-storage/src/util/settings.rs b/iota-storage/src/util/settings.rs new file mode 100644 index 0000000..6fae52b --- /dev/null +++ b/iota-storage/src/util/settings.rs @@ -0,0 +1,163 @@ +use crate::storage_error::StorageError; +use crate::util::db; +use iota_util::file_util::get_directory; +use rusqlite::{OptionalExtension, params}; +use std::fs; +use std::path::Path; + +pub const GLOBAL_SESSION_ID: i64 = 0; +const GLOBAL_SETTINGS_NAME: &str = "__global__"; + +pub fn save(user_id: i64, session_id: i64, name: &str, payload: &str) -> Result<(), StorageError> { + db::with_db(|conn| { + conn.execute( + "INSERT INTO settings (user_id, session_id, name, payload) VALUES (?1, ?2, ?3, ?4)\n ON CONFLICT(user_id, session_id, name) DO UPDATE SET payload = excluded.payload", + params![user_id, session_id, name, payload], + )?; + Ok(()) + }) +} + +pub fn load(user_id: i64, session_id: i64, name: &str) -> Result, StorageError> { + db::with_db(|conn| { + conn.query_row( + "SELECT payload FROM settings WHERE user_id = ?1 AND session_id = ?2 AND name = ?3", + params![user_id, session_id, name], + |row| row.get(0), + ) + .optional() + .map_err(StorageError::from) + }) +} + +pub fn list(user_id: i64, session_id: i64) -> Result, StorageError> { + db::with_db(|conn| { + let mut statement = conn.prepare( + "SELECT name FROM settings WHERE user_id = ?1 AND session_id = ?2 ORDER BY name", + )?; + let rows = statement.query_map(params![user_id, session_id], |row| row.get(0))?; + rows.collect::, _>>() + .map_err(StorageError::from) + }) +} + +pub fn save_global(user_id: i64, payload: &str) -> Result<(), StorageError> { + save(user_id, GLOBAL_SESSION_ID, GLOBAL_SETTINGS_NAME, payload) +} + +pub fn load_global(user_id: i64) -> Result, StorageError> { + load(user_id, GLOBAL_SESSION_ID, GLOBAL_SETTINGS_NAME) +} + +pub fn migrate_legacy_files() -> Result<(), StorageError> { + let users_dir = Path::new(&get_directory()).join("users"); + let Ok(users) = fs::read_dir(users_dir) else { + return Ok(()); + }; + + for user_entry in users { + let user_entry = user_entry?; + let Ok(user_id) = user_entry.file_name().to_string_lossy().parse::() else { + continue; + }; + let user_dir = user_entry.path(); + + migrate_file_if_missing( + user_id, + GLOBAL_SESSION_ID, + GLOBAL_SETTINGS_NAME, + &user_dir.join("global.settings"), + )?; + + let settings_dir = user_dir.join("settings"); + let Ok(settings_entries) = fs::read_dir(settings_dir) else { + continue; + }; + for settings_entry in settings_entries { + let settings_entry = settings_entry?; + let path = settings_entry.path(); + if path.is_file() { + if let Some(name) = setting_name(&path) { + migrate_file_if_missing(user_id, GLOBAL_SESSION_ID, &name, &path)?; + } + continue; + } + + let Ok(session_id) = settings_entry.file_name().to_string_lossy().parse::() else { + continue; + }; + let Ok(device_settings) = fs::read_dir(path) else { + continue; + }; + for setting_entry in device_settings { + let setting_entry = setting_entry?; + let path = setting_entry.path(); + if let Some(name) = setting_name(&path) { + migrate_file_if_missing(user_id, session_id, &name, &path)?; + } + } + } + } + + Ok(()) +} + +fn setting_name(path: &Path) -> Option { + (path.extension()?.to_str()? == "settings").then(|| { + path.file_stem() + .and_then(|name| name.to_str()) + .unwrap_or_default() + .to_string() + }) +} + +fn migrate_file_if_missing( + user_id: i64, + session_id: i64, + name: &str, + path: &Path, +) -> Result<(), StorageError> { + if !path.is_file() || load(user_id, session_id, name)?.is_some() { + return Ok(()); + } + let payload = fs::read_to_string(path)?; + save(user_id, session_id, name, &payload) +} + +#[cfg(test)] +mod tests { + use super::*; + use rusqlite::Connection; + + #[test] + fn settings_schema_supports_user_and_session_keys() -> Result<(), StorageError> { + let conn = Connection::open_in_memory()?; + conn.execute_batch( + "CREATE TABLE settings ( + user_id INTEGER NOT NULL, + session_id INTEGER NOT NULL, + name TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY (user_id, session_id, name) + );", + )?; + + conn.execute( + "INSERT INTO settings VALUES (?1, ?2, ?3, ?4)", + params![7, 11, "theme", "dark"], + )?; + conn.execute( + "INSERT INTO settings VALUES (?1, ?2, ?3, ?4)", + params![7, 12, "theme", "light"], + )?; + + let payload: String = conn.query_row( + "SELECT payload FROM settings WHERE user_id = 7 AND session_id = 11 AND name = 'theme'", + [], + |row| row.get(0), + )?; + assert_eq!(payload, "dark"); + + Ok(()) + } +} diff --git a/iota-util/Cargo.toml b/iota-util/Cargo.toml index e88cebf..23ed0b7 100644 --- a/iota-util/Cargo.toml +++ b/iota-util/Cargo.toml @@ -4,8 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } -mtp-crypto = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["pqc"] } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ + "crypto" +] } reqwest = "0.13.2" tokio = { version = "1.50.0", features = ["full"] } diff --git a/iota-util/src/crypto_helper.rs b/iota-util/src/crypto_helper.rs index 2909c04..9375eb4 100644 --- a/iota-util/src/crypto_helper.rs +++ b/iota-util/src/crypto_helper.rs @@ -1,5 +1,5 @@ use base64::{Engine as _, engine::general_purpose::STANDARD}; -use mtp_crypto::{Keyring, PublicKeyBundle}; +use mtp::crypto::{Keyring, PublicKeyBundle}; pub fn generate_keyring() -> Keyring { Keyring::generate() @@ -24,5 +24,5 @@ pub fn public_key_bundle_from_base64(s: &str) -> Option { } pub fn hex_hash(input: &str) -> String { - hex::encode(mtp_crypto::sha256(input.as_bytes())) + hex::encode(mtp::crypto::sha256(input.as_bytes())) } diff --git a/iota-util/src/crypto_util.rs b/iota-util/src/crypto_util.rs index defc41c..010ceae 100644 --- a/iota-util/src/crypto_util.rs +++ b/iota-util/src/crypto_util.rs @@ -1,5 +1,5 @@ use base64::{Engine as _, engine::general_purpose::STANDARD}; -use mtp_crypto::{EncryptionType, Keyring, PublicKeyBundle, encrypt_for, decrypt_with}; +use mtp::crypto::{EncryptionType, Keyring, PublicKeyBundle, decrypt_with, encrypt_for}; #[derive(Clone, Copy, Debug)] pub enum DataFormat { @@ -22,13 +22,8 @@ pub fn encrypt( .map_err(|e| format!("encryption error: {:?}", e)) } -pub fn decrypt( - ciphertext: &[u8], - aad: &[u8], - keyring: &Keyring, -) -> Result, String> { - decrypt_with(ciphertext, keyring, aad) - .map_err(|e| format!("decryption error: {:?}", e)) +pub fn decrypt(ciphertext: &[u8], aad: &[u8], keyring: &Keyring) -> Result, String> { + decrypt_with(ciphertext, keyring, aad).map_err(|e| format!("decryption error: {:?}", e)) } pub fn encrypt_challenge( @@ -39,12 +34,10 @@ pub fn encrypt_challenge( Ok(STANDARD.encode(&blob)) } -pub fn decrypt_challenge( - encrypted: &str, - keyring: &Keyring, -) -> Result { - let blob = - STANDARD.decode(encrypted).map_err(|e| format!("base64 decode error: {}", e))?; +pub fn decrypt_challenge(encrypted: &str, keyring: &Keyring) -> Result { + let blob = STANDARD + .decode(encrypted) + .map_err(|e| format!("base64 decode error: {}", e))?; let pt = decrypt(&blob, b"challenge", keyring)?; String::from_utf8(pt).map_err(|e| format!("utf8 decode error: {}", e)) } diff --git a/omikron-connector/Cargo.toml b/omikron-connector/Cargo.toml index caaab62..f083794 100644 --- a/omikron-connector/Cargo.toml +++ b/omikron-connector/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +iota-connection = { path = "../iota-connection" } iota-logger = { path = "../iota-logger" } iota-state = { path = "../iota-state" } iota-storage = { path = "../iota-storage" } diff --git a/omikron-connector/src/omikron_connection.rs b/omikron-connector/src/omikron_connection.rs index 19ed8da..e68a12f 100755 --- a/omikron-connector/src/omikron_connection.rs +++ b/omikron-connector/src/omikron_connection.rs @@ -1,159 +1,27 @@ use dashmap::DashMap; use iota_logger::{log, log_cv_in, log_cv_out, log_t}; use iota_state::{ACTIVE_TASKS, SHUTDOWN}; -use iota_storage::users::contact::Contact; use iota_storage::util::chat_files::{self, MessageState, change_message_state}; -use iota_storage::util::chats_util::{self, get_user, mod_user}; -use iota_storage::util::communities_util::CommunitiesUtil; use iota_storage::util::config_util::{CONFIG, modify_config}; -use iota_storage::util::e2ee_storage::{ - self, ChatSecretQuery, PendingChatSecretForward, StoredChatSecret, -}; +use iota_storage::util::e2ee_storage::{self, PendingChatSecretForward, StoredChatSecret}; use iota_util::crypto_helper::{self, keyring_from_base64}; use iota_util::crypto_util::{self}; -use iota_util::file_util::{get_children, has_file, load_file, save_file}; use mtp::client::{Client, ClientConfig, Policy, Receiver, SendMode, Sender}; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; use mtp::crypto::{Keyring, PublicKeyBundle}; -use mtp::type_map::TypeMap; -use std::collections::HashMap; use std::env; +use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, LazyLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, RwLock, mpsc, watch}; +use std::time::{Duration, Instant}; +use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch}; use tokio::task::JoinHandle; use tokio::time::sleep; use uuid::Uuid; use crate::omega_discovery; -fn typed_container(items: Vec<(DataType, DataValue)>) -> DataValue { - use mtp::type_map::{DataTypeId, TypeMap}; - let tm = TypeMap::latest(); - DataValue::Container( - items - .into_iter() - .filter_map(|(dt, dv)| tm.data_id_enum(dt).map(|id| (DataTypeId(id), dv))) - .collect(), - ) -} - -fn data_string(cv: &CommunicationValue, dt: DataType) -> Option { - cv.get_data(dt) - .as_str() - .map(|s| s.to_string()) - .or_else(|| cv.get_data(dt).as_number().map(|n| n.to_string())) - .or_else(|| cv.get_data(dt).as_signed_number().map(|n| n.to_string())) -} - -fn data_i64(cv: &CommunicationValue, dt: DataType) -> Option { - cv.get_data(dt) - .as_number() - .and_then(|n| i64::try_from(n).ok()) - .or_else(|| { - cv.get_data(dt) - .as_signed_number() - .and_then(|n| i64::try_from(n).ok()) - }) - .or_else(|| cv.get_data(dt).as_str().and_then(|s| s.parse::().ok())) -} - -#[derive(Debug, Clone)] -struct ChatSecretRecipient { - user_id: String, - encrypted_secret: Vec, - kem_ciphertext: Vec, -} - -fn recipient_from_value(value: &DataValue) -> Option { - let tm = TypeMap::latest(); - let user_id = value - .get_field(DataType::UserId.to_id(&tm))? - .as_str() - .map(|s| s.to_string()) - .or_else(|| { - value - .get_field(DataType::UserId.to_id(&tm))? - .as_number() - .map(|n| n.to_string()) - })?; - let encrypted_secret = value - .get_field(DataType::EncryptedSecret.to_id(&tm))? - .as_bytes()?; - let kem_ciphertext = value - .get_field(DataType::KemCiphertext.to_id(&tm))? - .as_bytes()?; - - Some(ChatSecretRecipient { - user_id, - encrypted_secret, - kem_ciphertext, - }) -} - -fn chat_secret_recipients(cv: &CommunicationValue) -> Option> { - let recipients = cv.get_data(DataType::Recipients).as_array()?; - let parsed = recipients - .iter() - .map(recipient_from_value) - .collect::>>()?; - - if parsed.is_empty() { - None - } else { - Some(parsed) - } -} - -fn set_chat_secret_cv_for_recipient( - source: &CommunicationValue, - recipient: &ChatSecretRecipient, -) -> CommunicationValue { - let recipient_value = typed_container(vec![ - (DataType::UserId, DataValue::Str(recipient.user_id.clone())), - ( - DataType::EncryptedSecret, - DataValue::Bytes(recipient.encrypted_secret.clone()), - ), - ( - DataType::KemCiphertext, - DataValue::Bytes(recipient.kem_ciphertext.clone()), - ), - ]); - - CommunicationValue::new(CommunicationType::SetChatSecret) - .with_id(source.get_id()) - .with_sender(source.get_sender()) - .with_receiver(recipient.user_id.parse::().unwrap_or(0)) - .add_typed_default(DataType::ChatId, source.get_data(DataType::ChatId).clone()) - .add_typed_default( - DataType::SecretId, - source.get_data(DataType::SecretId).clone(), - ) - .add_typed_default( - DataType::VersionNumber, - source.get_data(DataType::VersionNumber).clone(), - ) - .add_typed_default( - DataType::WrappingScheme, - source.get_data(DataType::WrappingScheme).clone(), - ) - .add_typed_default( - DataType::CreatedAt, - source.get_data(DataType::CreatedAt).clone(), - ) - .add_typed_default( - DataType::Recipients, - DataValue::Array(vec![recipient_value]), - ) -} - -fn now_millis_i64() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64 -} +use iota_connection::message_common::*; +use iota_connection::message_handlers; fn pending_chat_secret_forward_from_cv( cv: &CommunicationValue, @@ -212,12 +80,6 @@ fn chat_secret_forward_cv(record: &PendingChatSecretForward) -> CommunicationVal .add_typed_default(DataType::Recipients, DataValue::Array(vec![recipient])) } -fn error_response(request: &CommunicationValue, ty: CommunicationType) -> CommunicationValue { - CommunicationValue::new(ty) - .with_id(request.get_id()) - .with_receiver(request.get_sender()) -} - // Helper function to check if read receipts are enabled globally async fn is_read_receipts_enabled() -> bool { CONFIG.load().read_receipts_enabled @@ -235,13 +97,15 @@ 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); +const MAX_MISSED_PONGS: u32 = 3; +const MAX_CONCURRENT_HANDLERS: usize = 20; // ============================================================================ // Waiting Task System // ============================================================================ pub struct WaitingTask { - pub task: Box, CommunicationValue) -> bool + Send + Sync>, + pub task: Box, CommunicationValue) -> bool + Send + Sync>, pub inserted_at: Instant, } @@ -284,6 +148,7 @@ impl ConnectionState { #[allow(dead_code)] // message_send_times is unused. pub struct OmikronConnection { state: Arc>, + state_watch_tx: watch::Sender, sender: Arc>>>, connection_loop_handle: Arc>>>, pub last_ping: Arc>, @@ -292,16 +157,20 @@ pub struct OmikronConnection { shutdown_tx: Arc>>>, reconnect_on_close: Arc>, auth_failure: Arc>>, - pub app_challenges: Arc>>, - pub app_sessions: Arc>>, + pub app_challenges: Arc>, + pub app_sessions: Arc>, + pub(crate) missed_pongs: Arc, + handler_semaphore: Arc, } impl OmikronConnection { pub fn new() -> Self { let (shutdown_tx, _) = watch::channel(false); + let (state_watch_tx, _) = watch::channel(ConnectionState::Disconnected); OmikronConnection { state: Arc::new(RwLock::new(ConnectionState::Disconnected)), + state_watch_tx, sender: Arc::new(RwLock::new(None)), connection_loop_handle: Arc::new(Mutex::new(None)), last_ping: Arc::new(Mutex::new(-1)), @@ -310,11 +179,18 @@ impl OmikronConnection { shutdown_tx: Arc::new(Mutex::new(Some(shutdown_tx))), reconnect_on_close: Arc::new(RwLock::new(true)), auth_failure: Arc::new(RwLock::new(None)), - app_challenges: Arc::new(RwLock::new(HashMap::new())), - app_sessions: Arc::new(RwLock::new(HashMap::new())), + app_challenges: Arc::new(DashMap::new()), + app_sessions: Arc::new(DashMap::new()), + missed_pongs: Arc::new(AtomicU32::new(0)), + handler_semaphore: Arc::new(Semaphore::new(MAX_CONCURRENT_HANDLERS)), } } + async fn set_state(&self, new_state: ConnectionState) { + *self.state.write().await = new_state; + let _ = self.state_watch_tx.send(new_state); + } + // ------------------------------------------------------------------------- // Connection Management // ------------------------------------------------------------------------- @@ -364,7 +240,7 @@ impl OmikronConnection { sender.close(); } - *self.state.write().await = ConnectionState::Disconnected; + self.set_state(ConnectionState::Disconnected).await; *self.sender.write().await = None; } @@ -418,7 +294,7 @@ impl OmikronConnection { } async fn connect_once(self: Arc) -> Result<(), String> { - *self.state.write().await = ConnectionState::Connecting; + self.set_state(ConnectionState::Connecting).await; log_t!("omikron_connecting"); let keyring = self.load_or_migrate_keyring().await; @@ -437,6 +313,7 @@ impl OmikronConnection { .with_policy(Policy { send_mode: SendMode::SingleStreamPerMessage, max_message_size: 1_000_000_000, + handshake_max_message_size: 1_000_000_000, close_frame_len: u32::MAX, application_close_code: 0, open_stream_timeout: Duration::from_millis(2_000), @@ -446,9 +323,11 @@ impl OmikronConnection { keep_alive_interval: Some(Duration::from_secs(6)), max_idle_timeout: Some(Duration::from_secs(30)), force_close_delay: Duration::from_millis(300), - max_transient_recv_errors: 20, - transient_recv_backoff: Duration::from_millis(100), receiver_queue_capacity: 1000, + max_concurrent_stream_tasks: 10, + persistent_stream_max_retries: 5, + persistent_stream_retry_backoff: Duration::from_secs(5), + max_frames_per_stream: None, }); let connection = match Client::auth_connect_or_register( @@ -467,7 +346,7 @@ impl OmikronConnection { ); *self.reconnect_on_close.write().await = false; *self.auth_failure.write().await = Some(reason.clone()); - *self.state.write().await = ConnectionState::Disconnected; + self.set_state(ConnectionState::Disconnected).await; return Err(reason); } Err(e) => return Err(format!("Connection failed: {}", e)), @@ -482,7 +361,8 @@ impl OmikronConnection { let sender_arc = Arc::new(connection.sender); *self.sender.write().await = Some(sender_arc.clone()); - *self.state.write().await = ConnectionState::Connected { identified: true }; + self.set_state(ConnectionState::Connected { identified: true }) + .await; // Start read loop let mut receiver = connection.receiver; @@ -507,7 +387,7 @@ impl OmikronConnection { // Wait for read loop to complete let result = read_handle.await; *self.sender.write().await = None; - *self.state.write().await = ConnectionState::Disconnected; + self.set_state(ConnectionState::Disconnected).await; { ACTIVE_TASKS.remove("Omikron Listener"); } @@ -540,16 +420,25 @@ impl OmikronConnection { * that still read it directly. */ async fn load_or_migrate_keyring(&self) -> Keyring { - if let Ok(kr) = mtp::files::load_keyring(IOTA_KEYRING_PATH) { + if let Ok(kr) = mtp::files::load_keyring_raw(IOTA_KEYRING_PATH) { return kr; } let legacy = CONFIG.load().keyring.clone(); let keyring = legacy .and_then(|b64| keyring_from_base64(&b64)) - .unwrap_or_else(crypto_helper::generate_keyring); + .unwrap_or_else(|| { + log!( + "WARNING: No existing keyring found. Neither {} nor config.json \ + contain a keyring; generating a new identity. If you already had \ + an Iota identity, restore {} from a backup to avoid losing access.", + IOTA_KEYRING_PATH, + IOTA_KEYRING_PATH + ); + crypto_helper::generate_keyring() + }); - if let Err(e) = mtp::files::save_keyring(&keyring, IOTA_KEYRING_PATH) { + if let Err(e) = mtp::files::save_keyring_raw(&keyring, IOTA_KEYRING_PATH) { log!("Failed to persist {}: {}", IOTA_KEYRING_PATH, e); } @@ -672,7 +561,23 @@ impl OmikronConnection { let result = receiver.receive().await; match result { Ok(cv) => { - self.clone().handle_message(cv).await; + 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::Pong) { + self.handle_pong(&cv).await; + continue; + } + + let permit = self.handler_semaphore.clone().acquire_owned().await; + let self_clone = self.clone(); + tokio::spawn(async move { + let _permit = permit; + self_clone.handle_message_impl(cv).await; + }); } Err(e) => { self.fail_all_waiting_tasks(format!( @@ -710,6 +615,17 @@ impl OmikronConnection { break; } + if self.missed_pongs.load(Ordering::Relaxed) > MAX_MISSED_PONGS { + log!( + "Connection appears dead ({} consecutive missed pongs), closing sender", + self.missed_pongs.load(Ordering::Relaxed) + ); + if let Some(sender) = self.sender.read().await.as_ref() { + sender.close(); + } + break; + } + self.flush_pending_chat_secret_forwards().await; self.send_ping().await; } @@ -754,8 +670,151 @@ impl OmikronConnection { } } + async fn forward_message_live( + &self, + message_id: u32, + receiver_id: u64, + sender_id: i64, + timestamp: i64, + content: &str, + height: i64, + reply_to: Option, + ) -> Option { + let mut msg_fields = vec![ + (DataType::Content, DataValue::Str(content.to_string())), + ( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ), + (DataType::Height, DataValue::SignedNumber(height as i128)), + ]; + if let Some(rt) = reply_to { + msg_fields.push(( + DataType::ReplyId, + DataValue::UnsignedNumber(rt as u64 as u128), + )); + } + + let user_forward = CommunicationValue::new(CommunicationType::MessageLive) + .with_id(message_id) + .with_receiver(receiver_id) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(sender_id as i128), + ) + .add_typed_default(DataType::Message, typed_container(msg_fields)); + + match self + .await_response(&user_forward, Some(Duration::from_secs(3))) + .await + { + Ok(user_resp) => { + let ms_raw = user_resp + .get_data(DataType::MessageState) + .as_string() + .unwrap_or_else(|| "".to_string()); + Some(MessageState::from_str(&ms_raw).upgrade(MessageState::Received)) + } + Err(_) => None, + } + } + + async fn forward_to_remote_iota( + &self, + cv: &CommunicationValue, + sender_id: i64, + receiver_id: i64, + timestamp: i64, + content: &str, + height: i64, + reply_to: Option, + ) { + let mut fw_msg = CommunicationValue::new(CommunicationType::MessageOtherIota) + .with_id(cv.get_id()) + .with_receiver(receiver_id as u64) + .with_sender(sender_id as u64) + .add_typed_default(DataType::Height, DataValue::SignedNumber(height as i128)) + .add_typed_default(DataType::Content, DataValue::Str(content.to_string())) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ); + if let Some(rt) = reply_to { + fw_msg = fw_msg.add_typed_default( + DataType::ReplyId, + DataValue::UnsignedNumber(rt as u64 as u128), + ); + } + + match self + .await_response(&fw_msg, Some(Duration::from_secs(10))) + .await + { + Ok(resp) => { + let ms_raw = resp + .get_data(DataType::MessageState) + .as_string() + .unwrap_or_else(|| "".to_string()); + let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + + let _ = + chat_files::change_message_state(timestamp, sender_id, receiver_id, ms.clone()); + + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } + Err(_) => { + let _ = chat_files::change_message_state( + timestamp, + sender_id, + receiver_id, + MessageState::Sent, + ); + + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; + } + } + } + // ------------------------------------------------------------------------- - // Message Handling (Preserved from original) + // Message Handling — Dispatch // ------------------------------------------------------------------------- pub async fn handle_message(self: Arc, cv: CommunicationValue) { @@ -765,7 +824,6 @@ impl OmikronConnection { 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; @@ -777,821 +835,775 @@ impl OmikronConnection { return; } - if cv.is_type(CommunicationType::SetChatSecret) { - let sender_id = cv.get_sender().to_string(); - let recipients = match chat_secret_recipients(&cv) { - Some(recipients) => recipients, - None => { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; + self.clone().handle_message_impl(cv).await; + } + + async fn handle_message_impl(self: Arc, cv: CommunicationValue) { + macro_rules! dispatch { + ($ty:ident, $method:ident) => { + if cv.is_type(CommunicationType::$ty) { + self.clone().$method(&cv).await; return; } }; - let now = now_millis_i64(); - let chat_id = data_string(&cv, DataType::ChatId); - let secret_id = data_string(&cv, DataType::SecretId); - let version = data_i64(&cv, DataType::VersionNumber); - let wrapping_scheme = data_string(&cv, DataType::WrappingScheme); - let created_at = data_i64(&cv, DataType::CreatedAt).unwrap_or(now); + } - let Some((((chat_id, secret_id), version), wrapping_scheme)) = - chat_id.zip(secret_id).zip(version).zip(wrapping_scheme) - else { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) + dispatch!(SetChatSecret, handle_set_chat_secret); + dispatch!(GetChatSecret, handle_get_chat_secret); + dispatch!(ChatSecretForward, handle_chat_secret_forward); + dispatch!(AppIdentification, handle_app_identification); + dispatch!(AppChallengeResponse, handle_app_challenge_response); + dispatch!(SaveAppData, handle_save_app_data); + dispatch!(LoadAppData, handle_load_app_data); + dispatch!(CreateApp, handle_create_app); + dispatch!(DeleteApp, handle_delete_app); + dispatch!(ClientConnected, handle_client_connected); + dispatch!(MessageState, handle_message_state); + dispatch!(MessageSend, handle_message_send); + dispatch!(MessageEdit, handle_message_edit); + dispatch!(MessageEditLive, handle_message_edit_live); + dispatch!(MessageReactionAdd, handle_message_reaction_add); + dispatch!(MessageReactionRemove, handle_message_reaction_remove); + dispatch!(MessageReactionLive, handle_message_reaction_live); + dispatch!(MessageDeleteLive, handle_message_delete_live); + dispatch!(MessageOtherIota, handle_message_other_iota); + dispatch!(MessagesGet, handle_messages_get); + dispatch!(GetChats, handle_get_chats); + dispatch!(AddConversation, handle_add_conversation); + dispatch!(AddCommunity, handle_add_community); + dispatch!(GetCommunities, handle_get_communities); + dispatch!(RemoveCommunity, handle_remove_community); + dispatch!(GlobalSettingsSave, handle_global_settings_save); + dispatch!(GlobalSettingsLoad, handle_global_settings_load); + dispatch!(SettingsSave, handle_settings_save); + dispatch!(SettingsLoad, handle_settings_load); + dispatch!(SettingsList, handle_settings_list); + } + + // ------------------------------------------------------------------------- + // Message Handlers + // ------------------------------------------------------------------------- + + async fn handle_set_chat_secret(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender().to_string(); + let recipients = match chat_secret_recipients(cv) { + Some(recipients) => recipients, + None => { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) .await; return; - }; + } + }; + let now = now_millis_i64(); + let chat_id = data_string(cv, DataType::ChatId); + let secret_id = data_string(cv, DataType::SecretId); + let version = data_i64(cv, DataType::VersionNumber); + let wrapping_scheme = data_string(cv, DataType::WrappingScheme); + let created_at = data_i64(cv, DataType::CreatedAt).unwrap_or(now); - for recipient in &recipients { - let recipient_id = recipient.user_id.parse::().unwrap_or(0); - let is_local = iota_storage::users::user_manager::get_user(recipient_id).is_some(); + let Some((((chat_id, secret_id), version), wrapping_scheme)) = + chat_id.zip(secret_id).zip(version).zip(wrapping_scheme) + else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; - if is_local { - if e2ee_storage::put_chat_secret(StoredChatSecret { - user_id: recipient.user_id.clone(), - chat_id: chat_id.clone(), - secret_id: secret_id.clone(), - version, - encrypted_secret: recipient.encrypted_secret.clone(), - kem_ciphertext: recipient.kem_ciphertext.clone(), - wrapping_scheme: wrapping_scheme.clone(), - created_at, - updated_at: now, - }) - .is_err() - { - self.send_message(&error_response( - &cv, - CommunicationType::ErrorInvalidData, - )) + let mut non_local_forwards: Vec = Vec::new(); + + for recipient in &recipients { + let recipient_id = recipient.user_id.parse::().unwrap_or(0); + let is_local = iota_storage::users::user_manager::get_user(recipient_id).is_some(); + + if is_local { + if e2ee_storage::put_chat_secret(StoredChatSecret { + user_id: recipient.user_id.clone(), + chat_id: chat_id.clone(), + secret_id: secret_id.clone(), + version, + encrypted_secret: recipient.encrypted_secret.clone(), + kem_ciphertext: recipient.kem_ciphertext.clone(), + wrapping_scheme: wrapping_scheme.clone(), + created_at, + updated_at: now, + }) + .is_err() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) .await; - return; - } - continue; - } - - if recipient.user_id != sender_id { - let forward = set_chat_secret_cv_for_recipient(&cv, recipient); - if !self.forward_chat_secret(&forward).await { - self.store_pending_chat_secret_forward(&forward).await; - } + return; } + continue; } - self.send_message(&error_response(&cv, CommunicationType::Success)) + if recipient.user_id != sender_id { + non_local_forwards.push(set_chat_secret_cv_for_recipient(cv, recipient)); + } + } + + if !non_local_forwards.is_empty() { + let mut handles = Vec::new(); + for forward in &non_local_forwards { + let self_clone = self.clone(); + let fwd = forward.clone(); + handles.push(tokio::spawn(async move { + self_clone.forward_chat_secret(&fwd).await + })); + } + + for (forward, handle) in non_local_forwards.into_iter().zip(handles) { + match handle.await { + Ok(true) => {} + _ => self.store_pending_chat_secret_forward(&forward).await, + } + } + } + + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } + + async fn handle_get_chat_secret(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_get_chat_secret(cv)) + .await; + } + + async fn handle_chat_secret_forward(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender().to_string(); + let recipient_user_id = data_string(cv, DataType::RecipientUserId).unwrap_or_default(); + if data_string(cv, DataType::SenderUserId).as_deref() != Some(sender_id.as_str()) + || recipient_user_id.is_empty() + || pending_chat_secret_forward_from_cv(cv).is_none() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) .await; return; } - if cv.is_type(CommunicationType::GetChatSecret) { - let Some(user_id) = data_string(&cv, DataType::UserId) else { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - }; - if user_id != cv.get_sender().to_string() { - self.send_message(&error_response(&cv, CommunicationType::ErrorNotFound)) - .await; - return; - } - let Some(chat_id) = data_string(&cv, DataType::ChatId) else { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - }; + let forward = cv + .clone() + .with_receiver(recipient_user_id.parse::().unwrap_or(0)); + if self.forward_chat_secret(&forward).await { + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } else { + self.store_pending_chat_secret_forward(cv).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; + } + } - match e2ee_storage::get_chat_secret(ChatSecretQuery { - user_id, - chat_id, - secret_id: data_string(&cv, DataType::SecretId), - }) { - Ok(Some(record)) => { - let response = CommunicationValue::new(CommunicationType::ChatSecretResponse) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_typed_default(DataType::UserId, DataValue::Str(record.user_id)) - .add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id)) - .add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id)) - .add_typed_default( - DataType::VersionNumber, - DataValue::SignedNumber(record.version as i128), - ) - .add_typed_default( - DataType::EncryptedSecret, - DataValue::Bytes(record.encrypted_secret), - ) - .add_typed_default( - DataType::KemCiphertext, - DataValue::Bytes(record.kem_ciphertext), - ) - .add_typed_default( - DataType::WrappingScheme, - DataValue::Str(record.wrapping_scheme), - ) - .add_typed_default( - DataType::CreatedAt, - DataValue::SignedNumber(record.created_at as i128), - ) - .add_typed_default( - DataType::UpdatedAt, - DataValue::SignedNumber(record.updated_at as i128), - ); - self.send_message(&response).await; - } - Ok(None) => { - self.send_message(&error_response(&cv, CommunicationType::ErrorNotSet)) - .await - } - Err(_) => { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await + async fn handle_app_identification(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender(); + let app_identifier = cv + .get_data(DataType::AppIdentifier) + .as_str() + .unwrap_or("") + .to_string(); + let app_public_key = cv + .get_data(DataType::AppPublicKey) + .as_str() + .unwrap_or("") + .to_string(); + let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; + + let mut trusted = false; + if let Some(user) = iota_storage::users::user_manager::get_user(user_id) { + if let Some(pub_k) = user.trusted_apps.get(&app_identifier) { + if pub_k == &app_public_key { + trusted = true; } } - return; } - if cv.is_type(CommunicationType::ChatSecretForward) { - let sender_id = cv.get_sender().to_string(); - let recipient_user_id = data_string(&cv, DataType::RecipientUserId).unwrap_or_default(); - if data_string(&cv, DataType::SenderUserId).as_deref() != Some(sender_id.as_str()) - || recipient_user_id.is_empty() - || pending_chat_secret_forward_from_cv(&cv).is_none() + if trusted { + let challenge = Uuid::new_v4().to_string(); + + self.app_challenges.insert(sender_id, challenge.clone()); + self.app_sessions + .insert(sender_id, (user_id, app_identifier.clone())); + + if let Some(app_pub_bundle) = + iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key) { - self.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData)) - .await; - return; - } + let kr_str = CONFIG.load().keyring.clone().unwrap_or_default(); - let forward = cv - .clone() - .with_receiver(recipient_user_id.parse::().unwrap_or(0)); - if self.forward_chat_secret(&forward).await { - self.send_message(&error_response(&cv, CommunicationType::Success)) - .await; - } else { - self.store_pending_chat_secret_forward(&cv).await; - self.send_message(&error_response(&cv, CommunicationType::Success)) - .await; - } - return; - } + if let Some(keyring) = keyring_from_base64(&kr_str) { + if let Ok(encrypted_challenge) = + crypto_util::encrypt_challenge(&challenge, &app_pub_bundle) + { + let bundle = keyring.public_key_bundle(); + let pub_k_b64 = crypto_helper::public_key_bundle_to_base64(&bundle); - if cv.is_type(CommunicationType::AppIdentification) { - let sender_id = cv.get_sender(); - let app_identifier = cv - .get_data(DataType::AppIdentifier) - .as_str() - .unwrap_or("") - .to_string(); - let app_public_key = cv - .get_data(DataType::AppPublicKey) - .as_str() - .unwrap_or("") - .to_string(); - let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; + let res = CommunicationValue::new(CommunicationType::AppChallenge) + .with_id(cv.get_id()) + .with_receiver(sender_id) + .add_typed_default(DataType::PublicKey, DataValue::Str(pub_k_b64)) + .add_typed_default( + DataType::Challenge, + DataValue::Str(encrypted_challenge), + ); - let mut trusted = false; - if let Some(user) = iota_storage::users::user_manager::get_user(user_id) { - if let Some(pub_k) = user.trusted_apps.get(&app_identifier) { - if pub_k == &app_public_key { - trusted = true; - } - } - } - - if trusted { - let challenge = Uuid::new_v4().to_string(); - - self.app_challenges - .write() - .await - .insert(sender_id, challenge.clone()); - self.app_sessions - .write() - .await - .insert(sender_id, (user_id, app_identifier.clone())); - - if let Some(app_pub_bundle) = - iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key) - { - let kr_str = CONFIG.load().keyring.clone().unwrap_or_default(); - - if let Some(keyring) = keyring_from_base64(&kr_str) { - if let Ok(encrypted_challenge) = - crypto_util::encrypt_challenge(&challenge, &app_pub_bundle) - { - let bundle = keyring.public_key_bundle(); - let pub_k_b64 = crypto_helper::public_key_bundle_to_base64(&bundle); - - let res = CommunicationValue::new(CommunicationType::AppChallenge) - .with_id(cv.get_id()) - .with_receiver(sender_id) - .add_typed_default(DataType::PublicKey, DataValue::Str(pub_k_b64)) - .add_typed_default( - DataType::Challenge, - DataValue::Str(encrypted_challenge), - ); - - self.send_message(&res).await; - return; - } - } - } - } - - let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) - .with_id(cv.get_id()) - .with_receiver(sender_id); - self.send_message(&res).await; - return; - } - - if cv.is_type(CommunicationType::AppChallengeResponse) { - let sender_id = cv.get_sender(); - let mut challenges = self.app_challenges.write().await; - if let Some(expected) = challenges.remove(&sender_id) { - if let DataValue::Str(response) = cv.get_data(DataType::Challenge) { - if expected == *response { - let res = - CommunicationValue::new(CommunicationType::AppIdentificationResponse) - .with_id(cv.get_id()) - .with_receiver(sender_id); - self.send_message(&res).await; + let _ = self.send_message(&res).await; return; } } } - let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) - .with_id(cv.get_id()) - .with_receiver(sender_id); - self.send_message(&res).await; - return; } - if cv.is_type(CommunicationType::SaveAppData) { - let sender_id = cv.get_sender(); - let app_data = cv - .get_data(DataType::AppData) - .as_str() - .unwrap_or("") - .to_string(); + let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) + .with_id(cv.get_id()) + .with_receiver(sender_id); + let _ = self.send_message(&res).await; + } - let sessions = self.app_sessions.read().await; - if let Some((user_id, app_identifier)) = sessions.get(&sender_id) { - iota_storage::users::user_manager::save_app_data( - *user_id, - app_identifier, - &app_data, - ); - } - - let res = CommunicationValue::new(CommunicationType::SaveAppData) - .with_id(cv.get_id()) - .with_receiver(sender_id); - self.send_message(&res).await; - return; - } - - if cv.is_type(CommunicationType::LoadAppData) { - let sender_id = cv.get_sender(); - let mut app_data = String::new(); - - let sessions = self.app_sessions.read().await; - if let Some((user_id, app_identifier)) = sessions.get(&sender_id) { - app_data = - iota_storage::users::user_manager::load_app_data(*user_id, app_identifier); - } - - let res = CommunicationValue::new(CommunicationType::LoadAppData) - .with_id(cv.get_id()) - .with_receiver(sender_id) - .add_typed_default(DataType::AppData, DataValue::Str(app_data)); - self.send_message(&res).await; - return; - } - - if cv.is_type(CommunicationType::CreateApp) { - let sender_id = cv.get_sender() as i64; - let app_identifier = cv - .get_data(DataType::AppIdentifier) - .as_str() - .unwrap_or("") - .to_string(); - let app_public_key = cv - .get_data(DataType::AppPublicKey) - .as_str() - .unwrap_or("") - .to_string(); - - if !app_identifier.is_empty() && !app_public_key.is_empty() { - if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { - if !user.trusted_apps.contains_key(&app_identifier) { - user.trusted_apps.insert(app_identifier, app_public_key); - iota_storage::users::user_manager::update_user(user); - } + async fn handle_app_challenge_response(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender(); + if let Some((_, expected_challenge)) = self.app_challenges.remove(&sender_id) { + if let DataValue::Str(response) = cv.get_data(DataType::Challenge) { + if expected_challenge == *response { + let res = CommunicationValue::new(CommunicationType::AppIdentificationResponse) + .with_id(cv.get_id()) + .with_receiver(sender_id); + let _ = self.send_message(&res).await; + return; } } + } + let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge) + .with_id(cv.get_id()) + .with_receiver(sender_id); + let _ = self.send_message(&res).await; + } - let res = CommunicationValue::new(CommunicationType::CreateApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&res).await; - return; + async fn handle_save_app_data(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender(); + let app_data = cv + .get_data(DataType::AppData) + .as_str() + .unwrap_or("") + .to_string(); + + if let Some(session) = self.app_sessions.get(&sender_id) { + let (user_id, app_identifier) = session.value(); + iota_storage::users::user_manager::save_app_data(*user_id, app_identifier, &app_data); } - if cv.is_type(CommunicationType::DeleteApp) { - let sender_id = cv.get_sender() as i64; - let app_identifier = cv - .get_data(DataType::AppIdentifier) - .as_str() - .unwrap_or("") - .to_string(); + let res = CommunicationValue::new(CommunicationType::SaveAppData) + .with_id(cv.get_id()) + .with_receiver(sender_id); + let _ = self.send_message(&res).await; + } - if !app_identifier.is_empty() { - if let Some(mut user) = iota_storage::users::user_manager::get_user(sender_id) { - if user.trusted_apps.contains_key(&app_identifier) { - user.trusted_apps.remove(&app_identifier); - iota_storage::users::user_manager::update_user(user); - } - } - } + async fn handle_load_app_data(self: Arc, cv: &CommunicationValue) { + let sender_id = cv.get_sender(); + let mut app_data = String::new(); - let res = CommunicationValue::new(CommunicationType::DeleteApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&res).await; - return; + if let Some(session) = self.app_sessions.get(&sender_id) { + let (user_id, app_identifier) = session.value(); + app_data = iota_storage::users::user_manager::load_app_data(*user_id, app_identifier); } - if cv.is_type(CommunicationType::ClientConnected) { - let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64; - let _session_id = cv.get_data(DataType::SessionId).as_number().unwrap_or(0) as i64; + let res = CommunicationValue::new(CommunicationType::LoadAppData) + .with_id(cv.get_id()) + .with_receiver(sender_id) + .add_typed_default(DataType::AppData, DataValue::Str(app_data)); + let _ = self.send_message(&res).await; + } - let contacts = chats_util::get_users(user_id); - let mut contacts_array = Vec::new(); + async fn handle_create_app(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_create_app(cv)) + .await; + } - for (i, contact) in contacts.iter().enumerate() { - let mut contact_container = Vec::new(); - contact_container.push(( - DataType::UserId, - DataValue::SignedNumber(contact.user_id as i128), - )); - contact_container.push(( - DataType::LastMessageAt, - DataValue::SignedNumber(contact.last_message_at.unwrap_or(0) as i128), - )); + async fn handle_delete_app(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_delete_app(cv)) + .await; + } - if let Some(ref name) = contact.user_name { - contact_container.push((DataType::Username, DataValue::Str(name.clone()))); - } + async fn handle_client_connected(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_client_connected(cv)) + .await; + } - let amount = if i < 10 { 20 } else { 1 }; - let messages = chat_files::get_messages(user_id, contact.user_id, 0, amount); + async fn handle_message_state(self: Arc, cv: &CommunicationValue) { + message_handlers::handle_message_state(cv); + } - let mut msg_array = Vec::new(); - for m in messages.members() { - let message_time = m["message_time"].as_i64().unwrap_or(0); - let content = m["content"].as_str().unwrap_or("").to_string(); - let sent_by_self = m["sent_by_self"].as_bool().unwrap_or(false); - let height = m["height"].as_i64().unwrap_or(0); - let message_state = m["message_state"].as_str().unwrap_or("").to_string(); - - let mut msg_container = Vec::new(); - msg_container.push(( - DataType::SendTime, - DataValue::SignedNumber(message_time as i128), - )); - msg_container.push((DataType::Content, DataValue::Str(content.clone()))); - msg_container.push((DataType::MessageState, DataValue::Str(message_state))); - msg_container.push((DataType::Height, DataValue::SignedNumber(height as i128))); - msg_container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if sent_by_self { - user_id as u128 - } else { - contact.user_id as u128 - }), - )); - msg_array.push(typed_container(msg_container)); - - if msg_array.len() == 1 { - let sender_id = if sent_by_self { - user_id - } else { - contact.user_id - }; - let mut last_msg = Vec::new(); - last_msg.push((DataType::Content, DataValue::Str(content))); - last_msg.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - contact_container.push((DataType::LastMessage, typed_container(last_msg))); - } - } - contact_container.push((DataType::Messages, DataValue::Array(msg_array))); - contacts_array.push(typed_container(contact_container)); - } - - let resp = CommunicationValue::new(CommunicationType::ClientConnected) - .with_id(cv.get_id()) - .add_typed_default(DataType::Contacts, DataValue::Array(contacts_array)); - self.send_message(&resp).await; - return; - } - - // ************************************************ // - // Direct messages // - // ************************************************ // - - if cv.is_type(CommunicationType::MessageState) { - let sender_id = &cv.get_sender(); - let receiver_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(id) => id, - _ => return, - }; - - // Parse send_time robustly: accept numeric or string, fallback to current time - let send_time_val = cv.get_data(DataType::SendTime); - 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(DataType::MessageState).as_str().unwrap_or("")), + fn mutation_live_message( + ty: CommunicationType, + request: &CommunicationValue, + mutation: &message_handlers::MessageMutation, + extra: Vec<(DataType, DataValue)>, + ) -> CommunicationValue { + let mut message = CommunicationValue::new(ty) + .with_id(request.get_id()) + .with_sender(mutation.sender_id as u64) + .with_receiver(mutation.partner_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(mutation.sender_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(mutation.send_time as i128), ); + for (data_type, value) in extra { + message = message.add_typed_default(data_type, value); + } + message + } + + async fn persist_and_deliver_remote_edit(&self, cv: &CommunicationValue) { + let sender_id = match i64::try_from(cv.get_sender()) { + Ok(sender_id) => sender_id, + Err(_) => return, + }; + let receiver_id = match i64::try_from(cv.get_receiver()) { + Ok(receiver_id) if receiver_id > 0 => receiver_id, + _ => return, + }; + let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { + return; + }; + let Some(content) = cv.get_data(DataType::Content).as_str() else { + return; + }; + if chat_files::apply_remote_edit(receiver_id, sender_id, send_time, sender_id, content) + .is_ok() + { + let _ = self.send_message(cv).await; + } + } + + async fn persist_and_deliver_remote_reaction(&self, cv: &CommunicationValue, add: bool) { + let sender_id = match i64::try_from(cv.get_sender()) { + Ok(sender_id) => sender_id, + Err(_) => return, + }; + let receiver_id = match i64::try_from(cv.get_receiver()) { + Ok(receiver_id) if receiver_id > 0 => receiver_id, + _ => return, + }; + let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { + return; + }; + let Some(reaction) = cv.get_data(DataType::Reaction).as_str() else { + return; + }; + if reaction.is_empty() || reaction.len() > 64 { + return; + } + let result = if add { + chat_files::add_reaction(receiver_id, sender_id, send_time, sender_id, reaction) + } else { + chat_files::remove_reaction(receiver_id, sender_id, send_time, sender_id, reaction) + }; + if result.is_ok() { + let _ = self.send_message(cv).await; + } + } + + async fn persist_and_deliver_remote_delete(&self, cv: &CommunicationValue) { + let sender_id = match i64::try_from(cv.get_sender()) { + Ok(sender_id) => sender_id, + Err(_) => return, + }; + let receiver_id = match i64::try_from(cv.get_receiver()) { + Ok(receiver_id) if receiver_id > 0 => receiver_id, + _ => return, + }; + let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else { + return; + }; + if chat_files::apply_remote_delete(receiver_id, sender_id, send_time, sender_id).is_ok() { + let _ = self.send_message(cv).await; + } + } + + async fn handle_message_edit(self: Arc, cv: &CommunicationValue) { + let response = message_handlers::handle_message_edit(cv); + if !response.is_type(CommunicationType::Success) { + let _ = self.send_message(&response).await; + return; + } + let Ok(mutation) = message_handlers::message_mutation(cv) else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let Some(content) = cv.get_data(DataType::Content).as_str() else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let live = Self::mutation_live_message( + CommunicationType::MessageEditLive, + cv, + &mutation, + vec![(DataType::Content, DataValue::Str(content.to_string()))], + ); + if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some() + && chat_files::apply_remote_edit( + mutation.partner_id, + mutation.sender_id, + mutation.send_time, + mutation.sender_id, + content, + ) + .is_err() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorNotFound)) + .await; + return; + } + let _ = self.send_message(&response).await; + let _ = self.send_message(&live).await; + } + + async fn handle_message_edit_live(self: Arc, cv: &CommunicationValue) { + self.persist_and_deliver_remote_edit(cv).await; + } + + async fn handle_message_reaction_add(self: Arc, cv: &CommunicationValue) { + self.handle_message_reaction(cv, true).await; + } + + async fn handle_message_reaction_remove(self: Arc, cv: &CommunicationValue) { + self.handle_message_reaction(cv, false).await; + } + + async fn handle_message_reaction(self: Arc, cv: &CommunicationValue, add: bool) { + let response = message_handlers::handle_message_reaction(cv, add); + if !response.is_type(CommunicationType::Success) { + let _ = self.send_message(&response).await; + return; + } + let Ok(mutation) = message_handlers::message_mutation(cv) else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let Some(reaction) = cv.get_data(DataType::Reaction).as_str() else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let live = Self::mutation_live_message( + CommunicationType::MessageReactionLive, + cv, + &mutation, + vec![ + (DataType::Reaction, DataValue::Str(reaction.to_string())), + ( + DataType::SenderId, + DataValue::SignedNumber(mutation.sender_id as i128), + ), + (DataType::Accepted, DataValue::Bool(add)), + ], + ); + if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some() { + let result = if add { + chat_files::add_reaction( + mutation.partner_id, + mutation.sender_id, + mutation.send_time, + mutation.sender_id, + reaction, + ) + } else { + chat_files::remove_reaction( + mutation.partner_id, + mutation.sender_id, + mutation.send_time, + mutation.sender_id, + reaction, + ) + }; + if result.is_err() { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorNotFound)) + .await; + return; + } + } + let _ = self.send_message(&response).await; + let _ = self.send_message(&live).await; + } + + async fn handle_message_reaction_live(self: Arc, cv: &CommunicationValue) { + let add = cv.get_data(DataType::Accepted).as_bool().unwrap_or(true); + self.persist_and_deliver_remote_reaction(cv, add).await; + } + + async fn handle_message_delete_live(self: Arc, cv: &CommunicationValue) { + let sender_id = match i64::try_from(cv.get_sender()) { + Ok(sender_id) => sender_id, + Err(_) => return, + }; + if iota_storage::users::user_manager::get_user(sender_id).is_none() { + self.persist_and_deliver_remote_delete(cv).await; + return; } - // Incoming storsed message: store for the recipient, attempt local delivery, notify sender. - if cv.is_type(CommunicationType::MessageSend) { - let sender_id: u64 = cv.get_sender(); + let response = message_handlers::handle_message_delete(cv); + if !response.is_type(CommunicationType::Success) { + let _ = self.send_message(&response).await; + return; + } + let Ok(mutation) = message_handlers::message_mutation(cv) else { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; + return; + }; + let live = Self::mutation_live_message( + CommunicationType::MessageDeleteLive, + cv, + &mutation, + Vec::new(), + ); + if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some() + && chat_files::apply_remote_delete( + mutation.partner_id, + mutation.sender_id, + mutation.send_time, + mutation.sender_id, + ) + .is_err() + { + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorNotFound)) + .await; + return; + } + let _ = self.send_message(&response).await; + let _ = self.send_message(&live).await; + } - // parse receiver_id (the storage owner for this incoming message) - let receiver_id: i64 = if let Some(n) = cv.get_data(DataType::ReceiverId).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataType::ReceiverId).as_str() { - s.parse::().unwrap_or(0) - } else { - 0 - }; + async fn handle_message_send(self: Arc, cv: &CommunicationValue) { + let sender_id: u64 = cv.get_sender(); - // parse send_time robustly (number or string), fallback to now - let send_time_val = cv.get_data(DataType::SendTime); - 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; + let receiver_id: i64 = if let Some(n) = cv.get_data(DataType::ReceiverId).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::ReceiverId).as_str() { + s.parse::().unwrap_or(0) + } else { + 0 + }; - // content may be missing; default to empty string - let content = cv - .get_data(DataType::Content) - .as_str() - .unwrap_or("") - .to_string(); + let timestamp_i64 = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { + s.parse::().unwrap_or_else(|_| now_millis_i64()) + } else { + now_millis_i64() + }; + let timestamp_u128 = timestamp_i64 as u128; - let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; - let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); + let content = cv + .get_data(DataType::Content) + .as_str() + .unwrap_or("") + .to_string(); - let is_local = iota_storage::users::user_manager::get_user(receiver_id).is_some(); + let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; + let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); - if is_local { - // 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, - reply_to, - ); - } + let is_local = iota_storage::users::user_manager::get_user(receiver_id).is_some(); - // persist message for the sender (storage_owner = sender_id) + if is_local { chat_files::add_message( timestamp_u128, - true, - sender_id as i64, + false, receiver_id as i64, + sender_id as i64, &content, height, reply_to, ); + } - // send confirmation back to sender - let conf_msg = CommunicationValue::new(CommunicationType::MessageSend) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - self.send_message(&conf_msg).await; + chat_files::add_message( + timestamp_u128, + true, + sender_id as i64, + receiver_id as i64, + &content, + height, + reply_to, + ); - if !is_local { - let mut fw_msg = CommunicationValue::new(CommunicationType::MessageOtherIota) - .with_id(cv.get_id()) - .with_receiver(receiver_id as u64) - .with_sender(sender_id as u64) - .add_typed_default(DataType::Height, DataValue::SignedNumber(height as i128)) - .add_typed_default(DataType::Content, DataValue::Str(content)) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ); - if let Some(rt) = reply_to { - fw_msg = fw_msg.add_typed_default( - DataType::ReplyId, - DataValue::UnsignedNumber(rt as u64 as u128), - ); - } - - let other_iota_resp = self - .clone() - .await_response(&fw_msg, Some(Duration::from_secs(10))) - .await; - - if let Ok(resp) = other_iota_resp { - let ms_raw = resp - .get_data(DataType::MessageState) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); + let conf_msg = CommunicationValue::new(CommunicationType::MessageSend) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64); + let _ = self.send_message(&conf_msg).await; + if !is_local { + self.forward_to_remote_iota( + cv, + sender_id as i64, + receiver_id, + timestamp_i64, + &content, + height, + reply_to, + ) + .await; + } else { + match self + .forward_message_live( + cv.get_id(), + receiver_id as u64, + sender_id as i64, + timestamp_i64, + &content, + height, + reply_to, + ) + .await + { + Some(ms) => { let _ = chat_files::change_message_state( timestamp_i64, - sender_id as i64, - receiver_id as i64, - ms.clone(), - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::MessageState) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_typed_default( - DataType::ChatPartnerId, - DataValue::SignedNumber(receiver_id as i128), - ) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ) - .add_typed_default( - DataType::MessageState, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; - } else { - let _ = chat_files::change_message_state( - timestamp_i64, - sender_id as i64, - receiver_id as i64, - MessageState::Sent, - ); - - self.send_message( - &CommunicationValue::new(CommunicationType::MessageState) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_typed_default( - DataType::ChatPartnerId, - DataValue::SignedNumber(receiver_id as i128), - ) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ) - .add_typed_default( - DataType::MessageState, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; - } - return; - } else { - // Build a live-delivery message for the local client (recipient) - let user_forward = CommunicationValue::new(CommunicationType::MessageLive) - .with_id(cv.get_id()) - .with_receiver(receiver_id as u64) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - ) - .add_typed_default(DataType::Message, { - let mut msg_fields = vec![ - (DataType::Content, DataValue::Str(content.clone())), - ( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ), - (DataType::Height, DataValue::SignedNumber(height as i128)), - ]; - if let Some(rt) = reply_to { - msg_fields.push(( - DataType::ReplyId, - DataValue::UnsignedNumber(rt as u64 as u128), - )); - } - typed_container(msg_fields) - }); - - // Attempt delivery and await a response from the local client - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(3))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataType::MessageState) - .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, + receiver_id, 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, + receiver_id, ms.clone(), ); - - // notify original sender about the delivered/read state (if read receipts are enabled) if is_read_receipts_enabled().await { - self.send_message( - &CommunicationValue::new(CommunicationType::MessageState) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_typed_default( - DataType::ChatPartnerId, - DataValue::SignedNumber(receiver_id as i128), - ) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ) - .add_typed_default( - DataType::MessageState, - DataValue::Str(ms.as_str().to_string()), - ), - ) - .await; + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp_i64 as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; } - } else { - // Delivery failed or timed out; mark as Sent + } + None => { let _ = chat_files::change_message_state( timestamp_i64, - receiver_id as i64, + receiver_id, sender_id as i64, MessageState::Sent, ); - let _ = chat_files::change_message_state( timestamp_i64, sender_id as i64, - receiver_id as i64, + receiver_id, MessageState::Sent, ); - - // Send push notification to Omega since user is offline let push_msg = CommunicationValue::new(CommunicationType::PushNotification) .with_receiver(receiver_id as u64) .add_typed_default( DataType::SenderId, DataValue::SignedNumber(sender_id as i128), ); - self.send_message(&push_msg).await; - - // notify sender - self.send_message( - &CommunicationValue::new(CommunicationType::MessageState) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64) - .with_sender(receiver_id as u64) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp_i64 as i128), - ) - .add_typed_default( - DataType::ChatPartnerId, - DataValue::SignedNumber(receiver_id as i128), - ) - .add_typed_default( - DataType::MessageState, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; + let _ = self.send_message(&push_msg).await; + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(sender_id as u64) + .with_sender(receiver_id as u64) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp_i64 as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(receiver_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; } - return; } } + } - if cv.is_type(CommunicationType::MessageOtherIota) { - let sender_id = &cv.get_sender(); - let receiver_id = &cv.get_receiver(); + async fn handle_message_other_iota(self: Arc, cv: &CommunicationValue) { + 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(DataType::SendTime); - 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 - }; + let timestamp = if let Some(n) = cv.get_data(DataType::SendTime).as_number() { + n as i64 + } else if let Some(s) = cv.get_data(DataType::SendTime).as_str() { + s.parse::().unwrap_or_else(|_| now_millis_i64()) + } else { + now_millis_i64() + }; - // content may be missing or non-string; default to empty string - let content = cv - .get_data(DataType::Content) - .as_str() - .unwrap_or("") - .to_string(); + let content = cv + .get_data(DataType::Content) + .as_str() + .unwrap_or("") + .to_string(); - let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; - let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); + let height = cv.get_data(DataType::Height).as_number().unwrap_or(0) as i64; + let reply_to = cv.get_data(DataType::ReplyId).as_number().map(|n| n as i64); - chat_files::add_message( - timestamp as u128, - false, - *receiver_id as i64, + chat_files::add_message( + timestamp as u128, + false, + *receiver_id as i64, + *sender_id as i64, + &content, + height, + reply_to, + ); + + match self + .forward_message_live( + cv.get_id(), + *receiver_id, *sender_id as i64, + timestamp, &content, height, reply_to, - ); - - // Build user_forward using the parsed numeric timestamp and safe content string - let user_forward = CommunicationValue::new(CommunicationType::MessageLive) - .with_id(cv.get_id()) - .with_receiver(*receiver_id) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(*sender_id as i128), - ) - .add_typed_default(DataType::Message, { - let mut msg_fields = vec![ - (DataType::Content, DataValue::Str(content.clone())), - ( - DataType::SendTime, - DataValue::SignedNumber(timestamp as i128), - ), - (DataType::Height, DataValue::SignedNumber(height as i128)), - ]; - if let Some(rt) = reply_to { - msg_fields.push(( - DataType::ReplyId, - DataValue::UnsignedNumber(rt as u64 as u128), - )); - } - typed_container(msg_fields) - }); - - let user_resp = self - .clone() - .await_response(&user_forward, Some(Duration::from_secs(3))) - .await; - - if let Ok(user_resp) = user_resp { - let ms_raw = user_resp - .get_data(DataType::MessageState) - .as_string() - .unwrap_or_else(|| "".to_string()); - let ms = MessageState::from_str(&ms_raw).upgrade(MessageState::Received); - + ) + .await + { + Some(ms) => { let _ = change_message_state( timestamp, *receiver_id as i64, @@ -1599,9 +1611,47 @@ impl OmikronConnection { ms.clone(), ); - // notify original sender about the delivered/read state (if read receipts are enabled) if is_read_receipts_enabled().await { - self.send_message( + let _ = self + .send_message( + &CommunicationValue::new(CommunicationType::MessageState) + .with_id(cv.get_id()) + .with_receiver(*sender_id) + .with_sender(*receiver_id) + .add_typed_default( + DataType::SendTime, + DataValue::SignedNumber(timestamp as i128), + ) + .add_typed_default( + DataType::ChatPartnerId, + DataValue::SignedNumber(*sender_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(ms.as_str().to_string()), + ), + ) + .await; + } + } + None => { + let _ = chat_files::change_message_state( + timestamp, + *receiver_id as i64, + *sender_id as i64, + MessageState::Sent, + ); + + let push_msg = CommunicationValue::new(CommunicationType::PushNotification) + .with_receiver(*receiver_id) + .add_typed_default( + DataType::SenderId, + DataValue::SignedNumber(*sender_id as i128), + ); + let _ = self.send_message(&push_msg).await; + + let _ = self + .send_message( &CommunicationValue::new(CommunicationType::MessageState) .with_id(cv.get_id()) .with_receiver(*sender_id) @@ -1612,547 +1662,89 @@ impl OmikronConnection { ) .add_typed_default( DataType::ChatPartnerId, - DataValue::SignedNumber(*sender_id as i128), + DataValue::SignedNumber(*receiver_id as i128), ) .add_typed_default( DataType::MessageState, - DataValue::Str(ms.as_str().to_string()), + DataValue::Str(MessageState::Sent.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, - ); - - // Send push notification to Omega since user is offline - let push_msg = CommunicationValue::new(CommunicationType::PushNotification) - .with_receiver(*receiver_id) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(*sender_id as i128), - ); - self.send_message(&push_msg).await; - - self.send_message( - &CommunicationValue::new(CommunicationType::MessageState) - .with_id(cv.get_id()) - .with_receiver(*sender_id) - .with_sender(*receiver_id) - .add_typed_default( - DataType::SendTime, - DataValue::SignedNumber(timestamp as i128), - ) - .add_typed_default( - DataType::ChatPartnerId, - DataValue::SignedNumber(*receiver_id as i128), - ) - .add_typed_default( - DataType::MessageState, - DataValue::Str(MessageState::Sent.as_str().to_string()), - ), - ) - .await; } - return; } + } - if cv.is_type(CommunicationType::MessagesGet) { - let my_id = cv.get_sender(); - let partner_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0); - let offset = cv.get_data(DataType::Offset).as_number().unwrap_or(0); - let amount = cv.get_data(DataType::Amount).as_number().unwrap_or(0); - let messages = chat_files::get_messages( - my_id as i64, - partner_id as i64, - offset as i64, - amount as i64, - ); - 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(DataType::ChatPartnerId).as_number() { - n as i64 - } else if let Some(s) = cv.get_data(DataType::ChatPartnerId).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(); + async fn handle_messages_get(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_messages_get(cv)) + .await; + } - let mut container = Vec::new(); - container.push(( - DataType::SendTime, - DataValue::SignedNumber(message_time as i128), - )); - container.push((DataType::Content, DataValue::Str(content))); - container.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - container.push((DataType::MessageState, DataValue::Str(message_state))); - container.push((DataType::Height, DataValue::SignedNumber(height as i128))); - container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if sent_by_self { - my_id as u128 - } else { - partner_id as u128 - }), - )); - if let Some(rt) = m["reply_to"].as_i64() { - container.push(( - DataType::ReplyId, - DataValue::UnsignedNumber(rt as u64 as u128), - )); - } - msg_array.push(typed_container(container)); - } + async fn handle_get_chats(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_get_chats(cv)) + .await; + } - let resp = CommunicationValue::new(CommunicationType::MessagesGet) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Messages, DataValue::Array(msg_array)); + async fn handle_add_conversation(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_add_conversation(cv)) + .await; + } - self.send_message(&resp).await; - return; - } + async fn handle_add_community(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_add_community(cv)) + .await; + } - if cv.is_type(CommunicationType::GetChats) { - 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(( - DataType::UserId, - DataValue::SignedNumber(user.user_id as i128), - )); - if let Some(name) = user.user_name { - container.push((DataType::Username, DataValue::Str(name))); - } - if let Some(ts) = user.last_message_at { - container.push((DataType::LastMessageAt, DataValue::SignedNumber(ts as i128))); - } - user_array.push(typed_container(container)); - } - let resp = CommunicationValue::new(CommunicationType::GetChats) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_typed_default(DataType::UserIds, DataValue::Array(user_array)); - self.send_message(&resp).await; - return; - } + async fn handle_get_communities(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_get_communities(cv)) + .await; + } - if cv.is_type(CommunicationType::AddConversation) { - let user_id = cv.get_sender(); - let other_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(n) => n as i64, - None => cv - .get_data(DataType::ChatPartnerId) - .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)); + async fn handle_remove_community(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_remove_community(cv)) + .await; + } - if let Some(name) = cv.get_data(DataType::ChatPartnerName).as_str() { - contact.user_name = Some(name.to_string()); - } + async fn handle_global_settings_save(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_global_settings_save(cv)) + .await; + } - 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::AddConversation) - .with_id(cv.get_id()) - .with_receiver(user_id); - self.send_message(&resp).await; - return; - } + async fn handle_global_settings_load(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_global_settings_load(cv)) + .await; + } - if cv.is_type(CommunicationType::AddCommunity) { - CommunitiesUtil::add_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::CommunityTitle) - .as_str() - .unwrap() - .to_string(), - cv.get_data(DataType::Position) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::AddCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } + async fn handle_settings_save(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_settings_save(cv, 0)) + .await; + } - if cv.is_type(CommunicationType::GetCommunities) { - let mut comm_array = Vec::new(); - for c in CommunitiesUtil::get_communities(cv.get_sender() as i64) { - let mut container: Vec<(DataType, DataValue)> = Vec::new(); - if let Some(address) = c["address"].as_str() { - container.push(( - DataType::CommunityAddress, - DataValue::Str(address.to_string()), - )); - } - if let Some(title) = c["title"].as_str() { - container.push((DataType::CommunityTitle, DataValue::Str(title.to_string()))); - } - if let Some(position) = c["position"].as_str() { - container.push((DataType::Position, DataValue::Str(position.to_string()))); - } - comm_array.push(typed_container(container)); - } + async fn handle_settings_load(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_settings_load(cv, 0)) + .await; + } - let resp = CommunicationValue::new(CommunicationType::GetCommunities) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()) - .add_typed_default(DataType::Communities, DataValue::Array(comm_array)); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::RemoveCommunity) { - CommunitiesUtil::remove_community( - cv.get_sender() as i64, - cv.get_data(DataType::CommunityAddress) - .as_str() - .unwrap() - .to_string(), - ); - let resp = CommunicationValue::new(CommunicationType::RemoveCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - self.send_message(&resp).await; - return; - } - - if cv.is_type(CommunicationType::GlobalSettingsSave) { - let my_id = cv.get_sender(); - let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing settings payload".to_string()), - ); - self.send_message(&response).await; - return; - }; - - save_file( - &format!("users/{}", my_id), - "global.settings", - settings_value, - ); - - let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsSave) - .with_receiver(my_id) - .with_id(cv.get_id()); - - if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { - response = response.add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - } - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::GlobalSettingsLoad) { - let my_id = cv.get_sender(); - let path = format!("users/{}", my_id); - let name = "global.settings"; - - if !has_file(&path, name) { - let mut response = CommunicationValue::new(CommunicationType::ErrorNotFound) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Path, DataValue::Str(name.to_string())); - - if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { - response = response.add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - } - - self.send_message(&response).await; - return; - } - - let settings_value_str = load_file(&path, name); - let mut response = CommunicationValue::new(CommunicationType::GlobalSettingsLoad) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)); - - if let Some(session_id) = cv.get_data(DataType::SessionId).as_number() { - response = response.add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - } - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::SettingsSave) { - let my_id = cv.get_sender(); - let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing session_id".to_string()), - ); - self.send_message(&response).await; - return; - }; - let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing settings_name".to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - }; - let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing settings payload".to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - }; - - if !settings_name - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') - || settings_name.contains("..") - { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Invalid settings_name".to_string()), - ) - .add_typed_default( - DataType::SettingsName, - DataValue::Str(settings_name.to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - } - - save_file( - &format!("users/{}/settings/{}/", my_id, session_id), - &format!("{}.settings", settings_name), - settings_value, - ); - - let response = CommunicationValue::new(CommunicationType::SettingsSave) - .with_receiver(my_id) - .with_id(cv.get_id()) - .add_typed_default( - DataType::SettingsName, - DataValue::Str(settings_name.to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::SettingsLoad) { - let my_id = cv.get_sender(); - let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing session_id".to_string()), - ); - self.send_message(&response).await; - return; - }; - let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing settings_name".to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - }; - - if !settings_name - .chars() - .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.') - || settings_name.contains("..") - { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Invalid settings_name".to_string()), - ) - .add_typed_default( - DataType::SettingsName, - DataValue::Str(settings_name.to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - } - - let settings_file = format!("{}.settings", settings_name); - let settings_path = format!("users/{}/settings/{}/", my_id, session_id); - if !has_file(&settings_path, &settings_file) { - let response = CommunicationValue::new(CommunicationType::ErrorNotFound) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::SettingsName, - DataValue::Str(settings_name.to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - self.send_message(&response).await; - return; - } - - let settings_value_str = load_file(&settings_path, &settings_file); - let response = CommunicationValue::new(CommunicationType::SettingsLoad) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Payload, DataValue::Str(settings_value_str)) - .add_typed_default( - DataType::SettingsName, - DataValue::Str(settings_name.to_string()), - ) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - - self.send_message(&response).await; - return; - } - - if cv.is_type(CommunicationType::SettingsList) { - let my_id = cv.get_sender(); - let Some(session_id) = cv.get_data(DataType::SessionId).as_number() else { - let response = CommunicationValue::new(CommunicationType::ErrorInvalidData) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default( - DataType::Message, - DataValue::Str("Missing session_id".to_string()), - ); - self.send_message(&response).await; - return; - }; - - let settings = get_children(&format!("users/{}/settings/{}/", my_id, session_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::SettingsList) - .with_id(cv.get_id()) - .with_receiver(my_id) - .add_typed_default(DataType::Settings, DataValue::Array(settings_json)) - .add_typed_default( - DataType::SessionId, - DataValue::SignedNumber(session_id as i128), - ); - - self.send_message(&response).await; - return; - } + async fn handle_settings_list(self: Arc, cv: &CommunicationValue) { + let _ = self + .send_message(&message_handlers::handle_settings_list(cv, 0)) + .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> { + pub async fn send_message(&self, cv: &CommunicationValue) -> Result<(), String> { let sender_guard = self.sender.read().await; if let Some(sender) = sender_guard.as_ref() { if !sender.is_open() { @@ -2216,24 +1808,21 @@ impl OmikronConnection { cv: &CommunicationValue, timeout_duration: Option, ) -> Result { - let (tx, mut rx) = mpsc::channel(1); + let (tx, rx) = oneshot::channel(); 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; - }); + let _ = tx.send(response_cv); true }), inserted_at: Instant::now(), }, ); - if let Err(send_err) = self.send_message_result(cv).await { + if let Err(send_err) = self.send_message(cv).await { WAITING_TASKS.remove(&msg_id); return Err(format!( "Request send failed (msg_id={}, reason={})", @@ -2243,8 +1832,8 @@ impl OmikronConnection { let timeout = timeout_duration.unwrap_or(Duration::from_secs(10)); - match tokio::time::timeout(timeout, rx.recv()).await { - Ok(Some(response_cv)) => { + match tokio::time::timeout(timeout, rx).await { + Ok(Ok(response_cv)) => { let is_error = response_cv.is_type(CommunicationType::Error) || response_cv.is_type(CommunicationType::ErrorInternal) || response_cv.is_type(CommunicationType::ErrorNotFound) @@ -2265,7 +1854,7 @@ impl OmikronConnection { Ok(response_cv) } } - Ok(_) => { + Ok(Err(_)) => { WAITING_TASKS.remove(&msg_id); Err("Channel closed while awaiting response".to_string()) } @@ -2284,27 +1873,32 @@ impl OmikronConnection { } pub async fn await_connection(&self, timeout_duration: Option) -> Result<(), String> { - if self.state.read().await.is_connected() { + let mut rx = self.state_watch_tx.subscribe(); + if rx.borrow().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(()); + let result: Result<(), String> = tokio::time::timeout(timeout, async { + loop { + rx.changed() + .await + .map_err(|_| "State watch channel closed".to_string())?; + if rx.borrow().is_connected() { + return Ok(()); + } } + }) + .await + .map_err(|_| { + format!( + "Connection not established within {} seconds", + timeout.as_secs() + ) + })?; - if start.elapsed() >= timeout { - return Err(format!( - "Connection not established within {} seconds", - timeout.as_secs() - )); - } - - sleep(Duration::from_millis(100)).await; - } + result } pub async fn has_auth_failure(&self) -> bool { @@ -2345,3 +1939,29 @@ pub async fn get_omikron_connection() -> Arc { conn.connect().await; conn } + +impl iota_connection::connection_handler::ConnectionHandler for OmikronConnection { + async fn send_message(&self, cv: &CommunicationValue) -> Result<(), String> { + OmikronConnection::send_message(self, cv).await + } + + async fn await_response( + &self, + cv: &CommunicationValue, + timeout: Option, + ) -> Result { + OmikronConnection::await_response(self, cv, timeout).await + } + + async fn is_connected(&self) -> bool { + OmikronConnection::is_connected(self).await + } + + async fn is_identified(&self) -> bool { + OmikronConnection::is_identified(self).await + } + + async fn stop(&self) { + OmikronConnection::stop(self).await + } +} diff --git a/omikron-connector/src/ping_pong_task.rs b/omikron-connector/src/ping_pong_task.rs index 9c81c60..2155b6c 100644 --- a/omikron-connector/src/ping_pong_task.rs +++ b/omikron-connector/src/ping_pong_task.rs @@ -2,6 +2,7 @@ use crate::omikron_connection::OmikronConnection; use dashmap::DashMap; use iota_state::APP_STATE; use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; +use std::sync::atomic::Ordering; use std::sync::LazyLock; use std::time::Instant; use tokio::time::Duration; @@ -16,6 +17,8 @@ impl OmikronConnection { PING_TIMES.retain(|_, v| v.elapsed() < Duration::from_secs(30)); + self.missed_pongs.fetch_add(1, Ordering::Relaxed); + let ping_message = CommunicationValue::new(CommunicationType::Ping) .with_id(id) .add_typed_default( @@ -23,10 +26,12 @@ impl OmikronConnection { DataValue::Array(vec![DataValue::SignedNumber(*self.last_ping.lock().await as i128)]), ); - self.send_message(&ping_message).await; + let _ = self.send_message(&ping_message).await; } pub async fn handle_pong(&self, cv: &CommunicationValue) { + self.missed_pongs.store(0, Ordering::Relaxed); + let id = cv.get_id(); if let Some((_, send_time)) = PING_TIMES.remove(&id) { diff --git a/type-maps.yaml b/type-maps.yaml index dddd7a2..ce3d5e6 100644 --- a/type-maps.yaml +++ b/type-maps.yaml @@ -148,6 +148,7 @@ type_maps: MessageReactionAdd: 146 MessageReactionRemove: 147 MessageReactionLive: 148 + MessageDeleteLive: 150 DataTypes: ErrorType: 32 ErrorProtocol: 33 @@ -169,7 +170,7 @@ type_maps: CallState: 49 ScreenShare: 50 PrivateKeyHash: 51 - Accepted: 52 + # Accepted: 52 now part of default MTP AcceptedProfiles: 53 DeniedProfiles: 54 Content: 55 diff --git a/web-server/Cargo.toml b/web-server/Cargo.toml index 74de956..9827408 100644 --- a/web-server/Cargo.toml +++ b/web-server/Cargo.toml @@ -5,4 +5,10 @@ version = "0.1.0" edition = "2024" [dependencies] -mtp = { git = "https://git.methanium.net/Methanium/mtp.git" } +mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-server"] } +bytes = "1" +http = "1" +iota-state = { path = "../iota-state" } +iota-util = { path = "../iota-util" } +iota-logger = { path = "../iota-logger" } +tokio = { version = "1.50.0", features = ["full"] } diff --git a/web-server/src/lib.rs b/web-server/src/lib.rs index e886571..4dc18ab 100644 --- a/web-server/src/lib.rs +++ b/web-server/src/lib.rs @@ -1,6 +1,133 @@ -// The web server is a TTP host & identification system, -// it "upgrades" connections after identification to -// -// either Own User (Cut down version of the Omikron Connection), -// or Community (Custom Connection), -// or Iota (Custom Connection). +use bytes::Bytes; +use iota_logger::log; +use iota_state::{ACTIVE_TASKS, SHUTDOWN}; +use iota_util::file_util::load_file_vec; +use mtp::host::HostConfig; +use mtp::webserver::{Http3Request, Http3Response, MTPWebServer, WebServerConfig}; +use std::net::{IpAddr, Ipv4Addr}; +use tokio::time::{Duration, sleep}; + +const CERT_PATH: &str = "certs/cert.pem"; +const KEY_PATH: &str = "certs/cert.key"; + +async fn root(_request: Http3Request, response: Http3Response) -> Http3Response { + static_file("index.html", response).await +} + +async fn static_file(path: &str, response: Http3Response) -> Http3Response { + let file = path.trim_start_matches('/'); + let file = if file.is_empty() { "index.html" } else { file }; + + if file.split('/').any(|component| component == "..") { + return response + .status(http::StatusCode::BAD_REQUEST) + .body("invalid path"); + } + + let path = std::path::Path::new("web").join(file); + let Some(parent) = path.parent().and_then(|path| path.to_str()) else { + return response + .status(http::StatusCode::NOT_FOUND) + .body("not found"); + }; + let Some(name) = path.file_name().and_then(|name| name.to_str()) else { + return response + .status(http::StatusCode::NOT_FOUND) + .body("not found"); + }; + + match load_file_vec(parent, name) { + Ok(body) => response + .status(http::StatusCode::OK) + .header("content-type", content_type(name)) + .body(Bytes::from(body)), + Err(_) => response + .status(http::StatusCode::NOT_FOUND) + .body("not found"), + } +} + +fn content_type(name: &str) -> &'static str { + match std::path::Path::new(name) + .extension() + .and_then(|ext| ext.to_str()) + { + Some("html") => "text/html; charset=utf-8", + Some("css") => "text/css; charset=utf-8", + Some("js") => "application/javascript; charset=utf-8", + Some("json") => "application/json", + Some("png") => "image/png", + Some("ico") => "image/x-icon", + Some("woff2") => "font/woff2", + _ => "application/octet-stream", + } +} + +pub async fn start(port: u16) -> bool { + let certificate = match tokio::fs::read(CERT_PATH).await { + Ok(certificate) => certificate, + Err(error) => { + log!("MTP web server certificate load failed: {}", error); + return false; + } + }; + let key = match tokio::fs::read(KEY_PATH).await { + Ok(key) => key, + Err(error) => { + log!("MTP web server key load failed: {}", error); + return false; + } + }; + + let host_config = HostConfig::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port, certificate, key); + let web_config = match WebServerConfig::new().route("/", root).and_then(|config| { + config.fallback(|request, response| async move { + static_file(request.uri.path(), response).await + }) + }) { + Ok(config) => config, + Err(error) => { + log!("MTP web server route setup failed: {}", error); + return false; + } + }; + + let mut server = match MTPWebServer::new(host_config, web_config).await { + Ok(server) => server, + Err(error) => { + log!("MTP web server startup failed: {}", error); + return false; + } + }; + + log!("MTP web server running on port {}", port); + tokio::spawn(async move { + ACTIVE_TASKS.insert("WebServer".into()); + loop { + tokio::select! { + result = server.accept() => { + match result { + Ok(Some(_connection)) => {} + Ok(None) => break, + Err(error) => log!("MTP webserver connection failed: {}", error), + } + } + _ = wait_for_shutdown() => { + server.shutdown().await; + break; + } + } + } + ACTIVE_TASKS.remove("WebServer"); + }); + true +} + +async fn wait_for_shutdown() { + loop { + if *SHUTDOWN.read().await { + break; + } + sleep(Duration::from_millis(100)).await; + } +}