use crate::constants::{MAX_MESSAGES_IN_CHAT, TDLIB_MESSAGE_LIMIT}; use crate::types::{ChatId, MessageId}; use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, MessageContent, MessageSender, SearchMessagesFilter, TextParseMode}; use tdlib_rs::functions; use tdlib_rs::types::{FormattedText, InputMessageReplyToMessage, InputMessageText, Message as TdMessage, TextParseModeMarkdown}; use super::types::{ForwardInfo, MessageBuilder, MessageInfo, ReactionInfo, ReplyInfo}; /// Менеджер сообщений TDLib. /// /// Управляет загрузкой, отправкой, редактированием и удалением сообщений. /// Кеширует сообщения текущего открытого чата и закрепленные сообщения. /// /// # Основные возможности /// /// - Загрузка истории сообщений чата /// - Отправка текстовых сообщений с поддержкой Markdown /// - Редактирование и удаление сообщений /// - Пересылка сообщений между чатами /// - Поиск сообщений по тексту /// - Управление закрепленными сообщениями /// - Управление черновиками /// - Автоматическая отметка сообщений как прочитанных /// /// # Examples /// /// ```ignore /// let mut msg_manager = MessageManager::new(client_id); /// /// // Загрузить историю чата /// let messages = msg_manager.get_chat_history(chat_id, 50).await?; /// /// // Отправить сообщение /// let msg = msg_manager.send_message( /// chat_id, /// "Hello, **world**!".to_string(), /// None, /// None /// ).await?; /// ``` pub struct MessageManager { /// Список сообщений текущего открытого чата (до MAX_MESSAGES_IN_CHAT). pub current_chat_messages: Vec, /// ID текущего открытого чата. pub current_chat_id: Option, /// Текущее закрепленное сообщение открытого чата. pub current_pinned_message: Option, /// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids). pub pending_view_messages: Vec<(ChatId, Vec)>, /// ID клиента TDLib для API вызовов. client_id: i32, } impl MessageManager { /// Создает новый менеджер сообщений. /// /// # Arguments /// /// * `client_id` - ID клиента TDLib для API вызовов /// /// # Returns /// /// Новый экземпляр `MessageManager` с пустым списком сообщений. pub fn new(client_id: i32) -> Self { Self { current_chat_messages: Vec::new(), current_chat_id: None, current_pinned_message: None, pending_view_messages: Vec::new(), client_id, } } /// Добавляет сообщение в список текущего чата. /// /// Автоматически ограничивает размер списка до [`MAX_MESSAGES_IN_CHAT`], /// удаляя старые сообщения при превышении лимита. /// /// # Arguments /// /// * `msg` - Сообщение для добавления /// /// # Note /// /// Сообщение добавляется в конец списка. При превышении лимита /// удаляются самые старые сообщения из начала списка. pub fn push_message(&mut self, msg: MessageInfo) { self.current_chat_messages.push(msg); // Добавляем в конец // Ограничиваем размер списка (удаляем старые с начала) if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT { self.current_chat_messages.drain(0..(self.current_chat_messages.len() - MAX_MESSAGES_IN_CHAT)); } } /// Загружает историю сообщений чата с динамической подгрузкой. /// /// Загружает сообщения чанками, ожидая пока TDLib синхронизирует их с сервера. /// Продолжает загрузку пока не будет достигнут `limit` или пока TDLib отдает сообщения. /// /// # Arguments /// /// * `chat_id` - ID чата /// * `limit` - Желаемое минимальное количество сообщений (для заполнения экрана) /// /// # Returns /// /// * `Ok(Vec)` - Список сообщений (от старых к новым) /// * `Err(String)` - Ошибка загрузки /// /// # Examples /// /// ```ignore /// // Загрузить достаточно сообщений для экрана высотой 30 строк /// let messages = msg_manager.get_chat_history(chat_id, 30).await?; /// ``` pub async fn get_chat_history( &mut self, chat_id: ChatId, limit: i32, ) -> Result, String> { use tokio::time::{sleep, Duration}; // ВАЖНО: Сначала открываем чат в TDLib // Это сообщает TDLib что пользователь открыл чат и нужно загрузить историю let _ = functions::open_chat(chat_id.as_i64(), self.client_id).await; // Открываем чат - TDLib начнет синхронизацию автоматически // НЕ устанавливаем current_chat_id здесь! // Он будет установлен снаружи ПОСЛЕ сохранения истории // Это предотвращает race condition с Update::NewMessage let mut all_messages = Vec::new(); let mut from_message_id = 0i64; // 0 = начинаем с последних сообщений let max_attempts_per_chunk = 20; // Максимум попыток на чанк let mut consecutive_empty_results = 0; // Счетчик пустых результатов подряд // Загружаем чанками по TDLIB_MESSAGE_LIMIT пока не достигнем limit while (all_messages.len() as i32) < limit { let remaining = limit - (all_messages.len() as i32); let chunk_size = std::cmp::min(TDLIB_MESSAGE_LIMIT, remaining); let mut chunk_loaded = false; // Пробуем загрузить чанк (TDLib подгружает с сервера по мере готовности) for attempt in 1..=max_attempts_per_chunk { let result = functions::get_chat_history( chat_id.as_i64(), from_message_id, 0, // offset chunk_size, false, // only_local - false means can fetch from server self.client_id, ) .await; let messages_obj = match result { Ok(tdlib_rs::enums::Messages::Messages(obj)) => obj, Err(e) => { // При первой загрузке (from_message_id == 0) возвращаем ошибку // При последующих чанках - прерываем цикл (возможно кончились сообщения) if all_messages.is_empty() { return Err(format!("Ошибка загрузки истории: {:?}", e)); } else { break; } } }; let received_count = messages_obj.messages.len(); // Если получили пустой результат if messages_obj.messages.is_empty() { consecutive_empty_results += 1; // Если несколько раз подряд пусто - прерываем if consecutive_empty_results >= 3 { break; } // Пробуем еще раз continue; } // Получили сообщения - сбрасываем счетчик consecutive_empty_results = 0; // Если это первая загрузка и получили мало сообщений - продолжаем попытки // TDLib может подгружать данные с сервера постепенно if all_messages.is_empty() && received_count < (chunk_size as usize) && attempt < max_attempts_per_chunk { continue; } // Конвертируем сообщения (от новых к старым, потом реверсим) let mut chunk_messages = Vec::new(); for msg in messages_obj.messages.iter().flatten() { if let Some(info) = self.convert_message(msg).await { chunk_messages.push(info); } } // Реверсим чтобы получить порядок от старых к новым chunk_messages.reverse(); // Добавляем загруженные сообщения if !chunk_messages.is_empty() { // Для следующей итерации: ID самого старого сообщения из текущего чанка from_message_id = chunk_messages[0].id().as_i64(); // ВАЖНО: Вставляем чанк В НАЧАЛО списка! // Первый чанк содержит НОВЫЕ сообщения (например 51-100) // Второй чанк содержит СТАРЫЕ сообщения (например 1-50) // Поэтому более старые чанки должны быть в начале списка if all_messages.is_empty() { // Первый чанк - просто добавляем all_messages = chunk_messages; } else { // Последующие чанки - вставляем в начало all_messages.splice(0..0, chunk_messages); } chunk_loaded = true; } // Если получили меньше чем chunk_size, значит это последний доступный чанк if (messages_obj.messages.len() as i32) < chunk_size { return Ok(all_messages); } break; // Чанк успешно загружен } // Если чанк не загрузился после всех попыток - прерываем if !chunk_loaded { break; } } Ok(all_messages) } /// Загружает более старые сообщения для пагинации. /// /// Используется для подгрузки предыдущих сообщений при прокрутке /// истории чата вверх. /// /// # Arguments /// /// * `chat_id` - ID чата /// * `from_message_id` - ID сообщения, от которого загружать историю /// /// # Returns /// /// * `Ok(Vec)` - Список старых сообщений (от старых к новым) /// * `Err(String)` - Ошибка загрузки /// /// # Examples /// /// ```ignore /// // Загрузить сообщения старше указанного /// let older = msg_manager.load_older_messages( /// chat_id, /// MessageId::new(12345) /// ).await?; /// ``` pub async fn load_older_messages( &mut self, chat_id: ChatId, from_message_id: MessageId, ) -> Result, String> { let result = functions::get_chat_history( chat_id.as_i64(), from_message_id.as_i64(), 0, // offset TDLIB_MESSAGE_LIMIT, false, self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Messages::Messages(messages_obj)) => { let mut messages = Vec::new(); for msg_opt in messages_obj.messages.iter().rev() { if let Some(msg) = msg_opt { if let Some(info) = self.convert_message(msg).await { messages.push(info); } } } Ok(messages) } Err(e) => Err(format!("Ошибка загрузки старых сообщений: {:?}", e)), } } /// Получает все закрепленные сообщения чата. /// /// Выполняет поиск всех сообщений с фильтром "pinned" и возвращает их список. /// /// # Arguments /// /// * `chat_id` - ID чата /// /// # Returns /// /// * `Ok(Vec)` - Список закрепленных сообщений (до 100) /// * `Err(String)` - Ошибка загрузки /// /// # Examples /// /// ```ignore /// let pinned = msg_manager.get_pinned_messages(chat_id).await?; /// println!("Found {} pinned messages", pinned.len()); /// ``` pub async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result, String> { let result = functions::search_chat_messages( chat_id.as_i64(), String::new(), None, 0, // from_message_id 0, // offset 100, // limit Some(SearchMessagesFilter::Pinned), 0, // message_thread_id 0, // saved_messages_topic_id self.client_id, ) .await; match result { Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(messages_obj)) => { let mut pinned_messages = Vec::new(); for msg in messages_obj.messages.iter().rev() { if let Some(info) = self.convert_message(msg).await { pinned_messages.push(info); } } Ok(pinned_messages) } Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)), } } /// Загружает текущее верхнее закрепленное сообщение. /// /// # Arguments /// /// * `chat_id` - ID чата /// /// # Note /// /// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`. /// Нужно использовать `getChatPinnedMessage` или альтернативный способ. /// Временно отключено, возвращает `None`. pub async fn load_current_pinned_message(&mut self, _chat_id: ChatId) { // TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat. // Нужно использовать getChatPinnedMessage или альтернативный способ. // Временно отключено. self.current_pinned_message = None; // match functions::get_chat(chat_id, self.client_id).await { // Ok(tdlib_rs::enums::Chat::Chat(chat)) => { // // chat.pinned_message_id больше не существует // } // _ => {} // } } /// Выполняет поиск сообщений по тексту в указанном чате. /// /// # Arguments /// /// * `chat_id` - ID чата для поиска /// * `query` - Текстовый запрос для поиска /// /// # Returns /// /// * `Ok(Vec)` - Найденные сообщения (до 100) /// * `Err(String)` - Ошибка поиска /// /// # Examples /// /// ```ignore /// let results = msg_manager.search_messages(chat_id, "hello").await?; /// ``` pub async fn search_messages( &self, chat_id: ChatId, query: &str, ) -> Result, String> { let result = functions::search_chat_messages( chat_id.as_i64(), query.to_string(), None, 0, // from_message_id 0, // offset 100, // limit None, 0, // message_thread_id 0, // saved_messages_topic_id self.client_id, ) .await; match result { Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(messages_obj)) => { let mut search_results = Vec::new(); for msg in messages_obj.messages.iter().rev() { if let Some(info) = self.convert_message(msg).await { search_results.push(info); } } Ok(search_results) } Err(e) => Err(format!("Ошибка поиска: {:?}", e)), } } /// Отправляет текстовое сообщение в чат с поддержкой Markdown. /// /// Автоматически парсит Markdown v2 форматирование (**bold**, *italic*, `code` и т.д.). /// /// # Arguments /// /// * `chat_id` - ID чата-получателя /// * `text` - Текст сообщения (поддерживает Markdown v2) /// * `reply_to_message_id` - Опциональный ID сообщения для ответа /// * `reply_info` - Опциональная информация об исходном сообщении /// /// # Returns /// /// * `Ok(MessageInfo)` - Отправленное сообщение /// * `Err(String)` - Ошибка отправки /// /// # Examples /// /// ```ignore /// // Простое сообщение /// let msg = msg_manager.send_message( /// chat_id, /// "Hello, **world**!".to_string(), /// None, /// None /// ).await?; /// /// // Ответ на сообщение /// let reply = msg_manager.send_message( /// chat_id, /// "Got it!".to_string(), /// Some(MessageId::new(123)), /// Some(reply_info) /// ).await?; /// ``` pub async fn send_message( &self, chat_id: ChatId, text: String, reply_to_message_id: Option, reply_info: Option, ) -> Result { // Парсим 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(_) => FormattedText { text: text.clone(), entities: vec![], }, }; let content = InputMessageContent::InputMessageText(InputMessageText { text: formatted_text, link_preview_options: None, clear_draft: true, }); let reply_to = reply_to_message_id.map(|msg_id| { InputMessageReplyTo::Message(InputMessageReplyToMessage { chat_id: 0, message_id: msg_id.as_i64(), quote: None, }) }); let result = functions::send_message( chat_id.as_i64(), 0, // message_thread_id reply_to, None, // options content, self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => { let mut msg_info = self .convert_message(&msg) .await .ok_or_else(|| "Не удалось конвертировать сообщение".to_string())?; // Добавляем reply_info если был передан if let Some(reply) = reply_info { msg_info.interactions.reply_to = Some(reply); } Ok(msg_info) } Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)), } } /// Редактирует существующее сообщение. /// /// # Arguments /// /// * `chat_id` - ID чата /// * `message_id` - ID сообщения для редактирования /// * `text` - Новый текст (поддерживает Markdown v2) /// /// # Returns /// /// * `Ok(MessageInfo)` - Отредактированное сообщение /// * `Err(String)` - Ошибка (нет прав, сообщение слишком старое и т.д.) pub async fn edit_message( &self, chat_id: ChatId, message_id: MessageId, text: String, ) -> Result { 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(_) => 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.as_i64(), message_id.as_i64(), content, self.client_id).await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => self .convert_message(&msg) .await .ok_or_else(|| "Не удалось конвертировать отредактированное сообщение".to_string()), Err(e) => Err(format!("Ошибка редактирования: {:?}", e)), } } /// Удаляет одно или несколько сообщений. /// /// # Arguments /// /// * `chat_id` - ID чата /// * `message_ids` - Список ID сообщений для удаления /// * `revoke` - `true` - удалить для всех, `false` - только для себя /// /// # Returns /// /// * `Ok(())` - Сообщения удалены /// * `Err(String)` - Ошибка удаления pub async fn delete_messages( &self, chat_id: ChatId, message_ids: Vec, revoke: bool, ) -> Result<(), String> { let message_ids_i64: Vec = message_ids.into_iter().map(|id| id.as_i64()).collect(); let result = functions::delete_messages(chat_id.as_i64(), message_ids_i64, revoke, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка удаления: {:?}", e)), } } /// Пересылает сообщения из одного чата в другой. /// /// # Arguments /// /// * `to_chat_id` - ID чата-получателя /// * `from_chat_id` - ID чата-источника /// * `message_ids` - Список ID сообщений для пересылки /// /// # Returns /// /// * `Ok(())` - Сообщения переслань /// * `Err(String)` - Ошибка пересылки pub async fn forward_messages( &self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec, ) -> Result<(), String> { let message_ids_i64: Vec = message_ids.into_iter().map(|id| id.as_i64()).collect(); let result = functions::forward_messages( to_chat_id.as_i64(), 0, // message_thread_id from_chat_id.as_i64(), message_ids_i64, None, // options false, // send_copy false, // remove_caption self.client_id, ) .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка пересылки: {:?}", e)), } } /// Сохраняет черновик сообщения для чата. /// /// Черновик отображается в списке чатов и восстанавливается /// при следующем открытии чата. /// /// # Arguments /// /// * `chat_id` - ID чата /// * `text` - Текст черновика (пустая строка удаляет черновик) /// /// # Returns /// /// * `Ok(())` - Черновик сохранен /// * `Err(String)` - Ошибка сохранения pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> { use tdlib_rs::types::DraftMessage; let draft = if text.is_empty() { None } else { Some(DraftMessage { reply_to: None, date: 0, input_message_text: InputMessageContent::InputMessageText(InputMessageText { text: FormattedText { text: text.clone(), entities: vec![], }, link_preview_options: None, clear_draft: false, }), }) }; let result = functions::set_chat_draft_message(chat_id.as_i64(), 0, draft, self.client_id).await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка сохранения черновика: {:?}", e)), } } /// Обрабатывает очередь сообщений для отметки как прочитанных. /// /// Автоматически отмечает просмотренные сообщения как прочитанные, /// что сбрасывает счетчик непрочитанных сообщений в чате. /// /// # Note /// /// Вызывайте периодически (например, в основном цикле) для обработки накопленной очереди. pub async fn process_pending_view_messages(&mut self) { if self.pending_view_messages.is_empty() { return; } let batch = std::mem::take(&mut self.pending_view_messages); for (chat_id, message_ids) in batch { let ids: Vec = message_ids.iter().map(|id| id.as_i64()).collect(); let _ = functions::view_messages(chat_id.as_i64(), ids, None, true, self.client_id).await; } } /// Конвертировать TdMessage в MessageInfo async fn convert_message(&self, msg: &TdMessage) -> Option { use crate::tdlib::message_conversion::{ extract_content_text, extract_entities, extract_forward_info, extract_reactions, extract_reply_info, extract_sender_name, }; // Извлекаем все части сообщения используя вспомогательные функции let content_text = extract_content_text(msg); let entities = extract_entities(msg); let sender_name = extract_sender_name(msg, self.client_id).await; let forward_from = extract_forward_info(msg); let reply_to = extract_reply_info(msg); let reactions = extract_reactions(msg); let mut builder = MessageBuilder::new(MessageId::new(msg.id)) .sender_name(sender_name) .text(content_text) .entities(entities) .date(msg.date) .edit_date(msg.edit_date); if msg.is_outgoing { builder = builder.outgoing(); } else { builder = builder.incoming(); } if !msg.contains_unread_mention { builder = builder.read(); } else { builder = builder.unread(); } if msg.can_be_edited { builder = builder.editable(); } if msg.can_be_deleted_only_for_self { builder = builder.deletable_for_self(); } if msg.can_be_deleted_for_all_users { builder = builder.deletable_for_all(); } if let Some(reply) = reply_to { builder = builder.reply_to(reply); } if let Some(forward) = forward_from { builder = builder.forward_from(forward); } builder = builder.reactions(reactions); Some(builder.build()) } /// Загружает недостающую информацию об исходных сообщениях для ответов. /// /// Ищет все reply-сообщения с `sender_name == "Unknown"` и загружает /// полную информацию (имя отправителя, текст) из TDLib. /// /// # Note /// /// Вызывайте после загрузки истории чата для заполнения информации о цитируемых сообщениях. pub async fn fetch_missing_reply_info(&mut self) { // Early return if no chat selected let Some(chat_id) = self.current_chat_id else { return; }; // Collect message IDs with missing reply info using filter_map let to_fetch: Vec = self .current_chat_messages .iter() .filter_map(|msg| { msg.interactions .reply_to .as_ref() .filter(|reply| reply.sender_name == "Unknown") .map(|reply| reply.message_id) }) .collect(); // Fetch and update each missing message for message_id in to_fetch { self.fetch_and_update_reply(chat_id, message_id).await; } } /// Загружает одно сообщение и обновляет reply информацию. async fn fetch_and_update_reply(&mut self, chat_id: ChatId, message_id: MessageId) { // Try to fetch the original message let Ok(original_msg_enum) = functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await else { return; }; let tdlib_rs::enums::Message::Message(original_msg) = original_msg_enum; let Some(orig_info) = self.convert_message(&original_msg).await else { return; }; // Extract text preview (first 50 chars) let text_preview: String = orig_info .content .text .chars() .take(50) .collect(); // Update reply info in all messages that reference this message self.current_chat_messages .iter_mut() .filter_map(|msg| msg.interactions.reply_to.as_mut()) .filter(|reply| reply.message_id == message_id) .for_each(|reply| { reply.sender_name = orig_info.metadata.sender_name.clone(); reply.text = text_preview.clone(); }); } }