This commit is contained in:
Mikhail Kilin
2026-01-30 23:55:01 +03:00
parent 433233d766
commit bba5cbd22d
25 changed files with 5896 additions and 1469 deletions

72
src/tdlib/auth.rs Normal file
View File

@@ -0,0 +1,72 @@
use tdlib_rs::enums::{AuthorizationState, Update};
use tdlib_rs::functions;
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum AuthState {
WaitTdlibParameters,
WaitPhoneNumber,
WaitCode,
WaitPassword,
Ready,
Closed,
Error(String),
}
/// Менеджер авторизации TDLib
pub struct AuthManager {
pub state: AuthState,
client_id: i32,
}
impl AuthManager {
pub fn new(client_id: i32) -> Self {
Self {
state: AuthState::WaitTdlibParameters,
client_id,
}
}
pub fn is_authenticated(&self) -> bool {
self.state == AuthState::Ready
}
/// Обработать обновление авторизации
pub fn handle_auth_update(&mut self, update: &Update) {
if let Update::AuthorizationState(auth_update) = update {
self.state = match &auth_update.authorization_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,
_ => return,
};
}
}
/// Отправить номер телефона
pub async fn send_phone_number(&self, phone: String) -> Result<(), String> {
functions::set_authentication_phone_number(phone, None, self.client_id)
.await
.map(|_| ())
.map_err(|e| format!("Ошибка отправки номера: {:?}", e))
}
/// Отправить код подтверждения
pub async fn send_code(&self, code: String) -> Result<(), String> {
functions::check_authentication_code(code, self.client_id)
.await
.map(|_| ())
.map_err(|e| format!("Ошибка проверки кода: {:?}", e))
}
/// Отправить пароль 2FA
pub async fn send_password(&self, password: String) -> Result<(), String> {
functions::check_authentication_password(password, self.client_id)
.await
.map(|_| ())
.map_err(|e| format!("Ошибка проверки пароля: {:?}", e))
}
}

211
src/tdlib/chats.rs Normal file
View File

@@ -0,0 +1,211 @@
use crate::constants::TDLIB_CHAT_LIMIT;
use std::time::Instant;
use tdlib_rs::enums::{ChatAction, ChatList, ChatType};
use tdlib_rs::functions;
use super::types::{ChatInfo, FolderInfo, MessageInfo, ProfileInfo};
/// Менеджер чатов
pub struct ChatManager {
pub chats: Vec<ChatInfo>,
pub folders: Vec<FolderInfo>,
pub main_chat_list_position: i32,
/// Typing status для текущего чата: (user_id, action_text, timestamp)
pub typing_status: Option<(i64, String, Instant)>,
client_id: i32,
}
impl ChatManager {
pub fn new(client_id: i32) -> Self {
Self {
chats: Vec::new(),
folders: Vec::new(),
main_chat_list_position: 0,
typing_status: None,
client_id,
}
}
/// Загрузить чаты из основного списка
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 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 get_profile_info(&self, chat_id: i64) -> Result<ProfileInfo, String> {
// Получаем основную информацию о чате
let chat_result = functions::get_chat(chat_id, self.client_id).await;
let chat_enum = match chat_result {
Ok(c) => c,
Err(e) => return Err(format!("Ошибка получения чата: {:?}", e)),
};
let chat = match chat_enum {
tdlib_rs::enums::Chat::Chat(c) => c,
_ => return Err("Неожиданный тип чата".to_string()),
};
let chat_type_str = match &chat.r#type {
ChatType::Private(_) => "Личный чат",
ChatType::Supergroup(sg) => {
if sg.is_channel {
"Канал"
} else {
"Группа"
}
}
ChatType::BasicGroup(_) => "Группа",
ChatType::Secret(_) => "Секретный чат",
};
let is_group = matches!(
&chat.r#type,
ChatType::Supergroup(_) | ChatType::BasicGroup(_)
);
// Для личных чатов получаем информацию о пользователе
let (bio, phone_number, username, online_status) = if let ChatType::Private(private_chat) =
&chat.r#type
{
match functions::get_user(private_chat.user_id, self.client_id).await {
Ok(tdlib_rs::enums::User::User(user)) => {
let bio_opt = if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) =
functions::get_user_full_info(private_chat.user_id, self.client_id).await
{
full_info.bio.map(|b| b.text)
} else {
None
};
let online_status_str = match user.status {
tdlib_rs::enums::UserStatus::Online(_) => Some("В сети".to_string()),
tdlib_rs::enums::UserStatus::Recently(_) => {
Some("Был(а) недавно".to_string())
}
tdlib_rs::enums::UserStatus::LastWeek(_) => {
Some("Был(а) на этой неделе".to_string())
}
tdlib_rs::enums::UserStatus::LastMonth(_) => {
Some("Был(а) в этом месяце".to_string())
}
tdlib_rs::enums::UserStatus::Offline(s) => {
// Форматируем время последнего визита
Some(format!("Был(а) в сети {}", s.was_online))
}
_ => None,
};
let username_opt = user
.usernames
.as_ref()
.map(|u| u.editable_username.clone());
(bio_opt, Some(user.phone_number.clone()), username_opt, online_status_str)
}
_ => (None, None, None, None),
}
} else {
(None, None, None, None)
};
// Для групп/каналов получаем полную информацию
let (member_count, description, invite_link) = if is_group {
if let ChatType::Supergroup(sg) = &chat.r#type {
match functions::get_supergroup_full_info(sg.supergroup_id, self.client_id).await {
Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) => {
let desc = if !full_info.description.is_empty() {
Some(full_info.description.clone())
} else {
None
};
let link = full_info.invite_link.as_ref().map(|l| l.invite_link.clone());
(Some(full_info.member_count), desc, link)
}
_ => (None, None, None),
}
} else if let ChatType::BasicGroup(bg) = &chat.r#type {
match functions::get_basic_group_full_info(bg.basic_group_id, self.client_id).await
{
Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) => {
let desc = if !full_info.description.is_empty() {
Some(full_info.description.clone())
} else {
None
};
let link = full_info.invite_link.map(|l| l.invite_link);
(Some(full_info.members.len() as i32), desc, link)
}
Err(_) => (None, None, None),
}
} else {
(None, None, None)
}
} else {
(None, None, None)
};
Ok(ProfileInfo {
chat_id,
title: chat.title,
username,
bio,
phone_number,
chat_type: chat_type_str.to_string(),
member_count,
description,
invite_link,
is_group,
online_status,
})
}
/// Отправить typing action
pub async fn send_chat_action(&self, chat_id: i64, action: ChatAction) {
let _ = functions::send_chat_action(chat_id, 0, Some(action), self.client_id).await;
}
/// Очистить устаревший typing status (вызывать периодически)
pub fn clear_stale_typing_status(&mut self) -> bool {
if let Some((_, _, timestamp)) = self.typing_status {
if timestamp.elapsed().as_secs() > 5 {
self.typing_status = None;
return true;
}
}
false
}
/// Получить текст typing индикатора
pub fn get_typing_text(&self) -> Option<String> {
self.typing_status
.as_ref()
.map(|(_, action, _)| action.clone())
}
}

File diff suppressed because it is too large Load Diff

2036
src/tdlib/client.rs.backup Normal file

File diff suppressed because it is too large Load Diff

2036
src/tdlib/client.rs.old Normal file

File diff suppressed because it is too large Load Diff

545
src/tdlib/messages.rs Normal file
View File

@@ -0,0 +1,545 @@
use crate::constants::{MAX_MESSAGES_IN_CHAT, TDLIB_MESSAGE_LIMIT};
use tdlib_rs::enums::{ChatAction, InputMessageContent, InputMessageReplyTo, MessageContent, MessageSender, SearchMessagesFilter, TextParseMode};
use tdlib_rs::functions;
use tdlib_rs::types::{Chat as TdChat, FormattedText, InputMessageReplyToMessage, InputMessageText, Message as TdMessage, TextEntity, TextParseModeMarkdown};
use super::types::{ForwardInfo, MessageInfo, ReactionInfo, ReplyInfo};
/// Менеджер сообщений
pub struct MessageManager {
pub current_chat_messages: Vec<MessageInfo>,
pub current_chat_id: Option<i64>,
pub current_pinned_message: Option<MessageInfo>,
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids)
pub pending_view_messages: Vec<(i64, Vec<i64>)>,
client_id: i32,
}
impl MessageManager {
pub fn new(client_id: i32) -> Self {
Self {
current_chat_messages: Vec::new(),
current_chat_id: None,
current_pinned_message: None,
pending_view_messages: Vec::new(),
client_id,
}
}
/// Добавить сообщение в список текущего чата
pub fn push_message(&mut self, msg: MessageInfo) {
self.current_chat_messages.insert(0, msg);
// Ограничиваем размер списка
if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT {
self.current_chat_messages.truncate(MAX_MESSAGES_IN_CHAT);
}
}
/// Получить историю чата
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 result = functions::get_chat_history(
chat_id,
0, // from_message_id
0, // offset
limit,
false,
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::Messages::Messages(messages_obj)) => {
let mut messages = Vec::new();
for msg_opt in messages_obj.messages.iter().rev() {
if let Some(msg) = msg_opt {
if let Some(info) = self.convert_message(msg).await {
messages.push(info);
}
}
}
Ok(messages)
}
Ok(_) => Err("Неожиданный тип сообщений".to_string()),
Err(e) => Err(format!("Ошибка загрузки истории: {:?}", e)),
}
}
/// Загрузить более старые сообщения
pub async fn load_older_messages(
&mut self,
chat_id: i64,
from_message_id: i64,
) -> Result<Vec<MessageInfo>, String> {
let result = functions::get_chat_history(
chat_id,
from_message_id,
0, // offset
TDLIB_MESSAGE_LIMIT,
false,
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::Messages::Messages(messages_obj)) => {
let mut messages = Vec::new();
for msg_opt in messages_obj.messages.iter().rev() {
if let Some(msg) = msg_opt {
if let Some(info) = self.convert_message(msg).await {
messages.push(info);
}
}
}
Ok(messages)
}
Ok(_) => Err("Неожиданный тип сообщений".to_string()),
Err(e) => Err(format!("Ошибка загрузки старых сообщений: {:?}", e)),
}
}
/// Получить закреплённые сообщения
pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> {
let result = functions::search_chat_messages(
chat_id,
String::new(),
None,
0, // from_message_id
0, // offset
100, // limit
Some(SearchMessagesFilter::Pinned),
0, // message_thread_id
0, // saved_messages_topic_id
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(messages_obj)) => {
let mut pinned_messages = Vec::new();
for msg in messages_obj.messages.iter().rev() {
if let Some(info) = self.convert_message(msg).await {
pinned_messages.push(info);
}
}
Ok(pinned_messages)
}
Ok(_) => Err("Неожиданный тип результата поиска".to_string()),
Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)),
}
}
/// Загрузить текущее закреплённое сообщение
pub async fn load_current_pinned_message(&mut self, chat_id: i64) {
// TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat.
// Нужно использовать getChatPinnedMessage или альтернативный способ.
// Временно отключено.
let _ = chat_id;
self.current_pinned_message = None;
// match functions::get_chat(chat_id, self.client_id).await {
// Ok(tdlib_rs::enums::Chat::Chat(chat)) => {
// // chat.pinned_message_id больше не существует
// }
// _ => {}
// }
}
/// Поиск сообщений в чате
pub async fn search_messages(
&self,
chat_id: i64,
query: &str,
) -> Result<Vec<MessageInfo>, String> {
let result = functions::search_chat_messages(
chat_id,
query.to_string(),
None,
0, // from_message_id
0, // offset
100, // limit
None,
0, // message_thread_id
0, // saved_messages_topic_id
self.client_id,
)
.await;
match result {
Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(messages_obj)) => {
let mut search_results = Vec::new();
for msg in messages_obj.messages.iter().rev() {
if let Some(info) = self.convert_message(msg).await {
search_results.push(info);
}
}
Ok(search_results)
}
Ok(_) => Err("Неожиданный тип результата поиска".to_string()),
Err(e) => Err(format!("Ошибка поиска: {:?}", e)),
}
}
/// Отправить сообщение
pub async fn send_message(
&self,
chat_id: i64,
text: String,
reply_to_message_id: Option<i64>,
_reply_info: Option<ReplyInfo>,
) -> Result<MessageInfo, String> {
// Парсим 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(_) => FormattedText {
text: text.clone(),
entities: vec![],
},
};
let content = InputMessageContent::InputMessageText(InputMessageText {
text: formatted_text,
link_preview_options: None,
clear_draft: true,
});
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)) => self
.convert_message(&msg)
.await
.ok_or_else(|| "Не удалось конвертировать сообщение".to_string()),
Ok(_) => Err("Неожиданный тип сообщения".to_string()),
Err(e) => Err(format!("Ошибка отправки сообщения: {:?}", e)),
}
}
/// Редактировать сообщение
pub async fn edit_message(
&self,
chat_id: i64,
message_id: i64,
text: String,
) -> Result<MessageInfo, String> {
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(_) => 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)) => self
.convert_message(&msg)
.await
.ok_or_else(|| "Не удалось конвертировать отредактированное сообщение".to_string()),
Ok(_) => Err("Неожиданный тип сообщения".to_string()),
Err(e) => Err(format!("Ошибка редактирования: {:?}", e)),
}
}
/// Удалить сообщения
pub async fn delete_messages(
&self,
chat_id: i64,
message_ids: Vec<i64>,
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<i64>,
) -> 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 set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
use tdlib_rs::types::DraftMessage;
let draft = if text.is_empty() {
None
} else {
Some(DraftMessage {
reply_to: None,
date: 0,
input_message_text: InputMessageContent::InputMessageText(InputMessageText {
text: FormattedText {
text: text.clone(),
entities: vec![],
},
link_preview_options: None,
clear_draft: false,
}),
})
};
let result = functions::set_chat_draft_message(chat_id, 0, draft, self.client_id).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка сохранения черновика: {:?}", e)),
}
}
/// Обработать очередь просмотра сообщений
pub async fn process_pending_view_messages(&mut self) {
if self.pending_view_messages.is_empty() {
return;
}
let batch = std::mem::take(&mut self.pending_view_messages);
for (chat_id, message_ids) in batch {
let _ = functions::view_messages(chat_id, message_ids, None, true, self.client_id).await;
}
}
/// Конвертировать TdMessage в MessageInfo
async fn convert_message(&self, msg: &TdMessage) -> Option<MessageInfo> {
let content_text = match &msg.content {
MessageContent::MessageText(t) => t.text.text.clone(),
MessageContent::MessagePhoto(p) => {
let caption_text = p.caption.text.clone();
if caption_text.is_empty() { "[Фото]".to_string() } else { caption_text }
}
MessageContent::MessageVideo(v) => {
let caption_text = v.caption.text.clone();
if caption_text.is_empty() { "[Видео]".to_string() } else { caption_text }
}
MessageContent::MessageDocument(d) => {
let caption_text = d.caption.text.clone();
if caption_text.is_empty() { format!("[Файл: {}]", d.document.file_name) } else { caption_text }
}
MessageContent::MessageSticker(s) => {
format!("[Стикер: {}]", s.sticker.emoji)
}
MessageContent::MessageAnimation(a) => {
let caption_text = a.caption.text.clone();
if caption_text.is_empty() { "[GIF]".to_string() } else { caption_text }
}
MessageContent::MessageVoiceNote(v) => {
let caption_text = v.caption.text.clone();
if caption_text.is_empty() { "[Голосовое]".to_string() } else { caption_text }
}
MessageContent::MessageAudio(a) => {
let caption_text = a.caption.text.clone();
if caption_text.is_empty() {
let title = a.audio.title.clone();
let performer = a.audio.performer.clone();
if !title.is_empty() || !performer.is_empty() {
format!("[Аудио: {} - {}]", performer, title)
} else {
"[Аудио]".to_string()
}
} else {
caption_text
}
}
_ => "[Неподдерживаемый тип сообщения]".to_string(),
};
let entities = if let MessageContent::MessageText(t) = &msg.content {
t.text.entities.clone()
} else {
vec![]
};
let sender_name = match &msg.sender_id {
MessageSender::User(user) => {
match functions::get_user(user.user_id, self.client_id).await {
Ok(tdlib_rs::enums::User::User(u)) => format!("{} {}", u.first_name, u.last_name).trim().to_string(),
_ => format!("User {}", user.user_id),
}
}
MessageSender::Chat(chat) => format!("Chat {}", chat.chat_id),
};
let forward_from = msg.forward_info.as_ref().and_then(|fi| {
if let tdlib_rs::enums::MessageOrigin::User(origin_user) = &fi.origin {
Some(ForwardInfo {
sender_name: format!("User {}", origin_user.sender_user_id),
date: fi.date,
})
} else {
None
}
});
let reply_to = if let Some(ref reply_to) = msg.reply_to {
if let tdlib_rs::enums::MessageReplyTo::Message(reply_msg) = reply_to {
// Здесь можно загрузить информацию об оригинальном сообщении
Some(ReplyInfo {
message_id: reply_msg.message_id,
sender_name: "Unknown".to_string(),
text: "...".to_string(),
})
} else {
None
}
} else {
None
};
let reactions: Vec<ReactionInfo> = msg
.interaction_info
.as_ref()
.and_then(|ii| ii.reactions.as_ref())
.map(|reactions| {
reactions
.reactions
.iter()
.filter_map(|r| {
if let tdlib_rs::enums::ReactionType::Emoji(emoji_type) = &r.r#type {
Some(ReactionInfo {
emoji: emoji_type.emoji.clone(),
count: r.total_count,
is_chosen: r.is_chosen,
})
} else {
None
}
})
.collect()
})
.unwrap_or_default();
Some(MessageInfo {
id: msg.id,
sender_name,
is_outgoing: msg.is_outgoing,
content: content_text,
entities,
date: msg.date,
edit_date: msg.edit_date,
is_read: !msg.contains_unread_mention,
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,
forward_from,
reactions,
})
}
/// Получить недостающую reply информацию для сообщений
pub async fn fetch_missing_reply_info(&mut self) {
// Collect message IDs that need to be fetched
let mut to_fetch = Vec::new();
for msg in &self.current_chat_messages {
if let Some(ref reply) = msg.reply_to {
if reply.sender_name == "Unknown" {
to_fetch.push(reply.message_id);
}
}
}
// Fetch missing messages
if let Some(chat_id) = self.current_chat_id {
for message_id in to_fetch {
if let Ok(original_msg_enum) =
functions::get_message(chat_id, message_id, self.client_id).await
{
if let tdlib_rs::enums::Message::Message(original_msg) = original_msg_enum {
if let Some(orig_info) = self.convert_message(&original_msg).await {
// Update the reply info
for msg in &mut self.current_chat_messages {
if let Some(ref mut reply) = msg.reply_to {
if reply.message_id == message_id {
reply.sender_name = orig_info.sender_name.clone();
reply.text = orig_info
.content
.chars()
.take(50)
.collect::<String>();
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,13 +1,19 @@
// Модули
pub mod auth;
pub mod chats;
pub mod client;
pub mod messages;
pub mod reactions;
pub mod types;
pub mod users;
pub use client::ChatInfo;
pub use client::FolderInfo;
pub use client::ForwardInfo;
pub use client::MessageInfo;
pub use client::NetworkState;
pub use client::ProfileInfo;
pub use client::ReactionInfo;
pub use client::ReplyInfo;
// Экспорт основных типов
pub use auth::AuthState;
pub use client::TdClient;
pub use client::UserOnlineStatus;
pub use types::{
ChatInfo, FolderInfo, ForwardInfo, MessageInfo, NetworkState, ProfileInfo, ReactionInfo,
ReplyInfo, UserOnlineStatus,
};
// Re-export ChatAction для удобства
pub use tdlib_rs::enums::ChatAction;

126
src/tdlib/reactions.rs Normal file
View File

@@ -0,0 +1,126 @@
use tdlib_rs::enums::ReactionType;
use tdlib_rs::functions;
use tdlib_rs::types::ReactionTypeEmoji;
/// Менеджер реакций на сообщения
pub struct ReactionManager {
client_id: i32,
}
impl ReactionManager {
pub fn new(client_id: i32) -> Self {
Self { client_id }
}
/// Получить доступные реакции для сообщения
pub async fn get_message_available_reactions(
&self,
chat_id: i64,
message_id: i64,
) -> Result<Vec<String>, String> {
// Получаем сообщение
let msg_result = functions::get_message(chat_id, message_id, self.client_id).await;
let msg = match msg_result {
Ok(m) => m,
Err(e) => return Err(format!("Ошибка получения сообщения: {:?}", e)),
};
// Получаем доступные реакции для чата
let reactions_result = functions::get_message_available_reactions(
chat_id,
message_id,
10, // row_size
self.client_id,
)
.await;
match reactions_result {
Ok(_available) => {
// TODO: В tdlib-rs 1.8.29 структура AvailableReactions изменилась
// Временно используем fallback на стандартные реакции
let emojis: Vec<String> = Vec::new();
// let emojis: Vec<String> = if let tdlib_rs::enums::AvailableReactions::AvailableReactions(ar) = available {
// ar.top_reactions.iter().filter_map(...).collect()
// } else {
// Vec::new()
// };
if emojis.is_empty() {
// Фолбек на стандартные реакции
Ok(vec![
"👍".to_string(),
"👎".to_string(),
"❤️".to_string(),
"🔥".to_string(),
"😊".to_string(),
"😢".to_string(),
"😮".to_string(),
"🎉".to_string(),
"🤔".to_string(),
"😡".to_string(),
"😎".to_string(),
"🤝".to_string(),
])
} else {
Ok(emojis)
}
}
Err(_) => {
// В случае ошибки возвращаем стандартный набор
Ok(vec![
"👍".to_string(),
"👎".to_string(),
"❤️".to_string(),
"🔥".to_string(),
"😊".to_string(),
"😢".to_string(),
"😮".to_string(),
"🎉".to_string(),
"🤔".to_string(),
"😡".to_string(),
"😎".to_string(),
"🤝".to_string(),
])
}
}
}
/// Переключить реакцию на сообщение
pub async fn toggle_reaction(
&self,
chat_id: i64,
message_id: i64,
emoji: String,
) -> Result<(), String> {
let reaction = ReactionType::Emoji(ReactionTypeEmoji { emoji });
let result = functions::add_message_reaction(
chat_id,
message_id,
reaction.clone(),
false, // is_big
false, // update_recent_reactions
self.client_id,
)
.await;
match result {
Ok(_) => Ok(()),
Err(_) => {
// Если добавление не удалось, пытаемся удалить
let remove_result = functions::remove_message_reaction(
chat_id,
message_id,
reaction,
self.client_id,
)
.await;
match remove_result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка переключения реакции: {:?}", e)),
}
}
}
}
}

136
src/tdlib/types.rs Normal file
View File

@@ -0,0 +1,136 @@
use tdlib_rs::types::TextEntity;
#[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 unread_mention_count: i32,
pub is_pinned: bool,
pub order: i64,
/// ID последнего прочитанного исходящего сообщения (для галочек)
pub last_read_outbox_message_id: i64,
/// ID папок, в которых находится чат
pub folder_ids: Vec<i32>,
/// Чат замьючен (уведомления отключены)
pub is_muted: bool,
/// Черновик сообщения
pub draft_text: Option<String>,
}
/// Информация о сообщении, на которое отвечают
#[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<TextEntity>,
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<ReplyInfo>,
/// Информация о forward (если сообщение переслано)
pub forward_from: Option<ForwardInfo>,
/// Реакции на сообщение
pub reactions: Vec<ReactionInfo>,
}
#[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<String>,
pub bio: Option<String>,
pub phone_number: Option<String>,
pub chat_type: String, // "Личный чат", "Группа", "Канал"
pub member_count: Option<i32>,
pub description: Option<String>,
pub invite_link: Option<String>,
pub is_group: bool,
pub online_status: Option<String>,
}
/// Состояние сетевого соединения
#[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),
}

205
src/tdlib/users.rs Normal file
View File

@@ -0,0 +1,205 @@
use crate::constants::{LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_USER_CACHE_SIZE};
use std::collections::HashMap;
use tdlib_rs::enums::{User, UserStatus};
use tdlib_rs::functions;
use super::types::UserOnlineStatus;
/// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка
pub struct LruCache<V> {
map: HashMap<i64, V>,
/// Порядок доступа: последний элемент — самый недавно использованный
order: Vec<i64>,
capacity: usize,
}
impl<V: Clone> LruCache<V> {
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()
}
}
/// Кеш пользователей и их данных
pub struct UserCache {
/// LRU-кэш usernames: user_id -> username
pub user_usernames: LruCache<String>,
/// LRU-кэш имён: user_id -> display_name (first_name + last_name)
pub user_names: LruCache<String>,
/// Связь chat_id -> user_id для приватных чатов
pub chat_user_ids: HashMap<i64, i64>,
/// Очередь user_id для загрузки имён
pub pending_user_ids: Vec<i64>,
/// LRU-кэш онлайн-статусов пользователей: user_id -> status
pub user_statuses: LruCache<UserOnlineStatus>,
client_id: i32,
}
impl UserCache {
pub fn new(client_id: i32) -> Self {
Self {
user_usernames: LruCache::new(MAX_USER_CACHE_SIZE),
user_names: LruCache::new(MAX_USER_CACHE_SIZE),
chat_user_ids: HashMap::with_capacity(MAX_CHAT_USER_IDS),
pending_user_ids: Vec::new(),
user_statuses: LruCache::new(MAX_USER_CACHE_SIZE),
client_id,
}
}
/// Получить username пользователя
pub fn get_username(&mut self, user_id: &i64) -> Option<&String> {
self.user_usernames.get(user_id)
}
/// Получить имя пользователя
pub fn get_name(&mut self, user_id: &i64) -> Option<&String> {
self.user_names.get(user_id)
}
/// Получить user_id по chat_id
pub fn get_user_id_by_chat(&self, chat_id: i64) -> Option<i64> {
self.chat_user_ids.get(&chat_id).copied()
}
/// Получить статус пользователя по chat_id
pub fn get_status_by_chat_id(&self, chat_id: i64) -> Option<&UserOnlineStatus> {
let user_id = self.chat_user_ids.get(&chat_id)?;
self.user_statuses.peek(user_id)
}
/// Обработать обновление пользователя
pub fn handle_user_update(&mut self, user_enum: &User) {
if let User::User(user) = user_enum {
let user_id = user.id;
// Сохраняем username
if let Some(username) = user.usernames.as_ref().map(|u| u.editable_username.clone()) {
self.user_usernames.insert(user_id, username);
}
// Сохраняем имя
let display_name = format!("{} {}", user.first_name, user.last_name).trim().to_string();
self.user_names.insert(user_id, display_name);
// Обновляем статус
self.update_status(user_id, &user.status);
}
}
/// Обработать обновление статуса пользователя
pub fn update_status(&mut self, user_id: i64, status: &UserStatus) {
let online_status = match status {
UserStatus::Online(_) => UserOnlineStatus::Online,
UserStatus::Recently(_) => UserOnlineStatus::Recently,
UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek,
UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth,
UserStatus::Offline(s) => UserOnlineStatus::Offline(s.was_online),
_ => return,
};
self.user_statuses.insert(user_id, online_status);
}
/// Сохранить связь chat_id -> user_id
pub fn register_private_chat(&mut self, chat_id: i64, user_id: i64) {
self.chat_user_ids.insert(chat_id, user_id);
}
/// Получить имя пользователя (асинхронно с загрузкой если нужно)
pub async fn get_user_name(&self, user_id: i64) -> String {
// Сначала пытаемся получить из кэша
if let Some(name) = self.user_names.peek(&user_id) {
return name.clone();
}
// Загружаем пользователя
match functions::get_user(user_id, self.client_id).await {
Ok(User::User(user)) => {
let name = format!("{} {}", user.first_name, user.last_name).trim().to_string();
name
}
_ => format!("User {}", user_id),
}
}
/// Обработать очередь отложенных user_ids (загрузка имён небольшими порциями)
pub async fn process_pending_user_ids(&mut self) {
if self.pending_user_ids.is_empty() {
return;
}
// Берём первые N user_ids для загрузки
let batch: Vec<i64> = self
.pending_user_ids
.drain(..self.pending_user_ids.len().min(LAZY_LOAD_USERS_PER_TICK))
.collect();
for user_id in batch {
if self.user_names.contains_key(&user_id) {
continue; // Уже в кэше
}
match functions::get_user(user_id, self.client_id).await {
Ok(user_enum) => {
self.handle_user_update(&user_enum);
}
Err(_) => {
// Если не удалось загрузить, сохраняем placeholder
self.user_names
.insert(user_id, format!("User {}", user_id));
}
}
}
}
}