Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify core interface to start bot interaction #75

Merged
merged 2 commits into from
May 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,32 +67,26 @@ func main() {
cacheConfig := sarah.NewCacheConfig()
storage := sarah.NewUserContextStorage(cacheConfig)

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

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

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

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

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

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

var GuessProps = sarah.NewCommandPropsBuilder().
Expand Down
33 changes: 21 additions & 12 deletions bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,41 @@ import (
"golang.org/x/net/context"
)

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

// Respond receives user input, look for corresponding command, execute it, and send result back to user if possible.
// Respond 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 message to destination depending on the Bot implementation.
// 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 user.
// sent back to the user.
AppendCommand(Command)

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

type defaultBot struct {
Expand Down Expand Up @@ -185,3 +192,5 @@ func NewSuppressedResponseWithNext(next ContextualFunc) *CommandResponse {
UserContext: NewUserContext(next),
}
}

type botRunner struct{}
4 changes: 3 additions & 1 deletion error.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package sarah

import "fmt"
import (
"fmt"
)

// 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.
Expand Down
57 changes: 12 additions & 45 deletions examples/simple/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"flag"
"github.com/oklahomer/go-sarah"
"github.com/oklahomer/go-sarah/alerter/line"
"github.com/oklahomer/go-sarah/examples/simple/plugins/count"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/count"
"github.com/oklahomer/go-sarah/examples/simple/plugins/echo"
"github.com/oklahomer/go-sarah/examples/simple/plugins/fixedtimer"
"github.com/oklahomer/go-sarah/examples/simple/plugins/guess"
"github.com/oklahomer/go-sarah/examples/simple/plugins/hello"
"github.com/oklahomer/go-sarah/examples/simple/plugins/morning"
"github.com/oklahomer/go-sarah/examples/simple/plugins/timer"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/fixedtimer"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/guess"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/hello"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/morning"
_ "github.com/oklahomer/go-sarah/examples/simple/plugins/timer"
"github.com/oklahomer/go-sarah/examples/simple/plugins/todo"
"github.com/oklahomer/go-sarah/log"
"github.com/oklahomer/go-sarah/slack"
Expand Down Expand Up @@ -43,7 +43,7 @@ func newMyConfig() *myConfig {
}

func main() {
var path = flag.String("config", "", "apth to apllication configuration file.")
var path = flag.String("config", "", "path to application configuration file.")
flag.Parse()
if *path == "" {
panic("./bin/examples -config=/path/to/config/app.yml")
Expand All @@ -55,13 +55,9 @@ func main() {
panic(err)
}

// A handy helper that holds arbitrary amount of RunnerOptions.
runnerOptions := sarah.NewRunnerOptions()

// When Bot encounters critical states, send alert to LINE.
// Any number of Alerter implementation can be registered.
alerter := line.New(config.LineAlerter)
runnerOptions.Append(sarah.WithAlerter(alerter))
sarah.RegisterAlerter(line.New(config.LineAlerter))

// Setup storage that can be shared among different Bot implementation.
storage := sarah.NewUserContextStorage(config.CacheConfig)
Expand All @@ -77,45 +73,19 @@ func main() {
slackBot.AppendCommand(todoCmd)

// Register bot to run.
runnerOptions.Append(sarah.WithBot(slackBot))

// Setup some plugins to build on the fly.
// Each configuration file, if exists, is subject to supervise.
// If updated, Command is re-built with new configuration.
runnerOptions.Append(sarah.WithCommandProps(hello.SlackProps))
runnerOptions.Append(sarah.WithCommandProps(morning.SlackProps))
runnerOptions.Append(sarah.WithCommandProps(count.SlackProps))
runnerOptions.Append(sarah.WithCommandProps(guess.SlackProps))

// Setup scheduled tasks.
// Each configuration file, if exists, is subject to supervise.
// If updated, Command is re-built with new configuration.
runnerOptions.Append(sarah.WithScheduledTaskProps(timer.SlackProps))
runnerOptions.Append(sarah.WithScheduledTaskProps(fixedtimer.SlackProps))
sarah.RegisterBot(slackBot)

// Directly add Command to Bot.
// This Command is not subject to config file supervision.
slackBot.AppendCommand(echo.Command)

// Setup sarah.Runner.
runner, err := sarah.NewRunner(config.Runner, runnerOptions.Arg())
// Run
ctx, cancel := context.WithCancel(context.Background())
err = sarah.Run(ctx, config.Runner)
if err != nil {
panic(err)
}

// Run sarah.Runner.
run(runner)
}

func run(runner sarah.Runner) {
ctx, cancel := context.WithCancel(context.Background())
runnerStop := make(chan struct{})
go func() {
// Blocks til all belonging Bots stop, or context is canceled.
runner.Run(ctx)
runnerStop <- struct{}{}
}()

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
signal.Notify(c, syscall.SIGTERM)
Expand All @@ -125,9 +95,6 @@ func run(runner sarah.Runner) {
log.Info("Stopping due to signal reception.")
cancel()

case <-runnerStop:
log.Error("Runner stopped.")

}
}

Expand Down
5 changes: 5 additions & 0 deletions examples/simple/plugins/count/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,8 @@ var GitterProps = sarah.NewCommandPropsBuilder().
return gitter.NewStringResponse(fmt.Sprint(globalCounter.increment())), nil
}).
MustBuild()

func init() {
sarah.RegisterCommandProps(SlackProps)
sarah.RegisterCommandProps(GitterProps)
}
4 changes: 4 additions & 0 deletions examples/simple/plugins/fixedtimer/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ var SlackProps = sarah.NewScheduledTaskPropsBuilder().
}).
Schedule("@every 1m").
MustBuild()

func init() {
sarah.RegisterScheduledTaskProps(SlackProps)
}
4 changes: 4 additions & 0 deletions examples/simple/plugins/guess/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ func guessFunc(_ context.Context, input sarah.Input, answer int) (*sarah.Command
return slack.NewStringResponseWithNext("Bigger!", retry), nil
}
}

func init() {
sarah.RegisterCommandProps(SlackProps)
}
4 changes: 4 additions & 0 deletions examples/simple/plugins/hello/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ var SlackProps = sarah.NewCommandPropsBuilder().
return slack.NewStringResponse("Hello, 世界"), nil
}).
MustBuild()

func init() {
sarah.RegisterCommandProps(SlackProps)
}
4 changes: 4 additions & 0 deletions examples/simple/plugins/morning/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ var SlackProps = sarah.NewCommandPropsBuilder().
return slack.NewStringResponse("Good morning."), nil
}).
MustBuild()

func init() {
sarah.RegisterCommandProps(SlackProps)
}
4 changes: 4 additions & 0 deletions examples/simple/plugins/timer/props.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ var SlackProps = sarah.NewScheduledTaskPropsBuilder().
}, nil
}).
MustBuild()

func init() {
sarah.RegisterScheduledTaskProps(SlackProps)
}
15 changes: 3 additions & 12 deletions examples/status/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,7 @@ import (
"runtime"
)

// statusGetter defines an interface that returns sarah.Status, which is satisfied by sarah.Runner.
// While the caller of setStatusHandler passes sarah.Runner directly,
// setStatusHandler receives it as a statusGetter interface so nothing nasty can be done against sarah.Runner.
type statusGetter interface {
Status() sarah.Status
}

var _ statusGetter = sarah.Runner(nil)

// setStatusHandler sets an endpoint that returns current status of sarah.Runner, its belonging sarah.Bots and sarah.Worker.
// setStatusHandler sets an endpoint that returns current status of go-sarah, its belonging sarah.Bot implementations and sarah.Worker.
//
// curl -s -XGET "http://localhost:8080/status" | jq .
// {
Expand Down Expand Up @@ -62,9 +53,9 @@ var _ statusGetter = sarah.Runner(nil)
// ]
// }
// }
func setStatusHandler(mux *http.ServeMux, sg statusGetter, ws *workerStats) {
func setStatusHandler(mux *http.ServeMux, ws *workerStats) {
mux.HandleFunc("/status", func(writer http.ResponseWriter, request *http.Request) {
runnerStatus := sg.Status()
runnerStatus := sarah.CurrentStatus()
systemStatus := &botSystemStatus{}
systemStatus.Running = runnerStatus.Running
for _, b := range runnerStatus.Bots {
Expand Down
21 changes: 7 additions & 14 deletions examples/status/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/*
Package main provides an example that uses Runner.Status()
to return current sarah.Runner and its belonging Bot 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.

In this example two bots, slack and nullBot, are registered to sarah.Runner and become subject to supervise.
In this example two bots, slack and nullBot, are registered to go-sarah and become subject to supervise.
See handler.go for Runner.Status() usage.
*/
package main
Expand Down Expand Up @@ -35,19 +34,16 @@ func main() {
panic(err)
}

// A handy struct that stores all sarah.RunnerOption to be passed to sarah.Runner
runnerOptions := sarah.NewRunnerOptions()

// Setup a bot
nullBot := &nullBot{}
runnerOptions.Append(sarah.WithBot(nullBot))
sarah.RegisterBot(nullBot)

// Setup another bot
slackBot, err := setupSlackBot(cfg)
if err != nil {
panic(err)
}
runnerOptions.Append(sarah.WithBot(slackBot))
sarah.RegisterBot(slackBot)

// Setup worker
workerReporter := &workerStats{}
Expand All @@ -56,19 +52,16 @@ func main() {
if err != nil {
panic(err)
}
runnerOptions.Append(sarah.WithWorker(worker))
sarah.RegisterWorker(worker)

// Setup a Runner to run and supervise above bots
runner, err := sarah.NewRunner(cfg.Runner, runnerOptions.Arg())
err = sarah.Run(ctx, cfg.Runner)
if err != nil {
panic(err)
}

// Run sarah.Runner
go runner.Run(ctx)

// Run HTTP server that reports current status
server := newServer(runner, workerReporter)
server := newServer(workerReporter)
go server.Run(ctx)

// Wait til signal reception
Expand Down
5 changes: 2 additions & 3 deletions examples/status/server_go17.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package main

import (
"github.com/oklahomer/go-sarah"
"golang.org/x/net/context"
"net/http"
)
Expand All @@ -12,9 +11,9 @@ type server struct {
sv *http.Server
}

func newServer(runner sarah.Runner, wsr *workerStats) *server {
func newServer(wsr *workerStats) *server {
mux := http.NewServeMux()
setStatusHandler(mux, runner, wsr)
setStatusHandler(mux, wsr)
return &server{
sv: &http.Server{Addr: ":8080", Handler: mux},
}
Expand Down
5 changes: 2 additions & 3 deletions examples/status/server_go18.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package main

import (
"context"
"github.com/oklahomer/go-sarah"
"github.com/oklahomer/go-sarah/log"
"net/http"
"runtime"
Expand All @@ -14,9 +13,9 @@ type server struct {
sv *http.Server
}

func newServer(runner sarah.Runner, wsr *workerStats) *server {
func newServer(wsr *workerStats) *server {
mux := http.NewServeMux()
setStatusHandler(mux, runner, wsr)
setStatusHandler(mux, wsr)
return &server{
sv: &http.Server{Addr: ":8080", Handler: mux},
}
Expand Down
Loading