Skip to content

Commit

Permalink
Merge pull request #129 from oklahomer/feature/update-document
Browse files Browse the repository at this point in the history
Update documentation
  • Loading branch information
oklahomer authored Dec 30, 2021
2 parents af6e976 + 53d406f commit 977f60c
Show file tree
Hide file tree
Showing 44 changed files with 861 additions and 754 deletions.
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,30 @@
# 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](https://github.com/oklahomer/go-sarah/wiki/Live-Configuration-Update)**_, _**[customizable alerting mechanism](https://github.com/oklahomer/go-sarah/wiki/Alerter)**_, _**automated [command](https://github.com/oklahomer/go-sarah/wiki/CommandPropsBuilder)\/[task](https://github.com/oklahomer/go-sarah/wiki/ScheduledTaskPropsBuilder) (re-)building**_, and _**[panic-proofed concurrent command/task execution](https://github.com/oklahomer/go-sarah/wiki/Worker)**_.
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](https://github.com/oklahomer/go-sarah/wiki/Live-Configuration-Update)**_, _**[customizable alerting mechanism](https://github.com/oklahomer/go-sarah/wiki/Alerter)**_, _**automated [command](https://github.com/oklahomer/go-sarah/wiki/CommandPropsBuilder)\/[task](https://github.com/oklahomer/go-sarah/wiki/ScheduledTaskPropsBuilder) (re)building**_, and _**[panic-proofed concurrent command/task execution](https://github.com/oklahomer/go-sarah/wiki/Worker)**_.

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](https://github.com/oklahomer/go-sarah/wiki/Default-Bot-and-Adapter) 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.
Thanks to such segmentalized lifecycle management architecture, the [adapter component](https://github.com/oklahomer/go-sarah/wiki/Default-Bot-and-Adapter) 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](https://github.com/oklahomer/go-sarah/wiki/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 at `github.com/oklahomer/go-kasumi`
- 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 change for Slack adapter was inevitable and that is the sole reason for this major version up.
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](https://github.com/oklahomer/go-sarah/wiki/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.
This version still supports and maintains all functionalities, while better interfaces for easier integration are introduced.
See [Migrating from v1.x to v2.x](https://github.com/oklahomer/go-sarah/wiki/Migrating-from-v1.x-to-v2.x) to migrate from the older version.

# Supported Chat Services/Protocols
Expand All @@ -47,12 +47,12 @@ some adapters are provided as reference implementations:
![hello world](/doc/img/hello.png)

Above is a general use of `go-sarah`.
Registered commands are checked against user input and matching one is executed;
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 bot command "**state-aware**."
The idea and implementation of "user's conversational context" is `go-sarah`'s signature feature that makes a bot command "**state-aware**."

![](/doc/img/todo_captioned.png)

Expand Down Expand Up @@ -83,16 +83,16 @@ import (
"syscall"

// Below packages register commands in their init().
// Importing with blank identifier will do the magic.
// Importing with a blank identifier will do the magic.
_ "guess"
_ "hello"
)

func main() {
// Setup Slack adapter
// Set up Slack adapter
setupSlack()

// Prepare go-sarah's core context.
// Prepare Sarah's core context.
ctx, cancel := context.WithCancel(context.Background())

// Run
Expand All @@ -102,7 +102,7 @@ func main() {
panic(fmt.Errorf("failed to run: %s", err.Error()))
}

// Stop when signal is sent.
// Stop when a signal is sent.
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM)
select {
Expand All @@ -113,19 +113,19 @@ func main() {
}

func setupSlack() {
// Setup slack adapter.
// 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()))
}

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

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

sarah.RegisterBot(bot)
Expand Down Expand Up @@ -159,31 +159,31 @@ var props = sarah.NewCommandPropsBuilder().
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.
// Generate an answer value at the very beginning.
rand.Seed(time.Now().UnixNano())
answer := rand.Intn(10)

// Let user guess the right answer.
// 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 user input right answer.
// 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 user inputs valid number.
// 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 guess is right, tell user and finish current user context.
// Otherwise let user input next guess with bit of a hint.
// 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 {
Expand Down Expand Up @@ -245,7 +245,7 @@ introduced in the later versions. However, not all projects can immediately swit
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.
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](https://github.com/oklahomer/go-sarah/blob/master/.travis.yml).
In other words, new features/interfaces introduced in 1.10 can be used in this project only after 1.12 is out.

Expand Down
26 changes: 12 additions & 14 deletions _examples/simple/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
/*
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.
package main

import (
Expand Down Expand Up @@ -35,7 +33,7 @@ type myConfig struct {
}

func newMyConfig() *myConfig {
// Use constructor for each config struct, so default values are pre-set.
// Use a constructor function for each config struct, so default values are pre-set.
return &myConfig{
CacheConfig: sarah.NewCacheConfig(),
Slack: slack.NewConfig(),
Expand All @@ -51,37 +49,37 @@ func main() {
panic("./bin/examples -config=/path/to/config/app.yml")
}

// Read configuration file.
// Read a configuration file.
config := readConfig(*path)

// When Bot encounters critical states, send alert to LINE.
// Any number of Alerter implementation can be registered.
// When the Bot encounters critical states, send an alert to LINE.
// Any number of Alerter implementations can be registered.
sarah.RegisterAlerter(line.New(config.LineAlerter))

// Setup storage that can be shared among different Bot implementation.
// Set up a storage that can be shared among different Bot implementations.
storage := sarah.NewUserContextStorage(config.CacheConfig)

// Setup Slack Bot.
// Set up Slack Bot.
setupSlack(config.Slack, storage)

// Setup some commands.
// Set up some commands.
todoCmd := todo.BuildCommand(&todo.DummyStorage{})
sarah.RegisterCommand(slack.SLACK, todoCmd)

// Directly add Command to Bot.
// This Command is not subject to config file supervision.
sarah.RegisterCommand(slack.SLACK, echo.Command)

// Prepare go-sarah's core context
// Prepare Sarah's core context.
ctx, cancel := context.WithCancel(context.Background())

// Prepare watcher that reads configuration from filesystem
// Prepare a watcher that reads configuration from filesystem.
if config.PluginConfigDir != "" {
configWatcher, _ := watchers.NewFileWatcher(ctx, config.PluginConfigDir)
sarah.RegisterConfigWatcher(configWatcher)
}

// Run
// Run.
err := sarah.Run(ctx, config.Runner)
if err != nil {
panic(err)
Expand Down Expand Up @@ -123,6 +121,6 @@ func setupSlack(config *slack.Config, storage sarah.UserContextStorage) {

bot := sarah.NewBot(adapter, sarah.BotWithStorage(storage))

// Register bot to run.
// Register the bot to run.
sarah.RegisterBot(bot)
}
12 changes: 5 additions & 7 deletions _examples/simple/plugins/count/props.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/*
Package count provides example code to setup sarah.CommandProps.
One counter instance is shared between two CommandPropsBuilder.Func,
which means resulting Slack/Gitter Commands access to same counter instance.
This illustrates that, when multiple Bots are registered to Runner, same memory space can be shared.
*/
// Package count provides an example to set up sarah.CommandProps.
//
// One counter instance is shared between two CommandPropsBuilder.Func,
// which means both Slack command and Gitter command access to the same counter instance.
// This illustrates that, when multiple Bots are registered to Runner, same memory space can be shared.
package count

import (
Expand Down
28 changes: 13 additions & 15 deletions _examples/simple/plugins/echo/command.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/*
Package echo provides example code to sarah.Command implementation.
CommandProps is a better way to provide set of command properties to Runner
especially when configuration file must be supervised and configuration struct needs to be updated on file update;
Developer may implement Command interface herself and feed its instance to Bot via Bot.AppendCommand
when command specification is simple.
*/
// Package echo provides an example of sarah.Command implementation.
//
// The use of sarah.CommandProps is a better way to provide a set of command properties to Sarah
// especially when a configuration file must be supervised and the configuration values need to be updated on file update.
// When such a configuration supervision is not required, a developer may implement sarah.Command interface herself
// and feed its instance to Sarah via sarah.RegisterCommand or to sarah.Bot via its AppendCommand method.
package echo

import (
Expand All @@ -25,23 +23,23 @@ func (c *command) Identifier() string {
return "echo"
}

// Execute receives user input and returns results of this Command.
// Execute receives a user input and returns a result of this Command execution.
func (c *command) Execute(_ context.Context, input sarah.Input) (*sarah.CommandResponse, error) {
return slack.NewResponse(input, sarah.StripMessage(matchPattern, input.Message()))
}

// Instruction provides input instruction for user.
// Instruction provides a guide for the requesting user.
func (c *command) Instruction(_ *sarah.HelpInput) string {
return ".echo foo"
}

// Match checks if user input matches to this Command.
// Match checks if the user input matches and this Command must be executed.
func (c *command) Match(input sarah.Input) bool {
// Once Runner receives input from Bot, it dispatches task to worker where multiple tasks may run in concurrent manner.
// Searching for corresponding Command is an important part of this task, which means Command.Match is called simultaneously from multiple goroutines.
// To avoid lock contention, Command developer should consider copying the *regexp.Regexp object.
// Once Sarah receives input from sarah.Bot, it dispatches a task to the worker where multiple tasks can run in a concurrent manner.
// Searching for a corresponding Command is an important part of this task, which means Command.Match is called simultaneously from multiple goroutines.
// To avoid a lock contention, Command developer should consider copying the *regexp.Regexp object.
return matchPattern.Copy().MatchString(input.Message())
}

// Command is a command instance that can directly fed to Bot.AppendCommand.
// Command is a command instance that can directly fed to sarah.RegisterCommand or Bot.AppendCommand.
var Command = &command{}
12 changes: 5 additions & 7 deletions _examples/simple/plugins/fixedtimer/props.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/*
Package fixedtimer provides example code to setup ScheduledTaskProps with fixed schedule.
The configuration struct, timerConfig, does not implement ScheduledConfig interface,
but instead fixed schedule is provided via ScheduledTaskPropsBuilder.Schedule.
Schedule never changes no matter how many times the configuration file, fixed_timer.yaml, is updated.
*/
// Package fixedtimer provides an example to set up sarah.ScheduledTaskProps with fixed schedule.
//
// The configuration struct, timerConfig, does not implement sarah.ScheduledConfig interface,
// but instead fixed schedule is provided via sarah.ScheduledTaskPropsBuilder's Schedule method.
// The schedule never changes no matter how many times the configuration file, fixed_timer.yaml, is updated.
package fixedtimer

import (
Expand Down
30 changes: 14 additions & 16 deletions _examples/simple/plugins/guess/props.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/*
Package guess provides example code to setup stateful command.
This command returns sarah.UserContext as part of sarah.CommandResponse until user inputs correct number.
As long as sarah.UserContext is returned, the next input from the same user is fed to the function defined in sarah.UserContext.
When user guesses right number or input .abort, the context is removed and user is free to input next desired command.
This example uses in-memory storage to store user context.
See https://github.com/oklahomer/go-sarah-rediscontext to use external storage.
*/
// Package guess provides an example to set up a stateful command.
//
// This command returns sarah.UserContext as part of sarah.CommandResponse until the user inputs a correct number.
// As long as sarah.UserContext is returned, the next input from the same user is fed to the function defined in sarah.UserContext.
// When the user guesses the right number or inputs .abort, the user context is removed and the user is free to input the next desired command.
//
// This example uses an in-memory storage to store user contexts.
// See https://github.com/oklahomer/go-sarah-rediscontext to use external storage.
package guess

import (
Expand All @@ -33,11 +31,11 @@ var SlackProps = sarah.NewCommandPropsBuilder().
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.
// Generate an answer value at the very beginning.
rand.Seed(time.Now().UnixNano())
answer := rand.Intn(10)

// Let user guess the right answer.
// Let the user guess the right answer.
return slack.NewResponse(
input,
"Input number.",
Expand All @@ -50,19 +48,19 @@ var SlackProps = sarah.NewCommandPropsBuilder().
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.
// For handiness, create a function that recursively calls guessFunc until the user inputs the right answer.
retry := func(c context.Context, i sarah.Input) (*sarah.CommandResponse, error) {
return guessFunc(c, i, answer)
}

// See if user inputs valid number.
// 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), slack.RespAsThreadReply(true))
}

// If guess is right, tell user and finish current user context.
// Otherwise let user input next guess with bit of a hint.
// If the guess is right, tell the user and finish the current user context.
// Otherwise, let the user input the next guess with a bit of a hint.
if guess == answer {
return slack.NewResponse(input, "Correct!", slack.RespAsThreadReply(true))
} else if guess > answer {
Expand Down
17 changes: 5 additions & 12 deletions _examples/simple/plugins/hello/props.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/*
Package hello provides example code to setup relatively simple sarah.CommandProps.
In this example, instead of simply assigning regular expression to CommandPropsBuilder.MatchPattern,
a function is set via CommandPropsBuilder.MatchFunc to do the equivalent task.
With CommandPropsBuilder.MatchFunc, developers may define more complex matching logic than assigning simple regular expression to CommandPropsBuilder.MatchPattern.
One more benefit is that strings package or other packages with higher performance can be used internally like this example.
This sarah.CommandProps can be fed to sarah.NewRunner() as below.
runner, err := sarah.NewRunner(config.Runner, sarah.WithCommandProps(hello.SlackProps), ... )
*/
// Package hello provides an example to set up a relatively simple sarah.CommandProps.
//
// In this example, instead of simply assigning a regular expression to CommandPropsBuilder.MatchPattern,
// a function with a more complex matching logic is set via CommandPropsBuilder.MatchFunc to do the equivalent task.
// One more benefit of using CommandPropsBuilder.MatchFunc is that strings package or other packages with higher performance can be used internally like this example.
package hello

import (
Expand Down
Loading

0 comments on commit 977f60c

Please sign in to comment.