commit d90d3d117744b723810a25b67b35dcf426379a25 Author: Nikolay Tatarinov Date: Tue Feb 17 12:16:47 2026 +0300 Add initial project structure for Telegram bot - Created Docker configuration files for development and production. - Added Dockerfile for building the bot image. - Implemented configuration loading from environment variables. - Developed main application logic and command handlers. - Included README with setup instructions and usage details. - Added .gitignore and .dockerignore files to exclude unnecessary files. - Provided example environment file (.env.example) for bot token configuration. - Established basic error handling for the bot. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..266ef28 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +venv/ +.env +.git/ +__pycache__/ +*.pyc +*.pyo +*.md +.cursor/ +*.plan.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..69c2963 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +BOT_TOKEN=your_bot_token_here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b746f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +__pycache__/ +venv/ +*.pyc +*.pyo diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cc3d7f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# Single image for both dev and prod; Compose files differentiate behavior. +FROM python:3.12-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Application code +COPY config.py main.py ./ +COPY handlers/ ./handlers/ + +# Run as non-root +RUN adduser --disabled-password --gecos "" botuser && chown -R botuser:botuser /app +USER botuser + +CMD ["python", "main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..13a6075 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Duty Teller (Telegram Bot) + +A minimal Telegram bot boilerplate using [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) v22 with the `Application` API. + +## Get a bot token + +1. Open Telegram and search for [@BotFather](https://t.me/BotFather). +2. Send `/newbot` and follow the prompts to create a bot. +3. Copy the token BotFather gives you. + +## Setup + +1. **Clone and enter the project** + ```bash + cd duty-teller + ``` + +2. **Create a virtual environment (recommended)** + ```bash + python -m venv venv + source venv/bin/activate # Linux/macOS + # or: venv\Scripts\activate # Windows + ``` + +3. **Install dependencies** + ```bash + pip install -r requirements.txt + ``` + +4. **Configure the bot** + ```bash + cp .env.example .env + ``` + Edit `.env` and set `BOT_TOKEN` to the token from BotFather. + +## Run + +```bash +python main.py +``` + +The bot runs in polling mode. Send `/start` or `/help` to your bot in Telegram to test. + +## Run with Docker + +Ensure `.env` exists (e.g. `cp .env.example .env`) and contains `BOT_TOKEN`. + +- **Dev** (volume mount; code changes apply without rebuild): + ```bash + docker compose -f docker-compose.dev.yml up --build + ``` + Stop with `Ctrl+C` or `docker compose -f docker-compose.dev.yml down`. + +- **Prod** (no volume; runs the built image; restarts on failure): + ```bash + docker compose -f docker-compose.prod.yml up -d --build + ``` + For production deployments you may use Docker secrets or your orchestrator’s env instead of a `.env` file. + +## Project layout + +- `main.py` – Builds the `Application`, registers handlers, runs polling. +- `config.py` – Loads `BOT_TOKEN` from env; exits if missing. +- `handlers/` – Command and error handlers; add new handlers here. +- `requirements.txt` – Pinned dependencies (PTB with job-queue, python-dotenv). + +To add commands, define async handlers in `handlers/commands.py` (or a new module) and register them in `handlers/__init__.py`. diff --git a/config.py b/config.py new file mode 100644 index 0000000..f201ee5 --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +"""Load configuration from environment. Fail fast if BOT_TOKEN is missing.""" +import os + +from dotenv import load_dotenv + +load_dotenv() + +BOT_TOKEN = os.getenv("BOT_TOKEN") +if not BOT_TOKEN: + raise SystemExit("BOT_TOKEN is not set. Copy .env.example to .env and set your token from @BotFather.") diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..4f76a47 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,9 @@ +services: + bot: + build: + context: . + dockerfile: Dockerfile + env_file: .env + volumes: + - .:/app + restart: "no" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c49938b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,12 @@ +services: + bot: + build: + context: . + dockerfile: Dockerfile + env_file: .env + restart: always + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..4c2494a --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1,10 @@ +"""Expose a single register_handlers(app) that registers all handlers.""" +from telegram.ext import Application + +from . import commands, errors + + +def register_handlers(app: Application) -> None: + app.add_handler(commands.start_handler) + app.add_handler(commands.help_handler) + app.add_error_handler(errors.error_handler) diff --git a/handlers/commands.py b/handlers/commands.py new file mode 100644 index 0000000..ad30c0f --- /dev/null +++ b/handlers/commands.py @@ -0,0 +1,21 @@ +"""Command handlers: /start, /help (and placeholder for more).""" +from telegram import Update +from telegram.ext import CommandHandler, ContextTypes + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if update.message: + await update.message.reply_text("Hello! I'm your bot. Use /help to see available commands.") + + +async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if update.message: + await update.message.reply_text( + "Available commands:\n" + "/start - Start the bot\n" + "/help - Show this help" + ) + + +start_handler = CommandHandler("start", start) +help_handler = CommandHandler("help", help_cmd) diff --git a/handlers/errors.py b/handlers/errors.py new file mode 100644 index 0000000..12b69fe --- /dev/null +++ b/handlers/errors.py @@ -0,0 +1,13 @@ +"""Global error handler: log exception and notify user.""" +import logging + +from telegram import Update +from telegram.ext import ContextTypes + +logger = logging.getLogger(__name__) + + +async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: + logger.exception("Exception while handling an update") + if isinstance(update, Update) and update.effective_message: + await update.effective_message.reply_text("Something went wrong. Please try again later.") diff --git a/main.py b/main.py new file mode 100644 index 0000000..eba5bd0 --- /dev/null +++ b/main.py @@ -0,0 +1,24 @@ +"""Single entry point: build Application, register handlers, run polling.""" +import logging + +import config +from telegram.ext import ApplicationBuilder + +from handlers import register_handlers + +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + + +def main() -> None: + app = ApplicationBuilder().token(config.BOT_TOKEN).build() + register_handlers(app) + logger.info("Bot starting (polling)...") + app.run_polling(allowed_updates=["message"]) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f603749 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-telegram-bot[job-queue]>=22.0,<23.0 +python-dotenv>=1.0,<2.0