botopolis/bot

View on GitHub
robot.go

Summary

Maintainability
A
0 mins
Test Coverage
package bot

import (
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gorilla/mux"
)

const timeout = 15

// Robot is the central structure for bot
type Robot struct {
    // Chat adapter
    Chat Chat
    // Data store
    Brain *Brain
    // HTTP Router
    Router *mux.Router
    // Logger with levels
    Logger Logger

    plugins   *pluginRegistry
    internals *pluginRegistry
    queue     *responderQueue
}

// New creates an instance of a bot.Robot and loads in the chat adapter
// Typically you would install plugins before running the robot.
func New(c Chat, plugins ...Plugin) *Robot {
    r := &Robot{
        Chat:   c,
        Brain:  NewBrain(),
        Router: mux.NewRouter(),
        Logger: defaultLogger,

        plugins:   newPluginRegistry(),
        internals: newPluginRegistry(),
        queue:     newResponderQueue(10),
    }
    r.internals.Add(
        c,
        newServer(":"+os.Getenv("PORT")),
    )
    r.plugins.Add(plugins...)
    return r
}

// Plugin allows you to fetch a loaded plugin for direct use
// See ExamplePluginUsage
func (r *Robot) Plugin(p Plugin) bool {
    ok := r.plugins.Get(p)
    if !ok {
        return r.internals.Get(p)
    }
    return ok
}

// ListPlugins returns a slice of all loaded plugins.
func (r *Robot) ListPlugins() []Plugin {
    return append(r.internals.List(), r.plugins.List()...)
}

// Run is responsible for:
// 1. Loading internals (chat, http)
// 2. Loading all plugins
// 3. Running RTM Slack until termination
// 4. Unloading all plugins in reverse order
// 5. Unloading internals in reverse order
func (r *Robot) Run() {
    r.gracefulShutdown()
    r.internals.Load(r)
    r.plugins.Load(r)
    r.queue.Forward(r, r.Chat.Messages())
    r.stop()
}

func (r *Robot) gracefulShutdown() {
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        <-c
        done := make(chan bool)
        go func() {
            r.stop()
            done <- true
        }()
        select {
        case <-time.After(timeout * time.Second):
            r.Logger.Infof("Force quitting after %ds timeout", timeout)
            os.Exit(1)
        case <-done:
            os.Exit(0)
        }
    }()
}

func (r *Robot) stop() {
    r.Logger.Info("Shutting down...")
    r.plugins.Unload(r)
    r.internals.Unload(r)
}

func (r *Robot) onMessage(t messageType, m Matcher, h hook) {
    if m == nil || h == nil {
        return
    }
    for rs := range r.queue.On(t) {
        if m(&rs) {
            if err := h(rs); err != nil {
                r.Logger.Errorf("Hook error: %s", err.Error())
            }
        }
    }
}

// Hear is triggered on any message event.
func (r *Robot) Hear(m Matcher, h hook) { go r.onMessage(DefaultMessage, m, h) }

// Respond is triggered on messages to the bot
func (r *Robot) Respond(m Matcher, h hook) { go r.onMessage(Response, m, h) }

// Enter is triggered when someone enters a room
func (r *Robot) Enter(h hook) { go r.onMessage(Enter, nil, h) }

// Leave is triggered when someone leaves a room
func (r *Robot) Leave(h hook) { go r.onMessage(Leave, nil, h) }

// Topic is triggered when someone changes the topic
func (r *Robot) Topic(h hook) { go r.onMessage(Topic, nil, h) }

// Username provides the robot's username
func (r *Robot) Username() string { return r.Chat.Username() }