Telemax/setup.sh
Folist 03af95d13f setup.sh: неудачная проверка Telegram больше не роняет установку
Раньше при недоступности Telegram скрипт делал exit 1 — но частая причина
это просто опечатка в адресе прокси, из-за которой глупо выкидывать из
установщика. Теперь:

- MAX недоступен → по-прежнему фатально (там прокси нет, чинить нечего —
  это блокировка окружения).
- Telegram недоступен → цикл: вписать прокси заново и перепроверить, либо
  пустой Enter (перепроверить напрямую), либо skip (продолжить без проверки,
  задать прокси позже в панели). Если прокси не вводили, а напрямую не
  достучались — подсказываем, что помогает прокси (частый случай в РФ).

Проверено на живых прокси: верный → OK, битый порт/пароль → перезапрос, skip
не оставляет тупика; под set -euo pipefail без падений.
2026-08-16 23:04:26 +03:00

434 lines
22 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Interactive first-time setup for Telemax. Generates the two secret keys
# automatically, asks only for the two things nobody but you can provide
# (the Telegram bot token and target group id), writes .env, and offers to
# build + start the bridge right away.
set -euo pipefail
cd "$(dirname "$0")"
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
# Waits until the container's HTTPS panel answers /health. MAX connects a moment
# after the container starts, so /auth/* would 503 if we asked too early. Expects
# API_URL set. Panel uses a self-signed cert generated on first start — hence -k.
wait_for_server() {
echo "Жду готовности сервера..."
for attempt in 1 2 3 4 5 6 7 8 9 10; do
curl -sk --max-time 2 "$API_URL/health" >/dev/null 2>&1 && break
sleep 2
done
}
# Interactive MAX auth (phone -> SMS -> optional cloud password) against the running
# container's /api/auth/*, the same endpoints the web panel uses. Runs both on a
# fresh install and on the re-auth path below (an already-configured install where
# auth was never finished). Expects API_URL, API_KEY, HOST, PORT set; sets AUTHED=1
# on success.
run_max_auth() {
echo
echo "════════════════════════════════════════════════════════════════"
bold " ШАГ АВТОРИЗАЦИИ MAX — БЕЗ НЕГО МОСТ НЕ ЗАРАБОТАЕТ"
echo "════════════════════════════════════════════════════════════════"
echo "Введите номер MAX и код из SMS. Если пропустить (просто Enter) —"
echo "контейнер останется запущенным, но НЕ подключённым к аккаунту, пока"
echo "вы не авторизуетесь позже через веб-панель."
echo
AUTHED=""
read -rp "Номер телефона MAX (с кодом страны, напр. +79991234567), или Enter чтобы позже: " MAX_PHONE
if [ -n "$MAX_PHONE" ]; then
PHONE_OK=""
for attempt in 1 2 3; do
PHONE_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"phone\":\"$MAX_PHONE\"}" "$API_URL/auth/phone")
if echo "$PHONE_RESP" | grep -q '"success":true'; then
PHONE_OK=1
break
fi
sleep 2
done
if [ -n "$PHONE_OK" ]; then
echo "Код отправлен на $MAX_PHONE."
read -rp "Код из SMS: " MAX_CODE
VERIFY_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"code\":\"$MAX_CODE\"}" "$API_URL/auth/verify")
if echo "$VERIFY_RESP" | grep -q '"passwordRequired":true'; then
# Some MAX accounts have a password set as a second factor on top of SMS.
# A wrong password can be retried freely — the auth session behind it
# doesn't expire until a correct one goes through (confirmed live 2026-08-14).
HINT=$(echo "$VERIFY_RESP" | grep -o '"hint":"[^"]*"' | sed 's/"hint":"//;s/"$//')
echo "Этот MAX-аккаунт защищён паролем (второй фактор поверх SMS)."
[ -n "$HINT" ] && echo "Подсказка: $HINT"
PASSWORD_OK=""
while [ -z "$PASSWORD_OK" ]; do
read -rsp "Пароль: " MAX_PASSWORD
echo
PASSWORD_RESP=$(curl -sk -X POST -H "x-api-key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"password\":\"$MAX_PASSWORD\"}" "$API_URL/auth/password")
if echo "$PASSWORD_RESP" | grep -q '"success":true'; then
PASSWORD_OK=1
AUTHED=1
bold "Готово — мост авторизован и подключён к MAX."
else
echo "❌ Неверный пароль, попробуйте ещё раз (или Ctrl+C — тогда через веб-панель: https://$HOST:$PORT)."
fi
done
elif echo "$VERIFY_RESP" | grep -q '"success":true'; then
AUTHED=1
bold "Готово — мост авторизован и подключён к MAX."
else
echo "Не удалось подтвердить код: $VERIFY_RESP"
echo "Попробуйте ещё раз через веб-панель: https://$HOST:$PORT"
fi
else
echo "Не удалось запросить SMS: $PHONE_RESP"
echo "Попробуйте через веб-панель: https://$HOST:$PORT"
fi
else
echo "Ок — авторизацию можно завершить позже через веб-панель (см. ниже)."
fi
}
# Big, unmissable closing summary — authed vs not, plus the panel URL and key.
# Expects AUTHED, HOST, PORT, API_KEY.
print_final_status() {
echo
echo "════════════════════════════════════════════════════════════════"
if [ -n "$AUTHED" ]; then
bold " ✅ ГОТОВО. Мост авторизован, запущен и подключён к MAX."
else
bold " ⚠️ МОСТ ЗАПУЩЕН, НО MAX ПОКА НЕ АВТОРИЗОВАН"
echo " Без авторизации сообщения не будут пересылаться. Завершите её:"
echo " откройте https://$HOST:$PORT, введите ключ ниже, затем номер и код из SMS."
fi
echo "════════════════════════════════════════════════════════════════"
echo " Веб-панель: https://$HOST:$PORT"
echo " Ключ входа: $API_KEY"
echo " (сертификат самоподписанный — браузер предупредит один раз:"
echo " «Дополнительно» → «Перейти на сайт». Ключ можно вернуть командой /apikey у бота.)"
echo "════════════════════════════════════════════════════════════════"
}
# Runs on every invocation, even on an already-configured install (the .env
# check below exits before the rest of setup) — an update pulled in via the
# watcher itself needs the watcher already installed to have gotten here, and
# re-running enable is harmless, so this is the one place that's safe to do
# unconditionally.
if [ "$(id -u)" = "0" ] && command -v systemctl >/dev/null 2>&1; then
REPO_DIR="$(pwd)"
# systemd services run with a stripped environment (notably a different or
# unset $HOME), so git's "dubious ownership" check can reject the repo even
# when a normal interactive `git config --global` already covers it for an
# SSH session as the same user — hit live 2026-08-14, silently broke every
# update the watcher tried to run. --system doesn't depend on $HOME at all.
git config --system --add safe.directory "$REPO_DIR" 2>/dev/null || true
cat > /etc/systemd/system/telemax-updater.service <<EOF
[Unit]
Description=Telemax update watcher (one-shot)
[Service]
Type=oneshot
WorkingDirectory=$REPO_DIR
ExecStart=$REPO_DIR/update-watcher.sh
EOF
cat > /etc/systemd/system/telemax-updater.timer <<'EOF'
[Unit]
Description=Run the Telemax update watcher every minute
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
Unit=telemax-updater.service
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now telemax-updater.timer >/dev/null 2>&1
echo "Вотчер обновлений (telemax-updater, systemd-таймер) установлен и включён в автозапуск — см. README."
else
echo "Пропускаю установку вотчера обновлений — нужны root и systemd. Обновляться придётся вручную: ./update.sh"
fi
if [ -f .env ]; then
# Already configured — but the MAX auth step might never have been finished
# (interrupted mid-flow, or only the bot/group part was done, or the account
# has a cloud password and the person bailed at that prompt). Re-running the
# installer used to just say "нечего делать" and exit, leaving the web panel as
# the only recovery path. Instead: check whether MAX is actually authorized and,
# if not, offer to finish it right here in the console.
set -a
# shellcheck disable=SC1091
. ./.env 2>/dev/null || true
set +a
PORT="${PORT:-3000}"
API_URL="https://localhost:$PORT/api"
PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me || true)
HOST="${PUBLIC_IP:-<адрес-сервера>}"
if ! command -v docker >/dev/null 2>&1; then
echo ".env уже существует, но Docker недоступен на этой машине —"
echo "запустите контейнер там, где есть Docker, и авторизуйтесь через веб-панель."
exit 0
fi
if [ -z "$(docker compose ps --status running --format '{{.Name}}' 2>/dev/null)" ]; then
read -rp ".env есть, но контейнер не запущен. Запустить сейчас? [Y/n] " RUN_NOW
if [ "${RUN_NOW:-Y}" = "n" ] || [ "${RUN_NOW:-Y}" = "N" ]; then
echo "Ок. Когда будете готовы: docker compose up -d, затем ./setup.sh или веб-панель."
exit 0
fi
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo unknown) docker compose up -d --build
fi
wait_for_server
# /api/status returns the active phone once MAX is authorized; empty until then.
STATUS=$(curl -sk --max-time 3 -H "x-api-key: ${API_KEY:-}" "$API_URL/status" 2>/dev/null || true)
if echo "$STATUS" | grep -q '"phone":"[^"]'; then
echo "✅ Уже настроено и авторизовано в MAX. Ничего делать не нужно."
echo " Веб-панель: https://$HOST:$PORT"
exit 0
fi
echo
bold "⚠️ .env есть, но MAX ещё не авторизован — шаг авторизации не завершён."
read -rp "Пройти авторизацию сейчас? [Y/n] " DO_AUTH
if [ "${DO_AUTH:-Y}" = "n" ] || [ "${DO_AUTH:-Y}" = "N" ]; then
echo "Ок — можно позже через веб-панель: https://$HOST:$PORT (ключ: /apikey у бота или grep API_KEY .env)."
exit 0
fi
run_max_auth
print_final_status
exit 0
fi
if ! command -v openssl >/dev/null 2>&1; then
echo "Не найден openssl — он нужен, чтобы сгенерировать ключи. Установите его (обычно уже есть на Ubuntu/Debian: apt install openssl) и запустите скрипт снова."
exit 1
fi
bold "=== Telemax — первая настройка ==="
echo
# A leftover container from a previous attempt (e.g. .env was deleted to redo
# setup, but the container itself was never stopped) keeps long-polling
# Telegram with its own bot token — races the getUpdates call below and
# silently breaks group auto-detection. Confirmed live 2026-08-14.
if command -v docker >/dev/null 2>&1; then
RUNNING=$(docker compose ps --status running --format '{{.Name}}' 2>/dev/null || true)
if [ -n "$RUNNING" ]; then
echo "⚠️ Уже запущен контейнер предыдущей установки: $RUNNING"
echo "Пока он работает, его бот перехватывает Telegram-обновления — автоопределение"
echo "группы ниже не найдёт сообщение."
read -rp "Остановить его сейчас? [Y/n] " STOP_OLD
if [ "${STOP_OLD:-Y}" != "n" ] && [ "${STOP_OLD:-Y}" != "N" ]; then
docker compose down
echo "Остановлено."
fi
echo
fi
fi
# Fails fast on a server whose network can't reach one of the two services this
# bridge depends on (firewall, geo-blocking, restrictive hosting policy) —
# better to say so now than after the user has typed in a bot token. Retries a
# few times first: a single attempt right after boot can spuriously fail on a
# transient blip (DNS not warmed up yet, a flaky first packet) even though the
# server is perfectly reachable a couple seconds later — confirmed live.
check_tcp() {
for attempt in 1 2 3; do
if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$1/$2" 2>/dev/null; then
return 0
fi
[ "$attempt" -lt 3 ] && sleep 2
done
return 1
}
# Starting from $1, returns the first port nothing is already listening on
# locally. Used instead of hard-failing when 3000 is taken (e.g. a leftover
# container from an earlier install attempt) — just use the next one free.
find_free_port() {
local port=$1
while (exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null; do
exec 3>&- 3<&-
port=$((port + 1))
done
echo "$port"
}
# Ask about a Telegram proxy BEFORE the reachability check: on a host where Telegram
# is reachable only through a proxy (e.g. a home server with no direct route to
# api.telegram.org), a direct check would falsely fail and abort setup. MAX always
# stays direct. The value doubles as its own validation via the check below and is
# written to .env as TELEGRAM_PROXY (the container's Telegraf client picks it up).
echo
echo "Если этот сервер выходит в Telegram только через прокси (напр. домашний"
echo "сервер, где прямой доступ к api.telegram.org закрыт) — укажите его. MAX при"
echo "этом идёт напрямую. Форматы: socks5://[логин:пароль@]хост:порт или"
echo "http://[логин:пароль@]хост:порт. Ошибётесь — проверка ниже даст поправить."
read -rp "Прокси для Telegram (Enter — без прокси): " TELEGRAM_PROXY
echo
echo "Проверяю связь с серверами MAX и Telegram..."
# MAX is fatal: it's always direct, so nothing the user could re-type fixes a blocked
# MAX — a firewall/geo-block is an environment problem, not a typo.
if check_tcp 155.212.204.150 443; then
echo " MAX (155.212.204.150:443): OK"
else
echo " MAX (155.212.204.150:443): нет связи"
echo
echo "Без связи с сервером MAX мост работать не сможет — обычно это файрвол хостинга"
echo "или гео-блокировка. Проверьте сеть сервера и запустите setup.sh снова."
exit 1
fi
# Telegram is NOT fatal: the usual cause is a mistyped proxy (or a host that needs a
# proxy at all) — both fixable right here. So loop and let the user re-enter the proxy
# and re-check instead of aborting, with an explicit "skip" escape so a genuinely
# blocked host isn't a dead end (the proxy can still be set later in the panel).
while true; do
TG_PROXY_ARGS=()
if [ -n "$TELEGRAM_PROXY" ]; then
TG_PROXY_ARGS=(--proxy "$TELEGRAM_PROXY")
if curl -sS "${TG_PROXY_ARGS[@]}" --max-time 8 -o /dev/null https://api.telegram.org 2>/dev/null; then
echo " Telegram (через прокси): OK"
break
fi
echo " Telegram через прокси недоступен — возможно, неверный адрес/логин/пароль прокси."
else
if check_tcp api.telegram.org 443; then
echo " Telegram (api.telegram.org:443): OK"
break
fi
echo " Telegram напрямую недоступен (частая причина на хостингах в РФ — блокировка; помогает прокси)."
fi
echo " • впишите прокси (socks5://… или http://…) и Enter — перепроверю через него;"
echo " • пустой Enter — перепроверить напрямую, без прокси;"
echo " • skip — продолжить установку без проверки (прокси можно задать позже в панели)."
read -rp " > " TG_INPUT
if [ "$TG_INPUT" = "skip" ]; then
echo " Пропускаю проверку Telegram. Мост поднимется, но без связи с Telegram пересылки"
echo " не будет — задайте прокси в веб-панели (Configuration → Telegram-прокси)."
break
fi
TELEGRAM_PROXY="$TG_INPUT"
done
echo
echo "Понадобится токен бота — создайте его через @BotFather (https://t.me/BotFather), команда /newbot."
echo
read -rp "Токен бота (TELEGRAM_BOT_TOKEN): " TELEGRAM_BOT_TOKEN
while [ -z "$TELEGRAM_BOT_TOKEN" ]; do
read -rp "Токен не может быть пустым, введите ещё раз: " TELEGRAM_BOT_TOKEN
done
echo
bold "Теперь группа:"
echo " 1. Создайте Telegram-группу (или возьмите существующую) и включите в ней Темы:"
echo " настройки группы → Темы → включить."
echo " 2. Добавьте бота в группу администратором — ОБЯЗАТЕЛЬНО включите ему право"
echo " «Управление темами» (Manage Topics), оно не входит в базовый набор прав."
echo " 3. Отправьте в группу любое сообщение — id вычислю сам, ничего искать не нужно."
echo
TARGET_TELEGRAM_GROUP=""
if command -v jq >/dev/null 2>&1; then
read -rp "Сделали? Нажмите Enter, когда отправите сообщение в группу... "
echo "Ищу группу..."
for attempt in 1 2 3 4 5; do
UPDATES=$(curl -s "${TG_PROXY_ARGS[@]}" "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=100" || true)
TARGET_TELEGRAM_GROUP=$(echo "$UPDATES" | jq -r '
[.result[] | (.message // .channel_post // empty)
| select(.chat.type == "supergroup" or .chat.type == "group")
| .chat.id] | last // empty' 2>/dev/null || true)
if [ -n "$TARGET_TELEGRAM_GROUP" ]; then
echo "Нашёл группу, id: $TARGET_TELEGRAM_GROUP"
break
fi
echo " Пока не вижу сообщений от бота в группе, жду 3с и пробую снова ($attempt/5)..."
sleep 3
done
else
echo "(jq не найден — автоопределение пропущено, введите id вручную)"
fi
if [ -z "$TARGET_TELEGRAM_GROUP" ]; then
echo
echo "Не нашёл автоматически. Id группы можно узнать, например, переслав любое"
echo "сообщение из неё боту @getidsbot."
read -rp "Id группы (TARGET_TELEGRAM_GROUP, вида -100...): " TARGET_TELEGRAM_GROUP
while [ -z "$TARGET_TELEGRAM_GROUP" ]; do
read -rp "Id не может быть пустым, введите ещё раз: " TARGET_TELEGRAM_GROUP
done
fi
# Best-effort — needs the bot to already be a group admin with "Change Group
# Info" rights, which setup already asked for above. setChatPhoto needs an
# actual file upload (multipart), not a URL, unlike sendPhoto.
AVATAR="$(dirname "$0")/assets/group-avatar.png"
if [ -f "$AVATAR" ]; then
if curl -s "${TG_PROXY_ARGS[@]}" -F "chat_id=$TARGET_TELEGRAM_GROUP" -F "photo=@$AVATAR" \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setChatPhoto" | grep -q '"ok":true'; then
echo "Аватарка группы установлена."
else
echo "Не удалось установить аватарку группы (не критично, можно поставить вручную)."
fi
fi
API_KEY=$(openssl rand -hex 24)
MAX_SESSION_KEY=$(openssl rand -hex 32)
PORT=$(find_free_port 3000)
if [ "$PORT" != "3000" ]; then
echo "Порт 3000 занят — использую $PORT вместо него."
fi
# .env carries every secret this install has (API key, session-encryption key,
# bot token) — don't leave it readable to other local users.
umask 077
cat > .env <<EOF
# сгенерировано setup.sh $(date -u +%Y-%m-%dT%H:%M:%SZ)
API_KEY=$API_KEY
MAX_SESSION_KEY=$MAX_SESSION_KEY
TELEGRAM_BOT_TOKEN=$TELEGRAM_BOT_TOKEN
TARGET_TELEGRAM_GROUP=$TARGET_TELEGRAM_GROUP
PORT=$PORT
TELEGRAM_PROXY=$TELEGRAM_PROXY
EOF
echo
bold "Готово: .env создан."
echo "Ключ для входа в веб-панель (сохраните — если потеряете, его всегда можно"
echo "запросить снова у бота командой /apikey прямо в вашей Telegram-группе):"
bold " $API_KEY"
echo
if ! command -v docker >/dev/null 2>&1; then
echo "Docker не найден на этой машине — установите Docker и Docker Compose, затем запустите:"
echo " docker compose up -d --build"
exit 0
fi
read -rp "Собрать и запустить контейнер прямо сейчас? [Y/n] " RUN_NOW
if [ "${RUN_NOW:-Y}" = "n" ] || [ "${RUN_NOW:-Y}" = "N" ]; then
echo "Ок, когда будете готовы: docker compose up -d --build"
exit 0
fi
echo
echo "Собираю образ — первая сборка дольше обычного (внутри headless-браузер для рендера стикеров), обычно несколько минут."
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo unknown) docker compose up -d --build
PUBLIC_IP=$(curl -s --max-time 3 ifconfig.me || true)
HOST="${PUBLIC_IP:-<адрес-сервера>}"
echo
bold "Контейнер собран и запущен. Остался ОДИН обязательный шаг ниже."
# Авторизация в MAX прямо здесь, без переключения в браузер — тот же /api/auth/*,
# которым пользуется веб-панель, просто из консоли.
API_URL="https://localhost:$PORT/api"
wait_for_server
run_max_auth
print_final_status