diff --git a/Cargo.lock b/Cargo.lock index cc8ec44..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" @@ -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" @@ -2180,7 +2258,7 @@ dependencies = [ "mtp", "once_cell", "r2d2", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -2190,7 +2268,7 @@ dependencies = [ "serde_yaml", "sha2 0.10.9", "sysinfo", - "thiserror 2.0.18", + "thiserror 2.0.19", "tokio", "uuid", "walkdir", @@ -2245,7 +2323,6 @@ dependencies = [ "base64", "hex", "mtp", - "mtp-crypto", "reqwest", "sysinfo", "tokio", @@ -2315,7 +2392,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror 2.0.19", "walkdir", "windows-link", ] @@ -2330,7 +2407,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2349,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]] @@ -2387,7 +2464,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.18", + "thiserror 2.0.19", ] [[package]] @@ -2461,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]] @@ -2516,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", ] @@ -2604,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", @@ -2669,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", @@ -2678,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", @@ -2730,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" @@ -2806,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", @@ -2862,7 +2975,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2898,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]] @@ -2913,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" @@ -2933,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", @@ -2962,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", @@ -2976,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", @@ -2992,7 +3106,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3051,7 +3165,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -3094,7 +3208,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3185,7 +3299,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3224,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]] @@ -3237,7 +3351,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3266,7 +3380,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3291,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", ] @@ -3346,7 +3460,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3417,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" @@ -3453,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", ] @@ -3468,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", @@ -3488,6 +3603,7 @@ checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", + "fastbloom", "getrandom 0.4.3", "lru-slab", "rand 0.10.2", @@ -3496,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", @@ -3512,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", ] @@ -3551,25 +3668,15 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - [[package]] name = "rand" version = "0.10.2" @@ -3591,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" @@ -3616,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" @@ -3662,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", @@ -3672,7 +3760,7 @@ dependencies = [ "palette", "serde", "strum 0.28.0", - "thiserror 2.0.18", + "thiserror 2.0.19", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -3727,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", @@ -3762,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", @@ -3779,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", @@ -3859,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]] @@ -3868,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", @@ -3907,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", @@ -3916,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", @@ -4057,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", @@ -4093,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", @@ -4103,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]] @@ -4161,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", @@ -4282,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", @@ -4332,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", @@ -4357,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]] @@ -4420,7 +4508,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4432,7 +4520,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4454,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", @@ -4480,7 +4579,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4503,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", ] @@ -4537,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", @@ -4573,7 +4672,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.13.0", + "bitflags 2.13.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -4618,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]] @@ -4633,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]] @@ -4691,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", ] @@ -4706,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", @@ -4716,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]] @@ -4754,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", @@ -4800,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", @@ -4844,7 +4943,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4864,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", @@ -4874,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]] @@ -4986,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", @@ -5120,7 +5219,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5137,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]] @@ -5189,7 +5294,7 @@ dependencies = [ "once_cell", "open", "pnet", - "rand 0.8.6", + "rand 0.8.7", "rand_core 0.6.4", "ratatui", "reqwest", @@ -5213,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", ] @@ -5376,7 +5481,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5387,7 +5492,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5552,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", @@ -5570,7 +5675,7 @@ checksum = "d5867c629e4252f7439d82315923daaf27f4fa442410d51b78ab93ef4c432a11" dependencies = [ "httlib-huffman", "octets", - "thiserror 2.0.18", + "thiserror 2.0.19", "url", ] @@ -5612,7 +5717,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror 2.0.19", "time", ] @@ -5645,7 +5750,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5666,7 +5771,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5686,7 +5791,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5707,7 +5812,7 @@ checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5740,7 +5845,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5763,7 +5868,7 @@ dependencies = [ "memchr", "pbkdf2", "ppmd-rust", - "sha1 0.10.6", + "sha1 0.10.7", "time", "zeroize", "zopfli", @@ -5772,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 f1474ec..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,126 +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 { - let mut msg_container = Vec::new(); - msg_container.push(( - DataType::SendTime, - DataValue::SignedNumber(m.message_time as i128), - )); - msg_container.push((DataType::Content, DataValue::Str(m.content.clone()))); - msg_container.push((DataType::MessageState, DataValue::Str(m.message_state.clone()))); - msg_container.push((DataType::Height, DataValue::SignedNumber(m.height as i128))); - msg_container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if m.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 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)); - } - - 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; } @@ -562,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. @@ -603,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() { @@ -732,175 +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 { - let sender_id: i64 = if m.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 mut container = Vec::new(); - container.push(( - DataType::SendTime, - DataValue::SignedNumber(m.message_time as i128), - )); - container.push((DataType::Content, DataValue::Str(m.content.clone()))); - container.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - container.push((DataType::MessageState, DataValue::Str(m.message_state.clone()))); - container.push((DataType::Height, DataValue::SignedNumber(m.height as i128))); - container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if m.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(); - 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)); - } - - 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; } @@ -909,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) @@ -926,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) @@ -942,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) @@ -997,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-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/src/users/user_manager.rs b/iota-storage/src/users/user_manager.rs index 68afe5e..67c4e6a 100644 --- a/iota-storage/src/users/user_manager.rs +++ b/iota-storage/src/users/user_manager.rs @@ -3,8 +3,8 @@ 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 rusqlite::params; use rand_core::{OsRng, RngCore}; +use rusqlite::params; pub fn add_user(user: UserProfile) { if let Err(e) = db::with_db(|conn| { @@ -166,9 +166,8 @@ pub fn get_users() -> Vec { 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 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)?)) })?; @@ -191,7 +190,10 @@ fn load_trusted_apps(user_id: i64) -> std::collections::HashMap 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 trusted_apps WHERE user_id = ?1", + params![user_id], + )?; conn.execute("DELETE FROM users WHERE user_id = ?1", params![user_id])?; Ok(()) }) { diff --git a/iota-storage/src/util/chat_files.rs b/iota-storage/src/util/chat_files.rs index 4aba87d..b09d750 100644 --- a/iota-storage/src/util/chat_files.rs +++ b/iota-storage/src/util/chat_files.rs @@ -1,7 +1,7 @@ +use crate::storage_error::StorageError; use crate::util::db; use iota_logger::log; use rusqlite::params; -use crate::storage_error::StorageError; #[derive(PartialEq, Debug, Clone)] pub enum MessageState { @@ -53,7 +53,13 @@ pub struct StoredMessage { pub message_state: String, pub height: i64, pub reply_to: Option, - pub reactions: Vec, + pub reactions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StoredReaction { + pub reaction: String, + pub user_id: i64, } /* @@ -66,6 +72,48 @@ pub fn edit_message( 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( @@ -86,11 +134,16 @@ pub fn edit_message( )?; let (msg_id, old_content, sent_by_self) = msg; - if sent_by_self != 1 { + 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) @@ -118,7 +171,11 @@ pub fn edit_message( }) } -pub fn hard_delete_message(storage_owner: i64, external_user: i64, message_time: i64) -> Result<(), StorageError> { +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#" @@ -130,18 +187,79 @@ pub fn hard_delete_message(storage_owner: i64, external_user: i64, 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 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> { +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#" @@ -163,7 +281,11 @@ pub fn flag_deleted_by_external(storage_owner: i64, external_user: i64, message_ * 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> { +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#" @@ -175,7 +297,10 @@ pub fn delete_edit_history(storage_owner: i64, external_user: i64, message_time: |row| row.get(0), )?; - conn.execute("DELETE FROM message_edits WHERE message_id = ?1", params![msg_id])?; + conn.execute( + "DELETE FROM message_edits WHERE message_id = ?1", + params![msg_id], + )?; Ok(()) }) } @@ -339,26 +464,39 @@ pub fn change_message_state( .map_err(|e: StorageError| std::io::Error::new(std::io::ErrorKind::Other, e.to_string())) } -fn load_reactions(conn: &rusqlite::Connection, msg_ids: &[i64]) -> std::collections::HashMap> { +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() + let placeholders: Vec = msg_ids + .iter() + .enumerate() .map(|(i, _)| format!("?{}", i + 1)) .collect(); let query = format!( - "SELECT message_id, reaction || ':' || COUNT(*) FROM reactions WHERE message_id IN ({}) GROUP BY message_id, reaction", + "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(); + 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() + 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)?, row.get::<_, String>(1)?)) + 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); diff --git a/iota-storage/src/util/db.rs b/iota-storage/src/util/db.rs index 3263f68..dfe9d2d 100644 --- a/iota-storage/src/util/db.rs +++ b/iota-storage/src/util/db.rs @@ -64,6 +64,30 @@ fn db_file_path(db_name: &str) -> String { 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); @@ -78,8 +102,7 @@ fn run_migrations(pool: &r2d2::Pool) -> Result<(), StorageError> 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 + message_state TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_messages_lookup ON messages (storage_owner, external_user, message_time DESC); @@ -129,29 +152,28 @@ fn run_migrations(pool: &r2d2::Pool) -> Result<(), StorageError> } if current_version < 2 { - conn.execute_batch( - r#" - ALTER TABLE messages ADD COLUMN height INTEGER NOT NULL DEFAULT 0; - PRAGMA user_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 { - conn.execute_batch( - r#" - ALTER TABLE messages ADD COLUMN reply_to INTEGER; - PRAGMA user_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#" - ALTER TABLE messages ADD COLUMN edited_count INTEGER NOT NULL DEFAULT 0; - ALTER TABLE messages ADD COLUMN deleted_by_external INTEGER NOT NULL DEFAULT 0; - CREATE TABLE IF NOT EXISTS message_edits ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL REFERENCES messages(id), @@ -179,6 +201,24 @@ fn run_migrations(pool: &r2d2::Pool) -> Result<(), StorageError> )?; } + 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(()) } @@ -221,3 +261,40 @@ where pub fn create_general_messages_db() -> Result>, String> { create_shared_connection(DB_NAME, "") } + +#[cfg(test)] +mod tests { + use super::*; + + #[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; + "#, + )?; + + 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 984c9dd..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::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, oneshot, watch, Semaphore}; +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 @@ -451,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), @@ -460,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( @@ -496,7 +361,8 @@ impl OmikronConnection { let sender_arc = Arc::new(connection.sender); *self.sender.write().await = Some(sender_arc.clone()); - self.set_state(ConnectionState::Connected { identified: true }).await; + self.set_state(ConnectionState::Connected { identified: true }) + .await; // Start read loop let mut receiver = connection.receiver; @@ -554,7 +420,7 @@ 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; } @@ -566,12 +432,13 @@ impl OmikronConnection { "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 + 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); } @@ -815,20 +682,32 @@ impl OmikronConnection { ) -> Option { let mut msg_fields = vec![ (DataType::Content, DataValue::Str(content.to_string())), - (DataType::SendTime, DataValue::SignedNumber(timestamp as i128)), + ( + 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))); + 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::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 { + match self + .await_response(&user_forward, Some(Duration::from_secs(3))) + .await + { Ok(user_resp) => { let ms_raw = user_resp .get_data(DataType::MessageState) @@ -856,7 +735,10 @@ impl OmikronConnection { .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)); + .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, @@ -864,7 +746,10 @@ impl OmikronConnection { ); } - match self.await_response(&fw_msg, Some(Duration::from_secs(10))).await { + match self + .await_response(&fw_msg, Some(Duration::from_secs(10))) + .await + { Ok(resp) => { let ms_raw = resp .get_data(DataType::MessageState) @@ -872,34 +757,58 @@ impl OmikronConnection { .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 _ = + 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; + 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, + 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; + 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; } } } @@ -951,6 +860,12 @@ impl OmikronConnection { 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); @@ -974,7 +889,9 @@ impl OmikronConnection { let recipients = match chat_secret_recipients(cv) { Some(recipients) => recipients, None => { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorInvalidData)).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; return; } }; @@ -988,7 +905,9 @@ impl OmikronConnection { 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; + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; return; }; @@ -1012,18 +931,16 @@ impl OmikronConnection { }) .is_err() { - let _ = self.send_message(&error_response( - cv, - CommunicationType::ErrorInvalidData, - )).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; return; } continue; } if recipient.user_id != sender_id { - non_local_forwards - .push(set_chat_secret_cv_for_recipient(cv, recipient)); + non_local_forwards.push(set_chat_secret_cv_for_recipient(cv, recipient)); } } @@ -1045,68 +962,15 @@ impl OmikronConnection { } } - let _ = self.send_message(&error_response(cv, CommunicationType::Success)).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; } async fn handle_get_chat_secret(self: Arc, cv: &CommunicationValue) { - let Some(user_id) = data_string(cv, DataType::UserId) else { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorInvalidData)).await; - return; - }; - if user_id != cv.get_sender().to_string() { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorNotFound)).await; - return; - } - let Some(chat_id) = data_string(cv, DataType::ChatId) else { - let _ = 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), - ); - let _ = self.send_message(&response).await; - } - Ok(None) => { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorNotSet)).await; - } - Err(_) => { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorInvalidData)).await; - } - } + let _ = self + .send_message(&message_handlers::handle_get_chat_secret(cv)) + .await; } async fn handle_chat_secret_forward(self: Arc, cv: &CommunicationValue) { @@ -1116,7 +980,9 @@ impl OmikronConnection { || recipient_user_id.is_empty() || pending_chat_secret_forward_from_cv(cv).is_none() { - let _ = self.send_message(&error_response(cv, CommunicationType::ErrorInvalidData)).await; + let _ = self + .send_message(&error_response(cv, CommunicationType::ErrorInvalidData)) + .await; return; } @@ -1124,10 +990,14 @@ impl OmikronConnection { .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; + 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; + let _ = self + .send_message(&error_response(cv, CommunicationType::Success)) + .await; } } @@ -1158,7 +1028,8 @@ impl OmikronConnection { 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())); + 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) @@ -1199,10 +1070,9 @@ impl OmikronConnection { 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 res = CommunicationValue::new(CommunicationType::AppIdentificationResponse) + .with_id(cv.get_id()) + .with_receiver(sender_id); let _ = self.send_message(&res).await; return; } @@ -1224,11 +1094,7 @@ impl OmikronConnection { 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, - ); + iota_storage::users::user_manager::save_app_data(*user_id, app_identifier, &app_data); } let res = CommunicationValue::new(CommunicationType::SaveAppData) @@ -1243,8 +1109,7 @@ impl OmikronConnection { 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); + app_data = iota_storage::users::user_manager::load_app_data(*user_id, app_identifier); } let res = CommunicationValue::new(CommunicationType::LoadAppData) @@ -1255,147 +1120,281 @@ impl OmikronConnection { } async fn handle_create_app(self: Arc, cv: &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); - } - } - } - - let res = CommunicationValue::new(CommunicationType::CreateApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - let _ = self.send_message(&res).await; + let _ = self + .send_message(&message_handlers::handle_create_app(cv)) + .await; } async fn handle_delete_app(self: Arc, cv: &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); - } - } - } - - let res = CommunicationValue::new(CommunicationType::DeleteApp) - .with_id(cv.get_id()) - .with_receiver(sender_id as u64); - let _ = self.send_message(&res).await; + let _ = self + .send_message(&message_handlers::handle_delete_app(cv)) + .await; } async fn handle_client_connected(self: Arc, cv: &CommunicationValue) { - 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 { - let mut msg_container = Vec::new(); - msg_container.push(( - DataType::SendTime, - DataValue::SignedNumber(m.message_time as i128), - )); - msg_container.push((DataType::Content, DataValue::Str(m.content.clone()))); - msg_container.push((DataType::MessageState, DataValue::Str(m.message_state.clone()))); - msg_container.push((DataType::Height, DataValue::SignedNumber(m.height as i128))); - msg_container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if m.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 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)); - } - - let resp = CommunicationValue::new(CommunicationType::ClientConnected) - .with_id(cv.get_id()) - .add_typed_default(DataType::Contacts, DataValue::Array(contacts_array)); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_client_connected(cv)) + .await; } async fn handle_message_state(self: Arc, cv: &CommunicationValue) { - let sender_id = &cv.get_sender(); - let receiver_id = match cv.get_data(DataType::ChatPartnerId).as_number() { - Some(id) => id, + message_handlers::handle_message_state(cv); + } + + 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 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 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; + } + } - 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("")), + 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; + } + + 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; } async fn handle_message_send(self: Arc, cv: &CommunicationValue) { @@ -1496,24 +1495,22 @@ impl OmikronConnection { if is_read_receipts_enabled().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()), - ), + &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; } @@ -1531,36 +1528,31 @@ impl OmikronConnection { receiver_id, MessageState::Sent, ); - let push_msg = - CommunicationValue::new(CommunicationType::PushNotification) - .with_receiver(receiver_id as u64) - .add_typed_default( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - ); + let push_msg = CommunicationValue::new(CommunicationType::PushNotification) + .with_receiver(receiver_id as u64) + .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 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(), + &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; } @@ -1620,24 +1612,26 @@ impl OmikronConnection { ); if is_read_receipts_enabled().await { - 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; + 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 => { @@ -1648,524 +1642,102 @@ impl OmikronConnection { 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 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) - .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; + 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(*receiver_id as i128), + ) + .add_typed_default( + DataType::MessageState, + DataValue::Str(MessageState::Sent.as_str().to_string()), + ), + ) + .await; } } } async fn handle_messages_get(self: Arc, cv: &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 { - let sender_id: i64 = if m.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 mut container = Vec::new(); - container.push(( - DataType::SendTime, - DataValue::SignedNumber(m.message_time as i128), - )); - container.push((DataType::Content, DataValue::Str(m.content.clone()))); - container.push(( - DataType::SenderId, - DataValue::SignedNumber(sender_id as i128), - )); - container.push((DataType::MessageState, DataValue::Str(m.message_state.clone()))); - container.push((DataType::Height, DataValue::SignedNumber(m.height as i128))); - container.push(( - DataType::SenderId, - DataValue::UnsignedNumber(if m.sent_by_self { - my_id as u128 - } else { - partner_id as u128 - }), - )); - if let Some(rt) = m.reply_to { - container.push(( - DataType::ReplyId, - DataValue::UnsignedNumber(rt as u64 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)); - - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_messages_get(cv)) + .await; } async fn handle_get_chats(self: Arc, cv: &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)); - } - let resp = CommunicationValue::new(CommunicationType::GetChats) - .with_id(cv.get_id()) - .with_receiver(user_id) - .add_typed_default(DataType::UserIds, DataValue::Array(user_array)); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_get_chats(cv)) + .await; } async fn handle_add_conversation(self: Arc, cv: &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(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); - let resp = CommunicationValue::new(CommunicationType::AddConversation) - .with_id(cv.get_id()) - .with_receiver(user_id); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_add_conversation(cv)) + .await; } async fn handle_add_community(self: Arc, cv: &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(), - ); - let resp = CommunicationValue::new(CommunicationType::AddCommunity) - .with_id(cv.get_id()) - .with_receiver(cv.get_sender()); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_add_community(cv)) + .await; } async fn handle_get_communities(self: Arc, cv: &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)); - } - - 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)); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_get_communities(cv)) + .await; } async fn handle_remove_community(self: Arc, cv: &CommunicationValue) { - 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()); - let _ = self.send_message(&resp).await; + let _ = self + .send_message(&message_handlers::handle_remove_community(cv)) + .await; } async fn handle_global_settings_save(self: Arc, cv: &CommunicationValue) { - 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()), - ); - let _ = 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), - ); - } - - let _ = self.send_message(&response).await; + let _ = self + .send_message(&message_handlers::handle_global_settings_save(cv)) + .await; } async fn handle_global_settings_load(self: Arc, cv: &CommunicationValue) { - 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), - ); - } - - let _ = 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), - ); - } - - let _ = self.send_message(&response).await; + let _ = self + .send_message(&message_handlers::handle_global_settings_load(cv)) + .await; } async fn handle_settings_save(self: Arc, cv: &CommunicationValue) { - 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()), - ); - let _ = self.send_message(&response).await; - return; - }; - if session_id == 0 || session_id > 1_000_000 { - let response = 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 _ = 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), - ); - let _ = 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), - ); - let _ = 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), - ); - let _ = 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), - ); - - let _ = self.send_message(&response).await; + let _ = self + .send_message(&message_handlers::handle_settings_save(cv, 0)) + .await; } async fn handle_settings_load(self: Arc, cv: &CommunicationValue) { - 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()), - ); - let _ = self.send_message(&response).await; - return; - }; - if session_id == 0 || session_id > 1_000_000 { - let response = 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()), - ); - let _ = 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), - ); - let _ = 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), - ); - let _ = 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), - ); - let _ = 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), - ); - - let _ = self.send_message(&response).await; + let _ = self + .send_message(&message_handlers::handle_settings_load(cv, 0)) + .await; } async fn handle_settings_list(self: Arc, cv: &CommunicationValue) { - 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()), - ); - let _ = self.send_message(&response).await; - return; - }; - if session_id == 0 || session_id > 1_000_000 { - let response = 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()), - ); - let _ = 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), - ); - - let _ = self.send_message(&response).await; + let _ = self + .send_message(&message_handlers::handle_settings_list(cv, 0)) + .await; } // ------------------------------------------------------------------------- @@ -2310,7 +1882,9 @@ impl OmikronConnection { let result: Result<(), String> = tokio::time::timeout(timeout, async { loop { - rx.changed().await.map_err(|_| "State watch channel closed".to_string())?; + rx.changed() + .await + .map_err(|_| "State watch channel closed".to_string())?; if rx.borrow().is_connected() { return Ok(()); } @@ -2365,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/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; + } +}