Skip to content

Default Bot and Adapter

Oklahomer edited this page May 6, 2018 · 7 revisions

Overview

sarah.Bot implementation is responsible for receiving input from chat service, respond with propper command execution, sending output back to chat service. While sarah.Runner is responsible for coordinating all components, this works as a bridge between go-sarah's system and connected chat service. The interface is defined as below:

// Bot provides interface for each bot implementation.
// Instance of concrete type can be fed to sarah.Runner to have its lifecycle under control.
// Multiple Bot implementation may be registered to single Runner.
type Bot interface {
	// BotType represents what this Bot implements. e.g. slack, gitter, cli, etc...
	// This can be used as a unique ID to distinguish one from another.
	BotType() BotType

	// Respond receives user input, look for corresponding command, execute it, and send result back to user if possible.
	Respond(context.Context, Input) error

	// SendMessage sends message to destination depending on the Bot implementation.
	// This is mainly used to send scheduled task's result.
	// Be advised: this method may be called simultaneously from multiple workers.
	SendMessage(context.Context, Output)

	// AppendCommand appends given Command implementation to Bot internal stash.
	// Stashed commands are checked against user input in Bot.Respond, and if Command.Match returns true, the
	// Command is considered as "corresponds" to the input, hence its Command.Execute is called and the result is
	// sent back to user.
	AppendCommand(Command)

	// Run is called on Runner.Run to let this Bot interact with corresponding service provider.
	// For example, this is where Bot or Bot's corresponding Adapter initiates connection with service provider.
	// This may run in a blocking manner til given context is canceled since a new goroutine is allocated for this task.
	// When the service provider sends message to us, convert that message payload to Input and send to Input channel.
	// Runner will receive the Input instance and proceed to find and execute corresponding command.
	Run(context.Context, func(Input) error, func(error))
}

Developers are free to implement their own sarah.Bot to have very own bot experiences. In most cases, however, some tasks such as sarah.Command stashing and its execution on bot.Respond is common. To ease sarah.Bot implementation, this project extracts those common tasks and implements with sarah.defaultBot. Other chat-service specific tasks are to be implemented by sarah.Adapter. The struct sarah.defaultBot is package private and its initialization can only be done by calling sarah.NewBot, which takes sarah.Aapter implementation and series of sarah.DefaultBotOptions as arguments. Returned value is a sarah.Bot interface so this can be fed to sarah.Runner to work as a sarah.Bot.

func NewBot(adapter sarah.Adapter, options ...sarah.DefaultBotOption) (sarah.Bot, error) {
        ...
}

Default Bot

As previously introduced, sarah.defaultBot implements some common tasks that most bot implementations must deal with. This sarah.defaultBot wraps given sarah.Adapter implementation, handle common tasks internally, and delegate chat-service specific tasks to sarah.Adapter.

Stashing group of sarah.Commands and executing corresponding one against user input are example of such common tasks. defaultBot.AppendCommand and defaultBot.Respond take care of those tasks. Since the idea of user's conversational context and its implementation is go-sarah's signature feature, sarah.defaultBot also takes care of this when sarah.UserContextStorage is passed on its initialization. When defaultBot.Respond receives sarah.Input, this searches for stored user's conversational context with Input.SenderKey. If sarah.ContextualFunc is returned from sarah.UserContextStorage, default bot considers the user is in the middle of conversation, execute sarah.ContextualFunc, and send returned sarah.Output back to user. When sarah.UserContextFunc is not returned, user is not in the conversational context so first matching sarah.Command is executed. Stashed sarah.Commands' Command.Match is called in the appended order. To prioritize commands, call Bot.AppendCommand in the order of priority. When commands are created from sarah.CommandProps on the fly, pass sarah.CommandPropss to sarah.Runner in order of priority.

Other than those common tasks, sarah.defaultBot simply delegates chat-service specific tasks to wrapped sarah.Adapter; defaultBot.SendMessage proxies arguments to Adapter.SendMessage, defaultBot.Run proxies arguments to Adapter.Run. Remember defaultBot.Respond also calls Adapter.SendMessage internally to send output back to user.

Adapter

Since all common tasks are handled by sarah.defaultBot, sarah.Adapter's interface is rather minimal:

// Adapter defines interface that each bot adapter implementation has to satisfy.
// Instance of its concrete struct and series of sarah.DefaultBotOptions can be fed to defaultBot via sarah.NewBot() to have sarah.Bot.
// Returned bot instance can be fed to Runner to have its life cycle managed.
type Adapter interface {
	// BotType represents what this Bot implements. e.g. slack, gitter, cli, etc...
	// This can be used as a unique ID to distinguish one from another.
	BotType() BotType

	// Run is called on Runner.Run by wrapping bot instance.
	// On this call, start interacting with corresponding service provider.
	// This may run in a blocking manner til given context is canceled since a new goroutine is allocated for this task.
	// When the service provider sends message to us, convert that message payload to Input and send to Input channel.
	// Runner will receive the Input instance and proceed to find and execute corresponding command.
	Run(context.Context, func(Input) error, func(error))

	// SendMessage sends message to corresponding service provider.
	// This can be called by scheduled task or in response to input from service provider.
	// Be advised: this method may be called simultaneously from multiple workers.
	SendMessage(context.Context, Output)
}

sarah.Adapter's implementation is responsible for receiving input from and sending output to corresponding chat-service. If persistent connection such as WebSocket is required, this is adapter's responsibility to sustain stable connection until bot context is canceled. When adapter can not proceed its operation, adapter may send error state to sarah.Runner via func(error) that is passed on Adapter.Run. Runner then receives the critical state, sends alerts via registered sarah.Alerters and cancels bot context. Note that all components' life cycles are managed by sarah.Runner.

Adapters for LINE, Slack, XMPP and Gitter are provided as reference implementations.

Clone this wiki locally