transmission-telegram/transmission-telegram.go

530 lines
13 KiB
Go
Raw Normal View History

2016-06-19 08:07:00 +03:00
package main
import (
2016-06-20 05:09:41 +03:00
"bytes"
2016-06-19 08:07:00 +03:00
"flag"
"fmt"
"os"
2016-06-21 18:42:30 +03:00
"regexp"
"strconv"
2016-06-19 08:07:00 +03:00
"strings"
2016-06-20 05:09:41 +03:00
"unicode/utf8"
2016-06-19 08:07:00 +03:00
2016-06-19 22:02:15 +03:00
"gopkg.in/telegram-bot-api.v4"
2016-06-21 18:42:30 +03:00
"github.com/pivotal-golang/bytefmt"
2016-06-19 08:07:00 +03:00
"github.com/pyed/transmission"
)
2016-06-19 22:33:57 +03:00
const VERSION = "0.0"
2016-06-19 08:07:00 +03:00
var (
2016-06-19 22:02:15 +03:00
// flags
2016-06-19 22:33:57 +03:00
BotToken string
Master string
RpcUrl string
Username string
Password string
2016-06-19 08:07:00 +03:00
2016-06-19 22:02:15 +03:00
// transmission
2016-06-21 18:42:30 +03:00
Client transmission.TransmissionClient
2016-06-19 22:02:15 +03:00
// telegram
Bot *tgbotapi.BotAPI
Updates <-chan tgbotapi.Update
2016-06-19 08:07:00 +03:00
)
2016-06-19 22:02:15 +03:00
// init flags
2016-06-19 08:07:00 +03:00
func init() {
// define arguments and parse them.
2016-06-19 22:33:57 +03:00
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")
2016-06-19 08:07:00 +03:00
// set the usage message
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage: transmission-bot -token=<TOKEN> -master=<@tuser> -url=[http://] -username=[user] -password=[pass]\n")
flag.PrintDefaults()
}
flag.Parse()
// make sure that we have the two madatory arguments: telegram token & master's handler.
2016-06-19 22:33:57 +03:00
if BotToken == "" ||
Master == "" {
2016-06-19 08:07:00 +03:00
fmt.Fprintf(os.Stderr, "Error: Mandatory argument missing!\n\n")
flag.Usage()
os.Exit(1)
}
2016-06-20 05:09:41 +03:00
// make sure that the handler doesn't contain @
if strings.Contains(Master, "@") {
Master = strings.Replace(Master, "@", "", -1)
}
2016-06-19 22:02:15 +03:00
}
2016-06-19 08:07:00 +03:00
2016-06-19 22:02:15 +03:00
// init transmission
func init() {
2016-06-19 08:07:00 +03:00
// set transmission.Config, needed to establish a connection with transmission
2016-06-21 18:42:30 +03:00
// conf := transmission.Config{
// Address: RpcUrl,
// User: Username,
// Password: Password,
// }
2016-06-19 08:07:00 +03:00
// transmission.New() never returns an error, we will ignore it and test with client.Session.Update()
2016-06-21 18:42:30 +03:00
Client = transmission.New(RpcUrl, Username, Password)
// if err := Client.Session.Update(); err != nil {
// try to predict the error message, as it vague coming from pyed/transmission
// if strings.HasPrefix(err.Error(), "invalid character") { // means the user or the pass is wrong.
// fmt.Fprintf(os.Stderr, "Transmission's Username or Password is wrong.\n\n")
// } else { // any other error is probaby because of the URL
// fmt.Fprintf(os.Stderr, "Error: Couldn't connect to: %s\n", RpcUrl)
// fmt.Fprintf(os.Stderr, "Make sure to pass the right full RPC URL e.g. http://localhost:9091/transmission/rpc\n\n")
// }
// // send the vague error message too
// fmt.Fprintf(os.Stderr, "JSONError: %s\n", err)
// os.Exit(1)
// }
2016-06-19 22:02:15 +03:00
}
// init telegram
func init() {
// authorize using the token
var err error
2016-06-19 22:33:57 +03:00
Bot, err = tgbotapi.NewBotAPI(BotToken)
2016-06-19 22:02:15 +03:00
if err != nil {
fmt.Fprintf(os.Stderr, "Telegram Error: %s\n", err)
os.Exit(1)
}
2016-06-20 05:09:41 +03:00
fmt.Fprintf(os.Stdout, "Authorized on %s", Bot.Self.UserName)
2016-06-19 22:02:15 +03:00
// get a channel and sign it to 'Updates'
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
Updates, err = Bot.GetUpdatesChan(u)
if err != nil {
fmt.Fprintf(os.Stderr, "Telegram Error: %s\n", err)
2016-06-19 08:07:00 +03:00
os.Exit(1)
}
}
2016-06-19 22:33:57 +03:00
func main() {
for update := range Updates {
// ignore anyone other than 'master'
2016-06-20 05:09:41 +03:00
if strings.ToLower(update.Message.From.UserName) != strings.ToLower(Master) {
2016-06-19 22:33:57 +03:00
continue
}
// ignore edited messages
if update.Message == nil {
continue
}
// tokenize the update
tokens := strings.Split(update.Message.Text, " ")
command := strings.ToLower(tokens[0])
switch command {
case "list", "/list":
// list torrents
2016-06-21 18:42:30 +03:00
// TODO take argument as tracker and list those only
2016-06-20 05:09:41 +03:00
go list(&update)
2016-06-19 22:33:57 +03:00
case "downs", "/downs":
// list downloading
2016-06-21 18:42:30 +03:00
go downs(&update)
2016-06-19 22:33:57 +03:00
case "active", "/active":
// list active torrents
2016-06-21 18:42:30 +03:00
go active(&update)
2016-06-19 22:33:57 +03:00
case "errors", "/errors":
// list torrents with errors
2016-06-21 18:42:30 +03:00
go errors(&update)
2016-06-19 22:33:57 +03:00
case "trackers", "/trackers":
// list trackers
2016-06-21 18:42:30 +03:00
go trackers(&update)
2016-06-19 22:33:57 +03:00
case "add", "/add":
// takes url to a torrent to add it
case "search", "/search":
// search for a torrent
2016-06-21 18:42:30 +03:00
go search(&update, tokens[1:])
2016-06-19 22:33:57 +03:00
case "latest", "/latest":
// get the latest torrents
2016-06-21 18:42:30 +03:00
go latest(&update, tokens[1:])
2016-06-19 22:33:57 +03:00
case "info", "/info":
// gets info on specific torrent
case "stop", "/stop":
// stop one torrent or more
2016-06-21 18:42:30 +03:00
go stop(&update, tokens[1:])
2016-06-19 22:33:57 +03:00
case "stopall", "/stopall":
// stops all the torrents
2016-06-21 18:42:30 +03:00
go stopall(&update)
2016-06-19 22:33:57 +03:00
case "start", "/start":
// starts one torrent or more
2016-06-21 18:42:30 +03:00
go start(&update, tokens[1:])
2016-06-19 22:33:57 +03:00
case "startall", "/startall":
// starts all the torrents
2016-06-21 18:42:30 +03:00
go startall(&update)
2016-06-19 22:33:57 +03:00
case "stats", "/stats":
// print transmission stats
case "speed", "/speed":
// print current download and upload speeds
case "del", "/del":
// deletes a torrent but keep its data
case "deldata", "/deldata":
// deletes a torrents and its data
case "help", "/help":
// prints a help message
case "version", "/version":
// print transmission and transmission-telegram versions
default:
// no such command, try help
}
}
}
2016-06-20 05:09:41 +03:00
// list will form and send a list of all the torrents
func list(ud *tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("list: "+err.Error(), ud.Message.Chat.ID)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
if buf.Len() == 0 {
send("No torrents exist!", ud.Message.Chat.ID)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
2016-06-21 18:42:30 +03:00
// downs will send the names of torrents with status: Downloading
func downs(ud *tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("downs: "+err.Error(), ud.Message.Chat.ID)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
// Downloading or in queue to download
if torrents[i].Status == 4 ||
torrents[i].Status == 3 {
buf.WriteString(fmt.Sprintf("<%d> %s\n", torrents[i].ID, torrents[i].Name))
}
}
if buf.Len() == 0 {
send("No downloads", ud.Message.Chat.ID)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
// active will send torrents that are actively uploading
func active(ud *tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("active: "+err.Error(), ud.Message.Chat.ID)
return
}
buf := new(bytes.Buffer)
for i := range torrents {
if torrents[i].RateUpload > 0 {
buf.WriteString(fmt.Sprintf("<%d> %s\n\t⬆ %s\n",
torrents[i].ID, torrents[i].Name, bytefmt.ByteSize(uint64(torrents[i].RateUpload))))
}
}
if buf.Len() == 0 {
send("No active torrents", ud.Message.Chat.ID)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
// 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)
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)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
var trackerRegex = regexp.MustCompile(`https?://([^:/]*)`)
// 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)
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)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
// add
// 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)
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)
return
}
torrents, err := Client.GetTorrents()
if err != nil {
send("search: "+err.Error(), ud.Message.Chat.ID)
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)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
// 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)
return
}
}
torrents, err := Client.GetTorrents()
if err != nil {
send("latest: "+err.Error(), ud.Message.Chat.ID)
return
}
// make sure that we stay in the boundaries
torrentsLen := len(torrents)
if n <= 0 || n > torrentsLen {
n = torrentsLen
}
// sort by addedDate, and set reverse to true to get the latest first
torrents.SortByAddedDate(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("No torrents", ud.Message.Chat.ID)
return
}
send(buf.String(), ud.Message.Chat.ID)
}
// stop takes one or more torrent's ids and 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)
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)
continue
}
status, err := Client.StopTorrent(num)
if err != nil {
send("stop: "+err.Error(), ud.Message.Chat.ID)
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)
return
}
send(fmt.Sprintf("[%s] stop: %s", status, torrent.Name), ud.Message.Chat.ID)
}
}
// stopall will stop all the torrents
func stopall(ud *tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("stopall: "+err.Error(), ud.Message.Chat.ID)
return
}
for i := range torrents {
if status, err := Client.StopTorrent(torrents[i].ID); err != nil {
send(fmt.Sprintf("[%s] stopall: error stopping %s : %s", status, torrents[i].Name, err.Error()), ud.Message.Chat.ID)
}
}
// this will get sent no matter what.
send("stopall: success", ud.Message.Chat.ID)
}
// start takes an id of a torrent and starts it
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)
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)
continue
}
status, err := Client.StartTorrent(num)
if err != nil {
send("stop: "+err.Error(), ud.Message.Chat.ID)
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)
return
}
send(fmt.Sprintf("[%s] start: %s", status, torrent.Name), ud.Message.Chat.ID)
}
}
// startall will start all the torrents
func startall(ud *tgbotapi.Update) {
torrents, err := Client.GetTorrents()
if err != nil {
send("startall: "+err.Error(), ud.Message.Chat.ID)
return
}
for i := range torrents {
if status, err := Client.StartTorrent(torrents[i].ID); err != nil {
send(fmt.Sprintf("[%s] startall: error starting %s : %s", status, torrents[i].Name, err.Error()), ud.Message.Chat.ID)
}
}
// this will get sent no matter what.
send("startall: success", ud.Message.Chat.ID)
}
2016-06-20 05:09:41 +03:00
// send takes a chat id and a message to send.
func send(text string, chatID int64) {
// 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:
if msgRuneCount > 4096 {
msg := tgbotapi.NewMessage(chatID, text[:4095])
2016-06-21 18:42:30 +03:00
msg.DisableWebPagePreview = true
2016-06-20 05:09:41 +03:00
// send current chunk
if _, err := Bot.Send(msg); err != nil {
fmt.Fprintf(os.Stderr, "send error: %s\n", err)
}
// move to the next chunk
text = text[4095:]
msgRuneCount = utf8.RuneCountInString(text)
goto LenCheck
}
// if msgRuneCount < 4096, send it normally
msg := tgbotapi.NewMessage(chatID, text)
2016-06-21 18:42:30 +03:00
msg.DisableWebPagePreview = true
2016-06-20 05:09:41 +03:00
if _, err := Bot.Send(msg); err != nil {
fmt.Fprint(os.Stderr, "send error: %s\n", err)
}
}