feat: integrate GoTelegram features into SwiftGram

- Added interactive port selection (443, 8443, and manual text input).
- Enhanced /status with a list of other running Docker containers.
- Improved MTG container stability with DNS tag -t 1.0.0.1.
- Implemented state-based text handler for custom port configuration.
This commit is contained in:
kobaltgit
2026-04-06 13:00:00 +03:00
parent e1b495ca47
commit 3c6ce21d31

View File

@@ -1,6 +1,6 @@
#!/bin/bash
# 🚀 SwiftGram MTProxy — Smart Modular Manager (Self-contained)
# v1.1.0 — Фикс звонков (UDP) + Автономный режим
# v1.2.0 — UDP Calls + Port Selection + Advanced Diagnostics
# ── Цвета ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
@@ -130,22 +130,8 @@ install_base_deps() {
echo ""
}
# ── Утилиты IP и Портов ──────────────────────────────────────────────────────
# ── Утилиты IP ──────────────────────────────────────────────────────────────
get_ip4() { curl -s -4 --max-time 5 https://api.ipify.org || echo "0.0.0.0"; }
get_ip6() { curl -s -6 --max-time 5 https://api6.ipify.org || echo ""; }
find_smart_port() {
local port=443
if ss -tlnp | grep -qE ":${port}\b"; then
echo -e " ${YELLOW} Порт 443 занят (Hiddify/Nginx). Пробую 8443...${NC}" >&2
port=8443
if ss -tlnp | grep -qE ":${port}\b"; then
port=$(( (RANDOM % 10000) + 20000 ))
echo -e " ${YELLOW} Порт 8443 тоже занят. Выбран случайный: $port${NC}" >&2
fi
fi
echo "$port"
}
# ── Вшитые исходники бота ────────────────────────────────────────────────────
prepare_bot_source() {
@@ -185,6 +171,8 @@ from telegram.ext import (
CommandHandler,
CallbackQueryHandler,
ContextTypes,
MessageHandler,
filters,
)
BOT_TOKEN = os.environ.get("BOT_TOKEN")
@@ -229,12 +217,6 @@ async def get_ip4() -> str:
if m: return m.group(0)
return "0.0.0.0"
async def get_ip6() -> str:
code, out, _ = await sh("curl", "-s", "-6", "--max-time", "5", "https://api6.ipify.org", timeout=8)
if code == 0 and out:
return out.strip()
return ""
async def check_bbr() -> bool:
code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5)
return "bbr" in out.lower()
@@ -247,6 +229,10 @@ 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 docker_containers_info() -> str:
code, out, _ = await sh("docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Ports}}", timeout=10)
return out if code == 0 else ""
async def check_port(port: int) -> str | None:
if await proxy_running():
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
@@ -270,16 +256,15 @@ 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.split()[0] if hp else "443"
ip4 = await get_ip4()
ip6 = await get_ip6()
cfg = load_config()
return {
"ip4": ip4, "ip6": ip6, "port": port, "secret": secret,
"ip4": ip4, "port": port, "secret": secret,
"domain": cfg.get("domain", "—"),
"link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}",
"link6": f"tg://proxy?server={ip6}&port={port}&secret={secret}" if ip6 else None
"link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}"
}
def main_menu_kb() -> InlineKeyboardMarkup:
@@ -296,7 +281,6 @@ def main_menu_kb() -> InlineKeyboardMarkup:
HELP_TEXT = (
"🚀 <b>SwiftGram MTProxy Manager</b>\n\n"
"• TCP + UDP (звонки) активны\n"
"• IPv6 поддержка включена\n"
"• BBR оптимизация\n\n"
)
@@ -315,6 +299,8 @@ async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
text = "❌ <b>Прокси не запущен.</b>"
else:
bbr = "✅ Активен" if await check_bbr() else "❌ Выключен"
containers = await docker_containers_info()
other = "\n".join(l for l in containers.splitlines() if CONTAINER_NAME not in l)
text = (
"✅ <b>SwiftGram работает</b>\n\n"
f"🌐 IPv4: <code>{info['ip4']}</code>\n"
@@ -322,6 +308,8 @@ async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
f"🚀 BBR: <code>{bbr}</code>\n"
f"📞 Звонки: <code>✅ UDP открыт</code>\n"
)
if other:
text += f"\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 update.message.reply_text(text, parse_mode="HTML", reply_markup=kb)
@@ -330,8 +318,7 @@ async def cmd_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.effective_user or not _ok(update.effective_user.id): return
info = await proxy_info()
if not info: text = "❌ Прокси не запущен."
else:
text = f"<b>Ссылка:</b>\n<code>{info['link4']}</code>"
else: text = f"<b>Ссылка:</b>\n<code>{info['link4']}</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 update.message.reply_text(text, parse_mode="HTML", reply_markup=kb)
@@ -342,6 +329,48 @@ async def cmd_restart(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
if update.callback_query: await update.callback_query.edit_message_text("✅ Перезапущено", reply_markup=kb)
async def cmd_logs(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
if not update.effective_user or not _ok(update.effective_user.id): return
code, out, err = await sh("docker", "logs", "--tail", "30", CONTAINER_NAME, timeout=15)
text = f"<pre>{html.escape(out or err or 'Логов нет.')}</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 update.message.reply_text(text, parse_mode="HTML", reply_markup=kb)
async def do_install(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
domain = ctx.user_data.get("install_domain", "google.com")
port = ctx.user_data.get("install_port", "443")
msg = update.callback_query.message if update.callback_query else update.message
await msg.reply_text(f"⏳ Установка на порт {port}...")
_, s_out, _ = await sh("docker", "run", "--rm", "nineseconds/mtg:2", "generate-secret", "--hex", domain)
secret = s_out.strip().split()[-1] if s_out else ""
await sh("docker", "stop", CONTAINER_NAME)
await sh("docker", "rm", CONTAINER_NAME)
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", "-i", "prefer-ipv4",
f"0.0.0.0:{port}", secret
)
if code == 0:
save_config({"domain": domain, "port": port, "secret": secret})
await msg.reply_text(f"✅ Установлено!")
await cmd_status(update, ctx)
else:
await msg.reply_text(f"❌ Ошибка: {err}")
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 text.isdigit() and (1 <= int(text) <= 65535):
ctx.user_data["install_port"] = text
ctx.user_data["install_wait_port"] = False
await do_install(update, ctx)
async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
query = update.callback_query
await query.answer()
@@ -350,12 +379,40 @@ async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> No
elif data == "menu_status": await cmd_status(update, ctx)
elif data == "menu_link": await cmd_link(update, ctx)
elif data == "menu_restart": await cmd_restart(update, ctx)
elif data == "menu_logs": await cmd_logs(update, ctx)
elif data == "menu_install":
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 = []
await query.edit_message_text("🌐 Выберите домен:", reply_markup=InlineKeyboardMarkup(buttons))
elif data.startswith("dom_"):
idx = int(data[4:])
ctx.user_data["install_domain"] = DOMAINS[idx]
busy_443 = await check_port(443)
kb = InlineKeyboardMarkup([
[InlineKeyboardButton("443 " + ("⚠️" if busy_443 else "✅"), callback_data="port_443"),
InlineKeyboardButton("8443", callback_data="port_8443")],
[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]
])
ctx.user_data["install_wait_port"] = True
await query.edit_message_text("🔌 Выберите порт или введите свой:", reply_markup=kb)
elif data.startswith("port_"):
ctx.user_data["install_port"] = data[5:]
ctx.user_data["install_wait_port"] = False
await do_install(update, ctx)
elif data == "menu_remove":
await sh("docker", "stop", CONTAINER_NAME)
await sh("docker", "rm", CONTAINER_NAME)
await query.edit_message_text("✅ Удалено.")
def main() -> None:
if not BOT_TOKEN: return
app = Application.builder().token(BOT_TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(CallbackQueryHandler(callback_handler))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler))
app.run_polling()
if __name__ == "__main__":
@@ -384,47 +441,13 @@ show_config() {
qrencode -t ANSIUTF8 "$LINK"
}
# ── 1) Установка MTProxy ─────────────────────────────────────────────────────
# ── Меню установки ───────────────────────────────────────────────────────────
menu_install() {
clear
local domains=("google.com" "wikipedia.org" "github.com" "habr.com")
echo -e "${CYAN}Выберите домен (Fake TLS):${NC}"
for i in "${!domains[@]}"; do echo -e " ${YELLOW}$((i+1)))${NC} ${domains[$i]}"; done
read -p "Выбор: " d_idx
DOMAIN=${domains[$((d_idx-1))]:-google.com}
local PORT=$(find_smart_port)
optimize_system
fix_firewall "$PORT"
spinner_start "Запуск контейнера..."
docker pull nineseconds/mtg:2 >/dev/null 2>&1
local SECRET=$(docker run --rm nineseconds/mtg:2 generate-secret --hex "$DOMAIN" 2>/dev/null)
docker stop "$CONTAINER_NAME" &>/dev/null
docker rm "$CONTAINER_NAME" &>/dev/null
# ВАЖНО: проброс UDP для звонков
docker run -d --name "$CONTAINER_NAME" --restart always \
-p "$PORT":"$PORT"/tcp \
-p "$PORT":"$PORT"/udp \
nineseconds/mtg:2 simple-run \
-n 1.1.1.1 -i prefer-ipv4 \
0.0.0.0:"$PORT" "$SECRET" > /dev/null 2>&1
sleep 2
spinner_stop
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
mkdir -p "$BOT_DIR"
echo "{\"domain\": \"$DOMAIN\", \"port\": \"$PORT\", \"secret\": \"$SECRET\"}" > "$BOT_DIR/proxy.json"
echo -e "\n${GREEN}✓ SwiftGram успешно запущен!${NC}"
show_config
fi
read -p "Нажмите Enter..."
echo -e "${CYAN}Установка производится через бота.${NC}"
echo -e "Настройте бота командой в меню, затем используйте /start в Telegram."
}
# ── 3) Настройка бота ────────────────────────────────────────────────────────
# ── Настройка бота ───────────────────────────────────────────────────────────
menu_setup_bot() {
clear
echo -e "${CYAN}Настройка Telegram бота...${NC}"
@@ -473,12 +496,12 @@ SELF="$(realpath "$0")"
while true; do
clear
echo -e "${MAGENTA}SWIFTGRAM MANAGER (UDP Fixed)${NC}"
echo -e "${MAGENTA}SWIFTGRAM MANAGER (UDP + Diagnostics)${NC}"
docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$" && echo -e " Прокси: ${GREEN}РАБОТАЕТ${NC}" || echo -e " Прокси: ${RED}ВЫКЛЮЧЕН${NC}"
echo -e "\n ${GREEN}1)${NC} Установить / Обновить прокси\n ${GREEN}2)${NC} Показать данные (QR)\n ${CYAN}3)${NC} Настроить бота\n ${RED}6)${NC} Удалить\n ${WHITE}0)${NC} Выход"
echo -e "\n ${GREEN}1)${NC} Установить (через бота)\n ${GREEN}2)${NC} Показать данные (QR)\n ${CYAN}3)${NC} Настроить бота\n ${RED}6)${NC} Удалить\n ${WHITE}0)${NC} Выход"
read -p "Пункт: " m_idx
case $m_idx in
1) menu_install ;; 2) clear; show_config; read -p "Enter..." ;; 3) menu_setup_bot ;;
1) menu_install; read -p "Enter..." ;; 2) clear; show_config; read -p "Enter..." ;; 3) menu_setup_bot ;;
6) docker stop "$CONTAINER_NAME" && docker rm "$CONTAINER_NAME"; rm -rf "$BOT_DIR"; exit 0 ;; 0) exit 0 ;;
esac
done