Files
telegram-tui/src/tdlib/client.rs
Mikhail Kilin 0acf864c28 refactor: extract NewMessage and ChatAction handlers (client.rs) - FIXED
Phase 6 начало - рефакторинг handle_update():
- handle_new_message_update() - обработка новых сообщений (~45 строк)
- handle_chat_action_update() - typing статусы (~50 строк)
- Добавлены импорты: UpdateNewMessage, UpdateChatAction

Результат:
- handle_update() сокращена с 351 до ~268 строк (24% ✂️)
- 2/17 веток извлечены в отдельные методы
- Файл: 1167 → 1178 строк (+11 чистых строк кода)

Phase 6: client.rs рефакторинг в процессе!

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 22:03:51 +03:00

1179 lines
49 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 std::time::Instant;
use tdlib_rs::enums::{
AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState,
MessageSender, Update, UserStatus,
Chat as TdChat
};
use tdlib_rs::types::{Message as TdMessage, UpdateNewMessage, UpdateChatAction};
use tdlib_rs::functions;
use crate::constants::{MAX_CHAT_USER_IDS, MAX_CHATS};
use super::auth::{AuthManager, AuthState};
use super::chats::ChatManager;
use super::messages::MessageManager;
use super::reactions::ReactionManager;
use super::types::{ChatInfo, FolderInfo, ForwardInfo, MessageInfo, NetworkState, ProfileInfo, ReactionInfo, ReplyInfo, 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
}
/// Обрабатываем одно обновление от TDLib
pub fn handle_update(&mut self, update: Update) {
match update {
Update::AuthorizationState(state) => {
self.handle_auth_state(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());
self.add_or_update_chat(&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();
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == chat_id) {
chat.last_message = last_message_text;
chat.last_message_date = last_message_date;
}
// Обновляем позиции если они пришли
for pos in &update.positions {
if matches!(pos.list, ChatList::Main) {
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == chat_id) {
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) => {
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
chat.unread_count = update.unread_count;
}
}
Update::ChatUnreadMentionCount(update) => {
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
chat.unread_mention_count = update.unread_mention_count;
}
}
Update::ChatNotificationSettings(update) => {
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
// 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);
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
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) => {
// Обновляем позицию чата или удаляем его из списка
let chat_id = ChatId::new(update.chat_id);
match &update.position.list {
ChatList::Main => {
if update.position.order == 0 {
// Чат больше не в Main (перемещён в архив и т.д.)
self.chats_mut().retain(|c| c.id != chat_id);
} else if let Some(chat) =
self.chats_mut().iter_mut().find(|c| c.id == chat_id)
{
// Обновляем позицию существующего чата
chat.order = update.position.order;
chat.is_pinned = update.position.is_pinned;
}
// Пересортируем по order
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
}
ChatList::Folder(folder) => {
// Обновляем folder_ids для чата
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == chat_id) {
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::NewMessage(new_msg) => {
self.handle_new_message_update(new_msg);
}
Update::User(update) => {
// Сохраняем имя и username пользователя
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 = self.user_cache.chat_user_ids.clone();
self.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)
};
self.user_cache.user_names.insert(UserId::new(user.id), display_name);
// Сохраняем username если есть
if let Some(usernames) = user.usernames {
if let Some(username) = usernames.active_usernames.first() {
self.user_cache.user_usernames.insert(UserId::new(user.id), username.clone());
// Обновляем username в чатах, связанных с этим пользователем
for (&chat_id, &user_id) in &self.user_cache.chat_user_ids.clone() {
if user_id == UserId::new(user.id) {
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == chat_id)
{
chat.username = Some(format!("@{}", username));
}
}
}
}
}
// LRU-кэш автоматически удаляет старые записи при вставке
}
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) => {
self.handle_chat_action_update(update);
}
Update::ChatDraftMessage(update) => {
// Обновляем черновик в списке чатов
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
chat.draft_text = update.draft_message.as_ref().and_then(|draft| {
// Извлекаем текст из InputMessageText
if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) =
&draft.input_message_text
{
Some(text_msg.text.text.clone())
} else {
None
}
});
}
}
Update::MessageInteractionInfo(update) => {
// Обновляем реакции в текущем открытом чате
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
if let Some(msg) = self
.current_chat_messages_mut()
.iter_mut()
.find(|m| m.id() == MessageId::new(update.message_id))
{
// Извлекаем реакции из 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(update) => {
// Сообщение успешно отправлено, заменяем временный ID на настоящий
let old_id = MessageId::new(update.old_message_id);
let chat_id = ChatId::new(update.message.chat_id);
// Обрабатываем только если это текущий открытый чат
if Some(chat_id) == self.current_chat_id() {
// Находим сообщение с временным ID
if let Some(idx) = self
.current_chat_messages()
.iter()
.position(|m| m.id() == old_id)
{
// Конвертируем новое сообщение
let mut new_msg = self.convert_message(&update.message, chat_id);
// Сохраняем reply_info из старого сообщения (если было)
let old_reply = self.current_chat_messages()[idx]
.interactions
.reply_to
.clone();
if let Some(reply) = old_reply {
new_msg.interactions.reply_to = Some(reply);
}
// Заменяем старое сообщение на новое
self.current_chat_messages_mut()[idx] = new_msg;
}
}
}
_ => {}
}
}
/// Обрабатывает Update::NewMessage - добавление нового сообщения
fn handle_new_message_update(&mut self, new_msg: UpdateNewMessage) {
// Добавляем новое сообщение если это текущий открытый чат
let chat_id = ChatId::new(new_msg.message.chat_id);
if Some(chat_id) == self.current_chat_id() {
let msg_info = self.convert_message(&new_msg.message, chat_id);
let msg_id = msg_info.id();
let is_incoming = !msg_info.is_outgoing();
// Проверяем, есть ли уже сообщение с таким id
let existing_idx = self
.current_chat_messages()
.iter()
.position(|m| m.id() == msg_info.id());
match existing_idx {
Some(idx) => {
// Сообщение уже есть - обновляем
if is_incoming {
self.current_chat_messages_mut()[idx] = msg_info;
} else {
// Для исходящих: обновляем can_be_edited и другие поля,
// но сохраняем reply_to (добавленный при отправке)
let existing = &mut self.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 => {
// Нового сообщения нет - добавляем
self.push_message(msg_info.clone());
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
if is_incoming {
self.pending_view_messages_mut().push((chat_id, vec![msg_id]));
}
}
}
}
}
/// Обрабатывает Update::ChatAction - статус набора текста/отправки файлов
fn handle_chat_action_update(&mut self, update: UpdateChatAction) {
// Обрабатываем только для текущего открытого чата
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
// Извлекаем user_id из sender_id
let user_id = match update.sender_id {
MessageSender::User(user) => Some(UserId::new(user.user_id)),
MessageSender::Chat(_) => None, // Игнорируем действия от имени чата
};
if let Some(user_id) = 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, // Отмена — сбрасываем статус
_ => None,
};
if let Some(text) = action_text {
self.set_typing_status(Some((user_id, text, Instant::now())));
} else {
// Cancel или неизвестное действие — сбрасываем
self.set_typing_status(None);
}
}
}
}
fn handle_auth_state(&mut self, state: AuthorizationState) {
self.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,
_ => self.auth.state.clone(),
};
}
fn add_or_update_chat(&mut self, td_chat_enum: &TdChat) {
// Pattern match to get inner Chat struct
let TdChat::Chat(td_chat) = td_chat_enum;
// Пропускаем удалённые аккаунты
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
// Удаляем из списка если уже был добавлен
self.chats_mut().retain(|c| c.id != ChatId::new(td_chat.id));
return;
}
// Ищем позицию в Main списке (если есть)
let main_position = td_chat
.positions
.iter()
.find(|pos| matches!(pos.list, ChatList::Main));
// Получаем order и is_pinned из позиции, или используем значения по умолчанию
let (order, is_pinned) = main_position
.map(|p| (p.order, p.is_pinned))
.unwrap_or((1, false)); // order=1 чтобы чат отображался
let (last_message, last_message_date) = td_chat
.last_message
.as_ref()
.map(|m| (Self::extract_message_text_static(m).0, m.date))
.unwrap_or_default();
// Извлекаем user_id для приватных чатов и сохраняем связь
let username = match &td_chat.r#type {
ChatType::Private(private) => {
// Ограничиваем размер chat_user_ids
let chat_id = ChatId::new(td_chat.id);
if self.user_cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS
&& !self.user_cache.chat_user_ids.contains_key(&chat_id)
{
// Удаляем случайную запись (первую найденную)
if let Some(&key) = self.user_cache.chat_user_ids.keys().next() {
self.user_cache.chat_user_ids.remove(&key);
}
}
let user_id = UserId::new(private.user_id);
self.user_cache.chat_user_ids.insert(chat_id, user_id);
// Проверяем, есть ли уже username в кэше (peek не обновляет LRU)
self.user_cache.user_usernames
.peek(&user_id)
.map(|u| format!("@{}", u))
}
_ => None,
};
// Извлекаем ID папок из позиций
let folder_ids: Vec<i32> = td_chat
.positions
.iter()
.filter_map(|pos| {
if let ChatList::Folder(folder) = &pos.list {
Some(folder.chat_folder_id)
} else {
None
}
})
.collect();
// Проверяем mute статус
let is_muted = td_chat.notification_settings.mute_for > 0;
let chat_info = ChatInfo {
id: ChatId::new(td_chat.id),
title: td_chat.title.clone(),
username,
last_message,
last_message_date,
unread_count: td_chat.unread_count,
unread_mention_count: td_chat.unread_mention_count,
is_pinned,
order,
last_read_outbox_message_id: MessageId::new(td_chat.last_read_outbox_message_id),
folder_ids,
is_muted,
draft_text: None,
};
if let Some(existing) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(td_chat.id)) {
existing.title = chat_info.title;
existing.last_message = chat_info.last_message;
existing.last_message_date = chat_info.last_message_date;
existing.unread_count = chat_info.unread_count;
existing.unread_mention_count = chat_info.unread_mention_count;
existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id;
existing.folder_ids = chat_info.folder_ids;
existing.is_muted = chat_info.is_muted;
// Обновляем username если он появился
if chat_info.username.is_some() {
existing.username = chat_info.username;
}
// Обновляем позицию только если она пришла
if main_position.is_some() {
existing.is_pinned = chat_info.is_pinned;
existing.order = chat_info.order;
}
} else {
self.chats_mut().push(chat_info);
// Ограничиваем количество чатов
if self.chats_mut().len() > MAX_CHATS {
// Удаляем чат с наименьшим order (наименее активный)
if let Some(min_idx) = self
.chats()
.iter()
.enumerate()
.min_by_key(|(_, c)| c.order)
.map(|(i, _)| i)
{
self.chats_mut().remove(min_idx);
}
}
}
// Сортируем чаты по order (TDLib order учитывает pinned и время)
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
}
fn convert_message(&mut self, 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);
if let Some(name) = self.user_cache.user_names.get(&user_id).cloned() {
name
} else {
// Добавляем в очередь для загрузки
if !self.pending_user_ids().contains(&user_id) {
self.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);
self.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 чата
self.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) = Self::extract_message_text_static(message);
// Извлекаем информацию о reply
let reply_to = self.extract_reply_info(message);
// Извлекаем информацию о forward
let forward_from = self.extract_forward_info(message);
// Извлекаем реакции
let reactions = self.extract_reactions(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 из сообщения
fn extract_reply_info(&self, message: &TdMessage) -> Option<ReplyInfo> {
use tdlib_rs::enums::MessageReplyTo;
match &message.reply_to {
Some(MessageReplyTo::Message(reply)) => {
// Получаем имя отправителя из origin или ищем сообщение в текущем списке
let sender_name = if let Some(origin) = &reply.origin {
self.get_origin_sender_name(origin)
} else {
// Пробуем найти оригинальное сообщение в текущем списке
let reply_msg_id = MessageId::new(reply.message_id);
self.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 = if let Some(quote) = &reply.quote {
quote.text.text.clone()
} else if let Some(content) = &reply.content {
Self::extract_content_text(content)
} else {
// Пробуем найти в текущих сообщениях
self.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 из сообщения
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
message.forward_info.as_ref().map(|info| {
let sender_name = self.get_origin_sender_name(&info.origin);
ForwardInfo { sender_name }
})
}
/// Извлекает информацию о реакциях из сообщения
fn extract_reactions(&self, message: &TdMessage) -> Vec<ReactionInfo> {
message
.interaction_info
.as_ref()
.and_then(|info| info.reactions.as_ref())
.map(|reactions| {
reactions
.reactions
.iter()
.filter_map(|reaction| {
// Извлекаем эмодзи из ReactionType
let emoji = match &reaction.r#type {
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji
};
Some(ReactionInfo {
emoji,
count: reaction.total_count,
is_chosen: reaction.is_chosen,
})
})
.collect()
})
.unwrap_or_default()
}
/// Получает имя отправителя из MessageOrigin
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
use tdlib_rs::enums::MessageOrigin;
match origin {
MessageOrigin::User(u) => self
.user_cache.user_names
.peek(&UserId::new(u.sender_user_id))
.cloned()
.unwrap_or_else(|| format!("User_{}", u.sender_user_id)),
MessageOrigin::Chat(c) => self
.chats()
.iter()
.find(|chat| chat.id == ChatId::new(c.sender_chat_id))
.map(|chat| chat.title.clone())
.unwrap_or_else(|| "Чат".to_string()),
MessageOrigin::HiddenUser(h) => h.sender_name.clone(),
MessageOrigin::Channel(c) => self
.chats()
.iter()
.find(|chat| chat.id == ChatId::new(c.chat_id))
.map(|chat| chat.title.clone())
.unwrap_or_else(|| "Канал".to_string()),
}
}
/// Обновляет reply info для сообщений, где данные не были загружены
/// Вызывается после загрузки истории, когда все сообщения уже в списке
fn update_reply_info_from_loaded_messages(&mut self) {
// Собираем данные для обновления (id -> (sender_name, content))
let msg_data: std::collections::HashMap<i64, (String, String)> = self
.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 self.current_chat_messages_mut().iter_mut() {
if let Some(ref mut reply) = msg.interactions.reply_to {
// Если sender_name = "..." или text пустой — пробуем заполнить
if reply.sender_name == "..." || reply.text.is_empty() {
if let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) {
if reply.sender_name == "..." {
reply.sender_name = sender.clone();
}
if reply.text.is_empty() {
reply.text = content.clone();
}
}
}
}
}
}
// 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(),
}
}
}