Files
GPTTeamBOT/internal/bot/telegram.go
2026-03-11 21:55:20 +08:00

2105 lines
63 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"go-helper/internal/chatgpt"
"go-helper/internal/config"
"go-helper/internal/database"
"go-helper/internal/model"
"go-helper/internal/redeem"
)
// chatSession tracks a two-step flow state per user (redeem or login).
type chatSession struct {
flowType string // "redeem" or "login"
code string // redemption code (for redeem flow)
loginMsgID int // message ID of the login link message (for login flow)
}
// Bot wraps the Telegram bot with application dependencies.
type Bot struct {
api *tgbotapi.BotAPI
db *database.DB
cfg *config.Config
client *chatgpt.Client
oauth *chatgpt.OAuthManager
sessions map[int64]*chatSession // chatID -> session
panelMsgs map[int64]int // chatID -> last panel message ID
mu sync.Mutex
}
// Start initialises and runs the Telegram bot (blocking).
func Start(db *database.DB, cfg *config.Config, client *chatgpt.Client, oauth *chatgpt.OAuthManager) {
api, err := tgbotapi.NewBotAPI(cfg.TelegramBotToken)
if err != nil {
log.Fatalf("[Bot] 启动失败: %v", err)
}
log.Printf("[Bot] 已登录: @%s", api.Self.UserName)
b := &Bot{
api: api,
db: db,
cfg: cfg,
client: client,
oauth: oauth,
sessions: make(map[int64]*chatSession),
panelMsgs: make(map[int64]int),
}
// Register commands with Telegram.
b.registerCommands()
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := api.GetUpdatesChan(u)
for update := range updates {
if update.CallbackQuery != nil {
go b.handleCallbackQuery(update.CallbackQuery)
continue
}
if update.Message == nil {
continue
}
go b.handleMessage(update.Message)
}
}
// isAdmin checks both config and database for admin rights.
func (b *Bot) isAdmin(userID int64) bool {
if b.cfg.IsAdmin(userID) {
return true
}
isAdmin, _ := b.db.IsAdmin(userID)
return isAdmin
}
func (b *Bot) registerCommands() {
// Clear old admin-scoped commands first.
for _, adminID := range b.cfg.TelegramAdminIDs {
delCfg := tgbotapi.NewDeleteMyCommandsWithScope(
tgbotapi.NewBotCommandScopeChat(adminID),
)
b.api.Request(delCfg)
}
// Only register /start for all users.
cmds := []tgbotapi.BotCommand{
{Command: "start", Description: "打开控制面板"},
}
cfg := tgbotapi.NewSetMyCommands(cmds...)
if _, err := b.api.Request(cfg); err != nil {
log.Printf("[Bot] 注册命令失败: %v", err)
}
log.Printf("[Bot] 命令注册完成")
}
func (b *Bot) handleCallbackQuery(cq *tgbotapi.CallbackQuery) {
chatID := cq.Message.Chat.ID
userID := cq.From.ID
msgID := cq.Message.MessageID
// Answer the callback to remove the loading indicator.
b.api.Request(tgbotapi.NewCallback(cq.ID, ""))
switch cq.Data {
case "cmd:back":
b.sendMainMenu(chatID, userID, msgID)
return
case "cmd:cancel":
b.mu.Lock()
_, had := b.sessions[chatID]
delete(b.sessions, chatID)
b.mu.Unlock()
if had {
b.sendMainMenu(chatID, userID, msgID)
}
return
case "cmd:cancel_to_list":
b.mu.Lock()
_, had := b.sessions[chatID]
delete(b.sessions, chatID)
b.mu.Unlock()
if had {
b.callbackListAccounts(chatID, msgID)
}
return
case "cmd:cancel_to_admin":
b.mu.Lock()
_, had := b.sessions[chatID]
delete(b.sessions, chatID)
b.mu.Unlock()
if had {
b.callbackAdminMenu(chatID, msgID, userID)
}
return
case "cmd:stock":
b.callbackStock(chatID, msgID)
return
case "cmd:redeem":
// Start redeem flow: ask for code.
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "redeem_code", loginMsgID: msgID}
b.mu.Unlock()
kb := cancelKeyboard()
b.editMsgWithKeyboard(chatID, msgID, "🎫 *使用兑换码*\n\n请输入兑换码", &kb)
return
case "cmd:random_invite":
if !b.isAdmin(userID) {
return
}
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "random_invite", loginMsgID: msgID}
b.mu.Unlock()
kb := cancelKeyboard()
b.editMsgWithKeyboard(chatID, msgID, "🎲 *随机拉人*\n\n请输入要邀请的用户邮箱", &kb)
return
case "cmd:add_admin":
// Only "super admins" (in config) can add admins dynamically.
if !b.cfg.IsAdmin(userID) {
b.sendAutoDelete(chatID, "❌ 权限不足:只有配置文件中的超级管理员才能添加新管理员。")
return
}
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "add_admin", loginMsgID: msgID}
b.mu.Unlock()
kb := cancelToAdminKeyboard()
b.editMsgWithKeyboard(chatID, msgID, "👑 *添加管理员*\n\n请输入要添加的 Telegram 用户 ID", &kb)
return
}
// All other buttons are admin-only.
if !b.isAdmin(userID) {
return
}
switch cq.Data {
case "cmd:admin_menu":
b.callbackAdminMenu(chatID, msgID, userID)
case "cmd:list_accounts":
b.callbackListAccounts(chatID, msgID)
case "cmd:status":
b.callbackStatus(chatID, msgID)
case "cmd:refresh_all":
b.callbackRefresh(chatID, msgID)
case "cmd:check_sub_all":
b.callbackCheckSub(chatID, msgID)
case "cmd:list_invites_all":
b.callbackListInvites(chatID, msgID)
case "cmd:codes_menu":
b.callbackCodesMenu(chatID, msgID)
case "cmd:clear_unused_codes":
count, err := b.db.DeleteUnusedCodes()
msg := ""
if err != nil {
msg = fmt.Sprintf("❌ 清空失败: %v", err)
} else {
msg = fmt.Sprintf("✅ 已成功清空 %d 个未使用的兑换码", count)
}
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu")))
b.editMsgWithKeyboard(chatID, msgID, msg, &kb)
case "cmd:list_codes_all":
b.callbackListCodes(chatID, msgID, 0)
case "cmd:gen_codes":
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "gen_codes_accounts", loginMsgID: msgID}
b.mu.Unlock()
kb := cancelKeyboard()
b.editMsgWithKeyboard(chatID, msgID, " *生成兑换码*\n\n请输入要生成的账号数量", &kb)
case "cmd:login":
b.handleLogin(chatID, msgID)
case "cmd:add_rt":
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "add_rt", loginMsgID: msgID}
b.mu.Unlock()
kb := cancelToListKeyboard()
b.editMsgWithKeyboard(chatID, msgID, "🔑 *RT添加账号*\n\n请输入 Refresh Token", &kb)
case "act:del_pick":
b.callbackActionPick(chatID, msgID, "del", userID, 0)
case "act:ref_pick":
b.callbackActionPick(chatID, msgID, "ref", userID, 0)
case "act:pending_invites_pick":
b.callbackActionPick(chatID, msgID, "pending_invite", userID, 0)
case "act:remove_admin_pick":
// Only super admins can remove admins
if !b.cfg.IsAdmin(userID) {
b.sendAutoDelete(chatID, "❌ 权限不足:只有超级管理员才能移除管理员。")
return
}
b.callbackActionPick(chatID, msgID, "deladmin", userID, 0)
default:
// Dynamic callbacks: del:<id>, ref:<id>, codes_page:<n>, deladmin:<id>, pending_invite:<id>, delinv:<acc_id>:<email>, act_page:<action>:<page>
if strings.HasPrefix(cq.Data, "act_page:") {
parts := strings.SplitN(strings.TrimPrefix(cq.Data, "act_page:"), ":", 2)
if len(parts) == 2 {
action := parts[0]
page, _ := strconv.Atoi(parts[1])
b.callbackActionPick(chatID, msgID, action, userID, page)
}
} else if strings.HasPrefix(cq.Data, "del:") {
b.callbackDelAccount(chatID, msgID, strings.TrimPrefix(cq.Data, "del:"))
} else if strings.HasPrefix(cq.Data, "ref:") {
b.callbackRefAccount(chatID, msgID, strings.TrimPrefix(cq.Data, "ref:"))
} else if strings.HasPrefix(cq.Data, "deladmin:") {
if !b.cfg.IsAdmin(userID) {
return
}
b.callbackDelAdmin(chatID, msgID, strings.TrimPrefix(cq.Data, "deladmin:"))
} else if strings.HasPrefix(cq.Data, "codes_page:") {
page, _ := strconv.Atoi(strings.TrimPrefix(cq.Data, "codes_page:"))
b.callbackListCodes(chatID, msgID, page)
} else if strings.HasPrefix(cq.Data, "pending_invite:") {
b.callbackListPendingInvites(chatID, msgID, strings.TrimPrefix(cq.Data, "pending_invite:"))
} else if strings.HasPrefix(cq.Data, "delinv:") {
b.callbackDelInvite(chatID, msgID, strings.TrimPrefix(cq.Data, "delinv:"))
} else if strings.HasPrefix(cq.Data, "delcode:") {
parts := strings.SplitN(strings.TrimPrefix(cq.Data, "delcode:"), ":", 2)
if len(parts) == 2 {
page, _ := strconv.Atoi(parts[0])
codeStr := parts[1]
b.callbackDelCode(chatID, msgID, page, codeStr)
}
}
}
}
// sendMainMenu sends or edits the main interactive panel.
func (b *Bot) callbackAdminMenu(chatID int64, msgID int, userID int64) {
admins, err := b.db.GetAllAdmins()
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 查询失败: %v", err), &kb)
return
}
text := "👑 *管理员管理*\n\n"
if len(admins) == 0 {
text += "暂无数据库记录的管理员 (仅配置项生效)。"
} else {
for i, a := range admins {
text += fmt.Sprintf("*%d.* ID: `%d` (由 `%d` 于 %s 添加)\n", i+1, a.UserID, a.AddedBy, formatDate(a.CreatedAt.Format(time.RFC3339)))
}
}
var kbrows [][]tgbotapi.InlineKeyboardButton
// If the user is a super admin (in config), they get the add/remove options.
if b.cfg.IsAdmin(userID) {
kbrows = append(kbrows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(" 添加管理员", "cmd:add_admin"),
tgbotapi.NewInlineKeyboardButtonData(" 移除管理员", "act:remove_admin_pick"),
))
}
// Everyone gets the back button.
kbrows = append(kbrows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:back"),
))
kb := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: kbrows}
b.editMsgWithKeyboard(chatID, msgID, text, &kb)
}
func (b *Bot) callbackDelAdmin(chatID int64, msgID int, idStr string) {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return
}
if err := b.db.RemoveAdmin(id); err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:admin_menu"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 移除失败: %v", err), &kb)
return
}
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:admin_menu"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("✅ 已移除管理员: `%d`", id), &kb)
}
func (b *Bot) sendMainMenu(chatID int64, userID int64, editMsgID int) {
caption := "🤖 *ChatGPT Team Helper*\n_Unleashing Team Collaboration Powered by AI_"
var keyboard tgbotapi.InlineKeyboardMarkup
if b.isAdmin(userID) {
// Build status summary for admin caption.
accounts, _ := b.db.GetAllAccounts()
totalAccounts := len(accounts)
openAccounts, bannedAccounts, totalUsers, totalInvites := 0, 0, 0, 0
for _, a := range accounts {
if a.IsOpen && !a.IsBanned {
openAccounts++
}
if a.IsBanned {
bannedAccounts++
}
totalUsers += a.UserCount
totalInvites += a.InviteCount
}
codeCount, _ := b.db.CountAvailableCodes()
caption += fmt.Sprintf("\n\n⚙ *系统状态*\n"+
"👥 账号: *%d* (开放 %d / 封号 %d)\n"+
"🎫 可用兑换码: *%d*",
totalAccounts, openAccounts, bannedAccounts,
codeCount)
keyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📝 账号列表", "cmd:list_accounts"),
tgbotapi.NewInlineKeyboardButtonData("📩 待进入邀请", "act:pending_invites_pick"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🎫 兑换码", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🎲 随机拉人", "cmd:random_invite"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("👑 管理员管理", "cmd:admin_menu"),
),
)
} else {
keyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🎫 使用兑换码", "cmd:redeem"),
),
)
}
if editMsgID != 0 {
b.editMsgWithKeyboard(chatID, editMsgID, caption, &keyboard)
b.mu.Lock()
b.panelMsgs[chatID] = editMsgID
b.mu.Unlock()
} else {
// Send image.png as photo with buttons.
photo := tgbotapi.NewPhoto(chatID, tgbotapi.FilePath("image.png"))
photo.Caption = caption
photo.ParseMode = "Markdown"
photo.ReplyMarkup = keyboard
if sent, err := b.api.Send(photo); err != nil {
// Fallback to text if image not found.
msg := tgbotapi.NewMessage(chatID, caption)
msg.ParseMode = "Markdown"
msg.ReplyMarkup = keyboard
if sent2, err2 := b.api.Send(msg); err2 == nil {
b.mu.Lock()
b.panelMsgs[chatID] = sent2.MessageID
b.mu.Unlock()
}
} else {
b.mu.Lock()
b.panelMsgs[chatID] = sent.MessageID
b.mu.Unlock()
}
}
}
func backButton() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:back"),
),
)
}
func cancelKeyboard() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ 取消", "cmd:cancel"),
),
)
}
func cancelToListKeyboard() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ 取消", "cmd:cancel_to_list"),
),
)
}
func cancelToAdminKeyboard() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("❌ 取消", "cmd:cancel_to_admin"),
),
)
}
func (b *Bot) callbackStock(chatID int64, msgID int) {
count, err := b.db.CountAvailableCodes()
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, "❌ 查询库存失败", &kb)
return
}
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("📦 当前可用兑换码: *%d* 个", count), &kb)
}
func (b *Bot) callbackListAccounts(chatID int64, msgID int) {
accounts, err := b.db.GetAllAccounts()
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 查询失败: %v", err), &kb)
return
}
if len(accounts) == 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔗 登录添加", "cmd:login"),
tgbotapi.NewInlineKeyboardButtonData("🔑 RT添加", "cmd:add_rt"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, "📋 暂无账号\n\n请添加账号", &kb)
return
}
// Limit to first 10.
shown := accounts
if len(shown) > 10 {
shown = shown[:10]
}
var lines []string
for _, a := range shown {
status := "✅"
if a.IsBanned {
status = "🚫"
} else if !a.IsOpen {
status = "⏸"
}
codeCount, _ := b.db.CountAvailableCodesByAccount(a.Email)
expInfo := ""
if a.ExpireAt != "" {
expInfo = fmt.Sprintf(" | 📅 %s", formatDate(a.ExpireAt))
}
invInfo := ""
if a.InviteCount > 0 {
invInfo = fmt.Sprintf(" (+%d待入)", a.InviteCount)
}
accIDShort := a.ChatgptAccountID
if len(accIDShort) > 8 {
accIDShort = accIDShort[:8]
}
lines = append(lines, fmt.Sprintf(
"%s `%s` | 👥 %d%s | 🎫 %d码%s",
status, a.Email, a.UserCount, invInfo,
codeCount, expInfo))
}
text := fmt.Sprintf("📋 *账号列表* (%d 个):\n\n%s", len(accounts), strings.Join(lines, "\n"))
if len(accounts) > 10 {
text += fmt.Sprintf("\n\n_仅显示前 10 个,共 %d 个_", len(accounts))
}
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🗑 删除账号", "act:del_pick"),
tgbotapi.NewInlineKeyboardButtonData("🔄 刷新账号", "act:ref_pick"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 刷新全部", "cmd:refresh_all"),
tgbotapi.NewInlineKeyboardButtonData("📅 查订阅", "cmd:check_sub_all"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔗 登录添加", "cmd:login"),
tgbotapi.NewInlineKeyboardButtonData("🔑 RT添加", "cmd:add_rt"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, text, &kb)
}
// callbackActionPick shows numbered buttons for selecting an account for delete or refresh.
func (b *Bot) callbackActionPick(chatID int64, msgID int, action string, userID int64, page int) {
accounts, err := b.db.GetAllAccounts()
if err != nil || len(accounts) == 0 {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, "❌ 暂无账号", &kb)
return
}
label := "📧 选择要删除的账号"
var backCallback string
// deladmin has its own simple logic without pagination
if action == "deladmin" {
label = "👑 选择要移除的管理员"
backCallback = "cmd:admin_menu"
admins, err := b.db.GetAllAdmins()
if err != nil || len(admins) == 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:admin_menu")))
b.editMsgWithKeyboard(chatID, msgID, "❌ 暂无动态添加的管理员", &kb)
return
}
shownAdmins := admins
if len(shownAdmins) > 10 {
shownAdmins = shownAdmins[:10]
}
var lns []string
var rws [][]tgbotapi.InlineKeyboardButton
var rBtns []tgbotapi.InlineKeyboardButton
for i, a := range shownAdmins {
lns = append(lns, fmt.Sprintf("*%d.* ID: `%d`", i+1, a.UserID))
rBtns = append(rBtns, tgbotapi.NewInlineKeyboardButtonData(
fmt.Sprintf("%d", i+1),
fmt.Sprintf("%s:%d", action, a.UserID),
))
if len(rBtns) == 5 {
rws = append(rws, tgbotapi.NewInlineKeyboardRow(rBtns...))
rBtns = nil
}
}
if len(rBtns) > 0 {
rws = append(rws, tgbotapi.NewInlineKeyboardRow(rBtns...))
}
rws = append(rws, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", backCallback),
))
kba := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rws}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("%s:\n\n%s", label, strings.Join(lns, "\n")), &kba)
return
}
backCallback = "cmd:list_accounts"
if action == "del" {
label = "🗑️ 选择要删除的账号"
} else if action == "ref" {
label = "🔄 选择要刷新的账号"
} else if action == "pending_invite" {
label = "📩 选择管理账户"
backCallback = "cmd:back"
}
// Pagination parameters
pageSize := 8
total := len(accounts)
totalPages := (total + pageSize - 1) / pageSize
if page < 0 {
page = 0
}
if page >= totalPages && totalPages > 0 {
page = totalPages - 1
}
start := page * pageSize
end := start + pageSize
if end > total {
end = total
}
shown := accounts[start:end]
var rows [][]tgbotapi.InlineKeyboardButton
var lns []string
var rBtns []tgbotapi.InlineKeyboardButton
for i, a := range shown {
btnAction := fmt.Sprintf("%s:%d", action, a.ID)
idx := start + i + 1
lns = append(lns, fmt.Sprintf("*%d.* ID: `%d` (%s)", idx, a.ID, a.Email))
rBtns = append(rBtns, tgbotapi.NewInlineKeyboardButtonData(
fmt.Sprintf("%d", idx),
btnAction,
))
// Row break every 5 items
if len(rBtns) == 5 {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(rBtns...))
rBtns = nil
}
}
// Append remaining buttons
if len(rBtns) > 0 {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(rBtns...))
}
// Pagination buttons
var navRow []tgbotapi.InlineKeyboardButton
if page > 0 {
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("⬅️ 上一页", fmt.Sprintf("act_page:%s:%d", action, page-1)))
}
if page < totalPages-1 {
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("下一页 ➡️", fmt.Sprintf("act_page:%s:%d", action, page+1)))
}
if len(navRow) > 0 {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(navRow...))
}
if action == "ref" {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🔄 刷新全部", "cmd:refresh_all"),
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", backCallback),
))
} else {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", backCallback),
))
}
kb := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rows}
pageInfo := ""
if totalPages > 1 {
pageInfo = fmt.Sprintf("\n(第 %d/%d 页)", page+1, totalPages)
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("%s%s:\n\n%s", label, pageInfo, strings.Join(lns, "\n")), &kb)
}
func (b *Bot) callbackDelAccount(chatID int64, msgID int, idStr string) {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return
}
acct, err := b.db.GetAccountByID(id)
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 账号不存在: %v", err), &kb)
return
}
if err := b.db.DeleteAccount(id); err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 删除失败: %v", err), &kb)
return
}
// Show result then go back to list.
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("✅ 已删除账号 ID=%d (%s)\n关联的兑换码也已清除", id, acct.Email), &kb)
}
func (b *Bot) callbackRefAccount(chatID int64, msgID int, idStr string) {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
return
}
acc, err := b.db.GetAccountByID(id)
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 账号不存在: %v", err), &kb)
return
}
if acc.RefreshToken == "" {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, "❌ 该账号未配置 Refresh Token", &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("⏳ 正在刷新 ID=%d...", id), nil)
result, err := b.client.RefreshAccessToken(acc.RefreshToken)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 刷新失败: %s", err.Error()), &kb)
return
}
_ = b.db.UpdateAccountTokens(id, result.AccessToken, result.RefreshToken)
// Sync member counts after token refresh.
acc.Token = result.AccessToken
if userTotal, _, err2 := b.client.GetUsers(acc); err2 == nil {
invTotal := acc.InviteCount
if inv, _, err3 := b.client.GetInvites(acc); err3 == nil {
invTotal = inv
}
_ = b.db.UpdateAccountCounts(id, userTotal, invTotal)
}
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
),
)
msg := fmt.Sprintf("✅ 已刷新账号 ID=%d (%s)", id, acc.Email)
b.editMsgWithKeyboard(chatID, msgID, msg, &kb)
}
func (b *Bot) callbackStatus(chatID int64, msgID int) {
accounts, err := b.db.GetAllAccounts()
if err != nil {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 查询失败: %v", err), &kb)
return
}
totalAccounts := len(accounts)
openAccounts := 0
bannedAccounts := 0
totalUsers := 0
totalInvites := 0
for _, a := range accounts {
if a.IsOpen && !a.IsBanned {
openAccounts++
}
if a.IsBanned {
bannedAccounts++
}
totalUsers += a.UserCount
totalInvites += a.InviteCount
}
codeCount, _ := b.db.CountAvailableCodes()
text := fmt.Sprintf("📊 *系统状态:*\n\n"+
"👥 账号总数: *%d* (开放 %d / 封号 %d)\n"+
"👤 总用户数: *%d* / 总邀请: *%d*\n"+
"🎫 可用兑换码: *%d*",
totalAccounts, openAccounts, bannedAccounts,
totalUsers, totalInvites, codeCount)
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, text, &kb)
}
func (b *Bot) callbackRefresh(chatID int64, msgID int) {
kb := backButton()
b.editMsgWithKeyboard(chatID, msgID, "⏳ 正在刷新全部 Token...", nil)
accs, err := b.db.GetAccountsWithRT()
if err != nil || len(accs) == 0 {
b.editMsgWithKeyboard(chatID, msgID, "❌ 没有可刷新的账号", &kb)
return
}
ok, fail := 0, 0
for _, a := range accs {
result, err := b.client.RefreshAccessToken(a.RefreshToken)
if err != nil {
fail++
continue
}
if err := b.db.UpdateAccountTokens(a.ID, result.AccessToken, result.RefreshToken); err != nil {
fail++
continue
}
// Sync member counts after token refresh.
a.Token = result.AccessToken
if userTotal, _, err2 := b.client.GetUsers(&a); err2 == nil {
invTotal := a.InviteCount
if inv, _, err3 := b.client.GetInvites(&a); err3 == nil {
invTotal = inv
}
_ = b.db.UpdateAccountCounts(a.ID, userTotal, invTotal)
}
ok++
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("✅ 刷新完成: 成功 %d, 失败 %d", ok, fail), &kb)
}
func (b *Bot) callbackCheckSub(chatID int64, msgID int) {
kb := backButton()
accs, err := b.db.GetAllAccounts()
if err != nil || len(accs) == 0 {
b.editMsgWithKeyboard(chatID, msgID, "📋 暂无账号", &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("⏳ 正在查询 %d 个账号的订阅信息...", len(accs)), nil)
var lines []string
for _, acc := range accs {
if acc.Token == "" {
lines = append(lines, fmt.Sprintf("⚠️ ID=%d %s: 无 Token", acc.ID, acc.Email))
continue
}
infos, err := b.client.FetchAccountInfo(acc.Token)
if err != nil {
lines = append(lines, fmt.Sprintf("❌ ID=%d %s: %s", acc.ID, acc.Email, err.Error()))
continue
}
for _, info := range infos {
subStatus := "❌ 无有效订阅"
if info.HasActiveSubscription {
subStatus = "✅ 订阅有效"
}
expiry := formatDate(info.ExpiresAt)
if info.ExpiresAt == "" {
expiry = "未知"
}
lines = append(lines, fmt.Sprintf(
"`%d` %s\n %s | 📅 到期: %s | %s",
acc.ID, acc.Email, info.Name, expiry, subStatus))
if info.ExpiresAt != "" && info.AccountID == acc.ChatgptAccountID {
_ = b.db.UpdateAccountInfo(acc.ID, acc.ChatgptAccountID, info.ExpiresAt)
}
}
}
b.editMsgWithKeyboard(chatID, msgID, "📋 *订阅信息:*\n\n"+strings.Join(lines, "\n\n"), &kb)
}
func (b *Bot) callbackListInvites(chatID int64, msgID int) {
kb := backButton()
accs, err := b.db.GetAllAccounts()
if err != nil || len(accs) == 0 {
b.editMsgWithKeyboard(chatID, msgID, "📋 暂无账号", &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("⏳ 正在查询 %d 个账号的待进入邀请...", len(accs)), nil)
var lines []string
totalInvites := 0
for _, acc := range accs {
if acc.Token == "" {
continue
}
_, invites, err := b.client.GetInvites(&acc)
if err != nil {
lines = append(lines, fmt.Sprintf("❌ ID=%d %s: %s", acc.ID, acc.Email, err.Error()))
continue
}
if len(invites) == 0 {
continue
}
totalInvites += len(invites)
var inviteLines []string
for _, inv := range invites {
inviteLines = append(inviteLines, fmt.Sprintf(" 📧 `%s` (%s)", inv.EmailAddress, inv.Role))
}
lines = append(lines, fmt.Sprintf("*ID=%d* %s (%d 个):\n%s",
acc.ID, acc.Email, len(invites), strings.Join(inviteLines, "\n")))
}
if totalInvites == 0 {
b.editMsgWithKeyboard(chatID, msgID, "📭 暂无待进入的邀请", &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("📧 *待进入邀请* (%d 个):\n\n%s",
totalInvites, strings.Join(lines, "\n\n")), &kb)
}
func (b *Bot) callbackListCodes(chatID int64, msgID int, page int) {
const pageSize = 10
codes, err := b.db.GetAllCodes()
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 查询失败: %v", err), &kb)
return
}
if len(codes) == 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, "📭 暂无兑换码", &kb)
return
}
totalPages := (len(codes) + pageSize - 1) / pageSize
if page < 0 {
page = 0
}
if page >= totalPages {
page = totalPages - 1
}
start := page * pageSize
end := start + pageSize
if end > len(codes) {
end = len(codes)
}
var lines []string
var codeRows [][]tgbotapi.InlineKeyboardButton
var codeBtns []tgbotapi.InlineKeyboardButton
for i, c := range codes[start:end] {
status := "🟢"
extra := ""
if c.IsRedeemed {
status = "🔴"
by := ""
if c.RedeemedBy != nil && *c.RedeemedBy != "" {
by = *c.RedeemedBy
}
extra = fmt.Sprintf(" → %s", by)
} else {
codeBtns = append(codeBtns, tgbotapi.NewInlineKeyboardButtonData(
fmt.Sprintf("🗑️ %d", i+1),
fmt.Sprintf("delcode:%d:%s", page, c.Code),
))
if len(codeBtns) == 5 {
codeRows = append(codeRows, tgbotapi.NewInlineKeyboardRow(codeBtns...))
codeBtns = nil
}
}
lines = append(lines, fmt.Sprintf("%d. %s `%s` (%s)%s", i+1, status, c.Code, c.AccountEmail, extra))
}
if len(codeBtns) > 0 {
codeRows = append(codeRows, tgbotapi.NewInlineKeyboardRow(codeBtns...))
}
// Build keyboard with pagination buttons.
var rows [][]tgbotapi.InlineKeyboardButton
rows = append(rows, codeRows...)
if totalPages > 1 {
var navBtns []tgbotapi.InlineKeyboardButton
if page > 0 {
navBtns = append(navBtns, tgbotapi.NewInlineKeyboardButtonData("⬅️ 上一页", fmt.Sprintf("codes_page:%d", page-1)))
}
navBtns = append(navBtns, tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("%d/%d", page+1, totalPages), "noop"))
if page < totalPages-1 {
navBtns = append(navBtns, tgbotapi.NewInlineKeyboardButtonData("➡️ 下一页", fmt.Sprintf("codes_page:%d", page+1)))
}
rows = append(rows, navBtns)
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
))
kb := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rows}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("🎫 *兑换码列表* (%d 个):\n\n%s", len(codes), strings.Join(lines, "\n")), &kb)
}
func (b *Bot) callbackDelCode(chatID int64, msgID int, page int, code string) {
_ = b.db.DeleteCode(code)
b.callbackListCodes(chatID, msgID, page)
}
func (b *Bot) callbackCodesMenu(chatID int64, msgID int) {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("📋 查看兑换码", "cmd:list_codes_all"),
tgbotapi.NewInlineKeyboardButtonData(" 生成兑换码", "cmd:gen_codes"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🗑️ 一键清空未使用", "cmd:clear_unused_codes"),
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, "🎫 *兑换码管理*\n\n请选择操作", &kb)
}
func (b *Bot) handleMessage(msg *tgbotapi.Message) {
chatID := msg.Chat.ID
userID := msg.From.ID
text := strings.TrimSpace(msg.Text)
// Check two-step session flows (redeem or login).
b.mu.Lock()
sess, hasSess := b.sessions[chatID]
b.mu.Unlock()
if hasSess && !strings.HasPrefix(text, "/") {
switch sess.flowType {
case "redeem_code":
b.deleteMsg(chatID, msg.MessageID)
b.handleRedeemCode(chatID, text)
case "redeem":
b.deleteMsg(chatID, msg.MessageID)
b.handleRedeemEmail(chatID, sess, text)
case "login":
b.handleLoginCallback(chatID, msg.MessageID, text)
case "random_invite":
b.mu.Lock()
panelMsgID := sess.loginMsgID
delete(b.sessions, chatID)
b.mu.Unlock()
b.deleteMsg(chatID, msg.MessageID)
// Inline random invite logic.
email := strings.ToLower(strings.TrimSpace(text))
if email == "" {
return
}
accounts, err := b.db.GetOpenAccounts(b.cfg.TeamCapacity - 1)
if err != nil || len(accounts) == 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, "❌ 暂无可用的空位账号", &kb)
return
}
account := &accounts[0]
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("⏳ 正在邀请到账号 ID=%d (%s)...", account.ID, account.Email), nil)
if err := b.client.InviteUser(email, account); err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("❌ 邀请失败: %s", err.Error()), &kb)
return
}
time.Sleep(time.Second)
syncCounts(b.db, b.client, account)
// Consume one unused redemption code for this account.
if codes, err := b.db.GetCodesByAccount(account.Email); err == nil {
for _, c := range codes {
if !c.IsRedeemed {
_ = b.db.RedeemCode(c.ID, email)
break
}
}
}
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("✅ 已成功邀请 `%s` 到账号 ID=%d (%s)", email, account.ID, account.Email), &kb)
case "gen_codes_accounts":
n, err := strconv.Atoi(strings.TrimSpace(text))
if err != nil || n <= 0 || n > 20 {
b.sendAutoDelete(chatID, "❌ 请输入1-20的整数")
return
}
b.mu.Lock()
panelMsgID := sess.loginMsgID
delete(b.sessions, chatID)
b.mu.Unlock()
b.deleteMsg(chatID, msg.MessageID)
b.handleGenCodes(chatID, panelMsgID, strconv.Itoa(n))
case "add_rt":
rt := strings.TrimSpace(text)
b.mu.Lock()
panelMsgID := sess.loginMsgID
delete(b.sessions, chatID)
b.mu.Unlock()
b.deleteMsg(chatID, msg.MessageID)
b.handleAddAccount(chatID, panelMsgID, rt)
case "add_admin":
idStr := strings.TrimSpace(text)
b.mu.Lock()
panelMsgID := sess.loginMsgID
delete(b.sessions, chatID)
b.mu.Unlock()
b.deleteMsg(chatID, msg.MessageID)
b.handleAddAdminMsg(chatID, panelMsgID, idStr, userID)
}
return
}
if !msg.IsCommand() {
return
}
cmd := msg.Command()
args := strings.TrimSpace(msg.CommandArguments())
// Handle /cancel for any active session.
if cmd == "cancel" {
b.deleteMsg(chatID, msg.MessageID) // delete user's /cancel immediately
b.mu.Lock()
sess, had := b.sessions[chatID]
var panelMsgID int
if had {
panelMsgID = sess.loginMsgID
}
delete(b.sessions, chatID)
b.mu.Unlock()
if had {
if panelMsgID != 0 {
b.sendMainMenu(chatID, userID, panelMsgID)
} else {
replyID := b.sendAndGetID(chatID, "❎ 已取消操作")
go func() {
time.Sleep(20 * time.Second)
b.deleteMsg(chatID, replyID)
}()
}
}
return
}
switch cmd {
case "start":
b.sendMainMenu(chatID, userID, 0)
case "redeem":
b.handleRedeemStart(chatID, args)
case "add_account":
b.requireAdmin(chatID, userID, func() { b.handleAddAccount(chatID, 0, args) })
case "gen_codes":
b.requireAdmin(chatID, userID, func() { b.handleGenCodes(chatID, 0, args) })
case "add_admin": // Super admin only
if b.cfg.IsAdmin(userID) {
b.handleAddAdmin(chatID, userID, args)
} else {
b.sendAutoDelete(chatID, "❌ 权限不足:只有配置文件中的超级管理员才能添加新管理员。")
}
case "login":
b.requireAdmin(chatID, userID, func() { b.handleLogin(chatID, 0) })
}
}
// ─── User Commands ──────────────────────────────────────────
func (b *Bot) handleRedeemStart(chatID int64, args string) {
code := strings.TrimSpace(strings.ToUpper(args))
if code == "" {
// No code provided: start interactive flow.
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "redeem_code"}
b.mu.Unlock()
b.send(chatID, "🎫 请输入兑换码:")
return
}
// Code provided directly: validate and proceed.
b.handleRedeemCode(chatID, code)
}
func (b *Bot) handleRedeemCode(chatID int64, input string) {
code := strings.TrimSpace(strings.ToUpper(input))
// Get the panel message ID from the session.
b.mu.Lock()
panelMsgID := 0
if s, ok := b.sessions[chatID]; ok {
panelMsgID = s.loginMsgID
}
b.mu.Unlock()
backKb := backButton()
// Validate code exists and is unused before asking for email.
rc, err := b.db.GetCodeByCode(code)
if err != nil {
b.mu.Lock()
delete(b.sessions, chatID)
b.mu.Unlock()
if panelMsgID != 0 {
b.editMsgWithKeyboard(chatID, panelMsgID, "❌ 兑换码不存在或已失效", &backKb)
} else {
b.sendAutoDelete(chatID, "❌ 兑换码不存在或已失效")
}
return
}
if rc.IsRedeemed {
b.mu.Lock()
delete(b.sessions, chatID)
b.mu.Unlock()
if panelMsgID != 0 {
b.editMsgWithKeyboard(chatID, panelMsgID, "❌ 该兑换码已被使用", &backKb)
} else {
b.sendAutoDelete(chatID, "❌ 该兑换码已被使用")
}
return
}
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "redeem", code: code, loginMsgID: panelMsgID}
b.mu.Unlock()
if panelMsgID != 0 {
kb := cancelKeyboard()
b.editMsgWithKeyboard(chatID, panelMsgID, "✅ 兑换码有效!请输入您的邮箱地址:", &kb)
} else {
b.send(chatID, "✅ 兑换码有效!请输入您的邮箱地址:")
}
}
func (b *Bot) handleRedeemEmail(chatID int64, sess *chatSession, email string) {
panelMsgID := sess.loginMsgID
b.mu.Lock()
delete(b.sessions, chatID)
b.mu.Unlock()
backKb := backButton()
if panelMsgID != 0 {
b.editMsgWithKeyboard(chatID, panelMsgID, "⏳ 正在处理兑换,请稍候...", nil)
result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity-1)
if err != nil {
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("❌ 兑换失败: %s", err.Error()), &backKb)
return
}
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("🎉 %s", result.Message), &backKb)
} else {
msgID := b.sendAndGetID(chatID, "⏳ 正在处理兑换,请稍候...")
result, err := redeem.Redeem(b.db, b.client, sess.code, email, b.cfg.TeamCapacity-1)
if err != nil {
b.editMsg(chatID, msgID, fmt.Sprintf("❌ 兑换失败: %s", err.Error()))
return
}
b.editMsg(chatID, msgID, fmt.Sprintf("🎉 %s", result.Message))
}
}
func (b *Bot) handleStock(chatID int64) {
count, err := b.db.CountAvailableCodes()
if err != nil {
b.sendAutoDelete(chatID, "❌ 查询库存失败")
return
}
b.send(chatID, fmt.Sprintf("📦 当前可用兑换码: *%d* 个", count))
}
// ─── Admin Commands ─────────────────────────────────────────
func (b *Bot) handleAddAccount(chatID int64, panelMsgID int, args string) {
rt := strings.TrimSpace(args)
if rt == "" {
b.sendAutoDelete(chatID, "❌ 用法: /add\\_account `<refresh_token>`")
return
}
// If panelMsgID is provided, show progress on the panel; otherwise send a new message.
msgID := panelMsgID
if msgID == 0 {
msgID = b.sendAndGetID(chatID, "⏳ 正在刷新 Token 并获取账号信息...")
} else {
b.editMsgWithKeyboard(chatID, msgID, "⏳ 正在刷新 Token 并获取账号信息...", nil)
}
// 1. Refresh to get access token.
tokenResult, err := b.client.RefreshAccessToken(rt)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
if panelMsgID != 0 {
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ Refresh Token 无效: %s", err.Error()), &kb)
} else {
b.editMsg(chatID, msgID, fmt.Sprintf("❌ Refresh Token 无效: %s", err.Error()))
}
return
}
// 2. Fetch account info.
infos, err := b.client.FetchAccountInfo(tokenResult.AccessToken)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
if panelMsgID != 0 {
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 获取账号信息失败: %s", err.Error()), &kb)
} else {
b.editMsg(chatID, msgID, fmt.Sprintf("❌ 获取账号信息失败: %s", err.Error()))
}
return
}
email := tokenResult.GetEmail()
// 3. Create DB records for each team account found.
var lines []string
for _, info := range infos {
accountEmail := email
if accountEmail == "" {
accountEmail = info.Name // Fallback
}
// Check for duplicate by OpenAI Account ID (most reliable)
if existing, err2 := b.db.GetAccountByChatGPTAccountID(info.AccountID); err2 == nil && existing != nil {
lines = append(lines, fmt.Sprintf("⚠️ %s (%s): 已存在 (ID=%d),跳过", info.Name, accountEmail, existing.ID))
continue
}
id, err := b.db.CreateAccount(&model.GptAccount{
Email: accountEmail,
Token: tokenResult.AccessToken,
RefreshToken: tokenResult.RefreshToken,
ChatgptAccountID: info.AccountID,
ExpireAt: info.ExpiresAt,
IsOpen: true,
})
if err != nil {
lines = append(lines, fmt.Sprintf("❌ %s (%s): 创建失败 - %v", info.Name, accountEmail, err))
continue
}
// Generate codes based on remaining capacity: (b.cfg.TeamCapacity - 1) - (current user count + pending invites).
newAcct, _ := b.db.GetAccountByID(id)
codeCount := b.cfg.TeamCapacity
if newAcct != nil {
var userTotal, inviteTotal int
if ut, _, err2 := b.client.GetUsers(newAcct); err2 == nil {
userTotal = ut
}
if it, _, err3 := b.client.GetInvites(newAcct); err3 == nil {
inviteTotal = it
}
codeCount = b.cfg.TeamCapacity - (userTotal + inviteTotal)
_ = b.db.UpdateAccountCounts(id, userTotal, inviteTotal)
}
if codeCount < 0 {
codeCount = 0
}
codes := redeem.GenerateCodes(codeCount)
if err := b.db.CreateCodes(accountEmail, codes); err != nil {
lines = append(lines, fmt.Sprintf("⚠️ ID=%d %s: 账号已创建但生成兑换码失败", id, accountEmail))
continue
}
subInfo := ""
if info.ExpiresAt != "" {
subInfo = fmt.Sprintf("\n 📅 订阅到期: %s", formatDate(info.ExpiresAt))
}
lines = append(lines, fmt.Sprintf("✅ %s | %s%s\n 🎫 已生成 %d 个兑换码",
accountEmail, info.AccountID, subInfo, len(codes)))
}
resultText := "📋 *添加结果:*\n\n" + strings.Join(lines, "\n")
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回列表", "cmd:list_accounts"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, resultText, &kb)
} else {
b.editMsg(chatID, msgID, resultText)
}
}
func (b *Bot) callbackListPendingInvites(chatID int64, msgID int, accountIDStr string) {
accountID, err := strconv.ParseInt(accountIDStr, 10, 64)
if err != nil {
return
}
account, err := b.db.GetAccountByID(accountID)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "act:pending_invites_pick"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 账号不存在: %v", err), &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("⏳ 正在查询账号 ID=%d 的待进入邀请...", accountID), nil)
_, invites, err := b.client.GetInvites(account)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "act:pending_invites_pick"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 查询失败: %v", err), &kb)
return
}
// Sync invite count to DB so the main page status is accurate.
_ = b.db.UpdateAccountCounts(account.ID, account.UserCount, len(invites))
var rows [][]tgbotapi.InlineKeyboardButton
if len(invites) == 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "act:pending_invites_pick"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("📭 账号 ID=%d (%s) 暂无待进入的邀请", accountID, account.Email), &kb)
return
}
text := fmt.Sprintf("📩 *待进入邀请* — ID=%d (%s)\n共 %d 个\n\n点击可删除对应邀请", accountID, account.Email, len(invites))
for _, inv := range invites {
cbData := fmt.Sprintf("delinv:%d:%s", accountID, inv.EmailAddress)
// Telegram callback data has a 64-byte limit.
if len(cbData) <= 64 {
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData(fmt.Sprintf("❌ %s", inv.EmailAddress), cbData),
))
}
}
rows = append(rows, tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "act:pending_invites_pick"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
))
kb := tgbotapi.InlineKeyboardMarkup{InlineKeyboard: rows}
b.editMsgWithKeyboard(chatID, msgID, text, &kb)
}
func (b *Bot) callbackDelInvite(chatID int64, msgID int, data string) {
// data format: <acc_id>:<email>
idx := strings.Index(data, ":")
if idx < 0 {
return
}
accIDStr := data[:idx]
email := data[idx+1:]
accountID, err := strconv.ParseInt(accIDStr, 10, 64)
if err != nil {
return
}
account, err := b.db.GetAccountByID(accountID)
if err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "act:pending_invites_pick"),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 账号不存在: %v", err), &kb)
return
}
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("⏳ 正在删除邀请 %s...", email), nil)
if err := b.client.DeleteInvite(account, email); err != nil {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", fmt.Sprintf("pending_invite:%d", accountID)),
),
)
b.editMsgWithKeyboard(chatID, msgID, fmt.Sprintf("❌ 删除邀请失败: %s", err.Error()), &kb)
return
}
time.Sleep(2 * time.Second)
syncCounts(b.db, b.client, account)
// Re-trigger the list to show updated invites.
b.callbackListPendingInvites(chatID, msgID, accIDStr)
}
func (b *Bot) handleGenCodes(chatID int64, panelMsgID int, args string) {
n, err := strconv.Atoi(strings.TrimSpace(args))
if err != nil || n <= 0 {
b.sendAutoDelete(chatID, "❌ 参数错误")
return
}
accounts, err := b.db.GetAllAccounts()
if err != nil || len(accounts) == 0 {
b.sendAutoDelete(chatID, "❌ 暂无账号")
return
}
// Actually, let's look at the remaining capacity of the account to see how many we CAN generate.
// We'll calculate the available capacity below per account based on TeamCapacity and UserCount.
var eligible []model.GptAccount
for _, acc := range accounts {
if acc.IsBanned || !acc.IsOpen {
continue // Skip banned or closed accounts
}
// We want to generate codes for this account up to the allowed capacity.
existing, _ := b.db.CountAvailableCodesByAccount(acc.Email)
maxCodes := b.cfg.TeamCapacity - acc.UserCount - acc.InviteCount
if maxCodes < 0 {
maxCodes = 0
}
if existing <= maxCodes {
eligible = append(eligible, acc)
}
}
if len(eligible) == 0 {
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, "✅ 所有账号均已满员或已生成充足兑换码,无需继续生成", &kb)
} else {
b.send(chatID, "✅ 所有账号均已满员或已生成充足兑换码,无需继续生成")
}
return
}
if n > len(eligible) {
n = len(eligible)
}
var allLines []string
for i := 0; i < n; i++ {
email := eligible[i].Email
existing, _ := b.db.CountAvailableCodesByAccount(email)
maxCodes := (b.cfg.TeamCapacity - 1) - eligible[i].UserCount - eligible[i].InviteCount
if maxCodes < 0 {
maxCodes = 0
}
needed := maxCodes - existing
if needed <= 0 {
continue
}
codes := redeem.GenerateCodes(needed)
if err := b.db.CreateCodes(email, codes); err != nil {
allLines = append(allLines, fmt.Sprintf("❌ `%s`: %v", email, err))
continue
}
codeList := make([]string, len(codes))
for j, c := range codes {
codeList[j] = fmt.Sprintf("`%s`", c)
}
allLines = append(allLines, fmt.Sprintf("✅ `%s` (+%d个):\n%s",
email, needed, strings.Join(codeList, "\n")))
}
availLeft := len(eligible) - n
suffix := ""
if availLeft > 0 {
suffix = fmt.Sprintf("\n⚠ 还有 %d 个账号可继续生成", availLeft)
}
result := fmt.Sprintf("✅ 已处理 %d 个账号,补充兑换码至名额上限:\n\n%s%s",
n, strings.Join(allLines, "\n\n"), suffix)
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 返回", "cmd:codes_menu"),
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, result, &kb)
} else {
b.send(chatID, result)
}
}
func (b *Bot) handleStatus(chatID int64) {
accounts, err := b.db.GetAllAccounts()
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 查询失败: %v", err))
return
}
total := len(accounts)
open, banned := 0, 0
totalUsers, totalInvites := 0, 0
for _, a := range accounts {
if a.IsBanned {
banned++
} else if a.IsOpen {
open++
}
totalUsers += a.UserCount
totalInvites += a.InviteCount
}
codeCount, _ := b.db.CountAvailableCodes()
b.send(chatID, fmt.Sprintf(
"📊 *系统状态*\n\n"+
"📁 账号总数: *%d*\n"+
" ├ 开放中: *%d*\n"+
" ├ 已封号: *%d*\n"+
" └ 已关闭: *%d*\n\n"+
"👥 总用户数: *%d* | 待邀请: *%d*\n"+
"🎫 可用兑换码: *%d*",
total, open, banned, total-open-banned,
totalUsers, totalInvites, codeCount))
}
func (b *Bot) handleAddAdmin(chatID int64, addedBy int64, args string) {
b.handleAddAdminMsg(chatID, 0, args, addedBy)
}
func (b *Bot) handleAddAdminMsg(chatID int64, panelMsgID int, args string, addedBy int64) {
idStr := strings.TrimSpace(args)
if idStr == "" {
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, "❌ 用法: `/add_admin <TG用户ID>` 或者在面板中输入有效ID", &kb)
} else {
b.sendAutoDelete(chatID, "❌ 用法: /add\\_admin `<TG用户ID>`")
}
return
}
newID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("🏠 主界面", "cmd:back"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, "❌ 用户ID格式错误请输入数字", &kb)
} else {
b.sendAutoDelete(chatID, "❌ 用户ID格式错误请输入数字")
}
return
}
if b.isAdmin(newID) {
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 列表后台", "cmd:admin_menu"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf(" 用户 `%d` 已经是管理员", newID), &kb)
} else {
b.send(chatID, fmt.Sprintf(" 用户 `%d` 已经是管理员", newID))
}
return
}
if err := b.db.AddAdmin(newID, addedBy); err != nil {
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 列表后台", "cmd:admin_menu"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, fmt.Sprintf("❌ 添加失败: %v", err), &kb)
} else {
b.send(chatID, fmt.Sprintf("❌ 添加失败: %v", err))
}
return
}
msg := fmt.Sprintf("✅ 已成功将用户 `%d` 添加为管理员!", newID)
if panelMsgID != 0 {
kb := tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("⬅️ 列表后台", "cmd:admin_menu"),
),
)
b.editMsgWithKeyboard(chatID, panelMsgID, msg, &kb)
} else {
b.send(chatID, msg)
}
}
func (b *Bot) handleLogin(chatID int64, editMsgID int) {
authURL, _, err := b.oauth.GenerateAuthURL()
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 生成登录链接失败: %s", err.Error()))
return
}
text := fmt.Sprintf(
"🔗 *OpenAI 登录链接*\n\n"+
"[点击这里登录](%s)\n\n"+
" 登录完成后,浏览器会跳转到一个无法访问的页面,这是正常的\n"+
"👉 请复制浏览器地址栏中的 *完整 URL* 粘贴回这里",
authURL)
kb := cancelToListKeyboard()
var loginMsgID int
if editMsgID != 0 {
b.editMsgWithKeyboard(chatID, editMsgID, text, &kb)
loginMsgID = editMsgID
} else {
loginMsgID = b.sendAndGetID(chatID, text)
}
b.mu.Lock()
b.sessions[chatID] = &chatSession{flowType: "login", loginMsgID: loginMsgID}
b.mu.Unlock()
// Auto-delete login link after 2 minutes if session is still active.
go func() {
time.Sleep(2 * time.Minute)
b.mu.Lock()
sess, exists := b.sessions[chatID]
if exists && sess.flowType == "login" && sess.loginMsgID == loginMsgID {
delete(b.sessions, chatID)
b.mu.Unlock()
if editMsgID == 0 {
b.deleteMsg(chatID, loginMsgID)
}
} else {
b.mu.Unlock()
}
}()
}
func (b *Bot) handleLoginCallback(chatID int64, pastedMsgID int, callbackURL string) {
// Get session and clean up.
b.mu.Lock()
sess := b.sessions[chatID]
loginMsgID := 0
if sess != nil {
loginMsgID = sess.loginMsgID
}
delete(b.sessions, chatID)
b.mu.Unlock()
// Delete the pasted URL message (contains sensitive code).
b.deleteMsg(chatID, pastedMsgID)
// Edit the original login message to show progress.
b.editMsg(chatID, loginMsgID, "⏳ 正在处理登录信息...")
result, err := b.oauth.ExchangeCallbackURL(callbackURL)
if err != nil {
b.editMsg(chatID, loginMsgID, fmt.Sprintf("❌ 登录失败: %s", err.Error()))
return
}
b.editMsg(chatID, loginMsgID, "✅ Token 获取成功,正在获取账号信息...")
infos, err := b.client.FetchAccountInfo(result.AccessToken)
if err != nil {
id, dbErr := b.db.CreateAccount(&model.GptAccount{
Email: result.Email,
Token: result.AccessToken,
RefreshToken: result.RefreshToken,
ChatgptAccountID: result.AccountID,
IsOpen: true,
})
if dbErr != nil {
b.editMsg(chatID, loginMsgID, fmt.Sprintf("❌ 创建账号失败: %v", dbErr))
return
}
b.editMsg(chatID, loginMsgID, fmt.Sprintf("⚠️ 账号已创建 (ID=%d),但获取详情失败: %s\nRT 已保存。", id, err.Error()))
return
}
var lines []string
for _, info := range infos {
accountEmail := result.Email
if accountEmail == "" {
accountEmail = info.Name // Fallback
}
// Check for duplicate by OpenAI Account ID (most reliable)
if existing, err2 := b.db.GetAccountByChatGPTAccountID(info.AccountID); err2 == nil && existing != nil {
lines = append(lines, fmt.Sprintf("⚠️ %s (%s): 已存在 (ID=%d),跳过", info.Name, accountEmail, existing.ID))
continue
}
id, dbErr := b.db.CreateAccount(&model.GptAccount{
Email: accountEmail,
Token: result.AccessToken,
RefreshToken: result.RefreshToken,
ChatgptAccountID: info.AccountID,
ExpireAt: info.ExpiresAt,
IsOpen: true,
})
if dbErr != nil {
lines = append(lines, fmt.Sprintf("❌ %s (%s): 创建失败 - %v", info.Name, accountEmail, dbErr))
continue
}
subInfo := ""
if info.ExpiresAt != "" {
subInfo = fmt.Sprintf(" | 📅 %s", formatDate(info.ExpiresAt))
}
lines = append(lines, fmt.Sprintf("✅ ID=%d %s%s",
id, info.Name, subInfo))
}
b.editMsg(chatID, loginMsgID, "📋 *登录添加结果:*\n\n"+strings.Join(lines, "\n"))
}
func (b *Bot) handleListCodes(chatID int64, args string) {
target := strings.TrimSpace(args)
if target == "" {
b.sendAutoDelete(chatID, "❌ 用法: /list\\_codes `<账号邮箱|all>`")
return
}
var codes []model.RedemptionCode
var err error
if strings.EqualFold(target, "all") {
codes, err = b.db.GetAllCodes()
} else {
codes, err = b.db.GetCodesByAccount(target)
}
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 查询失败: %v", err))
return
}
if len(codes) == 0 {
b.send(chatID, "📭 暂无兑换码")
return
}
var lines []string
for _, c := range codes {
status := "🟢"
extra := ""
if c.IsRedeemed {
status = "🔴"
by := ""
if c.RedeemedBy != nil && *c.RedeemedBy != "" {
by = *c.RedeemedBy
}
extra = fmt.Sprintf(" → %s", by)
}
lines = append(lines, fmt.Sprintf("%s `%s` (%s)%s", status, c.Code, c.AccountEmail, extra))
}
header := fmt.Sprintf("🎫 *兑换码列表* (%d 个):\n\n", len(codes))
b.send(chatID, header+strings.Join(lines, "\n"))
}
func (b *Bot) handleListInvites(chatID int64, args string) {
target := strings.TrimSpace(args)
if target == "" {
b.sendAutoDelete(chatID, "❌ 用法: /list\\_invites `<账号ID|all>`")
return
}
var accountList []model.GptAccount
if strings.EqualFold(target, "all") {
accs, err := b.db.GetAllAccounts()
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 查询失败: %v", err))
return
}
accountList = accs
} else {
id, err := strconv.ParseInt(target, 10, 64)
if err != nil {
b.sendAutoDelete(chatID, "❌ 账号ID格式错误")
return
}
acc, err := b.db.GetAccountByID(id)
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 账号不存在: %v", err))
return
}
accountList = append(accountList, *acc)
}
if len(accountList) == 0 {
b.send(chatID, "📋 暂无账号")
return
}
msgID := b.sendAndGetID(chatID, fmt.Sprintf("⏳ 正在查询 %d 个账号的待进入邀请...", len(accountList)))
var lines []string
totalInvites := 0
for _, acc := range accountList {
if acc.Token == "" {
continue
}
_, invites, err := b.client.GetInvites(&acc)
if err != nil {
lines = append(lines, fmt.Sprintf("❌ ID=%d %s: %s", acc.ID, acc.Email, err.Error()))
continue
}
if len(invites) == 0 {
continue
}
totalInvites += len(invites)
var inviteLines []string
for _, inv := range invites {
inviteLines = append(inviteLines, fmt.Sprintf(" 📧 `%s` (%s)", inv.EmailAddress, inv.Role))
}
lines = append(lines, fmt.Sprintf("*ID=%d* %s (%d 个):\n%s",
acc.ID, acc.Email, len(invites), strings.Join(inviteLines, "\n")))
}
if totalInvites == 0 {
b.editMsg(chatID, msgID, "📭 暂无待进入的邀请")
return
}
b.editMsg(chatID, msgID, fmt.Sprintf("📋 *待进入邀请* (%d 个):\n\n%s\n\n💡 使用 /kick `<账号ID>` `<邮箱>` 可取消邀请",
totalInvites, strings.Join(lines, "\n\n")))
}
func (b *Bot) handleDelAccount(chatID int64, args string) {
idStr := strings.TrimSpace(args)
if idStr == "" {
b.sendAutoDelete(chatID, "❌ 用法: /del\\_account `<账号ID>`")
return
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
b.sendAutoDelete(chatID, "❌ 账号ID格式错误请输入数字")
return
}
// Get account info first for display.
acct, err := b.db.GetAccountByID(id)
if err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 账号不存在: %v", err))
return
}
if err := b.db.DeleteAccount(id); err != nil {
b.sendAutoDelete(chatID, fmt.Sprintf("❌ 删除失败: %v", err))
return
}
b.send(chatID, fmt.Sprintf("✅ 已删除账号 ID=%d (%s)\n关联的兑换码也已清除", id, acct.Email))
}
// ─── Helpers ────────────────────────────────────────────────
func (b *Bot) send(chatID int64, text string) {
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
msg.DisableWebPagePreview = true
if _, err := b.api.Send(msg); err != nil {
log.Printf("[Bot] 发送消息失败: %v", err)
}
}
// sendAutoDelete sends a message and auto-deletes it after 1 minute.
func (b *Bot) sendAutoDelete(chatID int64, text string) {
msgID := b.sendAndGetID(chatID, text)
if msgID != 0 {
go func() {
time.Sleep(time.Minute)
b.deleteMsg(chatID, msgID)
}()
}
}
func (b *Bot) sendAndGetID(chatID int64, text string) int {
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "Markdown"
msg.DisableWebPagePreview = true
sentMsg, err := b.api.Send(msg)
if err != nil {
log.Printf("[Bot] 发送消息失败: %v", err)
return 0
}
return sentMsg.MessageID
}
func (b *Bot) editMsg(chatID int64, msgID int, text string) {
if msgID == 0 {
b.send(chatID, text)
return
}
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ParseMode = "Markdown"
edit.DisableWebPagePreview = true
if _, err := b.api.Send(edit); err != nil {
// Fallback: try editing as caption (for photo messages).
editCaption := tgbotapi.NewEditMessageCaption(chatID, msgID, text)
editCaption.ParseMode = "Markdown"
if _, err2 := b.api.Send(editCaption); err2 != nil {
log.Printf("[Bot] 编辑消息失败: %v", err2)
}
}
}
func (b *Bot) editMsgWithKeyboard(chatID int64, msgID int, text string, keyboard *tgbotapi.InlineKeyboardMarkup) {
edit := tgbotapi.NewEditMessageText(chatID, msgID, text)
edit.ParseMode = "Markdown"
edit.DisableWebPagePreview = true
if keyboard != nil {
edit.ReplyMarkup = keyboard
}
if _, err := b.api.Send(edit); err != nil {
// Fallback: try editing as caption (for photo messages).
editCaption := tgbotapi.NewEditMessageCaption(chatID, msgID, text)
editCaption.ParseMode = "Markdown"
if keyboard != nil {
editCaption.ReplyMarkup = keyboard
}
if _, err2 := b.api.Send(editCaption); err2 != nil {
log.Printf("[Bot] 编辑消息失败: %v", err2)
}
}
}
func (b *Bot) deleteMsg(chatID int64, msgID int) {
if msgID == 0 {
return
}
del := tgbotapi.NewDeleteMessage(chatID, msgID)
if _, err := b.api.Request(del); err != nil {
log.Printf("[Bot] 删除消息失败: %v", err)
}
}
func (b *Bot) requireAdmin(chatID int64, userID int64, fn func()) {
if !b.cfg.IsAdmin(userID) {
b.send(chatID, "🚫 该命令仅限管理员使用")
return
}
fn()
}
func syncCounts(db *database.DB, client *chatgpt.Client, account *model.GptAccount) {
userTotal, _, err := client.GetUsers(account)
if err != nil {
return
}
inviteTotal, _, err := client.GetInvites(account)
if err != nil {
return
}
_ = db.UpdateAccountCounts(account.ID, userTotal, inviteTotal)
}
// formatDate extracts the date part (YYYY-MM-DD) from an ISO datetime string.
func formatDate(s string) string {
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t.Format("2006-01-02")
}
if len(s) >= 10 {
return s[:10]
}
return s
}