-
Notifications
You must be signed in to change notification settings - Fork 376
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
- Loading branch information
Showing
10 changed files
with
743 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"math/rand" | ||
"os" | ||
"os/user" | ||
|
||
qrterminal "github.com/mdp/qrterminal/v3" | ||
"go.uber.org/zap" | ||
"moul.io/srand" | ||
"moul.io/u" | ||
"moul.io/zapconfig" | ||
|
||
"berty.tech/berty/v2/go/pkg/bertybot" | ||
) | ||
|
||
var ( | ||
nodeAddr = flag.String("addr", "127.0.0.1:9091", "remote 'berty daemon' address") | ||
displayName = flag.String("display-name", safeDefaultDisplayName(), "bot's display name") | ||
debug = flag.Bool("debug", false, "debug mode") | ||
skipReplay = flag.Bool("skip-replay", true, "skip replay") | ||
replyDelay = flag.Duration("reply-delay", 0, "reply delay") | ||
) | ||
|
||
func main() { | ||
flag.Parse() | ||
rand.Seed(srand.MustSecure()) | ||
if err := Main(); err != nil { | ||
fmt.Fprintf(os.Stderr, "error: %+v\n", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func Main() error { | ||
rootLogger := zapconfig.Configurator{}.MustBuildLogger() | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
logger := rootLogger.Named("testbot") | ||
|
||
// init bot | ||
opts := []bertybot.NewOption{} | ||
opts = append(opts, | ||
bertybot.WithLogger(logger.Named("botlib")), // configure a logger | ||
bertybot.WithDisplayName(*displayName), // bot name | ||
bertybot.WithInsecureMessengerGRPCAddr(*nodeAddr), // connect to running berty messenger daemon | ||
bertybot.WithSkipAcknowledge(), // skip acknowledge events | ||
bertybot.WithSkipMyself(), // skip my own interactions | ||
bertybot.WithRecipe(bertybot.DelayResponseRecipe(*replyDelay)), // add a delay before sending replies | ||
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 | ||
) | ||
if *skipReplay { | ||
opts = append(opts, bertybot.WithSkipReplay()) // skip old events, only consume fresh ones | ||
} | ||
if *debug { | ||
opts = append(opts, bertybot.WithRecipe(bertybot.DebugEventRecipe(rootLogger.Named("debug")))) // debug events | ||
} | ||
bot, err := bertybot.New(opts...) | ||
if err != nil { | ||
return fmt.Errorf("bot initialization failed: %w", err) | ||
} | ||
// display link and qr code | ||
logger.Info("retrieve instance Berty ID", | ||
zap.String("pk", bot.PublicKey()), | ||
zap.String("link", bot.BertyIDURL()), | ||
) | ||
qrterminal.GenerateHalfBlock(bot.BertyIDURL(), qrterminal.L, os.Stdout) | ||
|
||
// signal handling | ||
go func() { | ||
u.WaitForCtrlC() | ||
cancel() | ||
}() | ||
|
||
// start bot | ||
return bot.Start(ctx) | ||
} | ||
|
||
func safeDefaultDisplayName() string { | ||
var name string | ||
current, err := user.Current() | ||
if err == nil { | ||
name = current.Username | ||
} | ||
if name == "" { | ||
name = os.Getenv("USER") | ||
} | ||
if name == "" { | ||
name = "anon" | ||
} | ||
return fmt.Sprintf("%s (testbot)", name) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package bertybot | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
"go.uber.org/zap" | ||
"moul.io/u" | ||
|
||
"berty.tech/berty/v2/go/pkg/bertymessenger" | ||
) | ||
|
||
type Bot struct { | ||
client bertymessenger.MessengerServiceClient | ||
logger *zap.Logger | ||
displayName string | ||
bertyID *bertymessenger.InstanceShareableBertyID_Reply | ||
skipReplay bool | ||
skipAcknowledge bool | ||
skipMyself bool | ||
handlers map[HandlerType][]Handler | ||
isReplaying bool | ||
handledEvents uint | ||
store struct { | ||
conversations map[string]*bertymessenger.Conversation | ||
mutex sync.Mutex | ||
} | ||
} | ||
|
||
// New initializes a new Bot. | ||
// The order of the passed options may have an impact. | ||
func New(opts ...NewOption) (*Bot, error) { | ||
b := Bot{ | ||
logger: zap.NewNop(), | ||
handlers: make(map[HandlerType][]Handler), | ||
} | ||
b.store.conversations = make(map[string]*bertymessenger.Conversation) | ||
|
||
// configure bot with options | ||
for _, opt := range opts { | ||
if err := opt(&b); err != nil { | ||
return nil, fmt.Errorf("bot: opt failed: %w", err) | ||
} | ||
} | ||
|
||
// check minimal requirements | ||
if b.client == nil { | ||
return nil, fmt.Errorf("bot: missing messenger client") | ||
} | ||
|
||
// apply defaults | ||
if b.displayName == "" { | ||
b.displayName = "My Berty Bot" | ||
} | ||
|
||
// retrieve Berty ID to check if everything is well configured, and cache it for easy access | ||
{ | ||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
defer cancel() | ||
req := &bertymessenger.InstanceShareableBertyID_Request{ | ||
DisplayName: b.displayName, | ||
} | ||
ret, err := b.client.InstanceShareableBertyID(ctx, req) | ||
if err != nil { | ||
return nil, fmt.Errorf("bot: cannot retrieve berty ID: %w", err) | ||
} | ||
b.bertyID = ret | ||
} | ||
|
||
return &b, nil | ||
} | ||
|
||
// BertyIDURL returns the shareable Berty ID in the form of `https://berty.tech/id#xxx`. | ||
func (b *Bot) BertyIDURL() string { | ||
return b.bertyID.HTMLURL | ||
} | ||
|
||
// PublicKey returns the public key of the messenger node. | ||
func (b *Bot) PublicKey() string { | ||
return u.B64Encode(b.bertyID.BertyID.AccountPK) | ||
} | ||
|
||
// Start starts the main event loop and can be stopped by canceling the passed context. | ||
func (b *Bot) Start(ctx context.Context) error { | ||
b.logger.Info("connecting to the event stream") | ||
s, err := b.client.EventStream(ctx, &bertymessenger.EventStream_Request{}) | ||
if err != nil { | ||
return fmt.Errorf("failed to listen to EventStream: %w", err) | ||
} | ||
|
||
b.isReplaying = true | ||
for { | ||
gme, err := s.Recv() | ||
if err != nil { | ||
return fmt.Errorf("stream error: %w", err) | ||
} | ||
|
||
if b.isReplaying { | ||
if gme.Event.Type == bertymessenger.StreamEvent_TypeListEnd { | ||
b.logger.Info("finished replaying logs from the previous sessions", zap.Uint("count", b.handledEvents)) | ||
b.isReplaying = false | ||
} | ||
b.handledEvents++ | ||
|
||
if b.skipReplay { | ||
continue | ||
} | ||
} | ||
|
||
if err := b.handleEvent(ctx, gme.Event); err != nil { | ||
b.logger.Error("bot.handleEvent failed", zap.Error(err)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package bertybot | ||
|
||
type Command struct{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package bertybot | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/gogo/protobuf/proto" | ||
"go.uber.org/zap" | ||
|
||
"berty.tech/berty/v2/go/pkg/bertymessenger" | ||
) | ||
|
||
// Context is the main argument passed to handlers. | ||
type Context struct { | ||
// common | ||
HandlerType HandlerType | ||
EventPayload proto.Message `json:"-"` // content of the payload is already available in the parsed payloads | ||
EventType bertymessenger.StreamEvent_Type | ||
Context context.Context | ||
Client bertymessenger.MessengerServiceClient | ||
Logger *zap.Logger | ||
IsReplay bool // whether the event is a replayed or a fresh event | ||
IsMe bool // whether the bot is the author | ||
IsAck bool // whether the event is an ack | ||
|
||
// parsed payloads, depending on the context | ||
Contact *bertymessenger.Contact `json:"Contact,omitempty"` | ||
Conversation *bertymessenger.Conversation `json:"Conversation,omitempty"` | ||
Interaction *bertymessenger.Interaction `json:"Interaction,omitempty"` | ||
Member *bertymessenger.Member `json:"Member,omitempty"` | ||
Account *bertymessenger.Account `json:"Account,omitempty"` | ||
Device *bertymessenger.Device `json:"Device,omitempty"` | ||
ConversationPK string `json:"ConversationPK,omitempty"` | ||
UserMessage string `json:"UserMessage,omitempty"` | ||
|
||
// internal | ||
initialized bool | ||
} | ||
|
||
// ReplyString sends a text message on the conversation related to the context. | ||
// The conversation can be 1-1 or multi-member. | ||
func (ctx *Context) ReplyString(text string) error { | ||
if ctx.ConversationPK == "" { | ||
return fmt.Errorf("unknown conversation PK, cannot reply") | ||
} | ||
// FIXME: support group conversation | ||
userMessage, err := proto.Marshal(&bertymessenger.AppMessage_UserMessage{Body: text}) | ||
if err != nil { | ||
return fmt.Errorf("marshal user message failed: %w", err) | ||
} | ||
_, err = ctx.Client.Interact(ctx.Context, &bertymessenger.Interact_Request{ | ||
Type: bertymessenger.AppMessage_TypeUserMessage, | ||
Payload: userMessage, | ||
ConversationPublicKey: ctx.ConversationPK, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("interact failed: %w", err) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package bertybot_test | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"time" | ||
|
||
qrterminal "github.com/mdp/qrterminal/v3" | ||
"go.uber.org/zap" | ||
"moul.io/u" | ||
|
||
"berty.tech/berty/v2/go/pkg/bertybot" | ||
) | ||
|
||
func Example() { | ||
logger, _ := zap.NewDevelopment() | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
// init bot | ||
bot, _ := bertybot.New( | ||
bertybot.WithLogger(logger.Named("botlib")), // configure a logger | ||
bertybot.WithDisplayName("example bot"), // bot name | ||
bertybot.WithInsecureMessengerGRPCAddr("127.0.0.1:9091"), // connect to running berty messenger daemon | ||
bertybot.WithSkipReplay(), // skip old events, only consume fresh ones | ||
bertybot.WithSkipAcknowledge(), // skip acknowledge events | ||
bertybot.WithSkipMyself(), // skip my own interactions | ||
bertybot.WithRecipe(bertybot.DebugEventRecipe(logger.Named("debug"))), // debug events | ||
bertybot.WithRecipe(bertybot.DelayResponseRecipe(time.Second)), // add a delay before sending replies | ||
bertybot.WithRecipe(bertybot.AutoAcceptIncomingContactRequestRecipe()), // accept incoming contact requests | ||
bertybot.WithRecipe(bertybot.WelcomeMessageRecipe("welcome to example bot")), // send welcome message to new contacts and new conversations | ||
bertybot.WithRecipe(bertybot.EchoRecipe("you said: ")), // reply to messages with the same message | ||
bertybot.WithHandler(bertybot.UserMessageHandler, func(ctx bertybot.Context) { // custom handler | ||
ctx.ReplyString("hello world!") | ||
}), | ||
) | ||
|
||
// display link and qr code | ||
logger.Info("retrieve instance Berty ID", zap.String("pk", bot.PublicKey()), zap.String("link", bot.BertyIDURL())) | ||
qrterminal.GenerateHalfBlock(bot.BertyIDURL(), qrterminal.L, os.Stdout) | ||
|
||
// signal handling | ||
go func() { | ||
u.WaitForCtrlC() | ||
cancel() | ||
}() | ||
|
||
// start bot | ||
bot.Start(ctx) | ||
} |
Oops, something went wrong.