Fixes "Message not found" error when editing immediately after sending. **Problem**: When sending a message, TDLib may return a temporary ID, then send UpdateMessageSendSucceeded with the real server ID. We weren't handling this update, so the cache kept the old ID while the server had a different one, causing "Message not found" errors during edits. **Solution**: 1. Added UpdateMessageSendSucceeded handler (client.rs:801-830) - Finds message with temporary ID - Replaces it with new message containing real server ID - Preserves reply_info if present 2. Added validation before editing (main_input.rs:574-589) - Checks message exists in cache - Better error messages with chat_id and message_id 3. Added positive ID check in start_editing_selected (mod.rs:240) - Blocks editing messages with temporary IDs (negative) **Test**: - Added test_edit_immediately_after_send (edit_message.rs:156-181) - Verifies editing works right after send_message - All 22 edit_message tests pass Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1207 lines
50 KiB
Rust
1207 lines
50 KiB
Rust
use crate::types::{ChatId, MessageId, UserId};
|
||
use std::env;
|
||
use std::time::Instant;
|
||
use tdlib_rs::enums::{
|
||
AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState,
|
||
MessageSender, Update, UserStatus,
|
||
Chat as TdChat
|
||
};
|
||
use tdlib_rs::types::{Message as TdMessage};
|
||
use tdlib_rs::functions;
|
||
|
||
use crate::constants::{MAX_CHAT_USER_IDS, MAX_CHATS};
|
||
|
||
use super::auth::{AuthManager, AuthState};
|
||
use super::chats::ChatManager;
|
||
use super::messages::MessageManager;
|
||
use super::reactions::ReactionManager;
|
||
use super::types::{ChatInfo, FolderInfo, ForwardInfo, MessageInfo, NetworkState, ProfileInfo, ReactionInfo, ReplyInfo, UserOnlineStatus};
|
||
use super::users::UserCache;
|
||
|
||
/// 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();
|
||
///
|
||
/// // 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,
|
||
client_id: i32,
|
||
|
||
// Менеджеры (делегируем им функциональность)
|
||
pub auth: AuthManager,
|
||
pub chat_manager: ChatManager,
|
||
pub message_manager: MessageManager,
|
||
pub user_cache: UserCache,
|
||
pub reaction_manager: ReactionManager,
|
||
|
||
// Состояние сети
|
||
pub network_state: NetworkState,
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
impl TdClient {
|
||
/// Creates a new TDLib client instance.
|
||
///
|
||
/// Reads API credentials from environment variables `API_ID` and `API_HASH`.
|
||
/// Initializes all managers and sets initial network state to Connecting.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// A new `TdClient` instance ready for authentication.
|
||
pub fn new() -> Self {
|
||
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();
|
||
let client_id = tdlib_rs::create_client();
|
||
|
||
Self {
|
||
api_id,
|
||
api_hash,
|
||
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),
|
||
network_state: NetworkState::Connecting,
|
||
}
|
||
}
|
||
|
||
// Делегирование к auth
|
||
|
||
/// Checks if the user is authenticated.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// `true` if authentication is complete, `false` otherwise.
|
||
pub fn is_authenticated(&self) -> bool {
|
||
self.auth.is_authenticated()
|
||
}
|
||
|
||
/// 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 get_typing_text(&self) -> Option<String> {
|
||
self.chat_manager.get_typing_text()
|
||
}
|
||
|
||
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 async fn get_user_name(&self, user_id: UserId) -> String {
|
||
self.user_cache.get_user_name(user_id).await
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// Вспомогательные методы
|
||
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),
|
||
Ok(_) => Err("Неожиданный тип пользователя".to_string()),
|
||
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
|
||
}
|
||
|
||
/// Инициализация 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) => {
|
||
// new_chat.chat is already a Chat struct, wrap it in TdChat enum
|
||
let td_chat = TdChat::Chat(new_chat.chat.clone());
|
||
self.add_or_update_chat(&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();
|
||
|
||
if let Some(chat) = self.chats_mut().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_mut().iter_mut().find(|c| c.id == chat_id) {
|
||
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) => {
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
|
||
chat.unread_count = update.unread_count;
|
||
}
|
||
}
|
||
Update::ChatUnreadMentionCount(update) => {
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
|
||
chat.unread_mention_count = update.unread_mention_count;
|
||
}
|
||
}
|
||
Update::ChatNotificationSettings(update) => {
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
|
||
// 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);
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
|
||
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) => {
|
||
// Обновляем позицию чата или удаляем его из списка
|
||
let chat_id = ChatId::new(update.chat_id);
|
||
match &update.position.list {
|
||
ChatList::Main => {
|
||
if update.position.order == 0 {
|
||
// Чат больше не в Main (перемещён в архив и т.д.)
|
||
self.chats_mut().retain(|c| c.id != chat_id);
|
||
} else if let Some(chat) =
|
||
self.chats_mut().iter_mut().find(|c| c.id == chat_id)
|
||
{
|
||
// Обновляем позицию существующего чата
|
||
chat.order = update.position.order;
|
||
chat.is_pinned = update.position.is_pinned;
|
||
}
|
||
// Пересортируем по order
|
||
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
|
||
}
|
||
ChatList::Folder(folder) => {
|
||
// Обновляем folder_ids для чата
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == 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 = ChatId::new(new_msg.message.chat_id);
|
||
if Some(chat_id) == self.current_chat_id() {
|
||
let msg_info = self.convert_message(&new_msg.message, chat_id);
|
||
let msg_id = msg_info.id();
|
||
let is_incoming = !msg_info.is_outgoing();
|
||
|
||
// Проверяем, есть ли уже сообщение с таким id
|
||
let existing_idx = self
|
||
.current_chat_messages()
|
||
.iter()
|
||
.position(|m| m.id() == msg_info.id());
|
||
|
||
match existing_idx {
|
||
Some(idx) => {
|
||
// Сообщение уже есть - обновляем
|
||
if is_incoming {
|
||
self.current_chat_messages_mut()[idx] = msg_info;
|
||
} else {
|
||
// Для исходящих: обновляем can_be_edited и другие поля,
|
||
// но сохраняем reply_to (добавленный при отправке)
|
||
let existing = &mut self.current_chat_messages_mut()[idx];
|
||
existing.state.can_be_edited = msg_info.state.can_be_edited;
|
||
existing.state.can_be_deleted_only_for_self =
|
||
msg_info.state.can_be_deleted_only_for_self;
|
||
existing.state.can_be_deleted_for_all_users =
|
||
msg_info.state.can_be_deleted_for_all_users;
|
||
existing.state.is_read = msg_info.state.is_read;
|
||
}
|
||
}
|
||
None => {
|
||
// Нового сообщения нет - добавляем
|
||
self.push_message(msg_info.clone());
|
||
// Если это входящее сообщение — добавляем в очередь для отметки как прочитанное
|
||
if is_incoming {
|
||
self.pending_view_messages_mut().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;
|
||
// Clone chat_user_ids to avoid borrow conflict
|
||
let chat_user_ids = self.user_cache.chat_user_ids.clone();
|
||
self.chats_mut()
|
||
.retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(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_cache.user_names.insert(UserId::new(user.id), display_name);
|
||
|
||
// Сохраняем username если есть
|
||
if let Some(usernames) = user.usernames {
|
||
if let Some(username) = usernames.active_usernames.first() {
|
||
self.user_cache.user_usernames.insert(UserId::new(user.id), username.clone());
|
||
// Обновляем username в чатах, связанных с этим пользователем
|
||
for (&chat_id, &user_id) in &self.user_cache.chat_user_ids.clone() {
|
||
if user_id == UserId::new(user.id) {
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == chat_id)
|
||
{
|
||
chat.username = Some(format!("@{}", username));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// LRU-кэш автоматически удаляет старые записи при вставке
|
||
}
|
||
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) => {
|
||
// Обрабатываем только для текущего открытого чата
|
||
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
|
||
// Извлекаем user_id из sender_id
|
||
let user_id = match update.sender_id {
|
||
MessageSender::User(user) => Some(UserId::new(user.user_id)),
|
||
MessageSender::Chat(_) => None, // Игнорируем действия от имени чата
|
||
};
|
||
|
||
if let Some(user_id) = user_id {
|
||
// Определяем текст действия
|
||
let action_text = match update.action {
|
||
ChatAction::Typing => Some("печатает...".to_string()),
|
||
ChatAction::RecordingVideo => Some("записывает видео...".to_string()),
|
||
ChatAction::UploadingVideo(_) => {
|
||
Some("отправляет видео...".to_string())
|
||
}
|
||
ChatAction::RecordingVoiceNote => {
|
||
Some("записывает голосовое...".to_string())
|
||
}
|
||
ChatAction::UploadingVoiceNote(_) => {
|
||
Some("отправляет голосовое...".to_string())
|
||
}
|
||
ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()),
|
||
ChatAction::UploadingDocument(_) => {
|
||
Some("отправляет файл...".to_string())
|
||
}
|
||
ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()),
|
||
ChatAction::RecordingVideoNote => {
|
||
Some("записывает видеосообщение...".to_string())
|
||
}
|
||
ChatAction::UploadingVideoNote(_) => {
|
||
Some("отправляет видеосообщение...".to_string())
|
||
}
|
||
ChatAction::Cancel => None, // Отмена — сбрасываем статус
|
||
_ => None,
|
||
};
|
||
|
||
if let Some(text) = action_text {
|
||
self.set_typing_status(Some((user_id, text, Instant::now())));
|
||
} else {
|
||
// Cancel или неизвестное действие — сбрасываем
|
||
self.set_typing_status(None);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Update::ChatDraftMessage(update) => {
|
||
// Обновляем черновик в списке чатов
|
||
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(update.chat_id)) {
|
||
chat.draft_text = update.draft_message.as_ref().and_then(|draft| {
|
||
// Извлекаем текст из InputMessageText
|
||
if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) =
|
||
&draft.input_message_text
|
||
{
|
||
Some(text_msg.text.text.clone())
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
}
|
||
}
|
||
Update::MessageInteractionInfo(update) => {
|
||
// Обновляем реакции в текущем открытом чате
|
||
if Some(ChatId::new(update.chat_id)) == self.current_chat_id() {
|
||
if let Some(msg) = self
|
||
.current_chat_messages_mut()
|
||
.iter_mut()
|
||
.find(|m| m.id() == MessageId::new(update.message_id))
|
||
{
|
||
// Извлекаем реакции из interaction_info
|
||
msg.interactions.reactions = update
|
||
.interaction_info
|
||
.as_ref()
|
||
.and_then(|info| info.reactions.as_ref())
|
||
.map(|reactions| {
|
||
reactions
|
||
.reactions
|
||
.iter()
|
||
.filter_map(|reaction| {
|
||
let emoji = match &reaction.r#type {
|
||
tdlib_rs::enums::ReactionType::Emoji(e) => {
|
||
e.emoji.clone()
|
||
}
|
||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => {
|
||
return None
|
||
}
|
||
};
|
||
|
||
Some(ReactionInfo {
|
||
emoji,
|
||
count: reaction.total_count,
|
||
is_chosen: reaction.is_chosen,
|
||
})
|
||
})
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
}
|
||
}
|
||
}
|
||
Update::MessageSendSucceeded(update) => {
|
||
// Сообщение успешно отправлено, заменяем временный ID на настоящий
|
||
let old_id = MessageId::new(update.old_message_id);
|
||
let chat_id = ChatId::new(update.message.chat_id);
|
||
|
||
// Обрабатываем только если это текущий открытый чат
|
||
if Some(chat_id) == self.current_chat_id() {
|
||
// Находим сообщение с временным ID
|
||
if let Some(idx) = self
|
||
.current_chat_messages()
|
||
.iter()
|
||
.position(|m| m.id() == old_id)
|
||
{
|
||
// Конвертируем новое сообщение
|
||
let mut new_msg = self.convert_message(&update.message, chat_id);
|
||
|
||
// Сохраняем reply_info из старого сообщения (если было)
|
||
let old_reply = self.current_chat_messages()[idx]
|
||
.interactions
|
||
.reply_to
|
||
.clone();
|
||
if let Some(reply) = old_reply {
|
||
new_msg.interactions.reply_to = Some(reply);
|
||
}
|
||
|
||
// Заменяем старое сообщение на новое
|
||
self.current_chat_messages_mut()[idx] = new_msg;
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
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_enum: &TdChat) {
|
||
// Pattern match to get inner Chat struct
|
||
let td_chat = match td_chat_enum {
|
||
TdChat::Chat(chat) => chat,
|
||
_ => return,
|
||
};
|
||
|
||
// Пропускаем удалённые аккаунты
|
||
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
|
||
// Удаляем из списка если уже был добавлен
|
||
self.chats_mut().retain(|c| c.id != ChatId::new(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| (Self::extract_message_text_static(m).0, m.date))
|
||
.unwrap_or_default();
|
||
|
||
// Извлекаем user_id для приватных чатов и сохраняем связь
|
||
let username = match &td_chat.r#type {
|
||
ChatType::Private(private) => {
|
||
// Ограничиваем размер chat_user_ids
|
||
let chat_id = ChatId::new(td_chat.id);
|
||
if self.user_cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS
|
||
&& !self.user_cache.chat_user_ids.contains_key(&chat_id)
|
||
{
|
||
// Удаляем случайную запись (первую найденную)
|
||
if let Some(&key) = self.user_cache.chat_user_ids.keys().next() {
|
||
self.user_cache.chat_user_ids.remove(&key);
|
||
}
|
||
}
|
||
let user_id = UserId::new(private.user_id);
|
||
self.user_cache.chat_user_ids.insert(chat_id, user_id);
|
||
// Проверяем, есть ли уже username в кэше (peek не обновляет LRU)
|
||
self.user_cache.user_usernames
|
||
.peek(&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();
|
||
|
||
// Проверяем mute статус
|
||
let is_muted = td_chat.notification_settings.mute_for > 0;
|
||
|
||
let chat_info = ChatInfo {
|
||
id: ChatId::new(td_chat.id),
|
||
title: td_chat.title.clone(),
|
||
username,
|
||
last_message,
|
||
last_message_date,
|
||
unread_count: td_chat.unread_count,
|
||
unread_mention_count: td_chat.unread_mention_count,
|
||
is_pinned,
|
||
order,
|
||
last_read_outbox_message_id: MessageId::new(td_chat.last_read_outbox_message_id),
|
||
folder_ids,
|
||
is_muted,
|
||
draft_text: None,
|
||
};
|
||
|
||
if let Some(existing) = self.chats_mut().iter_mut().find(|c| c.id == ChatId::new(td_chat.id)) {
|
||
existing.title = chat_info.title;
|
||
existing.last_message = chat_info.last_message;
|
||
existing.last_message_date = chat_info.last_message_date;
|
||
existing.unread_count = chat_info.unread_count;
|
||
existing.unread_mention_count = chat_info.unread_mention_count;
|
||
existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id;
|
||
existing.folder_ids = chat_info.folder_ids;
|
||
existing.is_muted = chat_info.is_muted;
|
||
// Обновляем username если он появился
|
||
if chat_info.username.is_some() {
|
||
existing.username = chat_info.username;
|
||
}
|
||
// Обновляем позицию только если она пришла
|
||
if main_position.is_some() {
|
||
existing.is_pinned = chat_info.is_pinned;
|
||
existing.order = chat_info.order;
|
||
}
|
||
} else {
|
||
self.chats_mut().push(chat_info);
|
||
// Ограничиваем количество чатов
|
||
if self.chats_mut().len() > MAX_CHATS {
|
||
// Удаляем чат с наименьшим order (наименее активный)
|
||
if let Some(min_idx) = self
|
||
.chats()
|
||
.iter()
|
||
.enumerate()
|
||
.min_by_key(|(_, c)| c.order)
|
||
.map(|(i, _)| i)
|
||
{
|
||
self.chats_mut().remove(min_idx);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Сортируем чаты по order (TDLib order учитывает pinned и время)
|
||
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
|
||
}
|
||
|
||
fn convert_message(&mut self, message: &TdMessage, chat_id: ChatId) -> MessageInfo {
|
||
let sender_name = match &message.sender_id {
|
||
tdlib_rs::enums::MessageSender::User(user) => {
|
||
// Пробуем получить имя из кеша (get обновляет LRU порядок)
|
||
let user_id = UserId::new(user.user_id);
|
||
if let Some(name) = self.user_cache.user_names.get(&user_id).cloned() {
|
||
name
|
||
} else {
|
||
// Добавляем в очередь для загрузки
|
||
if !self.pending_user_ids().contains(&user_id) {
|
||
self.pending_user_ids_mut().push(user_id);
|
||
}
|
||
format!("User_{}", user_id.as_i64())
|
||
}
|
||
}
|
||
tdlib_rs::enums::MessageSender::Chat(chat) => {
|
||
// Для чатов используем название чата
|
||
let sender_chat_id = ChatId::new(chat.chat_id);
|
||
self.chats()
|
||
.iter()
|
||
.find(|c| c.id == sender_chat_id)
|
||
.map(|c| c.title.clone())
|
||
.unwrap_or_else(|| format!("Chat_{}", sender_chat_id.as_i64()))
|
||
}
|
||
};
|
||
|
||
// Определяем, прочитано ли исходящее сообщение
|
||
let message_id = MessageId::new(message.id);
|
||
let is_read = if message.is_outgoing {
|
||
// Сообщение прочитано, если его ID <= last_read_outbox_message_id чата
|
||
self.chats()
|
||
.iter()
|
||
.find(|c| c.id == chat_id)
|
||
.map(|c| message_id <= c.last_read_outbox_message_id)
|
||
.unwrap_or(false)
|
||
} else {
|
||
true // Входящие сообщения не показывают галочки
|
||
};
|
||
|
||
let (content, entities) = Self::extract_message_text_static(message);
|
||
|
||
// Извлекаем информацию о reply
|
||
let reply_to = self.extract_reply_info(message);
|
||
|
||
// Извлекаем информацию о forward
|
||
let forward_from = self.extract_forward_info(message);
|
||
|
||
// Извлекаем реакции
|
||
let reactions = self.extract_reactions(message);
|
||
|
||
// Используем MessageBuilder для более читабельного создания
|
||
let mut builder = crate::tdlib::MessageBuilder::new(message_id)
|
||
.sender_name(sender_name)
|
||
.text(content)
|
||
.entities(entities)
|
||
.date(message.date)
|
||
.edit_date(message.edit_date);
|
||
|
||
// Применяем флаги
|
||
if message.is_outgoing {
|
||
builder = builder.outgoing();
|
||
}
|
||
if is_read {
|
||
builder = builder.read();
|
||
}
|
||
if message.can_be_edited {
|
||
builder = builder.editable();
|
||
}
|
||
if message.can_be_deleted_only_for_self {
|
||
builder = builder.deletable_for_self();
|
||
}
|
||
if message.can_be_deleted_for_all_users {
|
||
builder = builder.deletable_for_all();
|
||
}
|
||
|
||
// Добавляем опциональные данные
|
||
if let Some(reply) = reply_to {
|
||
builder = builder.reply_to(reply);
|
||
}
|
||
if let Some(forward) = forward_from {
|
||
builder = builder.forward_from(forward);
|
||
}
|
||
if !reactions.is_empty() {
|
||
builder = builder.reactions(reactions);
|
||
}
|
||
|
||
builder.build()
|
||
}
|
||
|
||
/// Извлекает информацию о reply из сообщения
|
||
fn extract_reply_info(&self, message: &TdMessage) -> Option<ReplyInfo> {
|
||
use tdlib_rs::enums::MessageReplyTo;
|
||
|
||
match &message.reply_to {
|
||
Some(MessageReplyTo::Message(reply)) => {
|
||
// Получаем имя отправителя из origin или ищем сообщение в текущем списке
|
||
let sender_name = if let Some(origin) = &reply.origin {
|
||
self.get_origin_sender_name(origin)
|
||
} else {
|
||
// Пробуем найти оригинальное сообщение в текущем списке
|
||
let reply_msg_id = MessageId::new(reply.message_id);
|
||
self.current_chat_messages()
|
||
.iter()
|
||
.find(|m| m.id() == reply_msg_id)
|
||
.map(|m| m.sender_name().to_string())
|
||
.unwrap_or_else(|| "...".to_string())
|
||
};
|
||
|
||
// Получаем текст из content или quote
|
||
let reply_msg_id = MessageId::new(reply.message_id);
|
||
let text = if let Some(quote) = &reply.quote {
|
||
quote.text.text.clone()
|
||
} else if let Some(content) = &reply.content {
|
||
Self::extract_content_text(content)
|
||
} else {
|
||
// Пробуем найти в текущих сообщениях
|
||
self.current_chat_messages()
|
||
.iter()
|
||
.find(|m| m.id() == reply_msg_id)
|
||
.map(|m| m.text().to_string())
|
||
.unwrap_or_default()
|
||
};
|
||
|
||
Some(ReplyInfo { message_id: reply_msg_id, sender_name, text })
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Извлекает информацию о forward из сообщения
|
||
fn extract_forward_info(&self, message: &TdMessage) -> Option<ForwardInfo> {
|
||
message.forward_info.as_ref().map(|info| {
|
||
let sender_name = self.get_origin_sender_name(&info.origin);
|
||
ForwardInfo { sender_name, date: info.date }
|
||
})
|
||
}
|
||
|
||
/// Извлекает информацию о реакциях из сообщения
|
||
fn extract_reactions(&self, message: &TdMessage) -> Vec<ReactionInfo> {
|
||
message
|
||
.interaction_info
|
||
.as_ref()
|
||
.and_then(|info| info.reactions.as_ref())
|
||
.map(|reactions| {
|
||
reactions
|
||
.reactions
|
||
.iter()
|
||
.filter_map(|reaction| {
|
||
// Извлекаем эмодзи из ReactionType
|
||
let emoji = match &reaction.r#type {
|
||
tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(),
|
||
tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, // Пока игнорируем custom emoji
|
||
};
|
||
|
||
Some(ReactionInfo {
|
||
emoji,
|
||
count: reaction.total_count,
|
||
is_chosen: reaction.is_chosen,
|
||
})
|
||
})
|
||
.collect()
|
||
})
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// Получает имя отправителя из MessageOrigin
|
||
fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String {
|
||
use tdlib_rs::enums::MessageOrigin;
|
||
match origin {
|
||
MessageOrigin::User(u) => self
|
||
.user_cache.user_names
|
||
.peek(&UserId::new(u.sender_user_id))
|
||
.cloned()
|
||
.unwrap_or_else(|| format!("User_{}", u.sender_user_id)),
|
||
MessageOrigin::Chat(c) => self
|
||
.chats()
|
||
.iter()
|
||
.find(|chat| chat.id == ChatId::new(c.sender_chat_id))
|
||
.map(|chat| chat.title.clone())
|
||
.unwrap_or_else(|| "Чат".to_string()),
|
||
MessageOrigin::HiddenUser(h) => h.sender_name.clone(),
|
||
MessageOrigin::Channel(c) => self
|
||
.chats()
|
||
.iter()
|
||
.find(|chat| chat.id == ChatId::new(c.chat_id))
|
||
.map(|chat| chat.title.clone())
|
||
.unwrap_or_else(|| "Канал".to_string()),
|
||
}
|
||
}
|
||
|
||
/// Обновляет reply info для сообщений, где данные не были загружены
|
||
/// Вызывается после загрузки истории, когда все сообщения уже в списке
|
||
fn update_reply_info_from_loaded_messages(&mut self) {
|
||
// Собираем данные для обновления (id -> (sender_name, content))
|
||
let msg_data: std::collections::HashMap<i64, (String, String)> = self
|
||
.current_chat_messages()
|
||
.iter()
|
||
.map(|m| (m.id().as_i64(), (m.sender_name().to_string(), m.text().to_string())))
|
||
.collect();
|
||
|
||
// Обновляем reply_to для сообщений с неполными данными
|
||
for msg in self.current_chat_messages_mut().iter_mut() {
|
||
if let Some(ref mut reply) = msg.interactions.reply_to {
|
||
// Если sender_name = "..." или text пустой — пробуем заполнить
|
||
if reply.sender_name == "..." || reply.text.is_empty() {
|
||
if let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) {
|
||
if reply.sender_name == "..." {
|
||
reply.sender_name = sender.clone();
|
||
}
|
||
if reply.text.is_empty() {
|
||
reply.text = content.clone();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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()),
|
||
}
|
||
}
|
||
|
||
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(),
|
||
}
|
||
}
|
||
}
|