diff --git a/.env b/.env new file mode 100644 index 0000000..9829081 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +API_ID=36457397 +API_HASH=f74f670f33f3fa30a89b46c58dac2ff7 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb54683 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# Telegram TUI + +## Prompt + +Проект - TUI интерфейс для телеграмма + +Порядок чтения: +1) DEVELOPMENT.md - правило работы (обязательно) +2) CONTEXT.md - текущий статус +3) ROADMAP.md - план и задачи +4) REQUIREMENTS.md / ARCHITECTURE.md - по необходимости +5) E2E_TESTS.md - перед написанием тестов + +После работы обнови CONTEXT.md файл + +После прочтения скажи "Жду инструкций" +--- + +## Важные файлы + +- [DEVELOPMENT.md](DEVELOPMENT.md) — **читай первым!** Правила локальной разработки +- [CONTEXT.md](CONTEXT.md) — текущий статус, что сделано +- [ROADMAP.md](ROADMAP.md) — план разработки, задачи по фазам +- [REQUIREMENTS.md](REQUIREMENTS.md) — требования к продукту +- [ARCHITECTURE.md](ARCHITECTURE.md) — C4, sequence diagrams, API контракты, UI прототипы +- [E2E_TESTING.md](E2E_TESTING.md) — **читай перед написанием тестов!** Гайд по e2e тестированию + + + diff --git a/Cargo.lock b/Cargo.lock index 42f3989..8db8dc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,10 +35,19 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.100" +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" @@ -46,6 +55,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "2.10.0" @@ -61,27 +76,43 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -99,11 +130,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -113,12 +146,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.43" @@ -126,9 +153,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", - "js-sys", "num-traits", - "wasm-bindgen", + "serde", "windows-link", ] @@ -156,21 +182,28 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -180,6 +213,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -189,6 +237,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.28.1" @@ -199,7 +253,7 @@ dependencies = [ "crossterm_winapi", "mio", "parking_lot", - "rustix", + "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", @@ -224,14 +278,38 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", ] [[package]] @@ -247,13 +325,51 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", "quote", "syn", ] @@ -270,21 +386,64 @@ dependencies = [ ] [[package]] -name = "dispatch2" -version = "0.3.0" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "bitflags", - "objc2", + "dirs-sys", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -302,10 +461,16 @@ dependencies = [ ] [[package]] -name = "find-msvc-tools" -version = "0.1.7" +name = "fastrand" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "flate2" @@ -317,18 +482,70 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + [[package]] name = "futures-task" version = "0.3.31" @@ -342,9 +559,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -369,130 +590,43 @@ dependencies = [ ] [[package]] -name = "glass_pumpkin" -version = "1.9.0" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41923edfeadce2964192d185f7f9f9e5d8737e6f5b8ee2580b0f02f717e81bd0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "core2", - "num-bigint", - "num-integer", - "num-traits", - "once_cell", - "rand_core", + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", ] [[package]] -name = "grammers-client" -version = "0.7.0" +name = "h2" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c97c2ce2ade23eb00a35d192929a678bd5b1650c7f36eea8a7f0ecd2c79c33" -dependencies = [ - "chrono", - "futures-util", - "grammers-crypto", - "grammers-mtproto", - "grammers-mtsender", - "grammers-session", - "grammers-tl-types", - "locate-locale", - "log", - "md5", - "mime_guess", - "os_info", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "grammers-crypto" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c75ce8d715d407a5767a94b5fd7b210106ec5c29c7be8ff4dfdca58de4dcf6" -dependencies = [ - "aes", - "getrandom", - "glass_pumpkin", - "hmac", - "num-bigint", - "num-traits", - "pbkdf2", - "sha1", - "sha2", -] - -[[package]] -name = "grammers-mtproto" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d057562ccd5ac7437683534634e469875db27827a7febd42cb6b775ffd01dff" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ + "atomic-waker", "bytes", - "crc32fast", - "flate2", - "getrandom", - "grammers-crypto", - "grammers-tl-types", - "log", - "num-bigint", - "sha1", -] - -[[package]] -name = "grammers-mtsender" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b077919dbbacab7644f8479f0c4456625714313334986b4f7c812691aac96a" -dependencies = [ - "bytes", - "futures-util", - "grammers-crypto", - "grammers-mtproto", - "grammers-tl-types", - "log", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "grammers-session" -version = "0.7.0" +name = "hashbrown" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d5fea440a79a78d0d68b57f062d737a0aed0decee813bf3c232252f3b3f7da" -dependencies = [ - "grammers-crypto", - "grammers-tl-gen", - "grammers-tl-parser", - "grammers-tl-types", - "log", -] - -[[package]] -name = "grammers-tl-gen" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c214cadfba8cd8ef9368bdf273f56f688537b0310ea42f91fc746f4fd59b70b9" -dependencies = [ - "grammers-tl-parser", -] - -[[package]] -name = "grammers-tl-parser" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949756519386578835cf5298befef465009accb0531d884eba6e2fc5ad8766e" -dependencies = [ - "crc32fast", -] - -[[package]] -name = "grammers-tl-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49566409a69e4ff3340c1b6f392e5754c89fa1ea43be4584ac8ee74c1eb0643a" -dependencies = [ - "grammers-tl-gen", - "grammers-tl-parser", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -505,12 +639,24 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -520,6 +666,125 @@ dependencies = [ "digest", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -544,12 +809,137 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "indoc" version = "2.0.7" @@ -574,13 +964,29 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", "syn", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itertools" version = "0.13.0" @@ -596,6 +1002,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -612,6 +1028,16 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -619,13 +1045,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] -name = "locate-locale" -version = "0.2.0" +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2835eaaed39a92511442aff277d4dca3d7674ca058df3bc45170661c2ccb4619" -dependencies = [ - "winapi", -] +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -648,14 +1077,29 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] -name = "md5" -version = "0.7.0" +name = "lzma-rs" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] [[package]] name = "memchr" @@ -669,16 +1113,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -702,36 +1136,27 @@ dependencies = [ ] [[package]] -name = "nix" -version = "0.30.1" +name = "native-tls" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", ] [[package]] -name = "num-bigint" -version = "0.4.6" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "rand", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" @@ -742,165 +1167,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "objc2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -908,20 +1174,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] -name = "os_info" -version = "3.14.0" +name = "openssl" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "android_system_properties", - "log", - "nix", - "objc2", - "objc2-foundation", - "objc2-ui-kit", - "windows-sys 0.61.2", + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", ] +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.5" @@ -961,6 +1262,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -973,6 +1280,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.105" @@ -992,22 +1320,10 @@ dependencies = [ ] [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "ratatui" @@ -1039,6 +1355,93 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "0.38.44" @@ -1048,10 +1451,56 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1064,12 +1513,68 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.228" @@ -1113,6 +1618,49 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1124,17 +1672,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "1.3.0" @@ -1178,6 +1715,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + [[package]] name = "smallvec" version = "1.15.1" @@ -1194,6 +1737,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1245,21 +1794,168 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tdlib-rs" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98c960258301bee0758a669fbe12ad8a97c6e764d2f30c5426eea008eebf2d2" +dependencies = [ + "dirs", + "futures-channel", + "log", + "once_cell", + "reqwest", + "serde", + "serde_json", + "serde_with", + "tdlib-rs-gen", + "tdlib-rs-parser", + "zip", +] + +[[package]] +name = "tdlib-rs-gen" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be6a2373951794ddcf612db2cd26fc67d9fb2721a1497e873c06bd87823fae80" +dependencies = [ + "tdlib-rs-parser", +] + +[[package]] +name = "tdlib-rs-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbdfae498e57fb48d380fff8eb5c9c98d4497c998f6de0d30d5d6b12f5358b" + [[package]] name = "tele-tui" version = "0.1.0" dependencies = [ - "anyhow", - "chrono", "crossterm", - "grammers-client", - "grammers-session", + "dotenvy", "ratatui", "serde", "serde_json", + "tdlib-rs", "tokio", ] +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.49.0" @@ -1288,18 +1984,115 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.22" @@ -1335,18 +2128,66 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.108" @@ -1360,6 +2201,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.108" @@ -1392,6 +2247,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1455,6 +2320,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -1473,6 +2349,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1630,7 +2515,195 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "zmij" -version = "1.0.14" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.13.0", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 6da0848..eb989ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,11 @@ edition = "2021" [dependencies] ratatui = "0.29" crossterm = "0.28" +tdlib-rs = { version = "1.1", features = ["download-tdlib"] } tokio = { version = "1", features = ["full"] } -grammers-client = "0.7" -grammers-session = "0.7" -anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -chrono = "0.4" +dotenvy = "0.15" + +[build-dependencies] +tdlib-rs = { version = "1.1", features = ["download-tdlib"] } diff --git a/docs/DEVELOPMENT.md b/DEVELOPMENT.md similarity index 100% rename from docs/DEVELOPMENT.md rename to DEVELOPMENT.md diff --git a/docs/README.md b/README.md similarity index 100% rename from docs/README.md rename to README.md diff --git a/docs/REQUIREMENTS.md b/REQUIREMENTS.md similarity index 94% rename from docs/REQUIREMENTS.md rename to REQUIREMENTS.md index 96a0c35..e1ee5e3 100644 --- a/docs/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -62,7 +62,10 @@ 4) "command + 1", "command + 2" и так далее - переключение между папками, которые созданы в телеграме 5) из интерфейса "**message**" - это инпут для ввода сообщения в открытый чат 6) ctrl + s - фокус в инпут поиска чата -7) `**commands**` - сюда вставь описания команд, которые есть в приложении +7) Esc - закрытие открытого чата +8) command + стрелка вверх (или ctrl + k) - выделяем самый верхний чат (без открытия) +9) поддержка русской раскладки: "р о л д" соответствует "h j k l" +10) `**commands**` - сюда вставь описания команд, которые есть в приложении ## Технологии Пишем на rust-е diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..cc8ba10 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + tdlib_rs::build::build(None); +} diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index 3f50b0a..0000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -1,20 +0,0 @@ -# Telegram TUI - -## Prompt - -Проект - TUI интерфейс для телеграмма - -Порядок чтения: -1) DEVELOPMENT.md - правило работы (обязательно) -2) CONTEXT.md - текущий статус -3) ROADMAP.md - план и задачи -4) REQUIREMENTS.md / ARCHITECTURE.md - по необходимости -5) E2E_TESTS.md - перед написанием тестов - -После работы обнови CONTEXT.md файл - -После прочтения скажи "Жду инструкций" - -## Архитектура -пока нет, никак не ограничиваю - diff --git a/docs/TDLIB_INTEGRATION.md b/docs/TDLIB_INTEGRATION.md new file mode 100644 index 0000000..b50c90d --- /dev/null +++ b/docs/TDLIB_INTEGRATION.md @@ -0,0 +1,636 @@ +# Интеграция TDLib в Telegram TUI + +## Обзор + +TDLib (Telegram Database Library) — это официальная кроссплатформенная библиотека для создания Telegram клиентов. Она предоставляет полный функционал Telegram API с автоматическим управлением сессиями, кэшированием и синхронизацией. + +## Выбор библиотеки для Rust + +Существует несколько Rust-оберток для TDLib: + +### 1. rust-tdlib +- **GitHub**: [antonio-antuan/rust-tdlib](https://github.com/antonio-antuan/rust-tdlib) +- **docs.rs**: https://docs.rs/rust-tdlib +- **Особенности**: + - Async/await с tokio + - Client/Worker архитектура + - Требует предварительной сборки TDLib + +### 2. tdlib-rs (Рекомендуется) +- **GitHub**: [FedericoBruzzone/tdlib-rs](https://github.com/FedericoBruzzone/tdlib-rs) +- **crates.io**: https://crates.io/crates/tdlib-rs +- **docs.rs**: https://docs.rs/tdlib/latest/tdlib/ +- **Преимущества**: + - ✅ Не требует предварительной установки TDLib + - ✅ Кроссплатформенность (Windows, Linux, macOS) + - ✅ Автоматическая загрузка прекомпилированных бинарников + - ✅ Поддержка TDLib v1.8.29 + - ✅ Автогенерация типов из TL схемы + +## Установка tdlib-rs + +### Вариант 1: Автоматическая загрузка (Рекомендуется) + +**Cargo.toml:** +```toml +[dependencies] +tdlib-rs = { version = "0.3", features = ["download-tdlib"] } +tokio = { version = "1", features = ["full"] } + +[build-dependencies] +tdlib-rs = { version = "0.3", features = ["download-tdlib"] } +``` + +**build.rs:** +```rust +fn main() { + tdlib_rs::build::build(None); +} +``` + +### Вариант 2: Локальная установка TDLib + +Если TDLib уже установлен (версия 1.8.29): + +```bash +export LOCAL_TDLIB_PATH=$HOME/lib/tdlib +``` + +```toml +[dependencies] +tdlib-rs = { version = "0.3", features = ["local-tdlib"] } +``` + +### Вариант 3: Через pkg-config + +```bash +export PKG_CONFIG_PATH=$HOME/lib/tdlib/lib/pkgconfig/:$PKG_CONFIG_PATH +export LD_LIBRARY_PATH=$HOME/lib/tdlib/lib/:$LD_LIBRARY_PATH +``` + +```toml +[dependencies] +tdlib-rs = { version = "0.3", features = ["pkg-config"] } +``` + +## Архитектура TDLib + +### Основные компоненты + +``` +┌─────────────────────────────────────────────────────────┐ +│ Your Application │ +├─────────────────────────────────────────────────────────┤ +│ ┌────────────┐ ┌──────────────┐ ┌────────────────┐ │ +│ │ Client │ │ Update Stream │ │ API Requests │ │ +│ └────────────┘ └──────────────┘ └────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ TDLib Client │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Auth State │ │ Local Cache │ │ API Handler │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ Telegram Servers │ +└─────────────────────────────────────────────────────────┘ +``` + +### Поток работы + +1. **Инициализация** → TDLib запускается с параметрами +2. **Авторизация** → Проход через стейт-машину авторизации +3. **Синхронизация** → Загрузка базовых данных (чаты, контакты) +4. **Updates Stream** → Постоянный поток обновлений от сервера +5. **API Requests** → Запросы на получение данных / отправку сообщений + +## Процесс авторизации + +### Стейт-машина авторизации + +TDLib работает через систему состояний. Приложение получает обновления `updateAuthorizationState` и реагирует на них: + +``` +authorizationStateWaitTdlibParameters + ↓ (вызываем setTdlibParameters) +authorizationStateWaitPhoneNumber + ↓ (вызываем setAuthenticationPhoneNumber) +authorizationStateWaitCode + ↓ (вызываем checkAuthenticationCode) +authorizationStateWaitPassword (опционально, если 2FA) + ↓ (вызываем checkAuthenticationPassword) +authorizationStateReady ✓ +``` + +### Шаг 1: Получение API ключей + +Перед началом работы нужно: +1. Зайти на https://my.telegram.org +2. Войти с номером телефона +3. Перейти в "API development tools" +4. Создать приложение и получить `api_id` и `api_hash` + +### Шаг 2: Инициализация TDLib + +```rust +use tdlib::{functions, types}; + +async fn init_tdlib() { + // Параметры инициализации + let params = types::TdlibParameters { + database_directory: "./tdlib_db".to_string(), + use_message_database: true, + use_secret_chats: true, + api_id: env::var("API_ID").unwrap().parse().unwrap(), + api_hash: env::var("API_HASH").unwrap(), + system_language_code: "en".to_string(), + device_model: "Desktop".to_string(), + system_version: "Unknown".to_string(), + application_version: "0.1.0".to_string(), + enable_storage_optimizer: true, + ignore_file_names: false, + }; + + // Отправляем параметры + functions::set_tdlib_parameters(params, &client).await?; +} +``` + +### Шаг 3: Ввод номера телефона + +```rust +async fn authenticate_with_phone(phone: String, client: &Client) { + let phone_number = types::SetAuthenticationPhoneNumber { + phone_number: phone, + settings: None, + }; + + functions::set_authentication_phone_number(phone_number, client).await?; +} +``` + +### Шаг 4: Ввод кода подтверждения + +```rust +async fn verify_code(code: String, client: &Client) { + let check_code = types::CheckAuthenticationCode { + code, + }; + + functions::check_authentication_code(check_code, client).await?; +} +``` + +### Шаг 5: Ввод пароля 2FA (если включен) + +```rust +async fn verify_password(password: String, client: &Client) { + let check_password = types::CheckAuthenticationPassword { + password, + }; + + functions::check_authentication_password(check_password, client).await?; +} +``` + +## Получение списка чатов + +### Концепция чатов в TDLib + +TDLib автоматически кэширует чаты локально. Приложение должно: +1. Подписаться на обновления `updateNewChat` +2. Вызвать `loadChats()` для загрузки чатов +3. Поддерживать локальный кэш с сортировкой + +### Типы списков чатов + +- **Main** — основные чаты +- **Archive** — архивные чаты +- **Folder** — пользовательские папки + +### Загрузка чатов + +```rust +use tdlib::{functions, types}; + +async fn load_chats(client: &Client) -> Result> { + // Указываем тип списка (Main, Archive, или конкретная папка) + let chat_list = types::ChatList::Main; + + // Загружаем чаты + // limit - количество чатов для загрузки + functions::load_chats( + types::LoadChats { + chat_list: Some(chat_list), + limit: 50, + }, + client + ).await?; + + // После вызова loadChats, чаты будут приходить через updateNewChat + Ok(vec![]) +} +``` + +### Получение информации о чате + +```rust +async fn get_chat_info(chat_id: i64, client: &Client) -> Result { + let chat = functions::get_chat( + types::GetChat { chat_id }, + client + ).await?; + + Ok(chat) +} +``` + +### Сортировка чатов + +Чаты нужно сортировать по паре `(position.order, chat.id)` в порядке убывания: + +```rust +chats.sort_by(|a, b| { + let order_a = a.positions.get(0).map(|p| p.order).unwrap_or(0); + let order_b = b.positions.get(0).map(|p| p.order).unwrap_or(0); + + order_b.cmp(&order_a) + .then_with(|| b.id.cmp(&a.id)) +}); +``` + +## Получение истории сообщений + +### Загрузка сообщений из чата + +```rust +async fn get_chat_history( + chat_id: i64, + from_message_id: i64, + limit: i32, + client: &Client +) -> Result> { + let history = functions::get_chat_history( + types::GetChatHistory { + chat_id, + from_message_id, // 0 для последних сообщений + offset: 0, + limit, + only_local: false, + }, + client + ).await?; + + Ok(history.messages.unwrap_or_default()) +} +``` + +### Пагинация сообщений + +Сообщения возвращаются в обратном хронологическом порядке (новые → старые). + +Для загрузки следующей страницы: +```rust +// Первая загрузка (последние сообщения) +let messages = get_chat_history(chat_id, 0, 50, &client).await?; + +// Загрузка более старых сообщений +if let Some(oldest_msg) = messages.last() { + let older_messages = get_chat_history( + chat_id, + oldest_msg.id, + 50, + &client + ).await?; +} +``` + +## Обработка обновлений (Updates Stream) + +### Типы обновлений + +TDLib отправляет обновления через `Update` enum: + +- `UpdateNewMessage` — новое сообщение +- `UpdateMessageContent` — изменение контента сообщения +- `UpdateMessageSendSucceeded` — сообщение успешно отправлено +- `UpdateMessageSendFailed` — ошибка отправки +- `UpdateChatLastMessage` — изменилось последнее сообщение чата +- `UpdateChatPosition` — изменилась позиция чата в списке +- `UpdateNewChat` — новый чат добавлен +- `UpdateUser` — обновилась информация о пользователе +- `UpdateUserStatus` — изменился статус пользователя (онлайн/оффлайн) +- `UpdateChatReadInbox` — прочитаны входящие сообщения +- `UpdateChatReadOutbox` — прочитаны исходящие сообщения + +### Слушатель обновлений + +```rust +use tdlib::types::Update; + +async fn handle_updates(client: Client) { + loop { + match client.receive() { + Some(Update::NewMessage(update)) => { + println!("New message in chat {}: {}", + update.message.chat_id, + update.message.content + ); + } + Some(Update::MessageSendSucceeded(update)) => { + println!("Message sent successfully: {}", update.message.id); + } + Some(Update::UserStatus(update)) => { + println!("User {} is now {:?}", + update.user_id, + update.status + ); + } + Some(Update::NewChat(update)) => { + println!("New chat added: {}", update.chat.title); + } + _ => {} + } + } +} +``` + +## Отправка сообщений + +### Отправка текстового сообщения + +```rust +async fn send_message( + chat_id: i64, + text: String, + client: &Client +) -> Result { + let input_content = types::InputMessageContent::InputMessageText( + types::InputMessageText { + text: types::FormattedText { + text, + entities: vec![], + }, + disable_web_page_preview: false, + clear_draft: true, + } + ); + + let message = functions::send_message( + types::SendMessage { + chat_id, + message_thread_id: 0, + reply_to: None, + options: None, + reply_markup: None, + input_message_content: input_content, + }, + client + ).await?; + + Ok(message) +} +``` + +### Статусы доставки и прочтения + +Для отображения ✓ и ✓✓: + +```rust +fn get_message_status(message: &types::Message) -> &str { + if message.is_outgoing { + match &message.sending_state { + Some(types::MessageSendingState::Pending) => "", // отправляется + Some(types::MessageSendingState::Failed(_)) => "✗", // ошибка + None => { + // Отправлено успешно + if message.chat_id > 0 { // личный чат + // Проверяем, прочитано ли + // (нужно следить за UpdateChatReadOutbox) + "✓✓" // или "✓" если не прочитано + } else { + "✓" // групповой чат + } + } + } + } else { + "" // входящее сообщение + } +} +``` + +## Работа с папками (Folders) + +### Получение списка папок + +```rust +async fn get_chat_folders(client: &Client) -> Result> { + let folders = functions::get_chat_folders( + types::GetChatFolders {}, + client + ).await?; + + Ok(folders.chat_folders) +} +``` + +### Фильтрация чатов по папке + +```rust +async fn get_chats_in_folder(folder_id: i32, client: &Client) { + let chat_list = types::ChatList::Folder { + chat_folder_id: folder_id + }; + + functions::load_chats( + types::LoadChats { + chat_list: Some(chat_list), + limit: 50, + }, + client + ).await?; +} +``` + +## Архитектура приложения + +### Рекомендуемая структура + +``` +src/ +├── main.rs # Entry point, UI loop +├── tdlib/ +│ ├── mod.rs # TDLib module +│ ├── client.rs # Client wrapper +│ ├── auth.rs # Authentication logic +│ └── updates.rs # Update handlers +├── ui/ +│ ├── mod.rs +│ ├── app.rs # App state +│ ├── layout.rs # UI layout +│ └── components/ # UI components +└── models/ + ├── chat.rs # Chat models + └── message.rs # Message models +``` + +### Разделение ответственности + +1. **TDLib Client** — управление клиентом, запросы к API +2. **Update Handler** — обработка обновлений в фоне +3. **App State** — состояние приложения (чаты, сообщения, UI) +4. **UI Layer** — отрисовка интерфейса (ratatui) + +### Коммуникация между слоями + +```rust +// Используем каналы для коммуникации +use tokio::sync::mpsc; + +#[derive(Debug)] +enum AppEvent { + NewMessage(Message), + ChatUpdated(Chat), + UserStatusChanged(i64, UserStatus), +} + +#[tokio::main] +async fn main() { + // Канал для событий от TDLib + let (tx, mut rx) = mpsc::channel::(100); + + // Запускаем TDLib в отдельной задаче + tokio::spawn(async move { + run_tdlib_client(tx).await; + }); + + // Основной UI loop + loop { + // Проверяем события + while let Ok(event) = rx.try_recv() { + match event { + AppEvent::NewMessage(msg) => { + // Обновляем UI + } + _ => {} + } + } + + // Отрисовываем UI + terminal.draw(|f| ui(f, &app))?; + + // Обрабатываем ввод пользователя + handle_input()?; + } +} +``` + +## Пример: Минимальный клиент + +```rust +use tdlib::{Client, ClientState, functions, types}; +use tokio::sync::mpsc; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // 1. Создаем клиент + let (sender, mut receiver) = mpsc::channel(100); + let client = Client::new(sender); + + // 2. Запускаем клиент + tokio::spawn(async move { + client.start().await; + }); + + // 3. Ждем авторизации + let mut authorized = false; + + while let Some(update) = receiver.recv().await { + match update { + types::Update::AuthorizationState(state) => { + match state.authorization_state { + types::AuthorizationState::WaitTdlibParameters => { + // Отправляем параметры + init_tdlib(&client).await?; + } + types::AuthorizationState::WaitPhoneNumber => { + // Запрашиваем номер у пользователя + let phone = read_phone_from_user(); + authenticate_with_phone(phone, &client).await?; + } + types::AuthorizationState::WaitCode(_) => { + // Запрашиваем код + let code = read_code_from_user(); + verify_code(code, &client).await?; + } + types::AuthorizationState::Ready => { + authorized = true; + break; + } + _ => {} + } + } + _ => {} + } + } + + // 4. Загружаем чаты + if authorized { + load_chats(&client).await?; + + // 5. Слушаем обновления + while let Some(update) = receiver.recv().await { + handle_update(update); + } + } + + Ok(()) +} +``` + +## Best Practices + +### 1. Кэширование +- Всегда включай `use_message_database: true` +- Храни кэш чатов и сообщений в памяти +- Используй `only_local: true` для быстрого доступа + +### 2. Обработка ошибок +- Все TDLib функции возвращают `Result` +- Обрабатывай потерю соединения +- Переподключайся при ошибках сети + +### 3. Производительность +- Не загружай все чаты сразу (используй пагинацию) +- Лимитируй количество сообщений в истории +- Используй `offset` для ленивой загрузки + +### 4. UI/UX +- Показывай индикаторы загрузки +- Кэшируй отрисованные элементы +- Обновляй UI только при изменениях + +## Полезные ссылки + +### Официальная документация +- [TDLib Getting Started](https://core.telegram.org/tdlib/getting-started) +- [TDLib Documentation](https://core.telegram.org/tdlib/docs/) + +### Rust библиотеки +- [rust-tdlib GitHub](https://github.com/antonio-antuan/rust-tdlib) +- [rust-tdlib docs.rs](https://docs.rs/rust-tdlib) +- [tdlib-rs GitHub](https://github.com/FedericoBruzzone/tdlib-rs) +- [tdlib-rs docs.rs](https://docs.rs/tdlib/latest/tdlib/) + +### API Reference +- [tdlib::functions](https://docs.rs/tdlib/latest/tdlib/functions/index.html) +- [tdlib::types](https://docs.rs/tdlib-types/latest/tdlib_types/types/index.html) + +## Следующие шаги + +1. ✅ Изучить документацию TDLib +2. ⬜ Добавить зависимость tdlib-rs в проект +3. ⬜ Реализовать модуль авторизации +4. ⬜ Реализовать загрузку чатов +5. ⬜ Реализовать загрузку сообщений +6. ⬜ Интегрировать с существующим UI +7. ⬜ Добавить отправку сообщений +8. ⬜ Реализовать обработку обновлений в реальном времени diff --git a/src/app/mod.rs b/src/app/mod.rs deleted file mode 100644 index ebaac7e..0000000 --- a/src/app/mod.rs +++ /dev/null @@ -1,201 +0,0 @@ -use crate::telegram::{Chat, Message}; - -#[derive(Debug)] -pub struct App { - pub tabs: Vec, - pub selected_tab: usize, - pub chats: Vec, - pub selected_chat: Option, - pub messages: Vec, - pub input: String, - pub search_query: String, -} - -impl App { - pub fn new() -> Self { - Self { - tabs: vec![ - "All".to_string(), - "Personal".to_string(), - "Work".to_string(), - "Bots".to_string(), - ], - selected_tab: 0, - chats: Self::mock_chats(), - selected_chat: Some(0), - messages: Self::mock_messages(), - input: String::new(), - search_query: String::new(), - } - } - - pub fn select_tab(&mut self, index: usize) { - if index < self.tabs.len() { - self.selected_tab = index; - } - } - - pub fn next_chat(&mut self) { - if !self.chats.is_empty() { - self.selected_chat = Some( - self.selected_chat - .map(|i| (i + 1) % self.chats.len()) - .unwrap_or(0), - ); - self.load_messages(); - } - } - - pub fn previous_chat(&mut self) { - if !self.chats.is_empty() { - self.selected_chat = Some( - self.selected_chat - .map(|i| if i == 0 { self.chats.len() - 1 } else { i - 1 }) - .unwrap_or(0), - ); - self.load_messages(); - } - } - - pub fn open_chat(&mut self) { - self.load_messages(); - } - - fn load_messages(&mut self) { - self.messages = Self::mock_messages(); - } - - fn mock_chats() -> Vec { - vec![ - Chat { - name: "Saved Messages".to_string(), - last_message: "My notes...".to_string(), - unread_count: 0, - is_pinned: true, - is_online: false, - }, - Chat { - name: "Mom".to_string(), - last_message: "Отлично, захвати хлеба.".to_string(), - unread_count: 2, - is_pinned: false, - is_online: true, - }, - Chat { - name: "Boss".to_string(), - last_message: "Meeting at 3pm".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "Rust Community".to_string(), - last_message: "Check out this crate...".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "Durov".to_string(), - last_message: "Privacy matters".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "News Channel".to_string(), - last_message: "Breaking news...".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "Spam Bot".to_string(), - last_message: "Click here!!!".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "Wife".to_string(), - last_message: "Don't forget the milk".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "Team Lead".to_string(), - last_message: "Code review please".to_string(), - unread_count: 0, - is_pinned: false, - is_online: false, - }, - Chat { - name: "DevOps Chat".to_string(), - last_message: "Server is down!".to_string(), - unread_count: 9, - is_pinned: false, - is_online: false, - }, - ] - } - - fn mock_messages() -> Vec { - vec![ - Message { - sender: "Mom".to_string(), - text: "Привет! Ты покормил кота?".to_string(), - time: "14:20".to_string(), - is_outgoing: false, - read_status: 0, - }, - Message { - sender: "You".to_string(), - text: "Да, конечно. Купил ему корм.".to_string(), - time: "14:22".to_string(), - is_outgoing: true, - read_status: 2, - }, - Message { - sender: "You".to_string(), - text: "Скоро буду дома.".to_string(), - time: "14:22".to_string(), - is_outgoing: true, - read_status: 2, - }, - Message { - sender: "Mom".to_string(), - text: "Отлично, захвати хлеба.".to_string(), - time: "14:23".to_string(), - is_outgoing: false, - read_status: 0, - }, - Message { - sender: "You".to_string(), - text: "Ок.".to_string(), - time: "14:25".to_string(), - is_outgoing: true, - read_status: 1, - }, - ] - } - - pub fn get_current_chat_name(&self) -> String { - self.selected_chat - .and_then(|i| self.chats.get(i)) - .map(|chat| { - if chat.is_online { - format!("👤 {} (online)", chat.name) - } else { - format!("👤 {}", chat.name) - } - }) - .unwrap_or_default() - } -} - -impl Default for App { - fn default() -> Self { - Self::new() - } -} diff --git a/src/main.rs b/src/main.rs index 2d1a1b0..4e18047 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,190 @@ -mod app; -mod telegram; -mod ui; +mod tdlib; -use anyhow::Result; use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ backend::CrosstermBackend, - Terminal, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + Frame, Terminal, }; +use std::ffi::CString; use std::io; +use std::os::raw::c_char; +use std::sync::mpsc; +use std::time::Duration; +use tdlib::client::{AuthState, ChatInfo, MessageInfo}; +use tdlib::TdClient; +use tdlib_rs::enums::Update; -use app::App; +// FFI для синхронного вызова TDLib (отключение логов до создания клиента) +#[link(name = "tdjson")] +extern "C" { + fn td_execute(request: *const c_char) -> *const c_char; +} + +/// Отключаем логи TDLib синхронно, до создания клиента +fn disable_tdlib_logs() { + let request = r#"{"@type":"setLogVerbosityLevel","new_verbosity_level":0}"#; + let c_request = CString::new(request).unwrap(); + unsafe { + let _ = td_execute(c_request.as_ptr()); + } + + // Также перенаправляем логи в никуда + let request2 = r#"{"@type":"setLogStream","log_stream":{"@type":"logStreamEmpty"}}"#; + let c_request2 = CString::new(request2).unwrap(); + unsafe { + let _ = td_execute(c_request2.as_ptr()); + } +} + +#[derive(PartialEq, Clone)] +enum AppScreen { + Loading, + Auth, + Main, +} + +struct App { + screen: AppScreen, + td_client: TdClient, + // Auth state + phone_input: String, + code_input: String, + password_input: String, + error_message: Option, + status_message: Option, + // Main app state + chats: Vec, + chat_list_state: ListState, + selected_chat: Option, + current_messages: Vec, + folders: Vec, + selected_folder: usize, + is_loading: bool, +} + +impl App { + fn new() -> App { + let mut state = ListState::default(); + state.select(Some(0)); + + App { + screen: AppScreen::Loading, + td_client: TdClient::new(), + phone_input: String::new(), + code_input: String::new(), + password_input: String::new(), + error_message: None, + status_message: Some("Инициализация TDLib...".to_string()), + chats: Vec::new(), + chat_list_state: state, + selected_chat: None, + current_messages: Vec::new(), + folders: vec!["All".to_string()], + selected_folder: 0, + is_loading: true, + } + } + + fn next_chat(&mut self) { + if self.chats.is_empty() { + return; + } + let i = match self.chat_list_state.selected() { + Some(i) => { + if i >= self.chats.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.chat_list_state.select(Some(i)); + } + + fn previous_chat(&mut self) { + if self.chats.is_empty() { + return; + } + let i = match self.chat_list_state.selected() { + Some(i) => { + if i == 0 { + self.chats.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.chat_list_state.select(Some(i)); + } + + fn select_current_chat(&mut self) { + if let Some(i) = self.chat_list_state.selected() { + if i < self.chats.len() { + self.selected_chat = Some(i); + } + } + } + + fn close_chat(&mut self) { + self.selected_chat = None; + self.current_messages.clear(); + } + + fn select_first_chat(&mut self) { + if !self.chats.is_empty() { + self.chat_list_state.select(Some(0)); + } + } + + fn get_selected_chat_id(&self) -> Option { + self.selected_chat + .and_then(|idx| self.chats.get(idx)) + .map(|chat| chat.id) + } +} #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> Result<(), io::Error> { + // Загружаем переменные окружения из .env + let _ = dotenvy::dotenv(); + + // Отключаем логи TDLib ДО создания клиента + disable_tdlib_logs(); + + // Запускаем поток для получения updates от TDLib + let (update_tx, update_rx) = mpsc::channel::(); + std::thread::spawn(move || { + loop { + if let Some((update, _client_id)) = tdlib_rs::receive() { + if update_tx.send(update).is_err() { + break; // Канал закрыт, выходим + } + } + } + }); + + // Setup terminal enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + // Create app state let mut app = App::new(); - let res = run_app(&mut terminal, &mut app).await; + let res = run_app(&mut terminal, &mut app, update_rx).await; + // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -45,24 +203,635 @@ async fn main() -> Result<()> { async fn run_app( terminal: &mut Terminal, app: &mut App, -) -> Result<()> { - loop { - terminal.draw(|f| ui::draw(f, app))?; + update_rx: mpsc::Receiver, +) -> io::Result<()> { + // Инициализируем TDLib + if let Err(e) = app.td_client.init().await { + app.error_message = Some(e); + } - if event::poll(std::time::Duration::from_millis(100))? { + loop { + // Обрабатываем все доступные обновления от TDLib (неблокирующе) + while let Ok(update) = update_rx.try_recv() { + app.td_client.handle_update(update); + } + + // Обновляем состояние экрана на основе auth_state + update_screen_state(app).await; + + terminal.draw(|f| ui(f, app))?; + + // Используем poll для неблокирующего чтения событий + if event::poll(Duration::from_millis(100))? { if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') | KeyCode::Esc => return Ok(()), - KeyCode::Char('1') => app.select_tab(0), - KeyCode::Char('2') => app.select_tab(1), - KeyCode::Char('3') => app.select_tab(2), - KeyCode::Char('4') => app.select_tab(3), - KeyCode::Up => app.previous_chat(), - KeyCode::Down => app.next_chat(), - KeyCode::Enter => app.open_chat(), - _ => {} + // Global quit command + if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + return Ok(()); + } + + match app.screen { + AppScreen::Loading => { + // В состоянии загрузки игнорируем ввод + } + AppScreen::Auth => handle_auth_input(app, key.code).await, + AppScreen::Main => handle_main_input(app, key).await, } } } } } + +async fn update_screen_state(app: &mut App) { + let prev_screen = app.screen.clone(); + + match &app.td_client.auth_state { + AuthState::WaitTdlibParameters => { + app.screen = AppScreen::Loading; + app.status_message = Some("Инициализация TDLib...".to_string()); + } + AuthState::WaitPhoneNumber | AuthState::WaitCode | AuthState::WaitPassword => { + app.screen = AppScreen::Auth; + app.is_loading = false; + } + AuthState::Ready => { + if prev_screen != AppScreen::Main { + app.screen = AppScreen::Main; + app.is_loading = true; + app.status_message = Some("Загрузка чатов...".to_string()); + + // Запрашиваем загрузку чатов (они придут через updates) + if let Err(e) = app.td_client.load_chats(50).await { + app.error_message = Some(e); + } + app.is_loading = false; + app.status_message = None; + } + + // Синхронизируем чаты из td_client в app + if !app.td_client.chats.is_empty() && app.chats.len() != app.td_client.chats.len() { + app.chats = app.td_client.chats.clone(); + if app.chat_list_state.selected().is_none() && !app.chats.is_empty() { + app.chat_list_state.select(Some(0)); + } + } + } + AuthState::Closed => { + app.status_message = Some("Соединение закрыто".to_string()); + } + AuthState::Error(e) => { + app.error_message = Some(e.clone()); + } + } +} + +async fn handle_auth_input(app: &mut App, key_code: KeyCode) { + match &app.td_client.auth_state { + AuthState::WaitPhoneNumber => match key_code { + KeyCode::Char(c) => { + app.phone_input.push(c); + app.error_message = None; + } + KeyCode::Backspace => { + app.phone_input.pop(); + app.error_message = None; + } + KeyCode::Enter => { + if !app.phone_input.is_empty() { + app.status_message = Some("Отправка номера...".to_string()); + match app.td_client.send_phone_number(app.phone_input.clone()).await { + Ok(_) => { + app.error_message = None; + app.status_message = None; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + } + } + } + } + _ => {} + }, + AuthState::WaitCode => match key_code { + KeyCode::Char(c) if c.is_numeric() => { + app.code_input.push(c); + app.error_message = None; + } + KeyCode::Backspace => { + app.code_input.pop(); + app.error_message = None; + } + KeyCode::Enter => { + if !app.code_input.is_empty() { + app.status_message = Some("Проверка кода...".to_string()); + match app.td_client.send_code(app.code_input.clone()).await { + Ok(_) => { + app.error_message = None; + app.status_message = None; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + } + } + } + } + _ => {} + }, + AuthState::WaitPassword => match key_code { + KeyCode::Char(c) => { + app.password_input.push(c); + app.error_message = None; + } + KeyCode::Backspace => { + app.password_input.pop(); + app.error_message = None; + } + KeyCode::Enter => { + if !app.password_input.is_empty() { + app.status_message = Some("Проверка пароля...".to_string()); + match app.td_client.send_password(app.password_input.clone()).await { + Ok(_) => { + app.error_message = None; + app.status_message = None; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + } + } + } + } + _ => {} + }, + _ => {} + } +} + +async fn handle_main_input(app: &mut App, key: event::KeyEvent) { + let has_super = key.modifiers.contains(KeyModifiers::SUPER); + let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + + match key.code { + // Navigate down: j, Down, д (Russian) + KeyCode::Char('j') | KeyCode::Char('д') | KeyCode::Down if !has_super && !has_ctrl => { + app.next_chat(); + } + // Navigate up: k, Up, л (Russian) + KeyCode::Char('k') | KeyCode::Char('л') | KeyCode::Up if !has_super && !has_ctrl => { + app.previous_chat(); + } + // Jump to first chat: Cmd+Up or Ctrl+k/л + KeyCode::Up if has_super => { + app.select_first_chat(); + } + KeyCode::Char('k') | KeyCode::Char('л') if has_ctrl => { + app.select_first_chat(); + } + KeyCode::Enter => { + let prev_selected = app.selected_chat; + app.select_current_chat(); + + // Если выбрали новый чат, загружаем историю + if app.selected_chat != prev_selected { + if let Some(chat_id) = app.get_selected_chat_id() { + app.status_message = Some("Загрузка сообщений...".to_string()); + match app.td_client.get_chat_history(chat_id, 30).await { + Ok(messages) => { + app.current_messages = messages; + app.status_message = None; + } + Err(e) => { + app.error_message = Some(e); + app.status_message = None; + } + } + } + } + } + KeyCode::Esc => { + app.close_chat(); + } + KeyCode::Char('r') if has_ctrl => { + // Обновить список чатов + app.status_message = Some("Обновление чатов...".to_string()); + if let Err(e) = app.td_client.load_chats(50).await { + app.error_message = Some(e); + } + app.status_message = None; + } + KeyCode::Char(c) if c >= '1' && c <= '9' => { + let folder_idx = (c as usize) - ('1' as usize); + if folder_idx < app.folders.len() { + app.selected_folder = folder_idx; + } + } + _ => {} + } +} + +fn ui(f: &mut Frame, app: &mut App) { + match app.screen { + AppScreen::Loading => render_loading_screen(f, app), + AppScreen::Auth => render_auth_screen(f, app), + AppScreen::Main => render_main_screen(f, app), + } +} + +fn render_loading_screen(f: &mut Frame, app: &App) { + let area = f.area(); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(40), + Constraint::Length(5), + Constraint::Percentage(40), + ]) + .split(area); + + let message = app + .status_message + .as_deref() + .unwrap_or("Загрузка..."); + + let loading = Paragraph::new(message) + .style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(" TTUI "), + ); + + f.render_widget(loading, chunks[1]); +} + +fn render_auth_screen(f: &mut Frame, app: &App) { + let area = f.area(); + + let vertical_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(30), + Constraint::Length(15), + Constraint::Percentage(30), + ]) + .split(area); + + let horizontal_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(25), + Constraint::Percentage(50), + Constraint::Percentage(25), + ]) + .split(vertical_chunks[1]); + + let auth_area = horizontal_chunks[1]; + + let auth_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Title + Constraint::Length(4), // Instructions + Constraint::Length(3), // Input + Constraint::Length(2), // Error/Status message + Constraint::Min(0), // Spacer + ]) + .split(auth_area); + + // Title + let title = Paragraph::new("TTUI - Telegram Authentication") + .style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL)); + f.render_widget(title, auth_chunks[0]); + + // Instructions and Input based on auth state + match &app.td_client.auth_state { + AuthState::WaitPhoneNumber => { + let instructions = vec![ + Line::from("Введите номер телефона в международном формате"), + Line::from("Пример: +79991111111"), + ]; + let instructions_widget = Paragraph::new(instructions) + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::NONE)); + f.render_widget(instructions_widget, auth_chunks[1]); + + let input_text = format!("📱 {}", app.phone_input); + let input = Paragraph::new(input_text) + .style(Style::default().fg(Color::Yellow)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Phone Number "), + ); + f.render_widget(input, auth_chunks[2]); + } + AuthState::WaitCode => { + let instructions = vec![ + Line::from("Введите код подтверждения из Telegram"), + Line::from("Код был отправлен на ваш номер"), + ]; + let instructions_widget = Paragraph::new(instructions) + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::NONE)); + f.render_widget(instructions_widget, auth_chunks[1]); + + let input_text = format!("🔐 {}", app.code_input); + let input = Paragraph::new(input_text) + .style(Style::default().fg(Color::Yellow)) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .title(" Verification Code "), + ); + f.render_widget(input, auth_chunks[2]); + } + AuthState::WaitPassword => { + let instructions = vec![ + Line::from("Введите пароль двухфакторной аутентификации"), + Line::from(""), + ]; + let instructions_widget = Paragraph::new(instructions) + .style(Style::default().fg(Color::Gray)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::NONE)); + f.render_widget(instructions_widget, auth_chunks[1]); + + let masked_password = "*".repeat(app.password_input.len()); + let input_text = format!("🔒 {}", masked_password); + let input = Paragraph::new(input_text) + .style(Style::default().fg(Color::Yellow)) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::ALL).title(" Password ")); + f.render_widget(input, auth_chunks[2]); + } + _ => {} + } + + // Error or status message + if let Some(error) = &app.error_message { + let error_widget = Paragraph::new(error.as_str()) + .style(Style::default().fg(Color::Red)) + .alignment(Alignment::Center); + f.render_widget(error_widget, auth_chunks[3]); + } else if let Some(status) = &app.status_message { + let status_widget = Paragraph::new(status.as_str()) + .style(Style::default().fg(Color::Yellow)) + .alignment(Alignment::Center); + f.render_widget(status_widget, auth_chunks[3]); + } +} + +fn render_main_screen(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Folders/tabs + Constraint::Min(0), // Main content + Constraint::Length(1), // Commands footer + ]) + .split(f.area()); + + render_folders(f, chunks[0], app); + + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(30), // Chat list + Constraint::Percentage(70), // Messages area + ]) + .split(chunks[1]); + + render_chat_list(f, main_chunks[0], app); + render_messages(f, main_chunks[1], app); + render_footer(f, chunks[2], app); +} + +fn render_folders(f: &mut Frame, area: Rect, app: &App) { + let mut spans = vec![]; + + for (i, folder) in app.folders.iter().enumerate() { + let style = if i == app.selected_folder { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + + spans.push(Span::styled(format!(" {}:{} ", i + 1, folder), style)); + if i < app.folders.len() - 1 { + spans.push(Span::raw("│")); + } + } + + let folders_line = Line::from(spans); + let folders_widget = Paragraph::new(folders_line).block( + Block::default() + .title(" TTUI ") + .borders(Borders::ALL), + ); + + f.render_widget(folders_widget, area); +} + +fn render_chat_list(f: &mut Frame, area: Rect, app: &mut App) { + let chat_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Search box + Constraint::Min(0), // Chat list + Constraint::Length(3), // User status + ]) + .split(area); + + // Search box + let search = Paragraph::new("🔍 Search...") + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::DarkGray)); + f.render_widget(search, chat_chunks[0]); + + // Chat list + let items: Vec = app + .chats + .iter() + .enumerate() + .map(|(idx, chat)| { + let is_selected = app.selected_chat == Some(idx); + let prefix = if is_selected { "▌ " } else { " " }; + + let unread_badge = if chat.unread_count > 0 { + format!(" ({})", chat.unread_count) + } else { + String::new() + }; + + let content = format!("{}{}{}", prefix, chat.title, unread_badge); + let style = Style::default().fg(Color::White); + + ListItem::new(content).style(style) + }) + .collect(); + + let chats_list = List::new(items) + .block(Block::default().borders(Borders::ALL)) + .highlight_style( + Style::default() + .add_modifier(Modifier::ITALIC) + .fg(Color::Yellow), + ); + + f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state); + + // User status + let status = Paragraph::new("[User: Online]") + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::Green)); + f.render_widget(status, chat_chunks[2]); +} + +fn render_messages(f: &mut Frame, area: Rect, app: &App) { + if let Some(chat_idx) = app.selected_chat { + if let Some(chat) = app.chats.get(chat_idx) { + let message_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Chat header + Constraint::Min(0), // Messages + Constraint::Length(3), // Input box + ]) + .split(area); + + // Chat header + let header = Paragraph::new(format!("👤 {}", chat.title)) + .block(Block::default().borders(Borders::ALL)) + .style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ); + f.render_widget(header, message_chunks[0]); + + // Messages + let mut lines: Vec = Vec::new(); + + for msg in &app.current_messages { + let sender_style = if msg.is_outgoing { + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) + } else { + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + }; + + let sender_name = if msg.is_outgoing { + "You".to_string() + } else { + msg.sender_name.clone() + }; + + let read_mark = if msg.is_outgoing { + if msg.is_read { " ✓✓" } else { " ✓" } + } else { + "" + }; + + // Форматируем время + let time = format_timestamp(msg.date); + + lines.push(Line::from(vec![ + Span::styled(format!("{} ", sender_name), sender_style), + Span::raw("── "), + Span::styled(format!("{}{}", time, read_mark), Style::default().fg(Color::DarkGray)), + ])); + lines.push(Line::from(msg.content.clone())); + lines.push(Line::from("")); + } + + if lines.is_empty() { + lines.push(Line::from(Span::styled( + "Нет сообщений", + Style::default().fg(Color::DarkGray), + ))); + } + + let messages_widget = + Paragraph::new(lines).block(Block::default().borders(Borders::ALL)); + f.render_widget(messages_widget, message_chunks[1]); + + // Input box + let input = Paragraph::new("> ...") + .block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::Yellow)); + f.render_widget(input, message_chunks[2]); + } + } else { + let empty = Paragraph::new("Выберите чат").block(Block::default().borders(Borders::ALL)) + .style(Style::default().fg(Color::DarkGray)) + .alignment(Alignment::Center); + f.render_widget(empty, area); + } +} + +fn render_footer(f: &mut Frame, area: Rect, app: &App) { + let status = if let Some(msg) = &app.status_message { + format!(" {} ", msg) + } else if let Some(err) = &app.error_message { + format!(" Error: {} ", err) + } else { + " j/k: Navigate | Ctrl+k: First | Enter: Open | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ".to_string() + }; + + let style = if app.error_message.is_some() { + Style::default().fg(Color::Red) + } else if app.status_message.is_some() { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::DarkGray) + }; + + let footer = Paragraph::new(status).style(style); + f.render_widget(footer, area); +} + +fn format_timestamp(timestamp: i32) -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i32; + + let diff = now - timestamp; + + if diff < 60 { + "just now".to_string() + } else if diff < 3600 { + format!("{}m ago", diff / 60) + } else if diff < 86400 { + format!("{}h ago", diff / 3600) + } else { + // Показываем дату + let secs = timestamp as u64; + let days = secs / 86400; + format!("{}d ago", (now as u64 / 86400) - days) + } +} diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs new file mode 100644 index 0000000..f826847 --- /dev/null +++ b/src/tdlib/client.rs @@ -0,0 +1,331 @@ +use std::env; +use tdlib_rs::enums::{AuthorizationState, ChatList, MessageContent, Update, User}; +use tdlib_rs::functions; +use tdlib_rs::types::{Chat as TdChat, Message as TdMessage}; + +#[derive(Debug, Clone, PartialEq)] +pub enum AuthState { + WaitTdlibParameters, + WaitPhoneNumber, + WaitCode, + WaitPassword, + Ready, + Closed, + Error(String), +} + +#[derive(Debug, Clone)] +pub struct ChatInfo { + pub id: i64, + pub title: String, + pub last_message: String, + pub unread_count: i32, + pub is_pinned: bool, + pub order: i64, +} + +#[derive(Debug, Clone)] +pub struct MessageInfo { + pub id: i64, + pub sender_name: String, + pub is_outgoing: bool, + pub content: String, + pub date: i32, + pub is_read: bool, +} + +pub struct TdClient { + pub auth_state: AuthState, + pub api_id: i32, + pub api_hash: String, + client_id: i32, + pub chats: Vec, + pub current_chat_messages: Vec, +} + +impl TdClient { + pub fn new() -> Self { + let api_id: i32 = env::var("API_ID") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + let api_hash = env::var("API_HASH").unwrap_or_default(); + + let client_id = tdlib_rs::create_client(); + + TdClient { + auth_state: AuthState::WaitTdlibParameters, + api_id, + api_hash, + client_id, + chats: Vec::new(), + current_chat_messages: Vec::new(), + } + } + + pub fn is_authenticated(&self) -> bool { + matches!(self.auth_state, AuthState::Ready) + } + + /// Инициализация TDLib с параметрами + pub async fn init(&mut self) -> Result<(), String> { + let result = functions::set_tdlib_parameters( + false, // use_test_dc + "tdlib_data".to_string(), // database_directory + "".to_string(), // files_directory + "".to_string(), // database_encryption_key (String, not Vec) + true, // use_file_database + true, // use_chat_info_database + true, // use_message_database + false, // use_secret_chats + self.api_id, // api_id + self.api_hash.clone(), // api_hash + "en".to_string(), // system_language_code + "Desktop".to_string(), // device_model + "".to_string(), // system_version + env!("CARGO_PKG_VERSION").to_string(), // application_version + self.client_id, + ) + .await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Failed to set TDLib parameters: {:?}", e)), + } + } + + /// Обрабатываем одно обновление от TDLib + pub fn handle_update(&mut self, update: Update) { + match update { + Update::AuthorizationState(state) => { + self.handle_auth_state(state.authorization_state); + } + Update::NewChat(new_chat) => { + self.add_or_update_chat(&new_chat.chat); + } + Update::ChatLastMessage(update) => { + let chat_id = update.chat_id; + let last_message_text = update + .last_message + .as_ref() + .map(|msg| extract_message_text_static(msg)) + .unwrap_or_default(); + + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) { + chat.last_message = last_message_text; + } + } + Update::ChatReadInbox(update) => { + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { + chat.unread_count = update.unread_count; + } + } + Update::NewMessage(_new_msg) => { + // Новые сообщения обрабатываются при обновлении UI + } + _ => {} + } + } + + fn handle_auth_state(&mut self, state: AuthorizationState) { + self.auth_state = match state { + AuthorizationState::WaitTdlibParameters => AuthState::WaitTdlibParameters, + AuthorizationState::WaitPhoneNumber => AuthState::WaitPhoneNumber, + AuthorizationState::WaitCode(_) => AuthState::WaitCode, + AuthorizationState::WaitPassword(_) => AuthState::WaitPassword, + AuthorizationState::Ready => AuthState::Ready, + AuthorizationState::Closed => AuthState::Closed, + _ => self.auth_state.clone(), + }; + } + + fn add_or_update_chat(&mut self, td_chat: &TdChat) { + let last_message = td_chat + .last_message + .as_ref() + .map(|m| extract_message_text_static(m)) + .unwrap_or_default(); + + let chat_info = ChatInfo { + id: td_chat.id, + title: td_chat.title.clone(), + last_message, + unread_count: td_chat.unread_count, + is_pinned: false, + order: 0, + }; + + if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) { + existing.title = chat_info.title; + existing.last_message = chat_info.last_message; + existing.unread_count = chat_info.unread_count; + } else { + self.chats.push(chat_info); + } + } + + fn convert_message(&self, message: &TdMessage) -> MessageInfo { + let sender_name = match &message.sender_id { + tdlib_rs::enums::MessageSender::User(user) => format!("User_{}", user.user_id), + tdlib_rs::enums::MessageSender::Chat(chat) => format!("Chat_{}", chat.chat_id), + }; + + MessageInfo { + id: message.id, + sender_name, + is_outgoing: message.is_outgoing, + content: extract_message_text_static(message), + date: message.date, + is_read: !message.is_outgoing || message.id <= 0, + } + } + + /// Отправка номера телефона + pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> { + let result = functions::set_authentication_phone_number( + phone, + None, + self.client_id, + ) + .await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Ошибка отправки номера: {:?}", e)), + } + } + + /// Отправка кода подтверждения + pub async fn send_code(&mut self, code: String) -> Result<(), String> { + let result = functions::check_authentication_code(code, self.client_id).await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Неверный код: {:?}", e)), + } + } + + /// Отправка пароля 2FA + pub async fn send_password(&mut self, password: String) -> Result<(), String> { + let result = functions::check_authentication_password(password, self.client_id).await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Неверный пароль: {:?}", e)), + } + } + + /// Загрузка списка чатов + pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { + let result = functions::load_chats( + Some(ChatList::Main), + limit, + self.client_id, + ) + .await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Ошибка загрузки чатов: {:?}", e)), + } + } + + /// Загрузка истории сообщений чата + pub async fn get_chat_history( + &mut self, + chat_id: i64, + limit: i32, + ) -> Result, String> { + let _ = functions::open_chat(chat_id, self.client_id).await; + + let result = functions::get_chat_history( + chat_id, + 0, + 0, + limit, + false, + self.client_id, + ) + .await; + + match result { + Ok(tdlib_rs::enums::Messages::Messages(messages)) => { + let mut result_messages: Vec = messages + .messages + .into_iter() + .filter_map(|m| m.map(|msg| self.convert_message(&msg))) + .collect(); + + result_messages.reverse(); + self.current_chat_messages = result_messages.clone(); + Ok(result_messages) + } + Err(e) => Err(format!("Ошибка загрузки сообщений: {:?}", e)), + } + } + + /// Получение информации о пользователе по ID + pub async fn get_user_name(&self, user_id: i64) -> String { + match functions::get_user(user_id, self.client_id).await { + Ok(user) => { + // User is an enum, need to match it + match user { + User::User(u) => { + let first = u.first_name; + let last = u.last_name; + if last.is_empty() { + first + } else { + format!("{} {}", first, last) + } + } + } + } + Err(_) => format!("User_{}", user_id), + } + } + + /// Получение моего user_id + pub async fn get_me(&self) -> Result { + match functions::get_me(self.client_id).await { + Ok(user) => { + match user { + User::User(u) => Ok(u.id), + } + } + Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)), + } + } +} + +/// Статическая функция для извлечения текста сообщения (без &self) +fn extract_message_text_static(message: &TdMessage) -> String { + match &message.content { + MessageContent::MessageText(text) => text.text.text.clone(), + MessageContent::MessagePhoto(photo) => { + if photo.caption.text.is_empty() { + "[Фото]".to_string() + } else { + format!("[Фото] {}", photo.caption.text) + } + } + MessageContent::MessageVideo(_) => "[Видео]".to_string(), + MessageContent::MessageDocument(doc) => { + format!("[Файл: {}]", doc.document.file_name) + } + MessageContent::MessageVoiceNote(_) => "[Голосовое сообщение]".to_string(), + MessageContent::MessageVideoNote(_) => "[Видеосообщение]".to_string(), + MessageContent::MessageSticker(sticker) => { + format!("[Стикер: {}]", sticker.sticker.emoji) + } + MessageContent::MessageAnimation(_) => "[GIF]".to_string(), + MessageContent::MessageAudio(audio) => { + format!("[Аудио: {}]", audio.audio.title) + } + MessageContent::MessageCall(_) => "[Звонок]".to_string(), + MessageContent::MessagePoll(poll) => { + format!("[Опрос: {}]", poll.poll.question.text) + } + _ => "[Сообщение]".to_string(), + } +} diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs new file mode 100644 index 0000000..116fb61 --- /dev/null +++ b/src/tdlib/mod.rs @@ -0,0 +1,3 @@ +pub mod client; + +pub use client::TdClient; diff --git a/src/telegram/mod.rs b/src/telegram/mod.rs deleted file mode 100644 index b91f903..0000000 --- a/src/telegram/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -#[derive(Debug, Clone)] -pub struct Chat { - pub name: String, - pub last_message: String, - pub unread_count: usize, - pub is_pinned: bool, - pub is_online: bool, -} - -#[derive(Debug, Clone)] -pub struct Message { - pub sender: String, - pub text: String, - pub time: String, - pub is_outgoing: bool, - pub read_status: u8, -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs deleted file mode 100644 index 216e8f9..0000000 --- a/src/ui/mod.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::app::App; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, Paragraph}, - Frame, -}; - -pub fn draw(f: &mut Frame, app: &App) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(0), - Constraint::Length(3), - ]) - .split(f.area()); - - draw_tabs(f, app, chunks[0]); - - let main_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(30), Constraint::Percentage(70)]) - .split(chunks[1]); - - draw_chat_list(f, app, main_chunks[0]); - draw_messages(f, app, main_chunks[1]); - draw_status_bar(f, app, chunks[2]); -} - -fn draw_tabs(f: &mut Frame, app: &App, area: Rect) { - let tabs: Vec = app - .tabs - .iter() - .enumerate() - .map(|(i, t)| { - let style = if i == app.selected_tab { - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(Color::White) - }; - Span::styled(format!(" {}:{} ", i + 1, t), style) - }) - .collect(); - - let tabs_line = Line::from(tabs); - let tabs_paragraph = Paragraph::new(tabs_line).block( - Block::default() - .borders(Borders::ALL) - .title("Telegram TUI"), - ); - - f.render_widget(tabs_paragraph, area); -} - -fn draw_chat_list(f: &mut Frame, app: &App, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(0)]) - .split(area); - - let search = Paragraph::new(format!("🔍 {}", app.search_query)) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(search, chunks[0]); - - let items: Vec = app - .chats - .iter() - .enumerate() - .map(|(i, chat)| { - let pin_icon = if chat.is_pinned { "📌 " } else { " " }; - let unread_badge = if chat.unread_count > 0 { - format!(" ({})", chat.unread_count) - } else { - String::new() - }; - - let content = format!("{}{}{}", pin_icon, chat.name, unread_badge); - - let style = if Some(i) == app.selected_chat { - Style::default() - .fg(Color::Yellow) - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::REVERSED) - } else if chat.unread_count > 0 { - Style::default().fg(Color::Cyan) - } else { - Style::default() - }; - - ListItem::new(content).style(style) - }) - .collect(); - - let list = List::new(items).block(Block::default().borders(Borders::ALL)); - - f.render_widget(list, chunks[1]); -} - -fn draw_messages(f: &mut Frame, app: &App, area: Rect) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Min(0), - Constraint::Length(3), - ]) - .split(area); - - let header = Paragraph::new(app.get_current_chat_name()).block( - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(Color::White)), - ); - f.render_widget(header, chunks[0]); - - let mut message_lines: Vec = vec![]; - - for msg in &app.messages { - message_lines.push(Line::from("")); - - let time_and_name = if msg.is_outgoing { - let status = match msg.read_status { - 2 => "✓✓", - 1 => "✓", - _ => "", - }; - format!("{} ────────────────────────────────────── {} {}", - msg.sender, msg.time, status) - } else { - format!("{} ──────────────────────────────────────── {}", - msg.sender, msg.time) - }; - - let style = if msg.is_outgoing { - Style::default().fg(Color::Green) - } else { - Style::default().fg(Color::Cyan) - }; - - message_lines.push(Line::from(Span::styled(time_and_name, style))); - message_lines.push(Line::from(msg.text.clone())); - } - - let messages = Paragraph::new(message_lines) - .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::White)); - - f.render_widget(messages, chunks[1]); - - let input = Paragraph::new(format!("> {}_", app.input)) - .block(Block::default().borders(Borders::ALL)); - f.render_widget(input, chunks[2]); -} - -fn draw_status_bar(f: &mut Frame, _app: &App, area: Rect) { - let status_text = " Esc: Back | Enter: Open | ^R: Reply | ^E: Edit | ^D: Delete"; - let status = Paragraph::new(status_text) - .style(Style::default().fg(Color::Gray)) - .block( - Block::default() - .borders(Borders::TOP) - .title("[User: Online]"), - ); - - f.render_widget(status, area); -} diff --git a/tdlib_data/db.sqlite b/tdlib_data/db.sqlite new file mode 100644 index 0000000..27c0e02 Binary files /dev/null and b/tdlib_data/db.sqlite differ diff --git a/tdlib_data/db.sqlite-shm b/tdlib_data/db.sqlite-shm new file mode 100644 index 0000000..138386c Binary files /dev/null and b/tdlib_data/db.sqlite-shm differ diff --git a/tdlib_data/db.sqlite-wal b/tdlib_data/db.sqlite-wal new file mode 100644 index 0000000..f18d8bc Binary files /dev/null and b/tdlib_data/db.sqlite-wal differ diff --git a/tdlib_data/td.binlog b/tdlib_data/td.binlog new file mode 100644 index 0000000..2c8db61 Binary files /dev/null and b/tdlib_data/td.binlog differ