mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
437 lines
16 KiB
Python
437 lines
16 KiB
Python
"""
|
|
Сервис управления триггерами сообщений.
|
|
Отвечает за проверку триггеров, их активацию и обновление статистики.
|
|
"""
|
|
|
|
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") |