Split fake TDLib client helpers
This commit is contained in:
250
docs/REFACTOR_PLAN.md
Normal file
250
docs/REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# tele-tui Refactor Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Finish the next review/refactor layer after the TDLib facade split, keeping behavior stable while making the code easier to test, review, and change.
|
||||||
|
|
||||||
|
**Architecture:** The current working tree already introduces scoped TDLib traits, removes the local `build.rs`, switches message formatting to the system local timezone, moves media chat handlers into a submodule, and makes fake TDLib state more explicit. The remaining work should continue in small vertical slices with focused tests after each slice.
|
||||||
|
|
||||||
|
**Tech Stack:** Rust 2021, Tokio, tdlib-rs, ratatui, crossterm, insta, criterion, Woodpecker CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Baseline
|
||||||
|
|
||||||
|
The current uncommitted layer should be treated as the baseline before starting the next refactor tasks.
|
||||||
|
|
||||||
|
- TDLib facade is split into scoped traits in `src/tdlib/trait.rs`.
|
||||||
|
- `src/tdlib/client_impl.rs` implements the scoped traits for `TdClient`.
|
||||||
|
- `current_chat_messages()` returns `Cow<'_, [MessageInfo]>`; mutation goes through `update_current_chat_messages`.
|
||||||
|
- Runtime date formatting uses the system local timezone; tests can inject deterministic time through `FixedLocalTime`.
|
||||||
|
- Media/image/voice chat handling is moved from `src/input/handlers/chat.rs` into `src/input/handlers/chat/media.rs`.
|
||||||
|
- The repository no longer uses the local `build.rs` that tried to link `tdlib-rs` during build-script execution.
|
||||||
|
|
||||||
|
Verification already used for this baseline:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo check --all-targets --all-features
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
cargo test --all-features
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 0: Commit Current Layer
|
||||||
|
|
||||||
|
Goal: preserve the completed facade/timezone/media/test-cleanup work before deeper refactors.
|
||||||
|
|
||||||
|
Files to review before commit:
|
||||||
|
|
||||||
|
- `CONTEXT.md`
|
||||||
|
- `Cargo.toml`
|
||||||
|
- `src/tdlib/trait.rs`
|
||||||
|
- `src/tdlib/mod.rs`
|
||||||
|
- `src/tdlib/client_impl.rs`
|
||||||
|
- `src/utils/formatting.rs`
|
||||||
|
- `src/input/handlers/chat.rs`
|
||||||
|
- `src/input/handlers/chat/media.rs`
|
||||||
|
- `tests/helpers/fake_tdclient.rs`
|
||||||
|
- `tests/helpers/fake_tdclient_impl.rs`
|
||||||
|
- touched tests and benches
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [x] Review `git diff --stat` and `git diff --check`.
|
||||||
|
- [x] Run the full verification commands from the baseline section.
|
||||||
|
- [x] Commit this layer separately from the follow-up refactors.
|
||||||
|
|
||||||
|
## Task 1: Split `FakeTdClient`
|
||||||
|
|
||||||
|
Goal: reduce `tests/helpers/fake_tdclient.rs` from one large mixed helper into smaller modules with clear responsibilities.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `tests/helpers/fake_tdclient.rs`
|
||||||
|
- `tests/helpers/fake_tdclient_impl.rs`
|
||||||
|
- `tests/helpers/mod.rs`
|
||||||
|
- new `tests/helpers/fake_tdclient/state.rs`
|
||||||
|
- new `tests/helpers/fake_tdclient/builders.rs`
|
||||||
|
- new `tests/helpers/fake_tdclient/operations.rs`
|
||||||
|
- new `tests/helpers/fake_tdclient/inspect.rs`
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [x] Move state aliases and shared storage fields into `state.rs`.
|
||||||
|
- [x] Move fixture construction helpers such as `with_chat`, `with_messages`, and account setup helpers into `builders.rs`.
|
||||||
|
- [x] Move behavior helpers such as send/edit/delete/reaction operations into `operations.rs`.
|
||||||
|
- [x] Move read/assertion helpers such as sent-message inspection and viewed-message inspection into `inspect.rs`.
|
||||||
|
- [x] Keep the public test API stable unless a call site becomes simpler and safer.
|
||||||
|
- [x] Remove direct test access to internal `Arc<Mutex<...>>` fields where helper methods are clearer.
|
||||||
|
- [x] Run `cargo test --all-features`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- `FakeTdClient` remains easy to construct in integration tests.
|
||||||
|
- No test loses behavior coverage.
|
||||||
|
- `tests/helpers/fake_tdclient.rs` becomes a small module entry point instead of the main implementation body.
|
||||||
|
|
||||||
|
## Task 2: Tighten Internal TDLib Mutation API
|
||||||
|
|
||||||
|
Goal: limit raw mutable access to TDLib client internals and replace cross-module state poking with domain-specific methods.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `src/tdlib/client.rs`
|
||||||
|
- `src/tdlib/chat_helpers.rs`
|
||||||
|
- `src/tdlib/update_handlers.rs`
|
||||||
|
- `src/tdlib/message_converter.rs`
|
||||||
|
- `src/tdlib/client_impl.rs`
|
||||||
|
|
||||||
|
Search command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "current_chat_messages_mut|chats_mut|folders_mut|pending_user_ids_mut|user_cache_mut" src/tdlib
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [ ] Add focused methods on `TdClient` for common mutations: update chat, update message by id, queue pending user, update user cache, update folders.
|
||||||
|
- [ ] Replace raw `*_mut()` usage in helper/update modules with those methods.
|
||||||
|
- [ ] Keep raw mutable access private to `TdClient` implementation where it is still needed.
|
||||||
|
- [ ] Add or update tests around message updates, user-cache updates, and chat-list updates.
|
||||||
|
- [ ] Run `cargo test --all-features`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- External and helper modules express intent through domain methods.
|
||||||
|
- Raw state access is either gone or contained in a small internal area.
|
||||||
|
|
||||||
|
## Task 3: Split Remaining Large Input and UI Files
|
||||||
|
|
||||||
|
Goal: make modal, message rendering, and app/input code easier to review independently.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `src/input/handlers/modal.rs`
|
||||||
|
- `src/input/handlers/chat.rs`
|
||||||
|
- `src/app/mod.rs`
|
||||||
|
- `src/ui/messages.rs`
|
||||||
|
- new `src/input/handlers/modal/account.rs`
|
||||||
|
- new `src/input/handlers/modal/delete.rs`
|
||||||
|
- new `src/input/handlers/modal/profile.rs`
|
||||||
|
- new `src/input/handlers/modal/reactions.rs`
|
||||||
|
- new `src/input/handlers/modal/pinned.rs`
|
||||||
|
- new `src/ui/messages/header.rs`
|
||||||
|
- new `src/ui/messages/list.rs`
|
||||||
|
- new `src/ui/messages/pinned.rs`
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [ ] Split modal handlers by modal type and keep `modal.rs` as the dispatcher/module entry point.
|
||||||
|
- [ ] Split message UI rendering into header, pinned-message, and list rendering modules.
|
||||||
|
- [ ] Keep public function names stable until each split is covered by tests.
|
||||||
|
- [ ] Avoid mixing behavior changes with file movement.
|
||||||
|
- [ ] Run focused modal/navigation/message tests after each split.
|
||||||
|
- [ ] Run `cargo test --all-features` after the full split.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Large files are reduced to dispatch/orchestration roles.
|
||||||
|
- The split does not change key handling or rendering behavior.
|
||||||
|
- Module names match user-facing concepts instead of implementation accidents.
|
||||||
|
|
||||||
|
## Task 4: Remove Production `unwrap()` Risk
|
||||||
|
|
||||||
|
Goal: keep test unwraps where useful, but remove production unwraps where runtime data can be absent.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `src/input/handlers/chat/media.rs`
|
||||||
|
- `src/input/handlers/chat.rs`
|
||||||
|
- `src/ui/components/message_bubble.rs`
|
||||||
|
- `src/utils/tdlib.rs`
|
||||||
|
- `src/audio/player.rs`
|
||||||
|
|
||||||
|
Search command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "unwrap\\(|expect\\(|panic!\\(" src
|
||||||
|
```
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [ ] Replace `photo_info().unwrap()` and `voice_info().unwrap()` with `let Some(...) else { ... }`.
|
||||||
|
- [ ] Replace `selected_chat_id.unwrap()` with an early return or status message.
|
||||||
|
- [ ] Review playback/message unwraps in `message_bubble.rs` and convert absent data into graceful UI fallback.
|
||||||
|
- [ ] Audit mutex unwraps separately; leave only cases where poisoning should be fatal and documented by context.
|
||||||
|
- [ ] Add tests for missing media metadata and absent selected chat.
|
||||||
|
- [ ] Run `cargo clippy --all-targets --all-features -- -D warnings`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Malformed or partial TDLib data does not panic in normal UI paths.
|
||||||
|
- Error handling stays local and does not add noisy user-facing text.
|
||||||
|
|
||||||
|
## Task 5: Resolve TODO and Compatibility Paths
|
||||||
|
|
||||||
|
Goal: make unfinished behavior explicit: either implement it, test it, or remove stale comments.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `src/input/key_handler.rs`
|
||||||
|
- `src/tdlib/reactions.rs`
|
||||||
|
- `src/tdlib/messages/operations.rs`
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [ ] Review every TODO in `src/`.
|
||||||
|
- [ ] Convert active TODOs into tests or tracked plan items.
|
||||||
|
- [ ] Remove stale TODOs whose behavior is already implemented.
|
||||||
|
- [ ] For pinned-message compatibility in `messages/operations.rs`, decide whether the fallback is still needed and document the decision in code or tests.
|
||||||
|
- [ ] Run `cargo test --all-features`.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- Remaining TODOs point to real unresolved behavior.
|
||||||
|
- No stale TODO describes behavior that no longer exists.
|
||||||
|
|
||||||
|
## Task 6: Add CI Quality Gate
|
||||||
|
|
||||||
|
Goal: make local quality checks reproducible in CI.
|
||||||
|
|
||||||
|
Target files:
|
||||||
|
|
||||||
|
- `.woodpecker/check.yml`
|
||||||
|
- `DEVELOPMENT.md`
|
||||||
|
- `AGENT.md`
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
|
||||||
|
- [ ] Add CI steps for `cargo check --all-targets --all-features`.
|
||||||
|
- [ ] Add CI steps for `cargo clippy --all-targets --all-features -- -D warnings`.
|
||||||
|
- [ ] Add CI steps for `cargo test --all-features`.
|
||||||
|
- [ ] Document the same commands in `DEVELOPMENT.md` or `AGENT.md`.
|
||||||
|
- [ ] Keep CI commands aligned with the commands used by agents and humans locally.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
|
||||||
|
- CI catches compile, lint, and test failures before merge.
|
||||||
|
- Local documentation and CI use the same command set.
|
||||||
|
|
||||||
|
## Global Acceptance Criteria
|
||||||
|
|
||||||
|
Before considering the refactor layer complete:
|
||||||
|
|
||||||
|
- [ ] `cargo check --all-targets --all-features` passes.
|
||||||
|
- [ ] `cargo clippy --all-targets --all-features -- -D warnings` passes.
|
||||||
|
- [ ] `cargo test --all-features` passes.
|
||||||
|
- [ ] `git diff --check` passes.
|
||||||
|
- [ ] No unexpected `*.snap.new` files remain.
|
||||||
|
- [ ] `rg -n "current_chat_messages_mut|chats_mut|folders_mut|pending_user_ids_mut|user_cache_mut" src/tdlib` shows only intentionally contained internal access.
|
||||||
|
- [ ] `rg -n "unwrap\\(|expect\\(|panic!\\(" src` has no risky production UI or TDLib data-path panics left.
|
||||||
|
|
||||||
|
## Recommended Commit Order
|
||||||
|
|
||||||
|
1. Baseline commit for already completed facade/timezone/media/test cleanup.
|
||||||
|
2. `FakeTdClient` split.
|
||||||
|
3. TDLib internal mutation API cleanup.
|
||||||
|
4. Modal and message UI file splits.
|
||||||
|
5. Production unwrap cleanup.
|
||||||
|
6. TODO cleanup.
|
||||||
|
7. CI quality gate.
|
||||||
@@ -1,891 +1,15 @@
|
|||||||
// Fake TDLib client for testing
|
// Fake TDLib client for testing.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
mod builders;
|
||||||
use std::sync::{Arc, Mutex};
|
mod inspect;
|
||||||
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
|
mod operations;
|
||||||
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
|
mod state;
|
||||||
use tele_tui::types::{ChatId, MessageId, UserId};
|
|
||||||
use tokio::sync::mpsc;
|
#[allow(unused_imports)]
|
||||||
|
pub use state::{
|
||||||
pub type ViewedMessages = Vec<(i64, Vec<i64>)>;
|
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, PendingViewMessages,
|
||||||
pub type PendingViewMessages = Vec<(ChatId, Vec<MessageId>)>;
|
SearchQuery, SentMessage, TdUpdate, ViewedMessages,
|
||||||
|
};
|
||||||
/// Update события от TDLib (упрощённая версия)
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub enum TdUpdate {
|
|
||||||
NewMessage {
|
|
||||||
chat_id: ChatId,
|
|
||||||
message: Box<MessageInfo>,
|
|
||||||
},
|
|
||||||
MessageContent {
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_id: MessageId,
|
|
||||||
new_text: String,
|
|
||||||
},
|
|
||||||
DeleteMessages {
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_ids: Vec<MessageId>,
|
|
||||||
},
|
|
||||||
ChatAction {
|
|
||||||
chat_id: ChatId,
|
|
||||||
user_id: UserId,
|
|
||||||
action: String,
|
|
||||||
},
|
|
||||||
MessageInteractionInfo {
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_id: MessageId,
|
|
||||||
reactions: Vec<ReactionInfo>,
|
|
||||||
},
|
|
||||||
ConnectionState {
|
|
||||||
state: NetworkState,
|
|
||||||
},
|
|
||||||
ChatReadOutbox {
|
|
||||||
chat_id: ChatId,
|
|
||||||
last_read_outbox_message_id: MessageId,
|
|
||||||
},
|
|
||||||
ChatDraftMessage {
|
|
||||||
chat_id: ChatId,
|
|
||||||
draft_text: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Упрощённый mock TDLib клиента для тестов
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct FakeTdClient {
|
|
||||||
// Данные
|
|
||||||
pub chats: Arc<Mutex<Vec<ChatInfo>>>,
|
|
||||||
pub messages: Arc<Mutex<HashMap<i64, Vec<MessageInfo>>>>,
|
|
||||||
pub folders: Arc<Mutex<Vec<FolderInfo>>>,
|
|
||||||
pub user_names: Arc<Mutex<HashMap<i64, String>>>,
|
|
||||||
pub profiles: Arc<Mutex<HashMap<i64, ProfileInfo>>>,
|
|
||||||
pub drafts: Arc<Mutex<HashMap<i64, String>>>,
|
|
||||||
pub available_reactions: Arc<Mutex<Vec<String>>>,
|
|
||||||
|
|
||||||
// Состояние
|
|
||||||
pub network_state: Arc<Mutex<NetworkState>>,
|
|
||||||
pub typing_chat_id: Arc<Mutex<Option<i64>>>,
|
|
||||||
pub current_chat_id: Arc<Mutex<Option<i64>>>,
|
|
||||||
pub current_pinned_message: Arc<Mutex<Option<MessageInfo>>>,
|
|
||||||
pub auth_state: Arc<Mutex<AuthState>>,
|
|
||||||
|
|
||||||
// История действий (для проверки в тестах)
|
|
||||||
pub sent_messages: Arc<Mutex<Vec<SentMessage>>>,
|
|
||||||
pub edited_messages: Arc<Mutex<Vec<EditedMessage>>>,
|
|
||||||
pub deleted_messages: Arc<Mutex<Vec<DeletedMessages>>>,
|
|
||||||
pub forwarded_messages: Arc<Mutex<Vec<ForwardedMessages>>>,
|
|
||||||
pub searched_queries: Arc<Mutex<Vec<SearchQuery>>>,
|
|
||||||
pub viewed_messages: Arc<Mutex<ViewedMessages>>, // (chat_id, message_ids)
|
|
||||||
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>, // (chat_id, action)
|
|
||||||
pub pending_view_messages: Arc<Mutex<PendingViewMessages>>, // Очередь для отметки как прочитанные
|
|
||||||
|
|
||||||
// Update channel для симуляции событий
|
|
||||||
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
|
|
||||||
|
|
||||||
// Скачанные файлы (file_id -> local_path)
|
|
||||||
pub downloaded_files: Arc<Mutex<HashMap<i32, String>>>,
|
|
||||||
|
|
||||||
// Настройки поведения
|
|
||||||
pub simulate_delays: bool,
|
|
||||||
pub fail_next_operation: Arc<Mutex<bool>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct SentMessage {
|
|
||||||
pub chat_id: i64,
|
|
||||||
pub text: String,
|
|
||||||
pub reply_to: Option<MessageId>,
|
|
||||||
pub reply_info: Option<ReplyInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct EditedMessage {
|
|
||||||
pub chat_id: i64,
|
|
||||||
pub message_id: MessageId,
|
|
||||||
pub new_text: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct DeletedMessages {
|
|
||||||
pub chat_id: i64,
|
|
||||||
pub message_ids: Vec<MessageId>,
|
|
||||||
pub revoke: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct ForwardedMessages {
|
|
||||||
pub from_chat_id: i64,
|
|
||||||
pub to_chat_id: i64,
|
|
||||||
pub message_ids: Vec<MessageId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct SearchQuery {
|
|
||||||
pub chat_id: i64,
|
|
||||||
pub query: String,
|
|
||||||
pub results_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for FakeTdClient {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Clone for FakeTdClient {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self {
|
|
||||||
chats: Arc::clone(&self.chats),
|
|
||||||
messages: Arc::clone(&self.messages),
|
|
||||||
folders: Arc::clone(&self.folders),
|
|
||||||
user_names: Arc::clone(&self.user_names),
|
|
||||||
profiles: Arc::clone(&self.profiles),
|
|
||||||
drafts: Arc::clone(&self.drafts),
|
|
||||||
available_reactions: Arc::clone(&self.available_reactions),
|
|
||||||
network_state: Arc::clone(&self.network_state),
|
|
||||||
typing_chat_id: Arc::clone(&self.typing_chat_id),
|
|
||||||
current_chat_id: Arc::clone(&self.current_chat_id),
|
|
||||||
current_pinned_message: Arc::clone(&self.current_pinned_message),
|
|
||||||
auth_state: Arc::clone(&self.auth_state),
|
|
||||||
sent_messages: Arc::clone(&self.sent_messages),
|
|
||||||
edited_messages: Arc::clone(&self.edited_messages),
|
|
||||||
deleted_messages: Arc::clone(&self.deleted_messages),
|
|
||||||
forwarded_messages: Arc::clone(&self.forwarded_messages),
|
|
||||||
searched_queries: Arc::clone(&self.searched_queries),
|
|
||||||
viewed_messages: Arc::clone(&self.viewed_messages),
|
|
||||||
chat_actions: Arc::clone(&self.chat_actions),
|
|
||||||
pending_view_messages: Arc::clone(&self.pending_view_messages),
|
|
||||||
downloaded_files: Arc::clone(&self.downloaded_files),
|
|
||||||
update_tx: Arc::clone(&self.update_tx),
|
|
||||||
simulate_delays: self.simulate_delays,
|
|
||||||
fail_next_operation: Arc::clone(&self.fail_next_operation),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl FakeTdClient {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
chats: Arc::new(Mutex::new(vec![])),
|
|
||||||
messages: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
folders: Arc::new(Mutex::new(vec![FolderInfo { id: 0, name: "All".to_string() }])),
|
|
||||||
user_names: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
profiles: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
drafts: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
available_reactions: Arc::new(Mutex::new(vec![
|
|
||||||
"👍".to_string(),
|
|
||||||
"❤️".to_string(),
|
|
||||||
"😂".to_string(),
|
|
||||||
"😮".to_string(),
|
|
||||||
"😢".to_string(),
|
|
||||||
"🙏".to_string(),
|
|
||||||
"👏".to_string(),
|
|
||||||
"🔥".to_string(),
|
|
||||||
])),
|
|
||||||
network_state: Arc::new(Mutex::new(NetworkState::Ready)),
|
|
||||||
typing_chat_id: Arc::new(Mutex::new(None)),
|
|
||||||
current_chat_id: Arc::new(Mutex::new(None)),
|
|
||||||
current_pinned_message: Arc::new(Mutex::new(None)),
|
|
||||||
auth_state: Arc::new(Mutex::new(AuthState::Ready)),
|
|
||||||
sent_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
edited_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
deleted_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
forwarded_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
searched_queries: Arc::new(Mutex::new(vec![])),
|
|
||||||
viewed_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
chat_actions: Arc::new(Mutex::new(vec![])),
|
|
||||||
pending_view_messages: Arc::new(Mutex::new(vec![])),
|
|
||||||
downloaded_files: Arc::new(Mutex::new(HashMap::new())),
|
|
||||||
update_tx: Arc::new(Mutex::new(None)),
|
|
||||||
simulate_delays: false,
|
|
||||||
fail_next_operation: Arc::new(Mutex::new(false)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Создать update channel для получения событий
|
|
||||||
pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver<TdUpdate>) {
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
|
||||||
*self.update_tx.lock().unwrap() = Some(tx);
|
|
||||||
(self, rx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Включить симуляцию задержек (как в реальном TDLib)
|
|
||||||
pub fn with_delays(mut self) -> Self {
|
|
||||||
self.simulate_delays = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Builder Methods ====================
|
|
||||||
|
|
||||||
/// Добавить чат
|
|
||||||
pub fn with_chat(self, chat: ChatInfo) -> Self {
|
|
||||||
self.chats.lock().unwrap().push(chat);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить несколько чатов
|
|
||||||
pub fn with_chats(self, chats: Vec<ChatInfo>) -> Self {
|
|
||||||
self.chats.lock().unwrap().extend(chats);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить сообщение в чат
|
|
||||||
pub fn with_message(self, chat_id: i64, message: MessageInfo) -> Self {
|
|
||||||
self.messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.entry(chat_id)
|
|
||||||
.or_default()
|
|
||||||
.push(message);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить несколько сообщений в чат
|
|
||||||
pub fn with_messages(self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
|
|
||||||
self.messages.lock().unwrap().insert(chat_id, messages);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить папку
|
|
||||||
pub fn with_folder(self, id: i32, name: &str) -> Self {
|
|
||||||
self.folders
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push(FolderInfo { id, name: name.to_string() });
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить пользователя
|
|
||||||
pub fn with_user(self, id: i64, name: &str) -> Self {
|
|
||||||
self.user_names.lock().unwrap().insert(id, name.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить профиль
|
|
||||||
pub fn with_profile(self, chat_id: i64, profile: ProfileInfo) -> Self {
|
|
||||||
self.profiles.lock().unwrap().insert(chat_id, profile);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить состояние сети
|
|
||||||
pub fn with_network_state(self, state: NetworkState) -> Self {
|
|
||||||
*self.network_state.lock().unwrap() = state;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить состояние авторизации
|
|
||||||
pub fn with_auth_state(self, state: AuthState) -> Self {
|
|
||||||
*self.auth_state.lock().unwrap() = state;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Добавить скачанный файл (для mock download_file)
|
|
||||||
pub fn with_downloaded_file(self, file_id: i32, path: &str) -> Self {
|
|
||||||
self.downloaded_files
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(file_id, path.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить доступные реакции
|
|
||||||
pub fn with_available_reactions(self, reactions: Vec<String>) -> Self {
|
|
||||||
*self.available_reactions.lock().unwrap() = reactions;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Async TDLib Operations ====================
|
|
||||||
|
|
||||||
/// Загрузить список чатов
|
|
||||||
pub async fn load_chats(&self, limit: usize) -> Result<Vec<ChatInfo>, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to load chats".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chats = self
|
|
||||||
.chats
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.iter()
|
|
||||||
.take(limit)
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
Ok(chats)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Открыть чат
|
|
||||||
pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to open chat".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
*self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить историю чата
|
|
||||||
pub async fn get_chat_history(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
limit: i32,
|
|
||||||
) -> Result<Vec<MessageInfo>, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to load history".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messages = self
|
|
||||||
.messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(&chat_id.as_i64())
|
|
||||||
.map(|msgs| msgs.iter().take(limit as usize).cloned().collect())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
Ok(messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Загрузить старые сообщения
|
|
||||||
pub async fn load_older_messages(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
from_message_id: MessageId,
|
|
||||||
) -> Result<Vec<MessageInfo>, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to load older messages".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let messages = self.messages.lock().unwrap();
|
|
||||||
let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?;
|
|
||||||
|
|
||||||
// Найти индекс сообщения и вернуть предыдущие
|
|
||||||
if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) {
|
|
||||||
let older: Vec<_> = chat_messages.iter().take(idx).cloned().collect();
|
|
||||||
Ok(older)
|
|
||||||
} else {
|
|
||||||
Ok(vec![])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Отправить сообщение
|
|
||||||
pub async fn send_message(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
text: String,
|
|
||||||
reply_to: Option<MessageId>,
|
|
||||||
reply_info: Option<ReplyInfo>,
|
|
||||||
) -> Result<MessageInfo, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to send message".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000);
|
|
||||||
|
|
||||||
self.sent_messages.lock().unwrap().push(SentMessage {
|
|
||||||
chat_id: chat_id.as_i64(),
|
|
||||||
text: text.clone(),
|
|
||||||
reply_to,
|
|
||||||
reply_info: reply_info.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let message = MessageInfo::new(
|
|
||||||
message_id,
|
|
||||||
"You".to_string(),
|
|
||||||
true, // is_outgoing
|
|
||||||
text.clone(),
|
|
||||||
vec![], // entities
|
|
||||||
chrono::Utc::now().timestamp() as i32,
|
|
||||||
0,
|
|
||||||
false, // is_read (станет true после Update)
|
|
||||||
true, // can_be_edited
|
|
||||||
true, // can_be_deleted_only_for_self
|
|
||||||
true, // can_be_deleted_for_all_users
|
|
||||||
reply_info,
|
|
||||||
None, // forward_from
|
|
||||||
vec![], // reactions
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем в историю
|
|
||||||
self.messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.entry(chat_id.as_i64())
|
|
||||||
.or_default()
|
|
||||||
.push(message.clone());
|
|
||||||
|
|
||||||
// Отправляем Update::NewMessage
|
|
||||||
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message.clone()) });
|
|
||||||
|
|
||||||
Ok(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Редактировать сообщение
|
|
||||||
pub async fn edit_message(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_id: MessageId,
|
|
||||||
new_text: String,
|
|
||||||
) -> Result<MessageInfo, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to edit message".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.edited_messages.lock().unwrap().push(EditedMessage {
|
|
||||||
chat_id: chat_id.as_i64(),
|
|
||||||
message_id,
|
|
||||||
new_text: new_text.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обновляем сообщение
|
|
||||||
let mut messages = self.messages.lock().unwrap();
|
|
||||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
|
||||||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
|
||||||
msg.content.text = new_text.clone();
|
|
||||||
msg.metadata.edit_date = msg.metadata.date + 60;
|
|
||||||
|
|
||||||
let updated = msg.clone();
|
|
||||||
drop(messages); // Освобождаем lock перед отправкой update
|
|
||||||
|
|
||||||
// Отправляем Update
|
|
||||||
self.send_update(TdUpdate::MessageContent { chat_id, message_id, new_text });
|
|
||||||
|
|
||||||
return Ok(updated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err("Message not found".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Удалить сообщения
|
|
||||||
pub async fn delete_messages(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_ids: Vec<MessageId>,
|
|
||||||
revoke: bool,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to delete messages".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.deleted_messages.lock().unwrap().push(DeletedMessages {
|
|
||||||
chat_id: chat_id.as_i64(),
|
|
||||||
message_ids: message_ids.clone(),
|
|
||||||
revoke,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удаляем из истории
|
|
||||||
let mut messages = self.messages.lock().unwrap();
|
|
||||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
|
||||||
chat_msgs.retain(|m| !message_ids.contains(&m.id()));
|
|
||||||
}
|
|
||||||
drop(messages);
|
|
||||||
|
|
||||||
// Отправляем Update
|
|
||||||
self.send_update(TdUpdate::DeleteMessages { chat_id, message_ids });
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Переслать сообщения
|
|
||||||
pub async fn forward_messages(
|
|
||||||
&self,
|
|
||||||
to_chat_id: ChatId,
|
|
||||||
from_chat_id: ChatId,
|
|
||||||
message_ids: Vec<MessageId>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to forward messages".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.simulate_delays {
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.forwarded_messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push(ForwardedMessages {
|
|
||||||
from_chat_id: from_chat_id.as_i64(),
|
|
||||||
to_chat_id: to_chat_id.as_i64(),
|
|
||||||
message_ids,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Поиск сообщений в чате
|
|
||||||
pub async fn search_messages(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
query: &str,
|
|
||||||
) -> Result<Vec<MessageInfo>, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to search messages".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let messages = self.messages.lock().unwrap();
|
|
||||||
let results: Vec<_> = messages
|
|
||||||
.get(&chat_id.as_i64())
|
|
||||||
.map(|msgs| {
|
|
||||||
msgs.iter()
|
|
||||||
.filter(|m| m.text().to_lowercase().contains(&query.to_lowercase()))
|
|
||||||
.cloned()
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
self.searched_queries.lock().unwrap().push(SearchQuery {
|
|
||||||
chat_id: chat_id.as_i64(),
|
|
||||||
query: query.to_string(),
|
|
||||||
results_count: results.len(),
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить черновик
|
|
||||||
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
|
||||||
if text.is_empty() {
|
|
||||||
self.drafts.lock().unwrap().remove(&chat_id.as_i64());
|
|
||||||
} else {
|
|
||||||
self.drafts
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(chat_id.as_i64(), text.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.send_update(TdUpdate::ChatDraftMessage {
|
|
||||||
chat_id,
|
|
||||||
draft_text: if text.is_empty() { None } else { Some(text) },
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Отправить действие в чате (typing, etc.)
|
|
||||||
pub async fn send_chat_action(&self, chat_id: ChatId, action: String) {
|
|
||||||
self.chat_actions
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push((chat_id.as_i64(), action.clone()));
|
|
||||||
|
|
||||||
if action == "Typing" {
|
|
||||||
*self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
|
||||||
} else if action == "Cancel" {
|
|
||||||
*self.typing_chat_id.lock().unwrap() = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить доступные реакции для сообщения
|
|
||||||
pub async fn get_message_available_reactions(
|
|
||||||
&self,
|
|
||||||
_chat_id: ChatId,
|
|
||||||
_message_id: MessageId,
|
|
||||||
) -> Result<Vec<String>, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to get available reactions".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.available_reactions.lock().unwrap().clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить/удалить реакцию
|
|
||||||
pub async fn toggle_reaction(
|
|
||||||
&self,
|
|
||||||
chat_id: ChatId,
|
|
||||||
message_id: MessageId,
|
|
||||||
emoji: String,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to toggle reaction".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем реакции на сообщении
|
|
||||||
let mut messages = self.messages.lock().unwrap();
|
|
||||||
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
|
||||||
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
|
||||||
let reactions = &mut msg.interactions.reactions;
|
|
||||||
|
|
||||||
// Toggle logic
|
|
||||||
if let Some(pos) = reactions
|
|
||||||
.iter()
|
|
||||||
.position(|r| r.emoji == emoji && r.is_chosen)
|
|
||||||
{
|
|
||||||
// Удаляем свою реакцию
|
|
||||||
reactions.remove(pos);
|
|
||||||
} else if let Some(reaction) = reactions.iter_mut().find(|r| r.emoji == emoji) {
|
|
||||||
// Добавляем себя к существующей реакции
|
|
||||||
reaction.is_chosen = true;
|
|
||||||
reaction.count += 1;
|
|
||||||
} else {
|
|
||||||
// Добавляем новую реакцию
|
|
||||||
reactions.push(ReactionInfo {
|
|
||||||
emoji: emoji.clone(),
|
|
||||||
count: 1,
|
|
||||||
is_chosen: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let updated_reactions = reactions.clone();
|
|
||||||
drop(messages);
|
|
||||||
|
|
||||||
// Отправляем Update
|
|
||||||
self.send_update(TdUpdate::MessageInteractionInfo {
|
|
||||||
chat_id,
|
|
||||||
message_id,
|
|
||||||
reactions: updated_reactions,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Скачать файл (mock)
|
|
||||||
pub async fn download_file(&self, file_id: i32) -> Result<String, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to download file".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.downloaded_files
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(&file_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| format!("File {} not found", file_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить информацию о профиле
|
|
||||||
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to get profile info".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.profiles
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(&chat_id.as_i64())
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| "Profile not found".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Отметить сообщения как просмотренные
|
|
||||||
pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
|
||||||
self.viewed_messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Загрузить чаты папки
|
|
||||||
pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> {
|
|
||||||
if self.should_fail() {
|
|
||||||
return Err("Failed to load folder chats".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Helper Methods ====================
|
|
||||||
|
|
||||||
/// Отправить update в канал (если он установлен)
|
|
||||||
fn send_update(&self, update: TdUpdate) {
|
|
||||||
if let Some(tx) = self.update_tx.lock().unwrap().as_ref() {
|
|
||||||
let _ = tx.send(update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Проверить нужно ли симулировать ошибку
|
|
||||||
fn should_fail(&self) -> bool {
|
|
||||||
let mut fail = self.fail_next_operation.lock().unwrap();
|
|
||||||
if *fail {
|
|
||||||
*fail = false; // Сбрасываем после первого использования
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Симулировать ошибку в следующей операции
|
|
||||||
pub fn fail_next(&self) {
|
|
||||||
*self.fail_next_operation.lock().unwrap() = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Симулировать входящее сообщение
|
|
||||||
pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) {
|
|
||||||
let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp());
|
|
||||||
|
|
||||||
let message = MessageInfo::new(
|
|
||||||
message_id,
|
|
||||||
sender_name.to_string(),
|
|
||||||
false, // is_outgoing
|
|
||||||
text,
|
|
||||||
vec![],
|
|
||||||
chrono::Utc::now().timestamp() as i32,
|
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
vec![],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем в историю
|
|
||||||
self.messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.entry(chat_id.as_i64())
|
|
||||||
.or_default()
|
|
||||||
.push(message.clone());
|
|
||||||
|
|
||||||
// Отправляем Update
|
|
||||||
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message) });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Симулировать typing от собеседника
|
|
||||||
pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) {
|
|
||||||
self.send_update(TdUpdate::ChatAction { chat_id, user_id, action: "Typing".to_string() });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Симулировать изменение состояния сети
|
|
||||||
pub fn simulate_network_change(&self, state: NetworkState) {
|
|
||||||
*self.network_state.lock().unwrap() = state.clone();
|
|
||||||
self.send_update(TdUpdate::ConnectionState { state });
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Симулировать прочтение сообщений
|
|
||||||
pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) {
|
|
||||||
self.send_update(TdUpdate::ChatReadOutbox {
|
|
||||||
chat_id,
|
|
||||||
last_read_outbox_message_id: last_read_message_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Getters for Test Assertions ====================
|
|
||||||
|
|
||||||
/// Получить все чаты
|
|
||||||
pub fn get_chats(&self) -> Vec<ChatInfo> {
|
|
||||||
self.chats.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить все папки
|
|
||||||
pub fn get_folders(&self) -> Vec<FolderInfo> {
|
|
||||||
self.folders.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить сообщения чата
|
|
||||||
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
|
|
||||||
self.messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.get(&chat_id)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить отправленные сообщения
|
|
||||||
pub fn get_sent_messages(&self) -> Vec<SentMessage> {
|
|
||||||
self.sent_messages.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить отредактированные сообщения
|
|
||||||
pub fn get_edited_messages(&self) -> Vec<EditedMessage> {
|
|
||||||
self.edited_messages.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить удалённые сообщения
|
|
||||||
pub fn get_deleted_messages(&self) -> Vec<DeletedMessages> {
|
|
||||||
self.deleted_messages.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить пересланные сообщения
|
|
||||||
pub fn get_forwarded_messages(&self) -> Vec<ForwardedMessages> {
|
|
||||||
self.forwarded_messages.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить поисковые запросы
|
|
||||||
pub fn get_search_queries(&self) -> Vec<SearchQuery> {
|
|
||||||
self.searched_queries.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить просмотренные сообщения
|
|
||||||
pub fn get_viewed_messages(&self) -> Vec<(i64, Vec<i64>)> {
|
|
||||||
self.viewed_messages.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить действия в чатах
|
|
||||||
pub fn get_chat_actions(&self) -> Vec<(i64, String)> {
|
|
||||||
self.chat_actions.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить текущее состояние сети
|
|
||||||
pub fn get_network_state(&self) -> NetworkState {
|
|
||||||
self.network_state.lock().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Получить ID текущего открытого чата
|
|
||||||
pub fn get_current_chat_id(&self) -> Option<i64> {
|
|
||||||
*self.current_chat_id.lock().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
|
|
||||||
*self.current_pinned_message.lock().unwrap() = msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn process_pending_view_messages(&mut self) {
|
|
||||||
let mut pending = self.pending_view_messages.lock().unwrap();
|
|
||||||
for (chat_id, message_ids) in pending.drain(..) {
|
|
||||||
let ids: Vec<i64> = message_ids.iter().map(|id| id.as_i64()).collect();
|
|
||||||
self.viewed_messages
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.push((chat_id.as_i64(), ids));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить update channel для получения событий
|
|
||||||
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
|
|
||||||
*self.update_tx.lock().unwrap() = Some(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Очистить всю историю действий
|
|
||||||
pub fn clear_all_history(&self) {
|
|
||||||
self.sent_messages.lock().unwrap().clear();
|
|
||||||
self.edited_messages.lock().unwrap().clear();
|
|
||||||
self.deleted_messages.lock().unwrap().clear();
|
|
||||||
self.forwarded_messages.lock().unwrap().clear();
|
|
||||||
self.searched_queries.lock().unwrap().clear();
|
|
||||||
self.viewed_messages.lock().unwrap().clear();
|
|
||||||
self.chat_actions.lock().unwrap().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -970,12 +94,10 @@ mod tests {
|
|||||||
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
let (client, mut rx) = FakeTdClient::new().with_update_channel();
|
||||||
let chat_id = ChatId::new(123);
|
let chat_id = ChatId::new(123);
|
||||||
|
|
||||||
// Отправляем сообщение
|
|
||||||
let _ = client
|
let _ = client
|
||||||
.send_message(chat_id, "Test".to_string(), None, None)
|
.send_message(chat_id, "Test".to_string(), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Проверяем что получили Update
|
|
||||||
if let Some(update) = rx.recv().await {
|
if let Some(update) = rx.recv().await {
|
||||||
match update {
|
match update {
|
||||||
TdUpdate::NewMessage { chat_id: updated_chat, .. } => {
|
TdUpdate::NewMessage { chat_id: updated_chat, .. } => {
|
||||||
@@ -995,14 +117,12 @@ mod tests {
|
|||||||
|
|
||||||
client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob");
|
client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob");
|
||||||
|
|
||||||
// Проверяем Update
|
|
||||||
if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await {
|
if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await {
|
||||||
assert_eq!(message.text(), "Hello from Bob");
|
assert_eq!(message.text(), "Hello from Bob");
|
||||||
assert_eq!(message.sender_name(), "Bob");
|
assert_eq!(message.sender_name(), "Bob");
|
||||||
assert!(!message.is_outgoing());
|
assert!(!message.is_outgoing());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем что сообщение добавилось
|
|
||||||
assert_eq!(client.get_messages(123).len(), 1);
|
assert_eq!(client.get_messages(123).len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,16 +131,13 @@ mod tests {
|
|||||||
let client = FakeTdClient::new();
|
let client = FakeTdClient::new();
|
||||||
let chat_id = ChatId::new(123);
|
let chat_id = ChatId::new(123);
|
||||||
|
|
||||||
// Устанавливаем флаг ошибки
|
|
||||||
client.fail_next();
|
client.fail_next();
|
||||||
|
|
||||||
// Следующая операция должна упасть
|
|
||||||
let result = client
|
let result = client
|
||||||
.send_message(chat_id, "Test".to_string(), None, None)
|
.send_message(chat_id, "Test".to_string(), None, None)
|
||||||
.await;
|
.await;
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
// Но следующая должна пройти
|
|
||||||
let result2 = client
|
let result2 = client
|
||||||
.send_message(chat_id, "Test2".to_string(), None, None)
|
.send_message(chat_id, "Test2".to_string(), None, None)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
86
tests/helpers/fake_tdclient/builders.rs
Normal file
86
tests/helpers/fake_tdclient/builders.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use super::{FakeTdClient, TdUpdate};
|
||||||
|
use tele_tui::tdlib::types::FolderInfo;
|
||||||
|
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl FakeTdClient {
|
||||||
|
/// Create an update channel for receiving simulated TDLib events.
|
||||||
|
pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver<TdUpdate>) {
|
||||||
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
*self.update_tx.lock().unwrap() = Some(tx);
|
||||||
|
(self, rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable simulated delays, closer to real TDLib behavior.
|
||||||
|
pub fn with_delays(mut self) -> Self {
|
||||||
|
self.simulate_delays = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_chat(self, chat: ChatInfo) -> Self {
|
||||||
|
self.chats.lock().unwrap().push(chat);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_chats(self, chats: Vec<ChatInfo>) -> Self {
|
||||||
|
self.chats.lock().unwrap().extend(chats);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_message(self, chat_id: i64, message: MessageInfo) -> Self {
|
||||||
|
self.messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(chat_id)
|
||||||
|
.or_default()
|
||||||
|
.push(message);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_messages(self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
|
||||||
|
self.messages.lock().unwrap().insert(chat_id, messages);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_folder(self, id: i32, name: &str) -> Self {
|
||||||
|
self.folders
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(FolderInfo { id, name: name.to_string() });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_user(self, id: i64, name: &str) -> Self {
|
||||||
|
self.user_names.lock().unwrap().insert(id, name.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_profile(self, chat_id: i64, profile: ProfileInfo) -> Self {
|
||||||
|
self.profiles.lock().unwrap().insert(chat_id, profile);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_network_state(self, state: NetworkState) -> Self {
|
||||||
|
*self.network_state.lock().unwrap() = state;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_auth_state(self, state: AuthState) -> Self {
|
||||||
|
*self.auth_state.lock().unwrap() = state;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_downloaded_file(self, file_id: i32, path: &str) -> Self {
|
||||||
|
self.downloaded_files
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(file_id, path.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_available_reactions(self, reactions: Vec<String>) -> Self {
|
||||||
|
*self.available_reactions.lock().unwrap() = reactions;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tests/helpers/fake_tdclient/inspect.rs
Normal file
92
tests/helpers/fake_tdclient/inspect.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use super::{
|
||||||
|
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, SearchQuery, SentMessage,
|
||||||
|
TdUpdate,
|
||||||
|
};
|
||||||
|
use tele_tui::tdlib::types::FolderInfo;
|
||||||
|
use tele_tui::tdlib::{ChatInfo, MessageInfo, NetworkState};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl FakeTdClient {
|
||||||
|
pub fn get_chats(&self) -> Vec<ChatInfo> {
|
||||||
|
self.chats.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_folders(&self) -> Vec<FolderInfo> {
|
||||||
|
self.folders.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_messages(&self, chat_id: i64) -> Vec<MessageInfo> {
|
||||||
|
self.messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&chat_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sent_messages(&self) -> Vec<SentMessage> {
|
||||||
|
self.sent_messages.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_edited_messages(&self) -> Vec<EditedMessage> {
|
||||||
|
self.edited_messages.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_deleted_messages(&self) -> Vec<DeletedMessages> {
|
||||||
|
self.deleted_messages.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_forwarded_messages(&self) -> Vec<ForwardedMessages> {
|
||||||
|
self.forwarded_messages.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_search_queries(&self) -> Vec<SearchQuery> {
|
||||||
|
self.searched_queries.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_viewed_messages(&self) -> Vec<(i64, Vec<i64>)> {
|
||||||
|
self.viewed_messages.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_chat_actions(&self) -> Vec<(i64, String)> {
|
||||||
|
self.chat_actions.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_network_state(&self) -> NetworkState {
|
||||||
|
self.network_state.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_chat_id(&self) -> Option<i64> {
|
||||||
|
*self.current_chat_id.lock().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
|
||||||
|
*self.current_pinned_message.lock().unwrap() = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_pending_view_messages(&mut self) {
|
||||||
|
let mut pending = self.pending_view_messages.lock().unwrap();
|
||||||
|
for (chat_id, message_ids) in pending.drain(..) {
|
||||||
|
let ids: Vec<i64> = message_ids.iter().map(|id| id.as_i64()).collect();
|
||||||
|
self.viewed_messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((chat_id.as_i64(), ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_update_channel(&self, tx: mpsc::UnboundedSender<TdUpdate>) {
|
||||||
|
*self.update_tx.lock().unwrap() = Some(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_all_history(&self) {
|
||||||
|
self.sent_messages.lock().unwrap().clear();
|
||||||
|
self.edited_messages.lock().unwrap().clear();
|
||||||
|
self.deleted_messages.lock().unwrap().clear();
|
||||||
|
self.forwarded_messages.lock().unwrap().clear();
|
||||||
|
self.searched_queries.lock().unwrap().clear();
|
||||||
|
self.viewed_messages.lock().unwrap().clear();
|
||||||
|
self.chat_actions.lock().unwrap().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
458
tests/helpers/fake_tdclient/operations.rs
Normal file
458
tests/helpers/fake_tdclient/operations.rs
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
use super::{
|
||||||
|
DeletedMessages, EditedMessage, FakeTdClient, ForwardedMessages, SearchQuery, SentMessage,
|
||||||
|
TdUpdate,
|
||||||
|
};
|
||||||
|
use tele_tui::tdlib::types::ReactionInfo;
|
||||||
|
use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo};
|
||||||
|
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl FakeTdClient {
|
||||||
|
pub async fn load_chats(&self, limit: usize) -> Result<Vec<ChatInfo>, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to load chats".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let chats = self
|
||||||
|
.chats
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.take(limit)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
Ok(chats)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to open chat".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
*self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_chat_history(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
limit: i32,
|
||||||
|
) -> Result<Vec<MessageInfo>, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to load history".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messages = self
|
||||||
|
.messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&chat_id.as_i64())
|
||||||
|
.map(|msgs| msgs.iter().take(limit as usize).cloned().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_older_messages(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
from_message_id: MessageId,
|
||||||
|
) -> Result<Vec<MessageInfo>, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to load older messages".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let messages = self.messages.lock().unwrap();
|
||||||
|
let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?;
|
||||||
|
|
||||||
|
if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) {
|
||||||
|
let older = chat_messages.iter().take(idx).cloned().collect();
|
||||||
|
Ok(older)
|
||||||
|
} else {
|
||||||
|
Ok(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_message(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
text: String,
|
||||||
|
reply_to: Option<MessageId>,
|
||||||
|
reply_info: Option<ReplyInfo>,
|
||||||
|
) -> Result<MessageInfo, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to send message".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000);
|
||||||
|
|
||||||
|
self.sent_messages.lock().unwrap().push(SentMessage {
|
||||||
|
chat_id: chat_id.as_i64(),
|
||||||
|
text: text.clone(),
|
||||||
|
reply_to,
|
||||||
|
reply_info: reply_info.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let message = MessageInfo::new(
|
||||||
|
message_id,
|
||||||
|
"You".to_string(),
|
||||||
|
true,
|
||||||
|
text,
|
||||||
|
vec![],
|
||||||
|
chrono::Utc::now().timestamp() as i32,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
reply_info,
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
self.messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(chat_id.as_i64())
|
||||||
|
.or_default()
|
||||||
|
.push(message.clone());
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message.clone()) });
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit_message(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_id: MessageId,
|
||||||
|
new_text: String,
|
||||||
|
) -> Result<MessageInfo, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to edit message".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.edited_messages.lock().unwrap().push(EditedMessage {
|
||||||
|
chat_id: chat_id.as_i64(),
|
||||||
|
message_id,
|
||||||
|
new_text: new_text.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut messages = self.messages.lock().unwrap();
|
||||||
|
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||||
|
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||||||
|
msg.content.text = new_text.clone();
|
||||||
|
msg.metadata.edit_date = msg.metadata.date + 60;
|
||||||
|
|
||||||
|
let updated = msg.clone();
|
||||||
|
drop(messages);
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::MessageContent { chat_id, message_id, new_text });
|
||||||
|
|
||||||
|
return Ok(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("Message not found".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_messages(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_ids: Vec<MessageId>,
|
||||||
|
revoke: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to delete messages".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.deleted_messages.lock().unwrap().push(DeletedMessages {
|
||||||
|
chat_id: chat_id.as_i64(),
|
||||||
|
message_ids: message_ids.clone(),
|
||||||
|
revoke,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut messages = self.messages.lock().unwrap();
|
||||||
|
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||||
|
chat_msgs.retain(|m| !message_ids.contains(&m.id()));
|
||||||
|
}
|
||||||
|
drop(messages);
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::DeleteMessages { chat_id, message_ids });
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn forward_messages(
|
||||||
|
&self,
|
||||||
|
to_chat_id: ChatId,
|
||||||
|
from_chat_id: ChatId,
|
||||||
|
message_ids: Vec<MessageId>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to forward messages".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.simulate_delays {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.forwarded_messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(ForwardedMessages {
|
||||||
|
from_chat_id: from_chat_id.as_i64(),
|
||||||
|
to_chat_id: to_chat_id.as_i64(),
|
||||||
|
message_ids,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_messages(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
query: &str,
|
||||||
|
) -> Result<Vec<MessageInfo>, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to search messages".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let messages = self.messages.lock().unwrap();
|
||||||
|
let results: Vec<_> = messages
|
||||||
|
.get(&chat_id.as_i64())
|
||||||
|
.map(|msgs| {
|
||||||
|
msgs.iter()
|
||||||
|
.filter(|m| m.text().to_lowercase().contains(&query.to_lowercase()))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
self.searched_queries.lock().unwrap().push(SearchQuery {
|
||||||
|
chat_id: chat_id.as_i64(),
|
||||||
|
query: query.to_string(),
|
||||||
|
results_count: results.len(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
||||||
|
if text.is_empty() {
|
||||||
|
self.drafts.lock().unwrap().remove(&chat_id.as_i64());
|
||||||
|
} else {
|
||||||
|
self.drafts
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(chat_id.as_i64(), text.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::ChatDraftMessage {
|
||||||
|
chat_id,
|
||||||
|
draft_text: if text.is_empty() { None } else { Some(text) },
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_chat_action(&self, chat_id: ChatId, action: String) {
|
||||||
|
self.chat_actions
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((chat_id.as_i64(), action.clone()));
|
||||||
|
|
||||||
|
if action == "Typing" {
|
||||||
|
*self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64());
|
||||||
|
} else if action == "Cancel" {
|
||||||
|
*self.typing_chat_id.lock().unwrap() = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_message_available_reactions(
|
||||||
|
&self,
|
||||||
|
_chat_id: ChatId,
|
||||||
|
_message_id: MessageId,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to get available reactions".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.available_reactions.lock().unwrap().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn toggle_reaction(
|
||||||
|
&self,
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_id: MessageId,
|
||||||
|
emoji: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to toggle reaction".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut messages = self.messages.lock().unwrap();
|
||||||
|
if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) {
|
||||||
|
if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) {
|
||||||
|
let reactions = &mut msg.interactions.reactions;
|
||||||
|
|
||||||
|
if let Some(pos) = reactions
|
||||||
|
.iter()
|
||||||
|
.position(|reaction| reaction.emoji == emoji && reaction.is_chosen)
|
||||||
|
{
|
||||||
|
reactions.remove(pos);
|
||||||
|
} else if let Some(reaction) = reactions
|
||||||
|
.iter_mut()
|
||||||
|
.find(|reaction| reaction.emoji == emoji)
|
||||||
|
{
|
||||||
|
reaction.is_chosen = true;
|
||||||
|
reaction.count += 1;
|
||||||
|
} else {
|
||||||
|
reactions.push(ReactionInfo {
|
||||||
|
emoji: emoji.clone(),
|
||||||
|
count: 1,
|
||||||
|
is_chosen: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated_reactions = reactions.clone();
|
||||||
|
drop(messages);
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::MessageInteractionInfo {
|
||||||
|
chat_id,
|
||||||
|
message_id,
|
||||||
|
reactions: updated_reactions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_file(&self, file_id: i32) -> Result<String, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to download file".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.downloaded_files
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&file_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format!("File {} not found", file_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to get profile info".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profiles
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&chat_id.as_i64())
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| "Profile not found".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec<MessageId>) {
|
||||||
|
self.viewed_messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect()));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> {
|
||||||
|
if self.should_fail() {
|
||||||
|
return Err("Failed to load folder chats".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_update(&self, update: TdUpdate) {
|
||||||
|
if let Some(tx) = self.update_tx.lock().unwrap().as_ref() {
|
||||||
|
let _ = tx.send(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_fail(&self) -> bool {
|
||||||
|
let mut fail = self.fail_next_operation.lock().unwrap();
|
||||||
|
if *fail {
|
||||||
|
*fail = false;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fail_next(&self) {
|
||||||
|
*self.fail_next_operation.lock().unwrap() = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) {
|
||||||
|
let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp());
|
||||||
|
|
||||||
|
let message = MessageInfo::new(
|
||||||
|
message_id,
|
||||||
|
sender_name.to_string(),
|
||||||
|
false,
|
||||||
|
text,
|
||||||
|
vec![],
|
||||||
|
chrono::Utc::now().timestamp() as i32,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
|
self.messages
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(chat_id.as_i64())
|
||||||
|
.or_default()
|
||||||
|
.push(message.clone());
|
||||||
|
|
||||||
|
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message) });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) {
|
||||||
|
self.send_update(TdUpdate::ChatAction { chat_id, user_id, action: "Typing".to_string() });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_network_change(&self, state: tele_tui::tdlib::NetworkState) {
|
||||||
|
*self.network_state.lock().unwrap() = state.clone();
|
||||||
|
self.send_update(TdUpdate::ConnectionState { state });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) {
|
||||||
|
self.send_update(TdUpdate::ChatReadOutbox {
|
||||||
|
chat_id,
|
||||||
|
last_read_outbox_message_id: last_read_message_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
201
tests/helpers/fake_tdclient/state.rs
Normal file
201
tests/helpers/fake_tdclient/state.rs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tele_tui::tdlib::types::{FolderInfo, ReactionInfo};
|
||||||
|
use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo};
|
||||||
|
use tele_tui::types::{ChatId, MessageId, UserId};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
pub type ViewedMessages = Vec<(i64, Vec<i64>)>;
|
||||||
|
pub type PendingViewMessages = Vec<(ChatId, Vec<MessageId>)>;
|
||||||
|
|
||||||
|
/// Update events from TDLib, simplified for tests.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum TdUpdate {
|
||||||
|
NewMessage {
|
||||||
|
chat_id: ChatId,
|
||||||
|
message: Box<MessageInfo>,
|
||||||
|
},
|
||||||
|
MessageContent {
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_id: MessageId,
|
||||||
|
new_text: String,
|
||||||
|
},
|
||||||
|
DeleteMessages {
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_ids: Vec<MessageId>,
|
||||||
|
},
|
||||||
|
ChatAction {
|
||||||
|
chat_id: ChatId,
|
||||||
|
user_id: UserId,
|
||||||
|
action: String,
|
||||||
|
},
|
||||||
|
MessageInteractionInfo {
|
||||||
|
chat_id: ChatId,
|
||||||
|
message_id: MessageId,
|
||||||
|
reactions: Vec<ReactionInfo>,
|
||||||
|
},
|
||||||
|
ConnectionState {
|
||||||
|
state: NetworkState,
|
||||||
|
},
|
||||||
|
ChatReadOutbox {
|
||||||
|
chat_id: ChatId,
|
||||||
|
last_read_outbox_message_id: MessageId,
|
||||||
|
},
|
||||||
|
ChatDraftMessage {
|
||||||
|
chat_id: ChatId,
|
||||||
|
draft_text: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simplified mock TDLib client for tests.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct FakeTdClient {
|
||||||
|
pub chats: Arc<Mutex<Vec<ChatInfo>>>,
|
||||||
|
pub messages: Arc<Mutex<HashMap<i64, Vec<MessageInfo>>>>,
|
||||||
|
pub folders: Arc<Mutex<Vec<FolderInfo>>>,
|
||||||
|
pub user_names: Arc<Mutex<HashMap<i64, String>>>,
|
||||||
|
pub profiles: Arc<Mutex<HashMap<i64, ProfileInfo>>>,
|
||||||
|
pub drafts: Arc<Mutex<HashMap<i64, String>>>,
|
||||||
|
pub available_reactions: Arc<Mutex<Vec<String>>>,
|
||||||
|
|
||||||
|
pub network_state: Arc<Mutex<NetworkState>>,
|
||||||
|
pub typing_chat_id: Arc<Mutex<Option<i64>>>,
|
||||||
|
pub current_chat_id: Arc<Mutex<Option<i64>>>,
|
||||||
|
pub current_pinned_message: Arc<Mutex<Option<MessageInfo>>>,
|
||||||
|
pub auth_state: Arc<Mutex<AuthState>>,
|
||||||
|
|
||||||
|
pub sent_messages: Arc<Mutex<Vec<SentMessage>>>,
|
||||||
|
pub edited_messages: Arc<Mutex<Vec<EditedMessage>>>,
|
||||||
|
pub deleted_messages: Arc<Mutex<Vec<DeletedMessages>>>,
|
||||||
|
pub forwarded_messages: Arc<Mutex<Vec<ForwardedMessages>>>,
|
||||||
|
pub searched_queries: Arc<Mutex<Vec<SearchQuery>>>,
|
||||||
|
pub viewed_messages: Arc<Mutex<ViewedMessages>>,
|
||||||
|
pub chat_actions: Arc<Mutex<Vec<(i64, String)>>>,
|
||||||
|
pub pending_view_messages: Arc<Mutex<PendingViewMessages>>,
|
||||||
|
|
||||||
|
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
|
||||||
|
pub downloaded_files: Arc<Mutex<HashMap<i32, String>>>,
|
||||||
|
|
||||||
|
pub simulate_delays: bool,
|
||||||
|
pub fail_next_operation: Arc<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct SentMessage {
|
||||||
|
pub chat_id: i64,
|
||||||
|
pub text: String,
|
||||||
|
pub reply_to: Option<MessageId>,
|
||||||
|
pub reply_info: Option<ReplyInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct EditedMessage {
|
||||||
|
pub chat_id: i64,
|
||||||
|
pub message_id: MessageId,
|
||||||
|
pub new_text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct DeletedMessages {
|
||||||
|
pub chat_id: i64,
|
||||||
|
pub message_ids: Vec<MessageId>,
|
||||||
|
pub revoke: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct ForwardedMessages {
|
||||||
|
pub from_chat_id: i64,
|
||||||
|
pub to_chat_id: i64,
|
||||||
|
pub message_ids: Vec<MessageId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct SearchQuery {
|
||||||
|
pub chat_id: i64,
|
||||||
|
pub query: String,
|
||||||
|
pub results_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FakeTdClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for FakeTdClient {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
chats: Arc::clone(&self.chats),
|
||||||
|
messages: Arc::clone(&self.messages),
|
||||||
|
folders: Arc::clone(&self.folders),
|
||||||
|
user_names: Arc::clone(&self.user_names),
|
||||||
|
profiles: Arc::clone(&self.profiles),
|
||||||
|
drafts: Arc::clone(&self.drafts),
|
||||||
|
available_reactions: Arc::clone(&self.available_reactions),
|
||||||
|
network_state: Arc::clone(&self.network_state),
|
||||||
|
typing_chat_id: Arc::clone(&self.typing_chat_id),
|
||||||
|
current_chat_id: Arc::clone(&self.current_chat_id),
|
||||||
|
current_pinned_message: Arc::clone(&self.current_pinned_message),
|
||||||
|
auth_state: Arc::clone(&self.auth_state),
|
||||||
|
sent_messages: Arc::clone(&self.sent_messages),
|
||||||
|
edited_messages: Arc::clone(&self.edited_messages),
|
||||||
|
deleted_messages: Arc::clone(&self.deleted_messages),
|
||||||
|
forwarded_messages: Arc::clone(&self.forwarded_messages),
|
||||||
|
searched_queries: Arc::clone(&self.searched_queries),
|
||||||
|
viewed_messages: Arc::clone(&self.viewed_messages),
|
||||||
|
chat_actions: Arc::clone(&self.chat_actions),
|
||||||
|
pending_view_messages: Arc::clone(&self.pending_view_messages),
|
||||||
|
downloaded_files: Arc::clone(&self.downloaded_files),
|
||||||
|
update_tx: Arc::clone(&self.update_tx),
|
||||||
|
simulate_delays: self.simulate_delays,
|
||||||
|
fail_next_operation: Arc::clone(&self.fail_next_operation),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl FakeTdClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
chats: Arc::new(Mutex::new(vec![])),
|
||||||
|
messages: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
folders: Arc::new(Mutex::new(vec![FolderInfo { id: 0, name: "All".to_string() }])),
|
||||||
|
user_names: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
profiles: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
drafts: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
available_reactions: Arc::new(Mutex::new(vec![
|
||||||
|
"👍".to_string(),
|
||||||
|
"❤️".to_string(),
|
||||||
|
"😂".to_string(),
|
||||||
|
"😮".to_string(),
|
||||||
|
"😢".to_string(),
|
||||||
|
"🙏".to_string(),
|
||||||
|
"👏".to_string(),
|
||||||
|
"🔥".to_string(),
|
||||||
|
])),
|
||||||
|
network_state: Arc::new(Mutex::new(NetworkState::Ready)),
|
||||||
|
typing_chat_id: Arc::new(Mutex::new(None)),
|
||||||
|
current_chat_id: Arc::new(Mutex::new(None)),
|
||||||
|
current_pinned_message: Arc::new(Mutex::new(None)),
|
||||||
|
auth_state: Arc::new(Mutex::new(AuthState::Ready)),
|
||||||
|
sent_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
edited_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
deleted_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
forwarded_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
searched_queries: Arc::new(Mutex::new(vec![])),
|
||||||
|
viewed_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
chat_actions: Arc::new(Mutex::new(vec![])),
|
||||||
|
pending_view_messages: Arc::new(Mutex::new(vec![])),
|
||||||
|
downloaded_files: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
update_tx: Arc::new(Mutex::new(None)),
|
||||||
|
simulate_delays: false,
|
||||||
|
fail_next_operation: Arc::new(Mutex::new(false)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user