use std::env; use std::collections::HashMap; use tdlib_rs::enums::{AuthorizationState, ChatList, ChatType, MessageContent, Update, User, UserStatus}; 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, pub last_message: String, pub last_message_date: i32, pub unread_count: i32, pub is_pinned: bool, pub order: i64, /// ID последнего прочитанного исходящего сообщения (для галочек) pub last_read_outbox_message_id: i64, /// ID папок, в которых находится чат pub folder_ids: Vec, } #[derive(Debug, Clone)] pub struct MessageInfo { pub id: i64, pub sender_name: String, pub is_outgoing: bool, pub content: String, pub date: i32, pub is_read: bool, } #[derive(Debug, Clone)] pub struct FolderInfo { pub id: i32, pub name: String, } /// Онлайн-статус пользователя #[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, pub current_chat_messages: Vec, /// ID текущего открытого чата (для получения новых сообщений) pub current_chat_id: Option, /// Кэш usernames: user_id -> username user_usernames: HashMap, /// Кэш имён: user_id -> display_name (first_name + last_name) user_names: HashMap, /// Связь chat_id -> user_id для приватных чатов chat_user_ids: HashMap, /// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids) pub pending_view_messages: Vec<(i64, Vec)>, /// Очередь user_id для загрузки имён pub pending_user_ids: Vec, /// Папки чатов pub folders: Vec, /// Позиция основного списка среди папок pub main_chat_list_position: i32, /// Онлайн-статусы пользователей: user_id -> status user_statuses: HashMap, } #[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: HashMap::new(), user_names: HashMap::new(), 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: HashMap::new(), } } pub fn is_authenticated(&self) -> bool { matches!(self.auth_state, AuthState::Ready) } pub fn client_id(&self) -> i32 { self.client_id } /// Получение онлайн-статуса пользователя по chat_id (для приватных чатов) 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.get(user_id)) } /// Инициализация 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), 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::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) if !self.current_chat_messages.iter().any(|m| m.id == msg_info.id) { self.current_chat_messages.push(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)); } } } } } } 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); } _ => {} } } 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), m.date)) .unwrap_or_default(); // Извлекаем user_id для приватных чатов и сохраняем связь let username = match &td_chat.r#type { ChatType::Private(private) => { self.chat_user_ids.insert(td_chat.id, private.user_id); // Проверяем, есть ли уже username в кэше self.user_usernames.get(&private.user_id).map(|u| format!("@{}", u)) } _ => None, }; // Извлекаем ID папок из позиций let folder_ids: Vec = td_chat .positions .iter() .filter_map(|pos| { if let ChatList::Folder(folder) = &pos.list { Some(folder.chat_folder_id) } else { None } }) .collect(); 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, is_pinned, order, last_read_outbox_message_id: td_chat.last_read_outbox_message_id, folder_ids, }; 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.last_read_outbox_message_id = chat_info.last_read_outbox_message_id; existing.folder_ids = chat_info.folder_ids; // Обновляем 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); } // Сортируем чаты по 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) => { // Пробуем получить имя из кеша if let Some(name) = self.user_names.get(&user.user_id) { name.clone() } 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 // Входящие сообщения не показывают галочки }; MessageInfo { id: message.id, sender_name, is_outgoing: message.is_outgoing, content: extract_message_text_static(message), date: message.date, is_read, } } /// Отправка номера телефона 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, String> { // Устанавливаем текущий чат для получения новых сообщений self.current_chat_id = Some(chat_id); let _ = functions::open_chat(chat_id, self.client_id).await; // Пробуем загрузить несколько раз, так как сообщения могут подгружаться с сервера let mut all_messages: Vec = 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 = 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(); // Отмечаем сообщения как прочитанные if !all_messages.is_empty() { let message_ids: Vec = 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 load_older_messages( &mut self, chat_id: i64, from_message_id: i64, limit: i32, ) -> Result, 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 = 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 { match functions::get_me(self.client_id).await { Ok(user) => { match user { User::User(u) => Ok(u.id), } } Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)), } } /// Отправка текстового сообщения pub async fn send_message(&self, chat_id: i64, text: String) -> Result { use tdlib_rs::types::{FormattedText, InputMessageText}; use tdlib_rs::enums::InputMessageContent; let content = InputMessageContent::InputMessageText(InputMessageText { text: FormattedText { text: text.clone(), entities: vec![], }, link_preview_options: None, clear_draft: true, }); let result = functions::send_message( chat_id, 0, // message_thread_id None, // reply_to None, // options content, self.client_id, ) .await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => { // Конвертируем отправленное сообщение в MessageInfo Ok(MessageInfo { id: msg.id, sender_name: "You".to_string(), is_outgoing: true, content: text, date: msg.date, is_read: false, }) } 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 для загрузки имён pub async fn process_pending_user_ids(&mut self) { let pending = std::mem::take(&mut self.pending_user_ids); for user_id in pending { // Пропускаем если имя уже есть if self.user_names.contains_key(&user_id) { continue; } // Загружаем информацию о пользователе 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(); } } } } } } /// Статическая функция для извлечения текста сообщения (без &self) fn extract_message_text_static(message: &TdMessage) -> String { match &message.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(_) => "[Видео]".to_string(), 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(), } }