Skip to content

Commit

Permalink
telegram: Fix pushing events to an authorised client list (thrasher-c…
Browse files Browse the repository at this point in the history
…orp#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 <[email protected]>
  • Loading branch information
thrasher- and shanhuhai5739 authored Jun 5, 2023
1 parent 3eac6d1 commit 400bcb6
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 54 deletions.
25 changes: 15 additions & 10 deletions cmd/documentation/communications_templates/telegram.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 6 additions & 5 deletions communications/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
25 changes: 15 additions & 10 deletions communications/telegram/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
68 changes: 50 additions & 18 deletions communications/telegram/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"

Expand All @@ -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"
)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down
21 changes: 13 additions & 8 deletions communications/telegram/telegram_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package telegram

import (
"errors"
"testing"

"github.com/thrasher-corp/gocryptotrader/communications/base"
Expand All @@ -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,
Expand All @@ -45,10 +47,18 @@ func TestPushEvent(t *testing.T) {
t.Parallel()
var T Telegram
err := T.PushEvent(base.Event{})
if !errors.Is(err, ErrNotConnected) {
t.Errorf("expected %s, got %s", ErrNotConnected, err)
}

T.Connected = true
T.AuthorisedClients = map[string]int64{"sender": 0}
err = T.PushEvent(base.Event{})
if err != nil {
t.Error("telegram PushEvent() error", err)
t.Errorf("expected nil, got %s", err)
}
T.AuthorisedClients = append(T.AuthorisedClients, 1337)

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'",
Expand All @@ -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'",
Expand Down
9 changes: 8 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand Down Expand Up @@ -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.")
}
Expand Down
5 changes: 4 additions & 1 deletion config_example.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@
"name": "Telegram",
"enabled": false,
"verbose": false,
"verificationToken": "testest"
"verificationToken": "testest",
"authorisedClients": {
"user_example": 0
}
}
},
"remoteControl": {
Expand Down
5 changes: 4 additions & 1 deletion testdata/configtest.json
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,10 @@
"name": "Telegram",
"enabled": false,
"verbose": false,
"verificationToken": "testest"
"verificationToken": "testest",
"authorisedClients": {
"user_example": 0
}
}
},
"remoteControl": {
Expand Down

0 comments on commit 400bcb6

Please sign in to comment.