Skip to content
A general-purpose bot library inspired by Hubot but written in Go.
Go Makefile
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci Make sure examples in README.md compile and are up to date Apr 21, 2019
_examples Cleanup all example dependencies Apr 21, 2019
joetest joetest: add more dedicated unit tests Apr 21, 2019
.codecov.yml ci: disable confusing patch diff coverage in PRs Mar 17, 2019
.golangci.yml Update golangci-lint and fix or mute new linter issues Oct 6, 2019
CHANGELOG.md Bump version Apr 21, 2019
CONTRIBUTING.md Add note about examples in CONTRIBUTING.md Apr 21, 2019
LICENSE Switch license to BSD-3-Clause Mar 14, 2019
README.md README.md: Group other modules and fix link to contributors Oct 8, 2019
adapter.go Add AuthorID and Data fields to ReceiveMessageEvent and Message type Mar 30, 2019
adapter_test.go Fix flaky TestCLIAdapter_Register test Apr 18, 2019
auth.go Refactor Memory interface and access to storage Apr 20, 2019
auth_test.go Refactor Memory interface and access to storage Apr 20, 2019
bot.go Fix small issue in documentation Apr 22, 2019
bot_test.go Fix writing to stdout in TestNewBot Apr 18, 2019
brain.go Refactor Memory interface and access to storage Apr 20, 2019
brain_test.go Update golangci-lint and fix or mute new linter issues Oct 6, 2019
error.go Add Grant(…) and CheckPermissions(…) functions Apr 14, 2019
error_test.go Fix issue found by linter Apr 18, 2019
events.go Refactor Memory interface and access to storage Apr 20, 2019
go.mod Make sure examples in README.md compile and are up to date Apr 21, 2019
go.sum bot: add some more unit tests Mar 8, 2019
message.go Add AuthorID and Data fields to ReceiveMessageEvent and Message type Mar 30, 2019
message_test.go joetest: move old TestBot type into joetest package Mar 17, 2019
options.go Add missing godoc Apr 20, 2019
options_test.go Refactor Memory interface and access to storage Apr 20, 2019
storage.go Add missing godoc Apr 20, 2019
storage_test.go Refactor Memory interface and access to storage Apr 20, 2019
user.go Split out modules Feb 24, 2019

README.md

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.

Getting Started

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

All significant (e.g. breaking) changes are documented in the CHANGELOG.md.

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 bot 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 (
	"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.Store.Set(key, value)
}

func (b *ExampleBot) WhatIs(msg joe.Message) error {
	key := msg.Matches[0]
	var value string
	ok, err := b.Store.Get(key, &value)
	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)
}

Granting and checking user permissions

Joe supports a simple way to manage user permissions. For instance you may want to define a message handler that will run an operation which only admins should be allowed to trigger.

To implement this, joe has a concept of permission scopes. A scope is a string which is granted to a specific user ID so you can later check if the author of the event you are handling (e.g. a message from Slack) has this scope or any scope that contains it.

Scopes are interpreted in a hierarchical way where scope A can contain scope B if A is a prefix to B. 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 the api.example.write scope.

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 want you could also allow even more general access to everything in the api via the api scope.

Scopes can be granted statically in code or dynamically in a handler like this:

package main

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

type ExampleBot struct {
	*joe.Bot
}

func main() {
	b := &ExampleBot{
		Bot: joe.New("HAL"),
	}

	// If you know the user ID in advance you may hard-code it at startup.
	b.Auth.Grant("api.example", "DAVE")

	// An example of a message handler that checks permissions.
	b.Respond("open the pod bay doors", b.OpenPodBayDoors)

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

func (b *ExampleBot) OpenPodBayDoors(msg joe.Message) error {
	err := b.Auth.CheckPermission("api.example.admin", msg.AuthorID)
	if err != nil {
		return msg.RespondE("I'm sorry Dave, I'm afraid I can't do that")
	}

	return msg.RespondE("OK")
}

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 (
	"context"
	"errors"

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

Chat Adapters

Memory Modules

Other Modules

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

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

After the v1.0 release we plan to 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
  • embedmd for a cool tool to embed source code in markdown files
You can’t perform that action at this time.