Files
telegram-tui/src/tdlib/client.rs
2026-01-27 12:09:05 +03:00

1605 lines
68 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<V> {
map: HashMap<i64, V>,
/// Порядок доступа: последний элемент — самый недавно использованный
order: Vec<i64>,
capacity: usize,
}
impl<V: Clone> LruCache<V> {
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<String>,
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<i32>,
/// Чат замьючен (уведомления отключены)
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<TextEntity>,
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<ReplyInfo>,
/// Информация о forward (если сообщение переслано)
pub forward_from: Option<ForwardInfo>,
}
#[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<ChatInfo>,
pub current_chat_messages: Vec<MessageInfo>,
/// ID текущего открытого чата (для получения новых сообщений)
pub current_chat_id: Option<i64>,
/// LRU-кэш usernames: user_id -> username
user_usernames: LruCache<String>,
/// LRU-кэш имён: user_id -> display_name (first_name + last_name)
user_names: LruCache<String>,
/// Связь chat_id -> user_id для приватных чатов
chat_user_ids: HashMap<i64, i64>,
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids)
pub pending_view_messages: Vec<(i64, Vec<i64>)>,
/// Очередь user_id для загрузки имён
pub pending_user_ids: Vec<i64>,
/// Папки чатов
pub folders: Vec<FolderInfo>,
/// Позиция основного списка среди папок
pub main_chat_list_position: i32,
/// LRU-кэш онлайн-статусов пользователей: user_id -> status
user_statuses: LruCache<UserOnlineStatus>,
/// Состояние сетевого соединения
pub network_state: NetworkState,
/// Typing status для текущего чата: (user_id, action_text, timestamp)
pub typing_status: Option<(i64, String, Instant)>,
/// Последнее закреплённое сообщение текущего чата
pub current_pinned_message: Option<MessageInfo>,
}
#[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<String> {
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<i32> = 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<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();
}
}
}
}
}
/// Отправка номера телефона
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<Vec<MessageInfo>, String> {
// Устанавливаем текущий чат для получения новых сообщений
self.current_chat_id = Some(chat_id);
let _ = functions::open_chat(chat_id, self.client_id).await;
// Пробуем загрузить несколько раз, так как сообщения могут подгружаться с сервера
let mut all_messages: Vec<MessageInfo> = 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<MessageInfo> = 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<i64> = 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<Vec<MessageInfo>, 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<MessageInfo> = 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<Vec<MessageInfo>, 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<MessageInfo> = 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<Vec<MessageInfo>, 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<MessageInfo> = 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<i64, String> {
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<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(
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<MessageInfo, String> {
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<i64>, 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<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);
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<i64> = 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<TextEntity>) {
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<TextEntity> = 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<TextEntity> = 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<TextEntity> = 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(),
}
}