mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
implement the basic flow of interact
This commit is contained in:
parent
ba4c694179
commit
7eba6b20c9
140
examples/interact/main.go
Normal file
140
examples/interact/main.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
tb "gopkg.in/tucnak/telebot.v2"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
|
||||||
|
"github.com/c9s/bbgo/pkg/interact"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseFloatPercent(s string, bitSize int) (f float64, err error) {
|
||||||
|
i := strings.Index(s, "%")
|
||||||
|
if i < 0 {
|
||||||
|
return strconv.ParseFloat(s, bitSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err = strconv.ParseFloat(s[:i], bitSize)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return f / 100.0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type closePositionTask struct {
|
||||||
|
symbol string
|
||||||
|
percentage float64
|
||||||
|
confirmed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PositionInteraction struct {
|
||||||
|
closePositionTask closePositionTask
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *PositionInteraction) Commands(i *interact.Interact) {
|
||||||
|
i.Command("/closePosition", func(reply interact.Reply) error {
|
||||||
|
// send symbol options
|
||||||
|
reply.Message("Choose your position")
|
||||||
|
for _, symbol := range []string{"BTCUSDT", "ETHUSDT"} {
|
||||||
|
reply.AddButton(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).Next(func(reply interact.Reply, symbol string) error {
|
||||||
|
// get symbol from user
|
||||||
|
if len(symbol) == 0 {
|
||||||
|
reply.Message("Please enter a symbol")
|
||||||
|
return fmt.Errorf("empty symbol")
|
||||||
|
}
|
||||||
|
switch symbol {
|
||||||
|
case "BTCUSDT", "ETHUSDT":
|
||||||
|
|
||||||
|
default:
|
||||||
|
reply.Message("Invalid symbol")
|
||||||
|
return fmt.Errorf("invalid symbol")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
m.closePositionTask.symbol = symbol
|
||||||
|
|
||||||
|
reply.Message("Choose or enter the percentage to close")
|
||||||
|
for _, symbol := range []string{"25%", "50%", "100%"} {
|
||||||
|
reply.AddButton(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send percentage options
|
||||||
|
return nil
|
||||||
|
}).Next(func(reply interact.Reply, percentageStr string) error {
|
||||||
|
p, err := parseFloatPercent(percentageStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
reply.Message("Not a valid percentage string")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get percentage from user
|
||||||
|
m.closePositionTask.percentage = p
|
||||||
|
|
||||||
|
// send confirmation
|
||||||
|
reply.Message("Are you sure to close the position?")
|
||||||
|
reply.AddButton("Yes")
|
||||||
|
return nil
|
||||||
|
}).Next(func(reply interact.Reply, confirm string) error {
|
||||||
|
switch strings.ToLower(confirm) {
|
||||||
|
case "yes":
|
||||||
|
m.closePositionTask.confirmed = true
|
||||||
|
reply.Message(fmt.Sprintf("Your %s position is closed", m.closePositionTask.symbol))
|
||||||
|
reply.RemoveKeyboard()
|
||||||
|
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
// call position close
|
||||||
|
|
||||||
|
// reply result
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b, 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: os.Getenv("TELEGRAM_BOT_TOKEN"),
|
||||||
|
Poller: &tb.LongPoller{Timeout: 10 * time.Second},
|
||||||
|
// Synchronous: false,
|
||||||
|
// Verbose: true,
|
||||||
|
// ParseMode: "",
|
||||||
|
// Reporter: nil,
|
||||||
|
// Client: nil,
|
||||||
|
// Offline: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
globalInteraction := interact.New()
|
||||||
|
globalInteraction.SetMessenger(&interact.Telegram{
|
||||||
|
Interact: globalInteraction,
|
||||||
|
Bot: b,
|
||||||
|
})
|
||||||
|
|
||||||
|
globalInteraction.AddCustomInteraction(&PositionInteraction{})
|
||||||
|
if err := globalInteraction.Start(ctx); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
tb "gopkg.in/tucnak/telebot.v2"
|
tb "gopkg.in/tucnak/telebot.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +32,7 @@ func main() {
|
||||||
Token: os.Getenv("TELEGRAM_BOT_TOKEN"),
|
Token: os.Getenv("TELEGRAM_BOT_TOKEN"),
|
||||||
Poller: &tb.LongPoller{Timeout: 10 * time.Second},
|
Poller: &tb.LongPoller{Timeout: 10 * time.Second},
|
||||||
// Synchronous: false,
|
// Synchronous: false,
|
||||||
Verbose: true,
|
Verbose: false,
|
||||||
// ParseMode: "",
|
// ParseMode: "",
|
||||||
// Reporter: nil,
|
// Reporter: nil,
|
||||||
// Client: nil,
|
// Client: nil,
|
||||||
|
@ -55,6 +54,8 @@ func main() {
|
||||||
|
|
||||||
// On reply button pressed (message)
|
// On reply button pressed (message)
|
||||||
b.Handle(&btnHelp, func(m *tb.Message) {
|
b.Handle(&btnHelp, func(m *tb.Message) {
|
||||||
|
log.Infof("btnHelp: %#v", m)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Inline buttons.
|
// Inline buttons.
|
||||||
//
|
//
|
||||||
|
@ -86,18 +87,18 @@ func main() {
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Handle("/hello", func(m *tb.Message) {
|
b.Handle("/hello", func(m *tb.Message) {
|
||||||
fmt.Printf("message: %#v\n", m)
|
log.Infof("/hello %#v", m)
|
||||||
// b.Send(m.Sender, "Hello World!")
|
// b.Send(m.Sender, "Hello World!")
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Handle(tb.OnText, func(m *tb.Message) {
|
b.Handle(tb.OnText, func(m *tb.Message) {
|
||||||
fmt.Printf("text: %#v\n", m)
|
log.Infof("[onText] %#v", m)
|
||||||
// all the text messages that weren't
|
// all the text messages that weren't
|
||||||
// captured by existing handlers
|
// captured by existing handlers
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Handle(tb.OnQuery, func(q *tb.Query) {
|
b.Handle(tb.OnQuery, func(q *tb.Query) {
|
||||||
fmt.Printf("query: %#v\n", q)
|
log.Infof("[onQuery] %#v", q)
|
||||||
|
|
||||||
// r := &tb.ReplyMarkup{}
|
// r := &tb.ReplyMarkup{}
|
||||||
// r.URL("test", "https://media.tenor.com/images/f176705ae1bb3c457e19d8cd71718ac0/tenor.gif")
|
// r.URL("test", "https://media.tenor.com/images/f176705ae1bb3c457e19d8cd71718ac0/tenor.gif")
|
||||||
|
|
70
pkg/interact/command.go
Normal file
70
pkg/interact/command.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package interact
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// Command is a domain specific language syntax helper
|
||||||
|
// It's used for helping developer define the state and transition function
|
||||||
|
type Command struct {
|
||||||
|
// Name is the command name
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// StateF is the command handler function
|
||||||
|
F interface{}
|
||||||
|
|
||||||
|
stateID int
|
||||||
|
states map[State]State
|
||||||
|
statesFunc map[State]interface{}
|
||||||
|
initState, lastState State
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCommand(name string, f interface{}) *Command {
|
||||||
|
c := &Command{
|
||||||
|
Name: name,
|
||||||
|
F: f,
|
||||||
|
states: make(map[State]State),
|
||||||
|
statesFunc: make(map[State]interface{}),
|
||||||
|
initState: State(name + "_" + strconv.Itoa(0)),
|
||||||
|
}
|
||||||
|
return c.Next(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transit defines the state transition that is not related to the last defined state.
|
||||||
|
func (c *Command) Transit(state1, state2 State, f interface{}) *Command {
|
||||||
|
c.states[state1] = state2
|
||||||
|
c.statesFunc[state1] = f
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Command) NamedNext(n string, f interface{}) *Command {
|
||||||
|
var curState State
|
||||||
|
if c.lastState == "" {
|
||||||
|
curState = State(c.Name + "_" + strconv.Itoa(c.stateID))
|
||||||
|
} else {
|
||||||
|
curState = c.lastState
|
||||||
|
}
|
||||||
|
|
||||||
|
nextState := State(n)
|
||||||
|
c.states[curState] = nextState
|
||||||
|
c.statesFunc[curState] = f
|
||||||
|
c.lastState = nextState
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next defines the next state with the transition function from the last defined state.
|
||||||
|
func (c *Command) Next(f interface{}) *Command {
|
||||||
|
var curState State
|
||||||
|
if c.lastState == "" {
|
||||||
|
curState = State(c.Name + "_" + strconv.Itoa(c.stateID))
|
||||||
|
} else {
|
||||||
|
curState = c.lastState
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate the next state by the stateID
|
||||||
|
c.stateID++
|
||||||
|
nextState := State(c.Name + "_" + strconv.Itoa(c.stateID))
|
||||||
|
|
||||||
|
c.states[curState] = nextState
|
||||||
|
c.statesFunc[curState] = f
|
||||||
|
c.lastState = nextState
|
||||||
|
return c
|
||||||
|
}
|
|
@ -1,128 +1,148 @@
|
||||||
package interact
|
package interact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/scanner"
|
"text/scanner"
|
||||||
|
|
||||||
"gopkg.in/tucnak/telebot.v2"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Command struct {
|
type Reply interface {
|
||||||
// Name is the command name
|
Message(message string)
|
||||||
Name string
|
AddButton(text string)
|
||||||
|
RemoveKeyboard()
|
||||||
// StateF is the command handler function
|
|
||||||
F interface{}
|
|
||||||
|
|
||||||
stateID int
|
|
||||||
states map[string]string
|
|
||||||
statesFunc map[string]interface{}
|
|
||||||
initState string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommand(name string, f interface{}) *Command {
|
type Responder func(reply Reply, response string) error
|
||||||
c := &Command{
|
|
||||||
Name: name,
|
type CustomInteraction interface {
|
||||||
F: f,
|
Commands(interact *Interact)
|
||||||
states: make(map[string]string),
|
|
||||||
statesFunc: make(map[string]interface{}),
|
|
||||||
initState: name + "_" + strconv.Itoa(0),
|
|
||||||
}
|
|
||||||
return c.Next(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Command) Next(f interface{}) *Command {
|
type State string
|
||||||
curState := c.Name + "_" + strconv.Itoa(c.stateID)
|
|
||||||
c.stateID++
|
|
||||||
nextState := c.Name + "_" + strconv.Itoa(c.stateID)
|
|
||||||
|
|
||||||
c.states[curState] = nextState
|
const (
|
||||||
c.statesFunc[curState] = f
|
StatePublic State = "public"
|
||||||
return c
|
StateAuthenticated State = "authenticated"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Messenger interface {
|
||||||
|
AddCommand(command string, responder Responder)
|
||||||
|
Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interact implements the interaction between bot and message software.
|
// Interact implements the interaction between bot and message software.
|
||||||
type Interact struct {
|
type Interact struct {
|
||||||
commands map[string]*Command
|
commands map[string]*Command
|
||||||
|
|
||||||
states map[string]string
|
states map[State]State
|
||||||
statesFunc map[string]interface{}
|
statesFunc map[State]interface{}
|
||||||
curState string
|
|
||||||
|
originState, currentState State
|
||||||
|
|
||||||
|
messenger Messenger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *Interact {
|
func New() *Interact {
|
||||||
return &Interact{
|
return &Interact{
|
||||||
commands: make(map[string]*Command),
|
commands: make(map[string]*Command),
|
||||||
states: make(map[string]string),
|
originState: StatePublic,
|
||||||
statesFunc: make(map[string]interface{}),
|
currentState: StatePublic,
|
||||||
|
states: make(map[State]State),
|
||||||
|
statesFunc: make(map[State]interface{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Interact) SetOriginState(s State) {
|
||||||
|
i.originState = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interact) AddCustomInteraction(custom CustomInteraction) {
|
||||||
|
custom.Commands(i)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Interact) Command(command string, f interface{}) *Command {
|
func (i *Interact) Command(command string, f interface{}) *Command {
|
||||||
cmd := NewCommand(command, f)
|
cmd := NewCommand(command, f)
|
||||||
i.commands[command] = cmd
|
i.commands[command] = cmd
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interact) getNextState(currentState string) (nextState string, end bool) {
|
func (i *Interact) getNextState(currentState State) (nextState State, final bool) {
|
||||||
var ok bool
|
var ok bool
|
||||||
|
final = false
|
||||||
nextState, ok = i.states[currentState]
|
nextState, ok = i.states[currentState]
|
||||||
if ok {
|
if ok {
|
||||||
end = false
|
// check if it's the final state
|
||||||
return nextState, end
|
if _, hasTransition := i.statesFunc[nextState]; !hasTransition {
|
||||||
|
final = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState, final
|
||||||
}
|
}
|
||||||
|
|
||||||
end = true
|
// state not found, return to the origin state
|
||||||
return nextState, end
|
return i.originState, final
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interact) handleResponse(text string) error {
|
func (i *Interact) setState(s State) {
|
||||||
|
log.Infof("[interact]: tansiting state from %s -> %s", i.currentState, s)
|
||||||
|
i.currentState = s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interact) handleResponse(text string, ctxObjects ...interface{}) error {
|
||||||
args := parseCommand(text)
|
args := parseCommand(text)
|
||||||
|
|
||||||
f, ok := i.statesFunc[i.curState]
|
f, ok := i.statesFunc[i.currentState]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("state function of %s is not defined", i.curState)
|
return fmt.Errorf("state function of %s is not defined", i.currentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := parseFuncArgsAndCall(f, args)
|
err := parseFuncArgsAndCall(f, args, ctxObjects...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nextState, end := i.getNextState(i.curState)
|
nextState, end := i.getNextState(i.currentState)
|
||||||
if end {
|
if end {
|
||||||
|
i.setState(i.originState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i.curState = nextState
|
i.setState(nextState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interact) runCommand(command string, args ...string) error {
|
func (i *Interact) runCommand(command string, args []string, ctxObjects ...interface{}) error {
|
||||||
cmd, ok := i.commands[command]
|
cmd, ok := i.commands[command]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("command %s not found", command)
|
return fmt.Errorf("command %s not found", command)
|
||||||
}
|
}
|
||||||
|
|
||||||
i.curState = cmd.initState
|
i.setState(cmd.initState)
|
||||||
err := parseFuncArgsAndCall(cmd.F, args)
|
err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we can successfully execute the command, then we can go to the next state.
|
// if we can successfully execute the command, then we can go to the next state.
|
||||||
nextState, end := i.getNextState(i.curState)
|
nextState, end := i.getNextState(i.currentState)
|
||||||
if end {
|
if end {
|
||||||
|
i.setState(i.originState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i.curState = nextState
|
i.setState(nextState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Interact) SetMessenger(messenger Messenger) {
|
||||||
|
i.messenger = messenger
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Interact) init() error {
|
func (i *Interact) init() error {
|
||||||
for n, cmd := range i.commands {
|
for n, cmd := range i.commands {
|
||||||
_ = n
|
_ = n
|
||||||
|
@ -136,17 +156,29 @@ func (i *Interact) init() error {
|
||||||
for s, f := range cmd.statesFunc {
|
for s, f := range cmd.statesFunc {
|
||||||
i.statesFunc[s] = f
|
i.statesFunc[s] = f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register commands to the service
|
||||||
|
if i.messenger == nil {
|
||||||
|
return fmt.Errorf("messenger is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
i.messenger.AddCommand(n, func(reply Reply, response string) error {
|
||||||
|
args := parseCommand(response)
|
||||||
|
return i.runCommand(n, args, reply)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interact) HandleTelegramMessage(msg *telebot.Message) {
|
func (i *Interact) Start(ctx context.Context) error {
|
||||||
// For registered commands, will contain the string payload
|
if err := i.init(); err != nil {
|
||||||
// msg.Payload
|
return err
|
||||||
// msg.Text
|
}
|
||||||
args := parseCommand(msg.Text)
|
|
||||||
_ = args
|
// TODO: use go routine and context
|
||||||
|
i.messenger.Start()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCommand(src string) (args []string) {
|
func parseCommand(src string) (args []string) {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
tb "gopkg.in/tucnak/telebot.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) {
|
func Test_parseFuncArgsAndCall_NoErrorFunction(t *testing.T) {
|
||||||
|
@ -66,11 +67,8 @@ type TestInteraction struct {
|
||||||
closePositionTask closePositionTask
|
closePositionTask closePositionTask
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reply interface {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *TestInteraction) Commands(interact *Interact) {
|
func (m *TestInteraction) Commands(interact *Interact) {
|
||||||
interact.Command("closePosition", func(reply Reply) error {
|
interact.Command("/closePosition", func(reply Reply) error {
|
||||||
// send symbol options
|
// send symbol options
|
||||||
return nil
|
return nil
|
||||||
}).Next(func(symbol string) error {
|
}).Next(func(symbol string) error {
|
||||||
|
@ -95,29 +93,43 @@ func (m *TestInteraction) Commands(interact *Interact) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCustomInteraction(t *testing.T) {
|
func TestCustomInteraction(t *testing.T) {
|
||||||
|
b, err := tb.NewBot(tb.Settings{
|
||||||
|
Offline: true,
|
||||||
|
})
|
||||||
|
if !assert.NoError(t, err, "should have bot setup without error") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
globalInteraction := New()
|
globalInteraction := New()
|
||||||
|
|
||||||
|
telegram := &Telegram{
|
||||||
|
Bot: b,
|
||||||
|
Interact: globalInteraction,
|
||||||
|
}
|
||||||
|
globalInteraction.SetMessenger(telegram)
|
||||||
|
|
||||||
testInteraction := &TestInteraction{}
|
testInteraction := &TestInteraction{}
|
||||||
testInteraction.Commands(globalInteraction)
|
testInteraction.Commands(globalInteraction)
|
||||||
|
|
||||||
err := globalInteraction.init()
|
err = globalInteraction.init()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = globalInteraction.runCommand("closePosition")
|
err = globalInteraction.runCommand("/closePosition", []string{}, telegram.newReply())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "closePosition_1", globalInteraction.curState)
|
assert.Equal(t, State("/closePosition_1"), globalInteraction.currentState)
|
||||||
|
|
||||||
err = globalInteraction.handleResponse("BTCUSDT")
|
err = globalInteraction.handleResponse("BTCUSDT", telegram.newReply())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "closePosition_2", globalInteraction.curState)
|
assert.Equal(t, State("/closePosition_2"), globalInteraction.currentState)
|
||||||
|
|
||||||
err = globalInteraction.handleResponse("0.20")
|
err = globalInteraction.handleResponse("0.20", telegram.newReply())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "closePosition_3", globalInteraction.curState)
|
assert.Equal(t, State("/closePosition_3"), globalInteraction.currentState)
|
||||||
|
|
||||||
err = globalInteraction.handleResponse("true")
|
err = globalInteraction.handleResponse("true", telegram.newReply())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "closePosition_4", globalInteraction.curState)
|
assert.Equal(t, State("public"), globalInteraction.currentState)
|
||||||
|
|
||||||
assert.Equal(t, closePositionTask{
|
assert.Equal(t, closePositionTask{
|
||||||
symbol: "BTCUSDT",
|
symbol: "BTCUSDT",
|
||||||
|
|
89
pkg/interact/telegram.go
Normal file
89
pkg/interact/telegram.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package interact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/tucnak/telebot.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramReply struct {
|
||||||
|
bot *telebot.Bot
|
||||||
|
message string
|
||||||
|
menu *telebot.ReplyMarkup
|
||||||
|
buttons [][]telebot.Btn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TelegramReply) Message(message string) {
|
||||||
|
r.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TelegramReply) RemoveKeyboard() {
|
||||||
|
r.menu.ReplyKeyboardRemove = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TelegramReply) AddButton(text string) {
|
||||||
|
var button = r.menu.Text(text)
|
||||||
|
if len(r.buttons) == 0 {
|
||||||
|
r.buttons = append(r.buttons, []telebot.Btn{})
|
||||||
|
}
|
||||||
|
r.buttons[len(r.buttons)-1] = append(r.buttons[len(r.buttons)-1], button)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TelegramReply) build() {
|
||||||
|
var rows []telebot.Row
|
||||||
|
for _, buttons := range r.buttons {
|
||||||
|
rows = append(rows, telebot.Row(buttons))
|
||||||
|
}
|
||||||
|
r.menu.Reply(rows...)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate callbackgen -type Telegram
|
||||||
|
type Telegram struct {
|
||||||
|
Interact *Interact
|
||||||
|
Bot *telebot.Bot
|
||||||
|
|
||||||
|
// messageCallbacks is used for interact to register its message handler
|
||||||
|
messageCallbacks []func(reply Reply, message string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Telegram) Start() {
|
||||||
|
b.Bot.Handle(telebot.OnText, func(m *telebot.Message) {
|
||||||
|
log.Infof("onText: %+v", m)
|
||||||
|
|
||||||
|
reply := b.newReply()
|
||||||
|
if err := b.Interact.handleResponse(m.Text, reply); err != nil {
|
||||||
|
log.WithError(err).Errorf("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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
go b.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))
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Telegram) newReply() *TelegramReply {
|
||||||
|
return &TelegramReply{
|
||||||
|
bot: b.Bot,
|
||||||
|
menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user