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>
This commit is contained in:
Mikhail Kilin
2026-02-04 01:29:26 +03:00
parent b081886e34
commit 5ac10ea24c
9 changed files with 1198 additions and 780 deletions

View File

@@ -0,0 +1,302 @@
//! Update handlers for TDLib events.
//!
//! This module contains functions that process various types of updates from TDLib.
//! Each handler is responsible for updating the application state based on the received update.
use crate::types::{ChatId, MessageId, UserId};
use std::time::Instant;
use tdlib_rs::enums::{
AuthorizationState, ChatAction, ChatList, MessageSender,
};
use tdlib_rs::types::{
UpdateChatAction, UpdateChatDraftMessage, UpdateChatPosition,
UpdateMessageInteractionInfo, UpdateMessageSendSucceeded, UpdateNewMessage, UpdateUser,
};
use super::auth::AuthState;
use super::client::TdClient;
use super::types::ReactionInfo;
/// Обрабатывает Update::NewMessage - добавление нового сообщения
pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessage) {
// Добавляем новое сообщение если это текущий открытый чат
let chat_id = ChatId::new(new_msg.message.chat_id);
if Some(chat_id) != client.current_chat_id() {
return;
}
let msg_info = crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id);
let msg_id = msg_info.id();
let is_incoming = !msg_info.is_outgoing();
// Проверяем, есть ли уже сообщение с таким id
let existing_idx = client
.current_chat_messages()
.iter()
.position(|m| m.id() == msg_info.id());
match existing_idx {
Some(idx) => {
// Сообщение уже есть - обновляем
if is_incoming {
client.current_chat_messages_mut()[idx] = msg_info;
} else {
// Для исходящих: обновляем can_be_edited и другие поля,
// но сохраняем reply_to (добавленный при отправке)
let existing = &mut client.current_chat_messages_mut()[idx];
existing.state.can_be_edited = msg_info.state.can_be_edited;
existing.state.can_be_deleted_only_for_self =
msg_info.state.can_be_deleted_only_for_self;
existing.state.can_be_deleted_for_all_users =
msg_info.state.can_be_deleted_for_all_users;
existing.state.is_read = msg_info.state.is_read;
}
}
None => {
// Нового сообщения нет - добавляем
client.push_message(msg_info.clone());
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
if is_incoming {
client.pending_view_messages_mut().push((chat_id, vec![msg_id]));
}
}
}
}
/// Обрабатывает Update::ChatAction - статус набора текста/отправки файлов
pub fn handle_chat_action_update(client: &mut TdClient, update: UpdateChatAction) {
// Обрабатываем только для текущего открытого чата
if Some(ChatId::new(update.chat_id)) != client.current_chat_id() {
return;
}
// Извлекаем user_id из sender_id
let MessageSender::User(user) = update.sender_id else {
return; // Игнорируем действия от имени чата
};
let user_id = UserId::new(user.user_id);
// Определяем текст действия
let action_text = match update.action {
ChatAction::Typing => Some("печатает...".to_string()),
ChatAction::RecordingVideo => Some("записывает видео...".to_string()),
ChatAction::UploadingVideo(_) => Some("отправляет видео...".to_string()),
ChatAction::RecordingVoiceNote => Some("записывает голосовое...".to_string()),
ChatAction::UploadingVoiceNote(_) => Some("отправляет голосовое...".to_string()),
ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()),
ChatAction::UploadingDocument(_) => Some("отправляет файл...".to_string()),
ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()),
ChatAction::RecordingVideoNote => Some("записывает видеосообщение...".to_string()),
ChatAction::UploadingVideoNote(_) => Some("отправляет видеосообщение...".to_string()),
ChatAction::Cancel | _ => None, // Отмена или неизвестное действие
};
match action_text {
Some(text) => client.set_typing_status(Some((user_id, text, Instant::now()))),
None => client.set_typing_status(None),
}
}
/// Обрабатывает Update::ChatPosition - изменение позиции чата в списке.
///
/// Обновляет order и is_pinned для чатов в Main списке,
/// управляет folder_ids для чатов в папках.
pub fn handle_chat_position_update(client: &mut TdClient, update: UpdateChatPosition) {
let chat_id = ChatId::new(update.chat_id);
match &update.position.list {
ChatList::Main => {
if update.position.order == 0 {
// Чат больше не в Main (перемещён в архив и т.д.)
client.chats_mut().retain(|c| c.id != chat_id);
} else {
// Обновляем позицию существующего чата
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
chat.order = update.position.order;
chat.is_pinned = update.position.is_pinned;
});
}
// Пересортируем по order
client.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
}
ChatList::Folder(folder) => {
// Обновляем folder_ids для чата
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
if update.position.order == 0 {
// Чат удалён из папки
chat.folder_ids.retain(|&id| id != folder.chat_folder_id);
} else {
// Чат добавлен в папку
if !chat.folder_ids.contains(&folder.chat_folder_id) {
chat.folder_ids.push(folder.chat_folder_id);
}
}
});
}
ChatList::Archive => {
// Архив пока не обрабатываем
}
}
}
/// Обрабатывает Update::User - обновление информации о пользователе.
///
/// Сохраняет display name и username в кэше,
/// обновляет username в связанных чатах,
/// удаляет "Deleted Account" из списка чатов.
pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) {
let user = update.user;
// Пропускаем удалённые аккаунты (пустое имя)
if user.first_name.is_empty() && user.last_name.is_empty() {
// Удаляем чаты с этим пользователем из списка
let user_id = user.id;
// Clone chat_user_ids to avoid borrow conflict
let chat_user_ids = client.user_cache.chat_user_ids.clone();
client
.chats_mut()
.retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(user_id)));
return;
}
// Сохраняем display name (first_name + last_name)
let display_name = if user.last_name.is_empty() {
user.first_name.clone()
} else {
format!("{} {}", user.first_name, user.last_name)
};
client.user_cache.user_names.insert(UserId::new(user.id), display_name);
// Сохраняем username если есть (с упрощённым извлечением через and_then)
if let Some(username) = user.usernames
.as_ref()
.and_then(|u| u.active_usernames.first())
{
client.user_cache.user_usernames.insert(UserId::new(user.id), username.to_string());
// Обновляем username в чатах, связанных с этим пользователем
for (&chat_id, &user_id) in &client.user_cache.chat_user_ids.clone() {
if user_id == UserId::new(user.id) {
crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| {
chat.username = Some(format!("@{}", username));
});
}
}
}
// LRU-кэш автоматически удаляет старые записи при вставке
}
/// Обрабатывает Update::MessageInteractionInfo - обновление реакций на сообщение.
///
/// Обновляет список реакций для сообщения в текущем открытом чате.
pub fn handle_message_interaction_info_update(
client: &mut TdClient,
update: UpdateMessageInteractionInfo,
) {
// Обновляем реакции в текущем открытом чате
if Some(ChatId::new(update.chat_id)) != client.current_chat_id() {
return;
}
let Some(msg) = client
.current_chat_messages_mut()
.iter_mut()
.find(|m| m.id() == MessageId::new(update.message_id))
else {
return;
};
// Извлекаем реакции из interaction_info
msg.interactions.reactions = update
.interaction_info
.as_ref()
.and_then(|info| info.reactions.as_ref())
.map(|reactions| {
reactions
.reactions
.iter()
.filter_map(|reaction| {
let emoji = match &reaction.r#type {
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None,
};
Some(ReactionInfo {
emoji,
count: reaction.total_count,
is_chosen: reaction.is_chosen,
})
})
.collect()
})
.unwrap_or_default();
}
/// Обрабатывает Update::MessageSendSucceeded - успешная отправка сообщения.
///
/// Заменяет временный ID сообщения на настоящий ID от сервера,
/// сохраняя reply_info из временного сообщения.
pub fn handle_message_send_succeeded_update(
client: &mut TdClient,
update: UpdateMessageSendSucceeded,
) {
let old_id = MessageId::new(update.old_message_id);
let chat_id = ChatId::new(update.message.chat_id);
// Обрабатываем только если это текущий открытый чат
if Some(chat_id) != client.current_chat_id() {
return;
}
// Находим сообщение с временным ID
let Some(idx) = client
.current_chat_messages()
.iter()
.position(|m| m.id() == old_id)
else {
return;
};
// Конвертируем новое сообщение
let mut new_msg = crate::tdlib::message_converter::convert_message(client, &update.message, chat_id);
// Сохраняем reply_info из старого сообщения (если было)
let old_reply = client.current_chat_messages()[idx]
.interactions
.reply_to
.clone();
if let Some(reply) = old_reply {
new_msg.interactions.reply_to = Some(reply);
}
// Заменяем старое сообщение на новое
client.current_chat_messages_mut()[idx] = new_msg;
}
/// Обрабатывает Update::ChatDraftMessage - обновление черновика сообщения в чате.
///
/// Извлекает текст черновика и сохраняет его в ChatInfo для отображения в списке чатов.
pub fn handle_chat_draft_message_update(client: &mut TdClient, update: UpdateChatDraftMessage) {
crate::tdlib::chat_helpers::update_chat(client, ChatId::new(update.chat_id), |chat| {
chat.draft_text = update.draft_message.as_ref().and_then(|draft| {
// Извлекаем текст из InputMessageText с помощью pattern matching
match &draft.input_message_text {
tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) => {
Some(text_msg.text.text.clone())
}
_ => None,
}
});
});
}
/// Обрабатывает изменение состояния авторизации
pub fn handle_auth_state(client: &mut TdClient, state: AuthorizationState) {
client.auth.state = match state {
AuthorizationState::WaitTdlibParameters => AuthState::WaitTdlibParameters,
AuthorizationState::WaitPhoneNumber => AuthState::WaitPhoneNumber,
AuthorizationState::WaitCode(_) => AuthState::WaitCode,
AuthorizationState::WaitPassword(_) => AuthState::WaitPassword,
AuthorizationState::Ready => AuthState::Ready,
AuthorizationState::Closed => AuthState::Closed,
_ => client.auth.state.clone(),
};
}