fix problems
This commit is contained in:
parent
30d43be793
commit
acc3d50f28
Binary file not shown.
|
|
@ -1,14 +1,12 @@
|
|||
"""Scheduler for sending reminder notifications."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Bot
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
|
||||
from bot.db.base import get_session
|
||||
from bot.db.operations import get_due_reminders
|
||||
from bot.keyboards.reminders import get_reminder_notification_keyboard
|
||||
from bot.services.time_service import get_time_service
|
||||
|
|
@ -50,7 +48,6 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
bot: Bot instance
|
||||
"""
|
||||
from bot.db.base import async_session_maker
|
||||
from bot.db.operations import update_reminder
|
||||
|
||||
if not async_session_maker:
|
||||
logger.error("Session maker not initialized")
|
||||
|
|
@ -60,7 +57,6 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
time_service = get_time_service()
|
||||
current_time = time_service.get_now()
|
||||
|
||||
# Get due reminders from database using proper async session
|
||||
async with async_session_maker() as session:
|
||||
due_reminders = await get_due_reminders(session, current_time)
|
||||
|
||||
|
|
@ -70,7 +66,6 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
|
||||
logger.info(f"Found {len(due_reminders)} due reminders")
|
||||
|
||||
# Send notifications
|
||||
for reminder in due_reminders:
|
||||
await send_reminder_notification(
|
||||
bot=bot,
|
||||
|
|
@ -79,20 +74,17 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
text=reminder.text,
|
||||
)
|
||||
|
||||
# Update next_run_at to prevent sending again
|
||||
# (it will be properly updated when user clicks "Done" or by periodic update)
|
||||
temp_next_run = time_service.calculate_next_occurrence(
|
||||
next_run = time_service.calculate_next_occurrence(
|
||||
current_run=reminder.next_run_at,
|
||||
days_interval=reminder.days_interval,
|
||||
)
|
||||
await update_reminder(session, reminder.id, next_run_at=temp_next_run)
|
||||
reminder.next_run_at = next_run
|
||||
reminder.updated_at = time_service.get_now()
|
||||
|
||||
# Commit all updates
|
||||
await session.commit()
|
||||
|
||||
# Small delay to avoid rate limits
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in check_and_send_reminders: {e}", exc_info=True)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -8,6 +8,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|||
from bot.db.base import Base
|
||||
|
||||
|
||||
def _now_local() -> datetime:
|
||||
from bot.services.time_service import get_time_service
|
||||
return get_time_service().get_now()
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model - represents Telegram users."""
|
||||
|
||||
|
|
@ -18,9 +23,9 @@ class User(Base):
|
|||
username: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
first_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
last_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_now_local, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
||||
DateTime, default=_now_local, onupdate=_now_local, nullable=False
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
|
|
@ -44,9 +49,9 @@ class Reminder(Base):
|
|||
next_run_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
||||
last_done_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=_now_local, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
||||
DateTime, default=_now_local, onupdate=_now_local, nullable=False
|
||||
)
|
||||
|
||||
# Optional fields for statistics
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"""CRUD operations for database models."""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.db.models import User, Reminder
|
||||
from bot.db.models import User, Reminder, _now_local
|
||||
from bot.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -46,7 +46,7 @@ async def get_or_create_user(
|
|||
user.username = username
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.updated_at = datetime.utcnow()
|
||||
user.updated_at = _now_local()
|
||||
await session.commit()
|
||||
logger.debug(f"Updated user info: {tg_user_id}")
|
||||
return user
|
||||
|
|
@ -90,7 +90,7 @@ async def create_reminder(
|
|||
user_id: int,
|
||||
text: str,
|
||||
days_interval: int,
|
||||
time_of_day: datetime.time,
|
||||
time_of_day: time,
|
||||
next_run_at: datetime,
|
||||
) -> Reminder:
|
||||
"""
|
||||
|
|
@ -212,7 +212,7 @@ async def update_reminder(
|
|||
if hasattr(reminder, key):
|
||||
setattr(reminder, key, value)
|
||||
|
||||
reminder.updated_at = datetime.utcnow()
|
||||
reminder.updated_at = _now_local()
|
||||
await session.commit()
|
||||
await session.refresh(reminder)
|
||||
logger.debug(f"Updated reminder {reminder_id}")
|
||||
|
|
@ -261,10 +261,10 @@ async def mark_reminder_done(
|
|||
if not reminder:
|
||||
return None
|
||||
|
||||
reminder.last_done_at = datetime.utcnow()
|
||||
reminder.last_done_at = _now_local()
|
||||
reminder.next_run_at = next_run_at
|
||||
reminder.total_done_count += 1
|
||||
reminder.updated_at = datetime.utcnow()
|
||||
reminder.updated_at = _now_local()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(reminder)
|
||||
|
|
@ -294,7 +294,7 @@ async def snooze_reminder(
|
|||
|
||||
reminder.next_run_at = next_run_at
|
||||
reminder.snooze_count += 1
|
||||
reminder.updated_at = datetime.utcnow()
|
||||
reminder.updated_at = _now_local()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(reminder)
|
||||
|
|
@ -323,7 +323,7 @@ async def toggle_reminder_active(
|
|||
return None
|
||||
|
||||
reminder.is_active = is_active
|
||||
reminder.updated_at = datetime.utcnow()
|
||||
reminder.updated_at = _now_local()
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(reminder)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -20,10 +20,36 @@ logger = get_logger(__name__)
|
|||
|
||||
router = Router(name="callbacks")
|
||||
reminders_service = RemindersService()
|
||||
time_service = get_time_service()
|
||||
|
||||
|
||||
# ==================== Done Action ====================
|
||||
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()
|
||||
|
||||
|
||||
@router.callback_query(ReminderActionCallback.filter(F.action == "done"))
|
||||
|
|
@ -32,21 +58,18 @@ async def handle_done(
|
|||
callback_data: ReminderActionCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle 'Done' button - mark reminder as completed.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
session: Database session
|
||||
"""
|
||||
reminder = await reminders_service.mark_as_done(session, callback_data.reminder_id)
|
||||
|
||||
if not reminder:
|
||||
await callback.answer("Напоминание не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
next_run_str = time_service.format_next_run(reminder.next_run_at)
|
||||
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Отлично! Отметил как выполненное.\n\n"
|
||||
|
|
@ -57,21 +80,18 @@ async def handle_done(
|
|||
logger.info(f"Reminder {reminder.id} marked as done by user {callback.from_user.id}")
|
||||
|
||||
|
||||
# ==================== Snooze Action ====================
|
||||
|
||||
|
||||
@router.callback_query(ReminderActionCallback.filter(F.action == "snooze"))
|
||||
async def handle_snooze_select(
|
||||
callback: CallbackQuery,
|
||||
callback_data: ReminderActionCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle 'Snooze' button - show delay options.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
"""
|
||||
await callback.message.edit_text(
|
||||
"На сколько отложить напоминание?",
|
||||
reply_markup=get_snooze_delay_keyboard(callback_data.reminder_id),
|
||||
|
|
@ -85,23 +105,20 @@ async def handle_snooze_delay(
|
|||
callback_data: SnoozeDelayCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle snooze delay selection.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
session: Database session
|
||||
"""
|
||||
reminder = await reminders_service.snooze(
|
||||
session, callback_data.reminder_id, callback_data.hours
|
||||
)
|
||||
|
||||
if not reminder:
|
||||
await callback.answer("Напоминание не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
next_run_str = time_service.format_next_run(reminder.next_run_at)
|
||||
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"⏰ Напоминание отложено.\n\n"
|
||||
|
|
@ -115,25 +132,19 @@ async def handle_snooze_delay(
|
|||
)
|
||||
|
||||
|
||||
# ==================== Pause/Resume Actions ====================
|
||||
|
||||
|
||||
@router.callback_query(ReminderActionCallback.filter(F.action == "pause"))
|
||||
async def handle_pause(
|
||||
callback: CallbackQuery,
|
||||
callback_data: ReminderActionCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle 'Pause' button - deactivate reminder.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
session: Database session
|
||||
"""
|
||||
reminder = await reminders_service.pause_reminder(session, callback_data.reminder_id)
|
||||
|
||||
if not reminder:
|
||||
await callback.answer("Напоминание не найдено", show_alert=True)
|
||||
return
|
||||
|
|
@ -153,21 +164,18 @@ async def handle_resume(
|
|||
callback_data: ReminderActionCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle 'Resume' button - reactivate reminder.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
session: Database session
|
||||
"""
|
||||
reminder = await reminders_service.resume_reminder(session, callback_data.reminder_id)
|
||||
|
||||
if not reminder:
|
||||
await callback.answer("Напоминание не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
next_run_str = time_service.format_next_run(reminder.next_run_at)
|
||||
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"▶️ Напоминание возобновлено!\n\n"
|
||||
|
|
@ -178,21 +186,18 @@ async def handle_resume(
|
|||
logger.info(f"Reminder {reminder.id} resumed by user {callback.from_user.id}")
|
||||
|
||||
|
||||
# ==================== Delete Action ====================
|
||||
|
||||
|
||||
@router.callback_query(ReminderActionCallback.filter(F.action == "delete"))
|
||||
async def handle_delete_confirm(
|
||||
callback: CallbackQuery,
|
||||
callback_data: ReminderActionCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Handle 'Delete' button - ask for confirmation.
|
||||
"""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
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
"""
|
||||
await callback.message.edit_text(
|
||||
"Точно удалить напоминание?",
|
||||
reply_markup=get_confirmation_keyboard(
|
||||
|
|
@ -209,14 +214,7 @@ async def handle_delete_execute(
|
|||
callback_data: ConfirmCallback,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Execute reminder deletion after confirmation.
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
callback_data: Parsed callback data
|
||||
session: Database session
|
||||
"""
|
||||
"""Execute reminder deletion after confirmation."""
|
||||
if callback_data.action == "no":
|
||||
await callback.message.edit_text("Удаление отменено.")
|
||||
await callback.answer()
|
||||
|
|
@ -238,17 +236,9 @@ async def handle_delete_execute(
|
|||
)
|
||||
|
||||
|
||||
# ==================== Settings Placeholder ====================
|
||||
|
||||
|
||||
@router.message(F.text == "⚙️ Настройки")
|
||||
async def handle_settings(message: Message) -> None:
|
||||
"""
|
||||
Handle settings button (placeholder).
|
||||
|
||||
Args:
|
||||
message: Telegram message
|
||||
"""
|
||||
"""Handle settings button (placeholder)."""
|
||||
await message.answer(
|
||||
"⚙️ Настройки\n\n"
|
||||
"Функционал настроек будет добавлен в следующих версиях.\n\n"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
from aiogram import Router
|
||||
from aiogram.types import ErrorEvent
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot.logging_config import get_logger
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ router = Router(name="errors")
|
|||
|
||||
|
||||
@router.error()
|
||||
async def error_handler(event: ErrorEvent) -> None:
|
||||
async def error_handler(event: ErrorEvent) -> bool:
|
||||
"""
|
||||
Global error handler for all unhandled exceptions.
|
||||
|
||||
|
|
@ -41,3 +40,5 @@ async def error_handler(event: ErrorEvent) -> None:
|
|||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send error callback to user: {e}")
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ logger = get_logger(__name__)
|
|||
|
||||
router = Router(name="reminders_create")
|
||||
reminders_service = RemindersService()
|
||||
time_service = get_time_service()
|
||||
|
||||
|
||||
@router.message(F.text == "➕ Новое напоминание")
|
||||
|
|
@ -42,7 +41,7 @@ async def start_create_reminder(message: Message, state: FSMContext) -> None:
|
|||
)
|
||||
|
||||
|
||||
@router.message(CreateReminderStates.waiting_for_text)
|
||||
@router.message(CreateReminderStates.waiting_for_text, F.text)
|
||||
async def process_reminder_text(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Process reminder text input.
|
||||
|
|
@ -106,7 +105,7 @@ async def process_interval_button(
|
|||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(CreateReminderStates.waiting_for_interval)
|
||||
@router.message(CreateReminderStates.waiting_for_interval, F.text)
|
||||
async def process_interval_text(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Process interval input as text.
|
||||
|
|
@ -134,7 +133,7 @@ async def process_interval_text(message: Message, state: FSMContext) -> None:
|
|||
)
|
||||
|
||||
|
||||
@router.message(CreateReminderStates.waiting_for_time)
|
||||
@router.message(CreateReminderStates.waiting_for_time, F.text)
|
||||
async def process_reminder_time(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Process reminder time input.
|
||||
|
|
@ -225,7 +224,7 @@ async def process_create_confirmation(
|
|||
await state.clear()
|
||||
|
||||
# Format next run time
|
||||
next_run_str = time_service.format_next_run(reminder.next_run_at)
|
||||
next_run_str = get_time_service().format_next_run(reminder.next_run_at)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Напоминание создано!\n\n"
|
||||
|
|
@ -238,3 +237,11 @@ async def process_create_confirmation(
|
|||
await callback.answer("Напоминание создано!")
|
||||
|
||||
logger.info(f"User {user.tg_user_id} created reminder {reminder.id}")
|
||||
|
||||
|
||||
@router.message(CreateReminderStates.waiting_for_text)
|
||||
@router.message(CreateReminderStates.waiting_for_interval)
|
||||
@router.message(CreateReminderStates.waiting_for_time)
|
||||
async def fsm_non_text_fallback(message: Message) -> None:
|
||||
"""Fallback for non-text messages during FSM creation flow."""
|
||||
await message.answer("Пожалуйста, отправь текстовое сообщение.")
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ async def edit_text_start(
|
|||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(EditReminderStates.editing_text)
|
||||
@router.message(EditReminderStates.editing_text, F.text)
|
||||
async def edit_text_process(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
|
|
@ -368,7 +368,7 @@ async def edit_interval_button(
|
|||
await callback.answer("Обновлено!")
|
||||
|
||||
|
||||
@router.message(EditReminderStates.editing_interval)
|
||||
@router.message(EditReminderStates.editing_interval, F.text)
|
||||
async def edit_interval_text(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
|
|
@ -429,7 +429,7 @@ async def edit_time_start(
|
|||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(EditReminderStates.editing_time)
|
||||
@router.message(EditReminderStates.editing_time, F.text)
|
||||
async def edit_time_process(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
|
|
@ -466,3 +466,11 @@ async def edit_time_process(
|
|||
f"✅ Время обновлено!\n\nНовое время: {time_of_day.strftime('%H:%M')}",
|
||||
reply_markup=get_main_menu_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.message(EditReminderStates.editing_text)
|
||||
@router.message(EditReminderStates.editing_interval)
|
||||
@router.message(EditReminderStates.editing_time)
|
||||
async def edit_non_text_fallback(message: Message) -> None:
|
||||
"""Fallback for non-text messages during FSM edit flow."""
|
||||
await message.answer("Пожалуйста, отправь текстовое сообщение.")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -5,6 +5,7 @@ from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|||
from aiogram.filters.callback_data import CallbackData
|
||||
|
||||
from bot.db.models import Reminder
|
||||
from bot.utils.formatting import truncate_text
|
||||
|
||||
|
||||
class PaginationCallback(CallbackData, prefix="page"):
|
||||
|
|
@ -39,7 +40,7 @@ def get_reminders_list_keyboard(
|
|||
# Add buttons for reminders on current page
|
||||
for reminder in reminders[start_idx:end_idx]:
|
||||
status_icon = "✅" if reminder.is_active else "⏸"
|
||||
button_text = f"{status_icon} #{reminder.id} {reminder.text[:30]}..."
|
||||
button_text = f"{status_icon} #{reminder.id} {truncate_text(reminder.text, 30)}"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -25,9 +25,9 @@ logger = get_logger(__name__)
|
|||
class RemindersService:
|
||||
"""Service for reminder-related operations."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize RemindersService."""
|
||||
self.time_service = get_time_service()
|
||||
@property
|
||||
def time_service(self):
|
||||
return get_time_service()
|
||||
|
||||
async def create_new_reminder(
|
||||
self,
|
||||
|
|
@ -50,13 +50,10 @@ class RemindersService:
|
|||
Returns:
|
||||
Created Reminder instance
|
||||
"""
|
||||
# Calculate next run time
|
||||
next_run_at = self.time_service.calculate_next_run_time(
|
||||
next_run_at = self.time_service.calculate_first_run_time(
|
||||
time_of_day=time_of_day,
|
||||
days_interval=days_interval,
|
||||
)
|
||||
|
||||
# Create reminder
|
||||
reminder = await create_reminder(
|
||||
session=session,
|
||||
user_id=user_id,
|
||||
|
|
@ -134,7 +131,7 @@ class RemindersService:
|
|||
new_days_interval: int,
|
||||
) -> Optional[Reminder]:
|
||||
"""
|
||||
Update reminder interval.
|
||||
Update reminder interval and recalculate next_run_at with new interval.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
|
|
@ -148,8 +145,7 @@ class RemindersService:
|
|||
if not reminder:
|
||||
return None
|
||||
|
||||
# Recalculate next_run_at with new interval
|
||||
next_run_at = self.time_service.calculate_next_run_time(
|
||||
next_run_at = self.time_service.calculate_next_run_with_interval(
|
||||
time_of_day=reminder.time_of_day,
|
||||
days_interval=new_days_interval,
|
||||
)
|
||||
|
|
@ -168,7 +164,7 @@ class RemindersService:
|
|||
new_time_of_day: time,
|
||||
) -> Optional[Reminder]:
|
||||
"""
|
||||
Update reminder time.
|
||||
Update reminder time and recalculate next_run_at.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
|
|
@ -182,10 +178,8 @@ class RemindersService:
|
|||
if not reminder:
|
||||
return None
|
||||
|
||||
# Recalculate next_run_at with new time
|
||||
next_run_at = self.time_service.calculate_next_run_time(
|
||||
next_run_at = self.time_service.calculate_first_run_time(
|
||||
time_of_day=new_time_of_day,
|
||||
days_interval=reminder.days_interval,
|
||||
)
|
||||
|
||||
return await update_reminder(
|
||||
|
|
@ -231,7 +225,6 @@ class RemindersService:
|
|||
if not reminder:
|
||||
return None
|
||||
|
||||
# Calculate next occurrence
|
||||
next_run_at = self.time_service.calculate_next_occurrence(
|
||||
current_run=reminder.next_run_at,
|
||||
days_interval=reminder.days_interval,
|
||||
|
|
@ -284,7 +277,7 @@ class RemindersService:
|
|||
reminder_id: int,
|
||||
) -> Optional[Reminder]:
|
||||
"""
|
||||
Resume (activate) a reminder.
|
||||
Resume (activate) a reminder. Recalculates next_run_at atomically.
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
|
|
@ -297,12 +290,13 @@ class RemindersService:
|
|||
if not reminder:
|
||||
return None
|
||||
|
||||
# Recalculate next_run_at from now
|
||||
next_run_at = self.time_service.calculate_next_run_time(
|
||||
next_run_at = self.time_service.calculate_first_run_time(
|
||||
time_of_day=reminder.time_of_day,
|
||||
days_interval=reminder.days_interval,
|
||||
)
|
||||
|
||||
# Update and activate
|
||||
await update_reminder(session, reminder_id, next_run_at=next_run_at)
|
||||
return await toggle_reminder_active(session, reminder_id, is_active=True)
|
||||
return await update_reminder(
|
||||
session,
|
||||
reminder_id,
|
||||
next_run_at=next_run_at,
|
||||
is_active=True,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,14 +46,41 @@ class TimeService:
|
|||
"""
|
||||
return datetime.combine(date.date(), time_of_day)
|
||||
|
||||
def calculate_next_run_time(
|
||||
def calculate_first_run_time(
|
||||
self,
|
||||
time_of_day: time,
|
||||
from_datetime: Optional[datetime] = None,
|
||||
) -> datetime:
|
||||
"""
|
||||
Calculate the nearest future occurrence of time_of_day (today or tomorrow).
|
||||
Used for new reminders and resume.
|
||||
|
||||
Args:
|
||||
time_of_day: Desired time of day
|
||||
from_datetime: Base datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
Next run datetime (today if time hasn't passed, otherwise tomorrow)
|
||||
"""
|
||||
if from_datetime is None:
|
||||
from_datetime = self.get_now()
|
||||
|
||||
next_run = self.combine_date_time(from_datetime, time_of_day)
|
||||
|
||||
if next_run <= from_datetime:
|
||||
next_run += timedelta(days=1)
|
||||
|
||||
return next_run
|
||||
|
||||
def calculate_next_run_with_interval(
|
||||
self,
|
||||
time_of_day: time,
|
||||
days_interval: int,
|
||||
from_datetime: Optional[datetime] = None,
|
||||
) -> datetime:
|
||||
"""
|
||||
Calculate next run datetime for a reminder.
|
||||
Calculate next run datetime using interval offset from now.
|
||||
Used when interval is changed.
|
||||
|
||||
Args:
|
||||
time_of_day: Desired time of day
|
||||
|
|
@ -61,19 +88,13 @@ class TimeService:
|
|||
from_datetime: Base datetime (defaults to now)
|
||||
|
||||
Returns:
|
||||
Next run datetime
|
||||
Datetime at time_of_day, days_interval days from from_datetime
|
||||
"""
|
||||
if from_datetime is None:
|
||||
from_datetime = self.get_now()
|
||||
|
||||
# Start with today at the specified time
|
||||
next_run = self.combine_date_time(from_datetime, time_of_day)
|
||||
|
||||
# If the time has already passed today, start from tomorrow
|
||||
if next_run <= from_datetime:
|
||||
next_run += timedelta(days=1)
|
||||
|
||||
return next_run
|
||||
target_date = from_datetime + timedelta(days=days_interval)
|
||||
return self.combine_date_time(target_date, time_of_day)
|
||||
|
||||
def calculate_next_occurrence(
|
||||
self,
|
||||
|
|
@ -81,16 +102,23 @@ class TimeService:
|
|||
days_interval: int,
|
||||
) -> datetime:
|
||||
"""
|
||||
Calculate next occurrence after a completed reminder.
|
||||
Calculate next occurrence after a completed/sent reminder.
|
||||
Guarantees the result is in the future.
|
||||
|
||||
Args:
|
||||
current_run: Current run datetime
|
||||
days_interval: Days between reminders
|
||||
|
||||
Returns:
|
||||
Next occurrence datetime
|
||||
Next occurrence datetime (always in the future)
|
||||
"""
|
||||
return current_run + timedelta(days=days_interval)
|
||||
now = self.get_now()
|
||||
next_run = current_run + timedelta(days=days_interval)
|
||||
|
||||
while next_run <= now:
|
||||
next_run += timedelta(days=days_interval)
|
||||
|
||||
return next_run
|
||||
|
||||
def add_hours(self, dt: datetime, hours: int) -> datetime:
|
||||
"""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -15,7 +15,9 @@ def validate_time_format(time_str: str) -> Optional[time]:
|
|||
Returns:
|
||||
time object if valid, None otherwise
|
||||
"""
|
||||
# Remove extra whitespace
|
||||
if not time_str:
|
||||
return None
|
||||
|
||||
time_str = time_str.strip()
|
||||
|
||||
# Check format with regex
|
||||
|
|
@ -47,6 +49,9 @@ def validate_days_interval(days_str: str) -> Optional[int]:
|
|||
Returns:
|
||||
Integer days if valid (>0 and <=365), None otherwise
|
||||
"""
|
||||
if not days_str:
|
||||
return None
|
||||
|
||||
try:
|
||||
days = int(days_str.strip())
|
||||
if 0 < days <= 365: # Max 1 year
|
||||
|
|
|
|||
Loading…
Reference in New Issue