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"
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".

View File

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

View File

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

View File

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