Skip to content

Commit

Permalink
feat: support bot commands + add some tests
Browse files Browse the repository at this point in the history
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
moul committed Oct 9, 2020
1 parent cf0adaf commit e9831cc
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 65 deletions.
4 changes: 4 additions & 0 deletions go/cmd/testbot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"moul.io/zapconfig"

"berty.tech/berty/v2/go/pkg/bertybot"
"berty.tech/berty/v2/go/pkg/bertyversion"
)

var (
Expand Down Expand Up @@ -52,6 +53,9 @@ func Main() error {
bertybot.WithRecipe(bertybot.AutoAcceptIncomingContactRequestRecipe()), // accept incoming contact requests
bertybot.WithRecipe(bertybot.WelcomeMessageRecipe("welcome to testbot")), // send welcome message to new contacts and new conversations
bertybot.WithRecipe(bertybot.EchoRecipe("you said: ")), // reply to messages with the same message
bertybot.WithCommand("version", "show version", func(ctx bertybot.Context) {
_ = ctx.ReplyString("version: " + bertyversion.Version)
}),
)
if *skipReplay {
opts = append(opts, bertybot.WithSkipReplay()) // skip old events, only consume fresh ones
Expand Down
2 changes: 2 additions & 0 deletions go/pkg/bertybot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Bot struct {
handlers map[HandlerType][]Handler
isReplaying bool
handledEvents uint
commands map[string]command
store struct {
conversations map[string]*bertymessenger.Conversation
mutex sync.Mutex
Expand All @@ -35,6 +36,7 @@ func New(opts ...NewOption) (*Bot, error) {
b := Bot{
logger: zap.NewNop(),
handlers: make(map[HandlerType][]Handler),
commands: make(map[string]command),
}
b.store.conversations = make(map[string]*bertymessenger.Conversation)

Expand Down
111 changes: 111 additions & 0 deletions go/pkg/bertybot/bot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package bertybot

import (
"context"
"testing"
"time"

"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/require"

"berty.tech/berty/v2/go/internal/testutil"
"berty.tech/berty/v2/go/pkg/bertymessenger"
)

func TestUnstableBotCommunication(t *testing.T) {
testutil.FilterStability(t, testutil.Unstable)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
logger, cleanup := testutil.Logger(t)
defer cleanup()
clients, cleanup := bertymessenger.TestingInfra(ctx, t, 2, logger)
defer cleanup()

botClient, userClient := clients[0], clients[1]

var commandCalled bool

// create bot
var bot *Bot
{
botLogger := logger.Named("botlib")
var err error
bot, err = New(
WithLogger(botLogger),
WithMessengerClient(botClient),
WithRecipe(DebugEventRecipe(botLogger)),
WithRecipe(AutoAcceptIncomingContactRequestRecipe()),
WithRecipe(EchoRecipe("hello ")),
WithCommand("ping", "reply pong", func(ctx Context) {
ctx.ReplyString("pong")
commandCalled = true
}),
)
require.NoError(t, err)
}

go func() {
err := bot.Start(ctx)
if err != nil {
require.Contains(t, err.Error(), "is closing") // FIXME: better closing handling
}
}()

// enable contact request on client
{
_, err := userClient.InstanceShareableBertyID(ctx, &bertymessenger.InstanceShareableBertyID_Request{})
require.NoError(t, err)
time.Sleep(200 * time.Millisecond) // FIXME: replace with dynamic waiting
}

// send contact request
{
parsed, err := botClient.ParseDeepLink(ctx, &bertymessenger.ParseDeepLink_Request{Link: bot.BertyIDURL()})
require.NoError(t, err)
_, err = userClient.SendContactRequest(ctx, &bertymessenger.SendContactRequest_Request{
BertyID: parsed.BertyID,
})
require.NoError(t, err)
time.Sleep(200 * time.Millisecond) // FIXME: replace with dynamic waiting
}

require.Len(t, bot.store.conversations, 1)

// get the conversation
var theConv *bertymessenger.Conversation
{
for _, conv := range bot.store.conversations {
theConv = conv
}
}

// send 'world!'
{
userMessage, err := proto.Marshal(&bertymessenger.AppMessage_UserMessage{Body: "world!"})
require.NoError(t, err)
req := &bertymessenger.Interact_Request{
Type: bertymessenger.AppMessage_TypeUserMessage,
Payload: userMessage,
ConversationPublicKey: theConv.PublicKey,
}
_, err = userClient.Interact(ctx, req)
require.NoError(t, err)
time.Sleep(200 * time.Millisecond) // FIXME: replace with dynamic waiting
}

// send /ping
{
userMessage, err := proto.Marshal(&bertymessenger.AppMessage_UserMessage{Body: "/ping"})
require.NoError(t, err)
req := &bertymessenger.Interact_Request{
Type: bertymessenger.AppMessage_TypeUserMessage,
Payload: userMessage,
ConversationPublicKey: theConv.PublicKey,
}
_, err = userClient.Interact(ctx, req)
require.NoError(t, err)
time.Sleep(200 * time.Millisecond) // FIXME: replace with dynamic waiting
}

require.True(t, commandCalled)
}
8 changes: 7 additions & 1 deletion go/pkg/bertybot/commands.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
package bertybot

type Command struct{}
type CommandFn func(ctx Context)

type command struct {
name string
description string
handler CommandFn
}
1 change: 1 addition & 0 deletions go/pkg/bertybot/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Context struct {
Device *bertymessenger.Device `json:"Device,omitempty"`
ConversationPK string `json:"ConversationPK,omitempty"`
UserMessage string `json:"UserMessage,omitempty"`
CommandArgs []string

// internal
initialized bool
Expand Down
18 changes: 18 additions & 0 deletions go/pkg/bertybot/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package bertybot
import (
"context"
"fmt"
"strings"

"go.uber.org/zap"

"berty.tech/berty/v2/go/pkg/bertymessenger"
)
Expand Down Expand Up @@ -35,6 +38,8 @@ const (
AcceptedContactHandler
UserMessageHandler
NewConversationHandler
CommandHandler
CommandNotFoundHandler
)

type Handler func(ctx Context)
Expand Down Expand Up @@ -85,6 +90,19 @@ func (b *Bot) handleEvent(ctx context.Context, event *bertymessenger.StreamEvent
case bertymessenger.AppMessage_TypeUserMessage:
receivedMessage := payload.(*bertymessenger.AppMessage_UserMessage)
context.UserMessage = receivedMessage.GetBody()
if len(b.commands) > 0 && len(context.UserMessage) > 1 && strings.HasPrefix(context.UserMessage, "/") {
if !context.IsMe && !context.IsReplay && !context.IsAck {
context.CommandArgs = strings.Split(context.UserMessage[1:], " ")
command, found := b.commands[context.CommandArgs[0]]
if found {
b.logger.Debug("found handler", zap.Strings("args", context.CommandArgs))
command.handler(*context)
b.callHandlers(context, CommandHandler)
} else {
b.callHandlers(context, CommandNotFoundHandler)
}
}
}
b.callHandlers(context, UserMessageHandler)
default:
return fmt.Errorf("unsupported interaction type: %q", context.Interaction.Type)
Expand Down
37 changes: 34 additions & 3 deletions go/pkg/bertybot/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package bertybot

import (
"fmt"
"sort"
"strings"

"go.uber.org/zap"
"google.golang.org/grpc"
Expand Down Expand Up @@ -73,7 +75,7 @@ func WithHandler(typ HandlerType, handler Handler) NewOption {
}
}

// WithRecipe
// WithRecipe configures the passed recipe.
func WithRecipe(recipe Recipe) NewOption {
return func(b *Bot) error {
for typ, handlers := range recipe {
Expand All @@ -85,18 +87,47 @@ func WithRecipe(recipe Recipe) NewOption {
}
}

// WithSkipAcknowledge
// WithSkipAcknowledge disables sending Acknowledge events.
func WithSkipAcknowledge() NewOption {
return func(b *Bot) error {
b.skipAcknowledge = true
return nil
}
}

// WithSkipMyself
// WithSkipMyself disables sending events sent by myself.
func WithSkipMyself() NewOption {
return func(b *Bot) error {
b.skipMyself = true
return nil
}
}

// WithCommand registers a new command that can be called with the '/' prefix.
// If name was already used, the preview command is replaced by the new one.
func WithCommand(name, description string, handler CommandFn) NewOption {
return func(b *Bot) error {
b.commands[name] = command{
name: name,
description: description,
handler: handler,
}
if _, found := b.commands["help"]; !found {
b.commands["help"] = command{
name: "help",
description: "show this help",
handler: func(ctx Context) {
lines := []string{}
for _, command := range b.commands {
lines = append(lines, fmt.Sprintf(" /%-20s %s", command.name, command.description))
}
sort.Strings(lines)
msg := "Available commands:\n" + strings.Join(lines, "\n")
_ = ctx.ReplyString(msg)
// FIXME: submit suggestions
},
}
}
return nil
}
}
5 changes: 0 additions & 5 deletions go/pkg/bertybot/recipes.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,3 @@ func AutoAcceptIncomingGroupInviteRecipe() Recipe {
func SendErrorToClientRecipe() Recipe {
panic("not implemented")
}

// CommandVersionRecipe registers a command that replies bot & protocol versions on '/version'.
func CommandVersionRecipe(botVersion string) Recipe {
panic("not implemented")
}
14 changes: 7 additions & 7 deletions go/pkg/bertymessenger/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ func TestBroken1To1AddContact(t *testing.T) {
defer cleanup()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clients, cleanup := testingInfra(ctx, t, 2, logger)
clients, cleanup := TestingInfra(ctx, t, 2, logger)
defer cleanup()

// Init accounts
Expand Down Expand Up @@ -360,7 +360,7 @@ func TestBroken1To1Exchange(t *testing.T) {
defer cleanup()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clients, cleanup := testingInfra(ctx, t, 2, logger)
clients, cleanup := TestingInfra(ctx, t, 2, logger)
defer cleanup()

// Init accounts
Expand Down Expand Up @@ -415,7 +415,7 @@ func TestBrokenPeersCreateJoinConversation(t *testing.T) {
logger, cleanup := testutil.Logger(t)
defer cleanup()
accountsAmount := 3
clients, cleanup := testingInfra(ctx, t, accountsAmount, logger)
clients, cleanup := TestingInfra(ctx, t, accountsAmount, logger)
defer cleanup()

// create nodes
Expand Down Expand Up @@ -499,7 +499,7 @@ func TestBroken3PeersExchange(t *testing.T) {
defer cancel()
logger, cleanup := testutil.Logger(t)
defer cleanup()
clients, cleanup := testingInfra(ctx, t, 3, logger)
clients, cleanup := TestingInfra(ctx, t, 3, logger)
defer cleanup()

// create nodes
Expand Down Expand Up @@ -558,7 +558,7 @@ func TestBrokenConversationInvitation(t *testing.T) {
defer cancel()
logger, cleanup := testutil.Logger(t)
defer cleanup()
clients, cleanup := testingInfra(ctx, t, 3, logger)
clients, cleanup := TestingInfra(ctx, t, 3, logger)
defer cleanup()

// create nodes
Expand Down Expand Up @@ -624,7 +624,7 @@ func TestBrokenConversationInvitationAndExchange(t *testing.T) {
defer cancel()
logger, cleanup := testutil.Logger(t)
defer cleanup()
clients, cleanup := testingInfra(ctx, t, 3, logger)
clients, cleanup := TestingInfra(ctx, t, 3, logger)
defer cleanup()

// create nodes
Expand Down Expand Up @@ -698,7 +698,7 @@ func TestBrokenConversationOpenClose(t *testing.T) {
defer cleanup()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
clients, cleanup := testingInfra(ctx, t, 2, logger)
clients, cleanup := TestingInfra(ctx, t, 2, logger)
defer cleanup()

// Init accounts
Expand Down

0 comments on commit e9831cc

Please sign in to comment.