interact: support authorizer

This commit is contained in:
c9s 2022-01-14 01:58:04 +08:00
parent 086127e8f7
commit 72a925f659
4 changed files with 111 additions and 54 deletions

View File

@ -10,6 +10,7 @@ import (
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
tb "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2"
"github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/cmd/cmdutil"
@ -104,6 +105,8 @@ func (m *PositionInteraction) Commands(i *interact.Interact) {
} }
func main() { func main() {
log.SetFormatter(&prefixed.TextFormatter{})
b, err := tb.NewBot(tb.Settings{ b, err := tb.NewBot(tb.Settings{
// You can also set custom API URL. // You can also set custom API URL.
// If field is empty it equals to "https://api.telegram.org". // If field is empty it equals to "https://api.telegram.org".

View File

@ -16,7 +16,13 @@ const (
var ErrAuthenticationFailed = errors.New("authentication failed") var ErrAuthenticationFailed = errors.New("authentication failed")
type Authorizer interface {
Authorize() error
}
type AuthInteract struct { type AuthInteract struct {
Strict bool `json:"strict,omitempty"`
Mode AuthMode `json:"authMode"` Mode AuthMode `json:"authMode"`
Token string `json:"authToken,omitempty"` Token string `json:"authToken,omitempty"`
@ -25,25 +31,47 @@ type AuthInteract struct {
} }
func (it *AuthInteract) Commands(interact *Interact) { func (it *AuthInteract) Commands(interact *Interact) {
interact.Command("/auth", func(reply Reply) error { if it.Strict {
reply.Message("Enter your authentication code") interact.Command("/auth", func(reply Reply) error {
return nil reply.Message("Enter your authentication token")
}).NamedNext(StateAuthenticated, func(reply Reply, code string) error { return nil
switch it.Mode { }).Next(func(token string, reply Reply) error {
case AuthModeToken: if token == it.Token {
if code == it.Token { reply.Message("Token passed, please enter your one-time password")
reply.Message("Great! You're authenticated!")
return nil return nil
} }
return ErrAuthenticationFailed
case AuthModeOTP: }).NamedNext(StateAuthenticated, func(code string, reply Reply, authorizer Authorizer) error {
if totp.Validate(code, it.OneTimePasswordKey.Secret()) { if totp.Validate(code, it.OneTimePasswordKey.Secret()) {
reply.Message("Great! You're authenticated!") reply.Message("Great! You're authenticated!")
return nil return authorizer.Authorize()
} }
}
reply.Message("Incorrect authentication code") reply.Message("Incorrect authentication code")
return ErrAuthenticationFailed 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
})
}
} }

View File

@ -17,7 +17,8 @@ type Reply interface {
RemoveKeyboard() 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 { type CustomInteraction interface {
Commands(interact *Interact) Commands(interact *Interact)
@ -41,7 +42,7 @@ type CommandResponder interface {
type Messenger interface { type Messenger interface {
TextMessageResponder TextMessageResponder
CommandResponder CommandResponder
Start() Start(ctx context.Context)
} }
// Interact implements the interaction between bot and message software. // 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) { 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 it.currentState = s
} }
@ -179,8 +180,9 @@ func (it *Interact) runCommand(command string, args []string, ctxObjects ...inte
} }
func (it *Interact) SetMessenger(messenger Messenger) { func (it *Interact) SetMessenger(messenger Messenger) {
messenger.SetTextMessageResponder(func(reply Reply, response string) error { // pass Responder function
return it.handleResponse(response, reply) messenger.SetTextMessageResponder(func(message string, reply Reply, ctxObjects ...interface{}) error {
return it.handleResponse(message, append(ctxObjects, reply)...)
}) })
it.messenger = messenger it.messenger = messenger
} }
@ -218,9 +220,9 @@ func (it *Interact) init() error {
} }
commandName := n commandName := n
it.messenger.AddCommand(commandName, func(reply Reply, response string) error { it.messenger.AddCommand(commandName, func(message string, reply Reply, ctxObjects ...interface{}) error {
args := parseCommand(response) args := parseCommand(message)
return it.runCommand(commandName, args, reply) 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 // TODO: use go routine and context
it.messenger.Start() it.messenger.Start(ctx)
return nil return nil
} }
@ -256,7 +258,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
fv := reflect.ValueOf(f) fv := reflect.ValueOf(f)
ft := reflect.TypeOf(f) ft := reflect.TypeOf(f)
objectIndex := 0
argIndex := 0 argIndex := 0
var rArgs []reflect.Value var rArgs []reflect.Value
@ -268,11 +269,7 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
case reflect.Interface: case reflect.Interface:
found := false found := false
if objectIndex >= len(objects) { for oi := 0; oi < len(objects); oi++ {
return "", fmt.Errorf("found interface type %s, but object args are empty", at)
}
for oi := objectIndex; oi < len(objects); oi++ {
obj := objects[oi] obj := objects[oi]
objT := reflect.TypeOf(obj) objT := reflect.TypeOf(obj)
objV := reflect.ValueOf(obj) objV := reflect.ValueOf(obj)
@ -286,7 +283,6 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
if objT.Implements(at) { if objT.Implements(at) {
found = true found = true
rArgs = append(rArgs, objV) rArgs = append(rArgs, objV)
objectIndex = oi + 1
break break
} }
} }

View File

@ -1,6 +1,7 @@
package interact package interact
import ( import (
"context"
"fmt" "fmt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -38,56 +39,85 @@ func (r *TelegramReply) build() {
r.menu.Reply(rows...) 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 { 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 is used for interact to register its message handler
textMessageResponder Responder textMessageResponder Responder
} }
func (b *Telegram) SetTextMessageResponder(textMessageResponder Responder) { func (tm *Telegram) newAuthorizer(message *telebot.Message) *TelegramAuthorizer {
b.textMessageResponder = textMessageResponder return &TelegramAuthorizer{
Telegram: tm,
Message: message,
}
} }
func (b *Telegram) Start() { func (tm *Telegram) SetTextMessageResponder(textMessageResponder Responder) {
b.Bot.Handle(telebot.OnText, func(m *telebot.Message) { tm.textMessageResponder = textMessageResponder
log.Infof("onText: %+v", m) }
reply := b.newReply() func (tm *Telegram) Start(context.Context) {
if b.textMessageResponder != nil { tm.Bot.Handle(telebot.OnText, func(m *telebot.Message) {
if err := b.textMessageResponder(reply, m.Text); err != nil { log.Infof("[interact][telegram] onText: %+v", m)
log.WithError(err).Errorf("response handling error")
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() reply.build()
if _, err := b.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil {
log.WithError(err).Errorf("message send error") 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) { func (tm *Telegram) AddCommand(command string, responder Responder) {
b.Bot.Handle(command, func(m *telebot.Message) { tm.Bot.Handle(command, func(m *telebot.Message) {
reply := b.newReply() reply := tm.newReply()
if err := responder(reply, m.Payload); err != nil { if err := responder(m.Payload, reply); err != nil {
log.WithError(err).Errorf("responder error") log.WithError(err).Errorf("[interact][telegram] responder error")
b.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err)) tm.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err))
return return
} }
// build up the response objects // build up the response objects
reply.build() reply.build()
if _, err := b.Bot.Send(m.Sender, reply.message, reply.menu); err != nil { if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil {
log.WithError(err).Errorf("message send error") log.WithError(err).Errorf("[interact][telegram] message send error")
} }
}) })
} }
func (b *Telegram) newReply() *TelegramReply { func (tm *Telegram) newReply() *TelegramReply {
return &TelegramReply{ return &TelegramReply{
bot: b.Bot, bot: tm.Bot,
menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true}, menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true},
} }
} }