Telegram workout tracker bot with Mini App web UI, SQLite database, API server, and cloudflared tunnel support. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
6.6 KiB
Python
198 lines
6.6 KiB
Python
"""
|
||
Telegram Fitness Bot — handles chat commands, reminders, and launches the Mini App.
|
||
"""
|
||
import logging
|
||
|
||
from telegram import (
|
||
Update,
|
||
WebAppInfo,
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
MenuButtonWebApp,
|
||
)
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
ContextTypes,
|
||
MessageHandler,
|
||
filters,
|
||
)
|
||
|
||
import database as db
|
||
from config import BOT_TOKEN, WEBAPP_URL
|
||
|
||
logging.basicConfig(
|
||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||
level=logging.INFO,
|
||
)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ── Helpers ──────────────────────────────────────────────────────
|
||
|
||
def ensure_user(update: Update) -> dict:
|
||
"""Create or update the user record from the Telegram message."""
|
||
tg_user = update.effective_user
|
||
return db.upsert_user(
|
||
telegram_id=tg_user.id,
|
||
first_name=tg_user.first_name or "",
|
||
username=tg_user.username or "",
|
||
)
|
||
|
||
|
||
def format_summary(summary: dict) -> str:
|
||
"""Format a workout summary dict into a nice chat message."""
|
||
if not summary:
|
||
return "No workout data found."
|
||
|
||
lines = [f"*Workout Summary*"]
|
||
lines.append(f"Started: {summary['started_at']}")
|
||
if summary.get("finished_at"):
|
||
lines.append(f"Finished: {summary['finished_at']}")
|
||
lines.append("")
|
||
|
||
for exercise_name, sets in summary["exercises"].items():
|
||
lines.append(f"*{exercise_name}*")
|
||
for i, s in enumerate(sets, 1):
|
||
lines.append(f" Set {i}: {s['reps']} reps × {s['weight']} kg")
|
||
lines.append("")
|
||
|
||
lines.append(f"Total sets: {summary['total_sets']}")
|
||
lines.append(f"Total volume: {summary['total_volume']} kg")
|
||
return "\n".join(lines)
|
||
|
||
|
||
# ── Command handlers ─────────────────────────────────────────────
|
||
|
||
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
"""Greet the user and show the Mini App button."""
|
||
ensure_user(update)
|
||
webapp_btn = InlineKeyboardButton(
|
||
text="Open Workout Tracker",
|
||
web_app=WebAppInfo(url=WEBAPP_URL),
|
||
)
|
||
keyboard = InlineKeyboardMarkup([[webapp_btn]])
|
||
|
||
await update.message.reply_text(
|
||
"Hey! I'm your fitness tracker bot.\n\n"
|
||
"Tap the button below to open the workout logger, "
|
||
"or use these commands:\n"
|
||
"/workout — start a new workout via chat\n"
|
||
"/history — see your recent workouts\n"
|
||
"/help — list all commands",
|
||
reply_markup=keyboard,
|
||
)
|
||
|
||
|
||
async def cmd_help(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text(
|
||
"*Commands*\n"
|
||
"/start — show the Mini App button\n"
|
||
"/workout — quick-start a new workout\n"
|
||
"/finish — finish the current workout\n"
|
||
"/history — recent workout summaries\n"
|
||
"/help — this message",
|
||
parse_mode="Markdown",
|
||
)
|
||
|
||
|
||
async def cmd_workout(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
"""Start a new workout from chat."""
|
||
user = ensure_user(update)
|
||
active = db.get_active_workout(user["telegram_id"])
|
||
if active:
|
||
await update.message.reply_text(
|
||
"You already have an active workout! "
|
||
"Use /finish to end it, or open the Mini App to keep logging."
|
||
)
|
||
return
|
||
|
||
workout = db.start_workout(user["telegram_id"])
|
||
webapp_btn = InlineKeyboardButton(
|
||
text="Log Sets",
|
||
web_app=WebAppInfo(url=WEBAPP_URL),
|
||
)
|
||
keyboard = InlineKeyboardMarkup([[webapp_btn]])
|
||
await update.message.reply_text(
|
||
f"Workout #{workout['id']} started! Open the app to log your sets.",
|
||
reply_markup=keyboard,
|
||
)
|
||
|
||
|
||
async def cmd_finish(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
"""Finish the active workout and send a summary."""
|
||
user = ensure_user(update)
|
||
active = db.get_active_workout(user["telegram_id"])
|
||
if not active:
|
||
await update.message.reply_text("No active workout to finish.")
|
||
return
|
||
|
||
db.finish_workout(active["id"], user["telegram_id"])
|
||
summary = db.get_workout_summary(active["id"])
|
||
text = format_summary(summary)
|
||
await update.message.reply_text(text, parse_mode="Markdown")
|
||
|
||
|
||
async def cmd_history(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
"""Show recent workout summaries."""
|
||
user = ensure_user(update)
|
||
workouts = db.get_recent_workouts(user["telegram_id"], limit=5)
|
||
if not workouts:
|
||
await update.message.reply_text("No workouts yet! Tap /workout to start one.")
|
||
return
|
||
|
||
for w in workouts:
|
||
summary = db.get_workout_summary(w["id"])
|
||
text = format_summary(summary)
|
||
await update.message.reply_text(text, parse_mode="Markdown")
|
||
|
||
|
||
# ── Web App data handler ────────────────────────────────────────
|
||
|
||
async def handle_web_app_data(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||
"""Handle data sent from the Mini App via Telegram.WebApp.sendData()."""
|
||
data = update.effective_message.web_app_data.data
|
||
logger.info("Received web app data: %s", data)
|
||
await update.message.reply_text("Got it! Your workout has been saved.")
|
||
|
||
|
||
# ── Post-init: set the menu button ──────────────────────────────
|
||
|
||
async def post_init(app: Application):
|
||
"""Set the bot's menu button to open the Mini App."""
|
||
await app.bot.set_chat_menu_button(
|
||
menu_button=MenuButtonWebApp(
|
||
text="Workout",
|
||
web_app=WebAppInfo(url=WEBAPP_URL),
|
||
)
|
||
)
|
||
logger.info("Menu button set to Mini App at %s", WEBAPP_URL)
|
||
|
||
|
||
# ── Main ─────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
db.init_db()
|
||
|
||
app = (
|
||
Application.builder()
|
||
.token(BOT_TOKEN)
|
||
.post_init(post_init)
|
||
.build()
|
||
)
|
||
|
||
app.add_handler(CommandHandler("start", cmd_start))
|
||
app.add_handler(CommandHandler("help", cmd_help))
|
||
app.add_handler(CommandHandler("workout", cmd_workout))
|
||
app.add_handler(CommandHandler("finish", cmd_finish))
|
||
app.add_handler(CommandHandler("history", cmd_history))
|
||
app.add_handler(
|
||
MessageHandler(filters.StatusUpdate.WEB_APP_DATA, handle_web_app_data)
|
||
)
|
||
|
||
logger.info("Bot starting...")
|
||
app.run_polling(drop_pending_updates=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|