diff --git a/examples/interact/main.go b/examples/interact/main.go index 7499ac6ed..f09e37f0d 100644 --- a/examples/interact/main.go +++ b/examples/interact/main.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" tb "gopkg.in/tucnak/telebot.v2" "github.com/c9s/bbgo/pkg/cmd/cmdutil" @@ -104,6 +105,8 @@ func (m *PositionInteraction) Commands(i *interact.Interact) { } func main() { + log.SetFormatter(&prefixed.TextFormatter{}) + b, err := tb.NewBot(tb.Settings{ // You can also set custom API URL. // If field is empty it equals to "https://api.telegram.org". diff --git a/pkg/interact/auth.go b/pkg/interact/auth.go index ff7babd9f..dba8b0267 100644 --- a/pkg/interact/auth.go +++ b/pkg/interact/auth.go @@ -16,7 +16,13 @@ const ( var ErrAuthenticationFailed = errors.New("authentication failed") +type Authorizer interface { + Authorize() error +} + type AuthInteract struct { + Strict bool `json:"strict,omitempty"` + Mode AuthMode `json:"authMode"` Token string `json:"authToken,omitempty"` @@ -25,25 +31,47 @@ type AuthInteract struct { } func (it *AuthInteract) Commands(interact *Interact) { - interact.Command("/auth", func(reply Reply) error { - reply.Message("Enter your authentication code") - return nil - }).NamedNext(StateAuthenticated, func(reply Reply, code string) error { - switch it.Mode { - case AuthModeToken: - if code == it.Token { - reply.Message("Great! You're authenticated!") + if it.Strict { + interact.Command("/auth", func(reply Reply) error { + reply.Message("Enter your authentication token") + return nil + }).Next(func(token string, reply Reply) error { + if token == it.Token { + reply.Message("Token passed, please enter your one-time password") return nil } - - case AuthModeOTP: + return ErrAuthenticationFailed + }).NamedNext(StateAuthenticated, func(code string, reply Reply, authorizer Authorizer) error { if totp.Validate(code, it.OneTimePasswordKey.Secret()) { reply.Message("Great! You're authenticated!") - return nil + return authorizer.Authorize() } - } - reply.Message("Incorrect authentication code") - return ErrAuthenticationFailed - }) + reply.Message("Incorrect authentication code") + return ErrAuthenticationFailed + }) + } else { + interact.Command("/auth", func(reply Reply) error { + reply.Message("Enter your authentication code") + return nil + }).NamedNext(StateAuthenticated, func(code string, reply Reply, authorizer Authorizer) error { + switch it.Mode { + case AuthModeToken: + if code == it.Token { + reply.Message("Great! You're authenticated!") + return authorizer.Authorize() + } + + case AuthModeOTP: + if totp.Validate(code, it.OneTimePasswordKey.Secret()) { + reply.Message("Great! You're authenticated!") + return authorizer.Authorize() + } + } + + reply.Message("Incorrect authentication code") + return ErrAuthenticationFailed + }) + } + } diff --git a/pkg/interact/interact.go b/pkg/interact/interact.go index 3468674bf..abc2ddda9 100644 --- a/pkg/interact/interact.go +++ b/pkg/interact/interact.go @@ -17,7 +17,8 @@ type Reply interface { RemoveKeyboard() } -type Responder func(reply Reply, response string) error +// Responder defines the logic of responding the message +type Responder func(message string, reply Reply, ctxObjects ...interface{}) error type CustomInteraction interface { Commands(interact *Interact) @@ -41,7 +42,7 @@ type CommandResponder interface { type Messenger interface { TextMessageResponder CommandResponder - Start() + Start(ctx context.Context) } // Interact implements the interaction between bot and message software. @@ -108,7 +109,7 @@ func (it *Interact) getNextState(currentState State) (nextState State, final boo } func (it *Interact) setState(s State) { - log.Infof("[interact]: transiting state from %s -> %s", it.currentState, s) + log.Infof("[interact] transiting state from %s -> %s", it.currentState, s) it.currentState = s } @@ -179,8 +180,9 @@ func (it *Interact) runCommand(command string, args []string, ctxObjects ...inte } func (it *Interact) SetMessenger(messenger Messenger) { - messenger.SetTextMessageResponder(func(reply Reply, response string) error { - return it.handleResponse(response, reply) + // pass Responder function + messenger.SetTextMessageResponder(func(message string, reply Reply, ctxObjects ...interface{}) error { + return it.handleResponse(message, append(ctxObjects, reply)...) }) it.messenger = messenger } @@ -218,9 +220,9 @@ func (it *Interact) init() error { } commandName := n - it.messenger.AddCommand(commandName, func(reply Reply, response string) error { - args := parseCommand(response) - return it.runCommand(commandName, args, reply) + it.messenger.AddCommand(commandName, func(message string, reply Reply, ctxObjects ...interface{}) error { + args := parseCommand(message) + return it.runCommand(commandName, args, append(ctxObjects, reply)...) }) } @@ -233,7 +235,7 @@ func (it *Interact) Start(ctx context.Context) error { } // TODO: use go routine and context - it.messenger.Start() + it.messenger.Start(ctx) return nil } @@ -256,7 +258,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) fv := reflect.ValueOf(f) ft := reflect.TypeOf(f) - objectIndex := 0 argIndex := 0 var rArgs []reflect.Value @@ -268,11 +269,7 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) case reflect.Interface: found := false - if objectIndex >= len(objects) { - return "", fmt.Errorf("found interface type %s, but object args are empty", at) - } - - for oi := objectIndex; oi < len(objects); oi++ { + for oi := 0; oi < len(objects); oi++ { obj := objects[oi] objT := reflect.TypeOf(obj) objV := reflect.ValueOf(obj) @@ -286,7 +283,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) if objT.Implements(at) { found = true rArgs = append(rArgs, objV) - objectIndex = oi + 1 break } } diff --git a/pkg/interact/telegram.go b/pkg/interact/telegram.go index 3e34d9d71..f151af8f7 100644 --- a/pkg/interact/telegram.go +++ b/pkg/interact/telegram.go @@ -1,6 +1,7 @@ package interact import ( + "context" "fmt" log "github.com/sirupsen/logrus" @@ -38,56 +39,85 @@ func (r *TelegramReply) build() { r.menu.Reply(rows...) } +type TelegramAuthorizer struct { + Telegram *Telegram + Message *telebot.Message +} + +func (a *TelegramAuthorizer) Authorize() error { + a.Telegram.Owner = a.Message.Sender + a.Telegram.OwnerChat = a.Message.Chat + + log.Infof("[interact][telegram] authorized owner %+v and chat %+v", a.Message.Sender, a.Message.Chat) + return nil +} + type Telegram struct { - Bot *telebot.Bot + Bot *telebot.Bot `json:"-"` + + // Owner is the authorized bot owner + // This field is exported in order to be stored in file + Owner *telebot.User `json:"owner"` + + // OwnerChat is the chat of the authorized bot owner + // This field is exported in order to be stored in file + OwnerChat *telebot.Chat `json:"chat"` // textMessageResponder is used for interact to register its message handler textMessageResponder Responder } -func (b *Telegram) SetTextMessageResponder(textMessageResponder Responder) { - b.textMessageResponder = textMessageResponder +func (tm *Telegram) newAuthorizer(message *telebot.Message) *TelegramAuthorizer { + return &TelegramAuthorizer{ + Telegram: tm, + Message: message, + } } -func (b *Telegram) Start() { - b.Bot.Handle(telebot.OnText, func(m *telebot.Message) { - log.Infof("onText: %+v", m) +func (tm *Telegram) SetTextMessageResponder(textMessageResponder Responder) { + tm.textMessageResponder = textMessageResponder +} - reply := b.newReply() - if b.textMessageResponder != nil { - if err := b.textMessageResponder(reply, m.Text); err != nil { - log.WithError(err).Errorf("response handling error") +func (tm *Telegram) Start(context.Context) { + tm.Bot.Handle(telebot.OnText, func(m *telebot.Message) { + log.Infof("[interact][telegram] onText: %+v", m) + + authorizer := tm.newAuthorizer(m) + reply := tm.newReply() + if tm.textMessageResponder != nil { + if err := tm.textMessageResponder(m.Text, reply, authorizer); err != nil { + log.WithError(err).Errorf("[interact][telegram] response handling error") } } reply.build() - if _, err := b.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { - log.WithError(err).Errorf("message send error") + if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { + log.WithError(err).Errorf("[interact][telegram] message send error") } }) - go b.Bot.Start() + go tm.Bot.Start() } -func (b *Telegram) AddCommand(command string, responder Responder) { - b.Bot.Handle(command, func(m *telebot.Message) { - reply := b.newReply() - if err := responder(reply, m.Payload); err != nil { - log.WithError(err).Errorf("responder error") - b.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err)) +func (tm *Telegram) AddCommand(command string, responder Responder) { + tm.Bot.Handle(command, func(m *telebot.Message) { + reply := tm.newReply() + if err := responder(m.Payload, reply); err != nil { + log.WithError(err).Errorf("[interact][telegram] responder error") + tm.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err)) return } // build up the response objects reply.build() - if _, err := b.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { - log.WithError(err).Errorf("message send error") + if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { + log.WithError(err).Errorf("[interact][telegram] message send error") } }) } -func (b *Telegram) newReply() *TelegramReply { +func (tm *Telegram) newReply() *TelegramReply { return &TelegramReply{ - bot: b.Bot, + bot: tm.Bot, menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true}, } }