Files
transmission-telegram/internal/bot/handlers.go

1059 lines
29 KiB
Go

package bot
import (
"bytes"
"context"
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/dustin/go-humanize"
"github.com/pyed/transmission"
tgbotapi "gopkg.in/telegram-bot-api.v4"
"transmission-telegram/internal/config"
"transmission-telegram/internal/formatter"
)
const (
// maxRegexPatternLength limits regex pattern length to prevent ReDoS attacks
maxRegexPatternLength = 1000
// regexCompileTimeout is the maximum time allowed for regex compilation
regexCompileTimeout = 5 * time.Second
)
var (
trackerRegex = regexp.MustCompile(`[https?|udp]://([^:/]*)`)
// bufferPool is a pool for reusing bytes.Buffer instances
bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// regexCache caches compiled regular expressions
regexCache = sync.Map{}
)
// getCompiledRegex gets or compiles a regex pattern with caching
// Includes protection against ReDoS attacks
func getCompiledRegex(pattern string) (*regexp.Regexp, error) {
// Validate pattern length to prevent ReDoS
if len(pattern) > maxRegexPatternLength {
return nil, fmt.Errorf("regex pattern too long (max %d characters)", maxRegexPatternLength)
}
// Check cache first
if cached, ok := regexCache.Load(pattern); ok {
// Safe type assertion with check
if regx, ok := cached.(*regexp.Regexp); ok {
return regx, nil
}
// Type mismatch - remove from cache and continue
regexCache.Delete(pattern)
}
// Compile with timeout protection
ctx, cancel := context.WithTimeout(context.Background(), regexCompileTimeout)
defer cancel()
type result struct {
regx *regexp.Regexp
err error
}
resultCh := make(chan result, 1)
go func() {
regx, err := regexp.Compile("(?i)" + pattern)
select {
case resultCh <- result{regx: regx, err: err}:
case <-ctx.Done():
// Context cancelled, goroutine exits without blocking
}
}()
select {
case <-ctx.Done():
patternPreview := pattern
if len(patternPreview) > 50 {
patternPreview = patternPreview[:50]
}
return nil, fmt.Errorf("regex compilation timeout (pattern may be too complex): %s", patternPreview)
case res := <-resultCh:
if res.err != nil {
return nil, fmt.Errorf("regex compilation error: %w", res.err)
}
regexCache.Store(pattern, res.regx)
return res.regx, nil
}
}
// minInt returns the minimum of two integers
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
// getBuffer gets a buffer from the pool
func getBuffer() *bytes.Buffer {
item := bufferPool.Get()
if buf, ok := item.(*bytes.Buffer); ok {
return buf
}
// Type mismatch - return a new buffer
return new(bytes.Buffer)
}
// putBuffer returns a buffer to the pool after resetting it
func putBuffer(buf *bytes.Buffer) {
buf.Reset()
bufferPool.Put(buf)
}
// rateLimitWait waits for the next available API call slot
// This function is defined in helpers.go to access the rate limiter
// handleClientError sends an error message to the user and logs it
func (b *Bot) handleClientError(chatID int64, prefix string, err error) {
if err != nil {
b.logger.Printf("[ERROR] %s: %v", prefix, err)
b.SendMessage(chatID, prefix+": "+err.Error(), false)
}
}
// handleTorrentList is a generic handler for listing torrents with filtering and formatting
func (b *Bot) handleTorrentList(update tgbotapi.Update, errorPrefix, emptyMessage string, filter func(*transmission.Torrent) bool, format func(*transmission.Torrent) string) {
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, errorPrefix, err)
return
}
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents {
if filter(torrents[i]) {
buf.WriteString(format(torrents[i]))
}
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, emptyMessage, false)
return
}
b.SendMessage(update.Message.Chat.ID, buf.String(), false)
}
// Handler methods for bot commands - these are placeholders that need full implementation
// For now, they provide basic functionality
func (b *Bot) handleList(update tgbotapi.Update, tokens []string) {
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*list*", err)
return
}
buf := getBuffer()
defer putBuffer(buf)
if len(tokens) != 0 {
regx, err := getCompiledRegex(tokens[0])
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*list*", err)
return
}
for i := range torrents {
if regx.MatchString(torrents[i].GetTrackers()) {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
} else {
for i := range torrents {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
if len(tokens) != 0 {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*list:* No tracker matches: *%s*", tokens[0]), true)
return
}
b.SendMessage(update.Message.Chat.ID, "*list:* no torrents", false)
return
}
b.SendMessage(update.Message.Chat.ID, buf.String(), false)
}
func (b *Bot) handleHead(update tgbotapi.Update, tokens []string) {
n := 5
if len(tokens) > 0 {
if parsed, err := strconv.Atoi(tokens[0]); err == nil {
n = parsed
}
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*head*", err)
return
}
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents[:n] {
buf.WriteString(formatter.FormatTorrentDetailed(torrents[i]))
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "*head:* no torrents", false)
return
}
msgID := b.SendMessage(update.Message.Chat.ID, buf.String(), true)
if !b.cfg.NoLive {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(b.cfg.Duration)*b.cfg.Interval+time.Second)
defer cancel()
b.liveUpdateTorrents(ctx, update.Message.Chat.ID, msgID, func(torrents transmission.Torrents) transmission.Torrents {
if n > len(torrents) {
return torrents[:len(torrents)]
}
return torrents[:n]
}, formatter.FormatTorrentDetailed)
}
}
func (b *Bot) handleTail(update tgbotapi.Update, tokens []string) {
n := 5
if len(tokens) > 0 {
if parsed, err := strconv.Atoi(tokens[0]); err == nil {
n = parsed
}
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*tail*", err)
return
}
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
buf := getBuffer()
defer putBuffer(buf)
for _, torrent := range torrents[len(torrents)-n:] {
buf.WriteString(formatter.FormatTorrentDetailed(torrent))
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "*tail:* no torrents", false)
return
}
msgID := b.SendMessage(update.Message.Chat.ID, buf.String(), true)
if !b.cfg.NoLive {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(b.cfg.Duration)*b.cfg.Interval+time.Second)
defer cancel()
b.liveUpdateTorrents(ctx, update.Message.Chat.ID, msgID, func(torrents transmission.Torrents) transmission.Torrents {
if n > len(torrents) {
return torrents
}
return torrents[len(torrents)-n:]
}, formatter.FormatTorrentDetailed)
}
}
func (b *Bot) handleDowns(update tgbotapi.Update) {
b.handleTorrentList(update, "*downs*", "No downloads",
func(t *transmission.Torrent) bool {
return t.Status == transmission.StatusDownloading ||
t.Status == transmission.StatusDownloadPending
},
func(t *transmission.Torrent) string {
return formatter.FormatTorrentShort(t) + "\n"
})
}
func (b *Bot) handleSeeding(update tgbotapi.Update) {
b.handleTorrentList(update, "*seeding*", "No torrents seeding",
func(t *transmission.Torrent) bool {
return t.Status == transmission.StatusSeeding ||
t.Status == transmission.StatusSeedPending
},
func(t *transmission.Torrent) string {
return formatter.FormatTorrentShort(t) + "\n"
})
}
func (b *Bot) handlePaused(update tgbotapi.Update) {
b.handleTorrentList(update, "*paused*", "No paused torrents",
func(t *transmission.Torrent) bool {
return t.Status == transmission.StatusStopped
},
func(t *transmission.Torrent) string {
return formatter.FormatTorrentPaused(t)
})
}
func (b *Bot) handleChecking(update tgbotapi.Update) {
b.handleTorrentList(update, "*checking*", "No torrents verifying",
func(t *transmission.Torrent) bool {
return t.Status == transmission.StatusChecking ||
t.Status == transmission.StatusCheckPending
},
func(t *transmission.Torrent) string {
return formatter.FormatTorrentChecking(t)
})
}
func (b *Bot) handleActive(update tgbotapi.Update) {
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*active*", err)
return
}
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents {
if torrents[i].RateDownload > 0 || torrents[i].RateUpload > 0 {
buf.WriteString(formatter.FormatTorrentDetailed(torrents[i]))
}
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "No active torrents", false)
return
}
msgID := b.SendMessage(update.Message.Chat.ID, buf.String(), true)
if !b.cfg.NoLive {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(b.cfg.Duration)*b.cfg.Interval+time.Second)
defer cancel()
b.liveUpdateActive(ctx, update.Message.Chat.ID, msgID)
}
}
func (b *Bot) handleErrors(update tgbotapi.Update) {
b.handleTorrentList(update, "*errors*", "No errors",
func(t *transmission.Torrent) bool {
return t.Error != 0
},
func(t *transmission.Torrent) string {
return fmt.Sprintf("<%d> %s\n%s\n", t.ID, t.Name, t.ErrorString)
})
}
func (b *Bot) handleSort(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, `*sort* takes one of:
(*id, name, age, size, progress, downspeed, upspeed, download, upload, ratio*)
optionally start with (*rev*) for reversed order
e.g. "*sort rev size*" to get biggest torrents first.`, true)
return
}
var reversed bool
if strings.ToLower(tokens[0]) == "rev" {
reversed = true
tokens = tokens[1:]
}
sortMap := map[string]struct {
normal transmission.Sorting
rev transmission.Sorting
}{
"id": {transmission.SortID, transmission.SortRevID},
"name": {transmission.SortName, transmission.SortRevName},
"age": {transmission.SortAge, transmission.SortRevAge},
"size": {transmission.SortSize, transmission.SortRevSize},
"progress": {transmission.SortProgress, transmission.SortRevProgress},
"downspeed": {transmission.SortDownSpeed, transmission.SortRevDownSpeed},
"upspeed": {transmission.SortUpSpeed, transmission.SortRevUpSpeed},
"download": {transmission.SortDownloaded, transmission.SortRevDownloaded},
"upload": {transmission.SortUploaded, transmission.SortRevUploaded},
"ratio": {transmission.SortRatio, transmission.SortRevRatio},
}
if sort, ok := sortMap[strings.ToLower(tokens[0])]; ok {
if reversed {
b.client.SetSort(sort.rev)
b.SendMessage(update.Message.Chat.ID, "*sort:* reversed "+tokens[0], false)
} else {
b.client.SetSort(sort.normal)
b.SendMessage(update.Message.Chat.ID, "*sort:* "+tokens[0], false)
}
} else {
b.SendMessage(update.Message.Chat.ID, "unknown sorting method", false)
}
}
func (b *Bot) handleTrackers(update tgbotapi.Update) {
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*trackers*", err)
return
}
trackers := make(map[string]int)
for i := range torrents {
for _, tracker := range torrents[i].Trackers {
sm := trackerRegex.FindSubmatch([]byte(tracker.Announce))
if len(sm) > 1 {
currentTracker := string(sm[1])
trackers[currentTracker]++
}
}
}
buf := getBuffer()
defer putBuffer(buf)
for k, v := range trackers {
buf.WriteString(fmt.Sprintf("%d - %s\n", v, k))
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "No trackers!", false)
return
}
b.SendMessage(update.Message.Chat.ID, buf.String(), false)
}
func (b *Bot) handleDownloadDir(update tgbotapi.Update, tokens []string) {
if len(tokens) < 1 {
b.SendMessage(update.Message.Chat.ID, "Please, specify a path for downloaddir", false)
return
}
cmd := transmission.NewSessionSetCommand()
cmd.SetDownloadDir(tokens[0])
out, err := b.client.ExecuteCommand(cmd)
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*downloaddir*", err)
return
}
if out.Result != "success" {
b.SendMessage(update.Message.Chat.ID, "*downloaddir:* "+out.Result, false)
return
}
b.SendMessage(update.Message.Chat.ID, "*downloaddir:* downloaddir has been successfully changed to "+tokens[0], false)
}
func (b *Bot) handleAdd(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*add:* needs at least one URL", false)
return
}
for _, url := range tokens {
cmd := transmission.NewAddCmdByURL(url)
torrent, err := b.client.ExecuteAddCommand(cmd)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*add:* "+err.Error(), false)
continue
}
if torrent.Name == "" {
b.SendMessage(update.Message.Chat.ID, "*add:* error adding "+url, false)
continue
}
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*Added:* <%d> %s", torrent.ID, torrent.Name), false)
}
}
func (b *Bot) handleSearch(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*search:* needs an argument", false)
return
}
query := strings.Join(tokens, " ")
regx, err := getCompiledRegex(query)
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*search*", err)
return
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*search*", err)
return
}
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents {
if regx.MatchString(torrents[i].Name) {
buf.WriteString(formatter.FormatTorrentShort(torrents[i]) + "\n")
}
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "No matches!", false)
return
}
b.SendMessage(update.Message.Chat.ID, buf.String(), false)
}
func (b *Bot) handleLatest(update tgbotapi.Update, tokens []string) {
n := 5
if len(tokens) > 0 {
if parsed, err := strconv.Atoi(tokens[0]); err == nil {
n = parsed
}
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*latest*", err)
return
}
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
torrents.SortAge(true)
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents[:n] {
buf.WriteString(formatter.FormatTorrentShort(torrents[i]) + "\n")
}
if buf.Len() == 0 {
b.SendMessage(update.Message.Chat.ID, "*latest:* No torrents", false)
return
}
b.SendMessage(update.Message.Chat.ID, buf.String(), false)
}
func (b *Bot) handleInfo(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*info:* needs a torrent ID number", false)
return
}
for _, id := range tokens {
torrentID, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*info:* %s is not a number", id), false)
continue
}
torrent, err := b.client.GetTorrent(torrentID)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*info:* Can't find a torrent with an ID of %d", torrentID), false)
continue
}
trackers := formatter.ExtractTrackers(torrent, trackerRegex)
info := formatter.FormatTorrentInfo(torrent, trackers)
msgID := b.SendMessage(update.Message.Chat.ID, info, true)
if !b.cfg.NoLive {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(b.cfg.Duration)*b.cfg.Interval+time.Second)
defer cancel()
b.liveUpdateInfo(ctx, update.Message.Chat.ID, msgID, torrentID, trackers)
}
}
}
func (b *Bot) handleStop(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*stop:* needs an argument", false)
return
}
if tokens[0] == "all" {
if err := b.client.StopAll(); err != nil {
b.SendMessage(update.Message.Chat.ID, "*stop:* error occurred while stopping some torrents", false)
return
}
b.SendMessage(update.Message.Chat.ID, "Stopped all torrents", false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*stop:* %s is not a number", id), false)
continue
}
status, err := b.client.StopTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*stop:* "+err.Error(), false)
continue
}
torrent, err := b.client.GetTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[fail] *stop:* No torrent with an ID of %d", num), false)
continue
}
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[%s] *stop:* %s", status, torrent.Name), false)
}
}
func (b *Bot) handleStart(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*start:* needs an argument", false)
return
}
if tokens[0] == "all" {
if err := b.client.StartAll(); err != nil {
b.SendMessage(update.Message.Chat.ID, "*start:* error occurred while starting some torrents", false)
return
}
b.SendMessage(update.Message.Chat.ID, "Started all torrents", false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*start:* %s is not a number", id), false)
continue
}
status, err := b.client.StartTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*start:* "+err.Error(), false)
continue
}
torrent, err := b.client.GetTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[fail] *start:* No torrent with an ID of %d", num), false)
continue
}
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[%s] *start:* %s", status, torrent.Name), false)
}
}
func (b *Bot) handleCheck(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*check:* needs an argument", false)
return
}
if tokens[0] == "all" {
if err := b.client.VerifyAll(); err != nil {
b.SendMessage(update.Message.Chat.ID, "*check:* error occurred while verifying some torrents", false)
return
}
b.SendMessage(update.Message.Chat.ID, "Verifying all torrents", false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*check:* %s is not a number", id), false)
continue
}
status, err := b.client.VerifyTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*check:* "+err.Error(), false)
continue
}
torrent, err := b.client.GetTorrent(num)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[fail] *check:* No torrent with an ID of %d", num), false)
continue
}
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("[%s] *check:* %s", status, torrent.Name), false)
}
}
func (b *Bot) handleStats(update tgbotapi.Update) {
stats, err := b.client.GetStats()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*stats*", err)
return
}
msg := fmt.Sprintf(`
Total: *%d*
Active: *%d*
Paused: *%d*
_Current Stats_
Downloaded: *%s*
Uploaded: *%s*
Running time: *%s*
_Accumulative Stats_
Sessions: *%d*
Downloaded: *%s*
Uploaded: *%s*
Total Running time: *%s*
`,
stats.TorrentCount,
stats.ActiveTorrentCount,
stats.PausedTorrentCount,
humanize.Bytes(stats.CurrentStats.DownloadedBytes),
humanize.Bytes(stats.CurrentStats.UploadedBytes),
stats.CurrentActiveTime(),
stats.CumulativeStats.SessionCount,
humanize.Bytes(stats.CumulativeStats.DownloadedBytes),
humanize.Bytes(stats.CumulativeStats.UploadedBytes),
stats.CumulativeActiveTime(),
)
b.SendMessage(update.Message.Chat.ID, msg, true)
}
func (b *Bot) handleDownLimit(update tgbotapi.Update, tokens []string) {
b.handleSpeedLimit(update, tokens, transmission.DownloadLimitType)
}
func (b *Bot) handleUpLimit(update tgbotapi.Update, tokens []string) {
b.handleSpeedLimit(update, tokens, transmission.UploadLimitType)
}
func (b *Bot) handleSpeedLimit(update tgbotapi.Update, tokens []string, limitType transmission.SpeedLimitType) {
if len(tokens) < 1 {
b.SendMessage(update.Message.Chat.ID, "Please, specify the limit", false)
return
}
limit, err := strconv.ParseUint(tokens[0], 10, 32)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "Please, specify the limit as number of kilobytes", false)
return
}
speedLimitCmd := transmission.NewSpeedLimitCommand(limitType, uint(limit))
if speedLimitCmd == nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*%s:* internal error", limitType), false)
return
}
out, err := b.client.ExecuteCommand(speedLimitCmd)
if err != nil {
b.handleClientError(update.Message.Chat.ID, fmt.Sprintf("*%s*", limitType), err)
return
}
if out.Result != "success" {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*%s:* %v", limitType, out.Result), false)
return
}
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*%s:* limit has been successfully changed to %d KB/s", limitType, limit), false)
}
func (b *Bot) handleSpeed(update tgbotapi.Update) {
stats, err := b.client.GetStats()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*speed*", err)
return
}
msg := fmt.Sprintf("↓ %s ↑ %s", humanize.Bytes(stats.DownloadSpeed), humanize.Bytes(stats.UploadSpeed))
msgID := b.SendMessage(update.Message.Chat.ID, msg, false)
if !b.cfg.NoLive {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(b.cfg.Duration)*b.cfg.Interval+time.Second)
defer cancel()
b.liveUpdateSpeed(ctx, update.Message.Chat.ID, msgID)
}
}
func (b *Bot) handleCount(update tgbotapi.Update) {
torrents, err := b.client.GetTorrents()
if err != nil {
b.handleClientError(update.Message.Chat.ID, "*count*", err)
return
}
var downloading, seeding, stopped, checking, downloadingQ, seedingQ, checkingQ int
for i := range torrents {
switch torrents[i].Status {
case transmission.StatusDownloading:
downloading++
case transmission.StatusSeeding:
seeding++
case transmission.StatusStopped:
stopped++
case transmission.StatusChecking:
checking++
case transmission.StatusDownloadPending:
downloadingQ++
case transmission.StatusSeedPending:
seedingQ++
case transmission.StatusCheckPending:
checkingQ++
}
}
msg := fmt.Sprintf("Downloading: %d\nSeeding: %d\nPaused: %d\nVerifying: %d\n\n- Waiting to -\nDownload: %d\nSeed: %d\nVerify: %d\n\nTotal: %d",
downloading, seeding, stopped, checking, downloadingQ, seedingQ, checkingQ, len(torrents))
b.SendMessage(update.Message.Chat.ID, msg, false)
}
func (b *Bot) handleDel(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*del:* needs an ID", false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*del:* %s is not an ID", id), false)
continue
}
name, err := b.client.DeleteTorrent(num, false)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*del:* "+err.Error(), false)
continue
}
b.SendMessage(update.Message.Chat.ID, "*Deleted:* "+name, false)
}
}
func (b *Bot) handleDelData(update tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
b.SendMessage(update.Message.Chat.ID, "*deldata:* needs an ID", false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("*deldata:* %s is not an ID", id), false)
continue
}
name, err := b.client.DeleteTorrent(num, true)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*deldata:* "+err.Error(), false)
continue
}
b.SendMessage(update.Message.Chat.ID, "Deleted with data: "+name, false)
}
}
func (b *Bot) handleVersion(update tgbotapi.Update) {
b.SendMessage(update.Message.Chat.ID, fmt.Sprintf("Transmission *%s*\nTransmission-telegram *%s*", b.client.Version(), config.Version), true)
}
func (b *Bot) handleReceiveTorrent(update tgbotapi.Update) {
if update.Message.Document == nil {
return
}
fconfig := tgbotapi.FileConfig{
FileID: update.Message.Document.FileID,
}
file, err := b.api.GetFile(fconfig)
if err != nil {
b.SendMessage(update.Message.Chat.ID, "*receiver:* "+err.Error(), false)
return
}
b.handleAdd(update, []string{file.Link(b.cfg.BotToken)})
}
// Live update helpers
func (b *Bot) liveUpdateTorrents(ctx context.Context, chatID int64, msgID int, filter func(transmission.Torrents) transmission.Torrents, formatter func(*transmission.Torrent) string) {
for i := 0; i < b.cfg.Duration; i++ {
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.logger.Printf("[ERROR] Failed to get torrents for live update: %s", err)
continue
}
if len(torrents) < 1 {
continue
}
filtered := filter(torrents)
buf := getBuffer()
defer putBuffer(buf)
for i := range filtered {
buf.WriteString(formatter(filtered[i]))
}
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
return
}
}
}
func (b *Bot) liveUpdateActive(ctx context.Context, chatID int64, msgID int) {
for i := 0; i < b.cfg.Duration; i++ {
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.logger.Printf("[ERROR] Failed to get torrents for live update: %s", err)
continue
}
buf := getBuffer()
for i := range torrents {
if torrents[i].RateDownload > 0 || torrents[i].RateUpload > 0 {
buf.WriteString(formatter.FormatTorrentDetailed(torrents[i]))
}
}
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
putBuffer(buf)
return
}
putBuffer(buf)
}
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
torrents, err := b.client.GetTorrents()
if err != nil {
b.logger.Printf("[ERROR] Failed to get torrents for final live update: %s", err)
return
}
buf := getBuffer()
defer putBuffer(buf)
for i := range torrents {
if torrents[i].RateDownload > 0 || torrents[i].RateUpload > 0 {
buf.WriteString(formatter.FormatTorrentActiveStopped(torrents[i]))
}
}
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
}
}
func (b *Bot) liveUpdateInfo(ctx context.Context, chatID int64, msgID int, torrentID int, trackers string) {
for i := 0; i < b.cfg.Duration; i++ {
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
torrent, err := b.client.GetTorrent(torrentID)
if err != nil {
b.logger.Printf("[ERROR] Failed to get torrent %d for live update: %s", torrentID, err)
continue
}
info := formatter.FormatTorrentInfo(torrent, trackers)
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, info)
editConf.ParseMode = tgbotapi.ModeMarkdown
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
return
}
}
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
torrent, err := b.client.GetTorrent(torrentID)
if err != nil {
b.logger.Printf("[ERROR] Failed to get torrent %d for final live update: %s", torrentID, err)
return
}
info := formatter.FormatTorrentInfoStopped(torrent, trackers)
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, info)
editConf.ParseMode = tgbotapi.ModeMarkdown
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
}
}
func (b *Bot) liveUpdateSpeed(ctx context.Context, chatID int64, msgID int) {
for i := 0; i < b.cfg.Duration; i++ {
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
stats, err := b.client.GetStats()
if err != nil {
b.logger.Printf("[ERROR] Failed to get stats for live update: %s", err)
continue
}
msg := fmt.Sprintf("↓ %s ↑ %s", humanize.Bytes(stats.DownloadSpeed), humanize.Bytes(stats.UploadSpeed))
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, msg)
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
return
}
}
select {
case <-ctx.Done():
return
case <-time.After(b.cfg.Interval):
}
// Rate limit before sending
rateLimitWait()
editConf := tgbotapi.NewEditMessageText(chatID, msgID, "↓ - ↑ -")
if _, err := b.api.Send(editConf); err != nil {
b.logger.Printf("[ERROR] Failed to update message: %s", err)
}
}