From acc3d50f287aa3a0c5fbb809dfeda2bfbde4a749 Mon Sep 17 00:00:00 2001 From: itqop Date: Tue, 17 Feb 2026 12:52:46 +0300 Subject: [PATCH] fix problems --- .../__pycache__/scheduler.cpython-312.pyc | Bin 6054 -> 5893 bytes bot/core/scheduler.py | 18 +-- bot/db/__pycache__/models.cpython-312.pyc | Bin 0 -> 4440 bytes bot/db/__pycache__/operations.cpython-312.pyc | Bin 0 -> 11856 bytes bot/db/models.py | 13 +- bot/db/operations.py | 18 +-- .../__pycache__/callbacks.cpython-312.pyc | Bin 11491 -> 13665 bytes .../__pycache__/errors.cpython-312.pyc | Bin 2346 -> 2295 bytes .../reminders_create.cpython-312.pyc | Bin 11093 -> 11796 bytes .../reminders_manage.cpython-312.pyc | Bin 20972 -> 21696 bytes bot/handlers/callbacks.py | 140 ++++++++---------- bot/handlers/errors.py | 5 +- bot/handlers/reminders_create.py | 17 ++- bot/handlers/reminders_manage.py | 14 +- .../__pycache__/pagination.cpython-312.pyc | Bin 0 -> 2920 bytes bot/keyboards/pagination.py | 3 +- .../reminders_service.cpython-312.pyc | Bin 0 -> 9323 bytes .../__pycache__/time_service.cpython-312.pyc | Bin 0 -> 6444 bytes bot/services/reminders_service.py | 38 ++--- bot/services/time_service.py | 56 +++++-- .../__pycache__/validators.cpython-312.pyc | Bin 0 -> 2300 bytes bot/utils/validators.py | 7 +- 22 files changed, 180 insertions(+), 149 deletions(-) create mode 100644 bot/db/__pycache__/models.cpython-312.pyc create mode 100644 bot/db/__pycache__/operations.cpython-312.pyc create mode 100644 bot/keyboards/__pycache__/pagination.cpython-312.pyc create mode 100644 bot/services/__pycache__/reminders_service.cpython-312.pyc create mode 100644 bot/services/__pycache__/time_service.cpython-312.pyc create mode 100644 bot/utils/__pycache__/validators.cpython-312.pyc diff --git a/bot/core/__pycache__/scheduler.cpython-312.pyc b/bot/core/__pycache__/scheduler.cpython-312.pyc index 4c216418172eae45c2fc33f7272f5a7bc5116d6b..5741813551bc95613ea05e85ec30a85ce1062df7 100644 GIT binary patch delta 2513 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 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() + + await asyncio.sleep(0.5) - # Commit all updates await session.commit() - # Small delay to avoid rate limits - await asyncio.sleep(0.5) - except Exception as e: logger.error(f"Error in check_and_send_reminders: {e}", exc_info=True) diff --git a/bot/db/__pycache__/models.cpython-312.pyc b/bot/db/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6834c048ff43bc40908753b491d937ed2980a3d2 GIT binary patch literal 4440 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 literal 0 HcmV?d00001 diff --git a/bot/db/__pycache__/operations.cpython-312.pyc b/bot/db/__pycache__/operations.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3ccf5e39b094a0ab4c326a29f9ff8b5e5c1d9ff GIT binary patch literal 11856 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 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 diff --git a/bot/db/operations.py b/bot/db/operations.py index 486a871..fc2ad2f 100644 --- a/bot/db/operations.py +++ b/bot/db/operations.py @@ -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) diff --git a/bot/handlers/__pycache__/callbacks.cpython-312.pyc b/bot/handlers/__pycache__/callbacks.cpython-312.pyc index ce2b30027374961cb515d3c0d0aa9bc4c7966d2e..2a70de5c53bbbeb6d7c5078467131fdcbb1fbe28 100644 GIT binary patch literal 13665 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 G!T$i%;P&nS diff --git a/bot/handlers/__pycache__/errors.cpython-312.pyc b/bot/handlers/__pycache__/errors.cpython-312.pyc index 558a073ca653529534e8c0f04f9acff8564285cd..8e9d386857e4f4119db00138602b34c26cbe7cef 100644 GIT binary patch delta 620 zcmYjOO=uHA6rQ)UKQl=-No`}3N>gnEF@cB{(@+qhZDImJp*LFuq1|mriCx$<#1?Hu zizkJK_2Ai)+MevygEq;@OF=IRf<5#qTLTK#gKugS2j2JI_q{hC^Yik=j~@M5pR6~!zMkAiOArVtGwJi`GLS`9q$xyb{e+oDGV(3rzf86R4Lo;QAncfB2;2ZK^ zb=J4yNq8HtGY*Gj|9sc|c#Deo0HgD2rw9dzusdY&_*`AXZnE>vw)9wID=b4E%{Y^^ z7-EKU7uKK-b#j}|kUC&z%y~wOz;?dUyadb{Q`4RZI_MbHNvEbh^tnXR{ce=_f^M%P zX%i?=-GAs8!jLnqjd=HbSKO0GkKBl}swLO=eEBclz>%u~FTk(r8hYKI7|r#=+w|F~ zak4uIQyF+mC8${oAcJvLb+rs zcsjdBfVlG~)wXKOrfvHLk>@O{VvF~9K{|-U4kP?zmu^``tzurbhmnfEsa{2JDFA#V zrw+)#XA(OgiGAL?S$NL}@2UGduzCFhcfPaW=_mxBuDS%%bK{#x9ZIC&p@fxv25o)= D?<(Yya&J}^d4>vp9#GR;&~j7MrP163 zbq$8RJHZ4qB{XglQ17O+e1y8!qWK{0`&bAI{UAfC9}%MIfMcjL2c?Q7OiT0a4gRQE zvFZ(*;pa}m53FXoYL&~Cdinl_Ro|?XZ3PQ1&5NL I>`6TO2aRNqrT_o{ diff --git a/bot/handlers/__pycache__/reminders_create.cpython-312.pyc b/bot/handlers/__pycache__/reminders_create.cpython-312.pyc index 267a88103e717a263e5db4a012272e225f010ca1..30f51e0693dceb63e50e23a6eb8c6ed33d7adb72 100644 GIT binary patch delta 1344 zcmZ`&UrbY17{A}?xwWlQJKBbd+Fle_F4j7kZ2<}X%e)3QA=$*u7zxln0+s4*foVt( zHQCHara2n-G~>gJ30YFTVWP`wKx8JqoV#v;__WLieK4lti_r&n&W$!h=1uPJ`+a|Y z-}l{n&;6_I`~my@k`kN1u04_S-A8@r>>jiYpR0OO9$VYf%;O8Mn5pMea=Fw=TA)hw z^BS@Zx!1QYtl}Q8TpS%k1N{+U^eMaNwENOkB&sf^CL?P# z+g?Vp>`uSd8Y@e|U72^Tn{|eCXDEAUHr%9#oAShbiN z?N;X8T@OkFz8m08XFQ-Z;+Yj5&Nsns;S)Sl6JpNqGH?tSxY58G149PZ8dz^&gMp14 z?+F~cfE*EB%XYBmZ5BYdC=3bYtmwi~7z480=NUeR*gFz`HVuMoI$pc0Bbn^%=<1c? zDOKuA^=-(+zRpO8W9f89B9@k-{c3k#LfZBDXHu6M>&SGc`lNU=H5lyY{}jDMbMzOQ zrN8Q@=uLe@KS{FziRPHSt)F1=>-wm~oN;=~NapBuI-H~9l0L$OIeJY$!*ajT2|6C! zBZKN?J(=j>$NP*GYym0-SFRuGt(C^yVKJkP!7CIM|Fe^w8u1xQIcAOq}Inq z#Z{pT%XO=+l!6G#Y;@tGX&Qwh7@`#8PF#sX)Rhm=Gq*%4Ubx@=&-u?i_srayd6O}3 znoI@`*HZ6j?nC#Axsx9gR-{i7&nd14nt?0pQ+o0f+j=JzA%BQY>8*4aBvI08XdAJ? zG__!^;n+nF5RpCwyV#H35zV8(TU)SoTRw|LPTIx$#c=Z?njL?yQQj^_nhu=$r{#C_ zqRp0((>q7n*7$-YpZ!0N1D|07&j~3pcG=eFR?m?i0=U$(x}O4s)LG+25ysSamMVdH z^`ZTS2uo_T>ka^?zUrRjc_*j#yRU%%S(3X6%eNVej3*dVj6F$i`6KSv>ZWIvH`k2| zxq0PeQ7J6uW)*s=S2bjCxW~AQztp&|9opSn)=1SFd8FGK7^x17)U2a)X5ag(@_x7n zvX|WVqFU2|>?3RXpr+#>OJptKKwUC{93UmLr0E36VNyzJ`E@hMQL^5eMqSo}93y3e zmM?oiPLQ&9Sj1q%4|0lZNLtfM2gqqsv2|)X1oF^9QVGW~cXhb4)g46W;um#gP$7 z;0_X7c(N`#@8o4HCM>C%S>coWST!a)m~%{i#%8H5k|LVMlA_;g;fyRZqU zVXH#@#YP~#@5085oj|$07j|6Os^_Q4SfmFu6G+JcsbWSTp-^N5ByWjr z_%x_RpgbyUdk-ohAJeOZu;39-w@as669z zMp>2_Z1Rjun$DXWy#kq-in=DB_mSdW1e6EaSDeMV`HRm_Ce96l+r>7DU1qeH{J~$B z(Q>j}z!T05f!o72hFxZ~0gKrJ#dIep1O_6h-4dwFY59RknUU>-598#8fx=-o7}?w9 z8s#nsC|wsYz9?XPnb8DUNdKaM{$)mk4@^vq8H_8`KQMsk4Jn_%^k-%UCPNTm3TAy| L14$Mw1UeG{Ps`4Q delta 155 zcmX@GlJU)AM!wU$yj%=GP;2O#dER0op9GT-`$mmbERzK|cqgZ`@oi>ib!3EcuEPW; z=d<&&RElcqZ+78ubY{|Y+I-z7kZH26zc}{-pjnJST>Oz`v%mjOCg#hG29pDWbQujN k*9ScT@=U-yQy@=w^6}t6u)Jc3F1O)FCS^vpqWM6*0P;X9C;$Ke diff --git a/bot/handlers/callbacks.py b/bot/handlers/callbacks.py index 22606cb..21f6f16 100644 --- a/bot/handlers/callbacks.py +++ b/bot/handlers/callbacks.py @@ -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" diff --git a/bot/handlers/errors.py b/bot/handlers/errors.py index e47bcff..9669738 100644 --- a/bot/handlers/errors.py +++ b/bot/handlers/errors.py @@ -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 diff --git a/bot/handlers/reminders_create.py b/bot/handlers/reminders_create.py index 7a3f4a3..9908703 100644 --- a/bot/handlers/reminders_create.py +++ b/bot/handlers/reminders_create.py @@ -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("Пожалуйста, отправь текстовое сообщение.") diff --git a/bot/handlers/reminders_manage.py b/bot/handlers/reminders_manage.py index ead7264..76e6d4f 100644 --- a/bot/handlers/reminders_manage.py +++ b/bot/handlers/reminders_manage.py @@ -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("Пожалуйста, отправь текстовое сообщение.") diff --git a/bot/keyboards/__pycache__/pagination.cpython-312.pyc b/bot/keyboards/__pycache__/pagination.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9839451f9b509dbff8f36af7f8030fd2f56d352 GIT binary patch literal 2920 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 b)L(s(*N!atHoxq-;dz$l{Qo8h=@S0~UB?1I literal 0 HcmV?d00001 diff --git a/bot/services/__pycache__/time_service.cpython-312.pyc b/bot/services/__pycache__/time_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54d531fc9a732d0f4b8caff8ca59c73b84c6e9e2 GIT binary patch literal 6444 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 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, + ) diff --git a/bot/services/time_service.py b/bot/services/time_service.py index 68684cb..04691ea 100644 --- a/bot/services/time_service.py +++ b/bot/services/time_service.py @@ -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: """ diff --git a/bot/utils/__pycache__/validators.cpython-312.pyc b/bot/utils/__pycache__/validators.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30b0c5e0a95578fb47d4bfab8e85b8f621e2ee08 GIT binary patch literal 2300 zcmah~-ES0C6u);q_G`A?wJoj)p#yD&G1?*xh%1(q0G1GHVo{`~u1jayPT8;Axhrh5 zC4Ep+j0q)>v=2P=u}w{EjQ;?<`hr;!>jWc)r@oDr2g8%++?grGRJqBVbMC$8+^_Td zo%yM$DM6s!Ida?1L7o?&*EK?j)3QAEOR*RA0 z2=rAeYDO&8l6OMfm|{l1REA?_EEtUkqj4BbmFN{$_-%w90rdpRrrK{5AR|#RN84-F7D8 zMPmOIrl&gTPqFt(T%ZRxuUa#Ux%yLz@*64&Q+a>O* zTlvWh+2Fe z{b}Y>nfD+aoSuLmW#)8OYHnN5wQ}MO{WoR+2oi(O`2oZX`Lm5A+tyO8k5ZW%*REe{ zq}pc&ya-7ho_YUaviZiq^?|ty3lojx{+UyMXlZctl&ISDFx@(LYX0oavy1fBU?bfz zt3GVmF=x$RzIl0Z@2yg!rE50&NK4MC542qiEg-xENv1uG?A^C`;emc=RRST#p2SJp zD+?bjz5SrI_kOb1i-F(YPoktLbEo;fw*N0e)uy#r@{yMEqF_FC_Q0`M`{{{ha`#XZ z$a^G-vN?4^m+!SxkZ#P(*Bs>gb38|m3ZBn`5%L@uxyf_j{3fHpC1@e%R|3CjsCRWL z_ya&-5mOfYQR;MvAiLr|Q#LCC$7G7OEmM@K9|a1xVG7;?C;}LOL~hHC@Sa)yg)2#;bYtOf4JQYhL`>;Pd%Y!@h3 z!KEFfjIJ}R^X|)=uoTA+FUT;lufykj4&oUwJw*k)rCR2W&iCEyYqWMWQk}EPnwFg9 z-yWZHzB#_unwdT8$t1Z6vcURZ7`4JCUv>4671~3-PxVvzE}0wU_B?Nkc++O}C0Yz5!!)~Nrea{lt%GM}rf z2Simh!~v$%u7PaVIru-ufDvQ3xItMv+`~ac2T95J)nT(zp&Z^@-%j=ST?H#tB)Db#q&BtAUv5;`YYM{n8bf0JAWtpyhuAuFZOx_ u)Y8$_lgoV@XnH4U8;lQm1k} 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