Files
telegram-tui/src/tdlib/users.rs
Mikhail Kilin 3b7ef41cae
Some checks failed
ci/woodpecker/pr/check Pipeline was successful
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
fix: resolve all 40 clippy warnings (dead_code, unused_imports, lints)
- Add #[allow(unused_imports)] on pub re-exports used only by lib/tests
- Add #[allow(dead_code)] on public API items unused in binary target
- Fix collapsible_if, redundant_closure, unnecessary_map_or in main.rs
- Prefix unused test variables with underscore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 17:50:18 +03:00

271 lines
10 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use 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;
use super::types::UserOnlineStatus;
/// LRU (Least Recently Used) кэш с фиксированной ёмкостью.
///
/// Автоматически удаляет самые давно использованные элементы при достижении лимита.
/// Основан на HashMap для быстрого доступа и Vec для отслеживания порядка использования.
///
/// # Type Parameters
///
/// * `K` - Тип ключа (должен реализовывать `Eq + Hash + Clone + Copy`)
/// * `V` - Тип значения (должен реализовывать `Clone`)
///
/// # Examples
///
/// ```ignore
/// let mut cache = LruCache::<UserId, String>::new(100);
/// cache.insert(UserId::new(1), "Alice".to_string());
/// assert_eq!(cache.get(&UserId::new(1)), Some(&"Alice".to_string()));
/// ```
pub struct LruCache<K, V> {
/// Хранилище ключ-значение.
map: HashMap<K, V>,
/// Порядок доступа: последний элемент — самый недавно использованный.
order: Vec<K>,
/// Максимальная ёмкость кэша.
capacity: usize,
}
impl<K, V> LruCache<K, V>
where
K: Eq + std::hash::Hash + Clone + Copy,
V: Clone,
{
/// Создает новый LRU кэш с заданной ёмкостью.
pub fn new(capacity: usize) -> Self {
Self {
map: HashMap::with_capacity(capacity),
order: Vec::with_capacity(capacity),
capacity,
}
}
/// Получает значение и обновляет порядок доступа (помечает как использованное).
pub fn get(&mut self, key: &K) -> Option<&V> {
if self.map.contains_key(key) {
// Перемещаем ключ в конец (самый недавно использованный)
self.order.retain(|k| k != key);
self.order.push(*key);
self.map.get(key)
} else {
None
}
}
/// Получить значение без обновления порядка (для read-only доступа)
pub fn peek(&self, key: &K) -> Option<&V> {
self.map.get(key)
}
/// Вставить значение
pub fn insert(&mut self, key: K, value: V) {
if self.map.contains_key(&key) {
// Обновляем существующее значение
self.map.insert(key, value);
self.order.retain(|k| *k != key);
self.order.push(key);
} else {
// Если кэш полон, удаляем самый старый элемент
if self.map.len() >= self.capacity {
if let Some(oldest) = self.order.first().copied() {
self.order.remove(0);
self.map.remove(&oldest);
}
}
self.map.insert(key, value);
self.order.push(key);
}
}
/// Проверить наличие ключа
pub fn contains_key(&self, key: &K) -> bool {
self.map.contains_key(key)
}
}
/// Кэш информации о пользователях Telegram.
///
/// Хранит данные пользователей (имена, usernames, статусы) в LRU-кэшах
/// для быстрого доступа без повторных запросов к TDLib.
///
/// # Возможности
///
/// - Кэширование имен пользователей (first_name + last_name)
/// - Кэширование usernames (@username)
/// - Кэширование онлайн-статусов
/// - Связь chat_id → user_id для приватных чатов
/// - Ленивая загрузка данных пользователей порциями
///
/// # Examples
///
/// ```ignore
/// let mut cache = UserCache::new(client_id);
///
/// // Обработать обновление пользователя
/// cache.handle_user_update(&user_enum);
///
/// // Получить имя
/// let name = cache.get_user_name(user_id).await;
/// ```
pub struct UserCache {
/// LRU-кэш usernames: user_id → username.
pub user_usernames: LruCache<UserId, String>,
/// LRU-кэш имён: user_id → display_name (first_name + last_name).
pub user_names: LruCache<UserId, String>,
/// Связь chat_id → user_id для приватных чатов.
pub chat_user_ids: HashMap<ChatId, UserId>,
/// Очередь user_id для ленивой загрузки имён.
pub pending_user_ids: Vec<UserId>,
/// LRU-кэш онлайн-статусов: user_id → status.
pub user_statuses: LruCache<UserId, UserOnlineStatus>,
/// ID клиента TDLib для API вызовов.
client_id: i32,
}
impl UserCache {
/// Создает новый кэш пользователей.
///
/// # Arguments
///
/// * `client_id` - ID клиента TDLib для API вызовов
pub fn new(client_id: i32) -> Self {
Self {
user_usernames: LruCache::new(MAX_USER_CACHE_SIZE),
user_names: LruCache::new(MAX_USER_CACHE_SIZE),
chat_user_ids: HashMap::with_capacity(MAX_CHAT_USER_IDS),
pending_user_ids: Vec::new(),
user_statuses: LruCache::new(MAX_USER_CACHE_SIZE),
client_id,
}
}
/// Получить статус пользователя по chat_id
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)
}
/// Обрабатывает обновление пользователя от TDLib.
///
/// Сохраняет username, имя и статус пользователя в соответствующие кэши.
///
/// # Arguments
///
/// * `user_enum` - Обновление пользователя от TDLib
pub fn handle_user_update(&mut self, user_enum: &User) {
let User::User(user) = user_enum;
let user_id = user.id;
// Сохраняем username
if let Some(username) = user.usernames.as_ref().map(|u| u.editable_username.clone()) {
self.user_usernames.insert(UserId::new(user_id), username);
}
// Сохраняем имя
let display_name = format!("{} {}", user.first_name, user.last_name)
.trim()
.to_string();
self.user_names.insert(UserId::new(user_id), display_name);
// Обновляем статус
self.update_status(UserId::new(user_id), &user.status);
}
/// Обновляет онлайн-статус пользователя.
///
/// # Arguments
///
/// * `user_id` - ID пользователя
/// * `status` - Новый статус от TDLib
pub fn update_status(&mut self, user_id: UserId, status: &UserStatus) {
let online_status = match status {
UserStatus::Online(_) => UserOnlineStatus::Online,
UserStatus::Recently(_) => UserOnlineStatus::Recently,
UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek,
UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth,
UserStatus::Offline(s) => UserOnlineStatus::Offline(s.was_online),
_ => return,
};
self.user_statuses.insert(user_id, online_status);
}
/// Получает имя пользователя из кэша или загружает из TDLib.
///
/// Сначала проверяет кэш, затем при необходимости загружает из API.
///
/// # Arguments
///
/// * `user_id` - ID пользователя
///
/// # Returns
///
/// Имя пользователя (first_name + last_name) или "User {id}" если не найден.
#[allow(dead_code)]
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.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.as_i64()),
}
}
/// Обрабатывает очередь отложенных user_ids для ленивой загрузки.
///
/// Загружает данные пользователей небольшими порциями (по [`LAZY_LOAD_USERS_PER_TICK`])
/// для избежания блокировки UI.
///
/// # Note
///
/// Вызывайте периодически в основном цикле приложения.
pub async fn process_pending_user_ids(&mut self) {
if self.pending_user_ids.is_empty() {
return;
}
// Берём первые N user_ids для загрузки
let batch: Vec<UserId> = self
.pending_user_ids
.drain(..self.pending_user_ids.len().min(LAZY_LOAD_USERS_PER_TICK))
.collect();
for user_id in batch {
if self.user_names.contains_key(&user_id) {
continue; // Уже в кэше
}
match functions::get_user(user_id.as_i64(), self.client_id).await {
Ok(user_enum) => {
self.handle_user_update(&user_enum);
}
Err(_) => {
// Если не удалось загрузить, сохраняем placeholder
self.user_names.insert(user_id, format!("User {}", user_id));
}
}
}
}
}