""" Управление конфигурацией групп для различных мессенджеров. Автор: Сергей Антропов Сайт: https://devops.org.ru """ import json import logging from typing import Dict, Optional, Any, Union import aiofiles from datetime import datetime, timedelta logger = logging.getLogger(__name__) class GroupsConfig: """Менеджер конфигурации групп для различных мессенджеров с кэшированием.""" def __init__(self, config_path: Optional[str] = None): """ Инициализация менеджера конфигурации. Args: config_path: Путь к файлу конфигурации групп. """ from app.core.config import get_settings settings = get_settings() self.config_path = config_path or settings.groups_config_path self._cache: Optional[Dict[str, Any]] = None self._cache_time: Optional[datetime] = None self._cache_ttl = timedelta(minutes=5) # Кэш на 5 минут self.default_messenger = settings.default_messenger async def _load_config(self) -> Dict[str, Any]: """ Загрузить конфигурацию групп из файла. Returns: Конфигурация групп в формате: { "group_name": { "messenger": "telegram", "chat_id": -1001997464975, "thread_id": 0, "config": {} } } Raises: FileNotFoundError: Если файл конфигурации не найден. json.JSONDecodeError: Если файл содержит некорректный JSON. ValueError: Если конфигурация имеет неверный формат. """ try: async with aiofiles.open(self.config_path, 'r', encoding='utf-8') as f: content = await f.read() config = json.loads(content) logger.info(f"Конфигурация групп загружена из {self.config_path}") # Валидация формата конфигурации for group_name, group_value in config.items(): if not isinstance(group_value, dict): raise ValueError( f"Неверный формат конфигурации для группы '{group_name}': " f"ожидается словарь, получен {type(group_value)}. " f"Используйте формат: {{'messenger': 'telegram', 'chat_id': ..., 'thread_id': 0, 'config': {{}}}}" ) if "chat_id" not in group_value: raise ValueError( f"Отсутствует обязательное поле 'chat_id' для группы '{group_name}'" ) if "messenger" not in group_value: raise ValueError( f"Отсутствует обязательное поле 'messenger' для группы '{group_name}'" ) return config except FileNotFoundError: logger.error(f"Файл конфигурации групп не найден: {self.config_path}") raise except json.JSONDecodeError as e: logger.error(f"Ошибка парсинга JSON в файле конфигурации: {e}") raise def _is_cache_valid(self) -> bool: """Проверить, валиден ли кэш.""" if self._cache is None or self._cache_time is None: return False return datetime.now() - self._cache_time < self._cache_ttl async def get_group_config(self, group_name: str, messenger: Optional[str] = None) -> Optional[Dict[str, Any]]: """ Получить конфигурацию группы. Args: group_name: Имя группы из конфигурации. messenger: Тип мессенджера (опционально, для фильтрации). Returns: Конфигурация группы или None, если группа не найдена. Формат: { "messenger": "telegram", "chat_id": -1001997464975, "thread_id": 0, "config": {} } """ # Проверяем кэш if not self._is_cache_valid(): try: self._cache = await self._load_config() self._cache_time = datetime.now() except (FileNotFoundError, json.JSONDecodeError): logger.error("Не удалось загрузить конфигурацию групп") return None group_config = self._cache.get(group_name) if group_config is None: logger.warning(f"Группа '{group_name}' не найдена в конфигурации") return None # Если указан messenger, проверяем соответствие if messenger and group_config.get("messenger") != messenger: logger.warning( f"Группа '{group_name}' имеет мессенджер '{group_config.get('messenger')}', " f"но запрошен '{messenger}'" ) return None logger.info(f"Найдена конфигурация для группы '{group_name}': {group_config}") return group_config async def get_chat_id(self, group_name: str, messenger: Optional[str] = None) -> Optional[Union[int, str]]: """ Получить ID чата по имени группы. Args: group_name: Имя группы из конфигурации. messenger: Тип мессенджера (опционально). Returns: ID чата или None, если группа не найдена. """ group_config = await self.get_group_config(group_name, messenger) if group_config is None: return None return group_config.get("chat_id") async def refresh_cache(self) -> None: """Принудительно обновить кэш конфигурации.""" try: self._cache = await self._load_config() self._cache_time = datetime.now() logger.info("Кэш конфигурации групп обновлен") except (FileNotFoundError, json.JSONDecodeError) as e: logger.error(f"Ошибка обновления кэша: {e}") async def _save_config(self, config: Dict[str, Any]) -> None: """ Сохранить конфигурацию групп в файл. Args: config: Нормализованная конфигурация групп в новом формате. Raises: IOError: Если не удалось записать файл. """ try: async with aiofiles.open(self.config_path, 'w', encoding='utf-8') as f: await f.write(json.dumps(config, indent=2, ensure_ascii=False)) logger.info(f"Конфигурация групп сохранена в {self.config_path}") # Обновляем кэш self._cache = config self._cache_time = datetime.now() except IOError as e: logger.error(f"Ошибка записи конфигурации групп: {e}") raise async def get_all_groups(self, include_ids: bool = False, messenger: Optional[str] = None) -> Dict[str, Any]: """ Получить все группы из конфигурации. Args: include_ids: Включать ли полную конфигурацию групп (включая ID, мессенджер и т.д.). messenger: Фильтр по типу мессенджера (опционально). Returns: Словарь с группами. Если include_ids=False, возвращает только названия групп. Если include_ids=True, возвращает полную конфигурацию групп. """ if not self._is_cache_valid(): try: self._cache = await self._load_config() self._cache_time = datetime.now() except (FileNotFoundError, json.JSONDecodeError): logger.error("Не удалось загрузить конфигурацию групп") return {} # Фильтруем по мессенджеру, если указан filtered_config = self._cache.copy() if messenger: filtered_config = { name: config for name, config in filtered_config.items() if config.get("messenger") == messenger } if include_ids: return filtered_config.copy() else: # Возвращаем только названия групп без конфигурации return {name: None for name in filtered_config.keys()} async def create_group( self, group_name: str, chat_id: Union[int, str], messenger: str = "telegram", thread_id: int = 0, config: Optional[Dict[str, Any]] = None ) -> bool: """ Создать новую группу в конфигурации. Args: group_name: Имя группы. chat_id: ID чата (может быть int или str). messenger: Тип мессенджера (telegram, max). thread_id: ID треда в группе (по умолчанию 0). config: Дополнительная конфигурация для мессенджера (опционально). Returns: True если группа создана успешно, False если группа уже существует. """ # Загружаем текущую конфигурацию if not self._is_cache_valid(): try: self._cache = await self._load_config() self._cache_time = datetime.now() except (FileNotFoundError, json.JSONDecodeError): # Если файл не существует, создаем новый self._cache = {} # Проверяем, существует ли группа if group_name in self._cache: logger.warning(f"Группа '{group_name}' уже существует") return False # Добавляем группу self._cache[group_name] = { "messenger": messenger, "chat_id": chat_id, "thread_id": thread_id, "config": config or {} } await self._save_config(self._cache) logger.info(f"Группа '{group_name}' создана с мессенджером '{messenger}' и ID {chat_id}") return True async def update_group( self, group_name: str, chat_id: Optional[Union[int, str]] = None, messenger: Optional[str] = None, thread_id: Optional[int] = None, config: Optional[Dict[str, Any]] = None ) -> bool: """ Обновить существующую группу в конфигурации. Args: group_name: Имя группы. chat_id: Новый ID чата (опционально). messenger: Новый тип мессенджера (опционально). thread_id: Новый ID треда (опционально). config: Новая дополнительная конфигурация (опционально). Returns: True если группа обновлена успешно, False если группа не найдена. """ # Загружаем текущую конфигурацию if not self._is_cache_valid(): try: self._cache = await self._load_config() self._cache_time = datetime.now() except (FileNotFoundError, json.JSONDecodeError): logger.error("Не удалось загрузить конфигурацию групп") return False # Проверяем, существует ли группа if group_name not in self._cache: logger.warning(f"Группа '{group_name}' не найдена") return False # Обновляем группу (обновляем только указанные поля) old_config = self._cache[group_name].copy() if chat_id is not None: self._cache[group_name]["chat_id"] = chat_id if messenger is not None: self._cache[group_name]["messenger"] = messenger if thread_id is not None: self._cache[group_name]["thread_id"] = thread_id if config is not None: self._cache[group_name]["config"] = config await self._save_config(self._cache) logger.info(f"Группа '{group_name}' обновлена: {old_config} -> {self._cache[group_name]}") return True async def delete_group(self, group_name: str) -> bool: """ Удалить группу из конфигурации. Args: group_name: Имя группы. Returns: True если группа удалена успешно, False если группа не найдена. """ # Загружаем текущую конфигурацию if not self._is_cache_valid(): try: self._cache = await self._load_config() self._cache_time = datetime.now() except (FileNotFoundError, json.JSONDecodeError): logger.error("Не удалось загрузить конфигурацию групп") return False # Проверяем, существует ли группа if group_name not in self._cache: logger.warning(f"Группа '{group_name}' не найдена") return False # Удаляем группу del self._cache[group_name] await self._save_config(self._cache) logger.info(f"Группа '{group_name}' удалена") return True # Глобальный экземпляр менеджера конфигурации (lazy initialization) _groups_config_instance = None def get_groups_config() -> GroupsConfig: """ Получить экземпляр менеджера конфигурации групп (lazy initialization). Returns: Экземпляр GroupsConfig. """ global _groups_config_instance if _groups_config_instance is None: _groups_config_instance = GroupsConfig() return _groups_config_instance # Глобальный экземпляр менеджера конфигурации (lazy initialization) class _GroupsConfigProxy: """Прокси для ленивой инициализации groups_config.""" def __getattr__(self, name): """Получить атрибут из groups_config.""" return getattr(get_groups_config(), name) groups_config = _GroupsConfigProxy()