Files
telegram-tui/src/tdlib/client.rs
Mikhail Kilin 25c57c55fb feat: add per-account lock file protection via fs2
Prevent running multiple tele-tui instances with the same account by
using advisory file locks (flock). Lock is acquired before raw mode so
errors print to normal terminal. Account switching acquires new lock
before releasing old. Also log set_tdlib_parameters errors via tracing
instead of silently discarding them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:35:06 +03:00

705 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::types::{ChatId, MessageId, UserId};
use std::env;
use std::path::PathBuf;
use tdlib_rs::enums::{Chat as TdChat, ChatList, ConnectionState, Update, UserStatus};
use tdlib_rs::functions;
use tdlib_rs::types::Message as TdMessage;
use super::auth::{AuthManager, AuthState};
use super::chats::ChatManager;
use super::messages::MessageManager;
use super::reactions::ReactionManager;
use super::types::{
ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus,
};
use super::users::UserCache;
use crate::notifications::NotificationManager;
/// TDLib client wrapper for Telegram integration.
///
/// Provides high-level API for authentication, chat management, messaging,
/// and user caching. Delegates functionality to specialized managers:
/// - `AuthManager` for authentication flow
/// - `ChatManager` for chat operations
/// - `MessageManager` for message operations
/// - `UserCache` for user information caching
/// - `ReactionManager` for message reactions
///
/// # Examples
///
/// ```ignore
/// use tele_tui::tdlib::TdClient;
///
/// let mut client = TdClient::new(std::path::PathBuf::from("tdlib_data"));
///
/// // Start authorization
/// client.send_phone_number("+1234567890".to_string()).await?;
/// client.send_code("12345".to_string()).await?;
///
/// // Load chats
/// client.load_chats(50).await?;
/// # Ok::<(), String>(())
/// ```
pub struct TdClient {
pub api_id: i32,
pub api_hash: String,
pub db_path: PathBuf,
client_id: i32,
// Менеджеры (делегируем им функциональность)
pub auth: AuthManager,
pub chat_manager: ChatManager,
pub message_manager: MessageManager,
pub user_cache: UserCache,
pub reaction_manager: ReactionManager,
pub notification_manager: NotificationManager,
// Состояние сети
pub network_state: NetworkState,
}
#[allow(dead_code)]
impl TdClient {
/// Creates a new TDLib client instance.
///
/// Reads API credentials from:
/// 1. ~/.config/tele-tui/credentials file
/// 2. Environment variables `API_ID` and `API_HASH` (fallback)
///
/// Initializes all managers and sets initial network state to Connecting.
///
/// # Returns
///
/// A new `TdClient` instance ready for authentication.
pub fn new(db_path: PathBuf) -> Self {
// Пробуем загрузить credentials из Config (файл или env)
let (api_id, api_hash) = crate::config::Config::load_credentials().unwrap_or_else(|_| {
// Fallback на прямое чтение из env (старое поведение)
let api_id = env::var("API_ID")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
let api_hash = env::var("API_HASH").unwrap_or_default();
(api_id, api_hash)
});
let client_id = tdlib_rs::create_client();
Self {
api_id,
api_hash,
db_path,
client_id,
auth: AuthManager::new(client_id),
chat_manager: ChatManager::new(client_id),
message_manager: MessageManager::new(client_id),
user_cache: UserCache::new(client_id),
reaction_manager: ReactionManager::new(client_id),
notification_manager: NotificationManager::new(),
network_state: NetworkState::Connecting,
}
}
/// Configures notification manager from app config
pub fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) {
self.notification_manager.set_enabled(config.enabled);
self.notification_manager
.set_only_mentions(config.only_mentions);
self.notification_manager.set_timeout(config.timeout_ms);
self.notification_manager
.set_urgency(config.urgency.clone());
// Note: show_preview is used when formatting notification body
}
/// Synchronizes muted chats from Telegram to notification manager.
///
/// Should be called after chats are loaded to ensure muted chats don't trigger notifications.
pub fn sync_notification_muted_chats(&mut self) {
self.notification_manager
.sync_muted_chats(&self.chat_manager.chats);
}
// Делегирование к auth
/// Sends phone number for authentication.
///
/// This is the first step of the authentication flow.
///
/// # Arguments
///
/// * `phone` - Phone number in international format (e.g., "+1234567890")
///
/// # Errors
///
/// Returns an error if the phone number is invalid or network request fails.
pub async fn send_phone_number(&self, phone: String) -> Result<(), String> {
self.auth.send_phone_number(phone).await
}
/// Sends authentication code received via SMS.
///
/// This is the second step of the authentication flow.
///
/// # Arguments
///
/// * `code` - Authentication code (typically 5 digits)
///
/// # Errors
///
/// Returns an error if the code is invalid or expired.
pub async fn send_code(&self, code: String) -> Result<(), String> {
self.auth.send_code(code).await
}
/// Sends 2FA password if required.
///
/// This is the third step of the authentication flow (if 2FA is enabled).
///
/// # Arguments
///
/// * `password` - Two-factor authentication password
///
/// # Errors
///
/// Returns an error if the password is incorrect.
pub async fn send_password(&self, password: String) -> Result<(), String> {
self.auth.send_password(password).await
}
// Делегирование к chat_manager
/// Loads chats from the main chat list.
///
/// Loads up to `limit` chats from ChatList::Main, excluding archived chats.
/// Filters out "Deleted Account" chats automatically.
///
/// # Arguments
///
/// * `limit` - Maximum number of chats to load (typically 50-200)
///
/// # Errors
///
/// Returns an error if the network request fails.
pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
self.chat_manager.load_chats(limit).await
}
/// Loads chats from a specific folder.
///
/// # Arguments
///
/// * `folder_id` - Folder ID (1-9 for user folders)
/// * `limit` - Maximum number of chats to load
///
/// # Errors
///
/// Returns an error if the folder doesn't exist or network request fails.
pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
self.chat_manager.load_folder_chats(folder_id, limit).await
}
/// Leaves a group or channel.
///
/// # Arguments
///
/// * `chat_id` - ID of the chat to leave
///
/// # Errors
///
/// Returns an error if the user is not a member or network request fails.
pub async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String> {
self.chat_manager.leave_chat(chat_id).await
}
/// Gets profile information for a chat.
///
/// Fetches detailed information including bio, username, member count, etc.
///
/// # Arguments
///
/// * `chat_id` - ID of the chat
///
/// # Returns
///
/// `ProfileInfo` with chat details
///
/// # Errors
///
/// Returns an error if the chat doesn't exist or network request fails.
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
self.chat_manager.get_profile_info(chat_id).await
}
pub async fn send_chat_action(&self, chat_id: ChatId, action: tdlib_rs::enums::ChatAction) {
self.chat_manager.send_chat_action(chat_id, action).await
}
pub fn clear_stale_typing_status(&mut self) -> bool {
self.chat_manager.clear_stale_typing_status()
}
// Делегирование к message_manager
pub async fn get_chat_history(
&mut self,
chat_id: ChatId,
limit: i32,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager.get_chat_history(chat_id, limit).await
}
pub async fn load_older_messages(
&mut self,
chat_id: ChatId,
from_message_id: MessageId,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager
.load_older_messages(chat_id, from_message_id)
.await
}
pub async fn get_pinned_messages(
&mut self,
chat_id: ChatId,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager.get_pinned_messages(chat_id).await
}
pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) {
self.message_manager
.load_current_pinned_message(chat_id)
.await
}
pub async fn search_messages(
&self,
chat_id: ChatId,
query: &str,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager.search_messages(chat_id, query).await
}
pub async fn send_message(
&self,
chat_id: ChatId,
text: String,
reply_to_message_id: Option<MessageId>,
reply_info: Option<super::types::ReplyInfo>,
) -> Result<MessageInfo, String> {
self.message_manager
.send_message(chat_id, text, reply_to_message_id, reply_info)
.await
}
pub async fn edit_message(
&self,
chat_id: ChatId,
message_id: MessageId,
text: String,
) -> Result<MessageInfo, String> {
self.message_manager
.edit_message(chat_id, message_id, text)
.await
}
pub async fn delete_messages(
&self,
chat_id: ChatId,
message_ids: Vec<MessageId>,
revoke: bool,
) -> Result<(), String> {
self.message_manager
.delete_messages(chat_id, message_ids, revoke)
.await
}
pub async fn forward_messages(
&self,
to_chat_id: ChatId,
from_chat_id: ChatId,
message_ids: Vec<MessageId>,
) -> Result<(), String> {
self.message_manager
.forward_messages(to_chat_id, from_chat_id, message_ids)
.await
}
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
self.message_manager.set_draft_message(chat_id, text).await
}
pub fn push_message(&mut self, msg: MessageInfo) {
self.message_manager.push_message(msg)
}
pub async fn fetch_missing_reply_info(&mut self) {
self.message_manager.fetch_missing_reply_info().await
}
pub async fn process_pending_view_messages(&mut self) {
self.message_manager.process_pending_view_messages().await
}
// Делегирование к user_cache
pub fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
self.user_cache.get_status_by_chat_id(chat_id)
}
pub async fn process_pending_user_ids(&mut self) {
self.user_cache.process_pending_user_ids().await
}
// Делегирование к reaction_manager
pub async fn get_message_available_reactions(
&self,
chat_id: ChatId,
message_id: MessageId,
) -> Result<Vec<String>, String> {
self.reaction_manager
.get_message_available_reactions(chat_id, message_id)
.await
}
pub async fn toggle_reaction(
&self,
chat_id: ChatId,
message_id: MessageId,
emoji: String,
) -> Result<(), String> {
self.reaction_manager
.toggle_reaction(chat_id, message_id, emoji)
.await
}
// Делегирование файловых операций
/// Скачивает файл по file_id и возвращает локальный путь.
pub async fn download_file(&self, file_id: i32) -> Result<String, String> {
match functions::download_file(file_id, 1, 0, 0, true, self.client_id).await {
Ok(tdlib_rs::enums::File::File(file)) => {
if file.local.is_downloading_completed && !file.local.path.is_empty() {
Ok(file.local.path)
} else {
Err("Файл не скачан".to_string())
}
}
Err(e) => Err(format!("Ошибка скачивания файла: {:?}", e)),
}
}
// Вспомогательные методы
pub fn client_id(&self) -> i32 {
self.client_id
}
pub async fn get_me(&self) -> Result<i64, String> {
match functions::get_me(self.client_id).await {
Ok(tdlib_rs::enums::User::User(user)) => Ok(user.id),
Err(e) => Err(format!("Ошибка получения текущего пользователя: {:?}", e)),
}
}
// Accessor methods для обратной совместимости
pub fn auth_state(&self) -> &AuthState {
&self.auth.state
}
pub fn chats(&self) -> &[ChatInfo] {
&self.chat_manager.chats
}
pub fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
&mut self.chat_manager.chats
}
pub fn folders(&self) -> &[FolderInfo] {
&self.chat_manager.folders
}
pub fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
&mut self.chat_manager.folders
}
pub fn current_chat_messages(&self) -> &[MessageInfo] {
&self.message_manager.current_chat_messages
}
pub fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo> {
&mut self.message_manager.current_chat_messages
}
pub fn current_chat_id(&self) -> Option<ChatId> {
self.message_manager.current_chat_id
}
pub fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
self.message_manager.current_chat_id = chat_id;
}
pub fn current_pinned_message(&self) -> Option<&MessageInfo> {
self.message_manager.current_pinned_message.as_ref()
}
pub fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
self.message_manager.current_pinned_message = msg;
}
pub fn typing_status(&self) -> Option<&(crate::types::UserId, String, std::time::Instant)> {
self.chat_manager.typing_status.as_ref()
}
pub fn set_typing_status(
&mut self,
status: Option<(crate::types::UserId, String, std::time::Instant)>,
) {
self.chat_manager.typing_status = status;
}
pub fn pending_view_messages(&self) -> &[(crate::types::ChatId, Vec<crate::types::MessageId>)] {
&self.message_manager.pending_view_messages
}
pub fn pending_view_messages_mut(
&mut self,
) -> &mut Vec<(crate::types::ChatId, Vec<crate::types::MessageId>)> {
&mut self.message_manager.pending_view_messages
}
pub fn pending_user_ids(&self) -> &[crate::types::UserId] {
&self.user_cache.pending_user_ids
}
pub fn pending_user_ids_mut(&mut self) -> &mut Vec<crate::types::UserId> {
&mut self.user_cache.pending_user_ids
}
pub fn main_chat_list_position(&self) -> i32 {
self.chat_manager.main_chat_list_position
}
pub fn set_main_chat_list_position(&mut self, position: i32) {
self.chat_manager.main_chat_list_position = position;
}
// User cache accessors
pub fn user_cache(&self) -> &UserCache {
&self.user_cache
}
pub fn user_cache_mut(&mut self) -> &mut UserCache {
&mut self.user_cache
}
// ==================== Helper методы для упрощения обработки updates ====================
/// Обрабатываем одно обновление от TDLib
pub fn handle_update(&mut self, update: Update) {
match update {
Update::AuthorizationState(state) => {
crate::tdlib::update_handlers::handle_auth_state(self, state.authorization_state);
}
Update::NewChat(new_chat) => {
// new_chat.chat is already a Chat struct, wrap it in TdChat enum
let td_chat = TdChat::Chat(new_chat.chat.clone());
crate::tdlib::chat_helpers::add_or_update_chat(self, &td_chat);
}
Update::ChatLastMessage(update) => {
let chat_id = ChatId::new(update.chat_id);
let (last_message_text, last_message_date) = update
.last_message
.as_ref()
.map(|msg| (Self::extract_message_text_static(msg).0, msg.date))
.unwrap_or_default();
crate::tdlib::chat_helpers::update_chat(self, chat_id, |chat| {
chat.last_message = last_message_text;
chat.last_message_date = last_message_date;
});
// Обновляем позиции если они пришли
for pos in update
.positions
.iter()
.filter(|p| matches!(p.list, ChatList::Main))
{
crate::tdlib::chat_helpers::update_chat(self, chat_id, |chat| {
chat.order = pos.order;
chat.is_pinned = pos.is_pinned;
});
}
// Пересортируем по order
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
}
Update::ChatReadInbox(update) => {
crate::tdlib::chat_helpers::update_chat(
self,
ChatId::new(update.chat_id),
|chat| {
chat.unread_count = update.unread_count;
},
);
}
Update::ChatUnreadMentionCount(update) => {
crate::tdlib::chat_helpers::update_chat(
self,
ChatId::new(update.chat_id),
|chat| {
chat.unread_mention_count = update.unread_mention_count;
},
);
}
Update::ChatNotificationSettings(update) => {
crate::tdlib::chat_helpers::update_chat(
self,
ChatId::new(update.chat_id),
|chat| {
// mute_for > 0 означает что чат замьючен
chat.is_muted = update.notification_settings.mute_for > 0;
},
);
}
Update::ChatReadOutbox(update) => {
// Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения
let last_read_msg_id = MessageId::new(update.last_read_outbox_message_id);
crate::tdlib::chat_helpers::update_chat(
self,
ChatId::new(update.chat_id),
|chat| {
chat.last_read_outbox_message_id = last_read_msg_id;
},
);
// Если это текущий открытый чат — обновляем is_read у сообщений
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
for msg in self.current_chat_messages_mut().iter_mut() {
if msg.is_outgoing() && msg.id() <= last_read_msg_id {
msg.state.is_read = true;
}
}
}
}
Update::ChatPosition(update) => {
crate::tdlib::update_handlers::handle_chat_position_update(self, update);
}
Update::NewMessage(new_msg) => {
crate::tdlib::update_handlers::handle_new_message_update(self, new_msg);
}
Update::User(update) => {
crate::tdlib::update_handlers::handle_user_update(self, update);
}
Update::ChatFolders(update) => {
// Обновляем список папок
*self.folders_mut() = update
.chat_folders
.into_iter()
.map(|f| FolderInfo { id: f.id, name: f.title })
.collect();
self.set_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_cache
.user_statuses
.insert(UserId::new(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) => {
crate::tdlib::update_handlers::handle_chat_action_update(self, update);
}
Update::ChatDraftMessage(update) => {
crate::tdlib::update_handlers::handle_chat_draft_message_update(self, update);
}
Update::MessageInteractionInfo(update) => {
crate::tdlib::update_handlers::handle_message_interaction_info_update(self, update);
}
Update::MessageSendSucceeded(update) => {
crate::tdlib::update_handlers::handle_message_send_succeeded_update(self, update);
}
_ => {}
}
}
// Helper functions
pub fn extract_message_text_static(
message: &TdMessage,
) -> (String, Vec<tdlib_rs::types::TextEntity>) {
use tdlib_rs::enums::MessageContent;
match &message.content {
MessageContent::MessageText(text) => {
(text.text.text.clone(), text.text.entities.clone())
}
_ => (String::new(), Vec::new()),
}
}
/// Recreates the TDLib client with a new database path.
///
/// Closes the old client, creates a new one, and spawns TDLib parameter initialization.
pub async fn recreate_client(&mut self, db_path: PathBuf) -> Result<(), String> {
// 1. Close old client
let _ = functions::close(self.client_id).await;
// 2. Create new client
let new_client = TdClient::new(db_path);
// 3. Spawn set_tdlib_parameters for new client
let new_client_id = new_client.client_id;
let api_id = new_client.api_id;
let api_hash = new_client.api_hash.clone();
let db_path_str = new_client.db_path.to_string_lossy().to_string();
tokio::spawn(async move {
if let Err(e) = functions::set_tdlib_parameters(
false,
db_path_str,
"".to_string(),
"".to_string(),
true,
true,
true,
false,
api_id,
api_hash,
"en".to_string(),
"Desktop".to_string(),
"".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
new_client_id,
)
.await
{
tracing::error!("set_tdlib_parameters failed on recreate: {:?}", e);
}
});
// 4. Replace self
*self = new_client;
Ok(())
}
pub fn extract_content_text(content: &tdlib_rs::enums::MessageContent) -> String {
use tdlib_rs::enums::MessageContent;
match content {
MessageContent::MessageText(text) => text.text.text.clone(),
_ => String::new(),
}
}
}