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
3 changes: 2 additions & 1 deletion sql/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ CREATE TABLE tasks (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
task_name VARCHAR(255) NOT NULL,
status BOOLEAN NOT NULL DEFAULT FALSE,
start_time TIMESTAMP,
start_time TIMESTAMP UNIQUE NOT NULL,
started BOOLEAN DEFAULT FALSE NOT NULL,
creation_date TIMESTAMP NOT NULL DEFAULT NOW()
);

Expand Down
12 changes: 10 additions & 2 deletions telegram_bot_project/bot/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def idea_reply_keyboard() -> ReplyKeyboardMarkup:
def task_reply_keyboard() -> InlineKeyboardMarkup:
inline_markup = InlineKeyboardMarkup(inline_keyboard=[], row_width=2)

button_yes: InlineKeyboardButton = InlineKeyboardButton(text=BUTTON_YES, callback_data="yes_task")
button_no: InlineKeyboardButton = InlineKeyboardButton(text=BUTTON_NO, callback_data="no_task")
button_yes: InlineKeyboardButton = InlineKeyboardButton(text=BUTTON_YES_BTN, callback_data="yes_task")
button_no: InlineKeyboardButton = InlineKeyboardButton(text=BUTTON_NO_BTN, callback_data="no_task")

inline_markup.inline_keyboard.append([button_yes, button_no])
return inline_markup
Expand Down Expand Up @@ -195,3 +195,11 @@ def focus_title_ask_keyboard() -> InlineKeyboardMarkup:

return inline_markup

def work_buttons_keyboard(task_id: int) -> InlineKeyboardMarkup:
inline_markup: InlineKeyboardMarkup = InlineKeyboardMarkup(inline_keyboard=[], row_width=2)

start_work_btn: InlineKeyboardButton = InlineKeyboardButton(text=START_WORK_BTN, callback_data=f"complete_task:{task_id}")
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
39 changes: 38 additions & 1 deletion telegram_bot_project/bot/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# bot/callbacks.py
from typing import Optional

from aiogram import types
from aiogram.fsm.context import FSMContext

from bot.buttons import *
Expand Down Expand Up @@ -179,4 +181,39 @@ async def callback_focus_title(callback_query: types.CallbackQuery, state: FSMCo
await callback_query.message.answer(MESSAGES[language]["SAVED_FOCUS_MSG"], reply_markup=focus_menu_keyboard())
await state.clear()
case _:
await callback_query.message.answer(MESSAGES[language]["FOCUS_INVALID"], reply_markup=focus_menu_keyboard())
await callback_query.message.answer(MESSAGES[language]["FOCUS_INVALID"], reply_markup=focus_menu_keyboard())

async def callback_work_buttons(callback_query: types.CallbackQuery, state: FSMContext) -> None:
await callback_query.answer()

user_id: int = callback_query.from_user.id
user_find: Optional[dict] = await UserService.get_user_by_id(user_id)
if not user_find:
await callback_query.message.answer(MESSAGES['ENGLISH']["AUTHORIZATION_PROBLEM"])
return

async def callback_task_menu(callback_query: types.CallbackQuery) -> None:
await callback_query.answer()

user_id: int = callback_query.from_user.id
user_find: Optional[dict] = await UserService.get_user_by_id(user_id)
language: str = await UserService.get_user_language(user_id)
if not language:
language = 'ENGLISH'
if not user_find:
await callback_query.message.answer(MESSAGES['ENGLISH']["AUTHORIZATION_PROBLEM"])
return

data = callback_query.data
action, task_id_str = data.split(":")
task_id = int(task_id_str)

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())
case "cancel_task":
await TaskService.update_started_status(task_id)
await callback_query.message.answer(MESSAGES[language]['REMIND_WORK_CANCEL'],
reply_markup=menu_reply_keyboard())
2 changes: 2 additions & 0 deletions telegram_bot_project/bot/commands.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# bot/commands.py
from aiogram import types
from aiogram.fsm.context import FSMContext

from service.focus import FocusService
Expand All @@ -7,6 +8,7 @@
from service.task import TaskService
from bot.buttons import *
from states import DialogStates
from messages import MESSAGES
from service.myday import MyDayService

# Start Command Handler
Expand Down
2 changes: 2 additions & 0 deletions telegram_bot_project/bot/fallbacks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# bot/fallbacks.py
from aiogram import types

from bot.handlers import *
from states import DialogStates

Expand Down
86 changes: 85 additions & 1 deletion telegram_bot_project/bot/scheduler.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# bot/scheduler.py
from datetime import time
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 service.task import TaskService
from service.user import UserService

scheduler: AsyncIOScheduler = AsyncIOScheduler()
notified_task_ids = set()

def initialize_scheduler():
scheduler.start()
Expand Down Expand Up @@ -35,3 +40,82 @@ async def schedule_all_users_jobs(bot: Bot):
update_user_job(user["id"], user["wake_time"], bot, job_type="wake")
if user["sleep_time"]:
update_user_job(user["id"], user["sleep_time"], bot, job_type="sleep")

async def check_upcoming_tasks(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)


async def send_task_notification(language: str, bot: Bot, user_id: int, task_name: str, minutes: int, task_id: int):
messages = {
"UKRAINIAN": {
"soon": f"⏰ Завдання *\"{task_name}\"* почнеться через {minutes} хвилин!",
"now": f"🚀 Завдання *\"{task_name}\"* починається прямо зараз!\nПочинаємо?",
"reminder": f"⚠️ Нагадування: завдання *\"{task_name}\"* мало початися {minutes} хвилин тому, але не розпочате."
},
"ENGLISH": {
"soon": f"⏰ Task *\"{task_name}\"* will start in {minutes} minutes!",
"now": f"🚀 Task *\"{task_name}\"* is starting right now!\nStart it?",
"reminder": f"⚠️ Reminder: task *\"{task_name}\"* was supposed to start {minutes} minutes ago but not started yet."
}
}

lang_key = language.upper()
message_set = messages.get(lang_key, messages["ENGLISH"])

if minutes == 0:
text = message_set["now"]
elif minutes in [5, 10, 15]:
text = message_set["reminder"]
else:
text = message_set["soon"]

try:
await bot.send_message(user_id, text, reply_markup=work_buttons_keyboard(task_id))
print(f"[BOT] Sent message to {user_id}: {text}")
except Exception as e:
print(f"[ERROR] Failed to send message to {user_id}: {e}")
18 changes: 18 additions & 0 deletions telegram_bot_project/bot/timer_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from datetime import datetime
from typing import Dict

active_timers: Dict[int, datetime] = {}
active_work: Dict[int, str] = {}

def start_timer(user_id: int):
active_timers[user_id] = datetime.now()

def stop_timer(user_id: int) -> int:
start_time = active_timers.pop(user_id, None)
if start_time:
elapsed = (datetime.now() - start_time).total_seconds()
return int(elapsed)
return 0

def is_timer_running(user_id: int) -> bool:
return user_id in active_timers
4 changes: 3 additions & 1 deletion telegram_bot_project/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# config.py
import os
import logging
from dotenv import load_dotenv
Expand Down Expand Up @@ -41,4 +42,5 @@ class SmtpData:
smtp_subject: str = os.getenv("SMTP_MESSAGE_SUBJECT")

def get_smtp_data() -> SmtpData:
return SmtpData()
return SmtpData()

16 changes: 14 additions & 2 deletions telegram_bot_project/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# main.py
import asyncio
from datetime import datetime
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import CallbackQuery
from apscheduler.schedulers.asyncio import AsyncIOScheduler

from config import TOKEN
from bot.scheduler import initialize_scheduler, schedule_all_users_jobs
from bot.scheduler import initialize_scheduler, schedule_all_users_jobs, check_upcoming_tasks
from bot.commands import *
from bot.callbacks import *
from bot.fallbacks import *
Expand Down Expand Up @@ -262,13 +262,18 @@ async def callback_focus_save(callback_query: CallbackQuery, state: FSMContext):
async def callback_focus_title_save(callback_query: CallbackQuery, state: FSMContext):
await callback_focus_title(callback_query, state)

@dp.callback_query(F.data.startswith(("complete_task", "cancel_task")))
async def callback_task_status(callback_query: CallbackQuery) -> None:
await callback_task_menu(callback_query)

@dp.message()
async def process_fallback(message: Message, state: FSMContext):
await fallback(message, state)

# Main Function
async def main():
bot = Bot(token=TOKEN)
storage = dp.storage

scheduler: AsyncIOScheduler = initialize_scheduler()
scheduler.add_job(
Expand All @@ -278,6 +283,13 @@ async def main():
minute='0'
)

scheduler.add_job(
check_upcoming_tasks,
trigger='interval',
minutes=1,
args={bot}
)

await schedule_all_users_jobs(bot)
await dp.start_polling(bot)

Expand Down
12 changes: 9 additions & 3 deletions telegram_bot_project/messages.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# messages.py
from typing import Any
from aiogram import types, Bot
from aiogram import Bot
from service.user import UserService
from service.routine import RoutineService

Expand Down Expand Up @@ -122,6 +123,7 @@
"LANGUAGE_INVALID": "❌ Некоректний вибір мови. Спробуйте ще раз. 🔢",
"DELETE_FOCUS_SESSION_MSG": "Вкажіть номер сесії, яку ви хочете видалити.",
"FOCUS_DELETED": "✅ Фокус-сесію №{} з назвою \"{}\" успішно видалено.",
"REMIND_WORK_CANCEL": "Нагадування для цього завдання було вимкнено",
},
"ENGLISH": {
"START_MSG": (
Expand Down Expand Up @@ -234,6 +236,7 @@
"FOCUS_LIST_TITLE": "🧠 Your Focus Sessions",
"NO_FOCUS_SESSIONS": "😕 No focus sessions found.",
"DELETE_FOCUS_SESSION_MSG": "Provide a number of session which you want to delete.",
"REMIND_WORK_CANCEL": "Reminders for this task was deactivated",
"LANGUAGE_ASK": (
"🌐 Choose your language. \n"
"Select an option below. 📚"
Expand All @@ -258,8 +261,8 @@
MENU_BUTTON: str = "🏠 Main Menu"
UPDATE_IDEA_BUTTON: str = "🆙 Update Idea"
ALL_IDEAS: str = "🔍 View All Ideas"
BUTTON_YES: str = "👍 Yes"
BUTTON_NO: str = "🙅 No"
BUTTON_YES_BTN: str = "👍 Yes"
BUTTON_NO_BTN: str = "🙅 No"
BUTTON_DELETE_TASK = "🗑️ Delete Task"
BUTTON_EDIT_TASK = "✏️ Edit Task"
BUTTON_TOGGLE_STATUS = "✅ Mark Complete"
Expand Down Expand Up @@ -291,6 +294,9 @@
ALL_FOCUSES_BTN = "All Focuses"
DELETE_FOCUS = "Delete"

START_WORK_BTN = "Start"
CANCEL_WORK_BTN = "Cancel"

USER_FEEDBACK_MAIL_TEXT = """
📬 New feedback received!

Expand Down
1 change: 1 addition & 0 deletions telegram_bot_project/service/focus.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# service/focus.py
from sqlalchemy import text
from config import get_session
from typing import Optional
Expand Down
1 change: 1 addition & 0 deletions telegram_bot_project/service/idea.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# service/idea.py
from sqlalchemy import text
from typing import Optional
from config import get_session
Expand Down
1 change: 1 addition & 0 deletions telegram_bot_project/service/myday.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# service/myday.py
from sqlalchemy import text
from config import get_session
from typing import Any, Optional
Expand Down
1 change: 1 addition & 0 deletions telegram_bot_project/service/routine.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# service/routine.py
from sqlalchemy import text
from config import get_session
from typing import Any, List, Optional
Expand Down
1 change: 1 addition & 0 deletions telegram_bot_project/service/smtp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# service/smtp.py
import aiosmtplib
from datetime import datetime
from email.mime.text import MIMEText
Expand Down
Loading