From 6b27cbece9d189fc00eb845b074ad773b2966a50 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Sun, 17 May 2026 18:25:18 +0300 Subject: [PATCH] Encapsulate TDLib state mutations --- docs/REFACTOR_PLAN.md | 10 +-- src/tdlib/chat_helpers.rs | 91 ++++++++------------ src/tdlib/client.rs | 153 +++++++++++++++++++++++++++------ src/tdlib/client_impl.rs | 12 +-- src/tdlib/message_converter.rs | 40 ++++----- src/tdlib/update_handlers.rs | 78 +++++++++-------- 6 files changed, 237 insertions(+), 147 deletions(-) diff --git a/docs/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md index 095f264..1868f71 100644 --- a/docs/REFACTOR_PLAN.md +++ b/docs/REFACTOR_PLAN.md @@ -104,11 +104,11 @@ rg -n "current_chat_messages_mut|chats_mut|folders_mut|pending_user_ids_mut|user Steps: -- [ ] Add focused methods on `TdClient` for common mutations: update chat, update message by id, queue pending user, update user cache, update folders. -- [ ] Replace raw `*_mut()` usage in helper/update modules with those methods. -- [ ] Keep raw mutable access private to `TdClient` implementation where it is still needed. -- [ ] Add or update tests around message updates, user-cache updates, and chat-list updates. -- [ ] Run `cargo test --all-features`. +- [x] Add focused methods on `TdClient` for common mutations: update chat, update message by id, queue pending user, update user cache, update folders. +- [x] Replace raw `*_mut()` usage in helper/update modules with those methods. +- [x] Keep raw mutable access private to `TdClient` implementation where it is still needed. +- [x] Add or update tests around message updates, user-cache updates, and chat-list updates. +- [x] Run `cargo test --all-features`. Acceptance criteria: diff --git a/src/tdlib/chat_helpers.rs b/src/tdlib/chat_helpers.rs index b022ee7..09fbe55 100644 --- a/src/tdlib/chat_helpers.rs +++ b/src/tdlib/chat_helpers.rs @@ -10,19 +10,12 @@ use tdlib_rs::enums::{Chat as TdChat, ChatList, ChatType}; use super::client::TdClient; use super::types::ChatInfo; -/// Находит мутабельную ссылку на чат по ID. -pub fn find_chat_mut(client: &mut TdClient, chat_id: ChatId) -> Option<&mut ChatInfo> { - client.chats_mut().iter_mut().find(|c| c.id == chat_id) -} - /// Обновляет поле чата, если чат найден. pub fn update_chat(client: &mut TdClient, chat_id: ChatId, updater: F) where F: FnOnce(&mut ChatInfo), { - if let Some(chat) = find_chat_mut(client, chat_id) { - updater(chat); - } + client.update_chat(chat_id, updater); } /// Добавляет новый чат или обновляет существующий @@ -33,9 +26,7 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) { // Пропускаем удалённые аккаунты if td_chat.title == "Deleted Account" || td_chat.title.is_empty() { // Удаляем из списка если уже был добавлен - client - .chats_mut() - .retain(|c| c.id != ChatId::new(td_chat.id)); + client.remove_chat(ChatId::new(td_chat.id)); return; } @@ -61,22 +52,23 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) { ChatType::Private(private) => { // Ограничиваем размер chat_user_ids let chat_id = ChatId::new(td_chat.id); - if client.user_cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS - && !client.user_cache.chat_user_ids.contains_key(&chat_id) - { - // Удаляем случайную запись (первую найденную) - if let Some(&key) = client.user_cache.chat_user_ids.keys().next() { - client.user_cache.chat_user_ids.remove(&key); - } - } let user_id = UserId::new(private.user_id); - client.user_cache.chat_user_ids.insert(chat_id, user_id); - // Проверяем, есть ли уже username в кэше (peek не обновляет LRU) - client - .user_cache - .user_usernames - .peek(&user_id) - .map(|u| format!("@{}", u)) + client.update_user_cache(|cache| { + if cache.chat_user_ids.len() >= MAX_CHAT_USER_IDS + && !cache.chat_user_ids.contains_key(&chat_id) + { + // Удаляем случайную запись (первую найденную) + if let Some(&key) = cache.chat_user_ids.keys().next() { + cache.chat_user_ids.remove(&key); + } + } + cache.chat_user_ids.insert(chat_id, user_id); + // Проверяем, есть ли уже username в кэше (peek не обновляет LRU) + cache + .user_usernames + .peek(&user_id) + .map(|u| format!("@{}", u)) + }) } _ => None, }; @@ -110,44 +102,35 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) { draft_text: None, }; - if let Some(existing) = find_chat_mut(client, 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; + let chat_info_for_update = chat_info.clone(); + let updated_existing = client.update_chat(ChatId::new(td_chat.id), |existing| { + existing.title = chat_info_for_update.title; + existing.last_message = chat_info_for_update.last_message; + existing.last_message_date = chat_info_for_update.last_message_date; + existing.unread_count = chat_info_for_update.unread_count; + existing.unread_mention_count = chat_info_for_update.unread_mention_count; + existing.last_read_outbox_message_id = chat_info_for_update.last_read_outbox_message_id; + existing.folder_ids = chat_info_for_update.folder_ids; + existing.is_muted = chat_info_for_update.is_muted; // Обновляем username если он появился - if let Some(username) = chat_info.username { + if let Some(username) = chat_info_for_update.username { existing.username = Some(username); } // Обновляем позицию только если она пришла if main_position.is_some() { - existing.is_pinned = chat_info.is_pinned; - existing.order = chat_info.order; + existing.is_pinned = chat_info_for_update.is_pinned; + existing.order = chat_info_for_update.order; } - } else { - client.chats_mut().push(chat_info); + }); + + if !updated_existing { + client.push_chat(chat_info); // Ограничиваем количество чатов - if client.chats_mut().len() > MAX_CHATS { - // Удаляем чат с наименьшим order (наименее активный) - let Some(min_idx) = client - .chats() - .iter() - .enumerate() - .min_by_key(|(_, c)| c.order) - .map(|(i, _)| i) - else { - return; // Нет чатов для удаления (не должно произойти) - }; - client.chats_mut().remove(min_idx); - } + client.trim_chats_to_max_by_order(MAX_CHATS); } // Сортируем чаты по order (TDLib order учитывает pinned и время) - client.chats_mut().sort_by(|a, b| b.order.cmp(&a.order)); + client.sort_chats_by_order(); } diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 98a39fa..94d48f0 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -105,7 +105,8 @@ impl TdClient { self.notification_manager.set_enabled(config.enabled); self.notification_manager .set_only_mentions(config.only_mentions); - self.notification_manager.set_show_preview(config.show_preview); + self.notification_manager + .set_show_preview(config.show_preview); self.notification_manager.set_timeout(config.timeout_ms); self.notification_manager .set_urgency(config.urgency.clone()); @@ -433,24 +434,117 @@ impl TdClient { &self.chat_manager.chats } - pub fn chats_mut(&mut self) -> &mut Vec { - &mut self.chat_manager.chats + pub fn update_chats(&mut self, updater: F) -> R + where + F: FnOnce(&mut Vec) -> R, + { + updater(&mut self.chat_manager.chats) + } + + pub fn update_chat(&mut self, chat_id: ChatId, updater: F) -> bool + where + F: FnOnce(&mut ChatInfo), + { + let Some(chat) = self.chat_manager.chats.iter_mut().find(|c| c.id == chat_id) else { + return false; + }; + + updater(chat); + true + } + + pub fn remove_chat(&mut self, chat_id: ChatId) { + self.chat_manager.chats.retain(|c| c.id != chat_id); + } + + pub fn push_chat(&mut self, chat: ChatInfo) { + self.chat_manager.chats.push(chat); + } + + pub fn trim_chats_to_max_by_order(&mut self, max_chats: usize) { + if self.chat_manager.chats.len() <= max_chats { + return; + } + + let Some(min_idx) = self + .chat_manager + .chats + .iter() + .enumerate() + .min_by_key(|(_, chat)| chat.order) + .map(|(idx, _)| idx) + else { + return; + }; + + self.chat_manager.chats.remove(min_idx); + } + + pub fn sort_chats_by_order(&mut self) { + self.chat_manager + .chats + .sort_by(|a, b| b.order.cmp(&a.order)); } pub fn folders(&self) -> &[FolderInfo] { &self.chat_manager.folders } - pub fn folders_mut(&mut self) -> &mut Vec { - &mut self.chat_manager.folders + pub fn update_folders(&mut self, updater: F) -> R + where + F: FnOnce(&mut Vec) -> R, + { + updater(&mut self.chat_manager.folders) + } + + pub fn set_folders(&mut self, folders: Vec) { + self.chat_manager.folders = folders; } pub fn current_chat_messages(&self) -> &[MessageInfo] { &self.message_manager.current_chat_messages } - pub fn current_chat_messages_mut(&mut self) -> &mut Vec { - &mut self.message_manager.current_chat_messages + pub fn clear_current_chat_messages(&mut self) { + self.message_manager.current_chat_messages.clear(); + } + + pub fn set_current_chat_messages(&mut self, messages: Vec) { + self.message_manager.current_chat_messages = messages; + } + + pub fn update_current_chat_messages(&mut self, updater: F) -> R + where + F: FnOnce(&mut Vec) -> R, + { + updater(&mut self.message_manager.current_chat_messages) + } + + pub fn update_current_chat_message(&mut self, message_id: MessageId, updater: F) -> bool + where + F: FnOnce(&mut MessageInfo), + { + let Some(message) = self + .message_manager + .current_chat_messages + .iter_mut() + .find(|message| message.id() == message_id) + else { + return false; + }; + + updater(message); + true + } + + pub fn replace_current_chat_message( + &mut self, + message_id: MessageId, + new_message: MessageInfo, + ) -> bool { + self.update_current_chat_message(message_id, |message| { + *message = new_message; + }) } pub fn current_chat_id(&self) -> Option { @@ -498,8 +592,10 @@ impl TdClient { &self.user_cache.pending_user_ids } - pub fn pending_user_ids_mut(&mut self) -> &mut Vec { - &mut self.user_cache.pending_user_ids + pub fn queue_pending_user_id(&mut self, user_id: crate::types::UserId) { + if !self.user_cache.pending_user_ids.contains(&user_id) { + self.user_cache.pending_user_ids.push(user_id); + } } pub fn main_chat_list_position(&self) -> i32 { @@ -515,8 +611,11 @@ impl TdClient { &self.user_cache } - pub fn user_cache_mut(&mut self) -> &mut UserCache { - &mut self.user_cache + pub fn update_user_cache(&mut self, updater: F) -> R + where + F: FnOnce(&mut UserCache) -> R, + { + updater(&mut self.user_cache) } // ==================== Helper методы для упрощения обработки updates ==================== @@ -558,7 +657,7 @@ impl TdClient { } // Пересортируем по order - self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order)); + self.sort_chats_by_order(); } Update::ChatReadInbox(update) => { crate::tdlib::chat_helpers::update_chat( @@ -600,11 +699,13 @@ impl TdClient { ); // Если это текущий открытый чат — обновляем 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; + self.update_current_chat_messages(|messages| { + for msg in messages { + if msg.is_outgoing() && msg.id() <= last_read_msg_id { + msg.state.is_read = true; + } } - } + }); } } Update::ChatPosition(update) => { @@ -618,11 +719,13 @@ impl TdClient { } Update::ChatFolders(update) => { // Обновляем список папок - *self.folders_mut() = update - .chat_folders - .into_iter() - .map(|f| FolderInfo { id: f.id, name: f.title }) - .collect(); + self.set_folders( + 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) => { @@ -635,9 +738,11 @@ impl TdClient { UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth, UserStatus::Empty => UserOnlineStatus::LongTimeAgo, }; - self.user_cache - .user_statuses - .insert(UserId::new(update.user_id), status); + self.update_user_cache(|cache| { + cache + .user_statuses + .insert(UserId::new(update.user_id), status); + }); } Update::ConnectionState(update) => { // Обновляем состояние сетевого соединения diff --git a/src/tdlib/client_impl.rs b/src/tdlib/client_impl.rs index 0511f8a..5a70043 100644 --- a/src/tdlib/client_impl.rs +++ b/src/tdlib/client_impl.rs @@ -70,14 +70,14 @@ impl ChatClient for TdClient { where F: FnOnce(&mut Vec), { - updater(self.chats_mut()); + TdClient::update_chats(self, updater); } fn update_folders(&mut self, updater: F) where F: FnOnce(&mut Vec), { - updater(self.folders_mut()); + TdClient::update_folders(self, updater); } } @@ -204,18 +204,18 @@ impl MessageClient for TdClient { } fn clear_current_chat_messages(&mut self) { - self.current_chat_messages_mut().clear() + TdClient::clear_current_chat_messages(self) } fn set_current_chat_messages(&mut self, messages: Vec) { - *self.current_chat_messages_mut() = messages; + TdClient::set_current_chat_messages(self, messages); } fn update_current_chat_messages(&mut self, updater: F) where F: FnOnce(&mut Vec), { - updater(self.current_chat_messages_mut()); + TdClient::update_current_chat_messages(self, updater); } fn set_current_chat_id(&mut self, chat_id: Option) { @@ -253,7 +253,7 @@ impl UserClient for TdClient { where F: FnOnce(&mut UserCache), { - updater(self.user_cache_mut()); + TdClient::update_user_cache(self, updater); } async fn process_pending_user_ids(&mut self) { diff --git a/src/tdlib/message_converter.rs b/src/tdlib/message_converter.rs index 5cdf92a..05d897a 100644 --- a/src/tdlib/message_converter.rs +++ b/src/tdlib/message_converter.rs @@ -23,9 +23,7 @@ pub fn convert_message(client: &mut TdClient, message: &TdMessage, chat_id: Chat .cloned() .unwrap_or_else(|| { // Добавляем в очередь для загрузки - if !client.pending_user_ids().contains(&user_id) { - client.pending_user_ids_mut().push(user_id); - } + client.queue_pending_user_id(user_id); format!("User_{}", user_id.as_i64()) }) } @@ -210,25 +208,27 @@ pub fn update_reply_info_from_loaded_messages(client: &mut TdClient) { .collect(); // Обновляем reply_to для сообщений с неполными данными - for msg in client.current_chat_messages_mut().iter_mut() { - let Some(ref mut reply) = msg.interactions.reply_to else { - continue; - }; + client.update_current_chat_messages(|messages| { + for msg in messages { + let Some(ref mut reply) = msg.interactions.reply_to else { + continue; + }; - // Если sender_name = "..." или text пустой — пробуем заполнить - if reply.sender_name != "..." && !reply.text.is_empty() { - continue; - } + // Если sender_name = "..." или text пустой — пробуем заполнить + if reply.sender_name != "..." && !reply.text.is_empty() { + continue; + } - let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) else { - continue; - }; + let Some((sender, content)) = msg_data.get(&reply.message_id.as_i64()) else { + continue; + }; - if reply.sender_name == "..." { - reply.sender_name = sender.clone(); + if reply.sender_name == "..." { + reply.sender_name = sender.clone(); + } + if reply.text.is_empty() { + reply.text = content.clone(); + } } - if reply.text.is_empty() { - reply.text = content.clone(); - } - } + }); } diff --git a/src/tdlib/update_handlers.rs b/src/tdlib/update_handlers.rs index 1c65e29..5b6ec3c 100644 --- a/src/tdlib/update_handlers.rs +++ b/src/tdlib/update_handlers.rs @@ -54,17 +54,19 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag Some(idx) => { // Сообщение уже есть - обновляем if is_incoming { - client.current_chat_messages_mut()[idx] = msg_info; + client.replace_current_chat_message(msg_id, msg_info); } else { // Для исходящих: обновляем can_be_edited и другие поля, // но сохраняем reply_to (добавленный при отправке) - let existing = &mut client.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; + client.update_current_chat_messages(|messages| { + let existing = &mut messages[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 => { @@ -122,7 +124,7 @@ pub fn handle_chat_position_update(client: &mut TdClient, update: UpdateChatPosi ChatList::Main => { if update.position.order == 0 { // Чат больше не в Main (перемещён в архив и т.д.) - client.chats_mut().retain(|c| c.id != chat_id); + client.remove_chat(chat_id); } else { // Обновляем позицию существующего чата crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| { @@ -131,7 +133,7 @@ pub fn handle_chat_position_update(client: &mut TdClient, update: UpdateChatPosi }); } // Пересортируем по order - client.chats_mut().sort_by(|a, b| b.order.cmp(&a.order)); + client.sort_chats_by_order(); } ChatList::Folder(folder) => { // Обновляем folder_ids для чата @@ -166,10 +168,10 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) { // Удаляем чаты с этим пользователем из списка let user_id = user.id; // Clone chat_user_ids to avoid borrow conflict - let chat_user_ids = client.user_cache.chat_user_ids.clone(); - client - .chats_mut() - .retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(user_id))); + let chat_user_ids = client.user_cache().chat_user_ids.clone(); + client.update_chats(|chats| { + chats.retain(|c| chat_user_ids.get(&c.id) != Some(&UserId::new(user_id))); + }); return; } @@ -179,10 +181,9 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) { } else { format!("{} {}", user.first_name, user.last_name) }; - client - .user_cache - .user_names - .insert(UserId::new(user.id), display_name); + client.update_user_cache(|cache| { + cache.user_names.insert(UserId::new(user.id), display_name); + }); // Сохраняем username если есть (с упрощённым извлечением через and_then) if let Some(username) = user @@ -190,17 +191,23 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) { .as_ref() .and_then(|u| u.active_usernames.first()) { - client - .user_cache - .user_usernames - .insert(UserId::new(user.id), username.to_string()); + let affected_chat_ids = client.update_user_cache(|cache| { + cache + .user_usernames + .insert(UserId::new(user.id), username.to_string()); + cache + .chat_user_ids + .iter() + .filter_map(|(&chat_id, &user_id)| { + (user_id == UserId::new(user.id)).then_some(chat_id) + }) + .collect::>() + }); // Обновляем username в чатах, связанных с этим пользователем - for (&chat_id, &user_id) in &client.user_cache.chat_user_ids.clone() { - if user_id == UserId::new(user.id) { - crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| { - chat.username = Some(format!("@{}", username)); - }); - } + for chat_id in affected_chat_ids { + crate::tdlib::chat_helpers::update_chat(client, chat_id, |chat| { + chat.username = Some(format!("@{}", username)); + }); } } // LRU-кэш автоматически удаляет старые записи при вставке @@ -218,16 +225,8 @@ pub fn handle_message_interaction_info_update( return; } - let Some(msg) = client - .current_chat_messages_mut() - .iter_mut() - .find(|m| m.id() == MessageId::new(update.message_id)) - else { - return; - }; - // Извлекаем реакции из interaction_info - msg.interactions.reactions = update + let reactions = update .interaction_info .as_ref() .and_then(|info| info.reactions.as_ref()) @@ -250,6 +249,9 @@ pub fn handle_message_interaction_info_update( .collect() }) .unwrap_or_default(); + client.update_current_chat_message(MessageId::new(update.message_id), |msg| { + msg.interactions.reactions = reactions; + }); } /// Обрабатывает Update::MessageSendSucceeded - успешная отправка сообщения. @@ -291,7 +293,7 @@ pub fn handle_message_send_succeeded_update( } // Заменяем старое сообщение на новое - client.current_chat_messages_mut()[idx] = new_msg; + client.replace_current_chat_message(old_id, new_msg); } /// Обрабатывает Update::ChatDraftMessage - обновление черновика сообщения в чате.