dorm_alarm/handlers.py
2025-09-26 00:22:38 +03:00

256 lines
No EOL
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import logging
from datetime import datetime
from typing import Dict
from aiogram import F, Router
from aiogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
from aiogram.filters import Command
from sql import DatabaseManager
from init import bot
router = Router()
db = DatabaseManager()
# Глобальный реестр активных задач уведомлений для управления жизненным циклом
notification_tasks: Dict[int, asyncio.Task] = {}
@router.message(Command("start"))
async def start_handler(msg: Message) -> None:
"""Регистрация пользователя и инициализация уведомлений"""
user_id = msg.from_user.id
username = msg.from_user.username or msg.from_user.first_name
if db.add_user(user_id, username):
keyboard = create_main_keyboard()
await msg.answer(
f"<b>🔔 Система мониторинга активирована!</b>\n\n"
f"👤 Пользователь: <code>{username}</code>\n"
f"🆔 ID: <code>{user_id}</code>\n\n"
f"⏰ Интервал по умолчанию: <b>60 минут</b>\n"
f"📊 Статус уведомлений: <b>Включены</b>\n\n"
f"<i>Используйте кнопки ниже для настройки параметров</i>",
reply_markup=keyboard
)
# Запуск персональной задачи уведомлений
start_user_notifications(user_id)
else:
await msg.answer("❌ Ошибка инициализации системы")
@router.message(Command("status"))
async def status_handler(msg: Message) -> None:
"""Отображение текущего статуса пользователя"""
user_info = db.get_user_info(msg.from_user.id)
if not user_info:
await msg.answer("❌ Пользователь не найден. Используйте /start для регистрации")
return
user_id, username, interval, is_active = user_info
status_emoji = "🟢" if is_active else "🔴"
task_status = "Включены" if user_id in notification_tasks else "Остановлены"
keyboard = create_main_keyboard()
await msg.answer(
f"<b>📊 Статус системы мониторинга</b>\n\n"
f"Пользователь: <code>{username}</code>\n"
f"ID: <code>{user_id}</code>\n\n"
f"⏰ Интервал: <b>{interval} мин</b>\n"
f"🔄 Статус уведомлений: {status_emoji} <b>{task_status}</b>\n\n"
f"👥 Всего пользователей: <b>{db.get_user_count()}</b>",
reply_markup=keyboard
)
@router.message(Command("help"))
async def help_handler(msg: Message) -> None:
"""Справочная информация по командам"""
help_text = (
"<b>Бот мониторинга работоспособности</b>\n\n"
"<b>Доступные команды:</b>\n"
"/start - Регистрация и активация\n"
"/status - Текущий статус сервера\n"
"/help - Справочная информация\n\n"
"<b>Функциональность:</b>\n"
"• Периодические уведомления о работе\n"
"• Настройка интервала (5-1440 мин)\n"
"• Включение/отключение уведомлений"
)
await msg.answer(help_text)
# === CALLBACK HANDLERS ===
@router.callback_query(F.data == "toggle_notifications")
async def toggle_notifications_handler(call: CallbackQuery) -> None:
"""Переключение активности уведомлений"""
user_id = call.from_user.id
new_status = db.toggle_notifications(user_id)
if new_status is None:
await call.answer("❌ Ошибка обновления настроек")
return
if new_status:
start_user_notifications(user_id)
await call.answer("✅ Уведомления включены")
else:
stop_user_notifications(user_id)
await call.answer("⏸️ Уведомления отключены")
# Обновление интерфейса с новыми данными
await update_status_message(call)
@router.callback_query(F.data.startswith("set_interval_"))
async def set_interval_handler(call: CallbackQuery) -> None:
"""Установка нового интервала уведомлений"""
interval = int(call.data.split("_")[2])
user_id = call.from_user.id
if db.update_interval(user_id, interval):
# Перезапуск задачи с новым интервалом
restart_user_notifications(user_id)
await call.answer(f"⏰ Интервал установлен: {interval} мин")
await update_status_message(call)
else:
await call.answer("❌ Ошибка обновления интервала")
@router.callback_query(F.data == "interval_menu")
async def interval_menu_handler(call: CallbackQuery) -> None:
"""Отображение меню выбора интервала"""
keyboard = create_interval_keyboard()
await call.message.edit_reply_markup(reply_markup=keyboard)
@router.callback_query(F.data == "main_menu")
async def main_menu_handler(call: CallbackQuery) -> None:
"""Возврат к главному меню"""
keyboard = create_main_keyboard()
await call.message.edit_reply_markup(reply_markup=keyboard)
# === NOTIFICATION SYSTEM ===
async def send_notification(user_id: int) -> bool:
"""Отправка уведомления конкретному пользователю"""
try:
current_time = datetime.now().strftime("%H:%M:%S")
message = (
f"<b>🟢 Сервер работает</b>\n\n"
f"Время проверки: <code>{current_time}</code>\n"
f"Статус уведомлений: <b>Включены</b>"
)
await bot.send_message(user_id, message)
db.log_notification(user_id, "sent")
logging.info(f"Notification sent to user {user_id}")
return True
except Exception as e:
logging.error(f"Failed to send notification to {user_id}: {e}")
db.log_notification(user_id, "failed")
return False
async def notification_task(user_id: int, interval_minutes: int):
"""Асинхронная задача периодических уведомлений"""
while True:
try:
await asyncio.sleep(interval_minutes * 60)
# Проверка активности пользователя перед отправкой
user_info = db.get_user_info(user_id)
if user_info and user_info[3]: # is_active
await send_notification(user_id)
else:
logging.info(f"User {user_id} notifications disabled, stopping task")
break
except asyncio.CancelledError:
logging.info(f"Notification task for user {user_id} cancelled")
break
except Exception as e:
logging.error(f"Error in notification task for user {user_id}: {e}")
await asyncio.sleep(60) # Retry delay при ошибках
def start_user_notifications(user_id: int):
"""Запуск задачи уведомлений для пользователя"""
stop_user_notifications(user_id) # Остановка существующей задачи
user_info = db.get_user_info(user_id)
if user_info and user_info[3]: # is_active
interval = user_info[2] # interval_minutes
task = asyncio.create_task(notification_task(user_id, interval))
notification_tasks[user_id] = task
logging.info(f"Started notifications for user {user_id} with {interval}min interval")
def stop_user_notifications(user_id: int):
"""Остановка задачи уведомлений для пользователя"""
if user_id in notification_tasks:
notification_tasks[user_id].cancel()
del notification_tasks[user_id]
logging.info(f"Stopped notifications for user {user_id}")
def restart_user_notifications(user_id: int):
"""Перезапуск задачи уведомлений с обновленными параметрами"""
stop_user_notifications(user_id)
start_user_notifications(user_id)
# === UTILITY FUNCTIONS ===
def create_main_keyboard() -> InlineKeyboardMarkup:
"""Создание основной клавиатуры управления"""
buttons = [
[InlineKeyboardButton(text="Изменить интервал", callback_data="interval_menu")],
[InlineKeyboardButton(text="Вкл/выкл уведомления", callback_data="toggle_notifications")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
def create_interval_keyboard() -> InlineKeyboardMarkup:
"""Создание клавиатуры выбора интервала"""
intervals = [5, 15, 30, 60, 120, 360, 720, 1440] # От 5 минут до суток
buttons = []
# Группировка кнопок по 2 в ряду для компактности
for i in range(0, len(intervals), 2):
row = []
for j in range(i, min(i + 2, len(intervals))):
interval = intervals[j]
text = f"{interval}м" if interval < 60 else f"{interval//60}ч"
row.append(InlineKeyboardButton(
text=text,
callback_data=f"set_interval_{interval}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="↩️ Назад", callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
async def update_status_message(call: CallbackQuery):
"""Обновление сообщения со статусом"""
user_info = db.get_user_info(call.from_user.id)
if not user_info:
return
user_id, username, interval, is_active = user_info
status_emoji = "🟢" if is_active else "🔴"
task_status = "Включены" if user_id in notification_tasks else "Остановлены"
text = (
f"<b>📊 Статус системы мониторинга</b>\n\n"
f"Пользователь: <code>{username}</code>\n"
f"ID: <code>{user_id}</code>\n\n"
f"⏰ Интервал: <b>{interval} мин</b>\n"
f"🔄 Статус уведомлений: {status_emoji} <b>{task_status}</b>\n\n"
f"👥 Всего пользователей: <b>{db.get_user_count()}</b>",
)
try:
await call.message.edit_text(text, reply_markup=keyboard)
except:
pass # Игнорирование ошибок при неизменном контенте
# Старт функций
async def initialize_existing_users():
"""Инициализация уведомлений для существующих пользователей при запуске"""
active_users = db.get_active_users()
for user_id, interval in active_users:
start_user_notifications(user_id)
logging.info(f"Initialized notifications for {len(active_users)} active users")