Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 44 additions & 29 deletions internal/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,66 +18,70 @@ import (
"gorm.io/gorm"
)

// Bot represents the Telegram bot
// Bot represents the main Telegram bot controller that handles all interactions with the Telegram API
// and coordinates operations with Real-Debrid and the database.
type Bot struct {
api *bot.Bot
rdClient *realdebrid.Client
middleware *Middleware
config *config.Config
db *gorm.DB
userRepo *db.UserRepository
activityRepo *db.ActivityRepository
torrentRepo *db.TorrentRepository
downloadRepo *db.DownloadRepository
commandRepo *db.CommandRepository
api *bot.Bot // Telegram bot API client
rdClient *realdebrid.Client // Real-Debrid client for torrent operations
middleware *Middleware // Middleware for authorization and rate limiting
config *config.Config // Application configuration
db *gorm.DB // Database connection
userRepo *db.UserRepository // User repository for database operations
activityRepo *db.ActivityRepository // Activity repository for logging user actions
torrentRepo *db.TorrentRepository // Torrent repository for torrent-related operations
downloadRepo *db.DownloadRepository // Download repository for download-related operations
commandRepo *db.CommandRepository // Command repository for command logging
}

// NewBot creates and returns a fully configured Bot.
// NewBot creates and returns a fully configured Bot instance.
// It initializes all components including the Telegram bot, Real-Debrid client,
// middleware, and database repositories.
func NewBot(cfg *config.Config, proxyURL, ipTestURL, ipVerifyURL string) (*Bot, error) {
// Perform IP tests first
// Perform IP tests to verify network configuration
if err := performIPTests(proxyURL, ipTestURL, ipVerifyURL); err != nil {
return nil, fmt.Errorf("IP test failed: %w", err)
}

// Create bot options
// Create bot options with default handler and debug mode if configured
opts := []bot.Option{
bot.WithDefaultHandler(defaultHandler),
}

if cfg.App.LogLevel == "debug" {
opts = append(opts, bot.WithDebug())
}

// Create Telegram bot
// Create Telegram bot with the provided token and options
api, err := bot.New(cfg.Telegram.BotToken, opts...)
if err != nil {
return nil, fmt.Errorf("failed to create bot: %w", err)
}

// Create Real-Debrid client
// Create Real-Debrid client with configuration from the config
rdClient := realdebrid.NewClient(
cfg.RealDebrid.BaseURL,
cfg.RealDebrid.APIToken,
proxyURL,
time.Duration(cfg.RealDebrid.Timeout)*time.Second,
)

// Create middleware
// Create middleware for authorization and rate limiting
middleware := NewMiddleware(cfg)

// Get bot information from Telegram API
me, err := api.GetMe(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to get bot info: %w", err)
}

log.Printf("Authorized on account @%s", me.Username)

// Initialize database
// Initialize database connection
database, err := db.Init(cfg.Database.GetDSN())
if err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err)
}

// Create and return a fully configured Bot instance
return &Bot{
api: api,
rdClient: rdClient,
Expand All @@ -92,15 +96,17 @@ func NewBot(cfg *config.Config, proxyURL, ipTestURL, ipVerifyURL string) (*Bot,
}, nil
}

// Start begins processing updates
// Start begins processing updates from the Telegram API.
// It registers all command handlers and starts the bot's update loop.
func (b *Bot) Start(ctx context.Context) error {
b.registerHandlers()
log.Println("Bot started. Waiting for messages...")
b.api.Start(ctx)
return nil
}

// registerHandlers sets up all command and callback handlers
// registerHandlers sets up all command and callback handlers for the bot.
// It registers handlers for various commands and message types that the bot will respond to.
func (b *Bot) registerHandlers() {
// Command handlers
b.api.RegisterHandler(bot.HandlerTypeMessageText, "/start", bot.MatchTypeExact, b.handleStartCommand)
Expand All @@ -121,7 +127,8 @@ func (b *Bot) registerHandlers() {
b.api.RegisterHandler(bot.HandlerTypeMessageText, "https://", bot.MatchTypePrefix, b.handleHosterLink)
}

// Stop gracefully stops the bot
// Stop gracefully stops the bot and closes the database connection.
// It performs cleanup operations to ensure resources are properly released.
func (b *Bot) Stop() {
log.Println("Bot stopping...")
if err := db.Close(); err != nil {
Expand All @@ -130,12 +137,14 @@ func (b *Bot) Stop() {
log.Println("Bot stopped")
}

// defaultHandler ignores unhandled updates
// defaultHandler ignores unhandled updates.
// This is the default handler that will be called for any updates that don't match registered handlers.
func defaultHandler(ctx context.Context, b *bot.Bot, update *models.Update) {
// Silently ignore
// Silently ignore unhandled updates
}

// getUserFromUpdate extracts user information from an update
// getUserFromUpdate extracts user information from an update object.
// It retrieves the user's ID, username, first name, last name, and chat ID from either a message or callback query.
func (b *Bot) getUserFromUpdate(update *models.Update) (chatID int64, messageThreadID int, username, firstName, lastName string, userID int64) {
if update.Message != nil {
chatID = update.Message.Chat.ID
Expand Down Expand Up @@ -167,7 +176,9 @@ func (b *Bot) getUserFromUpdate(update *models.Update) (chatID int64, messageThr
return
}

// withAuth is a middleware to check authorization and execute the handler
// withAuth is a middleware function that checks user authorization and executes the handler.
// It verifies if the user is allowed to use the bot, creates or updates the user record,
// and executes the provided handler function with the user information.
func (b *Bot) withAuth(ctx context.Context, update *models.Update, handler func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User)) {
chatID, messageThreadID, username, firstName, lastName, userID := b.getUserFromUpdate(update)

Expand Down Expand Up @@ -201,7 +212,8 @@ func (b *Bot) withAuth(ctx context.Context, update *models.Update, handler func(
handler(ctx, chatID, messageThreadID, isSuperAdmin, user)
}

// sendUnauthorizedMessage sends an unauthorized message
// sendUnauthorizedMessage sends an unauthorized access message to the user.
// It informs the user that they are not authorized to use the bot and provides their user ID and chat ID.
func (b *Bot) sendUnauthorizedMessage(ctx context.Context, chatID int64, messageThreadID int, userID int64) {
text := fmt.Sprintf(
"[UNAUTHORIZED]\n\n"+
Expand Down Expand Up @@ -232,7 +244,8 @@ func (b *Bot) sendUnauthorizedMessage(ctx context.Context, chatID int64, message
}
}

// maskUsername masks the username for privacy
// maskUsername masks the username for privacy by replacing the first 5 characters with asterisks.
// This helps protect user privacy by not exposing their full username in logs and messages.
func (b *Bot) maskUsername(username string) string {
if len(username) <= 5 {
return "*****"
Expand All @@ -241,7 +254,9 @@ func (b *Bot) maskUsername(username string) string {
}

// performIPTests performs IP address checks using an optional proxy and test endpoints.
// When ipVerifyURL is provided, it verifies the primary IP (from ipTestURL or default) matches the verification endpoint and returns an error if the primary IP cannot be obtained, the verification request or response parsing fails, or the IPs do not match; it returns nil on success.
// It verifies that the bot can reach the specified IP test URL and, if provided,
// that the verification URL returns the same IP address. This helps ensure proper
// network configuration and proxy settings.
func performIPTests(proxyURL, ipTestURL, ipVerifyURL string) error {
var ipTestClient *http.Client
var primaryIP string
Expand Down
59 changes: 37 additions & 22 deletions internal/bot/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import (
"github.com/go-telegram/bot/models"
)

// handleStartCommand handles the /start command
// handleStartCommand handles the /start command.
// It sends a welcome message to the user with basic information about the bot.
func (b *Bot) handleStartCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand All @@ -33,15 +34,16 @@ func (b *Bot) handleStartCommand(ctx context.Context, tgBot *bot.Bot, update *mo

b.sendHTMLMessage(ctx, chatID, messageThreadID, text)

// Log command
// Log command and activity
if user != nil {
b.commandRepo.LogCommand(user.ID, chatID, user.Username, "start", update.Message.Text, messageThreadID, time.Since(startTime).Milliseconds(), true, "", len(text))
b.activityRepo.LogActivity(user.ID, chatID, user.Username, db.ActivityTypeCommandStart, "start", messageThreadID, true, "", nil)
}
})
}

// handleHelpCommand handles the /help command
// handleHelpCommand handles the /help command.
// It sends a message with a list of all available commands and their descriptions.
func (b *Bot) handleHelpCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand All @@ -50,28 +52,29 @@ func (b *Bot) handleHelpCommand(ctx context.Context, tgBot *bot.Bot, update *mod
text := "<b>🧭 Available Commands</b>\n\n" +
"<b>🎬 Torrent Management:</b>\n" +
"• <code>/list</code> — List all active torrents\n" +
"• <code>/add &lt;magnet&gt;</code> — Add a new torrent via magnet link\n" +
"• <code>/info &lt;id&gt;</code> — Get detailed information about a torrent\n" +
"• <code>/delete &lt;id&gt;</code> — Delete a torrent <i>(superadmin only)</i>\n\n" +
"• <code>/add <magnet></code> — Add a new torrent via magnet link\n" +
"• <code>/info <id></code> — Get detailed information about a torrent\n" +
"• <code>/delete <id></code> — Delete a torrent <i>(superadmin only)</i>\n\n" +
"<b>📦 Hoster Link Management:</b>\n" +
"• <code>/unrestrict &lt;link&gt;</code> — Unrestrict a hoster link\n" +
"• <code>/unrestrict <link></code> — Unrestrict a hoster link\n" +
"• <code>/downloads</code> — List recent downloads\n" +
"• <code>/removelink &lt;id&gt;</code> — Remove a download from history <i>(superadmin only)</i>\n\n" +
"• <code>/removelink <id></code> — Remove a download from history <i>(superadmin only)</i>\n\n" +
"<b>⚙️ General Commands:</b>\n" +
"• <code>/status</code> — Show your Real-Debrid account status\n" +
"• <code>/help</code> — Display this help message"

b.sendHTMLMessage(ctx, chatID, messageThreadID, text)

// Log command
// Log command and activity
if user != nil {
b.commandRepo.LogCommand(user.ID, chatID, user.Username, "help", update.Message.Text, messageThreadID, time.Since(startTime).Milliseconds(), true, "", len(text))
b.activityRepo.LogActivity(user.ID, chatID, user.Username, db.ActivityTypeCommandHelp, "help", messageThreadID, true, "", nil)
}
})
}

// handleListCommand handles the /list command
// handleListCommand handles the /list command.
// It retrieves and displays a list of active torrents from Real-Debrid.
func (b *Bot) handleListCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -140,7 +143,7 @@ func (b *Bot) handleListCommand(ctx context.Context, tgBot *bot.Bot, update *mod
text.WriteString(fmt.Sprintf("<i>Showing the first %d torrents to avoid exceeding message length limits.</i>\n\n", torrentsShown))
}

text.WriteString("Use <code>/info &lt;id&gt;</code> for more details on a specific torrent.")
text.WriteString("Use <code>/info <id></code> for more details on a specific torrent.")
b.sendHTMLMessage(ctx, chatID, messageThreadID, text.String())

if user != nil {
Expand All @@ -150,7 +153,8 @@ func (b *Bot) handleListCommand(ctx context.Context, tgBot *bot.Bot, update *mod
})
}

// handleAddCommand handles the /add command
// handleAddCommand handles the /add command.
// It adds a new torrent to Real-Debrid using a magnet link provided by the user.
func (b *Bot) handleAddCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -205,7 +209,8 @@ func (b *Bot) handleAddCommand(ctx context.Context, tgBot *bot.Bot, update *mode
})
}

// handleInfoCommand handles the /info command
// handleInfoCommand handles the /info command.
// It retrieves and displays detailed information about a specific torrent.
func (b *Bot) handleInfoCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand All @@ -229,7 +234,8 @@ func (b *Bot) handleInfoCommand(ctx context.Context, tgBot *bot.Bot, update *mod
})
}

// sendTorrentInfo sends detailed torrent information
// sendTorrentInfo sends detailed torrent information to the user.
// It can either send a new message or update an existing one if messageID is provided.
func (b *Bot) sendTorrentInfo(ctx context.Context, chatID int64, messageThreadID int, torrentID string, user *db.User, messageID int) {
torrent, err := b.rdClient.GetTorrentInfo(torrentID)
if err != nil {
Expand Down Expand Up @@ -280,7 +286,8 @@ func (b *Bot) sendTorrentInfo(ctx context.Context, chatID int64, messageThreadID
}
}

// handleDeleteCommand handles the /delete command
// handleDeleteCommand handles the /delete command.
// It deletes a torrent from Real-Debrid. Only superadmins can use this command.
func (b *Bot) handleDeleteCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -324,7 +331,8 @@ func (b *Bot) handleDeleteCommand(ctx context.Context, tgBot *bot.Bot, update *m
})
}

// handleUnrestrictCommand handles the /unrestrict command
// handleUnrestrictCommand handles the /unrestrict command.
// It unrestricts a hoster link provided by the user.
func (b *Bot) handleUnrestrictCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -370,7 +378,8 @@ func (b *Bot) handleUnrestrictCommand(ctx context.Context, tgBot *bot.Bot, updat
})
}

// handleDownloadsCommand handles the /downloads command
// handleDownloadsCommand handles the /downloads command.
// It retrieves and displays a list of recent downloads from Real-Debrid.
func (b *Bot) handleDownloadsCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -421,7 +430,7 @@ func (b *Bot) handleDownloadsCommand(ctx context.Context, tgBot *bot.Bot, update
downloadsShown++
}

text.WriteString("Use <code>/removelink &lt;id&gt;</code> to remove an item from this list.")
text.WriteString("Use <code>/removelink <id></code> to remove an item from this list.")
b.sendHTMLMessage(ctx, chatID, messageThreadID, text.String())

if user != nil {
Expand All @@ -431,7 +440,8 @@ func (b *Bot) handleDownloadsCommand(ctx context.Context, tgBot *bot.Bot, update
})
}

// handleRemoveLinkCommand handles the /removelink command
// handleRemoveLinkCommand handles the /removelink command.
// It removes a download from Real-Debrid's history. Only superadmins can use this command.
func (b *Bot) handleRemoveLinkCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -475,7 +485,8 @@ func (b *Bot) handleRemoveLinkCommand(ctx context.Context, tgBot *bot.Bot, updat
})
}

// handleStatusCommand handles the /status command
// handleStatusCommand handles the /status command.
// It retrieves and displays the user's Real-Debrid account status.
func (b *Bot) handleStatusCommand(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -521,7 +532,8 @@ func (b *Bot) handleStatusCommand(ctx context.Context, tgBot *bot.Bot, update *m
})
}

// handleMagnetLink handles magnet links sent as messages
// handleMagnetLink handles magnet links sent as messages.
// It adds the magnet link as a new torrent to Real-Debrid.
func (b *Bot) handleMagnetLink(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -558,7 +570,8 @@ func (b *Bot) handleMagnetLink(ctx context.Context, tgBot *bot.Bot, update *mode
})
}

// handleHosterLink handles hoster links sent as messages
// handleHosterLink handles hoster links sent as messages.
// It unrestricts the hoster link using Real-Debrid.
func (b *Bot) handleHosterLink(ctx context.Context, tgBot *bot.Bot, update *models.Update) {
b.withAuth(ctx, update, func(ctx context.Context, chatID int64, messageThreadID int, isSuperAdmin bool, user *db.User) {
startTime := time.Now()
Expand Down Expand Up @@ -597,6 +610,7 @@ func (b *Bot) handleHosterLink(ctx context.Context, tgBot *bot.Bot, update *mode

// --- Helper Functions ---

// sendMessage sends a plain text message to the specified chat.
func (b *Bot) sendMessage(ctx context.Context, chatID int64, messageThreadID int, text string) {
params := &bot.SendMessageParams{
ChatID: chatID,
Expand All @@ -613,6 +627,7 @@ func (b *Bot) sendMessage(ctx context.Context, chatID int64, messageThreadID int
}
}

// sendHTMLMessage sends an HTML-formatted message to the specified chat.
func (b *Bot) sendHTMLMessage(ctx context.Context, chatID int64, messageThreadID int, text string) {
params := &bot.SendMessageParams{
ChatID: chatID,
Expand Down
Loading