From 400bcb6b56f4a661881811301cfe8e1e9e398ebc Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Mon, 5 Jun 2023 10:37:13 +1000 Subject: [PATCH] telegram: Fix pushing events to an authorised client list (#1208) * fix_communications_authorised_clients * Telegram: Link config to authorised clients list * Telegram: Prevent multiple spam messages from unauthed user * Telegram: Improve command handling for authenticated users Telegram doesn't allow you to easily fetch the user ID of a user unless they have previously sent you a message and is currently waiting to be processed, or if they message you on the fly once the bot is connected. This ensures that the user ID is stored for future usage upon a single successful auth command. It also fixes the offset as the previous code wouldn't be able to process incoming messages once connected and instead only relay them. * Bump docs * default to UTC time in case bot is run on a server with diff time zones * Enhance config for already upgraded configs --------- Co-authored-by: shanhuhai5739 --- .../communications_templates/telegram.tmpl | 25 ++++--- communications/base/base.go | 11 +-- communications/telegram/README.md | 25 ++++--- communications/telegram/telegram.go | 68 ++++++++++++++----- communications/telegram/telegram_test.go | 23 ++++--- config/config.go | 9 ++- config_example.json | 5 +- testdata/configtest.json | 5 +- 8 files changed, 116 insertions(+), 55 deletions(-) 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": {