use crate::types::{ChatId, MessageId, UserId}; use std::env; use std::path::PathBuf; use tdlib_rs::enums::{Chat as TdChat, ChatList, ConnectionState, Update, UserStatus}; use tdlib_rs::functions; use tdlib_rs::types::Message as TdMessage; use super::auth::{AuthManager, AuthState}; use super::chats::ChatManager; use super::messages::MessageManager; use super::reactions::ReactionManager; use super::types::{ ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus, }; use super::users::UserCache; use crate::notifications::NotificationManager; /// TDLib client wrapper for Telegram integration. /// /// Provides high-level API for authentication, chat management, messaging, /// and user caching. Delegates functionality to specialized managers: /// - `AuthManager` for authentication flow /// - `ChatManager` for chat operations /// - `MessageManager` for message operations /// - `UserCache` for user information caching /// - `ReactionManager` for message reactions /// /// # Examples /// /// ```ignore /// use tele_tui::tdlib::TdClient; /// /// let mut client = TdClient::new(std::path::PathBuf::from("tdlib_data")); /// /// // Start authorization /// client.send_phone_number("+1234567890".to_string()).await?; /// client.send_code("12345".to_string()).await?; /// /// // Load chats /// client.load_chats(50).await?; /// # Ok::<(), String>(()) /// ``` pub struct TdClient { pub api_id: i32, pub api_hash: String, pub db_path: PathBuf, client_id: i32, // Менеджеры (делегируем им функциональность) pub auth: AuthManager, pub chat_manager: ChatManager, pub message_manager: MessageManager, pub user_cache: UserCache, pub reaction_manager: ReactionManager, pub notification_manager: NotificationManager, // Состояние сети pub network_state: NetworkState, } #[allow(dead_code)] impl TdClient { /// Creates a new TDLib client instance. /// /// Reads API credentials from: /// 1. ~/.config/tele-tui/credentials file /// 2. Environment variables `API_ID` and `API_HASH` (fallback) /// /// Initializes all managers and sets initial network state to Connecting. /// /// # Returns /// /// A new `TdClient` instance ready for authentication. pub fn new(db_path: PathBuf) -> Self { // Пробуем загрузить credentials из Config (файл или env) let (api_id, api_hash) = crate::config::Config::load_credentials().unwrap_or_else(|_| { // Fallback на прямое чтение из env (старое поведение) let api_id = env::var("API_ID") .unwrap_or_else(|_| "0".to_string()) .parse() .unwrap_or(0); let api_hash = env::var("API_HASH").unwrap_or_default(); (api_id, api_hash) }); let client_id = tdlib_rs::create_client(); Self { api_id, api_hash, db_path, client_id, auth: AuthManager::new(client_id), chat_manager: ChatManager::new(client_id), message_manager: MessageManager::new(client_id), user_cache: UserCache::new(client_id), reaction_manager: ReactionManager::new(client_id), notification_manager: NotificationManager::new(), network_state: NetworkState::Connecting, } } /// Configures notification manager from app config pub fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) { self.notification_manager.set_enabled(config.enabled); self.notification_manager .set_only_mentions(config.only_mentions); self.notification_manager.set_timeout(config.timeout_ms); self.notification_manager .set_urgency(config.urgency.clone()); // Note: show_preview is used when formatting notification body } /// Synchronizes muted chats from Telegram to notification manager. /// /// Should be called after chats are loaded to ensure muted chats don't trigger notifications. pub fn sync_notification_muted_chats(&mut self) { self.notification_manager .sync_muted_chats(&self.chat_manager.chats); } // Делегирование к auth /// Sends phone number for authentication. /// /// This is the first step of the authentication flow. /// /// # Arguments /// /// * `phone` - Phone number in international format (e.g., "+1234567890") /// /// # Errors /// /// Returns an error if the phone number is invalid or network request fails. pub async fn send_phone_number(&self, phone: String) -> Result<(), String> { self.auth.send_phone_number(phone).await } /// Sends authentication code received via SMS. /// /// This is the second step of the authentication flow. /// /// # Arguments /// /// * `code` - Authentication code (typically 5 digits) /// /// # Errors /// /// Returns an error if the code is invalid or expired. pub async fn send_code(&self, code: String) -> Result<(), String> { self.auth.send_code(code).await } /// Sends 2FA password if required. /// /// This is the third step of the authentication flow (if 2FA is enabled). /// /// # Arguments /// /// * `password` - Two-factor authentication password /// /// # Errors /// /// Returns an error if the password is incorrect. pub async fn send_password(&self, password: String) -> Result<(), String> { self.auth.send_password(password).await } // Делегирование к chat_manager /// Loads chats from the main chat list. /// /// Loads up to `limit` chats from ChatList::Main, excluding archived chats. /// Filters out "Deleted Account" chats automatically. /// /// # Arguments /// /// * `limit` - Maximum number of chats to load (typically 50-200) /// /// # Errors /// /// Returns an error if the network request fails. pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { self.chat_manager.load_chats(limit).await } /// Loads chats from a specific folder. /// /// # Arguments /// /// * `folder_id` - Folder ID (1-9 for user folders) /// * `limit` - Maximum number of chats to load /// /// # Errors /// /// Returns an error if the folder doesn't exist or network request fails. pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> { self.chat_manager.load_folder_chats(folder_id, limit).await } /// Leaves a group or channel. /// /// # Arguments /// /// * `chat_id` - ID of the chat to leave /// /// # Errors /// /// Returns an error if the user is not a member or network request fails. pub async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String> { self.chat_manager.leave_chat(chat_id).await } /// Gets profile information for a chat. /// /// Fetches detailed information including bio, username, member count, etc. /// /// # Arguments /// /// * `chat_id` - ID of the chat /// /// # Returns /// /// `ProfileInfo` with chat details /// /// # Errors /// /// Returns an error if the chat doesn't exist or network request fails. pub async fn get_profile_info(&self, chat_id: ChatId) -> Result { self.chat_manager.get_profile_info(chat_id).await } pub async fn send_chat_action(&self, chat_id: ChatId, action: tdlib_rs::enums::ChatAction) { self.chat_manager.send_chat_action(chat_id, action).await } pub fn clear_stale_typing_status(&mut self) -> bool { self.chat_manager.clear_stale_typing_status() } // Делегирование к message_manager pub async fn get_chat_history( &mut self, chat_id: ChatId, limit: i32, ) -> Result, String> { self.message_manager.get_chat_history(chat_id, limit).await } pub async fn load_older_messages( &mut self, chat_id: ChatId, from_message_id: MessageId, ) -> Result, String> { self.message_manager .load_older_messages(chat_id, from_message_id) .await } pub async fn get_pinned_messages( &mut self, chat_id: ChatId, ) -> Result, String> { self.message_manager.get_pinned_messages(chat_id).await } pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) { self.message_manager .load_current_pinned_message(chat_id) .await } pub async fn search_messages( &self, chat_id: ChatId, query: &str, ) -> Result, String> { self.message_manager.search_messages(chat_id, query).await } pub async fn send_message( &self, chat_id: ChatId, text: String, reply_to_message_id: Option, reply_info: Option, ) -> Result { self.message_manager .send_message(chat_id, text, reply_to_message_id, reply_info) .await } pub async fn edit_message( &self, chat_id: ChatId, message_id: MessageId, text: String, ) -> Result { self.message_manager .edit_message(chat_id, message_id, text) .await } pub async fn delete_messages( &self, chat_id: ChatId, message_ids: Vec, revoke: bool, ) -> Result<(), String> { self.message_manager .delete_messages(chat_id, message_ids, revoke) .await } pub async fn forward_messages( &self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec, ) -> Result<(), String> { self.message_manager .forward_messages(to_chat_id, from_chat_id, message_ids) .await } pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> { self.message_manager.set_draft_message(chat_id, text).await } pub fn push_message(&mut self, msg: MessageInfo) { self.message_manager.push_message(msg) } pub async fn fetch_missing_reply_info(&mut self) { self.message_manager.fetch_missing_reply_info().await } pub async fn process_pending_view_messages(&mut self) { self.message_manager.process_pending_view_messages().await } // Делегирование к user_cache pub fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> { self.user_cache.get_status_by_chat_id(chat_id) } pub async fn process_pending_user_ids(&mut self) { self.user_cache.process_pending_user_ids().await } // Делегирование к reaction_manager pub async fn get_message_available_reactions( &self, chat_id: ChatId, message_id: MessageId, ) -> Result, String> { self.reaction_manager .get_message_available_reactions(chat_id, message_id) .await } pub async fn toggle_reaction( &self, chat_id: ChatId, message_id: MessageId, emoji: String, ) -> Result<(), String> { self.reaction_manager .toggle_reaction(chat_id, message_id, emoji) .await } // Делегирование файловых операций /// Скачивает файл по file_id и возвращает локальный путь. pub async fn download_file(&self, file_id: i32) -> Result { match functions::download_file(file_id, 1, 0, 0, true, self.client_id).await { Ok(tdlib_rs::enums::File::File(file)) => { if file.local.is_downloading_completed && !file.local.path.is_empty() { Ok(file.local.path) } else { Err("Файл не скачан".to_string()) } } Err(e) => Err(format!("Ошибка скачивания файла: {:?}", e)), } } // Вспомогательные методы pub fn client_id(&self) -> i32 { self.client_id } pub async fn get_me(&self) -> Result { match functions::get_me(self.client_id).await { Ok(tdlib_rs::enums::User::User(user)) => Ok(user.id), Err(e) => Err(format!("Ошибка получения текущего пользователя: {:?}", e)), } } // Accessor methods для обратной совместимости pub fn auth_state(&self) -> &AuthState { &self.auth.state } pub fn chats(&self) -> &[ChatInfo] { &self.chat_manager.chats } pub fn chats_mut(&mut self) -> &mut Vec { &mut self.chat_manager.chats } pub fn folders(&self) -> &[FolderInfo] { &self.chat_manager.folders } pub fn folders_mut(&mut self) -> &mut Vec { &mut self.chat_manager.folders } pub fn current_chat_messages(&self) -> &[MessageInfo] { &self.message_manager.current_chat_messages } pub fn current_chat_messages_mut(&mut self) -> &mut Vec { &mut self.message_manager.current_chat_messages } pub fn current_chat_id(&self) -> Option { self.message_manager.current_chat_id } pub fn set_current_chat_id(&mut self, chat_id: Option) { self.message_manager.current_chat_id = chat_id; } pub fn current_pinned_message(&self) -> Option<&MessageInfo> { self.message_manager.current_pinned_message.as_ref() } pub fn set_current_pinned_message(&mut self, msg: Option) { self.message_manager.current_pinned_message = msg; } pub fn typing_status(&self) -> Option<&(crate::types::UserId, String, std::time::Instant)> { self.chat_manager.typing_status.as_ref() } pub fn set_typing_status( &mut self, status: Option<(crate::types::UserId, String, std::time::Instant)>, ) { self.chat_manager.typing_status = status; } pub fn pending_view_messages(&self) -> &[(crate::types::ChatId, Vec)] { &self.message_manager.pending_view_messages } pub fn pending_view_messages_mut( &mut self, ) -> &mut Vec<(crate::types::ChatId, Vec)> { &mut self.message_manager.pending_view_messages } pub fn pending_user_ids(&self) -> &[crate::types::UserId] { &self.user_cache.pending_user_ids } pub fn pending_user_ids_mut(&mut self) -> &mut Vec { &mut self.user_cache.pending_user_ids } pub fn main_chat_list_position(&self) -> i32 { self.chat_manager.main_chat_list_position } pub fn set_main_chat_list_position(&mut self, position: i32) { self.chat_manager.main_chat_list_position = position; } // User cache accessors pub fn user_cache(&self) -> &UserCache { &self.user_cache } pub fn user_cache_mut(&mut self) -> &mut UserCache { &mut self.user_cache } // ==================== Helper методы для упрощения обработки updates ==================== /// Обрабатываем одно обновление от TDLib pub fn handle_update(&mut self, update: Update) { match update { Update::AuthorizationState(state) => { crate::tdlib::update_handlers::handle_auth_state(self, state.authorization_state); } Update::NewChat(new_chat) => { // new_chat.chat is already a Chat struct, wrap it in TdChat enum let td_chat = TdChat::Chat(new_chat.chat.clone()); crate::tdlib::chat_helpers::add_or_update_chat(self, &td_chat); } Update::ChatLastMessage(update) => { let chat_id = ChatId::new(update.chat_id); let (last_message_text, last_message_date) = update .last_message .as_ref() .map(|msg| (Self::extract_message_text_static(msg).0, msg.date)) .unwrap_or_default(); crate::tdlib::chat_helpers::update_chat(self, chat_id, |chat| { chat.last_message = last_message_text; chat.last_message_date = last_message_date; }); // Обновляем позиции если они пришли for pos in update .positions .iter() .filter(|p| matches!(p.list, ChatList::Main)) { crate::tdlib::chat_helpers::update_chat(self, chat_id, |chat| { chat.order = pos.order; chat.is_pinned = pos.is_pinned; }); } // Пересортируем по order self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order)); } Update::ChatReadInbox(update) => { crate::tdlib::chat_helpers::update_chat( self, ChatId::new(update.chat_id), |chat| { chat.unread_count = update.unread_count; }, ); } Update::ChatUnreadMentionCount(update) => { crate::tdlib::chat_helpers::update_chat( self, ChatId::new(update.chat_id), |chat| { chat.unread_mention_count = update.unread_mention_count; }, ); } Update::ChatNotificationSettings(update) => { crate::tdlib::chat_helpers::update_chat( self, ChatId::new(update.chat_id), |chat| { // mute_for > 0 означает что чат замьючен chat.is_muted = update.notification_settings.mute_for > 0; }, ); } Update::ChatReadOutbox(update) => { // Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения let last_read_msg_id = MessageId::new(update.last_read_outbox_message_id); crate::tdlib::chat_helpers::update_chat( self, ChatId::new(update.chat_id), |chat| { chat.last_read_outbox_message_id = last_read_msg_id; }, ); // Если это текущий открытый чат — обновляем is_read у сообщений if Some(ChatId::new(update.chat_id)) == self.current_chat_id() { for msg in self.current_chat_messages_mut().iter_mut() { if msg.is_outgoing() && msg.id() <= last_read_msg_id { msg.state.is_read = true; } } } } Update::ChatPosition(update) => { crate::tdlib::update_handlers::handle_chat_position_update(self, update); } Update::NewMessage(new_msg) => { crate::tdlib::update_handlers::handle_new_message_update(self, new_msg); } Update::User(update) => { crate::tdlib::update_handlers::handle_user_update(self, update); } Update::ChatFolders(update) => { // Обновляем список папок *self.folders_mut() = update .chat_folders .into_iter() .map(|f| FolderInfo { id: f.id, name: f.title }) .collect(); self.set_main_chat_list_position(update.main_chat_list_position); } Update::UserStatus(update) => { // Обновляем онлайн-статус пользователя let status = match update.status { UserStatus::Online(_) => UserOnlineStatus::Online, UserStatus::Offline(offline) => UserOnlineStatus::Offline(offline.was_online), UserStatus::Recently(_) => UserOnlineStatus::Recently, UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek, UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth, UserStatus::Empty => UserOnlineStatus::LongTimeAgo, }; self.user_cache .user_statuses .insert(UserId::new(update.user_id), status); } Update::ConnectionState(update) => { // Обновляем состояние сетевого соединения self.network_state = match update.state { ConnectionState::WaitingForNetwork => NetworkState::WaitingForNetwork, ConnectionState::ConnectingToProxy => NetworkState::ConnectingToProxy, ConnectionState::Connecting => NetworkState::Connecting, ConnectionState::Updating => NetworkState::Updating, ConnectionState::Ready => NetworkState::Ready, }; } Update::ChatAction(update) => { crate::tdlib::update_handlers::handle_chat_action_update(self, update); } Update::ChatDraftMessage(update) => { crate::tdlib::update_handlers::handle_chat_draft_message_update(self, update); } Update::MessageInteractionInfo(update) => { crate::tdlib::update_handlers::handle_message_interaction_info_update(self, update); } Update::MessageSendSucceeded(update) => { crate::tdlib::update_handlers::handle_message_send_succeeded_update(self, update); } _ => {} } } // Helper functions pub fn extract_message_text_static( message: &TdMessage, ) -> (String, Vec) { use tdlib_rs::enums::MessageContent; match &message.content { MessageContent::MessageText(text) => { (text.text.text.clone(), text.text.entities.clone()) } _ => (String::new(), Vec::new()), } } /// Recreates the TDLib client with a new database path. /// /// Closes the old client, creates a new one, and spawns TDLib parameter initialization. pub async fn recreate_client(&mut self, db_path: PathBuf) -> Result<(), String> { // 1. Close old client let _ = functions::close(self.client_id).await; // 2. Create new client let new_client = TdClient::new(db_path); // 3. Spawn set_tdlib_parameters for new client let new_client_id = new_client.client_id; let api_id = new_client.api_id; let api_hash = new_client.api_hash.clone(); let db_path_str = new_client.db_path.to_string_lossy().to_string(); tokio::spawn(async move { if let Err(e) = functions::set_tdlib_parameters( false, db_path_str, "".to_string(), "".to_string(), true, true, true, false, api_id, api_hash, "en".to_string(), "Desktop".to_string(), "".to_string(), env!("CARGO_PKG_VERSION").to_string(), new_client_id, ) .await { tracing::error!("set_tdlib_parameters failed on recreate: {:?}", e); } }); // 4. Replace self *self = new_client; Ok(()) } pub fn extract_content_text(content: &tdlib_rs::enums::MessageContent) -> String { use tdlib_rs::enums::MessageContent; match content { MessageContent::MessageText(text) => text.text.text.clone(), _ => String::new(), } } }