diff --git a/bot/tasks/cleanup.py b/bot/tasks/cleanup.py new file mode 100644 index 0000000..5033258 --- /dev/null +++ b/bot/tasks/cleanup.py @@ -0,0 +1,263 @@ +""" +Фоновые задачи для автоматической очистки и обслуживания бота +""" +import asyncio +from datetime import datetime +from pathlib import Path +import shutil + +from database import get_manager +from middleware.loggers import logger +from configs import settings + +__all__ = ('cleanup_expired_banwords', 'cleanup_spam_stats', 'auto_backup_database', 'start_background_tasks', 'check_system_health') + + +async def cleanup_expired_banwords() -> None: + """ + Периодически удаляет истёкшие временные банворды. + + Запускается каждый час и проверяет все временные слова. + Удаляет те, у которых истёк срок действия. + """ + logger.info("🔄 Запущена задача автоочистки временных банвордов", log_type="CLEANUP") + + manager = get_manager() + + while True: + try: + # Ждём 1 час между проверками + await asyncio.sleep(3600) + + # Выполняем очистку + deleted = await manager.cleanup_expired_temp_words() + + if deleted > 0: + logger.info( + f"✅ Очищено {deleted} истёкших временных банвордов", + log_type="CLEANUP" + ) + else: + logger.debug( + "✓ Проверка временных банвордов: истёкших не найдено", + log_type="CLEANUP" + ) + + except Exception as e: + logger.error( + f"❌ Ошибка в задаче очистки банвордов: {e}", + log_type="CLEANUP" + ) + # При ошибке ждём 5 минут перед следующей попыткой + await asyncio.sleep(300) + + +async def cleanup_spam_stats(max_age_days: int = 30) -> None: + """ + Очищает старую статистику антиспама. + + Запускается каждые 24 часа и удаляет записи старше max_age_days дней. + + Args: + max_age_days: Возраст записей для удаления (по умолчанию 30 дней) + """ + logger.info( + f"🔄 Запущена задача очистки статистики (возраст > {max_age_days} дней)", + log_type="CLEANUP" + ) + + from bot.middlewares.spam_mdw import spam_stats + + while True: + try: + # Ждём 24 часа между проверками + await asyncio.sleep(86400) + + # Очищаем старую статистику + max_age_seconds = max_age_days * 86400 + deleted = spam_stats.cleanup(max_age=max_age_seconds) + + if deleted > 0: + logger.info( + f"✅ Очищена статистика: {deleted} неактивных пользователей", + log_type="CLEANUP" + ) + else: + logger.debug( + "✓ Проверка статистики: старых записей не найдено", + log_type="CLEANUP" + ) + + except Exception as e: + logger.error( + f"❌ Ошибка в задаче очистки статистики: {e}", + log_type="CLEANUP" + ) + await asyncio.sleep(3600) # При ошибке ждём 1 час + + +async def auto_backup_database(backup_interval_hours: int = 24, keep_backups: int = 7) -> None: + """ + Автоматически создаёт резервные копии базы данных. + + Args: + backup_interval_hours: Интервал между бэкапами (по умолчанию 24 часа) + keep_backups: Количество хранимых копий (по умолчанию 7) + """ + logger.info( + f"🔄 Запущена задача автобэкапа (каждые {backup_interval_hours}ч, хранить {keep_backups})", + log_type="CLEANUP" + ) + + while True: + try: + # Ждём указанное время + await asyncio.sleep(backup_interval_hours * 3600) + + # Путь к базе данных + db_path = Path(settings.DATABASE_PATH) + + if not db_path.exists(): + logger.warning( + f"⚠️ База данных не найдена: {db_path}", + log_type="CLEANUP" + ) + continue + + # Создаём папку для бэкапов + backup_dir = Path("backups") + backup_dir.mkdir(exist_ok=True) + + # Имя файла бэкапа с датой и временем + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = backup_dir / f"banwords_backup_{timestamp}.db" + + # Копируем базу данных + shutil.copy2(db_path, backup_path) + + # Получаем размер файла + backup_size = backup_path.stat().st_size / 1024 # KB + + logger.info( + f"✅ Создан backup: {backup_path.name} ({backup_size:.1f} KB)", + log_type="CLEANUP" + ) + + # Удаляем старые бэкапы (оставляем только последние N) + backups = sorted(backup_dir.glob("banwords_backup_*.db")) + + if len(backups) > keep_backups: + old_backups = backups[:-keep_backups] + + for old_backup in old_backups: + old_backup.unlink() + logger.debug( + f"🗑️ Удалён старый backup: {old_backup.name}", + log_type="CLEANUP" + ) + + logger.info( + f"✓ Удалено старых бэкапов: {len(old_backups)}", + log_type="CLEANUP" + ) + + except Exception as e: + logger.error( + f"❌ Ошибка создания backup: {e}", + log_type="CLEANUP" + ) + await asyncio.sleep(3600) # При ошибке ждём 1 час + + +async def check_system_health() -> None: + """ + Мониторинг здоровья системы. + + Проверяет каждые 5 минут: + - Размер базы данных + - Количество записей в БД + - Использование памяти (статистика антиспама) + """ + logger.info("🔄 Запущена задача мониторинга системы", log_type="CLEANUP") + + manager = get_manager() + + while True: + try: + await asyncio.sleep(300) # 5 минут + + # Проверяем размер БД + db_path = Path(settings.DATABASE_PATH) + if db_path.exists(): + db_size_mb = db_path.stat().st_size / (1024 * 1024) + + if db_size_mb > 100: # Если больше 100 MB + logger.warning( + f"⚠️ Большой размер БД: {db_size_mb:.2f} MB", + log_type="CLEANUP" + ) + + # Проверяем количество временных слов + stats = await manager.get_stats() + temp_count = stats.get('temp_total', 0) + + if temp_count > 100: # Если больше 100 временных слов + logger.warning( + f"⚠️ Много временных банвордов: {temp_count}", + log_type="CLEANUP" + ) + + # Проверяем статистику антиспама + from bot.middlewares.spam_mdw import spam_stats + spam_summary = spam_stats.get_stats_summary() + + active_blocks = spam_summary.get('active_blocks', 0) + if active_blocks > 10: + logger.warning( + f"⚠️ Много активных блокировок: {active_blocks}", + log_type="CLEANUP" + ) + + except Exception as e: + logger.error( + f"❌ Ошибка в задаче мониторинга: {e}", + log_type="CLEANUP" + ) + await asyncio.sleep(600) # При ошибке ждём 10 минут + + +def start_background_tasks() -> list[asyncio.Task]: + """ + Запускает все фоновые задачи. + + Returns: + List[asyncio.Task]: Список запущенных задач + """ + tasks = [] + + # 1. Автоочистка временных банвордов (каждый час) + task1 = asyncio.create_task(cleanup_expired_banwords()) + tasks.append(task1) + logger.info("✅ Задача 'cleanup_expired_banwords' запущена", log_type="STARTUP") + + # 2. Очистка старой статистики (каждые 24 часа) + task2 = asyncio.create_task(cleanup_spam_stats(max_age_days=30)) + tasks.append(task2) + logger.info("✅ Задача 'cleanup_spam_stats' запущена", log_type="STARTUP") + + # 3. Автобэкап базы данных (каждые 24 часа, хранить 7 копий) + task3 = asyncio.create_task(auto_backup_database(backup_interval_hours=24, keep_backups=7)) + tasks.append(task3) + logger.info("✅ Задача 'auto_backup_database' запущена", log_type="STARTUP") + + # 4. Мониторинг здоровья системы (каждые 5 минут) + task4 = asyncio.create_task(check_system_health()) + tasks.append(task4) + logger.info("✅ Задача 'check_system_health' запущена", log_type="STARTUP") + + logger.success( + f"🚀 Все фоновые задачи запущены: {len(tasks)} шт.", + log_type="STARTUP" + ) + + return tasks