Adapters let you interact with the outside world by receiving and sending messages. Joe currently has the following seven Adapter implementations:
If you want to integrate with a chat service that is not listed above, you can write your own Adapter implementation.
Firstly, your adapter should be available as joe.Module
so it can
easily be integrated into the bot via the joe.New(…)
function.
The Module
interface looks like this:
// A Module is an optional Bot extension that can add new capabilities such as
// a different Memory implementation or Adapter.
type Module interface {
Apply(*Config) error
}
To easily implement a Module without having to declare an Apply
function on
your chat adapter type, you can use the joe.ModuleFunc
type. For instance the
Slack adapter uses the following, to implement it’s Adapter(…)
function:
// Adapter returns a new Slack adapter as joe.Module.
//
// Apart from the typical joe.ReceiveMessageEvent event, this adapter also emits
// the joe.UserTypingEvent. The ReceiveMessageEvent.Data field is always a
// pointer to the corresponding github.com/nlopes/slack.MessageEvent instance.
func Adapter(token string, opts ...Option) joe.Module {
return joe.ModuleFunc(func(joeConf *joe.Config) error {
conf, err := newConf(token, joeConf, opts)
if err != nil {
return err
}
a, err := NewAdapter(joeConf.Context, conf)
if err != nil {
return err
}
joeConf.SetAdapter(a)
return nil
})
}
The passed *joe.Config
parameter can be used to lookup general options such as
the context.Context
used by the bot. Additionally you can create a named
logger via the Config.Logger(…)
function and you can register extra handlers
or emit events via the Config.EventEmitter()
function.
Most importantly for an Adapter implementation however is, that it finally needs
to register itself via the Config.SetAdapter(…)
function.
By defining an Adapter(…)
function in your package, it is now possible to use
your adapter as Module passed to joe.New(…)
. Additionally your NewAdapter(…)
function is useful to directly create a new adapter instance which can be used
during unit tests. Last but not least, the options pattern has proven useful in
this kind of setup and is considered good practice when writing modules in general.
// 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 Adapter interface {
RegisterAt(*Brain)
Send(text, channel string) error
Close() error
}
The most straight forwards function to implement should be the Send(…)
and
Close(…)
functions. The Send
function should output the given text to the
specified channel as the Bot. The initial connection and authentication to send
these messages should have been setup earlier by your Adapter
function as
shown above. When the bot shuts down, it will call the Close()
function of
your adapter so you can terminate your connection and release all resources you
have opened.
In order to also receive messages and pass them to Joe’s event handler you
need to implement a RegisterAt(*joe.Brain)
function. This function gets called
during the setup of the bot and allows the adapter to directly access to the Brain.
This function must not block and thus will typically spawn a new goroutine which
should be stopped when the Close()
function of your adapter implementation is
called.
In this goroutine you should listen for new messages from your chat application
(e.g. via a callback or polling it). When a new message is received, you need to
emit it as joe.ReceiveMessageEvent
to the brain.
E.g. for the Slack adapter, this looks like this:
func (a *BotAdapter) handleMessageEvent(ev *slack.MessageEvent, brain *joe.Brain) {
// Check if the message comes from ourselves.
if ev.User == a.userID {
// Message is from us, ignore it!
return
}
// Check if we have a direct message, or standard channel post.
selfLink := a.userLink(a.userID)
direct := strings.HasPrefix(ev.Msg.Channel, "D")
if !direct && !strings.Contains(ev.Msg.Text, selfLink) {
// Message is not meant for us!
return
}
text := strings.TrimSpace(strings.TrimPrefix(ev.Text, selfLink))
brain.Emit(joe.ReceiveMessageEvent{
Text: text,
Channel: ev.Channel,
ID: ev.Timestamp, // slack uses the message timestamps as identifiers within the channel
AuthorID: ev.User,
Data: ev,
})
}
In the snippet above you can see some of the common pitfalls:
joe.ReceiveMessageEvent
Currently there is only a single optional interface that can be implemented by an
Adapter, which is the joe.ReactionAwareAdapter
:
// ReactionAwareAdapter is an optional interface that Adapters can implement if
// they support reacting to messages with emojis.
type ReactionAwareAdapter interface {
React(reactions.Reaction, Message) error
}
This interface is meant for chat adapters that have emoji support to attach reactions to previously received messages (e.g. 👍 or 🤖).
Generally writing an adapter should not be very hard but it’s a good idea to look at the other adapter implementations to get a better understanding of how to implement your own. If you have questions or need help, simply open an issue at the Joe repository at GitHub.
Happy adaptering 🤖🎉