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
|
#!/bin/bash
|
||||||
# 🚀 SwiftGram MTProxy — Smart Modular Manager (Self-contained)
|
# 🚀 SwiftGram MTProxy — Smart Modular Manager (Self-contained)
|
||||||
# v1.1.0 — Фикс звонков (UDP) + Автономный режим
|
# v1.2.0 — UDP Calls + Port Selection + Advanced Diagnostics
|
||||||
|
|
||||||
# ── Цвета ────────────────────────────────────────────────────────────────────
|
# ── Цвета ────────────────────────────────────────────────────────────────────
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
@@ -130,22 +130,8 @@ install_base_deps() {
|
|||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Утилиты IP и Портов ──────────────────────────────────────────────────────
|
# ── Утилиты IP ──────────────────────────────────────────────────────────────
|
||||||
get_ip4() { curl -s -4 --max-time 5 https://api.ipify.org || echo "0.0.0.0"; }
|
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() {
|
prepare_bot_source() {
|
||||||
@@ -185,6 +171,8 @@ from telegram.ext import (
|
|||||||
CommandHandler,
|
CommandHandler,
|
||||||
CallbackQueryHandler,
|
CallbackQueryHandler,
|
||||||
ContextTypes,
|
ContextTypes,
|
||||||
|
MessageHandler,
|
||||||
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
||||||
@@ -229,12 +217,6 @@ async def get_ip4() -> str:
|
|||||||
if m: return m.group(0)
|
if m: return m.group(0)
|
||||||
return "0.0.0.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:
|
async def check_bbr() -> bool:
|
||||||
code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5)
|
code, out, _ = await sh("sysctl", "net.ipv4.tcp_congestion_control", timeout=5)
|
||||||
return "bbr" in out.lower()
|
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)
|
code, out, _ = await sh("docker", "inspect", CONTAINER_NAME, "--format", fmt, timeout=10)
|
||||||
return out.strip() if code == 0 else ""
|
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:
|
async def check_port(port: int) -> str | None:
|
||||||
if await proxy_running():
|
if await proxy_running():
|
||||||
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
|
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
|
if not await proxy_running(): return None
|
||||||
cmd_str = await docker_val("{{range .Config.Cmd}}{{.}} {{end}}")
|
cmd_str = await docker_val("{{range .Config.Cmd}}{{.}} {{end}}")
|
||||||
secret = cmd_str.split()[-1] if cmd_str else ""
|
secret = cmd_str.split()[-1] if cmd_str else ""
|
||||||
|
# Фикс склеивания портов
|
||||||
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
|
hp = await docker_val("{{range $p,$c := .HostConfig.PortBindings}}{{(index $c 0).HostPort}} {{end}}")
|
||||||
port = hp.split()[0] if hp else "443"
|
port = hp.split()[0] if hp else "443"
|
||||||
ip4 = await get_ip4()
|
ip4 = await get_ip4()
|
||||||
ip6 = await get_ip6()
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
return {
|
return {
|
||||||
"ip4": ip4, "ip6": ip6, "port": port, "secret": secret,
|
"ip4": ip4, "port": port, "secret": secret,
|
||||||
"domain": cfg.get("domain", "—"),
|
"domain": cfg.get("domain", "—"),
|
||||||
"link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}",
|
"link4": f"tg://proxy?server={ip4}&port={port}&secret={secret}"
|
||||||
"link6": f"tg://proxy?server={ip6}&port={port}&secret={secret}" if ip6 else None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def main_menu_kb() -> InlineKeyboardMarkup:
|
def main_menu_kb() -> InlineKeyboardMarkup:
|
||||||
@@ -296,7 +281,6 @@ def main_menu_kb() -> InlineKeyboardMarkup:
|
|||||||
HELP_TEXT = (
|
HELP_TEXT = (
|
||||||
"🚀 <b>SwiftGram MTProxy Manager</b>\n\n"
|
"🚀 <b>SwiftGram MTProxy Manager</b>\n\n"
|
||||||
"• TCP + UDP (звонки) активны\n"
|
"• TCP + UDP (звонки) активны\n"
|
||||||
"• IPv6 поддержка включена\n"
|
|
||||||
"• BBR оптимизация\n\n"
|
"• BBR оптимизация\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -315,6 +299,8 @@ async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
|||||||
text = "❌ <b>Прокси не запущен.</b>"
|
text = "❌ <b>Прокси не запущен.</b>"
|
||||||
else:
|
else:
|
||||||
bbr = "✅ Активен" if await check_bbr() 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 = (
|
text = (
|
||||||
"✅ <b>SwiftGram работает</b>\n\n"
|
"✅ <b>SwiftGram работает</b>\n\n"
|
||||||
f"🌐 IPv4: <code>{info['ip4']}</code>\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"🚀 BBR: <code>{bbr}</code>\n"
|
||||||
f"📞 Звонки: <code>✅ UDP открыт</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")]])
|
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)
|
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)
|
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
|
if not update.effective_user or not _ok(update.effective_user.id): return
|
||||||
info = await proxy_info()
|
info = await proxy_info()
|
||||||
if not info: text = "❌ Прокси не запущен."
|
if not info: text = "❌ Прокси не запущен."
|
||||||
else:
|
else: text = f"<b>Ссылка:</b>\n<code>{info['link4']}</code>"
|
||||||
text = f"<b>Ссылка:</b>\n<code>{info['link4']}</code>"
|
|
||||||
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
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)
|
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)
|
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")]])
|
kb = InlineKeyboardMarkup([[InlineKeyboardButton("◀️ Меню", callback_data="menu_main")]])
|
||||||
if update.callback_query: await update.callback_query.edit_message_text("✅ Перезапущено", reply_markup=kb)
|
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:
|
async def callback_handler(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
await query.answer()
|
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_status": await cmd_status(update, ctx)
|
||||||
elif data == "menu_link": await cmd_link(update, ctx)
|
elif data == "menu_link": await cmd_link(update, ctx)
|
||||||
elif data == "menu_restart": await cmd_restart(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:
|
def main() -> None:
|
||||||
if not BOT_TOKEN: return
|
if not BOT_TOKEN: return
|
||||||
app = Application.builder().token(BOT_TOKEN).build()
|
app = Application.builder().token(BOT_TOKEN).build()
|
||||||
app.add_handler(CommandHandler("start", start))
|
app.add_handler(CommandHandler("start", start))
|
||||||
app.add_handler(CallbackQueryHandler(callback_handler))
|
app.add_handler(CallbackQueryHandler(callback_handler))
|
||||||
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_handler))
|
||||||
app.run_polling()
|
app.run_polling()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -384,47 +441,13 @@ show_config() {
|
|||||||
qrencode -t ANSIUTF8 "$LINK"
|
qrencode -t ANSIUTF8 "$LINK"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 1) Установка MTProxy ─────────────────────────────────────────────────────
|
# ── Меню установки ───────────────────────────────────────────────────────────
|
||||||
menu_install() {
|
menu_install() {
|
||||||
clear
|
echo -e "${CYAN}Установка производится через бота.${NC}"
|
||||||
local domains=("google.com" "wikipedia.org" "github.com" "habr.com")
|
echo -e "Настройте бота командой в меню, затем используйте /start в Telegram."
|
||||||
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..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 3) Настройка бота ────────────────────────────────────────────────────────
|
# ── Настройка бота ───────────────────────────────────────────────────────────
|
||||||
menu_setup_bot() {
|
menu_setup_bot() {
|
||||||
clear
|
clear
|
||||||
echo -e "${CYAN}Настройка Telegram бота...${NC}"
|
echo -e "${CYAN}Настройка Telegram бота...${NC}"
|
||||||
@@ -473,12 +496,12 @@ SELF="$(realpath "$0")"
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
clear
|
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}"
|
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
|
read -p "Пункт: " m_idx
|
||||||
case $m_idx in
|
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 ;;
|
6) docker stop "$CONTAINER_NAME" && docker rm "$CONTAINER_NAME"; rm -rf "$BOT_DIR"; exit 0 ;; 0) exit 0 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
Reference in New Issue
Block a user