sarah

package module
v3.0.1 Latest Latest
Warning

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

Go to latest
Published: Mar 1, 2022 License: MIT Imports: 13 Imported by: 0

README

GoDoc Go Report Card Build Status Coverage Status Maintainability

Introduction

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

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 fundamental features, this project provides rich life cycle management including live configuration update, customizable alerting mechanism, automated command/task (re-)building, and panic-proofed concurrent command/task execution.

Such features are achieved with a composition of fine-grained components. Each component has its own interface and a default implementation, so developers are free to customize their bot experience by replacing the default implementation for a particular component with their own implementation. Thanks to such segmentalized lifecycle management architecture, the adapter component to interact with each chat service has fewer responsibilities comparing to other bot frameworks; An adapter developer may focus on implementing the protocol to interacting with the corresponding chat service. To take a look at those components and their relations, see Components.

IMPORTANT NOTICE

v3 Release

This is the third major version of go-sarah, which introduces the Slack adapter's improvement to support both RTM and Events API. Breaking interface change for Slack adapter was inevitable and that is the sole reason for this major version up. Other than that, this does not include any breaking change. See Migrating from v2.x to v3.x for details.

v2 Release

The second major version introduced some breaking changes to go-sarah. This version still supports and maintains all functionalities, better interfaces for easier integration are added. See Migrating from v1.x to v2.x to migrate from the older version.

Supported Chat Services/Protocols

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

At a Glance

General Command Execution

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.

Stateful Command Execution

The below image depicts how a command with a 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."

The above example is a good way to let a user input a series of arguments in a conversational manner. Below is another example that uses a stateful command to entertain the user.

Example Code

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 (
	"context"
	"fmt"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	
	"os"
	"os/signal"
	"syscall"
	
	// Below packages register commands in their init().
	// Importing with blank identifier will do the magic.
	_ "guess"
	_ "hello"
)

func main() {
	// Setup Slack adapter
	setupSlack()
	
	// Prepare go-sarah's core context.
	ctx, cancel := context.WithCancel(context.Background())

	// Run
	config := sarah.NewConfig()
	err := sarah.Run(ctx, config)
	if err != nil {
		panic(fmt.Errorf("failed to run: %s", err.Error()))
	}
	
	// Stop when signal is sent.
	c := make(chan os.Signal, 1)
   	signal.Notify(c, syscall.SIGTERM)
   	select {
   	case <-c:
   		cancel()
   
   	}
}

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

	// Setup optional storage so conversational context can be stored.
	cacheConfig := sarah.NewCacheConfig()
	storage := sarah.NewUserContextStorage(cacheConfig)

	// 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()))
	}
	sarah.RegisterBot(bot)
}

package guess

import (
	"context"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

func init() {
	sarah.RegisterCommandProps(props)
}

var props = sarah.NewCommandPropsBuilder().
	BotType(slack.SLACK).
	Identifier("guess").
	Instruction("Input .guess to start a game.").
	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.NewResponse(input, "Input number.", slack.RespWithNext(func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error){
			return guessFunc(c, i, answer)
		}))
	}).
	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.NewResponse(input, "Invalid input format.", slack.RespWithNext(retry))
	}

	// 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.NewResponse(input, "Correct!")
	} else if guess > answer {
		return slack.NewResponse(input, "Smaller!", slack.RespWithNext(retry))
	} else {
		return slack.NewResponse(input, "Bigger!", slack.RespWithNext(retry))
	}
}

package hello

import (
	"context"
	"github.com/oklahomer/go-sarah/v3"
	"github.com/oklahomer/go-sarah/v3/slack"
	"strings"
)

func init() {
    sarah.RegisterCommand(slack.SLACK, &command{})	
}

type command struct {
}

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

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

func (hello *command) Execute(_ context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
	return slack.NewResponse(i, "Hello!")
}

func (hello *command) Instruction(input *sarah.HelpInput) string {
	if 12 < input.SentAt().Hour() {
		// This command is only active in the morning.
		// Do not show instruction in the afternoon.
		return ""
	}
	return "Input .hello to greet"
}

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

Supported Golang Versions

Official Release Policy says "each major Go release is supported until there are two newer major releases." Following this policy would help this project enjoy the improvements introduced in the later versions. However, not all projects can immediately switch to a newer environment. Migration could especially be difficult when this project cuts off the older version's support right after a new major Go release.

As a transition period, this project includes support for one older version than Go project does. Such a version is guaranteed to be listed in .travis.ci. In other words, new features/interfaces introduced in 1.10 can be used in this project only after 1.12 is out.

Further Readings

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 = xerrors.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 = xerrors.New("task schedule is not set or given from config struct")
)
View Source
var ErrAlreadySubscribing = xerrors.New("already subscribing")

ErrAlreadySubscribing is returned when duplicated calls to ConfigWatcher.Watch() occur.

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 = xerrors.New("BotType, Identifier, InstructionFunc, MatchFunc and (Configurable)Func must be set.")
)
View Source
var ErrRunnerAlreadyRunning = xerrors.New("go-sarah's process is already running")

ErrRunnerAlreadyRunning indicates that sarah.Run() is already called and the process is already running. When this is returned, a second or later activations are prevented so the initially activated process is still protected.

View Source
var ErrWatcherNotRunning = xerrors.New("context is already canceled")

ErrWatcherNotRunning is returned when ConfigWatcher.Unwatch() is called but the context is already canceled.

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 RegisterAlerter

func RegisterAlerter(alerter Alerter)

RegisterAlerter registers given sarah.Alerter implementation. When registered sarah.Bot implementation encounters critical state, given alerter is called to notify such state.

func RegisterBot

func RegisterBot(bot Bot)

RegisterBot registers given sarah.Bot implementation to be run on sarah.Run(). This may be called multiple times to register as many bot instances as wanted. When a Bot with same sarah.BotType is already registered, this returns error on sarah.Run().

func RegisterBotErrorSupervisor

func RegisterBotErrorSupervisor(fnc func(BotType, error) *SupervisionDirective)

RegisterBotErrorSupervisor registers a given supervising function that is called when a Bot escalates an error. This function judges if the given error is worth being notified to administrators and if the Bot should stop. A developer may return *SupervisionDirective to tell such order. If the escalated error can simply be ignored, a nil value can be returned.

Bot/Adapter can escalate an error via a function, func(error), that is passed to Run() as a third argument. When BotNonContinuableError is escalated, go-sarah's core cancels failing Bot's context and thus the Bot and related resources stop working. If one or more sarah.Alerters implementations are registered, such critical error is passed to the alerters and administrators will be notified. When other types of error are escalated, the error is passed to the supervising function registered via sarah.RegisterBotErrorSupervisor(). The function may return *SupervisionDirective to tell how go-sarah's core should react.

Bot/Adapter's implementation should be simple. It should not handle serious errors by itself. Instead, it should simply escalate an error every time when a noteworthy error occurs and let core judge how to react. For example, if the bot should stop when three reconnection trial fails in ten seconds, the scenario could be somewhat like below:

  1. Bot escalates reconnection error, FooReconnectionFailureError, each time it fails to reconnect
  2. Supervising function counts the error and ignores the first two occurrence
  3. When the third error comes within ten seconds from the initial error escalation, return *SupervisionDirective with StopBot value of true

Similarly, if there should be a rate limiter to limit the calls to alerters, the supervising function should take care of this instead of the failing Bot. Each Bot/Adapter's implementation can be kept simple in this way. go-sarah's core should always supervise and control its belonging Bots.

func RegisterCommand

func RegisterCommand(botType BotType, command Command)

RegisterCommand registers given sarah.Command. On sarah.Run(), Commands are registered to corresponding bot via Bot.AppendCommand().

func RegisterCommandProps

func RegisterCommandProps(props *CommandProps)

RegisterCommandProps registers given sarah.CommandProps to build sarah.Command on sarah.Run(). This props is re-used when configuration file is updated and a corresponding sarah.Command needs to be re-built.

func RegisterConfigWatcher

func RegisterConfigWatcher(watcher ConfigWatcher)

RegisterConfigWatcher registers given ConfigWatcher implementation.

func RegisterScheduledTask

func RegisterScheduledTask(botType BotType, task ScheduledTask)

RegisterScheduledTask registers given sarah.ScheduledTask. On sarah.Run(), schedule is set for this task.

func RegisterScheduledTaskProps

func RegisterScheduledTaskProps(props *ScheduledTaskProps)

RegisterScheduledTaskProps registers given sarah.ScheduledTaskProps to build sarah.ScheduledTask on sarah.Run(). This props is re-used when configuration file is updated and a corresponding sarah.ScheduledTask needs to be re-built.

func RegisterWorker

func RegisterWorker(worker workers.Worker)

RegisterWorker registers given workers.Worker implementation. When this is not called, a worker instance with default setting is used.

func Run

func Run(ctx context.Context, config *Config) error

Run is a non-blocking function that starts running go-sarah's process with pre-registered options. Workers, schedulers and other required resources for bot interaction starts running on this function call. This returns error when bot interaction cannot start; No error is returned when process starts successfully.

Refer to ctx.Done() or sarah.CurrentStatus() to reference current running status.

To control its lifecycle, a developer may cancel ctx to stop go-sarah at any moment. When bot interaction stops unintentionally without such context cancellation, the critical state is notified to administrators via registered sarah.Alerter. This is recommended to register multiple sarah.Alerter implementations to make sure critical states are notified.

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 {
	OriginalInput Input
	// 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(input Input) *AbortInput

NewAbortInput creates a new AbortInput instance with given input. When this type is given, each Bot/Adapter implementation should cancel the user's conversational context.

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 the corresponding command, execute it, and send the result back to the user if possible.
	Respond(context.Context, Input) error

	// SendMessage sends given message to the 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 the user.
	AppendCommand(Command)

	// Run is called on sarah.Run() to let this Bot start interacting with corresponding service provider.
	// When the service provider sends a message to us, convert that message payload to sarah.Input and send to inputReceiver.
	// An internal worker will receive the Input instance and proceed to find and execute the corresponding command.
	// The worker is managed by go-sarah's core; Bot/Adapter developers do not have to worry about implementing one.
	//
	// sarah.Run() allocates a new goroutine for each bot so this method can block til interaction ends.
	// When this method returns, the interaction is considered finished.
	//
	// The bot lifecycle is entirely managed by go-sarah's core.
	// On critical situation, notify such event via notifyErr and let go-sarah's core handle the error.
	// When the bot is indeed in a critical state and cannot proceed further operation, ctx is canceled by go-sarah.
	// Bot/Adapter developers may listen to this ctx.Done() to clean up its internal resources.
	Run(ctx context.Context, inputReceiver func(Input) error, notifyErr func(error))
}

Bot provides an interface that each bot implementation must satisfy. Instance of concrete type can be registered via sarah.RegisterBot() to have its lifecycle under control. Multiple Bot implementation may be registered by multiple sarah.RegisterBot() calls.

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)

	// Instruction returns example of user input. This should be used to provide command usage for end users.
	Instruction(input *HelpInput) 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
	Instruction 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) Instruction

func (builder *CommandPropsBuilder) Instruction(instruction string) *CommandPropsBuilder

Instruction is a setter to provide an instruction of command execution. This should be used to provide command usage for end users.

func (*CommandPropsBuilder) InstructionFunc

func (builder *CommandPropsBuilder) InstructionFunc(fnc func(input *HelpInput) string) *CommandPropsBuilder

InstructionFunc is a setter to provide a function that receives user input and returns instruction. Use Instruction() when a simple text instruction can always be returned. If the instruction has to be customized per user or the instruction has to be hidden in a certain group or from a certain user, use InstructionFunc(). Use receiving *HelpInput and judge if an instruction should be returned. e.g. .reboot command is only supported for administrator users in admin group so this command should be hidden in other groups.

Also see MatchFunc() for such authentication mechanism.

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(input *HelpInput) *CommandHelps

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

type Config

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

Config contains some basic configuration variables for go-sarah.

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 ConfigNotFoundError

type ConfigNotFoundError struct {
	BotType BotType
	ID      string
}

ConfigNotFoundError is returned when corresponding configuration is not found. This is typically returned when the caller tries to see if there is any configuration available via ConfigWatcher.Read().

func (*ConfigNotFoundError) Error

func (err *ConfigNotFoundError) Error() string

Error returns stringified representation of the error.

type ConfigWatcher

type ConfigWatcher interface {
	// Read reads the latest configuration value and apply that value to configPtr.
	Read(botCtx context.Context, botType BotType, id string, configPtr interface{}) error
	// Watch subscribes to given id's configuration.
	// When a change to the corresponding configuration value occurs, callback is called.
	// A call to callback function triggers go-sarah's core to call Read() to reflect the latest configuration value.
	Watch(botCtx context.Context, botType BotType, id string, callback func()) error
	// Unwatch is called when Bot is stopped and subscription is no longer required.
	Unwatch(botType BotType) error
}

ConfigWatcher provides an interface for such a component that subscribes to any changes for configuration value of Command and ScheduledTask. One example could be watchers.fileWatcher that subscribes to directory changes: while another reference implementation to subscribe changes on GitHub repository is hosted at https://github.com/oklahomer/go-sarah-githubconfig .

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)

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 {
	OriginalInput Input
	// 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' instructions and send them back to user.

func NewHelpInput

func NewHelpInput(input Input) *HelpInput

NewHelpInput creates a new HelpInput instance with given user input and returns it. This is Bot/Adapter's responsibility to receive an input from user, convert it to sarah.Input and see if the input requests for "help." For example, a slack adapter may check if the given message is equal to :help: emoji. If true, create a HelpInput instant with NewHelpInput and pass it to go-sarah's core.

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

func CurrentStatus

func CurrentStatus() Status

CurrentStatus returns the current status of go-sarah. This can still be called even if sarah.Run() is not called, yet. So developers can safely build two different goroutines:

  • One to setup bot configuration and call sarah.Run()
  • Another to periodically call sarah.CurrentStatus() and monitor status. When Status.Running is false and Status.Bots is empty, then bot is not initiated yet.

type SupervisionDirective

type SupervisionDirective struct {
	// StopBot tells the core to stop the failing Bot and cleanup related resources.
	// When two or more Bots are registered and one or more Bots are to be still running after the failing Bot stops,
	// internal workers and scheduler keep running.
	//
	// When all Bots stop, then the core stops all resources.
	StopBot bool
	// AlertingErr is sent registered alerters and administrators will be notified.
	// Set nil when such alert notification is not required.
	AlertingErr error
}

SupervisionDirective tells go-sarah's core how to react when a Bot escalates an error. A customized supervisor can be defined and registered via RegisterBotErrorSupervisor().

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

Jump to

Keyboard shortcuts

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