Refactor TDLib facade and local time handling

This commit is contained in:
Mikhail Kilin
2026-05-17 17:58:29 +03:00
parent e09b83be69
commit 2e510dc932
38 changed files with 1025 additions and 862 deletions

View File

@@ -146,7 +146,7 @@ impl TestAppBuilder {
pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.push(message);
self
}
@@ -155,7 +155,7 @@ impl TestAppBuilder {
pub fn with_messages(mut self, chat_id: i64, messages: Vec<MessageInfo>) -> Self {
self.messages
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.extend(messages);
self
}

View File

@@ -7,13 +7,16 @@ use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInf
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 события от TDLib (упрощённая версия)
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum TdUpdate {
NewMessage {
chat_id: ChatId,
message: MessageInfo,
message: Box<MessageInfo>,
},
MessageContent {
chat_id: ChatId,
@@ -72,9 +75,9 @@ pub struct FakeTdClient {
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<Vec<(i64, Vec<i64>)>>>, // (chat_id, message_ids)
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<Vec<(ChatId, Vec<MessageId>)>>>, // Очередь для отметки как прочитанные
pub pending_view_messages: Arc<Mutex<PendingViewMessages>>, // Очередь для отметки как прочитанные
// Update channel для симуляции событий
pub update_tx: Arc<Mutex<Option<mpsc::UnboundedSender<TdUpdate>>>>,
@@ -238,7 +241,7 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id)
.or_insert_with(Vec::new)
.or_default()
.push(message);
self
}
@@ -424,11 +427,11 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.or_default()
.push(message.clone());
// Отправляем Update::NewMessage
self.send_update(TdUpdate::NewMessage { chat_id, message: message.clone() });
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message.clone()) });
Ok(message)
}
@@ -759,11 +762,11 @@ impl FakeTdClient {
.lock()
.unwrap()
.entry(chat_id.as_i64())
.or_insert_with(Vec::new)
.or_default()
.push(message.clone());
// Отправляем Update
self.send_update(TdUpdate::NewMessage { chat_id, message });
self.send_update(TdUpdate::NewMessage { chat_id, message: Box::new(message) });
}
/// Симулировать typing от собеседника
@@ -852,6 +855,21 @@ impl FakeTdClient {
*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);

View File

@@ -1,10 +1,14 @@
//! Implementation of TdClientTrait for FakeTdClient
//! Test implementation of the TDLib client traits for FakeTdClient.
use super::fake_tdclient::FakeTdClient;
use async_trait::async_trait;
use std::borrow::Cow;
use std::path::PathBuf;
use tdlib_rs::enums::{ChatAction, Update};
use tele_tui::tdlib::TdClientTrait;
use tele_tui::tdlib::{
AccountClient, AuthClient, ChatActionClient, ChatClient, ClientState, FileClient,
MessageClient, NotificationClient, ReactionClient, UpdateClient, UserClient,
};
use tele_tui::tdlib::{
AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache,
UserOnlineStatus,
@@ -12,8 +16,7 @@ use tele_tui::tdlib::{
use tele_tui::types::{ChatId, MessageId, UserId};
#[async_trait]
impl TdClientTrait for FakeTdClient {
// ============ Auth methods (not implemented for fake) ============
impl AuthClient for FakeTdClient {
async fn send_phone_number(&self, _phone: String) -> Result<(), String> {
Ok(())
}
@@ -25,10 +28,11 @@ impl TdClientTrait for FakeTdClient {
async fn send_password(&self, _password: String) -> Result<(), String> {
Ok(())
}
}
// ============ Chat methods ============
#[async_trait]
impl ChatClient for FakeTdClient {
async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
// FakeTdClient loads chats but returns void
let _ = FakeTdClient::load_chats(self, limit as usize).await?;
Ok(())
}
@@ -38,7 +42,6 @@ impl TdClientTrait for FakeTdClient {
}
async fn leave_chat(&self, _chat_id: ChatId) -> Result<(), String> {
// Not implemented for fake client
Ok(())
}
@@ -46,18 +49,54 @@ impl TdClientTrait for FakeTdClient {
FakeTdClient::get_profile_info(self, chat_id).await
}
// ============ Chat actions ============
fn chats(&self) -> &[ChatInfo] {
&[]
}
fn folders(&self) -> &[FolderInfo] {
&[]
}
fn main_chat_list_position(&self) -> i32 {
0
}
fn set_main_chat_list_position(&mut self, _position: i32) {}
fn update_chats<F>(&mut self, updater: F)
where
F: FnOnce(&mut Vec<ChatInfo>),
{
updater(&mut self.chats.lock().unwrap());
}
fn update_folders<F>(&mut self, updater: F)
where
F: FnOnce(&mut Vec<FolderInfo>),
{
updater(&mut self.folders.lock().unwrap());
}
}
#[async_trait]
impl ChatActionClient for FakeTdClient {
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
let action_str = format!("{:?}", action);
FakeTdClient::send_chat_action(self, chat_id, action_str).await;
FakeTdClient::send_chat_action(self, chat_id, format!("{:?}", action)).await;
}
fn clear_stale_typing_status(&mut self) -> bool {
// Not implemented for fake
false
}
// ============ Message methods ============
fn typing_status(&self) -> Option<&(UserId, String, std::time::Instant)> {
None
}
fn set_typing_status(&mut self, _status: Option<(UserId, String, std::time::Instant)>) {}
}
#[async_trait]
impl MessageClient for FakeTdClient {
async fn get_chat_history(
&mut self,
chat_id: ChatId,
@@ -75,13 +114,10 @@ impl TdClientTrait for FakeTdClient {
}
async fn get_pinned_messages(&mut self, _chat_id: ChatId) -> Result<Vec<MessageInfo>, String> {
// Not implemented for fake
Ok(vec![])
}
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {
// Not implemented for fake
}
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {}
async fn search_messages(
&self,
@@ -132,16 +168,77 @@ impl TdClientTrait for FakeTdClient {
FakeTdClient::set_draft_message(self, chat_id, text).await
}
fn push_message(&mut self, _msg: MessageInfo) {
// Not used in fake client
fn current_chat_messages(&self) -> Cow<'_, [MessageInfo]> {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
Cow::Owned(self.get_messages(chat_id))
} else {
Cow::Owned(Vec::new())
}
}
async fn fetch_missing_reply_info(&mut self) {
// Not used in fake client
fn current_chat_id(&self) -> Option<ChatId> {
self.get_current_chat_id().map(ChatId::new)
}
fn current_pinned_message(&self) -> Option<MessageInfo> {
self.current_pinned_message.lock().unwrap().clone()
}
fn push_message(&mut self, msg: MessageInfo) {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
self.messages
.lock()
.unwrap()
.entry(chat_id)
.or_default()
.push(msg);
}
}
fn clear_current_chat_messages(&mut self) {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
self.messages.lock().unwrap().remove(&chat_id);
}
}
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
self.messages.lock().unwrap().insert(chat_id, messages);
}
}
fn update_current_chat_messages<F>(&mut self, updater: F)
where
F: FnOnce(&mut Vec<MessageInfo>),
{
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
let mut all_messages = self.messages.lock().unwrap();
updater(all_messages.entry(chat_id).or_default());
}
}
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
*self.current_chat_id.lock().unwrap() = chat_id.map(|id| id.as_i64());
}
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
*self.current_pinned_message.lock().unwrap() = msg;
}
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)] {
&[]
}
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>) {
self.pending_view_messages
.lock()
.unwrap()
.push((chat_id, message_ids));
}
async fn fetch_missing_reply_info(&mut self) {}
async fn process_pending_view_messages(&mut self) {
// Перемещаем pending в viewed для проверки в тестах
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();
@@ -151,18 +248,35 @@ impl TdClientTrait for FakeTdClient {
.push((chat_id.as_i64(), ids));
}
}
}
// ============ User methods ============
#[async_trait]
impl UserClient for FakeTdClient {
fn get_user_status_by_chat_id(&self, _chat_id: ChatId) -> Option<&UserOnlineStatus> {
// Not implemented for fake
None
}
async fn process_pending_user_ids(&mut self) {
// Not used in fake client
fn pending_user_ids(&self) -> &[UserId] {
&[]
}
// ============ Reaction methods ============
fn user_cache(&self) -> &UserCache {
use std::sync::OnceLock;
static EMPTY_CACHE: OnceLock<UserCache> = OnceLock::new();
EMPTY_CACHE.get_or_init(|| UserCache::new(0))
}
fn update_user_cache<F>(&mut self, _updater: F)
where
F: FnOnce(&mut UserCache),
{
}
async fn process_pending_user_ids(&mut self) {}
}
#[async_trait]
impl ReactionClient for FakeTdClient {
async fn get_message_available_reactions(
&self,
chat_id: ChatId,
@@ -179,29 +293,30 @@ impl TdClientTrait for FakeTdClient {
) -> Result<(), String> {
FakeTdClient::toggle_reaction(self, chat_id, message_id, reaction).await
}
}
// ============ File methods ============
#[async_trait]
impl FileClient for FakeTdClient {
async fn download_file(&self, file_id: i32) -> Result<String, String> {
FakeTdClient::download_file(self, file_id).await
}
async fn download_voice_note(&self, file_id: i32) -> Result<String, String> {
// Fake implementation: return a fake path
Ok(format!("/tmp/fake_voice_{}.ogg", file_id))
}
}
// ============ Getters (immutable) ============
#[async_trait]
impl ClientState for FakeTdClient {
fn client_id(&self) -> i32 {
0 // Fake client ID
0
}
async fn get_me(&self) -> Result<i64, String> {
Ok(12345) // Fake user ID
Ok(12345)
}
fn auth_state(&self) -> &AuthState {
// Can't return reference from Arc<Mutex>, need to use a different approach
// For now, return a static reference based on the current state
use std::sync::OnceLock;
static AUTH_STATE_READY: AuthState = AuthState::Ready;
static AUTH_STATE_WAIT_PHONE: OnceLock<AuthState> = OnceLock::new();
@@ -222,133 +337,24 @@ impl TdClientTrait for FakeTdClient {
}
}
fn chats(&self) -> &[ChatInfo] {
// FakeTdClient uses Arc<Mutex>, can't return direct reference
// This is a limitation - we'll need to work around it
&[]
}
fn folders(&self) -> &[FolderInfo] {
&[]
}
fn current_chat_messages(&self) -> Vec<MessageInfo> {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
return self.get_messages(chat_id);
}
Vec::new()
}
fn current_chat_id(&self) -> Option<ChatId> {
self.get_current_chat_id().map(ChatId::new)
}
fn current_pinned_message(&self) -> Option<MessageInfo> {
self.current_pinned_message.lock().unwrap().clone()
}
fn typing_status(&self) -> Option<&(UserId, String, std::time::Instant)> {
None
}
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)] {
&[]
}
fn pending_user_ids(&self) -> &[UserId] {
&[]
}
fn main_chat_list_position(&self) -> i32 {
0
}
fn user_cache(&self) -> &UserCache {
// Not implemented for fake - return empty cache
use std::sync::OnceLock;
static EMPTY_CACHE: OnceLock<UserCache> = OnceLock::new();
EMPTY_CACHE.get_or_init(|| UserCache::new(0))
}
fn network_state(&self) -> tele_tui::tdlib::types::NetworkState {
FakeTdClient::get_network_state(self)
}
}
// ============ Setters (mutable) ============
fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
// Can't return mutable reference from Arc<Mutex>
// This is a design limitation - we need a different approach
panic!("chats_mut not supported for FakeTdClient - use get_chats() instead")
}
impl NotificationClient for FakeTdClient {
fn configure_notifications(&mut self, _config: &tele_tui::config::NotificationsConfig) {}
fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
panic!("folders_mut not supported for FakeTdClient")
}
fn sync_notification_muted_chats(&mut self) {}
}
fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo> {
panic!("current_chat_messages_mut not supported for FakeTdClient")
}
fn clear_current_chat_messages(&mut self) {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
self.messages.lock().unwrap().remove(&chat_id);
}
}
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
self.messages.lock().unwrap().insert(chat_id, messages);
}
}
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
*self.current_chat_id.lock().unwrap() = chat_id.map(|id| id.as_i64());
}
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
*self.current_pinned_message.lock().unwrap() = msg;
}
fn set_typing_status(&mut self, _status: Option<(UserId, String, std::time::Instant)>) {
// Not implemented
}
fn enqueue_pending_view_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>) {
self.pending_view_messages
.lock()
.unwrap()
.push((chat_id, message_ids));
}
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId> {
panic!("pending_user_ids_mut not supported for FakeTdClient")
}
fn set_main_chat_list_position(&mut self, _position: i32) {
// Not implemented
}
fn user_cache_mut(&mut self) -> &mut UserCache {
panic!("user_cache_mut not supported for FakeTdClient")
}
// ============ Notification methods ============
fn configure_notifications(&mut self, _config: &tele_tui::config::NotificationsConfig) {
// Not implemented for fake client (notifications are not tested)
}
fn sync_notification_muted_chats(&mut self) {
// Not implemented for fake client (notifications are not tested)
}
// ============ Account switching ============
#[async_trait]
impl AccountClient for FakeTdClient {
async fn recreate_client(&mut self, _db_path: PathBuf) -> Result<(), String> {
// No-op for fake client
Ok(())
}
// ============ Update handling ============
fn handle_update(&mut self, _update: Update) {
// Not implemented for fake client
}
}
impl UpdateClient for FakeTdClient {
fn handle_update(&mut self, _update: Update) {}
}

View File

@@ -6,7 +6,4 @@ mod fake_tdclient_impl; // TdClientTrait implementation for FakeTdClient
pub mod snapshot_utils;
pub mod test_data;
pub use app_builder::TestAppBuilder;
pub use fake_tdclient::FakeTdClient;
pub use snapshot_utils::{buffer_to_string, render_to_buffer};
pub use test_data::{create_test_chat, create_test_message, create_test_user};

View File

@@ -219,20 +219,22 @@ impl TestMessageBuilder {
}
/// Хелперы для быстрого создания тестовых данных
pub fn create_test_chat(title: &str, id: i64) -> ChatInfo {
TestChatBuilder::new(title, id).build()
}
#[allow(dead_code)]
pub fn create_test_message(content: &str, id: i64) -> MessageInfo {
TestMessageBuilder::new(content, id).build()
}
#[allow(dead_code)]
pub fn create_test_user(name: &str, id: i64) -> (i64, String) {
(id, name.to_string())
}
/// Хелпер для создания профиля
#[allow(dead_code)]
pub fn create_test_profile(title: &str, chat_id: i64) -> ProfileInfo {
ProfileInfo {
chat_id: ChatId::new(chat_id),