README

GoDoc Go Report Card Build Status Coverage Status Maintainability

Sarah is a general purpose bot framework named after author's firstborn daughter.

While the first goal is to prep author to write Go-ish code, the second goal is to provide simple yet highly customizable bot framework.

Supported Chat Services/Protocols

Although a developer may implement sarah.Adapter to integrate with a desired chat service, some adapters are provided as reference implementations:

At a Glance

hello world

Above is a general use of go-sarah. Registered commands are checked against user input and matching one is executed; when a user inputs ".hello," hello command is executed and a message "Hello, 世界" is returned.

Below image depicts how a command with user's conversational context works. The idea and implementation of "user's conversational context" is go-sarah's signature feature that makes bot command "state-aware."

Above example is a good way to let user input series of arguments in a conversational manner. Below is another example that use stateful command to entertain user.

Following is the minimal code that implements such general command and stateful command introduced above. In this example, two ways to implement sarah.Command are shown. One simply implements sarah.Command interface; while another uses sarah.CommandPropsBuilder for lazy construction. Detailed benefits of using sarah.CommandPropsBuilder and sarah.CommandProps are described at its wiki page, CommandPropsBuilder.

For more practical examples, see ./examples.

package main

import (
	"fmt"
	"github.com/oklahomer/go-sarah"
	"github.com/oklahomer/go-sarah/slack"
	"golang.org/x/net/context"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

func main() {
	// Setup slack adapter.
	slackConfig := slack.NewConfig()
	slackConfig.Token = "REPLACE THIS"
	adapter, err := slack.NewAdapter(slackConfig)
	if err != nil {
		panic(fmt.Errorf("faileld to setup Slack Adapter: %s", err.Error()))
	}

	// Setup storage.
	cacheConfig := sarah.NewCacheConfig()
	storage := sarah.NewUserContextStorage(cacheConfig)

	// A helper to stash sarah.RunnerOptions for later use.
	options := sarah.NewRunnerOptions()

	// Setup Bot with slack adapter and default storage.
	bot, err := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
	if err != nil {
		panic(fmt.Errorf("faileld to setup Slack Bot: %s", err.Error()))
	}
	options.Append(sarah.WithBot(bot))

	// Setup .hello command
	hello := &HelloCommand{}
	bot.AppendCommand(hello)

	// Setup properties to setup .guess command on the fly
	options.Append(sarah.WithCommandProps(GuessProps))

	// Setup sarah.Runner.
	runnerConfig := sarah.NewConfig()
	runner, err := sarah.NewRunner(runnerConfig, options.Arg())
	if err != nil {
		panic(fmt.Errorf("failed to initialize Runner: %s", err.Error()))
	}

	// Run sarah.Runner.
	runner.Run(context.TODO())
}

var GuessProps = sarah.NewCommandPropsBuilder().
	BotType(slack.SLACK).
	Identifier("guess").
	InputExample(".guess").
	MatchFunc(func(input sarah.Input) bool {
		return strings.HasPrefix(strings.TrimSpace(input.Message()), ".guess")
	}).
	Func(func(ctx context.Context, input sarah.Input) (*sarah.CommandResponse, error) {
		// Generate answer value at the very beginning.
		rand.Seed(time.Now().UnixNano())
		answer := rand.Intn(10)

		// Let user guess the right answer.
		return slack.NewStringResponseWithNext("Input number.", func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
			return guessFunc(c, i, answer)
		}), nil
	}).
	MustBuild()

func guessFunc(_ context.Context, input sarah.Input, answer int) (*sarah.CommandResponse, error) {
	// For handiness, create a function that recursively calls guessFunc until user input right answer.
	retry := func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
		return guessFunc(c, i, answer)
	}

	// See if user inputs valid number.
	guess, err := strconv.Atoi(strings.TrimSpace(input.Message()))
	if err != nil {
		return slack.NewStringResponseWithNext("Invalid input format.", retry), nil
	}

	// If guess is right, tell user and finish current user context.
	// Otherwise let user input next guess with bit of a hint.
	if guess == answer {
		return slack.NewStringResponse("Correct!"), nil
	} else if guess > answer {
		return slack.NewStringResponseWithNext("Smaller!", retry), nil
	} else {
		return slack.NewStringResponseWithNext("Bigger!", retry), nil
	}
}

type HelloCommand struct {
}

var _ sarah.Command = (*HelloCommand)(nil)

func (hello *HelloCommand) Identifier() string {
	return "hello"
}

func (hello *HelloCommand) Execute(context.Context, sarah.Input) (*sarah.CommandResponse, error) {
	return slack.NewStringResponse("Hello!"), nil
}

func (hello *HelloCommand) InputExample() string {
	return ".hello"
}

func (hello *HelloCommand) Match(input sarah.Input) bool {
	return strings.TrimSpace(input.Message()) == ".hello"
}

Overview

go-sarah is a general purpose bot framework that enables developers to create and customize their own bot experiences with any chat service. This comes with a unique feature called "stateful command" as well as some basic features such as command and scheduled task. In addition to those features, this provides rich life cycle management including live configuration update, customizable alerting mechanism, automated command/task (re-)building and concurrent command/task execution.

go-sarah is composed of fine grained components to provide above features. Those components have their own interfaces and default implementations, so developers are free to customize bot behavior by supplying own implementation.

component diagram

Follow below links for details:

Expand ▾ Collapse ▴

Documentation

Overview

Package sarah is a general purpose bot framework that comes with set of utility packages.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrTaskInsufficientArgument is returned when required parameters are not set.
	ErrTaskInsufficientArgument = errors.New("one or more of required fields -- BotType, Identifier or Func -- are empty")

	// ErrTaskScheduleNotGiven is returned when schedule is provided by neither ScheduledTaskPropsBuilder's parameter nor config.
	ErrTaskScheduleNotGiven = errors.New("task schedule is not set or given from config struct")
)
View Source
var (
	// ErrCommandInsufficientArgument depicts an error that not enough arguments are set to CommandProps.
	// This is returned on CommandProps.Build() inside of runner.Run()
	ErrCommandInsufficientArgument = errors.New("BotType, Identifier, InputExample, MatchFunc, and (Configurable)Func must be set.")
)

Functions

func NewBlockedInputError

func NewBlockedInputError(i int) error

NewBlockedInputError creates and return new BlockedInputError instance.

func NewBotNonContinuableError

func NewBotNonContinuableError(errorContent string) error

NewBotNonContinuableError creates and return new BotNonContinuableError instance.

func StripMessage

func StripMessage(pattern *regexp.Regexp, input string) string

StripMessage is a utility function that strips string from given message based on given regular expression. This is to extract usable input value out of entire user message. e.g. ".echo Hey!" becomes "Hey!"

Types

type AbortInput

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

AbortInput is a common Input implementation that represents user's request for context cancellation. When this type is given, each Bot/Adapter implementation should cancel and remove corresponding user's conversational context.

func NewAbortInput

func NewAbortInput(senderKey, message string, sentAt time.Time, replyTo OutputDestination) *AbortInput

NewAbortInput creates a new AbortInput instance with given arguments and returns it.

func (*AbortInput) Message

func (ai *AbortInput) Message() string

Message returns sent message.

func (*AbortInput) ReplyTo

func (ai *AbortInput) ReplyTo() OutputDestination

ReplyTo returns slack channel to send reply to.

func (*AbortInput) SenderKey

func (ai *AbortInput) SenderKey() string

SenderKey returns string representing message sender.

func (*AbortInput) SentAt

func (ai *AbortInput) SentAt() time.Time

SentAt returns message event's timestamp.

type Adapter

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)
}

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 Alerter

type Alerter interface {
	// Alert sends notification to developer/administrator so one may notify Bot's critical state.
	Alert(context.Context, BotType, error) error
}

Alerter can be used to report Bot's critical state to developer/administrator. Anything that implements this interface can be registered as Alerter via Runner.RegisterAlerter.

type BlockedInputError

type BlockedInputError struct {
	ContinuationCount int
}

BlockedInputError indicates incoming input is blocked due to lack of resource. Excessive increase in message volume may result in this error. When this error occurs, Runner does not wait to enqueue input, but just skip the overflowing message and proceed.

Possible cure includes having more workers and/or more worker queue size, but developers MUST aware that this modification may cause more concurrent Command.Execute and Bot.SendMessage operation. With that said, increase workers by setting bigger number to worker.Config.WorkerNum to allow more concurrent executions and minimize the delay; increase worker queue size by setting bigger number to worker.Config.QueueSize to allow delay and have same concurrent execution number.

func (BlockedInputError) Error

func (e BlockedInputError) Error() string

Error returns the detailed error about this blocking situation including the number of continuous occurrence. Do err.(*BlockedInputError).ContinuationCount to get the number of continuous occurrence. e.g. log if the remainder of this number divided by N is 0 to avoid excessive logging.

type Bot

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))
}

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.

func NewBot

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

NewBot creates and returns new defaultBot instance with given Adapter. While Adapter takes care of actual collaboration with each chat service provider, defaultBot takes care of some common tasks including:

- receive Input
- see if sending user is in the middle of conversational context
  - if so, execute the next step with given Input
  - if not, find corresponding Command for given Input and execute it
- call Adapter.SendMessage to send output

The aim of defaultBot is to lessen the tasks of Adapter developer by providing some common tasks' implementations, and achieve easier creation of Bot implementation. Hence this method returns Bot interface instead of any concrete instance so this can be ONLY treated as Bot implementation to be fed to Runner.RegisterBot.

Some optional settings can be supplied by passing sarah.WithStorage and others that return DefaultBotOption.

// Use pre-defined storage.
storage := sarah.NewUserContextStorage(sarah.NewCacheConfig())
bot, err := sarah.NewBot(myAdapter, sarah.WithStorage(sarah.NewUserContextStorage(sarah.NewCacheConfig())))

It is highly recommended to provide concrete implementation of sarah.UserContextStorage, so the users' conversational context can be stored and executed on next Input. sarah.userContextStorage is provided by default to store user context in memory. This storage can be initialized by sarah.NewUserContextStorage like above example.

type BotNonContinuableError

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

BotNonContinuableError represents critical error that Bot can't continue its operation. When Runner receives this, it must stop corresponding Bot, and should inform administrator by available mean.

func (BotNonContinuableError) Error

func (e BotNonContinuableError) Error() string

Error returns detailed error about Bot's non-continuable state.

type BotStatus

type BotStatus struct {
	Type    BotType
	Running bool
}

BotStatus represents the current status of a Bot.

type BotType

type BotType string

BotType indicates what bot implementation a particular Bot/Plugin is corresponding to.

func (BotType) String

func (botType BotType) String() string

String returns a stringified form of BotType

type CacheConfig

type CacheConfig struct {
	ExpiresIn       time.Duration `json:"expires_in" yaml:"expires_in"`
	CleanupInterval time.Duration `json:"cleanup_interval" yaml:"cleanup_interval"`
}

CacheConfig contains some configuration variables for cache mechanism.

func NewCacheConfig

func NewCacheConfig() *CacheConfig

NewCacheConfig creates and returns new CacheConfig instance with default settings. Use json.Unmarshal, yaml.Unmarshal, or manual manipulation to overload default values.

type Command

type Command interface {
	// Identifier returns unique id that represents this Command.
	Identifier() string

	// Execute receives input from user and returns response.
	Execute(context.Context, Input) (*CommandResponse, error)

	// InputExample returns example of user input. This should be used to provide command usage for end users.
	InputExample() string

	// Match is used to judge if this command corresponds to given user input.
	// If this returns true, Bot implementation should proceed to Execute with current user input.
	Match(Input) bool
}

Command defines interface that all command MUST satisfy.

type CommandConfig

type CommandConfig interface{}

CommandConfig provides an interface that every command configuration must satisfy, which actually means empty.

type CommandHelp

type CommandHelp struct {
	Identifier   string
	InputExample string
}

CommandHelp represents help messages for corresponding Command.

type CommandHelps

type CommandHelps []*CommandHelp

CommandHelps is an alias to slice of CommandHelps' pointers.

type CommandProps

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

CommandProps is a designated non-serializable configuration struct to be used in Command construction. This holds relatively complex set of Command construction arguments that should be treated as one in logical term.

type CommandPropsBuilder

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

CommandPropsBuilder helps to construct CommandProps. Developer may set desired property as she goes and call CommandPropsBuilder.Build or CommandPropsBuilder.MustBuild to construct CommandProps at the end. A validation logic runs on build, so the returning CommandProps instant is safe to be passed to Runner.

func NewCommandPropsBuilder

func NewCommandPropsBuilder() *CommandPropsBuilder

NewCommandPropsBuilder returns new CommandPropsBuilder instance.

func (*CommandPropsBuilder) BotType

func (builder *CommandPropsBuilder) BotType(botType BotType) *CommandPropsBuilder

BotType is a setter to provide belonging BotType.

func (*CommandPropsBuilder) Build

func (builder *CommandPropsBuilder) Build() (*CommandProps, error)

Build builds new CommandProps instance with provided values.

func (*CommandPropsBuilder) ConfigurableFunc

ConfigurableFunc is a setter to provide command function. While Func let developers set simple function, this allows them to provide function that requires some sort of configuration struct. On Runner.Run configuration is read from YAML/JSON file located at /path/to/config/dir/{commandIdentifier}.(yaml|yml|json) and mapped to given CommandConfig struct. If no YAML/JSON file is found, runner considers the given CommandConfig is fully configured and ready to use. This configuration struct is passed to command function as its third argument.

func (*CommandPropsBuilder) Func

Func is a setter to provide command function that requires no configuration. If ConfigurableFunc and Func are both called, later call overrides the previous one.

func (*CommandPropsBuilder) Identifier

func (builder *CommandPropsBuilder) Identifier(id string) *CommandPropsBuilder

Identifier is a setter for Command identifier.

func (*CommandPropsBuilder) InputExample

func (builder *CommandPropsBuilder) InputExample(example string) *CommandPropsBuilder

InputExample is a setter to provide example of command execution. This should be used to provide command usage for end users.

func (*CommandPropsBuilder) MatchFunc

func (builder *CommandPropsBuilder) MatchFunc(matchFunc func(Input) bool) *CommandPropsBuilder

MatchFunc is a setter to provide a function that judges if an incoming input "matches" to this Command. When this returns true, this Command is considered as "corresponding to user input" and becomes Command execution candidate.

MatchPattern may be used to specify a regular expression that is checked against user input, Input.Message(); MatchFunc can specify more customizable matching logic. e.g. only return true on specific sender's specific message on specific time range.

func (*CommandPropsBuilder) MatchPattern

func (builder *CommandPropsBuilder) MatchPattern(pattern *regexp.Regexp) *CommandPropsBuilder

MatchPattern is a setter to provide command match pattern. This regular expression is used to find matching command with given Input.

Use MatchFunc to set more customizable matching logic.

func (*CommandPropsBuilder) MustBuild

func (builder *CommandPropsBuilder) MustBuild() *CommandProps

MustBuild is like Build but panics if any error occurs on Build. It simplifies safe initialization of global variables holding built CommandProps instances.

type CommandResponse

type CommandResponse struct {
	Content     interface{}
	UserContext *UserContext
}

CommandResponse is returned by Command or Task when the execution is finished.

func NewSuppressedResponseWithNext

func NewSuppressedResponseWithNext(next ContextualFunc) *CommandResponse

NewSuppressedResponseWithNext creates new sarah.CommandResponse instance with no message and next function to continue

type Commands

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

Commands stashes all registered Command.

func NewCommands

func NewCommands() *Commands

NewCommands creates and returns new Commands instance.

func (*Commands) Append

func (commands *Commands) Append(command Command)

Append let developers register new Command to its internal stash. If any command is registered with the same ID, the old one is replaced in favor of new one.

func (*Commands) ExecuteFirstMatched

func (commands *Commands) ExecuteFirstMatched(ctx context.Context, input Input) (*CommandResponse, error)

ExecuteFirstMatched tries find matching command with the given input, and execute it if one is available.

func (*Commands) FindFirstMatched

func (commands *Commands) FindFirstMatched(input Input) Command

FindFirstMatched look for first matching command by calling Command's Match method: First Command.Match to return true is considered as "first matched" and is returned.

This check is run in the order of Command registration: Earlier the Commands.Append is called, the command is checked earlier. So register important Command first.

func (*Commands) Helps

func (commands *Commands) Helps() *CommandHelps

Helps returns underlying commands help messages in a form of *CommandHelps.

type Config

type Config struct {
	PluginConfigRoot string `json:"plugin_config_root" yaml:"plugin_config_root"`
	TimeZone         string `json:"timezone" yaml:"timezone"`
}

Config contains some configuration variables for Runner.

func NewConfig

func NewConfig() *Config

NewConfig creates and returns new Config instance with default settings. Use json.Unmarshal, yaml.Unmarshal, or manual manipulation to override default values.

type ContextualFunc

type ContextualFunc func(context.Context, Input) (*CommandResponse, error)

ContextualFunc defines a function signature that defines user's next step. When a function or instance method is given as CommandResponse.Next, Bot implementation must store this with Input.SenderKey. On user's next input, inside of Bot.Respond, Bot retrieves stored ContextualFunc and execute this. If CommandResponse.Next is given again as part of result, the same step must be followed.

type DefaultBotOption

type DefaultBotOption func(bot *defaultBot) error

DefaultBotOption defines function that defaultBot's functional option must satisfy.

func BotWithStorage

func BotWithStorage(storage UserContextStorage) DefaultBotOption

BotWithStorage creates and returns DefaultBotOption to set preferred UserContextStorage implementation. Below example utilizes pre-defined in-memory storage.

config := sarah.NewCacheConfig()
configBuf, _ := ioutil.ReadFile("/path/to/storage/config.yaml")
yaml.Unmarshal(configBuf, config)
bot, err := sarah.NewBot(myAdapter, storage)

type DestinatedConfig

type DestinatedConfig interface {
	DefaultDestination() OutputDestination
}

DestinatedConfig defines an interface that config with default destination MUST satisfy. When no default output destination is set with ScheduledTaskPropsBuilder.DefaultDestination, this value is taken as default on ScheduledTaskPropsBuilder.Build.

type HelpInput

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

HelpInput is a common Input implementation that represents user's request for help. When this type is given, each Bot/Adapter implementation should list up registered Commands' input examples, and send them back to user.

func NewHelpInput

func NewHelpInput(senderKey, message string, sentAt time.Time, replyTo OutputDestination) *HelpInput

NewHelpInput creates a new HelpInput instance with given arguments and returns it.

func (*HelpInput) Message

func (hi *HelpInput) Message() string

Message returns sent message.

func (*HelpInput) ReplyTo

func (hi *HelpInput) ReplyTo() OutputDestination

ReplyTo returns slack channel to send reply to.

func (*HelpInput) SenderKey

func (hi *HelpInput) SenderKey() string

SenderKey returns string representing message sender.

func (*HelpInput) SentAt

func (hi *HelpInput) SentAt() time.Time

SentAt returns message event's timestamp.

type Input

type Input interface {
	// SenderKey returns the text form of sender identifier.
	// This value can be used internally as a key to store the sender's conversational context in UserContextStorage.
	// Generally, When connecting chat service has the concept of group or chat room,
	// this sender key should contain the group/room identifier along with user identifier
	// so the user's conversational context is only applied in the exact same group/room.
	//
	// e.g. senderKey := fmt.Sprintf("%d_%d", roomID, userID)
	SenderKey() string

	// Message returns the text form of user input.
	// This may return empty string when this Input implementation represents non-text payload such as photo,
	// video clip or file.
	Message() string

	// SentAt returns the timestamp when the message is sent.
	// This may return a message reception time if the connecting chat service does not provide one.
	// e.g. XMPP server only provides timestamp as part of XEP-0203 when delayed message is delivered.
	SentAt() time.Time

	// ReplyTo returns the sender's address or location to be used to reply message.
	// This may be passed to Bot.SendMessage() as part of Output value to specify the sending destination.
	// This typically contains chat room, member id or mail address.
	// e.g. JID of XMPP server/client.
	ReplyTo() OutputDestination
}

Input defines interface that each incoming message must satisfy. Each Bot/Adapter implementation may define customized Input implementation for each messaging content.

See slack.MessageInput.

type Output

type Output interface {
	Destination() OutputDestination
	Content() interface{}
}

Output defines interface that each outgoing message must satisfy.

func NewOutputMessage

func NewOutputMessage(destination OutputDestination, content interface{}) Output

NewOutputMessage creates and returns new OutputMessage instance. This satisfies Output interface so can be passed to Bot.SendMessage.

type OutputDestination

type OutputDestination interface{}

OutputDestination defines interface that every Bot/Adapter MUST satisfy to represent where the sending message is heading to, which actually means empty interface.

type OutputMessage

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

OutputMessage represents outgoing message.

func (*OutputMessage) Content

func (output *OutputMessage) Content() interface{}

Content returns sending content. This is just an empty interface, so each Bot/Adapter developer may define depending on whatever the struct should contain.

func (*OutputMessage) Destination

func (output *OutputMessage) Destination() OutputDestination

Destination returns its destination in a form of OutputDestination interface. Each Bot/Adapter implementation must explicitly define destination type that satisfies OutputDestination.

type Runner

type Runner interface {
	// Run starts Bot interaction.
	// At this point Runner starts its internal workers and schedulers, runs each bot, and starts listening to incoming messages.
	Run(context.Context)

	// Status returns the status of Runner and belonging Bots.
	// The returned Status value represents a snapshot of the status when this method is called,
	// which means each field value is not subject to update.
	// To reflect the latest status, this is recommended to call this method whenever the value is needed.
	Status() Status
}

Runner is the core of sarah.

This is responsible for each Bot implementation's lifecycle and plugin execution; Bot is responsible for bot-specific implementation such as connection handling, message reception and sending.

Developers can register desired number of Bots and Commands to create own bot experience. While developers may provide own implementation for interfaces in this project to customize behavior, this particular interface is not meant to be implemented and replaced. See https://github.com/oklahomer/go-sarah/pull/47

func NewRunner

func NewRunner(config *Config, options ...RunnerOption) (Runner, error)

NewRunner creates and return new instance that satisfies Runner interface.

The reason for returning interface instead of concrete implementation is to avoid developers from executing RunnerOption outside of NewRunner, where sarah can not be aware of and severe side-effect may occur.

Ref. https://github.com/oklahomer/go-sarah/pull/47

So the aim is not to let developers switch its implementations.

type RunnerOption

type RunnerOption func(*runner) error

RunnerOption defines a function signature that NewRunner's functional option must satisfy.

func WithAlerter

func WithAlerter(alerter Alerter) RunnerOption

WithAlerter creates RunnerOperation that feeds given Alerter implementation to Runner.

func WithBot

func WithBot(bot Bot) RunnerOption

WithBot creates RunnerOption that feeds given Bot implementation to Runner.

func WithCommandProps

func WithCommandProps(props *CommandProps) RunnerOption

WithCommandProps creates RunnerOption that feeds given CommandProps to Runner. Command is built on runner.Run with given CommandProps. This props is re-used when configuration file is updated and Command needs to be re-built.

func WithScheduledTask

func WithScheduledTask(botType BotType, task ScheduledTask) RunnerOption

WithScheduledTask creates RunnerOperation that feeds given ScheduledTask to Runner.

func WithScheduledTaskProps

func WithScheduledTaskProps(props *ScheduledTaskProps) RunnerOption

WithScheduledTaskProps creates RunnerOption that feeds given ScheduledTaskProps to Runner. ScheduledTask is built on runner.Run with given ScheduledTaskProps. This props is re-used when configuration file is updated and ScheduledTask needs to be re-built.

func WithWatcher

func WithWatcher(watcher watchers.Watcher) RunnerOption

WithWatcher creates RunnerOption that feeds given Watcher implementation to Runner. If Config.PluginConfigRoot is set without WithWatcher option, Runner creates Watcher with default configuration on Runner.Run.

func WithWorker

func WithWorker(worker workers.Worker) RunnerOption

WithWorker creates RunnerOperation that feeds given Worker implementation to Runner. If no WithWorker is supplied, Runner creates worker with default configuration on runner.Run.

type RunnerOptions

type RunnerOptions []RunnerOption

RunnerOptions stashes group of RunnerOption for later use with NewRunner().

On typical setup, especially when a process consists of multiple Bots and Commands, each construction step requires more lines of codes. Each step ends with creating new RunnerOption instance to be fed to NewRunner(), but as code gets longer it gets harder to keep track of each RunnerOption. In that case RunnerOptions becomes a handy helper to temporary stash RunnerOption.

options := NewRunnerOptions()

// 5-10 lines of codes to configure Slack bot.
slackBot, _ := sarah.NewBot(slack.NewAdapter(slackConfig), sarah.BotWithStorage(storage))
options.Append(sarah.WithBot(slackBot))

// Here comes other 5-10 codes to configure another bot.
myBot, _ := NewMyBot(...)
optionsAppend(sarah.WithBot(myBot))

// Some more codes to register Commands/ScheduledTasks.
myTask := customizedTask()
options.Append(sarah.WithScheduledTask(myTask))

// Finally feed stashed options to NewRunner at once
runner, _ := NewRunner(sarah.NewConfig(), options.Arg())
runner.Run(ctx)

func NewRunnerOptions

func NewRunnerOptions() *RunnerOptions

NewRunnerOptions creates and returns new RunnerOptions instance.

func (*RunnerOptions) Append

func (options *RunnerOptions) Append(opt RunnerOption)

Append adds given RunnerOption to internal stash. When more than two RunnerOption instances are stashed, they are executed in the order of addition.

func (*RunnerOptions) Arg

func (options *RunnerOptions) Arg() RunnerOption

Arg returns stashed RunnerOptions in a form that can be directly fed to NewRunner's second argument.

type ScheduledConfig

type ScheduledConfig interface {
	Schedule() string
}

ScheduledConfig defines an interface that config with schedule MUST satisfy. When no execution schedule is set with ScheduledTaskPropsBuilder.Schedule, this value is taken as default on ScheduledTaskPropsBuilder.Build.

type ScheduledTask

type ScheduledTask interface {
	Identifier() string
	Execute(context.Context) ([]*ScheduledTaskResult, error)
	DefaultDestination() OutputDestination
	Schedule() string
}

ScheduledTask defines interface that all scheduled task MUST satisfy. As long as a struct satisfies this interface, the struct can be registered as ScheduledTask via Runner.RegisterScheduledTask.

type ScheduledTaskProps

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

ScheduledTaskProps is a designated non-serializable configuration struct to be used in ScheduledTask construction. This holds relatively complex set of ScheduledTask construction arguments that should be treated as one in logical term.

type ScheduledTaskPropsBuilder

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

ScheduledTaskPropsBuilder helps to construct ScheduledTaskProps. Developer may set desired property as she goes and call ScheduledTaskPropsBuilder.Build or ScheduledTaskPropsBuilder.MustBuild to construct ScheduledTaskProps at the end. A validation logic runs on build, so the returning ScheduledTaskProps instant is safe to be passed to Runner.

func NewScheduledTaskPropsBuilder

func NewScheduledTaskPropsBuilder() *ScheduledTaskPropsBuilder

NewScheduledTaskPropsBuilder creates and returns ScheduledTaskPropsBuilder instance.

func (*ScheduledTaskPropsBuilder) BotType

func (builder *ScheduledTaskPropsBuilder) BotType(botType BotType) *ScheduledTaskPropsBuilder

BotType is a setter to provide belonging BotType.

func (*ScheduledTaskPropsBuilder) Build

func (builder *ScheduledTaskPropsBuilder) Build() (*ScheduledTaskProps, error)

Build builds new ScheduledProps instance with provided values.

func (*ScheduledTaskPropsBuilder) ConfigurableFunc

ConfigurableFunc sets function for ScheduledTask with configuration struct. Passed configuration struct is passed to function as a third argument.

When resulting ScheduledTaskProps is passed to sarah.NewRunner() as part of sarah.WithScheduledTaskProps and Runner runs with Config.PluginConfigRoot, configuration struct gets updated automatically when corresponding configuration file is updated.

func (*ScheduledTaskPropsBuilder) DefaultDestination

func (builder *ScheduledTaskPropsBuilder) DefaultDestination(dest OutputDestination) *ScheduledTaskPropsBuilder

DefaultDestination sets default output destination of this task. OutputDestination returned by task execution has higher priority.

func (*ScheduledTaskPropsBuilder) Func

Func sets function to be called on task execution. To set function that requires some sort of configuration struct, use ConfigurableFunc.

func (*ScheduledTaskPropsBuilder) Identifier

Identifier sets unique ID of this task. This is used to identify re-configure tasks and replace old ones.

func (*ScheduledTaskPropsBuilder) MustBuild

func (builder *ScheduledTaskPropsBuilder) MustBuild() *ScheduledTaskProps

MustBuild is like Build, but panics if any error occurs on Build. It simplifies safe initialization of global variables holding built ScheduledTaskProps instances.

func (*ScheduledTaskPropsBuilder) Schedule

func (builder *ScheduledTaskPropsBuilder) Schedule(schedule string) *ScheduledTaskPropsBuilder

Schedule sets execution schedule. Representation spec. is identical to that of github.com/robfig/cron.

type ScheduledTaskResult

type ScheduledTaskResult struct {
	Content     interface{}
	Destination OutputDestination
}

ScheduledTaskResult is a struct that ScheduledTask returns on its execution.

type SerializableArgument

type SerializableArgument struct {
	FuncIdentifier string
	Argument       interface{}
}

SerializableArgument defines user context data to be stored in external storage. This data is read from storage on next user input, deserialized, and executed to continue previous conversation.

type Status

type Status struct {
	Running bool
	Bots    []BotStatus
}

Status represents the current status of the bot system including Runner and all registered Bots.

type TaskConfig

type TaskConfig interface{}

TaskConfig provides an interface that every task configuration must satisfy, which actually means empty.

type UserContext

type UserContext struct {
	// Next contains a function to be called on next user input.
	// Default implementation of UserContextStorage, defaultUserContextStorage, uses this to store conversational contexts.
	//
	// Since this is a plain function, this is stored in the exact same memory space the Bot is currently running,
	// which means this function can not be shared with other Bot instance or can not be stored in external storage such as Redis.
	// To store user context in externally, set Serializable to store serialized arguments in external storage.
	Next ContextualFunc

	// Serializable, on the other hand, contains arguments and function identifier to be stored in external storage.
	// When user input is given next time, serialized SerializableArgument is fetched from storage, deserialized, and fed to pre-registered function.
	// Pre-registered function is identified by SerializableArgument.FuncIdentifier.
	// A reference implementation is available at https://github.com/oklahomer/go-sarah-rediscontext
	Serializable *SerializableArgument
}

UserContext represents a user's conversational context. If this is returned as part of CommandResponse, user is considered "in the middle of conversation," which means the next input of the user MUST be fed to a function declared in UserContext to continue the conversation. This has higher priority than finding and executing Command by checking Command.Match against Input.

Currently this structure supports two forms of context storage. See GitHub issue, https://github.com/oklahomer/go-sarah/issues/34, for detailed motives.

func NewUserContext

func NewUserContext(next ContextualFunc) *UserContext

NewUserContext creates and returns new UserContext with given ContextualFunc. Once this instance is stored in Bot's internal storage, the next input from the same user must be fed to this ContextualFunc so the conversation continues.

type UserContextStorage

type UserContextStorage interface {
	Get(string) (ContextualFunc, error)
	Set(string, *UserContext) error
	Delete(string) error
	Flush() error
}

UserContextStorage defines an interface of Bot's storage mechanism for users' conversational contexts.

func NewUserContextStorage

func NewUserContextStorage(config *CacheConfig) UserContextStorage

NewUserContextStorage creates and returns new defaultUserContextStorage instance to store users' conversational contexts.

Directories

Path Synopsis
alerter Package alerter and its sub packages provide alerting mechanisms that implement sarah.Alerter interface to inform bot's critical states to administrator.
alerter/line Package line provides sarah.Alerter implementation for LINE Notify.
examples/simple Package main provides a simple bot experience using slack.Adapter with multiple plugin commands and scheduled tasks.
examples/simple/plugins/count Package count provides example code to setup sarah.CommandProps.
examples/simple/plugins/echo Package echo provides example code to sarah.Command implementation.
examples/simple/plugins/fixedtimer Package fixedtimer provides example code to setup ScheduledTaskProps with fixed schedule.
examples/simple/plugins/guess Package guess provides example code to setup stateful command.
examples/simple/plugins/hello Package hello provides example code to setup relatively simple sarah.CommandProps.
examples/simple/plugins/morning Package morning provides example code to setup sarah.CommandProps with relatively complex matching function.
examples/simple/plugins/timer Package timer provides example code to setup ScheduledTaskProps with re-configurable schedule and sending room.
examples/simple/plugins/todo Package todo is an example of stateful command that let users input required arguments step by step in a conversational manner.
examples/status Package main provides an example that uses Runner.Status() to return current sarah.Runner and its belonging Bot status via HTTP server.
gitter Package gitter provides sarah.Adapter implementation for gitter.
log Package log provides logging mechanism including replaceable Logger interface and its default implementation.
plugins Package plugins and its sub packages gives some reference implementations of sarah.Command.
plugins/echo Package echo is an reference implementation to provide the simplest form of sarah.CommandProps.
plugins/hello Package hello is an reference implementation to provide the simplest form of sarah.CommandProps.
plugins/worldweather Package worldweather is an reference implementation that provides relatively practical sarah.CommandProps.
retry Package retry provides general retry logic and designated error structure that contains multiple errors.
slack Package slack provides sarah.Adapter implementation for Slack.
watchers Package watchers provides mechanism to subscribe events under given directory.
workers Package workers provides general purpose worker mechanism that outputs stacktrace when given job panics.