Building a Telegram Bot with Python and python-telegram-bot: Receive Notifications and Control Your Server Remotely

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Running a VPS and want to get notified the moment your disk fills up, or need to restart a service without SSH-ing into the server? A Telegram Bot is exactly what I’ve been using to solve that problem — lightweight, fast, and no complex web UI needed.

I use Python as my automation tool for most daily tasks, from deploy scripts to monitoring alerts. The Telegram Bot has become my “control panel” — type a command from my phone and the server responds instantly, no laptop needed.

How Does a Telegram Bot Work?

Every Telegram Bot is a special account controlled via an HTTP API. When a user sends a message to your bot, Telegram stores it on their servers. Your bot retrieves messages in one of two ways:

  • Long polling: The bot continuously asks the API “any new messages?” — simple, no domain required, great for testing and small VPS setups.
  • Webhook: Telegram actively pushes messages to a URL you register — requires HTTPS and a real domain, ideal for high-traffic production environments.

The python-telegram-bot library wraps the entire HTTP API, letting you work in pure Python instead of making raw requests calls. Note: from version 20+, the library is fully asyncio-based — the code looks quite different from v13 that many older tutorials still use.

Setup: Create a Bot and Get Your Token

Before writing any code, you need to create a bot through BotFather — Telegram’s official bot for managing bots.

  1. Open Telegram and search for @BotFather
  2. Send the /newbot command
  3. Set a display name (e.g., My Server Monitor)
  4. Set a username — must end with bot (e.g., myserver_monitor_bot)
  5. BotFather will return a token in the format 1234567890:ABCdef... — keep this private

You’ll also need your Chat ID so the bot knows who to send notifications to. The quickest way: send any message to your bot, then visit this URL in your browser:

https://api.telegram.org/bot<TOKEN>/getUpdates

Find the message.chat.id field in the returned JSON — that’s your Chat ID.

Installation and Project Structure

pip install python-telegram-bot==21.0.1

Create a clean directory structure:

server-bot/
├── bot.py          # Main logic
├── config.py       # Token and Chat ID
└── notify.py       # Send alerts from other scripts

File config.py:

TOKEN = "1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ"
ALLOWED_CHAT_ID = 987654321  # Only allow a single user

Hands-On: Building the Bot Step by Step

Step 1 — Basic Bot with /start Command

from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
import config

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "Bot is running. Available commands:\n"
        "/status — Check server uptime\n"
        "/disk — Check disk usage\n"
        "/restart <service> — Restart a service"
    )

def main():
    app = Application.builder().token(config.TOKEN).build()
    app.add_handler(CommandHandler("start", start))
    print("Bot is running...")
    app.run_polling()

if __name__ == "__main__":
    main()

Step 2 — Security: Block Unauthorized Users

The bot runs publicly — anyone who knows the username can send it messages. Add a decorator to check the Chat ID before processing any command:

from functools import wraps
import config

def restricted(func):
    @wraps(func)
    async def wrapped(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs):
        if update.effective_chat.id != config.ALLOWED_CHAT_ID:
            await update.message.reply_text("Access denied.")
            return
        return await func(update, context, *args, **kwargs)
    return wrapped

Apply this decorator to every sensitive command:

@restricted
async def disk_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    ...

Step 3 — Server Inspection Commands

import subprocess
import shutil

@restricted
async def disk_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    total, used, free = shutil.disk_usage("/")
    gb = 1024 ** 3
    msg = (
        f"Disk Usage:\n"
        f"Total: {total / gb:.1f} GB\n"
        f"Used:  {used / gb:.1f} GB ({used/total*100:.1f}%)\n"
        f"Free:  {free / gb:.1f} GB"
    )
    await update.message.reply_text(msg)

@restricted
async def system_status(update: Update, context: ContextTypes.DEFAULT_TYPE):
    uptime = subprocess.run(["uptime", "-p"], capture_output=True, text=True)
    load = subprocess.run(["cat", "/proc/loadavg"], capture_output=True, text=True)
    msg = f"Uptime: {uptime.stdout.strip()}\nLoad: {load.stdout.strip()}"
    await update.message.reply_text(msg)

Step 4 — Restart Service Command with Confirmation

Restarting a service can cause downtime — add a confirmation step via an Inline Keyboard before executing:

from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import CallbackQueryHandler

@restricted
async def restart_service(update: Update, context: ContextTypes.DEFAULT_TYPE):
    if not context.args:
        await update.message.reply_text("Usage: /restart <service_name>")
        return

    service = context.args[0]
    keyboard = [[
        InlineKeyboardButton("Confirm", callback_data=f"restart:{service}"),
        InlineKeyboardButton("Cancel", callback_data="cancel"),
    ]]
    await update.message.reply_text(
        f"Are you sure you want to restart `{service}`?",
        reply_markup=InlineKeyboardMarkup(keyboard),
        parse_mode="Markdown"
    )

async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    await query.answer()

    if query.data == "cancel":
        await query.edit_message_text("Cancelled.")
        return

    _, service = query.data.split(":", 1)
    result = subprocess.run(
        ["sudo", "systemctl", "restart", service],
        capture_output=True, text=True
    )
    if result.returncode == 0:
        await query.edit_message_text(f"Successfully restarted `{service}`.")
    else:
        await query.edit_message_text(f"Error: {result.stderr}")

Register the handlers in main():

app.add_handler(CommandHandler("disk", disk_status))
app.add_handler(CommandHandler("status", system_status))
app.add_handler(CommandHandler("restart", restart_service))
app.add_handler(CallbackQueryHandler(button_handler))

Step 5 — Sending Proactive Alerts from a Cron Job

This is the part I use most often: a monitoring script sends Telegram notifications without the bot running — just call the Telegram HTTP API directly.

# notify.py
import requests
import config

def send_alert(message: str) -> bool:
    url = f"https://api.telegram.org/bot{config.TOKEN}/sendMessage"
    payload = {
        "chat_id": config.ALLOWED_CHAT_ID,
        "text": message,
        "parse_mode": "HTML"
    }
    resp = requests.post(url, json=payload, timeout=10)
    return resp.ok

Use it in a cron job to monitor disk space — add to /etc/cron.hourly/:

#!/usr/bin/env python3
import shutil, sys
sys.path.insert(0, "/home/ubuntu/server-bot")
from notify import send_alert

_, _, free = shutil.disk_usage("/")
gb = 1024 ** 3

if free / gb < 5:
    send_alert(f"<b>Disk Warning</b>: Only {free/gb:.1f} GB free remaining!")

Running the Bot 24/7 with systemd

Create a service file so the bot automatically restarts on crash or VPS reboot:

# /etc/systemd/system/telegram-bot.service
[Unit]
Description=Telegram Server Bot
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/server-bot
ExecStart=/home/ubuntu/server-bot/venv/bin/python bot.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
sudo systemctl status telegram-bot

Conclusion

From a simple bot, you now have the foundation to build a full “remote control” for your server — receive alerts when something goes wrong, check system status, restart services, all from your phone via Telegram without opening a laptop.

I’m currently using a similar setup to monitor 3 VPS instances simultaneously. Whenever something goes wrong, Telegram alerts me immediately instead of me having to watch a dashboard. With python-telegram-bot v20+, adding inline keyboards or multi-step ConversationHandlers isn’t much more complex than what you’ve just built.

Next steps to try: integrate psutil to fetch real-time CPU and RAM metrics, or use ConversationHandler to build multi-step conversation flows when you need more complex user input.

Share: