2025-12-19 11:19:54 +01:00
|
|
|
|
"""Handlers for inline button callbacks (done, snooze, pause, delete)."""
|
|
|
|
|
|
|
|
|
|
|
|
from aiogram import Router, F
|
|
|
|
|
|
from aiogram.types import CallbackQuery, Message
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
|
|
|
|
|
from bot.keyboards.reminders import (
|
|
|
|
|
|
ReminderActionCallback,
|
|
|
|
|
|
SnoozeDelayCallback,
|
|
|
|
|
|
ConfirmCallback,
|
|
|
|
|
|
get_snooze_delay_keyboard,
|
|
|
|
|
|
get_confirmation_keyboard,
|
|
|
|
|
|
)
|
|
|
|
|
|
from bot.keyboards.main_menu import get_main_menu_keyboard
|
|
|
|
|
|
from bot.services.reminders_service import RemindersService
|
|
|
|
|
|
from bot.services.time_service import get_time_service
|
|
|
|
|
|
from bot.logging_config import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
router = Router(name="callbacks")
|
|
|
|
|
|
reminders_service = RemindersService()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
async def _verify_owner(
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
reminder_id: int,
|
|
|
|
|
|
tg_user_id: int,
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""Fetch reminder and verify it belongs to tg_user_id."""
|
|
|
|
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
from bot.db.models import Reminder
|
|
|
|
|
|
|
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
|
select(Reminder)
|
|
|
|
|
|
.options(selectinload(Reminder.user))
|
|
|
|
|
|
.where(Reminder.id == reminder_id)
|
|
|
|
|
|
)
|
|
|
|
|
|
reminder = result.scalar_one_or_none()
|
|
|
|
|
|
|
|
|
|
|
|
if not reminder:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if reminder.user.tg_user_id != tg_user_id:
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(F.data == "noop")
|
|
|
|
|
|
async def handle_noop(callback: CallbackQuery) -> None:
|
|
|
|
|
|
"""Handle noop callback (page counter button)."""
|
|
|
|
|
|
await callback.answer()
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ReminderActionCallback.filter(F.action == "done"))
|
|
|
|
|
|
async def handle_done(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ReminderActionCallback,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle 'Done' button - mark reminder as completed."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
reminder = await reminders_service.mark_as_done(session, callback_data.reminder_id)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
if not reminder:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
f"✅ Отлично! Отметил как выполненное.\n\n"
|
|
|
|
|
|
f"Следующее напоминание будет {next_run_str}."
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer("Выполнено!")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Reminder {reminder.id} marked as done by user {callback.from_user.id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ReminderActionCallback.filter(F.action == "snooze"))
|
|
|
|
|
|
async def handle_snooze_select(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ReminderActionCallback,
|
2026-02-17 10:52:46 +01:00
|
|
|
|
session: AsyncSession,
|
2025-12-19 11:19:54 +01:00
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle 'Snooze' button - show delay options."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
"На сколько отложить напоминание?",
|
|
|
|
|
|
reply_markup=get_snooze_delay_keyboard(callback_data.reminder_id),
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(SnoozeDelayCallback.filter())
|
|
|
|
|
|
async def handle_snooze_delay(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: SnoozeDelayCallback,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle snooze delay selection."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2025-12-19 11:19:54 +01:00
|
|
|
|
reminder = await reminders_service.snooze(
|
|
|
|
|
|
session, callback_data.reminder_id, callback_data.hours
|
|
|
|
|
|
)
|
|
|
|
|
|
if not reminder:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
f"⏰ Напоминание отложено.\n\n"
|
|
|
|
|
|
f"Напомню {next_run_str}."
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer("Отложено!")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Reminder {reminder.id} snoozed for {callback_data.hours}h "
|
|
|
|
|
|
f"by user {callback.from_user.id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ReminderActionCallback.filter(F.action == "pause"))
|
|
|
|
|
|
async def handle_pause(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ReminderActionCallback,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle 'Pause' button - deactivate reminder."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
reminder = await reminders_service.pause_reminder(session, callback_data.reminder_id)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
if not reminder:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
"⏸ Напоминание поставлено на паузу.\n\n"
|
|
|
|
|
|
"Можешь возобновить его в любое время через «📋 Мои напоминания»."
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer("Поставлено на паузу")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Reminder {reminder.id} paused by user {callback.from_user.id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ReminderActionCallback.filter(F.action == "resume"))
|
|
|
|
|
|
async def handle_resume(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ReminderActionCallback,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle 'Resume' button - reactivate reminder."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
reminder = await reminders_service.resume_reminder(session, callback_data.reminder_id)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
if not reminder:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-02-17 10:52:46 +01:00
|
|
|
|
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
f"▶️ Напоминание возобновлено!\n\n"
|
|
|
|
|
|
f"Следующее напоминание будет {next_run_str}."
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer("Возобновлено!")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"Reminder {reminder.id} resumed by user {callback.from_user.id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ReminderActionCallback.filter(F.action == "delete"))
|
|
|
|
|
|
async def handle_delete_confirm(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ReminderActionCallback,
|
2026-02-17 10:52:46 +01:00
|
|
|
|
session: AsyncSession,
|
2025-12-19 11:19:54 +01:00
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle 'Delete' button - ask for confirmation."""
|
|
|
|
|
|
owner_check = await _verify_owner(session, callback_data.reminder_id, callback.from_user.id)
|
|
|
|
|
|
if not owner_check:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
2025-12-19 11:19:54 +01:00
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text(
|
|
|
|
|
|
"Точно удалить напоминание?",
|
|
|
|
|
|
reply_markup=get_confirmation_keyboard(
|
|
|
|
|
|
entity="delete",
|
|
|
|
|
|
entity_id=callback_data.reminder_id
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.callback_query(ConfirmCallback.filter(F.entity == "delete"))
|
|
|
|
|
|
async def handle_delete_execute(
|
|
|
|
|
|
callback: CallbackQuery,
|
|
|
|
|
|
callback_data: ConfirmCallback,
|
|
|
|
|
|
session: AsyncSession,
|
|
|
|
|
|
) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Execute reminder deletion after confirmation."""
|
2025-12-19 11:19:54 +01:00
|
|
|
|
if callback_data.action == "no":
|
|
|
|
|
|
await callback.message.edit_text("Удаление отменено.")
|
|
|
|
|
|
await callback.answer()
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
deleted = await reminders_service.delete_reminder_by_id(
|
|
|
|
|
|
session, callback_data.entity_id
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not deleted:
|
|
|
|
|
|
await callback.answer("Напоминание не найдено", show_alert=True)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
await callback.message.edit_text("🗑 Напоминание удалено.")
|
|
|
|
|
|
await callback.answer("Удалено")
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"Reminder {callback_data.entity_id} deleted by user {callback.from_user.id}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.message(F.text == "⚙️ Настройки")
|
|
|
|
|
|
async def handle_settings(message: Message) -> None:
|
2026-02-17 10:52:46 +01:00
|
|
|
|
"""Handle settings button (placeholder)."""
|
2025-12-19 11:19:54 +01:00
|
|
|
|
await message.answer(
|
|
|
|
|
|
"⚙️ Настройки\n\n"
|
|
|
|
|
|
"Функционал настроек будет добавлен в следующих версиях.\n\n"
|
|
|
|
|
|
"Пока доступны основные функции:\n"
|
|
|
|
|
|
"• Создание напоминаний\n"
|
|
|
|
|
|
"• Просмотр и управление\n"
|
|
|
|
|
|
"• Редактирование параметров",
|
|
|
|
|
|
reply_markup=get_main_menu_keyboard(),
|
|
|
|
|
|
)
|