Files
MessageGateway/app/core/groups.py
Sergey Antropov b90def35ed Initial commit: Message Gateway project
- FastAPI приложение для отправки мониторинговых алертов в мессенджеры
- Поддержка Telegram и MAX/VK
- Интеграция с Grafana, Zabbix, AlertManager
- Автоматическое создание тикетов в Jira
- Управление группами мессенджеров через API
- Декораторы для авторизации и скрытия эндпоинтов
- Подробная документация в папке docs/

Автор: Сергей Антропов
Сайт: https://devops.org.ru
2025-11-12 20:25:11 +03:00

371 lines
16 KiB
Python
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.

"""
Управление конфигурацией групп для различных мессенджеров.
Автор: Сергей Антропов
Сайт: 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()