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 <shanhu5739@gmail.com>
This commit is contained in:
Adrian Gallagher
2023-06-05 10:37:13 +10:00
committed by GitHub
parent 3eac6d12bd
commit 400bcb6b56
8 changed files with 116 additions and 55 deletions

View File

@@ -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

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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'",

View File

@@ -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.")
}

View File

@@ -173,7 +173,10 @@
"name": "Telegram",
"enabled": false,
"verbose": false,
"verificationToken": "testest"
"verificationToken": "testest",
"authorisedClients": {
"user_example": 0
}
}
},
"remoteControl": {

View File

@@ -173,7 +173,10 @@
"name": "Telegram",
"enabled": false,
"verbose": false,
"verificationToken": "testest"
"verificationToken": "testest",
"authorisedClients": {
"user_example": 0
}
}
},
"remoteControl": {