Files
swiftgram/bot.py

635 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
GoTelegram MTProxy — Telegram-бот для управления MTProxy на сервере.
Кнопочное меню, проверка портов, совместимость с Amnezia/3x-ui,
кнопка «Поделиться ключом», TCP+UDP для звонков.
"""
import asyncio
import html
import json
import os
import re
from pathlib import Path
_env_path = Path(__file__).resolve().parent / ".env"
if not _env_path.exists():
_env_path = Path("/etc/gotelegram-bot/.env")
if _env_path.exists():
with open(_env_path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
ContextTypes,
MessageHandler,
filters,
)
# ── Конфиг ────────────────────────────────────────────────────────────────────
BOT_TOKEN = os.environ.get("BOT_TOKEN")
_allowed = os.environ.get("ALLOWED_IDS", "").strip()
try:
ALLOWED_IDS = set(int(x) for x in _allowed.split(",") if x.strip()) if _allowed else None
except ValueError:
ALLOWED_IDS = None
CONTAINER_NAME = "mtproto-proxy"
CONFIG_FILE = Path("/opt/gotelegram-bot/proxy.json")
DOMAINS = [
"google.com", "wikipedia.org", "habr.com", "github.com",
"coursera.org", "udemy.com", "medium.com", "stackoverflow.com",
"bbc.com", "cnn.com", "reuters.com", "nytimes.com",
"lenta.ru", "rbc.ru", "ria.ru", "kommersant.ru",
"stepik.org", "duolingo.com", "khanacademy.org", "ted.com",
]
PROMO_LINK = "https://vk.cc/ct29NQ"
TIP_LINK = "https://pay.cloudtips.ru/p/7410814f"
# ── Утилиты ──────────────────────────────────────────────────────────────────
def _ok(uid: int) -> bool:
return ALLOWED_IDS is None or uid in ALLOWED_IDS
def _decode(data: bytes) -> str:
return (data or b"").decode("utf-8", errors="replace").strip()
async def sh(*args: str, timeout: int = 60) -> tuple[int, str, str]:
proc = await asyncio.create_subprocess_exec(
*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
)
try:
out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
await proc.wait()
return -1, "", "Timeout"
return proc.returncode or 0, _decode(out), _decode(err)
async def get_ip() -> str:
for url in ("https://api.ipify.org", "https://icanhazip.com", "https://ifconfig.me"):
code, out, _ = await sh("curl", "-s", "-4", "--max-time", "5", url, timeout=8)
if code == 0 and out:
m = re.search(r"(\d{1,3}\.){3}\d{1,3}", out)
if m:
return m.group(0)
return "0.0.0.0"
async def proxy_running() -> bool:
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}", timeout=10)
return code == 0 and CONTAINER_NAME in out
async def docker_val(fmt: str) -> str:
code, out, _ = await sh("docker", "inspect", CONTAINER_NAME, "--format", fmt, timeout=10)
return out.strip() if code == 0 else ""
async def check_port(port: int) -> str | None:
"""Если порт занят — возвращает описание процесса; иначе None."""
# Пропускаем, если порт занят нашим же контейнером
if await proxy_running():
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
if str(port) in hp.split():
return None
code, out, _ = await sh("ss", "-tlnp", timeout=5)
if code != 0:
code, out, _ = await sh("netstat", "-tlnp", timeout=5)
for line in out.splitlines():
if f":{port} " in line or f":{port}\t" in line:
return line
return None
async def docker_containers_info() -> str:
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Ports}}", timeout=10)
if code != 0 or not out:
return ""
return out
def save_config(data: dict) -> None:
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def load_config() -> dict:
if CONFIG_FILE.exists():
try:
return json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
except Exception:
pass
return {}
# ── Получение данных прокси ──────────────────────────────────────────────────
async def proxy_info() -> dict | None:
if not await proxy_running():
return None
cmd_str = await docker_val("{{range .Config.Cmd}}{{.}} {{end}}")
secret = cmd_str.split()[-1] if cmd_str else ""
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}}{{end}}")
port = hp or "443"
ip = await get_ip()
link = f"tg://proxy?server={ip}&port={port}&secret={secret}"
cfg = load_config()
return {"ip": ip, "port": port, "secret": secret, "link": link, "domain": cfg.get("domain", "")}
# ── Главное меню (кнопки) ────────────────────────────────────────────────────
def main_menu_kb() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup([
[InlineKeyboardButton("🔧 Установить / Обновить", callback_data="menu_install")],
[InlineKeyboardButton("📊 Статус", callback_data="menu_status"),
InlineKeyboardButton("🔗 Ссылка", callback_data="menu_link")],
[InlineKeyboardButton("📤 Поделиться ключом", callback_data="menu_share")],
[InlineKeyboardButton("🔄 Перезапуск", callback_data="menu_restart"),
InlineKeyboardButton("📋 Логи", callback_data="menu_logs")],
[InlineKeyboardButton("🗑 Удалить", callback_data="menu_remove"),
InlineKeyboardButton("🏷 Промо", callback_data="menu_promo")],
])
HELP_TEXT = (
"🚀 <b>GoTelegram MTProxy Bot</b>\n\n"
"Управление MTProxy (Fake TLS) на сервере.\n"
"TCP + UDP (звонки) поддержаны.\n\n"
"Используйте кнопки ниже или команды:\n"
"/install /status /link /share /restart /logs /remove /promo"
)
async def start(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.effective_user:
return
if not _ok(update.effective_user.id):
msg = update.message or (update.callback_query and update.callback_query.message)
if msg:
await msg.reply_text("⛔ Доступ запрещён.")
return
if update.message:
await update.message.reply_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
elif update.callback_query:
await update.callback_query.edit_message_text(HELP_TEXT, parse_mode="HTML", reply_markup=main_menu_kb())
# ── Статус ───────────────────────────────────────────────────────────────────
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
await msg.reply_text("")
return
info = await proxy_info()
if not info:
text = "❌ Прокси не запущен.\nНажмите <b>Установить</b> для настройки."
else:
containers = await docker_containers_info()
other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l)
text = (
"✅ <b>Прокси работает</b>\n\n"
f"IP: <code>{html.escape(info['ip'])}</code>\n"
f"Порт: <code>{html.escape(info['port'])}</code>\n"
f"Домен: <code>{html.escape(info['domain'])}</code>\n"
f"Secret: <code>{html.escape(info['secret'])}</code>\n\n"
f"Ссылка:\n<code>{html.escape(info['link'])}</code>"
)
if other:
text += f"\n\n📦 <b>Другие контейнеры:</b>\n<pre>{html.escape(other)}</pre>"
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
else:
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
# ── Ссылка ───────────────────────────────────────────────────────────────────
async def cmd_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
info = await proxy_info()
if not info:
text = "❌ Прокси не запущен."
else:
text = f"<code>{html.escape(info['link'])}</code>"
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
else:
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
# ── Поделиться ключом ────────────────────────────────────────────────────────
async def cmd_share(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
info = await proxy_info()
if not info:
text = "❌ Прокси не запущен."
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text(text, reply_markup=kb)
else:
await msg.reply_text(text, reply_markup=kb)
return
# tg://proxy ссылка, которую Telegram распознает при пересылке
tg_link = info["link"]
# Красивое сообщение для пересылки
share_text = (
f"🔐 <b>MTProxy для Telegram</b>\n\n"
f"🌍 Сервер: <code>{html.escape(info['ip'])}</code>\n"
f"🔌 Порт: <code>{html.escape(info['port'])}</code>\n"
f"🔑 Secret: <code>{html.escape(info['secret'])}</code>\n\n"
f"👉 <b>Подключиться одним нажатием:</b>\n"
f"{html.escape(tg_link)}\n\n"
f"Просто нажмите на ссылку или перешлите это сообщение."
)
kb = InlineKeyboardMarkup([
[InlineKeyboardButton("📤 Переслать другу", switch_inline_query=tg_link)],
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
])
if update.callback_query:
await update.callback_query.edit_message_text(share_text, parse_mode="HTML", reply_markup=kb)
else:
await msg.reply_text(share_text, parse_mode="HTML", reply_markup=kb)
# ── Удалить ──────────────────────────────────────────────────────────────────
async def cmd_remove(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
chat = msg.chat
if update.callback_query:
await update.callback_query.edit_message_text("⏳ Удаляю прокси...")
else:
await chat.send_message("⏳ Удаляю прокси...")
await sh("docker", "stop", CONTAINER_NAME, timeout=15)
await sh("docker", "rm", CONTAINER_NAME, timeout=10)
text = "✅ Прокси удалён." if not await proxy_running() else "⚠️ Не удалось удалить."
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
await chat.send_message(text, reply_markup=kb)
# ── Рестарт ──────────────────────────────────────────────────────────────────
async def cmd_restart(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
if not await proxy_running():
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text("❌ Прокси не запущен.", reply_markup=kb)
else:
await msg.reply_text("❌ Прокси не запущен.", reply_markup=kb)
return
chat = msg.chat
if update.callback_query:
await update.callback_query.edit_message_text("⏳ Перезапуск...")
code, _, err = await sh("docker", "restart", CONTAINER_NAME, timeout=30)
text = "✅ Перезапущен." if code == 0 else f"❌ Ошибка: {err or 'unknown'}"
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
await chat.send_message(text, reply_markup=kb)
# ── Логи ─────────────────────────────────────────────────────────────────────
async def cmd_logs(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
if not await proxy_running():
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text("❌ Прокси не запущен.", reply_markup=kb)
else:
await msg.reply_text("❌ Прокси не запущен.", reply_markup=kb)
return
code, out, err = await sh("docker", "logs", "--tail", "40", CONTAINER_NAME, timeout=15)
text = (out or "") + (("\n" + err) if err else "") or "Нет вывода."
if len(text) > 4000:
text = text[-4000:]
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text(f"<pre>{html.escape(text)}</pre>", parse_mode="HTML", reply_markup=kb)
else:
await msg.reply_text(f"<pre>{html.escape(text)}</pre>", parse_mode="HTML", reply_markup=kb)
# ── Промо ────────────────────────────────────────────────────────────────────
async def cmd_promo(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
text = (
"💰 <b>Хостинг со скидкой до -60%</b>\n"
f"Ссылка: {PROMO_LINK}\n\n"
"Промокоды: OFF60, antenka20, antenka6, antenka12\n\n"
f"Донат: {TIP_LINK}"
)
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query:
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
else:
await msg.reply_text(text, parse_mode="HTML", reply_markup=kb)
# ── Установка: домен → порт → проверка → запуск ─────────────────────────────
async def install_step_domain(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
msg = update.message or (update.callback_query and update.callback_query.message)
if not update.effective_user or not msg:
return
if not _ok(update.effective_user.id):
return
buttons = []
row = []
for i, d in enumerate(DOMAINS):
row.append(InlineKeyboardButton(d, callback_data=f"dom_{i}"))
if len(row) == 2:
buttons.append(row)
row = []
if row:
buttons.append(row)
text = "🌐 <b>Выберите домен для маскировки (Fake TLS):</b>"
if update.callback_query:
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons))
else:
await msg.reply_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(buttons))
async def install_step_port(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
domain = ctx.user_data.get("install_domain", "google.com")
# Проверяем порт 443
busy_443 = await check_port(443)
busy_8443 = await check_port(8443)
rows = []
label_443 = "443 (рекомендуется)" if not busy_443 else "443 ⚠️ занят"
label_8443 = "8443" if not busy_8443 else "8443 ⚠️ занят"
rows.append([
InlineKeyboardButton(label_443, callback_data="port_443"),
InlineKeyboardButton(label_8443, callback_data="port_8443"),
])
rows.append([InlineKeyboardButton("◀️ Меню", callback_data="menu_main")])
port_info = ""
if busy_443:
port_info += f"\n⚠️ Порт 443 занят:\n<pre>{html.escape(busy_443[:300])}</pre>\n"
if busy_8443:
port_info += f"\n⚠️ Порт 8443 занят:\n<pre>{html.escape(busy_8443[:300])}</pre>\n"
text = (
f"Домен: <b>{html.escape(domain)}</b>\n\n"
"🔌 <b>Выберите порт</b> или введите свой (1-65535):"
f"{port_info}"
)
ctx.user_data["install_wait_port"] = True
await query.edit_message_text(text, parse_mode="HTML", reply_markup=InlineKeyboardMarkup(rows))
async def install_port_chosen(update: Update, ctx: ContextTypes.DEFAULT_TYPE, port_str: str) -> None:
"""Порт выбран кнопкой или текстом — проверяем и ставим."""
port = int(port_str)
msg = None
chat = None
if update.callback_query:
msg = update.callback_query.message
elif update.message:
msg = update.message
if not msg:
return
chat = msg.chat
# Проверка занятости
busy = await check_port(port)
if busy:
kb = InlineKeyboardMarkup([
[InlineKeyboardButton(f"Всё равно использовать {port}", callback_data=f"force_{port}")],
[InlineKeyboardButton("Выбрать другой порт", callback_data="reselect_port")],
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
])
text = (
f"⚠️ <b>Порт {port} занят!</b>\n\n"
f"<pre>{html.escape(busy[:500])}</pre>\n\n"
"Можно использовать всё равно (если это ваш процесс) или выбрать другой."
)
if update.callback_query:
await update.callback_query.edit_message_text(text, parse_mode="HTML", reply_markup=kb)
else:
await chat.send_message(text, parse_mode="HTML", reply_markup=kb)
ctx.user_data["install_port"] = port_str
return
ctx.user_data["install_port"] = port_str
ctx.user_data["install_wait_port"] = False
await do_install(update, ctx)
async def do_install(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
domain = ctx.user_data.get("install_domain") or "google.com"
port = ctx.user_data.get("install_port") or "443"
msg = None
if update.callback_query:
msg = update.callback_query.message
await msg.edit_text("⏳ Генерация secret и запуск контейнера...", reply_markup=None)
elif update.message:
msg = update.message
await msg.reply_text("⏳ Генерация secret и запуск контейнера...")
if not msg:
return
chat = msg.chat
# Docker check
code, _, _ = await sh("docker", "info", timeout=10)
if code != 0:
await chat.send_message(
"❌ Docker не установлен или не запущен.\n"
"Установите: <code>curl -fsSL https://get.docker.com | sh</code>",
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]]),
)
return
# generate secret
code, secret_out, err = await sh(
"docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain,
timeout=60,
)
if code != 0:
await chat.send_message(f"❌ Генерация secret: {err or secret_out}")
return
secret = secret_out.strip().split()[-1] if secret_out.strip() else ""
if not secret:
await chat.send_message("❌ Пустой secret.")
return
# Остановка старого
await sh("docker", "stop", CONTAINER_NAME, timeout=15)
await sh("docker", "rm", CONTAINER_NAME, timeout=10)
# Запуск с TCP + UDP (UDP нужен для звонков Telegram)
# --network host не используем, чтобы не мешать Amnezia/3x-ui
code, _, err = await sh(
"docker", "run", "-d",
"--name", CONTAINER_NAME,
"--restart", "always",
"-p", f"{port}:{port}/tcp",
"-p", f"{port}:{port}/udp",
"nineseconds/mtg:2",
"simple-run",
"-n", "1.1.1.1",
"-t", "1.0.0.1", # tag DNS для TLS
"-i", "prefer-ipv4",
f"0.0.0.0:{port}", secret,
timeout=90,
)
if code != 0:
await chat.send_message(f"❌ Запуск контейнера: {err}")
return
save_config({"domain": domain, "port": port, "secret": secret})
ip = await get_ip()
link = f"tg://proxy?server={ip}&port={port}&secret={secret}"
text = (
"✅ <b>Прокси установлен!</b>\n\n"
f"🌍 IP: <code>{html.escape(ip)}</code>\n"
f"🔌 Порт: <code>{html.escape(port)}</code> (TCP + UDP)\n"
f"🎭 Домен: <code>{html.escape(domain)}</code>\n"
f"🔑 Secret: <code>{html.escape(secret)}</code>\n\n"
f"👉 Ссылка:\n<code>{html.escape(link)}</code>\n\n"
"📞 Звонки в Telegram поддержаны (UDP)."
)
kb = InlineKeyboardMarkup([
[InlineKeyboardButton("📤 Поделиться ключом", callback_data="menu_share")],
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")],
])
await chat.send_message(text, parse_mode="HTML", reply_markup=kb)
for k in ("install_domain", "install_port", "install_wait_port"):
ctx.user_data.pop(k, None)
# ── Callback router ──────────────────────────────────────────────────────────
async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
if not query or not update.effective_user:
return
await query.answer()
if not _ok(update.effective_user.id):
await query.edit_message_text("⛔ Доступ запрещён.")
return
data = query.data or ""
if data == "menu_main":
await start(update, ctx)
elif data == "menu_install":
await install_step_domain(update, ctx)
elif data == "menu_status":
await cmd_status(update, ctx)
elif data == "menu_link":
await cmd_link(update, ctx)
elif data == "menu_share":
await cmd_share(update, ctx)
elif data == "menu_restart":
await cmd_restart(update, ctx)
elif data == "menu_logs":
await cmd_logs(update, ctx)
elif data == "menu_remove":
await cmd_remove(update, ctx)
elif data == "menu_promo":
await cmd_promo(update, ctx)
elif data.startswith("dom_"):
try:
idx = int(data[4:])
except ValueError:
await query.edit_message_text("❌ Ошибка. /install")
return
if not (0 <= idx < len(DOMAINS)):
await query.edit_message_text("❌ Неверный выбор. /install")
return
ctx.user_data["install_domain"] = DOMAINS[idx]
await install_step_port(update, ctx)
elif data == "port_443":
await install_port_chosen(update, ctx, "443")
elif data == "port_8443":
await install_port_chosen(update, ctx, "8443")
elif data.startswith("force_"):
port_str = data[6:]
ctx.user_data["install_port"] = port_str
ctx.user_data["install_wait_port"] = False
await do_install(update, ctx)
elif data == "reselect_port":
await install_step_port(update, ctx)
# ── Ввод порта текстом ──────────────────────────────────────────────────────
async def text_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.message or not ctx.user_data.get("install_wait_port"):
return
text = (update.message.text or "").strip()
if not re.match(r"^\d+$", text):
return
port = int(text)
if not (1 <= port <= 65535):
await update.message.reply_text("Введите число от 1 до 65535.")
return
await install_port_chosen(update, ctx, str(port))
# ── main ─────────────────────────────────────────────────────────────────────
def main() -> None:
if not BOT_TOKEN:
raise SystemExit("Задайте BOT_TOKEN в .env")
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", start))
app.add_handler(CommandHandler("install", install_step_domain))
app.add_handler(CommandHandler("status", cmd_status))
app.add_handler(CommandHandler("link", cmd_link))
app.add_handler(CommandHandler("share", cmd_share))
app.add_handler(CommandHandler("remove", cmd_remove))
app.add_handler(CommandHandler("restart", cmd_restart))
app.add_handler(CommandHandler("logs", cmd_logs))
app.add_handler(CommandHandler("promo", cmd_promo))
app.add_handler(CallbackQueryHandler(callback_handler))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler))
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()