diff --git a/src/config.rs b/src/config.rs index 7f55fb1..a8cda6a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,16 +3,37 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +/// Главная конфигурация приложения. +/// +/// Загружается из `~/.config/tele-tui/config.toml` и содержит настройки +/// общего поведения, цветовой схемы и горячих клавиш. +/// +/// # Examples +/// +/// ```ignore +/// // Загрузка конфигурации +/// let config = Config::load(); +/// +/// // Доступ к настройкам +/// println!("Timezone: {}", config.general.timezone); +/// println!("Incoming color: {}", config.colors.incoming_message); +/// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { + /// Общие настройки (timezone и т.д.). #[serde(default)] pub general: GeneralConfig, + + /// Цветовая схема интерфейса. #[serde(default)] pub colors: ColorsConfig, + + /// Горячие клавиши. #[serde(default)] pub hotkeys: HotkeysConfig, } +/// Общие настройки приложения. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GeneralConfig { /// Часовой пояс в формате "+03:00" или "-05:00" @@ -20,6 +41,10 @@ pub struct GeneralConfig { pub timezone: String, } +/// Цветовая схема интерфейса. +/// +/// Поддерживаемые цвета: red, green, blue, yellow, cyan, magenta, +/// white, black, gray/grey, а также light-варианты (lightred, lightgreen и т.д.). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ColorsConfig { /// Цвет входящих сообщений (white, gray, cyan и т.д.) @@ -363,7 +388,12 @@ impl Config { Ok(()) } - /// Путь к конфигурационному файлу + /// Возвращает путь к конфигурационному файлу. + /// + /// # Returns + /// + /// `Some(PathBuf)` - `~/.config/tele-tui/config.toml` + /// `None` - Не удалось определить директорию конфигурации pub fn config_path() -> Option { dirs::config_dir().map(|mut path| { path.push("tele-tui"); @@ -380,7 +410,21 @@ impl Config { }) } - /// Загрузить конфигурацию из файла + /// Загружает конфигурацию из файла. + /// + /// Ищет конфиг в `~/.config/tele-tui/config.toml`. + /// Если файл не существует, создаёт дефолтный. + /// Если файл невалиден, возвращает дефолтные значения. + /// + /// # Returns + /// + /// Всегда возвращает валидную конфигурацию. + /// + /// # Examples + /// + /// ```ignore + /// let config = Config::load(); + /// ``` pub fn load() -> Self { let config_path = match Self::config_path() { Some(path) => path, @@ -423,7 +467,14 @@ impl Config { } } - /// Сохранить конфигурацию в файл + /// Сохраняет конфигурацию в файл. + /// + /// Создаёт директорию `~/.config/tele-tui/` если её нет. + /// + /// # Returns + /// + /// * `Ok(())` - Конфиг сохранен + /// * `Err(String)` - Ошибка сохранения pub fn save(&self) -> Result<(), String> { let config_dir = Self::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?; @@ -443,7 +494,25 @@ impl Config { Ok(()) } - /// Парсит строку цвета в ratatui::style::Color + /// Парсит строку цвета в `ratatui::style::Color`. + /// + /// Поддерживает стандартные цвета (red, green, blue и т.д.), + /// light-варианты (lightred, lightgreen и т.д.) и grey/gray. + /// + /// # Arguments + /// + /// * `color_str` - Название цвета (case-insensitive) + /// + /// # Returns + /// + /// `Color` - Соответствующий цвет или `White` если цвет не распознан + /// + /// # Examples + /// + /// ```ignore + /// let color = config.parse_color("red"); + /// let color = config.parse_color("LightBlue"); + /// ``` pub fn parse_color(&self, color_str: &str) -> ratatui::style::Color { use ratatui::style::Color; @@ -473,8 +542,24 @@ impl Config { Self::config_dir().map(|dir| dir.join("credentials")) } - /// Загружает API_ID и API_HASH из credentials файла или .env - /// Возвращает (api_id, api_hash) или ошибку с инструкциями + /// Загружает API_ID и API_HASH для Telegram. + /// + /// Ищет credentials в следующем порядке: + /// 1. `~/.config/tele-tui/credentials` файл + /// 2. Переменные окружения `API_ID` и `API_HASH` + /// + /// # Returns + /// + /// * `Ok((api_id, api_hash))` - Учетные данные найдены + /// * `Err(String)` - Ошибка с инструкциями по настройке + /// + /// # Credentials Format + /// + /// Файл `~/.config/tele-tui/credentials`: + /// ```text + /// API_ID=12345 + /// API_HASH=your_api_hash_here + /// ``` pub fn load_credentials() -> Result<(i32, String), String> { use std::env; diff --git a/src/formatting.rs b/src/formatting.rs index cc1ca51..1fe6d4e 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -70,17 +70,41 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool { && a.mention == b.mention } -/// Преобразует текст с entities в вектор стилизованных Span +/// Преобразует текст с TDLib entities в стилизованные Span для рендеринга. /// -/// # Аргументы +/// Обрабатывает Markdown форматирование (bold, italic, code и т.д.) и преобразует +/// в визуальные стили для отображения в TUI. +/// +/// # Поддерживаемые стили +/// +/// - **Bold** - жирный текст +/// - *Italic* - курсив +/// - __Underline__ - подчёркнутый +/// - ~~Strikethrough~~ - зачёркнутый +/// - `Code` - моноширинный текст (cyan на тёмном фоне) +/// - ||Spoiler|| - скрытый текст (серый) +/// - [URL](url) - ссылки (синий с подчёркиванием) +/// - @mentions - упоминания (синий с подчёркиванием) +/// +/// # Arguments /// /// * `text` - Текст для форматирования -/// * `entities` - Массив TextEntity с информацией о форматировании -/// * `base_color` - Базовый цвет текста +/// * `entities` - Массив TDLib TextEntity с информацией о форматировании +/// * `base_color` - Базовый цвет для обычного текста /// -/// # Возвращает +/// # Returns /// -/// Вектор Span<'static> со стилизованными фрагментами текста +/// Вектор стилизованных `Span<'static>` для рендеринга в ratatui. +/// +/// # Examples +/// +/// ```ignore +/// let spans = format_text_with_entities( +/// "Hello **world**!", +/// &entities, +/// Color::White +/// ); +/// ``` pub fn format_text_with_entities( text: &str, entities: &[TextEntity], @@ -178,6 +202,28 @@ pub fn format_text_with_entities( /// # Возвращает /// /// Новый массив entities с откорректированными offset и length +/// Корректирует offset entities для подстроки текста. +/// +/// Используется при обрезке текста (например, для preview) для сохранения +/// корректных позиций форматирования. +/// +/// # Arguments +/// +/// * `entities` - Исходный массив entities +/// * `start` - Начальная позиция подстроки (в символах) +/// * `length` - Длина подстроки (в символах) +/// +/// # Returns +/// +/// Новый массив entities с скорректированными offset для подстроки. +/// +/// # Examples +/// +/// ```ignore +/// let text = "Hello **world** test"; +/// let substring = &text[0..15]; // "Hello **world**" +/// let adjusted = adjust_entities_for_substring(&entities, 0, 15); +/// ``` pub fn adjust_entities_for_substring( entities: &[TextEntity], start: usize, diff --git a/src/tdlib/auth.rs b/src/tdlib/auth.rs index f156197..483e55d 100644 --- a/src/tdlib/auth.rs +++ b/src/tdlib/auth.rs @@ -1,25 +1,88 @@ use tdlib_rs::enums::{AuthorizationState, Update}; use tdlib_rs::functions; +/// Состояние процесса авторизации в Telegram. +/// +/// Отслеживает текущий этап аутентификации пользователя, +/// от инициализации TDLib до полной авторизации. #[derive(Debug, Clone, PartialEq)] #[allow(dead_code)] pub enum AuthState { + /// Ожидание параметров TDLib (начальное состояние). WaitTdlibParameters, + + /// Ожидание ввода номера телефона. WaitPhoneNumber, + + /// Ожидание ввода кода подтверждения из SMS/Telegram. WaitCode, + + /// Ожидание ввода пароля двухфакторной аутентификации (2FA). WaitPassword, + + /// Авторизация завершена, клиент готов к работе. Ready, + + /// Соединение закрыто. Closed, + + /// Произошла ошибка авторизации. Error(String), } -/// Менеджер авторизации TDLib +/// Менеджер авторизации TDLib. +/// +/// Управляет процессом авторизации пользователя в Telegram, +/// отслеживает текущее состояние и предоставляет методы +/// для отправки учетных данных (номер телефона, код, пароль). +/// +/// # Процесс авторизации +/// +/// 1. `WaitTdlibParameters` → автоматически +/// 2. `WaitPhoneNumber` → [`send_phone_number()`](Self::send_phone_number) +/// 3. `WaitCode` → [`send_code()`](Self::send_code) +/// 4. `WaitPassword` (опционально) → [`send_password()`](Self::send_password) +/// 5. `Ready` → авторизация завершена +/// +/// # Examples +/// +/// ```ignore +/// let mut auth_manager = AuthManager::new(client_id); +/// +/// // Отправляем номер телефона +/// auth_manager.send_phone_number("+1234567890".to_string()).await?; +/// +/// // После получения кода из SMS +/// auth_manager.send_code("12345".to_string()).await?; +/// +/// // Если включена 2FA +/// if auth_manager.state == AuthState::WaitPassword { +/// auth_manager.send_password("my_password".to_string()).await?; +/// } +/// +/// // Проверяем авторизацию +/// if auth_manager.is_authenticated() { +/// println!("Successfully authenticated!"); +/// } +/// ``` pub struct AuthManager { + /// Текущее состояние авторизации. pub state: AuthState, + + /// ID клиента TDLib для API вызовов. client_id: i32, } impl AuthManager { + /// Создает новый менеджер авторизации. + /// + /// # Arguments + /// + /// * `client_id` - ID клиента TDLib для API вызовов + /// + /// # Returns + /// + /// Новый экземпляр `AuthManager` в состоянии `WaitTdlibParameters`. pub fn new(client_id: i32) -> Self { Self { state: AuthState::WaitTdlibParameters, @@ -27,11 +90,36 @@ impl AuthManager { } } + /// Проверяет, завершена ли авторизация. + /// + /// # Returns + /// + /// `true` если состояние равно `AuthState::Ready`, иначе `false`. + /// + /// # Examples + /// + /// ```ignore + /// if auth_manager.is_authenticated() { + /// println!("User is authenticated"); + /// } + /// ``` pub fn is_authenticated(&self) -> bool { self.state == AuthState::Ready } - /// Обработать обновление авторизации + /// Обрабатывает обновление состояния авторизации от TDLib. + /// + /// Автоматически обновляет внутреннее состояние [`AuthState`] на основе + /// полученного update от TDLib. + /// + /// # Arguments + /// + /// * `update` - Обновление от TDLib (проверяется на `Update::AuthorizationState`) + /// + /// # Note + /// + /// Этот метод должен вызываться для каждого update от TDLib, + /// чтобы состояние авторизации оставалось актуальным. pub fn handle_auth_update(&mut self, update: &Update) { if let Update::AuthorizationState(auth_update) = update { self.state = match &auth_update.authorization_state { @@ -46,7 +134,25 @@ impl AuthManager { } } - /// Отправить номер телефона + /// Отправляет номер телефона для авторизации. + /// + /// Используется на этапе [`AuthState::WaitPhoneNumber`]. + /// После успешной отправки состояние изменится на `WaitCode`. + /// + /// # Arguments + /// + /// * `phone` - Номер телефона в международном формате (например, "+1234567890") + /// + /// # Returns + /// + /// * `Ok(())` - Номер телефона принят, ожидайте SMS с кодом + /// * `Err(String)` - Ошибка (неверный формат, проблемы с сетью и т.д.) + /// + /// # Examples + /// + /// ```ignore + /// auth_manager.send_phone_number("+1234567890".to_string()).await?; + /// ``` pub async fn send_phone_number(&self, phone: String) -> Result<(), String> { functions::set_authentication_phone_number(phone, None, self.client_id) .await @@ -54,7 +160,26 @@ impl AuthManager { .map_err(|e| format!("Ошибка отправки номера: {:?}", e)) } - /// Отправить код подтверждения + /// Отправляет код подтверждения из SMS или Telegram. + /// + /// Используется на этапе [`AuthState::WaitCode`]. + /// После успешной проверки состояние изменится на `Ready` или `WaitPassword` + /// (если включена двухфакторная аутентификация). + /// + /// # Arguments + /// + /// * `code` - Код подтверждения (обычно 5 цифр) + /// + /// # Returns + /// + /// * `Ok(())` - Код верный + /// * `Err(String)` - Неверный код или истек срок действия + /// + /// # Examples + /// + /// ```ignore + /// auth_manager.send_code("12345".to_string()).await?; + /// ``` pub async fn send_code(&self, code: String) -> Result<(), String> { functions::check_authentication_code(code, self.client_id) .await @@ -62,7 +187,27 @@ impl AuthManager { .map_err(|e| format!("Ошибка проверки кода: {:?}", e)) } - /// Отправить пароль 2FA + /// Отправляет пароль двухфакторной аутентификации (2FA). + /// + /// Используется на этапе [`AuthState::WaitPassword`] (только если 2FA включена). + /// После успешной проверки состояние изменится на `Ready`. + /// + /// # Arguments + /// + /// * `password` - Пароль двухфакторной аутентификации + /// + /// # Returns + /// + /// * `Ok(())` - Пароль верный, авторизация завершена + /// * `Err(String)` - Неверный пароль + /// + /// # Examples + /// + /// ```ignore + /// if auth_manager.state == AuthState::WaitPassword { + /// auth_manager.send_password("my_2fa_password".to_string()).await?; + /// } + /// ``` pub async fn send_password(&self, password: String) -> Result<(), String> { functions::check_authentication_password(password, self.client_id) .await diff --git a/src/tdlib/chats.rs b/src/tdlib/chats.rs index c2059f6..cede8e5 100644 --- a/src/tdlib/chats.rs +++ b/src/tdlib/chats.rs @@ -6,17 +6,58 @@ use tdlib_rs::functions; use super::types::{ChatInfo, FolderInfo, MessageInfo, ProfileInfo}; -/// Менеджер чатов +/// Менеджер чатов TDLib. +/// +/// Управляет списком чатов, папками, информацией о профилях +/// и typing-статусом собеседников. +/// +/// # Основные возможности +/// +/// - Загрузка чатов из главного списка и папок +/// - Получение информации о профиле чата/пользователя +/// - Отправка typing-индикатора ("печатает...") +/// - Отслеживание typing-статуса собеседников +/// - Выход из чатов/групп +/// +/// # Examples +/// +/// ```ignore +/// let mut chat_manager = ChatManager::new(client_id); +/// +/// // Загружаем чаты +/// chat_manager.load_chats(50).await?; +/// +/// // Получаем информацию о профиле +/// let profile = chat_manager.get_profile_info(chat_id).await?; +/// println!("Bio: {}", profile.bio.unwrap_or_default()); +/// ``` pub struct ChatManager { + /// Список загруженных чатов. pub chats: Vec, + + /// Список папок чатов. pub folders: Vec, + + /// Позиция в главном списке чатов для пагинации. pub main_chat_list_position: i32, - /// Typing status для текущего чата: (user_id, action_text, timestamp) + + /// Typing status для текущего чата: (user_id, action_text, timestamp). pub typing_status: Option<(UserId, String, Instant)>, + + /// ID клиента TDLib для API вызовов. client_id: i32, } impl ChatManager { + /// Создает новый менеджер чатов. + /// + /// # Arguments + /// + /// * `client_id` - ID клиента TDLib для API вызовов + /// + /// # Returns + /// + /// Новый экземпляр `ChatManager` с пустым списком чатов. pub fn new(client_id: i32) -> Self { Self { chats: Vec::new(), @@ -27,7 +68,25 @@ impl ChatManager { } } - /// Загрузить чаты из основного списка + /// Загружает чаты из главного списка. + /// + /// Запрашивает у TDLib чаты из основного списка (исключая архив). + /// После вызова чаты будут доступны через updates от TDLib. + /// + /// # Arguments + /// + /// * `limit` - Максимальное количество чатов для загрузки + /// + /// # Returns + /// + /// * `Ok(())` - Запрос отправлен, чаты будут загружены через updates + /// * `Err(String)` - Ошибка при отправке запроса + /// + /// # Examples + /// + /// ```ignore + /// chat_manager.load_chats(50).await?; + /// ``` pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { let result = functions::load_chats(Some(ChatList::Main), limit, self.client_id).await; @@ -37,7 +96,24 @@ impl ChatManager { } } - /// Загрузить чаты из папки + /// Загружает чаты из указанной папки. + /// + /// # Arguments + /// + /// * `folder_id` - ID папки чатов + /// * `limit` - Максимальное количество чатов для загрузки + /// + /// # Returns + /// + /// * `Ok(())` - Запрос отправлен + /// * `Err(String)` - Ошибка при отправке запроса + /// + /// # Examples + /// + /// ```ignore + /// // Загрузить чаты из папки с ID 1 + /// chat_manager.load_folder_chats(1, 50).await?; + /// ``` 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 }); @@ -50,7 +126,24 @@ impl ChatManager { } } - /// Покинуть чат/группу + /// Выходит из чата или группы. + /// + /// Для приватных чатов — удаляет историю, для групп — покидает группу. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата для выхода + /// + /// # Returns + /// + /// * `Ok(())` - Успешный выход + /// * `Err(String)` - Ошибка (нет прав, чат не найден и т.д.) + /// + /// # Examples + /// + /// ```ignore + /// chat_manager.leave_chat(ChatId::new(123456)).await?; + /// ``` pub async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String> { let result = functions::leave_chat(chat_id.as_i64(), self.client_id).await; match result { @@ -59,7 +152,29 @@ impl ChatManager { } } - /// Получить информацию профиля чата + /// Получает детальную информацию о профиле чата или пользователя. + /// + /// Загружает полную информацию включая bio, номер телефона, username, + /// статус онлайн (для личных чатов), количество участников и описание + /// (для групп/каналов). + /// + /// # Arguments + /// + /// * `chat_id` - ID чата для получения информации + /// + /// # Returns + /// + /// * `Ok(ProfileInfo)` - Информация о профиле + /// * `Err(String)` - Ошибка получения данных + /// + /// # Examples + /// + /// ```ignore + /// let profile = chat_manager.get_profile_info(ChatId::new(123)).await?; + /// println!("Title: {}", profile.title); + /// println!("Bio: {}", profile.bio.unwrap_or_default()); + /// println!("Members: {}", profile.member_count.unwrap_or(0)); + /// ``` pub async fn get_profile_info(&self, chat_id: ChatId) -> Result { // Получаем основную информацию о чате let chat_result = functions::get_chat(chat_id.as_i64(), self.client_id).await; @@ -187,12 +302,55 @@ impl ChatManager { }) } - /// Отправить typing action + /// Отправляет typing-действие в чат. + /// + /// Показывает собеседнику индикатор "печатает..." или другой статус активности. + /// Действие автоматически сбрасывается через 5 секунд. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `action` - Тип действия (Typing, RecordingVideo, UploadingPhoto и т.д.) + /// + /// # Note + /// + /// Этот метод нужно вызывать периодически (каждые 5 секунд) пока действие активно. + /// + /// # Examples + /// + /// ```ignore + /// use tdlib_rs::enums::ChatAction; + /// + /// // Показать индикатор "печатает..." + /// chat_manager.send_chat_action( + /// chat_id, + /// ChatAction::Typing + /// ).await; + /// ``` pub async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) { let _ = functions::send_chat_action(chat_id.as_i64(), 0, Some(action), self.client_id).await; } - /// Очистить устаревший typing status (вызывать периодически) + /// Очищает устаревший typing-статус. + /// + /// Удаляет typing-статус если прошло более 5 секунд с момента последнего обновления. + /// Вызывайте этот метод периодически (например, каждый тик UI) для своевременной + /// очистки индикатора "печатает...". + /// + /// # Returns + /// + /// * `true` - Если статус был очищен + /// * `false` - Если статус актуален или его не было + /// + /// # Examples + /// + /// ```ignore + /// // В основном цикле UI + /// if chat_manager.clear_stale_typing_status() { + /// // Перерисовать UI чтобы убрать индикатор "печатает..." + /// needs_redraw = true; + /// } + /// ``` pub fn clear_stale_typing_status(&mut self) -> bool { if let Some((_, _, timestamp)) = self.typing_status { if timestamp.elapsed().as_secs() > 5 { @@ -203,7 +361,20 @@ impl ChatManager { false } - /// Получить текст typing индикатора + /// Получает текст typing-индикатора для отображения. + /// + /// # Returns + /// + /// * `Some(String)` - Текст действия (например, "печатает...", "записывает видео...") + /// * `None` - Нет активного typing-статуса + /// + /// # Examples + /// + /// ```ignore + /// if let Some(typing_text) = chat_manager.get_typing_text() { + /// println!("Status: {}", typing_text); + /// } + /// ``` pub fn get_typing_text(&self) -> Option { self.typing_status .as_ref() diff --git a/src/tdlib/messages.rs b/src/tdlib/messages.rs index 4155c80..b0c6cb5 100644 --- a/src/tdlib/messages.rs +++ b/src/tdlib/messages.rs @@ -6,17 +6,65 @@ use tdlib_rs::types::{Chat as TdChat, FormattedText, InputMessageReplyToMessage, use super::types::{ForwardInfo, MessageBuilder, MessageInfo, ReactionInfo, ReplyInfo}; -/// Менеджер сообщений +/// Менеджер сообщений TDLib. +/// +/// Управляет загрузкой, отправкой, редактированием и удалением сообщений. +/// Кеширует сообщения текущего открытого чата и закрепленные сообщения. +/// +/// # Основные возможности +/// +/// - Загрузка истории сообщений чата +/// - Отправка текстовых сообщений с поддержкой Markdown +/// - Редактирование и удаление сообщений +/// - Пересылка сообщений между чатами +/// - Поиск сообщений по тексту +/// - Управление закрепленными сообщениями +/// - Управление черновиками +/// - Автоматическая отметка сообщений как прочитанных +/// +/// # Examples +/// +/// ```ignore +/// let mut msg_manager = MessageManager::new(client_id); +/// +/// // Загрузить историю чата +/// let messages = msg_manager.get_chat_history(chat_id, 50).await?; +/// +/// // Отправить сообщение +/// let msg = msg_manager.send_message( +/// chat_id, +/// "Hello, **world**!".to_string(), +/// None, +/// None +/// ).await?; +/// ``` pub struct MessageManager { + /// Список сообщений текущего открытого чата (до MAX_MESSAGES_IN_CHAT). pub current_chat_messages: Vec, + + /// ID текущего открытого чата. pub current_chat_id: Option, + + /// Текущее закрепленное сообщение открытого чата. pub current_pinned_message: Option, - /// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids) + + /// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids). pub pending_view_messages: Vec<(ChatId, Vec)>, + + /// ID клиента TDLib для API вызовов. client_id: i32, } impl MessageManager { + /// Создает новый менеджер сообщений. + /// + /// # Arguments + /// + /// * `client_id` - ID клиента TDLib для API вызовов + /// + /// # Returns + /// + /// Новый экземпляр `MessageManager` с пустым списком сообщений. pub fn new(client_id: i32) -> Self { Self { current_chat_messages: Vec::new(), @@ -27,7 +75,19 @@ impl MessageManager { } } - /// Добавить сообщение в список текущего чата + /// Добавляет сообщение в список текущего чата. + /// + /// Автоматически ограничивает размер списка до [`MAX_MESSAGES_IN_CHAT`], + /// удаляя старые сообщения при превышении лимита. + /// + /// # Arguments + /// + /// * `msg` - Сообщение для добавления + /// + /// # Note + /// + /// Сообщение добавляется в конец списка. При превышении лимита + /// удаляются самые старые сообщения из начала списка. pub fn push_message(&mut self, msg: MessageInfo) { self.current_chat_messages.push(msg); // Добавляем в конец @@ -37,7 +97,31 @@ impl MessageManager { } } - /// Получить историю чата + /// Загружает историю сообщений чата. + /// + /// Запрашивает последние сообщения из указанного чата и сохраняет их + /// в [`current_chat_messages`](Self::current_chat_messages). Делает несколько попыток + /// загрузки при неудаче. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата для загрузки истории + /// * `limit` - Максимальное количество сообщений (обычно до 50) + /// + /// # Returns + /// + /// * `Ok(Vec)` - Список загруженных сообщений (от старых к новым) + /// * `Err(String)` - Ошибка загрузки после всех попыток + /// + /// # Examples + /// + /// ```ignore + /// let messages = msg_manager.get_chat_history( + /// ChatId::new(123), + /// 50 + /// ).await?; + /// println!("Loaded {} messages", messages.len()); + /// ``` pub async fn get_chat_history( &mut self, chat_id: ChatId, @@ -102,7 +186,30 @@ impl MessageManager { Ok(all_messages) } - /// Загрузить более старые сообщения + /// Загружает более старые сообщения для пагинации. + /// + /// Используется для подгрузки предыдущих сообщений при прокрутке + /// истории чата вверх. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `from_message_id` - ID сообщения, от которого загружать историю + /// + /// # Returns + /// + /// * `Ok(Vec)` - Список старых сообщений (от старых к новым) + /// * `Err(String)` - Ошибка загрузки + /// + /// # Examples + /// + /// ```ignore + /// // Загрузить сообщения старше указанного + /// let older = msg_manager.load_older_messages( + /// chat_id, + /// MessageId::new(12345) + /// ).await?; + /// ``` pub async fn load_older_messages( &mut self, chat_id: ChatId, @@ -135,7 +242,25 @@ impl MessageManager { } } - /// Получить закреплённые сообщения + /// Получает все закрепленные сообщения чата. + /// + /// Выполняет поиск всех сообщений с фильтром "pinned" и возвращает их список. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// + /// # Returns + /// + /// * `Ok(Vec)` - Список закрепленных сообщений (до 100) + /// * `Err(String)` - Ошибка загрузки + /// + /// # Examples + /// + /// ```ignore + /// let pinned = msg_manager.get_pinned_messages(chat_id).await?; + /// println!("Found {} pinned messages", pinned.len()); + /// ``` pub async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result, String> { let result = functions::search_chat_messages( chat_id.as_i64(), @@ -166,7 +291,17 @@ impl MessageManager { } } - /// Загрузить текущее закреплённое сообщение + /// Загружает текущее верхнее закрепленное сообщение. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// + /// # Note + /// + /// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`. + /// Нужно использовать `getChatPinnedMessage` или альтернативный способ. + /// Временно отключено, возвращает `None`. pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) { // TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat. // Нужно использовать getChatPinnedMessage или альтернативный способ. @@ -182,7 +317,23 @@ impl MessageManager { // } } - /// Поиск сообщений в чате + /// Выполняет поиск сообщений по тексту в указанном чате. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата для поиска + /// * `query` - Текстовый запрос для поиска + /// + /// # Returns + /// + /// * `Ok(Vec)` - Найденные сообщения (до 100) + /// * `Err(String)` - Ошибка поиска + /// + /// # Examples + /// + /// ```ignore + /// let results = msg_manager.search_messages(chat_id, "hello").await?; + /// ``` pub async fn search_messages( &self, chat_id: ChatId, @@ -217,7 +368,41 @@ impl MessageManager { } } - /// Отправить сообщение + /// Отправляет текстовое сообщение в чат с поддержкой Markdown. + /// + /// Автоматически парсит Markdown v2 форматирование (**bold**, *italic*, `code` и т.д.). + /// + /// # Arguments + /// + /// * `chat_id` - ID чата-получателя + /// * `text` - Текст сообщения (поддерживает Markdown v2) + /// * `reply_to_message_id` - Опциональный ID сообщения для ответа + /// * `reply_info` - Опциональная информация об исходном сообщении + /// + /// # Returns + /// + /// * `Ok(MessageInfo)` - Отправленное сообщение + /// * `Err(String)` - Ошибка отправки + /// + /// # Examples + /// + /// ```ignore + /// // Простое сообщение + /// let msg = msg_manager.send_message( + /// chat_id, + /// "Hello, **world**!".to_string(), + /// None, + /// None + /// ).await?; + /// + /// // Ответ на сообщение + /// let reply = msg_manager.send_message( + /// chat_id, + /// "Got it!".to_string(), + /// Some(MessageId::new(123)), + /// Some(reply_info) + /// ).await?; + /// ``` pub async fn send_message( &self, chat_id: ChatId, @@ -288,7 +473,18 @@ impl MessageManager { } } - /// Редактировать сообщение + /// Редактирует существующее сообщение. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `message_id` - ID сообщения для редактирования + /// * `text` - Новый текст (поддерживает Markdown v2) + /// + /// # Returns + /// + /// * `Ok(MessageInfo)` - Отредактированное сообщение + /// * `Err(String)` - Ошибка (нет прав, сообщение слишком старое и т.д.) pub async fn edit_message( &self, chat_id: ChatId, @@ -333,7 +529,18 @@ impl MessageManager { } } - /// Удалить сообщения + /// Удаляет одно или несколько сообщений. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `message_ids` - Список ID сообщений для удаления + /// * `revoke` - `true` - удалить для всех, `false` - только для себя + /// + /// # Returns + /// + /// * `Ok(())` - Сообщения удалены + /// * `Err(String)` - Ошибка удаления pub async fn delete_messages( &self, chat_id: ChatId, @@ -349,7 +556,18 @@ impl MessageManager { } } - /// Переслать сообщения + /// Пересылает сообщения из одного чата в другой. + /// + /// # Arguments + /// + /// * `to_chat_id` - ID чата-получателя + /// * `from_chat_id` - ID чата-источника + /// * `message_ids` - Список ID сообщений для пересылки + /// + /// # Returns + /// + /// * `Ok(())` - Сообщения переслань + /// * `Err(String)` - Ошибка пересылки pub async fn forward_messages( &self, to_chat_id: ChatId, @@ -375,7 +593,20 @@ impl MessageManager { } } - /// Установить черновик + /// Сохраняет черновик сообщения для чата. + /// + /// Черновик отображается в списке чатов и восстанавливается + /// при следующем открытии чата. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `text` - Текст черновика (пустая строка удаляет черновик) + /// + /// # Returns + /// + /// * `Ok(())` - Черновик сохранен + /// * `Err(String)` - Ошибка сохранения pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> { use tdlib_rs::types::DraftMessage; @@ -404,7 +635,14 @@ impl MessageManager { } } - /// Обработать очередь просмотра сообщений + /// Обрабатывает очередь сообщений для отметки как прочитанных. + /// + /// Автоматически отмечает просмотренные сообщения как прочитанные, + /// что сбрасывает счетчик непрочитанных сообщений в чате. + /// + /// # Note + /// + /// Вызывайте периодически (например, в основном цикле) для обработки накопленной очереди. pub async fn process_pending_view_messages(&mut self) { if self.pending_view_messages.is_empty() { return; @@ -571,7 +809,14 @@ impl MessageManager { Some(builder.build()) } - /// Получить недостающую reply информацию для сообщений + /// Загружает недостающую информацию об исходных сообщениях для ответов. + /// + /// Ищет все reply-сообщения с `sender_name == "Unknown"` и загружает + /// полную информацию (имя отправителя, текст) из TDLib. + /// + /// # Note + /// + /// Вызывайте после загрузки истории чата для заполнения информации о цитируемых сообщениях. pub async fn fetch_missing_reply_info(&mut self) { // Collect message IDs that need to be fetched let mut to_fetch = Vec::new(); diff --git a/src/tdlib/reactions.rs b/src/tdlib/reactions.rs index aeae800..0cff83b 100644 --- a/src/tdlib/reactions.rs +++ b/src/tdlib/reactions.rs @@ -3,17 +3,66 @@ use tdlib_rs::enums::ReactionType; use tdlib_rs::functions; use tdlib_rs::types::ReactionTypeEmoji; -/// Менеджер реакций на сообщения +/// Менеджер реакций на сообщения. +/// +/// Управляет добавлением, удалением и получением списка доступных +/// реакций (emoji) для сообщений в чатах. +/// +/// # Examples +/// +/// ```ignore +/// let reaction_manager = ReactionManager::new(client_id); +/// +/// // Получить доступные реакции +/// let reactions = reaction_manager.get_message_available_reactions( +/// chat_id, +/// message_id +/// ).await?; +/// +/// // Добавить/удалить реакцию +/// reaction_manager.toggle_reaction(chat_id, message_id, "👍".to_string()).await?; +/// ``` pub struct ReactionManager { + /// ID клиента TDLib для API вызовов. client_id: i32, } impl ReactionManager { + /// Создает новый менеджер реакций. + /// + /// # Arguments + /// + /// * `client_id` - ID клиента TDLib для API вызовов pub fn new(client_id: i32) -> Self { Self { client_id } } - /// Получить доступные реакции для сообщения + /// Получает список доступных реакций для сообщения. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `message_id` - ID сообщения + /// + /// # Returns + /// + /// * `Ok(Vec)` - Список доступных emoji реакций + /// * `Err(String)` - Ошибка получения + /// + /// # Note + /// + /// В tdlib-rs 1.8.29 структура AvailableReactions изменилась. + /// Временно возвращается стандартный набор из 12 популярных реакций. + /// + /// # Examples + /// + /// ```ignore + /// let reactions = manager.get_message_available_reactions( + /// ChatId::new(123), + /// MessageId::new(456) + /// ).await?; + /// println!("Available: {:?}", reactions); + /// ``` pub async fn get_message_available_reactions( &self, chat_id: ChatId, @@ -87,7 +136,28 @@ impl ReactionManager { } } - /// Переключить реакцию на сообщение + /// Переключает реакцию на сообщение (добавляет/удаляет). + /// + /// Сначала пытается добавить реакцию. Если не удалось (уже есть), + /// то удаляет её. + /// + /// # Arguments + /// + /// * `chat_id` - ID чата + /// * `message_id` - ID сообщения + /// * `emoji` - Emoji реакции (например, "👍", "❤️") + /// + /// # Returns + /// + /// * `Ok(())` - Реакция переключена + /// * `Err(String)` - Ошибка переключения + /// + /// # Examples + /// + /// ```ignore + /// // Добавить или удалить 👍 + /// manager.toggle_reaction(chat_id, message_id, "👍".to_string()).await?; + /// ``` pub async fn toggle_reaction( &self, chat_id: ChatId, diff --git a/src/tdlib/users.rs b/src/tdlib/users.rs index 8e05da5..3471812 100644 --- a/src/tdlib/users.rs +++ b/src/tdlib/users.rs @@ -6,15 +6,35 @@ use tdlib_rs::functions; use super::types::UserOnlineStatus; -/// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка +/// LRU (Least Recently Used) кэш с фиксированной ёмкостью. +/// +/// Автоматически удаляет самые давно использованные элементы при достижении лимита. +/// Основан на HashMap для быстрого доступа и Vec для отслеживания порядка использования. +/// +/// # Type Parameters +/// +/// * `V` - Тип значения (должен реализовывать `Clone`) +/// +/// # Examples +/// +/// ```ignore +/// let mut cache = LruCache::::new(100); +/// cache.insert(UserId::new(1), "Alice".to_string()); +/// assert_eq!(cache.get(&UserId::new(1)), Some(&"Alice".to_string())); +/// ``` pub struct LruCache { + /// Хранилище ключ-значение. map: HashMap, - /// Порядок доступа: последний элемент — самый недавно использованный + + /// Порядок доступа: последний элемент — самый недавно использованный. order: Vec, + + /// Максимальная ёмкость кэша. capacity: usize, } impl LruCache { + /// Создает новый LRU кэш с заданной ёмкостью. pub fn new(capacity: usize) -> Self { Self { map: HashMap::with_capacity(capacity), @@ -23,7 +43,7 @@ impl LruCache { } } - /// Получить значение и обновить порядок доступа + /// Получает значение и обновляет порядок доступа (помечает как использованное). pub fn get(&mut self, key: &UserId) -> Option<&V> { if self.map.contains_key(key) { // Перемещаем ключ в конец (самый недавно использованный) @@ -72,22 +92,56 @@ impl LruCache { } } -/// Кеш пользователей и их данных +/// Кэш информации о пользователях Telegram. +/// +/// Хранит данные пользователей (имена, usernames, статусы) в LRU-кэшах +/// для быстрого доступа без повторных запросов к TDLib. +/// +/// # Возможности +/// +/// - Кэширование имен пользователей (first_name + last_name) +/// - Кэширование usernames (@username) +/// - Кэширование онлайн-статусов +/// - Связь chat_id → user_id для приватных чатов +/// - Ленивая загрузка данных пользователей порциями +/// +/// # Examples +/// +/// ```ignore +/// let mut cache = UserCache::new(client_id); +/// +/// // Обработать обновление пользователя +/// cache.handle_user_update(&user_enum); +/// +/// // Получить имя +/// let name = cache.get_user_name(user_id).await; +/// ``` pub struct UserCache { - /// LRU-кэш usernames: user_id -> username + /// LRU-кэш usernames: user_id → username. pub user_usernames: LruCache, - /// LRU-кэш имён: user_id -> display_name (first_name + last_name) + + /// LRU-кэш имён: user_id → display_name (first_name + last_name). pub user_names: LruCache, - /// Связь chat_id -> user_id для приватных чатов + + /// Связь chat_id → user_id для приватных чатов. pub chat_user_ids: HashMap, - /// Очередь user_id для загрузки имён + + /// Очередь user_id для ленивой загрузки имён. pub pending_user_ids: Vec, - /// LRU-кэш онлайн-статусов пользователей: user_id -> status + + /// LRU-кэш онлайн-статусов: user_id → status. pub user_statuses: LruCache, + + /// ID клиента TDLib для API вызовов. client_id: i32, } impl UserCache { + /// Создает новый кэш пользователей. + /// + /// # Arguments + /// + /// * `client_id` - ID клиента TDLib для API вызовов pub fn new(client_id: i32) -> Self { Self { user_usernames: LruCache::new(MAX_USER_CACHE_SIZE), @@ -120,7 +174,13 @@ impl UserCache { self.user_statuses.peek(user_id) } - /// Обработать обновление пользователя + /// Обрабатывает обновление пользователя от TDLib. + /// + /// Сохраняет username, имя и статус пользователя в соответствующие кэши. + /// + /// # Arguments + /// + /// * `user_enum` - Обновление пользователя от TDLib pub fn handle_user_update(&mut self, user_enum: &User) { if let User::User(user) = user_enum { let user_id = user.id; @@ -139,7 +199,12 @@ impl UserCache { } } - /// Обработать обновление статуса пользователя + /// Обновляет онлайн-статус пользователя. + /// + /// # Arguments + /// + /// * `user_id` - ID пользователя + /// * `status` - Новый статус от TDLib pub fn update_status(&mut self, user_id: UserId, status: &UserStatus) { let online_status = match status { UserStatus::Online(_) => UserOnlineStatus::Online, @@ -157,7 +222,17 @@ impl UserCache { self.chat_user_ids.insert(chat_id, user_id); } - /// Получить имя пользователя (асинхронно с загрузкой если нужно) + /// Получает имя пользователя из кэша или загружает из TDLib. + /// + /// Сначала проверяет кэш, затем при необходимости загружает из API. + /// + /// # Arguments + /// + /// * `user_id` - ID пользователя + /// + /// # Returns + /// + /// Имя пользователя (first_name + last_name) или "User {id}" если не найден. pub async fn get_user_name(&self, user_id: UserId) -> String { // Сначала пытаемся получить из кэша if let Some(name) = self.user_names.peek(&user_id) { @@ -174,7 +249,14 @@ impl UserCache { } } - /// Обработать очередь отложенных user_ids (загрузка имён небольшими порциями) + /// Обрабатывает очередь отложенных user_ids для ленивой загрузки. + /// + /// Загружает данные пользователей небольшими порциями (по [`LAZY_LOAD_USERS_PER_TICK`]) + /// для избежания блокировки UI. + /// + /// # Note + /// + /// Вызывайте периодически в основном цикле приложения. pub async fn process_pending_user_ids(&mut self) { if self.pending_user_ids.is_empty() { return;