diff --git a/cmd/documentation/communications_templates/telegram.tmpl b/cmd/documentation/communications_templates/telegram.tmpl index ffe97d2c..b6188d86 100644 --- a/cmd/documentation/communications_templates/telegram.tmpl +++ b/cmd/documentation/communications_templates/telegram.tmpl @@ -17,22 +17,28 @@ developed by Telegram Messenger LLP + [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-communications-via-config-example) - + Individual package example below: + + See the individual package example below. NOTE: For privacy considerations, it's not possible to directly request a user's ID through the + Telegram Bot API unless the user interacts first. The user must message the bot directly. This allows the bot to identify and save the user's ID. + If this wasn't set initially, the user's ID will be stored by this package following a successful authentication when any supported command is issued. + ```go import ( - "github.com/thrasher-corp/gocryptotrader/communications/telegram" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/communications/telegram" ) t := new(telegram.Telegram) // Define Telegram configuration - commsConfig := config.CommunicationsConfig{TelegramConfig: config.TelegramConfig{ - Name: "Telegram", - Enabled: true, - Verbose: false, - VerificationToken: "token", - }} + commsConfig := &base.CommunicationsConfig{ + TelegramConfig: base.TelegramConfig{ + Name: "Telegram", + Enabled: true, + Verbose: false, + VerificationToken: "token", + AuthorisedClients: map[string]int64{"pepe": 0}, // 0 represents a placeholder for the user's ID, see note above for more info. + }, + } t.Setup(commsConfig) err := t.Connect @@ -46,7 +52,6 @@ via Telegram: /start - Will authenticate your ID /status - Displays the status of the bot /help - Displays current command list -/settings - Displays current bot settings ``` ### Please click GoDocs chevron above to view current GoDoc information for this package diff --git a/communications/base/base.go b/communications/base/base.go index 7ba9fbfe..bb1e6d4d 100644 --- a/communications/base/base.go +++ b/communications/base/base.go @@ -45,7 +45,7 @@ func (b *Base) GetName() string { func (b *Base) GetStatus() string { return ` GoCryptoTrader Service: Online - Service Started: ` + b.ServiceStarted.String() + Service Started: ` + b.ServiceStarted.UTC().String() } // SetServiceStarted sets the time the service started @@ -117,8 +117,9 @@ type SMTPConfig struct { // TelegramConfig holds all variables to start and run the Telegram package type TelegramConfig struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - Verbose bool `json:"verbose"` - VerificationToken string `json:"verificationToken"` + Name string `json:"name"` + Enabled bool `json:"enabled"` + Verbose bool `json:"verbose"` + VerificationToken string `json:"verificationToken"` + AuthorisedClients map[string]int64 `json:"authorisedClients"` } diff --git a/communications/telegram/README.md b/communications/telegram/README.md index 35686001..99f2c4c2 100644 --- a/communications/telegram/README.md +++ b/communications/telegram/README.md @@ -35,22 +35,28 @@ developed by Telegram Messenger LLP + [Enable via configuration](https://github.com/thrasher-corp/gocryptotrader/tree/master/config#enable-communications-via-config-example) - + Individual package example below: + + See the individual package example below. NOTE: For privacy considerations, it's not possible to directly request a user's ID through the + Telegram Bot API unless the user interacts first. The user must message the bot directly. This allows the bot to identify and save the user's ID. + If this wasn't set initially, the user's ID will be stored by this package following a successful authentication when any supported command is issued. + ```go import ( - "github.com/thrasher-corp/gocryptotrader/communications/telegram" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/communications/base" + "github.com/thrasher-corp/gocryptotrader/communications/telegram" ) t := new(telegram.Telegram) // Define Telegram configuration - commsConfig := config.CommunicationsConfig{TelegramConfig: config.TelegramConfig{ - Name: "Telegram", - Enabled: true, - Verbose: false, - VerificationToken: "token", - }} + commsConfig := &base.CommunicationsConfig{ + TelegramConfig: base.TelegramConfig{ + Name: "Telegram", + Enabled: true, + Verbose: false, + VerificationToken: "token", + AuthorisedClients: map[string]int64{"pepe": 0}, // 0 represents a placeholder for the user's ID, see note above for more info. + }, + } t.Setup(commsConfig) err := t.Connect @@ -64,7 +70,6 @@ via Telegram: /start - Will authenticate your ID /status - Displays the status of the bot /help - Displays current command list -/settings - Displays current bot settings ``` ### Please click GoDocs chevron above to view current GoDoc information for this package diff --git a/communications/telegram/telegram.go b/communications/telegram/telegram.go index b239268c..1d70f0d5 100644 --- a/communications/telegram/telegram.go +++ b/communications/telegram/telegram.go @@ -10,6 +10,8 @@ import ( "errors" "fmt" "net/http" + "net/url" + "strconv" "strings" "time" @@ -25,17 +27,15 @@ const ( methodGetUpdates = "getUpdates" methodSendMessage = "sendMessage" - cmdStart = "/start" - cmdStatus = "/status" - cmdHelp = "/help" - cmdSettings = "/settings" + cmdStart = "/start" + cmdStatus = "/status" + cmdHelp = "/help" cmdHelpReply = `GoCryptoTrader TelegramBot, thank you for using this service! Current commands are: /start - Will authenticate your ID /status - Displays the status of the bot - /help - Displays current command list - /settings - Displays current bot settings` + /help - Displays current command list` talkRoot = "GoCryptoTrader bot" ) @@ -44,6 +44,9 @@ var ( // ErrWaiter is the default timer to wait if an err occurs // before retrying after successfully connecting ErrWaiter = time.Second * 30 + + // ErrNotConnected is the error message returned if Telegram is not connected + ErrNotConnected = errors.New("Telegram not connected") ) // Telegram is the overarching type across this package @@ -52,7 +55,7 @@ type Telegram struct { initConnected bool Token string Offset int64 - AuthorisedClients []int64 + AuthorisedClients map[string]int64 } // IsConnected returns whether or not the connection is connected @@ -64,6 +67,7 @@ func (t *Telegram) Setup(cfg *base.CommunicationsConfig) { t.Enabled = cfg.TelegramConfig.Enabled t.Token = cfg.TelegramConfig.VerificationToken t.Verbose = cfg.TelegramConfig.Verbose + t.AuthorisedClients = cfg.TelegramConfig.AuthorisedClients } // Connect starts an initial connection @@ -80,15 +84,24 @@ func (t *Telegram) Connect() error { // PushEvent sends an event to a supplied recipient list via telegram func (t *Telegram) PushEvent(event base.Event) error { + if !t.Connected { + return ErrNotConnected + } + msg := fmt.Sprintf("Type: %s Message: %s", event.Type, event.Message) - for i := range t.AuthorisedClients { - err := t.SendMessage(msg, t.AuthorisedClients[i]) - if err != nil { - return err + + var errors error + for user, ID := range t.AuthorisedClients { + if ID == 0 { + log.Warnf(log.CommunicationMgr, "Telegram: Unable to send message to %s as their ID isn't set. A user must issue any supported command to begin a session.\n", user) + continue + } + if err := t.SendMessage(msg, ID); err != nil { + errors = common.AppendError(errors, err) } } - return nil + return errors } // PollerStart starts the long polling sequence @@ -116,7 +129,11 @@ func (t *Telegram) PollerStart() { for i := range resp.Result { if resp.Result[i].UpdateID > t.Offset { - if string(resp.Result[i].Message.Text[0]) == "/" { + username := resp.Result[i].Message.From.UserName + if id, ok := t.AuthorisedClients[username]; ok && resp.Result[i].Message.Text[0] == '/' { + if id == 0 { + t.AuthorisedClients[username] = resp.Result[i].Message.From.ID + } err = t.HandleMessages(resp.Result[i].Message.Text, resp.Result[i].Message.From.ID) if err != nil { log.Errorf(log.CommunicationMgr, "Telegram: Unable to HandleMessages. Error: %s\n", err) @@ -141,14 +158,25 @@ func (t *Telegram) InitialConnect() error { return errors.New(resp.Description) } - warmWelcomeList := make(map[string]int64) + knownBadUsers := make(map[string]bool) // Used to prevent multiple warnings for the same unauthorised user for i := range resp.Result { - if resp.Result[i].Message.From.ID != 0 { - warmWelcomeList[resp.Result[i].Message.From.UserName] = resp.Result[i].Message.From.ID + if resp.Result[i].Message.From.UserName != "" && resp.Result[i].Message.From.ID != 0 { + username := resp.Result[i].Message.From.UserName + if _, ok := t.AuthorisedClients[username]; !ok { + if !knownBadUsers[username] { + log.Warnf(log.CommunicationMgr, "Telegram: Received message from unauthorised user: %s\n", username) + knownBadUsers[username] = true + } + continue + } + t.AuthorisedClients[username] = resp.Result[i].Message.From.ID } } - for userName, ID := range warmWelcomeList { + for userName, ID := range t.AuthorisedClients { + if ID == 0 { + continue + } err = t.SendMessage(fmt.Sprintf("GoCryptoTrader bot has connected: Hello, %s!", userName), ID) if err != nil { log.Errorf(log.CommunicationMgr, "Telegram: Unable to send welcome message. Error: %s\n", err) @@ -188,7 +216,11 @@ func (t *Telegram) HandleMessages(text string, chatID int64) error { // GetUpdates gets new updates via a long poll connection func (t *Telegram) GetUpdates() (GetUpdateResponse, error) { var newUpdates GetUpdateResponse - path := fmt.Sprintf(apiURL, t.Token, methodGetUpdates) + vals := url.Values{} + if t.Offset != 0 { + vals.Set("offset", strconv.FormatInt(t.Offset+1, 10)) + } + path := common.EncodeURLValues(fmt.Sprintf(apiURL, t.Token, methodGetUpdates), vals) return newUpdates, t.SendHTTPRequest(path, nil, &newUpdates) } diff --git a/communications/telegram/telegram_test.go b/communications/telegram/telegram_test.go index 400f0152..af08406d 100644 --- a/communications/telegram/telegram_test.go +++ b/communications/telegram/telegram_test.go @@ -1,6 +1,7 @@ package telegram import ( + "errors" "testing" "github.com/thrasher-corp/gocryptotrader/communications/base" @@ -19,12 +20,13 @@ func TestSetup(t *testing.T) { Enabled: false, Verbose: false, VerificationToken: "testest", + AuthorisedClients: map[string]int64{"sender": 0}, }, }} commsCfg := cfg.GetCommunicationsConfig() var T Telegram T.Setup(&commsCfg) - if T.Name != "Telegram" || T.Enabled || T.Token != "testest" || T.Verbose { + if T.Name != "Telegram" || T.Enabled || T.Token != "testest" || T.Verbose || len(T.AuthorisedClients) != 1 { t.Error("telegram Setup() error, unexpected setup values", T.Name, T.Enabled, @@ -45,10 +47,18 @@ func TestPushEvent(t *testing.T) { t.Parallel() var T Telegram err := T.PushEvent(base.Event{}) - if err != nil { - t.Error("telegram PushEvent() error", err) + if !errors.Is(err, ErrNotConnected) { + t.Errorf("expected %s, got %s", ErrNotConnected, err) } - T.AuthorisedClients = append(T.AuthorisedClients, 1337) + + T.Connected = true + T.AuthorisedClients = map[string]int64{"sender": 0} + err = T.PushEvent(base.Event{}) + if err != nil { + t.Errorf("expected nil, got %s", err) + } + + T.AuthorisedClients = map[string]int64{"sender": 1337} err = T.PushEvent(base.Event{}) if err.Error() != testErrNotFound { t.Errorf("telegram PushEvent() error, expected 'Not found' got '%s'", @@ -75,11 +85,6 @@ func TestHandleMessages(t *testing.T) { t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", err) } - err = T.HandleMessages(cmdSettings, chatID) - if err.Error() != testErrNotFound { - t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", - err) - } err = T.HandleMessages("Not a command", chatID) if err.Error() != testErrNotFound { t.Errorf("telegram HandleMessages() error, expected 'Not found' got '%s'", diff --git a/config/config.go b/config/config.go index b4c890b7..5d4b20e8 100644 --- a/config/config.go +++ b/config/config.go @@ -302,6 +302,10 @@ func (c *Config) CheckCommunicationsConfig() { } } + if c.Communications.TelegramConfig.AuthorisedClients == nil { + c.Communications.TelegramConfig.AuthorisedClients = map[string]int64{"user_example": 0} + } + if c.Communications.SlackConfig.Name != "Slack" || c.Communications.SMSGlobalConfig.Name != "SMSGlobal" || c.Communications.SMTPConfig.Name != "SMTP" || @@ -334,7 +338,10 @@ func (c *Config) CheckCommunicationsConfig() { } } if c.Communications.TelegramConfig.Enabled { - if c.Communications.TelegramConfig.VerificationToken == "" { + if _, ok := c.Communications.TelegramConfig.AuthorisedClients["user_example"]; ok || + len(c.Communications.TelegramConfig.AuthorisedClients) == 0 || + c.Communications.TelegramConfig.VerificationToken == "" || + c.Communications.TelegramConfig.VerificationToken == "testest" { c.Communications.TelegramConfig.Enabled = false log.Warnln(log.ConfigMgr, "Telegram enabled in config but variable data not set, disabling.") } diff --git a/config_example.json b/config_example.json index 4a5de200..af9bf60c 100644 --- a/config_example.json +++ b/config_example.json @@ -173,7 +173,10 @@ "name": "Telegram", "enabled": false, "verbose": false, - "verificationToken": "testest" + "verificationToken": "testest", + "authorisedClients": { + "user_example": 0 + } } }, "remoteControl": { diff --git a/testdata/configtest.json b/testdata/configtest.json index 0fdf5344..0b9f696d 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -173,7 +173,10 @@ "name": "Telegram", "enabled": false, "verbose": false, - "verificationToken": "testest" + "verificationToken": "testest", + "authorisedClients": { + "user_example": 0 + } } }, "remoteControl": {