644 lines
18 KiB
Go
644 lines
18 KiB
Go
// Copyright (c) 2016 Tristan Colgate-McFarlane
|
|
//
|
|
// This file is part of hugot.
|
|
//
|
|
// hugot is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// hugot is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with hugot. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package hugot
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
|
|
"context"
|
|
|
|
"github.com/golang/glog"
|
|
"github.com/mattn/go-shellwords"
|
|
)
|
|
|
|
var (
|
|
// ErrSkipHears is used by Command handlers to indicate they have
|
|
// handled a mesage and that any following Hear handlers should be
|
|
// skipped.
|
|
ErrSkipHears = errors.New("skip hear messages")
|
|
|
|
// ErrUnknownCommand is returned by a command mux if the command did
|
|
// not match any of it's registered handlers.
|
|
ErrUnknownCommand = errors.New("unknown command")
|
|
|
|
// ErrBadCLI implies that we could not process this message as a
|
|
// command line. E.g. due to potentially mismatched quoting or bad
|
|
// escaping.
|
|
ErrBadCLI = errors.New("could not process as command line")
|
|
)
|
|
|
|
// ErrUsage indicates that Command handler was used incorrectly. The
|
|
// string returned is a usage message generated by a call to -help
|
|
// for this command
|
|
type ErrUsage struct {
|
|
string
|
|
}
|
|
|
|
// Error implements the Error interface for an ErrUsage.
|
|
func (e ErrUsage) Error() string {
|
|
return e.string
|
|
}
|
|
|
|
type errNextCommand struct {
|
|
ctx context.Context
|
|
}
|
|
|
|
func (e errNextCommand) Error() string {
|
|
return "use next command"
|
|
}
|
|
|
|
// ErrNextCommand is returned if the command wishes the message
|
|
// to be passed to one of the sub-ommands of a CommandSet. The
|
|
// ctx will be passed through to the next CommandHandler
|
|
func ErrNextCommand(ctx context.Context) error {
|
|
return errNextCommand{ctx}
|
|
}
|
|
|
|
// Describer returns the name and description of a handler. This
|
|
// is used to identify the handler within Command and HTTP Muxs,
|
|
// and to provide a descriptive name for the handler in help text.
|
|
type Describer interface {
|
|
Describe() (string, string)
|
|
}
|
|
|
|
// Handler is a handler with no actual functionality
|
|
type Handler interface {
|
|
Describer
|
|
}
|
|
|
|
// ResponseWriter is used to Send messages back to a user.
|
|
type ResponseWriter interface {
|
|
Sender
|
|
io.Writer
|
|
|
|
SetChannel(c string) // Forces messages to a certain channel
|
|
SetTo(to string) // Forces messages to a certain user
|
|
SetSender(a Sender) // Forces messages to a different sender or adapter
|
|
|
|
Copy() ResponseWriter // Returns a copy of this response writer
|
|
}
|
|
|
|
type responseWriter struct {
|
|
snd Sender
|
|
msg Message
|
|
an string
|
|
}
|
|
|
|
func newResponseWriter(s Sender, m Message, an string) ResponseWriter {
|
|
return &responseWriter{s, m, an}
|
|
}
|
|
|
|
// ResponseWriterFromContext constructs a ResponseWriter from the adapter
|
|
// stored in the context. A destination Channel/User must be set to send
|
|
// messages..
|
|
func ResponseWriterFromContext(ctx context.Context) (ResponseWriter, bool) {
|
|
s, ok := SenderFromContext(ctx)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
an := fmt.Sprintf("%T", s)
|
|
return newResponseWriter(s, Message{}, an), true
|
|
}
|
|
|
|
// Write implements the io.Writer interface. All writes create a single
|
|
// new message that is then sent to the ResoneWriter's current adapter
|
|
func (w *responseWriter) Write(bs []byte) (int, error) {
|
|
nmsg := w.msg
|
|
nmsg.Text = string(bs)
|
|
w.Send(context.TODO(), &nmsg)
|
|
return len(bs), nil
|
|
}
|
|
|
|
// SetChannel sets the outbound channel for message sent via this writer
|
|
func (w *responseWriter) SetChannel(s string) {
|
|
w.msg.Channel = s
|
|
}
|
|
|
|
// SetChannel sets the target user for message sent via this writer
|
|
func (w *responseWriter) SetTo(s string) {
|
|
w.msg.To = s
|
|
}
|
|
|
|
// SetSender sets the target adapter for sender sent via this writer
|
|
func (w *responseWriter) SetSender(s Sender) {
|
|
w.snd = s
|
|
}
|
|
|
|
// Send implements the Sender interface
|
|
func (w *responseWriter) Send(ctx context.Context, m *Message) {
|
|
messagesTx.WithLabelValues(w.an, m.Channel, m.From).Inc()
|
|
w.snd.Send(ctx, m)
|
|
}
|
|
|
|
// Copy returns a copy of this response writer
|
|
func (w *responseWriter) Copy() ResponseWriter {
|
|
return &responseWriter{w.snd, Message{}, w.an}
|
|
}
|
|
|
|
// nullSender is a sender which discards anything sent to it, this is
|
|
// useful for the help handler.
|
|
type nullSender struct {
|
|
}
|
|
|
|
// Send implements Send, and discards the message
|
|
func (nullSender) Send(ctx context.Context, m *Message) {
|
|
}
|
|
|
|
// NewNullResponseWriter creates a ResponseWriter that discards all
|
|
// message sent to it.
|
|
func NewNullResponseWriter(m Message) ResponseWriter {
|
|
return newResponseWriter(nullSender{}, m, "null")
|
|
}
|
|
|
|
type baseHandler struct {
|
|
name string
|
|
desc string
|
|
}
|
|
|
|
func (bh *baseHandler) Describe() (string, string) {
|
|
return bh.name, bh.desc
|
|
}
|
|
|
|
func newBaseHandler(name, desc string) Handler {
|
|
return &baseHandler{name, desc}
|
|
}
|
|
|
|
// RawHandler will recieve every message sent to the handler, without
|
|
// any filtering.
|
|
type RawHandler interface {
|
|
Handler
|
|
ProcessMessage(ctx context.Context, w ResponseWriter, m *Message) error
|
|
}
|
|
|
|
type baseRawHandler struct {
|
|
Handler
|
|
rhf RawFunc
|
|
}
|
|
|
|
// RawFunc describes the calling convention for RawHandler. m is the
|
|
// incoming message. Responses can be written to w.
|
|
type RawFunc func(ctx context.Context, w ResponseWriter, m *Message) error
|
|
|
|
// NewRawHandler will wrap the function f as a RawHandler with the name
|
|
// and description provided
|
|
func NewRawHandler(name, desc string, f RawFunc) RawHandler {
|
|
return &baseRawHandler{
|
|
Handler: newBaseHandler(name, desc),
|
|
rhf: f,
|
|
}
|
|
}
|
|
|
|
func (brh *baseRawHandler) ProcessMessage(ctx context.Context, w ResponseWriter, m *Message) error {
|
|
return brh.rhf(ctx, w, m)
|
|
}
|
|
|
|
// BackgroundHandler gets run when the bot starts listening. They are
|
|
// intended for publishing messages that are not in response to any
|
|
// specific incoming message.
|
|
type BackgroundHandler interface {
|
|
Handler
|
|
StartBackground(ctx context.Context, w ResponseWriter)
|
|
}
|
|
|
|
type baseBackgroundHandler struct {
|
|
Handler
|
|
bhf BackgroundFunc
|
|
}
|
|
|
|
// BackgroundFunc describes the calling convention for Background handlers
|
|
type BackgroundFunc func(ctx context.Context, w ResponseWriter)
|
|
|
|
// NewBackgroundHandler wraps f up as a BackgroundHandler with the name and
|
|
// description provided.
|
|
func NewBackgroundHandler(name, desc string, f BackgroundFunc) BackgroundHandler {
|
|
return &baseBackgroundHandler{
|
|
Handler: newBaseHandler(name, desc),
|
|
bhf: f,
|
|
}
|
|
}
|
|
|
|
func (bbh *baseBackgroundHandler) StartBackground(ctx context.Context, w ResponseWriter) {
|
|
bbh.bhf(ctx, w)
|
|
}
|
|
|
|
// HeardFunc describes the calling convention for a Hears handler.
|
|
type HeardFunc func(ctx context.Context, w ResponseWriter, m *Message, submatches [][]string) // Called once a message matches, and is passed any submatches from the regexp capture groups
|
|
|
|
// HearsHandler is a handler which responds to messages matching a specific
|
|
// pattern
|
|
type HearsHandler interface {
|
|
Handler
|
|
Hears() *regexp.Regexp // Returns the regexp we want to hear
|
|
Heard(ctx context.Context, w ResponseWriter, m *Message, submatches [][]string) // Called once a message matches, and is passed any submatches from the regexp capture groups
|
|
}
|
|
|
|
type baseHearsHandler struct {
|
|
Handler
|
|
rgxp *regexp.Regexp
|
|
hhf HeardFunc
|
|
}
|
|
|
|
// NewHearsHandler wraps f as a Hears handler that reponnds to the regexp provided, with the given name a description
|
|
func NewHearsHandler(name, desc string, rgxp *regexp.Regexp, f HeardFunc) HearsHandler {
|
|
return &baseHearsHandler{
|
|
Handler: newBaseHandler(name, desc),
|
|
rgxp: rgxp,
|
|
hhf: f,
|
|
}
|
|
}
|
|
|
|
func (bhh *baseHearsHandler) Hears() *regexp.Regexp {
|
|
return bhh.rgxp
|
|
}
|
|
|
|
func (bhh *baseHearsHandler) Heard(ctx context.Context, w ResponseWriter, m *Message, submatches [][]string) {
|
|
bhh.hhf(ctx, w, m, submatches)
|
|
}
|
|
|
|
// CommandFunc describes the calling convention for CommandHandler
|
|
type CommandFunc func(ctx context.Context, w ResponseWriter, m *Message) error
|
|
|
|
// CommandHandler handlers are used to implement CLI style commands. Before the
|
|
// Command method is called, the in the incoming message m will have the Text
|
|
// of the message parsed into invidual strings, accouting for quoting.
|
|
// m.Args(0) will be the name of the command as the handler was called, as per
|
|
// os.Args(). Command should add any requires falgs to m and then call m.Parse()
|
|
// ErrNextCommand(ctx) can be returned to inform the command mux to hand the resulting
|
|
// Args to any known sub CommandHandler.
|
|
type CommandHandler interface {
|
|
Handler
|
|
Command(ctx context.Context, w ResponseWriter, m *Message) error
|
|
}
|
|
|
|
// CommandWithSubsHandler should be implemented by any command that includes
|
|
// sub commands.
|
|
type CommandWithSubsHandler interface {
|
|
CommandHandler
|
|
SubCommands() *CommandSet // List the supported sub-commands
|
|
}
|
|
|
|
// CommandSet assists with supporting command handlers with sub-commands.
|
|
type CommandSet map[string]CommandHandler
|
|
|
|
// NewCommandSet creates an empty commands set.
|
|
func NewCommandSet() *CommandSet {
|
|
cs := make(CommandSet)
|
|
return &cs
|
|
}
|
|
|
|
// AddCommandHandler adds a CommandHandler to a CommandSet
|
|
func (cs *CommandSet) AddCommandHandler(c CommandHandler) {
|
|
n, _ := c.Describe()
|
|
|
|
(*cs)[n] = c
|
|
}
|
|
|
|
type byAlpha struct {
|
|
ns []string
|
|
ds []string
|
|
chs []CommandHandler
|
|
}
|
|
|
|
func (b *byAlpha) Len() int { return len(b.ns) }
|
|
func (b *byAlpha) Less(i, j int) bool { return b.ns[i] < b.ns[j] }
|
|
func (b *byAlpha) Swap(i, j int) {
|
|
b.ns[i], b.ns[j] = b.ns[j], b.ns[i]
|
|
b.ds[i], b.ds[j] = b.ns[j], b.ds[i]
|
|
b.chs[i], b.chs[j] = b.chs[j], b.chs[i]
|
|
}
|
|
|
|
// List returns the names and usage of the subcommands of
|
|
// a CommandSet.
|
|
func (cs *CommandSet) List() ([]string, []string, []CommandHandler) {
|
|
cmds := []string{}
|
|
descs := []string{}
|
|
chs := []CommandHandler{}
|
|
hasHelp := false
|
|
|
|
for _, ch := range *cs {
|
|
n, d := ch.Describe()
|
|
if n == "help" {
|
|
hasHelp = true
|
|
continue
|
|
}
|
|
cmds = append(cmds, n)
|
|
descs = append(descs, d)
|
|
chs = append(chs, ch)
|
|
}
|
|
|
|
sorted := &byAlpha{cmds, descs, chs}
|
|
sort.Sort(sorted)
|
|
if hasHelp {
|
|
hh := (*cs)["help"]
|
|
_, hd := hh.Describe()
|
|
sorted.ns = append([]string{"help"}, sorted.ns...)
|
|
sorted.ds = append([]string{hd}, sorted.ds...)
|
|
sorted.chs = append([]CommandHandler{hh}, sorted.chs...)
|
|
}
|
|
|
|
return sorted.ns, sorted.ds, sorted.chs
|
|
}
|
|
|
|
// NextCommand picks the next commands to run from this command set based on the content
|
|
// of the message
|
|
func (cs *CommandSet) NextCommand(ctx context.Context, w ResponseWriter, m *Message) error {
|
|
var err error
|
|
|
|
// This is repeated from RunCommandHandler, probably something wrong there
|
|
if m.args == nil {
|
|
m.args, err = shellwords.Parse(m.Text)
|
|
if err != nil {
|
|
return ErrBadCLI
|
|
}
|
|
}
|
|
if len(m.args) == 0 {
|
|
cmds, _, _ := cs.List()
|
|
return fmt.Errorf("required sub-command missing: %s", strings.Join(cmds, ", "))
|
|
}
|
|
|
|
matches := []CommandHandler{}
|
|
matchesns := []string{}
|
|
ematches := []CommandHandler{}
|
|
for name, cmd := range *cs {
|
|
if strings.HasPrefix(name, m.args[0]) {
|
|
matches = append(matches, cmd)
|
|
matchesns = append(matchesns, name)
|
|
}
|
|
if name == m.args[0] {
|
|
ematches = append(ematches, cmd)
|
|
}
|
|
}
|
|
if len(matches) == 0 && len(ematches) == 0 {
|
|
return ErrUnknownCommand
|
|
}
|
|
if len(ematches) > 1 {
|
|
return fmt.Errorf("multiple exact matches for %s", m.args[0])
|
|
}
|
|
if len(ematches) == 1 {
|
|
return runCommandHandler(ctx, ematches[0], w, m)
|
|
}
|
|
if len(matches) == 1 {
|
|
return runCommandHandler(ctx, matches[0], w, m)
|
|
}
|
|
return fmt.Errorf("ambigious command, %s: %s", m.args[0], strings.Join(matchesns, ", "))
|
|
}
|
|
|
|
type baseCommandHandler struct {
|
|
Handler
|
|
bcf CommandFunc
|
|
subs *CommandSet
|
|
}
|
|
|
|
func defaultCommandHandler(ctx context.Context, w ResponseWriter, m *Message) error {
|
|
if err := m.Parse(); err != nil {
|
|
return err
|
|
}
|
|
return ErrNextCommand(ctx)
|
|
}
|
|
|
|
// NewCommandHandler wraps the given function f as a CommandHandler with the
|
|
// provided name and description.
|
|
func NewCommandHandler(name, desc string, f CommandFunc, cs *CommandSet) CommandWithSubsHandler {
|
|
if f == nil {
|
|
f = defaultCommandHandler
|
|
}
|
|
return &baseCommandHandler{
|
|
Handler: newBaseHandler(name, desc),
|
|
bcf: f,
|
|
subs: cs,
|
|
}
|
|
}
|
|
|
|
func (bch *baseCommandHandler) Command(ctx context.Context, w ResponseWriter, m *Message) error {
|
|
var errnc errNextCommand
|
|
var ok bool
|
|
err := bch.bcf(ctx, w, m)
|
|
if errnc, ok = err.(errNextCommand); !ok {
|
|
return err
|
|
}
|
|
return bch.subs.NextCommand(errnc.ctx, w, m)
|
|
}
|
|
|
|
func (bch *baseCommandHandler) SubCommands() *CommandSet {
|
|
return bch.subs
|
|
}
|
|
|
|
// WebHookHandler handlers are used to expose a registered handler via a web server.
|
|
// The SetURL method is called to inform the handler what it's external URL will be.
|
|
// This will normally be done by the Mux. Other handlers can use URL to generate
|
|
// links suitable for external use.
|
|
// You can use the http.Handler Request.Context() to get a ResponseWriter to write
|
|
// into the bots adapters. You need to SetChannel the resulting ResponseWriter to
|
|
// send messages.
|
|
type WebHookHandler interface {
|
|
Handler
|
|
URL() *url.URL // Is called to retrieve the location of the Handler
|
|
SetURL(*url.URL) // Is called after the WebHook is added, to inform it where it lives
|
|
SetAdapter(Adapter) // Is called to set the default adapter for this handler to use
|
|
http.Handler
|
|
}
|
|
|
|
// WebHookHandlerFunc describes the called convention for a WebHook.
|
|
type WebHookHandlerFunc func(ctx context.Context, hw ResponseWriter, w http.ResponseWriter, r *http.Request)
|
|
|
|
type baseWebHookHandler struct {
|
|
ctx context.Context
|
|
a Adapter
|
|
Handler
|
|
hf http.HandlerFunc
|
|
url *url.URL
|
|
}
|
|
|
|
// ServeHTTP implement the http.Handler interface for a baseWebHandler
|
|
func (bwhh *baseWebHookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
ctx = NewAdapterContext(ctx, bwhh.a)
|
|
r = r.WithContext(ctx)
|
|
|
|
bwhh.hf(w, r)
|
|
}
|
|
|
|
// NewWebHookHandler creates a new WebHookHandler provided name and description.
|
|
func NewWebHookHandler(name, desc string, hf http.HandlerFunc) WebHookHandler {
|
|
return &baseWebHookHandler{
|
|
Handler: newBaseHandler(name, desc),
|
|
url: &url.URL{},
|
|
hf: hf,
|
|
}
|
|
}
|
|
|
|
func (bwhh *baseWebHookHandler) SetURL(u *url.URL) {
|
|
if glog.V(2) {
|
|
glog.Infof("SetURL to %s", *u)
|
|
}
|
|
bwhh.url = u
|
|
}
|
|
|
|
func (bwhh *baseWebHookHandler) URL() *url.URL {
|
|
return bwhh.url
|
|
}
|
|
|
|
func (bwhh *baseWebHookHandler) SetAdapter(a Adapter) {
|
|
if glog.V(3) {
|
|
glog.Infof("WebHander adapter set to %#v", a)
|
|
}
|
|
bwhh.a = a
|
|
}
|
|
|
|
func glogPanic() {
|
|
err := recover()
|
|
if err != nil && err != flag.ErrHelp {
|
|
glog.Error(err)
|
|
glog.Error(string(debug.Stack()))
|
|
}
|
|
}
|
|
|
|
// Loop processes messages from adapters a and as, and passes them
|
|
// to the provided handler h. ctx can be used to stop the processesing
|
|
// and inform any running handlers. WebHookHandlers and BackgroundHandlers
|
|
// will be configured to use a as the default handler
|
|
func Loop(ctx context.Context, h Handler, a Adapter, as ...Adapter) {
|
|
an := fmt.Sprintf("%T", a)
|
|
if bh, ok := h.(BackgroundHandler); ok {
|
|
runBackgroundHandler(ctx, bh, newResponseWriter(a, Message{}, an))
|
|
}
|
|
|
|
if wh, ok := h.(WebHookHandler); ok {
|
|
wh.SetAdapter(a)
|
|
}
|
|
|
|
type smrw struct {
|
|
w ResponseWriter
|
|
m *Message
|
|
}
|
|
mrws := make(chan smrw)
|
|
|
|
for _, a := range append(as, a) {
|
|
go func(a Adapter) {
|
|
an := fmt.Sprintf("%T", a)
|
|
for {
|
|
select {
|
|
case m := <-a.Receive():
|
|
rw := newResponseWriter(a, *m, an)
|
|
mrws <- smrw{rw, m}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}(a)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case mrw := <-mrws:
|
|
if glog.V(3) {
|
|
glog.Infof("Message: %#v", *mrw.m)
|
|
}
|
|
messagesRx.WithLabelValues(an, mrw.m.Channel, mrw.m.From).Inc()
|
|
|
|
if rh, ok := h.(RawHandler); ok {
|
|
go runRawHandler(ctx, rh, mrw.w, mrw.m)
|
|
}
|
|
|
|
if hh, ok := h.(HearsHandler); ok {
|
|
go runHearsHandler(ctx, hh, mrw.w, mrw.m)
|
|
}
|
|
|
|
if ch, ok := h.(CommandHandler); ok {
|
|
go runCommandHandler(ctx, ch, mrw.w, mrw.m)
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// runBackgroundHandler starts the provided BackgroundHandler in a new
|
|
// go routine.
|
|
func runBackgroundHandler(ctx context.Context, h BackgroundHandler, w ResponseWriter) {
|
|
glog.Infof("Starting background %v\n", h)
|
|
go func(ctx context.Context, bh BackgroundHandler) {
|
|
defer glogPanic()
|
|
h.StartBackground(ctx, w)
|
|
}(ctx, h)
|
|
}
|
|
|
|
// runRawHandler passing message m to the provided handler. go routine.
|
|
func runRawHandler(ctx context.Context, h RawHandler, w ResponseWriter, m *Message) bool {
|
|
defer glogPanic()
|
|
h.ProcessMessage(ctx, w, m)
|
|
|
|
return false
|
|
}
|
|
|
|
// runHearsHandler will match the go routine.
|
|
func runHearsHandler(ctx context.Context, h HearsHandler, w ResponseWriter, m *Message) bool {
|
|
defer glogPanic()
|
|
|
|
if mtchs := h.Hears().FindAllStringSubmatch(m.Text, -1); mtchs != nil {
|
|
go h.Heard(ctx, w, m, mtchs)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// runCommandHandler initializes the message m as a command message and passed
|
|
// it to the given handler.
|
|
func runCommandHandler(ctx context.Context, h CommandHandler, w ResponseWriter, m *Message) error {
|
|
if h != nil && glog.V(2) {
|
|
glog.Infof("RUNNING %v %v\n", h, m.args)
|
|
}
|
|
defer glogPanic()
|
|
var err error
|
|
|
|
if m.args == nil {
|
|
m.args, err = shellwords.Parse(m.Text)
|
|
if err != nil {
|
|
return ErrBadCLI
|
|
}
|
|
}
|
|
|
|
if len(m.args) == 0 {
|
|
//nothing to do.
|
|
return errors.New("command handler called with no possible arguments")
|
|
}
|
|
|
|
name := m.args[0]
|
|
m.flagOut = &bytes.Buffer{}
|
|
m.FlagSet = flag.NewFlagSet(name, flag.ContinueOnError)
|
|
m.FlagSet.SetOutput(m.flagOut)
|
|
|
|
err = h.Command(ctx, w, m)
|
|
if err == flag.ErrHelp {
|
|
fmt.Fprint(w, cmdUsage(h, name, nil).Error())
|
|
return ErrSkipHears
|
|
}
|
|
|
|
return err
|
|
}
|