Проблема: - При открытии чата видно только последнее сообщение - TDLib возвращал 1 сообщение при первом запросе - Не было retry логики для ожидания синхронизации с сервером Решение: 1. Динамическая загрузка с retry (до 20 попыток на чанк) 2. Загрузка всей доступной истории (без лимита) 3. Retry при получении малого количества сообщений 4. Корректная чанковая загрузка по 50 сообщений Алгоритм: - При открытии чата: get_chat_history(i32::MAX) - загружает всё - Чанками по 50: TDLIB_MESSAGE_LIMIT - Retry если получено < 50 при первой загрузке - Остановка если 3 раза подряд пусто - Порядок: старые чанки вставляются в начало (splice) - При скролле: load_older_messages_if_needed() подгружает автоматически Изменения: src/tdlib/messages.rs: - Убрана фиксированная задержка 100ms после open_chat - Добавлен счетчик consecutive_empty_results - Retry логика без искусственных sleep() - Проверка: если получено мало - продолжить попытки src/input/main_input.rs: - limit: 100 → i32::MAX (без ограничений) - timeout: 10s → 30s tests/chat_list.rs: - test_chat_history_chunked_loading: проверка 100, 120, 200 сообщений - test_chat_history_loads_all_without_limit: загрузка 200 без лимита - test_load_older_messages_pagination: подгрузка при скролле Все тесты: 104/104 ✅ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
836 lines
33 KiB
Rust
836 lines
33 KiB
Rust
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<MessageInfo>,
|
||
|
||
/// ID текущего открытого чата.
|
||
pub current_chat_id: Option<ChatId>,
|
||
|
||
/// Текущее закрепленное сообщение открытого чата.
|
||
pub current_pinned_message: Option<MessageInfo>,
|
||
|
||
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids).
|
||
pub pending_view_messages: Vec<(ChatId, Vec<MessageId>)>,
|
||
|
||
/// 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<MessageInfo>)` - Список сообщений (от старых к новым)
|
||
/// * `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<Vec<MessageInfo>, 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<MessageInfo>)` - Список старых сообщений (от старых к новым)
|
||
/// * `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<Vec<MessageInfo>, 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<MessageInfo>)` - Список закрепленных сообщений (до 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<Vec<MessageInfo>, 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<MessageInfo>)` - Найденные сообщения (до 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<Vec<MessageInfo>, 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<MessageId>,
|
||
reply_info: Option<ReplyInfo>,
|
||
) -> Result<MessageInfo, String> {
|
||
// Парсим 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<MessageInfo, String> {
|
||
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<MessageId>,
|
||
revoke: bool,
|
||
) -> Result<(), String> {
|
||
let message_ids_i64: Vec<i64> = 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<MessageId>,
|
||
) -> Result<(), String> {
|
||
let message_ids_i64: Vec<i64> = 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<i64> = 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<MessageInfo> {
|
||
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<MessageId> = 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();
|
||
});
|
||
}
|
||
}
|