Improve Telegram keyboards and fix admin user menu navigation

Unified inline menus, user callbacks for all users, Home button to exit admin panel.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
tgvpn
2026-05-21 01:40:25 +03:00
parent cbb2133991
commit 23f5e782f8
3 changed files with 228 additions and 119 deletions
+106 -108
View File
@@ -76,30 +76,25 @@ func (h *Handler) HandleUpdate(update tgbotapi.Update) {
if h.isAdmin(userID) && h.handleWizardMessage(chatID, userID, text) {
return
}
if text == "📲 Получить конфиг (1 день)" || strings.HasPrefix(text, "📲 Получить конфиг") {
switch text {
case userHomeLabel(), "/menu":
h.sendUserMenu(chatID, userID, update.Message.From.FirstName, update.Message.From.UserName)
return
case adminPanelLabel(), "🛠 Админ-меню":
if h.isAdmin(userID) {
h.sendAdminMenu(chatID)
}
return
}
if h.isUserConfigButtonText(text) {
h.handleUserConfig(chatID, userID)
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
}
// Старые подписи reply-клавиатуры (если остались у пользователя)
if h.isAdmin(userID) && h.handleLegacyAdminReply(chatID, userID, text) {
return
}
h.sendText(chatID, "Напишите /start, чтобы начать.")
h.sendText(chatID, "Напишите /start или нажмите 🏠 Главная в меню.")
}
}
@@ -133,33 +128,54 @@ func (h *Handler) handleCallback(cq *tgbotapi.CallbackQuery) {
log.Printf("callback answer: %v", err)
}
if !h.isAdmin(cq.From.ID) {
h.editOrSend(cq.Message.Chat.ID, cq.Message.MessageID, "Нет доступа.")
chatID := cq.Message.Chat.ID
userID := cq.From.ID
switch cq.Data {
case cbUserConfig:
h.handleUserConfig(chatID, userID)
return
case cbUserHome:
h.sendUserMenu(chatID, userID, cq.From.FirstName, cq.From.UserName)
return
}
if cq.Data == "user:config" {
h.handleUserConfig(cq.Message.Chat.ID, cq.From.ID)
return
if strings.HasPrefix(cq.Data, "wz:") {
if !h.isAdmin(userID) {
h.callbackDenied(cq)
return
}
if h.handleWizardCallback(cq) {
return
}
}
if h.handleWizardCallback(cq) {
if !h.isAdmin(userID) {
h.callbackDenied(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)
case cbAdminUser:
h.startUserWizard(chatID, userID)
case cbAdminSquads:
h.sendSquadsList(chatID)
case cbAdminConfig:
h.sendPanelConfig(chatID)
case cbAdminCheck:
h.sendPanelCheck(chatID)
case cbAdminMenu:
h.sendAdminMenu(chatID)
default:
h.editOrSend(cq.Message.Chat.ID, cq.Message.MessageID, "Неизвестное действие.")
h.editOrSend(chatID, cq.Message.MessageID, "Неизвестное действие.")
}
}
func (h *Handler) callbackDenied(cq *tgbotapi.CallbackQuery) {
cb := tgbotapi.NewCallback(cq.ID, "Нет доступа")
cb.ShowAlert = true
if _, err := h.api.Request(cb); err != nil {
log.Printf("callback alert: %v", err)
}
}
@@ -168,64 +184,82 @@ func (h *Handler) isAdmin(userID int64) bool {
}
func (h *Handler) sendStart(chatID, userID int64, firstName, tgUsername string) {
h.sendUserMenu(chatID, userID, firstName, tgUsername)
}
func (h *Handler) sendUserMenu(chatID, userID int64, firstName, tgUsername string) {
ctx := context.Background()
_ = h.database.UpsertTelegramUser(ctx, userID, tgUsername, firstName)
h.dismissReplyKeyboard(chatID)
name := firstName
if name == "" {
name = "друг"
}
days := h.cfg.TrialUserDays
if days <= 0 {
days = 1
}
text := fmt.Sprintf("Привет, %s!\n\nЯ VPN-бот. Нажмите кнопку ниже — получите конфиг на %d дн.\nИли команда /config", name, days)
days := trialDays(h.cfg)
text := fmt.Sprintf(
"👋 Привет, %s!\n\n"+
"🔐 Trial VPN — %d дн.\n"+
"Нажмите кнопку ниже или /config\n\n"+
"Импорт: V2rayNG, Hiddify, Streisand и др.",
name, days,
)
if h.isAdmin(userID) {
text += "\n\n/admin — админ-меню"
text += "\n\nВы администратор: кнопка «🛠 Админ-панель» или /admin"
}
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = h.startInlineKeyboard(userID)
msg.ReplyMarkup = userMenuKeyboard(h.cfg, userID, h.admin)
h.send(msg)
}
func (h *Handler) startInlineKeyboard(userID int64) tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(h.userConfigButtonLabel(), "user:config"),
),
func (h *Handler) dismissReplyKeyboard(chatID int64) {
rm := tgbotapi.NewMessage(chatID, "\u200b")
rm.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
rm.DisableNotification = true
if _, err := h.api.Send(rm); err != nil {
log.Printf("remove reply keyboard: %v", err)
}
if h.isAdmin(userID) {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🛠 Админ-меню", "admin:menu"),
))
}
return tgbotapi.NewInlineKeyboardMarkup(rows...)
}
func (h *Handler) userConfigButtonLabel() string {
days := h.cfg.TrialUserDays
if days <= 0 {
days = 1
func (h *Handler) isUserConfigButtonText(text string) bool {
return text == userConfigLabel(h.cfg) ||
strings.HasPrefix(text, "🔐 ") ||
strings.HasPrefix(text, "📲 Получить конфиг")
}
func (h *Handler) handleLegacyAdminReply(chatID, userID int64, text string) bool {
switch text {
case "📋 Конфиг панели", "⚙️ Настройки":
h.sendPanelConfig(chatID)
case "🔌 Проверить панель", "🔌 Проверка API":
h.sendPanelCheck(chatID)
case "👤 Создать пользователя", "👤 Новый пользователь":
h.startUserWizard(chatID, userID)
case "📡 Сквады":
h.sendSquadsList(chatID)
case "◀️ Выйти из админки":
h.sendUserMenu(chatID, userID, "", "")
return true
default:
return false
}
return fmt.Sprintf("📲 Получить конфиг (%d дн.)", days)
return true
}
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"+
"Или кнопки ниже.",
"🛠 *Админ-панель* — %s\n\n"+
"• /admin check — проверка API\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()
msg.ReplyMarkup = adminMenuKeyboard()
h.send(msg)
}
@@ -262,7 +296,7 @@ func (h *Handler) sendPanelConfig(chatID int64) {
"https://docs.rw/docs/install/subscription-page/bundled",
)
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = adminInlineKeyboard()
msg.ReplyMarkup = adminContextKeyboard()
h.send(msg)
}
@@ -276,46 +310,10 @@ func (h *Handler) sendPanelCheck(chatID int64) {
text := remnawave.FormatReport(report, h.cfg.RemnawaveName)
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = adminInlineKeyboard()
msg.ReplyMarkup = adminContextKeyboard()
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))
}