joe

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2019 License: BSD-3-Clause Imports: 19 Imported by: 30

README

Joe Bot

A general-purpose bot library inspired by Hubot but written in Go.


Joe is a library used to write chat bots in the Go programming language. It is very much inspired by the awesome Hubot framework developed by the folks at Github and brings its power to people who want to implement chat bots using Go.

THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.

Getting Started

Joe is packaged using the new Go modules. You can get joe via:

go get github.com/go-joe/joe

Minimal example

The simplest chat bot listens for messages on a chat Adapter and then executes a Handler function if it sees a message directed to the bot that matches a given pattern.

For example a bot that responds to a message "ping" with the answer "PONG" looks like this:

package main

import "github.com/go-joe/joe"

func main() {
	b := joe.New("example-bot")
	b.Respond("ping", Pong)

	err := b.Run()
	if err != nil {
		b.Logger.Fatal(err.Error())
	}
}

func Pong(msg joe.Message) error {
	msg.Respond("PONG")
	return nil
}

Useful example

Each bot consists of a chat Adapter (e.g. to integrate with Slack), a Memory implementation to remember key-value data (e.g. using Redis) and a Brain which routes new messages or custom events (e.g. receiving an HTTP call) to the corresponding registered handler functions.

By default joe.New(…) uses the CLI adapter which makes the bot read messages from stdin and respond on stdout. Additionally the brain will store key value data in-memory which means it will forget anything you told it when it is restarted. This default setup is useful for local development without any dependencies but you will quickly want to add other Modules to extend the bots capabilities.

For instance we can extend the previous example to connect the Bot with a Slack workspace and store key value data in Redis. To allow the message handlers to access the memory we define them as functions on a custom ExampleBottype which embeds the joe.Bot.

package main

import (
	"strings"
	"github.com/go-joe/joe"
	"github.com/go-joe/redis-memory"
	"github.com/go-joe/slack-adapter"
	"github.com/pkg/errors"
)

type ExampleBot struct {
	*joe.Bot
}

func main() {
	b := &ExampleBot{
		Bot: joe.New("example",
			redis.Memory("localhost:6379"),
			slack.Adapter("xoxb-1452345…"),
		),
	}

	b.Respond("remember (.+) is (.+)", b.Remember)
	b.Respond("what is (.+)", b.WhatIs)

	err := b.Run()
	if err != nil {
		b.Logger.Fatal(err.Error())
	}
}

func (b *ExampleBot) Remember(msg joe.Message) error {
	key, value := msg.Matches[0], msg.Matches[1]
	msg.Respond("OK, I'll remember %s is %s", key, value)
	return b.Brain.Set(key, value)
}

func (b *ExampleBot) WhatIs(msg joe.Message) error {
	key := msg.Matches[0]
	value, ok, err := b.Brain.Get(key)
	if err != nil {
		return errors.Wrapf(err, "failed to retrieve key %q from brain", key)
	}

	if ok {
		msg.Respond("%s is %s", key, value)
	} else {
		msg.Respond("I do not remember %q", key)
	}

	return nil
}

Handling custom events

The previous example should give you an idea already on how to write simple chat bots. It is missing one important part however: how can a bot trigger any interaction proactively, i.e. without a message from a user.

To solve this problem, joe's Brain implements an event handler that you can hook into. In fact the Bot.Respond(…) function that we used in the earlier examples is doing exactly that to listen for any joe.ReceiveMessageEvent that match the specified regular expression and then execute the handler function.

Implementing custom events is easy because you can emit any type as event and register handlers that match only this type. What this exactly means is best demonstrated with another example:

package main

import (
	"time"
	"github.com/go-joe/joe"
)

type ExampleBot struct {
	*joe.Bot
	Channel string // example for your custom bot configuration
}

type CustomEvent struct {
	Data string // just an example of attaching any data with a custom event
}

func main() {
	b := &ExampleBot{
		Bot:     joe.New("example"),
		Channel: "CDEADBEAF", // example reference to a slack channel
	}

	// Register our custom event handler. Joe inspects the function signature to
	// understand that this function should be invoked whenever a CustomEvent
	// is emitted.
	b.Brain.RegisterHandler(b.HandleCustomEvent)

	// For example purposes emit a CustomEvent in a second.
	time.AfterFunc(time.Second, func() {
		b.Brain.Emit(CustomEvent{Data: "Hello World!"})
	})

	err := b.Run()
	if err != nil {
		b.Logger.Fatal(err.Error())
	}
}

// HandleCustomEvent handles any CustomEvent that is emitted. Joe also supports
// event handlers that return an error or accept a context.Context as first argument.
func (b *ExampleBot) HandleCustomEvent(evt CustomEvent) {
	b.Say(b.Channel, "Received custom event: %v", evt.Data)
}

Integrating with other applications

You may want to integrate your bot with applications such as Github or Gitlab to trigger a handler or just send a message to Slack. Usually this is done by providing an HTTP callback to those applications so they can POST data when there is an event. We already saw in the previous section that is is very easy to implement custom events so we will use this feature to implement HTTP integrations as well. Since this is such a dominant use-case we already provide the github.com/go-joe/http-server module to make it easy for everybody to write their own custom integrations.

package main

import (
	"errors"
	"github.com/go-joe/http-server"
	"github.com/go-joe/joe"
)

type ExampleBot struct {
	*joe.Bot
}

func main() {
	b := &ExampleBot{Bot: joe.New("example",
		joehttp.Server(":8080"),
	)}

	b.Brain.RegisterHandler(b.HandleHTTP)

	err := b.Run()
	if err != nil {
		b.Logger.Fatal(err.Error())
	}
}

func (b *ExampleBot) HandleHTTP(context.Context, joehttp.RequestEvent) error {
	return errors.New("TODO: Add your custom logic here")
}

Available modules

Joe ships with no third-party modules such as Redis integration to avoid pulling in more dependencies than you actually require. There are however already some modules that you can use directly to extend the functionality of your bot without writing too much code yourself.

If you have written a module and want to share it, please add it to this list and open a pull request.

Built With

  • zap - Blazing fast, structured, leveled logging in Go
  • pkg/errors - Simple error handling primitives
  • testify - A simple unit test library

Contributing

Please read CONTRIBUTING.md for details on our code of conduct and on the process for submitting pull requests to this repository.

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

Authors

  • Friedrich Große - Initial work - fgrosse

See also the list of contributors who participated in this project.

License

This project is licensed under the BSD-3-Clause License - see the LICENSE file for details.

Acknowledgments

  • Hubot and its great community for the inspiration

Documentation

Overview

Package joe contains a general purpose bot library inspired by Hubot.

Index

Constants

View Source
const ErrNotAllowed = Error("not allowed")

ErrNotAllowed is returned if the user is not allowed access to a specific scope.

Variables

This section is empty.

Functions

This section is empty.

Types

type Adapter

type Adapter interface {
	RegisterAt(*Brain)
	Send(text, channel string) error
	Close() error
}

An Adapter connects the bot with the chat by enabling it to receive and send messages. Additionally advanced adapters can emit more events than just the ReceiveMessageEvent (e.g. the slack adapter also emits the UserTypingEvent). All adapter events must be setup in the RegisterAt function of the Adapter.

Joe provides a default CLIAdapter implementation which connects the bot with the local shell to receive messages from stdin and print messages to stdout.

type Auth added in v0.7.0

type Auth struct {
	// contains filtered or unexported fields
}

Auth implements logic to add user authorization checks to your bot.

func NewAuth added in v0.7.0

func NewAuth(logger *zap.Logger, memory Memory) *Auth

NewAuth creates a new Auth instance.

func (*Auth) CheckPermission added in v0.7.0

func (a *Auth) CheckPermission(scope, userID string) error

CheckPermission checks if a user has permissions to access a resource under a given scope. If the user is not permitted access this function returns ErrNotAllowed.

Scopes are interpreted in a hierarchical way where scope A can be contained in scope B if B is a prefix to A. For example, you can check if a user is allowed to read or write from the "Example" API by checking the "api.example.read" or "api.example.write" scope. When you grant the scope to a user you can now either decide only to grant the very specific "api.example.read" scope which means the user will not have write permissions or you can allow people write-only access via "api.example.write". Alternatively you can also grant any access to the Example API via "api.example" which includes both the read and write scope beneath it. If you choose to you could also allow even more general access to everything in the api via the "api" scope. The empty scope "" cannot be granted and will thus always return an error in the permission check.

func (*Auth) Grant added in v0.7.0

func (a *Auth) Grant(scope, userID string) error

Grant adds a new permission scope to the given user. When a scope was granted to a specific user it can be checked later via CheckPermission(…). The empty scope cannot be granted and trying to do so will result in an error. If you want to grant access to all scopes you should prefix them with a common scope such as "root." or "api.".

type Bot

type Bot struct {
	Name    string
	Adapter Adapter
	Brain   *Brain
	Auth    *Auth
	Logger  *zap.Logger
	// contains filtered or unexported fields
}

A Bot represents an event based chat bot. For the most simple usage you can use the Bot.Respond(…) function to make the bot execute a function when it receives a message that matches a given pattern.

More advanced usage includes persisting memory or emitting your own events using the Brain of the robot.

func New

func New(name string, modules ...Module) *Bot

New creates a new Bot and initializes it with the given Modules and Options. By default the Bot will use an in-memory in Brain and a CLI adapter that reads messages from stdin and writes to stdout.

The modules can be used to change the Memory or Adapter or register other new functionality. Additionally you can pass Options which allow setting some simple configuration such as the event handler timeouts or injecting a different context. All Options are available as functions in this package that start with "With…".

If there was an error initializing a Module it is stored and returned on the next call to Bot.Run(). Before you start the bot however you should register your custom event handlers.

Example:

b := joe.New("example",
    redis.Memory("localhost:6379"),
    slack.Adapter("xoxb-58942365423-…"),
    joehttp.Server(":8080"),
    joe.WithHandlerTimeout(time.Second),
)

b.Respond("ping", b.Pong)
b.Brain.RegisterHandler(b.Init)

err := b.Run()
…

func (*Bot) Respond

func (b *Bot) Respond(msg string, fun func(Message) error)

Respond registers an event handler that listens for the ReceiveMessageEvent and executes the given function only if the message text matches the given message. The message will be matched against the msg string as regular expression that must match the entire message in a case insensitive way.

You can use sub matches in the msg which will be passed to the function via Message.Matches.

If you need complete control over the regular expression, e.g. because you want the patter to match only a substring of the message but not all of it, you can use Bot.RespondRegex(…).

func (*Bot) RespondRegex

func (b *Bot) RespondRegex(expr string, fun func(Message) error)

RespondRegex is like Bot.Respond(…) but gives a little more control over the regular expression. However, also with this function messages are matched in a case insensitive way.

func (*Bot) Run

func (b *Bot) Run() error

Run starts the bot and runs its event handler loop until the bots context is canceled (by default via SIGINT, SIGQUIT or SIGTERM). If there was an an error when setting up the Bot via New() or when registering the event handlers it will be returned immediately.

func (*Bot) Say

func (b *Bot) Say(channel, msg string, args ...interface{})

Say is a helper function to makes the Bot output the message via its Adapter (e.g. to the CLI or to Slack). If there is at least one vararg the msg and args are formatted using fmt.Sprintf.

type Brain

type Brain struct {
	// contains filtered or unexported fields
}

The Brain contains the core logic of a Bot by implementing an event handling system that dispatches events to all registered event handlers. Additionally the Brain is directly connected to the Memory of the bot to manage concurrent access as well as to emit the BrainMemoryEvent if memory is created, edited or deleted on the brain.

func NewBrain

func NewBrain(logger *zap.Logger) *Brain

NewBrain creates a new robot Brain. By default the Brain will use a Memory implementation that stores all keys and values directly in memory. You can change the memory implementation afterwards by simply assigning to Brain.Memory. If the passed logger is nil it will fallback to the zap.NewNop() logger.

func (*Brain) Close

func (b *Brain) Close() error

Close shuts down the Memory of the brain.

func (*Brain) Delete

func (b *Brain) Delete(key string) (bool, error)

Delete is a wrapper around the Brains Memory.Delete function to allow concurrent access and emit the corresponding BrainMemoryEvent.

func (*Brain) Emit

func (b *Brain) Emit(event interface{}, callbacks ...func(Event))

Emit sends the first argument as event to the brain from where it is dispatched to all registered handlers. The events are dispatched asynchronously but in the same order in which they are send to this function. Emit does not block until the event is delivered to the registered event handlers. If you want to wait until all handlers have processed the event you can pass one or more callback functions that will be executed when all handlers finished execution of this event.

func (*Brain) Get

func (b *Brain) Get(key string) (string, bool, error)

Get is a wrapper around the Brains Memory.Get function to allow concurrent access and emit the corresponding BrainMemoryEvent.

func (*Brain) HandleEvents

func (b *Brain) HandleEvents()

HandleEvents starts the event handling loop of the Brain. This function blocks until Brain.Shutdown() is called and returned.

func (*Brain) Memories

func (b *Brain) Memories() (map[string]string, error)

Memories is a wrapper around the Brains Memory.Memories function to allow concurrent access.

func (*Brain) RegisterHandler

func (b *Brain) RegisterHandler(fun interface{})

RegisterHandler registers a function to be executed when a specific event is fired. The function signature must comply with the following rules or the bot that uses this Brain will return an error on its next Bot.Run() call:

Allowed function signatures:

// MyCustomEventStruct must be any struct but not a pointer to a struct.
func(MyCustomEventStruct)

// You can optionally accept a context as the first argument. The context
// is used to signal handler timeouts or when the bot is shutting down.
func(context.Context, MyCustomEventStruct)

// You can optionally return a single error value. Returning any other type
// or returning more than one value will lead to an error. If the handler
// returns an error it will be logged.
func(MyCustomEventStruct) error

// Event handlers can also accept an interface in which case they will be
// be called for all events which implement the interface. Consequently,
// you can register a function which accepts the empty interface which will
// will receive all emitted events. Such event handlers can optionally also
// accept a context and/or return an error like other handlers.
func(context.Context, interface{}) error

The event that will be dispatched to the passed handler function corresponds directly to the accepted function argument. For instance if you want to emit and receive a custom event you can implement it like this:

type CustomEvent struct {}

b := NewBrain(nil)
b.RegisterHandler(func(evt CustomEvent) {
    // TODO
})

func (*Brain) Set

func (b *Brain) Set(key, value string) error

Set is a wrapper around the Brains Memory.Set function to allow concurrent access and emit the corresponding BrainMemoryEvent.

func (*Brain) SetMemory added in v0.7.0

func (b *Brain) SetMemory(m Memory)

SetMemory assigns a different Memory to the Brain.

func (*Brain) Shutdown added in v0.2.0

func (b *Brain) Shutdown(ctx context.Context)

Shutdown stops the event handler loop of the Brain and waits until all pending events have been processed. After the brain is shutdown, it will no longer accept new events. The passed context can be used to stop waiting for any pending events or handlers and instead exit immediately (e.g. after a timeout or a second SIGTERM).

type BrainMemoryEvent

type BrainMemoryEvent struct {
	Key       string
	Value     string
	Operation string // "set", "get" or "del"
}

The BrainMemoryEvent is emitted whenever the Brain reads, writes or deletes a single key-value pair from the brain.

type CLIAdapter

type CLIAdapter struct {
	Prefix string
	Input  io.ReadCloser
	Output io.Writer
	Logger *zap.Logger
	Author string // used to set the author of the messages, defaults to os.Getenv("USER)
	// contains filtered or unexported fields
}

The CLIAdapter is the default Adapter implementation that the bot uses if no other adapter was configured. It emits a ReceiveMessageEvent for each line it receives from stdin and prints all sent messages to stdout.

The CLIAdapter does not set the Message.Data field.

func NewCLIAdapter

func NewCLIAdapter(name string, logger *zap.Logger) *CLIAdapter

NewCLIAdapter creates a new CLIAdapter. The caller must call Close to make the CLIAdapter stop reading messages and emitting events.

func (*CLIAdapter) Close

func (a *CLIAdapter) Close() error

Close makes the CLIAdapter stop emitting any new events or printing any output. Calling this function more than once will result in an error.

func (*CLIAdapter) RegisterAt added in v0.2.0

func (a *CLIAdapter) RegisterAt(brain *Brain)

RegisterAt starts the CLIAdapter by reading messages from stdin and emitting a ReceiveMessageEvent for each of them. Additionally the adapter hooks into the InitEvent to print a nice prefix to stdout to show to the user it is ready to accept input.

func (*CLIAdapter) Send

func (a *CLIAdapter) Send(text, channel string) error

Send implements the Adapter interface by sending the given text to stdout. The channel argument is required by the Adapter interface but is otherwise ignored.

type Config

type Config struct {
	Context        context.Context
	Name           string
	HandlerTimeout time.Duration
	// contains filtered or unexported fields
}

Config is the configuration of a Bot that can be used or changed during setup in a Module. Some configuration settings such as the Logger are read only can only be accessed via the corresponding getter function of the Config.

func NewConfig added in v0.6.0

func NewConfig(logger *zap.Logger, brain *Brain, adapter Adapter) Config

NewConfig creates a new Config. This function is mainly useful for unit testing but is normally not required to create or use a bot.

func (*Config) EventEmitter

func (c *Config) EventEmitter() EventEmitter

EventEmitter returns the EventEmitter that can be used to send events to the Bot and other modules.

func (*Config) Logger

func (c *Config) Logger(name string) *zap.Logger

Logger returns a new named logger.

func (*Config) RegisterHandler

func (c *Config) RegisterHandler(fun interface{})

RegisterHandler can be used to register an event handler in a Module.

func (*Config) SetAdapter

func (c *Config) SetAdapter(a Adapter)

SetAdapter can be used to change the Adapter implementation of the Bot.

func (*Config) SetMemory

func (c *Config) SetMemory(mem Memory)

SetMemory can be used to change the Memory implementation of the bots Brain.

type Error added in v0.7.0

type Error string

Error is the error type used by Joe. This allows joe errors to be defined as constants following https://dave.cheney.net/2016/04/07/constant-errors.

func (Error) Error added in v0.7.0

func (err Error) Error() string

Error implements the "error" interface of the standard library.

type Event

type Event struct {
	Data      interface{}
	Callbacks []func(Event)
}

An Event represents a concrete event type and optional callbacks that are triggered when the event was processed by all registered handlers.

type EventEmitter

type EventEmitter interface {
	Emit(event interface{}, callbacks ...func(Event))
}

The EventEmitter can be used by a Module by calling Config.EventEmitter(). Events are emitted asynchronously so every call to Emit is non-blocking.

type InitEvent

type InitEvent struct{}

The InitEvent is the first event that is handled by the Brain after the Bot is started via Bot.Run().

type Memory

type Memory interface {
	Set(key, value string) error
	Get(key string) (string, bool, error)
	Delete(key string) (bool, error)
	Memories() (map[string]string, error)
	Close() error
}

The Memory interface allows the robot Brain to persist data as key-value pairs. The default implementation of the Memory is to store all keys and values in a map (i.e. in-memory). Other implementations typically offer actual long term persistence into a file or to redis.

type Message

type Message struct {
	Context  context.Context
	Text     string
	AuthorID string
	Channel  string
	Matches  []string    // contains all sub matches of the regular expression that matched the Text
	Data     interface{} // corresponds to the ReceiveMessageEvent.Data field
	// contains filtered or unexported fields
}

A Message is automatically created from a ReceiveMessageEvent and then passed to the RespondFunc that was registered via Bot.Respond(…) or Bot.RespondRegex(…) when the message matches the regular expression of the handler.

func (*Message) Respond

func (msg *Message) Respond(text string, args ...interface{})

Respond is a helper function to directly send a response back to the channel the message originated from. This function ignores any error when sending the response. If you want to handle the error use Message.RespondE instead.

func (*Message) RespondE

func (msg *Message) RespondE(text string, args ...interface{}) error

RespondE is a helper function to directly send a response back to the channel the message originated from. If there was an error it will be returned from this function.

type Module

type Module interface {
	Apply(*Config) error
}

A Module is an optional Bot extension that can add new capabilities such as a different Brain.Memory implementation or a different Adapter.

func WithContext

func WithContext(ctx context.Context) Module

WithContext is an option to replace the default context of a bot.

func WithHandlerTimeout

func WithHandlerTimeout(timeout time.Duration) Module

WithHandlerTimeout is an option to set a timeout on event handlers functions. By default no timeout is enforced.

func WithLogger added in v0.3.0

func WithLogger(logger *zap.Logger) Module

WithLogger is an option to replace the default logger of a bot.

type ModuleFunc added in v0.4.0

type ModuleFunc func(*Config) error

ModuleFunc is a function implementation of a Module.

func (ModuleFunc) Apply added in v0.4.0

func (f ModuleFunc) Apply(conf *Config) error

Apply implements the Module interface.

type ReceiveMessageEvent

type ReceiveMessageEvent struct {
	Text     string // The message text.
	AuthorID string // A string identifying the author of the message on the adapter.
	Channel  string // The channel over which the message was received.

	// A message may optionally also contain additional information that was
	// received by the Adapter (e.g. with the slack adapter this may be the
	// *slack.MessageEvent. Each Adapter implementation should document if and
	// what information is available here, if any at all.
	Data interface{}
}

The ReceiveMessageEvent is typically emitted by an Adapter when the Bot sees a new message from the chat.

type ShutdownEvent

type ShutdownEvent struct{}

The ShutdownEvent is the last event that is handled by the Brain before it stops handling any events after the bot context is done.

type User

type User struct {
	ID       string
	Name     string
	RealName string
}

User contains all the information about a user.

type UserTypingEvent

type UserTypingEvent struct {
	User    User
	Channel string
}

The UserTypingEvent is emitted by the Adapter and indicates that the Bot sees that a user is typing. This event may not be emitted on all Adapter implementations but only when it is actually supported (e.g. on slack).

Directories

Path Synopsis
Package joetest implements helpers to implement unit tests for bots.
Package joetest implements helpers to implement unit tests for bots.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL