Build a Telegram Automation Bot with Python: A Practical Starter Project

Automation

A Telegram bot can be more than a chat interface. With a small Python service, it becomes a secure remote control for repeatable workflows: checking system health, launching an approved job, collecting a report, or notifying a team when a process finishes.

In this tutorial, we will build a practical automation bot with Python. The bot accepts a small set of commands, restricts access to approved Telegram users, runs only predefined tasks, and returns clear results. The design is intentionally simple enough for a first deployment but structured well enough to extend later.

What we are building

The bot will support three commands:

  • /start — show available commands;
  • /status — return basic host status;
  • /run backup — launch an approved automation task.

The important security rule is that the bot never executes arbitrary text as a shell command. Users select a task from a fixed allowlist. This prevents a convenient automation tool from becoming an accidental remote shell.

Project structure

telegram-automation-bot/
├── bot.py
├── requirements.txt
└── .env

1. Create the Telegram bot

Open Telegram, start a chat with @BotFather, run /newbot, and follow the prompts. BotFather will provide a token. Treat it like a password: do not commit it to Git, paste it into screenshots, or hard-code it in the Python file.

You also need your numeric Telegram user ID for the access allowlist. You can obtain it from a trusted ID bot or temporarily log update.effective_user.id during local testing.

2. Install the dependencies

python -m venv .venv
source .venv/bin/activate
pip install python-telegram-bot python-dotenv psutil

On Windows PowerShell, activate the environment with:

.venv\Scripts\Activate.ps1

Create requirements.txt:

python-telegram-bot>=21,<23
python-dotenv>=1,<2
psutil>=6,<8

3. Configure environment variables

Create a local .env file:

TELEGRAM_BOT_TOKEN=replace_with_your_token
ALLOWED_USER_IDS=123456789

For several administrators, separate IDs with commas:

ALLOWED_USER_IDS=123456789,987654321

Add .env and .venv/ to .gitignore.

4. Add the Python code

import asyncio
import logging
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path

import psutil
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
)

load_dotenv()

BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
ALLOWED_USER_IDS = {
    int(value.strip())
    for value in os.getenv("ALLOWED_USER_IDS", "").split(",")
    if value.strip()
}

logging.basicConfig(
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    level=logging.INFO,
)
logger = logging.getLogger(__name__)


def is_allowed(update: Update) -> bool:
    user = update.effective_user
    return bool(user and user.id in ALLOWED_USER_IDS)


async def reject_unauthorized(update: Update) -> None:
    user = update.effective_user
    logger.warning(
        "Rejected Telegram user id=%s username=%s",
        getattr(user, "id", None),
        getattr(user, "username", None),
    )
    if update.effective_message:
        await update.effective_message.reply_text("Access denied.")


async def start(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if not is_allowed(update):
        await reject_unauthorized(update)
        return

    await update.effective_message.reply_text(
        "Automation bot is online.

"
        "Commands:
"
        "/status — host health
"
        "/run backup — run the approved backup task"
    )


async def status(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if not is_allowed(update):
        await reject_unauthorized(update)
        return

    disk = shutil.disk_usage("/")
    memory = psutil.virtual_memory()
    boot_time = datetime.fromtimestamp(
        psutil.boot_time(),
        tz=timezone.utc,
    )
    uptime = datetime.now(timezone.utc) - boot_time

    message = (
        "Host status
"
        f"CPU: {psutil.cpu_percent(interval=0.3):.1f}%
"
        f"Memory: {memory.percent:.1f}%
"
        f"Disk: {disk.used / disk.total * 100:.1f}%
"
        f"Uptime: {str(uptime).split('.')[0]}"
    )
    await update.effective_message.reply_text(message)


async def backup_task() -> str:
    source = Path.home() / "automation-data"
    destination = Path.home() / "backups"
    destination.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    archive_base = destination / f"automation-data-{timestamp}"

    archive_path = await asyncio.to_thread(
        shutil.make_archive,
        str(archive_base),
        "zip",
        root_dir=source,
    )
    return f"Backup created: {archive_path}"


TASKS = {
    "backup": backup_task,
}


async def run_task(
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    if not is_allowed(update):
        await reject_unauthorized(update)
        return

    task_name = context.args[0].lower() if context.args else ""
    task = TASKS.get(task_name)

    if task is None:
        available = ", ".join(sorted(TASKS))
        await update.effective_message.reply_text(
            f"Unknown task. Available: {available}"
        )
        return

    await update.effective_message.reply_text(
        f"Starting task: {task_name}"
    )

    try:
        result = await task()
    except Exception:
        logger.exception("Task failed: %s", task_name)
        await update.effective_message.reply_text(
            f"Task failed: {task_name}. Check the service logs."
        )
        return

    await update.effective_message.reply_text(result)


def main() -> None:
    if not ALLOWED_USER_IDS:
        raise RuntimeError("ALLOWED_USER_IDS is empty")

    application = Application.builder().token(BOT_TOKEN).build()
    application.add_handler(CommandHandler("start", start))
    application.add_handler(CommandHandler("status", status))
    application.add_handler(CommandHandler("run", run_task))

    application.run_polling(drop_pending_updates=True)


if __name__ == "__main__":
    main()

5. Prepare test data and run the bot

mkdir -p ~/automation-data
echo "automation test" > ~/automation-data/example.txt
python bot.py

Open the bot in Telegram and send /start, then /status. Finally, run /run backup. A timestamped ZIP archive should appear in ~/backups.

Why the task allowlist matters

A common but dangerous pattern is to pass a Telegram message directly to subprocess.run(..., shell=True). Do not do that. Even with a private bot, leaked tokens, forwarded messages, configuration errors, or an accidentally broad access rule can expose the entire host.

The TASKS dictionary creates a narrow interface: the bot can call only functions that you explicitly register. Each function can validate inputs, use normal Python APIs, write structured logs, and return a controlled result.

Run it as a systemd service

For a small Linux server, create /etc/systemd/system/telegram-automation.service:

[Unit]
Description=Telegram Automation Bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=automation
WorkingDirectory=/opt/telegram-automation-bot
EnvironmentFile=/opt/telegram-automation-bot/.env
ExecStart=/opt/telegram-automation-bot/.venv/bin/python bot.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Then enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now telegram-automation
sudo systemctl status telegram-automation

Production checklist

  • Use a dedicated user — grant only the permissions the bot needs.
  • Protect the bot token — keep it in a restricted environment file.
  • Restrict access — allow only approved Telegram user IDs.
  • Block arbitrary commands — run only predefined automation tasks.
  • Handle failures — add timeouts, logs, and error notifications.
  • Limit backup access — expose only required directories.
  • Rotate leaked tokens — replace any token found in logs or Git.

Where to take it next

This starter can become a useful private operations console. Add approved tasks for generating reports, checking Docker containers, calling an n8n webhook, monitoring a local AI server, or moving files through a validated workflow. Keep the chat layer thin and place the real business logic in small, testable Python functions.

The result is a practical automation pattern: Telegram provides the interface, Python provides the control layer, and the allowlist keeps the system predictable.

Rate article
Add a comment