diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 6c52a5631..918901fe2 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -58,7 +58,7 @@ type SlackNotification struct { ErrorChannel string `json:"errorChannel,omitempty" yaml:"errorChannel,omitempty"` } -type NotificationRouting struct { +type SlackNotificationRouting struct { Trade string `json:"trade,omitempty" yaml:"trade,omitempty"` Order string `json:"order,omitempty" yaml:"order,omitempty"` SubmitOrder string `json:"submitOrder,omitempty" yaml:"submitOrder,omitempty"` @@ -71,7 +71,7 @@ type NotificationConfig struct { SymbolChannels map[string]string `json:"symbolChannels,omitempty" yaml:"symbolChannels,omitempty"` SessionChannels map[string]string `json:"sessionChannels,omitempty" yaml:"sessionChannels,omitempty"` - Routing *NotificationRouting `json:"routing,omitempty" yaml:"routing,omitempty"` + Routing *SlackNotificationRouting `json:"routing,omitempty" yaml:"routing,omitempty"` } type Session struct { diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 902ad1ff2..7ab10b72f 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -1,19 +1,29 @@ package bbgo import ( + "bytes" "context" "fmt" + "image/png" + "io/ioutil" "os" + "strings" "sync" "time" "github.com/codingconcepts/env" + "github.com/pkg/errors" + "github.com/pquerna/otp" log "github.com/sirupsen/logrus" "github.com/spf13/viper" + "gopkg.in/tucnak/telebot.v2" "github.com/c9s/bbgo/pkg/accounting/pnl" "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/notifier/slacknotifier" + "github.com/c9s/bbgo/pkg/notifier/telegramnotifier" "github.com/c9s/bbgo/pkg/service" + "github.com/c9s/bbgo/pkg/slack/slacklog" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" ) @@ -159,7 +169,7 @@ func (environ *Environment) AddExchange(name string, exchange types.Exchange) (s return environ.AddExchangeSession(name, session) } -func (environ *Environment) AddExchangesFromConfig(userConfig *Config) error { +func (environ *Environment) ConfigureExchangeSessions(userConfig *Config) error { if len(userConfig.Sessions) == 0 { return environ.AddExchangesByViperKeys() } @@ -285,7 +295,7 @@ func (environ *Environment) ConfigurePersistence(conf *PersistenceConfig) error // configure notification rules // for symbol-based routes, we should register the same symbol rules for each session. // for session-based routes, we should set the fixed callbacks for each session -func (environ *Environment) ConfigureNotification(conf *NotificationConfig) error { +func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfig) error { // configure routing here if conf.SymbolChannels != nil { environ.SymbolChannelRouter.AddRoute(conf.SymbolChannels) @@ -524,3 +534,165 @@ func getSessionSymbols(session *ExchangeSession, defaultSymbols ...string) ([]st return session.FindPossibleSymbols() } + +func (environ *Environment) ConfigureNotificationSystem(userConfig *Config) error { + // Configure persistence service, by default we will use memory service + var persistence service.PersistenceService = environ.PersistenceServiceFacade.Memory + if environ.PersistenceServiceFacade.Redis != nil { + persistence = environ.PersistenceServiceFacade.Redis + } + + environ.Notifiability = Notifiability{ + SymbolChannelRouter: NewPatternChannelRouter(nil), + SessionChannelRouter: NewPatternChannelRouter(nil), + ObjectChannelRouter: NewObjectChannelRouter(), + } + + slackToken := viper.GetString("slack-token") + if len(slackToken) > 0 && userConfig.Notifications != nil { + if conf := userConfig.Notifications.Slack; conf != nil { + if conf.ErrorChannel != "" { + log.Infof("found slack configured, setting up log hook...") + log.AddHook(slacklog.NewLogHook(slackToken, conf.ErrorChannel)) + } + + log.Infof("adding slack notifier with default channel: %s", conf.DefaultChannel) + var notifier = slacknotifier.New(slackToken, conf.DefaultChannel) + environ.AddNotifier(notifier) + } + } + + telegramBotToken := viper.GetString("telegram-bot-token") + if len(telegramBotToken) > 0 { + tt := strings.Split(telegramBotToken, ":") + telegramID := tt[0] + + bot, err := telebot.NewBot(telebot.Settings{ + // You can also set custom API URL. + // If field is empty it equals to "https://api.telegram.org". + // URL: "http://195.129.111.17:8012", + Token: telegramBotToken, + Poller: &telebot.LongPoller{Timeout: 10 * time.Second}, + }) + + if err != nil { + return err + } + + // allocate a store, so that we can save the chatID for the owner + var sessionStore = persistence.NewStore("bbgo", "telegram", telegramID) + var interaction = telegramnotifier.NewInteraction(bot, sessionStore) + + authToken := viper.GetString("telegram-bot-auth-token") + if len(authToken) > 0 { + interaction.SetAuthToken(authToken) + + log.Info("telegram bot auth token is set, using fixed token for authorization...") + + printTelegramAuthTokenGuide(authToken) + } + + var session telegramnotifier.Session + if err := sessionStore.Load(&session); err != nil || session.Owner == nil { + log.Warnf("telegram session not found, generating new one-time password key for new telegram session...") + + qrcodeImagePath := fmt.Sprintf("otp-%s.png", telegramID) + key, err := setupNewOTPKey(qrcodeImagePath) + if err != nil { + return errors.Wrapf(err, "failed to setup totp (time-based one time password) key") + } + + session = telegramnotifier.NewSession(key) + if err := sessionStore.Save(&session); err != nil { + return errors.Wrap(err, "failed to save session") + } + } + + go interaction.Start(session) + + var notifier = telegramnotifier.New(interaction) + environ.Notifiability.AddNotifier(notifier) + } + + if userConfig.Notifications != nil { + if err := environ.ConfigureNotificationRouting(userConfig.Notifications); err != nil { + return err + } + } + + return nil +} + +func writeOTPKeyAsQRCodePNG(key *otp.Key, imagePath string) error { + // Convert TOTP key into a PNG + var buf bytes.Buffer + img, err := key.Image(512, 512) + if err != nil { + return err + } + + if err := png.Encode(&buf, img); err != nil { + return err + } + + if err := ioutil.WriteFile(imagePath, buf.Bytes(), 0644); err != nil { + return err + } + + return nil +} + +// setupNewOTPKey generates a new otp key and save the secret as a qrcode image +func setupNewOTPKey(qrcodeImagePath string) (*otp.Key, error) { + key, err := service.NewDefaultTotpKey() + if err != nil { + return nil, errors.Wrapf(err, "failed to setup totp (time-based one time password) key") + } + + printOtpKey(key) + + if err := writeOTPKeyAsQRCodePNG(key, qrcodeImagePath); err != nil { + return nil, err + } + + printTelegramOtpAuthGuide(qrcodeImagePath) + + return key, nil +} + +func printOtpKey(key *otp.Key) { + fmt.Println("") + fmt.Println("====================PLEASE STORE YOUR OTP KEY=======================") + fmt.Println("") + fmt.Printf("Issuer: %s\n", key.Issuer()) + fmt.Printf("AccountName: %s\n", key.AccountName()) + fmt.Printf("Secret: %s\n", key.Secret()) + fmt.Printf("Key URL: %s\n", key.URL()) + fmt.Println("") + fmt.Println("====================================================================") + fmt.Println("") +} + +func printTelegramOtpAuthGuide(qrcodeImagePath string) { + log.Infof("To scan your OTP QR code, please run the following command:") + log.Infof("") + log.Infof("") + log.Infof(" open %s", qrcodeImagePath) + log.Infof("") + log.Infof("") + log.Infof("send the auth command with the generated one-time password to the bbgo bot you created to enable the notification") + log.Infof("") + log.Infof("") + log.Infof(" /auth {code}") + log.Infof("") + log.Infof("") +} + +func printTelegramAuthTokenGuide(token string) { + fmt.Println("send the following command to the bbgo bot you created to enable the notification") + fmt.Println("") + fmt.Println("") + fmt.Printf(" /auth %s\n", token) + fmt.Println("") + fmt.Println("") +} diff --git a/pkg/cmd/cancel.go b/pkg/cmd/cancel.go index fafd900ef..99c24e4d0 100644 --- a/pkg/cmd/cancel.go +++ b/pkg/cmd/cancel.go @@ -66,7 +66,7 @@ var CancelCmd = &cobra.Command{ return err } - if err := environ.AddExchangesFromConfig(userConfig); err != nil { + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err } diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index fae3225cd..cdaf0edf6 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -1,33 +1,22 @@ package cmd import ( - "bytes" "context" - "fmt" - "image/png" "io/ioutil" "os" "os/exec" "path/filepath" - "strings" "syscall" "time" "github.com/pkg/errors" - "github.com/pquerna/otp" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "github.com/spf13/viper" - tb "gopkg.in/tucnak/telebot.v2" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/cmd/cmdutil" - "github.com/c9s/bbgo/pkg/notifier/slacknotifier" - "github.com/c9s/bbgo/pkg/notifier/telegramnotifier" "github.com/c9s/bbgo/pkg/server" - "github.com/c9s/bbgo/pkg/service" - "github.com/c9s/bbgo/pkg/slack/slacklog" ) func init() { @@ -88,116 +77,23 @@ func runSetup(baseCtx context.Context, userConfig *bbgo.Config, enableApiServer return nil } -func newNotificationSystem(userConfig *bbgo.Config, persistence service.PersistenceService) (*bbgo.Notifiability, error) { - notification := &bbgo.Notifiability{ - SymbolChannelRouter: bbgo.NewPatternChannelRouter(nil), - SessionChannelRouter: bbgo.NewPatternChannelRouter(nil), - ObjectChannelRouter: bbgo.NewObjectChannelRouter(), - } - - slackToken := viper.GetString("slack-token") - if len(slackToken) > 0 && userConfig.Notifications != nil { - if conf := userConfig.Notifications.Slack; conf != nil { - if conf.ErrorChannel != "" { - log.Infof("found slack configured, setting up log hook...") - log.AddHook(slacklog.NewLogHook(slackToken, conf.ErrorChannel)) - } - - log.Infof("adding slack notifier with default channel: %s", conf.DefaultChannel) - var notifier = slacknotifier.New(slackToken, conf.DefaultChannel) - notification.AddNotifier(notifier) - } - } - - telegramBotToken := viper.GetString("telegram-bot-token") - if len(telegramBotToken) > 0 { - tt := strings.Split(telegramBotToken, ":") - telegramID := tt[0] - - bot, err := tb.NewBot(tb.Settings{ - // You can also set custom API URL. - // If field is empty it equals to "https://api.telegram.org". - // URL: "http://195.129.111.17:8012", - Token: telegramBotToken, - Poller: &tb.LongPoller{Timeout: 10 * time.Second}, - }) - - if err != nil { - return nil, err - } - - // allocate a store, so that we can save the chatID for the owner - var sessionStore = persistence.NewStore("bbgo", "telegram", telegramID) - var interaction = telegramnotifier.NewInteraction(bot, sessionStore) - - authToken := viper.GetString("telegram-bot-auth-token") - if len(authToken) > 0 { - interaction.SetAuthToken(authToken) - - log.Info("telegram bot auth token is set, using fixed token for authorization...") - - printTelegramAuthTokenGuide(authToken) - } - - var session telegramnotifier.Session - if err := sessionStore.Load(&session); err != nil || session.Owner == nil { - log.Warnf("telegram session not found, generating new one-time password key for new telegram session...") - - qrcodeImagePath := fmt.Sprintf("otp-%s.png", telegramID) - key, err := setupNewOTPKey(qrcodeImagePath) - if err != nil { - return nil, errors.Wrapf(err, "failed to setup totp (time-based one time password) key") - } - - session = telegramnotifier.NewSession(key) - if err := sessionStore.Save(&session); err != nil { - return nil, errors.Wrap(err, "failed to save session") - } - } - - go interaction.Start(session) - - var notifier = telegramnotifier.New(interaction) - notification.AddNotifier(notifier) - } - - return notification, nil -} - func BootstrapEnvironment(ctx context.Context, environ *bbgo.Environment, userConfig *bbgo.Config) error { - if err := environ.ConfigureDatabase(ctx) ; err != nil { + if err := environ.ConfigureDatabase(ctx); err != nil { return err } - if err := environ.AddExchangesFromConfig(userConfig); err != nil { + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err } if userConfig.Persistence != nil { if err := environ.ConfigurePersistence(userConfig.Persistence); err != nil { - return err + return errors.Wrap(err, "persistence configuration error") } } - // configure persistence service, by default we will use memory service - var persistence service.PersistenceService = service.NewMemoryService() - if environ.PersistenceServiceFacade != nil { - if environ.PersistenceServiceFacade.Redis != nil { - persistence = environ.PersistenceServiceFacade.Redis - } - } - - notification, err := newNotificationSystem(userConfig, persistence) - if err != nil { - return err - } - - environ.Notifiability = *notification - - if userConfig.Notifications != nil { - if err := environ.ConfigureNotification(userConfig.Notifications); err != nil { - return err - } + if err := environ.ConfigureNotificationSystem(userConfig) ; err != nil { + return errors.Wrap(err,"notification configuration error") } return nil @@ -363,77 +259,3 @@ func buildAndRun(ctx context.Context, userConfig *bbgo.Config, args ...string) ( return runCmd, runCmd.Start() } -func writeOTPKeyAsQRCodePNG(key *otp.Key, imagePath string) error { - // Convert TOTP key into a PNG - var buf bytes.Buffer - img, err := key.Image(512, 512) - if err != nil { - return err - } - - if err := png.Encode(&buf, img); err != nil { - return err - } - - if err := ioutil.WriteFile(imagePath, buf.Bytes(), 0644); err != nil { - return err - } - - return nil -} - - -// setupNewOTPKey generates a new otp key and save the secret as a qrcode image -func setupNewOTPKey(qrcodeImagePath string) (*otp.Key, error) { - key, err := service.NewDefaultTotpKey() - if err != nil { - return nil, errors.Wrapf(err, "failed to setup totp (time-based one time password) key") - } - - printOtpKey(key) - - if err := writeOTPKeyAsQRCodePNG(key, qrcodeImagePath) ; err != nil { - return nil, err - } - - printTelegramOtpAuthGuide(qrcodeImagePath) - - return key, nil -} - -func printOtpKey(key *otp.Key) { - fmt.Println("") - fmt.Println("====================PLEASE STORE YOUR OTP KEY=======================") - fmt.Println("") - fmt.Printf("Issuer: %s\n", key.Issuer()) - fmt.Printf("AccountName: %s\n", key.AccountName()) - fmt.Printf("Secret: %s\n", key.Secret()) - fmt.Printf("Key URL: %s\n", key.URL()) - fmt.Println("") - fmt.Println("====================================================================") - fmt.Println("") -} - -func printTelegramOtpAuthGuide(qrcodeImagePath string) { - log.Infof("To scan your OTP QR code, please run the following command:") - log.Infof("") - log.Infof("") - log.Infof(" open %s", qrcodeImagePath) - log.Infof("") - log.Infof("") - log.Infof("send the auth command with the generated one-time password to the bbgo bot you created to enable the notification") - log.Infof("") - log.Infof("") - log.Infof(" /auth {code}") - log.Infof("") - log.Infof("") -} - -func printTelegramAuthTokenGuide(token string) { - fmt.Println("send the following command to the bbgo bot you created to enable the notification") - fmt.Println("") - fmt.Println("") - fmt.Printf(" /auth %s\n", token) - fmt.Println("") - fmt.Println("") -} diff --git a/pkg/cmd/sync.go b/pkg/cmd/sync.go index f06087852..13cacd054 100644 --- a/pkg/cmd/sync.go +++ b/pkg/cmd/sync.go @@ -54,7 +54,7 @@ var SyncCmd = &cobra.Command{ return err } - if err := environ.AddExchangesFromConfig(userConfig); err != nil { + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { return err }