refactor: implement newtype pattern for IDs (P2.4)

Добавлены типобезопасные обёртки ChatId, MessageId, UserId для предотвращения
смешивания разных типов идентификаторов на этапе компиляции.

Изменения:
- Создан src/types.rs с тремя newtype структурами
- Реализованы методы: new(), as_i64(), From<i64>, Display
- Добавлены traits: Hash, Eq, Serialize, Deserialize
- Обновлены 15+ модулей для использования новых типов:
  * tdlib: types.rs, chats.rs, messages.rs, users.rs, reactions.rs, client.rs
  * app: mod.rs, chat_state.rs
  * input: main_input.rs
  * tests: app_builder.rs, test_data.rs
- Исправлены 53 ошибки компиляции связанные с type conversions

Преимущества:
- Компилятор предотвращает смешивание разных типов ID
- Улучшенная читаемость кода (явные типы вместо i64)
- Самодокументирующиеся типы

Статус: Priority 2 теперь 60% (3/5 задач)
-  Error enum
-  Config validation
-  Newtype для ID
-  MessageInfo реструктуризация
-  MessageBuilder pattern

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-01-31 01:33:18 +03:00
parent 38e73befc1
commit 7081a886ad
15 changed files with 458 additions and 177 deletions

View File

@@ -1,6 +1,7 @@
// Chat state management - type-safe state machine for chat modes
use crate::tdlib::{MessageInfo, ProfileInfo};
use crate::types::MessageId;
/// Состояния чата - взаимоисключающие режимы работы с чатом
#[derive(Debug, Clone)]
@@ -17,7 +18,7 @@ pub enum ChatState {
/// Редактирование сообщения
Editing {
/// ID редактируемого сообщения
message_id: i64,
message_id: MessageId,
/// Индекс сообщения в списке
selected_index: usize,
},
@@ -25,13 +26,13 @@ pub enum ChatState {
/// Ответ на сообщение (reply)
Reply {
/// ID сообщения, на которое отвечаем
message_id: i64,
message_id: MessageId,
},
/// Пересылка сообщения (forward)
Forward {
/// ID сообщения для пересылки
message_id: i64,
message_id: MessageId,
/// Находимся в режиме выбора чата для пересылки
selecting_chat: bool,
},
@@ -39,13 +40,13 @@ pub enum ChatState {
/// Подтверждение удаления сообщения
DeleteConfirmation {
/// ID сообщения для удаления
message_id: i64,
message_id: MessageId,
},
/// Выбор реакции на сообщение
ReactionPicker {
/// ID сообщения для реакции
message_id: i64,
message_id: MessageId,
/// Список доступных реакций
available_reactions: Vec<String>,
/// Индекс выбранной реакции в picker
@@ -139,7 +140,7 @@ impl ChatState {
}
/// Возвращает ID выбранного сообщения (если есть)
pub fn selected_message_id(&self) -> Option<i64> {
pub fn selected_message_id(&self) -> Option<MessageId> {
match self {
ChatState::Editing { message_id, .. } => Some(*message_id),
ChatState::Reply { message_id } => Some(*message_id),

View File

@@ -5,6 +5,7 @@ pub use chat_state::ChatState;
pub use state::AppScreen;
use crate::tdlib::{ChatInfo, TdClient};
use crate::types::ChatId;
use ratatui::widgets::ListState;
pub struct App {
@@ -22,7 +23,7 @@ pub struct App {
// Main app state
pub chats: Vec<ChatInfo>,
pub chat_list_state: ListState,
pub selected_chat_id: Option<i64>,
pub selected_chat_id: Option<ChatId>,
pub message_input: String,
/// Позиция курсора в message_input (в символах)
pub cursor_position: usize,

View File

@@ -1,5 +1,6 @@
use crate::app::App;
use crate::tdlib::ChatAction;
use crate::types::{ChatId, MessageId};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::{Duration, Instant};
use tokio::time::timeout;
@@ -187,6 +188,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
KeyCode::Enter => {
// Перейти к выбранному сообщению
if let Some(msg_id) = app.get_selected_search_result_id() {
let msg_id = MessageId::new(msg_id);
let msg_index = app
.td_client
.current_chat_messages()
@@ -260,6 +262,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
KeyCode::Enter => {
// Перейти к сообщению в истории
if let Some(msg_id) = app.get_selected_pinned_id() {
let msg_id = MessageId::new(msg_id);
// Ищем индекс сообщения в текущей истории
let msg_index = app
.td_client
@@ -324,6 +327,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
if let Some(emoji) = app.get_selected_reaction().cloned() {
if let Some(message_id) = app.get_selected_message_for_reaction() {
if let Some(chat_id) = app.selected_chat_id {
let message_id = MessageId::new(message_id);
app.status_message = Some("Отправка реакции...".to_string());
app.needs_redraw = true;

View File

@@ -4,7 +4,9 @@
pub mod app;
pub mod config;
pub mod constants;
pub mod error;
pub mod input;
pub mod tdlib;
pub mod types;
pub mod ui;
pub mod utils;

View File

@@ -1,4 +1,5 @@
use crate::constants::TDLIB_CHAT_LIMIT;
use crate::types::{ChatId, UserId};
use std::time::Instant;
use tdlib_rs::enums::{ChatAction, ChatList, ChatType};
use tdlib_rs::functions;
@@ -11,7 +12,7 @@ pub struct ChatManager {
pub folders: Vec<FolderInfo>,
pub main_chat_list_position: i32,
/// Typing status для текущего чата: (user_id, action_text, timestamp)
pub typing_status: Option<(i64, String, Instant)>,
pub typing_status: Option<(UserId, String, Instant)>,
client_id: i32,
}
@@ -50,8 +51,8 @@ impl ChatManager {
}
/// Покинуть чат/группу
pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> {
let result = functions::leave_chat(chat_id, self.client_id).await;
pub async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String> {
let result = functions::leave_chat(chat_id.as_i64(), self.client_id).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка выхода из чата: {:?}", e)),
@@ -59,9 +60,9 @@ impl ChatManager {
}
/// Получить информацию профиля чата
pub async fn get_profile_info(&self, chat_id: i64) -> Result<ProfileInfo, String> {
pub async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
// Получаем основную информацию о чате
let chat_result = functions::get_chat(chat_id, self.client_id).await;
let chat_result = functions::get_chat(chat_id.as_i64(), self.client_id).await;
let chat_enum = match chat_result {
Ok(c) => c,
Err(e) => return Err(format!("Ошибка получения чата: {:?}", e)),
@@ -187,8 +188,8 @@ impl ChatManager {
}
/// Отправить 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;
pub async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
let _ = functions::send_chat_action(chat_id.as_i64(), 0, Some(action), self.client_id).await;
}
/// Очистить устаревший typing status (вызывать периодически)

View File

@@ -1,3 +1,4 @@
use crate::types::{ChatId, MessageId, UserId};
use std::env;
use std::time::Instant;
use tdlib_rs::enums::{
@@ -82,15 +83,15 @@ impl TdClient {
self.chat_manager.load_folder_chats(folder_id, limit).await
}
pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> {
pub async fn leave_chat(&self, chat_id: ChatId) -> Result<(), String> {
self.chat_manager.leave_chat(chat_id).await
}
pub async fn get_profile_info(&self, chat_id: i64) -> Result<ProfileInfo, String> {
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: i64, action: tdlib_rs::enums::ChatAction) {
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
}
@@ -105,7 +106,7 @@ impl TdClient {
// Делегирование к message_manager
pub async fn get_chat_history(
&mut self,
chat_id: i64,
chat_id: ChatId,
limit: i32,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager.get_chat_history(chat_id, limit).await
@@ -113,25 +114,25 @@ impl TdClient {
pub async fn load_older_messages(
&mut self,
chat_id: i64,
from_message_id: i64,
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: i64) -> Result<Vec<MessageInfo>, String> {
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: i64) {
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: i64,
chat_id: ChatId,
query: &str,
) -> Result<Vec<MessageInfo>, String> {
self.message_manager.search_messages(chat_id, query).await
@@ -139,9 +140,9 @@ impl TdClient {
pub async fn send_message(
&self,
chat_id: i64,
chat_id: ChatId,
text: String,
reply_to_message_id: Option<i64>,
reply_to_message_id: Option<MessageId>,
reply_info: Option<super::types::ReplyInfo>,
) -> Result<MessageInfo, String> {
self.message_manager
@@ -151,8 +152,8 @@ impl TdClient {
pub async fn edit_message(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
text: String,
) -> Result<MessageInfo, String> {
self.message_manager
@@ -162,8 +163,8 @@ impl TdClient {
pub async fn delete_messages(
&self,
chat_id: i64,
message_ids: Vec<i64>,
chat_id: ChatId,
message_ids: Vec<MessageId>,
revoke: bool,
) -> Result<(), String> {
self.message_manager
@@ -173,16 +174,16 @@ impl TdClient {
pub async fn forward_messages(
&self,
to_chat_id: i64,
from_chat_id: i64,
message_ids: Vec<i64>,
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: i64, text: String) -> Result<(), String> {
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
self.message_manager.set_draft_message(chat_id, text).await
}
@@ -199,11 +200,11 @@ impl TdClient {
}
// Делегирование к user_cache
pub async fn get_user_name(&self, user_id: i64) -> String {
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: i64) -> Option<&UserOnlineStatus> {
pub fn get_user_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
self.user_cache.get_status_by_chat_id(chat_id)
}
@@ -214,8 +215,8 @@ impl TdClient {
// Делегирование к reaction_manager
pub async fn get_message_available_reactions(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
) -> Result<Vec<String>, String> {
self.reaction_manager
.get_message_available_reactions(chat_id, message_id)
@@ -224,8 +225,8 @@ impl TdClient {
pub async fn toggle_reaction(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
emoji: String,
) -> Result<(), String> {
self.reaction_manager
@@ -275,11 +276,11 @@ impl TdClient {
&mut self.message_manager.current_chat_messages
}
pub fn current_chat_id(&self) -> Option<i64> {
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<i64>) {
pub fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
self.message_manager.current_chat_id = chat_id;
}
@@ -371,7 +372,7 @@ impl TdClient {
self.add_or_update_chat(&td_chat);
}
Update::ChatLastMessage(update) => {
let chat_id = update.chat_id;
let chat_id = ChatId::new(update.chat_id);
let (last_message_text, last_message_date) = update
.last_message
.as_ref()
@@ -397,30 +398,31 @@ impl TdClient {
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 == update.chat_id) {
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 == update.chat_id) {
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 == update.chat_id) {
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 когда собеседник прочитал сообщения
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == update.chat_id) {
chat.last_read_outbox_message_id = 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(update.chat_id) == self.current_chat_id() {
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 <= update.last_read_outbox_message_id {
if msg.is_outgoing && msg.id <= last_read_msg_id {
msg.is_read = true;
}
}
@@ -428,13 +430,14 @@ impl TdClient {
}
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 != update.chat_id);
self.chats_mut().retain(|c| c.id != chat_id);
} else if let Some(chat) =
self.chats_mut().iter_mut().find(|c| c.id == update.chat_id)
self.chats_mut().iter_mut().find(|c| c.id == chat_id)
{
// Обновляем позицию существующего чата
chat.order = update.position.order;
@@ -445,7 +448,7 @@ impl TdClient {
}
ChatList::Folder(folder) => {
// Обновляем folder_ids для чата
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == update.chat_id) {
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);
@@ -464,7 +467,7 @@ impl TdClient {
}
Update::NewMessage(new_msg) => {
// Добавляем новое сообщение если это текущий открытый чат
let chat_id = new_msg.message.chat_id;
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;
@@ -563,7 +566,7 @@ impl TdClient {
UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth,
UserStatus::Empty => UserOnlineStatus::LongTimeAgo,
};
self.user_cache.user_statuses.insert(update.user_id, status);
self.user_cache.user_statuses.insert(UserId::new(update.user_id), status);
}
Update::ConnectionState(update) => {
// Обновляем состояние сетевого соединения
@@ -577,10 +580,10 @@ impl TdClient {
}
Update::ChatAction(update) => {
// Обрабатываем только для текущего открытого чата
if Some(update.chat_id) == self.current_chat_id() {
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(user.user_id),
MessageSender::User(user) => Some(UserId::new(user.user_id)),
MessageSender::Chat(_) => None, // Игнорируем действия от имени чата
};
@@ -624,7 +627,7 @@ impl TdClient {
}
Update::ChatDraftMessage(update) => {
// Обновляем черновик в списке чатов
if let Some(chat) = self.chats_mut().iter_mut().find(|c| c.id == update.chat_id) {
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) =
@@ -639,11 +642,11 @@ impl TdClient {
}
Update::MessageInteractionInfo(update) => {
// Обновляем реакции в текущем открытом чате
if Some(update.chat_id) == self.current_chat_id() {
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 == update.message_id)
.find(|m| m.id == MessageId::new(update.message_id))
{
// Извлекаем реакции из interaction_info
msg.reactions = update
@@ -702,7 +705,7 @@ impl TdClient {
// Пропускаем удалённые аккаунты
if td_chat.title == "Deleted Account" || td_chat.title.is_empty() {
// Удаляем из списка если уже был добавлен
self.chats_mut().retain(|c| c.id != td_chat.id);
self.chats_mut().retain(|c| c.id != ChatId::new(td_chat.id));
return;
}
@@ -727,18 +730,20 @@ impl TdClient {
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(&td_chat.id)
&& !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);
}
}
self.user_cache.chat_user_ids.insert(td_chat.id, private.user_id);
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(&private.user_id)
.peek(&user_id)
.map(|u| format!("@{}", u))
}
_ => None,
@@ -761,7 +766,7 @@ impl TdClient {
let is_muted = td_chat.notification_settings.mute_for > 0;
let chat_info = ChatInfo {
id: td_chat.id,
id: ChatId::new(td_chat.id),
title: td_chat.title.clone(),
username,
last_message,
@@ -770,13 +775,13 @@ impl TdClient {
unread_mention_count: td_chat.unread_mention_count,
is_pinned,
order,
last_read_outbox_message_id: td_chat.last_read_outbox_message_id,
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 == td_chat.id) {
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;
@@ -815,37 +820,40 @@ impl TdClient {
self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order));
}
fn convert_message(&mut self, message: &TdMessage, chat_id: i64) -> MessageInfo {
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 порядок)
if let Some(name) = self.user_cache.user_names.get(&user.user_id).cloned() {
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.user_id) {
self.pending_user_ids_mut().push(user.user_id);
if !self.pending_user_ids().contains(&user_id) {
self.pending_user_ids_mut().push(user_id);
}
format!("User_{}", user.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 == chat.chat_id)
.find(|c| c.id == sender_chat_id)
.map(|c| c.title.clone())
.unwrap_or_else(|| format!("Chat_{}", chat.chat_id))
.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)
.map(|c| message_id <= c.last_read_outbox_message_id)
.unwrap_or(false)
} else {
true // Входящие сообщения не показывают галочки
@@ -863,7 +871,7 @@ impl TdClient {
let reactions = self.extract_reactions(message);
MessageInfo {
id: message.id,
id: message_id,
sender_name,
is_outgoing: message.is_outgoing,
content,
@@ -891,14 +899,16 @@ impl TdClient {
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.message_id)
.find(|m| m.id == reply_msg_id)
.map(|m| m.sender_name.clone())
.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 {
@@ -907,12 +917,12 @@ impl TdClient {
// Пробуем найти в текущих сообщениях
self.current_chat_messages()
.iter()
.find(|m| m.id == reply.message_id)
.find(|m| m.id == reply_msg_id)
.map(|m| m.content.clone())
.unwrap_or_default()
};
Some(ReplyInfo { message_id: reply.message_id, sender_name, text })
Some(ReplyInfo { message_id: reply_msg_id, sender_name, text })
}
_ => None,
}

View File

@@ -1,4 +1,5 @@
use crate::constants::{MAX_MESSAGES_IN_CHAT, TDLIB_MESSAGE_LIMIT};
use crate::types::{ChatId, MessageId};
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};
@@ -8,10 +9,10 @@ use super::types::{ForwardInfo, MessageInfo, ReactionInfo, ReplyInfo};
/// Менеджер сообщений
pub struct MessageManager {
pub current_chat_messages: Vec<MessageInfo>,
pub current_chat_id: Option<i64>,
pub current_chat_id: Option<ChatId>,
pub current_pinned_message: Option<MessageInfo>,
/// Очередь сообщений для отметки как прочитанных: (chat_id, message_ids)
pub pending_view_messages: Vec<(i64, Vec<i64>)>,
pub pending_view_messages: Vec<(ChatId, Vec<MessageId>)>,
client_id: i32,
}
@@ -39,14 +40,14 @@ impl MessageManager {
/// Получить историю чата
pub async fn get_chat_history(
&mut self,
chat_id: i64,
chat_id: ChatId,
limit: i32,
) -> Result<Vec<MessageInfo>, String> {
// Устанавливаем текущий чат для получения новых сообщений
self.current_chat_id = Some(chat_id);
let result = functions::get_chat_history(
chat_id,
chat_id.as_i64(),
0, // from_message_id
0, // offset
limit,
@@ -75,12 +76,12 @@ impl MessageManager {
/// Загрузить более старые сообщения
pub async fn load_older_messages(
&mut self,
chat_id: i64,
from_message_id: i64,
chat_id: ChatId,
from_message_id: MessageId,
) -> Result<Vec<MessageInfo>, String> {
let result = functions::get_chat_history(
chat_id,
from_message_id,
chat_id.as_i64(),
from_message_id.as_i64(),
0, // offset
TDLIB_MESSAGE_LIMIT,
false,
@@ -106,9 +107,9 @@ impl MessageManager {
}
/// Получить закреплённые сообщения
pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> {
pub async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result<Vec<MessageInfo>, String> {
let result = functions::search_chat_messages(
chat_id,
chat_id.as_i64(),
String::new(),
None,
0, // from_message_id
@@ -137,7 +138,7 @@ impl MessageManager {
}
/// Загрузить текущее закреплённое сообщение
pub async fn load_current_pinned_message(&mut self, chat_id: i64) {
pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) {
// TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat.
// Нужно использовать getChatPinnedMessage или альтернативный способ.
// Временно отключено.
@@ -155,11 +156,11 @@ impl MessageManager {
/// Поиск сообщений в чате
pub async fn search_messages(
&self,
chat_id: i64,
chat_id: ChatId,
query: &str,
) -> Result<Vec<MessageInfo>, String> {
let result = functions::search_chat_messages(
chat_id,
chat_id.as_i64(),
query.to_string(),
None,
0, // from_message_id
@@ -190,9 +191,9 @@ impl MessageManager {
/// Отправить сообщение
pub async fn send_message(
&self,
chat_id: i64,
chat_id: ChatId,
text: String,
reply_to_message_id: Option<i64>,
reply_to_message_id: Option<MessageId>,
_reply_info: Option<ReplyInfo>,
) -> Result<MessageInfo, String> {
// Парсим markdown в тексте
@@ -224,13 +225,13 @@ impl MessageManager {
let reply_to = reply_to_message_id.map(|msg_id| {
InputMessageReplyTo::Message(InputMessageReplyToMessage {
chat_id: 0,
message_id: msg_id,
message_id: msg_id.as_i64(),
quote: None,
})
});
let result = functions::send_message(
chat_id,
chat_id.as_i64(),
0, // message_thread_id
reply_to,
None, // options
@@ -252,8 +253,8 @@ impl MessageManager {
/// Редактировать сообщение
pub async fn edit_message(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
text: String,
) -> Result<MessageInfo, String> {
let formatted_text = match functions::parse_text_entities(
@@ -282,7 +283,7 @@ impl MessageManager {
});
let result =
functions::edit_message_text(chat_id, message_id, content, self.client_id).await;
functions::edit_message_text(chat_id.as_i64(), message_id.as_i64(), content, self.client_id).await;
match result {
Ok(tdlib_rs::enums::Message::Message(msg)) => self
@@ -297,12 +298,13 @@ impl MessageManager {
/// Удалить сообщения
pub async fn delete_messages(
&self,
chat_id: i64,
message_ids: Vec<i64>,
chat_id: ChatId,
message_ids: Vec<MessageId>,
revoke: bool,
) -> Result<(), String> {
let message_ids_i64: Vec<i64> = message_ids.into_iter().map(|id| id.as_i64()).collect();
let result =
functions::delete_messages(chat_id, message_ids, revoke, self.client_id).await;
functions::delete_messages(chat_id.as_i64(), message_ids_i64, revoke, self.client_id).await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(format!("Ошибка удаления: {:?}", e)),
@@ -312,15 +314,16 @@ impl MessageManager {
/// Переслать сообщения
pub async fn forward_messages(
&self,
to_chat_id: i64,
from_chat_id: i64,
message_ids: Vec<i64>,
to_chat_id: ChatId,
from_chat_id: ChatId,
message_ids: Vec<MessageId>,
) -> Result<(), String> {
let message_ids_i64: Vec<i64> = message_ids.into_iter().map(|id| id.as_i64()).collect();
let result = functions::forward_messages(
to_chat_id,
to_chat_id.as_i64(),
0, // message_thread_id
from_chat_id,
message_ids,
from_chat_id.as_i64(),
message_ids_i64,
None, // options
false, // send_copy
false, // remove_caption
@@ -335,7 +338,7 @@ impl MessageManager {
}
/// Установить черновик
pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> {
pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
use tdlib_rs::types::DraftMessage;
let draft = if text.is_empty() {
@@ -355,7 +358,7 @@ impl MessageManager {
})
};
let result = functions::set_chat_draft_message(chat_id, 0, draft, self.client_id).await;
let result = functions::set_chat_draft_message(chat_id.as_i64(), 0, draft, self.client_id).await;
match result {
Ok(_) => Ok(()),

View File

@@ -1,3 +1,4 @@
use crate::types::{ChatId, MessageId};
use tdlib_rs::enums::ReactionType;
use tdlib_rs::functions;
use tdlib_rs::types::ReactionTypeEmoji;
@@ -15,11 +16,11 @@ impl ReactionManager {
/// Получить доступные реакции для сообщения
pub async fn get_message_available_reactions(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
) -> Result<Vec<String>, String> {
// Получаем сообщение
let msg_result = functions::get_message(chat_id, message_id, self.client_id).await;
let msg_result = functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await;
let msg = match msg_result {
Ok(m) => m,
Err(e) => return Err(format!("Ошибка получения сообщения: {:?}", e)),
@@ -27,8 +28,8 @@ impl ReactionManager {
// Получаем доступные реакции для чата
let reactions_result = functions::get_message_available_reactions(
chat_id,
message_id,
chat_id.as_i64(),
message_id.as_i64(),
10, // row_size
self.client_id,
)
@@ -89,15 +90,15 @@ impl ReactionManager {
/// Переключить реакцию на сообщение
pub async fn toggle_reaction(
&self,
chat_id: i64,
message_id: i64,
chat_id: ChatId,
message_id: MessageId,
emoji: String,
) -> Result<(), String> {
let reaction = ReactionType::Emoji(ReactionTypeEmoji { emoji });
let result = functions::add_message_reaction(
chat_id,
message_id,
chat_id.as_i64(),
message_id.as_i64(),
reaction.clone(),
false, // is_big
false, // update_recent_reactions
@@ -110,8 +111,8 @@ impl ReactionManager {
Err(_) => {
// Если добавление не удалось, пытаемся удалить
let remove_result = functions::remove_message_reaction(
chat_id,
message_id,
chat_id.as_i64(),
message_id.as_i64(),
reaction,
self.client_id,
)

View File

@@ -1,9 +1,11 @@
use tdlib_rs::types::TextEntity;
use crate::types::{ChatId, MessageId};
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ChatInfo {
pub id: i64,
pub id: ChatId,
pub title: String,
pub username: Option<String>,
pub last_message: String,
@@ -14,7 +16,7 @@ pub struct ChatInfo {
pub is_pinned: bool,
pub order: i64,
/// ID последнего прочитанного исходящего сообщения (для галочек)
pub last_read_outbox_message_id: i64,
pub last_read_outbox_message_id: MessageId,
/// ID папок, в которых находится чат
pub folder_ids: Vec<i32>,
/// Чат замьючен (уведомления отключены)
@@ -27,7 +29,7 @@ pub struct ChatInfo {
#[derive(Debug, Clone)]
pub struct ReplyInfo {
/// ID сообщения, на которое отвечают
pub message_id: i64,
pub message_id: MessageId,
/// Имя отправителя оригинального сообщения
pub sender_name: String,
/// Текст оригинального сообщения (превью)
@@ -57,7 +59,7 @@ pub struct ReactionInfo {
#[derive(Debug, Clone)]
pub struct MessageInfo {
pub id: i64,
pub id: MessageId,
pub sender_name: String,
pub is_outgoing: bool,
pub content: String,
@@ -90,7 +92,7 @@ pub struct FolderInfo {
/// Информация о профиле чата/пользователя
#[derive(Debug, Clone)]
pub struct ProfileInfo {
pub chat_id: i64,
pub chat_id: ChatId,
pub title: String,
pub username: Option<String>,
pub bio: Option<String>,

View File

@@ -1,4 +1,5 @@
use crate::constants::{LAZY_LOAD_USERS_PER_TICK, MAX_CHAT_USER_IDS, MAX_USER_CACHE_SIZE};
use crate::types::{ChatId, UserId};
use std::collections::HashMap;
use tdlib_rs::enums::{User, UserStatus};
use tdlib_rs::functions;
@@ -7,9 +8,9 @@ use super::types::UserOnlineStatus;
/// Простой LRU-кэш на основе HashMap + Vec для отслеживания порядка
pub struct LruCache<V> {
map: HashMap<i64, V>,
map: HashMap<UserId, V>,
/// Порядок доступа: последний элемент — самый недавно использованный
order: Vec<i64>,
order: Vec<UserId>,
capacity: usize,
}
@@ -23,7 +24,7 @@ impl<V: Clone> LruCache<V> {
}
/// Получить значение и обновить порядок доступа
pub fn get(&mut self, key: &i64) -> Option<&V> {
pub fn get(&mut self, key: &UserId) -> Option<&V> {
if self.map.contains_key(key) {
// Перемещаем ключ в конец (самый недавно использованный)
self.order.retain(|k| k != key);
@@ -35,12 +36,12 @@ impl<V: Clone> LruCache<V> {
}
/// Получить значение без обновления порядка (для read-only доступа)
pub fn peek(&self, key: &i64) -> Option<&V> {
pub fn peek(&self, key: &UserId) -> Option<&V> {
self.map.get(key)
}
/// Вставить значение
pub fn insert(&mut self, key: i64, value: V) {
pub fn insert(&mut self, key: UserId, value: V) {
if self.map.contains_key(&key) {
// Обновляем существующее значение
self.map.insert(key, value);
@@ -78,9 +79,9 @@ pub struct UserCache {
/// 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>,
pub chat_user_ids: HashMap<ChatId, UserId>,
/// Очередь user_id для загрузки имён
pub pending_user_ids: Vec<i64>,
pub pending_user_ids: Vec<UserId>,
/// LRU-кэш онлайн-статусов пользователей: user_id -> status
pub user_statuses: LruCache<UserOnlineStatus>,
client_id: i32,
@@ -99,22 +100,22 @@ impl UserCache {
}
/// Получить username пользователя
pub fn get_username(&mut self, user_id: &i64) -> Option<&String> {
pub fn get_username(&mut self, user_id: &UserId) -> Option<&String> {
self.user_usernames.get(user_id)
}
/// Получить имя пользователя
pub fn get_name(&mut self, user_id: &i64) -> Option<&String> {
pub fn get_name(&mut self, user_id: &UserId) -> 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> {
pub fn get_user_id_by_chat(&self, chat_id: ChatId) -> Option<UserId> {
self.chat_user_ids.get(&chat_id).copied()
}
/// Получить статус пользователя по chat_id
pub fn get_status_by_chat_id(&self, chat_id: i64) -> Option<&UserOnlineStatus> {
pub fn get_status_by_chat_id(&self, chat_id: ChatId) -> Option<&UserOnlineStatus> {
let user_id = self.chat_user_ids.get(&chat_id)?;
self.user_statuses.peek(user_id)
}
@@ -126,20 +127,20 @@ impl UserCache {
// Сохраняем username
if let Some(username) = user.usernames.as_ref().map(|u| u.editable_username.clone()) {
self.user_usernames.insert(user_id, username);
self.user_usernames.insert(UserId::new(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.user_names.insert(UserId::new(user_id), display_name);
// Обновляем статус
self.update_status(user_id, &user.status);
self.update_status(UserId::new(user_id), &user.status);
}
}
/// Обработать обновление статуса пользователя
pub fn update_status(&mut self, user_id: i64, status: &UserStatus) {
pub fn update_status(&mut self, user_id: UserId, status: &UserStatus) {
let online_status = match status {
UserStatus::Online(_) => UserOnlineStatus::Online,
UserStatus::Recently(_) => UserOnlineStatus::Recently,
@@ -152,24 +153,24 @@ impl UserCache {
}
/// Сохранить связь chat_id -> user_id
pub fn register_private_chat(&mut self, chat_id: i64, user_id: i64) {
pub fn register_private_chat(&mut self, chat_id: ChatId, user_id: UserId) {
self.chat_user_ids.insert(chat_id, user_id);
}
/// Получить имя пользователя (асинхронно с загрузкой если нужно)
pub async fn get_user_name(&self, user_id: i64) -> String {
pub async fn get_user_name(&self, user_id: UserId) -> String {
// Сначала пытаемся получить из кэша
if let Some(name) = self.user_names.peek(&user_id) {
return name.clone();
}
// Загружаем пользователя
match functions::get_user(user_id, self.client_id).await {
match functions::get_user(user_id.as_i64(), 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),
_ => format!("User {}", user_id.as_i64()),
}
}

170
src/types.rs Normal file
View File

@@ -0,0 +1,170 @@
/// Type-safe ID wrappers to prevent mixing up different ID types
use serde::{Deserialize, Serialize};
use std::fmt;
/// Chat identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChatId(pub i64);
impl ChatId {
pub fn new(id: i64) -> Self {
Self(id)
}
pub fn as_i64(&self) -> i64 {
self.0
}
}
impl From<i64> for ChatId {
fn from(id: i64) -> Self {
Self(id)
}
}
impl From<ChatId> for i64 {
fn from(id: ChatId) -> Self {
id.0
}
}
impl fmt::Display for ChatId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Message identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MessageId(pub i64);
impl MessageId {
pub fn new(id: i64) -> Self {
Self(id)
}
pub fn as_i64(&self) -> i64 {
self.0
}
}
impl From<i64> for MessageId {
fn from(id: i64) -> Self {
Self(id)
}
}
impl From<MessageId> for i64 {
fn from(id: MessageId) -> Self {
id.0
}
}
impl fmt::Display for MessageId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// User identifier
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UserId(pub i64);
impl UserId {
pub fn new(id: i64) -> Self {
Self(id)
}
pub fn as_i64(&self) -> i64 {
self.0
}
}
impl From<i64> for UserId {
fn from(id: i64) -> Self {
Self(id)
}
}
impl From<UserId> for i64 {
fn from(id: UserId) -> Self {
id.0
}
}
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chat_id() {
let id = ChatId::new(123);
assert_eq!(id.as_i64(), 123);
assert_eq!(i64::from(id), 123);
let id2: ChatId = 456.into();
assert_eq!(id2.0, 456);
}
#[test]
fn test_message_id() {
let id = MessageId::new(789);
assert_eq!(id.as_i64(), 789);
assert_eq!(i64::from(id), 789);
}
#[test]
fn test_user_id() {
let id = UserId::new(111);
assert_eq!(id.as_i64(), 111);
assert_eq!(i64::from(id), 111);
}
#[test]
fn test_type_safety() {
// Type safety is enforced at compile time
// The following would not compile:
// let chat_id = ChatId::new(1);
// let message_id = MessageId::new(1);
// if chat_id == message_id { } // ERROR: mismatched types
// Runtime values can be the same, but types are different
let chat_id = ChatId::new(1);
let message_id = MessageId::new(1);
assert_eq!(chat_id.as_i64(), 1);
assert_eq!(message_id.as_i64(), 1);
// But they cannot be compared directly due to type safety
}
#[test]
fn test_display() {
let chat_id = ChatId::new(123);
assert_eq!(format!("{}", chat_id), "123");
let message_id = MessageId::new(456);
assert_eq!(format!("{}", message_id), "456");
let user_id = UserId::new(789);
assert_eq!(format!("{}", user_id), "789");
}
#[test]
fn test_hash_map() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(ChatId::new(1), "chat1");
map.insert(ChatId::new(2), "chat2");
assert_eq!(map.get(&ChatId::new(1)), Some(&"chat1"));
assert_eq!(map.get(&ChatId::new(2)), Some(&"chat2"));
assert_eq!(map.get(&ChatId::new(3)), None);
}
}