Files
telegram-tui/src/tdlib/client.rs
Mikhail Kilin 5ac10ea24c refactor: complete large files/functions refactoring (Phase 6-7)
Phase 6: Refactor tdlib/client.rs 
- Extract update handlers to update_handlers.rs (302 lines, 8 functions)
- Extract message converter to message_converter.rs (250 lines, 6 functions)
- Extract chat helpers to chat_helpers.rs (149 lines, 3 functions)
- Result: client.rs 1259 → 599 lines (-52%)

Phase 7: Refactor tdlib/messages.rs 
- Create message_conversion.rs module (158 lines)
- Extract 6 helper functions:
  - extract_content_text() - content extraction (~80 lines)
  - extract_entities() - formatting extraction (~10 lines)
  - extract_sender_name() - sender name with API call (~15 lines)
  - extract_forward_info() - forward info (~12 lines)
  - extract_reply_info() - reply info (~15 lines)
  - extract_reactions() - reactions extraction (~26 lines)
- Result: convert_message() 150 → 57 lines (-62%)
- Result: messages.rs 850 → 757 lines (-11%)

Summary:
-  All 4 large files refactored (100%)
-  All 629 tests passing
-  Category #2 "Large files/functions" COMPLETE
-  Documentation updated (REFACTORING_OPPORTUNITIES.md, CONTEXT.md)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-04 01:29:26 +03:00

600 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::types::{ChatId, MessageId, UserId};
use std::env;
use tdlib_rs::enums::{
ChatList, ConnectionState, Update, UserStatus,
Chat as TdChat
};
use tdlib_rs::types::Message as TdMessage;
use tdlib_rs::functions;
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;
/// 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();
///
/// // 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,
client_id: i32,
// Менеджеры (делегируем им функциональность)
pub auth: AuthManager,
pub chat_manager: ChatManager,
pub message_manager: MessageManager,
pub user_cache: UserCache,
pub reaction_manager: ReactionManager,
// Состояние сети
pub network_state: NetworkState,
}
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() -> 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,
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),
network_state: NetworkState::Connecting,
}
}
// Делегирование к 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<ProfileInfo, String> {
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<Vec<MessageInfo>, 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<Vec<MessageInfo>, 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<Vec<MessageInfo>, 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<Vec<MessageInfo>, 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<MessageId>,
reply_info: Option<super::types::ReplyInfo>,
) -> Result<MessageInfo, String> {
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<MessageInfo, String> {
self.message_manager
.edit_message(chat_id, message_id, text)
.await
}
pub async fn delete_messages(
&self,
chat_id: ChatId,
message_ids: Vec<MessageId>,
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<MessageId>,
) -> 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<Vec<String>, 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
}
// Вспомогательные методы
pub fn client_id(&self) -> i32 {
self.client_id
}
pub async fn get_me(&self) -> Result<i64, String> {
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<ChatInfo> {
&mut self.chat_manager.chats
}
pub fn folders(&self) -> &[FolderInfo] {
&self.chat_manager.folders
}
pub fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
&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<MessageInfo> {
&mut self.message_manager.current_chat_messages
}
pub fn current_chat_id(&self) -> Option<ChatId> {
self.message_manager.current_chat_id
}
pub fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
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<MessageInfo>) {
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<crate::types::MessageId>)] {
&self.message_manager.pending_view_messages
}
pub fn pending_view_messages_mut(&mut self) -> &mut Vec<(crate::types::ChatId, Vec<crate::types::MessageId>)> {
&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<crate::types::UserId> {
&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 ====================
/// Находит мутабельную ссылку на чат по ID.
///
/// Упрощает повторяющийся паттерн `self.chats_mut().iter_mut().find(...)`.
///
/// # Arguments
///
/// * `chat_id` - ID чата для поиска
///
/// # Returns
///
/// * `Some(&mut ChatInfo)` - если чат найден
/// * `None` - если чат не найден
/// Обрабатываем одно обновление от 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<tdlib_rs::types::TextEntity>) {
use tdlib_rs::enums::MessageContent;
match &message.content {
MessageContent::MessageText(text) => (text.text.text.clone(), text.text.entities.clone()),
_ => (String::new(), Vec::new()),
}
}
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(),
}
}
}