Files
DragonCoreSSH-NewWEB/bot_core.go
T
2026-07-13 00:17:55 -03:00

268 lines
6.7 KiB
Go

package main
// bot_core.go — Bot lifecycle, Telegram update dispatch, and shared helpers.
import (
"context"
"log"
"strings"
"sync"
"time"
)
// Global bot instance (nil when disabled). Guarded by botMgrMu.
var (
botMgr *Bot
botMgrMu sync.Mutex
)
func currentBot() *Bot {
botMgrMu.Lock()
defer botMgrMu.Unlock()
return botMgr
}
type Bot struct {
store *Store
cfg *BotConfig
tg *tgClient
mp *mpClient
ctx context.Context
cancel context.CancelFunc
}
func newBot(store *Store, cfg *BotConfig) *Bot {
ctx, cancel := context.WithCancel(context.Background())
b := &Bot{
store: store,
cfg: cfg,
tg: newTGClient(cfg.TelegramToken),
ctx: ctx,
cancel: cancel,
}
if cfg.MPAccessToken != "" {
b.mp = newMPClient(cfg.MPAccessToken)
}
return b
}
// ---------- Lifecycle (called from main.go boot and bot_api.go on save) ----------
// startBotService loads config from the DB and starts the bot if enabled.
func startBotService(store *Store) {
if store == nil {
return
}
cfg, err := LoadBotConfig(context.Background(), store)
if err != nil {
log.Printf("[bot] load config: %v", err)
return
}
botMgrMu.Lock()
defer botMgrMu.Unlock()
if botMgr != nil {
botMgr.cancel()
botMgr = nil
}
if !cfg.Enabled {
log.Printf("[bot] disabled")
return
}
if cfg.TelegramToken == "" {
log.Printf("[bot] enabled but no telegram token configured; not starting")
return
}
b := newBot(store, cfg)
botMgr = b
b.start()
}
// reloadBotService restarts the bot after a config change.
func reloadBotService(store *Store) { startBotService(store) }
func (b *Bot) start() {
log.Printf("[bot] starting (telegram=long-polling, mp_confirm=%s)", b.cfg.MPConfirmMode)
// Telegram uses long-polling only. Clearing any stale webhook + polling both
// hit the network, so run off the goroutine that holds botMgrMu.
go func() {
_ = b.tg.deleteWebhook(b.ctx)
b.runPolling()
}()
if b.mp != nil && b.cfg.MPConfirmMode == "polling" {
go b.runPaymentPoller()
}
}
func (b *Bot) stop() { b.cancel() }
// ---------- Update polling ----------
func (b *Bot) runPolling() {
var offset int64
log.Printf("[bot] long-polling started")
for {
select {
case <-b.ctx.Done():
log.Printf("[bot] polling stopped")
return
default:
}
ups, err := b.tg.getUpdates(b.ctx, offset, 50)
if err != nil {
if b.ctx.Err() != nil {
return
}
log.Printf("[bot] getUpdates: %v", err)
time.Sleep(3 * time.Second)
continue
}
for i := range ups {
u := ups[i]
if u.UpdateID >= offset {
offset = u.UpdateID + 1
}
b.handleUpdate(&u)
}
}
}
// ---------- Dispatch ----------
func (b *Bot) handleUpdate(u *tgUpdate) {
defer func() {
if r := recover(); r != nil {
log.Printf("[bot] panic handling update: %v", r)
}
}()
switch {
case u.CallbackQuery != nil:
b.handleCallback(u.CallbackQuery)
case u.Message != nil && u.Message.From != nil:
b.handleMessage(u.Message)
}
}
func (b *Bot) handleMessage(m *tgMessage) {
b.touchUser(m.From)
text := strings.TrimSpace(m.Text)
switch {
case text == "/start" || text == "/menu" || text == "start":
b.showMainMenu(m.Chat.ID, m.From, 0)
case strings.HasPrefix(text, "/stats") && b.cfg.isAdmin(m.From.ID):
b.cmdAdminStats(m.Chat.ID)
case strings.HasPrefix(text, "/addcredit") && b.cfg.isAdmin(m.From.ID):
b.cmdAdminAddCredit(m.Chat.ID, text)
default:
b.showMainMenu(m.Chat.ID, m.From, 0)
}
}
func (b *Bot) handleCallback(cb *tgCallbackQuery) {
b.touchUser(&cb.From)
_ = b.tg.answerCallback(b.ctx, cb.ID, "")
if cb.Message == nil {
return
}
chatID := cb.Message.Chat.ID
msgID := cb.Message.MessageID
data := cb.Data
switch {
case data == "menu:main":
b.showMainMenu(chatID, &cb.From, msgID)
case data == "buy":
b.showPlanList(chatID, msgID, "ssh_or_xray", "buy")
case strings.HasPrefix(data, "buy:"):
b.startPlanPurchase(chatID, &cb.From, data[len("buy:"):], false)
case data == "renew":
b.showRenewList(chatID, &cb.From, msgID)
case strings.HasPrefix(data, "renew:"):
b.startRenew(chatID, &cb.From, msgID, data[len("renew:"):])
case strings.HasPrefix(data, "rnw:"):
b.startRenewPayment(chatID, &cb.From, data[len("rnw:"):])
case data == "trial":
b.handleTrial(chatID, &cb.From)
case data == "purchases":
b.showPurchases(chatID, &cb.From, msgID)
case data == "app":
b.showText(chatID, msgID, "app_text", "📥 App: (configure em bot_settings)")
case data == "contact":
b.showText(chatID, msgID, "contact_text", "👤 Contato: (configure em bot_settings)")
case data == "res:menu":
b.showResellerMenu(chatID, &cb.From, msgID)
case data == "res:topup":
b.showCreditPackages(chatID, msgID)
case strings.HasPrefix(data, "res:topup:"):
b.startTopup(chatID, &cb.From, data[len("res:topup:"):])
case data == "res:create":
b.showPlanList(chatID, msgID, "ssh_or_xray", "res:create")
case strings.HasPrefix(data, "res:create:"):
b.resellerCreateAccount(chatID, &cb.From, data[len("res:create:"):])
case data == "res:clients":
b.showResellerClients(chatID, &cb.From, msgID)
case strings.HasPrefix(data, "pay:check:"):
b.checkPaymentButton(chatID, &cb.From, data[len("pay:check:"):])
default:
// unknown — refresh menu
b.showMainMenu(chatID, &cb.From, msgID)
}
}
// ---------- User helpers ----------
func (b *Bot) touchUser(u *tgUser) {
if u == nil {
return
}
_ = b.store.UpsertBotUser(b.ctx, &BotUser{
TelegramID: u.ID,
Username: u.Username,
FirstName: u.FirstName,
})
}
func (b *Bot) botUser(telegramID int64) *BotUser {
u, err := b.store.GetBotUser(b.ctx, telegramID)
if err != nil {
return &BotUser{TelegramID: telegramID, Role: "customer"}
}
return u
}
// ---------- Message helpers ----------
func (b *Bot) send(chatID int64, text string, kb *tgInlineKeyboard) {
if _, err := b.tg.sendMessage(b.ctx, chatID, text, kb); err != nil {
log.Printf("[bot] sendMessage: %v", err)
}
}
// sendOrEdit edits an existing message if msgID>0, else sends a new one.
func (b *Bot) sendOrEdit(chatID, msgID int64, text string, kb *tgInlineKeyboard) {
if msgID > 0 {
if err := b.tg.editMessageText(b.ctx, chatID, msgID, text, kb); err == nil {
return
}
}
b.send(chatID, text, kb)
}
func (b *Bot) showText(chatID, msgID int64, key, def string) {
txt := b.store.GetSetting(b.ctx, key, def)
b.sendOrEdit(chatID, msgID, txt, backKeyboard())
}
// ---------- Keyboard builders ----------
func kb(rows ...[]tgInlineButton) *tgInlineKeyboard {
return &tgInlineKeyboard{InlineKeyboard: rows}
}
func btn(text, data string) tgInlineButton { return tgInlineButton{Text: text, CallbackData: data} }
func urlBtn(text, u string) tgInlineButton { return tgInlineButton{Text: text, URL: u} }
func backKeyboard() *tgInlineKeyboard {
return kb([]tgInlineButton{btn("⬅️ Voltar", "menu:main")})
}