From c89a5e13f831f36c035db8a5366d48879460f972 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Tue, 3 Mar 2026 01:17:47 +0300 Subject: [PATCH] chore: remove leftover backup files from src/ Co-Authored-By: Claude Sonnet 4.6 --- src/input/main_input.rs.backup | 1139 ------------------ src/tdlib/client.rs.backup | 2036 -------------------------------- src/tdlib/client.rs.old | 2036 -------------------------------- 3 files changed, 5211 deletions(-) delete mode 100644 src/input/main_input.rs.backup delete mode 100644 src/tdlib/client.rs.backup delete mode 100644 src/tdlib/client.rs.old diff --git a/src/input/main_input.rs.backup b/src/input/main_input.rs.backup deleted file mode 100644 index bf4c4b7..0000000 --- a/src/input/main_input.rs.backup +++ /dev/null @@ -1,1139 +0,0 @@ -use crate::app::App; -use crate::tdlib::ChatAction; -use crate::types::{ChatId, MessageId}; -use crate::utils::{with_timeout, with_timeout_msg}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use std::time::{Duration, Instant}; - -pub async fn handle(app: &mut App, key: KeyEvent) { - let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); - - // Глобальные команды (работают всегда) - match key.code { - KeyCode::Char('r') if has_ctrl => { - app.status_message = Some("Обновление чатов...".to_string()); - let _ = with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; - app.status_message = None; - return; - } - KeyCode::Char('s') if has_ctrl => { - // Ctrl+S - начать поиск (только если чат не открыт) - if app.selected_chat_id.is_none() { - app.start_search(); - } - return; - } - KeyCode::Char('p') if has_ctrl => { - // Ctrl+P - режим просмотра закреплённых сообщений - if app.selected_chat_id.is_some() && !app.is_pinned_mode() { - if let Some(chat_id) = app.get_selected_chat_id() { - app.status_message = Some("Загрузка закреплённых...".to_string()); - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.get_pinned_messages(ChatId::new(chat_id)), - "Таймаут загрузки", - ) - .await - { - Ok(messages) => { - let messages: Vec = messages; - if messages.is_empty() { - app.status_message = Some("Нет закреплённых сообщений".to_string()); - } else { - app.enter_pinned_mode(messages); - app.status_message = None; - } - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - } - } - } - } - return; - } - KeyCode::Char('f') if has_ctrl => { - // Ctrl+F - поиск по сообщениям в открытом чате - if app.selected_chat_id.is_some() - && !app.is_pinned_mode() - && !app.is_message_search_mode() - { - app.enter_message_search_mode(); - } - return; - } - - _ => {} - } - - // Режим профиля - if app.is_profile_mode() { - // Обработка подтверждения выхода из группы - let confirmation_step = app.get_leave_group_confirmation_step(); - if confirmation_step > 0 { - match key.code { - KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => { - if confirmation_step == 1 { - // Первое подтверждение - показываем второе - app.show_leave_group_final_confirmation(); - } else if confirmation_step == 2 { - // Второе подтверждение - выходим из группы - if let Some(chat_id) = app.selected_chat_id { - let leave_result = app.td_client.leave_chat(chat_id).await; - match leave_result { - Ok(_) => { - app.status_message = Some("Вы вышли из группы".to_string()); - app.exit_profile_mode(); - app.close_chat(); - } - Err(e) => { - app.error_message = Some(e); - app.cancel_leave_group(); - } - } - } - } - } - KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => { - // Отмена - app.cancel_leave_group(); - } - _ => {} - } - return; - } - - // Обычная навигация по профилю - match key.code { - KeyCode::Esc => { - app.exit_profile_mode(); - } - KeyCode::Up => { - app.select_previous_profile_action(); - } - KeyCode::Down => { - if let Some(profile) = app.get_profile_info() { - let max_actions = get_available_actions_count(profile); - app.select_next_profile_action(max_actions); - } - } - KeyCode::Enter => { - // Выполнить выбранное действие - if let Some(profile) = app.get_profile_info() { - let actions = get_available_actions_count(profile); - let action_index = app.get_selected_profile_action().unwrap_or(0); - - if action_index < actions { - // Определяем какое действие выбрано - let mut current_idx = 0; - - // Действие: Открыть в браузере - if profile.username.is_some() { - if action_index == current_idx { - if let Some(username) = &profile.username { - let url = format!( - "https://t.me/{}", - username.trim_start_matches('@') - ); - #[cfg(feature = "url-open")] - { - match open::that(&url) { - Ok(_) => { - app.status_message = Some(format!("Открыто: {}", url)); - } - Err(e) => { - app.error_message = - Some(format!("Ошибка открытия браузера: {}", e)); - } - } - } - #[cfg(not(feature = "url-open"))] - { - app.error_message = Some( - "Открытие URL недоступно (требуется feature 'url-open')".to_string() - ); - } - } - return; - } - current_idx += 1; - } - - // Действие: Скопировать ID - if action_index == current_idx { - app.status_message = - Some(format!("ID скопирован: {}", profile.chat_id)); - return; - } - current_idx += 1; - - // Действие: Покинуть группу - if profile.is_group && action_index == current_idx { - app.show_leave_group_confirmation(); - } - } - } - } - _ => {} - } - return; - } - - // Режим поиска по сообщениям - if app.is_message_search_mode() { - match key.code { - KeyCode::Esc => { - app.exit_message_search_mode(); - } - KeyCode::Up | KeyCode::Char('N') => { - app.select_previous_search_result(); - } - KeyCode::Down | KeyCode::Char('n') => { - app.select_next_search_result(); - } - KeyCode::Enter => { - // Перейти к выбранному сообщению - if let Some(msg_id) = app.get_selected_search_result_id() { - let msg_id = MessageId::new(msg_id); - let msg_index = app - .td_client - .current_chat_messages() - .iter() - .position(|m| m.id() == msg_id); - - if let Some(idx) = msg_index { - let total = app.td_client.current_chat_messages().len(); - app.message_scroll_offset = total.saturating_sub(idx + 5); - } - app.exit_message_search_mode(); - } - } - KeyCode::Backspace => { - // Удаляем символ из запроса - if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { - query.pop(); - app.update_search_query(query.clone()); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if !query.is_empty() { - if let Ok(results) = with_timeout( - Duration::from_secs(3), - app.td_client.search_messages(ChatId::new(chat_id), &query), - ) - .await - { - app.set_search_results(results); - } - } else { - app.set_search_results(Vec::new()); - } - } - } - } - KeyCode::Char(c) => { - // Добавляем символ к запросу - if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { - query.push(c); - app.update_search_query(query.clone()); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if let Ok(results) = with_timeout( - Duration::from_secs(3), - app.td_client.search_messages(ChatId::new(chat_id), &query), - ) - .await - { - app.set_search_results(results); - } - } - } - } - _ => {} - } - return; - } - - // Режим просмотра закреплённых сообщений - if app.is_pinned_mode() { - match key.code { - KeyCode::Esc => { - app.exit_pinned_mode(); - } - KeyCode::Up => { - app.select_previous_pinned(); - } - KeyCode::Down => { - app.select_next_pinned(); - } - KeyCode::Enter => { - // Перейти к сообщению в истории - if let Some(msg_id) = app.get_selected_pinned_id() { - let msg_id = MessageId::new(msg_id); - // Ищем индекс сообщения в текущей истории - let msg_index = app - .td_client - .current_chat_messages() - .iter() - .position(|m| m.id() == msg_id); - - if let Some(idx) = msg_index { - // Вычисляем scroll offset чтобы показать сообщение - let total = app.td_client.current_chat_messages().len(); - app.message_scroll_offset = total.saturating_sub(idx + 5); - } - app.exit_pinned_mode(); - } - } - _ => {} - } - return; - } - - // Обработка ввода в режиме выбора реакции - if app.is_reaction_picker_mode() { - match key.code { - KeyCode::Left => { - app.select_previous_reaction(); - app.needs_redraw = true; - } - KeyCode::Right => { - app.select_next_reaction(); - app.needs_redraw = true; - } - KeyCode::Up => { - // Переход на ряд выше (8 эмодзи в ряду) - if let crate::app::ChatState::ReactionPicker { - selected_index, - .. - } = &mut app.chat_state - { - if *selected_index >= 8 { - *selected_index = selected_index.saturating_sub(8); - app.needs_redraw = true; - } - } - } - KeyCode::Down => { - // Переход на ряд ниже (8 эмодзи в ряду) - if let crate::app::ChatState::ReactionPicker { - selected_index, - available_reactions, - .. - } = &mut app.chat_state - { - let new_index = *selected_index + 8; - if new_index < available_reactions.len() { - *selected_index = new_index; - app.needs_redraw = true; - } - } - } - KeyCode::Enter => { - // Добавить/убрать реакцию - if let Some(emoji) = app.get_selected_reaction().cloned() { - if let Some(message_id) = app.get_selected_message_for_reaction() { - if let Some(chat_id) = app.selected_chat_id { - let message_id = MessageId::new(message_id); - app.status_message = Some("Отправка реакции...".to_string()); - app.needs_redraw = true; - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .toggle_reaction(chat_id, message_id, emoji.clone()), - "Таймаут отправки реакции", - ) - .await - { - Ok(_) => { - app.status_message = - Some(format!("Реакция {} добавлена", emoji)); - app.exit_reaction_picker_mode(); - app.needs_redraw = true; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - app.needs_redraw = true; - } - } - } - } - } - } - KeyCode::Esc => { - app.exit_reaction_picker_mode(); - app.needs_redraw = true; - } - _ => {} - } - return; - } - - // Модалка подтверждения удаления - if app.is_confirm_delete_shown() { - match key.code { - KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => { - // Подтверждение удаления - if let Some(msg_id) = app.chat_state.selected_message_id() { - if let Some(chat_id) = app.get_selected_chat_id() { - // Находим сообщение для проверки can_be_deleted_for_all_users - let can_delete_for_all = app - .td_client - .current_chat_messages() - .iter() - .find(|m| m.id() == msg_id) - .map(|m| m.can_be_deleted_for_all_users()) - .unwrap_or(false); - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.delete_messages( - ChatId::new(chat_id), - vec![msg_id], - can_delete_for_all, - ), - "Таймаут удаления", - ) - .await - { - Ok(_) => { - // Удаляем из локального списка - app.td_client - .current_chat_messages_mut() - .retain(|m| m.id() != msg_id); - // Сбрасываем состояние - app.chat_state = crate::app::ChatState::Normal; - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - // Закрываем модалку - app.chat_state = crate::app::ChatState::Normal; - } - KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => { - // Отмена удаления - app.chat_state = crate::app::ChatState::Normal; - } - _ => {} - } - return; - } - - // Режим выбора чата для пересылки - if app.is_forwarding() { - match key.code { - KeyCode::Esc => { - app.cancel_forward(); - } - KeyCode::Enter => { - // Выбираем чат и пересылаем сообщение - let filtered = app.get_filtered_chats(); - if let Some(i) = app.chat_list_state.selected() { - if let Some(chat) = filtered.get(i) { - let to_chat_id = chat.id; - if let Some(msg_id) = app.chat_state.selected_message_id() { - if let Some(from_chat_id) = app.get_selected_chat_id() { - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.forward_messages( - to_chat_id, - ChatId::new(from_chat_id), - vec![msg_id], - ), - "Таймаут пересылки", - ) - .await - { - Ok(_) => { - app.status_message = - Some("Сообщение переслано".to_string()); - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - } - } - app.cancel_forward(); - } - KeyCode::Down => { - app.next_chat(); - } - KeyCode::Up => { - app.previous_chat(); - } - _ => {} - } - return; - } - - // Режим поиска - if app.is_searching { - match key.code { - KeyCode::Esc => { - app.cancel_search(); - } - KeyCode::Enter => { - // Выбрать чат из отфильтрованного списка - app.select_filtered_chat(); - if let Some(chat_id) = app.get_selected_chat_id() { - app.status_message = Some("Загрузка сообщений...".to_string()); - app.message_scroll_offset = 0; - match with_timeout_msg( - Duration::from_secs(10), - app.td_client.get_chat_history(ChatId::new(chat_id), 100), - "Таймаут загрузки сообщений", - ) - .await - { - Ok(messages) => { - // Сохраняем загруженные сообщения - *app.td_client.current_chat_messages_mut() = messages; - // ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории - // Это предотвращает race condition с Update::NewMessage - app.td_client.set_current_chat_id(Some(ChatId::new(chat_id))); - // Загружаем недостающие reply info - let _ = tokio::time::timeout( - Duration::from_secs(5), - app.td_client.fetch_missing_reply_info(), - ) - .await; - // Загружаем последнее закреплённое сообщение - let _ = tokio::time::timeout( - Duration::from_secs(2), - app.td_client.load_current_pinned_message(ChatId::new(chat_id)), - ) - .await; - // Загружаем черновик - app.load_draft(); - app.status_message = None; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - } - } - } - } - KeyCode::Backspace => { - app.search_query.pop(); - // Сбрасываем выделение при изменении запроса - app.chat_list_state.select(Some(0)); - } - KeyCode::Down => { - app.next_filtered_chat(); - } - KeyCode::Up => { - app.previous_filtered_chat(); - } - KeyCode::Char(c) => { - app.search_query.push(c); - // Сбрасываем выделение при изменении запроса - app.chat_list_state.select(Some(0)); - } - _ => {} - } - return; - } - - // Enter - открыть чат, отправить сообщение или редактировать - if key.code == KeyCode::Enter { - if app.selected_chat_id.is_some() { - // Режим выбора сообщения - if app.is_selecting_message() { - // Начать редактирование выбранного сообщения - if app.start_editing_selected() { - // Редактирование начато - } else { - // Нельзя редактировать это сообщение - app.chat_state = crate::app::ChatState::Normal; - } - return; - } - - // Отправка или редактирование сообщения - if !app.message_input.is_empty() { - if let Some(chat_id) = app.get_selected_chat_id() { - let text = app.message_input.clone(); - - if app.is_editing() { - // Режим редактирования - if let Some(msg_id) = app.chat_state.selected_message_id() { - // Проверяем, что сообщение есть в локальном кэше - let msg_exists = app.td_client.current_chat_messages() - .iter() - .any(|m| m.id() == msg_id); - - if !msg_exists { - app.error_message = Some(format!( - "Сообщение {} не найдено в кэше чата {}", - msg_id.as_i64(), chat_id - )); - app.chat_state = crate::app::ChatState::Normal; - app.message_input.clear(); - app.cursor_position = 0; - return; - } - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.edit_message(ChatId::new(chat_id), msg_id, text), - "Таймаут редактирования", - ) - .await - { - Ok(mut edited_msg) => { - // Сохраняем reply_to из старого сообщения (если есть) - let messages = app.td_client.current_chat_messages_mut(); - if let Some(pos) = messages.iter().position(|m| m.id() == msg_id) { - let old_reply_to = messages[pos].interactions.reply_to.clone(); - // Если в старом сообщении был reply и в новом он "Unknown" - сохраняем старый - if let Some(old_reply) = old_reply_to { - if edited_msg.interactions.reply_to.as_ref() - .map_or(true, |r| r.sender_name == "Unknown") { - edited_msg.interactions.reply_to = Some(old_reply); - } - } - // Заменяем сообщение - messages[pos] = edited_msg; - } - // Очищаем инпут и сбрасываем состояние ПОСЛЕ успешного редактирования - app.message_input.clear(); - app.cursor_position = 0; - app.chat_state = crate::app::ChatState::Normal; - app.needs_redraw = true; // ВАЖНО: перерисовываем UI - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } else { - // Обычная отправка (или reply) - let reply_to_id = if app.is_replying() { - app.chat_state.selected_message_id() - } else { - None - }; - // Создаём ReplyInfo ДО отправки, пока сообщение точно доступно - let reply_info = app.get_replying_to_message().map(|m| { - crate::tdlib::ReplyInfo { - message_id: m.id(), - sender_name: m.sender_name().to_string(), - text: m.text().to_string(), - } - }); - app.message_input.clear(); - app.cursor_position = 0; - // Сбрасываем режим reply если он был активен - if app.is_replying() { - app.chat_state = crate::app::ChatState::Normal; - } - app.last_typing_sent = None; - - // Отменяем typing status - app.td_client - .send_chat_action(ChatId::new(chat_id), ChatAction::Cancel) - .await; - - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .send_message(ChatId::new(chat_id), text, reply_to_id, reply_info), - "Таймаут отправки", - ) - .await - { - Ok(sent_msg) => { - // Добавляем отправленное сообщение в список (с лимитом) - app.td_client.push_message(sent_msg); - // Сбрасываем скролл чтобы видеть новое сообщение - app.message_scroll_offset = 0; - } - Err(e) => { - app.error_message = Some(e); - } - } - } - } - } - } else { - // Открываем чат - let prev_selected = app.selected_chat_id; - app.select_current_chat(); - - if app.selected_chat_id != prev_selected { - if let Some(chat_id) = app.get_selected_chat_id() { - app.status_message = Some("Загрузка сообщений...".to_string()); - app.message_scroll_offset = 0; - match with_timeout_msg( - Duration::from_secs(10), - app.td_client.get_chat_history(ChatId::new(chat_id), 100), - "Таймаут загрузки сообщений", - ) - .await - { - Ok(messages) => { - // Сохраняем загруженные сообщения - *app.td_client.current_chat_messages_mut() = messages; - // ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории - // Это предотвращает race condition с Update::NewMessage - app.td_client.set_current_chat_id(Some(ChatId::new(chat_id))); - // Загружаем недостающие reply info - let _ = tokio::time::timeout( - Duration::from_secs(5), - app.td_client.fetch_missing_reply_info(), - ) - .await; - // Загружаем последнее закреплённое сообщение - let _ = tokio::time::timeout( - Duration::from_secs(2), - app.td_client.load_current_pinned_message(ChatId::new(chat_id)), - ) - .await; - // Загружаем черновик - app.load_draft(); - app.status_message = None; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - } - } - } - } - } - return; - } - - // Esc - отменить выбор/редактирование/reply или закрыть чат - if key.code == KeyCode::Esc { - if app.is_selecting_message() { - // Отменить выбор сообщения - app.chat_state = crate::app::ChatState::Normal; - } else if app.is_editing() { - // Отменить редактирование - app.cancel_editing(); - } else if app.is_replying() { - // Отменить режим ответа - app.cancel_reply(); - } else if app.selected_chat_id.is_some() { - // Сохраняем черновик если есть текст в инпуте - if let Some(chat_id) = app.selected_chat_id { - if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() { - let draft_text = app.message_input.clone(); - let _ = app.td_client.set_draft_message(chat_id, draft_text).await; - } else if app.message_input.is_empty() { - // Очищаем черновик если инпут пустой - let _ = app - .td_client - .set_draft_message(chat_id, String::new()) - .await; - } - } - app.close_chat(); - } - return; - } - - // Режим открытого чата - if app.selected_chat_id.is_some() { - // Режим выбора сообщения для редактирования/удаления - if app.is_selecting_message() { - match key.code { - KeyCode::Up => { - app.select_previous_message(); - } - KeyCode::Down => { - app.select_next_message(); - // Если вышли из режима выбора (индекс стал None), ничего не делаем - } - KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => { - // Показать модалку подтверждения удаления - if let Some(msg) = app.get_selected_message() { - let can_delete = - msg.can_be_deleted_only_for_self() || msg.can_be_deleted_for_all_users(); - if can_delete { - app.chat_state = crate::app::ChatState::DeleteConfirmation { - message_id: msg.id(), - }; - } - } - } - KeyCode::Char('r') | KeyCode::Char('к') => { - // Начать режим ответа на выбранное сообщение - app.start_reply_to_selected(); - } - KeyCode::Char('f') | KeyCode::Char('а') => { - // Начать режим пересылки - app.start_forward_selected(); - } - KeyCode::Char('y') | KeyCode::Char('н') => { - // Копировать сообщение - if let Some(msg) = app.get_selected_message() { - let text = format_message_for_clipboard(msg); - match copy_to_clipboard(&text) { - Ok(_) => { - app.status_message = Some("Сообщение скопировано".to_string()); - } - Err(e) => { - app.error_message = Some(format!("Ошибка копирования: {}", e)); - } - } - } - } - KeyCode::Char('e') | KeyCode::Char('у') => { - // Открыть emoji picker для добавления реакции - if let Some(msg) = app.get_selected_message() { - let chat_id = app.selected_chat_id.unwrap(); - let message_id = msg.id(); - - app.status_message = Some("Загрузка реакций...".to_string()); - app.needs_redraw = true; - - // Запрашиваем доступные реакции - match with_timeout_msg( - Duration::from_secs(5), - app.td_client - .get_message_available_reactions(chat_id, message_id), - "Таймаут загрузки реакций", - ) - .await - { - Ok(reactions) => { - let reactions: Vec = reactions; - if reactions.is_empty() { - app.error_message = - Some("Реакции недоступны для этого сообщения".to_string()); - app.status_message = None; - app.needs_redraw = true; - } else { - app.enter_reaction_picker_mode(message_id.as_i64(), reactions); - app.status_message = None; - app.needs_redraw = true; - } - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - app.needs_redraw = true; - } - } - } - } - _ => {} - } - return; - } - - // Ctrl+U для профиля - if key.code == KeyCode::Char('u') && has_ctrl { - if let Some(chat_id) = app.selected_chat_id { - app.status_message = Some("Загрузка профиля...".to_string()); - match with_timeout_msg( - Duration::from_secs(5), - app.td_client.get_profile_info(chat_id), - "Таймаут загрузки профиля", - ) - .await - { - Ok(profile) => { - app.enter_profile_mode(profile); - app.status_message = None; - } - Err(e) => { - app.error_message = Some(e); - app.status_message = None; - } - } - } - return; - } - - match key.code { - KeyCode::Backspace => { - // Удаляем символ слева от курсора - if app.cursor_position > 0 { - let chars: Vec = app.message_input.chars().collect(); - let mut new_input = String::new(); - for (i, ch) in chars.iter().enumerate() { - if i != app.cursor_position - 1 { - new_input.push(*ch); - } - } - app.message_input = new_input; - app.cursor_position -= 1; - } - } - KeyCode::Delete => { - // Удаляем символ справа от курсора - let len = app.message_input.chars().count(); - if app.cursor_position < len { - let chars: Vec = app.message_input.chars().collect(); - let mut new_input = String::new(); - for (i, ch) in chars.iter().enumerate() { - if i != app.cursor_position { - new_input.push(*ch); - } - } - app.message_input = new_input; - } - } - KeyCode::Char(c) => { - // Вставляем символ в позицию курсора - let chars: Vec = app.message_input.chars().collect(); - let mut new_input = String::new(); - for (i, ch) in chars.iter().enumerate() { - if i == app.cursor_position { - new_input.push(c); - } - new_input.push(*ch); - } - if app.cursor_position >= chars.len() { - new_input.push(c); - } - app.message_input = new_input; - app.cursor_position += 1; - - // Отправляем typing status с throttling (не чаще 1 раза в 5 сек) - let should_send_typing = app - .last_typing_sent - .map(|t| t.elapsed().as_secs() >= 5) - .unwrap_or(true); - if should_send_typing { - if let Some(chat_id) = app.get_selected_chat_id() { - app.td_client - .send_chat_action(ChatId::new(chat_id), ChatAction::Typing) - .await; - app.last_typing_sent = Some(Instant::now()); - } - } - } - KeyCode::Left => { - // Курсор влево - if app.cursor_position > 0 { - app.cursor_position -= 1; - } - } - KeyCode::Right => { - // Курсор вправо - let len = app.message_input.chars().count(); - if app.cursor_position < len { - app.cursor_position += 1; - } - } - KeyCode::Home => { - // Курсор в начало - app.cursor_position = 0; - } - KeyCode::End => { - // Курсор в конец - app.cursor_position = app.message_input.chars().count(); - } - // Стрелки вверх/вниз - скролл сообщений или начало выбора - KeyCode::Down => { - // Скролл вниз (к новым сообщениям) - if app.message_scroll_offset > 0 { - app.message_scroll_offset = app.message_scroll_offset.saturating_sub(3); - } - } - KeyCode::Up => { - // Если инпут пустой и не в режиме редактирования — начать выбор сообщения - if app.message_input.is_empty() && !app.is_editing() { - app.start_message_selection(); - } else { - // Скролл вверх (к старым сообщениям) - app.message_scroll_offset += 3; - - // Проверяем, нужно ли подгрузить старые сообщения - if !app.td_client.current_chat_messages().is_empty() { - let oldest_msg_id = app - .td_client - .current_chat_messages() - .first() - .map(|m| m.id()) - .unwrap_or(MessageId::new(0)); - if let Some(chat_id) = app.get_selected_chat_id() { - // Подгружаем больше сообщений если скролл близко к верху - if app.message_scroll_offset - > app.td_client.current_chat_messages().len().saturating_sub(10) - { - if let Ok(older) = with_timeout( - Duration::from_secs(3), - app.td_client - .load_older_messages(ChatId::new(chat_id), oldest_msg_id), - ) - .await - { - let older: Vec = older; - if !older.is_empty() { - // Добавляем старые сообщения в начало - let msgs = app.td_client.current_chat_messages_mut(); - msgs.splice(0..0, older); - } - } - } - } - } - } - } - _ => {} - } - } else { - // В режиме списка чатов - навигация стрелками и переключение папок - match key.code { - KeyCode::Down => { - app.next_chat(); - } - KeyCode::Up => { - app.previous_chat(); - } - // Цифры 1-9 - переключение папок - KeyCode::Char(c) if c >= '1' && c <= '9' => { - let folder_num = (c as usize) - ('1' as usize); // 0-based - if folder_num == 0 { - // 1 = All - app.selected_folder_id = None; - } else { - // 2, 3, 4... = папки из TDLib - if let Some(folder) = app.td_client.folders().get(folder_num - 1) { - let folder_id = folder.id; - app.selected_folder_id = Some(folder_id); - // Загружаем чаты папки - app.status_message = Some("Загрузка чатов папки...".to_string()); - let _ = with_timeout( - Duration::from_secs(5), - app.td_client.load_folder_chats(folder_id, 50), - ) - .await; - app.status_message = None; - } - } - app.chat_list_state.select(Some(0)); - } - _ => {} - } - } -} - -/// Подсчёт количества доступных действий в профиле -fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize { - let mut count = 0; - - if profile.username.is_some() { - count += 1; // Открыть в браузере - } - - count += 1; // Скопировать ID - - if profile.is_group { - count += 1; // Покинуть группу - } - - count -} - -/// Копирует текст в системный буфер обмена -#[cfg(feature = "clipboard")] -fn copy_to_clipboard(text: &str) -> Result<(), String> { - use arboard::Clipboard; - - let mut clipboard = - Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?; - clipboard - .set_text(text) - .map_err(|e| format!("Не удалось скопировать: {}", e))?; - - Ok(()) -} - -/// Заглушка для copy_to_clipboard когда feature "clipboard" выключена -#[cfg(not(feature = "clipboard"))] -fn copy_to_clipboard(_text: &str) -> Result<(), String> { - Err("Копирование в буфер обмена недоступно (требуется feature 'clipboard')".to_string()) -} - -/// Форматирует сообщение для копирования с контекстом -fn format_message_for_clipboard(msg: &crate::tdlib::MessageInfo) -> String { - let mut result = String::new(); - - // Добавляем forward контекст если есть - if let Some(forward) = msg.forward_from() { - result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name)); - } - - // Добавляем reply контекст если есть - if let Some(reply) = msg.reply_to() { - result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text)); - } - - // Добавляем основной текст с markdown форматированием - result.push_str(&convert_entities_to_markdown(msg.text(), msg.entities())); - - result -} - -/// Конвертирует текст с entities в markdown -fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String { - use tdlib_rs::enums::TextEntityType; - - if entities.is_empty() { - return text.to_string(); - } - - // Создаём вектор символов для работы с unicode - let chars: Vec = text.chars().collect(); - let mut result = String::new(); - let mut i = 0; - - while i < chars.len() { - // Ищем entity, который начинается в текущей позиции - let mut entity_found = false; - - for entity in entities { - if entity.offset as usize == i { - entity_found = true; - let end = (entity.offset + entity.length) as usize; - let entity_text: String = chars[i..end.min(chars.len())].iter().collect(); - - // Применяем форматирование в зависимости от типа - let formatted = match &entity.r#type { - TextEntityType::Bold => format!("**{}**", entity_text), - TextEntityType::Italic => format!("*{}*", entity_text), - TextEntityType::Underline => format!("__{}__", entity_text), - TextEntityType::Strikethrough => format!("~~{}~~", entity_text), - TextEntityType::Code | TextEntityType::Pre | TextEntityType::PreCode(_) => { - format!("`{}`", entity_text) - } - TextEntityType::TextUrl(url_info) => { - format!("[{}]({})", entity_text, url_info.url) - } - TextEntityType::Url => format!("<{}>", entity_text), - TextEntityType::Mention | TextEntityType::MentionName(_) => { - format!("@{}", entity_text.trim_start_matches('@')) - } - TextEntityType::Spoiler => format!("||{}||", entity_text), - _ => entity_text, - }; - - result.push_str(&formatted); - i = end; - break; - } - } - - if !entity_found { - result.push(chars[i]); - i += 1; - } - } - - result -} diff --git a/src/tdlib/client.rs.backup b/src/tdlib/client.rs.backup deleted file mode 100644 index 4d075f4..0000000 --- a/src/tdlib/client.rs.backup +++ /dev/null @@ -1,2036 +0,0 @@ -use crate::constants::{ - LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_CHATS, MAX_MESSAGES_IN_CHAT, - MAX_USER_CACHE_SIZE, TDLIB_CHAT_LIMIT, TDLIB_MESSAGE_LIMIT, -}; -use std::collections::HashMap; -use std::env; -use std::time::Instant; -use tdlib_rs::enums::{ - AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, - MessageSender, SearchMessagesFilter, Update, User, UserStatus, -}; -use tdlib_rs::types::TextEntity; - -/// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка -pub struct LruCache { - map: HashMap, - /// Порядок доступа: последний элемент — самый недавно использованный - order: Vec, - capacity: usize, -} - -impl LruCache { - 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, - 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, - /// Чат замьючен (уведомления отключены) - pub is_muted: bool, - /// Черновик сообщения - pub draft_text: Option, -} - -/// Информация о сообщении, на которое отвечают -#[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 ReactionInfo { - /// Эмодзи реакции (например, "👍") - pub emoji: String, - /// Количество людей, поставивших эту реакцию - pub count: i32, - /// Поставил ли текущий пользователь эту реакцию - pub is_chosen: bool, -} - -#[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, - 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, - /// Информация о forward (если сообщение переслано) - pub forward_from: Option, - /// Реакции на сообщение - pub reactions: Vec, -} - -#[derive(Debug, Clone)] -pub struct FolderInfo { - pub id: i32, - pub name: String, -} - -/// Информация о профиле чата/пользователя -#[derive(Debug, Clone)] -pub struct ProfileInfo { - pub chat_id: i64, - pub title: String, - pub username: Option, - pub bio: Option, - pub phone_number: Option, - pub chat_type: String, // "Личный чат", "Группа", "Канал" - pub member_count: Option, - pub description: Option, - pub invite_link: Option, - pub is_group: bool, - pub online_status: Option, -} - -/// Состояние сетевого соединения -#[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, - pub current_chat_messages: Vec, - /// ID текущего открытого чата (для получения новых сообщений) - pub current_chat_id: Option, - /// LRU-кэш usernames: user_id -> username - user_usernames: LruCache, - /// LRU-кэш имён: user_id -> display_name (first_name + last_name) - user_names: LruCache, - /// Связь 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, - /// LRU-кэш онлайн-статусов пользователей: user_id -> status - user_statuses: LruCache, - /// Состояние сетевого соединения - pub network_state: NetworkState, - /// Typing status для текущего чата: (user_id, action_text, timestamp) - pub typing_status: Option<(i64, String, Instant)>, - /// Последнее закреплённое сообщение текущего чата - pub current_pinned_message: Option, -} - -#[allow(dead_code)] -impl TdClient { - pub fn new() -> Self { - // Загружаем credentials из ~/.config/tele-tui/credentials или .env - let (api_id, api_hash) = match crate::config::Config::load_credentials() { - Ok(creds) => creds, - Err(err_msg) => { - eprintln!("\n{}\n", err_msg); - // Используем дефолтные значения, чтобы приложение запустилось - // Пользователь увидит сообщение об ошибке в UI - (0, String::new()) - } - }; - - 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 { - 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; - } - } - } - } - Update::ChatDraftMessage(update) => { - // Обновляем черновик в списке чатов - if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { - chat.draft_text = update.draft_message.as_ref().and_then(|draft| { - // Извлекаем текст из InputMessageText - if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = - &draft.input_message_text - { - Some(text_msg.text.text.clone()) - } else { - None - } - }); - } - } - Update::MessageInteractionInfo(update) => { - // Обновляем реакции в текущем открытом чате - if Some(update.chat_id) == self.current_chat_id { - if let Some(msg) = self - .current_chat_messages - .iter_mut() - .find(|m| m.id == update.message_id) - { - // Извлекаем реакции из interaction_info - msg.reactions = update - .interaction_info - .as_ref() - .and_then(|info| info.reactions.as_ref()) - .map(|reactions| { - reactions - .reactions - .iter() - .filter_map(|reaction| { - let emoji = match &reaction.r#type { - tdlib_rs::enums::ReactionType::Emoji(e) => { - e.emoji.clone() - } - tdlib_rs::enums::ReactionType::CustomEmoji(_) => { - return None - } - }; - - Some(ReactionInfo { - emoji, - count: reaction.total_count, - is_chosen: reaction.is_chosen, - }) - }) - .collect() - }) - .unwrap_or_default(); - } - } - } - _ => {} - } - } - - 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 = 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, - draft_text: None, - }; - - 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); - - // Извлекаем реакции - let reactions = self.extract_reactions(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, - reactions, - } - } - - /// Извлекает информацию о reply из сообщения - fn extract_reply_info(&self, message: &TdMessage) -> Option { - 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 { - message.forward_info.as_ref().map(|info| { - let sender_name = self.get_origin_sender_name(&info.origin); - ForwardInfo { sender_name, date: info.date } - }) - } - - /// Извлекает информацию о реакциях из сообщения - fn extract_reactions(&self, message: &TdMessage) -> Vec { - message - .interaction_info - .as_ref() - .and_then(|info| info.reactions.as_ref()) - .map(|reactions| { - reactions - .reactions - .iter() - .filter_map(|reaction| { - // Извлекаем эмодзи из ReactionType - let emoji = match &reaction.r#type { - tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(), - tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji - }; - - Some(ReactionInfo { - emoji, - count: reaction.total_count, - is_chosen: reaction.is_chosen, - }) - }) - .collect() - }) - .unwrap_or_default() - } - - /// Получает имя отправителя из 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 = 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 = 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 = - 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, 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(); - - // Обновляем reply info для сообщений где данные не были загружены - self.update_reply_info_from_loaded_messages(); - - // Отмечаем сообщения как прочитанные - 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 get_pinned_messages(&mut self, chat_id: i64) -> Result, 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 = 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, 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 - TDLIB_MESSAGE_LIMIT, // 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 = 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 get_profile_info(&self, chat_id: i64) -> Result { - use tdlib_rs::enums::ChatType; - - // Получаем основную информацию о чате - let chat_result = functions::get_chat(chat_id, self.client_id).await; - let chat = match chat_result { - Ok(tdlib_rs::enums::Chat::Chat(c)) => c, - Err(e) => return Err(format!("Ошибка загрузки чата: {:?}", e)), - }; - - let mut profile = ProfileInfo { - chat_id, - title: chat.title.clone(), - username: None, - bio: None, - phone_number: None, - chat_type: String::new(), - member_count: None, - description: None, - invite_link: None, - is_group: false, - online_status: None, - }; - - match &chat.r#type { - ChatType::Private(private_chat) => { - profile.chat_type = "Личный чат".to_string(); - profile.is_group = false; - - // Получаем полную информацию о пользователе - let user_result = functions::get_user(private_chat.user_id, self.client_id).await; - if let Ok(tdlib_rs::enums::User::User(user)) = user_result { - // Username - if let Some(usernames) = user.usernames { - if let Some(username) = usernames.active_usernames.first() { - profile.username = Some(format!("@{}", username)); - } - } - - // Phone number - if !user.phone_number.is_empty() { - profile.phone_number = Some(format!("+{}", user.phone_number)); - } - - // Online status - profile.online_status = Some(match user.status { - tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(), - tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(), - tdlib_rs::enums::UserStatus::LastWeek(_) => { - "Был(а) на этой неделе".to_string() - } - tdlib_rs::enums::UserStatus::LastMonth(_) => { - "Был(а) в этом месяце".to_string() - } - tdlib_rs::enums::UserStatus::Offline(offline) => { - crate::utils::format_was_online(offline.was_online) - } - _ => "Давно не был(а)".to_string(), - }); - } - - // Bio (getUserFullInfo) - let full_info_result = - functions::get_user_full_info(private_chat.user_id, self.client_id).await; - if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result - { - if let Some(bio_obj) = full_info.bio { - profile.bio = Some(bio_obj.text); - } - } - } - ChatType::BasicGroup(basic_group) => { - profile.chat_type = "Группа".to_string(); - profile.is_group = true; - - // Получаем информацию о группе - let group_result = - functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; - if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result { - profile.member_count = Some(group.member_count); - } - - // Полная информация о группе - let full_info_result = functions::get_basic_group_full_info( - basic_group.basic_group_id, - self.client_id, - ) - .await; - if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = - full_info_result - { - if !full_info.description.is_empty() { - profile.description = Some(full_info.description); - } - if let Some(link) = full_info.invite_link { - profile.invite_link = Some(link.invite_link); - } - } - } - ChatType::Supergroup(supergroup) => { - // Получаем информацию о супергруппе - let sg_result = - functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; - if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result { - profile.chat_type = if sg.is_channel { - "Канал".to_string() - } else { - "Супергруппа".to_string() - }; - profile.is_group = !sg.is_channel; - profile.member_count = Some(sg.member_count); - - // Username - if let Some(usernames) = sg.usernames { - if let Some(username) = usernames.active_usernames.first() { - profile.username = Some(format!("@{}", username)); - } - } - } - - // Полная информация о супергруппе - let full_info_result = - functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id) - .await; - if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = - full_info_result - { - if !full_info.description.is_empty() { - profile.description = Some(full_info.description); - } - if let Some(link) = full_info.invite_link { - profile.invite_link = Some(link.invite_link); - } - } - } - ChatType::Secret(_) => { - profile.chat_type = "Секретный чат".to_string(); - } - } - - Ok(profile) - } - - /// Выйти из группы/канала - pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> { - let result = functions::leave_chat(chat_id, self.client_id).await; - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка выхода из чата: {:?}", e)), - } - } - - /// Загрузка старых сообщений (для скролла вверх) - 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)), - } - } - - /// Отправка статуса действия в чат (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, - reply_info: Option, - ) -> Result { - use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, TextParseMode}; - use tdlib_rs::types::{ - FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown, - }; - - // Парсим 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, - reactions: Vec::new(), - }) - } - Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)), - } - } - - /// Получить доступные реакции для сообщения - pub async fn get_message_available_reactions( - &mut self, - chat_id: i64, - message_id: i64, - ) -> Result, String> { - use tdlib_rs::functions; - - let result = functions::get_message_available_reactions( - chat_id, - message_id, - 8, // row_size - количество реакций в ряду - self.client_id, - ) - .await; - - match result { - Ok(tdlib_rs::enums::AvailableReactions::AvailableReactions(reactions)) => { - // Извлекаем эмодзи из доступных реакций - // Используем top_reactions (самые популярные реакции) - let mut emojis: Vec = reactions - .top_reactions - .iter() - .filter_map(|reaction| { - if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { - Some(e.emoji.clone()) - } else { - None - } - }) - .collect(); - - // Если top_reactions пустой, используем popular_reactions - if emojis.is_empty() { - emojis = reactions - .popular_reactions - .iter() - .filter_map(|reaction| { - if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { - Some(e.emoji.clone()) - } else { - None - } - }) - .collect(); - } - - Ok(emojis) - } - Err(e) => Err(format!("Ошибка получения реакций: {:?}", e)), - } - } - - /// Добавить реакцию на сообщение (или убрать, если уже поставлена) - pub async fn toggle_reaction( - &mut self, - chat_id: i64, - message_id: i64, - emoji: String, - ) -> Result<(), String> { - use tdlib_rs::enums::ReactionType; - use tdlib_rs::functions; - use tdlib_rs::types::ReactionTypeEmoji; - - let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji }); - - let result = functions::add_message_reaction( - chat_id, - message_id, - reaction_type, - false, // is_big - обычная реакция (не "большая" анимация) - true, // update_recent_reactions - обновить список недавних реакций - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка добавления реакции: {:?}", e)), - } - } - - /// Редактирование текстового сообщения с поддержкой Markdown - /// Устанавливает черновик для чата через TDLib API - pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { - use tdlib_rs::enums::InputMessageContent; - use tdlib_rs::types::{DraftMessage, FormattedText, InputMessageText}; - - if text.is_empty() { - // Очищаем черновик - let result = functions::set_chat_draft_message( - chat_id, - 0, // message_thread_id - None, // draft_message (None = очистить) - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка очистки черновика: {:?}", e)), - } - } else { - // Создаём черновик - let formatted_text = FormattedText { text: text.clone(), entities: vec![] }; - - let input_message = InputMessageContent::InputMessageText(InputMessageText { - text: formatted_text, - link_preview_options: None, - clear_draft: false, - }); - - let draft = DraftMessage { - reply_to: None, - date: 0, // TDLib установит текущее время - input_message_text: input_message, - }; - - let result = functions::set_chat_draft_message( - chat_id, - 0, // message_thread_id - Some(draft), - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка установки черновика: {:?}", e)), - } - } - } - - pub async fn edit_message( - &self, - chat_id: i64, - message_id: i64, - text: String, - ) -> Result { - use tdlib_rs::enums::{InputMessageContent, TextParseMode}; - use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; - - // Парсим 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 сохраняется из оригинала - reactions: Vec::new(), // При редактировании реакции сохраняются из оригинала - }) - } - Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)), - } - } - - /// Удаление сообщений - /// revoke = true удаляет для всех, false только для себя - pub async fn delete_messages( - &self, - chat_id: i64, - message_ids: Vec, - 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, - ) -> 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 LAZY_LOAD_USERS_PER_TICK: usize = 5; - - // Убираем дубликаты и уже загруженные - self.pending_user_ids - .retain(|id| !self.user_names.contains_key(id)); - self.pending_user_ids.dedup(); - - // Берём последние LAZY_LOAD_USERS_PER_TICK элементов - let start = self.pending_user_ids.len().saturating_sub(LAZY_LOAD_USERS_PER_TICK); - let batch: Vec = 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) { - 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 = 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 = 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 = 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(), - } -} diff --git a/src/tdlib/client.rs.old b/src/tdlib/client.rs.old deleted file mode 100644 index 4d075f4..0000000 --- a/src/tdlib/client.rs.old +++ /dev/null @@ -1,2036 +0,0 @@ -use crate::constants::{ - LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_CHATS, MAX_MESSAGES_IN_CHAT, - MAX_USER_CACHE_SIZE, TDLIB_CHAT_LIMIT, TDLIB_MESSAGE_LIMIT, -}; -use std::collections::HashMap; -use std::env; -use std::time::Instant; -use tdlib_rs::enums::{ - AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, - MessageSender, SearchMessagesFilter, Update, User, UserStatus, -}; -use tdlib_rs::types::TextEntity; - -/// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка -pub struct LruCache { - map: HashMap, - /// Порядок доступа: последний элемент — самый недавно использованный - order: Vec, - capacity: usize, -} - -impl LruCache { - 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, - 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, - /// Чат замьючен (уведомления отключены) - pub is_muted: bool, - /// Черновик сообщения - pub draft_text: Option, -} - -/// Информация о сообщении, на которое отвечают -#[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 ReactionInfo { - /// Эмодзи реакции (например, "👍") - pub emoji: String, - /// Количество людей, поставивших эту реакцию - pub count: i32, - /// Поставил ли текущий пользователь эту реакцию - pub is_chosen: bool, -} - -#[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, - 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, - /// Информация о forward (если сообщение переслано) - pub forward_from: Option, - /// Реакции на сообщение - pub reactions: Vec, -} - -#[derive(Debug, Clone)] -pub struct FolderInfo { - pub id: i32, - pub name: String, -} - -/// Информация о профиле чата/пользователя -#[derive(Debug, Clone)] -pub struct ProfileInfo { - pub chat_id: i64, - pub title: String, - pub username: Option, - pub bio: Option, - pub phone_number: Option, - pub chat_type: String, // "Личный чат", "Группа", "Канал" - pub member_count: Option, - pub description: Option, - pub invite_link: Option, - pub is_group: bool, - pub online_status: Option, -} - -/// Состояние сетевого соединения -#[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, - pub current_chat_messages: Vec, - /// ID текущего открытого чата (для получения новых сообщений) - pub current_chat_id: Option, - /// LRU-кэш usernames: user_id -> username - user_usernames: LruCache, - /// LRU-кэш имён: user_id -> display_name (first_name + last_name) - user_names: LruCache, - /// Связь 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, - /// LRU-кэш онлайн-статусов пользователей: user_id -> status - user_statuses: LruCache, - /// Состояние сетевого соединения - pub network_state: NetworkState, - /// Typing status для текущего чата: (user_id, action_text, timestamp) - pub typing_status: Option<(i64, String, Instant)>, - /// Последнее закреплённое сообщение текущего чата - pub current_pinned_message: Option, -} - -#[allow(dead_code)] -impl TdClient { - pub fn new() -> Self { - // Загружаем credentials из ~/.config/tele-tui/credentials или .env - let (api_id, api_hash) = match crate::config::Config::load_credentials() { - Ok(creds) => creds, - Err(err_msg) => { - eprintln!("\n{}\n", err_msg); - // Используем дефолтные значения, чтобы приложение запустилось - // Пользователь увидит сообщение об ошибке в UI - (0, String::new()) - } - }; - - 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 { - 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; - } - } - } - } - Update::ChatDraftMessage(update) => { - // Обновляем черновик в списке чатов - if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { - chat.draft_text = update.draft_message.as_ref().and_then(|draft| { - // Извлекаем текст из InputMessageText - if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = - &draft.input_message_text - { - Some(text_msg.text.text.clone()) - } else { - None - } - }); - } - } - Update::MessageInteractionInfo(update) => { - // Обновляем реакции в текущем открытом чате - if Some(update.chat_id) == self.current_chat_id { - if let Some(msg) = self - .current_chat_messages - .iter_mut() - .find(|m| m.id == update.message_id) - { - // Извлекаем реакции из interaction_info - msg.reactions = update - .interaction_info - .as_ref() - .and_then(|info| info.reactions.as_ref()) - .map(|reactions| { - reactions - .reactions - .iter() - .filter_map(|reaction| { - let emoji = match &reaction.r#type { - tdlib_rs::enums::ReactionType::Emoji(e) => { - e.emoji.clone() - } - tdlib_rs::enums::ReactionType::CustomEmoji(_) => { - return None - } - }; - - Some(ReactionInfo { - emoji, - count: reaction.total_count, - is_chosen: reaction.is_chosen, - }) - }) - .collect() - }) - .unwrap_or_default(); - } - } - } - _ => {} - } - } - - 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 = 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, - draft_text: None, - }; - - 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); - - // Извлекаем реакции - let reactions = self.extract_reactions(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, - reactions, - } - } - - /// Извлекает информацию о reply из сообщения - fn extract_reply_info(&self, message: &TdMessage) -> Option { - 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 { - message.forward_info.as_ref().map(|info| { - let sender_name = self.get_origin_sender_name(&info.origin); - ForwardInfo { sender_name, date: info.date } - }) - } - - /// Извлекает информацию о реакциях из сообщения - fn extract_reactions(&self, message: &TdMessage) -> Vec { - message - .interaction_info - .as_ref() - .and_then(|info| info.reactions.as_ref()) - .map(|reactions| { - reactions - .reactions - .iter() - .filter_map(|reaction| { - // Извлекаем эмодзи из ReactionType - let emoji = match &reaction.r#type { - tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(), - tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji - }; - - Some(ReactionInfo { - emoji, - count: reaction.total_count, - is_chosen: reaction.is_chosen, - }) - }) - .collect() - }) - .unwrap_or_default() - } - - /// Получает имя отправителя из 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 = 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 = 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 = - 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, 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(); - - // Обновляем reply info для сообщений где данные не были загружены - self.update_reply_info_from_loaded_messages(); - - // Отмечаем сообщения как прочитанные - 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 get_pinned_messages(&mut self, chat_id: i64) -> Result, 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 = 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, 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 - TDLIB_MESSAGE_LIMIT, // 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 = 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 get_profile_info(&self, chat_id: i64) -> Result { - use tdlib_rs::enums::ChatType; - - // Получаем основную информацию о чате - let chat_result = functions::get_chat(chat_id, self.client_id).await; - let chat = match chat_result { - Ok(tdlib_rs::enums::Chat::Chat(c)) => c, - Err(e) => return Err(format!("Ошибка загрузки чата: {:?}", e)), - }; - - let mut profile = ProfileInfo { - chat_id, - title: chat.title.clone(), - username: None, - bio: None, - phone_number: None, - chat_type: String::new(), - member_count: None, - description: None, - invite_link: None, - is_group: false, - online_status: None, - }; - - match &chat.r#type { - ChatType::Private(private_chat) => { - profile.chat_type = "Личный чат".to_string(); - profile.is_group = false; - - // Получаем полную информацию о пользователе - let user_result = functions::get_user(private_chat.user_id, self.client_id).await; - if let Ok(tdlib_rs::enums::User::User(user)) = user_result { - // Username - if let Some(usernames) = user.usernames { - if let Some(username) = usernames.active_usernames.first() { - profile.username = Some(format!("@{}", username)); - } - } - - // Phone number - if !user.phone_number.is_empty() { - profile.phone_number = Some(format!("+{}", user.phone_number)); - } - - // Online status - profile.online_status = Some(match user.status { - tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(), - tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(), - tdlib_rs::enums::UserStatus::LastWeek(_) => { - "Был(а) на этой неделе".to_string() - } - tdlib_rs::enums::UserStatus::LastMonth(_) => { - "Был(а) в этом месяце".to_string() - } - tdlib_rs::enums::UserStatus::Offline(offline) => { - crate::utils::format_was_online(offline.was_online) - } - _ => "Давно не был(а)".to_string(), - }); - } - - // Bio (getUserFullInfo) - let full_info_result = - functions::get_user_full_info(private_chat.user_id, self.client_id).await; - if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result - { - if let Some(bio_obj) = full_info.bio { - profile.bio = Some(bio_obj.text); - } - } - } - ChatType::BasicGroup(basic_group) => { - profile.chat_type = "Группа".to_string(); - profile.is_group = true; - - // Получаем информацию о группе - let group_result = - functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; - if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result { - profile.member_count = Some(group.member_count); - } - - // Полная информация о группе - let full_info_result = functions::get_basic_group_full_info( - basic_group.basic_group_id, - self.client_id, - ) - .await; - if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = - full_info_result - { - if !full_info.description.is_empty() { - profile.description = Some(full_info.description); - } - if let Some(link) = full_info.invite_link { - profile.invite_link = Some(link.invite_link); - } - } - } - ChatType::Supergroup(supergroup) => { - // Получаем информацию о супергруппе - let sg_result = - functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; - if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result { - profile.chat_type = if sg.is_channel { - "Канал".to_string() - } else { - "Супергруппа".to_string() - }; - profile.is_group = !sg.is_channel; - profile.member_count = Some(sg.member_count); - - // Username - if let Some(usernames) = sg.usernames { - if let Some(username) = usernames.active_usernames.first() { - profile.username = Some(format!("@{}", username)); - } - } - } - - // Полная информация о супергруппе - let full_info_result = - functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id) - .await; - if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = - full_info_result - { - if !full_info.description.is_empty() { - profile.description = Some(full_info.description); - } - if let Some(link) = full_info.invite_link { - profile.invite_link = Some(link.invite_link); - } - } - } - ChatType::Secret(_) => { - profile.chat_type = "Секретный чат".to_string(); - } - } - - Ok(profile) - } - - /// Выйти из группы/канала - pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> { - let result = functions::leave_chat(chat_id, self.client_id).await; - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка выхода из чата: {:?}", e)), - } - } - - /// Загрузка старых сообщений (для скролла вверх) - 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)), - } - } - - /// Отправка статуса действия в чат (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, - reply_info: Option, - ) -> Result { - use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, TextParseMode}; - use tdlib_rs::types::{ - FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown, - }; - - // Парсим 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, - reactions: Vec::new(), - }) - } - Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)), - } - } - - /// Получить доступные реакции для сообщения - pub async fn get_message_available_reactions( - &mut self, - chat_id: i64, - message_id: i64, - ) -> Result, String> { - use tdlib_rs::functions; - - let result = functions::get_message_available_reactions( - chat_id, - message_id, - 8, // row_size - количество реакций в ряду - self.client_id, - ) - .await; - - match result { - Ok(tdlib_rs::enums::AvailableReactions::AvailableReactions(reactions)) => { - // Извлекаем эмодзи из доступных реакций - // Используем top_reactions (самые популярные реакции) - let mut emojis: Vec = reactions - .top_reactions - .iter() - .filter_map(|reaction| { - if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { - Some(e.emoji.clone()) - } else { - None - } - }) - .collect(); - - // Если top_reactions пустой, используем popular_reactions - if emojis.is_empty() { - emojis = reactions - .popular_reactions - .iter() - .filter_map(|reaction| { - if let tdlib_rs::enums::ReactionType::Emoji(e) = &reaction.r#type { - Some(e.emoji.clone()) - } else { - None - } - }) - .collect(); - } - - Ok(emojis) - } - Err(e) => Err(format!("Ошибка получения реакций: {:?}", e)), - } - } - - /// Добавить реакцию на сообщение (или убрать, если уже поставлена) - pub async fn toggle_reaction( - &mut self, - chat_id: i64, - message_id: i64, - emoji: String, - ) -> Result<(), String> { - use tdlib_rs::enums::ReactionType; - use tdlib_rs::functions; - use tdlib_rs::types::ReactionTypeEmoji; - - let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji }); - - let result = functions::add_message_reaction( - chat_id, - message_id, - reaction_type, - false, // is_big - обычная реакция (не "большая" анимация) - true, // update_recent_reactions - обновить список недавних реакций - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка добавления реакции: {:?}", e)), - } - } - - /// Редактирование текстового сообщения с поддержкой Markdown - /// Устанавливает черновик для чата через TDLib API - pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { - use tdlib_rs::enums::InputMessageContent; - use tdlib_rs::types::{DraftMessage, FormattedText, InputMessageText}; - - if text.is_empty() { - // Очищаем черновик - let result = functions::set_chat_draft_message( - chat_id, - 0, // message_thread_id - None, // draft_message (None = очистить) - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка очистки черновика: {:?}", e)), - } - } else { - // Создаём черновик - let formatted_text = FormattedText { text: text.clone(), entities: vec![] }; - - let input_message = InputMessageContent::InputMessageText(InputMessageText { - text: formatted_text, - link_preview_options: None, - clear_draft: false, - }); - - let draft = DraftMessage { - reply_to: None, - date: 0, // TDLib установит текущее время - input_message_text: input_message, - }; - - let result = functions::set_chat_draft_message( - chat_id, - 0, // message_thread_id - Some(draft), - self.client_id, - ) - .await; - - match result { - Ok(_) => Ok(()), - Err(e) => Err(format!("Ошибка установки черновика: {:?}", e)), - } - } - } - - pub async fn edit_message( - &self, - chat_id: i64, - message_id: i64, - text: String, - ) -> Result { - use tdlib_rs::enums::{InputMessageContent, TextParseMode}; - use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; - - // Парсим 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 сохраняется из оригинала - reactions: Vec::new(), // При редактировании реакции сохраняются из оригинала - }) - } - Err(e) => Err(format!("Ошибка редактирования сообщения: {:?}", e)), - } - } - - /// Удаление сообщений - /// revoke = true удаляет для всех, false только для себя - pub async fn delete_messages( - &self, - chat_id: i64, - message_ids: Vec, - 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, - ) -> 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 LAZY_LOAD_USERS_PER_TICK: usize = 5; - - // Убираем дубликаты и уже загруженные - self.pending_user_ids - .retain(|id| !self.user_names.contains_key(id)); - self.pending_user_ids.dedup(); - - // Берём последние LAZY_LOAD_USERS_PER_TICK элементов - let start = self.pending_user_ids.len().saturating_sub(LAZY_LOAD_USERS_PER_TICK); - let batch: Vec = 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) { - 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 = 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 = 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 = 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(), - } -}