sarah

package module
v4.0.3 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2022 License: MIT Imports: 13 Imported by: 2

README

Go Reference Go Report Card 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 commands and scheduled tasks. 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 compared to other bot frameworks; An adapter developer may focus on implementing the protocol to interact with the corresponding chat service. To take a look at those components and their relations, see Components.

IMPORTANT NOTICE

v4 Release

This is the fourth major version of go-sarah, which involves some architectural changes:

  • sarah.NewBot now returns a single value: sarah.Bot
  • Utility packages including logger, retry, and worker are now hosted by github.com/oklahomer/go-kasumi

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 changes for the 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, while better interfaces for easier integration are introduced. 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 the user input and the 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 a 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/v4"
	"github.com/oklahomer/go-sarah/v4/slack"
	
	"os"
	"os/signal"
	"syscall"
	
	// Below packages register commands in their init().
	// Importing with a blank identifier will do the magic.
	_ "guess"
	_ "hello"
)

func main() {
	// Set up Slack adapter
	setupSlack()
	
	// Prepare 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 a signal is sent.
	c := make(chan os.Signal, 1)
   	signal.Notify(c, syscall.SIGTERM)
   	select {
   	case <-c:
   		cancel()
   
   	}
}

func setupSlack() {
	// Set up 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()))
	}

	// Set up an optional storage so conversational contexts can be stored.
	cacheConfig := sarah.NewCacheConfig()
	storage := sarah.NewUserContextStorage(cacheConfig)

	// Set up a bot with Slack adapter and a default storage.
	bot := sarah.NewBot(adapter, sarah.BotWithStorage(storage))
	
	sarah.RegisterBot(bot)
}

package guess

import (
	"context"
	"github.com/oklahomer/go-sarah/v4"
	"github.com/oklahomer/go-sarah/v4/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 an answer value at the very beginning.
		rand.Seed(time.Now().UnixNano())
		answer := rand.Intn(10)

		// Let a 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 the user input the right answer.
	retry := func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
		return guessFunc(c, i, answer)
	}

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

	// If the guess is right, tell the user and finish the current user context.
	// Otherwise, let the user input the 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/v4"
	"github.com/oklahomer/go-sarah/v4/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 the 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 fine-grained customizable interfaces and reference implementations.

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 and Func -- are empty")

	// ErrTaskScheduleNotGiven is returned when a 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 ErrAlreadySubscribing = errors.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 can be returned by CommandPropsBuilder.Build.
	ErrCommandInsufficientArgument = errors.New("BotType, Identifier, InstructionFunc, MatchFunc and (Configurable)Func must be set")
)
View Source
var ErrRunnerAlreadyRunning = errors.New("go-sarah's process is already running")

ErrRunnerAlreadyRunning indicates that Run is already called and the process is running. The second or later initiations are prevented by returning this error so the initially activated process is protected.

View Source
var ErrWatcherNotRunning = errors.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 returns a new BlockedInputError instance.

func NewBotNonContinuableError

func NewBotNonContinuableError(errorContent string) error

NewBotNonContinuableError creates and returns a new BotNonContinuableError instance.

func RegisterAlerter

func RegisterAlerter(alerter Alerter)

RegisterAlerter registers a given Alerter implementation to Sarah. When Sarah's process or a registered Bot implementation encounters a critical state, Alerter.Alert is called to notify such state. A developer may call this method multiple times to register multiple Alerters.

func RegisterBot

func RegisterBot(bot Bot)

RegisterBot registers a given Bot implementation to be run on Run call. This may be called multiple times to register as many bot instances as wanted.

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. When an action is required, the function may return non-nil *SupervisionDirective to pass the order; Return nil when the escalated error can simply be ignored.

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

Bot and Adapter's implementation should be simple. It should not handle serious errors by itself. Instead, they should simply escalate an error every time when a noteworthy error occurs and let Sarah 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. The 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 or Adapter's implementation can be kept simple in this way; Sarah should always supervise and control its belonging Bots.

func RegisterCommand

func RegisterCommand(botType BotType, command Command)

RegisterCommand registers a given Command implementation. On Run, each Command implementation is registered to the corresponding bot via Bot.AppendCommand. A Bot is considered to "correspond" when its BotType matches with the botType.

func RegisterCommandProps

func RegisterCommandProps(props *CommandProps)

RegisterCommandProps registers a given CommandProps to build Command implementation on Run call. This instance is reused when a configuration is updated and the corresponding Command needs to be rebuilt to reflect the changes.

func RegisterConfigWatcher

func RegisterConfigWatcher(watcher ConfigWatcher)

RegisterConfigWatcher registers a given ConfigWatcher implementation to Sarah. If a ConfigWatcher is registered, Sarah's process subscribes to the changes to Command or ScheduledTask's configuration. When a configuration is updated, ConfigWatcher reads the new configuration setting and reflects to the corresponding configuration instance so Sarah can rebuild the corresponding Command or ScheduledTask with the new setting.

func RegisterScheduledTask

func RegisterScheduledTask(botType BotType, task ScheduledTask)

RegisterScheduledTask registers a given ScheduledTask to Sarah. On Run, a schedule is set for this task.

func RegisterScheduledTaskProps

func RegisterScheduledTaskProps(props *ScheduledTaskProps)

RegisterScheduledTaskProps registers a given ScheduledTaskProps to build ScheduledTask on Run call. This instance is reused when a configuration file is updated and the corresponding ScheduledTask needs to be rebuilt.

func RegisterWorker

func RegisterWorker(worker worker.Worker)

RegisterWorker registers a given worker.Worker implementation to Sarah. When one is not registered, a worker instance with default setting is used.

func Run

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

Run sets up all required resources and initiates Sarah. Workers, schedulers, and other required resources for a bot interaction start running on this function call. This returns an error when bot interaction cannot start; No error is returned when the process starts successfully.

Call ctx.Done or CurrentStatus to reference current running status.

To control its lifecycle, a developer may cancel ctx and stop Sarah at any moment. When bot interaction stops unintentionally without such context cancellation, the critical state is notified to administrators via registered Alerter. Registering multiple Alerter implementations to ensure successful notification is recommended.

func StripMessage

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

StripMessage is a utility function that applies the given regular expression to the input string and replaces the matching part with the empty string. Use this to extract the meaningful input value out of the 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 the user's request for a context cancellation. When this type is given to Bot.Respond, the Bot implementation should cancel the user's current conversational context.

func NewAbortInput

func NewAbortInput(input Input) *AbortInput

NewAbortInput creates a new instance of an Input implementation -- AbortInput -- with the given input.

func (*AbortInput) Message

func (ai *AbortInput) Message() string

Message returns the stringified representation of the message.

func (*AbortInput) ReplyTo

func (ai *AbortInput) ReplyTo() OutputDestination

ReplyTo returns the sender's address or location to be used to reply a message.

func (*AbortInput) SenderKey

func (ai *AbortInput) SenderKey() string

SenderKey returns a stringified representation of the message sender.

func (*AbortInput) SentAt

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

SentAt returns the timestamp when the message is sent.

type Adapter

type Adapter interface {
	// BotType tells what type of chat service this bot is integrating with. e.g. slack, gitter, cli, etc...
	// This can also be used as a unique ID to distinguish one bot from another.
	BotType() BotType

	// Run is called on Sarah's initiation, and the Adapter initiates its interaction with the corresponding chat service.
	// Sarah allocates a new goroutine for this task, so this execution can block until the given context is canceled.
	// When the chat service sends a message, this implementation receives the message, converts into Input, and sends it to the Input channel.
	// Sarah then receives the Input and sees if the input must be applied to the currently cached user context or if there is any matching Command.
	Run(context.Context, func(Input) error, func(error))

	// SendMessage sends the given message to the chat service.
	// Sarah calls this method when ScheduledTask.Execute or Command.Execute returns a non-nil response.
	// This must be capable of being called simultaneously by multiple workers.
	SendMessage(context.Context, Output)
}

Adapter defines an interface that each bot adapter implementation must satisfy. An instance of its implementation and DefaultBotOption values can be passed to NewBot to set up a bot. The returned bot can be fed to Sarah via RegisterBot to have its life cycle managed.

type Alerter

type Alerter interface {
	// Alert sends a notification to administrators so they can acknowledge the current critical state.
	Alert(context.Context, BotType, error) error
}

Alerter notifies administrators when Sarah or a bot is in a critical state. This is recommended to design one Alerter implementation deal with one and only one communication channel. e.g. MailAlerter sends an e-mail to administrators; SMSAlerter sends an SMS message to administrators. To notify via multiple communication channels, register as many Alerter implementations as required with multiple RegisterAlerter calls.

type BlockedInputError

type BlockedInputError struct {
	ContinuationCount int
}

BlockedInputError indicates the incoming input is blocked due to a lack of worker resources. An excessive increase in message volume may result in this error. Upon this occurrence, Sarah does not wait until the input can be enqueued, but just skip the overflowing message and proceed with its operation.

Possible cure includes having more workers and/or more worker queue size, but developers MUST be aware that this modification may cause more concurrent Command.Execute and Bot.SendMessage operation. With that said, developers can set a bigger number to worker.Config.WorkerNum to increase the number of workers and allow more concurrent executions with less delay; set a bigger number to worker.Config.QueueSize to allow more delay with the same maximum number of concurrent executions.

func (BlockedInputError) Error

func (e BlockedInputError) Error() string

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

type Bot

type Bot interface {
	// BotType returns a BotType this Bot implementation represents,
	// which can be used as a unique ID to distinguish one Bot implementation from another.
	BotType() BotType

	// Respond receives a user input, executes a "task" against it, and sends the result back to the user when necessary.
	// A task can be one of the below depending on the user's current state:
	//   - When the user is in the middle of stateful command execution, the given input is treated as part of the instruction given in a user-interactive manner.
	//     - Respond executes a function tied to the current user state and sends the result back to the user.
	//   - When no user context is stored, then the input is treated as a brand new command execution.
	//     - Respond finds a Command, executes it, and sends the result back to the user.
	// In either way, a new user state is set to the storage when the task's result tells to do so.
	Respond(context.Context, Input) error

	// SendMessage sends a given message to the destination depending on the Bot implementation.
	// This is mainly used to send scheduled task's result.
	SendMessage(context.Context, Output)

	// AppendCommand receives a Command to be registered to this Bot implementation.
	// The Bot implementation must append the given Command to its internal stash so the corresponding Command can be found when the user gives an Input.
	//
	// Stashed commands are checked against user input in Bot.Respond.
	// If Command.Match returns true, the Command is considered to "correspond" to the given Input.
	// Then the corresponding Command's Command.Execute is called and the result is sent back to the user.
	//
	// A developer may call this method by oneself to register a Command.
	// Or one can register a Command with sarah package's public function, RegisterCommand.
	// The use of RegisterCommand would be easier because one does not have to carry around the reference to Bot implementation's instance to call its method.
	// A more advanced way is to call sarah package's RegisterCommandProps.
	// In this way, a Command is built and set when Sarah boots up or when the Command's configuration is updated.
	AppendCommand(Command)

	// Run is called when Sarah boots up.
	// On this execution, Bot implementation initiates its interaction with the corresponding chat service.
	// When the chat service sends a message, convert that message payload into Input implementation and send it to inputReceiver.
	// Sarah's internal worker receives the Input and proceeds to find and execute the corresponding command.
	//
	// The initiation of Sarah allocates a new goroutine for each bot so this method can block until when the interaction ends.
	// When this method returns, the interaction is considered finished.
	//
	// The bot lifecycle is entirely managed by Sarah.
	// On critical situation, notify such event via notifyErr and let Sarah handle the error.
	// When the bot is indeed in a critical state and can not proceed further operation, ctx is canceled by 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 defines an interface that each interacting bot must satisfy. Its implementation can be registered to Sarah with RegisterBot, and the lifecycle will be managed by Sarah. Multiple Bot implementations can be registered by multiple RegisterBot calls.

func NewBot

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

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

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

The purpose of defaultBot is to lessen the tasks of Adapter developers by providing some common tasks' implementations and ease the creation of Bot implementation. Instead of passing an Adapter implementation to NewBot, Developers can also develop a Bot implementation from scratch to highly customize the behavior.

Some optional settings can be supplied by passing DefaultBotOption values returned by functions including BotWithStorage.

// Use a storage.
storage := sarah.NewUserContextStorage(sarah.NewCacheConfig())
opt := sarah.BotWithStorage(storage)
bot, err := sarah.NewBot(myAdapter, opt)

It is highly recommended to provide an implementation of UserContextStorage, so the users' conversational context can be stored and executed on the next message reception. A reference implementation of UserContextStorage can be initialized with NewUserContextStorage. This caches user context information in process memory, so the stored context information is lost on process restart.

type BotNonContinuableError

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

BotNonContinuableError represents a critical error that Bot can't continue its operation. When Sarah receives this error, she must stop the failing Bot and should inform administrators with Alerter.

func (BotNonContinuableError) Error

func (e BotNonContinuableError) Error() string

Error returns a detailed message about the Bot's non-continuable state.

type BotStatus

type BotStatus struct {
	// Type represents a BotType the corresponding Bot.BotType returns.
	Type BotType

	// Running indicates if the Bot is currently "running."
	// The Bot is considered running when Bot.Run is already called and its process is context.Context is not yet canceled.
	// When this returns false, the state is final and the Bot is never recovered unless the process is rebooted.
	// In other words, a Bot is "running" even if the connection with the chat service is unstable and recovery is in progress.
	Running bool
}

BotStatus represents the current status of a Bot.

type BotType

type BotType string

BotType tells what type of chat service a Bot or a plugin integrates with. e.g. slack, gitter, cli, etc... This can be used as a unique ID to distinguish one Bot implementation from another.

func (BotType) String

func (botType BotType) String() string

String returns a stringified form of the BotType.

type CacheConfig

type CacheConfig struct {
	// ExpiresIn declares how long a stored UserContext lives.
	ExpiresIn time.Duration `json:"expires_in" yaml:"expires_in"`

	// CleanupInterval declares how often the expired items are removed from the storage.
	// The default UserContextStorage's cache mechanism still holds references to expired values until a cleanup function runs and completely removes the expired values.
	// However, cached items are considered "expired" once the expiration time is over, and they are not returned to the caller even though the value is still cached.
	CleanupInterval time.Duration `json:"cleanup_interval" yaml:"cleanup_interval"`
}

CacheConfig contains some configuration values for the default UserContextStorage implementation.

func NewCacheConfig

func NewCacheConfig() *CacheConfig

NewCacheConfig creates and returns a new CacheConfig instance with the default setting values. Use json.Unmarshal, yaml.Unmarshal, or manual manipulation to override them.

type Command

type Command interface {
	// Identifier returns a unique id of this Command.
	Identifier() string

	// Execute receives an Input sent by a user and returns a response in a form of CommandResponse.
	Execute(context.Context, Input) (*CommandResponse, error)

	// Instruction returns a help message to show the Command usage to the requesting user.
	// A list of instructions may be returned to a user at once, so the message should be simple.
	Instruction(*HelpInput) string

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

Command defines an interface that all executable command MUST satisfy. This is executed against a user's input.

type CommandConfig

type CommandConfig interface{}

CommandConfig provides an interface that every command configuration value must satisfy, which actually is empty. Think of this as a kind of marker interface with a more meaningful name.

type CommandHelp

type CommandHelp struct {
	// Identifier represents the unique id of the corresponding Command.
	Identifier string

	// Instruction represents a help message to guide the Command usage.
	Instruction string
}

CommandHelp represents an instruction for the corresponding Command.

type CommandHelps

type CommandHelps []*CommandHelp

CommandHelps is an alias to a slice of CommandHelp pointers.

type CommandProps

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

CommandProps is a designated non-serializable configuration struct to be used for Command construction. This holds a relatively complex set of Command construction arguments and properties.

type CommandPropsBuilder

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

CommandPropsBuilder helps to construct a CommandProps. A developer may set up a Command construction property -- CommandProps -- by calling CommandPropsBuilder.Build or CommandPropsBuilder.MustBuild at the end. A validation logic runs on build, so the returning CommandProps instant is safe to be passed to RegisterCommandProps.

func NewCommandPropsBuilder

func NewCommandPropsBuilder() *CommandPropsBuilder

NewCommandPropsBuilder returns new CommandPropsBuilder instance. Use this to set up a CommandProps that can be used to build a Command on the fly.

func (*CommandPropsBuilder) BotType

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

BotType is a setter to provide the belonging BotType.

func (*CommandPropsBuilder) Build

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

Build builds a new CommandProps instance with the provided values.

func (*CommandPropsBuilder) ConfigurableFunc

ConfigurableFunc is a setter to provide a command function that takes a configuration value as a struct. While Func lets developers set a simple function, this allows them to provide a function that requires some sort of configuration struct. On Sarah initiation, configuration settings are read by ConfigWatcher and mapped to the given CommandConfig value. This configuration value is passed to the command -- fn -- as its third argument.

func (*CommandPropsBuilder) Func

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

func (*CommandPropsBuilder) Identifier

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

Identifier is a setter for a 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 a user input and returns an 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() as it takes HelpInput as its argument. Use *HelpInput and judge if an instruction should be returned to the user. e.g. .reboot command is only supported for administrator users in the admin group so this command should be hidden in other groups.

Also, see MatchFunc() for such an authentication mechanism.

func (*CommandPropsBuilder) MatchFunc

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

MatchFunc is a setter for a function to judge if an incoming Input "matches" the Command. When this returns true, this command is considered to "match the user input" and becomes a 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 a specific sender's specific message on a specific time range.

func (*CommandPropsBuilder) MatchPattern

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

MatchPattern is a setter to provide a command match pattern. This regular expression is used against the given Input to see if the Command matches the Input.

Use MatchFunc to set a more customizable matcher logic.

func (*CommandPropsBuilder) MustBuild

func (builder *CommandPropsBuilder) MustBuild() *CommandProps

MustBuild is like Build but panics if any error occurs on Build. It simplifies the initialization of a global variable holding the built CommandProps instance.

type CommandResponse

type CommandResponse struct {
	// Content represents a group of data returned to the user.
	// Since this is passed to Bot.SendMessage as part of OutputMessage,
	// its type may vary depending on the Bot's integrating chat service.
	Content interface{}

	// UserContext represents a user's contextual state to be stored.
	// When this is non-nil and a UserContextStorage is present for the Bot, this value is passed to UserContextStorage.
	// The user's next Input is fed to UserContext.Next so the user can continue the interaction until UserContext is no longer returned.
	UserContext *UserContext
}

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

func NewSuppressedResponseWithNext

func NewSuppressedResponseWithNext(next ContextualFunc) *CommandResponse

NewSuppressedResponseWithNext creates a new CommandResponse without a returning message but with a next step to continue. When this is returned by Command execution, no response is returned to the user but the user context is still set.

type Commands

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

Commands stashes all registered Command. A Bot implementation can refer to this to register a given command on Bot.AppendCommand call, and to find a matching Command on Bot.Respond call.

func NewCommands

func NewCommands() *Commands

NewCommands creates and returns new Commands instance.

func (*Commands) Append

func (commands *Commands) Append(command Command)

Append lets developers register a new Command to its internal stash. If another command is already registered with the same ID, the existing one is replaced in favor of the new one.

func (*Commands) ExecuteFirstMatched

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

ExecuteFirstMatched tries finding a matching command with the given Input and executes a Command if one is available.

func (*Commands) FindFirstMatched

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

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

The check for each Command is run in the order of registration; The earlier the Commands.Append is called, the earlier the check. Be sure to register an important Command first.

func (*Commands) Helps

func (commands *Commands) Helps(input *HelpInput) *CommandHelps

Helps returns all belonging commands' help messages in a form of *CommandHelps.

type Config

type Config struct {
	// TimeZone tells the scheduler in what timezone the application runs.
	TimeZone string `json:"timezone" yaml:"timezone"`
}

Config is a serializable struct that contains some configuration variables.

func NewConfig

func NewConfig() *Config

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

type ConfigNotFoundError

type ConfigNotFoundError struct {
	BotType BotType
	ID      string
}

ConfigNotFoundError is returned when a 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 defines an interface that all "watcher" implementations must satisfy. A watcher subscribes to any change on the configuration setting of Command or ScheduledTask. When a change is detected, ConfigWatcher calls the callback function to apply the change to the configuration values Command or ScheduledTask is referring to. One example could be watchers.fileWatcher that subscribes to configuration file changes; while another reference implementation -- https://github.com/oklahomer/go-sarah-githubconfig -- subscribes to changes on a given GitHub repository.

type ContextualFunc

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

ContextualFunc is a function's signature that declares the user's next step. When a function or instance method is given as UserContext.Next, Bot implementation must store that with Input.SenderKey to UserContextStorage. On the next user input, in Bot.Respond, Bot retrieves the stored ContextualFunc from UserContextStorage and executes this.

type DefaultBotOption

type DefaultBotOption func(bot *defaultBot)

DefaultBotOption defines a type that a functional option of NewBot must satisfy.

func BotWithStorage

func BotWithStorage(storage UserContextStorage) DefaultBotOption

BotWithStorage creates and returns a DefaultBotOption to register a preferred UserContextStorage implementation. The below example utilizes pre-defined in-memory storage.

config := sarah.NewCacheConfig()
configBuf, _ := os.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 a configuration with a default destination MUST satisfy. When no output destination is set with ScheduledTaskPropsBuilder.DefaultDestination, this value is taken as a default value on ScheduledTaskPropsBuilder.Build.

type HelpInput

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

HelpInput is a common Input implementation that represents a user's request for a help. When this type is given to Bot.Respond, a Bot implementation should list up registered Commands' instructions and send them back to the user.

func NewHelpInput

func NewHelpInput(input Input) *HelpInput

NewHelpInput creates a new instance of an Input implementation -- HelpInput -- with the given Input.

func (*HelpInput) Message

func (hi *HelpInput) Message() string

Message returns the stringified representation of the message.

func (*HelpInput) ReplyTo

func (hi *HelpInput) ReplyTo() OutputDestination

ReplyTo returns the sender's address or location to be used to reply a message.

func (*HelpInput) SenderKey

func (hi *HelpInput) SenderKey() string

SenderKey returns a stringified representation of the message sender.

func (*HelpInput) SentAt

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

SentAt returns the timestamp when the message is sent.

type Input

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

	// Message returns the stringified representation of the user input.
	// This may return an empty string when this Input implementation represents a non-text payload such as a 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 provides a timestamp only when a delayed message is delivered with the XEP-0203 protocol.
	SentAt() time.Time

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

Input defines an interface that each incoming message must satisfy. Every Bot/Adapter implementation must define one or customized Input implementations for the corresponding incoming messages.

It is Bot/Adapter's responsibility to receive input from the messaging user, convert it to Input, and see if the input represents a specific request: help and context cancellation. When the Input represents a request for help, then pass the Input to NewHelpInput to wrap it with a HelpInput; when the Input represents a request for a context cancellation, pass the Input to AbortInput to wrap it with AbortInput.

HelpInput and AbortInput can be passed to Sarah through the function given to Bot.Run -- func(Input) error -- just like any other Input. Sarah then passes a job to the worker.Worker implementation to execute Bot.Respond, in a panic-proof concurrent manner, with the given Input.

type Output

type Output interface {
	// Destination returns the destination the output is to be sent.
	Destination() OutputDestination

	// Content returns the sending payload.
	Content() interface{}
}

Output defines an interface that each outgoing message must satisfy.

func NewOutputMessage

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

NewOutputMessage creates a new instance of an Output implementation -- OutputMessage -- with the given OutputDestination and the payload.

type OutputDestination

type OutputDestination interface{}

OutputDestination defines an interface that represents a destination where the outgoing message is heading to, which actually is empty. Think of this as a kind of marker interface with a more meaningful name. Every Bot and Adapter implementation MUST define a struct to express the destination for the connecting chat service.

type OutputMessage

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

OutputMessage represents an outgoing message.

func (*OutputMessage) Content

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

Content returns a sending payload. Each Bot/Adapter must be capable of properly handling the payload and sending the message to the given destination.

func (*OutputMessage) Destination

func (output *OutputMessage) Destination() OutputDestination

Destination returns its destination in a form of OutputDestination. Each Bot/Adapter implementation must explicitly define an OutputDestination implementation so that Bot.SendMessage and Adapter.SendMessage can specify where the message should be directed to.

type ScheduledConfig

type ScheduledConfig interface {
	Schedule() string
}

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

type ScheduledTask

type ScheduledTask interface {
	// Identifier returns a unique id of this ScheduledTask.
	Identifier() string

	// Execute runs the scheduled task and returns the result in a form of slice.
	// When the task needs to send multiple payloads to multiple destinations, then return as many ScheduledTaskResult as the destinations.
	// Note that scheduled task may result in sending messages to multiple destinations.
	// e.g. Sending taking-out-trash alarm to #dady-chores room while sending go-to-school alarm to #daughter room.
	Execute(context.Context) ([]*ScheduledTaskResult, error)

	// DefaultDestination returns the default destination to send the result to.
	// When ScheduledTaskResult does not specify an output destination, Sarah falls back to use this value as a default.
	// If a default destination is nil, then the task execution is considered a failure.
	DefaultDestination() OutputDestination

	// Schedule returns the stringified representation of the execution schedule.
	// The schedule can be expressed in a crontab way with seconds precision such as "0 30 * * * *" but some variations are also available.
	// See https://pkg.go.dev/github.com/robfig/cron/v3 for details.
	Schedule() string
}

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

ScheduledTaskPropsBuilder and RegisterScheduledTaskProps to set up a ScheduledTask on the fly. That will give more flexibility such as the task rebuild feature on live configuration updates.

type ScheduledTaskProps

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

ScheduledTaskProps is a designated non-serializable configuration struct to be used for ScheduledTask construction. This holds a relatively complex set of ScheduledTask construction arguments and properties.

type ScheduledTaskPropsBuilder

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

ScheduledTaskPropsBuilder helps to construct a ScheduledTaskProps. A developer may set up a ScheduledTask property -- ScheduledTaskProps -- by calling ScheduledTaskPropsBuilder.Build or ScheduledTaskPropsBuilder.MustBuild at the end. A validation logic runs on build, so the returning ScheduledTaskProps instant is safe to be passed to RegisterScheduledTaskProps.

func NewScheduledTaskPropsBuilder

func NewScheduledTaskPropsBuilder() *ScheduledTaskPropsBuilder

NewScheduledTaskPropsBuilder creates and returns a new ScheduledTaskPropsBuilder instance.

func (*ScheduledTaskPropsBuilder) BotType

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

BotType is a setter to provide the belonging BotType.

func (*ScheduledTaskPropsBuilder) Build

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

Build builds new ScheduledTaskProps instance with the provided values.

func (*ScheduledTaskPropsBuilder) ConfigurableFunc

ConfigurableFunc sets a function for the ScheduledTask with a configuration value. The given configuration value -- config -- is passed to the function as a third argument.

When the resulting ScheduledTaskProps is passed to RegisterScheduledTask and Sarah runs with a ConfigWatcher, the configuration value is updated automatically when the corresponding setting is updated.

func (*ScheduledTaskPropsBuilder) DefaultDestination

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

DefaultDestination sets a default output destination of this task. OutputDestination returned as part of ScheduledTaskResult has higher priority; When none is specified by the result, then the default output destination is used.

func (*ScheduledTaskPropsBuilder) Func

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

func (*ScheduledTaskPropsBuilder) Identifier

Identifier is a setter for a ScheduledTask identifier.

func (*ScheduledTaskPropsBuilder) MustBuild

func (builder *ScheduledTaskPropsBuilder) MustBuild() *ScheduledTaskProps

MustBuild is like Build, but panics if any error occurs on Build. It simplifies the initialization of a global variable holding the built ScheduledTaskProps instance.

func (*ScheduledTaskPropsBuilder) Schedule

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

Schedule sets the execution schedule. The schedule can be expressed in a crontab way with seconds precision such as "0 30 * * * *" but some variations are also available. See https://pkg.go.dev/github.com/robfig/cron/v3 for details.

type ScheduledTaskResult

type ScheduledTaskResult struct {
	// Content represents a group of data to be sent as a result of task execution.
	// Since this is passed to Bot.SendMessage as part of OutputMessage,
	// its type may vary depending on the Bot's integrating chat service.
	Content interface{}

	// Destination is passed to Bot.SendMessage as part of OutputMessage value to specify the sending destination.
	// This typically contains a chat room, member id, or e-mail address.
	// e.g. JID of XMPP server/client.
	//
	// When this is nil, Sarah tries to fall back to a default destination given by ScheduledTask.
	// If no default destination is set, then the task execution is considered a failure.
	Destination OutputDestination
}

ScheduledTaskResult is a struct that ScheduledTask returns on its execution.

type SerializableArgument

type SerializableArgument struct {
	// FuncIdentifier is a unique identifier of the function to be executed on the next user input.
	// A developer needs to register a series of functions beforehand following the UserContextStorage implementation's instruction
	// so the matching function can be fetched by this identifier.
	FuncIdentifier string

	// Argument is an argument to be passed to the function fetched by FuncIdentifier.
	// Therefore, its type must be equal to the one the fetched function receives as an argument.
	Argument interface{}
}

SerializableArgument defines the user context data to be stored in external storage. UserContextStorage implementation receives this, serializes this, and stores this to external storage.

type Status

type Status struct {
	// Running indicates if Sarah is currently "running."
	// Sarah is considered running when Run is called and at least one of its belonging Bot is actively running.
	Running bool

	// Bots holds a list of BotStatus values where each value represents its corresponding Bot's status.
	Bots []BotStatus
}

Status represents the current status of Sarah and all registered Bots.

func CurrentStatus

func CurrentStatus() Status

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

  • One that sets up the bot configuration and calls Run.
  • Another that periodically calls CurrentStatus and monitors status. When Status.Running is false and Status.Bots field is empty, then the bot is not initiated yet.

type SupervisionDirective

type SupervisionDirective struct {
	// StopBot tells if Sarah needs to stop the failing bot and cleanup related resources.
	// When two or more bots are registered and at least one bot is to stay running after the failing bot stops,
	// internal workers and a scheduler keep running.
	//
	// When all bots stop, then Sarah 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 Sarah how to react to Bot's escalating error.

A designated supervisor function judges if the error represents a critical state when a bot escalates an error. When the bot is in a critical state, the function can return non-nil *SupervisionDirective to tell Sarah how to treat the current state. A customized supervisor function can be defined and registered via RegisterBotErrorSupervisor.

type TaskConfig

type TaskConfig interface{}

TaskConfig provides an interface that every task configuration must satisfy, which actually is empty. Think of this as a kind of marker interface with a more meaningful name.

type UserContext

type UserContext struct {
	// Next contains a function to be called on the next user input.
	// The 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 instances or can not be stored in external storage such as Redis.
	// To store the user context in external storage, set Serializable and use a UserContextStorage implementation that integrates with external storage.
	Next ContextualFunc

	// Serializable, on contrary to Next, contains a function identifier and its arguments to be stored in external storage.
	// When the user input is given next time, the serialized SerializableArgument is fetched from storage, deserialized,
	// and its arguments are fed to pre-registered function.
	// The 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, the user is considered "in the middle of a conversation," which means the next input of the user MUST be fed to a function declared by UserContext to continue the conversation. This conversational context has a higher priority than executing a Command found by checking Command.Match against the user's Input.

Currently, this structure supports two forms of context storage. One to store the context in the process memory space; another to store the serialized context in the external storage.

Set one of Next or Serializable depending on the usage and the UserContextStorage implementation.

func NewUserContext

func NewUserContext(next ContextualFunc) *UserContext

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

type UserContextStorage

type UserContextStorage interface {
	// Get searches for the user's stored state with the given user key, and return it if one is found.
	Get(string) (ContextualFunc, error)

	// Set stores the given UserContext.
	// The stored context is tied to the given key, which represents a particular user.
	Set(string, *UserContext) error

	// Delete removes a currently stored user's conversational context.
	// This does nothing if a corresponding context is not stored.
	Delete(string) error

	// Flush removes all stored UserContext values.
	Flush() error
}

UserContextStorage defines an interface of the Bot's storage mechanism to store the users' conversational contexts.

func NewUserContextStorage

func NewUserContextStorage(config *CacheConfig) UserContextStorage

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

Directories

Path Synopsis
Package alerter and its sub packages provide sarah.Alerter implementations to alert administrators when a sarah.Bot is in a critical state.
Package alerter and its sub packages provide sarah.Alerter implementations to alert administrators when a sarah.Bot is in a critical state.
line
Package line provides sarah.Alerter implementation for LINE Notify.
Package line provides sarah.Alerter implementation for LINE Notify.
Package gitter provides a sarah.Adapter implementation for Gitter integration.
Package gitter provides a sarah.Adapter implementation for Gitter integration.
Package slack provides a sarah.Adapter implementation for Slack.
Package slack provides a sarah.Adapter implementation for Slack.
Package watchers provides a sarah.ConfigWatcher implementation that subscribes to changes on the filesystem.
Package watchers provides a sarah.ConfigWatcher implementation that subscribes to changes on the filesystem.

Jump to

Keyboard shortcuts

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