Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+437
View File
@@ -0,0 +1,437 @@
"""
Сервис управления триггерами сообщений.
Отвечает за проверку триггеров, их активацию и обновление статистики.
"""
import re
import logging
from typing import List, Dict, Optional, Tuple, Any
from datetime import datetime
from database.repository import TriggerRepository
from core.exceptions import ValidationError, DatabaseError
class TriggerService:
"""
Сервис для управления триггерами сообщений.
Отвечает за:
- Проверку текста сообщений на совпадение с триггерами
- Выполнение действий при срабатывании триггеров
- Обновление статистики использования триггеров
- Управление активными триггерами
"""
def __init__(self, database_url: str):
"""
Инициализация сервиса триггеров.
Args:
database_url: URL базы данных
"""
self.logger = logging.getLogger(__name__)
self.repository = TriggerRepository(database_url)
self._triggers_cache: Optional[List[Dict]] = None
self._last_cache_update: Optional[datetime] = None
self._cache_ttl_seconds = 300 # 5 минут
async def check_triggers(self, message_text: str, chat_type: str = "group") -> List[Dict]:
"""
Проверка текста сообщения на совпадение с активными триггерами.
Args:
message_text: Текст сообщения для проверки
chat_type: Тип чата ("group" или "private")
Returns:
Список сработавших триггеров с их данными
"""
if not message_text or not isinstance(message_text, str):
return []
try:
# Получаем активные триггеры
active_triggers = await self.get_active_triggers(chat_type)
if not active_triggers:
return []
matched_triggers = []
for trigger in active_triggers:
try:
# Компилируем регулярное выражение
pattern = re.compile(trigger['pattern'], re.IGNORECASE | re.MULTILINE)
# Проверяем совпадение
if pattern.search(message_text.strip()):
matched_triggers.append(trigger)
# Обновляем статистику асинхронно (не ждем завершения)
self._update_trigger_stats_async(trigger['id'])
except re.error as e:
self.logger.error(f"Invalid regex pattern in trigger {trigger['id']}: {e}")
continue
except Exception as e:
self.logger.error(f"Error checking trigger {trigger['id']}: {e}")
continue
return matched_triggers
except Exception as e:
self.logger.error(f"Error in check_triggers: {e}")
return []
async def get_active_triggers(self, chat_type: str = "group") -> List[Dict]:
"""
Получение активных триггеров для указанного типа чата.
Args:
chat_type: Тип чата ("group" или "private")
Returns:
Список активных триггеров
"""
try:
# Проверяем кеш
if self._should_use_cache():
return self._triggers_cache or []
# Получаем из базы данных
triggers = await self.repository.get_active_triggers_async(chat_type)
# Обновляем кеш
self._triggers_cache = triggers
self._last_cache_update = datetime.now()
return triggers
except DatabaseError as e:
self.logger.error(f"Database error getting active triggers: {e}")
return self._triggers_cache or []
except Exception as e:
self.logger.error(f"Error getting active triggers: {e}")
return []
async def execute_trigger_actions(self, matched_triggers: List[Dict],
chat_id: int, message_text: str, message_id: int = None) -> List[Dict]:
"""
Выполнение действий для сработавших триггеров.
Args:
matched_triggers: Список сработавших триггеров
chat_id: ID чата
message_text: Оригинальный текст сообщения
message_id: ID сообщения (для реакций)
Returns:
Список действий для выполнения
"""
actions = []
for trigger in matched_triggers:
try:
trigger_actions = self._build_trigger_actions(trigger, chat_id, message_text, message_id)
actions.extend(trigger_actions)
# Обновляем статистику
await self.update_trigger_stats(trigger['id'])
except Exception as e:
self.logger.error(f"Error executing actions for trigger {trigger['id']}: {e}")
continue
return actions
def _build_trigger_actions(self, trigger: Dict, chat_id: int, message_text: str, message_id: int = None) -> List[Dict]:
"""
Построение списка действий для триггера.
Args:
trigger: Данные триггера
chat_id: ID чата
message_text: Текст сообщения
message_id: ID сообщения (для реакций)
Returns:
Список действий
"""
actions = []
# Текстовый ответ
if trigger.get('response_text'):
actions.append({
'type': 'text',
'content': trigger['response_text'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# Стикер
if trigger.get('response_sticker'):
actions.append({
'type': 'sticker',
'content': trigger['response_sticker'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# GIF
if trigger.get('response_gif'):
actions.append({
'type': 'gif',
'content': trigger['response_gif'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# Реакция
if trigger.get('reaction_type') and trigger.get('action_data') and message_id is not None:
actions.append({
'type': 'reaction',
'reaction_type': trigger['reaction_type'],
'content': trigger['action_data'], # emoji
'message_id': message_id,
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
return actions
async def update_trigger_stats(self, trigger_id: int) -> bool:
"""
Обновление статистики срабатывания триггера.
Args:
trigger_id: ID триггера
Returns:
True если обновление успешно
"""
try:
success = self.repository.update_trigger_stats(trigger_id)
# Инвалидируем кеш
self._invalidate_cache()
return success
except Exception as e:
self.logger.error(f"Error updating trigger stats for {trigger_id}: {e}")
return False
def _update_trigger_stats_async(self, trigger_id: int):
"""Асинхронное обновление статистики триггера (не ждет завершения)"""
import asyncio
asyncio.create_task(self.update_trigger_stats(trigger_id))
async def add_trigger(self, trigger_data: Dict) -> Optional[int]:
"""
Добавление нового триггера.
Args:
trigger_data: Данные триггера
Returns:
ID созданного триггера или None при ошибке
"""
try:
# Валидация данных
self._validate_trigger_data(trigger_data)
# Проверяем регулярное выражение
self._validate_regex_pattern(trigger_data['pattern'])
trigger_id = self.repository.add_trigger(trigger_data)
# Инвалидируем кеш
self._invalidate_cache()
self.logger.info(f"Created new trigger: {trigger_data['name']} (ID: {trigger_id})")
return trigger_id
except ValidationError as e:
self.logger.warning(f"Validation error creating trigger: {e}")
raise
except Exception as e:
self.logger.error(f"Error creating trigger: {e}")
return None
async def update_trigger(self, trigger_id: int, update_data: Dict) -> bool:
"""
Обновление триггера.
Args:
trigger_id: ID триггера
update_data: Данные для обновления
Returns:
True если обновление успешно
"""
try:
# Валидация данных
if 'pattern' in update_data:
self._validate_regex_pattern(update_data['pattern'])
success = self.repository.update_trigger(trigger_id, update_data)
# Инвалидируем кеш
self._invalidate_cache()
if success:
self.logger.info(f"Updated trigger {trigger_id}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for update")
return success
except ValidationError as e:
self.logger.warning(f"Validation error updating trigger {trigger_id}: {e}")
raise
except Exception as e:
self.logger.error(f"Error updating trigger {trigger_id}: {e}")
return False
async def delete_trigger(self, trigger_id: int) -> bool:
"""
Удаление триггера.
Args:
trigger_id: ID триггера
Returns:
True если удаление успешно
"""
try:
success = self.repository.delete_trigger(trigger_id)
# Инвалидируем кеш
self._invalidate_cache()
if success:
self.logger.info(f"Deleted trigger {trigger_id}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for deletion")
return success
except Exception as e:
self.logger.error(f"Error deleting trigger {trigger_id}: {e}")
return False
async def toggle_trigger(self, trigger_id: int, is_active: bool) -> bool:
"""
Включение/выключение триггера.
Args:
trigger_id: ID триггера
is_active: Статус активности
Returns:
True если обновление успешно
"""
try:
success = self.repository.toggle_trigger(trigger_id, is_active)
# Инвалидируем кеш
self._invalidate_cache()
if success:
status = "activated" if is_active else "deactivated"
self.logger.info(f"Trigger {trigger_id} {status}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for toggle")
return success
except Exception as e:
self.logger.error(f"Error toggling trigger {trigger_id}: {e}")
return False
async def get_all_triggers(self) -> List[Dict]:
"""
Получение всех триггеров (для администрирования).
Returns:
Список всех триггеров
"""
try:
return self.repository.get_all_triggers()
except Exception as e:
self.logger.error(f"Error getting all triggers: {e}")
return []
async def get_trigger_by_id(self, trigger_id: int) -> Optional[Dict]:
"""
Получение триггера по ID.
Args:
trigger_id: ID триггера
Returns:
Данные триггера или None
"""
try:
return self.repository.get_trigger_by_id(trigger_id)
except Exception as e:
self.logger.error(f"Error getting trigger {trigger_id}: {e}")
return None
def _validate_trigger_data(self, data: Dict):
"""Валидация данных триггера"""
if not data.get('name') or not isinstance(data['name'], str):
raise ValidationError("Trigger name is required and must be a string")
if not data.get('pattern') or not isinstance(data['pattern'], str):
raise ValidationError("Trigger pattern is required and must be a string")
if len(data['name'].strip()) < 1 or len(data['name']) > 100:
raise ValidationError("Trigger name must be between 1 and 100 characters")
if len(data['pattern'].strip()) < 1 or len(data['pattern']) > 500:
raise ValidationError("Trigger pattern must be between 1 and 500 characters")
chat_type = data.get('chat_type', 'group')
if chat_type not in ['group', 'private']:
raise ValidationError("Chat type must be 'group' or 'private'")
# Валидация полей реакции
reaction_type = data.get('reaction_type')
if reaction_type is not None:
if not isinstance(reaction_type, str):
raise ValidationError("Reaction type must be a string")
if reaction_type not in ['emoji']:
raise ValidationError("Reaction type must be 'emoji'")
# action_data обязательно при наличии reaction_type
if not data.get('action_data') or not isinstance(data['action_data'], str):
raise ValidationError("Action data is required when reaction type is specified")
if len(data['action_data'].strip()) < 1 or len(data['action_data']) > 100:
raise ValidationError("Action data must be between 1 and 100 characters")
def _validate_regex_pattern(self, pattern: str):
"""Валидация регулярного выражения"""
try:
re.compile(pattern, re.IGNORECASE | re.MULTILINE)
except re.error as e:
raise ValidationError(f"Invalid regex pattern: {e}")
def _should_use_cache(self) -> bool:
"""Проверка необходимости использования кеша"""
if self._triggers_cache is None or self._last_cache_update is None:
return False
time_diff = (datetime.now() - self._last_cache_update).total_seconds()
return time_diff < self._cache_ttl_seconds
def _invalidate_cache(self):
"""Инвалидация кеша"""
self._triggers_cache = None
self._last_cache_update = None
async def clear_cache(self):
"""Очистка кеша"""
self._invalidate_cache()
self.logger.debug("Trigger cache cleared")