diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5e2ebe --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +TELEGRAM_TOKEN= +TELEGRAM_USER_ID= +# DEFAULT_DOWNLOADER put the full command +# example +# DEFAULT_DOWNLOADER=curl -O +DEFAULT_DOWNLOADER=wget --content-disposition +# Username of the user u want to run command on as +RUNAS= \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e4f5c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +vendor/ +.realize.yaml +build/ +.DS_Store \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..5d8215c --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,44 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/joho/godotenv" + packages = ["."] + revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b" + version = "v1.2.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/takama/daemon" + packages = ["."] + revision = "aa76b0035d1239b7bb3ae1bd84cd88ef2e0a1f1e" + version = "0.11.0" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = [ + "windows", + "windows/registry", + "windows/svc", + "windows/svc/mgr" + ] + revision = "d0faeb539838e250bd0a9db4182d48d4a1915181" + +[[projects]] + branch = "v2" + name = "gopkg.in/tucnak/telebot.v2" + packages = ["."] + revision = "ab26c41a13a14d620877bc21f61152d3a17493e9" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "41e6c8d9048175766bf95ffb0bceb929619ee87a4cb241a23ef643f338c273f7" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..d7072c2 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,30 @@ +# Gopkg.toml example +# +# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[prune] + go-tests = true + unused-packages = true diff --git a/README.md b/README.md index bc960bd..5ef7432 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ # telegram-commander -self deployed telegram bot that will help with running simple commands +Self deployed telegram bot that will help with running simple commands + + + +- [DESCRIPTION](#description) +- [INSTALLATION](#installation) +- [DEVELOPMENT](#development) +- [TODO](#todo) +- [LIMITATIONS](#limitations) +- [LICENSE](#license) + +## DESCRIPTION + +**telegram-commander** is a self deployed telegram bot that let's you run commands on a remote pc through [telegram](https://telegram.org). There are default commands that will help you run simple commands like Upload a file to pc, Download a file from pc, Download file from internet and Run simple commands and more to come. + +## REQUIREMENTS + +- GoLang > 1.9.1 +- One of terminal file downloaders wget, curl, axel + + +## INSTALLATION + +- Download the latest release binary zip +- unzip the file +- inside there is a .env.example file +- To install it first clone the project on your pc +- Create a telegram bot using [@BotFather](https://t.me/botfather) + - Set the name of the bot + - Set the username of the bot +- Get your telegram user id using [@IDBot](https://t.me/myidbot) +- Copy .env.example to .env +```bash +cp .env.example .env +``` +- Set your bot and telegram user id on .env +```YAML +TELEGRAM_TOKEN= +TELEGRAM_USER_ID= +# DEFAULT_DOWNLOADER put the full command +# example +# DEFAULT_DOWNLOADER=curl -O +DEFAULT_DOWNLOADER=wget --content-disposition +# Username of the user u want to run command on as +RUNAS= +``` +- Run the executable +```bash +./telegram-commander +``` +- If you want to run it as a service +- First install the service +```bash +sudo ./telegram-commander -service install +``` +- you can use your systems service manager to handle the service +```bash +sudo service telegram-commander start +sudo service telegram-commander stop +``` + +## DEVELOPMENT +- get the package +```bash +go get github.com/nathenapse/telegram-commander +``` + +## TODO + +- [ ] Torrent Downloads +- [ ] Youtube Downloads +- [ ] Better Documentation + +## LIMITATIONS + +- Works on linux and mac + +## LICENSE + +MIT \ No newline at end of file diff --git a/command/base.go b/command/base.go new file mode 100644 index 0000000..aa3c115 --- /dev/null +++ b/command/base.go @@ -0,0 +1,35 @@ +package command + +import ( + "os/exec" + "os/user" + "syscall" + + "github.com/nathenapse/telegram-commander/config" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Base is where all structs inherit +type Base struct { +} + +// Validate Default implementation of Validate +func (b *Base) Validate(m *tb.Message) bool { + return m.Payload != "" +} + +// SetUser is to set the user for the command +func SetUser(cmd *exec.Cmd, conf *config.Config) *exec.Cmd { + // TODO: handle error + usr, _ := user.Current() + + if usr.Uid == "0" { + cmd.SysProcAttr = &syscall.SysProcAttr{} + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: conf.Userid, Gid: conf.Groupid} + cmd.Dir = conf.User.HomeDir + } else { + cmd.Dir = usr.HomeDir + } + + return cmd +} diff --git a/command/command.go b/command/command.go new file mode 100644 index 0000000..a01c850 --- /dev/null +++ b/command/command.go @@ -0,0 +1,50 @@ +package command + +import ( + "bytes" + "os/exec" + + "github.com/nathenapse/telegram-commander/config" + "github.com/nathenapse/telegram-commander/custom" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Command the basic command +type Command struct { + *Base +} + +// Exec executes the command +func (c *Command) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + + cmd := exec.Command("sh", "-c", m.Text) + + var out bytes.Buffer + var errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + cmd = SetUser(cmd, conf) + + err := cmd.Run() + + if err != nil { + custom.SendLong(b)(m.Sender, err.Error()+":"+errb.String(), &tb.SendOptions{ + ReplyTo: m, + }) + } else { + custom.SendLong(b)(m.Sender, out.String(), &tb.SendOptions{ + ReplyTo: m, + }) + } + +} + +// IsInline return if command is /command somethingelse +func (c *Command) IsInline() bool { + return false +} + +// GetExample return the how to use of this command +func (c *Command) GetExample() string { + return "Just use it inline" +} diff --git a/command/download.go b/command/download.go new file mode 100644 index 0000000..a131d72 --- /dev/null +++ b/command/download.go @@ -0,0 +1,43 @@ +package command + +import ( + "os" + + "github.com/nathenapse/telegram-commander/config" + "github.com/nathenapse/telegram-commander/custom" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Download to download files +type Download struct { + *Base +} + +// Exec to locate file and send +func (d *Download) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + + path := conf.User.HomeDir + "/" + m.Payload + _, err := os.Stat(path) + + if err == nil { + file := &tb.Document{File: tb.FromDisk(path), Caption: path} + b.Send(m.Sender, file, &tb.SendOptions{ + ReplyTo: m, + }) + } else { + custom.SendLong(b)(m.Sender, err.Error(), &tb.SendOptions{ + ReplyTo: m, + }) + } + +} + +// IsInline return if command is /command somethingelse +func (d *Download) IsInline() bool { + return true +} + +// GetExample return the how to use of this command +func (d *Download) GetExample() string { + return "please use /file relative/path/to/home" +} diff --git a/command/downloader.go b/command/downloader.go new file mode 100644 index 0000000..731ce23 --- /dev/null +++ b/command/downloader.go @@ -0,0 +1,61 @@ +package command + +import ( + "bytes" + "os/exec" + + "github.com/nathenapse/telegram-commander/config" + "github.com/nathenapse/telegram-commander/custom" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Downloader to download files +type Downloader struct { + *Base +} + +// Exec to locate file and send +func (d *Downloader) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + + go b.Reply(m, conf.Downloader+" "+m.Payload) + + // TODO: confirm to overwrite + cmd := exec.Command("sh", "-c", conf.Downloader+" "+m.Payload) + + var out bytes.Buffer + var errb bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &errb + + cmd = SetUser(cmd, conf) + + go b.Reply(m, "Download, starting") + err := cmd.Run() + + if err != nil && errb.String() != "" { + go b.Send(m.Sender, "Download, Stoped with error", &tb.SendOptions{ + ReplyTo: m, + }) + go custom.SendLong(b)(m.Sender, err.Error()+":"+errb.String(), &tb.SendOptions{ + ReplyTo: m, + }) + } else { + go b.Send(m.Sender, "Download, Finished", &tb.SendOptions{ + ReplyTo: m, + }) + go custom.SendLong(b)(m.Sender, out.String(), &tb.SendOptions{ + ReplyTo: m, + }) + } + +} + +// IsInline return if command is /command somethingelse +func (d *Downloader) IsInline() bool { + return true +} + +// GetExample return the how to use of this command +func (d *Downloader) GetExample() string { + return "please use /download url" +} diff --git a/command/help.go b/command/help.go new file mode 100644 index 0000000..ee10f5b --- /dev/null +++ b/command/help.go @@ -0,0 +1,27 @@ +package command + +import ( + "github.com/nathenapse/telegram-commander/config" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Help run on help +type Help struct { + *ListCommands +} + +// Exec to locate file and send +func (h *Help) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + commands, descriptions := h.Setup() + message := `This Bot is used to do simple things to your unix based pc from the confort of telegram + like start downloading a file, run commands, get files, send files .. etc + + Commands To use + ` + + for i := 0; i < len(commands); i++ { + message = message + "/" + commands[i] + " - " + descriptions[i] + "\n" + } + + go b.Reply(m, message) +} diff --git a/command/listCommands.go b/command/listCommands.go new file mode 100644 index 0000000..98f2ef5 --- /dev/null +++ b/command/listCommands.go @@ -0,0 +1,57 @@ +package command + +import ( + "github.com/nathenapse/telegram-commander/config" + tb "gopkg.in/tucnak/telebot.v2" +) + +// ListCommands run on help +type ListCommands struct { + *Base +} + +// Setup where u set the commands +func (l *ListCommands) Setup() (commands []string, descriptions []string) { + commands = []string{ + "start", + "file", + "download", + "listcommands", + "help", + } + descriptions = []string{ + "Prints a welcome messge", + "Sends the file found on the pc to telegram /file path/to/file", + "Starts downloading the url with the set Downloader /download http://url", + "Prints a string that u can use to setcommand on @BotFather", + "Prints the help message", + } + return commands, descriptions +} + +// Exec to locate file and send +func (l *ListCommands) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + commands, descriptions := l.Setup() + message := "" + + for i := 0; i < len(commands); i++ { + message = message + commands[i] + " - " + descriptions[i] + "\n" + } + + go b.Reply(m, message) +} + +// IsInline return if command is /command somethingelse +func (l *ListCommands) IsInline() bool { + return false +} + +// GetExample return the how to use of this command +func (l *ListCommands) GetExample() string { + return "" +} + +// Validate Default implementation of Validate +func (l *ListCommands) Validate(m *tb.Message) bool { + return true +} diff --git a/command/upload.go b/command/upload.go new file mode 100644 index 0000000..0d65567 --- /dev/null +++ b/command/upload.go @@ -0,0 +1,40 @@ +package command + +import ( + "github.com/nathenapse/telegram-commander/config" + "github.com/nathenapse/telegram-commander/custom" + tb "gopkg.in/tucnak/telebot.v2" +) + +// Upload to download files +type Upload struct { + *Base +} + +// Exec to locate file and send +func (u *Upload) Exec(b *tb.Bot, m *tb.Message, conf *config.Config) { + + path := conf.User.HomeDir + "/Downloads/" + m.Document.FileName + // TODO: file already exists do u want to replace it + + if err := b.Download(&m.Document.File, path); err != nil { + custom.SendLong(b)(m.Sender, err.Error(), &tb.SendOptions{ + ReplyTo: m, + }) + return + } + b.Send(m.Sender, "Saved Successfully to "+path, &tb.SendOptions{ + ReplyTo: m, + }) + +} + +// IsInline return if command is /command somethingelse +func (u *Upload) IsInline() bool { + return false +} + +// GetExample return the how to use of this command +func (u *Upload) GetExample() string { + return "send documents" +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..66f188c --- /dev/null +++ b/config/config.go @@ -0,0 +1,72 @@ +package config + +import ( + "errors" + "os" + "os/user" + "strconv" +) + +// Config for the app to user got from .env file +type Config struct { + Token string + Admin int + Username string + User *user.User + Userid uint32 + Groupid uint32 + Downloader string +} + +// Set from .env +func (c *Config) Set() error { + token, found := os.LookupEnv("TELEGRAM_TOKEN") + if found == false { + return errors.New("Telegram Token not set, Please use go Build to build the app") + } + + s, found := os.LookupEnv("TELEGRAM_USER_ID") + if found == false { + return errors.New("Telegram user not set, Please use go Build to build the app") + } + + admin, err := strconv.Atoi(s) + if err != nil { + return err + } + + username, found := os.LookupEnv("RUNAS") + if found == false { + return errors.New("Username not set to run the app as, Please use go Build to build the app") + } + + usr, err := user.Lookup(username) + if err != nil { + return errors.New("User not found with set username") + } + + downloader, found := os.LookupEnv("DEFAULT_DOWNLOADER") + if found == false { + return errors.New("Downloader not set on the app not found") + } + + c.Token = token + c.Admin = admin + c.User = usr + c.Username = username + c.Downloader = downloader + c.Userid = converStringToInt32(usr.Uid) + c.Groupid = converStringToInt32(usr.Gid) + + return nil +} + +func converStringToInt32(str string) (result uint32) { + + i, err := strconv.ParseInt(str, 10, 32) + if err != nil { + panic(err) + } + result = uint32(i) + return +} diff --git a/custom/bot.go b/custom/bot.go new file mode 100644 index 0000000..954e5e4 --- /dev/null +++ b/custom/bot.go @@ -0,0 +1,46 @@ +package custom + +import ( + "bytes" + + tb "gopkg.in/tucnak/telebot.v2" +) + +// SendLong is used to send long messages +func SendLong(b *tb.Bot) func(to tb.Recipient, what interface{}, options ...interface{}) ([]*tb.Message, []error) { + return func(to tb.Recipient, what interface{}, options ...interface{}) ([]*tb.Message, []error) { + messages := []*tb.Message{} + errors := []error{} + switch object := what.(type) { + case string: + strings := splitString(object, 4000) + for _, str := range strings { + message, error := b.Send(to, str, options...) + messages = append(messages, message) + errors = append(errors, error) + } + return messages, errors + default: + return messages, errors + } + } +} + +func splitString(s string, n int) []string { + sub := "" + subs := []string{} + + runes := bytes.Runes([]byte(s)) + l := len(runes) + for i, r := range runes { + sub = sub + string(r) + if (i+1)%n == 0 { + subs = append(subs, sub) + sub = "" + } else if (i + 1) == l { + subs = append(subs, sub) + } + } + + return subs +} diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..8728493 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,106 @@ +package handlers + +import ( + "strings" + + "github.com/nathenapse/telegram-commander/command" + "github.com/nathenapse/telegram-commander/config" + tb "gopkg.in/tucnak/telebot.v2" +) + +type commands interface { + Exec(*tb.Bot, *tb.Message, *config.Config) + IsInline() bool + GetExample() string + Validate(m *tb.Message) bool +} + +// Handlers all command handlers +func Handlers(b *tb.Bot, conf *config.Config) { + // mutex := new(sync.Mutex) + // add new commands here + payloadCommands := map[string]commands{ + "/download": &command.Downloader{}, + "/listcommands": &command.ListCommands{}, + "/file": &command.Download{}, + "/help": &command.Help{}, + } + + b.Handle("/start", func(m *tb.Message) { + if ValidateUser(b, m, conf.Admin) { + go b.Send(m.Sender, "Welcome "+m.Sender.FirstName+" "+m.Sender.LastName) + } + }) + + for command, inter := range payloadCommands { + go func(command string, inter commands) { + attachHandlers(b, command, inter, conf) + }(command, inter) + } + + b.Handle(tb.OnText, func(m *tb.Message) { + if ValidateUser(b, m, conf.Admin) { + go func() { + command := &command.Command{} + command.Exec(b, m, conf) + }() + } + }) + + b.Handle(tb.OnEdited, func(m *tb.Message) { + isCommand := false + if ValidateUser(b, m, conf.Admin) { + for command, inter := range payloadCommands { + if strings.HasPrefix(m.Text, command) { + isCommand = true + m.Payload = strings.TrimLeft(m.Text, command+" ") + if inter.Validate(m) { + go inter.Exec(b, m, conf) + } else { + go b.Reply(m, inter.GetExample()) + } + } + } + if isCommand == false { + go func() { + command := &command.Command{} + command.Exec(b, m, conf) + }() + } + } + }) + + // Handle Document upload + b.Handle(tb.OnDocument, func(m *tb.Message) { + if ValidateUser(b, m, conf.Admin) { + go func() { + command := &command.Upload{} + command.Exec(b, m, conf) + }() + } + }) + +} + +// ValidateUser if message is sent from the user +func ValidateUser(b *tb.Bot, m *tb.Message, user int) bool { + if m.Sender.ID == user { + return true + } + b.Reply(m, "You do not have permission to access this bot.") + return false +} + +func attachHandlers(b *tb.Bot, command string, inter commands, conf *config.Config) { + + b.Handle(command, func(m *tb.Message) { + if ValidateUser(b, m, conf.Admin) { + if inter.Validate(m) { + go inter.Exec(b, m, conf) + } else { + go b.Reply(m, inter.GetExample()) + } + } + }) + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..c8e5072 --- /dev/null +++ b/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "flag" + "log" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/joho/godotenv" + "github.com/kardianos/osext" + "github.com/kardianos/service" + "github.com/nathenapse/telegram-commander/config" + "github.com/nathenapse/telegram-commander/handlers" + tb "gopkg.in/tucnak/telebot.v2" +) + +var logger service.Logger + +// Program structures. +// Define Start and Stop methods. +type program struct { + exit chan struct{} + service service.Service + *config.Config + cmd *exec.Cmd +} + +func (p *program) Start(s service.Service) error { + // Look for exec. + // Verify home directory. + + go p.run() + return nil +} + +func (p *program) run() error { + logger.Info("Starting Telegram Commander") + defer func() { + if service.Interactive() { + p.Stop(p.service) + } else { + p.service.Stop() + } + }() + + b, err := tb.NewBot(tb.Settings{ + Token: p.Config.Token, + Poller: &tb.LongPoller{Timeout: 10 * time.Second}, + }) + + if err != nil { + log.Fatal(err) + } + + handlers.Handlers(b, p.Config) + + b.Start() + + return nil +} +func (p *program) Stop(s service.Service) error { + close(p.exit) + logger.Info("Stopping Telegram Commander") + if p.cmd.ProcessState.Exited() == false { + p.cmd.Process.Kill() + } + if service.Interactive() { + os.Exit(0) + } + return nil +} + +func main() { + envpath, err := getConfigPath() + if err != nil { + log.Fatal(err) + } + godotenv.Load(envpath) + godotenv.Load() + + conf := &config.Config{} + + err = conf.Set() + + if err != nil { + log.Fatal(err) + } + + svcFlag := flag.String("service", "", "Control the system service.") + flag.Parse() + + svcConfig := &service.Config{ + Name: "telegram-commander", + DisplayName: "Telegram Commander", + Description: "Control your pc through telegram", + } + + prg := &program{ + exit: make(chan struct{}), + } + prg.Config = conf + + s, err := service.New(prg, svcConfig) + if err != nil { + log.Fatal(err) + } + prg.service = s + + errs := make(chan error, 5) + logger, err = s.Logger(errs) + if err != nil { + log.Fatal(err) + } + + go func() { + for { + err := <-errs + if err != nil { + log.Print(err) + } + } + }() + + if len(*svcFlag) != 0 { + err := service.Control(s, *svcFlag) + if err != nil { + log.Printf("Valid actions: %q\n", service.ControlAction) + log.Fatal(err) + } + return + } + err = s.Run() + if err != nil { + logger.Error(err) + } +} + +func getConfigPath() (string, error) { + fullexecpath, err := osext.Executable() + if err != nil { + return "", err + } + + dir, _ := filepath.Split(fullexecpath) + + return filepath.Join(dir, ".env"), nil +}