637 lines
19 KiB
Python
637 lines
19 KiB
Python
"""
|
||
Утилиты для автоматического удаления сообщений
|
||
"""
|
||
from typing import Optional, Callable, Awaitable, Dict, Any
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime, timedelta
|
||
from asyncio import sleep, create_task, Task, CancelledError
|
||
|
||
from aiogram import Bot
|
||
from aiogram.types import Message
|
||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||
|
||
from middleware.loggers import logger
|
||
from .format_time import format_duration
|
||
|
||
__all__ = (
|
||
'auto_delete_message',
|
||
'schedule_delete',
|
||
'cancel_delete',
|
||
'delete_after',
|
||
'auto_delete_manager',
|
||
'AutoDeleteManager',
|
||
'DeleteTask',
|
||
'delete_both_after',
|
||
'delete_messages_after',
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class DeleteTask:
|
||
"""
|
||
Задача на удаление сообщения.
|
||
|
||
Attributes:
|
||
chat_id: ID чата
|
||
message_id: ID сообщения
|
||
delete_at: Время удаления
|
||
task: Asyncio task
|
||
created_at: Время создания задачи
|
||
reason: Причина удаления
|
||
callback: Callback функция после удаления
|
||
"""
|
||
chat_id: int
|
||
message_id: int
|
||
delete_at: datetime
|
||
task: Optional[Task] = None
|
||
created_at: datetime = field(default_factory=datetime.now)
|
||
reason: Optional[str] = None
|
||
callback: Optional[Callable[[], Awaitable[None]]] = None
|
||
|
||
@property
|
||
def delay(self) -> int:
|
||
"""Задержка до удаления в секундах"""
|
||
delta = self.delete_at - datetime.now()
|
||
return max(0, int(delta.total_seconds()))
|
||
|
||
@property
|
||
def is_expired(self) -> bool:
|
||
"""Истекло ли время удаления"""
|
||
return datetime.now() >= self.delete_at
|
||
|
||
def __repr__(self) -> str:
|
||
return (
|
||
f"DeleteTask(chat={self.chat_id}, msg={self.message_id}, "
|
||
f"delay={self.delay}s, reason={self.reason})"
|
||
)
|
||
|
||
|
||
class AutoDeleteManager:
|
||
"""
|
||
Менеджер автоматического удаления сообщений.
|
||
|
||
Возможности:
|
||
- Планирование удаления с задержкой
|
||
- Отмена запланированного удаления
|
||
- Массовое удаление
|
||
- Callback функции
|
||
- История задач
|
||
- Автоматическая очистка завершенных задач
|
||
|
||
Example:
|
||
```python
|
||
from utils.auto_delete import auto_delete_manager
|
||
|
||
# Планирование удаления
|
||
await auto_delete_manager.schedule(
|
||
bot=bot,
|
||
chat_id=message.chat.id,
|
||
message_id=message.message_id,
|
||
delay=60,
|
||
reason="Временное сообщение"
|
||
)
|
||
|
||
# Отмена удаления
|
||
auto_delete_manager.cancel(message.chat.id, message.message_id)
|
||
|
||
# Получение статистики
|
||
stats = auto_delete_manager.get_stats()
|
||
```
|
||
"""
|
||
|
||
def __init__(self):
|
||
# Активные задачи: {(chat_id, message_id): DeleteTask}
|
||
self.tasks: Dict[tuple[int, int], DeleteTask] = {}
|
||
|
||
# Завершенные задачи (последние 100)
|
||
self.completed: list[DeleteTask] = []
|
||
self.max_completed = 100
|
||
|
||
# Статистика
|
||
self.total_scheduled: int = 0
|
||
self.total_deleted: int = 0
|
||
self.total_failed: int = 0
|
||
self.total_cancelled: int = 0
|
||
|
||
async def schedule(
|
||
self,
|
||
bot: Bot,
|
||
chat_id: int,
|
||
message_id: int,
|
||
delay: int,
|
||
reason: Optional[str] = None,
|
||
callback: Optional[Callable[[], Awaitable[None]]] = None,
|
||
log: bool = True
|
||
) -> DeleteTask:
|
||
"""
|
||
Планирует удаление сообщения.
|
||
|
||
Args:
|
||
bot: Экземпляр бота
|
||
chat_id: ID чата
|
||
message_id: ID сообщения
|
||
delay: Задержка в секундах
|
||
reason: Причина удаления
|
||
callback: Callback функция после удаления
|
||
log: Логировать планирование
|
||
|
||
Returns:
|
||
DeleteTask: Созданная задача
|
||
|
||
Example:
|
||
>> task = await auto_delete_manager.schedule(
|
||
... bot=bot,
|
||
... chat_id=message.chat.id,
|
||
... message_id=message.message_id,
|
||
... delay=60,
|
||
... reason="Спам"
|
||
... )
|
||
"""
|
||
# Отменяем предыдущую задачу если есть
|
||
key = (chat_id, message_id)
|
||
if key in self.tasks:
|
||
self.cancel(chat_id, message_id)
|
||
|
||
# Создаем задачу
|
||
delete_at = datetime.now() + timedelta(seconds=delay)
|
||
task_data = DeleteTask(
|
||
chat_id=chat_id,
|
||
message_id=message_id,
|
||
delete_at=delete_at,
|
||
reason=reason,
|
||
callback=callback
|
||
)
|
||
|
||
# Создаем asyncio task
|
||
task = create_task(self._delete_task(bot, task_data, log))
|
||
task_data.task = task
|
||
|
||
# Сохраняем
|
||
self.tasks[key] = task_data
|
||
self.total_scheduled += 1
|
||
|
||
if log:
|
||
delay_str = format_duration(delay)
|
||
logger.info(
|
||
f"Запланировано удаление сообщения через {delay_str}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
return task_data
|
||
|
||
async def _delete_task(
|
||
self,
|
||
bot: Bot,
|
||
task_data: DeleteTask,
|
||
log: bool
|
||
) -> None:
|
||
"""
|
||
Внутренняя функция для выполнения задачи удаления.
|
||
|
||
Args:
|
||
bot: Экземпляр бота
|
||
task_data: Данные задачи
|
||
log: Логировать выполнение
|
||
"""
|
||
key = (task_data.chat_id, task_data.message_id)
|
||
|
||
try:
|
||
# Ждем
|
||
await sleep(task_data.delay)
|
||
|
||
# Удаляем сообщение
|
||
await bot.delete_message(
|
||
chat_id=task_data.chat_id,
|
||
message_id=task_data.message_id
|
||
)
|
||
|
||
self.total_deleted += 1
|
||
|
||
if log:
|
||
reason_str = f" (причина: {task_data.reason})" if task_data.reason else ""
|
||
logger.info(
|
||
f"Сообщение удалено автоматически{reason_str}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
# Вызываем callback если есть
|
||
if task_data.callback:
|
||
try:
|
||
await task_data.callback()
|
||
except Exception as e:
|
||
logger.error(
|
||
f"Ошибка в callback автоудаления: {e}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
except CancelledError:
|
||
# Задача отменена
|
||
self.total_cancelled += 1
|
||
|
||
if log:
|
||
logger.debug(
|
||
f"Удаление сообщения отменено",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
raise
|
||
|
||
except (TelegramBadRequest, TelegramForbiddenError) as e:
|
||
# Ошибка удаления
|
||
self.total_failed += 1
|
||
|
||
if log:
|
||
logger.warning(
|
||
f"Не удалось автоматически удалить сообщение: {e}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
finally:
|
||
# Удаляем из активных задач
|
||
if key in self.tasks:
|
||
completed_task = self.tasks.pop(key)
|
||
|
||
# Сохраняем в завершенные
|
||
self.completed.append(completed_task)
|
||
if len(self.completed) > self.max_completed:
|
||
self.completed.pop(0)
|
||
|
||
def cancel(
|
||
self,
|
||
chat_id: int,
|
||
message_id: int,
|
||
log: bool = True
|
||
) -> bool:
|
||
"""
|
||
Отменяет запланированное удаление.
|
||
|
||
Args:
|
||
chat_id: ID чата
|
||
message_id: ID сообщения
|
||
log: Логировать отмену
|
||
|
||
Returns:
|
||
bool: True если задача была отменена
|
||
|
||
Example:
|
||
>> cancelled = auto_delete_manager.cancel(
|
||
... chat_id=message.chat.id,
|
||
... message_id=message.message_id
|
||
... )
|
||
"""
|
||
key = (chat_id, message_id)
|
||
|
||
if key in self.tasks:
|
||
task_data = self.tasks[key]
|
||
|
||
# Отменяем asyncio task
|
||
if task_data.task and not task_data.task.done():
|
||
task_data.task.cancel()
|
||
|
||
# Удаляем из активных
|
||
self.tasks.pop(key)
|
||
|
||
if log:
|
||
logger.debug(
|
||
f"Автоудаление отменено для сообщения {message_id}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
return True
|
||
|
||
return False
|
||
|
||
def cancel_all(self, chat_id: Optional[int] = None) -> int:
|
||
"""
|
||
Отменяет все запланированные удаления.
|
||
|
||
Args:
|
||
chat_id: ID чата (если None, отменяет для всех чатов)
|
||
|
||
Returns:
|
||
int: Количество отмененных задач
|
||
|
||
Example:
|
||
>> # Отменить для всех чатов
|
||
>> count = auto_delete_manager.cancel_all()
|
||
|
||
>> # Отменить для конкретного чата
|
||
>> count = auto_delete_manager.cancel_all(chat_id=message.chat.id)
|
||
"""
|
||
cancelled_count = 0
|
||
|
||
# Собираем ключи для отмены
|
||
keys_to_cancel = []
|
||
for key, task_data in self.tasks.items():
|
||
if chat_id is None or task_data.chat_id == chat_id:
|
||
keys_to_cancel.append(key)
|
||
|
||
# Отменяем
|
||
for key in keys_to_cancel:
|
||
if self.cancel(key[0], key[1], log=False):
|
||
cancelled_count += 1
|
||
|
||
if cancelled_count > 0:
|
||
logger.info(
|
||
f"Отменено {cancelled_count} задач автоудаления",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
return cancelled_count
|
||
|
||
def get_task(
|
||
self,
|
||
chat_id: int,
|
||
message_id: int
|
||
) -> Optional[DeleteTask]:
|
||
"""
|
||
Получает задачу по ID чата и сообщения.
|
||
|
||
Args:
|
||
chat_id: ID чата
|
||
message_id: ID сообщения
|
||
|
||
Returns:
|
||
Optional[DeleteTask]: Задача или None
|
||
"""
|
||
key = (chat_id, message_id)
|
||
return self.tasks.get(key)
|
||
|
||
def get_chat_tasks(self, chat_id: int) -> list[DeleteTask]:
|
||
"""
|
||
Получает все задачи для чата.
|
||
|
||
Args:
|
||
chat_id: ID чата
|
||
|
||
Returns:
|
||
list[DeleteTask]: Список задач
|
||
"""
|
||
return [
|
||
task for task in self.tasks.values()
|
||
if task.chat_id == chat_id
|
||
]
|
||
|
||
def get_stats(self) -> Dict[str, Any]:
|
||
"""
|
||
Возвращает статистику менеджера.
|
||
|
||
Returns:
|
||
Dict: Словарь со статистикой
|
||
|
||
Example:
|
||
>> stats = auto_delete_manager.get_stats()
|
||
>> print(f"Активных задач: {stats['active_tasks']}")
|
||
"""
|
||
return {
|
||
'active_tasks': len(self.tasks),
|
||
'completed_tasks': len(self.completed),
|
||
'total_scheduled': self.total_scheduled,
|
||
'total_deleted': self.total_deleted,
|
||
'total_failed': self.total_failed,
|
||
'total_cancelled': self.total_cancelled,
|
||
'success_rate': (
|
||
f"{(self.total_deleted / self.total_scheduled * 100):.1f}%"
|
||
if self.total_scheduled > 0 else "0%"
|
||
)
|
||
}
|
||
|
||
def cleanup_expired(self) -> int:
|
||
"""
|
||
Удаляет истекшие задачи (которые должны были выполниться, но не выполнились).
|
||
|
||
Returns:
|
||
int: Количество удаленных задач
|
||
"""
|
||
expired_keys = [
|
||
key for key, task in self.tasks.items()
|
||
if task.is_expired and (not task.task or task.task.done())
|
||
]
|
||
|
||
for key in expired_keys:
|
||
self.tasks.pop(key)
|
||
|
||
return len(expired_keys)
|
||
|
||
|
||
# Глобальный менеджер
|
||
auto_delete_manager = AutoDeleteManager()
|
||
|
||
|
||
# ================= УДОБНЫЕ ФУНКЦИИ =================
|
||
|
||
async def auto_delete_message(
|
||
bot: Bot,
|
||
chat_id: int,
|
||
message_id: int,
|
||
delay: int = 604800,
|
||
reason: Optional[str] = None
|
||
) -> DeleteTask:
|
||
"""
|
||
Автоматически удаляет сообщение через указанное время.
|
||
|
||
Args:
|
||
bot: Экземпляр бота
|
||
chat_id: ID чата
|
||
message_id: ID сообщения
|
||
delay: Задержка в секундах (по умолчанию 7 дней)
|
||
reason: Причина удаления
|
||
|
||
Returns:
|
||
DeleteTask: Созданная задача
|
||
|
||
Example:
|
||
>> # Удалить через 1 минуту
|
||
>> await auto_delete_message(bot, chat_id, message_id, delay=60)
|
||
|
||
>> # Удалить через 7 дней (по умолчанию)
|
||
>> await auto_delete_message(bot, chat_id, message_id)
|
||
"""
|
||
return await auto_delete_manager.schedule(
|
||
bot=bot,
|
||
chat_id=chat_id,
|
||
message_id=message_id,
|
||
delay=delay,
|
||
reason=reason
|
||
)
|
||
|
||
|
||
async def schedule_delete(
|
||
message: Message,
|
||
delay: int,
|
||
reason: Optional[str] = None
|
||
) -> DeleteTask:
|
||
"""
|
||
Планирует удаление сообщения (упрощенная версия).
|
||
|
||
Args:
|
||
message: Объект сообщения
|
||
delay: Задержка в секундах
|
||
reason: Причина удаления
|
||
|
||
Returns:
|
||
DeleteTask: Созданная задача
|
||
|
||
Example:
|
||
>> # Планируем удаление через 30 секунд
|
||
>> await schedule_delete(message, delay=30, reason="Временное")
|
||
"""
|
||
return await auto_delete_manager.schedule(
|
||
bot=message.bot,
|
||
chat_id=message.chat.id,
|
||
message_id=message.message_id,
|
||
delay=delay,
|
||
reason=reason
|
||
)
|
||
|
||
|
||
def cancel_delete(message: Message) -> bool:
|
||
"""
|
||
Отменяет запланированное удаление сообщения.
|
||
|
||
Args:
|
||
message: Объект сообщения
|
||
|
||
Returns:
|
||
bool: True если удаление было отменено
|
||
|
||
Example:
|
||
>> if cancel_delete(message):
|
||
... await message.answer("Удаление отменено")
|
||
"""
|
||
return auto_delete_manager.cancel(
|
||
chat_id=message.chat.id,
|
||
message_id=message.message_id
|
||
)
|
||
|
||
|
||
async def delete_after(
|
||
message: Message,
|
||
text: str,
|
||
delay: int = 10,
|
||
**kwargs
|
||
) -> Message:
|
||
"""
|
||
Отправляет сообщение и автоматически удаляет его через указанное время.
|
||
|
||
Args:
|
||
message: Исходное сообщение
|
||
text: Текст нового сообщения
|
||
delay: Задержка до удаления в секундах
|
||
**kwargs: Дополнительные параметры для message.answer()
|
||
|
||
Returns:
|
||
Message: Отправленное сообщение
|
||
|
||
Example:
|
||
>> # Отправить и удалить через 10 секунд
|
||
>> await delete_after(message, "Это временное сообщение")
|
||
|
||
>> # Отправить и удалить через 5 секунд
|
||
>> await delete_after(
|
||
... message,
|
||
... "⚠️ Ошибка!",
|
||
... delay=5,
|
||
... parse_mode="HTML"
|
||
... )
|
||
"""
|
||
sent_message = await message.answer(text, **kwargs)
|
||
|
||
await auto_delete_manager.schedule(
|
||
bot=message.bot,
|
||
chat_id=sent_message.chat.id,
|
||
message_id=sent_message.message_id,
|
||
delay=delay,
|
||
reason="delete_after"
|
||
)
|
||
|
||
return sent_message
|
||
|
||
|
||
async def delete_both_after(
|
||
original: Message,
|
||
reply_text: str,
|
||
delay: int = 10,
|
||
**kwargs
|
||
) -> Message:
|
||
"""
|
||
Отправляет ответ и удаляет оба сообщения через указанное время.
|
||
|
||
Args:
|
||
original: Исходное сообщение
|
||
reply_text: Текст ответа
|
||
delay: Задержка до удаления
|
||
**kwargs: Дополнительные параметры
|
||
|
||
Returns:
|
||
Message: Отправленное сообщение
|
||
|
||
Example:
|
||
>> # Удалить и команду, и ответ через 5 секунд
|
||
>> await delete_both_after(
|
||
... message,
|
||
... "✅ Команда выполнена",
|
||
... delay=5
|
||
... )
|
||
"""
|
||
# Отправляем ответ
|
||
sent = await delete_after(original, reply_text, delay, **kwargs)
|
||
|
||
# Планируем удаление оригинала
|
||
await auto_delete_manager.schedule(
|
||
bot=original.bot,
|
||
chat_id=original.chat.id,
|
||
message_id=original.message_id,
|
||
delay=delay,
|
||
reason="delete_both"
|
||
)
|
||
|
||
return sent
|
||
|
||
|
||
async def delete_messages_after(
|
||
bot: Bot,
|
||
chat_id: int,
|
||
message_ids: list[int],
|
||
delay: int
|
||
) -> int:
|
||
"""
|
||
Планирует удаление нескольких сообщений.
|
||
|
||
Args:
|
||
bot: Экземпляр бота
|
||
chat_id: ID чата
|
||
message_ids: Список ID сообщений
|
||
delay: Задержка до удаления
|
||
|
||
Returns:
|
||
int: Количество запланированных удалений
|
||
|
||
Example:
|
||
>> # Удалить все сообщения через 1 час
|
||
>> count = await delete_messages_after(
|
||
... bot,
|
||
... chat_id,
|
||
... [123, 124, 125, 126],
|
||
... delay=3600
|
||
... )
|
||
"""
|
||
count = 0
|
||
|
||
for message_id in message_ids:
|
||
await auto_delete_manager.schedule(
|
||
bot=bot,
|
||
chat_id=chat_id,
|
||
message_id=message_id,
|
||
delay=delay,
|
||
reason="mass_delete",
|
||
log=False
|
||
)
|
||
count += 1
|
||
|
||
logger.info(
|
||
f"Запланировано удаление {count} сообщений через {format_duration(delay)}",
|
||
log_type='AUTO_DELETE'
|
||
)
|
||
|
||
return count
|