Files
gotelegram_pro/install.sh
anten-ka 0d087831d8 v2.4.0 — internationalization (EN/RU) + custom git templates
- i18n engine (lib/i18n.sh, lib/lang/en.sh, lib/lang/ru.sh)
- first-run language picker, persisted to .language + config.json
- install.sh, common.sh, backup.sh, templates_catalog.sh wired through t()/tf()
- backup.sh preserves .language marker and records language in metadata.json
- custom git template feature (first item in pro template picker)
  * validates HTTPS URLs, rejects shell metachars
  * 100MB size guard, 90s clone timeout
  * auto-detects index.html in dist/public/build/_site/site/docs/out/www
- bot v2.4.0: i18n.py + lang/{en,ru}.json, /lang command, language toggle button
- bot: custom git template via text input with waiter gating
2026-04-10 11:26:02 +03:00

1265 lines
45 KiB
Bash
Executable File

#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# GoTelegram v2.4.0 — MTProxy powered by telemt (Rust + Tokio)
# Anti-DPI • Fake TLS • TCP Splice • JA3/JA4 Resistance • i18n (EN/RU)
#
# Install:
# curl -sL URL/install.sh | sudo bash
# ══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
# Script path and libraries
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
LIB_DIR="$SCRIPT_DIR/lib"
# Load libraries
source "$LIB_DIR/common.sh"
source "$LIB_DIR/i18n.sh"
source "$LIB_DIR/telemt.sh"
source "$LIB_DIR/telemt_config.sh"
source "$LIB_DIR/website.sh"
source "$LIB_DIR/templates_catalog.sh"
source "$LIB_DIR/backup.sh"
[ -f "$LIB_DIR/stats.sh" ] && source "$LIB_DIR/stats.sh"
# Load language (from config.json or marker file, default en)
load_language "$(detect_language)"
# ── Главное меню (Compact Dashboard + 5 Top-Level Items) ──────────────────────
show_main_menu() {
local proxy_status bot_status nginx_st mode domain secret port ip link ssl_expiry
proxy_status=$(telemt_status)
bot_status=$(bot_service_status)
nginx_st=$(nginx_status 2>/dev/null || echo "stopped")
mode=$(config_get mode 2>/dev/null || echo "—")
domain=$(config_get domain 2>/dev/null || echo "")
secret=$(get_config_value secret 2>/dev/null || echo "")
port=$(get_config_value port 2>/dev/null || echo "443")
ip=$(get_server_ip 2>/dev/null || echo "N/A")
local W=54
local line; line=$(printf '━%.0s' $(seq 1 $W))
local line2; line2=$(printf '─%.0s' $(seq 1 $W))
# ── Header (no right border — ANSI breaks alignment) ──
echo ""
echo -e " ${BOLD}${CYAN}${line}${NC}"
echo -e " ${BOLD}${WHITE} GoTelegram v${GOTELEGRAM_VERSION}${NC} ${DIM}$(t dashboard_title)${NC}"
echo -e " ${BOLD}${CYAN}${line}${NC}"
# ── Service health ──
echo ""
echo -e " ${DIM}${line2}${NC}"
# Proxy
local proxy_icon proxy_color
case "$proxy_status" in
running) proxy_icon="●"; proxy_color="${GREEN}" ;;
stopped) proxy_icon="○"; proxy_color="${YELLOW}" ;;
*) proxy_icon="✗"; proxy_color="${RED}" ;;
esac
echo -e " ${proxy_color}${proxy_icon}${NC} $(t svc_proxy) ${proxy_color}${proxy_status}${NC} ${DIM}(telemt ${mode})${NC}"
# nginx
local nginx_icon nginx_color
case "$nginx_st" in
running) nginx_icon="●"; nginx_color="${GREEN}" ;;
*) nginx_icon="✗"; nginx_color="${RED}" ;;
esac
echo -e " ${nginx_icon}${nginx_color}${NC} $(t svc_nginx) ${nginx_color}${nginx_st}${NC} ${DIM}(127.0.0.1:8443)${NC}"
# Site (pro)
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
local site_icon site_color
if curl -sk --max-time 3 "https://${domain}/" -o /dev/null 2>/dev/null; then
site_icon="●"; site_color="${GREEN}"
else
site_icon="✗"; site_color="${RED}"
fi
echo -e " ${site_color}${site_icon}${NC} $(t svc_site) ${site_color}https://${domain}${NC}"
ssl_expiry=$(get_ssl_expiry "$domain" 2>/dev/null || echo "N/A")
echo -e " ${GREEN}${NC} $(t svc_ssl) ${DIM}$(tf ssl_until "$ssl_expiry")${NC}"
fi
# Bot
case "$bot_status" in
running) echo -e " ${GREEN}${NC} $(t svc_bot) ${GREEN}$(t running)${NC}" ;;
stopped) echo -e " ${YELLOW}${NC} $(t svc_bot) ${YELLOW}$(t stopped)${NC}" ;;
esac
echo -e " ${DIM}${line2}${NC}"
# ── Network parameters ──
echo -e " ${WHITE}$(t net_ip)${NC} ${CYAN}${ip}${NC} ${WHITE}$(t net_port)${NC} ${CYAN}${port}${NC} ${WHITE}$(t net_mode)${NC} ${CYAN}${mode}${NC}"
if [ -n "$domain" ]; then
echo -e " ${WHITE}$(t net_domain)${NC} ${CYAN}${domain}${NC}"
fi
echo -e " ${DIM}${line2}${NC}"
# ── Proxy link + QR ──
local mask_host
mask_host=$(config_get mask_host 2>/dev/null || echo "")
if [ -n "$secret" ] && [ "$proxy_status" = "running" ]; then
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
else
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
fi
echo -e " ${BOLD}${WHITE}$(t connection_link)${NC}"
echo -e " ${GREEN}${link}${NC}"
if command -v qrencode &>/dev/null; then
echo ""
qrencode -t UTF8 -m 2 "$link" 2>/dev/null | while IFS= read -r qr_line; do
echo " ${qr_line}"
done
echo ""
fi
else
echo -e " ${DIM}$(t proxy_not_configured)${NC}"
echo ""
fi
# ── Menu ──
echo -e " ${DIM}${line2}${NC}"
echo -e " ${CYAN}1${NC}) $(t menu_proxy)"
echo -e " ${CYAN}2${NC}) $(t menu_stats)"
echo -e " ${CYAN}3${NC}) $(t menu_manage)"
echo -e " ${CYAN}4${NC}) $(t menu_telegram_bot)"
echo -e " ${CYAN}5${NC}) $(t menu_about)"
echo -e " ${CYAN}0${NC}) ${DIM}$(t exit)${NC}"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${DIM}$(t auto_refresh_30s)${NC}"
echo -ne " ${WHITE}${NC}"
}
# ── Submenu: Proxy ──────────────────────────────────────────────────────────
submenu_proxy() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE}$(t submenu_proxy_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) $(t proxy_install_update)"
echo -e " ${CYAN}2${NC}) $(t proxy_status_detail)"
echo -e " ${CYAN}3${NC}) $(t proxy_copy_link)"
echo -e " ${CYAN}4${NC}) $(t proxy_share)"
echo -e " ${CYAN}5${NC}) $(t proxy_restart)"
echo -e " ${CYAN}6${NC}) $(t proxy_logs)"
echo -e " ${CYAN}7${NC}) $(t proxy_change_mode)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1) menu_install ;;
2) menu_status ;;
3) menu_link ;;
4) menu_share ;;
5) menu_restart ;;
6) menu_logs ;;
7) menu_change_mode ;;
0) break ;;
*) log_error "$(t invalid_choice)" ;;
esac
echo ""
echo -ne " ${DIM}$(t press_enter)${NC}"
read -r
done
}
# ── Submenu: Management ─────────────────────────────────────────────────────
submenu_manage() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE}$(t submenu_manage_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) $(t manage_backup)"
echo -e " ${CYAN}2${NC}) $(t manage_restore)"
echo -e " ${CYAN}3${NC}) $(t manage_update_telemt)"
echo -e " ${CYAN}4${NC}) $(t manage_site_ssl)"
echo -e " ${CYAN}5${NC}) $(t manage_remove)"
echo -e " ${CYAN}6${NC}) $(t manage_language)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1) interactive_backup ;;
2) interactive_restore ;;
3) update_telemt ;;
4) menu_website ;;
5) menu_remove ;;
6) menu_language ;;
0) break ;;
*) log_error "$(t invalid_choice)" ;;
esac
echo ""
echo -ne " ${DIM}$(t press_enter)${NC}"
read -r
done
}
# ── Submenu: About ──────────────────────────────────────────────────────────
submenu_about() {
while true; do
echo ""
echo -e " ${BOLD}${WHITE}$(t submenu_about_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${CYAN}1${NC}) $(t about_version_info)"
echo -e " ${CYAN}2${NC}) $(t about_promo)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1) menu_version ;;
2) menu_promo ;;
0) break ;;
*) log_error "$(t invalid_choice)" ;;
esac
echo ""
echo -ne " ${DIM}$(t press_enter)${NC}"
read -r
done
}
# ── Version info ────────────────────────────────────────────────────────────
menu_version() {
echo ""
echo -e " ${BOLD}${WHITE}$(t version_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
echo -e " ${WHITE}$(t version_label)${NC} v${GOTELEGRAM_VERSION}"
echo -e " ${WHITE}$(t version_engine)${NC} telemt (Rust + Tokio)"
echo -e " ${WHITE}$(t version_tech)${NC} Anti-DPI, Fake TLS, TCP Splice"
echo -e " ${WHITE}$(t version_license)${NC} MIT"
echo -e " ${DIM}$(printf '─%.0s' {1..54})${NC}"
}
# ── Install: mode selection ─────────────────────────────────────────────────
menu_install() {
# Check for v1
if detect_v1_installation; then
echo ""
echo -e " ${YELLOW}$(t v1_detected)${NC}"
echo -e " ${DIM}$(tf v1_container "$V1_CONTAINER_NAME")${NC}"
echo ""
if ! migrate_v1_to_v2; then
return
fi
fi
echo ""
echo -e " ${BOLD}${WHITE}$(t install_select_mode)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1)${NC} ${GREEN}$(t install_lite_title)${NC}"
echo -e " ${DIM}$(t install_lite_desc1)${NC}"
echo -e " ${DIM}$(t install_lite_desc2)${NC}"
echo ""
echo -e " ${CYAN}2)${NC} ${MAGENTA}$(t install_pro_title)${NC}"
echo -e " ${DIM}$(t install_pro_desc1)${NC}"
echo -e " ${DIM}$(t install_pro_desc2)${NC}"
echo -e " ${DIM}$(t install_pro_desc3)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}$(t install_mode_choice)${NC} "
read -r mode_choice
mode_choice="${mode_choice:-}"
case "$mode_choice" in
1) install_lite_mode ;;
2) install_pro_mode ;;
*) log_error "$(tf install_bad_choice "${mode_choice:-<empty>}")" ;;
esac
}
# ── Lite mode ───────────────────────────────────────────────────────────────
install_lite_mode() {
log_step "$(t install_lite_step)"
# Domain selection
local domain
domain=$(select_quick_domain)
[ $? -ne 0 ] && return
# Port selection
local port
port=$(select_port)
[ $? -ne 0 ] && return
# Generate secret
local secret
secret=$(generate_hex 32)
# Confirm
local ip
ip=$(get_server_ip)
echo ""
echo -e " ${BOLD}${WHITE}$(t install_config_title)${NC}"
echo -e " $(t install_cfg_ip) ${CYAN}${ip}${NC}"
echo -e " $(t install_cfg_port) ${CYAN}${port}${NC}"
echo -e " $(t install_cfg_mask) ${CYAN}${domain}${NC}"
echo -e " $(t install_cfg_mode) ${GREEN}Lite${NC}"
echo ""
if ! confirm "$(t install_confirm_proxy)"; then
return
fi
# Install
ensure_deps
install_telemt_full || return
# Generate telemt config
generate_telemt_toml "$secret" "$port" "lite" "$domain" "443"
# Validate
validate_telemt_config || return
# Start
start_telemt || return
# Save GoTelegram config
save_gotelegram_config "telemt" "lite" "$port" "$secret" "$domain" "" ""
# Credits
show_credits
# Result
show_proxy_info
log_success "$(tf install_done "$GOTELEGRAM_VERSION" "Lite")"
}
# ── Pro mode ────────────────────────────────────────────────────────────────
install_pro_mode() {
log_step "$(t install_pro_step)"
# Enter domain
echo ""
echo -ne " ${WHITE}$(t install_enter_domain)${NC} "
read -r user_domain
if [ -z "$user_domain" ] || ! validate_domain "$user_domain"; then
log_error "$(tf install_bad_domain "${user_domain:-<empty>}")"
return
fi
# Check DNS
local resolved_ip server_ip
resolved_ip=$(dig +short "$user_domain" A 2>/dev/null | head -1)
server_ip=$(get_server_ip)
if [ -n "$resolved_ip" ] && [ "$resolved_ip" != "$server_ip" ]; then
log_warning "$(tf install_dns_mismatch "$user_domain" "$resolved_ip" "$server_ip")"
if ! confirm "$(t install_continue_anyway)"; then
return
fi
fi
# Email for Let's Encrypt
echo -ne " ${WHITE}$(t install_enter_email)${NC} "
read -r ssl_email
# Template selection
local template_dir
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
# Pro architecture:
# telemt listens on 0.0.0.0:443 (accepts ALL connections)
# nginx listens on 127.0.0.1:8443 with SSL (serves website)
# MTProxy client → :443 → telemt (proxies)
# Regular browser → :443 → telemt → 127.0.0.1:8443 → nginx (website)
# ISP only sees HTTPS on 443 to domain
local nginx_internal_port=8443
echo ""
echo -e " ${DIM}$(t install_arch_desc1)${NC}"
echo -e " ${DIM}$(tf install_arch_desc2 "$nginx_internal_port")${NC}"
echo -e " ${DIM}$(tf install_arch_desc3 "$user_domain")${NC}"
# Generate fake-TLS secret (ee + secret + hex domain)
# ee prefix tells Telegram client to masquerade traffic as TLS to domain
local raw_secret
raw_secret=$(generate_hex 32)
local domain_hex
domain_hex=$(printf '%s' "$user_domain" | xxd -p | tr -d '\n')
local faketls_secret="ee${raw_secret}${domain_hex}"
# Confirmation
echo ""
echo -e " ${BOLD}${WHITE}$(t install_config_title)${NC}"
echo -e " $(t install_cfg_domain) ${CYAN}${user_domain}${NC}"
echo -e " $(t install_cfg_port) ${CYAN}443 (telemt + nginx)${NC}"
echo -e " $(t install_cfg_mode) ${MAGENTA}Pro (fake-TLS)${NC}"
echo ""
if ! confirm "$(t install_confirm_proxy_site)"; then
return
fi
# Install
ensure_deps
install_telemt_full || return
# telemt config: listen 443, masquerade to local nginx via dns_override
generate_telemt_toml "$raw_secret" "443" "pro" "$user_domain" "$nginx_internal_port"
# Website setup (nginx on internal port + certbot + template)
setup_pro_mode "$user_domain" "$template_dir" "$nginx_internal_port" "$ssl_email" || return
# Stop nginx on 443 before starting telemt (telemt will take 443)
# nginx already reconfigured to internal port
systemctl restart nginx 2>/dev/null
# Start telemt
start_telemt || return
# Save config
local tpl_id
tpl_id=$(basename "$template_dir")
save_gotelegram_config "telemt" "pro" "443" "$raw_secret" "$user_domain" "$user_domain" "$tpl_id"
# Result — use domain and fake-TLS link
show_proxy_info_pro "$user_domain" "$faketls_secret"
echo -e " ${WHITE}$(t svc_site):${NC} ${GREEN}https://${user_domain}${NC}"
log_success "$(tf install_done "$GOTELEGRAM_VERSION" "Pro")"
}
# ── Статус ───────────────────────────────────────────────────────────────────
menu_status() {
show_proxy_info
# Extras for pro
local mode
mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "pro" ]; then
local domain
domain=$(config_get domain 2>/dev/null)
if [ -n "$domain" ]; then
local ssl_expiry
ssl_expiry=$(get_ssl_expiry "$domain")
local nginx_st
nginx_st=$(nginx_status)
echo -e " ${WHITE}$(t svc_nginx):${NC} ${nginx_st}"
echo -e " ${WHITE}$(t website_ssl_until)${NC} ${ssl_expiry}"
echo -e " ${WHITE}$(t svc_site):${NC} https://${domain}"
echo ""
fi
fi
}
# ── Ссылка ───────────────────────────────────────────────────────────────────
menu_link() {
local secret port ip link mode domain mask_host
secret=$(get_config_value secret)
port=$(get_config_value port)
ip=$(get_server_ip)
mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
mask_host=$(config_get mask_host 2>/dev/null || echo "")
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
else
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
fi
echo ""
echo -e " ${BOLD}${WHITE}$(t link_title)${NC}"
echo ""
echo -e " ${GREEN}${link}${NC}"
echo ""
if command -v qrencode &>/dev/null; then
qrencode -t UTF8 -m 2 "$link" 2>/dev/null
fi
}
# ── Поделиться ───────────────────────────────────────────────────────────────
menu_share() {
local secret port ip link mode domain mask_host server_display
secret=$(get_config_value secret)
port=$(get_config_value port)
ip=$(get_server_ip)
mode=$(config_get mode 2>/dev/null || echo "lite")
domain=$(config_get domain 2>/dev/null || echo "")
mask_host=$(config_get mask_host 2>/dev/null || echo "")
if [ "$mode" = "pro" ] && [ -n "$domain" ]; then
link=$(generate_proxy_link "$domain" "$port" "$secret" "$domain")
server_display="$domain"
else
link=$(generate_proxy_link "$ip" "$port" "$secret" "$mask_host")
server_display="$ip"
fi
echo ""
echo -e " ${BOLD}$(t share_title)${NC}"
echo ""
printf "$(t share_line1)\n" "$GOTELEGRAM_VERSION"
echo ""
printf "$(t share_server)\n" "$server_display"
printf "$(t share_port)\n" "$port"
echo ""
echo "$(t share_connect_cta)"
echo "$link"
echo ""
echo "$(t share_footer)"
echo ""
}
# ── Перезапуск ───────────────────────────────────────────────────────────────
menu_restart() {
restart_telemt
local mode
mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "pro" ]; then
restart_nginx
fi
}
# ── Logs ────────────────────────────────────────────────────────────────────
menu_logs() {
echo ""
echo -e " ${BOLD}${WHITE}$(tf logs_telemt_title 40)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
telemt_logs 40
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
}
# ── Change mode / template ──────────────────────────────────────────────────
menu_change_mode() {
local current_mode
current_mode=$(config_get mode 2>/dev/null)
echo ""
echo -e " ${WHITE}$(t change_current_mode)${NC} ${CYAN}${current_mode}${NC}"
echo ""
echo -e " ${CYAN}1${NC}) $(t change_template)"
echo -e " ${CYAN}2${NC}) $(t change_mode_switch)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1)
if [ "$current_mode" != "pro" ]; then
log_error "$(t change_only_pro)"
return
fi
local template_dir
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
switch_template "$template_dir"
;;
2)
log_warning "$(t change_requires_reinstall)"
if confirm "$(t change_reinstall_confirm)"; then
menu_install
fi
;;
esac
}
# ── Website management ─────────────────────────────────────────────────────
menu_website() {
local mode
mode=$(config_get mode 2>/dev/null)
if [ "$mode" != "pro" ]; then
log_info "$(t website_only_pro)"
return
fi
local domain
domain=$(config_get domain 2>/dev/null)
echo ""
echo -e " ${BOLD}${WHITE}$(t website_title)${NC}"
echo -e " $(t website_domain) ${CYAN}${domain}${NC}"
echo -e " $(t website_ssl_until) $(get_ssl_expiry "$domain")"
echo ""
echo -e " ${CYAN}1${NC}) $(t website_renew_ssl)"
echo -e " ${CYAN}2${NC}) $(t website_restart_nginx)"
echo -e " ${CYAN}3${NC}) $(t website_change_template)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1) renew_ssl_certificate ;;
2) restart_nginx ;;
3)
local template_dir
template_dir=$(interactive_template_selection)
[ $? -ne 0 ] && return
switch_template "$template_dir"
;;
esac
}
# ── Remove ─────────────────────────────────────────────────────────────────
menu_remove() {
echo ""
echo -e " ${BOLD}${RED}$(t remove_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1${NC}) $(t remove_proxy_only)"
echo -e " ${CYAN}2${NC}) $(t remove_bot_only)"
echo -e " ${CYAN}3${NC}) $(t remove_all)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r rm_choice
case "$rm_choice" in
1)
log_warning "$(t remove_warn_proxy)"
if ! confirm "$(t remove_confirm_proxy)"; then return; fi
if confirm "$(t remove_backup_before)"; then
interactive_backup
fi
remove_telemt
local mode
mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "pro" ]; then
remove_pro_mode
fi
rm -f "$GOTELEGRAM_CONFIG"
log_success "$(t remove_proxy_done)"
;;
2)
bot_remove
;;
3)
log_warning "$(t remove_warn_all)"
if ! confirm "$(t remove_confirm_all)"; then return; fi
if confirm "$(t remove_backup_before)"; then
interactive_backup
fi
# Proxy
remove_telemt
local mode
mode=$(config_get mode 2>/dev/null)
if [ "$mode" = "pro" ]; then
remove_pro_mode
fi
rm -f "$GOTELEGRAM_CONFIG"
# Bot
if [ "$(bot_service_status)" != "not_installed" ]; then
systemctl stop "$BOT_SERVICE" 2>/dev/null
systemctl disable "$BOT_SERVICE" 2>/dev/null
rm -f "/etc/systemd/system/${BOT_SERVICE}.service"
systemctl daemon-reload
rm -rf "$BOT_DIR"
fi
log_success "$(t remove_all_done)"
;;
esac
}
# ── Telegram-бот ────────────────────────────────────────────────────────────
BOT_DIR="/opt/gotelegram-bot"
BOT_SERVICE="gotelegram-bot"
bot_service_status() {
if ! systemctl list-unit-files "$BOT_SERVICE.service" &>/dev/null 2>&1; then
echo "not_installed"
elif systemctl is-active "$BOT_SERVICE" &>/dev/null 2>&1; then
echo "running"
else
echo "stopped"
fi
}
menu_bot() {
local st
st=$(bot_service_status)
echo ""
echo -e " ${BOLD}${WHITE}$(t bot_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
case "$st" in
running)
echo -e " $(t bot_status_colon) ${GREEN}$(t bot_status_running)${NC}"
echo ""
echo -e " ${CYAN}1${NC}) $(t bot_menu_status)"
echo -e " ${CYAN}2${NC}) $(t bot_menu_logs)"
echo -e " ${CYAN}3${NC}) $(t bot_menu_restart)"
echo -e " ${CYAN}4${NC}) $(t bot_menu_stop)"
echo -e " ${CYAN}5${NC}) $(t bot_menu_settings)"
echo -e " ${CYAN}6${NC}) $(t bot_menu_remove)"
;;
stopped)
echo -e " $(t bot_status_colon) ${YELLOW}$(t bot_status_stopped)${NC}"
echo ""
echo -e " ${CYAN}1${NC}) $(t bot_menu_status)"
echo -e " ${CYAN}2${NC}) $(t bot_menu_logs)"
echo -e " ${CYAN}3${NC}) $(t bot_menu_start)"
echo -e " ${CYAN}5${NC}) $(t bot_menu_settings)"
echo -e " ${CYAN}6${NC}) $(t bot_menu_remove)"
;;
*)
echo -e " $(t bot_status_colon) ${RED}$(t bot_status_not_installed)${NC}"
echo ""
echo -e " ${DIM}$(t bot_intro1)${NC}"
echo -e " ${DIM}$(t bot_intro2)${NC}"
echo ""
echo -e " ${CYAN}1${NC}) $(t bot_menu_install)"
;;
esac
echo -e " ${CYAN}0${NC}) $(t back)"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$st" in
running)
case "$ch" in
1) bot_show_status ;;
2) bot_show_logs ;;
3) systemctl restart "$BOT_SERVICE" && log_success "$(t bot_restarted)" ;;
4) systemctl stop "$BOT_SERVICE" && log_info "$(t bot_stopped)" ;;
5) bot_edit_config ;;
6) bot_remove ;;
esac
;;
stopped)
case "$ch" in
1) bot_show_status ;;
2) bot_show_logs ;;
3) systemctl start "$BOT_SERVICE" && log_success "$(t bot_started)" ;;
5) bot_edit_config ;;
6) bot_remove ;;
esac
;;
*)
case "$ch" in
1) bot_install ;;
esac
;;
esac
}
bot_install() {
log_step "$(t bot_install_step)"
# Python
if ! command -v python3 &>/dev/null; then
log_info "$(t bot_install_python)"
if command -v apt-get &>/dev/null; then
apt-get update -qq && apt-get install -y -qq python3 python3-pip python3-venv
elif command -v dnf &>/dev/null; then
dnf install -y -q python3 python3-pip
elif command -v yum &>/dev/null; then
yum install -y -q python3 python3-pip
fi
fi
# Copy bot files
mkdir -p "$BOT_DIR"
if [ -f "$SCRIPT_DIR/gotelegram-bot/bot.py" ]; then
cp "$SCRIPT_DIR/gotelegram-bot/bot.py" "$BOT_DIR/"
cp "$SCRIPT_DIR/gotelegram-bot/requirements.txt" "$BOT_DIR/"
[ -f "$SCRIPT_DIR/gotelegram-bot/config.example.env" ] && \
cp "$SCRIPT_DIR/gotelegram-bot/config.example.env" "$BOT_DIR/"
# Copy i18n language files for bot
if [ -d "$SCRIPT_DIR/gotelegram-bot/lang" ]; then
mkdir -p "$BOT_DIR/lang"
cp -f "$SCRIPT_DIR/gotelegram-bot/lang/"*.json "$BOT_DIR/lang/" 2>/dev/null
fi
[ -f "$SCRIPT_DIR/gotelegram-bot/i18n.py" ] && \
cp "$SCRIPT_DIR/gotelegram-bot/i18n.py" "$BOT_DIR/"
else
log_error "$(tf bot_files_not_found "$SCRIPT_DIR/gotelegram-bot/")"
return 1
fi
# Templates catalog
[ -f "$SCRIPT_DIR/templates_catalog.json" ] && \
cp "$SCRIPT_DIR/templates_catalog.json" "$GOTELEGRAM_DIR/"
# Venv
if [ ! -d "$BOT_DIR/venv" ]; then
log_info "$(t bot_create_venv)"
python3 -m venv "$BOT_DIR/venv"
fi
log_info "$(t bot_install_deps)"
"$BOT_DIR/venv/bin/pip" install -r "$BOT_DIR/requirements.txt" -q
# Configuration
if [ ! -f "$BOT_DIR/.env" ]; then
echo ""
echo -e " ${YELLOW}$(t bot_enter_token)${NC}"
local token=""
while [ -z "$token" ]; do
echo -ne " ${WHITE}$(t bot_token)${NC} "
read -r token
token=$(echo "$token" | tr -d '[:space:]')
[ -z "$token" ] && log_error "$(t bot_token_empty)"
done
echo ""
echo -e " ${WHITE}$(t bot_add_admin_how)${NC}"
echo -e " ${CYAN}1${NC}) $(t bot_admin_auto)"
echo -e " ${CYAN}2${NC}) $(t bot_admin_manual)"
echo -ne " ${WHITE}$(t choose) [1]:${NC} "
read -r admin_mode
admin_mode="${admin_mode:-1}"
local admin_ids=""
if [ "$admin_mode" = "2" ]; then
echo -ne " ${WHITE}$(t bot_admin_ids_prompt)${NC} "
read -r admin_ids
admin_ids=$(echo "$admin_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
fi
# Propagate selected language to bot so UI matches
local bot_lang
bot_lang=$(get_language 2>/dev/null || echo en)
{
echo "BOT_TOKEN=$token"
[ -n "$admin_ids" ] && echo "ALLOWED_IDS=$admin_ids"
echo "BOT_LANG=$bot_lang"
} > "$BOT_DIR/.env"
chmod 600 "$BOT_DIR/.env"
log_success "$(t bot_env_created)"
else
log_info "$(t bot_env_exists)"
fi
# Systemd
cat > "/etc/systemd/system/${BOT_SERVICE}.service" << SVCEOF
[Unit]
Description=GoTelegram v${GOTELEGRAM_VERSION} Telegram Bot
After=network.target
[Service]
Type=simple
WorkingDirectory=$BOT_DIR
ExecStart=$BOT_DIR/venv/bin/python $BOT_DIR/bot.py
Restart=always
RestartSec=5
Environment=PATH=$BOT_DIR/venv/bin:/usr/bin
[Install]
WantedBy=multi-user.target
SVCEOF
systemctl daemon-reload
systemctl enable "$BOT_SERVICE" &>/dev/null
systemctl restart "$BOT_SERVICE" 2>/dev/null || systemctl start "$BOT_SERVICE"
# If auto mode — wait until bot captures first admin
local has_ids
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ -z "$has_ids" ]; then
echo ""
echo -e " ${YELLOW}╔══════════════════════════════════════════════════════╗${NC}"
printf " ${YELLOW}${NC} ${BOLD}%-52s${NC} ${YELLOW}${NC}\n" "$(t bot_wait_admin_title)"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
printf " ${YELLOW}${NC} %s ${CYAN}/start${NC}%*s${YELLOW}${NC}\n" "$(t bot_wait_admin_msg1)" 0 ""
printf " ${YELLOW}${NC} %-52s ${YELLOW}${NC}\n" "$(t bot_wait_admin_msg2)"
echo -e " ${YELLOW}${NC} ${YELLOW}${NC}"
printf " ${YELLOW}${NC} ${DIM}%-52s${NC} ${YELLOW}${NC}\n" "$(t bot_wait_admin_skip)"
echo -e " ${YELLOW}╚══════════════════════════════════════════════════════╝${NC}"
echo ""
local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
local i=0
local waited=0
local max_wait=300 # 5 min max
# Catch Ctrl+C to skip waiting without killing the script
local interrupted=0
trap 'interrupted=1' INT
while [ $waited -lt $max_wait ] && [ $interrupted -eq 0 ]; do
printf "\r ${CYAN}${frames[$i]}${NC} $(tf bot_wait_spinner "$waited") " >&2
i=$(( (i+1) % ${#frames[@]} ))
sleep 1
waited=$((waited + 1))
# Check if ALLOWED_IDS has appeared
has_ids=$(grep "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ -n "$has_ids" ]; then
break
fi
done
trap - INT
printf "\r\033[K" >&2 # clear spinner line
if [ -n "$has_ids" ]; then
echo ""
log_success "$(t bot_admin_assigned)"
echo -e " ${WHITE}ID:${NC} ${GREEN}${has_ids}${NC}"
elif [ $interrupted -eq 1 ]; then
echo ""
log_warning "$(t bot_wait_skipped)"
else
echo ""
log_warning "$(t bot_wait_timeout)"
fi
fi
echo ""
log_success "$(t bot_installed)"
echo -e " ${DIM}systemctl status $BOT_SERVICE${NC}"
echo -e " ${DIM}journalctl -u $BOT_SERVICE -f${NC}"
}
bot_show_status() {
echo ""
echo -e " ${BOLD}${WHITE}$(t bot_status_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
systemctl status "$BOT_SERVICE" --no-pager -l 2>/dev/null | head -15 | while IFS= read -r line; do
echo " $line"
done
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
if [ -f "$BOT_DIR/.env" ]; then
local has_token has_ids
has_token=$(grep -c "BOT_TOKEN=" "$BOT_DIR/.env" 2>/dev/null || echo 0)
has_ids=$(grep "ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null | cut -d= -f2)
if [ "${has_token:-0}" -gt 0 ]; then
echo -e " $(t bot_token) ${GREEN}$(t bot_token_configured)${NC}"
fi
if [ -n "$has_ids" ]; then
echo -e " $(t bot_access_colon) $(tf bot_access_ids_fmt "$has_ids")"
else
echo -e " $(t bot_access_colon) ${YELLOW}$(t bot_access_open)${NC}"
fi
fi
}
bot_show_logs() {
echo ""
echo -e " ${BOLD}${WHITE}$(t bot_logs_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
journalctl -u "$BOT_SERVICE" --no-pager -n 30 2>/dev/null | while IFS= read -r line; do
echo " $line"
done
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
}
bot_edit_config() {
echo ""
echo -e " ${BOLD}${WHITE}$(t bot_settings_title)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
if [ -f "$BOT_DIR/.env" ]; then
echo -e " ${DIM}$(t bot_current_env)${NC}"
while IFS= read -r line; do
# Mask token for security
if [[ "$line" == BOT_TOKEN=* ]]; then
local tok="${line#BOT_TOKEN=}"
echo -e " BOT_TOKEN=${tok:0:10}...${tok: -5}"
else
echo " $line"
fi
done < "$BOT_DIR/.env"
fi
echo ""
echo -e " ${CYAN}1${NC}) $(t bot_change_token)"
echo -e " ${CYAN}2${NC}) $(t bot_change_allowed)"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1)
echo -ne " ${WHITE}$(t bot_new_token)${NC} "
read -r new_token
new_token=$(echo "$new_token" | tr -d '[:space:]')
if [ -n "$new_token" ]; then
sed -i "s|^BOT_TOKEN=.*|BOT_TOKEN=$new_token|" "$BOT_DIR/.env"
systemctl restart "$BOT_SERVICE"
log_success "$(t bot_token_updated)"
else
log_error "$(t bot_token_empty_err)"
fi
;;
2)
echo -ne " ${WHITE}$(t bot_allowed_prompt)${NC} "
read -r new_ids
# Normalize: spaces and commas → commas, strip extras
new_ids=$(echo "$new_ids" | tr ' ' ',' | sed 's/,,*/,/g; s/^,//; s/,$//')
if grep -q "^ALLOWED_IDS=" "$BOT_DIR/.env" 2>/dev/null; then
if [ -n "$new_ids" ]; then
sed -i "s|^ALLOWED_IDS=.*|ALLOWED_IDS=$new_ids|" "$BOT_DIR/.env"
else
sed -i '/^ALLOWED_IDS=/d' "$BOT_DIR/.env"
fi
else
[ -n "$new_ids" ] && echo "ALLOWED_IDS=$new_ids" >> "$BOT_DIR/.env"
fi
systemctl restart "$BOT_SERVICE"
log_success "$(t bot_access_updated)"
;;
esac
}
bot_remove() {
echo ""
log_warning "$(t bot_remove_warn)"
if ! confirm "$(t bot_remove_confirm)"; then
return
fi
systemctl stop "$BOT_SERVICE" 2>/dev/null
systemctl disable "$BOT_SERVICE" 2>/dev/null
rm -f "/etc/systemd/system/${BOT_SERVICE}.service"
systemctl daemon-reload
rm -rf "$BOT_DIR"
log_success "$(t bot_removed)"
}
# ── Promo ────────────────────────────────────────────────────────────────────
_promo_block() {
# Print a promo section without width-fragile box borders (i18n safe)
local line2; line2=$(printf '─%.0s' {1..54})
echo ""
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_host1_title)${NC}"
echo -e " $(t promo_link_label) ${CYAN}https://vk.cc/ct29NQ${NC}"
echo -e " ${WHITE}OFF60${NC}$(tf promo_off60)"
echo -e " ${WHITE}antenka20${NC}$(tf promo_ant20)"
echo -e " ${WHITE}antenka6${NC}$(tf promo_ant6)"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_host2_title)${NC}"
echo -e " $(t promo_link_label) ${CYAN}https://vk.cc/cUxAhj${NC}"
echo -e " ${WHITE}OFF60${NC}$(tf promo_off60)"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${BOLD}${YELLOW}$(t promo_tips_title)${NC}"
echo -e " ${CYAN}https://pay.cloudtips.ru/p/7410814f${NC}"
echo -e " ${DIM}${line2}${NC}"
echo ""
}
menu_promo() {
_promo_block
}
# ── Проверка: показывать ли промо (раз в сутки) ────────────────────────────
should_show_promo() {
local stamp_file="$GOTELEGRAM_DIR/.promo_last_shown"
if [ ! -f "$stamp_file" ]; then
return 0 # никогда не показывали
fi
local last_shown now diff
last_shown=$(cat "$stamp_file" 2>/dev/null || echo "0")
last_shown="${last_shown//[^0-9]/}"
last_shown="${last_shown:-0}"
now=$(date +%s)
diff=$(( now - last_shown ))
# 86400 = 24 часа
[ "$diff" -ge 86400 ]
}
mark_promo_shown() {
mkdir -p "$GOTELEGRAM_DIR"
date +%s > "$GOTELEGRAM_DIR/.promo_last_shown"
}
# ── Promo with QR + delay (on install + once per day) ───────────────────
show_promo_with_qr() {
_promo_block
# QR codes
if command -v qrencode &>/dev/null; then
echo -e " ${DIM}$(t promo_qr_host1)${NC}"
qrencode -t UTF8 -m 1 "https://vk.cc/ct29NQ" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
echo ""
echo -e " ${DIM}$(t promo_qr_host2)${NC}"
qrencode -t UTF8 -m 1 "https://vk.cc/cUxAhj" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
echo ""
echo -e " ${DIM}$(t promo_qr_tips)${NC}"
qrencode -t UTF8 -m 1 "https://pay.cloudtips.ru/p/7410814f" 2>/dev/null | while IFS= read -r qr_line; do
echo " $qr_line"
done
fi
mark_promo_shown
# 5-second countdown
for i in 5 4 3 2 1; do
echo -ne "\r ${DIM}$(tf promo_menu_in "$i")${NC} "
sleep 1
done
echo -ne "\r \r"
}
# ── First-run: pick language ─────────────────────────────────────────────────
first_run_language_picker() {
# Show picker only if language not yet saved
local marker="${GOTELEGRAM_DIR:-/opt/gotelegram}/.language"
local cfg_lang=""
if [ -f "$GOTELEGRAM_CONFIG" ] && command -v jq >/dev/null 2>&1; then
cfg_lang=$(jq -r '.language // empty' "$GOTELEGRAM_CONFIG" 2>/dev/null)
fi
if [ -f "$marker" ] || [ -n "$cfg_lang" ]; then
return 0
fi
local chosen
chosen=$(pick_language_interactive)
save_language "$chosen"
load_language "$chosen"
}
# ── Change language on demand ────────────────────────────────────────────────
menu_language() {
echo ""
echo -e " ${BOLD}${WHITE}$(t lang_change_prompt)${NC}"
echo -e " ${DIM}$(printf '─%.0s' {1..55})${NC}"
echo -e " ${CYAN}1${NC}) English"
echo -e " ${CYAN}2${NC}) Русский"
echo -e " ${CYAN}0${NC}) $(t back)"
echo -ne " ${WHITE}$(t choose):${NC} "
read -r ch
case "$ch" in
1) save_language "en"; load_language "en"; log_success "$(tf lang_saved English)" ;;
2) save_language "ru"; load_language "ru"; log_success "$(tf lang_saved Русский)" ;;
esac
}
# ── Точка входа / Entry point ───────────────────────────────────────────────
main() {
check_root
init_dirs
# First-run language picker (before banner so banner appears in chosen lang)
first_run_language_picker
show_banner
# Pre-flight
check_os
check_disk_space 500
# Promo once per day
if should_show_promo; then
show_promo_with_qr
fi
while true; do
clear
show_main_menu
# Auto-refresh: 30 sec timeout
if read -t 30 -r choice; then
case "$choice" in
1) submenu_proxy ;;
2) submenu_stats ;;
3) submenu_manage ;;
4) menu_bot ;;
5) submenu_about ;;
0|q|exit) echo ""; log_info "$(t bye)"; exit 0 ;;
*) log_error "$(t invalid_choice)" ;;
esac
# Pause after submenu (except stats — it has its own loop)
if [ "$choice" != "2" ]; then
echo ""
echo -ne " ${DIM}$(t press_enter_to_return)${NC}"
read -r
fi
fi
# If read timed out, loop refreshes the dashboard
done
}
# ── Статистика (авто-обновление 1 сек, без мерцания) ───────────────────────
submenu_stats() {
# Инициализируем статистику при первом входе
if type stats_init &>/dev/null; then
stats_init 2>/dev/null
fi
local line2; line2=$(printf '─%.0s' {1..54})
local first_draw=1
# Скрываем курсор для плавного обновления
tput civis 2>/dev/null
# Восстанавливаем курсор при выходе из функции
trap 'tput cnorm 2>/dev/null; trap - RETURN' RETURN
while true; do
if [ "$first_draw" -eq 1 ]; then
clear
first_draw=0
else
# Перемещаем курсор в начало экрана вместо clear — нет мерцания
tput cup 0 0 2>/dev/null || printf '\033[H'
fi
# Draw the whole screen over the previous content
echo -e "\033[J" # erase from cursor to end (removes trails)
echo -e " ${BOLD}${WHITE}$(t stats_title)${NC}"
echo -e " ${DIM}${line2}${NC}"
if type show_traffic_stats &>/dev/null; then
show_traffic_stats
else
echo -e " ${DIM}$(t stats_module_missing)${NC}"
echo -e " ${DIM}$(t stats_file_missing)${NC}"
echo ""
fi
echo -e " ${DIM}${line2}${NC}"
local stats_on
stats_on=$(t stats_on)
if type toggle_stats &>/dev/null; then
local cfg_val
cfg_val=$(config_get stats_enabled 2>/dev/null || echo "true")
[ "$cfg_val" = "false" ] && stats_on=$(t stats_off)
fi
echo -e " ${CYAN}1${NC}) $(tf stats_toggle "$stats_on")"
echo -e " ${CYAN}2${NC}) $(t stats_install_collector)"
echo -e " ${CYAN}0${NC}) ${DIM}$(t back)${NC}"
echo -e " ${DIM}${line2}${NC}"
echo -e " ${DIM}$(t stats_auto_refresh)${NC}"
# Show cursor for input, then hide again
tput cnorm 2>/dev/null
echo -ne " ${WHITE}${NC}"
if read -t 3 -r ch; then
tput civis 2>/dev/null
case "$ch" in
1)
if type toggle_stats &>/dev/null; then
toggle_stats
echo -ne " ${DIM}$(t press_enter)${NC}"; read -r
first_draw=1 # full redraw after action
fi
;;
2)
if type install_stats_collector &>/dev/null; then
install_stats_collector
echo -ne " ${DIM}$(t press_enter)${NC}"; read -r
first_draw=1
fi
;;
0|"") return ;;
esac
fi
tput civis 2>/dev/null
done
}
main "$@"