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:
|
Args:
|
||||||
bot: Bot instance
|
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:
|
try:
|
||||||
time_service = get_time_service()
|
time_service = get_time_service()
|
||||||
current_time = time_service.get_now()
|
current_time = time_service.get_now()
|
||||||
|
|
||||||
# Get due reminders from database
|
# Get due reminders from database using proper async session
|
||||||
async for session in get_session():
|
async with async_session_maker() as session:
|
||||||
due_reminders = await get_due_reminders(session, current_time)
|
due_reminders = await get_due_reminders(session, current_time)
|
||||||
|
|
||||||
if not due_reminders:
|
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
|
# Update next_run_at to prevent sending again
|
||||||
# (it will be properly updated when user clicks "Done" or by periodic update)
|
# (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(
|
temp_next_run = time_service.calculate_next_occurrence(
|
||||||
current_run=reminder.next_run_at,
|
current_run=reminder.next_run_at,
|
||||||
days_interval=reminder.days_interval,
|
days_interval=reminder.days_interval,
|
||||||
)
|
)
|
||||||
await update_reminder(session, reminder.id, next_run_at=temp_next_run)
|
await update_reminder(session, reminder.id, next_run_at=temp_next_run)
|
||||||
|
|
||||||
|
# Commit all updates
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
# Small delay to avoid rate limits
|
# Small delay to avoid rate limits
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -176,8 +176,11 @@ async def get_due_reminders(session: AsyncSession, current_time: datetime) -> Li
|
||||||
Returns:
|
Returns:
|
||||||
List of due Reminder instances
|
List of due Reminder instances
|
||||||
"""
|
"""
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Reminder)
|
select(Reminder)
|
||||||
|
.options(selectinload(Reminder.user)) # Eager load user relationship
|
||||||
.where(Reminder.is_active == True)
|
.where(Reminder.is_active == True)
|
||||||
.where(Reminder.next_run_at <= current_time)
|
.where(Reminder.next_run_at <= current_time)
|
||||||
.order_by(Reminder.next_run_at)
|
.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()
|
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 ====================
|
# ==================== 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()
|
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
|
return keyboard
|
||||||
|
|
|
||||||
11
bot/main.py
11
bot/main.py
|
|
@ -38,14 +38,23 @@ async def on_shutdown() -> None:
|
||||||
logger.info("Shutting down reminder bot...")
|
logger.info("Shutting down reminder bot...")
|
||||||
|
|
||||||
# Stop scheduler
|
# Stop scheduler
|
||||||
|
try:
|
||||||
stop_scheduler()
|
stop_scheduler()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping scheduler: {e}")
|
||||||
|
|
||||||
# Close database
|
# Close database
|
||||||
|
try:
|
||||||
await close_db()
|
await close_db()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error closing database: {e}")
|
||||||
|
|
||||||
# Close bot session
|
# Close bot session
|
||||||
if bot:
|
if bot and hasattr(bot, 'session'):
|
||||||
|
try:
|
||||||
await bot.session.close()
|
await bot.session.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error closing bot session: {e}")
|
||||||
|
|
||||||
logger.info("Bot shutdown completed")
|
logger.info("Bot shutdown completed")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,27 @@ class TimeService:
|
||||||
|
|
||||||
def get_now(self) -> datetime:
|
def get_now(self) -> datetime:
|
||||||
"""
|
"""
|
||||||
Get current datetime in configured timezone.
|
Get current datetime in configured timezone (as naive).
|
||||||
|
|
||||||
Returns:
|
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:
|
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:
|
Args:
|
||||||
date: Date component
|
date: Date component
|
||||||
time_of_day: Time component
|
time_of_day: Time component
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Combined datetime
|
Combined naive datetime
|
||||||
"""
|
"""
|
||||||
naive_dt = datetime.combine(date.date(), time_of_day)
|
return datetime.combine(date.date(), time_of_day)
|
||||||
return naive_dt.replace(tzinfo=self.timezone)
|
|
||||||
|
|
||||||
def calculate_next_run_time(
|
def calculate_next_run_time(
|
||||||
self,
|
self,
|
||||||
|
|
@ -109,13 +110,12 @@ class TimeService:
|
||||||
Format next run datetime for display.
|
Format next run datetime for display.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
next_run: Next run datetime
|
next_run: Next run datetime (naive UTC)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Formatted string
|
Formatted string
|
||||||
"""
|
"""
|
||||||
now = self.get_now()
|
now = self.get_now()
|
||||||
delta = next_run - now
|
|
||||||
|
|
||||||
# If it's today
|
# If it's today
|
||||||
if next_run.date() == now.date():
|
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
|
days_str: Days interval string to validate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Integer days if valid (>0), None otherwise
|
Integer days if valid (>0 and <=365), None otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
days = int(days_str.strip())
|
days = int(days_str.strip())
|
||||||
if days > 0:
|
if 0 < days <= 365: # Max 1 year
|
||||||
return days
|
return days
|
||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue