mirror of https://github.com/deuill/informbot.git
193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
package joe
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/go-joe/joe/reactions"
|
|
"github.com/pkg/errors"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// An Adapter connects the bot with the chat by enabling it to receive and send
|
|
// messages. Additionally advanced adapters can emit more events than just the
|
|
// ReceiveMessageEvent (e.g. the slack adapter also emits the UserTypingEvent).
|
|
// All adapter events must be setup in the RegisterAt function of the Adapter.
|
|
//
|
|
// Joe provides a default CLIAdapter implementation which connects the bot with
|
|
// the local shell to receive messages from stdin and print messages to stdout.
|
|
type Adapter interface {
|
|
RegisterAt(*Brain)
|
|
Send(text, channel string) error
|
|
Close() error
|
|
}
|
|
|
|
// ReactionAwareAdapter is an optional interface that Adapters can implement if
|
|
// they support reacting to messages with emojis.
|
|
type ReactionAwareAdapter interface {
|
|
React(reactions.Reaction, Message) error
|
|
}
|
|
|
|
// The CLIAdapter is the default Adapter implementation that the bot uses if no
|
|
// other adapter was configured. It emits a ReceiveMessageEvent for each line it
|
|
// receives from stdin and prints all sent messages to stdout.
|
|
//
|
|
// The CLIAdapter does not set the Message.Data field.
|
|
type CLIAdapter struct {
|
|
Prefix string
|
|
Input io.ReadCloser
|
|
Output io.Writer
|
|
Logger *zap.Logger
|
|
Author string // used to set the author of the messages, defaults to os.Getenv("USER)
|
|
mu sync.Mutex // protects the Output and closing channel
|
|
closing chan chan error
|
|
}
|
|
|
|
// NewCLIAdapter creates a new CLIAdapter. The caller must call Close
|
|
// to make the CLIAdapter stop reading messages and emitting events.
|
|
func NewCLIAdapter(name string, logger *zap.Logger) *CLIAdapter {
|
|
return &CLIAdapter{
|
|
Prefix: fmt.Sprintf("%s > ", name),
|
|
Input: os.Stdin,
|
|
Output: os.Stdout,
|
|
Logger: logger,
|
|
Author: os.Getenv("USER"),
|
|
closing: make(chan chan error),
|
|
}
|
|
}
|
|
|
|
// RegisterAt starts the CLIAdapter by reading messages from stdin and emitting
|
|
// a ReceiveMessageEvent for each of them. Additionally the adapter hooks into
|
|
// the InitEvent to print a nice prefix to stdout to show to the user it is
|
|
// ready to accept input.
|
|
func (a *CLIAdapter) RegisterAt(brain *Brain) {
|
|
brain.RegisterHandler(func(evt InitEvent) {
|
|
_ = a.print(a.Prefix)
|
|
})
|
|
|
|
go a.loop(brain)
|
|
}
|
|
|
|
func (a *CLIAdapter) loop(brain *Brain) {
|
|
input := a.readLines()
|
|
|
|
// The adapter loop is built to stay responsive even if the Brain stops
|
|
// processing events so we can safely close the CLIAdapter.
|
|
//
|
|
// We want to print the prefix each time when the Brain has completely
|
|
// processed a ReceiveMessageEvent and before we are emitting the next one.
|
|
// This gives us a shell-like behavior which signals to the user that she
|
|
// can input more data on the CLI. This channel is buffered so we do not
|
|
// block the Brain when it executes the callback.
|
|
callback := make(chan Event, 1)
|
|
callbackFun := func(evt Event) {
|
|
callback <- evt
|
|
}
|
|
|
|
var lines = input // channel represents the case that we receive a new message
|
|
|
|
for {
|
|
select {
|
|
case msg, ok := <-lines:
|
|
if !ok {
|
|
// no more input from stdin
|
|
lines = nil // disable this case and wait for closing signal
|
|
continue
|
|
}
|
|
|
|
lines = nil // disable this case and wait for the callback
|
|
brain.Emit(ReceiveMessageEvent{Text: msg, AuthorID: a.Author}, callbackFun)
|
|
|
|
case <-callback:
|
|
// This case is executed after all ReceiveMessageEvent handlers have
|
|
// completed and we can continue with the next line.
|
|
_ = a.print(a.Prefix)
|
|
lines = input // activate first case again
|
|
|
|
case result := <-a.closing:
|
|
if lines == nil {
|
|
// We were just waiting for our callback
|
|
_ = a.print(a.Prefix)
|
|
}
|
|
|
|
_ = a.print("\n")
|
|
result <- a.Input.Close()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// ReadLines reads lines from stdin and returns them in a channel.
|
|
// All strings in the returned channel will not include the trailing newline.
|
|
// The channel is closed automatically when a.Input is closed.
|
|
func (a *CLIAdapter) readLines() <-chan string {
|
|
r := bufio.NewReader(a.Input)
|
|
lines := make(chan string)
|
|
go func() {
|
|
// This goroutine will exit when we call a.Input.Close() which will make
|
|
// r.ReadString(…) return an io.EOF.
|
|
for {
|
|
line, err := r.ReadString('\n')
|
|
switch {
|
|
case err == io.EOF:
|
|
close(lines)
|
|
return
|
|
case err != nil:
|
|
a.Logger.Error("Failed to read messages from input", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
lines <- line[:len(line)-1]
|
|
}
|
|
}()
|
|
|
|
return lines
|
|
}
|
|
|
|
// Send implements the Adapter interface by sending the given text to stdout.
|
|
// The channel argument is required by the Adapter interface but is otherwise ignored.
|
|
func (a *CLIAdapter) Send(text, channel string) error {
|
|
return a.print(text + "\n")
|
|
}
|
|
|
|
// React implements the optional ReactionAwareAdapter interface by simply
|
|
// printing the given reaction as UTF8 emoji to the CLI.
|
|
func (a *CLIAdapter) React(r reactions.Reaction, _ Message) error {
|
|
return a.print(r.String() + "\n")
|
|
}
|
|
|
|
// Close makes the CLIAdapter stop emitting any new events or printing any output.
|
|
// Calling this function more than once will result in an error.
|
|
func (a *CLIAdapter) Close() error {
|
|
if a.closing == nil {
|
|
return errors.Errorf("already closed")
|
|
}
|
|
|
|
a.Logger.Debug("Closing CLIAdapter")
|
|
callback := make(chan error)
|
|
a.closing <- callback
|
|
err := <-callback
|
|
|
|
// Mark CLIAdapter as closed by setting its closing channel to nil.
|
|
// This will prevent any more output to be printed after this function returns.
|
|
a.mu.Lock()
|
|
a.closing = nil
|
|
a.mu.Unlock()
|
|
|
|
return err
|
|
}
|
|
|
|
func (a *CLIAdapter) print(msg string) error {
|
|
a.mu.Lock()
|
|
if a.closing == nil {
|
|
return errors.New("adapter is closed")
|
|
}
|
|
_, err := fmt.Fprint(a.Output, msg)
|
|
a.mu.Unlock()
|
|
|
|
return err
|
|
}
|