mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 11:26:03 +00:00
feat(v2.4.2): bot non-interactive install.sh actions
- install.sh: new bot_action_dispatch entry point for --action=X --json CLI invocation from the bot/scripts - install.sh: bot_action_change_template — reuses download_template + deploy_template_to_nginx, updates config.json template_id - install.sh: bot_action_change_lite_domain — regenerates telemt TOML with new fake-TLS mask domain, restarts telemt - bot.py: run_bot_action() subprocess wrapper parses JSON result - bot.py: cb_pro_confirm now performs real change-template when already in pro mode (fresh install still routes to CLI) - bot.py: cb_lite_domain now performs real change-lite-domain when already in lite mode - version -> 2.4.2
This commit is contained in:
@@ -100,13 +100,14 @@ logger = logging.getLogger(__name__)
|
||||
# CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
GOTELEGRAM_VERSION = "2.4.0"
|
||||
GOTELEGRAM_VERSION = "2.4.2"
|
||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||
TELEMT_SERVICE = "telemt"
|
||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
||||
INSTALL_SH = "/opt/gotelegram/install.sh"
|
||||
|
||||
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
||||
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
||||
@@ -239,6 +240,58 @@ async def sh(*args, timeout: int = 60) -> Tuple[int, str, str]:
|
||||
return (-1, "", str(e))
|
||||
|
||||
|
||||
async def run_bot_action(action: str, timeout: int = 300, **params) -> Dict:
|
||||
"""Invoke install.sh --action=X --json and parse the JSON result.
|
||||
|
||||
Args:
|
||||
action: action name (e.g. "change-template", "change-lite-domain")
|
||||
timeout: seconds to wait for completion (long ops: template download can take time)
|
||||
**params: arbitrary key→value pairs, each passed as --key=value
|
||||
|
||||
Returns:
|
||||
dict with at least {"status": "success|error", "message": "..."}.
|
||||
Transport errors are mapped to {"status":"error","message":..., "code":"transport"}
|
||||
"""
|
||||
cmd = ["bash", INSTALL_SH, f"--action={action}", "--json"]
|
||||
for k, v in params.items():
|
||||
if v is None:
|
||||
continue
|
||||
cmd.append(f"--{k.replace('_', '-')}={v}")
|
||||
|
||||
code, stdout, stderr = await sh(*cmd, timeout=timeout)
|
||||
stdout = (stdout or "").strip()
|
||||
|
||||
# install.sh may print multiple log lines to stderr; the JSON is on stdout.
|
||||
# Pick the last non-empty line that looks like JSON (robust to any stray output).
|
||||
json_line = None
|
||||
for line in reversed(stdout.splitlines()):
|
||||
line = line.strip()
|
||||
if line.startswith("{") and line.endswith("}"):
|
||||
json_line = line
|
||||
break
|
||||
|
||||
if json_line:
|
||||
try:
|
||||
data = json.loads(json_line)
|
||||
if isinstance(data, dict) and "status" in data:
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"run_bot_action: JSON parse failed: {e} | line={json_line!r}")
|
||||
|
||||
# No JSON from install.sh — synthesize an error result
|
||||
tail = (stderr or "")[-300:] if stderr else ""
|
||||
logger.error(
|
||||
f"run_bot_action({action}): no JSON output, rc={code}, "
|
||||
f"stdout={stdout[-300:]!r}, stderr={tail!r}"
|
||||
)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "install.sh did not return a JSON result",
|
||||
"code": "transport",
|
||||
"rc": str(code),
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: str) -> Optional[Dict]:
|
||||
"""Load JSON file."""
|
||||
try:
|
||||
@@ -808,16 +861,13 @@ async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
|
||||
|
||||
async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Lite domain selection callback — safe stub.
|
||||
"""Lite domain selection callback — real implementation (v2.4.2+).
|
||||
|
||||
Historic bug (v2.4.1): this callback used to overwrite
|
||||
/opt/gotelegram/config.json with a minimal fake config (no secret, no
|
||||
mask_host, no engine), silently corrupting the live proxy and showing a
|
||||
fake "✅ Lite mode installed!" — while never actually invoking install.sh.
|
||||
|
||||
Installing/changing modes non-interactively from the bot is not yet wired
|
||||
up. Until it is, refuse cleanly and route the user to the CLI instead of
|
||||
damaging the working configuration.
|
||||
Branches on current mode:
|
||||
* lite mode (active): invoke `install.sh --action=change-lite-domain`
|
||||
which regenerates the telemt TOML with a new fake-TLS mask domain and
|
||||
restarts the service. Preserves secret/port.
|
||||
* any other mode: route to CLI. Fresh Lite install is interactive.
|
||||
"""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
@@ -830,14 +880,52 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
await query.answer()
|
||||
|
||||
text = (
|
||||
"<b>⚠️ Установка из бота пока не поддерживается</b>\n\n"
|
||||
f"Выбранный домен: <code>{html.escape(domain)}</code>\n\n"
|
||||
"Чтобы установить или переключить режим Lite, запустите на сервере:\n"
|
||||
"<code>gotelegram</code>\n\n"
|
||||
"Затем: <b>1) Прокси → 1) Установить/Обновить → Lite</b>.\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>."
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
current_mode = config.get("mode", "")
|
||||
|
||||
if current_mode != "lite":
|
||||
text = (
|
||||
"<b>⚠️ Установка Lite из бота пока не поддерживается</b>\n\n"
|
||||
f"Выбранный домен: <code>{html.escape(domain)}</code>\n\n"
|
||||
"Чтобы установить Lite, запустите на сервере:\n"
|
||||
"<code>gotelegram</code> → <b>1) Прокси → 1) Установить/Обновить → Lite</b>\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>."
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||
return
|
||||
|
||||
# Lite active — switch fake-TLS mask domain in place
|
||||
progress_text = (
|
||||
"<b>⏳ Меняю маскировочный домен...</b>\n\n"
|
||||
f"Новый домен: <code>{html.escape(domain)}</code>\n\n"
|
||||
"Перегенерирую конфиг telemt и перезапускаю сервис."
|
||||
)
|
||||
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
||||
|
||||
result = await run_bot_action("change-lite-domain", timeout=30, domain=domain)
|
||||
|
||||
if result.get("status") == "success":
|
||||
text = (
|
||||
"<b>✅ Маскировочный домен обновлён</b>\n\n"
|
||||
f"Новый домен: <code>{html.escape(domain)}</code>\n\n"
|
||||
"telemt перезапущен. <b>Важно:</b> старые ссылки подключения больше "
|
||||
"не будут работать — нужно заново раздать новые."
|
||||
)
|
||||
else:
|
||||
err_msg = result.get("message", "unknown error")
|
||||
err_code = result.get("code", "")
|
||||
text = (
|
||||
"<b>❌ Не удалось сменить домен</b>\n\n"
|
||||
f"Домен: <code>{html.escape(domain)}</code>\n"
|
||||
f"Причина: <code>{html.escape(err_msg)}</code>"
|
||||
+ (f" (<code>{html.escape(err_code)}</code>)" if err_code else "")
|
||||
+ "\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>."
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
@@ -1111,19 +1199,19 @@ async def cb_pro_template(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
|
||||
async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Confirm Pro template selection — safe stub.
|
||||
"""Confirm Pro template selection — real implementation (v2.4.2+).
|
||||
|
||||
Historic bug (v2.4.1): this callback used to overwrite
|
||||
/opt/gotelegram/config.json with a fake {"mode":"pro","template":...,
|
||||
"port":443} blob — no secret, no domain, no mask_host — silently
|
||||
corrupting the live proxy so link generation and status broke. At the
|
||||
same time the user saw "✅ Pro mode installed!" although install.sh was
|
||||
never invoked, no template was actually downloaded, nginx was not
|
||||
reconfigured and certbot was not touched.
|
||||
Branches on current mode:
|
||||
* pro mode (active deployment): invoke `install.sh --action=change-template`
|
||||
which downloads the new template and redeploys it to nginx. Reuses the
|
||||
existing domain + SSL cert.
|
||||
* any other mode (or no install at all): route to CLI. Fresh Pro install
|
||||
still requires interactive flow (domain, email, DNS check) — not safe
|
||||
to run headless from the bot.
|
||||
|
||||
Non-interactive Pro install/change from the bot is not yet implemented.
|
||||
Until it is, refuse cleanly and route the user to the CLI instead of
|
||||
damaging the working configuration.
|
||||
Historic context: v2.4.1 stub used to overwrite config.json with a fake
|
||||
blob; that was replaced with a safe message in v2.4.1 hotfix; now in
|
||||
v2.4.2 we wire the real change-template path through install.sh.
|
||||
"""
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
@@ -1131,19 +1219,63 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
|
||||
await query.answer()
|
||||
|
||||
text = (
|
||||
"<b>⚠️ Установка шаблона из бота пока не поддерживается</b>\n\n"
|
||||
f"Выбранный шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
||||
"Чтобы установить или сменить шаблон, запустите на сервере:\n"
|
||||
"<code>gotelegram</code>\n\n"
|
||||
"Затем: <b>1) Прокси → 1) Установить/Обновить → Pro</b> "
|
||||
"(или <b>7) Сменить режим/шаблон</b> для смены текущего).\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>."
|
||||
# Read current config to decide: in-place change-template vs fresh install
|
||||
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||
current_mode = config.get("mode", "")
|
||||
|
||||
if current_mode != "pro":
|
||||
# Fresh install / mode switch — still routes to CLI (needs domain, SSL)
|
||||
text = (
|
||||
"<b>⚠️ Установка Pro из бота пока не поддерживается</b>\n\n"
|
||||
f"Выбранный шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
||||
"Pro-режим требует ввода домена, email и проверки DNS. "
|
||||
"Чтобы установить Pro, запустите на сервере:\n"
|
||||
"<code>gotelegram</code> → <b>1) Прокси → 1) Установить/Обновить → Pro</b>\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>."
|
||||
)
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||
return
|
||||
|
||||
# Pro mode is active — perform change-template in place
|
||||
progress_text = (
|
||||
"<b>⏳ Меняю шаблон сайта...</b>\n\n"
|
||||
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
||||
"Скачиваю репозиторий и разворачиваю в nginx. "
|
||||
"Это может занять 30–90 секунд."
|
||||
)
|
||||
await safe_edit_message(query, progress_text, parse_mode="HTML")
|
||||
|
||||
# Template download + git clone can be slow — generous timeout
|
||||
result = await run_bot_action("change-template", timeout=180, template=tpl_id)
|
||||
|
||||
if result.get("status") == "success":
|
||||
domain = result.get("domain", config.get("domain", ""))
|
||||
text = (
|
||||
"<b>✅ Шаблон обновлён</b>\n\n"
|
||||
f"Новый шаблон: <code>{html.escape(tpl_id)}</code>\n"
|
||||
f"Сайт: <a href=\"https://{html.escape(domain, quote=True)}\">https://{html.escape(domain)}</a>\n\n"
|
||||
"Прокси продолжает работать без перерыва."
|
||||
)
|
||||
else:
|
||||
err_msg = result.get("message", "unknown error")
|
||||
err_code = result.get("code", "")
|
||||
text = (
|
||||
"<b>❌ Не удалось сменить шаблон</b>\n\n"
|
||||
f"Шаблон: <code>{html.escape(tpl_id)}</code>\n"
|
||||
f"Причина: <code>{html.escape(err_msg)}</code>"
|
||||
+ (f" (<code>{html.escape(err_code)}</code>)" if err_code else "")
|
||||
+ "\n\n"
|
||||
"Существующая конфигурация <b>не была изменена</b>. "
|
||||
"Попробуйте другой шаблон или запустите <code>gotelegram</code> из консоли."
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
||||
)
|
||||
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML")
|
||||
await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML", disable_web_page_preview=True)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
246
install.sh
246
install.sh
@@ -1136,11 +1136,257 @@ menu_language() {
|
||||
esac
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Non-interactive action dispatcher (bot / CI / scripting interface)
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# Usage examples:
|
||||
# gotelegram --action=change-template --template=th_ariclaw --json
|
||||
# gotelegram --action=change-lite-domain --domain=google.com --json
|
||||
#
|
||||
# Rules for action handlers:
|
||||
# - Only JSON may be written to stdout (the caller parses it).
|
||||
# - All human-oriented logging must go to stderr (log_* already do that).
|
||||
# - Exit code 0 on success, non-zero on failure (caller still parses JSON).
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bot_emit_json() {
|
||||
# bot_emit_json <status> <message> [key=value ...]
|
||||
local status="$1"; shift
|
||||
local message="$1"; shift
|
||||
local extra="" kv k v
|
||||
for kv in "$@"; do
|
||||
k="${kv%%=*}"
|
||||
v="${kv#*=}"
|
||||
# escape backslashes and double quotes in value
|
||||
v="${v//\\/\\\\}"
|
||||
v="${v//\"/\\\"}"
|
||||
extra="${extra},\"${k}\":\"${v}\""
|
||||
done
|
||||
# escape message
|
||||
local msg_esc="${message//\\/\\\\}"
|
||||
msg_esc="${msg_esc//\"/\\\"}"
|
||||
printf '{"status":"%s","message":"%s"%s}\n' "$status" "$msg_esc" "$extra"
|
||||
}
|
||||
|
||||
# Update a single key in config.json without rewriting the whole file
|
||||
bot_update_config_field() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if [ ! -f "$GOTELEGRAM_CONFIG" ]; then
|
||||
return 1
|
||||
fi
|
||||
local tmp
|
||||
tmp=$(mktemp) || return 1
|
||||
if jq --arg k "$key" --arg v "$value" \
|
||||
'.[$k] = $v | .updated_at = (now | todate)' \
|
||||
"$GOTELEGRAM_CONFIG" > "$tmp" 2>/dev/null; then
|
||||
mv "$tmp" "$GOTELEGRAM_CONFIG"
|
||||
chmod 600 "$GOTELEGRAM_CONFIG"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Action: change-template (pro mode only) ──────────────────────────────────
|
||||
bot_action_change_template() {
|
||||
local tpl_id="$1"
|
||||
local json_out="${2:-0}"
|
||||
|
||||
if [ -z "$tpl_id" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "template id is required" "code=missing_arg"
|
||||
log_error "change-template: --template is required"
|
||||
return 2
|
||||
fi
|
||||
|
||||
# Must be in pro mode
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null || echo "")
|
||||
if [ "$mode" != "pro" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "change-template requires pro mode (current: ${mode:-none})" "code=wrong_mode"
|
||||
log_error "change-template: current mode is '${mode:-none}', requires 'pro'"
|
||||
return 3
|
||||
fi
|
||||
|
||||
# Validate template id exists in catalog
|
||||
if ! get_template_info "$tpl_id" >/dev/null 2>&1; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "unknown template: $tpl_id" "code=unknown_template"
|
||||
log_error "change-template: template not found in catalog: $tpl_id"
|
||||
return 4
|
||||
fi
|
||||
|
||||
# Make sure git (and other deps) are present. download_template uses git
|
||||
# clone under the hood — on a minimal host (bootstrap-only install) git may
|
||||
# not be installed yet, and the clone would fail silently.
|
||||
ensure_deps >&2
|
||||
|
||||
log_info "change-template: downloading $tpl_id..."
|
||||
local template_dir
|
||||
template_dir=$(download_template "$tpl_id")
|
||||
if [ $? -ne 0 ] || [ -z "$template_dir" ] || [ ! -d "$template_dir" ] || [ ! -f "$template_dir/index.html" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "download failed for $tpl_id" "code=download_failed"
|
||||
log_error "change-template: download_template failed for $tpl_id"
|
||||
return 5
|
||||
fi
|
||||
|
||||
log_info "change-template: deploying to nginx..."
|
||||
if ! deploy_template_to_nginx "$template_dir" >&2; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "deploy failed" "code=deploy_failed"
|
||||
return 6
|
||||
fi
|
||||
|
||||
# Reload nginx (no full restart needed for static files — but be safe)
|
||||
systemctl reload nginx 2>/dev/null || systemctl restart nginx 2>/dev/null
|
||||
|
||||
# Update config.json template_id field
|
||||
bot_update_config_field "template_id" "$tpl_id" || \
|
||||
log_warning "change-template: could not update config.json template_id"
|
||||
|
||||
local domain
|
||||
domain=$(config_get domain 2>/dev/null || echo "")
|
||||
log_success "change-template: $tpl_id deployed"
|
||||
|
||||
if [ "$json_out" = "1" ]; then
|
||||
bot_emit_json "success" "template changed to $tpl_id" \
|
||||
"template=$tpl_id" "domain=$domain" "mode=pro"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Action: change-lite-domain ───────────────────────────────────────────────
|
||||
# Regenerates telemt TOML with a new fake-TLS mask domain. Lite mode only.
|
||||
bot_action_change_lite_domain() {
|
||||
local new_domain="$1"
|
||||
local json_out="${2:-0}"
|
||||
|
||||
if [ -z "$new_domain" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "domain is required" "code=missing_arg"
|
||||
log_error "change-lite-domain: --domain is required"
|
||||
return 2
|
||||
fi
|
||||
|
||||
if ! validate_domain "$new_domain" 2>/dev/null; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "invalid domain: $new_domain" "code=invalid_domain"
|
||||
log_error "change-lite-domain: invalid domain: $new_domain"
|
||||
return 3
|
||||
fi
|
||||
|
||||
local mode
|
||||
mode=$(config_get mode 2>/dev/null || echo "")
|
||||
if [ "$mode" != "lite" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "change-lite-domain requires lite mode (current: ${mode:-none})" "code=wrong_mode"
|
||||
log_error "change-lite-domain: current mode is '${mode:-none}', requires 'lite'"
|
||||
return 4
|
||||
fi
|
||||
|
||||
local secret port
|
||||
secret=$(get_config_value secret 2>/dev/null || echo "")
|
||||
port=$(get_config_value port 2>/dev/null || echo "443")
|
||||
|
||||
if [ -z "$secret" ]; then
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "no secret in config" "code=no_secret"
|
||||
log_error "change-lite-domain: no secret in config.json"
|
||||
return 5
|
||||
fi
|
||||
|
||||
log_info "change-lite-domain: regenerating telemt TOML..."
|
||||
generate_telemt_toml "$secret" "$port" "lite" "$new_domain" "443" >&2 || {
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "config generation failed" "code=gen_failed"
|
||||
return 6
|
||||
}
|
||||
|
||||
validate_telemt_config >&2 || {
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "config validation failed" "code=validate_failed"
|
||||
return 7
|
||||
}
|
||||
|
||||
restart_telemt >&2 || {
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "telemt restart failed" "code=restart_failed"
|
||||
return 8
|
||||
}
|
||||
|
||||
# Update both domain and mask_host fields in config.json
|
||||
bot_update_config_field "mask_host" "$new_domain" || \
|
||||
log_warning "change-lite-domain: could not update mask_host"
|
||||
bot_update_config_field "domain" "$new_domain" || \
|
||||
log_warning "change-lite-domain: could not update domain"
|
||||
|
||||
log_success "change-lite-domain: switched to $new_domain"
|
||||
|
||||
if [ "$json_out" = "1" ]; then
|
||||
bot_emit_json "success" "lite mask domain changed to $new_domain" \
|
||||
"domain=$new_domain" "mode=lite" "port=$port"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main dispatcher — called from main() when --action=X is present
|
||||
bot_action_dispatch() {
|
||||
local action="" tpl_id="" domain="" json_out=0 arg
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--action=*) action="${arg#--action=}" ;;
|
||||
--template=*) tpl_id="${arg#--template=}" ;;
|
||||
--domain=*) domain="${arg#--domain=}" ;;
|
||||
--json) json_out=1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$action" in
|
||||
change-template)
|
||||
bot_action_change_template "$tpl_id" "$json_out"
|
||||
return $?
|
||||
;;
|
||||
change-lite-domain)
|
||||
bot_action_change_lite_domain "$domain" "$json_out"
|
||||
return $?
|
||||
;;
|
||||
"")
|
||||
log_error "no --action specified"
|
||||
return 64
|
||||
;;
|
||||
*)
|
||||
[ "$json_out" = "1" ] && bot_emit_json "error" "unknown action: $action" "code=unknown_action"
|
||||
log_error "unknown action: $action"
|
||||
return 64
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Точка входа / Entry point ───────────────────────────────────────────────
|
||||
main() {
|
||||
# Non-interactive action mode: if --action=X is in args, dispatch and exit.
|
||||
# Must run BEFORE interactive banner/menus so the bot gets clean JSON.
|
||||
local a has_action=0
|
||||
for a in "$@"; do
|
||||
case "$a" in --action=*) has_action=1; break ;; esac
|
||||
done
|
||||
if [ "$has_action" = "1" ]; then
|
||||
check_root
|
||||
init_dirs
|
||||
# Для bot-экшенов тоже нужны зависимости (git для change-template), но
|
||||
# без шумного apt-get update если всё уже на месте.
|
||||
if ! check_deps_present; then
|
||||
ensure_deps >&2 || exit 1
|
||||
fi
|
||||
bot_action_dispatch "$@"
|
||||
exit $?
|
||||
fi
|
||||
|
||||
check_root
|
||||
init_dirs
|
||||
|
||||
# Первый запуск: если критические зависимости отсутствуют — ставим их ДО
|
||||
# того как пользователь дойдёт до меню. На последующих запусках это просто
|
||||
# дёшево проверяет command -v по всем командам и ничего не делает.
|
||||
if ! check_deps_present; then
|
||||
log_step "Первый запуск: проверяю зависимости..."
|
||||
ensure_deps || {
|
||||
log_error "Не удалось установить зависимости. См. сообщения выше."
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
# First-run language picker (before banner so banner appears in chosen lang)
|
||||
first_run_language_picker
|
||||
|
||||
|
||||
156
lib/common.sh
Normal file → Executable file
156
lib/common.sh
Normal file → Executable file
@@ -3,7 +3,7 @@
|
||||
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||
|
||||
# ── Version ───────────────────────────────────────────────────────────────────
|
||||
GOTELEGRAM_VERSION="2.4.1"
|
||||
GOTELEGRAM_VERSION="2.4.2"
|
||||
GOTELEGRAM_NAME="GoTelegram"
|
||||
|
||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||
@@ -247,25 +247,149 @@ install_pkg() {
|
||||
esac
|
||||
}
|
||||
|
||||
# ── Зависимости GoTelegram ──────────────────────────────────────────────────
|
||||
# Полный список внешних команд, которые скрипт использует. Для каждой команды
|
||||
# указан пакет на apt и dnf/yum (имена различаются: например dig = dnsutils на
|
||||
# Debian, bind-utils на RHEL).
|
||||
#
|
||||
# КРИТИЧЕСКИЕ (без них скрипт просто не работает):
|
||||
# jq — парсинг config.json, templates_catalog.json
|
||||
# curl — скачивание telemt и проверки HTTPS
|
||||
# openssl — генерация секретов, шифрование бекапов, SSL проверка
|
||||
# git — клонирование шаблонов через download_template
|
||||
# xxd — hex-encode домена для fake-TLS секрета (ee-prefix)
|
||||
# tar — распаковка telemt архива и бекапы
|
||||
# dig — DNS-проверка домена в Pro-режиме
|
||||
#
|
||||
# ЖЕЛАТЕЛЬНЫЕ (есть fallback, но с ними лучше):
|
||||
# qrencode — QR-коды для прокси-ссылок
|
||||
# bc — красивое форматирование чисел в статистике
|
||||
#
|
||||
# Pro-режим доустанавливает nginx/certbot через install_nginx/install_certbot
|
||||
# (они большие и нужны только если пользователь выбрал Pro).
|
||||
|
||||
# Маппинг команды -> (apt_pkg, dnf_pkg). apt_pkg_for_cmd <cmd>
|
||||
apt_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig) echo "dnsutils" ;;
|
||||
xxd) echo "xxd" ;; # Ubuntu 22+: отдельный пакет, fallback ниже
|
||||
nslookup) echo "dnsutils" ;;
|
||||
host) echo "dnsutils" ;;
|
||||
ss) echo "iproute2" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
*) echo "$1" ;; # команда == имя пакета
|
||||
esac
|
||||
}
|
||||
|
||||
dnf_pkg_for_cmd() {
|
||||
case "$1" in
|
||||
dig|nslookup|host) echo "bind-utils" ;;
|
||||
xxd) echo "vim-common" ;;
|
||||
ss) echo "iproute" ;;
|
||||
netstat) echo "net-tools" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_deps() {
|
||||
local missing=()
|
||||
for cmd in curl jq openssl git qrencode; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
# Критические зависимости — без них скрипт не работает
|
||||
local critical=(curl jq openssl git xxd tar dig)
|
||||
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||
local optional=(qrencode bc)
|
||||
|
||||
local missing_critical=() missing_optional=() cmd
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
if type tf &>/dev/null; then
|
||||
log_step "$(tf deps_installing "${missing[*]}")"
|
||||
else
|
||||
log_step "Installing dependencies: ${missing[*]}"
|
||||
fi
|
||||
case "$(get_pkg_manager)" in
|
||||
apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;;
|
||||
dnf) dnf install -y -q "${missing[@]}" ;;
|
||||
yum) yum install -y -q "${missing[@]}" ;;
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
|
||||
done
|
||||
|
||||
local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
|
||||
[ ${#all_missing[@]} -eq 0 ] && return 0
|
||||
|
||||
# Собираем список пакетов для выбранного менеджера
|
||||
local pkg_mgr pkg pkgs=()
|
||||
pkg_mgr=$(get_pkg_manager)
|
||||
|
||||
for cmd in "${all_missing[@]}"; do
|
||||
case "$pkg_mgr" in
|
||||
apt) pkg=$(apt_pkg_for_cmd "$cmd") ;;
|
||||
dnf|yum) pkg=$(dnf_pkg_for_cmd "$cmd") ;;
|
||||
*) pkg="$cmd" ;;
|
||||
esac
|
||||
pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
# Убираем дубликаты (например dig+nslookup оба = dnsutils)
|
||||
local uniq_pkgs=()
|
||||
for pkg in "${pkgs[@]}"; do
|
||||
local found=0 p
|
||||
for p in "${uniq_pkgs[@]}"; do
|
||||
[ "$p" = "$pkg" ] && { found=1; break; }
|
||||
done
|
||||
[ "$found" = "0" ] && uniq_pkgs+=("$pkg")
|
||||
done
|
||||
|
||||
if type tf &>/dev/null; then
|
||||
log_step "$(tf deps_installing "${all_missing[*]}")"
|
||||
else
|
||||
log_step "Installing dependencies: ${all_missing[*]} (packages: ${uniq_pkgs[*]})"
|
||||
fi
|
||||
|
||||
case "$pkg_mgr" in
|
||||
apt)
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq 2>/dev/null
|
||||
apt-get install -y -qq "${uniq_pkgs[@]}" 2>/dev/null
|
||||
;;
|
||||
dnf) dnf install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
yum) yum install -y -q "${uniq_pkgs[@]}" 2>/dev/null ;;
|
||||
*)
|
||||
log_error "Unknown package manager — install manually: ${uniq_pkgs[*]}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Фолбэки для xxd: на некоторых системах нужен vim-common вместо xxd
|
||||
if ! command -v xxd &>/dev/null && [ "$pkg_mgr" = "apt" ]; then
|
||||
apt-get install -y -qq vim-common 2>/dev/null
|
||||
fi
|
||||
|
||||
# Повторная проверка критических команд
|
||||
local still_missing=()
|
||||
for cmd in "${critical[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing+=("$cmd")
|
||||
done
|
||||
|
||||
if [ ${#still_missing[@]} -gt 0 ]; then
|
||||
log_error "Critical dependencies still missing: ${still_missing[*]}"
|
||||
log_error "Install manually and re-run gotelegram"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Опциональные — только предупреждение
|
||||
local still_missing_opt=()
|
||||
for cmd in "${optional[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || still_missing_opt+=("$cmd")
|
||||
done
|
||||
if [ ${#still_missing_opt[@]} -gt 0 ]; then
|
||||
log_warning "Optional deps missing (features degraded): ${still_missing_opt[*]}"
|
||||
fi
|
||||
|
||||
log_success "Dependencies ready"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Быстрая проверка — только смотрит что критические установлены, ничего не ставит.
|
||||
# Возвращает 0 если всё ок, 1 если что-то отсутствует. Используется на старте
|
||||
# main() чтобы не дёргать apt-get update при каждом запуске меню.
|
||||
check_deps_present() {
|
||||
local cmd
|
||||
for cmd in curl jq openssl git xxd tar dig; do
|
||||
command -v "$cmd" &>/dev/null || return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
check_port() {
|
||||
|
||||
Reference in New Issue
Block a user