Add /config trial VPN generation for users (1 day default)

Users get Remnawave subscription via /config or inline button; TRIAL_USER_DAYS and panel lookup by Telegram ID.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
tgvpn
2026-05-21 01:29:55 +03:00
parent 30866bb244
commit cbb2133991
9 changed files with 278 additions and 24 deletions
+49 -13
View File
@@ -35,17 +35,18 @@ func NewHandler(cfg *config.Config, api *tgbotapi.BotAPI, database *db.DB) *Hand
}
func (h *Handler) RegisterCommands() {
commands := []tgbotapi.BotCommand{
public := []tgbotapi.BotCommand{
{Command: "start", Description: "Начать"},
{Command: "admin", Description: "Админ-меню Remnawave (панель 1)"},
{Command: "config", Description: "Получить VPN-конфиг"},
}
if _, err := h.api.Request(tgbotapi.NewSetMyCommands(public...)); err != nil {
log.Printf("команды (все пользователи): %v", err)
}
admin := append(public, tgbotapi.BotCommand{Command: "admin", Description: "Админ-меню"})
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)
if _, err := h.api.Request(tgbotapi.SetMyCommandsConfig{Commands: admin, Scope: &scope}); err != nil {
log.Printf("команды (админ): %v", err)
}
}
@@ -65,6 +66,8 @@ func (h *Handler) HandleUpdate(update tgbotapi.Update) {
switch {
case text == "/start":
h.sendStart(chatID, userID, update.Message.From.FirstName, update.Message.From.UserName)
case text == "/config", text == "/getconfig":
h.handleUserConfig(chatID, userID)
case strings.HasPrefix(text, "/admin"):
h.handleAdminCommand(chatID, userID, text)
case strings.HasPrefix(text, "/"):
@@ -73,6 +76,10 @@ func (h *Handler) HandleUpdate(update tgbotapi.Update) {
if h.isAdmin(userID) && h.handleWizardMessage(chatID, userID, text) {
return
}
if text == "📲 Получить конфиг (1 день)" || strings.HasPrefix(text, "📲 Получить конфиг") {
h.handleUserConfig(chatID, userID)
return
}
if h.isAdmin(userID) {
switch text {
case "📋 Конфиг панели":
@@ -131,6 +138,11 @@ func (h *Handler) handleCallback(cq *tgbotapi.CallbackQuery) {
return
}
if cq.Data == "user:config" {
h.handleUserConfig(cq.Message.Chat.ID, cq.From.ID)
return
}
if h.handleWizardCallback(cq) {
return
}
@@ -163,17 +175,41 @@ func (h *Handler) sendStart(chatID, userID int64, firstName, tgUsername string)
if name == "" {
name = "друг"
}
text := fmt.Sprintf("Привет, %s!\n\nЯ VPN-бот на базе панели Remnawave.", name)
days := h.cfg.TrialUserDays
if days <= 0 {
days = 1
}
text := fmt.Sprintf("Привет, %s!\n\nЯ VPN-бот. Нажмите кнопку ниже — получите конфиг на %d дн.\nИли команда /config", name, days)
if h.isAdmin(userID) {
text += "\n\n/admin — админ-меню\n/admin user — создать пользователя\n/admin squads — сквады"
text += "\n\n/admin — админ-меню"
}
msg := tgbotapi.NewMessage(chatID, text)
if h.isAdmin(userID) {
msg.ReplyMarkup = adminReplyKeyboard()
}
msg.ReplyMarkup = h.startInlineKeyboard(userID)
h.send(msg)
}
func (h *Handler) startInlineKeyboard(userID int64) tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(h.userConfigButtonLabel(), "user:config"),
),
}
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
}
return fmt.Sprintf("📲 Получить конфиг (%d дн.)", days)
}
func (h *Handler) sendAdminMenu(chatID int64) {
text := fmt.Sprintf(
"🛠 *Админ-меню* — %s\n\n"+
+136
View File
@@ -0,0 +1,136 @@
package bot
import (
"context"
"fmt"
"log"
"time"
"telegramvpn/internal/db"
"telegramvpn/internal/remnawave"
)
func (h *Handler) handleUserConfig(chatID, telegramID int64) {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
days := h.cfg.TrialUserDays
if days <= 0 {
days = 1
}
existing, err := h.database.GetVPNByTelegramID(ctx, telegramID)
if err != nil {
h.sendText(chatID, "Ошибка базы данных. Попробуйте позже.")
return
}
if existing != nil && existing.ExpireAt != nil && existing.ExpireAt.After(time.Now()) {
link := h.resolveSubscriptionLink(ctx, existing.RemnawaveUsername, telegramID)
h.sendConfigMessage(chatID, days, existing.RemnawaveUsername, *existing.ExpireAt, link)
return
}
panelUser, err := h.panel.GetUserByTelegramID(ctx, telegramID)
if err == nil && panelUser != nil && panelUser.ExpireAt.After(time.Now()) {
link := panelUser.SubscriptionURL
if link == "" {
link = h.subscriptionLink(panelUser.ShortUUID)
}
_ = h.saveVPNFromPanel(ctx, telegramID, panelUser)
h.sendConfigMessage(chatID, days, panelUser.Username, panelUser.ExpireAt, link)
return
}
username := fmt.Sprintf("u%d", telegramID)
var extPtr *string
if h.cfg.DefaultExternalSquadUUID != "" {
e := h.cfg.DefaultExternalSquadUUID
extPtr = &e
}
ints := h.cfg.DefaultInternalSquadUUIDs
tgID := telegramID
u, err := h.panel.CreateUser(ctx, remnawave.CreateUserInput{
Username: username,
ExpireAt: db.DefaultExpireAt(days),
TelegramID: &tgID,
ExternalSquadUUID: extPtr,
ActiveInternalSquads: ints,
Description: fmt.Sprintf("trial %d day via tgvpn bot", days),
})
if err != nil {
h.sendText(chatID, "Не удалось создать доступ: "+err.Error()+"\n\nПопробуйте позже или напишите администратору.")
return
}
if err := h.saveVPNFromPanel(ctx, telegramID, u); err != nil {
log.Printf("save vpn user: %v", err)
}
link := u.SubscriptionURL
if link == "" {
link = h.subscriptionLink(u.ShortUUID)
}
h.sendConfigMessage(chatID, days, u.Username, u.ExpireAt, link)
}
func (h *Handler) saveVPNFromPanel(ctx context.Context, telegramID int64, u *remnawave.PanelUser) error {
if u == nil {
return nil
}
var ext *string
if h.cfg.DefaultExternalSquadUUID != "" {
e := h.cfg.DefaultExternalSquadUUID
ext = &e
}
return h.database.SaveVPNUser(ctx, db.VPNUser{
TelegramID: &telegramID,
RemnawaveUUID: u.UUID,
RemnawaveUsername: u.Username,
ExternalSquadUUID: ext,
InternalSquadUUIDs: h.cfg.DefaultInternalSquadUUIDs,
ExpireAt: &u.ExpireAt,
})
}
func (h *Handler) resolveSubscriptionLink(ctx context.Context, username string, telegramID int64) string {
if u, err := h.panel.GetUserByUsername(ctx, username); err == nil && u != nil {
if u.SubscriptionURL != "" {
return u.SubscriptionURL
}
return h.subscriptionLink(u.ShortUUID)
}
if u, err := h.panel.GetUserByTelegramID(ctx, telegramID); err == nil && u != nil {
if u.SubscriptionURL != "" {
return u.SubscriptionURL
}
return h.subscriptionLink(u.ShortUUID)
}
return ""
}
func (h *Handler) subscriptionLink(shortUUID string) string {
if shortUUID != "" && h.cfg.RemnawaveSubscription != "" {
return h.cfg.RemnawaveSubscription + "/" + shortUUID
}
return ""
}
func (h *Handler) sendConfigMessage(chatID int64, days int, username string, expireAt time.Time, link string) {
text := fmt.Sprintf(
"✅ Ваш VPN-конфиг готов\n\n"+
"Срок: %d дн. (до %s)\n"+
"Логин: %s\n",
days,
expireAt.Local().Format("02.01.2006 15:04"),
username,
)
if link != "" {
text += "\n🔗 Ссылка подписки (добавьте в приложение):\n" + link
} else {
text += "\n⚠️ Ссылка подписки не настроена. Администратору нужно указать REMNAWAVE_SUBSCRIPTION_URL в .env"
}
text += "\n\nИмпортируйте ссылку в V2rayNG, Hiddify, Streisand и др."
h.sendText(chatID, text)
}