- DIY
- A
Telegram bot for discipline in Python: aiogram 3, APScheduler and deployment on VDS
I decided to write this not for promotion, but for constructive feedback, to continue working on the project, as I am currently deciding what to do next and what it could grow into.
Let me warn you right away: I made this with AI, so if that triggers someone, feel free to skip the article.
Yes, another bot, but the topic is close to me and I wanted to create something of my own.
What we have as is - a pet project about how I built and launched a Telegram bot from scratch that reminds you of the focus of the day, tracks completions, gives achievements, gently motivates, works across time zones, and runs on a VDS under systemd.
Goal: one focus per day without unnecessary services
I wanted to solve my personal problem with discipline: not to get lost in routine and not to neglect small but important tasks like charging, pet projects, or studying. Without a new app, registrations, and task managers — everything is inside Telegram.
This gave birth to a simple requirement for the product:
one active focus per user;
two touches a day: remind in the morning, ask in the evening how the day went;
light gaming shell: streaks, achievements, goal level;
minimum friction: start by command, everything else through the bot.
The result is the bot @focuscompanion_bot, which I currently use myself and am also refining based on feedback.
Functionality from the user's perspective
Work scenario:
At startup, the bot conducts onboarding: name, time of morning and evening notifications, time zone, domain (work/personal/health), and the formulation of the focus.
Every morning, a message arrives with a reminder about the focus.
In the evening, the bot asks to mark the result for the goal:
✅ done,🌓 partially,❌ not done.By
/week, it shows a snapshot of the goal: for short-term goals — a "weekly" view with a 7-day strip, for long-term ones — aggregated statistics without a 7-day tie.By
/streak, it shows the current and best series./achievements— a list of achievements obtained across all goals./time,/focus,/settings,/feedback— manage schedule, goal, minimal mode (just got a hint for this) and feedback, so users can immediately report what to fix.
I keep the project intentionally simple, but with a proper structure.
Stack:
Python 3.12
aiogram 3 (asynchronous Telegram framework)
SQLite as storage (for now)
APScheduler for periodic tasks
systemd service for launching on VDS Timeweb
Project files:
bot.py— main logic, command handlers, FSM onboarding and settings, APScheduler scheduler.db.py+models.sql— layer for working with SQLite and database schema.config.py— reading.env, bot token ultimately stored in variables later, dictionaries for achievements and thresholds.discipline.db— the database itself.requirements.txt— dependencies.images/— including welcome screen / startup image.
The bot is fully on async/await: aiogram 3, FSM out of the box, asynchronous APScheduler.
Data Model
Minimal set of entities:
users:id,tg_idnamemorning_time,checkin_time(stringsHH:MM)start_datetimezone(string likeEurope/Moscow)service fields:
last_morning_sent,last_checkin_reminder_sent,minimal_mode.
focuses:
iduser_idtitle(goal formulation)domain(work/personal/health)is_active,started_at,ended_atbest_streak(best streak for this goal).
checkins:
iduser_id,focus_iddate(YYYY‑MM‑DD)status(done,partial,fail).
achievements:
iduser_id,focus_idlevel(achievement level number)days(length of the streak at the time of achievement)created_at.
This set allows:
to quickly calculate the current and best streak for the active focus;
to build simple weekly and long-term statistics;
to grant achievements for reaching thresholds
ACHIEVEMENT_THRESHOLDS
Onboarding and FSM
Onboarding is done via FSM aiogram: each state is responsible for one step and stores intermediate data.
Step chain:
/start→ check if the user is in the database.If new:
name;
morning time (
morning_time);evening time (
checkin_time);time zone (selection from a fixed list of Russian time zones);
domain (work/personal/health);
formulation of the focus.
In the FSM data, all fields are collected, and at the end:
a record is created in
focuses(active focus);the user fields in
usersare updated;the user is brought to the main scenario with check-in buttons.
Time Zones and Schedule
One of the most non-trivial parts for the pet project is time zones and schedule.
Approach:
During onboarding, the user manually selects a time zone from a list of pre-set options (for example,
Europe/Moscow,Asia/Yekaterinburg, etc.).A string with the name of the time zone is saved in the database in
users.timezone.APScheduler runs in UTC and executes two jobs once a minute:
Read also:send_morning_focussend_daily_checkins.
Job operation scheme:
We take all users whose
last_morning_sent(orlast_checkin_reminder_sent) is not equal to today's date.For each:
convert
now_utcto local time usingpytz.timezone(user.timezone);compare local time
HH:MMwith savedmorning_time/checkin_time.
If "now" matches the user's time — send a message, mark
last_*_sent = today.
from datetime import datetime
from pytz import timezone as pytz_timezone
async def send_daily_checkins():
now_utc = datetime.now(pytz_timezone("UTC"))
today_str_utc = now_utc.strftime("%Y-%m-%d")
# Get all users who haven't received the evening message today
users = await get_users_for_evening(today_str_utc)
if not users:
return
ids_to_mark: list[int] = []
for user in users:
tg_id = user["tg_id"]
user_id = user["id"]
tz_name = user["timezone"] or "Europe/Moscow"
user_tz = pytz_timezone(tz_name)
now_user = now_utc.astimezone(user_tz)
today_str = now_user.strftime("%Y-%m-%d")
checkin_time_local = (user["checkin_time"] or "").replace(":", "")
if not checkin_time_local:
continue
current_time_local = now_user.strftime("%H%M")
if current_time_local != checkin_time_local:
continue
status = await get_today_checkin_status(user_id, today_str)
if status:
# there is already a check-in — just send a brief summary
text = get_summary_text(status, user.get("name"))
await bot.send_message(tg_id, text)
else:
# no check-in — send buttons done/partial/fail
text = "How was your day regarding your focus?"
await bot.send_message(tg_id, text, reply_markup=checkin_kb)
ids_to_mark.append(user_id)
if ids_to_mark:
await mark_evening_sent(ids_to_mark, today_str_utc)This solution:
does not create separate cron tasks for each;
works the same for different regions of Russia;
remains simple for debugging.
Statistics and Achievements
A daily check-in is a record in checkins with status done/partial/fail.
Further on top of this:
Streak: I count the number of consecutive days with
doneorpartial, starting from the last date with a check-in backward. A partial day does not break the streak.Weekly statistics:
for short goals (≤7 days) I show:
a 7-day status stream (emoji);
a progress bar of 10 blocks that uses the formula
done + partial * 0.5;a textual summary of the week.
for long goals (>7 days) — only aggregates for all days:
how many days total for the goal;
how many of them are
done/partial/fail;without tying to “week” and “7 days”.
The achievement system:
there is an array of thresholds
ACHIEVEMENT_THRESHOLDS = [3, 7, 14, 30, ...];upon each streak update, the level is calculated
get_achievement_level(streak);if a new level is reached and it is higher than the saved one for this
focus_id— a record is added toachievements.
Deploy: GitHub → Timeweb VDS → systemd
I have organized development and deployment this way.
Locally:
Work in the GitHub repository (private).
Cycle: code edits → local bot launch → onboarding and main command checks →
git commit→git push.
On the server (VDS Timeweb):
In the project directory:
git pull origin mainpip install -r requirements.txt(if necessary)schema migrations via
models.sql(minimal DDL).
The bot runs as discipline-bot.service under systemd:
ExecStart=/root/discipline_bot/venv/bin/python /root/discipline_bot/bot.pyrestart via
systemctl restart discipline-bot.
For convenience, there is a script deploy.sh that does a pull, updates dependencies, and restarts the service, which has greatly simplified commits lately, of which there have been many.
Previously, I used a specialized "bot hosting," but encountered issues with caching, race conditions, and debugging complexity, so I moved to a regular VDS with a bare terminal — this turned out to be easier and more predictable, and seems more professional.
What's next
The bot is currently live in production, sending notifications daily. From the next steps, I see:
migration from SQLite to PostgreSQL (for more complex analytics and parallel load);
detailed statistics by weeks and months (retention, "percentage of ideal weeks," distribution by status);
enhancement of the achievement system and difficulty levels;
data export and possibly a lightweight web panel for viewing progress from a desktop;
full i18n and English localization, so the bot can confidently be taken to Reddit and English-speaking platforms.
Link to the bot: https://t.me/focuscompanion_bot.
Question to tekkix readers: what would you definitely add to such a "discipline tracker bot" in terms of metrics and UX, or perhaps in the process? And how can it be promoted?
Thank you all in advance! I would appreciate any feedback!
Write comment