use std::env; use std::collections::HashMap; use std::time::Instant; use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, SearchMessagesFilter, Update, User, UserStatus}; use tdlib_rs::types::TextEntity; /// Максимальный размер кэшей пользователей const MAX_USER_CACHE_SIZE: usize = 500; /// Максимальное количество сообщений в текущем чате const MAX_MESSAGES_IN_CHAT: usize = 500; /// Максимальное количество чатов const MAX_CHATS: usize = 200; /// Максимальный размер кэша chat_user_ids const MAX_CHAT_USER_IDS: usize = 500; /// Простой 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, } /// Информация о сообщении, на которое отвечают #[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 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, } #[derive(Debug, Clone)] pub struct FolderInfo { pub id: i32, pub name: String, } /// Состояние сетевого соединения #[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 { let api_id: i32 = env::var("API_ID") .unwrap_or_else(|_| "0".to_string()) .parse() .unwrap_or(0); let api_hash = env::var("API_HASH").unwrap_or_default(); 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; } } } } _ => {} } } 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, }; 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); 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, } } /// Извлекает информацию о 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, } }) } /// Получает имя отправителя из 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 50, // 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 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::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage}; use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo}; // Парсим 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, }) } Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)), } } /// Редактирование текстового сообщения с поддержкой Markdown pub async fn edit_message(&self, chat_id: i64, message_id: i64, text: String) -> Result { use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; use tdlib_rs::enums::{InputMessageContent, TextParseMode}; // Парсим 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 сохраняется из оригинала }) } 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 BATCH_SIZE: usize = 5; // Убираем дубликаты и уже загруженные self.pending_user_ids.retain(|id| !self.user_names.contains_key(id)); self.pending_user_ids.dedup(); // Берём последние BATCH_SIZE элементов let start = self.pending_user_ids.len().saturating_sub(BATCH_SIZE); 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(), } }