Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion telegram_bot_project/bot/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,29 @@ def work_buttons_keyboard(task_id: int) -> InlineKeyboardMarkup:
cancel_work_btn: InlineKeyboardButton = InlineKeyboardButton(text=CANCEL_WORK_BTN, callback_data=f"cancel_task:{task_id}")

inline_markup.inline_keyboard.append([start_work_btn, cancel_work_btn])
return inline_markup
return inline_markup

def get_work_session_keyboard() -> None:
task_finished = KeyboardButton(text=STOP_WORK_SESSION)
task_break = KeyboardButton(text=CANCEL_WORK_BTN)

keyboard = [
[task_break, task_finished]
]

return ReplyKeyboardMarkup(
keyboard=keyboard,
resize_keyboard=True
)

def get_start_day_btn() -> ReplyKeyboardMarkup:
start_day_btn = KeyboardButton(text=START_DAY_BTN)

keyboard = [
[start_day_btn]
]

return ReplyKeyboardMarkup(
keyboard=keyboard,
resize_keyboard=True
)
12 changes: 10 additions & 2 deletions telegram_bot_project/bot/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# bot/callbacks.py
from typing import Optional
from datetime import datetime

from aiogram import types
from aiogram.fsm.context import FSMContext
Expand All @@ -12,6 +13,7 @@
from service.user import UserService
from states import DialogStates
from service.focus import FocusService
from states import user_task_start_time

async def start_callback_language(callback_query: types.CallbackQuery) -> None:
await callback_query.answer()
Expand Down Expand Up @@ -208,12 +210,18 @@ async def callback_task_menu(callback_query: types.CallbackQuery) -> None:
action, task_id_str = data.split(":")
task_id = int(task_id_str)

print(f"[DEBUG] - Task id: {task_id}")
task = await TaskService.get_task_by_id(task_id)
task_name = task['task_name']

match action:
case "complete_task":
await TaskService.update_started_status(task_id)
await callback_query.message.answer(MESSAGES[language]['REMIND_WORK_CANCEL'],
reply_markup=menu_reply_keyboard())
user_task_start_time[user_id] = (task_id, datetime.now())
await callback_query.message.answer(MESSAGES[language]['REMIND_WORK_START'].format(task_name),
reply_markup=get_work_session_keyboard())
case "cancel_task":
await TaskService.update_started_status(task_id)
user_task_start_time[user_id] = (task_id, datetime.now())
await callback_query.message.answer(MESSAGES[language]['REMIND_WORK_CANCEL'],
reply_markup=menu_reply_keyboard())
29 changes: 28 additions & 1 deletion telegram_bot_project/bot/commands.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# bot/commands.py
from aiogram import types
from aiogram.fsm.context import FSMContext
from datetime import datetime

from service.focus import FocusService
from bot.utills import format_date, calculate_awake_hours
Expand Down Expand Up @@ -385,7 +386,7 @@ async def show_evening_routines(message: types.Message):
print(f"[INFO] - User with id: {user_id} - opened /evening_routines.")
evening_routine = await RoutineService.get_user_routines(user_id, routine_type="evening")
if not evening_routine:
await message.answer(MESSAGES[language]['NO_MORNING_ROUTINE'])
await message.answer(MESSAGES[language]['NO_EVENING_ROUTINE'])
return

dividers: str = "\n" + ("-" * int(len(MESSAGES[language]['EVENING_ROUTINE_SHOW']) * 1.65))
Expand Down Expand Up @@ -499,3 +500,29 @@ async def delete_focus_session(message: types.Message, state: FSMContext) -> Non
await state.update_data(focuses=focuses)
await state.set_state(DialogStates.delete_focus)
await message.answer(MESSAGES[language]['DELETE_FOCUS_SESSION_MSG'])

async def start_day_command(message: types.Message) -> None:
user_id: int = message.from_user.id
user_find: Any = await UserService.get_user_by_id(user_id)
language: str = await UserService.get_user_language(user_id) or 'ENGLISH'

if not user_find:
await message.answer(MESSAGES['ENGLISH']['AUTHORIZATION_PROBLEM'])
return

morning_routine = await RoutineService.get_user_routines(user_id, routine_type="morning")
print(f"[INFO] - Sending morning routine to user with id {user_id}")

time_str = datetime.now().strftime("%H:%M")

await MyDayService.add_wake_up_time(user_id, time_str)

formatted_routine_items = "\n".join(
f"# {idx}. {routine['routine_name']}"
for idx, routine in enumerate(morning_routine, start=1)
)

await message.answer(
MESSAGES[language]['SEND_MORNING_MSG'] + '\n' + formatted_routine_items,
reply_markup=menu_reply_keyboard()
)
243 changes: 191 additions & 52 deletions telegram_bot_project/bot/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# bot/scheduler.py
from datetime import time, datetime, timedelta
from aiogram import Bot
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.memory import MemoryStorage
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from messages import send_morning_message, send_evening_message
from bot.buttons import work_buttons_keyboard
from apscheduler.triggers.date import DateTrigger
from bot.buttons import work_buttons_keyboard, get_start_day_btn
from service.task import TaskService
from service.user import UserService
from collections import defaultdict
from typing import Set
from messages import *

scheduler: AsyncIOScheduler = AsyncIOScheduler()
notified_task_ids = set()
sent_notifications: defaultdict[int, Set[str]] = defaultdict(set)

def initialize_scheduler():
scheduler.start()
Expand Down Expand Up @@ -41,54 +43,147 @@ async def schedule_all_users_jobs(bot: Bot):
if user["sleep_time"]:
update_user_job(user["id"], user["sleep_time"], bot, job_type="sleep")

async def check_upcoming_tasks(bot: Bot):
class PreciseTaskNotifier:
def __init__(self, bot: Bot, scheduler: AsyncIOScheduler):
self.bot = bot
self.scheduler = scheduler
self.scheduled_jobs = {}

async def schedule_task_notifications(self, task_id: int, user_id: int, task_name: str, start_time: datetime, language: str):
now = datetime.now()

notification_times = [
(start_time - timedelta(minutes=30), 30, "30min"),
(start_time - timedelta(minutes=15), 15, "15min"),
(start_time - timedelta(minutes=5), 5, "5min"),
(start_time, 0, "now")
]

for notification_time, minutes, notification_type in notification_times:
if notification_time > now:
job_id = f"task_{task_id}_{notification_type}"

try:
if self.scheduler.get_job(job_id):
self.scheduler.remove_job(job_id)
except:
pass

try:
self.scheduler.add_job(
self._send_notification,
trigger=DateTrigger(run_date=notification_time),
args=[language, user_id, task_name, minutes, task_id],
id=job_id,
replace_existing=True
)
self.scheduled_jobs[job_id] = True
print(f"[SCHEDULED] - Notification for task {task_id} at {notification_time.strftime('%H:%M:%S')}")
except Exception as e:
print(f"[ERROR] Failed to schedule notification for task {task_id}: {e}")

async def _send_notification(self, language: str, user_id: int, task_name: str, minutes: int, task_id: int):
try:
await send_task_notification(language, self.bot, user_id, task_name, minutes, task_id)
print(f"[PRECISE NOTIFY] - Sent notification to user {user_id} for task {task_id}")

notification_key = f"{task_id}_{minutes}min" if minutes > 0 else f"{task_id}_now"
sent_notifications[user_id].add(notification_key)

except Exception as e:
print(f"[ERROR] Failed to send precise notification: {e}")

async def schedule_all_task_notifications(self):
try:
tasks = await TaskService.get_all_upcoming_tasks()
print(f"[INFO] Scheduling notifications for {len(tasks)} tasks")

for task in tasks:
task_time = task.get("start_time")
if not task_time:
continue

task_id = task["id"]
user_id = task["user_id"]
task_name = task["task_name"]
language = await UserService.get_user_language(user_id)

await self.schedule_task_notifications(task_id, user_id, task_name, task_time, language)

except Exception as e:
print(f"[ERROR] Failed to schedule task notifications: {e}")

async def remove_task_notifications(self, task_id: int):
notification_types = ["30min", "15min", "5min", "now"]

for notification_type in notification_types:
job_id = f"task_{task_id}_{notification_type}"
try:
if self.scheduler.get_job(job_id):
self.scheduler.remove_job(job_id)
if job_id in self.scheduled_jobs:
del self.scheduled_jobs[job_id]
print(f"[REMOVED] - Notification job {job_id}")
except Exception as e:
print(f"[ERROR] Failed to remove notification job {job_id}: {e}")

async def check_upcoming_tasks_v2(bot: Bot):
now = datetime.now()
print("[TASK CHECK] - Checking for upcoming tasks at {}".format(now.strftime('%Y-%m-%d %H:%M:%S')))

tasks = await TaskService.get_all_upcoming_tasks()

for task in tasks:
task_time = task.get("start_time")
if not task_time:
continue

task_id = task["id"]
user_id = task["user_id"]
task_name = task["task_name"]
language: str = await UserService.get_user_language(user_id)

time_diff = (task_time - now).total_seconds()
time_after_start = (now - task_time).total_seconds()

print(f"[DEBUG] - Task: '{task_name}' | user_id: {user_id} | time_diff: {time_diff:.2f} sec | start_time: {task_time.strftime('%H:%M:%S')}")

if 1790 <= time_diff <= 1810:
print(f"[NOTIFY] - User {user_id} - Task '{task_name}' starts in 30 min")
await send_task_notification(language, bot, user_id, task_name, 30)
elif 890 <= time_diff <= 910:
print(f"[NOTIFY] - User {user_id} - Task '{task_name}' starts in 15 min")
await send_task_notification(language, bot, user_id, task_name, 15)
elif 290 <= time_diff <= 310:
print(f"[NOTIFY] - User {user_id} - Task '{task_name}' starts in 5 min")
await send_task_notification(language, bot, user_id, task_name, 5)
elif -60 <= time_diff <= 60:
print(f"[NOTIFY] - User {user_id} - Task '{task_name}' starts now")
await send_task_notification(language, bot, user_id, task_name, 0, task_id)

if time_after_start >= 0:
started = await TaskService.get_started_status(task_id)
print(f"[DEBUG REMINDER] Task {task_id}, started: {started}, time_after_start: {time_after_start:.2f}")

for target_minute in [5, 10, 15]:
lower = target_minute * 60 - 60
upper = target_minute * 60 + 60
if lower <= time_after_start <= upper:
reminder_key = (task_id, f"late_{target_minute}")
if reminder_key not in notified_task_ids:
print(f"[REMIND] - Sending reminder for task {task_id} after {target_minute} min")
await send_task_notification(language, bot, user_id, task_name, target_minute, task_id)
notified_task_ids.add(reminder_key)
print(f"[BACKUP CHECK] - Checking for upcoming tasks at {now.strftime('%Y-%m-%d %H:%M:%S')}")

try:
tasks = await TaskService.get_all_upcoming_tasks()

for task in tasks:
task_time = task.get("start_time")
if not task_time:
continue

task_id = task["id"]
user_id = task["user_id"]
task_name = task["task_name"]
language: str = await UserService.get_user_language(user_id)

time_diff = (task_time - now).total_seconds()
time_after_start = (now - task_time).total_seconds()

print(f"[DEBUG] - Task: '{task_name}' | user_id: {user_id} | time_diff: {time_diff:.2f} sec | start_time: {task_time.strftime('%H:%M:%S')}")

notifications_to_check = [
(1800, 30, "30min"),
(900, 15, "15min"),
(300, 5, "5min"),
(0, 0, "now")
]

for target_seconds, minutes, notification_type in notifications_to_check:
if target_seconds - 60 <= time_diff <= target_seconds + 60:
notification_key = f"{task_id}_{notification_type}"

if notification_key not in sent_notifications[user_id]:
print(f"[BACKUP NOTIFY] - User {user_id} - Task '{task_name}' notification: {notification_type}")
await send_task_notification(language, bot, user_id, task_name, minutes, task_id)
sent_notifications[user_id].add(notification_key)

if time_after_start >= 300:
try:
started = await TaskService.get_started_status(task_id)
print(f"[DEBUG REMINDER] Task {task_id}, started: {started}, time_after_start: {time_after_start:.2f}")

for target_minute in [5, 10, 15]:
lower = target_minute * 60 - 60
upper = target_minute * 60 + 60
if lower <= time_after_start <= upper:
reminder_key = f"{task_id}_late_{target_minute}"
if reminder_key not in sent_notifications[user_id]:
print(f"[BACKUP REMIND] - Sending reminder for task {task_id} after {target_minute} min")
await send_task_notification(language, bot, user_id, task_name, target_minute, task_id)
sent_notifications[user_id].add(reminder_key)
except Exception as e:
print(f"[ERROR] Failed to check task status for {task_id}: {e}")

except Exception as e:
print(f"[ERROR] Failed in backup task check: {e}")

async def send_task_notification(language: str, bot: Bot, user_id: int, task_name: str, minutes: int, task_id: int):
messages = {
Expand All @@ -115,7 +210,51 @@ async def send_task_notification(language: str, bot: Bot, user_id: int, task_nam
text = message_set["soon"]

try:
await bot.send_message(user_id, text, reply_markup=work_buttons_keyboard(task_id))
await bot.send_message(user_id, text, reply_markup=work_buttons_keyboard(task_id), parse_mode="Markdown")
print(f"[BOT] Sent message to {user_id}: {text}")
except Exception as e:
print(f"[ERROR] Failed to send message to {user_id}: {e}")
print(f"[ERROR] Failed to send message to {user_id}: {e}")

async def cleanup_old_notifications():
global sent_notifications
sent_notifications.clear()
print("[CLEANUP] - Cleared old notification cache")

_notifier_instance = None

def get_notifier_instance(bot: Bot = None) -> PreciseTaskNotifier:
global _notifier_instance
if _notifier_instance is None and bot:
_notifier_instance = PreciseTaskNotifier(bot, scheduler)
return _notifier_instance

async def schedule_new_task_notifications(task_id: int, user_id: int, task_name: str, start_time: datetime, language: str):
notifier = get_notifier_instance()
if notifier:
await notifier.schedule_task_notifications(task_id, user_id, task_name, start_time, language)

async def remove_task_notifications(task_id: int):
notifier = get_notifier_instance()
if notifier:
await notifier.remove_task_notifications(task_id)


async def send_morning_message(bot: Bot, user_id: int):
language = await UserService.get_user_language(user_id) or "ENGLISH"
user = await UserService.get_user_by_id(user_id)

user_name = user.get('user_name', 'User') if user else 'User'

await bot.send_message(
user_id,
MESSAGES[language]['WAKE_UP_MESSAGE'].format(user_name),
reply_markup=get_start_day_btn()
)

async def send_evening_message(bot: Bot, user_id: int):
language = await UserService.get_user_language(user_id) or "ENGLISH"
print(f"[INFO] - Sending evening routine to user with id, {user_id}")
await bot.send_message(
user_id,
MESSAGES[language]['SEND_EVENING_MSG'].format("👤")
)
Loading