1605 lines
68 KiB
Rust
1605 lines
68 KiB
Rust
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(),
|
||
}
|
||
}
|