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

143 lines
3.2 KiB
Go

package bot
import (
"bytes"
"fmt"
"strconv"
"sync"
"time"
"unicode/utf8"
"github.com/pyed/transmission"
tgbotapi "gopkg.in/telegram-bot-api.v4"
"transmission-telegram/internal/config"
)
// rateLimiter limits API calls to Telegram (30 messages per second)
// Uses a channel-based approach to avoid mutex contention
type rateLimiter struct {
ch chan struct{}
}
var (
// telegramRateLimiter limits API calls to 30 per second
telegramRateLimiter = func() *rateLimiter {
rl := &rateLimiter{
ch: make(chan struct{}, 1),
}
// Start the ticker goroutine
go func() {
ticker := time.NewTicker(time.Second / 30) // 30 calls per second
defer ticker.Stop()
for range ticker.C {
// Try to send a token, but don't block if channel is full
select {
case rl.ch <- struct{}{}:
default:
// Channel is full, skip this tick
}
}
}()
return rl
}()
)
// wait waits for the next available slot
func (rl *rateLimiter) wait() {
<-rl.ch
}
// rateLimitWait waits for the next available API call slot
func rateLimitWait() {
telegramRateLimiter.wait()
}
// sendMessage sends a message to Telegram, splitting if necessary
func sendMessage(bot *tgbotapi.BotAPI, chatID int64, text string, markdown bool) int {
// Rate limit: wait before sending
telegramRateLimiter.wait()
// Set typing action
action := tgbotapi.NewChatAction(chatID, tgbotapi.ChatTyping)
bot.Send(action)
// Check the rune count, telegram is limited to 4096 chars per message
msgRuneCount := utf8.RuneCountInString(text)
var lastMsgID int
// Convert to runes for proper UTF-8 handling
runes := []rune(text)
// Split message if too long
for len(runes) > config.TelegramMaxMessageLength {
stop := config.TelegramMaxMessageLength - 1
// Find the last newline before the limit
for stop > 0 && runes[stop] != '\n' {
stop--
}
// If no newline found, just cut at the limit
if stop == 0 {
stop = config.TelegramMaxMessageLength - 1
}
chunk := string(runes[:stop+1])
runes = runes[stop+1:]
telegramRateLimiter.wait()
msg := tgbotapi.NewMessage(chatID, chunk)
msg.DisableWebPagePreview = true
if markdown {
msg.ParseMode = tgbotapi.ModeMarkdown
}
if resp, err := bot.Send(msg); err == nil {
lastMsgID = resp.MessageID
}
}
// Send remaining text
if len(runes) > 0 {
telegramRateLimiter.wait()
msg := tgbotapi.NewMessage(chatID, string(runes))
msg.DisableWebPagePreview = true
if markdown {
msg.ParseMode = tgbotapi.ModeMarkdown
}
if resp, err := bot.Send(msg); err == nil {
lastMsgID = resp.MessageID
}
}
return lastMsgID
}
// parseTorrentIDs parses torrent IDs from tokens
func parseTorrentIDs(tokens []string) ([]int, []string) {
var ids []int
var errors []string
for _, token := range tokens {
if id, err := strconv.Atoi(token); err == nil {
ids = append(ids, id)
} else {
errors = append(errors, fmt.Sprintf("%s is not a number", token))
}
}
return ids, errors
}
// formatTorrentList formats a list of torrents as a simple list
func formatTorrentList(torrents []*transmission.Torrent) string {
var buf bytes.Buffer
for _, torrent := range torrents {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrent.ID, torrent.Name))
}
return buf.String()
}