use crate::constants::{ LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_CHATS, MAX_MESSAGES_IN_CHAT, MAX_USER_CACHE_SIZE, TDLIB_CHAT_LIMIT, TDLIB_MESSAGE_LIMIT, }; use std::collections::HashMap; use std::env; use std::time::Instant; use tdlib_rs::enums::{ AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, SearchMessagesFilter, Update, User, UserStatus, }; use tdlib_rs::types::TextEntity; /// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка pub struct LruCache { map: HashMap, /// Порядок доступа: последний элемент — самый недавно использованный order: Vec, capacity: usize, } impl LruCache { pub fn new(capacity: usize) -> Self { Self { map: HashMap::with_capacity(capacity), order: Vec::with_capacity(capacity), capacity, } } /// Получить значение и обновить порядок доступа pub fn get(&mut self, key: &i64) -> Option<&V> { if self.map.contains_key(key) { // Перемещаем ключ в конец (самый недавно использованный) self.order.retain(|k| k != key); self.order.push(*key); self.map.get(key) } else { None } } /// Получить значение без обновления порядка (для read-only доступа) pub fn peek(&self, key: &i64) -> Option<&V> { self.map.get(key) } /// Вставить значение pub fn insert(&mut self, key: i64, value: V) { if self.map.contains_key(&key) { // Обновляем существующее значение self.map.insert(key, value); self.order.retain(|k| *k != key); self.order.push(key); } else { // Если кэш полон, удаляем самый старый элемент if self.map.len() >= self.capacity { if let Some(oldest) = self.order.first().copied() { self.order.remove(0); self.map.remove(&oldest); } } self.map.insert(key, value); self.order.push(key); } } /// Проверить наличие ключа pub fn contains_key(&self, key: &i64) -> bool { self.map.contains_key(key) } /// Количество элементов #[allow(dead_code)] pub fn len(&self) -> usize { self.map.len() } } use tdlib_rs::functions; use tdlib_rs::types::{Chat as TdChat, Message as TdMessage}; #[derive(Debug, Clone, PartialEq)] #[allow(dead_code)] pub enum AuthState { WaitTdlibParameters, WaitPhoneNumber, WaitCode, WaitPassword, Ready, Closed, Error(String), } #[derive(Debug, Clone)] #[allow(dead_code)] pub struct ChatInfo { pub id: i64, pub title: String, pub username: Option, pub last_message: String, pub last_message_date: i32, pub unread_count: i32, /// Количество непрочитанных упоминаний (@) pub unread_mention_count: i32, pub is_pinned: bool, pub order: i64, /// ID последнего прочитанного исходящего сообщения (для галочек) pub last_read_outbox_message_id: i64, /// ID папок, в которых находится чат pub folder_ids: Vec, /// Чат замьючен (уведомления отключены) pub is_muted: bool, /// Черновик сообщения pub draft_text: Option, } /// Информация о сообщении, на которое отвечают #[derive(Debug, Clone)] pub struct ReplyInfo { /// ID сообщения, на которое отвечают pub message_id: i64, /// Имя отправителя оригинального сообщения pub sender_name: String, /// Текст оригинального сообщения (превью) pub text: String, } /// Информация о пересланном сообщении #[derive(Debug, Clone)] pub struct ForwardInfo { /// Имя оригинального отправителя pub sender_name: String, /// Дата оригинального сообщения (для будущего использования) #[allow(dead_code)] pub date: i32, } /// Информация о реакции на сообщение #[derive(Debug, Clone)] pub struct ReactionInfo { /// Эмодзи реакции (например, "👍") pub emoji: String, /// Количество людей, поставивших эту реакцию pub count: i32, /// Поставил ли текущий пользователь эту реакцию pub is_chosen: bool, } #[derive(Debug, Clone)] pub struct MessageInfo { pub id: i64, pub sender_name: String, pub is_outgoing: bool, pub content: String, /// Сущности форматирования (bold, italic, code и т.д.) pub entities: Vec, pub date: i32, /// Дата редактирования (0 если не редактировалось) pub edit_date: i32, pub is_read: bool, /// Можно ли редактировать сообщение pub can_be_edited: bool, /// Можно ли удалить только для себя pub can_be_deleted_only_for_self: bool, /// Можно ли удалить для всех pub can_be_deleted_for_all_users: bool, /// Информация о reply (если это ответ на сообщение) pub reply_to: Option, /// Информация о forward (если сообщение переслано) pub forward_from: Option, /// Реакции на сообщение pub reactions: Vec, } #[derive(Debug, Clone)] pub struct FolderInfo { pub id: i32, pub name: String, } /// Информация о профиле чата/пользователя #[derive(Debug, Clone)] pub struct ProfileInfo { pub chat_id: i64, pub title: String, pub username: Option, pub bio: Option, pub phone_number: Option, pub chat_type: String, // "Личный чат", "Группа", "Канал" pub member_count: Option, pub description: Option, pub invite_link: Option, pub is_group: bool, pub online_status: Option, } /// Состояние сетевого соединения #[derive(Debug, Clone, PartialEq)] pub enum NetworkState { /// Ожидание подключения к сети WaitingForNetwork, /// Подключение к прокси ConnectingToProxy, /// Подключение к серверам Telegram Connecting, /// Обновление данных Updating, /// Подключено Ready, } /// Онлайн-статус пользователя #[derive(Debug, Clone, PartialEq)] pub enum UserOnlineStatus { /// Онлайн Online, /// Был недавно (менее часа назад) Recently, /// Был на этой неделе LastWeek, /// Был в этом месяце LastMonth, /// Давно не был LongTimeAgo, /// Оффлайн с указанием времени (unix timestamp) Offline(i32), } pub struct TdClient { pub auth_state: AuthState, pub api_id: i32, pub api_hash: String, client_id: i32, pub chats: Vec, pub current_chat_messages: Vec, /// ID текущего открытого чата (для получения новых сообщений) pub current_chat_id: Option, /// LRU-кэш usernames: user_id -> username user_usernames: LruCache, /// LRU-кэш имён: user_id -> display_name (first_name + last_name) user_names: LruCache, /// Связь chat_id -> user_id для приватных чатов chat_user_ids: HashMap, /// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids) pub pending_view_messages: Vec<(i64, Vec)>, /// Очередь user_id для загрузки имён pub pending_user_ids: Vec, /// Папки чатов pub folders: Vec, /// Позиция основного списка среди папок pub main_chat_list_position: i32, /// LRU-кэш онлайн-статусов пользователей: user_id -> status user_statuses: LruCache, /// Состояние сетевого соединения pub network_state: NetworkState, /// Typing status для текущего чата: (user_id, action_text, timestamp) pub typing_status: Option<(i64, String, Instant)>, /// Последнее закреплённое сообщение текущего чата pub current_pinned_message: Option, } #[allow(dead_code)] impl TdClient { pub fn new() -> Self { // Загружаем credentials из ~/.config/tele-tui/credentials или .env let (api_id, api_hash) = match crate::config::Config::load_credentials() { Ok(creds) => creds, Err(err_msg) => { eprintln!("\n{}\n", err_msg); // Используем дефолтные значения, чтобы приложение запустилось // Пользователь увидит сообщение об ошибке в UI (0, String::new()) } }; let client_id = tdlib_rs::create_client(); TdClient { auth_state: AuthState::WaitTdlibParameters, api_id, api_hash, client_id, chats: Vec::new(), current_chat_messages: Vec::new(), current_chat_id: None, user_usernames: LruCache::new(MAX_USER_CACHE_SIZE), user_names: LruCache::new(MAX_USER_CACHE_SIZE), chat_user_ids: HashMap::new(), pending_view_messages: Vec::new(), pending_user_ids: Vec::new(), folders: Vec::new(), main_chat_list_position: 0, user_statuses: LruCache::new(MAX_USER_CACHE_SIZE), network_state: NetworkState::Connecting, typing_status: None, current_pinned_message: None, } } pub fn is_authenticated(&self) -> bool { matches!(self.auth_state, AuthState::Ready) } pub fn client_id(&self) -> i32 { self.client_id } /// Добавляет сообщение в текущий чат с соблюдением лимита /// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to) pub fn push_message(&mut self, msg: MessageInfo) { // Проверяем, есть ли уже сообщение с таким id if let Some(idx) = self .current_chat_messages .iter() .position(|m| m.id == msg.id) { // Если новое сообщение имеет reply_to, или старое не имеет — заменяем if msg.reply_to.is_some() || self.current_chat_messages[idx].reply_to.is_none() { self.current_chat_messages[idx] = msg; } return; } self.current_chat_messages.push(msg); // Ограничиваем количество сообщений (удаляем старые) if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT { self.current_chat_messages.remove(0); } } /// Получение онлайн-статуса пользователя по chat_id (для приватных чатов) /// Использует peek для read-only доступа (не обновляет LRU порядок) pub fn get_user_status_by_chat_id(&self, chat_id: i64) -> Option<&UserOnlineStatus> { self.chat_user_ids .get(&chat_id) .and_then(|user_id| self.user_statuses.peek(user_id)) } /// Очищает typing status если прошло более 6 секунд /// Возвращает true если статус был очищен (нужна перерисовка) pub fn clear_stale_typing_status(&mut self) -> bool { if let Some((_, _, timestamp)) = &self.typing_status { if timestamp.elapsed().as_secs() > 6 { self.typing_status = None; return true; } } false } /// Возвращает текст typing status с именем пользователя /// Например: "Вася печатает..." pub fn get_typing_text(&self) -> Option { self.typing_status.as_ref().map(|(user_id, action, _)| { let name = self .user_names .peek(user_id) .cloned() .unwrap_or_else(|| "Кто-то".to_string()); format!("{} {}", name, action) }) } /// Инициализация TDLib с параметрами pub async fn init(&mut self) -> Result<(), String> { let result = functions::set_tdlib_parameters( false, // use_test_dc "tdlib_data".to_string(), // database_directory "".to_string(), // files_directory "".to_string(), // database_encryption_key true, // use_file_database true, // use_chat_info_database true, // use_message_database false, // use_secret_chats self.api_id, // api_id self.api_hash.clone(), // api_hash "en".to_string(), // system_language_code "Desktop".to_string(), // device_model "".to_string(), // system_version env!("CARGO_PKG_VERSION").to_string(), // application_version self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Failed to set TDLib parameters: {:?}", e)), } } /// Обрабатываем одно обновление от 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) => { self.add_or_update_chat(&new_chat.chat); } Update::ChatLastMessage(update) => { let chat_id = update.chat_id; let (last_message_text, last_message_date) = update .last_message .as_ref() .map(|msg| (extract_message_text_static(msg).0, msg.date)) .unwrap_or_default(); if let Some(chat) = self.chats.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.iter_mut().find(|c| c.id == chat_id) { chat.order = pos.order; chat.is_pinned = pos.is_pinned; } } } // Пересортируем по order self.chats.sort_by(|a, b| b.order.cmp(&a.order)); } Update::ChatReadInbox(update) => { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { chat.unread_count = update.unread_count; } } Update::ChatUnreadMentionCount(update) => { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { chat.unread_mention_count = update.unread_mention_count; } } Update::ChatNotificationSettings(update) => { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { // mute_for > 0 означает что чат замьючен chat.is_muted = update.notification_settings.mute_for > 0; } } Update::ChatReadOutbox(update) => { // Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { chat.last_read_outbox_message_id = update.last_read_outbox_message_id; } // Если это текущий открытый чат — обновляем is_read у сообщений if Some(update.chat_id) == self.current_chat_id { for msg in &mut self.current_chat_messages { if msg.is_outgoing && msg.id <= update.last_read_outbox_message_id { msg.is_read = true; } } } } Update::ChatPosition(update) => { // Обновляем позицию чата или удаляем его из списка match &update.position.list { ChatList::Main => { if update.position.order == 0 { // Чат больше не в Main (перемещён в архив и т.д.) self.chats.retain(|c| c.id != update.chat_id); } else if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { // Обновляем позицию существующего чата chat.order = update.position.order; chat.is_pinned = update.position.is_pinned; } // Пересортируем по order self.chats.sort_by(|a, b| b.order.cmp(&a.order)); } ChatList::Folder(folder) => { // Обновляем folder_ids для чата if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.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) => { // Добавляем новое сообщение если это текущий открытый чат let chat_id = 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[idx] = msg_info; } else { // Для исходящих: обновляем can_be_edited и другие поля, // но сохраняем reply_to (добавленный при отправке) let existing = &mut self.current_chat_messages[idx]; existing.can_be_edited = msg_info.can_be_edited; existing.can_be_deleted_only_for_self = msg_info.can_be_deleted_only_for_self; existing.can_be_deleted_for_all_users = msg_info.can_be_deleted_for_all_users; existing.is_read = msg_info.is_read; } } None => { // Нового сообщения нет - добавляем self.push_message(msg_info); // Если это входящее сообщение — добавляем в очередь для отметки как прочитанное if is_incoming { self.pending_view_messages.push((chat_id, vec![msg_id])); } } } } } Update::User(update) => { // Сохраняем имя и username пользователя let user = update.user; // Пропускаем удалённые аккаунты (пустое имя) if user.first_name.is_empty() && user.last_name.is_empty() { // Удаляем чаты с этим пользователем из списка let user_id = user.id; self.chats .retain(|c| self.chat_user_ids.get(&c.id) != Some(&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_names.insert(user.id, display_name); // Сохраняем username если есть if let Some(usernames) = user.usernames { if let Some(username) = usernames.active_usernames.first() { self.user_usernames.insert(user.id, username.clone()); // Обновляем username в чатах, связанных с этим пользователем for (&chat_id, &user_id) in &self.chat_user_ids.clone() { if user_id == user.id { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) { chat.username = Some(format!("@{}", username)); } } } } } // LRU-кэш автоматически удаляет старые записи при вставке } Update::ChatFolders(update) => { // Обновляем список папок self.folders = update .chat_folders .into_iter() .map(|f| FolderInfo { id: f.id, name: f.title }) .collect(); self.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_statuses.insert(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) => { // Обрабатываем только для текущего открытого чата if Some(update.chat_id) == self.current_chat_id { // Извлекаем user_id из sender_id let user_id = match update.sender_id { MessageSender::User(user) => Some(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.typing_status = Some((user_id, text, Instant::now())); } else { // Cancel или неизвестное действие — сбрасываем self.typing_status = None; } } } } Update::ChatDraftMessage(update) => { // Обновляем черновик в списке чатов if let Some(chat) = self.chats.iter_mut().find(|c| c.id == 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(update.chat_id) == self.current_chat_id { if let Some(msg) = self .current_chat_messages .iter_mut() .find(|m| m.id == update.message_id) { // Извлекаем реакции из interaction_info msg.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(); } } } _ => {} } } 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: &TdChat) { // Пропускаем удалённые аккаунты if td_chat.title == "Deleted Account" || td_chat.title.is_empty() { // Удаляем из списка если уже был добавлен self.chats.retain(|c| c.id != 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| (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 if self.chat_user_ids.len() >= MAX_CHAT_USER_IDS && !self.chat_user_ids.contains_key(&td_chat.id) { // Удаляем случайную запись (первую найденную) if let Some(&key) = self.chat_user_ids.keys().next() { self.chat_user_ids.remove(&key); } } self.chat_user_ids.insert(td_chat.id, private.user_id); // Проверяем, есть ли уже username в кэше (peek не обновляет LRU) self.user_usernames .peek(&private.user_id) .map(|u| format!("@{}", u)) } _ => None, }; // Извлекаем ID папок из позиций let folder_ids: Vec = 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: 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: td_chat.last_read_outbox_message_id, folder_ids, is_muted, draft_text: None, }; if let Some(existing) = self.chats.iter_mut().find(|c| c.id == 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.push(chat_info); // Ограничиваем количество чатов if self.chats.len() > MAX_CHATS { // Удаляем чат с наименьшим order (наименее активный) if let Some(min_idx) = self .chats .iter() .enumerate() .min_by_key(|(_, c)| c.order) .map(|(i, _)| i) { self.chats.remove(min_idx); } } } // Сортируем чаты по order (TDLib order учитывает pinned и время) self.chats.sort_by(|a, b| b.order.cmp(&a.order)); } fn convert_message(&mut self, message: &TdMessage, chat_id: i64) -> MessageInfo { let sender_name = match &message.sender_id { tdlib_rs::enums::MessageSender::User(user) => { // Пробуем получить имя из кеша (get обновляет LRU порядок) if let Some(name) = self.user_names.get(&user.user_id).cloned() { name } else { // Добавляем в очередь для загрузки if !self.pending_user_ids.contains(&user.user_id) { self.pending_user_ids.push(user.user_id); } format!("User_{}", user.user_id) } } tdlib_rs::enums::MessageSender::Chat(chat) => { // Для чатов используем название чата self.chats .iter() .find(|c| c.id == chat.chat_id) .map(|c| c.title.clone()) .unwrap_or_else(|| format!("Chat_{}", chat.chat_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) = 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); MessageInfo { id: message.id, sender_name, is_outgoing: message.is_outgoing, content, entities, date: message.date, edit_date: message.edit_date, is_read, can_be_edited: message.can_be_edited, can_be_deleted_only_for_self: message.can_be_deleted_only_for_self, can_be_deleted_for_all_users: message.can_be_deleted_for_all_users, reply_to, forward_from, reactions, } } /// Извлекает информацию о reply из сообщения fn extract_reply_info(&self, message: &TdMessage) -> Option { 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 { // Пробуем найти оригинальное сообщение в текущем списке self.current_chat_messages .iter() .find(|m| m.id == reply.message_id) .map(|m| m.sender_name.clone()) .unwrap_or_else(|| "...".to_string()) }; // Получаем текст из content или quote let text = if let Some(quote) = &reply.quote { quote.text.text.clone() } else if let Some(content) = &reply.content { extract_content_text(content) } else { // Пробуем найти в текущих сообщениях self.current_chat_messages .iter() .find(|m| m.id == reply.message_id) .map(|m| m.content.clone()) .unwrap_or_default() }; Some(ReplyInfo { message_id: reply.message_id, sender_name, text }) } _ => None, } } /// Извлекает информацию о forward из сообщения fn extract_forward_info(&self, message: &TdMessage) -> Option { message.forward_info.as_ref().map(|info| { let sender_name = self.get_origin_sender_name(&info.origin); ForwardInfo { sender_name, date: info.date } }) } /// Извлекает информацию о реакциях из сообщения fn extract_reactions(&self, message: &TdMessage) -> Vec { 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_names .peek(&u.sender_user_id) .cloned() .unwrap_or_else(|| format!("User_{}", u.sender_user_id)), MessageOrigin::Chat(c) => self .chats .iter() .find(|chat| chat.id == 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 == 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 = self .current_chat_messages .iter() .map(|m| (m.id, (m.sender_name.clone(), m.content.clone()))) .collect(); // Обновляем reply_to для сообщений с неполными данными for msg in &mut self.current_chat_messages { if let Some(ref mut reply) = msg.reply_to { // Если sender_name = "..." или text пустой — пробуем заполнить if reply.sender_name == "..." || reply.text.is_empty() { if let Some((sender, content)) = msg_data.get(&reply.message_id) { if reply.sender_name == "..." { reply.sender_name = sender.clone(); } if reply.text.is_empty() { reply.text = content.clone(); } } } } } } /// Асинхронно обновляет reply info, загружая недостающие сообщения pub async fn fetch_missing_reply_info(&mut self) { let chat_id = match self.current_chat_id { Some(id) => id, None => return, }; // Собираем message_id для которых нужно загрузить данные let missing_ids: Vec = self .current_chat_messages .iter() .filter_map(|msg| { msg.reply_to.as_ref().and_then(|reply| { if reply.sender_name == "..." || reply.text.is_empty() { Some(reply.message_id) } else { None } }) }) .collect(); if missing_ids.is_empty() { return; } // Загружаем каждое сообщение и кэшируем данные let mut reply_cache: std::collections::HashMap = std::collections::HashMap::new(); for msg_id in missing_ids { if reply_cache.contains_key(&msg_id) { continue; } if let Ok(tdlib_rs::enums::Message::Message(msg)) = functions::get_message(chat_id, msg_id, self.client_id).await { let sender_name = match &msg.sender_id { tdlib_rs::enums::MessageSender::User(user) => self .user_names .get(&user.user_id) .cloned() .unwrap_or_else(|| format!("User_{}", user.user_id)), tdlib_rs::enums::MessageSender::Chat(chat) => self .chats .iter() .find(|c| c.id == chat.chat_id) .map(|c| c.title.clone()) .unwrap_or_else(|| "Чат".to_string()), }; let (content, _) = extract_message_text_static(&msg); reply_cache.insert(msg_id, (sender_name, content)); } } // Применяем загруженные данные for msg in &mut self.current_chat_messages { if let Some(ref mut reply) = msg.reply_to { if let Some((sender, content)) = reply_cache.get(&reply.message_id) { if reply.sender_name == "..." { reply.sender_name = sender.clone(); } if reply.text.is_empty() { reply.text = content.clone(); } } } } } /// Отправка номера телефона pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> { let result = functions::set_authentication_phone_number(phone, None, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка отправки номера: {:?}", e)), } } /// Отправка кода подтверждения pub async fn send_code(&mut self, code: String) -> Result<(), String> { let result = functions::check_authentication_code(code, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Неверный код: {:?}", e)), } } /// Отправка пароля 2FA pub async fn send_password(&mut self, password: String) -> Result<(), String> { let result = functions::check_authentication_password(password, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Неверный пароль: {:?}", e)), } } /// Загрузка списка чатов pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { let result = functions::load_chats(Some(ChatList::Main), limit, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка загрузки чатов: {:?}", e)), } } /// Загрузка чатов для конкретной папки pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> { let chat_list = ChatList::Folder(tdlib_rs::types::ChatListFolder { chat_folder_id: folder_id }); let result = functions::load_chats(Some(chat_list), limit, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка загрузки чатов папки: {:?}", e)), } } /// Загрузка истории сообщений чата pub async fn get_chat_history( &mut self, chat_id: i64, limit: i32, ) -> Result, String> { // Устанавливаем текущий чат для получения новых сообщений self.current_chat_id = Some(chat_id); let _ = functions::open_chat(chat_id, self.client_id).await; // Пробуем загрузить несколько раз, так как сообщения могут подгружаться с сервера let mut all_messages: Vec = Vec::new(); let mut from_message_id: i64 = 0; let mut attempts = 0; const MAX_ATTEMPTS: i32 = 3; while attempts < MAX_ATTEMPTS { let result = functions::get_chat_history( chat_id, from_message_id, 0, // offset limit, false, // only_local - загружаем с сервера! self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Messages::Messages(messages)) => { let mut batch: Vec = Vec::new(); for m in messages.messages.into_iter().flatten() { batch.push(self.convert_message(&m, chat_id)); } if batch.is_empty() { break; } // Запоминаем ID самого старого сообщения для следующей загрузки if let Some(oldest) = batch.last() { from_message_id = oldest.id; } // Добавляем сообщения (они приходят от новых к старым) all_messages.extend(batch); attempts += 1; // Если получили достаточно сообщений, выходим if all_messages.len() >= limit as usize { break; } } Err(e) => { if all_messages.is_empty() { return Err(format!("Ошибка загрузки сообщений: {:?}", e)); } break; } } } // Сообщения приходят от новых к старым, переворачиваем all_messages.reverse(); self.current_chat_messages = all_messages.clone(); // Обновляем reply info для сообщений где данные не были загружены self.update_reply_info_from_loaded_messages(); // Отмечаем сообщения как прочитанные if !all_messages.is_empty() { let message_ids: Vec = all_messages.iter().map(|m| m.id).collect(); let _ = functions::view_messages( chat_id, message_ids, None, // source true, // force_read self.client_id, ) .await; } Ok(all_messages) } /// Загрузка закреплённых сообщений чата pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result, String> { let result = functions::search_chat_messages( chat_id, "".to_string(), // query None, // sender_id 0, // from_message_id 0, // offset 100, // limit Some(SearchMessagesFilter::Pinned), // filter 0, // message_thread_id 0, // saved_messages_topic_id self.client_id, ) .await; match result { Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => { let mut messages: Vec = Vec::new(); for m in found.messages { messages.push(self.convert_message(&m, chat_id)); } // Сообщения приходят от новых к старым, оставляем как есть Ok(messages) } Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)), } } /// Загружает последнее закреплённое сообщение для текущего чата pub async fn load_current_pinned_message(&mut self, chat_id: i64) { let result = functions::search_chat_messages( chat_id, "".to_string(), None, 0, 0, 1, // Только одно сообщение Some(SearchMessagesFilter::Pinned), 0, 0, self.client_id, ) .await; match result { Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => { if let Some(m) = found.messages.first() { self.current_pinned_message = Some(self.convert_message(m, chat_id)); } else { self.current_pinned_message = None; } } Err(_) => { self.current_pinned_message = None; } } } /// Поиск сообщений в чате по тексту pub async fn search_messages( &mut self, chat_id: i64, query: &str, ) -> Result, String> { if query.trim().is_empty() { return Ok(Vec::new()); } let result = functions::search_chat_messages( chat_id, query.to_string(), None, // sender_id 0, // from_message_id 0, // offset TDLIB_MESSAGE_LIMIT, // limit None, // filter (no filter = search by text) 0, // message_thread_id 0, // saved_messages_topic_id self.client_id, ) .await; match result { Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => { let mut messages: Vec = Vec::new(); for m in found.messages { messages.push(self.convert_message(&m, chat_id)); } Ok(messages) } Err(e) => Err(format!("Ошибка поиска: {:?}", e)), } } /// Получение полной информации о чате для профиля pub async fn get_profile_info(&self, chat_id: i64) -> Result { use tdlib_rs::enums::ChatType; // Получаем основную информацию о чате let chat_result = functions::get_chat(chat_id, self.client_id).await; let chat = match chat_result { Ok(tdlib_rs::enums::Chat::Chat(c)) => c, Err(e) => return Err(format!("Ошибка загрузки чата: {:?}", e)), }; let mut profile = ProfileInfo { chat_id, title: chat.title.clone(), username: None, bio: None, phone_number: None, chat_type: String::new(), member_count: None, description: None, invite_link: None, is_group: false, online_status: None, }; match &chat.r#type { ChatType::Private(private_chat) => { profile.chat_type = "Личный чат".to_string(); profile.is_group = false; // Получаем полную информацию о пользователе let user_result = functions::get_user(private_chat.user_id, self.client_id).await; if let Ok(tdlib_rs::enums::User::User(user)) = user_result { // Username if let Some(usernames) = user.usernames { if let Some(username) = usernames.active_usernames.first() { profile.username = Some(format!("@{}", username)); } } // Phone number if !user.phone_number.is_empty() { profile.phone_number = Some(format!("+{}", user.phone_number)); } // Online status profile.online_status = Some(match user.status { tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(), tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(), tdlib_rs::enums::UserStatus::LastWeek(_) => { "Был(а) на этой неделе".to_string() } tdlib_rs::enums::UserStatus::LastMonth(_) => { "Был(а) в этом месяце".to_string() } tdlib_rs::enums::UserStatus::Offline(offline) => { crate::utils::format_was_online(offline.was_online) } _ => "Давно не был(а)".to_string(), }); } // Bio (getUserFullInfo) let full_info_result = functions::get_user_full_info(private_chat.user_id, self.client_id).await; if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result { if let Some(bio_obj) = full_info.bio { profile.bio = Some(bio_obj.text); } } } ChatType::BasicGroup(basic_group) => { profile.chat_type = "Группа".to_string(); profile.is_group = true; // Получаем информацию о группе let group_result = functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result { profile.member_count = Some(group.member_count); } // Полная информация о группе let full_info_result = functions::get_basic_group_full_info( basic_group.basic_group_id, self.client_id, ) .await; if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = full_info_result { if !full_info.description.is_empty() { profile.description = Some(full_info.description); } if let Some(link) = full_info.invite_link { profile.invite_link = Some(link.invite_link); } } } ChatType::Supergroup(supergroup) => { // Получаем информацию о супергруппе let sg_result = functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result { profile.chat_type = if sg.is_channel { "Канал".to_string() } else { "Супергруппа".to_string() }; profile.is_group = !sg.is_channel; profile.member_count = Some(sg.member_count); // Username if let Some(usernames) = sg.usernames { if let Some(username) = usernames.active_usernames.first() { profile.username = Some(format!("@{}", username)); } } } // Полная информация о супергруппе let full_info_result = functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id) .await; if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = full_info_result { if !full_info.description.is_empty() { profile.description = Some(full_info.description); } if let Some(link) = full_info.invite_link { profile.invite_link = Some(link.invite_link); } } } ChatType::Secret(_) => { profile.chat_type = "Секретный чат".to_string(); } } Ok(profile) } /// Выйти из группы/канала pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> { let result = functions::leave_chat(chat_id, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка выхода из чата: {:?}", e)), } } /// Загрузка старых сообщений (для скролла вверх) pub async fn load_older_messages( &mut self, chat_id: i64, from_message_id: i64, limit: i32, ) -> Result, String> { let result = functions::get_chat_history( chat_id, from_message_id, 0, // offset limit, false, // only_local self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Messages::Messages(messages)) => { let mut result_messages: Vec = Vec::new(); for m in messages.messages.into_iter().flatten() { result_messages.push(self.convert_message(&m, chat_id)); } // Сообщения приходят от новых к старым, переворачиваем result_messages.reverse(); Ok(result_messages) } Err(e) => Err(format!("Ошибка загрузки сообщений: {:?}", e)), } } /// Получение информации о пользователе по ID pub async fn get_user_name(&self, user_id: i64) -> String { match functions::get_user(user_id, self.client_id).await { Ok(user) => { // User is an enum, need to match it match user { User::User(u) => { let first = u.first_name; let last = u.last_name; if last.is_empty() { first } else { format!("{} {}", first, last) } } } } Err(_) => format!("User_{}", user_id), } } /// Получение моего user_id pub async fn get_me(&self) -> Result { match functions::get_me(self.client_id).await { Ok(user) => match user { User::User(u) => Ok(u.id), }, Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)), } } /// Отправка статуса действия в чат (typing, cancel и т.д.) pub async fn send_chat_action(&self, chat_id: i64, action: ChatAction) { let _ = functions::send_chat_action( chat_id, 0, // message_thread_id Some(action), self.client_id, ) .await; } /// Отправка текстового сообщения с поддержкой Markdown и reply pub async fn send_message( &self, chat_id: i64, text: String, reply_to_message_id: Option, reply_info: Option, ) -> Result { use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, TextParseMode}; use tdlib_rs::types::{ FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown, }; // Парсим markdown в тексте let formatted_text = match functions::parse_text_entities( text.clone(), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), self.client_id, ) .await { Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { FormattedText { text: ft.text, entities: ft.entities } } Err(_) => { // Если парсинг не удался, отправляем как plain text FormattedText { text: text.clone(), entities: vec![] } } }; let content = InputMessageContent::InputMessageText(InputMessageText { text: formatted_text, link_preview_options: None, clear_draft: true, }); // Создаём reply_to если есть message_id для ответа // chat_id: 0 означает ответ в том же чате let reply_to = reply_to_message_id.map(|msg_id| { InputMessageReplyTo::Message(InputMessageReplyToMessage { chat_id: 0, message_id: msg_id, quote: None, }) }); let result = functions::send_message( chat_id, 0, // message_thread_id reply_to, None, // options content, self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => { // Извлекаем текст и entities из отправленного сообщения let (content, entities) = extract_message_text_static(&msg); Ok(MessageInfo { id: msg.id, sender_name: "Вы".to_string(), is_outgoing: true, content, entities, date: msg.date, edit_date: msg.edit_date, is_read: false, can_be_edited: msg.can_be_edited, can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self, can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users, reply_to: reply_info, forward_from: None, reactions: Vec::new(), }) } Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)), } } /// Получить доступные реакции для сообщения pub async fn get_message_available_reactions( &mut self, chat_id: i64, message_id: i64, ) -> Result, String> { use tdlib_rs::functions; let result = functions::get_message_available_reactions( chat_id, message_id, 8, // row_size - количество реакций в ряду self.client_id, ) .await; match result { Ok(tdlib_rs::enums::AvailableReactions::AvailableReactions(reactions)) => { // Извлекаем эмодзи из доступных реакций // Используем top_reactions (самые популярные реакции) let mut emojis: Vec = reactions .top_reactions .iter() .filter_map(|reaction| { if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { Some(e.emoji.clone()) } else { None } }) .collect(); // Если top_reactions пустой, используем popular_reactions if emojis.is_empty() { emojis = reactions .popular_reactions .iter() .filter_map(|reaction| { if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { Some(e.emoji.clone()) } else { None } }) .collect(); } Ok(emojis) } Err(e) => Err(format!("Ошибка получения реакций: {:?}", e)), } } /// Добавить реакцию на сообщение (или убрать, если уже поставлена) pub async fn toggle_reaction( &mut self, chat_id: i64, message_id: i64, emoji: String, ) -> Result<(), String> { use tdlib_rs::enums::ReactionType; use tdlib_rs::functions; use tdlib_rs::types::ReactionTypeEmoji; let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji }); let result = functions::add_message_reaction( chat_id, message_id, reaction_type, false, // is_big - обычная реакция (не "большая" анимация) true, // update_recent_reactions - обновить список недавних реакций self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка добавления реакции: {:?}", e)), } } /// Редактирование текстового сообщения с поддержкой Markdown /// Устанавливает черновик для чата через TDLib API pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { use tdlib_rs::enums::InputMessageContent; use tdlib_rs::types::{DraftMessage, FormattedText, InputMessageText}; if text.is_empty() { // Очищаем черновик let result = functions::set_chat_draft_message( chat_id, 0, // message_thread_id None, // draft_message (None = очистить) self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка очистки черновика: {:?}", e)), } } else { // Создаём черновик let formatted_text = FormattedText { text: text.clone(), entities: vec![] }; let input_message = InputMessageContent::InputMessageText(InputMessageText { text: formatted_text, link_preview_options: None, clear_draft: false, }); let draft = DraftMessage { reply_to: None, date: 0, // TDLib установит текущее время input_message_text: input_message, }; let result = functions::set_chat_draft_message( chat_id, 0, // message_thread_id Some(draft), self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка установки черновика: {:?}", e)), } } } pub async fn edit_message( &self, chat_id: i64, message_id: i64, text: String, ) -> Result { use tdlib_rs::enums::{InputMessageContent, TextParseMode}; use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; // Парсим markdown в тексте let formatted_text = match functions::parse_text_entities( text.clone(), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), self.client_id, ) .await { Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { FormattedText { text: ft.text, entities: ft.entities } } Err(_) => { // Если парсинг не удался, отправляем как plain text FormattedText { text: text.clone(), entities: vec![] } } }; let content = InputMessageContent::InputMessageText(InputMessageText { text: formatted_text, link_preview_options: None, clear_draft: true, }); let result = functions::edit_message_text(chat_id, message_id, content, self.client_id).await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => { let (content, entities) = extract_message_text_static(&msg); Ok(MessageInfo { id: msg.id, sender_name: "Вы".to_string(), is_outgoing: true, content, entities, date: msg.date, edit_date: msg.edit_date, is_read: true, can_be_edited: msg.can_be_edited, can_be_deleted_only_for_self: msg.can_be_deleted_only_for_self, can_be_deleted_for_all_users: msg.can_be_deleted_for_all_users, reply_to: None, // При редактировании reply сохраняется из оригинала forward_from: None, // При редактировании forward сохраняется из оригинала reactions: Vec::new(), // При редактировании реакции сохраняются из оригинала }) } Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)), } } /// Удаление сообщений /// revoke = true удаляет для всех, false только для себя pub async fn delete_messages( &self, chat_id: i64, message_ids: Vec, revoke: bool, ) -> Result<(), String> { let result = functions::delete_messages(chat_id, message_ids, revoke, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка удаления сообщения: {:?}", e)), } } /// Пересылка сообщений pub async fn forward_messages( &self, to_chat_id: i64, from_chat_id: i64, message_ids: Vec, ) -> Result<(), String> { let result = functions::forward_messages( to_chat_id, 0, // message_thread_id from_chat_id, message_ids, None, // options false, // send_copy false, // remove_caption self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка пересылки сообщения: {:?}", e)), } } /// Обработка очереди сообщений для отметки как прочитанных pub async fn process_pending_view_messages(&mut self) { let pending = std::mem::take(&mut self.pending_view_messages); for (chat_id, message_ids) in pending { let _ = functions::view_messages( chat_id, message_ids, None, // source true, // force_read self.client_id, ) .await; } } /// Обработка очереди user_id для загрузки имён (lazy loading) /// Загружает только последние 5 запросов за цикл для снижения нагрузки pub async fn process_pending_user_ids(&mut self) { // Берём только последние запросы (они актуальнее — от недавних сообщений) const LAZY_LOAD_USERS_PER_TICK: usize = 5; // Убираем дубликаты и уже загруженные self.pending_user_ids .retain(|id| !self.user_names.contains_key(id)); self.pending_user_ids.dedup(); // Берём последние LAZY_LOAD_USERS_PER_TICK элементов let start = self.pending_user_ids.len().saturating_sub(LAZY_LOAD_USERS_PER_TICK); let batch: Vec = self.pending_user_ids.drain(start..).collect(); for user_id in batch { // Загружаем информацию о пользователе if let Ok(User::User(user)) = functions::get_user(user_id, self.client_id).await { let display_name = if user.last_name.is_empty() { user.first_name.clone() } else { format!("{} {}", user.first_name, user.last_name) }; self.user_names.insert(user_id, display_name.clone()); // Обновляем имя в текущих сообщениях for msg in &mut self.current_chat_messages { if msg.sender_name == format!("User_{}", user_id) { msg.sender_name = display_name.clone(); } } } } // Ограничиваем размер очереди (старые запросы отбрасываем) const MAX_QUEUE_SIZE: usize = 50; if self.pending_user_ids.len() > MAX_QUEUE_SIZE { let excess = self.pending_user_ids.len() - MAX_QUEUE_SIZE; self.pending_user_ids.drain(0..excess); } } } /// Статическая функция для извлечения текста и entities сообщения (без &self) fn extract_message_text_static(message: &TdMessage) -> (String, Vec) { match &message.content { MessageContent::MessageText(text) => (text.text.text.clone(), text.text.entities.clone()), MessageContent::MessagePhoto(photo) => { if photo.caption.text.is_empty() { ("[Фото]".to_string(), vec![]) } else { // Добавляем смещение для "[Фото] " к entities let prefix_len = "[Фото] ".chars().count() as i32; let adjusted_entities: Vec = photo .caption .entities .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, r#type: e.r#type.clone(), }) .collect(); (format!("[Фото] {}", photo.caption.text), adjusted_entities) } } MessageContent::MessageVideo(video) => { if video.caption.text.is_empty() { ("[Видео]".to_string(), vec![]) } else { let prefix_len = "[Видео] ".chars().count() as i32; let adjusted_entities: Vec = video .caption .entities .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, r#type: e.r#type.clone(), }) .collect(); (format!("[Видео] {}", video.caption.text), adjusted_entities) } } MessageContent::MessageDocument(doc) => { (format!("[Файл: {}]", doc.document.file_name), vec![]) } MessageContent::MessageVoiceNote(_) => ("[Голосовое сообщение]".to_string(), vec![]), MessageContent::MessageVideoNote(_) => ("[Видеосообщение]".to_string(), vec![]), MessageContent::MessageSticker(sticker) => { (format!("[Стикер: {}]", sticker.sticker.emoji), vec![]) } MessageContent::MessageAnimation(anim) => { if anim.caption.text.is_empty() { ("[GIF]".to_string(), vec![]) } else { let prefix_len = "[GIF] ".chars().count() as i32; let adjusted_entities: Vec = anim .caption .entities .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, r#type: e.r#type.clone(), }) .collect(); (format!("[GIF] {}", anim.caption.text), adjusted_entities) } } MessageContent::MessageAudio(audio) => (format!("[Аудио: {}]", audio.audio.title), vec![]), MessageContent::MessageCall(_) => ("[Звонок]".to_string(), vec![]), MessageContent::MessagePoll(poll) => { (format!("[Опрос: {}]", poll.poll.question.text), vec![]) } _ => ("[Сообщение]".to_string(), vec![]), } } /// Извлекает текст из MessageContent (для reply preview) fn extract_content_text(content: &MessageContent) -> String { match content { MessageContent::MessageText(text) => text.text.text.clone(), MessageContent::MessagePhoto(photo) => { if photo.caption.text.is_empty() { "[Фото]".to_string() } else { format!("[Фото] {}", photo.caption.text) } } MessageContent::MessageVideo(video) => { if video.caption.text.is_empty() { "[Видео]".to_string() } else { format!("[Видео] {}", video.caption.text) } } MessageContent::MessageDocument(doc) => format!("[Файл: {}]", doc.document.file_name), MessageContent::MessageVoiceNote(_) => "[Голосовое]".to_string(), MessageContent::MessageVideoNote(_) => "[Видеосообщение]".to_string(), MessageContent::MessageSticker(sticker) => format!("[Стикер: {}]", sticker.sticker.emoji), MessageContent::MessageAnimation(_) => "[GIF]".to_string(), MessageContent::MessageAudio(audio) => format!("[Аудио: {}]", audio.audio.title), MessageContent::MessageCall(_) => "[Звонок]".to_string(), MessageContent::MessagePoll(poll) => format!("[Опрос: {}]", poll.poll.question.text), _ => "[Сообщение]".to_string(), } }