transmission-telegram/transmission-telegram.go
2016-07-20 15:26:17 +03:00

1348 lines
37 KiB
Go

package main
import (
"bytes"
"flag"
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"gopkg.in/telegram-bot-api.v4"
"github.com/dustin/go-humanize"
"github.com/pyed/transmission"
)
const (
VERSION = "1.0"
HELP = `
*list* or *li*
Lists all the torrents, takes an optional argument which is a query to list only torrents that has a tracker matches the query, or some of it.
*head* or *he*
Lists the first n number of torrents, n defaults to 5 if no argument is provided.
*tail* or *ta*
Lists the last n number of torrents, n defaults to 5 if no argument is provided.
*down* or *dl*
Lists torrents with the status of Downloading or in the queue to download.
*seeding* or *sd*
Lists torrents with the status of Seeding or in the queue to seed.
*paused* or *pa*
Lists Paused torrents.
*checking* or *ch*
Lists torrents with the status of Verifying or in the queue to verify.
*active* or *ac*
Lists torrents that are actively uploading or downloading.
*errors* or *er*
Lists torrents with with errors along with the error message.
*sort* or *so*
Manipulate the sorting of the aforementioned commands, Call it without arguments for more.
*trackers* or *tr*
Lists all the trackers along with the number of torrents.
*add* or *ad*
Takes one or many URLs or magnets to add them, You can send a .torrent file via Telegram to add it.
*search* or *se*
Takes a query and lists torrents with matching names.
*latest* or *la*
Lists the newest n torrents, n defaults to 5 if no argument is provided.
*info* or *in*
Takes one or more torrent's IDs to list more info about them.
*stop* or *sp*
Takes one or more torrent's IDs to stop them.
*start* or *st*
Takes one or more torrent's IDs to start them.
*check* or *ck*
Takes one or more torrent's IDs to verify them.
*del*
Takes one or more torrent's IDs to delete them.
*deldata*
Takes one or more torrent's IDs to delete them and their data.
*stats* or *sa*
Shows Transmission's stats.
*speed* or *ss*
Shows the upload and download speeds.
*count* or *co*
Shows the torrents counts per status.
*help*
Shows this help message.
*version*
Shows version numbers.
- Prefix commands with '/' if you want to talk to your bot in a group.
`
)
var (
// flags
BotToken string
Master string
RpcUrl string
Username string
Password string
LogFile string
// transmission
Client *transmission.TransmissionClient
// telegram
Bot *tgbotapi.BotAPI
Updates <-chan tgbotapi.Update
// interval in seconds for live updates, affects: "active", "info", "speed", "head", "tail"
interval time.Duration = 2
// duration controls how many intervals will happen
duration = 60
// since telegram's markdown can't be escaped, we have to replace some chars
// affects only markdown users: info, active, head, tail
mdReplacer = strings.NewReplacer("*", "•",
"[", "(",
"]", ")",
"_", "-",
"`", "'")
)
// init flags
func init() {
// define arguments and parse them.
flag.StringVar(&BotToken, "token", "", "Telegram bot token")
flag.StringVar(&Master, "master", "", "Your telegram handler, So the bot will only respond to you")
flag.StringVar(&RpcUrl, "url", "http://localhost:9091/transmission/rpc", "Transmission RPC URL")
flag.StringVar(&Username, "username", "", "Transmission username")
flag.StringVar(&Password, "password", "", "Transmission password")
flag.StringVar(&LogFile, "logfile", "", "Send logs to a file")
// set the usage message
flag.Usage = func() {
fmt.Fprint(os.Stderr, "Usage: transmission-bot -token=<TOKEN> -master=<@tuser> -url=[http://] -username=[user] -password=[pass]\n\n")
flag.PrintDefaults()
}
flag.Parse()
// make sure that we have the two madatory arguments: telegram token & master's handler.
if BotToken == "" ||
Master == "" {
fmt.Fprintf(os.Stderr, "Error: Mandatory argument missing! (-token or -master)\n\n")
flag.Usage()
os.Exit(1)
}
// make sure that the handler doesn't contain @
Master = strings.Replace(Master, "@", "", -1)
// if we got a log file, log to it
if LogFile != "" {
logf, err := os.OpenFile(LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
log.SetOutput(logf)
}
}
// init transmission
func init() {
var err error
Client, err = transmission.New(RpcUrl, Username, Password)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Transmission: Make sure you have the right URL, Username and Password")
os.Exit(1)
}
}
// init telegram
func init() {
// authorize using the token
var err error
Bot, err = tgbotapi.NewBotAPI(BotToken)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Telegram: %s", err)
os.Exit(1)
}
log.Printf("[INFO] Authorized: %s", Bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
Updates, err = Bot.GetUpdatesChan(u)
if err != nil {
fmt.Fprintf(os.Stderr, "[ERROR] Telegram: %s", err)
os.Exit(1)
}
}
func main() {
for update := range Updates {
// ignore edited messages
if update.Message == nil {
continue
}
// ignore anyone other than 'master'
if strings.ToLower(update.Message.From.UserName) != strings.ToLower(Master) {
log.Printf("[INFO] Ignored a message from: %s", update.Message.From.String())
continue
}
// tokenize the update
tokens := strings.Split(update.Message.Text, " ")
command := strings.ToLower(tokens[0])
switch command {
case "list", "/list", "li", "/li":
go list(update, tokens[1:])
case "head", "/head", "he", "/he":
go head(update, tokens[1:])
case "tail", "/tail", "ta", "/ta":
go tail(update, tokens[1:])
case "downs", "/downs", "dl", "/dl":
go downs(update)
case "seeding", "/seeding", "sd", "/sd":
go seeding(update)
case "paused", "/paused", "pa", "/pa":
go paused(update)
case "checking", "/checking", "ch", "/ch":
go checking(update)
case "active", "/active", "ac", "/ac":
go active(update)
case "errors", "/errors", "er", "/er":
go errors(update)
case "sort", "/sort", "so", "/so":
go sort(update, tokens[1:])
case "trackers", "/trackers", "tr", "/tr":
go trackers(update)
case "add", "/add", "ad", "/ad":
go add(update, tokens[1:])
case "search", "/search", "se", "/se":
go search(update, tokens[1:])
case "latest", "/latest", "la", "/la":
go latest(update, tokens[1:])
case "info", "/info", "in", "/in":
go info(update, tokens[1:])
case "stop", "/stop", "sp", "/sp":
go stop(update, tokens[1:])
case "start", "/start", "st", "/st":
go start(update, tokens[1:])
case "check", "/check", "ck", "/ck":
go check(update, tokens[1:])
case "stats", "/stats", "sa", "/sa":
go stats(update)
case "speed", "/speed", "ss", "/ss":
go speed(update)
case "count", "/count", "co", "/co":
go count(update)
case "del", "/del":
go del(update, tokens[1:])
case "deldata", "/deldata":
go deldata(update, tokens[1:])
case "help", "/help":
go send(HELP, update.Message.Chat.ID, true)
case "version", "/version":
go version(update)
case "":
// might be a file received
go receiveTorrent(update)
default:
// no such command, try help
go send("no such command, try /help", update.Message.Chat.ID, false)
}
}
}
// list will form and send a list of all the torrents
// takes an optional argument which is a query to match against trackers
// to list only torrents that has a tracker that matchs.
func list(ud tgbotapi.Update, tokens []string) {
torrents, err := Client.GetTorrents()
if err != nil {
send("list: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
// if it gets a query, it will list torrents that has trackers that match the query
if len(tokens) != 0 {
// (?i) for case insensitivity
regx, err := regexp.Compile("(?i)" + tokens[0])
if err != nil {
send("list: "+err.Error(), ud.Message.Chat.ID, false)
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 { // if we did not get a query, list all torrents
for i := range torrents {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
// if we got a tracker query show different message
if len(tokens) != 0 {
send(fmt.Sprintf("list: No tracker matches: *%s*", tokens[0]), ud.Message.Chat.ID, true)
return
}
send("list: No torrents", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// head will list the first 5 or n torrents
func head(ud tgbotapi.Update, tokens []string) {
var (
n = 5 // default to 5
err error
)
if len(tokens) > 0 {
n, err = strconv.Atoi(tokens[0])
if err != nil {
send("head: argument must be a number", ud.Message.Chat.ID, false)
return
}
}
torrents, err := Client.GetTorrents()
if err != nil {
send("head: "+err.Error(), ud.Message.Chat.ID, false)
return
}
// make sure that we stay in the boundaries
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
buf := new(bytes.Buffer)
for i := range torrents[:n] {
torrentName := mdReplacer.Replace(torrents[i].Name) // escape markdown
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
}
if buf.Len() == 0 {
send("head: No torrents", ud.Message.Chat.ID, false)
return
}
msgID := send(buf.String(), ud.Message.Chat.ID, true)
// keep the info live
for i := 0; i < duration; i++ {
time.Sleep(time.Second * interval)
buf.Reset()
torrents, err := Client.GetTorrents()
if err != nil {
continue // try again if some error heppened
}
for i := range torrents[:n] {
torrentName := mdReplacer.Replace(torrents[i].Name) // escape markdown
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
}
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}
}
// tail lists the last 5 or n torrents
func tail(ud tgbotapi.Update, tokens []string) {
var (
n = 5 // default to 5
err error
)
if len(tokens) > 0 {
n, err = strconv.Atoi(tokens[0])
if err != nil {
send("tail: argument must be a number", ud.Message.Chat.ID, false)
return
}
}
torrents, err := Client.GetTorrents()
if err != nil {
send("tail: "+err.Error(), ud.Message.Chat.ID, false)
return
}
// make sure that we stay in the boundaries
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
buf := new(bytes.Buffer)
for _, torrent := range torrents[len(torrents)-n:] {
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()),
humanize.Bytes(torrent.SizeWhenDone), torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload),
humanize.Bytes(torrent.RateUpload), torrent.Ratio()))
}
if buf.Len() == 0 {
send("tail: No torrents", ud.Message.Chat.ID, false)
return
}
msgID := send(buf.String(), ud.Message.Chat.ID, true)
// keep the info live
for i := 0; i < duration; i++ {
time.Sleep(time.Second * interval)
buf.Reset()
torrents, err := Client.GetTorrents()
if err != nil {
continue // try again if some error heppened
}
for _, torrent := range torrents[len(torrents)-n:] {
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()),
humanize.Bytes(torrent.SizeWhenDone), torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload),
humanize.Bytes(torrent.RateUpload), torrent.Ratio()))
}
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}
}
// downs will send the names of torrents with status 'Downloading' or in queue to
func downs(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("downs: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
// Downloading or in queue to download
if torrents[i].Status == transmission.StatusDownloading ||
torrents[i].Status == transmission.StatusDownloadPending {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
send("No downloads", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// seeding will send the names of the torrents with the status 'Seeding' or in the queue to
func seeding(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("seeding: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].Status == transmission.StatusSeeding ||
torrents[i].Status == transmission.StatusSeedPending {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
send("No torrents seeding", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// paused will send the names of the torrents with status 'Paused'
func paused(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("paused: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].Status == transmission.StatusStopped {
buf.WriteString(fmt.Sprintf("<%d> %s\n%s (%.1f%%) DL: %s UL: %s R: %s\n\n",
torrents[i].ID, torrents[i].Name, torrents[i].TorrentStatus(),
torrents[i].PercentDone*100, humanize.Bytes(torrents[i].DownloadedEver),
humanize.Bytes(torrents[i].UploadedEver), torrents[i].Ratio()))
}
}
if buf.Len() == 0 {
send("No paused torrents", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// checking will send the names of torrents with the status 'verifying' or in the queue to
func checking(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("checking: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].Status == transmission.StatusChecking ||
torrents[i].Status == transmission.StatusCheckPending {
buf.WriteString(fmt.Sprintf("<%d> %s\n%s (%.1f%%)\n\n",
torrents[i].ID, torrents[i].Name, torrents[i].TorrentStatus(),
torrents[i].PercentDone*100))
}
}
if buf.Len() == 0 {
send("No torrents verifying", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// active will send torrents that are actively downloading or uploading
func active(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("active: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].RateDownload > 0 ||
torrents[i].RateUpload > 0 {
// escape markdown
torrentName := mdReplacer.Replace(torrents[i].Name)
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
}
}
if buf.Len() == 0 {
send("No active torrents", ud.Message.Chat.ID, false)
return
}
msgID := send(buf.String(), ud.Message.Chat.ID, true)
// keep the active list live for 'duration * interval'
for i := 0; i < duration; i++ {
time.Sleep(time.Second * interval)
// reset the buffer to reuse it
buf.Reset()
// update torrents
torrents, err = Client.GetTorrents()
if err != nil {
continue // if there was error getting torrents, skip to the next iteration
}
// do the same loop again
for i := range torrents {
if torrents[i].RateDownload > 0 ||
torrents[i].RateUpload > 0 {
torrentName := mdReplacer.Replace(torrents[i].Name) // replace markdown chars
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\n\n",
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, humanize.Bytes(torrents[i].RateDownload),
humanize.Bytes(torrents[i].RateUpload), torrents[i].Ratio()))
}
}
// no need to check if it is empty, as if the buffer is empty telegram won't change the message
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}
// replace the speed with dashes to indicate that we are done being live
buf.Reset()
for i := range torrents {
if torrents[i].RateDownload > 0 ||
torrents[i].RateUpload > 0 {
// escape markdown
torrentName := mdReplacer.Replace(torrents[i].Name)
buf.WriteString(fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *-* ↑ *-* R: *%s*\n\n",
torrents[i].ID, torrentName, torrents[i].TorrentStatus(), humanize.Bytes(torrents[i].Have()),
humanize.Bytes(torrents[i].SizeWhenDone), torrents[i].PercentDone*100, torrents[i].Ratio()))
}
}
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, buf.String())
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}
// errors will send torrents with errors
func errors(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("errors: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].Error != 0 {
buf.WriteString(fmt.Sprintf("<%d> %s\n%s\n",
torrents[i].ID, torrents[i].Name, torrents[i].ErrorString))
}
}
if buf.Len() == 0 {
send("No errors", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// sort changes torrents sorting
func sort(ud tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
send(`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.`, ud.Message.Chat.ID, true)
return
}
var reversed bool
if strings.ToLower(tokens[0]) == "rev" {
reversed = true
tokens = tokens[1:]
}
switch strings.ToLower(tokens[0]) {
case "id":
if reversed {
Client.SetSort(transmission.SortRevID)
break
}
Client.SetSort(transmission.SortID)
case "name":
if reversed {
Client.SetSort(transmission.SortRevName)
break
}
Client.SetSort(transmission.SortName)
case "age":
if reversed {
Client.SetSort(transmission.SortRevAge)
break
}
Client.SetSort(transmission.SortAge)
case "size":
if reversed {
Client.SetSort(transmission.SortRevSize)
break
}
Client.SetSort(transmission.SortSize)
case "progress":
if reversed {
Client.SetSort(transmission.SortRevProgress)
break
}
Client.SetSort(transmission.SortProgress)
case "downspeed":
if reversed {
Client.SetSort(transmission.SortRevDownSpeed)
break
}
Client.SetSort(transmission.SortDownSpeed)
case "upspeed":
if reversed {
Client.SetSort(transmission.SortRevUpSpeed)
break
}
Client.SetSort(transmission.SortUpSpeed)
case "download":
if reversed {
Client.SetSort(transmission.SortRevDownloaded)
break
}
Client.SetSort(transmission.SortDownloaded)
case "upload":
if reversed {
Client.SetSort(transmission.SortRevUploaded)
break
}
Client.SetSort(transmission.SortUploaded)
case "ratio":
if reversed {
Client.SetSort(transmission.SortRevRatio)
break
}
Client.SetSort(transmission.SortRatio)
default:
send("unkown sorting method", ud.Message.Chat.ID, false)
return
}
if reversed {
send("sort: reversed "+tokens[0], ud.Message.Chat.ID, false)
return
}
send("sort: "+tokens[0], ud.Message.Chat.ID, false)
}
var trackerRegex = regexp.MustCompile(`[https?|udp]://([^:/]*)`)
// trackers will send a list of trackers and how many torrents each one has
func trackers(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("trackers: "+err.Error(), ud.Message.Chat.ID, false)
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])
n, ok := trackers[currentTracker]
if !ok {
trackers[currentTracker] = 1
continue
}
trackers[currentTracker] = n + 1
}
}
}
buf := new(bytes.Buffer)
for k, v := range trackers {
buf.WriteString(fmt.Sprintf("%d - %s\n", v, k))
}
if buf.Len() == 0 {
send("No trackers!", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// add takes an URL to a .torrent file to add it to transmission
func add(ud tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
send("add: needs atleast one URL", ud.Message.Chat.ID, false)
return
}
// loop over the URL/s and add them
for _, url := range tokens {
cmd := transmission.NewAddCmdByURL(url)
torrent, err := Client.ExecuteAddCommand(cmd)
if err != nil {
send("add: "+err.Error(), ud.Message.Chat.ID, false)
continue
}
// check if torrent.Name is empty, then an error happened
if torrent.Name == "" {
send("add: error adding "+url, ud.Message.Chat.ID, false)
continue
}
send(fmt.Sprintf("Added: <%d> %s", torrent.ID, torrent.Name), ud.Message.Chat.ID, false)
}
}
// receiveTorrent gets an update that potentially has a .torrent file to add
func receiveTorrent(ud tgbotapi.Update) {
if ud.Message.Document.FileID == "" {
return // has no document
}
// get the file ID and make the config
fconfig := tgbotapi.FileConfig{
FileID: ud.Message.Document.FileID,
}
file, err := Bot.GetFile(fconfig)
if err != nil {
send("receiver: "+err.Error(), ud.Message.Chat.ID, false)
return
}
// add by file URL
add(ud, []string{file.Link(BotToken)})
}
// search takes a query and returns torrents with match
func search(ud tgbotapi.Update, tokens []string) {
// make sure that we got a query
if len(tokens) == 0 {
send("search: needs an argument", ud.Message.Chat.ID, false)
return
}
query := strings.Join(tokens, " ")
// "(?i)" for case insensitivity
regx, err := regexp.Compile("(?i)" + query)
if err != nil {
send("search: "+err.Error(), ud.Message.Chat.ID, false)
return
}
torrents, err := Client.GetTorrents()
if err != nil {
send("search: "+err.Error(), ud.Message.Chat.ID, false)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if regx.MatchString(torrents[i].Name) {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
send("No matches!", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// latest takes n and returns the latest n torrents
func latest(ud tgbotapi.Update, tokens []string) {
var (
n = 5 // default to 5
err error
)
if len(tokens) > 0 {
n, err = strconv.Atoi(tokens[0])
if err != nil {
send("latest: argument must be a number", ud.Message.Chat.ID, false)
return
}
}
torrents, err := Client.GetTorrents()
if err != nil {
send("latest: "+err.Error(), ud.Message.Chat.ID, false)
return
}
// make sure that we stay in the boundaries
if n <= 0 || n > len(torrents) {
n = len(torrents)
}
// sort by age, and set reverse to true to get the latest first
torrents.SortAge(true)
buf := new(bytes.Buffer)
for i := range torrents[:n] {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
if buf.Len() == 0 {
send("latest: No torrents", ud.Message.Chat.ID, false)
return
}
send(buf.String(), ud.Message.Chat.ID, false)
}
// info takes an id of a torrent and returns some info about it
func info(ud tgbotapi.Update, tokens []string) {
if len(tokens) == 0 {
send("info: needs a torrent ID number", ud.Message.Chat.ID, false)
return
}
for _, id := range tokens {
torrentID, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("info: %s is not a number", id), ud.Message.Chat.ID, false)
continue
}
// get the torrent
torrent, err := Client.GetTorrent(torrentID)
if err != nil {
send(fmt.Sprintf("info: Can't find a torrent with an ID of %d", torrentID), ud.Message.Chat.ID, false)
continue
}
// get the trackers using 'trackerRegex'
var trackers string
for _, tracker := range torrent.Trackers {
sm := trackerRegex.FindSubmatch([]byte(tracker.Announce))
if len(sm) > 1 {
trackers += string(sm[1]) + " "
}
}
// format the info
torrentName := mdReplacer.Replace(torrent.Name) // escape markdown
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *%s*\nTrackers: `%s`",
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload), humanize.Bytes(torrent.RateUpload), torrent.Ratio(),
humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver), time.Unix(torrent.AddedDate, 0).Format(time.Stamp),
torrent.ETA(), trackers)
// send it
msgID := send(info, ud.Message.Chat.ID, true)
// this go-routine will make the info live for 'duration * interval'
// takes trackers so we don't have to regex them over and over.
go func(trackers string, torrentID, msgID int) {
for i := 0; i < duration; i++ {
time.Sleep(time.Second * interval)
torrent, err := Client.GetTorrent(torrentID)
if err != nil {
continue // skip this iteration if there's an error retrieving the torrent's info
}
torrentName := mdReplacer.Replace(torrent.Name)
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *%s* ↑ *%s* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *%s*\nTrackers: `%s`",
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
torrent.PercentDone*100, humanize.Bytes(torrent.RateDownload), humanize.Bytes(torrent.RateUpload), torrent.Ratio(),
humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver), time.Unix(torrent.AddedDate, 0).Format(time.Stamp),
torrent.ETA(), trackers)
// update the message
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, info)
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}
// at the end write dashes to indicate that we are done being live.
torrentName := mdReplacer.Replace(torrent.Name)
info := fmt.Sprintf("`<%d>` *%s*\n%s *%s* of *%s* (*%.1f%%*) ↓ *- B* ↑ *- B* R: *%s*\nDL: *%s* UP: *%s*\nAdded: *%s*, ETA: *-*\nTrackers: `%s`",
torrent.ID, torrentName, torrent.TorrentStatus(), humanize.Bytes(torrent.Have()), humanize.Bytes(torrent.SizeWhenDone),
torrent.PercentDone*100, torrent.Ratio(), humanize.Bytes(torrent.DownloadedEver), humanize.Bytes(torrent.UploadedEver),
time.Unix(torrent.AddedDate, 0).Format(time.Stamp), trackers)
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, info)
editConf.ParseMode = tgbotapi.ModeMarkdown
Bot.Send(editConf)
}(trackers, torrentID, msgID)
}
}
// stop takes id[s] of torrent[s] or 'all' to stop them
func stop(ud tgbotapi.Update, tokens []string) {
// make sure that we got at least one argument
if len(tokens) == 0 {
send("stop: needs an argument", ud.Message.Chat.ID, false)
return
}
// if the first argument is 'all' then stop all torrents
if tokens[0] == "all" {
if err := Client.StopAll(); err != nil {
send("stop: error occurred while stopping some torrents", ud.Message.Chat.ID, false)
return
}
send("stopped all torrents", ud.Message.Chat.ID, false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("stop: %s is not a number", id), ud.Message.Chat.ID, false)
continue
}
status, err := Client.StopTorrent(num)
if err != nil {
send("stop: "+err.Error(), ud.Message.Chat.ID, false)
continue
}
torrent, err := Client.GetTorrent(num)
if err != nil {
send(fmt.Sprintf("[fail] stop: No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
return
}
send(fmt.Sprintf("[%s] stop: %s", status, torrent.Name), ud.Message.Chat.ID, false)
}
}
// start takes id[s] of torrent[s] or 'all' to start them
func start(ud tgbotapi.Update, tokens []string) {
// make sure that we got at least one argument
if len(tokens) == 0 {
send("start: needs an argument", ud.Message.Chat.ID, false)
return
}
// if the first argument is 'all' then start all torrents
if tokens[0] == "all" {
if err := Client.StartAll(); err != nil {
send("start: error occurred while starting some torrents", ud.Message.Chat.ID, false)
return
}
send("started all torrents", ud.Message.Chat.ID, false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("start: %s is not a number", id), ud.Message.Chat.ID, false)
continue
}
status, err := Client.StartTorrent(num)
if err != nil {
send("stop: "+err.Error(), ud.Message.Chat.ID, false)
continue
}
torrent, err := Client.GetTorrent(num)
if err != nil {
send(fmt.Sprintf("[fail] start: No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
return
}
send(fmt.Sprintf("[%s] start: %s", status, torrent.Name), ud.Message.Chat.ID, false)
}
}
// check takes id[s] of torrent[s] or 'all' to verify them
func check(ud tgbotapi.Update, tokens []string) {
// make sure that we got at least one argument
if len(tokens) == 0 {
send("check: needs an argument", ud.Message.Chat.ID, false)
return
}
// if the first argument is 'all' then start all torrents
if tokens[0] == "all" {
if err := Client.VerifyAll(); err != nil {
send("check: error occurred while verifying some torrents", ud.Message.Chat.ID, false)
return
}
send("verifying all torrents", ud.Message.Chat.ID, false)
return
}
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("check: %s is not a number", id), ud.Message.Chat.ID, false)
continue
}
status, err := Client.VerifyTorrent(num)
if err != nil {
send("stop: "+err.Error(), ud.Message.Chat.ID, false)
continue
}
torrent, err := Client.GetTorrent(num)
if err != nil {
send(fmt.Sprintf("[fail] check: No torrent with an ID of %d", num), ud.Message.Chat.ID, false)
return
}
send(fmt.Sprintf("[%s] check: %s", status, torrent.Name), ud.Message.Chat.ID, false)
}
}
// stats echo back transmission stats
func stats(ud tgbotapi.Update) {
stats, err := Client.GetStats()
if err != nil {
send("stats: "+err.Error(), ud.Message.Chat.ID, false)
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(),
)
send(msg, ud.Message.Chat.ID, true)
}
// speed will echo back the current download and upload speeds
func speed(ud tgbotapi.Update) {
// keep track of the returned message ID from 'send()' to edit the message.
var msgID int
for i := 0; i < duration; i++ {
stats, err := Client.GetStats()
if err != nil {
send("speed: "+err.Error(), ud.Message.Chat.ID, false)
return
}
msg := fmt.Sprintf("↓ %s ↑ %s", humanize.Bytes(stats.DownloadSpeed), humanize.Bytes(stats.UploadSpeed))
// if we haven't send a message, send it and save the message ID to edit it the next iteration
if msgID == 0 {
msgID = send(msg, ud.Message.Chat.ID, false)
time.Sleep(time.Second * interval)
continue
}
// we have sent the message, let's update.
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, msg)
Bot.Send(editConf)
time.Sleep(time.Second * interval)
}
// after the 10th iteration, show dashes to indicate that we are done updating.
editConf := tgbotapi.NewEditMessageText(ud.Message.Chat.ID, msgID, "↓ - B ↑ - B")
Bot.Send(editConf)
}
// count returns current torrents count per status
func count(ud tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("count: "+err.Error(), ud.Message.Chat.ID, false)
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))
send(msg, ud.Message.Chat.ID, false)
}
// del takes an id or more, and delete the corresponding torrent/s
func del(ud tgbotapi.Update, tokens []string) {
// make sure that we got an argument
if len(tokens) == 0 {
send("del: needs an ID", ud.Message.Chat.ID, false)
return
}
// loop over tokens to read each potential id
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("del: %s is not an ID", id), ud.Message.Chat.ID, false)
return
}
name, err := Client.DeleteTorrent(num, false)
if err != nil {
send("del: "+err.Error(), ud.Message.Chat.ID, false)
return
}
send("Deleted: "+name, ud.Message.Chat.ID, false)
}
}
// deldata takes an id or more, and delete the corresponding torrent/s with their data
func deldata(ud tgbotapi.Update, tokens []string) {
// make sure that we got an argument
if len(tokens) == 0 {
send("deldata: needs an ID", ud.Message.Chat.ID, false)
return
}
// loop over tokens to read each potential id
for _, id := range tokens {
num, err := strconv.Atoi(id)
if err != nil {
send(fmt.Sprintf("deldata: %s is not an ID", id), ud.Message.Chat.ID, false)
return
}
name, err := Client.DeleteTorrent(num, true)
if err != nil {
send("deldata: "+err.Error(), ud.Message.Chat.ID, false)
return
}
send("Deleted with data: "+name, ud.Message.Chat.ID, false)
}
}
// version sends transmission version + transmission-telegram version
func version(ud tgbotapi.Update) {
send(fmt.Sprintf("Transmission *%s*\nTransmission-telegram *%s*", Client.Version(), VERSION), ud.Message.Chat.ID, true)
}
// send takes a chat id and a message to send, returns the message id of the send message
func send(text string, chatID int64, markdown bool) int {
// set typing action
action := tgbotapi.NewChatAction(chatID, tgbotapi.ChatTyping)
Bot.Send(action)
// check the rune count, telegram is limited to 4096 chars per message;
// so if our message is > 4096, split it in chunks the send them.
msgRuneCount := utf8.RuneCountInString(text)
LenCheck:
stop := 4095
if msgRuneCount > 4096 {
for text[stop] != 10 { // '\n'
stop--
}
msg := tgbotapi.NewMessage(chatID, text[:stop])
msg.DisableWebPagePreview = true
if markdown {
msg.ParseMode = tgbotapi.ModeMarkdown
}
// send current chunk
if _, err := Bot.Send(msg); err != nil {
log.Printf("[ERROR] Send: %s", err)
}
// move to the next chunk
text = text[stop:]
msgRuneCount = utf8.RuneCountInString(text)
goto LenCheck
}
// if msgRuneCount < 4096, send it normally
msg := tgbotapi.NewMessage(chatID, text)
msg.DisableWebPagePreview = true
if markdown {
msg.ParseMode = tgbotapi.ModeMarkdown
}
resp, err := Bot.Send(msg)
if err != nil {
log.Printf("[ERROR] Send: %s", err)
}
return resp.MessageID
}