""" Хранилище зарегистрированных пользователей. """ import asyncio import json import logging from datetime import datetime from pathlib import Path logger = logging.getLogger(__name__) class AuthStorage: """Реестр пользователей бота в JSON-файле. Паролей не хранит: личность подтверждает Telegram, идентификатором служит user_id. """ def __init__(self, file_path: str = "data/users.json"): self.file_path = Path(file_path) self.file_path.parent.mkdir(parents=True, exist_ok=True) self._lock = asyncio.Lock() async def register( self, user_id: int, username: str = None, full_name: str = None ) -> bool: """Регистрирует пользователя и освежает его профиль. Профиль перезаписывается и для уже известных пользователей — ник в Telegram может смениться, а взять его неоткуда, кроме входящего апдейта. """ async with self._lock: data = await asyncio.to_thread(self._load_sync) key = str(user_id) is_new = key not in data record = data.get(key, {}) if is_new: record["registered_at"] = datetime.now().isoformat() if username is not None: record["username"] = username if full_name is not None: record["full_name"] = full_name data[key] = record await asyncio.to_thread(self._save_sync, data) if is_new: logger.info(f"User {user_id} registered ({username or full_name or 'no name'})") return is_new async def get_profile(self, user_id: int) -> dict: async with self._lock: data = await asyncio.to_thread(self._load_sync) return data.get(str(user_id), {}) async def get_all_profiles(self) -> dict: """Возвращает {user_id: профиль} для всех пользователей.""" async with self._lock: data = await asyncio.to_thread(self._load_sync) return {int(k): v for k, v in data.items()} @staticmethod def display_name(profile: dict) -> str: """Человекочитаемое имя: @ник, иначе имя, иначе пусто.""" if not profile: return "" username = profile.get("username") if username: return f"@{username}" return profile.get("full_name") or "" async def delete_user(self, user_id: int) -> bool: async with self._lock: data = await asyncio.to_thread(self._load_sync) user_str = str(user_id) if user_str not in data: return False del data[user_str] await asyncio.to_thread(self._save_sync, data) logger.info(f"User {user_id} deleted") return True async def user_exists(self, user_id: int) -> bool: async with self._lock: data = await asyncio.to_thread(self._load_sync) return str(user_id) in data async def get_all_user_ids(self) -> list[int]: async with self._lock: data = await asyncio.to_thread(self._load_sync) return [int(k) for k in data.keys()] def _load_sync(self) -> dict: if not self.file_path.exists(): return {} try: with open(self.file_path, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, IOError) as e: logger.warning(f"Failed to load auth storage: {e}") return {} def _save_sync(self, data: dict) -> None: try: with open(self.file_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) except IOError as e: logger.error(f"Failed to save auth storage: {e}")