This commit is contained in:
Mikhail Kilin
2026-01-24 18:53:35 +03:00
parent 22c4e17377
commit fa749d24c5
7 changed files with 576 additions and 29 deletions

View File

@@ -113,6 +113,26 @@ pub struct ChatInfo {
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,
/// Дата оригинального сообщения
pub date: i32,
}
#[derive(Debug, Clone)]
pub struct MessageInfo {
pub id: i64,
@@ -131,6 +151,10 @@ pub struct MessageInfo {
pub can_be_deleted_only_for_self: bool,
/// Можно ли удалить для всех
pub can_be_deleted_for_all_users: bool,
/// Информация о reply (если это ответ на сообщение)
pub reply_to: Option<ReplyInfo>,
/// Информация о forward (если сообщение переслано)
pub forward_from: Option<ForwardInfo>,
}
#[derive(Debug, Clone)]
@@ -240,7 +264,17 @@ impl TdClient {
}
/// Добавляет сообщение в текущий чат с соблюдением лимита
/// Если сообщение с таким 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 {
@@ -389,12 +423,25 @@ impl TdClient {
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)
if !self.current_chat_messages.iter().any(|m| m.id == msg_info.id) {
self.push_message(msg_info);
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
if is_incoming {
self.pending_view_messages.push((chat_id, vec![msg_id]));
// Проверяем, есть ли уже сообщение с таким id
let existing_idx = self.current_chat_messages.iter().position(|m| m.id == msg_info.id);
match existing_idx {
Some(idx) => {
// Сообщение уже есть - обновляем только если входящее
// (исходящие уже добавлены через send_message с правильным reply_to)
if is_incoming {
self.current_chat_messages[idx] = msg_info;
}
}
None => {
// Нового сообщения нет - добавляем
self.push_message(msg_info);
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
if is_incoming {
self.pending_view_messages.push((chat_id, vec![msg_id]));
}
}
}
}
@@ -630,6 +677,12 @@ impl TdClient {
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,
@@ -642,6 +695,187 @@ impl TdClient {
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<ReplyInfo> {
use tdlib_rs::enums::MessageReplyTo;
match &message.reply_to {
Some(MessageReplyTo::Message(reply)) => {
// Получаем имя отправителя из origin или ищем сообщение в текущем списке
let sender_name = if let Some(origin) = &reply.origin {
self.get_origin_sender_name(origin)
} else {
// Пробуем найти оригинальное сообщение в текущем списке
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<ForwardInfo> {
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<i64, (String, String)> = 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<i64> = 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<i64, (String, String)> =
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();
}
}
}
}
}
@@ -779,6 +1013,9 @@ impl TdClient {
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<i64> = all_messages.iter().map(|m| m.id).collect();
@@ -860,10 +1097,10 @@ impl TdClient {
}
}
/// Отправка текстового сообщения с поддержкой Markdown
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<MessageInfo, String> {
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown};
use tdlib_rs::enums::{InputMessageContent, TextParseMode};
/// Отправка текстового сообщения с поддержкой Markdown и reply
pub async fn send_message(&self, chat_id: i64, text: String, reply_to_message_id: Option<i64>, reply_info: Option<ReplyInfo>) -> Result<MessageInfo, String> {
use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage};
use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo};
// Парсим markdown в тексте
let formatted_text = match functions::parse_text_entities(
@@ -890,10 +1127,20 @@ impl TdClient {
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
None, // reply_to
reply_to,
None, // options
content,
self.client_id,
@@ -904,6 +1151,7 @@ impl TdClient {
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(),
@@ -916,6 +1164,8 @@ impl TdClient {
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)),
@@ -975,6 +1225,8 @@ impl TdClient {
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)),
@@ -998,6 +1250,26 @@ impl TdClient {
}
}
/// Пересылка сообщений
pub async fn forward_messages(&self, to_chat_id: i64, from_chat_id: i64, message_ids: Vec<i64>) -> 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);
@@ -1125,3 +1397,33 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec<TextEntity>)
_ => ("[Сообщение]".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(),
}
}