From b6d9291864326c64a638f899a0095731f0cd0140 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Sun, 18 Jan 2026 14:49:31 +0300 Subject: [PATCH] fixes --- .gitignore | 6 + CLAUDE.md | 29 + CONTEXT.md | 80 ++ Cargo.lock | 1887 +++++++++++++++++++++++++++++-------- Cargo.toml | 9 +- DEVELOPMENT.md | 102 ++ README.md | 1 + REQUIREMENTS.md | 89 ++ ROADMAP.md | 48 + build.rs | 3 + docs/README.md | 40 - docs/TDLIB_INTEGRATION.md | 636 +++++++++++++ src/app/mod.rs | 269 ++---- src/app/state.rs | 6 + src/input/auth.rs | 101 ++ src/input/main_input.rs | 174 ++++ src/input/mod.rs | 5 + src/main.rs | 154 ++- src/tdlib/client.rs | 423 +++++++++ src/tdlib/mod.rs | 3 + src/telegram/mod.rs | 17 - src/ui/auth.rs | 136 +++ src/ui/chat_list.rs | 61 ++ src/ui/footer.rs | 30 + src/ui/loading.rs | 40 + src/ui/main_screen.rs | 62 ++ src/ui/messages.rs | 116 +++ src/ui/mod.rs | 179 +--- src/utils.rs | 47 + 29 files changed, 3920 insertions(+), 833 deletions(-) create mode 100644 CLAUDE.md create mode 100644 CONTEXT.md create mode 100644 DEVELOPMENT.md create mode 100644 README.md create mode 100644 REQUIREMENTS.md create mode 100644 ROADMAP.md create mode 100644 build.rs delete mode 100644 docs/README.md create mode 100644 docs/TDLIB_INTEGRATION.md create mode 100644 src/app/state.rs create mode 100644 src/input/auth.rs create mode 100644 src/input/main_input.rs create mode 100644 src/input/mod.rs create mode 100644 src/tdlib/client.rs create mode 100644 src/tdlib/mod.rs delete mode 100644 src/telegram/mod.rs create mode 100644 src/ui/auth.rs create mode 100644 src/ui/chat_list.rs create mode 100644 src/ui/footer.rs create mode 100644 src/ui/loading.rs create mode 100644 src/ui/main_screen.rs create mode 100644 src/ui/messages.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..c1ace0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ /target + +# TDLib session data (contains auth tokens - NEVER commit!) +/tdlib_data/ + +# Environment variables (contains API keys) +.env 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/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..ea706f7 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,80 @@ +# Текущий контекст проекта + +## Статус: Базовая интеграция с TDLib работает + +### Что сделано + +#### TDLib интеграция +- Подключена библиотека `tdlib-rs` v1.1 с автоматической загрузкой TDLib +- Реализована авторизация через телефон + код + 2FA пароль +- Сессия сохраняется автоматически в папке `tdlib_data/` +- Отключены логи TDLib через FFI вызов `td_execute` до создания клиента +- Updates обрабатываются в отдельном потоке через `mpsc` канал (неблокирующе) + +#### Функциональность +- Загрузка списка чатов (до 50 штук) +- Отображение названия чата и счётчика непрочитанных +- Загрузка истории сообщений при открытии чата +- Отображение сообщений с именем отправителя и временем + +#### Управление +- `j/k` или стрелки — навигация по списку чатов +- `д/л` — русская раскладка для j/k +- `Enter` — открыть выбранный чат +- `Esc` — закрыть открытый чат +- `Ctrl+k` — перейти к первому чату +- `Ctrl+R` — обновить список чатов +- `Ctrl+C` — выход + +### Структура проекта + +``` +src/ +├── main.rs # Точка входа, UI рендеринг, event loop +├── tdlib/ +│ ├── mod.rs # Модуль экспорта +│ └── client.rs # TdClient: авторизация, загрузка чатов, сообщений +``` + +### Ключевые решения + +1. **Неблокирующий receive**: TDLib updates приходят в отдельном потоке и передаются в main loop через `mpsc::channel`. Это позволяет UI оставаться отзывчивым. + +2. **FFI для логов**: Используем прямой вызов `td_execute` для отключения логов синхронно, до создания клиента, чтобы избежать вывода в терминал. + +3. **Синхронизация чатов**: Чаты загружаются асинхронно через updates. Main loop периодически синхронизирует `app.chats` с `td_client.chats`. + +### Зависимости (Cargo.toml) + +```toml +ratatui = "0.29" +crossterm = "0.28" +tdlib-rs = { version = "1.1", features = ["download-tdlib"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +dotenvy = "0.15" +``` + +### Переменные окружения (.env) + +``` +API_ID=your_api_id +API_HASH=your_api_hash +``` + +## Что НЕ сделано / TODO + +- [ ] Отправка сообщений +- [ ] Поиск по чатам +- [ ] Папки телеграма (сейчас только "All") +- [ ] Отображение онлайн-статуса пользователя +- [ ] Markdown форматирование в сообщениях +- [ ] Скролл истории сообщений +- [ ] Отметка сообщений как прочитанные +- [ ] Обновление чатов в реальном времени (новые сообщения) + +## Известные проблемы + +1. При первом запуске нужно пройти авторизацию +2. Имя отправителя показывается как "User_ID" (нужно загружать имена пользователей) 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/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..8380470 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,102 @@ +# Правила локальной разработки + +> **Обязательно к прочтению перед началом работы!** + +--- + +## Инструменты + +### MCP серверы +- **Serena** — для работы с кодом (символьная навигация, редактирование) +- **Context7** — для получения актуальной документации по библиотекам + +Используй эти инструменты для эффективной работы с кодовой базой. + +--- + +## Правила работы + +### 1. Никогда не запускай сервисы самостоятельно + +**ЗАПРЕЩЕНО** запускать `cargo run`, `cargo build` и подобные команды. + +**Вместо этого попроси пользователя запустить:** + +``` +Запусти, пожалуйста: +cargo run +``` + +### 2. Тестирование — только ручное + +После завершения задачи: +1. Опиши сценарии для проверки +2. Попроси пользователя проверить вручную +3. Дождись фидбека + +**Формат:** +``` +Готово! Проверь, пожалуйста: + +1. Открой cargo run +2. понавигируйся в списке чатов кнопками h j k l +3. Нажми Enter для открытия чата +4. Убедись, что чат прогурзился + +Напиши, если что-то не работает. +``` + +### 3. Работа поэтапно + +Делай работу **небольшими итерациями**: + +1. **Один этап = одна логическая единица** + - Один endpoint + - Один компонент + - Одна фича + +2. **После каждого этапа:** + - Сообщи что сделано + - Дай сценарий проверки + - Дождись подтверждения + +3. **Не делай сразу много:** + - ❌ Весь CRUD за раз + - ✅ Сначала GET, проверили, потом POST, проверили... + +--- + +## Чеклист перед началом работы + +- [ ] Прочитал CONTEXT.md +- [ ] Прочитал ROADMAP.md (понял текущую фазу) +- [ ] Понял задачу +- [ ] Готов работать поэтапно +- [ ] Помню: не запускаю сервисы сам, прошу пользователя + +--- + +## Пример правильного workflow + +``` +Пользователь: Сделай endpoint для получения списка идей + +Агент: +1. Читает документацию через Context7 (NestJS, TypeORM) +2. Использует Serena для навигации по коду +3. Создаёт endpoint GET /api/ideas +4. Сообщает: + + "Создал endpoint GET /api/ideas. + + Запусти backend: + cd backend && npm run dev + + Проверь: + curl http://localhost:4001/api/ideas + + Должен вернуться пустой массив: { data: [], meta: {...} }" + +5. Ждёт фидбек +6. Переходит к следующему этапу +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b766576 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +telegram-tui консольный телеграм \ No newline at end of file diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..9e1278b --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,89 @@ +# TTUI - Требование к приложению + +## Описание приложения + +Терминальный интерфейс для telegram + +## Функциональные требования + +### Интерфейс + +┌─ TTUI ───────────────────────────────────────────────────────────────────────┐ +│ 1:All │ 2:Personal │ 3:Work │ 4:Bots │ +├──────────────────────┬───────────────────────────────────────────────────────┤ +│ 🔍 Search... │ 👤 Mom (online) │ +├──────────────────────┼───────────────────────────────────────────────────────┤ +│ 📌 Saved Messages │ Today, Dec 21│ +│ ▌ Mom (2)│ │ +│ Boss │ Mom ────────────────────────────────────────── 14:20 │ +│ Rust Community │ Привет! Ты покормил кота? │ +│ Durov │ │ +│ News Channel │ You ─────────────────────────────────────── 14:22 ✓✓ │ +│ Spam Bot │ Да, конечно. Купил ему корм. │ +│ Wife │ Скоро буду дома. │ +│ Team Lead │ │ +│ DevOps Chat (9)│ Mom ────────────────────────────────────────── 14:23 │ +│ Server Alerts │ Отлично, захвати хлеба. │ +│ Gym Bro │ │ +│ Design Team │ You ─────────────────────────────────────── 14:25 ✓ │ +│ Project X │ Ок. │ +│ HR │ │ +│ Mom's Friend │ │ +│ Taxi Bot │ │ +│ │ │ +│ │ │ +│ │ │ +│ │ │ +│ │ │ +├──────────────────────┼───────────────────────────────────────────────────────┤ +│ [User: Online] │ > **message** │ +└──────────────────────┴───────────────────────────────────────────────────────┘ + **commands** + + +### Список желаемого +1) футер - список папок в телеграме +2) список с чатами - лички и группы, сверху инпут для поиска чата +3) основной контент - открытый чат с сообщениями из чата, если никакой чат не открыт, то контент пустой, ничего не показываем. Снизу - инпут для ввода сообщения в чат, который открыт +4) снизу списка чата статус онлайн или нет сам пользователь приложения +5) при открытии чата должна загружаться история чата, а так же подгружаться новые сообщения от собеседника. +6) выделяем сообщения собеседника его никнеймом, группируем его сообщения и разделяем наши сообщения и сообщения собеседника, как на интерфейсе сверху +7) отображаем наше сообщение символом `✓`, если телеграм подтвердил, что сообщение отправлено, и выделяем `✓✓` если собеседник прочитал его +8) при навигации в чате выделяем строку курсивом, при выборе чата (то есть его открытии) ставим в начало символ ▌ +9) `(2)` — счетчик непрочитанных (можно красить в красный/зеленый). +10) `muted` — статус чата (серый цвет). +11) `@` — пинг/меншн. +12) с видео/картинками/голосовые пока ничего не делаем, ренденим заглушку (с упоминанием что это картинка или видео и тд) + +### Управление +1) ctrl+c или command+c - выход из программы +2) "h j k l" - влево, вниз, вверх, вправо (навигация в левом столбце) vim-style управление +3) стрелки - управление, так же как и "h j k l" +4) "command + 1", "command + 2" и так далее - переключение между папками, которые созданы в телеграме +5) из интерфейса "**message**" - это инпут для ввода сообщения в открытый чат +6) ctrl + s - фокус в инпут поиска чата +7) Esc - закрытие открытого чата +8) command + стрелка вверх (или ctrl + k) - выделяем самый верхний чат (без открытия) +9) поддержка русской раскладки: "р о л д" соответствует "h j k l" +10) Ctrl+R - обновить список чатов + +### Реализованные команды (footer) + +``` +j/k: Navigate | Ctrl+k: First | Enter: Open | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit +``` + +## Технологии +Пишем на rust-е + +1) ratatui - для tui интерфейса +2) tdlib-rs - для подключения апи телеграма (обёртка над TDLib) +3) tokio - async runtime +4) crossterm - кроссплатформенный терминал + +## Нефункциональные требования + +### Производительность +1) программа должна выдавать 60 фпс +2) интерфейс не должен мерцать +3) минимальное разрешение - 600 символов, максимального нет, не ограничиваем diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..6f3c57c --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,48 @@ +# Roadmap + +## Фаза 1: Базовая инфраструктура [DONE] + +- [x] Настройка проекта (Cargo.toml) +- [x] TUI фреймворк (ratatui + crossterm) +- [x] Базовый layout (папки, список чатов, область сообщений) +- [x] Vim-style навигация (hjkl, стрелки) +- [x] Русская раскладка (ролд) + +## Фаза 2: TDLib интеграция [DONE] + +- [x] Подключение tdlib-rs +- [x] Авторизация (телефон + код + 2FA) +- [x] Сохранение сессии +- [x] Загрузка списка чатов +- [x] Загрузка истории сообщений +- [x] Отключение логов TDLib + +## Фаза 3: Улучшение UX [IN PROGRESS] + +- [ ] Отправка сообщений +- [ ] Поиск по чатам (Ctrl+S) +- [ ] Скролл истории сообщений +- [ ] Загрузка имён пользователей (вместо User_ID) +- [ ] Отметка сообщений как прочитанные +- [ ] Реальное время: новые сообщения + +## Фаза 4: Папки и фильтрация + +- [ ] Загрузка папок из Telegram +- [ ] Переключение между папками (Cmd+1, Cmd+2, ...) +- [ ] Фильтрация чатов по папке + +## Фаза 5: Расширенный функционал + +- [ ] Отображение онлайн-статуса +- [ ] Статус доставки/прочтения (✓, ✓✓) +- [ ] Поддержка медиа-заглушек (фото, видео, голосовые) +- [ ] Mentions (@) +- [ ] Muted чаты (серый цвет) + +## Фаза 6: Полировка + +- [ ] Оптимизация 60 FPS +- [ ] Минимальное разрешение 600 символов +- [ ] Обработка ошибок сети +- [ ] Graceful shutdown 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/README.md b/docs/README.md deleted file mode 100644 index 2b82c6b..0000000 --- a/docs/README.md +++ /dev/null @@ -1,40 +0,0 @@ -Что нужно сделать - telegram TUI, то есть terminal user interface для телеграма -Ограничения технологий - используем rust-lang, TUI делаем на ratatui, используем telegram api для клиентских приложений - -Интерфейс - - -┌─ Telegram TUI ───────────────────────────────────────────────────────────────┐ -│ 1:All │ 2:Personal │ 3:Work │ 4:Bots │ -├──────────────────────┬───────────────────────────────────────────────────────┤ -│ 🔍 Search... │ 👤 Mom (online) │ -├──────────────────────┼───────────────────────────────────────────────────────┤ -│ 📌 Saved Messages │ Today, Dec 21│ -│ ▌ Mom (2)│ │ -│ Boss │ Mom ────────────────────────────────────────── 14:20 │ -│ Rust Community │ Привет! Ты покормил кота? │ -│ Durov │ │ -│ News Channel │ You ─────────────────────────────────────── 14:22 ✓✓ │ -│ Spam Bot │ Да, конечно. Купил ему корм. │ -│ Wife │ Скоро буду дома. │ -│ Team Lead │ │ -│ DevOps Chat (9)│ Mom ────────────────────────────────────────── 14:23 │ -│ Server Alerts │ Отлично, захвати хлеба. │ -│ Gym Bro │ │ -│ Design Team │ You ─────────────────────────────────────── 14:25 ✓ │ -│ Project X │ Ок. │ -│ HR │ │ -│ Mom's Friend │ │ -│ Taxi Bot │ │ -│ │ │ -│ │ │ -│ │ │ -│ │ │ -│ │ │ -├──────────────────────┼───────────────────────────────────────────────────────┤ -│ [User: Online] │ > Ок, скоро буд_ │ -└──────────────────────┴───────────────────────────────────────────────────────┘ - Esc: Back | Enter: Open | ^R: Reply | ^E: Edit | ^D: Delete - - -Так же еще добавляем: -1) Авторизацию через Telegram \ No newline at end of file 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 index ebaac7e..f71497b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,201 +1,118 @@ -use crate::telegram::{Chat, Message}; +mod state; + +pub use state::AppScreen; + +use ratatui::widgets::ListState; +use crate::tdlib::client::{ChatInfo, MessageInfo}; +use crate::tdlib::TdClient; -#[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, + pub screen: AppScreen, + pub td_client: TdClient, + // Auth state + pub phone_input: String, + pub code_input: String, + pub password_input: String, + pub error_message: Option, + pub status_message: Option, + // Main app state + pub chats: Vec, + pub chat_list_state: ListState, + pub selected_chat_id: Option, + pub current_messages: Vec, + pub message_input: String, + pub message_scroll_offset: usize, + pub folders: Vec, + pub selected_folder: usize, + pub is_loading: bool, } 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 new() -> App { + let mut state = ListState::default(); + state.select(Some(0)); - pub fn select_tab(&mut self, index: usize) { - if index < self.tabs.len() { - self.selected_tab = index; + 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_id: None, + current_messages: Vec::new(), + message_input: String::new(), + message_scroll_offset: 0, + folders: vec!["All".to_string()], + selected_folder: 0, + is_loading: true, } } 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(); + 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)); } 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(); + 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)); + } + + pub fn select_current_chat(&mut self) { + if let Some(i) = self.chat_list_state.selected() { + if let Some(chat) = self.chats.get(i) { + self.selected_chat_id = Some(chat.id); + } } } - pub fn open_chat(&mut self) { - self.load_messages(); + pub fn close_chat(&mut self) { + self.selected_chat_id = None; + self.current_messages.clear(); + self.message_input.clear(); + self.message_scroll_offset = 0; } - fn load_messages(&mut self) { - self.messages = Self::mock_messages(); + pub fn select_first_chat(&mut self) { + if !self.chats.is_empty() { + self.chat_list_state.select(Some(0)); + } } - 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, - }, - ] + pub fn get_selected_chat_id(&self) -> Option { + self.selected_chat_id } - 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() + pub fn get_selected_chat(&self) -> Option<&ChatInfo> { + self.selected_chat_id + .and_then(|id| self.chats.iter().find(|c| c.id == id)) } } diff --git a/src/app/state.rs b/src/app/state.rs new file mode 100644 index 0000000..71db9c7 --- /dev/null +++ b/src/app/state.rs @@ -0,0 +1,6 @@ +#[derive(PartialEq, Clone)] +pub enum AppScreen { + Loading, + Auth, + Main, +} diff --git a/src/input/auth.rs b/src/input/auth.rs new file mode 100644 index 0000000..d385a06 --- /dev/null +++ b/src/input/auth.rs @@ -0,0 +1,101 @@ +use crossterm::event::KeyCode; +use std::time::Duration; +use tokio::time::timeout; +use crate::app::App; +use crate::tdlib::client::AuthState; + +pub async fn handle(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 timeout(Duration::from_secs(10), app.td_client.send_phone_number(app.phone_input.clone())).await { + Ok(Ok(_)) => { + app.error_message = None; + app.status_message = None; + } + Ok(Err(e)) => { + app.error_message = Some(e); + app.status_message = None; + } + Err(_) => { + app.error_message = Some("Таймаут".to_string()); + 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 timeout(Duration::from_secs(10), app.td_client.send_code(app.code_input.clone())).await { + Ok(Ok(_)) => { + app.error_message = None; + app.status_message = None; + } + Ok(Err(e)) => { + app.error_message = Some(e); + app.status_message = None; + } + Err(_) => { + app.error_message = Some("Таймаут".to_string()); + 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 timeout(Duration::from_secs(10), app.td_client.send_password(app.password_input.clone())).await { + Ok(Ok(_)) => { + app.error_message = None; + app.status_message = None; + } + Ok(Err(e)) => { + app.error_message = Some(e); + app.status_message = None; + } + Err(_) => { + app.error_message = Some("Таймаут".to_string()); + app.status_message = None; + } + } + } + } + _ => {} + }, + _ => {} + } +} diff --git a/src/input/main_input.rs b/src/input/main_input.rs new file mode 100644 index 0000000..79355ee --- /dev/null +++ b/src/input/main_input.rs @@ -0,0 +1,174 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use std::time::Duration; +use tokio::time::timeout; +use crate::app::App; + +pub async fn handle(app: &mut App, key: KeyEvent) { + let has_super = key.modifiers.contains(KeyModifiers::SUPER); + let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + + // Глобальные команды (работают всегда) + match key.code { + KeyCode::Char('r') if has_ctrl => { + app.status_message = Some("Обновление чатов...".to_string()); + let _ = timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; + app.status_message = None; + return; + } + _ => {} + } + + // Cmd+j/k - навигация (работает и в списке чатов, и для скролла сообщений) + if has_super { + match key.code { + // Cmd+j - вниз (следующий чат ИЛИ скролл вниз) + KeyCode::Char('j') | KeyCode::Char('д') | KeyCode::Down => { + if app.selected_chat_id.is_some() { + // В открытом чате - скролл вниз (к новым сообщениям) + if app.message_scroll_offset > 0 { + app.message_scroll_offset = app.message_scroll_offset.saturating_sub(3); + } + } else { + // В списке чатов - следующий чат + app.next_chat(); + } + } + // Cmd+k - вверх (предыдущий чат ИЛИ скролл вверх) + KeyCode::Char('k') | KeyCode::Char('л') | KeyCode::Up => { + if app.selected_chat_id.is_some() { + // В открытом чате - скролл вверх (к старым сообщениям) + app.message_scroll_offset += 3; + + // Проверяем, нужно ли подгрузить старые сообщения + if !app.current_messages.is_empty() { + let oldest_msg_id = app.current_messages.first().map(|m| m.id).unwrap_or(0); + if let Some(chat_id) = app.get_selected_chat_id() { + // Подгружаем больше сообщений если скролл близко к верху + if app.message_scroll_offset > app.current_messages.len().saturating_sub(10) { + if let Ok(Ok(older)) = timeout( + Duration::from_secs(3), + app.td_client.load_older_messages(chat_id, oldest_msg_id, 20) + ).await { + if !older.is_empty() { + // Добавляем старые сообщения в начало + let mut new_messages = older; + new_messages.extend(app.current_messages.drain(..)); + app.current_messages = new_messages; + } + } + } + } + } + } else { + // В списке чатов - предыдущий чат + app.previous_chat(); + } + } + _ => {} + } + return; + } + + // Ctrl+k - в первый чат (только в режиме списка) + if has_ctrl && matches!(key.code, KeyCode::Char('k') | KeyCode::Char('л')) { + if app.selected_chat_id.is_none() { + app.select_first_chat(); + } + return; + } + + // Enter - открыть чат или отправить сообщение + if key.code == KeyCode::Enter { + if app.selected_chat_id.is_some() { + // Отправка сообщения + if !app.message_input.is_empty() { + if let Some(chat_id) = app.get_selected_chat_id() { + let text = app.message_input.clone(); + app.message_input.clear(); + + match timeout(Duration::from_secs(5), app.td_client.send_message(chat_id, text.clone())).await { + Ok(Ok(sent_msg)) => { + // Добавляем отправленное сообщение в список + app.current_messages.push(sent_msg); + // Сбрасываем скролл чтобы видеть новое сообщение + app.message_scroll_offset = 0; + } + Ok(Err(e)) => { + app.error_message = Some(e); + } + Err(_) => { + app.error_message = Some("Таймаут отправки".to_string()); + } + } + } + } + } else { + // Открываем чат + let prev_selected = app.selected_chat_id; + app.select_current_chat(); + + if app.selected_chat_id != prev_selected { + if let Some(chat_id) = app.get_selected_chat_id() { + app.status_message = Some("Загрузка сообщений...".to_string()); + app.message_scroll_offset = 0; + match timeout(Duration::from_secs(5), app.td_client.get_chat_history(chat_id, 50)).await { + Ok(Ok(messages)) => { + app.current_messages = messages; + app.status_message = None; + } + Ok(Err(e)) => { + app.error_message = Some(e); + app.status_message = None; + } + Err(_) => { + app.error_message = Some("Таймаут загрузки сообщений".to_string()); + app.status_message = None; + } + } + } + } + } + return; + } + + // Esc - закрыть чат + if key.code == KeyCode::Esc { + if app.selected_chat_id.is_some() { + app.close_chat(); + } + return; + } + + // Ввод текста в режиме открытого чата + if app.selected_chat_id.is_some() { + match key.code { + KeyCode::Backspace => { + app.message_input.pop(); + } + KeyCode::Char(c) => { + app.message_input.push(c); + } + _ => {} + } + } else { + // В режиме списка чатов - навигация j/k и переключение папок + match key.code { + // j или д - следующий чат + KeyCode::Char('j') | KeyCode::Char('д') | KeyCode::Down => { + app.next_chat(); + } + // k или л - предыдущий чат + KeyCode::Char('k') | KeyCode::Char('л') | KeyCode::Up => { + app.previous_chat(); + } + // Цифры - переключение папок + 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; + } + } + _ => {} + } + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..b7d31ea --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,5 @@ +mod auth; +mod main_input; + +pub use auth::handle as handle_auth_input; +pub use main_input::handle as handle_main_input; diff --git a/src/main.rs b/src/main.rs index 2d1a1b0..1786299 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,32 +1,44 @@ mod app; -mod telegram; +mod input; +mod tdlib; mod ui; +mod utils; -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, -}; +use ratatui::{backend::CrosstermBackend, Terminal}; use std::io; +use std::time::Duration; +use tdlib_rs::enums::Update; -use app::App; +use app::{App, AppScreen}; +use input::{handle_auth_input, handle_main_input}; +use tdlib::client::AuthState; +use utils::disable_tdlib_logs; #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> Result<(), io::Error> { + // Загружаем переменные окружения из .env + let _ = dotenvy::dotenv(); + + // Отключаем логи TDLib ДО создания клиента + disable_tdlib_logs(); + + // 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; + // Restore terminal disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -45,24 +57,120 @@ async fn main() -> Result<()> { async fn run_app( terminal: &mut Terminal, app: &mut App, -) -> Result<()> { - loop { - terminal.draw(|f| ui::draw(f, app))?; +) -> io::Result<()> { + // Канал для передачи updates из polling задачи в main loop + let (update_tx, mut update_rx) = tokio::sync::mpsc::unbounded_channel::(); - if event::poll(std::time::Duration::from_millis(100))? { + // Запускаем polling TDLib receive() в отдельной задаче + tokio::spawn(async move { + loop { + // receive() блокирующий, поэтому запускаем в blocking thread + let result = tokio::task::spawn_blocking(|| tdlib_rs::receive()).await; + if let Ok(Some((update, _client_id))) = result { + let _ = update_tx.send(update); + } + } + }); + + // Запускаем инициализацию TDLib в фоне + let client_id = app.td_client.client_id(); + let api_id = app.td_client.api_id; + let api_hash = app.td_client.api_hash.clone(); + + tokio::spawn(async move { + let _ = tdlib_rs::functions::set_tdlib_parameters( + false, // use_test_dc + "tdlib_data".to_string(), // database_directory + "".to_string(), // files_directory + "".to_string(), // database_encryption_key + true, // use_file_database + true, // use_chat_info_database + true, // use_message_database + false, // use_secret_chats + api_id, + 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 + client_id, + ) + .await; + }); + + loop { + // Обрабатываем updates от 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::render(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) { + use tokio::time::timeout; + + 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()); + + // Запрашиваем загрузку чатов с таймаутом + let _ = timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; + } + + // Синхронизируем чаты из td_client в app + if !app.td_client.chats.is_empty() { + 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)); + } + // Убираем статус загрузки когда чаты появились + if app.is_loading { + app.is_loading = false; + app.status_message = None; + } + } + } + AuthState::Closed => { + app.status_message = Some("Соединение закрыто".to_string()); + } + AuthState::Error(e) => { + app.error_message = Some(e.clone()); + } + } +} diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs new file mode 100644 index 0000000..ca253f5 --- /dev/null +++ b/src/tdlib/client.rs @@ -0,0 +1,423 @@ +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)] +#[allow(dead_code)] +pub enum AuthState { + WaitTdlibParameters, + WaitPhoneNumber, + WaitCode, + WaitPassword, + Ready, + Closed, + Error(String), +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct ChatInfo { + pub id: i64, + pub title: String, + pub last_message: String, + pub last_message_date: i32, + 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, +} + +#[allow(dead_code)] +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) + } + + pub fn client_id(&self) -> i32 { + self.client_id + } + + /// Инициализация 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 + 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, last_message_date) = update + .last_message + .as_ref() + .map(|msg| (extract_message_text_static(msg), msg.date)) + .unwrap_or_default(); + + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) { + chat.last_message = last_message_text; + chat.last_message_date = last_message_date; + } + + // Пересортируем после обновления + self.chats.sort_by(|a, b| b.last_message_date.cmp(&a.last_message_date)); + } + 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, last_message_date) = td_chat + .last_message + .as_ref() + .map(|m| (extract_message_text_static(m), m.date)) + .unwrap_or_default(); + + let chat_info = ChatInfo { + id: td_chat.id, + title: td_chat.title.clone(), + last_message, + last_message_date, + 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.last_message_date = chat_info.last_message_date; + existing.unread_count = chat_info.unread_count; + } else { + self.chats.push(chat_info); + } + + // Сортируем чаты по дате последнего сообщения (новые сверху) + self.chats.sort_by(|a, b| b.last_message_date.cmp(&a.last_message_date)); + } + + 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; + + // Загружаем историю с сервера (only_local=false) + let result = functions::get_chat_history( + chat_id, + 0, // from_message_id (0 = с последнего сообщения) + 0, // offset + limit, + false, // only_local - загружаем с сервера! + 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)), + } + } + + /// Загрузка старых сообщений (для скролла вверх) + pub async fn load_older_messages( + &mut self, + chat_id: i64, + from_message_id: i64, + limit: i32, + ) -> Result, String> { + let result = functions::get_chat_history( + chat_id, + from_message_id, + 0, // offset + limit, + false, // only_local + 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(); + 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)), + } + } + + /// Отправка текстового сообщения + pub async fn send_message(&self, chat_id: i64, text: String) -> Result { + use tdlib_rs::types::{FormattedText, InputMessageText}; + use tdlib_rs::enums::InputMessageContent; + + let content = InputMessageContent::InputMessageText(InputMessageText { + text: FormattedText { + text: text.clone(), + entities: vec![], + }, + link_preview_options: None, + clear_draft: true, + }); + + let result = functions::send_message( + chat_id, + 0, // message_thread_id + None, // reply_to + None, // options + content, + self.client_id, + ) + .await; + + match result { + Ok(tdlib_rs::enums::Message::Message(msg)) => { + // Конвертируем отправленное сообщение в MessageInfo + Ok(MessageInfo { + id: msg.id, + sender_name: "You".to_string(), + is_outgoing: true, + content: text, + date: msg.date, + is_read: false, + }) + } + 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/auth.rs b/src/ui/auth.rs new file mode 100644 index 0000000..a6fadb4 --- /dev/null +++ b/src/ui/auth.rs @@ -0,0 +1,136 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + text::Line, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use crate::app::App; +use crate::tdlib::client::AuthState; + +pub fn render(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]); + } +} diff --git a/src/ui/chat_list.rs b/src/ui/chat_list.rs new file mode 100644 index 0000000..64efba0 --- /dev/null +++ b/src/ui/chat_list.rs @@ -0,0 +1,61 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, List, ListItem, Paragraph}, + Frame, +}; +use crate::app::App; + +pub fn render(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() + .map(|chat| { + let is_selected = app.selected_chat_id == Some(chat.id); + 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]); +} diff --git a/src/ui/footer.rs b/src/ui/footer.rs new file mode 100644 index 0000000..2638232 --- /dev/null +++ b/src/ui/footer.rs @@ -0,0 +1,30 @@ +use ratatui::{ + layout::Rect, + style::{Color, Style}, + widgets::Paragraph, + Frame, +}; +use crate::app::App; + +pub fn render(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 if app.selected_chat_id.is_some() { + " Cmd+j/k: Scroll | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ".to_string() + } else { + " Cmd+j/k: Navigate | Ctrl+k: First | Enter: Open | 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); +} diff --git a/src/ui/loading.rs b/src/ui/loading.rs new file mode 100644 index 0000000..bb1a64b --- /dev/null +++ b/src/ui/loading.rs @@ -0,0 +1,40 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout}, + style::{Color, Modifier, Style}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use crate::app::App; + +pub fn render(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]); +} diff --git a/src/ui/main_screen.rs b/src/ui/main_screen.rs new file mode 100644 index 0000000..85bb007 --- /dev/null +++ b/src/ui/main_screen.rs @@ -0,0 +1,62 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use crate::app::App; +use super::{chat_list, messages, footer}; + +pub fn render(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]); + + chat_list::render(f, main_chunks[0], app); + messages::render(f, main_chunks[1], app); + footer::render(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); +} diff --git a/src/ui/messages.rs b/src/ui/messages.rs new file mode 100644 index 0000000..4f9f07e --- /dev/null +++ b/src/ui/messages.rs @@ -0,0 +1,116 @@ +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Paragraph}, + Frame, +}; +use crate::app::App; +use crate::utils::format_timestamp; + +pub fn render(f: &mut Frame, area: Rect, app: &App) { + if let Some(chat) = app.get_selected_chat() { + 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), + ))); + } + + // Вычисляем скролл с учётом пользовательского offset + let visible_height = message_chunks[1].height.saturating_sub(2) as usize; + let total_lines = lines.len(); + + let base_scroll = if total_lines > visible_height { + total_lines - visible_height + } else { + 0 + }; + + let scroll_offset = base_scroll.saturating_sub(app.message_scroll_offset) as u16; + + let messages_widget = Paragraph::new(lines) + .block(Block::default().borders(Borders::ALL)) + .scroll((scroll_offset, 0)); + f.render_widget(messages_widget, message_chunks[1]); + + // Input box + let input_text = if app.message_input.is_empty() { + "> Введите сообщение...".to_string() + } else { + format!("> {}", app.message_input) + }; + let input_style = if app.message_input.is_empty() { + Style::default().fg(Color::DarkGray) + } else { + Style::default().fg(Color::Yellow) + }; + let input = Paragraph::new(input_text) + .block(Block::default().borders(Borders::ALL)) + .style(input_style); + 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); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 216e8f9..ccd55a1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,170 +1,17 @@ -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, -}; +mod loading; +mod auth; +mod main_screen; +mod chat_list; +mod messages; +mod footer; -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()); +use ratatui::Frame; +use crate::app::{App, AppScreen}; - 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())); +pub fn render(f: &mut Frame, app: &mut App) { + match app.screen { + AppScreen::Loading => loading::render(f, app), + AppScreen::Auth => auth::render(f, app), + AppScreen::Main => main_screen::render(f, app), } - - 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/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d87d727 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,47 @@ +use std::ffi::CString; +use std::os::raw::c_char; + +#[link(name = "tdjson")] +extern "C" { + fn td_execute(request: *const c_char) -> *const c_char; +} + +/// Отключаем логи TDLib синхронно, до создания клиента +pub 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()); + } +} + +/// Форматирование timestamp в человекочитаемый формат +pub 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) + } +}