Initial commit: Message Gateway project
- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
370
app/core/groups.py
Normal file
370
app/core/groups.py
Normal file
@@ -0,0 +1,370 @@
|
||||
"""
|
||||
Управление конфигурацией групп для различных мессенджеров.
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: 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()
|
||||
Reference in New Issue
Block a user