Compare commits
4 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
30d43be793 | |
|
|
59966a2f17 | |
|
|
e675a245e6 | |
|
|
c1ec641ee0 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -49,12 +49,19 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
Args:
|
||||
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")
|
||||
return
|
||||
|
||||
try:
|
||||
time_service = get_time_service()
|
||||
current_time = time_service.get_now()
|
||||
|
||||
# Get due reminders from database
|
||||
async for session in get_session():
|
||||
# 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)
|
||||
|
||||
if not due_reminders:
|
||||
|
|
@ -74,16 +81,15 @@ async def check_and_send_reminders(bot: Bot) -> None:
|
|||
|
||||
# Update next_run_at to prevent sending again
|
||||
# (it will be properly updated when user clicks "Done" or by periodic update)
|
||||
from bot.db.operations import update_reminder
|
||||
from datetime import timedelta
|
||||
|
||||
# Temporarily set next_run_at to current + interval to avoid duplicate sends
|
||||
temp_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)
|
||||
|
||||
# Commit all updates
|
||||
await session.commit()
|
||||
|
||||
# Small delay to avoid rate limits
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
|
|
|
|||
|
|
@ -176,8 +176,11 @@ async def get_due_reminders(session: AsyncSession, current_time: datetime) -> Li
|
|||
Returns:
|
||||
List of due Reminder instances
|
||||
"""
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
result = await session.execute(
|
||||
select(Reminder)
|
||||
.options(selectinload(Reminder.user)) # Eager load user relationship
|
||||
.where(Reminder.is_active == True)
|
||||
.where(Reminder.next_run_at <= current_time)
|
||||
.order_by(Reminder.next_run_at)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -128,6 +128,36 @@ async def show_reminder_details(
|
|||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(ReminderActionCallback.filter(F.action == "back_to_list"))
|
||||
async def back_to_reminders_list(
|
||||
callback: CallbackQuery,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
"""
|
||||
Return to reminders list from details view.
|
||||
|
||||
Args:
|
||||
callback: Callback query
|
||||
session: Database session
|
||||
"""
|
||||
user = await UserService.ensure_user_exists(session, callback.from_user)
|
||||
reminders = await reminders_service.get_user_all_reminders(session, user.id)
|
||||
|
||||
if not reminders:
|
||||
await callback.message.edit_text(
|
||||
"У тебя пока нет напоминаний.\n\n"
|
||||
"Нажми «➕ Новое напоминание», чтобы создать первое!"
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"📋 Твои напоминания ({len(reminders)}):",
|
||||
reply_markup=get_reminders_list_keyboard(reminders, page=0),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ==================== Edit Reminder Flow ====================
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,12 @@ def get_reminder_details_keyboard(reminder_id: int, is_active: bool) -> InlineKe
|
|||
callback_data=ReminderActionCallback(action="delete", reminder_id=reminder_id).pack()
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад к списку",
|
||||
callback_data=ReminderActionCallback(action="back_to_list", reminder_id=0).pack()
|
||||
),
|
||||
],
|
||||
]
|
||||
)
|
||||
return keyboard
|
||||
|
|
|
|||
17
bot/main.py
17
bot/main.py
|
|
@ -38,14 +38,23 @@ async def on_shutdown() -> None:
|
|||
logger.info("Shutting down reminder bot...")
|
||||
|
||||
# Stop scheduler
|
||||
stop_scheduler()
|
||||
try:
|
||||
stop_scheduler()
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping scheduler: {e}")
|
||||
|
||||
# Close database
|
||||
await close_db()
|
||||
try:
|
||||
await close_db()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing database: {e}")
|
||||
|
||||
# Close bot session
|
||||
if bot:
|
||||
await bot.session.close()
|
||||
if bot and hasattr(bot, 'session'):
|
||||
try:
|
||||
await bot.session.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing bot session: {e}")
|
||||
|
||||
logger.info("Bot shutdown completed")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,26 +24,27 @@ class TimeService:
|
|||
|
||||
def get_now(self) -> datetime:
|
||||
"""
|
||||
Get current datetime in configured timezone.
|
||||
Get current datetime in configured timezone (as naive).
|
||||
|
||||
Returns:
|
||||
Current datetime
|
||||
Current datetime in local timezone (Moscow), naive
|
||||
"""
|
||||
return datetime.now(self.timezone)
|
||||
# Get aware datetime in configured timezone, then strip timezone
|
||||
aware_now = datetime.now(self.timezone)
|
||||
return aware_now.replace(tzinfo=None)
|
||||
|
||||
def combine_date_time(self, date: datetime, time_of_day: time) -> datetime:
|
||||
"""
|
||||
Combine date and time in configured timezone.
|
||||
Combine date and time as naive datetime.
|
||||
|
||||
Args:
|
||||
date: Date component
|
||||
time_of_day: Time component
|
||||
|
||||
Returns:
|
||||
Combined datetime
|
||||
Combined naive datetime
|
||||
"""
|
||||
naive_dt = datetime.combine(date.date(), time_of_day)
|
||||
return naive_dt.replace(tzinfo=self.timezone)
|
||||
return datetime.combine(date.date(), time_of_day)
|
||||
|
||||
def calculate_next_run_time(
|
||||
self,
|
||||
|
|
@ -109,13 +110,12 @@ class TimeService:
|
|||
Format next run datetime for display.
|
||||
|
||||
Args:
|
||||
next_run: Next run datetime
|
||||
next_run: Next run datetime (naive UTC)
|
||||
|
||||
Returns:
|
||||
Formatted string
|
||||
"""
|
||||
now = self.get_now()
|
||||
delta = next_run - now
|
||||
|
||||
# If it's today
|
||||
if next_run.date() == now.date():
|
||||
|
|
|
|||
|
|
@ -45,11 +45,11 @@ def validate_days_interval(days_str: str) -> Optional[int]:
|
|||
days_str: Days interval string to validate
|
||||
|
||||
Returns:
|
||||
Integer days if valid (>0), None otherwise
|
||||
Integer days if valid (>0 and <=365), None otherwise
|
||||
"""
|
||||
try:
|
||||
days = int(days_str.strip())
|
||||
if days > 0:
|
||||
if 0 < days <= 365: # Max 1 year
|
||||
return days
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
|
|
|||
Loading…
Reference in New Issue