mirror of
https://github.com/anten-ka/gotelegram_pro.git
synced 2026-05-19 14:36:05 +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
|
# CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
GOTELEGRAM_VERSION = "2.4.0"
|
GOTELEGRAM_VERSION = "2.4.2"
|
||||||
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
GOTELEGRAM_CONFIG = "/opt/gotelegram/config.json"
|
||||||
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
TELEMT_CONFIG = "/etc/telemt/config.toml"
|
||||||
TELEMT_SERVICE = "telemt"
|
TELEMT_SERVICE = "telemt"
|
||||||
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
WEBSITE_ROOT = "/var/www/gotelegram-site"
|
||||||
BACKUP_DIR = "/opt/gotelegram/backups"
|
BACKUP_DIR = "/opt/gotelegram/backups"
|
||||||
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
TEMPLATES_CATALOG = "/opt/gotelegram/templates_catalog.json"
|
||||||
|
INSTALL_SH = "/opt/gotelegram/install.sh"
|
||||||
|
|
||||||
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
PROMO_LINK_1 = "https://vk.cc/ct29NQ"
|
||||||
PROMO_LINK_2 = "https://vk.cc/cUxAhj"
|
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))
|
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]:
|
def load_json(path: str) -> Optional[Dict]:
|
||||||
"""Load JSON file."""
|
"""Load JSON file."""
|
||||||
try:
|
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:
|
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
|
Branches on current mode:
|
||||||
/opt/gotelegram/config.json with a minimal fake config (no secret, no
|
* lite mode (active): invoke `install.sh --action=change-lite-domain`
|
||||||
mask_host, no engine), silently corrupting the live proxy and showing a
|
which regenerates the telemt TOML with a new fake-TLS mask domain and
|
||||||
fake "✅ Lite mode installed!" — while never actually invoking install.sh.
|
restarts the service. Preserves secret/port.
|
||||||
|
* any other mode: route to CLI. Fresh Lite install is interactive.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
@@ -830,14 +880,52 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
text = (
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
"<b>⚠️ Установка из бота пока не поддерживается</b>\n\n"
|
current_mode = config.get("mode", "")
|
||||||
f"Выбранный домен: <code>{html.escape(domain)}</code>\n\n"
|
|
||||||
"Чтобы установить или переключить режим Lite, запустите на сервере:\n"
|
if current_mode != "lite":
|
||||||
"<code>gotelegram</code>\n\n"
|
text = (
|
||||||
"Затем: <b>1) Прокси → 1) Установить/Обновить → Lite</b>.\n\n"
|
"<b>⚠️ Установка Lite из бота пока не поддерживается</b>\n\n"
|
||||||
"Существующая конфигурация <b>не была изменена</b>."
|
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(
|
keyboard = InlineKeyboardMarkup(
|
||||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
[[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:
|
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
|
Branches on current mode:
|
||||||
/opt/gotelegram/config.json with a fake {"mode":"pro","template":...,
|
* pro mode (active deployment): invoke `install.sh --action=change-template`
|
||||||
"port":443} blob — no secret, no domain, no mask_host — silently
|
which downloads the new template and redeploys it to nginx. Reuses the
|
||||||
corrupting the live proxy so link generation and status broke. At the
|
existing domain + SSL cert.
|
||||||
same time the user saw "✅ Pro mode installed!" although install.sh was
|
* any other mode (or no install at all): route to CLI. Fresh Pro install
|
||||||
never invoked, no template was actually downloaded, nginx was not
|
still requires interactive flow (domain, email, DNS check) — not safe
|
||||||
reconfigured and certbot was not touched.
|
to run headless from the bot.
|
||||||
|
|
||||||
Non-interactive Pro install/change from the bot is not yet implemented.
|
Historic context: v2.4.1 stub used to overwrite config.json with a fake
|
||||||
Until it is, refuse cleanly and route the user to the CLI instead of
|
blob; that was replaced with a safe message in v2.4.1 hotfix; now in
|
||||||
damaging the working configuration.
|
v2.4.2 we wire the real change-template path through install.sh.
|
||||||
"""
|
"""
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
data = query.data
|
data = query.data
|
||||||
@@ -1131,19 +1219,63 @@ async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
|||||||
|
|
||||||
await query.answer()
|
await query.answer()
|
||||||
|
|
||||||
text = (
|
# Read current config to decide: in-place change-template vs fresh install
|
||||||
"<b>⚠️ Установка шаблона из бота пока не поддерживается</b>\n\n"
|
config = load_json(GOTELEGRAM_CONFIG) or {}
|
||||||
f"Выбранный шаблон: <code>{html.escape(tpl_id)}</code>\n\n"
|
current_mode = config.get("mode", "")
|
||||||
"Чтобы установить или сменить шаблон, запустите на сервере:\n"
|
|
||||||
"<code>gotelegram</code>\n\n"
|
if current_mode != "pro":
|
||||||
"Затем: <b>1) Прокси → 1) Установить/Обновить → Pro</b> "
|
# Fresh install / mode switch — still routes to CLI (needs domain, SSL)
|
||||||
"(или <b>7) Сменить режим/шаблон</b> для смены текущего).\n\n"
|
text = (
|
||||||
"Существующая конфигурация <b>не была изменена</b>."
|
"<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(
|
keyboard = InlineKeyboardMarkup(
|
||||||
[[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]]
|
[[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
|
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 ───────────────────────────────────────────────
|
# ── Точка входа / Entry point ───────────────────────────────────────────────
|
||||||
main() {
|
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
|
check_root
|
||||||
init_dirs
|
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 (before banner so banner appears in chosen lang)
|
||||||
first_run_language_picker
|
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
|
# Colors, logging, spinner, system helpers, v1 compat, i18n-aware
|
||||||
|
|
||||||
# ── Version ───────────────────────────────────────────────────────────────────
|
# ── Version ───────────────────────────────────────────────────────────────────
|
||||||
GOTELEGRAM_VERSION="2.4.1"
|
GOTELEGRAM_VERSION="2.4.2"
|
||||||
GOTELEGRAM_NAME="GoTelegram"
|
GOTELEGRAM_NAME="GoTelegram"
|
||||||
|
|
||||||
# ── Пути ──────────────────────────────────────────────────────────────────────
|
# ── Пути ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -247,25 +247,149 @@ install_pkg() {
|
|||||||
esac
|
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() {
|
ensure_deps() {
|
||||||
local missing=()
|
# Критические зависимости — без них скрипт не работает
|
||||||
for cmd in curl jq openssl git qrencode; do
|
local critical=(curl jq openssl git xxd tar dig)
|
||||||
if ! command -v "$cmd" &>/dev/null; then
|
# Желательные — есть fallback, устанавливать всё равно, но не падать если не смогли
|
||||||
missing+=("$cmd")
|
local optional=(qrencode bc)
|
||||||
fi
|
|
||||||
|
local missing_critical=() missing_optional=() cmd
|
||||||
|
for cmd in "${critical[@]}"; do
|
||||||
|
command -v "$cmd" &>/dev/null || missing_critical+=("$cmd")
|
||||||
done
|
done
|
||||||
if [ ${#missing[@]} -gt 0 ]; then
|
for cmd in "${optional[@]}"; do
|
||||||
if type tf &>/dev/null; then
|
command -v "$cmd" &>/dev/null || missing_optional+=("$cmd")
|
||||||
log_step "$(tf deps_installing "${missing[*]}")"
|
done
|
||||||
else
|
|
||||||
log_step "Installing dependencies: ${missing[*]}"
|
local all_missing=("${missing_critical[@]}" "${missing_optional[@]}")
|
||||||
fi
|
[ ${#all_missing[@]} -eq 0 ] && return 0
|
||||||
case "$(get_pkg_manager)" in
|
|
||||||
apt) apt-get update -qq && apt-get install -y -qq "${missing[@]}" ;;
|
# Собираем список пакетов для выбранного менеджера
|
||||||
dnf) dnf install -y -q "${missing[@]}" ;;
|
local pkg_mgr pkg pkgs=()
|
||||||
yum) yum install -y -q "${missing[@]}" ;;
|
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
|
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
|
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() {
|
check_port() {
|
||||||
|
|||||||
Reference in New Issue
Block a user