Files
tgvpn/internal/bot/handler.go
T

342 lines
9.3 KiB
Go

package bot
import (
"context"
"fmt"
"log"
"strings"
"time"
"telegramvpn/internal/config"
"telegramvpn/internal/db"
"telegramvpn/internal/remnawave"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
const docsURL = "https://docs.rw/"
type Handler struct {
cfg *config.Config
api *tgbotapi.BotAPI
panel *remnawave.Client
database *db.DB
admin int64
}
func NewHandler(cfg *config.Config, api *tgbotapi.BotAPI, database *db.DB) *Handler {
return &Handler{
cfg: cfg,
api: api,
panel: remnawave.NewClient(cfg.RemnawavePanelURL, cfg.RemnawaveAPIToken, cfg.CaddyAuthAPIToken),
database: database,
admin: cfg.TelegramAdminID,
}
}
func (h *Handler) RegisterCommands() {
commands := []tgbotapi.BotCommand{
{Command: "start", Description: "Начать"},
{Command: "admin", Description: "Админ-меню Remnawave (панель 1)"},
}
scope := tgbotapi.BotCommandScope{Type: "chat", ChatID: h.admin}
cfg := tgbotapi.SetMyCommandsConfig{
Commands: commands,
Scope: &scope,
}
if _, err := h.api.Request(cfg); err != nil {
log.Printf("не удалось зарегистрировать команды для админа: %v", err)
}
}
func (h *Handler) HandleUpdate(update tgbotapi.Update) {
if update.CallbackQuery != nil {
h.handleCallback(update.CallbackQuery)
return
}
if update.Message == nil {
return
}
chatID := update.Message.Chat.ID
userID := update.Message.From.ID
text := strings.TrimSpace(update.Message.Text)
switch {
case text == "/start":
h.sendStart(chatID, userID, update.Message.From.FirstName, update.Message.From.UserName)
case strings.HasPrefix(text, "/admin"):
h.handleAdminCommand(chatID, userID, text)
case strings.HasPrefix(text, "/"):
h.sendText(chatID, "Неизвестная команда. Для начала — /start")
default:
if h.isAdmin(userID) && h.handleWizardMessage(chatID, userID, text) {
return
}
if h.isAdmin(userID) {
switch text {
case "📋 Конфиг панели":
h.sendPanelConfig(chatID)
return
case "🔌 Проверить панель":
h.sendPanelCheck(chatID)
return
case "👤 Создать пользователя":
h.startUserWizard(chatID, userID)
return
case "📡 Сквады":
h.sendSquadsList(chatID)
return
case "◀️ Выйти из админки":
h.sendText(chatID, "Админ-меню закрыто. /admin — снова открыть.")
return
}
}
h.sendText(chatID, "Напишите /start, чтобы начать.")
}
}
func (h *Handler) handleAdminCommand(chatID, userID int64, text string) {
if !h.isAdmin(userID) {
h.sendText(chatID, "У вас нет доступа к админ-меню.")
return
}
args := strings.Fields(text)
if len(args) == 1 {
h.sendAdminMenu(chatID)
return
}
switch args[1] {
case "check", "проверка":
h.sendPanelCheck(chatID)
case "config", "конфиг":
h.sendPanelConfig(chatID)
case "user", "пользователь", "squads", "сквады", "assign", "сквад", "cancel", "отмена", "help":
h.handleAdminUsersSubcommand(chatID, userID, args[2:])
default:
h.sendAdminHelp(chatID)
}
}
func (h *Handler) handleCallback(cq *tgbotapi.CallbackQuery) {
answer := tgbotapi.NewCallback(cq.ID, "")
if _, err := h.api.Request(answer); err != nil {
log.Printf("callback answer: %v", err)
}
if !h.isAdmin(cq.From.ID) {
h.editOrSend(cq.Message.Chat.ID, cq.Message.MessageID, "Нет доступа.")
return
}
if h.handleWizardCallback(cq) {
return
}
switch cq.Data {
case "admin:user":
h.startUserWizard(cq.Message.Chat.ID, cq.From.ID)
case "admin:squads":
h.sendSquadsList(cq.Message.Chat.ID)
case "admin:config":
h.sendPanelConfig(cq.Message.Chat.ID)
case "admin:check":
h.sendPanelCheck(cq.Message.Chat.ID)
case "admin:menu":
h.sendAdminMenu(cq.Message.Chat.ID)
default:
h.editOrSend(cq.Message.Chat.ID, cq.Message.MessageID, "Неизвестное действие.")
}
}
func (h *Handler) isAdmin(userID int64) bool {
return userID == h.admin
}
func (h *Handler) sendStart(chatID, userID int64, firstName, tgUsername string) {
ctx := context.Background()
_ = h.database.UpsertTelegramUser(ctx, userID, tgUsername, firstName)
name := firstName
if name == "" {
name = "друг"
}
text := fmt.Sprintf("Привет, %s!\n\nЯ VPN-бот на базе панели Remnawave.", name)
if h.isAdmin(userID) {
text += "\n\n/admin — админ-меню\n/admin user — создать пользователя\n/admin squads — сквады"
}
msg := tgbotapi.NewMessage(chatID, text)
if h.isAdmin(userID) {
msg.ReplyMarkup = adminReplyKeyboard()
}
h.send(msg)
}
func (h *Handler) sendAdminMenu(chatID int64) {
text := fmt.Sprintf(
"🛠 *Админ-меню* — %s\n\n"+
"Команды:\n"+
"• /admin — это меню\n"+
"• /admin check — проверка панели, API и подписки\n"+
"• /admin config — конфиг панели\n"+
"• /admin user — создать пользователя\n"+
"• /admin squads — список сквадов\n"+
"• /admin assign <логин> — назначить сквады\n\n"+
"Или кнопки ниже.",
escapeMarkdown(h.cfg.RemnawaveName),
)
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
msg.ReplyMarkup = adminInlineKeyboard()
h.send(msg)
}
func (h *Handler) sendAdminHelp(chatID int64) {
h.sendText(chatID, "Команды:\n/admin — меню\n/admin check\n/admin config\n/admin user\n/admin squads\n/admin assign <логин>")
}
func (h *Handler) sendPanelConfig(chatID int64) {
subURL := h.cfg.RemnawaveSubscription
if subURL == "" {
subURL = "не задана (опционально)"
}
caddy := h.cfg.CaddyAuthAPIToken
if caddy == "" {
caddy = "не задан"
} else {
caddy = maskSecret(caddy)
}
text := fmt.Sprintf(
"⚙️ %s (Remnawave)\n\n"+
"REMNAWAVE_PANEL_URL:\n%s\n"+
"(API: %s/api/... + Bearer REMNAWAVE_API_TOKEN)\n\n"+
"REMNAWAVE_SUBSCRIPTION_URL (опц.):\n%s\n\n"+
"REMNAWAVE_API_TOKEN: %s\n"+
"CADDY_AUTH_API_TOKEN: %s\n\n"+
"Токен: Remnawave Settings → API Tokens\n"+
"Док: %s",
h.cfg.RemnawaveName,
h.cfg.RemnawavePanelURL,
h.cfg.RemnawavePanelURL,
subURL,
maskSecret(h.cfg.RemnawaveAPIToken),
caddy,
"https://docs.rw/docs/install/subscription-page/bundled",
)
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = adminInlineKeyboard()
h.send(msg)
}
func (h *Handler) sendPanelCheck(chatID int64) {
h.sendText(chatID, fmt.Sprintf("Проверяю «%s»: панель, API, подписка…", h.cfg.RemnawaveName))
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
report := h.panel.FullCheck(ctx, h.cfg.RemnawaveSubscription)
text := remnawave.FormatReport(report, h.cfg.RemnawaveName)
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = adminInlineKeyboard()
h.send(msg)
}
func adminInlineKeyboard() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("👤 Создать пользователя", "admin:user"),
tgbotapi.NewInlineKeyboardButtonData("📡 Сквады", "admin:squads"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔌 Проверить", "admin:check"),
tgbotapi.NewInlineKeyboardButtonData("📋 Конфиг", "admin:config"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("📖 Документация", docsURL),
),
)
}
func adminReplyKeyboard() tgbotapi.ReplyKeyboardMarkup {
return tgbotapi.ReplyKeyboardMarkup{
Keyboard: [][]tgbotapi.KeyboardButton{
{
tgbotapi.NewKeyboardButton("👤 Создать пользователя"),
tgbotapi.NewKeyboardButton("📡 Сквады"),
},
{
tgbotapi.NewKeyboardButton("🔌 Проверить панель"),
tgbotapi.NewKeyboardButton("📋 Конфиг панели"),
},
{
tgbotapi.NewKeyboardButton("◀️ Выйти из админки"),
},
},
ResizeKeyboard: true,
OneTimeKeyboard: false,
}
}
func (h *Handler) sendText(chatID int64, text string) {
h.send(tgbotapi.NewMessage(chatID, text))
}
func (h *Handler) send(msg tgbotapi.MessageConfig) {
if err := h.sendReturnErr(msg); err != nil {
log.Printf("ошибка отправки: %v", err)
}
}
func (h *Handler) sendReturnErr(msg tgbotapi.MessageConfig) error {
_, err := h.api.Send(msg)
return err
}
func (h *Handler) editOrSend(chatID int64, messageID int, text string) {
edit := tgbotapi.NewEditMessageText(chatID, messageID, text)
if _, err := h.api.Send(edit); err != nil {
h.sendText(chatID, text)
}
}
func maskSecret(s string) string {
if len(s) <= 8 {
return "****"
}
return s[:4] + "…" + s[len(s)-4:]
}
func caddyStatus(token string) string {
if token == "" {
return "не задан (опционально)"
}
return "`" + escapeMarkdown(maskSecret(token)) + "`"
}
func escapeMarkdown(s string) string {
replacer := strings.NewReplacer(
"_", "\\_",
"*", "\\*",
"[", "\\[",
"]", "\\]",
"(", "\\(",
")", "\\)",
"~", "\\~",
"`", "\\`",
">", "\\>",
"#", "\\#",
"+", "\\+",
"-", "\\-",
"=", "\\=",
"|", "\\|",
"{", "\\{",
"}", "\\}",
".", "\\.",
"!", "\\!",
)
return replacer.Replace(s)
}