269 lines
9.2 KiB
Go
269 lines
9.2 KiB
Go
package bot
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
|
|
tgbotapi "gopkg.in/telegram-bot-api.v4"
|
|
"transmission-telegram/internal/config"
|
|
"transmission-telegram/internal/logger"
|
|
"transmission-telegram/internal/monitor"
|
|
transmissionClient "transmission-telegram/internal/transmission"
|
|
)
|
|
|
|
// Bot represents the Telegram bot
|
|
type Bot struct {
|
|
api *tgbotapi.BotAPI
|
|
client transmissionClient.Client
|
|
cfg *config.Config
|
|
logger *logger.Logger
|
|
updates <-chan tgbotapi.Update
|
|
monitor *monitor.Monitor
|
|
chatID int64
|
|
chatMu sync.RWMutex
|
|
}
|
|
|
|
// NewBot creates a new bot instance
|
|
func NewBot(api *tgbotapi.BotAPI, client transmissionClient.Client, cfg *config.Config, log *logger.Logger, updates <-chan tgbotapi.Update, mon *monitor.Monitor) *Bot {
|
|
return &Bot{
|
|
api: api,
|
|
client: client,
|
|
cfg: cfg,
|
|
logger: log,
|
|
updates: updates,
|
|
monitor: mon,
|
|
}
|
|
}
|
|
|
|
// Run starts the bot's main loop
|
|
func (b *Bot) Run(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case update := <-b.updates:
|
|
b.handleUpdate(update)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SendMessage sends a message to a chat
|
|
func (b *Bot) SendMessage(chatID int64, text string, markdown bool) int {
|
|
return sendMessage(b.api, chatID, text, markdown)
|
|
}
|
|
|
|
// SendCompletionNotification sends a completion notification to the registered chat
|
|
func (b *Bot) SendCompletionNotification(msg string) {
|
|
b.chatMu.RLock()
|
|
chatID := b.chatID
|
|
b.chatMu.RUnlock()
|
|
|
|
if chatID != 0 {
|
|
b.SendMessage(chatID, msg, false)
|
|
}
|
|
}
|
|
|
|
// safeHandler wraps a handler function with panic recovery
|
|
func (b *Bot) safeHandler(handler func()) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
b.logger.Printf("[ERROR] Panic recovered in handler: %v", r)
|
|
}
|
|
}()
|
|
handler()
|
|
}
|
|
|
|
// commandHandler represents a handler function type
|
|
type commandHandler func(update tgbotapi.Update, tokens []string)
|
|
|
|
// getCommandHandler returns the appropriate handler for a command
|
|
func (b *Bot) getCommandHandler(command string, update tgbotapi.Update, tokens []string) func() {
|
|
// Map of command aliases to handlers
|
|
commandMap := map[string]func(update tgbotapi.Update, tokens []string){
|
|
"list": b.handleList,
|
|
"/list": b.handleList,
|
|
"li": b.handleList,
|
|
"/li": b.handleList,
|
|
"/ls": b.handleList,
|
|
"ls": b.handleList,
|
|
"head": b.handleHead,
|
|
"/head": b.handleHead,
|
|
"he": b.handleHead,
|
|
"/he": b.handleHead,
|
|
"tail": b.handleTail,
|
|
"/tail": b.handleTail,
|
|
"ta": b.handleTail,
|
|
"/ta": b.handleTail,
|
|
"downs": func(u tgbotapi.Update, _ []string) { b.handleDowns(u) },
|
|
"/downs": func(u tgbotapi.Update, _ []string) { b.handleDowns(u) },
|
|
"dg": func(u tgbotapi.Update, _ []string) { b.handleDowns(u) },
|
|
"/dg": func(u tgbotapi.Update, _ []string) { b.handleDowns(u) },
|
|
"seeding": func(u tgbotapi.Update, _ []string) { b.handleSeeding(u) },
|
|
"/seeding": func(u tgbotapi.Update, _ []string) { b.handleSeeding(u) },
|
|
"sd": func(u tgbotapi.Update, _ []string) { b.handleSeeding(u) },
|
|
"/sd": func(u tgbotapi.Update, _ []string) { b.handleSeeding(u) },
|
|
"paused": func(u tgbotapi.Update, _ []string) { b.handlePaused(u) },
|
|
"/paused": func(u tgbotapi.Update, _ []string) { b.handlePaused(u) },
|
|
"pa": func(u tgbotapi.Update, _ []string) { b.handlePaused(u) },
|
|
"/pa": func(u tgbotapi.Update, _ []string) { b.handlePaused(u) },
|
|
"checking": func(u tgbotapi.Update, _ []string) { b.handleChecking(u) },
|
|
"/checking": func(u tgbotapi.Update, _ []string) { b.handleChecking(u) },
|
|
"ch": func(u tgbotapi.Update, _ []string) { b.handleChecking(u) },
|
|
"/ch": func(u tgbotapi.Update, _ []string) { b.handleChecking(u) },
|
|
"active": func(u tgbotapi.Update, _ []string) { b.handleActive(u) },
|
|
"/active": func(u tgbotapi.Update, _ []string) { b.handleActive(u) },
|
|
"ac": func(u tgbotapi.Update, _ []string) { b.handleActive(u) },
|
|
"/ac": func(u tgbotapi.Update, _ []string) { b.handleActive(u) },
|
|
"errors": func(u tgbotapi.Update, _ []string) { b.handleErrors(u) },
|
|
"/errors": func(u tgbotapi.Update, _ []string) { b.handleErrors(u) },
|
|
"er": func(u tgbotapi.Update, _ []string) { b.handleErrors(u) },
|
|
"/er": func(u tgbotapi.Update, _ []string) { b.handleErrors(u) },
|
|
"sort": b.handleSort,
|
|
"/sort": b.handleSort,
|
|
"so": b.handleSort,
|
|
"/so": b.handleSort,
|
|
"trackers": func(u tgbotapi.Update, _ []string) { b.handleTrackers(u) },
|
|
"/trackers": func(u tgbotapi.Update, _ []string) { b.handleTrackers(u) },
|
|
"tr": func(u tgbotapi.Update, _ []string) { b.handleTrackers(u) },
|
|
"/tr": func(u tgbotapi.Update, _ []string) { b.handleTrackers(u) },
|
|
"downloaddir": b.handleDownloadDir,
|
|
"dd": b.handleDownloadDir,
|
|
"add": b.handleAdd,
|
|
"/add": b.handleAdd,
|
|
"ad": b.handleAdd,
|
|
"/ad": b.handleAdd,
|
|
"search": b.handleSearch,
|
|
"/search": b.handleSearch,
|
|
"se": b.handleSearch,
|
|
"/se": b.handleSearch,
|
|
"latest": b.handleLatest,
|
|
"/latest": b.handleLatest,
|
|
"la": b.handleLatest,
|
|
"/la": b.handleLatest,
|
|
"info": b.handleInfo,
|
|
"/info": b.handleInfo,
|
|
"in": b.handleInfo,
|
|
"/in": b.handleInfo,
|
|
"stop": b.handleStop,
|
|
"/stop": b.handleStop,
|
|
"sp": b.handleStop,
|
|
"/sp": b.handleStop,
|
|
"start": b.handleStart,
|
|
"/start": b.handleStart,
|
|
"st": b.handleStart,
|
|
"/st": b.handleStart,
|
|
"check": b.handleCheck,
|
|
"/check": b.handleCheck,
|
|
"ck": b.handleCheck,
|
|
"/ck": b.handleCheck,
|
|
"stats": func(u tgbotapi.Update, _ []string) { b.handleStats(u) },
|
|
"/stats": func(u tgbotapi.Update, _ []string) { b.handleStats(u) },
|
|
"sa": func(u tgbotapi.Update, _ []string) { b.handleStats(u) },
|
|
"/sa": func(u tgbotapi.Update, _ []string) { b.handleStats(u) },
|
|
"downlimit": b.handleDownLimit,
|
|
"dl": b.handleDownLimit,
|
|
"uplimit": b.handleUpLimit,
|
|
"ul": b.handleUpLimit,
|
|
"speed": func(u tgbotapi.Update, _ []string) { b.handleSpeed(u) },
|
|
"/speed": func(u tgbotapi.Update, _ []string) { b.handleSpeed(u) },
|
|
"ss": func(u tgbotapi.Update, _ []string) { b.handleSpeed(u) },
|
|
"/ss": func(u tgbotapi.Update, _ []string) { b.handleSpeed(u) },
|
|
"count": func(u tgbotapi.Update, _ []string) { b.handleCount(u) },
|
|
"/count": func(u tgbotapi.Update, _ []string) { b.handleCount(u) },
|
|
"co": func(u tgbotapi.Update, _ []string) { b.handleCount(u) },
|
|
"/co": func(u tgbotapi.Update, _ []string) { b.handleCount(u) },
|
|
"del": b.handleDel,
|
|
"/del": b.handleDel,
|
|
"rm": b.handleDel,
|
|
"/rm": b.handleDel,
|
|
"deldata": b.handleDelData,
|
|
"/deldata": b.handleDelData,
|
|
"help": func(u tgbotapi.Update, _ []string) { b.SendMessage(u.Message.Chat.ID, HelpText, true) },
|
|
"/help": func(u tgbotapi.Update, _ []string) { b.SendMessage(u.Message.Chat.ID, HelpText, true) },
|
|
"version": func(u tgbotapi.Update, _ []string) { b.handleVersion(u) },
|
|
"/version": func(u tgbotapi.Update, _ []string) { b.handleVersion(u) },
|
|
"ver": func(u tgbotapi.Update, _ []string) { b.handleVersion(u) },
|
|
"/ver": func(u tgbotapi.Update, _ []string) { b.handleVersion(u) },
|
|
"": func(u tgbotapi.Update, _ []string) { b.handleReceiveTorrent(u) },
|
|
}
|
|
|
|
if handler, ok := commandMap[command]; ok {
|
|
return func() { handler(update, tokens) }
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleUpdate processes a Telegram update
|
|
func (b *Bot) handleUpdate(update tgbotapi.Update) {
|
|
if update.Message == nil {
|
|
return
|
|
}
|
|
|
|
// Check if user is master
|
|
if update.Message.From == nil {
|
|
return
|
|
}
|
|
if !b.cfg.IsMaster(update.Message.From.UserName) {
|
|
b.logger.Printf("[INFO] Ignored a message from: %s", update.Message.From.String())
|
|
return
|
|
}
|
|
|
|
// Check if chat is available
|
|
if update.Message.Chat == nil {
|
|
b.logger.Printf("[WARN] Received message without chat information")
|
|
return
|
|
}
|
|
|
|
// Update chatID for completion notifications
|
|
b.chatMu.Lock()
|
|
if b.chatID != update.Message.Chat.ID {
|
|
b.chatID = update.Message.Chat.ID
|
|
if b.monitor != nil {
|
|
b.monitor.SetChatID(update.Message.Chat.ID)
|
|
}
|
|
}
|
|
b.chatMu.Unlock()
|
|
|
|
// Validate message text
|
|
if update.Message.Text == "" {
|
|
return
|
|
}
|
|
|
|
// Tokenize the update
|
|
tokens := strings.Split(update.Message.Text, " ")
|
|
|
|
// Filter out empty tokens
|
|
nonEmptyTokens := make([]string, 0, len(tokens))
|
|
for _, token := range tokens {
|
|
if token != "" {
|
|
nonEmptyTokens = append(nonEmptyTokens, token)
|
|
}
|
|
}
|
|
tokens = nonEmptyTokens
|
|
|
|
// If no tokens after filtering, return
|
|
if len(tokens) == 0 {
|
|
return
|
|
}
|
|
|
|
// Preprocess message based on URL schema
|
|
if len(tokens) > 0 && (strings.HasPrefix(tokens[0], "magnet") || strings.HasPrefix(tokens[0], "http")) {
|
|
tokens = append([]string{"add"}, tokens...)
|
|
}
|
|
|
|
command := strings.ToLower(tokens[0])
|
|
|
|
// Route to appropriate handler with panic recovery
|
|
// Use map for cleaner command routing
|
|
handler := b.getCommandHandler(command, update, tokens[1:])
|
|
if handler != nil {
|
|
go b.safeHandler(handler)
|
|
} else {
|
|
// no such command
|
|
go b.safeHandler(func() { b.SendMessage(update.Message.Chat.ID, "No such command, try /help", false) })
|
|
}
|
|
}
|
|
|