Align Remnawave config with official docs and fix admin health check

This commit is contained in:
tgvpn
2026-05-21 00:59:55 +03:00
parent 7bde0d8a26
commit 7d63603150
7 changed files with 231 additions and 105 deletions
+9 -8
View File
@@ -7,14 +7,15 @@ BOT_DEBUG=false
# Telegram user ID администратора (узнать: @userinfobot или @getidsbot) # Telegram user ID администратора (узнать: @userinfobot или @getidsbot)
TELEGRAM_ADMIN_ID=123456789 TELEGRAM_ADMIN_ID=123456789
# Remnawave — панель 1 (https://docs.rw/) # --- Remnawave (официальные имена: https://docs.rw/docs/install/subscription-page/bundled) ---
REMNAWAVE_PANEL_NAME=Панель 1 REMNAWAVE_PANEL_NAME=Панель 1
# URL панели: https://panel.example.com или http://remnawave:3000 (внутри Docker-сети)
REMNAWAVE_PANEL_URL=https://panel.example.com REMNAWAVE_PANEL_URL=https://panel.example.com
# Settings → API Tokens в панели Remnawave # API-токен: Remnawave Settings → API Tokens (Authorization: Bearer)
REMNAWAVE_API_TOKEN=your_api_token_here REMNAWAVE_API_TOKEN=API_TOKEN_FROM_REMNAWAVE
# Опционально, если перед панелью стоит Caddy с X-Api-Key # Если используется Caddy with security — X-Api-Key к панели
REMNAWAVE_CADDY_TOKEN= CADDY_AUTH_API_TOKEN=
# Публичная страница подписки (Subscription Page), для проверки доступности # Опционально: отдельный домен Subscription Page (sub.*), только для проверки доступности
REMNAWAVE_SUBSCRIPTION_URL=https://sub.example.com REMNAWAVE_SUBSCRIPTION_URL=
# Docker Compose читает этот файл как .env (скопируйте: cp .env.example .env) # Docker Compose: cp .env.example .env
+38 -4
View File
@@ -306,10 +306,10 @@ go build -o bot .
| `BOT_TOKEN` | да | Токен от @BotFather | | `BOT_TOKEN` | да | Токен от @BotFather |
| `TELEGRAM_ADMIN_ID` | да | Числовой Telegram user ID администратора (например, [@userinfobot](https://t.me/userinfobot)) | | `TELEGRAM_ADMIN_ID` | да | Числовой Telegram user ID администратора (например, [@userinfobot](https://t.me/userinfobot)) |
| `REMNAWAVE_PANEL_NAME` | нет | Название панели в админ-меню (по умолчанию «Панель 1») | | `REMNAWAVE_PANEL_NAME` | нет | Название панели в админ-меню (по умолчанию «Панель 1») |
| `REMNAWAVE_PANEL_URL` | да | URL панели Remnawave, например `https://vpn.example.com` | | `REMNAWAVE_PANEL_URL` | да | URL панели — сюда же идут запросы API (`/api/...`). Пример: `https://panel.example.com` ([док](https://docs.rw/docs/install/subscription-page/bundled)) |
| `REMNAWAVE_API_TOKEN` | да | API-токен: панель → **Settings → API Tokens** ([документация](https://docs.rw/)) | | `REMNAWAVE_API_TOKEN` | да | Токен из **Remnawave Settings → API Tokens**, заголовок `Authorization: Bearer` |
| `REMNAWAVE_CADDY_TOKEN` | нет | Доп. заголовок `X-Api-Key`, если панель за Caddy | | `CADDY_AUTH_API_TOKEN` | нет | `X-Api-Key`, если включён Caddy with security (как в оф. `.env` subscription-page) |
| `REMNAWAVE_SUBSCRIPTION_URL` | нет* | URL страницы подписки для проверки в `/admin check` (*рекомендуется) | | `REMNAWAVE_SUBSCRIPTION_URL` | нет | Опционально: домен Subscription Page (`sub.*`), отдельная проверка |
| `BOT_DEBUG` | нет | `true` — подробные логи Telegram API (только для отладки) | | `BOT_DEBUG` | нет | `true` — подробные логи Telegram API (только для отладки) |
### Админ-меню в боте ### Админ-меню в боте
@@ -323,6 +323,31 @@ go build -o bot .
--- ---
## Remnawave API (по официальной документации)
Как в [Bundled Subscription Page](https://docs.rw/docs/install/subscription-page/bundled):
```env
REMNAWAVE_PANEL_URL=https://panel.example.com
REMNAWAVE_API_TOKEN=API_TOKEN_FROM_REMNAWAVE
CADDY_AUTH_API_TOKEN=
```
- **Отдельного `REMNAWAVE_API_URL` нет** — API всегда на том же хосте, что и панель: `{REMNAWAVE_PANEL_URL}/api/...`
- Авторизация: `Authorization: Bearer {REMNAWAVE_API_TOKEN}`
- Внутри Docker-сети Remnawave: `REMNAWAVE_PANEL_URL=http://remnawave:3000`
- Домен `sub.*` — это Subscription Page, не панель; для API используйте `panel.*`
Пример проверки с сервера:
```bash
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $REMNAWAVE_API_TOKEN" \
"$REMNAWAVE_PANEL_URL/api/system/stats/recap"
```
---
## Полезные команды Docker ## Полезные команды Docker
```bash ```bash
@@ -381,6 +406,15 @@ docker compose logs bot # ошибки сети, токена
- Убедитесь, что на сервере нет блокировки Telegram (firewall, провайдер). - Убедитесь, что на сервере нет блокировки Telegram (firewall, провайдер).
- Проверьте: `curl -I https://api.telegram.org` с хоста. - Проверьте: `curl -I https://api.telegram.org` с хоста.
### API возвращает 502, веб-панель — 200
Частая причина: в `REMNAWAVE_PANEL_URL` указан домен **страницы подписки** (`sub.example.com`), а не **админ-панели** (`panel.example.com`).
1. Укажите URL **панели** (не sub): `REMNAWAVE_PANEL_URL=https://panel.evilfox.cc`
2. Токен API: `REMNAWAVE_API_TOKEN=...` (Settings → API Tokens)
3. Страницу подписки — опционально: `REMNAWAVE_SUBSCRIPTION_URL=https://sub.evilfox.cc`
4. Проверьте на сервере: `docker compose ps` (Remnawave Panel запущен), логи reverse proxy
### Контейнер постоянно перезапускается ### Контейнер постоянно перезапускается
```bash ```bash
+25 -26
View File
@@ -26,7 +26,7 @@ func NewHandler(cfg *config.Config, api *tgbotapi.BotAPI) *Handler {
return &Handler{ return &Handler{
cfg: cfg, cfg: cfg,
api: api, api: api,
panel: remnawave.NewClient(cfg.RemnawaveURL, cfg.RemnawaveToken, cfg.RemnawaveCaddy), panel: remnawave.NewClient(cfg.RemnawavePanelURL, cfg.RemnawaveAPIToken, cfg.CaddyAuthAPIToken),
admin: cfg.TelegramAdminID, admin: cfg.TelegramAdminID,
} }
} }
@@ -172,25 +172,32 @@ func (h *Handler) sendAdminHelp(chatID int64) {
func (h *Handler) sendPanelConfig(chatID int64) { func (h *Handler) sendPanelConfig(chatID int64) {
subURL := h.cfg.RemnawaveSubscription subURL := h.cfg.RemnawaveSubscription
if subURL == "" { if subURL == "" {
subURL = "не задан" subURL = "не задана (опционально)"
}
caddy := h.cfg.CaddyAuthAPIToken
if caddy == "" {
caddy = "не задан"
} else {
caddy = maskSecret(caddy)
} }
text := fmt.Sprintf( text := fmt.Sprintf(
"⚙️ *%s* (Remnawave)\n\n"+ "⚙️ %s (Remnawave)\n\n"+
"• URL панели: `%s`\n"+ "REMNAWAVE_PANEL_URL:\n%s\n"+
"• URL подписки: `%s`\n"+ "(API: %s/api/... + Bearer REMNAWAVE_API_TOKEN)\n\n"+
"• API token: `%s`\n"+ "REMNAWAVE_SUBSCRIPTION_URL (опц.):\n%s\n\n"+
"• Caddy token: %s\n\n"+ "REMNAWAVE_API_TOKEN: %s\n"+
"Токен API: панель → *Settings → API Tokens*.\n"+ "CADDY_AUTH_API_TOKEN: %s\n\n"+
"Документация: %s", "Токен: Remnawave Settings → API Tokens\n"+
escapeMarkdown(h.cfg.RemnawaveName), "Док: %s",
escapeMarkdown(h.cfg.RemnawaveURL), h.cfg.RemnawaveName,
escapeMarkdown(subURL), h.cfg.RemnawavePanelURL,
escapeMarkdown(maskSecret(h.cfg.RemnawaveToken)), h.cfg.RemnawavePanelURL,
caddyStatus(h.cfg.RemnawaveCaddy), subURL,
docsURL, maskSecret(h.cfg.RemnawaveAPIToken),
caddy,
"https://docs.rw/docs/install/subscription-page/bundled",
) )
msg := tgbotapi.NewMessage(chatID, text) msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
msg.ReplyMarkup = adminInlineKeyboard() msg.ReplyMarkup = adminInlineKeyboard()
h.send(msg) h.send(msg)
} }
@@ -202,19 +209,11 @@ func (h *Handler) sendPanelCheck(chatID int64) {
defer cancel() defer cancel()
report := h.panel.FullCheck(ctx, h.cfg.RemnawaveSubscription) report := h.panel.FullCheck(ctx, h.cfg.RemnawaveSubscription)
text := remnawave.FormatReport( text := remnawave.FormatReport(report, h.cfg.RemnawaveName)
report,
escapeMarkdown(h.cfg.RemnawaveName),
escapeMarkdown(h.cfg.RemnawaveURL),
)
msg := tgbotapi.NewMessage(chatID, text) msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
msg.ReplyMarkup = adminInlineKeyboard() msg.ReplyMarkup = adminInlineKeyboard()
if err := h.sendReturnErr(msg); err != nil { h.send(msg)
msg.ParseMode = ""
h.send(msg)
}
} }
func adminInlineKeyboard() tgbotapi.InlineKeyboardMarkup { func adminInlineKeyboard() tgbotapi.InlineKeyboardMarkup {
+22 -14
View File
@@ -7,14 +7,17 @@ import (
"strings" "strings"
) )
// См. официальную схему env: https://docs.rw/docs/install/subscription-page/bundled
// REMNAWAVE_PANEL_URL + REMNAWAVE_API_TOKEN (+ опционально CADDY_AUTH_API_TOKEN)
type Config struct { type Config struct {
BotToken string BotToken string
BotDebug bool BotDebug bool
TelegramAdminID int64 TelegramAdminID int64
RemnawaveName string RemnawaveName string
RemnawaveURL string RemnawavePanelURL string
RemnawaveToken string RemnawaveAPIToken string
RemnawaveCaddy string CaddyAuthAPIToken string
RemnawaveSubscription string RemnawaveSubscription string
} }
@@ -31,15 +34,15 @@ func Load() (*Config, error) {
panelURL := strings.TrimRight(strings.TrimSpace(os.Getenv("REMNAWAVE_PANEL_URL")), "/") panelURL := strings.TrimRight(strings.TrimSpace(os.Getenv("REMNAWAVE_PANEL_URL")), "/")
if panelURL == "" { if panelURL == "" {
return nil, fmt.Errorf("REMNAWAVE_PANEL_URL не задан") return nil, fmt.Errorf("REMNAWAVE_PANEL_URL не задан (URL панели, см. https://docs.rw/docs/install/subscription-page/bundled)")
} }
if !strings.HasPrefix(panelURL, "http://") && !strings.HasPrefix(panelURL, "https://") { if !strings.HasPrefix(panelURL, "http://") && !strings.HasPrefix(panelURL, "https://") {
return nil, fmt.Errorf("REMNAWAVE_PANEL_URL должен начинаться с http:// или https://") return nil, fmt.Errorf("REMNAWAVE_PANEL_URL должен быть с http:// или https:// (как в документации Remnawave)")
} }
panelToken := strings.TrimSpace(os.Getenv("REMNAWAVE_API_TOKEN")) apiToken := strings.TrimSpace(os.Getenv("REMNAWAVE_API_TOKEN"))
if panelToken == "" { if apiToken == "" {
return nil, fmt.Errorf("REMNAWAVE_API_TOKEN не задан (создайте в панели: Settings → API Tokens)") return nil, fmt.Errorf("REMNAWAVE_API_TOKEN не задан (Remnawave Settings → API Tokens)")
} }
name := strings.TrimSpace(os.Getenv("REMNAWAVE_PANEL_NAME")) name := strings.TrimSpace(os.Getenv("REMNAWAVE_PANEL_NAME"))
@@ -47,6 +50,11 @@ func Load() (*Config, error) {
name = "Панель 1" name = "Панель 1"
} }
caddy := strings.TrimSpace(os.Getenv("CADDY_AUTH_API_TOKEN"))
if caddy == "" {
caddy = strings.TrimSpace(os.Getenv("REMNAWAVE_CADDY_TOKEN")) // устаревшее имя
}
subURL := strings.TrimRight(strings.TrimSpace(os.Getenv("REMNAWAVE_SUBSCRIPTION_URL")), "/") subURL := strings.TrimRight(strings.TrimSpace(os.Getenv("REMNAWAVE_SUBSCRIPTION_URL")), "/")
if subURL != "" && !strings.HasPrefix(subURL, "http://") && !strings.HasPrefix(subURL, "https://") { if subURL != "" && !strings.HasPrefix(subURL, "http://") && !strings.HasPrefix(subURL, "https://") {
return nil, fmt.Errorf("REMNAWAVE_SUBSCRIPTION_URL должен начинаться с http:// или https://") return nil, fmt.Errorf("REMNAWAVE_SUBSCRIPTION_URL должен начинаться с http:// или https://")
@@ -57,9 +65,9 @@ func Load() (*Config, error) {
BotDebug: strings.EqualFold(strings.TrimSpace(os.Getenv("BOT_DEBUG")), "true"), BotDebug: strings.EqualFold(strings.TrimSpace(os.Getenv("BOT_DEBUG")), "true"),
TelegramAdminID: adminID, TelegramAdminID: adminID,
RemnawaveName: name, RemnawaveName: name,
RemnawaveURL: panelURL, RemnawavePanelURL: panelURL,
RemnawaveToken: panelToken, RemnawaveAPIToken: apiToken,
RemnawaveCaddy: strings.TrimSpace(os.Getenv("REMNAWAVE_CADDY_TOKEN")), CaddyAuthAPIToken: caddy,
RemnawaveSubscription: subURL, RemnawaveSubscription: subURL,
}, nil }, nil
} }
+25 -7
View File
@@ -10,24 +10,32 @@ import (
"time" "time"
) )
// Client обращается к Remnawave Panel API:
// - Base: REMNAWAVE_PANEL_URL (например https://panel.example.com)
// - Auth: Authorization: Bearer REMNAWAVE_API_TOKEN
// - Опционально: X-Api-Key: CADDY_AUTH_API_TOKEN
// Документация: https://docs.rw/docs/install/subscription-page/bundled
type Client struct { type Client struct {
baseURL string panelURL string
token string token string
caddyToken string caddyToken string
http *http.Client http *http.Client
} }
func NewClient(baseURL, apiToken, caddyToken string) *Client { func NewClient(panelURL, apiToken, caddyAuthToken string) *Client {
return &Client{ return &Client{
baseURL: strings.TrimRight(baseURL, "/"), panelURL: strings.TrimRight(panelURL, "/"),
token: apiToken, token: apiToken,
caddyToken: caddyToken, caddyToken: caddyAuthToken,
http: &http.Client{ http: &http.Client{
Timeout: 15 * time.Second, Timeout: 15 * time.Second,
}, },
} }
} }
func (c *Client) PanelURL() string { return c.panelURL }
func (c *Client) countFromEndpoint(ctx context.Context, path, arrayKey string) (int, error) { func (c *Client) countFromEndpoint(ctx context.Context, path, arrayKey string) (int, error) {
resp, body, err := c.get(ctx, path) resp, body, err := c.get(ctx, path)
if err != nil { if err != nil {
@@ -40,13 +48,23 @@ func (c *Client) countFromEndpoint(ctx context.Context, path, arrayKey string) (
} }
func (c *Client) get(ctx context.Context, path string) (*http.Response, []byte, error) { func (c *Client) get(ctx context.Context, path string) (*http.Response, []byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) return c.doRequest(ctx, c.panelURL+path, true)
}
func (c *Client) getPublic(ctx context.Context, path string) (*http.Response, []byte, error) {
return c.doRequest(ctx, c.panelURL+path, false)
}
func (c *Client) doRequest(ctx context.Context, url string, withBearer bool) (*http.Response, []byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Accept", "application/json, text/html, */*")
req.Header.Set("Accept", "application/json") if withBearer {
req.Header.Set("Authorization", "Bearer "+c.token)
}
if c.caddyToken != "" { if c.caddyToken != "" {
req.Header.Set("X-Api-Key", c.caddyToken) req.Header.Set("X-Api-Key", c.caddyToken)
} }
+111 -45
View File
@@ -8,41 +8,56 @@ import (
) )
type CheckItem struct { type CheckItem struct {
Name string Name string
OK bool OK bool
Status int Skipped bool
Detail string Status int
Detail string
} }
type HealthReport struct { type HealthReport struct {
PanelName string PanelURL string
PanelURL string Checks []CheckItem
Checks []CheckItem Users int
Users int Nodes int
Nodes int AllOK bool
AllOK bool Hint string
} }
func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthReport { func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthReport {
report := HealthReport{ report := HealthReport{
PanelName: "", PanelURL: c.panelURL,
PanelURL: c.baseURL,
} }
probes := []struct { probes := []struct {
name string name string
path string path string
isWeb bool
}{ }{
{"Панель (веб)", "/"}, {"Панель (веб)", "/", true},
{"API (статистика)", "/api/system/stats/recap"}, {"API (статистика)", "/api/system/stats/recap", false},
{"API (пользователи)", "/api/users"}, {"API (пользователи)", "/api/users", false},
{"API (ноды)", "/api/nodes"}, {"API (ноды)", "/api/nodes", false},
{"Подписка (настройки)", "/api/subscription-settings"}, {"Подписка (настройки)", "/api/subscription-settings", false},
{"Подписка (API список)", "/api/subscriptions"}, {"Подписка (API список)", "/api/subscriptions", false},
} }
apiFailures := 0
webOK := false
for _, p := range probes { for _, p := range probes {
item := c.probe(ctx, p.name, p.path) var item CheckItem
if p.isWeb {
item = c.probeWeb(ctx, p.name, p.path)
if item.OK {
webOK = true
}
} else {
item = c.probeAPI(ctx, p.name, p.path)
if !item.OK {
apiFailures++
}
}
report.Checks = append(report.Checks, item) report.Checks = append(report.Checks, item)
if p.path == "/api/users" && item.OK { if p.path == "/api/users" && item.OK {
@@ -62,16 +77,23 @@ func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthRe
report.Checks = append(report.Checks, c.probePublic(ctx, "Страница подписки", subURL)) report.Checks = append(report.Checks, c.probePublic(ctx, "Страница подписки", subURL))
} else { } else {
report.Checks = append(report.Checks, CheckItem{ report.Checks = append(report.Checks, CheckItem{
Name: "Страница подписки", Name: "Страница подписки",
OK: false, OK: true,
Status: 0, Skipped: true,
Detail: "не задана (REMNAWAVE_SUBSCRIPTION_URL)", Detail: "опционально, не задана",
}) })
} }
if webOK && apiFailures >= 4 {
report.Hint = "Веб открывается, но /api/* недоступен (502). " +
"По документации Remnawave API вызывается на REMNAWAVE_PANEL_URL (https://panel.example.com/api/...), " +
"а не на домене sub.*. Укажите URL админ-панели и токен REMNAWAVE_API_TOKEN. " +
"Док: https://docs.rw/docs/install/subscription-page/bundled"
}
report.AllOK = true report.AllOK = true
for _, ch := range report.Checks { for _, ch := range report.Checks {
if !ch.OK { if !ch.OK && !ch.Skipped {
report.AllOK = false report.AllOK = false
break break
} }
@@ -79,14 +101,27 @@ func (c *Client) FullCheck(ctx context.Context, subscriptionURL string) HealthRe
return report return report
} }
func (c *Client) probe(ctx context.Context, name, path string) CheckItem { func (c *Client) probeWeb(ctx context.Context, name, path string) CheckItem {
item := CheckItem{Name: name} item := CheckItem{Name: name}
resp, body, err := c.getPublic(ctx, path)
if err != nil {
item.Detail = err.Error()
return item
}
return finishProbe(&item, resp, body)
}
func (c *Client) probeAPI(ctx context.Context, name, path string) CheckItem {
item := CheckItem{Name: name}
resp, body, err := c.get(ctx, path) resp, body, err := c.get(ctx, path)
if err != nil { if err != nil {
item.Detail = err.Error() item.Detail = err.Error()
return item return item
} }
return finishProbe(&item, resp, body)
}
func finishProbe(item *CheckItem, resp *http.Response, body []byte) CheckItem {
item.Status = resp.StatusCode item.Status = resp.StatusCode
switch resp.StatusCode { switch resp.StatusCode {
@@ -94,11 +129,31 @@ func (c *Client) probe(ctx context.Context, name, path string) CheckItem {
item.OK = true item.OK = true
item.Detail = "OK" item.Detail = "OK"
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:
item.Detail = fmt.Sprintf("HTTP %d — неверный токен или нет прав", resp.StatusCode) item.Detail = "неверный REMNAWAVE_API_TOKEN или CADDY_AUTH_API_TOKEN"
default: default:
item.Detail = fmt.Sprintf("HTTP %d: %s", resp.StatusCode, trimBody(body, 120)) item.Detail = statusHint(resp.StatusCode)
if item.Detail == "" {
if s := trimBody(body, 80); s != "" {
item.Detail = s
} else {
item.Detail = http.StatusText(resp.StatusCode)
}
}
}
return *item
}
func statusHint(code int) string {
switch code {
case http.StatusBadGateway:
return "Bad Gateway — прокси не достучался до API панели"
case http.StatusServiceUnavailable:
return "Service Unavailable — бэкенд панели не запущен"
case http.StatusNotFound:
return "Not Found — путь /api не проксируется на панель"
default:
return ""
} }
return item
} }
func (c *Client) probePublic(ctx context.Context, name, url string) CheckItem { func (c *Client) probePublic(ctx context.Context, name, url string) CheckItem {
@@ -124,28 +179,35 @@ func (c *Client) probePublic(ctx context.Context, name, url string) CheckItem {
item.Status = resp.StatusCode item.Status = resp.StatusCode
if resp.StatusCode >= 200 && resp.StatusCode < 400 { if resp.StatusCode >= 200 && resp.StatusCode < 400 {
item.OK = true item.OK = true
item.Detail = fmt.Sprintf("OK (HTTP %d)", resp.StatusCode) item.Detail = "OK"
} else { } else {
item.Detail = fmt.Sprintf("HTTP %d", resp.StatusCode) item.Detail = statusHint(resp.StatusCode)
if item.Detail == "" {
item.Detail = http.StatusText(resp.StatusCode)
}
} }
return item return item
} }
func FormatReport(r HealthReport, panelName, panelURL string) string { // FormatReport — обычный текст (без Markdown), чтобы URL и имена env отображались корректно.
func FormatReport(r HealthReport, panelName string) string {
var b strings.Builder var b strings.Builder
if panelName != "" {
b.WriteString(fmt.Sprintf("Панель: *%s*\nURL: `%s`\n\n", panelName, panelURL))
}
icon := func(ok bool) string { if panelName != "" {
if ok { b.WriteString(fmt.Sprintf("Панель: %s\n", panelName))
return "✅"
}
return "❌"
} }
b.WriteString(fmt.Sprintf("REMNAWAVE_PANEL_URL: %s\n", r.PanelURL))
b.WriteString(fmt.Sprintf("API: %s/api/...\n\n", r.PanelURL))
for _, ch := range r.Checks { for _, ch := range r.Checks {
line := fmt.Sprintf("%s *%s*", icon(ch.OK), ch.Name) mark := "❌"
switch {
case ch.Skipped:
mark = "○"
case ch.OK:
mark = "✅"
}
line := fmt.Sprintf("%s %s", mark, ch.Name)
if ch.Status > 0 { if ch.Status > 0 {
line += fmt.Sprintf(" — HTTP %d", ch.Status) line += fmt.Sprintf(" — HTTP %d", ch.Status)
} }
@@ -156,13 +218,17 @@ func FormatReport(r HealthReport, panelName, panelURL string) string {
} }
if r.Users > 0 || r.Nodes > 0 { if r.Users > 0 || r.Nodes > 0 {
b.WriteString(fmt.Sprintf("\n👥 Пользователей: %d\n📡 Нод: %d", r.Users, r.Nodes)) b.WriteString(fmt.Sprintf("\nПользователей: %d\nНод: %d", r.Users, r.Nodes))
}
if r.Hint != "" {
b.WriteString("\n\n💡 " + r.Hint)
} }
if r.AllOK { if r.AllOK {
b.WriteString("\n\n✅ *Все проверки пройдены*") b.WriteString("\n\nВсе обязательные проверки пройдены.")
} else { } else {
b.WriteString("\n\n⚠️ *Есть ошибки*проверьте токен, URL и страницу подписки") b.WriteString("\n\nЕсть ошибки — см. подсказку выше.")
} }
return b.String() return b.String()
} }
+1 -1
View File
@@ -25,7 +25,7 @@ func main() {
api.Debug = cfg.BotDebug api.Debug = cfg.BotDebug
log.Printf("бот @%s запущен, админ ID %d, панель %q (%s)", log.Printf("бот @%s запущен, админ ID %d, панель %q (%s)",
api.Self.UserName, cfg.TelegramAdminID, cfg.RemnawaveName, cfg.RemnawaveURL) api.Self.UserName, cfg.TelegramAdminID, cfg.RemnawaveName, cfg.RemnawavePanelURL)
handler := bot.NewHandler(cfg, api) handler := bot.NewHandler(cfg, api)
handler.RegisterCommands() handler.RegisterCommands()