Files
telegram-tui/src/tdlib/client.rs
Mikhail Kilin 1ef341d907 commit
2026-01-21 21:20:18 +03:00

809 lines
32 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 std::env;
use std::collections::HashMap;
use tdlib_rs::enums::{AuthorizationState, ChatList, ChatType, MessageContent, Update, User, UserStatus};
use tdlib_rs::functions;
use tdlib_rs::types::{Chat as TdChat, Message as TdMessage};
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum AuthState {
WaitTdlibParameters,
WaitPhoneNumber,
WaitCode,
WaitPassword,
Ready,
Closed,
Error(String),
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ChatInfo {
pub id: i64,
pub title: String,
pub username: Option<String>,
pub last_message: String,
pub last_message_date: i32,
pub unread_count: i32,
pub is_pinned: bool,
pub order: i64,
/// ID последнего прочитанного исходящего сообщения (для галочек)
pub last_read_outbox_message_id: i64,
/// ID папок, в которых находится чат
pub folder_ids: Vec<i32>,
}
#[derive(Debug, Clone)]
pub struct MessageInfo {
pub id: i64,
pub sender_name: String,
pub is_outgoing: bool,
pub content: String,
pub date: i32,
pub is_read: bool,
}
#[derive(Debug, Clone)]
pub struct FolderInfo {
pub id: i32,
pub name: String,
}
/// Онлайн-статус пользователя
#[derive(Debug, Clone, PartialEq)]
pub enum UserOnlineStatus {
/// Онлайн
Online,
/// Был недавно (менее часа назад)
Recently,
/// Был на этой неделе
LastWeek,
/// Был в этом месяце
LastMonth,
/// Давно не был
LongTimeAgo,
/// Оффлайн с указанием времени (unix timestamp)
Offline(i32),
}
pub struct TdClient {
pub auth_state: AuthState,
pub api_id: i32,
pub api_hash: String,
client_id: i32,
pub chats: Vec<ChatInfo>,
pub current_chat_messages: Vec<MessageInfo>,
/// ID текущего открытого чата (для получения новых сообщений)
pub current_chat_id: Option<i64>,
/// Кэш usernames: user_id -> username
user_usernames: HashMap<i64, String>,
/// Кэш имён: user_id -> display_name (first_name + last_name)
user_names: HashMap<i64, String>,
/// Связь chat_id -> user_id для приватных чатов
chat_user_ids: HashMap<i64, i64>,
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids)
pub pending_view_messages: Vec<(i64, Vec<i64>)>,
/// Очередь user_id для загрузки имён
pub pending_user_ids: Vec<i64>,
/// Папки чатов
pub folders: Vec<FolderInfo>,
/// Позиция основного списка среди папок
pub main_chat_list_position: i32,
/// Онлайн-статусы пользователей: user_id -> status
user_statuses: HashMap<i64, UserOnlineStatus>,
}
#[allow(dead_code)]
impl TdClient {
pub fn new() -> Self {
let api_id: i32 = env::var("API_ID")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
let api_hash = env::var("API_HASH").unwrap_or_default();
let client_id = tdlib_rs::create_client();
TdClient {
auth_state: AuthState::WaitTdlibParameters,
api_id,
api_hash,
client_id,
chats: Vec::new(),
current_chat_messages: Vec::new(),
current_chat_id: None,
user_usernames: HashMap::new(),
user_names: HashMap::new(),
chat_user_ids: HashMap::new(),
pending_view_messages: Vec::new(),
pending_user_ids: Vec::new(),
folders: Vec::new(),
main_chat_list_position: 0,
user_statuses: HashMap::new(),
}
}
pub fn is_authenticated(&self) -> bool {
matches!(self.auth_state, AuthState::Ready)
}
pub fn client_id(&self) -> i32 {
self.client_id
}
/// Получение онлайн-статуса пользователя по chat_id (для приватных чатов)
pub fn get_user_status_by_chat_id(&self, chat_id: i64) -> Option<&UserOnlineStatus> {
self.chat_user_ids
.get(&chat_id)
.and_then(|user_id| self.user_statuses.get(user_id))
}
/// Инициализация TDLib с параметрами
pub async fn init(&mut self) -> Result<(), String> {
let result = functions::set_tdlib_parameters(
false, // use_test_dc
"tdlib_data".to_string(), // database_directory
"".to_string(), // files_directory
"".to_string(), // database_encryption_key
true, // use_file_database
true, // use_chat_info_database
true, // use_message_database
false, // use_secret_chats
self.api_id, // api_id
self.api_hash.clone(), // api_hash
"en".to_string(), // system_language_code
"Desktop".to_string(), // device_model
"".to_string(), // system_version
env!("CARGO_PKG_VERSION").to_string(), // application_version
self.client_id,
)
.await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Failed to set TDLib parameters: {:?}", e)),
}
}
/// Обрабатываем одно обновление от TDLib
pub fn handle_update(&mut self, update: Update) {
match update {
Update::AuthorizationState(state) => {
self.handle_auth_state(state.authorization_state);
}
Update::NewChat(new_chat) => {
self.add_or_update_chat(&new_chat.chat);
}
Update::ChatLastMessage(update) => {
let chat_id = update.chat_id;
let (last_message_text, last_message_date) = update
.last_message
.as_ref()
.map(|msg| (extract_message_text_static(msg), msg.date))
.unwrap_or_default();
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) {
chat.last_message = last_message_text;
chat.last_message_date = last_message_date;
}
// Обновляем позиции если они пришли
for pos in &update.positions {
if matches!(pos.list, ChatList::Main) {
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) {
chat.order = pos.order;
chat.is_pinned = pos.is_pinned;
}
}
}
// Пересортируем по order
self.chats.sort_by(|a, b| b.order.cmp(&a.order));
}
Update::ChatReadInbox(update) => {
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
chat.unread_count = update.unread_count;
}
}
Update::ChatReadOutbox(update) => {
// Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
chat.last_read_outbox_message_id = update.last_read_outbox_message_id;
}
// Если это текущий открытый чат — обновляем is_read у сообщений
if Some(update.chat_id) == self.current_chat_id {
for msg in &mut self.current_chat_messages {
if msg.is_outgoing && msg.id <= update.last_read_outbox_message_id {
msg.is_read = true;
}
}
}
}
Update::ChatPosition(update) => {
// Обновляем позицию чата или удаляем его из списка
match &update.position.list {
ChatList::Main => {
if update.position.order == 0 {
// Чат больше не в Main (перемещён в архив и т.д.)
self.chats.retain(|c| c.id != update.chat_id);
} else if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
// Обновляем позицию существующего чата
chat.order = update.position.order;
chat.is_pinned = update.position.is_pinned;
}
// Пересортируем по order
self.chats.sort_by(|a, b| b.order.cmp(&a.order));
}
ChatList::Folder(folder) => {
// Обновляем folder_ids для чата
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) {
if update.position.order == 0 {
// Чат удалён из папки
chat.folder_ids.retain(|&id| id != folder.chat_folder_id);
} else {
// Чат добавлен в папку
if !chat.folder_ids.contains(&folder.chat_folder_id) {
chat.folder_ids.push(folder.chat_folder_id);
}
}
}
}
ChatList::Archive => {
// Архив пока не обрабатываем
}
}
}
Update::NewMessage(new_msg) => {
// Добавляем новое сообщение если это текущий открытый чат
let chat_id = new_msg.message.chat_id;
if Some(chat_id) == self.current_chat_id {
let msg_info = self.convert_message(&new_msg.message, chat_id);
let msg_id = msg_info.id;
let is_incoming = !msg_info.is_outgoing;
// Проверяем, что сообщение ещё не добавлено (по id)
if !self.current_chat_messages.iter().any(|m| m.id == msg_info.id) {
self.current_chat_messages.push(msg_info);
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
if is_incoming {
self.pending_view_messages.push((chat_id, vec![msg_id]));
}
}
}
}
Update::User(update) => {
// Сохраняем имя и username пользователя
let user = update.user;
// Пропускаем удалённые аккаунты (пустое имя)
if user.first_name.is_empty() && user.last_name.is_empty() {
// Удаляем чаты с этим пользователем из списка
let user_id = user.id;
self.chats.retain(|c| {
self.chat_user_ids.get(&c.id) != Some(&user_id)
});
return;
}
// Сохраняем display name (first_name + last_name)
let display_name = if user.last_name.is_empty() {
user.first_name.clone()
} else {
format!("{} {}", user.first_name, user.last_name)
};
self.user_names.insert(user.id, display_name);
// Сохраняем username если есть
if let Some(usernames) = user.usernames {
if let Some(username) = usernames.active_usernames.first() {
self.user_usernames.insert(user.id, username.clone());
// Обновляем username в чатах, связанных с этим пользователем
for (&chat_id, &user_id) in &self.chat_user_ids.clone() {
if user_id == user.id {
if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) {
chat.username = Some(format!("@{}", username));
}
}
}
}
}
}
Update::ChatFolders(update) => {
// Обновляем список папок
self.folders = update
.chat_folders
.into_iter()
.map(|f| FolderInfo {
id: f.id,
name: f.title,
})
.collect();
self.main_chat_list_position = update.main_chat_list_position;
}
Update::UserStatus(update) => {
// Обновляем онлайн-статус пользователя
let status = match update.status {
UserStatus::Online(_) => UserOnlineStatus::Online,
UserStatus::Offline(offline) => UserOnlineStatus::Offline(offline.was_online),
UserStatus::Recently(_) => UserOnlineStatus::Recently,
UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek,
UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth,
UserStatus::Empty => UserOnlineStatus::LongTimeAgo,
};
self.user_statuses.insert(update.user_id, status);
}
_ => {}
}
}
fn handle_auth_state(&mut self, state: AuthorizationState) {
self.auth_state = match state {
AuthorizationState::WaitTdlibParameters => AuthState::WaitTdlibParameters,
AuthorizationState::WaitPhoneNumber => AuthState::WaitPhoneNumber,
AuthorizationState::WaitCode(_) => AuthState::WaitCode,
AuthorizationState::WaitPassword(_) => AuthState::WaitPassword,
AuthorizationState::Ready => AuthState::Ready,
AuthorizationState::Closed => AuthState::Closed,
_ => self.auth_state.clone(),
};
}
fn add_or_update_chat(&mut self, td_chat: &TdChat) {
// Пропускаем удалённые аккаунты
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
// Удаляем из списка если уже был добавлен
self.chats.retain(|c| c.id != td_chat.id);
return;
}
// Ищем позицию в Main списке (если есть)
let main_position = td_chat.positions.iter().find(|pos| {
matches!(pos.list, ChatList::Main)
});
// Получаем order и is_pinned из позиции, или используем значения по умолчанию
let (order, is_pinned) = main_position
.map(|p| (p.order, p.is_pinned))
.unwrap_or((1, false)); // order=1 чтобы чат отображался
let (last_message, last_message_date) = td_chat
.last_message
.as_ref()
.map(|m| (extract_message_text_static(m), m.date))
.unwrap_or_default();
// Извлекаем user_id для приватных чатов и сохраняем связь
let username = match &td_chat.r#type {
ChatType::Private(private) => {
self.chat_user_ids.insert(td_chat.id, private.user_id);
// Проверяем, есть ли уже username в кэше
self.user_usernames.get(&private.user_id).map(|u| format!("@{}", u))
}
_ => None,
};
// Извлекаем ID папок из позиций
let folder_ids: Vec<i32> = td_chat
.positions
.iter()
.filter_map(|pos| {
if let ChatList::Folder(folder) = &pos.list {
Some(folder.chat_folder_id)
} else {
None
}
})
.collect();
let chat_info = ChatInfo {
id: td_chat.id,
title: td_chat.title.clone(),
username,
last_message,
last_message_date,
unread_count: td_chat.unread_count,
is_pinned,
order,
last_read_outbox_message_id: td_chat.last_read_outbox_message_id,
folder_ids,
};
if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) {
existing.title = chat_info.title;
existing.last_message = chat_info.last_message;
existing.last_message_date = chat_info.last_message_date;
existing.unread_count = chat_info.unread_count;
existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id;
existing.folder_ids = chat_info.folder_ids;
// Обновляем username если он появился
if chat_info.username.is_some() {
existing.username = chat_info.username;
}
// Обновляем позицию только если она пришла
if main_position.is_some() {
existing.is_pinned = chat_info.is_pinned;
existing.order = chat_info.order;
}
} else {
self.chats.push(chat_info);
}
// Сортируем чаты по order (TDLib order учитывает pinned и время)
self.chats.sort_by(|a, b| b.order.cmp(&a.order));
}
fn convert_message(&mut self, message: &TdMessage, chat_id: i64) -> MessageInfo {
let sender_name = match &message.sender_id {
tdlib_rs::enums::MessageSender::User(user) => {
// Пробуем получить имя из кеша
if let Some(name) = self.user_names.get(&user.user_id) {
name.clone()
} else {
// Добавляем в очередь для загрузки
if !self.pending_user_ids.contains(&user.user_id) {
self.pending_user_ids.push(user.user_id);
}
format!("User_{}", user.user_id)
}
}
tdlib_rs::enums::MessageSender::Chat(chat) => {
// Для чатов используем название чата
self.chats
.iter()
.find(|c| c.id == chat.chat_id)
.map(|c| c.title.clone())
.unwrap_or_else(|| format!("Chat_{}", chat.chat_id))
}
};
// Определяем, прочитано ли исходящее сообщение
let is_read = if message.is_outgoing {
// Сообщение прочитано, если его ID <= last_read_outbox_message_id чата
self.chats
.iter()
.find(|c| c.id == chat_id)
.map(|c| message.id <= c.last_read_outbox_message_id)
.unwrap_or(false)
} else {
true // Входящие сообщения не показывают галочки
};
MessageInfo {
id: message.id,
sender_name,
is_outgoing: message.is_outgoing,
content: extract_message_text_static(message),
date: message.date,
is_read,
}
}
/// Отправка номера телефона
pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> {
let result = functions::set_authentication_phone_number(
phone,
None,
self.client_id,
)
.await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка отправки номера: {:?}", e)),
}
}
/// Отправка кода подтверждения
pub async fn send_code(&mut self, code: String) -> Result<(), String> {
let result = functions::check_authentication_code(code, self.client_id).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Неверный код: {:?}", e)),
}
}
/// Отправка пароля 2FA
pub async fn send_password(&mut self, password: String) -> Result<(), String> {
let result = functions::check_authentication_password(password, self.client_id).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Неверный пароль: {:?}", e)),
}
}
/// Загрузка списка чатов
pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
let result = functions::load_chats(
Some(ChatList::Main),
limit,
self.client_id,
)
.await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка загрузки чатов: {:?}", e)),
}
}
/// Загрузка чатов для конкретной папки
pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
let chat_list = ChatList::Folder(tdlib_rs::types::ChatListFolder {
chat_folder_id: folder_id,
});
let result = functions::load_chats(
Some(chat_list),
limit,
self.client_id,
)
.await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка загрузки чатов папки: {:?}", e)),
}
}
/// Загрузка истории сообщений чата
pub async fn get_chat_history(
&mut self,
chat_id: i64,
limit: i32,
) -> Result<Vec<MessageInfo>, String> {
// Устанавливаем текущий чат для получения новых сообщений
self.current_chat_id = Some(chat_id);
let _ = functions::open_chat(chat_id, self.client_id).await;
// Пробуем загрузить несколько раз, так как сообщения могут подгружаться с сервера
let mut all_messages: Vec<MessageInfo> = Vec::new();
let mut from_message_id: i64 = 0;
let mut attempts = 0;
const MAX_ATTEMPTS: i32 = 3;
while attempts < MAX_ATTEMPTS {
let result = functions::get_chat_history(
chat_id,
from_message_id,
0, // offset
limit,
false, // only_local - загружаем с сервера!
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::Messages::Messages(messages)) => {
let mut batch: Vec<MessageInfo> = Vec::new();
for m in messages.messages.into_iter().flatten() {
batch.push(self.convert_message(&m, chat_id));
}
if batch.is_empty() {
break;
}
// Запоминаем ID самого старого сообщения для следующей загрузки
if let Some(oldest) = batch.last() {
from_message_id = oldest.id;
}
// Добавляем сообщения (они приходят от новых к старым)
all_messages.extend(batch);
attempts += 1;
// Если получили достаточно сообщений, выходим
if all_messages.len() >= limit as usize {
break;
}
}
Err(e) => {
if all_messages.is_empty() {
return Err(format!("Ошибка загрузки сообщений: {:?}", e));
}
break;
}
}
}
// Сообщения приходят от новых к старым, переворачиваем
all_messages.reverse();
self.current_chat_messages = all_messages.clone();
// Отмечаем сообщения как прочитанные
if !all_messages.is_empty() {
let message_ids: Vec<i64> = all_messages.iter().map(|m| m.id).collect();
let _ = functions::view_messages(
chat_id,
message_ids,
None, // source
true, // force_read
self.client_id,
)
.await;
}
Ok(all_messages)
}
/// Загрузка старых сообщений (для скролла вверх)
pub async fn load_older_messages(
&mut self,
chat_id: i64,
from_message_id: i64,
limit: i32,
) -> Result<Vec<MessageInfo>, String> {
let result = functions::get_chat_history(
chat_id,
from_message_id,
0, // offset
limit,
false, // only_local
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::Messages::Messages(messages)) => {
let mut result_messages: Vec<MessageInfo> = Vec::new();
for m in messages.messages.into_iter().flatten() {
result_messages.push(self.convert_message(&m, chat_id));
}
// Сообщения приходят от новых к старым, переворачиваем
result_messages.reverse();
Ok(result_messages)
}
Err(e) => Err(format!("Ошибка загрузки сообщений: {:?}", e)),
}
}
/// Получение информации о пользователе по ID
pub async fn get_user_name(&self, user_id: i64) -> String {
match functions::get_user(user_id, self.client_id).await {
Ok(user) => {
// User is an enum, need to match it
match user {
User::User(u) => {
let first = u.first_name;
let last = u.last_name;
if last.is_empty() {
first
} else {
format!("{} {}", first, last)
}
}
}
}
Err(_) => format!("User_{}", user_id),
}
}
/// Получение моего user_id
pub async fn get_me(&self) -> Result<i64, String> {
match functions::get_me(self.client_id).await {
Ok(user) => {
match user {
User::User(u) => Ok(u.id),
}
}
Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)),
}
}
/// Отправка текстового сообщения
pub async fn send_message(&self, chat_id: i64, text: String) -> Result<MessageInfo, String> {
use tdlib_rs::types::{FormattedText, InputMessageText};
use tdlib_rs::enums::InputMessageContent;
let content = InputMessageContent::InputMessageText(InputMessageText {
text: FormattedText {
text: text.clone(),
entities: vec![],
},
link_preview_options: None,
clear_draft: true,
});
let result = functions::send_message(
chat_id,
0, // message_thread_id
None, // reply_to
None, // options
content,
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::Message::Message(msg)) => {
// Конвертируем отправленное сообщение в MessageInfo
Ok(MessageInfo {
id: msg.id,
sender_name: "You".to_string(),
is_outgoing: true,
content: text,
date: msg.date,
is_read: false,
})
}
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
}
}
/// Обработка очереди сообщений для отметки как прочитанных
pub async fn process_pending_view_messages(&mut self) {
let pending = std::mem::take(&mut self.pending_view_messages);
for (chat_id, message_ids) in pending {
let _ = functions::view_messages(
chat_id,
message_ids,
None, // source
true, // force_read
self.client_id,
)
.await;
}
}
/// Обработка очереди user_id для загрузки имён
pub async fn process_pending_user_ids(&mut self) {
let pending = std::mem::take(&mut self.pending_user_ids);
for user_id in pending {
// Пропускаем если имя уже есть
if self.user_names.contains_key(&user_id) {
continue;
}
// Загружаем информацию о пользователе
if let Ok(User::User(user)) = functions::get_user(user_id, self.client_id).await {
let display_name = if user.last_name.is_empty() {
user.first_name.clone()
} else {
format!("{} {}", user.first_name, user.last_name)
};
self.user_names.insert(user_id, display_name.clone());
// Обновляем имя в текущих сообщениях
for msg in &mut self.current_chat_messages {
if msg.sender_name == format!("User_{}", user_id) {
msg.sender_name = display_name.clone();
}
}
}
}
}
}
/// Статическая функция для извлечения текста сообщения (без &self)
fn extract_message_text_static(message: &TdMessage) -> String {
match &message.content {
MessageContent::MessageText(text) => text.text.text.clone(),
MessageContent::MessagePhoto(photo) => {
if photo.caption.text.is_empty() {
"[Фото]".to_string()
} else {
format!("[Фото] {}", photo.caption.text)
}
}
MessageContent::MessageVideo(_) => "[Видео]".to_string(),
MessageContent::MessageDocument(doc) => {
format!("[Файл: {}]", doc.document.file_name)
}
MessageContent::MessageVoiceNote(_) => "[Голосовое сообщение]".to_string(),
MessageContent::MessageVideoNote(_) => "[Видеосообщение]".to_string(),
MessageContent::MessageSticker(sticker) => {
format!("[Стикер: {}]", sticker.sticker.emoji)
}
MessageContent::MessageAnimation(_) => "[GIF]".to_string(),
MessageContent::MessageAudio(audio) => {
format!("[Аудио: {}]", audio.audio.title)
}
MessageContent::MessageCall(_) => "[Звонок]".to_string(),
MessageContent::MessagePoll(poll) => {
format!("[Опрос: {}]", poll.poll.question.text)
}
_ => "[Сообщение]".to_string(),
}
}