Bot & Automation

vps-monitor-bot

/root/hermes-projects/vps-monitor-bot

bot.py text
#!/usr/bin/env python3
"""
VPS Monitor Telegram Bot
Monitors VPS via Telegram. Owner-only access.
Stack: python-telegram-bot v20+, python-dotenv, psutil, subprocess (Docker)
"""

import os, subprocess, urllib.request
from datetime import datetime

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

load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")
OWNER_CHAT_ID = os.getenv("OWNER_CHAT_ID")
if not BOT_TOKEN: raise ValueError("BOT_TOKEN not set in .env")
if not OWNER_CHAT_ID: raise ValueError("OWNER_CHAT_ID not set in .env")
try: OWNER_CHAT_ID = int(OWNER_CHAT_ID)
except ValueError: raise ValueError("OWNER_CHAT_ID must be integer")

# Security decorator
def owner_only(f):
    async def w(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
        if update.effective_user.id != OWNER_CHAT_ID:
            await update.message.reply_text("\U0001f6ab *Akses ditolak.*\nBot ini hanya untuk owner.", parse_mode="Markdown")
            return
        return await f(update, ctx)
    return w

# Helpers
def public_ip():
    for u in ("https://api.ipify.org?format=text","https://ifconfig.me/ip"):
        try:
            with urllib.request.urlopen(u,timeout=5) as r: return r.read().decode().strip()
        except: continue
    return "N/A"

def uptime_str():
    try:
        with open("/proc/uptime") as f: s=float(f.readline().split()[0])
    except: return "N/A"
    d=int(s//86400); h=int((s%86400)//3600); m=int((s%3600)//60)
    p=[]; 
    if d: p.append(f"{d}d")
    if h: p.append(f"{h}h")
    p.append(f"{m}m")
    return " ".join(p)

def disk_str():
    u=psutil.disk_usage("/")
    return f"{u.used/1024**3:.1f} GB / {u.total/1024**3:.1f} GB ({u.percent}%)"

def docker_cmd(args, timeout=15):
    try:
        r=subprocess.run(["docker"]+args, capture_output=True, text=True, timeout=timeout)
        return (r.returncode==0, r.stdout.strip() if r.returncode==0 else f"Docker error:\n```\n{r.stderr.strip()}\n```")
    except FileNotFoundError: return (False, "Docker tidak terinstall.")
    except subprocess.TimeoutExpired: return (False, "Docker command timed out.")
    except Exception as e: return (False, f"Error: {e}")

def docker_ps():
    return docker_cmd(["ps","--format","{{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}"])

def docker_logs(c, tail=50):
    ok,out=docker_cmd(["ps","-a","--format","{{.Names}}"])
    if not ok: return (False,out)
    names=[n.strip() for n in out.split("\n") if n.strip()]
    if c not in names:
        return (False, f"Container `{c}` tidak ditemukan.\nTersedia: {', '.join(names) if names else '(tidak ada)'}")
    return docker_cmd(["logs","--tail",str(tail),c])

def docker_restart(c):
    ok,out=docker_cmd(["restart",c],timeout=30)
    if ok: return (True, f"\u2705 Container `{c}` berhasil direstart.")
    return (ok,out)

# Commands
@owner_only
async def cmd_start(update, ctx):
    n=update.effective_user.first_name or "Owner"
    await update.message.reply_text(f"\U0001f44b *Halo, {n}!*\n\nBot monitoring VPS via Telegram.\nHanya owner yang bisa menggunakannya.\n\nKetik /help untuk daftar perintah.", parse_mode="Markdown")

@owner_only
async def cmd_help(update, ctx):
    await update.message.reply_text("\U0001f4cb *Daftar Perintah*\n\n/start \u2014 Pesan sambutan\n/help \u2014 Daftar ini\n/status \u2014 Info sistem (CPU, RAM, Disk, Uptime, IP)\n/docker \u2014 Daftar container Docker\n/logs\\_n8n \u2014 50 baris log n8n\n/restart\\_n8n \u2014 Restart n8n\n\n\U0001f4cc Semua hanya untuk owner.", parse_mode="Markdown")

@owner_only
async def cmd_status(update, ctx):
    await update.message.reply_text("\u23f3 Mengambil data...")
    h=os.uname().nodename; u=uptime_str(); cpu=psutil.cpu_percent(interval=1)
    cores=psutil.cpu_count(); ram=psutil.virtual_memory(); d=disk_str()
    load=os.getloadavg(); ip=public_ip()
    msg=(f"\U0001f5a5 *Status VPS*\n\n"
         f"\u2022 *Hostname*: `{h}`\n\u2022 *Uptime*: {u}\n"
         f"\u2022 *CPU*: {cpu:.1f}% ({cores} core)\n"
         f"\u2022 *RAM*: {ram.used/1024**3:.1f} GB / {ram.total/1024**3:.1f} GB ({ram.percent:.1f}%)\n"
         f"\u2022 *Disk*: {d}\n\u2022 *Load*: {load[0]:.2f} / {load[1]:.2f} / {load[2]:.2f}\n"
         f"\u2022 *Public IP*: `{ip}`\n\n\U0001f550 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    await update.message.reply_text(msg, parse_mode="Markdown")

@owner_only
async def cmd_docker(update, ctx):
    await update.message.reply_text("\u23f3 Mengambil data Docker...")
    ok,out=docker_ps()
    if not ok: await update.message.reply_text(f"\u274c {out}", parse_mode="Markdown"); return
    if not out: await update.message.reply_text("\U0001f4e6 Tidak ada container berjalan."); return
    lines=out.strip().split("\n")
    table="\U0001f4e6 *Docker Containers*\n\n"
    for i,line in enumerate(lines,1):
        p=line.split("\t")
        table+=(f"*{i}. {p[0] if len(p)>0 else '-'}*\n"
                f"   Status: `{p[1] if len(p)>1 else '-'}`\n"
                f"   Image: `{p[2] if len(p)>2 else '-'}`\n"
                f"   Port: `{p[3] if len(p)>3 else '-'}`\n\n")
    await update.message.reply_text(table, parse_mode="Markdown")

@owner_only
async def cmd_logs_n8n(update, ctx):
    await update.message.reply_text("\u23f3 Mengambil log n8n...")
    ok,out=docker_logs("n8n",50)
    if not ok: await update.message.reply_text(f"\u274c {out}", parse_mode="Markdown"); return
    if not out: await update.message.reply_text("\U0001f4c4 Log n8n kosong."); return
    if len(out)>3800: out=out[-3800:]; h="\U0001f4c4 *Log n8n (50 baris)* \u2014 3800 karakter terakhir\n\n"
    else: h="\U0001f4c4 *Log n8n (50 baris terakhir)*\n\n"
    await update.message.reply_text(h+f"```\n{out}\n```", parse_mode="Markdown")

@owner_only
async def cmd_restart_n8n(update, ctx):
    await update.message.reply_text("\u23f3 Merestart n8n...")
    ok,msg=docker_restart("n8n")
    await update.message.reply_text(msg, parse_mode="Markdown")

def main():
    print("Starting VPS Monitor Bot...")
    app=Application.builder().token(BOT_TOKEN).build()
    for c,h in [("start",cmd_start),("help",cmd_help),("status",cmd_status),
                 ("docker",cmd_docker),("logs_n8n",cmd_logs_n8n),("restart_n8n",cmd_restart_n8n)]:
        app.add_handler(CommandHandler(c,h))
    print("Bot running. Ctrl+C to stop.")
    app.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__=="__main__": main()