mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -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")
|
||||
Reference in New Issue
Block a user