Files
tgvpn/internal/bot/admin_users.go
T

408 lines
12 KiB
Go
Raw 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.
package bot
import (
"context"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"time"
"telegramvpn/internal/db"
"telegramvpn/internal/remnawave"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var usernameRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{3,36}$`)
func (h *Handler) handleAdminUsersSubcommand(chatID, adminID int64, args []string) {
switch {
case len(args) == 0 || args[0] == "help":
h.sendText(chatID, "Пользователи Remnawave:\n\n"+
"/admin user — мастер создания\n"+
"/admin user <логин> [дней] — быстрое создание\n"+
"/admin squads — список сквадов\n"+
"/admin assign <логин> — назначить сквады (мастер)\n"+
"/admin cancel — отменить мастер")
case args[0] == "cancel", args[0] == "отмена":
_ = h.database.ClearWizard(context.Background(), adminID)
h.sendText(chatID, "Мастер отменён.")
case args[0] == "squads", args[0] == "сквады":
h.sendSquadsList(chatID)
case args[0] == "user", args[0] == "пользователь":
if len(args) >= 2 {
days := h.cfg.DefaultUserDays
if len(args) >= 3 {
if d, err := strconv.Atoi(args[2]); err == nil && d > 0 {
days = d
}
}
h.quickCreateUser(chatID, adminID, args[1], days)
} else {
h.startUserWizard(chatID, adminID)
}
case args[0] == "assign", args[0] == "сквад":
if len(args) < 2 {
h.sendText(chatID, "Укажите логин: /admin assign username")
return
}
h.startAssignWizard(chatID, adminID, args[1])
default:
h.sendAdminHelp(chatID)
}
}
func (h *Handler) startUserWizard(chatID, adminID int64) {
ctx := context.Background()
data := db.WizardData{"mode": "create"}
_ = h.database.SetWizard(ctx, adminID, db.StepAwaitUsername, data)
h.sendText(chatID, "Создание пользователя.\n\nВведите логин (3–36 символов, a-z, 0-9, _, -):\n\n/admin cancel — отмена")
}
func (h *Handler) startAssignWizard(chatID, adminID int64, username string) {
ctx := context.Background()
data := db.WizardData{
"mode": "assign",
"username": username,
}
_ = h.database.SetWizard(ctx, adminID, db.StepPickExternalSquad, data)
h.sendExternalSquadPicker(chatID, data)
}
func (h *Handler) handleWizardMessage(chatID, adminID int64, text string) bool {
ctx := context.Background()
w, err := h.database.GetWizard(ctx, adminID)
if err != nil || w == nil || w.Step == db.StepIdle || w.Step == "" {
return false
}
switch w.Step {
case db.StepAwaitUsername:
if !usernameRe.MatchString(text) {
h.sendText(chatID, "Неверный логин. Допустимы: a-z, 0-9, _, - (336 символов).")
return true
}
w.Data.Set("username", text)
_ = h.database.SetWizard(ctx, adminID, db.StepAwaitDays, w.Data)
h.sendText(chatID, fmt.Sprintf("Срок подписки в днях (по умолчанию %d):", h.cfg.DefaultUserDays))
return true
case db.StepAwaitDays:
days := h.cfg.DefaultUserDays
if text != "" {
if d, err := strconv.Atoi(text); err != nil || d <= 0 {
h.sendText(chatID, "Введите число дней больше 0.")
return true
} else {
days = d
}
}
w.Data.Set("days", days)
_ = h.database.SetWizard(ctx, adminID, db.StepPickExternalSquad, w.Data)
h.sendExternalSquadPicker(chatID, w.Data)
return true
}
return false
}
func (h *Handler) handleWizardCallback(cq *tgbotapi.CallbackQuery) bool {
if !strings.HasPrefix(cq.Data, "wz:") {
return false
}
chatID := cq.Message.Chat.ID
adminID := cq.From.ID
ctx := context.Background()
w, err := h.database.GetWizard(ctx, adminID)
if err != nil || w == nil {
return true
}
parts := strings.Split(cq.Data, ":")
if len(parts) < 2 {
return true
}
switch parts[1] {
case "ext":
if len(parts) < 3 {
return true
}
if parts[2] == "skip" {
w.Data.Set("external_squad", "")
} else {
w.Data.Set("external_squad", parts[2])
}
_ = h.database.SetWizard(ctx, adminID, db.StepPickInternalSquads, w.Data)
h.sendInternalSquadPicker(chatID, w.Data)
case "int":
if len(parts) < 3 {
return true
}
if parts[2] == "done" {
_ = h.database.SetWizard(ctx, adminID, db.StepConfirm, w.Data)
h.sendConfirm(chatID, w.Data)
return true
}
w.Data.ToggleUUID("internal_squads", parts[2])
_ = h.database.SetWizard(ctx, adminID, db.StepPickInternalSquads, w.Data)
h.sendInternalSquadPicker(chatID, w.Data)
case "ok":
h.finishWizard(chatID, adminID, w.Data)
case "no":
_ = h.database.ClearWizard(ctx, adminID)
h.sendText(chatID, "Отменено.")
}
return true
}
func (h *Handler) sendSquadsList(chatID int64) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
ext, err1 := h.panel.ListExternalSquads(ctx)
ints, err2 := h.panel.ListInternalSquads(ctx)
var b strings.Builder
b.WriteString("Сквады Remnawave:\n\n")
b.WriteString("External:\n")
if err1 != nil {
b.WriteString(" ошибка: " + err1.Error() + "\n")
} else if len(ext) == 0 {
b.WriteString(" (пусто)\n")
} else {
for _, s := range ext {
b.WriteString(fmt.Sprintf(" • %s\n %s\n", s.Name, s.UUID))
}
}
b.WriteString("\nInternal:\n")
if err2 != nil {
b.WriteString(" ошибка: " + err2.Error() + "\n")
} else if len(ints) == 0 {
b.WriteString(" (пусто)\n")
} else {
for _, s := range ints {
b.WriteString(fmt.Sprintf(" • %s\n %s\n", s.Name, s.UUID))
}
}
h.sendText(chatID, b.String())
}
func (h *Handler) sendExternalSquadPicker(chatID int64, data db.WizardData) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
squads, err := h.panel.ListExternalSquads(ctx)
if err != nil {
h.sendText(chatID, "Не удалось загрузить external squads: "+err.Error())
return
}
var rows [][]tgbotapi.InlineKeyboardButton
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⏭ Без external squad", "wz:ext:skip"),
))
for _, s := range squads {
label := s.Name
if len(label) > 40 {
label = label[:40]
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(label, "wz:ext:"+s.UUID),
))
}
msg := tgbotapi.NewMessage(chatID, "Выберите External Squad:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(rows...)
h.send(msg)
}
func (h *Handler) sendInternalSquadPicker(chatID int64, data db.WizardData) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
squads, err := h.panel.ListInternalSquads(ctx)
if err != nil {
h.sendText(chatID, "Не удалось загрузить internal squads: "+err.Error())
return
}
selected := map[string]bool{}
for _, id := range data.StringSlice("internal_squads") {
selected[id] = true
}
var rows [][]tgbotapi.InlineKeyboardButton
for _, s := range squads {
mark := "☐"
if selected[s.UUID] {
mark = "☑"
}
label := fmt.Sprintf("%s %s", mark, s.Name)
if len(label) > 60 {
label = label[:60]
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(label, "wz:int:"+s.UUID),
))
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Готово", "wz:int:done"),
))
msg := tgbotapi.NewMessage(chatID, "Выберите Internal Squads (можно несколько), затем «Готово»:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(rows...)
h.send(msg)
}
func (h *Handler) sendConfirm(chatID int64, data db.WizardData) {
ext := data.String("external_squad")
ints := data.StringSlice("internal_squads")
text := fmt.Sprintf(
"Подтвердите:\n\nЛогин: %s\nДней: %d\nExternal: %s\nInternal: %d шт.\n",
data.String("username"), data.Int("days"), squadLabel(ext), len(ints),
)
if data.String("mode") == "assign" {
text = fmt.Sprintf(
"Назначить сквады пользователю %s\n\nExternal: %s\nInternal: %d шт.\n",
data.String("username"), squadLabel(ext), len(ints),
)
}
msg := tgbotapi.NewMessage(chatID, text)
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("✅ Да", "wz:ok"),
tgbotapi.NewInlineKeyboardButtonData("❌ Нет", "wz:no"),
),
)
h.send(msg)
}
func (h *Handler) finishWizard(chatID, adminID int64, data db.WizardData) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
ext := data.String("external_squad")
var extPtr *string
if ext != "" {
extPtr = &ext
}
ints := data.StringSlice("internal_squads")
if len(ints) == 0 && len(h.cfg.DefaultInternalSquadUUIDs) > 0 {
ints = h.cfg.DefaultInternalSquadUUIDs
}
if extPtr == nil && h.cfg.DefaultExternalSquadUUID != "" {
e := h.cfg.DefaultExternalSquadUUID
extPtr = &e
}
mode := data.String("mode")
if mode == "assign" {
u, err := h.panel.AssignSquads(ctx, remnawave.AssignSquadsInput{
Username: data.String("username"),
ExternalSquadUUID: extPtr,
ActiveInternalSquads: ints,
})
_ = h.database.ClearWizard(ctx, adminID)
if err != nil {
h.sendText(chatID, "Ошибка назначения сквадов: "+err.Error())
return
}
h.sendText(chatID, fmt.Sprintf("✅ Сквады назначены пользователю %s\nUUID: %s", u.Username, u.UUID))
return
}
days := data.Int("days")
if days <= 0 {
days = h.cfg.DefaultUserDays
}
var tgID *int64
// при создании из мастера админом telegramId не обязателен
u, err := h.panel.CreateUser(ctx, remnawave.CreateUserInput{
Username: data.String("username"),
ExpireAt: db.DefaultExpireAt(days),
TelegramID: tgID,
ExternalSquadUUID: extPtr,
ActiveInternalSquads: ints,
Description: "created via tgvpn bot",
})
_ = h.database.ClearWizard(ctx, adminID)
if err != nil {
h.sendText(chatID, "Ошибка создания: "+err.Error())
return
}
vpn := db.VPNUser{
RemnawaveUUID: u.UUID,
RemnawaveUsername: u.Username,
ExternalSquadUUID: extPtr,
InternalSquadUUIDs: ints,
ExpireAt: &u.ExpireAt,
}
if err := h.database.SaveVPNUser(ctx, vpn); err != nil {
log.Printf("save vpn user: %v", err)
}
text := fmt.Sprintf("✅ Пользователь создан\n\nЛогин: %s\nUUID: %s\nИстекает: %s",
u.Username, u.UUID, u.ExpireAt.Format("2006-01-02"))
if u.SubscriptionURL != "" {
text += "\nПодписка: " + u.SubscriptionURL
} else if u.ShortUUID != "" && h.cfg.RemnawaveSubscription != "" {
text += "\nПодписка: " + h.cfg.RemnawaveSubscription + "/" + u.ShortUUID
}
h.sendText(chatID, text)
}
func (h *Handler) quickCreateUser(chatID, adminID int64, username string, days int) {
if !usernameRe.MatchString(username) {
h.sendText(chatID, "Неверный логин.")
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var extPtr *string
if h.cfg.DefaultExternalSquadUUID != "" {
e := h.cfg.DefaultExternalSquadUUID
extPtr = &e
}
ints := h.cfg.DefaultInternalSquadUUIDs
u, err := h.panel.CreateUser(ctx, remnawave.CreateUserInput{
Username: username,
ExpireAt: db.DefaultExpireAt(days),
ExternalSquadUUID: extPtr,
ActiveInternalSquads: ints,
Description: "created via tgvpn bot",
})
if err != nil {
h.sendText(chatID, "Ошибка: "+err.Error())
return
}
_ = h.database.SaveVPNUser(ctx, db.VPNUser{
RemnawaveUUID: u.UUID,
RemnawaveUsername: u.Username,
ExternalSquadUUID: extPtr,
InternalSquadUUIDs: ints,
ExpireAt: &u.ExpireAt,
})
h.sendText(chatID, fmt.Sprintf("✅ %s создан до %s", u.Username, u.ExpireAt.Format("2006-01-02")))
}
func squadLabel(uuid string) string {
if uuid == "" {
return "—"
}
if len(uuid) > 12 {
return uuid[:8] + "…"
}
return uuid
}