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:
163
install.sh
163
install.sh
@@ -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
|
||||
Reference in New Issue
Block a user