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,251 @@
//! Message conversion utilities for transforming TDLib messages.
//!
//! This module contains functions for converting TDLib message formats
//! to the application's internal MessageInfo format, including extraction
//! of replies, forwards, and reactions.
use crate::types::{ChatId, MessageId, UserId};
use tdlib_rs::types::Message as TdMessage;
use super::client::TdClient;
use super::types::{ForwardInfo, MessageInfo, ReactionInfo, ReplyInfo};
/// Конвертирует TDLib сообщение в MessageInfo
pub fn convert_message(
client: &mut TdClient,
message: &TdMessage,
chat_id: ChatId,
) -> MessageInfo {
let sender_name = match &message.sender_id {
tdlib_rs::enums::MessageSender::User(user) => {
// Пробуем получить имя из кеша (get обновляет LRU порядок)
let user_id = UserId::new(user.user_id);
client
.user_cache
.user_names
.get(&user_id)
.cloned()
.unwrap_or_else(|| {
// Добавляем в очередь для загрузки
if !client.pending_user_ids().contains(&user_id) {
client.pending_user_ids_mut().push(user_id);
}
format!("User_{}", user_id.as_i64())
})
}
tdlib_rs::enums::MessageSender::Chat(chat) => {
// Для чатов используем название чата
let sender_chat_id = ChatId::new(chat.chat_id);
client
.chats()
.iter()
.find(|c| c.id == sender_chat_id)
.map(|c| c.title.clone())
.unwrap_or_else(|| format!("Chat_{}", sender_chat_id.as_i64()))
}
};
// Определяем, прочитано ли исходящее сообщение
let message_id = MessageId::new(message.id);
let is_read = if message.is_outgoing {
// Сообщение прочитано, если его ID <= last_read_outbox_message_id чата
client
.chats()
.iter()
.find(|c| c.id == chat_id)
.map(|c| message_id <= c.last_read_outbox_message_id)
.unwrap_or(false)
} else {
true // Входящие сообщения не показывают галочки
};
let (content, entities) = TdClient::extract_message_text_static(message);
// Извлекаем информацию о reply
let reply_to = extract_reply_info(client, message);
// Извлекаем информацию о forward
let forward_from = extract_forward_info(client, message);
// Извлекаем реакции
let reactions = extract_reactions(client, message);
// Используем MessageBuilder для более читабельного создания
let mut builder = crate::tdlib::MessageBuilder::new(message_id)
.sender_name(sender_name)
.text(content)
.entities(entities)
.date(message.date)
.edit_date(message.edit_date);
// Применяем флаги
if message.is_outgoing {
builder = builder.outgoing();
}
if is_read {
builder = builder.read();
}
if message.can_be_edited {
builder = builder.editable();
}
if message.can_be_deleted_only_for_self {
builder = builder.deletable_for_self();
}
if message.can_be_deleted_for_all_users {
builder = builder.deletable_for_all();
}
// Добавляем опциональные данные
if let Some(reply) = reply_to {
builder = builder.reply_to(reply);
}
if let Some(forward) = forward_from {
builder = builder.forward_from(forward);
}
if !reactions.is_empty() {
builder = builder.reactions(reactions);
}
builder.build()
}
/// Извлекает информацию о reply из сообщения
pub fn extract_reply_info(client: &TdClient, message: &TdMessage) -> Option<ReplyInfo> {
use tdlib_rs::enums::MessageReplyTo;
match &message.reply_to {
Some(MessageReplyTo::Message(reply)) => {
// Получаем имя отправителя из origin или ищем сообщение в текущем списке
let sender_name = reply
.origin
.as_ref()
.map(|origin| get_origin_sender_name(origin))
.unwrap_or_else(|| {
// Пробуем найти оригинальное сообщение в текущем списке
let reply_msg_id = MessageId::new(reply.message_id);
client
.current_chat_messages()
.iter()
.find(|m| m.id() == reply_msg_id)
.map(|m| m.sender_name().to_string())
.unwrap_or_else(|| "...".to_string())
});
// Получаем текст из content или quote
let reply_msg_id = MessageId::new(reply.message_id);
let text = reply
.quote
.as_ref()
.map(|q| q.text.text.clone())
.or_else(|| {
reply
.content
.as_ref()
.map(TdClient::extract_content_text)
})
.unwrap_or_else(|| {
// Пробуем найти в текущих сообщениях
client
.current_chat_messages()
.iter()
.find(|m| m.id() == reply_msg_id)
.map(|m| m.text().to_string())
.unwrap_or_default()
});
Some(ReplyInfo {
message_id: reply_msg_id,
sender_name,
text,
})
}
_ => None,
}
}
/// Извлекает информацию о forward из сообщения
pub fn extract_forward_info(_client: &TdClient, message: &TdMessage) -> Option<ForwardInfo> {
message.forward_info.as_ref().map(|info| {
let sender_name = get_origin_sender_name(&info.origin);
ForwardInfo { sender_name }
})
}
/// Извлекает реакции из сообщения
pub fn extract_reactions(_client: &TdClient, message: &TdMessage) -> Vec<ReactionInfo> {
message
.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()
}
/// Получает имя отправителя из MessageOrigin
fn get_origin_sender_name(origin: &tdlib_rs::enums::MessageOrigin) -> String {
use tdlib_rs::enums::MessageOrigin;
match origin {
MessageOrigin::User(u) => format!("User_{}", u.sender_user_id),
MessageOrigin::Chat(c) => format!("Chat_{}", c.sender_chat_id),
MessageOrigin::Channel(c) => c.author_signature.clone(),
MessageOrigin::HiddenUser(h) => h.sender_name.clone(),
}
}
/// Обновляет reply info для сообщений, где данные не были загружены
/// Вызывается после загрузки истории, когда все сообщения уже в списке
#[allow(dead_code)]
pub fn update_reply_info_from_loaded_messages(client: &mut TdClient) {
// Собираем данные для обновления (id -> (sender_name, content))
let msg_data: std::collections::HashMap<i64, (String, String)> = client
.current_chat_messages()
.iter()
.map(|m| {
(
m.id().as_i64(),
(m.sender_name().to_string(), m.text().to_string()),
)
})
.collect();
// Обновляем reply_to для сообщений с неполными данными
for msg in client.current_chat_messages_mut().iter_mut() {
let Some(ref mut reply) = msg.interactions.reply_to else {
continue;
};
// Если sender_name = "..." или text пустой — пробуем заполнить
if reply.sender_name != "..." && !reply.text.is_empty() {
continue;
}
let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) else {
continue;
};
if reply.sender_name == "..." {
reply.sender_name = sender.clone();
}
if reply.text.is_empty() {
reply.text = content.clone();
}
}
}