Full Code of Netflix/hal-9001 for AI

master 2b3799df56b5 cached
71 files
467.1 KB
141.9k tokens
690 symbols
1 requests
Download .txt
Showing preview only (492K chars total). Download the full file or copy to clipboard to get everything.
Repository: Netflix/hal-9001
Branch: master
Commit: 2b3799df56b5
Files: 71
Total size: 467.1 KB

Directory structure:
gitextract_tlficdtk/

├── .gitignore
├── LICENSE
├── NOTICE
├── OSSMETADATA
├── README.md
├── brokers/
│   ├── console/
│   │   └── broker.go
│   ├── hipchat/
│   │   └── broker.go
│   └── slack/
│       └── broker.go
├── example/
│   ├── demos/
│   │   ├── colorparser.go
│   │   ├── imgtable.go
│   │   └── utf8table.go
│   ├── docker-repl/
│   │   └── main.go
│   ├── everything/
│   │   └── main.go
│   ├── minimal/
│   │   └── main.go
│   └── repl/
│       └── main.go
├── hal/
│   ├── asciitable.go
│   ├── asciitable_test.go
│   ├── broker.go
│   ├── cmd.go
│   ├── cmd_test.go
│   ├── counter.go
│   ├── directory.go
│   ├── event.go
│   ├── event_test.go
│   ├── kv.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── periodic.go
│   ├── persist_plugins.go
│   ├── plugins.go
│   ├── prefs.go
│   ├── router.go
│   ├── secrets.go
│   ├── secrets_test.go
│   ├── sqldb.go
│   ├── text2image.go
│   ├── text2image_test.go
│   ├── ttlcache.go
│   ├── ttlcache_test.go
│   ├── utf8table.go
│   └── utf8table_test.go
└── plugins/
    ├── archive/
    │   └── plugin.go
    ├── blabber/
    │   └── plugin.go
    ├── cross_the_streams/
    │   └── plugin.go
    ├── docker/
    │   └── plugin.go
    ├── google_calendar/
    │   ├── google.go
    │   └── plugin.go
    ├── guys/
    │   └── plugin.go
    ├── inspect/
    │   └── plugin.go
    ├── mark/
    │   └── plugin.go
    ├── pagerduty/
    │   ├── helpers.go
    │   ├── oncall_plugin.go
    │   ├── page_plugin.go
    │   ├── pd_events_v1.go
    │   ├── pd_events_v2.go
    │   ├── pd_oncall.go
    │   ├── pd_policy.go
    │   ├── pd_schedule.go
    │   ├── pd_service.go
    │   ├── pd_team.go
    │   ├── pd_types.go
    │   ├── pd_user.go
    │   ├── plugin.go
    │   └── poller.go
    ├── pluginmgr/
    │   └── plugin.go
    ├── prefmgr/
    │   ├── http.go
    │   └── plugin.go
    ├── roster/
    │   └── plugin.go
    ├── seppuku/
    │   └── plugin.go
    ├── spam/
    │   └── plugin.go
    └── uptime/
        └── plugin.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.exe
*.test
*.prof

.idea
*.iml

*.swp

example/everything/everything
example/minimal/minimal
example/repl/repl
plugins/*/console/console


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: NOTICE
================================================
The font in hal/text2image.go is derived from the IBM VGA 8x16 font
from http://int10h.org/oldschool-pc-fonts/ which is under Creative
Commons Attribution-ShareAlike 4.0 International.


================================================
FILE: OSSMETADATA
================================================
osslifecycle=active


================================================
FILE: README.md
================================================
# Hal-9001

Hal-9001 is a Go library that offers a number of facilities for creating a bot
and its plugins.

# Goals

* make easy things easy and hard things accessible
* 15 minutes from getting started to a working bot
* optimize for long-term maintenance

# Requirements

* Go >= 1.5

It should build with older versions of Go but it has not been tested.

# Creating your own bot

The easiest place to start is with the examples in the examples directory. Take
a look at what's there and copy the main.go of your favorite into a new repo
and start editing it to your taste.

examples/everything/main.go has the most coverage of Hal's features and has
commentary throughout the file that should help you get going.

TODO: add more of a tutorial here / on the wiki

# Building

A few dependencies are required by Hal's core library and plugins. For
building the examples/everything demo, you will need the following. Hal
core requires at least the mysql driver to build. Everything else is
a dependency of a plugin or broker and can be omitted if you don't import
those.

```
go get github.com/nlopes/slack
go get github.com/mattn/go-xmpp
go get github.com/codegangsta/cli
go get github.com/go-sql-driver/mysql

# optional - currently only used in examples/repl
go get gopkg.in/DATA-DOG/go-sqlmock.v1
```

# Using Hal in chat

Most bots built with hal start the pluginmgr plugin first. The pluginmgr
allows users to enable and configure plugins from inside the chat system.

e.g.

```
!plugin attach uptime
!plugin detach uptime
!plugin attach uptime --regex ^[[:space:]]*!up
!plugin list
```

# Terminology

### Event

Hal's events (hal.Evt) are an abstraction of the messages/events that
brokers produce/consume. An event has a Body, User, Room, and timestamp.

The handle offers some convenience methods for replying to events and
other tedious bits around processing them.

### Broker

A broker is a 2-way producer/consumer of events. The code that hooks
hal up to Slack, Hipchat, and others are brokers. There is a hal.Broker
interface that defines the required behavior of brokers.

### Plugin

A plugin is a function that processes events with metadata. Plugins do
nothing until they are attached to a room in the plugin manager.

### Instance

An instance is a plugin that has been attached to a room.

### Room

Hal calls all channels/rooms/related concepts rooms. Mostly "room" was picked
because calling things channels in Go code gets confusing when you're also
using channels extensively.

# Authoring Plugins

Hal plugins should be in a package. You can have more than one plugin
per package. Some ship with Hal, others are in their own repos and
can be added with go get/import.

Because plugins are not activated automatically and can be bound to channels
with separate configs, they have to be registered and then instantiated.

```go
package uptime

// uptime: the simplest useful plugin possible

import (
	"time"

	"github.com/netflix/hal-9001/hal"
)

var booted time.Time

func init() {
	booted = time.Now()
}

// The plugin's Register() should be called from main() in the bot to
// make the plugin available for use at runtime. It can be called anything
// you like, but most of the plugins call it Register().
//
// Plugins are not tied to a specific broker so if it is going to use
// the evt.Original field, be careful about double-checking the type
// of message or evt.Broker to make sure it's safe to use.
func Register() {
	p := hal.Plugin{
		Name:   "uptime",
		Func:   uptime,
		Regex:  "^!uptime",
	}

	p.Register()
}

// uptime implements the plugin itself
func uptime(evt hal.Evt) {
	ut := time.Since(booted)
	evt.Replyf("uptime: %s", ut.String())
}
```

# Rationale

Some constructs in Hal are the result of a few decisions that deserve explanation.

## MySQL as the only supported database driver

Right now, only mysql-compatible database backends are supported. This is
unlikely to change. Coding directly against a specific database allows Hal
to use database-specific features and avoid unncessarry abstractions or loss
of power required to support other databases.

Netflix runs its bot in AWS using Aurora with local testing against MariaDB.

## missing tests & ubiquitous assertions

This is not a permanent situation. The API changed a lot as the bot was being
built and tests were frequently invalidated. Now that the API is more stable,
tests are being added back over time.

In order to speed up development and reduce the frequence of error checking
in plugin/bot code, many parts of hal simply crash the program when errors
occur. This makes assumptions about errors obvious and immediately visible
without having to bubble errors up into consumer code at the cost of having
to run your hal bot under a supervisor. When reasons are found to convert
fatal errors into error returns, code should be refactored to do so.

# TODO

- [ ] implement sensible REST patterns for HTTP endpoints
- [ ] work on the TODOs sprinkled throughout the code
- [ ] provide more examples, e.g. slack-only, hipchat-only, console + slack
- [ ] logging hooks to redirect logs to a channel
- [ ] revive/update the Docker plugin
- [ ] update constants to match the Go standards

# Future Ideas

* [in progress] a Docker plugin that runs code in Docker over stdio
    * exists, but is not ready to be released yet
* integrate sshchat as a broker or an maybe an ssh server for admin stuff
* build in a simple arg parser something like evt.Getopts()
  along the lines of evt.BodyAsArgv()

# Community

The hangops slack seems like as good a place as any to start out.
Bot presence coming soon.

https://hangops.slack.com/messages/hal-9001/

# Author

Al Tobey <atobey@netflix.com>

# License

Apache 2


================================================
FILE: brokers/console/broker.go
================================================
package console

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/chzyer/readline"
	"github.com/netflix/hal-9001/hal"
)

var log hal.Logger

type Config struct{}

type Broker struct {
	User   string
	Room   string
	Topic  string
	Stdin  chan string
	Stdout chan string
}

type SlashReaction string

// REPL starts a readline-like bot REPL on the console.
// All plugins that are present and registered are automatically enabled.
// name should be a non-empty string. It is reported as the room name/id and
// will be the string in the REPL prompt, e.g. "foo" -> "foo> ".
// If prefix is set, it is prepended to every line so you can do, e.g.
// REPL("foo", "!foo") and every line in the REPL will show up in the hal
// Evt.Body as "!foo <whatever>". To avoid this, set it to empty string.
// This will start 2 goroutines.
func REPL(name, prefix string) {
	conf := Config{}
	broker := conf.NewBroker(name)
	router := hal.Router()
	router.AddBroker(broker)
	go router.Route()

	// automatically wire up all loaded & registered plugins
	pr := hal.PluginRegistry()
	for _, p := range pr.PluginList() {
		i := p.Instance(broker.Room, broker)
		i.Register()
	}

	lines := make(chan string, 100)
	quit := make(chan struct{}, 1)

	// a simple forwarder - lines from the REPL are forwarded to the router
	// lines from the router are printed to stdout
	// when the user quits, the goroutine is shut down gracefully
	go func() {
		for {
			select {
			case <-quit:
				close(quit)
				close(lines)
				return
			case line := <-broker.Stdout:
				println(line)
			case line := <-lines:
				broker.Stdin <- line
			}
		}
	}()

	rl, err := readline.New(name + "> ")
	if err != nil {
		panic(err)
	}
	defer rl.Close()

	// block forever reading lines from stdin
	for {
		line, err := rl.Readline()
		if err == io.EOF {
			time.Sleep(time.Millisecond * 100)
			continue
		} else if err != nil {
			panic(err)
		}

		// quit or exit immediately exit the REPL
		trimmed := strings.Trim(line, "\r\n		")
		if trimmed == "quit" || trimmed == "exit" {
			break
		}

		// no input, user likely hit enter on an empty line
		if trimmed == "" {
			continue
		}

		// e.g. prefix="!prefs" translates "list" to "!prefs list"
		// so the plugin system automatically takes care of things
		// without hacks in the core code
		if prefix != "" {
			lines <- prefix + " " + strings.Trim(line, " ")
		} else {
			lines <- line
		}
	}

	quit <- struct{}{}
}

// NewBroker returns a new console.Broker.
func (c Config) NewBroker(name string) Broker {
	user := os.Getenv("USER")
	if user == "" {
		user = "testuser"
	}

	out := Broker{
		User:   user,
		Room:   name,
		Stdin:  make(chan string, 1000),
		Stdout: make(chan string, 1000),
	}

	return out
}

func (cb Broker) Name() string {
	return cb.Room
}

func (cb Broker) Send(e hal.Evt) {
	cb.Stdout <- e.Body
}

func (cb Broker) SendDM(e hal.Evt) {
	cb.Stdout <- e.Body
}

func (cb Broker) Leave(roomId string) error {
	log.Println("Leave(roomId string) not implemented.")
	return nil
}

func (cb Broker) GetTopic(roomId string) (string, error) {
	return cb.Topic, nil
}

func (cb Broker) SetTopic(roomId, topic string) error {
	cb.Topic = topic
	cb.Stdout <- fmt.Sprintf("topic set to: %q", topic)
	return nil
}

func (cb Broker) SendTable(e hal.Evt, hdr []string, rows [][]string) {
	cb.Stdout <- hal.Utf8Table(hdr, rows)
}

func (cb Broker) LooksLikeRoomId(room string) bool {
	return true
}

func (cb Broker) LooksLikeUserId(user string) bool {
	return true
}

// SimpleStdin will loop forever reading stdin and publish each line
// as an event in the console broker.
// e.g. go cbroker.SimpleStdin()
func (cb Broker) SimpleStdin() {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		line := scanner.Text()

		if err := scanner.Err(); err != nil {
			log.Fatalf("Failed while reading from stdin: %s\n", err)
		}

		// ignore empty lines
		if len(line) == 0 {
			continue
		}

		cb.Stdin <- line
	}
}

// SimpleStdout prints all replies, etc to the broker on os.Stdout.
// e.g. go cbroker.SimpleStdout()
func (cb Broker) SimpleStdout() {
	for {
		select {
		case txt := <-cb.Stdout:
			// events from the Reply() method go through a go channel
			_, err := os.Stdout.WriteString(txt)
			if err != nil {
				log.Fatalf("Could not write to stdout: %s\n", err)
			}
		}
	}
}

func (cb Broker) Stream(out chan *hal.Evt) {
	for {
		input := <-cb.Stdin
		now := time.Now()

		e := hal.Evt{
			ID:       fmt.Sprintf("%d.%06d", now.Unix(), now.UnixNano()),
			User:     cb.User,
			UserId:   cb.User,
			Room:     cb.Room,
			RoomId:   cb.Room,
			Body:     input,
			Time:     now,
			Broker:   cb,
			IsChat:   true,
			Original: &input,
		}

		if strings.HasPrefix(e.Body, "/") {
			args := e.BodyAsArgv()

			// detect slash commands for creating specialized event types
			switch args[0] {
			case "/reaction":
				if len(args) == 2 {
					e.Body = args[1]
					// re-cast the reaction as a type that can be introspected by plugins
					orig := SlashReaction(args[1])
					e.Original = &orig
				} else {
					e.IsChat = true
					e.Reply("/reaction requires exactly one argument!")
				}
			}
		} else {
			// everything else is just a plain chat event
			out <- &e
		}
	}
}

// required by interface
func (b Broker) RoomIdToName(in string) string { return in }
func (b Broker) RoomNameToId(in string) string { return in }
func (b Broker) UserIdToName(in string) string { return in }
func (b Broker) UserNameToId(in string) string { return in }


================================================
FILE: brokers/hipchat/broker.go
================================================
package hipchat

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"strings"
	"time"

	"github.com/mattn/go-xmpp"
	"github.com/netflix/hal-9001/hal"
)

var log hal.Logger

// Broker contains the Hipchat API handles required for interacting
// with the hipchat service.
type Broker struct {
	Client *xmpp.Client
	Config Config
	inst   string
}

type Config struct {
	Host     string
	Jid      string
	Password string
	Rooms    map[string]string
}

// HIPCHAT_HOST is the only supported hipchat host.
const HIPCHAT_HOST = `chat.hipchat.com:5223`

// Hipchat is a singleton that returns an initialized and connected
// Broker. It can be called anywhere in the bot at any time.
// Host must be "chat.hipchat.com:5223". This requirement can go away
// once someone takes the time to integrate and test against an on-prem
// Hipchat server.
func (c Config) NewBroker(name string) Broker {
	// TODO: remove this once the TLS/SSL requirements are sorted
	if c.Host != HIPCHAT_HOST {
		log.Println("TODO: Only SSL and hosted Hipchat are supported at the moment.")
		log.Printf("Hipchat host must be %q.", HIPCHAT_HOST)
	}

	// for some reason Go's STARTTLS seems to be incompatible with
	// Hipchat's or maybe Hipchat TLS is broken, so don't bother and use SSL.
	options := xmpp.Options{
		Host:          c.Host,
		User:          c.Jid,
		Debug:         false,
		Password:      c.Password,
		Resource:      "bot",
		Session:       true,
		Status:        "Available",
		StatusMessage: "Hal-9001 online.",
	}

	client, err := options.NewClient()
	if err != nil {
		log.Fatalf("Could not connect to Hipchat over XMPP: %s\n", err)
	}

	for jid, name := range c.Rooms {
		_, err = client.JoinMUCNoHistory(jid, name)
		if err != nil {
			log.Fatalf("Could not join room %q/%q: %s", name, jid, err)
		}
	}

	hb := Broker{
		Client: client,
		Config: c,
		inst:   name,
	}

	return hb
}

func (hb Broker) Name() string {
	return hb.inst
}

func (hb Broker) Send(evt hal.Evt) {
	remote := fmt.Sprintf("%s/%s", evt.RoomId, hb.RoomIdToName(evt.RoomId))

	msg := xmpp.Chat{
		Text:   evt.Body,
		Stamp:  evt.Time,
		Type:   "groupchat",
		Remote: remote,
	}

	_, err := hb.Client.Send(msg)
	if err != nil {
		log.Printf("Failed to send message to Hipchat server: %s\n", err)
	}
}

// TODO: implement this - if Atlassian ever re-publishes the API docs.
func (hb Broker) SendDM(e hal.Evt) {
	panic("SendDM not implemented in Hipchat yet.")
}

// TODO: this is untested and may not be entirely correct
func (hb Broker) Leave(roomId string) error {
	for jid, name := range c.Rooms {
		if roomId == name {
			_, err := hb.Client.LeaveMUC(jid)
			return err
		}
	}
	return fmt.Errorf("Unable to determine JID of room %q.", roomId)
}

// TODO: implement
func (hb Broker) GetTopic(roomId string) (string, error) {
	panic("SetTopic not implemented in Hipchat yet. Pull requests welcome.")
}

// TODO: implement
func (hb Broker) SetTopic(roomId, topic string) error {
	panic("SetTopic not implemented in Hipchat yet. Pull requests welcome.")
}

func (hb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {
	out := evt.Clone()
	// TODO: verify if this works for bots - works fine in the client
	// will probably need to post with the API
	out.Body = fmt.Sprintf("/code %s", hal.Utf8Table(hdr, rows))
	hb.Send(out)
}

func (hb Broker) LooksLikeRoomId(room string) bool {
	log.Println("brokers/hipchat/LooksLikeRoomId() is a stub that always returns true!")
	return true
}

func (hb Broker) LooksLikeUserId(user string) bool {
	log.Println("brokers/hipchat/LooksLikeUserId() is a stub that always returns true!")
	return true
}

// Subscribe joins a room with the given alias.
// These names are specific to how Hipchat does things.
func (hb *Broker) Subscribe(room, alias string) {
	// TODO: take a room name and somehow look up the goofy MUC name
	// e.g. client.JoinMUC("99999_roomName@conf.hipchat.com", "Bot Name")
	hb.Client.JoinMUCNoHistory(room, alias)
	hb.Config.Rooms[room] = alias
}

// Keepalive is a timer loop that can be fired up to periodically
// send keepalive messages to the Hipchat server in order to prevent
// Hipchat from shutting the connection down due to inactivity.
func (hb *Broker) heartbeat(t time.Time) {
	// this seems to work but returns an error you'll see in the logs
	msg := xmpp.Chat{
		Text:  "heartbeat",
		Stamp: t,
	}
	msg.Stamp = t

	n, err := hb.Client.Send(msg)
	if err != nil {
		log.Fatalf("Failed to send keepalive (%d): %s\n", n, err)
	}
}

// Stream is an event loop for Hipchat events.
func (hb Broker) Stream(out chan *hal.Evt) {
	client := hb.Client
	incoming := make(chan *xmpp.Chat)
	timer := time.Tick(time.Minute * 1) // once a minute

	// grab chat messages using the blocking Recv() and forward them
	// on a channel so the select loop can also handle sending heartbeats
	go func() {
		for {
			msg, err := client.Recv()
			if err != nil {
				log.Printf("Error receiving from Hipchat: %s\n", err)
			}

			switch t := msg.(type) {
			case xmpp.Chat:
				m := msg.(xmpp.Chat)
				incoming <- &m
			case xmpp.Presence:
				continue // ignored
			default:
				log.Printf("Unhandled message of type '%T': %s ", t, t)
			}
		}
	}()

	for {
		select {
		case t := <-timer:
			hb.heartbeat(t)
		case chat := <-incoming:
			// Remote should look like "99999_roomName@conf.hipchat.com/User Name"
			parts := strings.SplitN(chat.Remote, "/", 2)
			now := time.Now()

			if len(parts) == 2 {
				// XMPP doesn't have IDs, use time like Slack
				e := hal.Evt{
					ID:       fmt.Sprintf("%d.%06d", now.Unix(), now.UnixNano()),
					Body:     chat.Text,
					Room:     hb.RoomIdToName(parts[0]),
					RoomId:   parts[0],
					User:     parts[1],
					UserId:   chat.Remote,
					Time:     now, // m.Stamp seems to be zeroed
					Broker:   hb,
					IsChat:   true,
					Original: &chat,
				}

				out <- &e
			} else {
				log.Printf("hipchat broker received an unsupported message: %+v", chat)
			}
		}
	}
}

// only considers rooms that have been configured in the bot
// and does not hit the Hipchat APIs at all
// TODO: hit the API and get the room/name lists and cache them
func (b Broker) RoomIdToName(in string) string {
	if name, exists := b.Config.Rooms[in]; exists {
		return name
	}

	return ""
}

func (b Broker) RoomNameToId(in string) string {
	for id, name := range b.Config.Rooms {
		if name == in {
			return id
		}
	}

	return ""
}

func (b Broker) UserIdToName(in string) string { return in }
func (b Broker) UserNameToId(in string) string { return in }


================================================
FILE: brokers/slack/broker.go
================================================
package slack

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io/ioutil"
	"os"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/netflix/hal-9001/hal"
	"github.com/nlopes/slack"
)

var log hal.Logger

// Broker interacts with the slack service.
// TODO: add a miss cache to avoid hammering the room/user info apis
type Broker struct {
	Client  *slack.Client     // slack API object
	RTM     *slack.RTM        // slack RTM object
	UserId  string            // slack Bot user ID (for preventing loops)
	inst    string            // broker instance name
	i2u     map[string]string // id->name cache
	i2c     map[string]string // id->name cache
	u2i     map[string]string // name->id cache
	c2i     map[string]string // name->id cache
	imcs    map[string]string // userId -> channelId im channels
	lufill  time.Time         // timestamp of the last user cache fill
	lrfill  time.Time         // timestamp of the last room cache fill
	idRegex *regexp.Regexp    // compiled RE to match user/room ids
	mut     sync.Mutex        // protect access to the lookup maps
}

type Config struct {
	Token string
}

var LooksLikeIdRE *regexp.Regexp

func init() {
	LooksLikeIdRE = regexp.MustCompile(`^[UCD]\w{8}$`)

	log.SetPrefix("brokers/slack")
}

func (c Config) NewBroker(name string) Broker {
	client := slack.New(c.Token)
	// TODO: check for failures and log.Fatalf()
	rtm := client.NewRTM()

	sb := Broker{
		Client: client,
		RTM:    rtm,
		inst:   name,
		i2u:    make(map[string]string),
		i2c:    make(map[string]string),
		u2i:    make(map[string]string),
		c2i:    make(map[string]string),
		imcs:   make(map[string]string),
	}

	// fill the caches at startup to cut down on API requests
	sb.FillUserCache()
	sb.FillRoomCache()

	go rtm.ManageConnection()

	return sb
}

// Name returns the name of the broker as set in NewBroker.
func (sb Broker) Name() string {
	return sb.inst
}

func (sb Broker) Send(evt hal.Evt) {
	// Slack refuses messages over 4000 characters. Most of the time that's
	// probably data so post it as a file. Using len instead of rune count since
	// slack is probably looking at bytes.
	if len(evt.Body) > 3999 {
		sb.SendAsSnippet(evt)
	} else {
		sb.SendAsIs(evt)
	}
}

func (sb Broker) SendAsSnippet(evt hal.Evt) {
	f, err := ioutil.TempFile(os.TempDir(), "hal")
	if err != nil {
		evt.Replyf("Could not create tempfile for large text upload: %s", err)
		return
	}
	defer os.Remove(f.Name())

	f.WriteString(evt.Body)
	f.Close()

	// upload the file
	params := slack.FileUploadParameters{
		File:     f.Name(),
		Filename: "reply.txt",
		Channels: []string{evt.RoomId},
	}
	_, err = sb.Client.UploadFile(params)
	if err != nil {
		evt.Replyf("Could not upload snippet file: %s", err)
	}
}

// SendAsIs directly sends a message without considering it for posting as a snippet.
func (sb Broker) SendAsIs(evt hal.Evt) {
	// if evt.Original is a slack.PostMessageParameters, assume that means that there is
	// a rich message in the body with params that need to be posted to the web API
	// rather than going through RTM.
	// See: https://api.slack.com/bot-users
	switch evt.Original.(type) {
	case *slack.PostMessageParameters:
		params := evt.Original.(*slack.PostMessageParameters)
		params.AsUser = true // if we've gotten here, we always want this
		sb.Client.PostMessage(evt.RoomId, evt.Body, *params)
	default:
		om := sb.RTM.NewOutgoingMessage(evt.Body, evt.RoomId)
		sb.RTM.SendMessage(om)
	}
}

func (sb Broker) SendDM(evt hal.Evt) {
	evt.Room = ""
	evt.RoomId = ""

	if roomId, exists := sb.imcs[evt.UserId]; exists {
		// cache hit
		// TODO: verify what happens if the destination user has closed the DM
		evt.RoomId = roomId
	} else {
		// try to open the channel, cache it if it works
		_, _, roomId, err := sb.RTM.OpenIMChannel(evt.UserId)
		if err != nil {
			log.Printf("Error from RTM.OpenIMChannel(%q): %s", evt.UserId, err)
		} else {
			sb.imcs[evt.UserId] = roomId
			sb.i2c[roomId] = evt.UserId // TODO: verify this isn't a stupid idea
			evt.RoomId = roomId
		}
	}

	if evt.RoomId != "" {
		sb.Send(evt)
	} else {
		log.Printf("SendDM() failed because it couldn't identify a DM RoomID!")
		log.Printf("Failed message: %q", evt.String())
	}
}

func (sb Broker) Leave(roomId string) error {
	_, err := sb.Client.LeaveChannel(roomId)
	return err
}

func (sb Broker) GetTopic(roomId string) (string, error) {
	ch, err := sb.Client.GetChannelInfo(roomId)
	return ch.Topic.Value, err
}

func (sb Broker) SetTopic(roomId, topic string) error {
	r, err := sb.Client.SetChannelTopic(roomId, topic)
	log.Debugf("SetTopic(%q, %q) = %q", roomId, topic, r)
	return err
}

func (sb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {
	out := evt.Clone()
	out.Body = hal.Utf8Table(hdr, rows)

	tblFmt := hal.FindPrefs("", "", "", "", "table.format").One()

	if tblFmt.Value == "image" {
		sb.SendAsImage(out)
	} else if tblFmt.Value == "snippet" {
		sb.SendAsSnippet(out)
	} else {
		sb.SendAsIs(out)
	}
}

// SendAsImage sends the body of the event as a png file. The png is rendered
// using hal's FixedFont facility.
// This is useful for making sure pre-formatted text stays legible in
// Slack while we wait for them to figure out a way to render things like
// tables of data consistently.
func (sb Broker) SendAsImage(evt hal.Evt) {
	fd := hal.FixedFont()

	// create a tempfile
	f, err := ioutil.TempFile(os.TempDir(), "hal")
	if err != nil {
		evt.Replyf("Could not create tempfile for image upload: %s", err)
		return
	}
	defer os.Remove(f.Name())

	// check for a color preference
	// need to figure out a way to have a helper around this
	var fg color.Color
	fg = color.Black
	// TODO: prefs --set isn't setting the room, etc. remove the filter for now
	fgprefs := hal.FindPrefs("", "", "", "", "image.fg")
	ufgprefs := fgprefs.User(evt.UserId)
	if len(ufgprefs) > 0 {
		fg = fd.ParseColor(ufgprefs[0].Value, fg)
	} else if len(fgprefs) > 0 {
		fg = fd.ParseColor(fgprefs[0].Value, fg)
	}

	var bg color.Color
	bg = color.Transparent
	// TODO: ditto from ft
	//bgprefs := hal.FindPrefs("", sb.Name(), evt.RoomId, "", "image.bg")
	bgprefs := hal.FindPrefs("", "", "", "", "image.bg")
	ubgprefs := bgprefs.User(evt.UserId)
	if len(ubgprefs) > 0 {
		bg = fd.ParseColor(ubgprefs[0].Value, fg)
	} else if len(bgprefs) > 0 {
		bg = fd.ParseColor(bgprefs[0].Value, fg)
	}

	// generate the image
	lines := strings.Split(strings.TrimSpace(evt.Body), "\n")
	textimg := fd.StringsToImage(lines, fg)

	// img has a background color, copy textimg onto it
	img := image.NewRGBA(textimg.Bounds())
	draw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.ZP, draw.Src)
	draw.Draw(img, img.Bounds(), textimg, image.ZP, draw.Src)

	// TODO: apply background color

	// write the png data to the temp file
	png.Encode(f, img)
	f.Close()

	// upload the file
	params := slack.FileUploadParameters{
		File:     f.Name(),
		Filename: "text.png",
		Channels: []string{evt.RoomId},
	}
	_, err = sb.Client.UploadFile(params)
	if err != nil {
		evt.Replyf("Could not upload image: %s", err)
	}
}

func (sb Broker) LooksLikeRoomId(room string) bool {
	sb.mut.Lock()
	defer sb.mut.Unlock()

	if _, exists := sb.i2c[room]; exists {
		return true
	}

	return LooksLikeIdRE.MatchString(room)
}

func (sb Broker) LooksLikeUserId(user string) bool {
	sb.mut.Lock()
	defer sb.mut.Unlock()

	if _, exists := sb.i2u[user]; exists {
		return true
	}

	return LooksLikeIdRE.MatchString(user)
}

// checks the cache to see if the room is known to this broker
func (sb Broker) HasRoom(room string) bool {
	sb.mut.Lock()
	defer sb.mut.Unlock()

	if LooksLikeIdRE.MatchString(room) {
		_, exists := sb.i2c[room]
		return exists
	} else {
		_, exists := sb.c2i[room]
		return exists
	}
}

// Stream is an event loop for Slack events & messages from the RTM API.
// Events are copied to a hal.Evt and forwarded to the exchange where they
// can be processed by registered handlers.
func (sb Broker) Stream(out chan *hal.Evt) {
	for {
		select {
		case msg := <-sb.RTM.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.UserTypingEvent:
				// frequent and mostly useless in a bot: ignore

			case *slack.HelloEvent:
				log.Debugf("HelloEvent")

			case *slack.ConnectedEvent:
				info := sb.RTM.GetInfo()
				sb.UserId = info.User.ID

				log.Debugf("ConnectedEvent - retreived bot ID %q", sb.UserId)

			case *slack.MessageEvent:
				// https://api.slack.com/events/message
				m := msg.Data.(*slack.MessageEvent)

				// mark messages generated by the bot user to prevent loops, etc.
				// but pass them through so stuff like the archive module can get them
				isBot := m.User == sb.UserId

				// A few other kinds of events are bundled as messages with a subtype.
				// Only allow isChat to remain true if it's an actual chat message.
				isChat := m.SubType == ""

				// slack channels = hal rooms, see hal-9001/hal/event.go
				e := hal.Evt{
					ID:       m.Timestamp,
					Body:     m.Text,
					Room:     sb.RoomIdToName(m.Channel),
					RoomId:   m.Channel,
					User:     sb.UserIdToName(m.User),
					UserId:   m.User,
					Broker:   sb,
					Time:     SlackTime(m.Timestamp),
					IsChat:   isChat,
					IsBot:    isBot,
					Original: m,
				}

				// let everyone know the bot is working if it appears to be a command
				if !isBot && strings.HasPrefix(strings.TrimSpace(m.Text), "!") {
					tm := sb.RTM.NewTypingMessage(m.Channel)
					sb.RTM.SendMessage(tm)
				}

				out <- &e

			case *slack.StarAddedEvent:
				sae := msg.Data.(*slack.StarAddedEvent)

				if sae.User == sb.UserId {
					log.Debugf("ignoring event from bot with id %s", sb.UserId)
					continue // ignore bot-created events
				}

				user := sb.UserIdToName(sae.User)

				e := hal.Evt{
					ID:       sae.EventTimestamp,
					Body:     fmt.Sprintf("%q added a star", user),
					Room:     sb.RoomIdToName(sae.Item.Channel),
					RoomId:   sae.Item.Channel,
					User:     user,
					UserId:   sae.User,
					Broker:   sb,
					Time:     SlackTime(sae.EventTimestamp),
					Original: sae,
				}

				out <- &e

			case *slack.StarRemovedEvent:
				sre := msg.Data.(*slack.StarRemovedEvent)

				if sre.User == sb.UserId {
					log.Debugf("ignoring event from bot with id %s", sb.UserId)
					continue // ignore bot-created events
				}

				user := sb.UserIdToName(sre.User)

				e := hal.Evt{
					ID:       sre.EventTimestamp,
					Body:     fmt.Sprintf("%q removed a star", user),
					Room:     sb.RoomIdToName(sre.Item.Channel),
					RoomId:   sre.Item.Channel,
					User:     user,
					UserId:   sre.User,
					Broker:   sb,
					Time:     SlackTime(sre.EventTimestamp),
					Original: sre,
				}

				out <- &e

			case *slack.ReactionAddedEvent:
				rae := msg.Data.(*slack.ReactionAddedEvent)

				if rae.User == sb.UserId {
					log.Debugf("ignoring event from bot with id %s", sb.UserId)
					continue // ignore bot-created events
				}

				user := sb.UserIdToName(rae.User)

				e := hal.Evt{
					ID:       rae.EventTimestamp,
					Body:     fmt.Sprintf("%q added reaction %q", user, rae.Reaction),
					Room:     sb.RoomIdToName(rae.Item.Channel),
					RoomId:   rae.Item.Channel,
					User:     user,
					UserId:   rae.User,
					Broker:   sb,
					Time:     SlackTime(rae.EventTimestamp),
					Original: rae,
				}

				out <- &e

			case *slack.ReactionRemovedEvent:
				rre := msg.Data.(*slack.ReactionRemovedEvent)

				if rre.User == sb.UserId {
					log.Debugf("ignoring event from bot with id %s", sb.UserId)
					continue // ignore bot-created events
				}

				user := sb.UserIdToName(rre.User)

				e := hal.Evt{
					ID:       rre.EventTimestamp,
					Body:     fmt.Sprintf("%q removed reaction %q", user, rre.Reaction),
					Room:     sb.RoomIdToName(rre.Item.Channel),
					RoomId:   rre.Item.Channel,
					User:     user,
					UserId:   rre.User,
					Broker:   sb,
					Time:     SlackTime(rre.EventTimestamp),
					Original: rre,
				}

				out <- &e

			case *slack.ChannelJoinedEvent:
				je := msg.Data.(*slack.ChannelJoinedEvent)
				now := time.Now()

				sb.injectRoomId(je.Channel.ID, je.Channel.Name) // cache the id:name

				e := hal.Evt{
					ID:       now.String(), // fake an id
					Body:     je.Channel.Name,
					Room:     je.Channel.Name,
					RoomId:   je.Channel.ID,
					User:     sb.UserId,
					UserId:   sb.UserId,
					Broker:   sb,
					Time:     now,
					Original: je,
				}

				out <- &e

			case *slack.GroupJoinedEvent:
				// exactly the same as ChannelJoinedEvent ^^ in a separate type
				je := msg.Data.(*slack.GroupJoinedEvent)
				now := time.Now()

				sb.injectRoomId(je.Channel.ID, je.Channel.Name) // cache the id:name

				e := hal.Evt{
					ID:       now.String(), // fake an id
					Body:     je.Channel.Name,
					Room:     je.Channel.Name,
					RoomId:   je.Channel.ID,
					User:     sb.UserId,
					UserId:   sb.UserId,
					Broker:   sb,
					Time:     now,
					Original: je,
				}

				out <- &e

			case *slack.PresenceChangeEvent:
				// ignored

			case *slack.LatencyReport:
				// ignored

			case *slack.FileCreatedEvent, *slack.FilePublicEvent, *slack.FileSharedEvent:
				// ignored

			case *slack.PrefChangeEvent:
				// ignored

			case *slack.RTMError:
				log.Printf("ignoring RTMError: %s\n", ev.Error())

			case *slack.InvalidAuthEvent:
				log.Debugf("InvalidAuthEvent")
				break

			default:
				log.Debugf("unexpected message: %+v\n", msg)
			}
		}
	}
}

// SlackTime converts the timestamp string to time.Time
func SlackTime(t string) time.Time {
	if t == "" {
		return time.Now()
	}

	// Slack advises not to parse the timestamp as a float.
	// I tried it. Turns out that string mangling is more accurate than
	// float conversions.
	parts := strings.SplitN(t, ".", 2)

	s, _ := strconv.ParseInt(parts[0], 10, 64)
	ns, _ := strconv.ParseInt(parts[1], 10, 64)

	return time.Unix(s, ns)
}

func (sb *Broker) FillUserCache() {
	// don't let this fire more than once every half hour
	now := time.Now()
	if now.Sub(sb.lufill) < time.Minute*30 {
		log.Debugf("refusing to fill cache because it has been less than 30 minutes since the last fill @ %s", sb.lufill.String())
		return
	}
	sb.lufill = now

	users, err := sb.Client.GetUsers()
	if err != nil {
		log.Printf("failed to fetch user list: %s", err)
		return
	}

	// push the users into the directory async so it doesn't hold up bot
	// startup (FillUserCache is called preemptively at startup)
	go func() {
		for _, user := range users {
			attrs := map[string]string{
				"username": user.Name,
				"name":     user.RealName,
				"email":    user.Profile.Email,
			}
			hal.Directory().Put(user.ID, "slack-user", attrs, []string{"email"})
		}
	}()

	sb.mut.Lock()
	defer sb.mut.Unlock()

	for _, user := range users {
		sb.u2i[user.Name] = user.ID
		sb.i2u[user.ID] = user.Name
	}
}

func (sb *Broker) FillRoomCache() {
	// don't let this fire more than once every half hour
	now := time.Now()
	if now.Sub(sb.lrfill) < time.Minute*30 {
		log.Printf("refusing to fill cache because it has been less than 30 minutes since the last fill @ %s", sb.lrfill.String())
		return
	}
	sb.lrfill = now

	rooms, err := sb.Client.GetChannels(true)
	if err != nil {
		log.Printf("failed to fetch room list: %s", err)
		return
	}

	// now get private channels a.k.a. groups
	groups, err := sb.Client.GetGroups(true)
	if err != nil {
		log.Printf("failed to fetch private channel list: %s", err)
		return
	}

	sb.mut.Lock()
	defer sb.mut.Unlock()

	for _, room := range rooms {
		sb.c2i[room.Name] = room.ID
		sb.i2c[room.ID] = room.Name
	}

	for _, group := range groups {
		sb.c2i[group.Name] = group.ID
		sb.i2c[group.ID] = group.Name
	}
}

// UserIdToName gets the human-readable username for a user ID using an
// in-memory cache that falls through to the Slack API
func (sb Broker) UserIdToName(id string) string {
	if id == "" {
		log.Debugf("UserIdToName(): Cannot look up empty string!")
		return ""
	}

	sb.mut.Lock()
	name, exists := sb.i2u[id]
	sb.mut.Unlock()

	if exists {
		return name
	} else {
		user, err := sb.Client.GetUserInfo(id)
		if err != nil {
			log.Printf("could not retrieve user info for '%s' via API: %s\n", id, err)
			return ""
		}

		// don't wait around for this - it can block
		go func() {
			attrs := map[string]string{
				"username": user.Name,
				"name":     user.RealName,
				"email":    user.Profile.Email,
			}

			hal.Directory().Put(user.ID, "slack-user", attrs, []string{"email"})
		}()

		sb.mut.Lock()
		defer sb.mut.Unlock()

		sb.i2u[user.ID] = user.Name
		sb.i2u[user.Name] = user.ID

		return user.Name
	}
}

// RoomIdToName gets the human-readable room name for a user ID using an
// in-memory cache that falls through to the Slack API
func (sb Broker) RoomIdToName(id string) string {
	sb.mut.Lock()
	defer sb.mut.Unlock()

	if id == "" {
		log.Debugf("RoomIdToName(): Cannot look up empty string!")
		return ""
	}

	if name, exists := sb.i2c[id]; exists {
		return name
	} else {
		var name string

		// private channels are on a different endpoint
		if strings.HasPrefix(id, "G") {
			grp, err := sb.Client.GetGroupInfo(id)
			if err != nil {
				log.Printf("could not retrieve room info for '%s' via API: %s\n", id, err)
				return ""
			}
			name = grp.Name
		} else if strings.HasPrefix(id, "D") {
			log.Println("DM CHANNELS ARE A WORK IN PROGRESS")
			//log.Printf("could not retrieve room info for '%s' via API: %s\n", id, err)
		} else {
			room, err := sb.Client.GetChannelInfo(id)
			if err != nil {
				log.Printf("could not retrieve room info for '%s' via API: %s\n", id, err)
				return ""
			}
			name = room.Name
		}

		sb.i2c[id] = name
		sb.c2i[name] = id

		return name
	}
}

// UserNameToId gets the human-readable username for a user ID using an
// in-memory cache that falls through to the Slack API
func (sb Broker) UserNameToId(name string) string {
	if name == "" {
		log.Debugf("UserNameToId(): Cannot look up empty string!")
		return ""
	}

	sb.mut.Lock()
	id, exists := sb.u2i[name]
	sb.mut.Unlock()

	if exists {
		return id
	} else {
		// there doesn't seem to be a name->id lookup so refresh the cache
		// and try again if we get here
		sb.FillUserCache()

		sb.mut.Lock()
		defer sb.mut.Unlock()

		if id, exists := sb.u2i[name]; exists {
			return id
		}

		log.Printf("service does not seem to have knowledge of username %q", name)
		return ""
	}
}

// RoomNameToId gets the id for a room name using an
// in-memory cache that falls through to the Slack API
func (sb Broker) RoomNameToId(name string) string {
	if name == "" {
		log.Println("RoomNameToId(): Cannot look up empty string!")
		return ""
	}

	sb.mut.Lock()
	id, exists := sb.c2i[name]
	sb.mut.Unlock()

	if exists {
		return id
	} else {
		sb.FillRoomCache()

		sb.mut.Lock()
		defer sb.mut.Unlock()

		if id, exists = sb.c2i[name]; exists {
			return id
		}

		log.Printf("service does not seem to have knowledge of room name %q", name)
		return ""
	}
}

// injectRoomId adds an id:name mapping to the forward and reverse lookup maps
// for internal use only, used to inject groups (private channels) on join
func (sb Broker) injectRoomId(id, name string) {
	sb.mut.Lock()
	defer sb.mut.Unlock()

	sb.c2i[name] = id
	sb.i2c[id] = name
}


================================================
FILE: example/demos/colorparser.go
================================================
package main

// go run utf8table.go

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"github.com/netflix/hal-9001/hal"
	"image/color"
)

func main() {
	samples := []string{
		"ffffff",
		"ffffffff",
		"000000ff",
		"000000aa",
		"888888ff",
		"888888",
		"f79e10",   // amber
		"f79e10ff", // amber with alpha
	}

	fd := hal.FixedFont()

	for _, sample := range samples {
		result := fd.ParseColor(sample, color.Black)
		fmt.Printf("%q => %q\n", sample, result)
	}
}


================================================
FILE: example/demos/imgtable.go
================================================
package main

// go run utf8table.go

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"github.com/netflix/hal-9001/hal"
	"image/color"
	"image/png"
	"os"
	"strings"
)

func main() {
	samples := [][][]string{
		{
			{"hdr"},
			{"one"},
		},
		{
			{"hdr"},
			{"one"},
			{"two"},
		},
		{
			{"left", "right"},
			{"one", "three"},
			{"two"},
		},
		{
			{"HEADER 1", "HDR 2", "LOL WUT"},
			{"one", "two", "three"},
			{"four", "five", "six"},
		},
		{
			{"Col 1", "Col 2", "3rd Column", "4th", "FIFTH"},
			{"one", "two", "three"},
			{"four", "five", "six"},
			{"hi"},
			{"", "", "", "-", "+"},
		},
	}

	fd := hal.FixedFont()

	for i, sample := range samples {
		out := hal.Utf8Table(sample[0], sample[1:])

		lines := strings.Split(strings.TrimSpace(out), "\n")

		img := fd.StringsToImage(lines, color.Black)

		filename := fmt.Sprintf("%d.png", i)
		f, err := os.Create(filename)
		if err != nil {
			panic(err)
		}
		defer f.Close()

		png.Encode(f, img)

		fmt.Printf("Created file: %q\n", filename)
	}
}


================================================
FILE: example/demos/utf8table.go
================================================
package main

// go run utf8table.go

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"github.com/netflix/hal-9001/hal"
)

func main() {
	samples := [][][]string{
		{
			{"hdr"},
			{"one"},
		},
		{
			{"hdr"},
			{"one"},
			{"two"},
		},
		{
			{"left", "right"},
			{"one", "three"},
			{"two"},
		},
		{
			{"HEADER 1", "HDR 2", "LOL WUT"},
			{"one", "two", "three"},
			{"four", "five", "six"},
		},
		{
			{"Col 1", "Col 2", "3rd Column", "4th", "FIFTH"},
			{"one", "two", "three"},
			{"four", "five", "six"},
			{"hi"},
			{"", "", "", "-", "+"},
		},
	}

	for _, sample := range samples {
		// first row is the header, the rest is data rows
		out := hal.Utf8Table(sample[0], sample[1:])
		fmt.Println(out)
	}
}


================================================
FILE: example/docker-repl/main.go
================================================
package main

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"github.com/chzyer/readline"

	"github.com/netflix/hal-9001/brokers/console"
	"github.com/netflix/hal-9001/hal"
	"github.com/netflix/hal-9001/plugins/docker"
	"github.com/netflix/hal-9001/plugins/pluginmgr"
	"github.com/netflix/hal-9001/plugins/prefmgr"
)

// a simple bot that only implements generic plugins on a repl
// possibly a basis for a command-line client for Slack, etc....

func main() {
	rl, err := readline.New("hal> ")
	if err != nil {
		panic(err)
	}
	defer rl.Close()

	bconf := console.Config{}
	broker := bconf.NewBroker("cli")

	docker.Register()
	pluginmgr.Register()
	prefmgr.Register()

	pr := hal.PluginRegistry()
	pmp, _ := pr.GetPlugin("pluginmgr")
	pmp.Instance(broker.Room, broker).Register()

	prmp, _ := pr.GetPlugin("prefmgr")
	prmp.Instance(broker.Room, broker).Register()

	dp, _ := pr.GetPlugin("docker")
	dp.Instance(broker.Room, broker).Register()

	router := hal.Router()
	router.AddBroker(broker)
	go router.Route()

	for {
		line, err := rl.Readline()
		if err != nil {
			break
		}

		broker.Line(line)
	}
}


================================================
FILE: example/everything/main.go
================================================
package main

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"io/ioutil"
	"log"
	"net/http"
	"os"

	"github.com/netflix/hal-9001/hal"

	"github.com/netflix/hal-9001/brokers/hipchat"
	"github.com/netflix/hal-9001/brokers/slack"

	"github.com/netflix/hal-9001/plugins/archive"
	"github.com/netflix/hal-9001/plugins/google_calendar"
	"github.com/netflix/hal-9001/plugins/mark"
	"github.com/netflix/hal-9001/plugins/pagerduty"
	"github.com/netflix/hal-9001/plugins/pluginmgr"
	"github.com/netflix/hal-9001/plugins/prefmgr"
	"github.com/netflix/hal-9001/plugins/roster"
	"github.com/netflix/hal-9001/plugins/seppuku"
	"github.com/netflix/hal-9001/plugins/uptime"
)

func main() {
	// configuration is in environment variables
	// if you prefer configuration files or flags, that's cool, just replace
	// this part with your thing
	dsn := requireEnv("HAL_DSN")
	keyfile := requireEnv("HAL_SECRETS_KEY_FILE")
	controlRoom := requireEnv("HAL_CONTROL_ROOM")
	hipchatRoomJid := requireEnv("HAL_HIPCHAT_ROOM_JID")
	hipchatRoomName := requireEnv("HAL_HIPCHAT_ROOM_NAME")
	webAddr := defaultEnv("HAL_HTTP_LISTEN_ADDR", ":9001")

	// hal provides a k/v API for managing secrets that the DB code uses to get
	// its DSN (which contains a password). Put the DSN there so the DB can find
	// it.
	secrets := hal.Secrets()
	secrets.Set(hal.SECRETS_KEY_DSN, dsn)

	// parts of hal rely on the database (prefs, secrets, etc.)
	// so make sure the DSN is valid and hal can connect before
	// doing anything else
	// hal can't do much without the database, so you probably want this
	db := hal.SqlDB()
	if err := db.Ping(); err != nil {
		log.Fatalf("Could not ping the database: %s", err)
	}

	// get the secrets encryption key from the file specified
	// this should be protected like any other private key
	// if you don't use the secrets persistence, this can be removed/ignored
	skey, err := ioutil.ReadFile(keyfile)
	if err != nil {
		log.Fatalf("Could not read key file '%s': %s", keyfile, err)
	}

	// Set the encryption key for persisted secrets.
	// Secrets can persist to the database, encrypting the key and value
	// with AES-GCM before writing so that database backups, etc only contain
	// ciphertext and no cleartext secrets.
	secrets.SetEncryptionKey(skey)

	// load secrets from the database
	secrets.LoadFromDB()

	// update the DSN again since the database might have a stale copy
	secrets.Set(hal.SECRETS_KEY_DSN, dsn)

	// configure the Hipchat broker
	hconf := hipchat.Config{
		Host:     hipchat.HIPCHAT_HOST, // TODO: not really configurable yet
		Jid:      secrets.Get("hipchat.jid"),
		Password: secrets.Get("hipchat.password"),

		// TODO: make this configurable via prefs (or maybe secrets?)
		Rooms: map[string]string{
			hipchatRoomJid: hipchatRoomName,
		},
	}
	hc := hconf.NewBroker("hipchat")

	// configure the Slack broker
	sconf := slack.Config{
		Token: secrets.Get("slack.token"),
	}
	slk := sconf.NewBroker("slack")

	// bind the slack and hipchat plugins to the router
	router := hal.Router()
	router.AddBroker(hc)
	router.AddBroker(slk)

	// Plugin registration makes them available to the bot but does not
	// activate them. That happens at runtime using e.g. pluginmgr or
	// the plugin registry's LoadInstances() (used below)
	archive.Register()
	google_calendar.Register()
	mark.Register()
	pagerduty.Register()
	pluginmgr.Register()
	prefmgr.Register()
	roster.Register()
	seppuku.Register()
	uptime.Register()

	// start up the router goroutine
	go router.Route()

	// load any previously configured plugin instances from the database
	pr := hal.PluginRegistry()
	pr.LoadInstances()

	// pluginmgr is needed to set up all the other plugins
	// so if it's not present, initialize it manually just this once
	// alternatively, you could poke config straight into the DB
	// TODO: remove the hard-coded room name or make it configurable
	for _, broker := range router.Brokers() {
		if len(pr.FindInstances(controlRoom, broker.Name(), "pluginmgr")) == 0 {
			mgr, _ := pr.GetPlugin("pluginmgr")
			mgrInst := mgr.Instance(controlRoom, broker)
			mgrInst.Register()
		}
	}

	// temporary ... (2016-03-02)
	// TODO: remove this or make it permanent by using the same method as
	// the pluginmgr bootstrap above to set the room name, etc.
	for _, broker := range router.Brokers() {
		broker.Send(hal.Evt{
			Body: "Ohai! HAL-9001 up and running.",
			Room: controlRoom,
			User: "HAL-9001",
		})
	}

	// start the webserver - some plugins register handlers to the default
	// net/http router. This makes them available. Remove this if you don't
	// want the webserver and the handlers will be silently ignored.
	go func() {
		err := http.ListenAndServe(webAddr, nil)
		if err != nil {
			log.Fatalf("Could not listen on '%s': %s\n", webAddr, err)
		}
	}()

	// block forever
	select {}
}

func requireEnv(key string) string {
	val := os.Getenv(key)
	if val == "" {
		log.Fatalf("The %q environment variable is required!", key)
	}

	return val
}

func defaultEnv(key, def string) string {
	val := os.Getenv(key)

	if val == "" {
		return def
	}

	return val
}


================================================
FILE: example/minimal/main.go
================================================
package main

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import "github.com/netflix/hal-9001/hal"

// This bot doesn't do anything except start the router and wait forever
// for messages that will never come.
//
// Most of hal's functionality is optional. It's still built along with the
// rest of hal but is not active unless it's used in main or a plugin.

func main() {
	router := hal.Router()
	router.Route()
}


================================================
FILE: example/repl/main.go
================================================
package main

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"gopkg.in/DATA-DOG/go-sqlmock.v1"

	"github.com/netflix/hal-9001/brokers/console"
	"github.com/netflix/hal-9001/hal"
	"github.com/netflix/hal-9001/plugins/pluginmgr"
	"github.com/netflix/hal-9001/plugins/prefmgr"
	"github.com/netflix/hal-9001/plugins/uptime"
)

// a simple bot that only implements generic plugins on a repl

func main() {
	// SqlInit calls will still throw errors at startup but
	// it seems the program will continue so this will do for now
	db, _, err := sqlmock.New()
	if err != nil {
		panic(err)
	}
	hal.ForceSqlDBHandle(db)
	defer db.Close()

	pluginmgr.Register()
	prefmgr.Register()
	uptime.Register()

	console.REPL("repl", "")
}


================================================
FILE: hal/asciitable.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"bytes"
	"fmt"
	"strings"
)

// Table takes a 2-dimensional array of strings and returns a single string
// formatted in a table appropriate for rendering in a fixed-width font.
// Should be suitable for Markdown table rendering.
// cheesy 2-pass technique, assuming straight fixed-width ASCII for now
func AsciiTable(hdr []string, rows [][]string) string {
	if len(rows) == 0 {
		return "NO DATA TO DISPLAY"
	} else if len(rows[0]) == 0 {
		panic("BUG: the first row seems to be empty!")
	}

	// find the needed width of each column
	colwidths := make([]int, len(hdr))
	// start with the headers' widths
	for j, col := range hdr {
		colwidths[j] = len(col)
	}

	// bump to the size of any larger cells
	for i, row := range rows {
		// handle empty/short rows gracefully by reallocating which
		// results in a default value of ""
		if len(row) < len(hdr) {
			newrow := make([]string, len(hdr))
			copy(newrow[0:len(row)], row)
			rows[i] = newrow
			row = newrow
		}

		for j, col := range row {
			if colwidths[j] < len(col) {
				colwidths[j] = len(col)
			}
		}
	}

	// generate format strings for the columns
	fmts := make([]string, len(colwidths))
	hrcs := make([]string, len(colwidths))
	if len(colwidths) > 1 {
		for i, width := range colwidths {
			if i == 0 {
				fmts[i] = fmt.Sprintf("| %%%ds |", width)
				hrcs[i] = fmt.Sprintf("|%s|", strings.Repeat("-", width+2))
			} else if i == len(colwidths)-1 {
				fmts[i] = fmt.Sprintf(" %%%ds |\n", width)
				hrcs[i] = fmt.Sprintf("%s|\n", strings.Repeat("-", width+2))
			} else {
				fmts[i] = fmt.Sprintf(" %%%ds |", width)
				hrcs[i] = fmt.Sprintf("%s|", strings.Repeat("-", width+2))
			}
		}
	} else {
		// single-column tables
		fmts[0] = fmt.Sprintf("| %%%ds |\n", colwidths[0])
		hrcs[0] = fmt.Sprintf("|%s|\n", strings.Repeat("-", colwidths[0]+2))
	}

	// horizontal rule
	hr := strings.Join(hrcs, "")

	buf := bytes.NewBuffer([]byte{})

	fmt.Fprint(buf, hr)

	for j, col := range hdr {
		fmt.Fprintf(buf, fmts[j], col)
	}

	fmt.Fprintf(buf, hr)

	for _, row := range rows {
		for j, col := range row {
			fmt.Fprintf(buf, fmts[j], col)
		}
	}

	fmt.Fprintf(buf, hr)

	return buf.String()
}


================================================
FILE: hal/asciitable_test.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"testing"
)

func TestAsciiTable(t *testing.T) {
	samples := [][][]string{
		{
			{"hdr"},
			{"one"},
		},
		{
			{"hdr"},
			{"one"},
			{"two"},
		},
		{
			{"left", "right"},
			{"one", "three"},
			{"two"},
		},
		{
			{"HEADER 1", "HDR 2", "LOL WUT"},
			{"one", "two", "three"},
			{"four", "five", "six"},
		},
		{
			{"Col 1", "Col 2", "3rd Column", "4th", "FIFTH"},
			{"one", "two", "three"},
			{"four", "five", "six"},
			{"hi"},
			{"", "", "", "-", "+"},
		},
	}

	for _, sample := range samples {
		// first row is the header, the rest is data rows
		out := AsciiTable(sample[0], sample[1:])
		// not a very useful test ... yet
		if len(out) == 0 {
			t.Fail()
		}

		fmt.Println(out)
	}
}


================================================
FILE: hal/broker.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Broker is an instance of a broker that can send/receive events.
type Broker interface {
	// the text name of the broker, arbitrary, but usually "slack" or "cli"
	Name() string
	Send(evt Evt)
	SendTable(evt Evt, header []string, rows [][]string)
	SendDM(evt Evt)
	SetTopic(roomId, topic string) error
	GetTopic(roomId string) (topic string, err error)
	Leave(roomId string) (err error)
	LooksLikeRoomId(room string) bool
	LooksLikeUserId(user string) bool
	RoomIdToName(id string) (name string)
	RoomNameToId(name string) (id string)
	UserIdToName(id string) (name string)
	UserNameToId(name string) (id string)
	Stream(out chan *Evt)
}


================================================
FILE: hal/cmd.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

/* While it's possible to use the standard library flags or an off-the-github
 * command-line parser, they have proven to be clunky and often hacky to use.
 * This API is purpose-built for building bot plugins, focusing on doing the
 * tedious parts of parsing commands without getting in the way.
 * Rules:
 *   1. "*" as user input means "whatever, from the current context" e.g. --room *
 *   2. "*" as a Cmd.Token means "anything and everything remaining in argv"
 */

// supported time formats for ParamInst.Time()
var TimeFormats = [...]string{
	"2006-01-02",
	"2006-01-02-07:00",
	"2006-01-02T15:04",
	"2006-01-02T15:04-07:00",
	"2006-01-02T15:04:05",
	"2006-01-02T15:04:05-07:00",
}

// Cmd models a tree of commands and subcommands along with their parameters.
// The tree will almost always be 1 or 2 levels deep. Deeper is possible but
// unlikely to be much higher, KISS.
// TODO: switc to maps for (kv|bool|idx)params and maybe subCmds
type Cmd struct {
	token      string // * => slurp everything remaining
	usage      string
	subCmds    []*SubCmd
	kvparams   []*KVParam
	boolparams []*BoolParam
	idxparams  map[int]*IdxParam
	aliases    []string
	prev       *Cmd // parent command, nil for root
	mustSubCmd bool // a subcommand is always required
}

type SubCmd struct {
	cmd *Cmd
	Cmd
}

type CmdInst struct {
	cmd            *Cmd
	subCmdInst     *SubCmdInst
	kvparaminsts   []*KVParamInst
	boolparaminsts []*BoolParamInst
	idxparaminsts  map[int]*IdxParamInst
	remainder      []string // args left over after parsing, usually empty
}

type SubCmdInst struct {
	subCmd *SubCmd
	CmdInst
}

// key/value parameters, e.g. "--foo=bar", "foo=bar", "-f bar", "--foo bar"
type KVParam struct {
	key      string   // the "foo" in --foo, -f, foo=bar
	aliases  []string // parameter aliases, e.g. foo => f
	usage    string   // usage string for generating help
	required bool     // whether or not this parameter is required
	def      string   // default value when omitted
	hasdef   bool     // whether or not a default has been provided
	cmd      *Cmd     // the (top-level) command the param is attached to
	subcmd   *SubCmd  // the subcommand the param is attached to
}

// keyed parameters that are boolean (flags), e.g. "--foo", "-f", "foo=true"
type BoolParam struct {
	key      string
	aliases  []string
	usage    string
	required bool
	def      bool
	hasdef   bool
	cmd      *Cmd
	subcmd   *SubCmd
}

// positional parameters (0 indexed)
type IdxParam struct {
	idx      int // positional arg index
	name     string
	usage    string
	required bool
	def      string
	hasdef   bool
	cmd      *Cmd
	subcmd   *SubCmd
}

// KVParamInst represents a key/value parameter found in the command
type KVParamInst struct {
	cmdinst    *CmdInst    // the top-level command
	subcmdinst *SubCmdInst // the subcommand the param belongs to, nil for top-level
	param      *KVParam
	found      bool   // was the parameter set in the command?
	isdef      bool   // was the parameter set using a default?
	arg        string // the original/unmodified argument (e.g. --foo, -f)
	key        string // the key, e.g. "foo"
	value      string
}

// BoolParamInst represents a flag/boolean parameter found in the command
type BoolParamInst struct {
	cmdinst    *CmdInst
	subcmdinst *SubCmdInst
	param      *BoolParam
	found      bool
	isdef      bool
	arg        string
	key        string
	value      bool
}

// IdxParamInst represents a positional parameter found in the command
type IdxParamInst struct {
	cmdinst    *CmdInst
	subcmdinst *SubCmdInst
	param      *IdxParam
	found      bool
	isdef      bool
	idx        int
	name       string
	value      string
}

// tmpParamInst used by the parser to hold keyed parameters before attaching to commands/subcommands.
type tmpParamInst struct {
	cmd        *Cmd
	cmdinst    *CmdInst
	subcmd     *SubCmd
	subcmdinst *SubCmdInst
	found      bool
	arg        string
	key        string
	value      string
}

type stringValuedParamInst interface {
	Found() bool
	Required() bool
	Value() string
	String() (string, error)
	Int() (int, error)
	Float() (float64, error)
	Bool() (bool, error)
	errParam() NamedParam
}

// cmdorsubcmd is used internally to pass either a Cmd or SubCmd
// to a helper function so I don't have to copy/paste the code
type cmdorsubcmd interface {
	HasKVParam(string) bool
	HasBoolParam(string) bool
	HasIdxParam(int) bool
	GetKVParam(string) *KVParam
	GetBoolParam(string) *BoolParam
	GetIdxParam(int) *IdxParam
	appendKVParamInst(*KVParamInst)
	appendBoolParamInst(*BoolParamInst)
	appendIdxParamInst(*IdxParamInst)
}

type NamedParam interface {
	Name() string
	Usage() string
	IsRequired() bool
}

type SubCmdNotFound struct {
	argv []string
}

func (e SubCmdNotFound) Error() string {
	return fmt.Sprintf("A subcommand is required but %q was provided.", strings.Join(e.argv, " "))
}

// RequiredParamNotFound is returned when a parameter has Required=true
// and a method was used to access the value but no value was set in the
// command.
type RequiredParamNotFound struct {
	Param NamedParam
}

// Error fulfills the Error interface.
func (e RequiredParamNotFound) Error() string {
	return fmt.Sprintf("Parameter %q is required but not set.", e.Param.Name())
}

// UnsupportedTimeFormatError is returned when a provided time string cannot
// be parsed with one of the pre-defined time formats.
type UnsupportedTimeFormatError struct {
	value string
}

// Error fulfills the Error interface for UnsupportedTimeFormatError.
func (e UnsupportedTimeFormatError) Error() string {
	return fmt.Sprintf("Time string %q does not appear to be in a supported format.", e.value)
}

// NewCmd returns an initialized Cmd.
func NewCmd(token string, mustsubcmd bool) *Cmd {
	cmd := Cmd{token: token, mustSubCmd: mustsubcmd}
	return &cmd
}

// ListSubCmds makes sure the SubCmds list is initialized and returns the list.
func (c *Cmd) ListSubCmds() []*SubCmd {
	if c.subCmds == nil {
		c.subCmds = make([]*SubCmd, 0)
	}

	return c.subCmds
}

// _kvparams makes sure the _kvparams list is initialized and returns the list.
func (c *Cmd) _kvparams() []*KVParam {
	if c.kvparams == nil {
		c.kvparams = make([]*KVParam, 0)
	}

	return c.kvparams
}

// _boolparams makes sure the _boolparams list is initialized and returns the list.
func (c *Cmd) _boolparams() []*BoolParam {
	if c.boolparams == nil {
		c.boolparams = make([]*BoolParam, 0)
	}

	return c.boolparams
}

// _idxparams makes sure the _idxparams map is initialized and returns the map.
func (c *Cmd) _idxparams() map[int]*IdxParam {
	if c.idxparams == nil {
		c.idxparams = make(map[int]*IdxParam)
	}

	return c.idxparams
}

// Aliases makes sure the Aliases list is initialized and returns the list.
func (c *Cmd) Aliases() []string {
	if c.aliases == nil {
		c.aliases = make([]string, 0)
	}

	return c.aliases
}

// assertZeroIdxParams panics if there are any IdxParam defined.
func (c *Cmd) assertZeroIdxParams() {
	pps := c._idxparams()
	if len(pps) > 0 {
		log.Panic("Illegal mixing of positional and key/value parameters.")
	}
}

// assertZeroKeyParams panics if there are any BoolParam or KVParam defined.
func (c *Cmd) assertZeroKeyParams() {
	kps := c._kvparams()
	bps := c._boolparams()
	if len(kps) > 0 || len(bps) > 0 {
		log.Panic("Illegal mixing of positional and key/value parameters.")
	}
}

// AddKVParam creates and adds a key/value parameter to the command handle
// and returns the new parameter.
func (c *Cmd) AddKVParam(key string, required bool) *KVParam {
	c.assertZeroIdxParams()

	p := KVParam{key: key}
	p.required = required
	p.cmd = c.Cmd()

	c.kvparams = append(c._kvparams(), &p)

	return &p
}

// AddBoolParam adds a boolean/flag parameter to the command and returns the
// new parameter.
func (c *Cmd) AddBoolParam(key string, required bool) *BoolParam {
	c.assertZeroIdxParams()

	p := BoolParam{}
	p.key = key
	p.required = required
	p.cmd = c.Cmd()

	c.boolparams = append(c._boolparams(), &p)

	return &p
}

// AddIdxParam adds a positional parameter to the command and returns the
// new parameter.
func (c *Cmd) AddIdxParam(position int, name string, required bool) *IdxParam {
	c.assertZeroKeyParams()

	ips := c._idxparams()

	if _, exists := ips[position]; exists {
		log.Panicf("position %d already has an IdxParam defined on this command", position)
	}

	ips[position] = &IdxParam{
		idx:      position,
		name:     name,
		usage:    name, // what you want most of the time
		required: required,
		cmd:      c.Cmd(),
	}

	return ips[position]
}

// AddKVParam creates and adds a key/value parameter to the subcommand
// and returns the new parameter.
func (c *SubCmd) AddKVParam(key string, required bool) *KVParam {
	c.assertZeroIdxParams()

	p := KVParam{key: key}
	p.required = required
	p.cmd = c.cmd
	p.subcmd = c

	c.kvparams = append(c._kvparams(), &p)

	return &p
}

// AddBoolParam adds a boolean/flag parameter to the subcommand and returns the
// new parameter.
func (c *SubCmd) AddBoolParam(key string, required bool) *BoolParam {
	c.assertZeroIdxParams()

	p := BoolParam{}
	p.key = key
	p.required = required
	p.cmd = c.cmd
	p.subcmd = c

	c.boolparams = append(c._boolparams(), &p)

	return &p
}

// AddIdxParam adds a positional parameter to the subcommand and returns the
// new parameter.
func (c *SubCmd) AddIdxParam(position int, name string, required bool) *IdxParam {
	c.assertZeroKeyParams()

	ips := c._idxparams()

	if _, exists := ips[position]; exists {
		log.Panicf("position %d already has an IdxParam defined on this subcommand", position)
	}

	ips[position] = &IdxParam{
		idx:      position,
		name:     name,
		usage:    name, // what you want most of the time
		required: required,
		cmd:      c.cmd,
		subcmd:   c,
	}

	return ips[position]
}

// AddAlias adds an alias to the command and returns the paramter.
func (c *Cmd) AddAlias(alias string) *Cmd {
	c.aliases = append(c.Aliases(), alias)
	return c
}

func (s *SubCmd) AddAlias(alias string) *SubCmd {
	s.aliases = append(s.Aliases(), alias)
	return s
}

// AddAlias adds an alias to the parameter and returns the paramter.
func (p *KVParam) AddAlias(alias string) *KVParam {
	p.aliases = append(p.Aliases(), alias)
	return p
}

func (c *Cmd) Parent() *Cmd {
	return c.prev
}

// MustSubCmd returns bool indicating if a subcommand is required.
func (c *Cmd) MustSubCmd() bool {
	return c.mustSubCmd
}

// Usage returns the auto-generated usage string.
func (c *Cmd) Usage() string {
	out := make([]string, 1)
	out[0] = c.token + " - " + c.usage

	for _, scmd := range c.ListSubCmds() {
		params := scmd.ListNamedParams()
		opttxt := make([]string, len(params))

		for i, p := range params {
			var txt, required string

			if p.IsRequired() {
				required = " (required)"
			}

			switch p.(type) {
			case *KVParam:
				txt = fmt.Sprintf("-%s <%s>", p.Name(), p.Name())
			case *BoolParam:
				txt = fmt.Sprintf("-%s", p.Name())
			case *IdxParam:
				txt = p.Name()
			}

			opttxt[i] = fmt.Sprintf("\t\t%s%s: %s", txt, required, p.Usage())
		}

		out = append(out, "\t"+scmd.Usage())
		out = append(out, opttxt...)
	}

	return strings.Join(out, "\n")
}

// SetUsage sets the usage string for the command. Returns the command.
func (c *Cmd) SetUsage(usage string) *Cmd {
	c.usage = usage
	return c
}

// SetUsage sets the subcommand's usage string.
func (s *SubCmd) SetUsage(usage string) *SubCmd {
	s.usage = usage
	return s
}

// Usage returns the auto-generated usage string for the Command Instance.
func (c *CmdInst) Usage() string {
	return c.cmd.Usage()
}

func (p *KVParam) Usage() string {
	return p.usage
}

func (p *BoolParam) Usage() string {
	return p.usage
}

func (p *IdxParam) Usage() string {
	return p.usage
}

// SetUsage sets the usage string for the paremeter. Returns the parameter.
func (p *KVParam) SetUsage(usage string) *KVParam {
	p.usage = usage
	return p
}

// SetUsage sets the usage string for the paremeter. Returns the parameter.
func (p *BoolParam) SetUsage(usage string) *BoolParam {
	p.usage = usage
	return p
}

// SetUsage sets the usage string for the paremeter. Returns the parameter.
func (p *IdxParam) SetUsage(usage string) *IdxParam {
	p.usage = usage
	return p
}

func (p *KVParam) SetDefault(def string) *KVParam {
	p.def = def
	p.hasdef = true
	return p
}

func (p *BoolParam) SetDefault(def bool) *BoolParam {
	p.def = def
	p.hasdef = true
	return p
}

func (p *IdxParam) SetDefault(def string) *IdxParam {
	p.def = def
	p.hasdef = true
	return p
}

func (p *KVParam) Key() string {
	return p.key
}

func (p *BoolParam) Key() string {
	return p.key
}

func (p *IdxParam) Idx() int {
	return p.idx
}

func (p *KVParamInst) Key() string {
	return p.key
}

func (p *BoolParamInst) Key() string {
	return p.key
}

func (p *IdxParamInst) Idx() int {
	return p.idx
}

// Name returns the key string. Mostly for use in printing errors, etc.
// Implements NamedParam.
func (p *KVParam) Name() string {
	return p.key
}

// Name returns the key string. Mostly for use in printing errors, etc.
// Implements NamedParam.
func (p *BoolParam) Name() string {
	return p.key
}

// Name returns the name given to the indexed param.
// Implements NamedParam.
func (p *IdxParam) Name() string {
	return p.name
}

func (p *KVParam) IsRequired() bool {
	return p.required
}
func (p *BoolParam) IsRequired() bool {
	return p.required
}
func (p *IdxParam) IsRequired() bool {
	return p.required
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *KVParam) Cmd() *Cmd {
	if p.cmd == nil {
		panic("Can't call Cmd() on this KVParam because it is not attached to a Cmd!")
	}

	return p.cmd
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *BoolParam) Cmd() *Cmd {
	if p.cmd == nil {
		panic("Can't call Cmd() on this BoolParam because it is not attached to a Cmd!")
	}

	return p.cmd
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *IdxParam) Cmd() *Cmd {
	if p.cmd == nil {
		panic("Can't call Cmd() on this IdxParam because it is not attached to a Cmd!")
	}

	return p.cmd
}

func (p *KVParam) SubCmd() *SubCmd {
	if p.subcmd == nil {
		panic("Can't call SubCmd() on this KVParam because it is not attached to a SubCmd!")
	}

	return p.subcmd
}

func (p *BoolParam) SubCmd() *SubCmd {
	if p.subcmd == nil {
		panic("Can't call SubCmd() on this BoolParam because it is not attached to a SubCmd!")
	}

	return p.subcmd
}

func (p *IdxParam) SubCmd() *SubCmd {
	if p.subcmd == nil {
		panic("Can't call SubCmd() on this IdxParam because it is not attached to a SubCmd!")
	}

	return p.subcmd
}

func (p *KVParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value string) *KVParamInst {
	return &KVParamInst{
		cmdinst:    cmdinst,
		subcmdinst: subcmdinst,
		isdef:      isdef,
		param:      p,
		key:        p.key,
		value:      value,
	}
}

func (p *BoolParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value bool) *BoolParamInst {
	return &BoolParamInst{
		cmdinst:    cmdinst,
		subcmdinst: subcmdinst,
		isdef:      isdef,
		param:      p,
		key:        p.key,
		value:      value,
	}
}

func (p *IdxParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value string) *IdxParamInst {
	return &IdxParamInst{
		cmdinst:    cmdinst,
		subcmdinst: subcmdinst,
		isdef:      isdef,
		param:      p,
		idx:        p.idx,
		name:       p.name,
		value:      value,
	}
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *KVParamInst) Cmd() *Cmd {
	if p.param == nil {
		panic("Can't call Cmd() on this KVParamInst because it is not attached to a KVParam!")
	}

	return p.param.Cmd()
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *BoolParamInst) Cmd() *Cmd {
	if p.param == nil {
		panic("Can't call Cmd() on this BoolParamInst because it is not attached to a BoolPararm!")
	}

	return p.param.Cmd()
}

// Cmd returns the command the parameter belongs to. Panics if no command is attached.
func (p *IdxParamInst) Cmd() *Cmd {
	if p.param == nil {
		panic("Can't call Cmd() on this IdxParamInst because it is not attached to a IdxParam!")
	}

	return p.param.Cmd()
}

func (p *KVParamInst) SubCmdInst() *SubCmdInst {
	if p.subcmdinst == nil {
		panic("Can't call SubCmdInst() on this KVParamInst because it is not attached to a SubCmdInst!")
	}

	return p.subcmdinst
}

func (p *BoolParamInst) SubCmdInst() *SubCmdInst {
	if p.subcmdinst == nil {
		panic("Can't call SubCmdInst() on this BoolParamInst because it is not attached to a SubCmdInst!")
	}

	return p.subcmdinst
}

func (p *IdxParamInst) SubCmdInst() *SubCmdInst {
	if p.subcmdinst == nil {
		panic("Can't call SubCmdInst() on this IdxParamInst because it is not attached to a SubCmd!")
	}

	return p.subcmdinst
}

func (p *KVParamInst) Found() bool {
	return p.found
}

func (p *BoolParamInst) Found() bool {
	return p.found
}

func (p *IdxParamInst) Found() bool {
	return p.found
}

func (p *KVParamInst) Required() bool {
	return p.param.required
}

func (p *BoolParamInst) Required() bool {
	return p.param.required
}

func (p *IdxParamInst) Required() bool {
	return p.param.required
}

func (p *KVParamInst) Param() *KVParam {
	return p.param
}

func (p *BoolParamInst) Param() *BoolParam {
	return p.param
}

func (p *IdxParamInst) Param() *IdxParam {
	return p.param
}

// errParam is used to get an interface{} handle to return in errors.
// See: RequiredParamNotFound
func (p *KVParamInst) errParam() NamedParam {
	return p.param
}

// errParam is used to get an interface{} handle to return in errors.
func (p *BoolParamInst) errParam() NamedParam {
	return p.param
}

// errParam is used to get an interface{} handle to return in errors.
func (p *IdxParamInst) errParam() NamedParam {
	return p.param
}

// Cmd returns the command it was called on. It does nothing and exists to
// make it possible to format chained calls nicely.
func (c *Cmd) Cmd() *Cmd {
	return c
}

func (s *SubCmd) SubCmd() *SubCmd {
	return s
}

func (c *Cmd) Token() string {
	return c.token
}

// AddCmd adds a subcommand to the handle and returns the new (sub-)command.
func (c *Cmd) AddSubCmd(token string) *SubCmd {
	sub := SubCmd{}
	sub.prev = c
	sub.token = token

	c.subCmds = append(c.ListSubCmds(), &sub)

	return &sub
}

func (c *Cmd) GetKVParam(key string) *KVParam {
	for _, p := range c._kvparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil")
}

func (c *Cmd) GetBoolParam(key string) *BoolParam {
	for _, p := range c._boolparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil")
}

// GetIdxParam gets a positional parameter by its index.
func (c *Cmd) GetIdxParam(idx int) *IdxParam {
	ips := c._idxparams()

	if p, exists := ips[idx]; exists {
		return p
	}

	panic("BUG: refusing to return nil")
}

func (c *Cmd) HasKVParam(key string) bool {
	for _, p := range c._kvparams() {
		if p.key == key {
			return true
		}
	}

	return false
}

func (c *Cmd) HasBoolParam(key string) bool {
	for _, p := range c._boolparams() {
		if p.key == key {
			return true
		}
	}

	return false
}

func (c *Cmd) HasIdxParam(idx int) bool {
	ips := c._idxparams()
	_, exists := ips[idx]
	return exists
}

// TODO: remove this?
func (c *Cmd) SubCmds() []*SubCmd {
	return c.ListSubCmds()
}

// GetSubCmd gets a subcommand by its token. Returns nil for no match.
func (c *Cmd) GetSubCmd(token string) *SubCmd {
	for _, s := range c.ListSubCmds() {
		if s.token == token {
			return s
		}
	}

	panic("BUG: refusing to return nil")
}

// parse a list of argv-style strings (0 is always the command name e.g. []string{"prefs"})
// foo bar --baz
// foo --bar baz --version
// foo bar=baz
// foo x=y z=q init --foo baz
// TODO: automatic emdash cleanup
// TODO: enforce MustSubCmd
// TODO: return errors instead of nil/panic
func (c *Cmd) Process(argv []string) (*CmdInst, error) {
	// a hand-coded argument processor that evaluates the provided argv list
	// against the command definition and returns a CmdInst with all of the
	// available data parsed and ready to use with CmdInst/ParamInst methods.

	// the top-level command instance
	topInst := CmdInst{cmd: c}

	// no arguments were provided
	if len(argv) == 1 {
		if c.mustSubCmd {
			return nil, SubCmdNotFound{argv: argv}
		} else {
			return &topInst, nil
		}
	}

	var curSubCmdInst *SubCmdInst // the current subcommand - changes during parsing
	var curSubCmdIdx int          // the idx the subcommand found in argv
	var skipNext bool
	var looseParams []*tmpParamInst

	// first pass: extract subcommands and parameters
	for i, arg := range argv[1:] {
		if skipNext {
			skipNext = false
			continue
		}

		var key, value, next string
		var nextExists bool

		if i+2 < len(argv) {
			next = argv[i+2]
			nextExists = true
		} else {
			nextExists = false
		}

		if c.HasIdxParam(i - 1) {
			// top-level command has positional parameters
			pi := IdxParamInst{
				cmdinst: &topInst,
				found:   true,
				idx:     i - 1,
				param:   c.GetIdxParam(i - 1),
				value:   arg,
			}

			topInst.appendIdxParamInst(&pi)
		} else if curSubCmdInst != nil && curSubCmdInst.HasIdxParam(0) {
			// subcommand has positional parameters
			paramIdx := i - curSubCmdIdx - 1

			pi := IdxParamInst{
				cmdinst:    &topInst,
				subcmdinst: curSubCmdInst,
				found:      true,
				idx:        paramIdx,
				param:      curSubCmdInst.GetIdxParam(paramIdx),
				value:      arg,
			}

			curSubCmdInst.appendIdxParamInst(&pi)
		} else if strings.Contains(arg, "=") {
			// looks like a key=value or --key=value parameter
			// could be --foo=bar but all that matters is the "foo"
			// could be --foo=true for BoolParam and that's fine too
			kv := strings.SplitN(arg, "=", 2)
			key = strings.TrimLeft(kv[0], "-")
			value = kv[1]
			// falls through, further processing below this if block...
		} else if looksLikeParam(arg) {
			// looks like a parameter
			// e.g. --foo bar -f bar
			key = strings.TrimLeft(arg, "-")
			// TODO: this handles many instances of boolean flags that indicate true
			// simply by being present. There are likely some edge cases where this
			// doesn't work because the following param doesn't look like a param
			// then again maybe it's no big deal...
			if nextExists && !looksLikeParam(next) {
				value = next
				skipNext = true
			}
			// falls through, further processing below this if block...
		} else if curSubCmdInst == nil && c.HasSubCmdToken(arg) {
			// the first subcommand - the "foo" in "!command foo bar --baz"
			for _, sc := range topInst.cmd.ListSubCmds() {
				if sc.token == arg {
					sci := SubCmdInst{subCmd: sc}
					sci.cmd = c
					curSubCmdInst = &sci
					topInst.subCmdInst = &sci
					break
				}
			}

			continue // processed a subcommand, move onto the next arg
		} else if curSubCmdInst != nil && curSubCmdInst.subCmd.HasSubCmdToken(arg) {
			// sub-subcommands - the "bar" or "blargh" in "!command foo bar blargh --baz"
			for _, sc := range curSubCmdInst.subCmd.ListSubCmds() {
				if arg == sc.token {
					sci := SubCmdInst{subCmd: sc}
					sci.cmd = c

					// point the current subcommand to the new one
					curSubCmdInst.subCmdInst = &sci

					// advance "current" to the new subcommand
					curSubCmdInst = &sci

					// set the index where the subcommand was discovered for use
					// in extracting postitional parameters (above)
					curSubCmdIdx = i
				}
			}

			continue // processed a subcommand, move onto the next arg
		} else {
			// leftover/unrecognized args go in .remainder
			topInst.remainder = append(topInst.Remainder(), arg)
			continue
		}

		pinst := tmpParamInst{
			key:     key,
			arg:     arg,
			value:   value,
			found:   true,
			cmd:     c,
			cmdinst: &topInst,
		}

		// the most recent subcommand seen gets the first shot at a parameter
		// !foo --bar baz --bar
		// !foo baz --bar
		// !foo --bar baz
		if curSubCmdInst != nil && curSubCmdInst.subCmd.HasKeyParam(key) {
			// the parameter belongs to the subcommand
			pinst.subcmd = curSubCmdInst.subCmd
			pinst.subcmdinst = curSubCmdInst
			pinst.attachKeyParam(curSubCmdInst)
		} else if c.HasKeyParam(key) {
			// the parameter belongs to the command
			pinst.attachKeyParam(&topInst)
		} else {
			// store (likely) out-of-order parameters to process after all args &
			// subcommands are discovered
			looseParams = append(looseParams, &pinst)
		}
	}

	if c.mustSubCmd && topInst.subCmdInst == nil {
		return nil, SubCmdNotFound{argv: argv}
	}

	// find a home for out-of-order parameters, panic if that fails since it's a bug
	for _, linst := range looseParams {
		if topInst.subCmdInst == nil {
			panic("found out-of-order params but no subcommand! Maybe bug, maybe I need to put a better error here...")
		}
		linst.findAndAttachKeyParam(topInst.subCmdInst)
	}

	// now that all the parameters have been parsed and attached to a *cmdInst,
	// find required parameters with defaults and create param instances
	// or return an error when a required param isn't found

	// check for required parameters on the command
	for _, p := range c.kvparams {
		if p.required && !topInst.HasKVParamInst(p.Key()) {
			if p.hasdef {
				pinst := p.newInst(&topInst, nil, true, p.def)
				topInst.appendKVParamInst(pinst)
			} else {
				return nil, RequiredParamNotFound{p}
			}
		}
	}

	for _, p := range c.boolparams {
		if p.required && !topInst.HasBoolParamInst(p.Key()) {
			if p.hasdef {
				pinst := p.newInst(&topInst, nil, true, p.def)
				topInst.appendBoolParamInst(pinst)
			} else {
				return nil, RequiredParamNotFound{p}
			}
		}
	}

	for idx, p := range c.idxparams {
		if p.required && !topInst.HasIdxParamInst(idx) {
			if p.hasdef {
				pinst := p.newInst(&topInst, nil, true, p.def)
				topInst.appendIdxParamInst(pinst)
			} else {
				return nil, RequiredParamNotFound{p}
			}
		}
	}

	// check subcommands one by one
	for _, sci := range topInst.listSubCmdInst() {
		// go over each parameter and see if it's present
		for _, p := range sci.subCmd._kvparams() {
			// see if it's required and was not found
			if p.required && !sci.HasKVParamInst(p.Key()) {
				// if there is a default, use it
				if p.hasdef {
					pinst := p.newInst(&topInst, sci, true, p.def)
					sci.appendKVParamInst(pinst)
				} else {
					// required parameter was missing, return an error
					return nil, RequiredParamNotFound{p}
				}
			}
		}
		for _, p := range sci.subCmd._boolparams() {
			if p.required && !sci.HasBoolParamInst(p.Key()) {
				if p.hasdef {
					pinst := p.newInst(&topInst, sci, true, p.def)
					sci.appendBoolParamInst(pinst)
				} else {
					return nil, RequiredParamNotFound{p}
				}
			}
		}
		for _, p := range sci.subCmd._idxparams() {
			if p.required && !sci.HasIdxParamInst(p.Idx()) {
				if p.hasdef {
					pinst := p.newInst(&topInst, sci, true, p.def)
					sci.appendIdxParamInst(pinst)
				} else {
					return nil, RequiredParamNotFound{p}
				}
			}
		}
	}

	return &topInst, nil
}

// looksLikeBool checks to see if the provided value contains "true" or "false"
// in any case combination.
func looksLikeBool(val string) bool {
	lcval := strings.ToLower(val)

	if strings.Contains(lcval, "true") {
		return true
	}

	if strings.Contains(lcval, "false") {
		return true
	}

	return false
}

// looksLikeParam returns true if there is a leading - or an = in the string.
func looksLikeParam(key string) bool {
	if strings.HasPrefix(key, "-") {
		return true
	} else if strings.Contains(key, "=") {
		return true
	} else {
		return false
	}
}

func (tmp *tmpParamInst) attachKeyParam(whatever cmdorsubcmd) {
	var haskvparam, hasboolparam bool
	switch whatever.(type) {
	case *SubCmdInst:
		i := whatever.(*SubCmdInst)
		haskvparam = i.subCmd.HasKVParam(tmp.key)
		hasboolparam = i.subCmd.HasBoolParam(tmp.key)
	case *CmdInst:
		i := whatever.(*CmdInst)
		haskvparam = i.cmd.HasKVParam(tmp.key)
		hasboolparam = i.cmd.HasBoolParam(tmp.key)
	}

	if haskvparam {
		p := whatever.GetKVParam(tmp.key)
		pi := KVParamInst{
			arg:        tmp.arg,
			cmdinst:    tmp.cmdinst,
			found:      tmp.found,
			key:        tmp.key,
			param:      p,
			subcmdinst: tmp.subcmdinst,
			value:      tmp.value,
		}

		switch whatever.(type) {
		case *CmdInst:
			ci := whatever.(*CmdInst)
			ci.kvparaminsts = append(ci.ListKVParamInsts(), &pi)
		case *SubCmdInst:
			sci := whatever.(*SubCmdInst)
			sci.kvparaminsts = append(sci.ListKVParamInsts(), &pi)
		}
	} else if hasboolparam {
		var val bool
		var err error
		// a provided flag with an empty value is true
		if tmp.found && tmp.value == "" {
			val = true
		} else {
			val, err = strconv.ParseBool(tmp.value)
			if err != nil {
				log.Panicf("invalid bool value %q for key %q", tmp.value, tmp.key)
			}
		}

		p := whatever.GetBoolParam(tmp.key)
		pi := BoolParamInst{
			arg:        tmp.arg,
			cmdinst:    tmp.cmdinst,
			found:      tmp.found,
			key:        tmp.key,
			param:      p,
			subcmdinst: tmp.subcmdinst,
			value:      val,
		}

		switch whatever.(type) {
		case *CmdInst:
			ci := whatever.(*CmdInst)
			ci.boolparaminsts = append(ci.ListBoolParamInsts(), &pi)
		case *SubCmdInst:
			sci := whatever.(*SubCmdInst)
			sci.boolparaminsts = append(sci.ListBoolParamInsts(), &pi)
		}
	} else {
		log.Panicf("BUG: arg %q does not have a matching parameter for key %q", tmp.arg, tmp.key)
	}
}

func (tmp *tmpParamInst) findAndAttachKeyParam(sub *SubCmdInst) {
	if sub.HasBoolParam(tmp.key) || sub.HasKVParam(tmp.key) {
		tmp.attachKeyParam(sub)
	} else if sub.subCmdInst != nil {
		tmp.findAndAttachKeyParam(sub.subCmdInst)
	}
}

// listSubCmdInst returns a list of subcommand instances from the command line
// in natural order, e.g. "cmd sub1 -f sub2 -x sub3 -y" => [sub1, sub2, sub3]
func (c *CmdInst) listSubCmdInst() []*SubCmdInst {
	var i int
	out := make([]*SubCmdInst, 0)

	if c.subCmdInst == nil {
		return out
	}

	out = append(out, c.subCmdInst)

	for {
		if out[i].subCmdInst != nil {
			out = append(out, out[i].subCmdInst)
			i++
		} else {
			break
		}
	}

	return out
}

// HasSubCmdToken returns whether or not the proivded token is defined as a subcommand.
func (c *Cmd) HasSubCmdToken(token string) bool {
	if c == nil {
		return false
	}

	for _, sc := range c.ListSubCmds() {
		if token == sc.token {
			return true
		}
	}

	return false
}

// HasKeyParam returns true if there are any parameters defined with
// the provided key of either key type (bool or kv).
func (c *Cmd) HasKeyParam(key string) bool {
	if c == nil {
		return false
	}

	for _, p := range c._boolparams() {
		if key == p.key {
			return true
		}
	}

	for _, p := range c._kvparams() {
		if key == p.key {
			return true
		}
	}

	return false
}

// ListNamedParams returns a list of all parameters via the interface NamedParam.
// Mainly for use in printing options, etc..
func (c *Cmd) ListNamedParams() []NamedParam {
	out := make([]NamedParam, 0)

	for _, p := range c._boolparams() {
		out = append(out, p)
	}

	for _, p := range c._kvparams() {
		out = append(out, p)
	}

	// use 2 passes to append idx parameters in order
	ipm := c._idxparams()
	ips := make([]*IdxParam, len(ipm))
	for _, p := range c._idxparams() {
		ips[p.idx] = p
	}
	for _, p := range ips {
		out = append(out, p)
	}

	return out
}

// SubCmdToken returns the subcommand's token string. Returns empty string
// if there is no subcommand.
func (c *CmdInst) SubCmdToken() string {
	if c.subCmdInst != nil {
		return c.subCmdInst.subCmd.token
	}

	return ""
}

func (c *SubCmdInst) SubCmdToken() string {
	if c.subCmdInst != nil {
		return c.subCmdInst.subCmd.token
	}

	return ""
}

func (c *CmdInst) SubCmdInst() *SubCmdInst {
	return c.subCmdInst
}

func (c *CmdInst) HasKVParamInst(key string) bool {
	for _, p := range c.ListKVParamInsts() {
		if p.key == key {
			return true
		}
	}

	return false
}

func (c *CmdInst) HasKVParam(key string) bool {
	return c.cmd.HasKVParam(key)
}

func (c *SubCmdInst) HasKVParam(key string) bool {
	return c.subCmd.HasKVParam(key)
}

func (c *CmdInst) HasBoolParamInst(key string) bool {
	for _, p := range c.ListBoolParamInsts() {
		if p.key == key {
			return true
		}
	}

	return false
}

func (c *CmdInst) HasBoolParam(key string) bool {
	return c.cmd.HasBoolParam(key)
}

func (c *CmdInst) HasIdxParamInst(idx int) bool {
	ipis := c.mapIdxParamInsts()
	_, exists := ipis[idx]
	return exists
}

func (c *CmdInst) HasIdxParam(idx int) bool {
	return c.cmd.HasIdxParam(idx)
}

func (c *SubCmdInst) HasIdxParam(idx int) bool {
	return c.subCmd.HasIdxParam(idx)
}

// GetKVParamInst gets a key/value parameter instance by its key.
func (c *CmdInst) GetKVParamInst(key string) *KVParamInst {
	for _, p := range c.ListKVParamInsts() {
		if p.key == key {
			return p
		}
	}

	if c.HasKVParam(key) {
		// not provided, return empty value with found: false
		return &KVParamInst{
			param:   c.GetKVParam(key),
			key:     key,
			cmdinst: c,
		}
	} else {
		panic("BUG: invalid KVParam key '" + key + "'")
	}

}

// GetKVParamInst gets a key/value parameter instance by its key.
func (c *SubCmdInst) GetKVParamInst(key string) *KVParamInst {
	for _, p := range c.ListKVParamInsts() {
		if p.key == key {
			return p
		}
	}

	if c.HasKVParam(key) {
		// not provided, return empty value with found: false
		return &KVParamInst{
			param:      c.GetKVParam(key),
			key:        key,
			cmdinst:    &c.CmdInst,
			subcmdinst: c,
		}
	} else {
		panic("BUG: invalid KVParam key '" + key + "'")
	}
}

func (c *CmdInst) GetKVParam(key string) *KVParam {
	for _, p := range c.cmd._kvparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil")
}

func (c *SubCmdInst) GetKVParam(key string) *KVParam {
	for _, p := range c.subCmd._kvparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil" + key)
}

// GetBoolParamInst gets a key/value parameter instance by its key.
func (c *CmdInst) GetBoolParamInst(key string) *BoolParamInst {
	for _, p := range c.ListBoolParamInsts() {
		if p.key == key {
			return p
		}
	}

	// not provided, return empty value with found: false
	if c.HasBoolParam(key) {
		return &BoolParamInst{
			param:   c.GetBoolParam(key),
			key:     key,
			cmdinst: c,
		}
	} else {
		panic("BUG: invalid BoolParam key '" + key + "'")
	}

}

// GetBoolParamInst gets a key/value parameter instance by its key.
func (c *SubCmdInst) GetBoolParamInst(key string) *BoolParamInst {
	for _, p := range c.ListBoolParamInsts() {
		if p.key == key {
			return p
		}
	}

	// not provided, return empty value with found: false
	if c.HasBoolParam(key) {
		return &BoolParamInst{
			param:      c.GetBoolParam(key),
			key:        key,
			cmdinst:    &c.CmdInst,
			subcmdinst: c,
		}
	} else {
		panic("BUG: invalid BoolParam key '" + key + "'")
	}
}

func (c *CmdInst) GetBoolParam(key string) *BoolParam {
	for _, p := range c.cmd._boolparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil")
}

func (c *SubCmdInst) GetBoolParam(key string) *BoolParam {
	for _, p := range c.subCmd._boolparams() {
		if p.key == key {
			return p
		}
	}

	panic("BUG: refusing to return nil")
}

// GetIdxParamInst gets a positional parameter instance by its index.
func (c *CmdInst) GetIdxParamInst(idx int) *IdxParamInst {
	ipis := c.mapIdxParamInsts()
	if p, exists := ipis[idx]; exists {
		return p
	}

	// not provided, return empty value with found: false
	if c.HasIdxParam(idx) {
		return &IdxParamInst{
			param:   c.GetIdxParam(idx),
			idx:     idx,
			cmdinst: c,
		}
	} else {
		panic(fmt.Sprintf("BUG: invalid IdxParam index: %d", idx))
	}
}

// GetIdxParamInst gets an indexed parameter instance by its index.
func (c *SubCmdInst) GetIdxParamInst(idx int) *IdxParamInst {
	ipis := c.mapIdxParamInsts()
	if p, exists := ipis[idx]; exists {
		return p
	}

	// not provided, return empty value with found: false
	if c.HasIdxParam(idx) {
		return &IdxParamInst{
			param:      c.GetIdxParam(idx),
			idx:        idx,
			cmdinst:    &c.CmdInst,
			subcmdinst: c,
		}
	} else {
		panic(fmt.Sprintf("BUG: invalid IdxParam index: %d", idx))
	}
}

// GetIdxParamInsByNamet gets an indexed parameter instance by its name.
func (c *CmdInst) GetIdxParamInstByName(name string) *IdxParamInst {
	ips := c.cmd._idxparams()
	for _, p := range ips {
		if p.name == name {
			return c.GetIdxParamInst(p.idx)
		}
	}

	panic("BUG: No indexed parameter with name: " + name)
}

// GetIdxParamInstByName gets an indexed parameter instance by its name.
func (c *SubCmdInst) GetIdxParamInstByName(name string) *IdxParamInst {
	ips := c.subCmd._idxparams()
	for _, p := range ips {
		if p.name == name {
			return c.GetIdxParamInst(p.idx)
		}
	}

	panic("BUG: No indexed parameter with name: " + name)
}

func (c *CmdInst) GetIdxParam(idx int) *IdxParam {
	ips := c.cmd._idxparams()
	if p, exists := ips[idx]; exists {
		return p
	}

	panic("BUG: refusing to return nil")
}

func (c *SubCmdInst) GetIdxParam(idx int) *IdxParam {
	ips := c.subCmd._idxparams()
	if p, exists := ips[idx]; exists {
		return p
	}

	panic("BUG: refusing to return nil")
}

func (c *CmdInst) appendKVParamInst(pi *KVParamInst) {
	c.kvparaminsts = append(c.ListKVParamInsts(), pi)
}

func (c *CmdInst) appendBoolParamInst(pi *BoolParamInst) {
	c.boolparaminsts = append(c.ListBoolParamInsts(), pi)
}

func (c *CmdInst) appendIdxParamInst(pi *IdxParamInst) {
	ipis := c.mapIdxParamInsts()
	ipis[pi.idx] = pi
}

// ListKVParamInsts initializes the kvparaminsts list on the fly and returns it.
func (c *CmdInst) ListKVParamInsts() []*KVParamInst {
	if c.kvparaminsts == nil {
		c.kvparaminsts = make([]*KVParamInst, 0)
	}

	return c.kvparaminsts
}

// ListBoolParamInsts initializes the boolparaminsts list on the fly and returns it.
func (c *CmdInst) ListBoolParamInsts() []*BoolParamInst {
	if c.boolparaminsts == nil {
		c.boolparaminsts = make([]*BoolParamInst, 0)
	}

	return c.boolparaminsts
}

// mapIdxParamInsts initializes the idxparaminsts list on the fly and returns it.
func (c *CmdInst) mapIdxParamInsts() map[int]*IdxParamInst {
	if c.idxparaminsts == nil {
		c.idxparaminsts = make(map[int]*IdxParamInst)
	}

	return c.idxparaminsts
}

func (c *CmdInst) ListIdxParamInsts() []*IdxParamInst {
	ipis := c.mapIdxParamInsts()
	out := make([]*IdxParamInst, len(ipis))

	for i, pi := range ipis {
		out[i] = pi
	}

	return out
}

// Remainder initializes the remainder list on the fly and returns it.
func (c *CmdInst) Remainder() []string {
	if c.remainder == nil {
		c.remainder = make([]string, 0)
	}

	return c.remainder
}

// Aliases initializes the aliases list on the fly and returns it.
func (p *KVParam) Aliases() []string {
	if p.aliases == nil {
		p.aliases = make([]string, 0)
	}

	return p.aliases
}

func (p *KVParamInst) Value() string {
	return p.value
}

func (p *BoolParamInst) Value() bool {
	return p.value
}

func (p *IdxParamInst) Value() string {
	return p.value
}

// Name returns the key string. Mostly for use in printing errors, etc.
// Implements NamedParam.
func (p *KVParamInst) Name() string {
	return p.key
}

// Name returns the key string. Mostly for use in printing errors, etc.
// Implements NamedParam.
func (p *BoolParamInst) Name() string {
	return p.key
}

// Name returns the name given to the indexed param.
// Implements NamedParam.
func (p *IdxParamInst) Name() string {
	return p.name
}

// String returns the value as a string.
func (p *KVParamInst) String() (string, error) {
	if !p.found && p.param.required {
		return "", RequiredParamNotFound{p.param}
	}

	return p.value, nil
}

// String returns the value as a string.
func (p *BoolParamInst) String() (string, error) {
	if !p.found && p.param.required {
		return "", RequiredParamNotFound{p.param}
	}

	if p.value {
		return "true", nil
	} else {
		return "false", nil
	}
}

// String returns the value as a string.
func (p *IdxParamInst) String() (string, error) {
	if !p.found && p.param.required {
		return "", RequiredParamNotFound{p.param}
	}

	return p.value, nil
}

// intParam returns the value as an int. If the param is required and it was
// not set, RequiredParamNotFound is returned. Additionally, any errors in
// conversion are returned.
func intParam(p stringValuedParamInst) (int, error) {
	if !p.Found() {
		if p.Required() {
			return 0, RequiredParamNotFound{p.errParam()}
		} else {
			return 0, nil
		}
	}

	val, err := strconv.ParseInt(p.Value(), 10, 64)
	return int(val), err // warning: doesn't handle overflow
}

func (p *KVParamInst) Int() (int, error) {
	return intParam(p)
}

func (p *IdxParamInst) Int() (int, error) {
	return intParam(p)
}

// Float returns the value of the parameter as a float. If the value cannot
// be converted, an error will be returned. See: strconv.ParseFloat
func floatParam(p stringValuedParamInst) (float64, error) {
	if !p.Found() {
		if p.Required() {
			return 0, RequiredParamNotFound{p.errParam()}
		} else {
			return 0, nil
		}
	}

	return strconv.ParseFloat(p.Value(), 64)
}

func (p *KVParamInst) Float() (float64, error) {
	return floatParam(p)
}

func (p *IdxParamInst) Float() (float64, error) {
	return floatParam(p)
}

// Bool returns the value of the parameter as a bool.
// If the value is required and not set, returns RequiredParamNotFound.
// If the value cannot be converted, an error will be returned.
// See: strconv.ParseBool
func boolParam(p stringValuedParamInst) (bool, error) {
	if !p.Found() {
		if p.Required() {
			return false, RequiredParamNotFound{p.errParam()}
		} else {
			return false, nil
		}
	}

	stripped := strings.Trim(p.Value(), `'"`)
	return strconv.ParseBool(stripped)
}

func (p *KVParamInst) Bool() (bool, error) {
	return boolParam(p)
}

func (p *IdxParamInst) Bool() (bool, error) {
	return boolParam(p)
}

// Duration returns the value of the parameter as a Go time.Duration.
// Day and Week (e.g. "1w", "1d") are converted to 168 and 24 hours respectively.
// If the value is required and not set, returns RequiredParamNotFound.
// If the value cannot be converted, an error will be returned.
// See: time.ParseDuration
func durationParam(p stringValuedParamInst) (time.Duration, error) {
	duration := p.Value()
	empty := time.Duration(0)

	if !p.Found() {
		if p.Required() {
			return empty, RequiredParamNotFound{p.errParam()}
		} else {
			return empty, nil
		}
	}

	if strings.HasSuffix(duration, "w") {
		weeks, err := strconv.Atoi(strings.TrimSuffix(duration, "w"))
		if err != nil {
			return empty, fmt.Errorf("Could not convert duration %q: %s", duration, err)
		}

		return time.Hour * time.Duration(weeks*24*7), nil
	} else if strings.HasSuffix(duration, "d") {
		days, err := strconv.Atoi(strings.TrimSuffix(duration, "d"))
		if err != nil {
			return empty, fmt.Errorf("Could not convert duration %q: %s", duration, err)
		}
		return time.Hour * time.Duration(days*24), nil
	} else {
		return time.ParseDuration(duration)
	}
}

func (p *KVParamInst) Duration() (time.Duration, error) {
	return durationParam(p)
}

func (p *IdxParamInst) Duration() (time.Duration, error) {
	return durationParam(p)
}

// Time returns the value of the parameter as a Go time.Time.
// Many formats are attempted before giving up.
// If the value is required and not set, returns RequiredParamNotFound.
// If the value cannot be converted, an error will be returned.
// See: TimeFormats in this package
// See: time.ParseDuration
func timeParam(p stringValuedParamInst) (time.Time, error) {
	if !p.Found() {
		if p.Required() {
			return time.Time{}, RequiredParamNotFound{p.errParam()}
		} else {
			return time.Time{}, nil
		}
	}

	t := p.Value()

	// convert Z suffix to +00:00
	if strings.HasSuffix(t, "Z") {
		t = strings.TrimSuffix(t, "Z") + "+00:00"
	}

	// try all of the formats
	for _, fmt := range TimeFormats {
		out, err := time.Parse(fmt, t)
		if err != nil {
			continue
		} else {
			return out, nil
		}
	}

	return time.Time{}, UnsupportedTimeFormatError{t}
}

func (p *KVParamInst) Time() (time.Time, error) {
	return timeParam(p)
}

func (p *IdxParamInst) Time() (time.Time, error) {
	return timeParam(p)
}

// MustString returns the value as a string. If it was required/not-set,
// panic ensues. Empty string may be returned for not-required+not-set.
func (p *KVParamInst) MustString() string {
	out, err := p.String()
	if p.Required() && err != nil {
		panic(err)
	}

	return out
}

func (p *IdxParamInst) MustString() string {
	out, err := p.String()
	if p.Required() && err != nil {
		panic(err)
	}

	return out
}

// DefString returns the value as a string. Rules:
// If the param is required and it was not set, return the provided default.
// If the param is not required and it was not set, return the empty string.
// If the param is set and the value is "*", return the provided default.
// If the param is set, return the value.
func defStringParam(p stringValuedParamInst, def string) string {
	if !p.Found() {
		if p.Required() {
			// not set, required
			return def
		} else {
			// not set, not required
			return ""
		}
	} else if p.Value() == "*" {
		return def
	}

	out, err := p.String()
	if err != nil {
		return def
	}
	return out
}

func (p *KVParamInst) DefString(def string) string {
	return defStringParam(p, def)
}

func (p *IdxParamInst) DefString(def string) string {
	return defStringParam(p, def)
}

// DefInt returns the value as an int. See DefString for the rules.
func defIntParam(p stringValuedParamInst, def int) int {
	if !p.Found() {
		if p.Required() {
			return def
		} else {
			return 0
		}
	} else if p.Value() == "*" {
		return def
	}

	out, err := p.Int()
	if err != nil {
		return def
	}
	return out
}

func (p *KVParamInst) DefInt(def int) int {
	return defIntParam(p, def)
}

func (p *IdxParamInst) DefInt(def int) int {
	return defIntParam(p, def)
}

// DefFloat returns the value as a float. See DefString for the rules.
func defFloatParam(p stringValuedParamInst, def float64) float64 {
	if !p.Found() {
		if p.Required() {
			return def
		} else {
			return 0
		}
	} else if p.Value() == "*" {
		return def
	}

	out, err := p.Float()
	if err != nil {
		return def
	}
	return out
}

// DefBool returns the value as a bool. See DefString for the rules.
func defBoolParam(p stringValuedParamInst, def bool) bool {
	if !p.Found() {
		if p.Required() {
			return def
		} else {
			return false
		}
	} else if p.Value() == "*" {
		return def
	}

	out, err := p.Bool()
	if err != nil {
		return def
	}
	return out
}


================================================
FILE: hal/cmd_test.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"strings"
	"testing"
)

func TestCmd(t *testing.T) {
	// example 1 - smoke test
	oc := NewCmd("oncall", false).
		SetUsage("search Pagerduty escalation policies for a string")
	oc.AddSubCmd("cache-status")
	oc.AddSubCmd("cache-interval").AddIdxParam(0, "interval", true)
	oc.AddSubCmd("help").AddAlias("h")

	oc.GetSubCmd("cache-status").SetUsage("check the status of the background caching job")
	oc.GetSubCmd("cache-interval").SetUsage("set the background caching job interval")

	var res *CmdInst
	var err error
	// make sure a command with no args doesn't blow up
	res, err = oc.Process([]string{"!oncall"})
	if err != nil {
		t.Fail()
	}

	res, err = oc.Process([]string{"!oncall", "help"})
	if err != nil {
		t.Fail()
	}

	res, err = oc.Process([]string{"!oncall", "h"})
	if err != nil {
		t.Error(err)
		t.Fail()
	}

	res, err = oc.Process([]string{"!oncall", "sre"})
	if len(res.Remainder()) != 1 || res.Remainder()[0] != "sre" {
		t.Fail()
	}

	res, err = oc.Process([]string{"!oncall", "cache-status"})
	if err != nil {
		t.Error(err)
		t.Fail()
	}
	if res.SubCmdToken() != "cache-status" {
		t.Fail()
	}

	res, err = oc.Process([]string{"!oncall", "cache-interval", "1h"})
	if err != nil {
		t.Error(err)
		t.Fail()
	}
	if res.SubCmdToken() != "cache-interval" {
		t.Fail()
	}

	// example 2
	// Alias: requiring explicit aliases instead of guessing seems right
	pc := NewCmd("prefs", true)
	pc.AddSubCmd("set").
		SetUsage("set a pref").
		SubCmd().AddKVParam("key", true).AddAlias("k").SetUsage("ohai!").
		SubCmd().AddKVParam("value", true).AddAlias("v").
		SubCmd().AddKVParam("room", false).AddAlias("r").
		SubCmd().AddKVParam("user", false).AddAlias("u").
		SubCmd().AddKVParam("broker", false).AddAlias("b")

	pc.AddSubCmd("get").
		SubCmd().AddKVParam("key", true).AddAlias("k").
		SubCmd().AddKVParam("value", true).AddAlias("v").
		SubCmd().AddKVParam("room", false).AddAlias("r").
		SubCmd().AddKVParam("user", false).AddAlias("u").SetDefault("*").
		SubCmd().AddKVParam("broker", false).AddAlias("b")

	pc.AddSubCmd("rm").AddIdxParam(0, "id", true)

	argv2 := strings.Split("prefs set --room * --user foo --broker console --key ohai --value nevermind", " ")
	res, err = pc.Process(argv2)
	if err != nil {
		t.Error(err)
		t.Fail()
	}

	if len(res.Remainder()) != 0 {
		t.Error("There should not be any remainder")
	}
	if res.SubCmdToken() != "set" {
		t.Errorf("wrong subcommand. Expected %q, got %q", "set", res.SubCmdToken())
	}
	if res.SubCmdInst() == nil {
		t.Error("result.SubCmdInst is nil when it should be an instance for 'set'")
		t.FailNow()
	}
	subcmd := res.SubCmdInst()
	if subcmd.GetKVParamInst("room").MustString() != "*" {
		t.Errorf("wrong room, expected *, got %q", subcmd.GetKVParamInst("room").MustString())
	}
	if subcmd.GetKVParamInst("key").MustString() != "ohai" {
		t.Errorf("wrong key, expected 'ohai', got %q", subcmd.GetKVParamInst("key").MustString())
	}
	if subcmd.GetKVParamInst("value").MustString() != "nevermind" {
		t.Errorf("wrong value, expected 'nevermind', got %q", subcmd.GetKVParamInst("value").MustString())
	}
	// check that defaults are working
	dval := "1234"
	rds := subcmd.GetKVParamInst("room").DefString(dval)
	if rds != dval {
		t.Errorf("DefString returned %q, expected %q", rds, dval)
	}
	irds := subcmd.GetKVParamInst("room").DefInt(999)
	if irds != 999 {
		t.Errorf("DefString returned %d, expected 999", irds)
	}

	// again with out-of-order parameters
	argv3 := strings.Split("prefs --user bob --key testing get --value lol", " ")
	res, err = pc.Process(argv3)
	if err != nil {
		t.Error(err)
		t.Fail()
	}
	if len(res.Remainder()) != 0 {
		t.Error("There should not be any remainder")
	}
	if res.SubCmdToken() != "get" {
		t.Errorf("wrong subcommand. Expected 'get', got %q", res.SubCmdToken())
	}
	if res.SubCmdInst() == nil {
		t.Error("result.SubCmdInst is nil when it should be an instance for 'get'")
		t.FailNow()
	}
	subcmd = res.SubCmdInst()
	kvpi := subcmd.GetKVParamInst("key")
	if kvpi == nil {
		t.Error("BUG: subcmd.GetKVParamInst('key') returned nil")
		t.FailNow()
	}
	if kvpi.MustString() != "testing" {
		t.Errorf("wrong key, expected 'testing', got %q", subcmd.GetKVParamInst("key").MustString())
	}

	argv4 := []string{"!prefs", "rm", "4"}
	res, err = pc.Process(argv4)
	if err != nil {
		t.Error(err)
		t.Fail()
	}
	if res.SubCmdToken() != "rm" {
		t.Errorf("Expected rm, got %q", res.SubCmdToken())
	}
	pp := res.SubCmdInst().GetIdxParamInst(0)
	if pp.Value() != "4" {
		t.Errorf("wrong value from positional parameter. got %d, expected 4", pp.idx)
	}

	dc := NewCmd("dc", false)
	dc.AddKVParam("dc_required_kvparam_with_default", true).SetDefault("this is the default")
	sdc := dc.AddSubCmd("test")
	sdc.AddKVParam("kvparam_with_default", false).SetDefault("this is the default 1")
	sdc.AddKVParam("required_kvparam_with_default", true).SetDefault("this is the default 2")
	sdc.AddKVParam("required_kvparam_without_default", true)
	sdc.AddBoolParam("boolparam_with_default", false).SetDefault(true)
	sdc.AddBoolParam("required_boolparam_with_default", true).SetDefault(false)
	sdc.AddBoolParam("required_boolparam_without_default", true)

	res, err = dc.Process([]string{"dc"})
	if err != nil {
		t.Errorf("command should parse ok with no arguments: %s", err)
		t.Fail()
	}

	res, err = dc.Process([]string{"dc", "--dc_required_kvparam_with_default", "whatever"})
	if err != nil {
		t.Fail()
	}

	res, err = dc.Process([]string{"dc", "test"})
	if res != nil || err == nil {
		t.Errorf("subcommand should NOT parse ok with no arguments: %s", err)
		t.Fail()
	}

	res, err = dc.Process([]string{"dc", "test", "required_kvparam_without_default=yes", "--required_boolparam_without_default"})
	if err != nil {
		t.Fail()
	}
}


================================================
FILE: hal/counter.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	dbsql "database/sql"
)

const CounterTable = `
CREATE TABLE IF NOT EXISTS counter (
	 pkey    VARCHAR(191) NOT NULL,
	 value   int,
	 ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	 PRIMARY KEY(pkey)
)`

func GetCounter(key string) (value int, err error) {
	db := SqlDB()
	SqlInit(CounterTable)

	sql := "SELECT value FROM counter WHERE pkey=?"
	err = db.QueryRow(sql, key).Scan(&value)
	if err == dbsql.ErrNoRows {
		return 0, nil
	} else if err != nil {
		log.Printf("GetCounter query failed: %s", err)
		return 0, err
	}

	return value, nil
}

func SetCounter(key string, value int) error {
	db := SqlDB()
	SqlInit(CounterTable)

	sql := `INSERT INTO counter (pkey,value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=?`

	_, err := db.Exec(sql, key, value, value)
	if err != nil {
		log.Printf("SetCounter upsert failed: %s", err)
	}

	return err
}

func IncrementCounter(key string) error {
	db := SqlDB()
	SqlInit(CounterTable)

	sql := `INSERT INTO counter (pkey,value) VALUES (?,1) ON DUPLICATE KEY UPDATE value=value+1`

	_, err := db.Exec(sql, key)
	if err != nil {
		log.Printf("IncrementCounter query failed: %s", err)
	}

	return err
}

func DecrementCounter(key string) error {
	db := SqlDB()
	SqlInit(CounterTable)

	sql := `INSERT INTO counter (pkey,value) VALUES (?,-1) ON DUPLICATE KEY UPDATE value=value-1`

	_, err := db.Exec(sql, key)
	if err != nil {
		log.Printf("DecrementCounter query failed: %s", err)
	}

	return err
}


================================================
FILE: hal/directory.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"sync"

	"github.com/juju/errors"
)

// directory is a simple graph of directory-style information that can be linked
// and queried on those links. Goals:
// 1. avoid coupling between plugins
// 2. make it easy to share data between plugins
// 3. make it easier to link data across various systems (e.g. Pagerduty, company directory, Slack)
type directory struct {
	initOnce sync.Once
}

const DirNodeTable = `
CREATE TABLE IF NOT EXISTS dir_node (
	pkey    VARCHAR(191) NOT NULL,
	kind    VARCHAR(191) NOT NULL,
	ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY(pkey, kind)
)`

type DirNode struct {
	Key  string `json:"key"`
	Kind string `json:"kind"`
	Ts   int    `json:"ts"`
}

const DirNodeAttrTable = `
CREATE TABLE IF NOT EXISTS dir_node_attr (
	pkey    VARCHAR(191) NOT NULL,
	kind    VARCHAR(191) NOT NULL,
	attr    VARCHAR(191) NOT NULL,
	value   MEDIUMTEXT,
	ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY(pkey, kind, attr),
	INDEX (pkey, kind),
	FOREIGN KEY (pkey, kind) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE
)`

type DirNodeAttr struct {
	Key   string `json:"key"`
	Kind  string `json:"kind"`
	Attr  string `json:"attr"`
	Value string `json:"value"`
	Ts    int    `json:"ts"`
}

const DirEdgeTable = `
CREATE TABLE IF NOT EXISTS dir_edge (
	keyA    VARCHAR(191) NOT NULL,
	kindA   VARCHAR(191) NOT NULL,
	keyB    VARCHAR(191) NOT NULL,
	kindB   VARCHAR(191) NOT NULL,
	ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY(keyA, kindA, keyB, kindB),
	INDEX (keyA, kindA),
	INDEX (keyB, kindB),
	FOREIGN KEY (keyA, kindA) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE,
	FOREIGN KEY (keyB, kindB) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE
)`

type DirEdge struct {
	KeyA  string `json:"key_a"`
	KindA string `json:"kind_a"`
	KeyB  string `json:"key_b"`
	KindB string `json:"kind_b"`
	Ts    int    `json:"ts"`
}

var dirSingleton directory

func Directory() *directory {
	dirSingleton.initOnce.Do(func() {
		SqlInit(DirNodeTable)
		SqlInit(DirNodeAttrTable)
		SqlInit(DirEdgeTable)
	})

	return &dirSingleton
}

func (dir *directory) exec(sql string, params ...interface{}) error {
	db := SqlDB()

	_, err := db.Exec(sql, params...)
	if err != nil {
		return errors.Annotatef(err, "SQL: %q, Values: %+q", sql, params)
	}

	return nil
}

func (dir *directory) query(sql string, params ...interface{}) ([][]string, error) {
	db := SqlDB()
	out := make([][]string, 0)

	rows, err := db.Query(sql, params...)
	if err != nil {
		return out, errors.Annotatef(err, "SQL: %q, Values: %+q", sql, params)
	}

	defer rows.Close()

	cols, err := rows.Columns()
	if err != nil {
		return out, errors.Annotate(err, "rows.Columns()")
	}

	for rows.Next() {
		irow := make([]interface{}, len(cols))
		row := make([]string, len(irow))

		for i, _ := range irow {
			irow[i] = &row[i]
		}

		err := rows.Scan(irow...)
		if err != nil {
			return out, errors.Annotate(err, "rows.Scan()")
		}

		out = append(out, row)
	}

	return out, nil
}

// Put adds/updates a node for the given key/attr and creates edges for
// the attributes where possible.
func (dir *directory) Put(key, kind string, attrs map[string]string, edgeAttrs []string) error {
	err := dir.PutNode(key, kind)
	if err != nil {
		return err
	}

	if attrs == nil {
		return errors.NotValidf("attrs cannot be nil")
	}

	for attr, value := range attrs {
		err := dir.PutNodeAttr(key, kind, attr, value)
		if err != nil {
			return err
		}
	}

	// experimental: use the provided list of keys to try to create edges based on attributes
	for _, ea := range edgeAttrs {
		if value, exists := attrs[ea]; exists {
			neighbors, err := dir.GetAttrNodes(ea, value)
			if err != nil {
				return errors.Annotate(err, "GetAttrNodes failed")
			}
			for _, neighbor := range neighbors {
				dir.PutEdge(key, kind, neighbor[0], neighbor[1])
			}
		}
	}

	return nil
}

func (dir *directory) PutNode(key, kind string) error {
	sql := `INSERT INTO dir_node (pkey, kind) VALUES (?, ?) ON DUPLICATE KEY UPDATE ts=NOW()`
	return dir.exec(sql, key, kind)
}

func (dir *directory) HasNode(key, kind string) (bool, error) {
	sql := `SELECT pkey, kind FROM dir_node WHERE pkey=? AND kind=?`

	data, err := dir.query(sql, &key, &kind)
	if err != nil {
		return false, err
	}

	if len(data) > 0 {
		return true, nil
	}

	return false, nil
}

func (dir *directory) DelNode(key, kind string) error {
	sql := `DELETE FROM dir_node WHERE key=? AND kind=?`
	return dir.exec(sql, key, kind)
}

func (dir *directory) PutNodeAttr(key, kind, attr, value string) error {
	sql := `INSERT INTO dir_node_attr (pkey, kind, attr, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value=?, ts=NOW()`
	return dir.exec(sql, key, kind, attr, value, value)
}

// GetAttrNodes takes an attribute and value and returns a list of nodes
// that have (exactly) matching attributes.
func (dir *directory) GetAttrNodes(attr, value string) ([][2]string, error) {
	out := make([][2]string, 0)
	sql := `SELECT pkey, kind FROM dir_node_attr WHERE attr=? AND value=? GROUP BY pkey, kind`

	data, err := dir.query(sql, &attr, &value)
	if err != nil {
		return out, err
	}

	for _, item := range data {
		// item is []string, return must be [2]string
		out = append(out, [2]string{item[0], item[1]})
	}

	return out, nil
}

func (dir *directory) HasEdge(keyA, kindA, keyB, kindB string) (bool, error) {
	sql := `SELECT "y" FROM dir_edge WHERE keyA=? AND kindA=? AND keyB=? AND kindB=?`

	data, err := dir.query(sql, &keyA, &kindA, &keyB, &kindB)
	if err != nil {
		return false, err
	}

	if len(data) > 0 {
		return true, nil
	}

	return false, nil
}

func (dir *directory) PutEdge(keyA, kindA, keyB, kindB string) error {
	sql := `INSERT INTO dir_edge (keyA, kindA, keyB, kindB) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE ts=NOW()`
	return dir.exec(sql, keyA, kindA, keyB, kindB)
}

func (dir *directory) DelEdge(keyA, kindA, keyB, kindB string) error {
	sql := `DELETE FROM dir_edge WHERE keyA=? AND kindA=? AND keyB=? AND kindB=?`
	return dir.exec(sql, keyA, kindA, keyB, kindB)
}

func (dir *directory) GetNeighbors(key, kind string) ([][2]string, error) {
	out := make([][2]string, 0)

	sql := `SELECT keyA, kindA, keyB, kindB FROM dir_edge WHERE (keyA=? AND kindA=?) OR (keyB=? AND kindB=?)`

	edges, err := dir.query(sql, &key, &kind, &key, &kind)
	if err != nil {
		return out, err
	}

	for _, e := range edges {
		if e[0] == key && e[1] == kind {
			out = append(out, [2]string{e[2], e[3]})
		} else {
			out = append(out, [2]string{e[0], e[1]})
		}
	}

	return out, nil
}

func (dir *directory) GetEdges() ([]DirEdge, error) {
	sql := `SELECT keyA, kindA, keyB, kindB, UNIX_TIMESTAMP(ts) FROM dir_edge WHERE keyA != '' AND kindA != '' AND keyB != '' AND kindB != ''`
	db := SqlDB()
	out := make([]DirEdge, 0)

	rows, err := db.Query(sql)
	if err != nil {
		return out, errors.Annotatef(err, "SQL: %q", sql)
	}

	defer rows.Close()

	for rows.Next() {
		row := DirEdge{}
		err := rows.Scan(&row.KeyA, &row.KindA, &row.KeyB, &row.KindB, &row.Ts)
		if err != nil {
			return out, errors.Annotate(err, "rows.Scan()")
		}

		out = append(out, row)
	}

	return out, nil
}

func (dir *directory) GetNodes() ([]DirNode, error) {
	sql := `SELECT pkey, kind, UNIX_TIMESTAMP(ts) FROM dir_node WHERE pkey != '' AND kind != ''`
	db := SqlDB()
	out := make([]DirNode, 0)

	rows, err := db.Query(sql)
	if err != nil {
		return out, errors.Annotatef(err, "SQL: %q", sql)
	}

	defer rows.Close()

	for rows.Next() {
		row := DirNode{}
		err := rows.Scan(&row.Key, &row.Kind, &row.Ts)
		if err != nil {
			return out, errors.Annotate(err, "rows.Scan()")
		}

		out = append(out, row)
	}

	return out, nil
}

func (dir *directory) GetNodeAttrs() ([]DirNodeAttr, error) {
	sql := `SELECT pkey, kind, attr, value, UNIX_TIMESTAMP(ts) FROM dir_node_attr WHERE pkey != '' AND kind != '' AND attr != ''`
	db := SqlDB()
	out := make([]DirNodeAttr, 0)

	rows, err := db.Query(sql)
	if err != nil {
		return out, errors.Annotatef(err, "SQL: %q", sql)
	}

	defer rows.Close()

	for rows.Next() {
		row := DirNodeAttr{}
		err := rows.Scan(&row.Key, &row.Kind, &row.Attr, &row.Value, &row.Ts)
		if err != nil {
			return out, errors.Annotate(err, "rows.Scan()")
		}

		out = append(out, row)
	}

	return out, nil
}

/* test data

INSERT INTO dir_node (kind, pkey) VALUES ("AD", "angua@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "carrot@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "aching@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "granny@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "vetinari@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "vimes@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("AD", "nobbs@dwmail.com");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Angua");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Carrot");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Tiffany");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Mistress Weatherwax");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Patrician");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Sam");
INSERT INTO dir_node (kind, pkey) VALUES ("slack", "Nobby");

INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "angua@dwmail.com", "slack", "Angua");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "carrot@dwmail.com", "slack", "Carrot");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "aching@dwmail.com", "slack", "Tiffany");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "granny@dwmail.com", "slack", "Mistress Weatherwax");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "vetinari@dwmail.com", "slack", "Patrician");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "vimes@dwmail.com", "slack", "Sam");
INSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES ("AD", "nobbs@dwmail.com", "slack", "Nobby");

INSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES ("AD", "angua@dwmail.com", "email", "angua@dwmail.com");
INSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES ("AD", "angua@dwmail.com", "sms", "5551234567"
INSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES ("AD", "carrot@dwmail.com", "sms", "5555555555");
*/


================================================
FILE: hal/event.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"regexp"
	"strings"
	"time"
)

// Evt is a generic container for events processed by the bot.
// Event sources are responsible for copying the appropriate data into
// the Evt fields. Routing and most plugins will not work if the body
// isn't copied, at a minimum.
// When ToUser and ToRoom are both true, the event will be delivered twice.
// The original event should usually be attached to the Original
type Evt struct {
	ID        string       `json:"id"`      // ID for the event (assigned by upstream or broker)
	Body      string       `json:"body"`    // body of the event, regardless of source
	Command   string       `json:"command"` // optional command associated with the body, typically empty
	Subject   string       `json:"subject"` // the subject of the message, if available, typically empty
	Room      string       `json:"room"`    // the room where the event originated
	RoomId    string       `json:"room_id"` // the room id from the source broker
	User      string       `json:"user"`    // the username that created the event
	UserId    string       `json:"user_id"` // the user id from the source broker
	Time      time.Time    `json:"time"`    // timestamp of the event
	Broker    Broker       `json:"broker"`  // the broker the event came from
	IsChat    bool         `json:"is_chat"` // lets the broker differentiate chats and other events
	IsBot     bool         `json:"is_bot"`  // message was generated by the bot
	ToUser    bool         `json:"to_user"` // when true, always deliver outgoing event via DM
	ToRoom    bool         `json:"to_room"` // when true, always deliver outgoing event to the room
	ToFunc    bool         `json:"to_func"` // when true, call the ReplyFunc instead of the usual reply path
	ReplyFunc func(string) // a function to be called with a reply rather than the usual process
	Original  interface{}  // the original message container (e.g. slack.MessageEvent)
	instance  *Instance    // used by the broker to provide plugin instance metadata
	isReply   bool         // the message is a reply
	isDM      bool         // the message is/should be a DM
	isTable   bool         // the message is a table that should be rendered
	oob       interface{}  // oob data that needs to flow between stages (e.g. tables)
}

// Clone() returns a copy of the event with the same broker/room/user
// and a current timestamp. Body, Command, and Subject will be empty.
// Time is updated to the current time.
// Original is carried through, so nil that if you don't want it preserved.
func (e *Evt) Clone() Evt {
	out := Evt{
		ID:       e.ID,
		Room:     e.Room,
		RoomId:   e.RoomId,
		User:     e.User,
		UserId:   e.UserId,
		Time:     time.Now(),
		Broker:   e.Broker,
		IsChat:   e.IsChat,
		IsBot:    e.IsBot,
		Original: e.Original,
		isReply:  e.isReply,
		isDM:     e.isDM,
		isTable:  e.isTable,
		// do not preserve oob
	}

	return out
}

// Reply is a helper that crafts a new event from the provided string
// and initiates the reply on the broker attached to the event.
// The message is routed according to preferences and the ToUser/ToRoom
// fields on the event. If no preferences are set for the user/room/plugin
// the response will go to the room where the command originated.
// The "reply-via-dm" preference can be set to "true" to default to
// having replies to to DM instead of the room.
func (e *Evt) Reply(msg string) {
	var delivered bool

	if e.ToFunc {
		e.ReplyFunc(msg)
		delivered = true
	}

	if e.ToRoom {
		e.ReplyToRoom(msg)
		delivered = true
	}

	if e.ToUser {
		e.ReplyDM(msg)
		delivered = true
	}

	if delivered {
		return
	}

	replyVia := e.AsPref().FindKey("reply-via-dm").One()

	// One() sets Success to false for no results.
	if replyVia.Success {
		if replyVia.Value == "true" {
			e.ReplyDM(msg)
			return
		}
	}

	// replyVia might be false (or invalid) in which case it falls through to here
	e.ReplyToRoom(msg)
}

// Replyf is the same as Reply but allows for string formatting using
// fmt.Sprintf()
func (e *Evt) Replyf(msg string, a ...interface{}) {
	e.Reply(fmt.Sprintf(msg, a...))
}

// Error replies to the event with the provided error.
// Future: need to figure out if there's going to be a kind of error
// handling module in Hal for making errors visible in a logging room,
// possibly on a different broker...
func (e *Evt) Error(err error) {
	e.Reply(fmt.Sprintf("%s", err))
}

// ReplyTable sends a table of data back, formatting it according to
// preferences.
// TODO: move code from brokers/slack/broker.go/SendTable here
// TODO: document preferences here
func (e *Evt) ReplyTable(hdr []string, rows [][]string) {
	out := e.Clone() // may not be necessary

	if e.Broker != nil {
		e.Broker.SendTable(out, hdr, rows)
	} else {
		panic("hal.Evt.ReplyTable called with nil Broker!")
	}
}

// ReplyDM makes it convenient to reply to a user via DM. The user is drawn
// from the event's UserId field and passed to the broker's SendDM() method.
func (e *Evt) ReplyDM(msg string) {
	out := e.Clone()
	out.Body = msg
	e.Broker.SendDM(out)
}

// ReplyToRoom crafts a new event from the provided string
// and sends it to the room the event originated from.
func (e *Evt) ReplyToRoom(msg string) {
	out := e.Clone()
	out.Body = msg

	if e.Broker != nil {
		e.Broker.Send(out)
	} else {
		panic("hal.Evt.Reply called with nil Broker!")
	}
}

// BrokerName returns the text name of current broker.
func (e *Evt) BrokerName() string {
	return e.Broker.Name()
}

// FindPrefs fetches the union of all matching settings from the database
// for user, broker, room, and plugin.
// Plugins can use the Prefs methods to filter from there.
func (e *Evt) FindPrefs() Prefs {
	broker := e.BrokerName()
	plugin := e.instance.Plugin.Name
	return FindPrefs(e.User, broker, e.RoomId, plugin, "")
}

// InstanceSettings gets all the settings matching the settings defined
// by the plugin's Settings field.
func (e *Evt) InstanceSettings() Prefs {
	broker := e.BrokerName()
	plugin := e.instance.Plugin.Name

	out := make(Prefs, 0)

	for _, stg := range e.instance.Plugin.Settings {
		// ignore room-specific settings for other rooms
		if stg.Room != "" && stg.Room != e.RoomId {
			continue
		}

		pref := GetPref("", broker, e.RoomId, plugin, stg.Key, stg.Default)
		out = append(out, pref)
	}

	return out
}

// AsPref returns a a pref with user, room, broker, and plugin set using data
// from the event handle.
func (e *Evt) AsPref() Pref {
	// AsPref can be called without an instance for errors, make sure
	// instance is set before accessing fields
	var plugin string
	if e.instance != nil {
		plugin = e.instance.Plugin.Name
	}

	p := Pref{
		User:   e.UserId,
		Room:   e.RoomId,
		Broker: e.BrokerName(),
		Plugin: plugin,
	}

	return p
}

// BodyAsArgv does minimal parsing of the event body, returning an argv-like
// array of strings with quoted strings intact (but with quotes removed).
// The goal is shell-like, and is not a full implementation.
// Leading/trailing whitespace is removed.
// Escaping quotes, etc. is not supported.
func (e *Evt) BodyAsArgv() []string {
	// use a simple RE rather than pulling in a package to do this
	re := regexp.MustCompile(`'[^']*'|"[^"]*"|\S+`)
	body := strings.TrimSpace(e.Body)
	argv := re.FindAllString(body, -1)

	// remove the outer quotes from quoted strings
	for i, val := range argv {
		if strings.HasPrefix(val, `'`) && strings.HasSuffix(val, `'`) {
			tmp := strings.TrimPrefix(val, `'`)
			argv[i] = strings.TrimSuffix(tmp, `'`)
		} else if strings.HasPrefix(val, `"`) && strings.HasSuffix(val, `"`) {
			tmp := strings.TrimPrefix(val, `"`)
			argv[i] = strings.TrimSuffix(tmp, `"`)
		}
	}

	return argv
}

// ForceToRoom clones the event and returns a copy with ToRoom set to true.
// Takes priority over reply-via-dm routing.
// Useful for chaining, e.g. evt.ToRoom().Replyf("go away!").
func (e *Evt) ForceToRoom() Evt {
	out := e.Clone()
	out.ToRoom = true
	return out
}

// ForceToUser clones the event and returns a copy with ToUser set to true.
// Takes priority over reply-via-dm routing.
// Useful for chaining.
func (e *Evt) ForceToUser() Evt {
	out := e.Clone()
	out.ToUser = true
	return out
}

func (e *Evt) String() string {
	return fmt.Sprintf("User: %q Room: %q Time: %q Body: %q", e.User, e.Room, e.Time.String(), e.Body)
}


================================================
FILE: hal/event_test.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"testing"
)

func TestEvtBodyAsArgv(t *testing.T) {
	evt := Evt{}
	evt.Body = "a simple flat test"
	argv := evt.BodyAsArgv()

	if len(argv) != 4 {
		fmt.Printf("expected 4 elements, got %d", len(argv))
		t.Fail()
	}

	//            1     2      3    4            5     6              7                    8
	evt.Body = ` !foo --this -one "is a little" more (complicated) 'becuase of the quotes' OK`
	argv = evt.BodyAsArgv()

	if len(argv) != 8 {
		fmt.Printf("expected 8 elements, got %d", len(argv))
		t.Fail()
	}

	// leading/trailing whitespace should be stripped and embedded quotes
	// should be intact. Escaped quotes are not supported.
	evt.Body = `	!bar "perhaps 'this challenge' will" '@%$*#@(**W(IOWIE-'------ break TEH BANK "" '' `
	argv = evt.BodyAsArgv()

	if len(argv) != 9 {
		fmt.Printf("expected 9 elements, got %d", len(argv))
		t.Fail()
	}
}


================================================
FILE: hal/kv.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	dbsql "database/sql"
	"sync"
	"time"
)

const KVTable = `
CREATE TABLE IF NOT EXISTS kv (
	 pkey    VARCHAR(191) NOT NULL,
	 value   MEDIUMTEXT,
	 expires DATETIME,
	 ttl     INT NOT NULL DEFAULT 0, -- ttl 0 is forever
	 ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
	 PRIMARY KEY(pkey)
)`

var kvLateInitOnce sync.Once

func kvLazyInit() {
	kvLateInitOnce.Do(func() {
		SqlInit(KVTable)
		go kvCleanup()
	})
}

func kvCleanup() {
	c := time.Tick(time.Minute)

	for _ = range c {
		db := SqlDB()
		_, err := db.Exec("DELETE FROM kv WHERE expires < NOW()")
		if err != nil {
			log.Printf("DELETE of expired keys from the DB failed: %s", err)
		}
	}
}

// ExistsKV checks to see if a key exists in the kv. False if any errors are
// encountered.
func ExistsKV(key string) bool {
	kvLazyInit()
	db := SqlDB()

	var count int64
	sql := "SELECT COUNT(pkey) FROM kv WHERE pkey=? AND expires > NOW()"
	err := db.QueryRow(sql, key).Scan(&count)
	if err != nil {
		log.Printf("ExistsKV query %q failed: %s", sql, err)
		return false
	}

	return count > 0
}

// GetKV retreives a value from the database. Returns value,ok style. Returns
// "", false if the query fails and "", true if there was no value available.
func GetKV(key string) (string, bool) {
	kvLazyInit()
	db := SqlDB()

	var value string
	sql := "SELECT value FROM kv WHERE pkey=? AND expires > NOW()"
	err := db.QueryRow(sql, key).Scan(&value)
	if err == dbsql.ErrNoRows {
		return "", true
	} else if err != nil {
		log.Printf("GetKV query %q failed: %s", sql, err)
		return "", false
	}

	return value, true
}

// SetKV inserts a new value in the database with the provided TTL. If the TTL
// is 0, it defaults to 10 years.
func SetKV(key, value string, ttl time.Duration) (err error) {
	kvLazyInit()
	db := SqlDB()
	now := time.Now()

	if ttl == 0 {
		ttl = time.Hour * 24 * 3650
	}

	sql := "INSERT INTO kv (pkey,value,expires,ttl) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE value=?, expires=?, ttl=?"

	expires := now.Add(ttl)
	ttlsecs := int(ttl.Seconds())
	_, err = db.Exec(sql, key, value, expires, ttlsecs, value, expires, ttlsecs)

	if err != nil {
		log.Printf("SetKV query %q failed: %s", sql, err)
	}

	return err
}


================================================
FILE: hal/logger.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"os"
	"sync"
	"time"
)

// Logger provides a handle for using Hal's logging facility. Any Logger created
// ultimately uses the same singleton.
type Logger struct {
	prefix string // logging prefix string - eventually will be prepended to all messages
}

type LogEntry struct {
	Time    time.Time
	Prefix  string
	Body    string
	IsDebug bool
}

// logger contains the state for the logger
type logger struct {
	debug       bool            // for enabling/disabling debug logs
	logSinks    []chan LogEntry // a list of channels to receive log messages
	dbgSinks    []chan LogEntry // a list of channels to receive debug messages
	logFwdQuit  chan struct{}   // used to quit the default log message forwarder
	dbgFwdQuit  chan struct{}   // used to quit the default debug message forwarder
	logFwdClose chan struct{}   // used to signal it's ok to close the log channel
	dbgFwdClose chan struct{}   // used to signal it's ok to close the debug channel
	listLock    sync.Mutex      // protect concurrent access to the sink lists
	once        sync.Once       // initialize on first use
}

// makes log available inside Hal
var log Logger

// the singleton logger state
var gl logger

// String returns the LogEntry as a formatted log string.
func (l *LogEntry) String() string {
	var prefix string

	if l.Prefix != "" {
		prefix = "[" + l.Prefix + "] "
	}

	return l.Time.Format(time.RFC3339) + " " + prefix + l.Body
}

// initialize allocates channels and starts the background goroutines
// that forward output to stdout
func (l *logger) initialize() {
	l.once.Do(func() {
		l.listLock.Lock()
		defer l.listLock.Unlock()

		l.debug = true
		l.logSinks = make([]chan LogEntry, 1)
		l.dbgSinks = make([]chan LogEntry, 1)
		l.logSinks[0] = make(chan LogEntry, 10)
		l.dbgSinks[0] = make(chan LogEntry, 10)
		l.logFwdClose = make(chan struct{})
		l.dbgFwdClose = make(chan struct{})

		// always print logs & debug to stdout by default
		go l.fwdStdout(l.logSinks[0], l.logFwdClose)
		go l.fwdStdout(l.dbgSinks[0], l.dbgFwdClose)
	})
}

// fwdStdout is run as a goroutine to read off a channel and print to stdout.
func (l *logger) fwdStdout(src chan LogEntry, closed chan struct{}) {
	for out := range src {
		print(out.String() + "\n")
	}

	closed <- struct{}{}
}

// SetPrefix sets a new prefix that will be prepended to every message from the logger handle.
func (l *Logger) SetPrefix(prefix string) {
	l.prefix = prefix
}

// Printf formats the message and propagates it as a log message.
func (l *Logger) Printf(msg string, a ...interface{}) {
	gl.initialize()

	out := LogEntry{
		Time:   time.Now(),
		Prefix: l.prefix,
		Body:   fmt.Sprintf(msg, a...),
	}

	for _, sink := range gl.logSinks {
		sink <- out
	}
}

// Println merges the arguments and propagates the result as a log message.
func (l *Logger) Println(a ...interface{}) {
	gl.initialize()

	out := LogEntry{
		Time:   time.Now(),
		Prefix: l.prefix,
		Body:   fmt.Sprintln(a...),
	}

	for _, sink := range gl.logSinks {
		sink <- out
	}
}

// Debugf formats the message and propagates it. No work is performed if debugging
// is disabled.
func (l *Logger) Debugf(msg string, a ...interface{}) {
	gl.initialize()

	if gl.debug {
		out := LogEntry{
			Time:    time.Now(),
			Prefix:  l.prefix,
			Body:    fmt.Sprintf(msg, a...),
			IsDebug: true,
		}

		for _, sink := range gl.dbgSinks {
			sink <- out
		}
	}
}

// Fatalf formats the message, propagates the log, then exits the program.
func (l *Logger) Fatalf(msg string, a ...interface{}) {
	gl.initialize()

	out := LogEntry{
		Time:   time.Now(),
		Prefix: l.prefix,
		Body:   fmt.Sprintf(msg, a...),
	}

	for i, sink := range gl.logSinks {
		sink <- out
		if i > 0 {
			close(sink)
		}
	}

	l.DisableLogStdout()
	l.DisableDbgStdout()

	os.Exit(1)
}

// Panic panics immediately. No attempt is made to forward/propagate.
func (l *Logger) Panic(msg string) {
	out := LogEntry{
		Time:   time.Now(),
		Prefix: l.prefix,
		Body:   msg,
	}
	panic(out.String())
}

// Panicf formats a message and panics. Not propagated.
func (l *Logger) Panicf(msg string, a ...interface{}) {
	out := LogEntry{
		Time:   time.Now(),
		Prefix: l.prefix,
		Body:   fmt.Sprintf(msg, a...),
	}
	panic(out.String())
}

// IsDebug returns true of debug messages are enabled.
func IsDebug() bool {
	return gl.debug
}

// IsDebug returns true of debug messages are enabled.
func (l *Logger) IsDebug() bool {
	return gl.debug
}

// EnableDebug enables debug message propagation.
func (l *Logger) EnableDebug() {
	gl.debug = true
}

// DisableDebug disables debug message propagation.
func (l *Logger) DisableDebug() {
	gl.debug = false
}

// NewLogSink creates a new channel that will receive log messages.
// It is allocated and ready to go on return. Do not close it.
func (l *Logger) NewLogSink() chan LogEntry {
	gl.initialize()
	gl.listLock.Lock()
	defer gl.listLock.Unlock()

	sink := make(chan LogEntry, 1000)
	gl.logSinks = append(gl.logSinks, sink)
	return sink
}

// NewLogSink creates a new channel that will receive debug messages.
// It is allocated and ready to go on return. Do not close it.
func (l *Logger) NewDebugSink() chan LogEntry {
	gl.initialize()
	gl.listLock.Lock()
	defer gl.listLock.Unlock()

	sink := make(chan LogEntry, 1000)
	gl.dbgSinks = append(gl.dbgSinks, sink)
	return sink
}

// DisableLogStdout disables the automatic forwarding of log messages to stdout.
func (l *Logger) DisableLogStdout() {
	gl.initialize()
	gl.listLock.Lock()
	defer gl.listLock.Unlock()

	close(gl.logSinks[0])
	<-gl.logFwdClose
	close(gl.logFwdClose)

	gl.logSinks = gl.logSinks[1:]
}

// DisableDbgStdout disables the automatic forwarding of debug messages to stdout.
func (l *Logger) DisableDbgStdout() {
	gl.initialize()

	gl.listLock.Lock()
	defer gl.listLock.Unlock()

	close(gl.dbgSinks[0])
	<-gl.dbgFwdClose
	close(gl.dbgFwdClose)

	gl.dbgSinks = gl.dbgSinks[1:]
}


================================================
FILE: hal/logger_test.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"testing"
)

func TestLogger(t *testing.T) {
	l := Logger{}

	l.Printf("you SHOULD see this log message on stdout 1/2")
	l.Debugf("you SHOULD see this debug message on stdout 2/2")

	l.DisableDebug()

	l.Debugf("you should NOT see this debug message on stdout 1/1")

	// these would most likely panic if something was wrong
	l.DisableDbgStdout()
	l.DisableLogStdout()

	// should print nothing, manually verifiable
	l.Printf("you should NOT see this log message on stdout 1/2")
	l.Debugf("you should NOT see this debug message on stdout 2/2")

}


================================================
FILE: hal/periodic.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"math/rand"
	"sync"
	"time"
)

type PeriodicFunc struct {
	Name     string
	Interval time.Duration
	Function func()
	NoRand   bool // set to true to disable randomizing the first execution
	last     time.Time
	status   string
	running  bool
	tick     <-chan time.Time
	run      chan time.Time
	exit     chan struct{}
	starting sync.WaitGroup
	stopping sync.WaitGroup
	init     sync.Once
	mut      sync.Mutex
}

var periodicData struct {
	funcs []*PeriodicFunc
	mut   sync.Mutex
}

func init() {
	periodicData.funcs = make([]*PeriodicFunc, 0)
}

// GetPeriodicFunc finds a periodic function by name and returns a pointer to it.
// If the name is not found, nil is returned.
func GetPeriodicFunc(name string) *PeriodicFunc {
	periodicData.mut.Lock()
	defer periodicData.mut.Unlock()

	for _, pf := range periodicData.funcs {
		if pf.Name == name {
			return pf
		}
	}

	return nil
}

// initialize internal fields, called automatically using pf.init.Do
func (pf *PeriodicFunc) initialize() {
	periodicData.mut.Lock()
	defer periodicData.mut.Unlock()

	pf.status = "initialized"
	pf.exit = make(chan struct{})
	pf.tick = make(<-chan time.Time)
	pf.run = make(chan time.Time)
}

// loop is the goroutine's program loop
func (pf *PeriodicFunc) loop() {
	pf.mut.Lock()
	pf.status = "running"
	pf.running = true
	pf.mut.Unlock()

	pf.starting.Done()

	// TODO: this should capture/handle panics like router.go does
pfLoop:
	for {
		select {
		case <-pf.exit:
			pf.status = "stopped"
			break pfLoop
		case t := <-pf.tick:
			log.Debugf("PeriodicFunc tick %q @ %s", pf.Name, t)
			pf.runFunc(t)
		case t := <-pf.run:
			log.Debugf("PeriodicFunc run %q @ %s", pf.Name, t)
			pf.runFunc(t)
		}
	}

	pf.mut.Lock()
	pf.running = false
	pf.mut.Unlock()

	pf.stopping.Done()
}

// runFunc runs the provided function while holding the pf's mutex.
func (pf *PeriodicFunc) runFunc(t time.Time) {
	pf.mut.Lock()
	defer pf.mut.Unlock()

	pf.last = t
	pf.Function()
}

// Register puts a pf in the global list and makes it available to GetPeriodicFunc.
// Anonymous pf's work fine but are not retreivable.
func (pf *PeriodicFunc) Register() {
	found := GetPeriodicFunc(pf.Name)
	if found != nil {
		log.Debugf("Found duplicate name %q in list of PeriodicFuncs while registering.", pf.Name)
		return
	}

	periodicData.mut.Lock()
	defer periodicData.mut.Unlock()

	periodicData.funcs = append(periodicData.funcs, pf)
}

// Start a periodic function.
func (pf *PeriodicFunc) Start() {
	pf.init.Do(pf.initialize)

	pf.mut.Lock()

	pf.tick = time.Tick(pf.Interval)
	pf.starting.Add(1)

	go func() {
		// avoid a thundering herd by sleeping for a random number of seconds
		if !pf.NoRand {
			pf.randSleep()
		}

		pf.loop() // may block on pf.mut until Unlock()
	}()

	pf.mut.Unlock()

	pf.starting.Wait() // wait for the goroutine to call Done()

	// run the first pass immediately
	pf.run <- time.Now()
}

// Stop a periodic function.
func (pf *PeriodicFunc) Stop() {
	pf.init.Do(pf.initialize)
	pf.mut.Lock()
	defer pf.mut.Unlock()

	pf.exit <- struct{}{}
}

// Bump schedules a periodic function to update outside of the scheduled times.
// The value of pf.Last() is updated when this is used.
func (pf *PeriodicFunc) Bump() {
	pf.init.Do(pf.initialize)
	pf.mut.Lock()
	defer pf.mut.Unlock()

	pf.run <- time.Now()
}

// Status returns initialized/running/stopped state as a string.
func (pf *PeriodicFunc) Status() string {
	pf.init.Do(pf.initialize)
	pf.mut.Lock()
	defer pf.mut.Unlock()

	return pf.status
}

// Last returns the wallclock time of the last run of the function.
func (pf *PeriodicFunc) Last() time.Time {
	pf.init.Do(pf.initialize)
	pf.mut.Lock()
	defer pf.mut.Unlock()

	return pf.last
}

// randSleep selects a random number between 0 and 60 and sleeps that many
// seconds before returning. While sleeping, the pf status is set to "sleeping".
func (pf *PeriodicFunc) randSleep() {
	pf.mut.Lock()
	pf.status = "sleeping"
	pf.mut.Unlock()

	randSecs := rand.Intn(60)
	time.Sleep(time.Second * time.Duration(randSecs))
}


================================================
FILE: hal/persist_plugins.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const PLUGIN_INST_TABLE = `
CREATE TABLE IF NOT EXISTS plugin_instances (
	plugin  varchar(191) NOT NULL,
	broker  varchar(191) NOT NULL,
	room    varchar(191) NOT NULL,
	regex   varchar(191) NOT NULL DEFAULT "",
	ts      TIMESTAMP,
	PRIMARY KEY(plugin, broker, room)
)
`

// LoadInstances loads the previously saved plugin instance configuration
// from the database and *merges* it with the plugin registry. This should be
// idempotent if run multiple times.
// TODO: decide if it makes sense to persist settings or just pull the prefs
// each time.
func (pr *pluginRegistry) LoadInstances() error {
	log.Printf("Loading plugin instances from the database.")

	db := SqlDB()

	err := SqlInit(PLUGIN_INST_TABLE)
	if err != nil {
		log.Printf("Failed to initialize the plugin_instances table: %s", err)
		return err
	}

	rows, err := db.Query(`SELECT plugin, broker, room, regex FROM plugin_instances`)
	if err != nil {
		log.Printf("LoadInstances SQL query failed: %s", err)
		return err
	}

	defer rows.Close()

	var pname, bname, roomId, re string
	for rows.Next() {
		err := rows.Scan(&pname, &bname, &roomId, &re)
		if err != nil {
			log.Printf("LoadInstances rows.Scan() failed: %s", err)
			return err
		}

		// check to see if there is already a runtime instance, create it
		// if it doesn't exist
		found := pr.FindInstances(pname, bname, roomId)
		if len(found) == 0 {
			// instance is in the DB but not registered, do it now
			plugin, err := pr.GetPlugin(pname)
			if err != nil {
				log.Printf("%q is configured in the database but is not registered. Ignoring.", pname)
				continue
			}

			broker := Router().GetBroker(bname)
			if broker == nil {
				log.Fatalf("Broker %q does not exist.", bname)
			}

			inst := plugin.Instance(roomId, broker)
			inst.Regex = re // RE can be overridden per instance

			// go over the settings and pull preferences before firing up the instance
			inst.LoadSettingsFromPrefs()

			err = inst.Register()
			if err != nil {
				log.Printf("Could not register plugin instance for plugin %q and room id %q: %s",
					pname, roomId, err)
				return err
			}
		} else if len(found) == 1 {
			// already there, move on
			continue
		} else {
			log.Fatalf("BUG: more than 1 plugin instance matched for plugin %q and room id %q",
				pname, roomId)
		}
	}

	log.Println("Done loading plugin instances.")

	return nil
}

// SaveInstances saves plugin instances configurations to the database.
func (pr *pluginRegistry) SaveInstances() error {
	log.Printf("Saving plugin instances to the database.")
	defer func() { log.Printf("Done saving plugin instances.") }()

	err := SqlInit(PLUGIN_INST_TABLE)
	if err != nil {
		log.Printf("Failed to initialize the plugin_instances table: %s", err)
		return err
	}

	instances := pr.InstanceList()

	// use a transaction to (relatively) safely wipe & rewrite the whole table
	db := SqlDB()
	tx, err := db.Begin()
	stmt, err := tx.Prepare(`INSERT INTO plugin_instances
	                          (plugin, broker, room, regex)
	                         VALUES (?, ?, ?, ?)`)

	// clear the table before writing new records
	_, err = tx.Exec("TRUNCATE TABLE plugin_instances")

	for _, inst := range instances {
		_, err = stmt.Exec(inst.Plugin.Name, inst.Broker.Name(), inst.RoomId, inst.Regex)
		if err != nil {
			log.Printf("insert failed: %s", err)
			return err
		}
	}

	err = tx.Commit()
	if err != nil {
		log.Printf("SaveInstances transaction failed: %s", err)
		return err
	}

	return nil
}


================================================
FILE: hal/plugins.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"errors"
	"fmt"
	"regexp"
	"sync"
)

// pluginRegistry contains the plugin registration data as a singleton
type pluginRegistry struct {
	plugins   []*Plugin   // registered plugins
	instances []*Instance // instances of plugins
	mut       sync.Mutex  // concurrent access
	init      sync.Once   // initialize the singleton once
}

// Plugin is a function with metadata to assist with message routing.
// Plugins are registered at startup by the main program and wired up
// to receive events when an instance is created e.g. by the pluginmgr
// plugin.
// The Command and Regex can be set to pre-filter messages. Regex should
// only be used for custom REs. Most commands should use Command to set
// a static string and the RE will be generated automatically and consistently.
type Plugin struct {
	Name      string          // a unique name (used to launch instances)
	Func      func(Evt)       // the code to execute for each matched event
	Init      func(*Instance) // plugin hook called at instance creation time
	Command   string          // the name of the command for invocation, e.g. "atlas", "uptime"
	Regex     string          // the default regex match, to be used when Command isn't sufficient
	BotEvents bool            // set to true to receive events generated by the bot user
	Settings  Prefs           // required+autoloaded preferences + defaults
	Secrets   []string        // required+autoloaded secret key names
}

// Instance is an instance of a plugin tied to a room.
type Instance struct {
	*Plugin
	RoomId   string         // room id
	Broker   Broker         // the broker that produces events
	Regex    string         // a regex for filtering messages
	Settings Prefs          // runtime settings for the instance
	regex    *regexp.Regexp // the compiled regex
}

var pluginRegSingleton pluginRegistry

func PluginRegistry() *pluginRegistry {
	pluginRegSingleton.init.Do(func() {
		pluginRegSingleton.plugins = make([]*Plugin, 0)
		pluginRegSingleton.instances = make([]*Instance, 0)
	})

	return &pluginRegSingleton
}

// Register registers a plugin with the bot.
func (p *Plugin) Register() error {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	for _, plugin := range pr.plugins {
		if plugin.Name == p.Name {
			log.Printf("Ignoring multiple calls to Register() for plugin '%s'", p.Name)
			return nil
		}
	}

	pr.plugins = append(pr.plugins, p)

	return nil
}

// Unregister removes a plugin from the bot.
func (p *Plugin) Unregister() error {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	plugins := make([]*Plugin, len(pr.plugins)-1)
	var i int
	for _, plugin := range pr.plugins {
		if plugin.Name == p.Name {
			continue
		} else {
			// TODO: this might segfault if this is called on an unregistered or never-registered plugin
			plugins[i] = plugin
			i++
		}
	}

	pr.plugins = plugins

	return nil
}

// Instance creates an instance of a plugin. It is *not* registered (and
// therefore not considered by the router until that is done).
func (p *Plugin) Instance(roomId string, broker Broker) *Instance {
	i := Instance{
		Plugin: p,
		RoomId: roomId,
		Broker: broker,
		Regex:  p.Regex,
	}

	return &i
}

// Register an instance with the bot so that it starts receiving messages.
func (inst *Instance) Register() error {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	// default to the plugin's default if no RE was provided
	if inst.Regex == "" {
		inst.Regex = inst.Plugin.Regex
	}
	// TODO: the default regex still doesn't always show up

	// TODO: manually check/return the error so the bot doesn't crash
	inst.regex = regexp.MustCompile(inst.Regex)

	// call the instance init handler
	if inst.Plugin.Init != nil {
		inst.Plugin.Init(inst)
	}

	// once an instance is registered, the router will automatically
	// pick it up on the next message it processes
	pr.instances = append(pr.instances, inst)

	log.Debugf("Registered plugin %q in room id %q on broker %q with RE match %q",
		inst.Name, inst.RoomId, inst.Broker.Name(), inst.regex)

	return nil
}

// Unregister removes an instance from the list of plugin instances.
func (inst *Instance) Unregister() error {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	var idx int
	for j, i := range pr.instances {
		// TODO: verify if pointer equality is sufficient
		if i == inst {
			idx = j
			break
		}
	}

	// delete the instance from the list
	pr.instances = append(pr.instances[:idx], pr.instances[idx+1:]...)

	log.Printf("Unregistered plugin '%s' from room id '%s'", inst.Name, inst.RoomId)

	return nil
}

// LoadSettingsFromPrefs loads all of the settings specified in the plugin
// Settings list into the instance's Settings list. Any current settings
// are replaced. The search is run with room and plugin set to whatever
// values the instance has.
func (inst *Instance) LoadSettingsFromPrefs() {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	ips := inst.Plugin.Settings.Clone()

	// wipe the previous settings
	inst.Settings = make(Prefs, len(ips))

	for i, ppref := range ips {
		ppref.Room = inst.RoomId
		ppref.Broker = inst.Broker.Name()
		ppref.Plugin = inst.Plugin.Name
		ipref := ppref.Get()
		inst.Settings[i] = ipref
	}
}

// SaveSettingsToPrefs saves runtime instance preferences to the prefs
// table in the database.
func (inst *Instance) SaveSettingsToPrefs() {
	pr := PluginRegistry()
	pr.mut.Lock()
	defer pr.mut.Unlock()

	for _, ipref := range inst.Settings {
		ipref.Set()
	}
}

// PluginList returns a snapshot of the plugin list at call time.
func (pr *pluginRegistry) PluginList() []*Plugin {
	pr.mut.Lock()
	defer pr.mut.Unlock()

	out := make([]*Plugin, len(pr.plugins))
	copy(out, pr.plugins) // intentional shallow copy
	return out
}

// InstanceList returns a snapshot of the instance list at call time.
func (pr *pluginRegistry) InstanceList() []*Instance {
	pr.mut.Lock()
	defer pr.mut.Unlock()

	// this gets called in the router for every message that comes in, so it
	// might come to pass that this will perform poorly, but for now with a
	// relatively small number of instances we'll take the copy hit in exchange
	// for not having to think about concurrent access to the list
	out := make([]*Instance, len(pr.instances))
	copy(out, pr.instances) // intentional shallow copy
	return out
}

// GetPlugin returns the plugin specified by its name string.
func (pr *pluginRegistry) GetPlugin(name string) (*Plugin, error) {
	pr.mut.Lock()
	defer pr.mut.Unlock()

	for _, p := range pr.plugins {
		if p.Name == name {
			return p, nil
		}
	}

	return nil, errors.New(fmt.Sprintf("no such plugin: %q", name))
}

// FindInstances returns the plugin instances that match the provided
// room id, broker, and plugin name.
func (pr *pluginRegistry) FindInstances(roomId, bname, plugin string) []*Instance {
	pr.mut.Lock()
	defer pr.mut.Unlock()

	out := make([]*Instance, 0)

	for _, i := range pr.instances {
		if i.Plugin.Name == plugin && i.Broker.Name() == bname && i.RoomId == roomId {
			out = append(out, i)
		}
	}

	return out
}

// ActivePluginList returns a list of plugins that have registered instances.
func (pr *pluginRegistry) ActivePluginList() []*Plugin {
	out := make([]*Plugin, 0)

	// create a unique list of plugins in use by instances and return that
	for _, i := range pr.InstanceList() {
		ip := i.Plugin

		seen := false
		for _, p := range out {
			if p.Name == ip.Name {
				seen = true
			}
		}

		// make sure each plugin is only inserted once
		if !seen {
			out = append(out, ip)
		}
	}

	return out
}

// InactivePluginList returns a list of plugins that do not have any registered instances.
func (pr *pluginRegistry) InactivePluginList() []*Plugin {
	out := make([]*Plugin, 0)
	inst := pr.InstanceList()

	// for each plugin, add it to the out list only if there are no instances using it
	for _, p := range pr.PluginList() {
		active := false
		for _, i := range inst {
			if p.Name == i.Plugin.Name {
				active = true
			}
		}

		if !active {
			out = append(out, p)
		}
	}

	return out
}

func (p *Plugin) String() string {
	return p.Name
}

func (inst *Instance) String() string {
	return fmt.Sprintf("%s/%s", inst.Name, inst.RoomId)
}


================================================
FILE: hal/prefs.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
)

// provides a persistent configuration store

// Order of precendence for prefs:
// user -> room -> broker -> plugin -> global -> default

// PrefsTable contains the SQL to create the prefs table
// key field is called pkey because key is a reserved word
const PrefsTable = `
CREATE TABLE IF NOT EXISTS prefs (
	 id      INT NOT NULL AUTO_INCREMENT, -- only used for deleting/updating by id
	 user    VARCHAR(191) DEFAULT "",
	 room    VARCHAR(191) DEFAULT "",
	 broker  VARCHAR(191) DEFAULT "",
	 plugin  VARCHAR(191) DEFAULT "",
	 pkey    VARCHAR(191) NOT NULL,
	 value   MEDIUMTEXT,
	 INDEX(id), -- required by mysql for non-PK auto_increment
	 -- InnoDB limits indexes to 767 bytes so have the PK only index the first
	 -- 32 characters of each column as a compromise
	 -- (5 cols * 4 bytes * 32 chars = 640)
	 PRIMARY KEY(user(32), room(32), broker(32), plugin(32), pkey(32))
)`

/*
   -- test data, will remove once there are automated tests
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "", "", "", "foo", "user");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "CORE", "", "", "foo",
   "user-room");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "CORE", "slack", "", "foo",
   "user-room-broker");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "CORE", "slack", "uptime",
   "foo", "user-room-broker-plugin");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "", "slack", "uptime", "foo",
   "user-broker-plugin");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "CORE", "", "uptime",
   "foo", "user-room-plugin");
   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES ("tobert", "", "", "uptime", "foo",
   "user-plugin");
*/

// !prefs list --scope plugin --plugin autoresponder
// !prefs get --scope room --plugin autoresponder --room CORE --key timezone
// !prefs set --scope user --plugin autoresponder --room CORE

// Pref is a key/value pair associated with a combination of user, plugin,
// borker, or room.
type Pref struct {
	User    string `json:"user"`
	Plugin  string `json:"plugin"`
	Broker  string `json:"broker"`
	Room    string `json:"room"`
	Key     string `json:"key"`
	Value   string `json:"value"`
	Default string `json:"default"`
	Success bool   `json:"-"`
	Error   error  `json:"-"`
	Id      int    `json:"id"`
}

type Prefs []Pref

// GetPref will retreive the most-specific preference from pref
// storage using the parameters provided. This is a bit like pattern
// matching. If no match is found, the provided default is returned.
// TODO: explain this better
func GetPref(user, broker, room, plugin, key, def string) Pref {
	pref := Pref{
		User:    user,
		Room:    room,
		Broker:  broker,
		Plugin:  plugin,
		Key:     key,
		Default: def,
	}

	up := pref.Get()
	if up.Success {
		return up
	}

	// no match, return the default
	pref.Value = def
	return pref
}

// SetPref sets a preference and is shorthand for Pref{}.Set().
func SetPref(user, broker, room, plugin, key, value string) error {
	pref := Pref{
		User:   user,
		Room:   room,
		Broker: broker,
		Plugin: plugin,
		Key:    key,
		Value:  value,
	}

	return pref.Set()
}

// GetPrefs retrieves a set of preferences from the database. The
// settings are matched exactly on user,broker,room,plugin.
// e.g. GetPrefs("", "", "", "uptime") would get only records that
// have user/broker/room set to the empty string and room
// set to "uptime". A record with user "pford" and plugin "uptime"
// would not be included.
func GetPrefs(user, broker, room, plugin string) Prefs {
	pref := Pref{
		User:   user,
		Broker: broker,
		Room:   room,
		Plugin: plugin,
	}
	return pref.get()
}

// FindPrefs gets all records that match any of the inputs that are
// not empty strings. (hint: user="x", broker="y"; WHERE user=? OR broker=?)
func FindPrefs(user, broker, room, plugin, key string) Prefs {
	pref := Pref{
		User:   user,
		Broker: broker,
		Room:   room,
		Plugin: plugin,
		Key:    key,
	}
	return pref.find(false)
}

// RmPrefId removes a preference from the database by its numeric id.
func RmPrefId(id int) error {
	db := SqlDB()
	SqlInit(PrefsTable)

	_, err := db.Exec("DELETE FROM prefs WHERE id=?", &id)
	return err
}

// Get retrieves a value from the database. If the database returns
// an error, Success will be false and the Error field will be populated.
func (in *Pref) Get() Pref {
	prefs := in.get()

	if len(prefs) == 1 {
		return prefs[0]
	} else if len(prefs) > 1 {
		panic("TOO MANY PREFS")
	} else if len(prefs) == 0 {
		out := *in
		// only set success to false if there is also an error
		// queries with 0 rows are successful
		if out.Error != nil {
			out.Success = false
		} else {
			out.Success = true
			out.Value = out.Default
		}
		return out
	}

	panic("BUG: should be impossible to reach this point")
}

// GetPrefs returns all preferences that match the fields set in the handle.
func (in *Pref) GetPrefs() Prefs {
	return in.get()
}

func (in *Pref) get() Prefs {
	db := SqlDB()
	SqlInit(PrefsTable)

	sql := `SELECT user,room,broker,plugin,pkey,value,id
	        FROM prefs
	        WHERE user=?
			  AND room=?
			  AND broker=?
			  AND plugin=?`
	params := []interface{}{&in.User, &in.Room, &in.Broker, &in.Plugin}

	// only query by key if it's specified, otherwise get all keys for the selection
	if in.Key != "" {
		sql += " AND pkey=?"
		params = append(params, &in.Key)
	}

	rows, err := db.Query(sql, params...)
	if err != nil {
		log.Printf("Returning default due to SQL query failure: %s", err)
		return Prefs{}
	}

	defer rows.Close()

	out := make(Prefs, 0)

	for rows.Next() {
		p := *in

		err := rows.Scan(&p.User, &p.Room, &p.Broker, &p.Plugin, &p.Key, &p.Value, &p.Id)

		if err != nil {
			log.Printf("Returning default due to row iteration failure: %s", err)
			p.Success = false
			p.Value = in.Default
			p.Error = err
		} else {
			p.Success = true
			p.Error = nil
		}

		out = append(out, p)
	}

	return out
}

// Set writes the value and returns a new struct with the new value.
func (in *Pref) Set() error {
	db := SqlDB()
	err := SqlInit(PrefsTable)
	if err != nil {
		log.Printf("Failed to initialize the prefs table: %s", err)
		return err
	}

	sql := `INSERT INTO prefs
						(value,user,room,broker,plugin,pkey)
			VALUES (?,?,?,?,?,?)
			ON DUPLICATE KEY
			UPDATE value=?, user=?, room=?, broker=?, plugin=?, pkey=?`

	params := []interface{}{
		&in.Value, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key,
		&in.Value, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key,
	}

	_, err = db.Exec(sql, params...)
	if err != nil {
		log.Printf("Pref.Set() write failed: %s", err)
		return err
	}

	return nil
}

// Set writes the value and returns a new struct with the new value.
func (in *Pref) Delete() error {
	db := SqlDB()

	err := SqlInit(PrefsTable)
	if err != nil {
		log.Printf("Failed to initialize the prefs table: %s", err)
		return err
	}

	sql := `DELETE FROM prefs
			WHERE user=?
			  AND room=?
			  AND broker=?
			  AND plugin=?
			  AND pkey=?`

	// TODO: verify only one row was deleted
	_, err = db.Exec(sql, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key)
	if err != nil {
		log.Printf("Pref.Delete() write failed: %s", err)
		return err
	}

	return nil
}

// Find retrieves all preferences from the database that match any field in the
// handle's fields. If the Key field is set, it is matched first.
// The resulting list is sorted before it is returned.
// Unlike Get(), empty string fields are not included in the (generated) query
// so it can potentially match a lot of rows.
// Returns an empty list and logs upon errors.
func (p Pref) Find() Prefs {
	return p.find(false)
}

func (p Pref) FindKey(key string) Prefs {
	p.Key = key
	return p.find(true)
}

// FindKey is like Find() but the provide key is required.
func FindKey(key string) Prefs {
	p := Pref{Key: key}
	return p.find(true)
}

func (p Pref) find(keyRequired bool) Prefs {
	db := SqlDB()
	SqlInit(PrefsTable)

	fields := make([]string, 0)
	params := make([]interface{}, 0)

	// NOTE: the order of these statements is important!
	if keyRequired {
		// ok for it to be "" to match no key, but still required
		// query is appended below
		params = append(params, p.Key)
	}

	if p.User != "" {
		fields = append(fields, "user=?")
		params = append(params, p.User)
	}

	if p.Room != "" {
		fields = append(fields, "room=?")
		params = append(params, p.Room)
	}

	if p.Broker != "" {
		fields = append(fields, "broker=?")
		params = append(params, p.Broker)
	}

	if p.Plugin != "" {
		fields = append(fields, "plugin=?")
		params = append(params, p.Plugin)
	}

	if !keyRequired && p.Key != "" {
		fields = append(fields, "pkey=?")
		params = append(params, p.Key)
	}

	q := bytes.NewBufferString("SELECT user,room,broker,plugin,pkey,value,id\n")
	q.WriteString("FROM prefs\n")

	if keyRequired || len(fields) > 0 {
		q.WriteString("\nWHERE ")
	}

	if keyRequired {
		q.WriteString("pkey=? AND (")
	}

	// TODO: maybe it's silly to make it easy for Find() to get all preferences
	// but let's cross that bridge when we come to it
	if len(fields) > 0 {
		// might make sense to add a param to this func to make it easy to
		// switch this between AND/OR for unions/intersections
		q.WriteString(strings.Join(fields, "\n  OR "))
	}

	if keyRequired {
		q.WriteString("\n)")
	}

	out := make(Prefs, 0)
	rows, err := db.Query(q.String(), params...)
	if err != nil {
		log.Println(q.String())
		log.Printf("Query failed: %s", err)
		return out
	}
	defer rows.Close()

	for rows.Next() {
		row := Pref{}
		err = rows.Scan(&row.User, &row.Room, &row.Broker, &row.Plugin, &row.Key, &row.Value, &row.Id)
		// improbable in practice - follows previously mentioned conventions for errors
		if err != nil {
			log.Printf("Fetching a row failed: %s\n", err)
			row.Error = err
			row.Success = false
			row.Value = p.Default
		} else {
			row.Error = nil
			row.Success = true
		}

		out = append(out, row)
	}

	sort.Sort(out)

	return out
}

// Clone returns a full/deep copy of the Prefs list.
func (prefs Prefs) Clone() Prefs {
	out := make(Prefs, len(prefs))

	for i, pref := range prefs {
		copy := pref
		out[i] = copy
	}

	return out
}

// One returns the most-specific preference from the Prefs according
// to the precedence order of user>room>broker>plugin>global.
//
func (prefs Prefs) One() Pref {
	if len(prefs) == 0 {
		return Pref{Success: false}
	}

	sort.Sort(prefs)
	return prefs[0]
}

// SetKey returns a copy of the pref with the key set to the provided string.
// Useful for chaining e.g. fooPrefs := p.SetKey("foo").Find().
func (pref Pref) SetKey(key string) Pref {
	pref.Key = key // already a copy
	return pref
}

// SetUser returns a copy of the pref with the User set to the provided string.
func (pref Pref) SetUser(user string) Pref {
	pref.User = user
	return pref
}

// SetBroker returns a copy of the pref with the Broker set to the provided string.
func (pref Pref) SetBroker(broker string) Pref {
	// TODO: validate?
	pref.Broker = broker
	return pref
}

// User filters the preference list by user, returning a new Prefs
// e.g. uprefs = prefs.User("adent")
func (prefs Prefs) User(user string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.User == user {
			out = append(out, pref)
		}
	}

	return out
}

// Room filters the preference list by room, returning a new Prefs
// e.g. instprefs = prefs.Room("magrathea").Plugin("uptime").Broker("slack")
func (prefs Prefs) Room(room string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.Room == room {
			out = append(out, pref)
		}
	}

	return out
}

// Broker filters the preference list by broker, returning a new Prefs
func (prefs Prefs) Broker(broker string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.Broker == broker {
			out = append(out, pref)
		}
	}

	return out
}

// Plugin filters the preference list by plugin, returning a new Prefs
func (prefs Prefs) Plugin(plugin string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.Plugin == plugin {
			out = append(out, pref)
		}
	}

	return out
}

// Key filters the preference list by key, returning a new Prefs
func (prefs Prefs) Key(key string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.Key == key {
			out = append(out, pref)
		}
	}

	return out
}

// Value filters the preference list by key, returning a new Prefs
func (prefs Prefs) Value(value string) Prefs {
	out := make(Prefs, 0)

	for _, pref := range prefs {
		if pref.Value == value {
			out = append(out, pref)
		}
	}

	return out
}

// Table returns Prefs as a 2d list ready to hand off to e.g. hal.AsciiTable()
func (prefs Prefs) Table() [][]string {
	out := make([][]string, 1)
	out[0] = []string{"User", "Room", "Broker", "Plugin", "Key", "Value", "ID"}

	for _, pref := range prefs {
		m := []string{
			pref.User,
			pref.Room,
			pref.Broker,
			pref.Plugin,
			pref.Key,
			pref.Value,
			fmt.Sprintf("%d", pref.Id),
		}

		out = append(out, m)
	}

	return out
}

func (ps Prefs) Len() int           { return len(ps) }
func (ps Prefs) Swap(i, j int)      { ps[i], ps[j] = ps[j], ps[i] }
func (ps Prefs) Less(i, j int) bool { return ps[i].precedence() < ps[j].precedence() }

func (p *Pref) precedence() int {
	if !p.Success {
		return 0
	}
	if p.User != "" {
		return 5
	}
	if p.Room != "" {
		return 4
	}
	if p.Broker != "" {
		return 3
	}
	if p.Plugin != "" {
		return 2
	}
	if p.Key != "" {
		return 1
	}
	return 0
}

func (p *Pref) String() string {
	return fmt.Sprintf(`Pref{
	User:    %q,
	Room:    %q,
	Broker:  %q,
	Plugin:  %q,
	Key:     %q,
	Value:   %q,
	Default: %q,
	Success: %t,
	Error:   %v,
	Id:      %d,
}`, p.User, p.Room, p.Broker, p.Plugin, p.Key, p.Value, p.Default, p.Success, p.Error, p.Id)

}

func (p *Prefs) String() string {
	data := p.Table()
	return AsciiTable(data[0], data[1:])
}


================================================
FILE: hal/router.go
================================================
package hal

/*
 * Copyright 2016-2017 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import (
	"fmt"
	"runtime/debug"
	"strings"
	"sync"
)

// RouterCTX holds the router's context, including input/output chans.
type RouterCTX struct {
	brokers map[string]Broker
	in      chan *Evt     // messages from brokers --> plugins
	out     chan *Evt     // messages from plugins --> brokers
	quit    chan struct{} // to shut down the router loop
	update  chan struct{} // to notify the router that the instance list changed
	mut     sync.Mutex
	init    sync.Once
}

type fwdBroker struct {
	from Broker
	to   Broker
}

var routerSingleton RouterCTX

// Router returns the singleton router context. The router is initialized
// on the first call to this function.
func Router() *RouterCTX {
	routerSingleton.init.Do(func() {
		routerSingleton.in = make(chan *Evt, 1000)
		routerSingleton.out = make(chan *Evt, 1000)
		routerSingleton.quit = make(chan struct{}, 1)
		routerSingleton.update = make(chan struct{}, 1)
		routerSingleton.brokers = make(map[string]Broker)
	})

	return &routerSingleton
}

// forwardChan forwards events from one chan of to another.
// TODO: figure out if this needs to check for closed channels, etc.
func forwardChan(from, to chan *Evt) {
	for {
		select {
		case evt := <-from:
			to <- evt
		}
	}
}

// AddBroker adds a broker to the router and starts forwarding
// events between it and the router.
func (r *RouterCTX) AddBroker(b Broker) {
	r.mut.Lock()
	defer r.mut.Unlock()

	if _, exists := r.brokers[b.Name()]; exists {
		panic(fmt.Sprintf("BUG: broker '%s' added > 1 times.", b.Name()))
	}

	b2r := make(chan *Evt, 1000) // messages from the broker to the router

	// start the broker's event stream
	go b.Stream(b2r)

	// forward events from the broker to the router's input channel
	go forwardChan(b2r, r.in)

	r.brokers[b.Name()] = b
}

func (r *RouterCTX) Send(evt Evt) {
	r.in <- &evt
}

// GetBroker retrieves a broker handle by name.
func (r *RouterCTX) GetBroker(name string) Broker {
	r.mut.Lock()
	defer r.mut.Unlock()

	if broker, exists := r.brokers[name]; exists {
		return broker
	}

	return nil
}

// Brokers returns all brokers that have been added to the router.
// The returned list is not in any particular order.
func (r *RouterCTX) Brokers() []Broker {
	r.mut.Lock()
	defer r.mut.Unlock()

	out := make([]Broker, len(r.brokers))
	i := 0
	for _, b := range r.brokers {
		out[i] = b
		i++
	}

	return out
}

func (r *RouterCTX) Quit() {
	r.quit <- struct{}{}
}

// Route is the main method for the router. It blocks and should be run in a
// goroutine exactly once. Running more than one router in the same process
// will result in shenanigans.
func (r *RouterCTX) Route() {
	for {
		select {
		case <-r.quit:
			close(r.quit)
			break
		case evt := <-r.in:
			// events are processed concurrently, plugins are not
			go r.inEvent(evt)
		case evt := <-r.out:
			go r.outEvent(evt)
		}
	}
}

// inEvent processes one event and is intended to run in a goroutine.
func (r *RouterCTX) inEvent(evt *Evt) {
	var pname string // must be in the recovery handler's scope

	// detect invalid commands & count executions
	var ranPlugins int

	// get a snapshot of the instance list
	// TODO: keep an eye on the cost of copying this list for every message
	pr := PluginRegistry()
	instances := pr.InstanceList()

	// if a plugin panics, catch it & log it
	// TODO: report errors to a channel?
	defer func() {
		if r := recover(); r != nil {
			log.Printf("recovered panic in plugin %q\n", pname)
			log.Printf("panic: %q", r)
			debug.PrintStack()
		}
	}()

	// if the event doesn't have a command, try to extract one
	if evt.Command == "" {
		body := strings.TrimSpace(evt.Body)
		// check for a leading "!", which indicates it's a command
		if strings.HasPrefix(body, "!") {
			parts := strings.SplitN(body, " ", 2) // split off the first word
			if len(parts) > 0 {
				evt.Command = strings.TrimPrefix(parts[0], "!")
			}
		}
	}

	for _, inst := range instances {
		// the recovery handler will pick this up in a panic to provide
		// the name of the plugin that caused the panic
		pname = inst.Plugin.Name

		// check if it's the correct room
		if evt.RoomId != inst.RoomId {
			continue
		}

		// only process IsBot messages if the plugin has asked for them
		if evt.IsBot && !inst.Plugin.BotEvents {
			continue
		}

		// plugins with no RE filter receive every event
		noFilter := (inst.Regex == "" && inst.Plugin.Command == "")
		// events that match the RE filter are passed onto the plugin
		matchesRegex := (inst.Regex != "" && inst.regex.MatchString(evt.Body))
		// events with a Command field that matches exactly are passed to the plugin
		isCommand := (inst.Plugin.Command != "" && evt.Command == inst.Plugin.Command)

		// forward to plugins when any of the above rules passes
		if noFilter || matchesRegex || isCommand {
			// this will copy the struct twice. It's intentional to avoid
			// mutating the evt between calls. The plugin func signature
			// forces the second copy.
			evtcpy := *evt

			// pass the plugin instance pointer to the plugin function so
			// it can access its fields for settings, etc.
			evtcpy.instance = inst

			// call the plugin function
			// this may block other plugins from processing the same event but
			// since it's already in a goroutine, other events won't be blocked
			inst.Func(evtcpy)

			ranPlugins++
		}
	}

	if evt.IsBot {
		return
	}

	// no plugins were executed but evt.Body looks like a command
	if ranPlugins == 0 && strings.HasPrefix(strings.TrimSpace(evt.Body), "!") {
		// automatically load the pluginmgr plugin if someone tries to use it and it wasn't attached
		// don't bother if the bot was built without pluginmgr, in which case err != nil
		if strings.HasPrefix(strings.TrimSpace(evt.Body), "!plugin") {
			mgr, err := pr.GetPlugin("pluginmgr")
			if err == nil {
				inst := mgr.Instance(evt.RoomId, evt.Broker)
				evtcpy := *evt
				evtcpy.instance = inst
				inst.Func(evtcpy)
			}
		} else {
			evt.Replyf("invalid bot command: %q (IsBot: %t) (%d plugins were executed for the event).", evt.Body, evt.IsBot, ranPlugins)
		}
	}
}

func (r *RouterCTX) outEvent(evt *Evt) {
	// TODO: fill in this stub
	log.Printf("STUB: did nothing with event %s", 
Download .txt
gitextract_tlficdtk/

├── .gitignore
├── LICENSE
├── NOTICE
├── OSSMETADATA
├── README.md
├── brokers/
│   ├── console/
│   │   └── broker.go
│   ├── hipchat/
│   │   └── broker.go
│   └── slack/
│       └── broker.go
├── example/
│   ├── demos/
│   │   ├── colorparser.go
│   │   ├── imgtable.go
│   │   └── utf8table.go
│   ├── docker-repl/
│   │   └── main.go
│   ├── everything/
│   │   └── main.go
│   ├── minimal/
│   │   └── main.go
│   └── repl/
│       └── main.go
├── hal/
│   ├── asciitable.go
│   ├── asciitable_test.go
│   ├── broker.go
│   ├── cmd.go
│   ├── cmd_test.go
│   ├── counter.go
│   ├── directory.go
│   ├── event.go
│   ├── event_test.go
│   ├── kv.go
│   ├── logger.go
│   ├── logger_test.go
│   ├── periodic.go
│   ├── persist_plugins.go
│   ├── plugins.go
│   ├── prefs.go
│   ├── router.go
│   ├── secrets.go
│   ├── secrets_test.go
│   ├── sqldb.go
│   ├── text2image.go
│   ├── text2image_test.go
│   ├── ttlcache.go
│   ├── ttlcache_test.go
│   ├── utf8table.go
│   └── utf8table_test.go
└── plugins/
    ├── archive/
    │   └── plugin.go
    ├── blabber/
    │   └── plugin.go
    ├── cross_the_streams/
    │   └── plugin.go
    ├── docker/
    │   └── plugin.go
    ├── google_calendar/
    │   ├── google.go
    │   └── plugin.go
    ├── guys/
    │   └── plugin.go
    ├── inspect/
    │   └── plugin.go
    ├── mark/
    │   └── plugin.go
    ├── pagerduty/
    │   ├── helpers.go
    │   ├── oncall_plugin.go
    │   ├── page_plugin.go
    │   ├── pd_events_v1.go
    │   ├── pd_events_v2.go
    │   ├── pd_oncall.go
    │   ├── pd_policy.go
    │   ├── pd_schedule.go
    │   ├── pd_service.go
    │   ├── pd_team.go
    │   ├── pd_types.go
    │   ├── pd_user.go
    │   ├── plugin.go
    │   └── poller.go
    ├── pluginmgr/
    │   └── plugin.go
    ├── prefmgr/
    │   ├── http.go
    │   └── plugin.go
    ├── roster/
    │   └── plugin.go
    ├── seppuku/
    │   └── plugin.go
    ├── spam/
    │   └── plugin.go
    └── uptime/
        └── plugin.go
Download .txt
SYMBOL INDEX (690 symbols across 66 files)

FILE: brokers/console/broker.go
  type Config (line 33) | type Config struct
    method NewBroker (line 129) | func (c Config) NewBroker(name string) Broker {
  type Broker (line 35) | type Broker struct
    method Name (line 145) | func (cb Broker) Name() string {
    method Send (line 149) | func (cb Broker) Send(e hal.Evt) {
    method SendDM (line 153) | func (cb Broker) SendDM(e hal.Evt) {
    method Leave (line 157) | func (cb Broker) Leave(roomId string) error {
    method GetTopic (line 162) | func (cb Broker) GetTopic(roomId string) (string, error) {
    method SetTopic (line 166) | func (cb Broker) SetTopic(roomId, topic string) error {
    method SendTable (line 172) | func (cb Broker) SendTable(e hal.Evt, hdr []string, rows [][]string) {
    method LooksLikeRoomId (line 176) | func (cb Broker) LooksLikeRoomId(room string) bool {
    method LooksLikeUserId (line 180) | func (cb Broker) LooksLikeUserId(user string) bool {
    method SimpleStdin (line 187) | func (cb Broker) SimpleStdin() {
    method SimpleStdout (line 207) | func (cb Broker) SimpleStdout() {
    method Stream (line 220) | func (cb Broker) Stream(out chan *hal.Evt) {
    method RoomIdToName (line 262) | func (b Broker) RoomIdToName(in string) string { return in }
    method RoomNameToId (line 263) | func (b Broker) RoomNameToId(in string) string { return in }
    method UserIdToName (line 264) | func (b Broker) UserIdToName(in string) string { return in }
    method UserNameToId (line 265) | func (b Broker) UserNameToId(in string) string { return in }
  type SlashReaction (line 43) | type SlashReaction
  function REPL (line 53) | func REPL(name, prefix string) {

FILE: brokers/hipchat/broker.go
  type Broker (line 32) | type Broker struct
    method Name (line 94) | func (hb Broker) Name() string {
    method Send (line 98) | func (hb Broker) Send(evt hal.Evt) {
    method SendDM (line 115) | func (hb Broker) SendDM(e hal.Evt) {
    method Leave (line 120) | func (hb Broker) Leave(roomId string) error {
    method GetTopic (line 131) | func (hb Broker) GetTopic(roomId string) (string, error) {
    method SetTopic (line 136) | func (hb Broker) SetTopic(roomId, topic string) error {
    method SendTable (line 140) | func (hb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {
    method LooksLikeRoomId (line 148) | func (hb Broker) LooksLikeRoomId(room string) bool {
    method LooksLikeUserId (line 153) | func (hb Broker) LooksLikeUserId(user string) bool {
    method Subscribe (line 160) | func (hb *Broker) Subscribe(room, alias string) {
    method heartbeat (line 170) | func (hb *Broker) heartbeat(t time.Time) {
    method Stream (line 185) | func (hb Broker) Stream(out chan *hal.Evt) {
    method RoomIdToName (line 246) | func (b Broker) RoomIdToName(in string) string {
    method RoomNameToId (line 254) | func (b Broker) RoomNameToId(in string) string {
    method UserIdToName (line 264) | func (b Broker) UserIdToName(in string) string { return in }
    method UserNameToId (line 265) | func (b Broker) UserNameToId(in string) string { return in }
  type Config (line 38) | type Config struct
    method NewBroker (line 53) | func (c Config) NewBroker(name string) Broker {
  constant HIPCHAT_HOST (line 46) | HIPCHAT_HOST = `chat.hipchat.com:5223`

FILE: brokers/slack/broker.go
  type Broker (line 41) | type Broker struct
    method Name (line 95) | func (sb Broker) Name() string {
    method Send (line 99) | func (sb Broker) Send(evt hal.Evt) {
    method SendAsSnippet (line 110) | func (sb Broker) SendAsSnippet(evt hal.Evt) {
    method SendAsIs (line 134) | func (sb Broker) SendAsIs(evt hal.Evt) {
    method SendDM (line 150) | func (sb Broker) SendDM(evt hal.Evt) {
    method Leave (line 178) | func (sb Broker) Leave(roomId string) error {
    method GetTopic (line 183) | func (sb Broker) GetTopic(roomId string) (string, error) {
    method SetTopic (line 188) | func (sb Broker) SetTopic(roomId, topic string) error {
    method SendTable (line 194) | func (sb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {
    method SendAsImage (line 214) | func (sb Broker) SendAsImage(evt hal.Evt) {
    method LooksLikeRoomId (line 277) | func (sb Broker) LooksLikeRoomId(room string) bool {
    method LooksLikeUserId (line 288) | func (sb Broker) LooksLikeUserId(user string) bool {
    method HasRoom (line 300) | func (sb Broker) HasRoom(room string) bool {
    method Stream (line 316) | func (sb Broker) Stream(out chan *hal.Evt) {
    method FillUserCache (line 548) | func (sb *Broker) FillUserCache() {
    method FillRoomCache (line 585) | func (sb *Broker) FillRoomCache() {
    method UserIdToName (line 623) | func (sb Broker) UserIdToName(id string) string {
    method RoomIdToName (line 665) | func (sb Broker) RoomIdToName(id string) string {
    method UserNameToId (line 708) | func (sb Broker) UserNameToId(name string) string {
    method RoomNameToId (line 739) | func (sb Broker) RoomNameToId(name string) string {
    method injectRoomId (line 768) | func (sb Broker) injectRoomId(id, name string) {
  type Config (line 57) | type Config struct
    method NewBroker (line 69) | func (c Config) NewBroker(name string) Broker {
  function init (line 63) | func init() {
  function SlackTime (line 532) | func SlackTime(t string) time.Time {

FILE: example/demos/colorparser.go
  function main (line 27) | func main() {

FILE: example/demos/imgtable.go
  function main (line 30) | func main() {

FILE: example/demos/utf8table.go
  function main (line 26) | func main() {

FILE: example/docker-repl/main.go
  function main (line 32) | func main() {

FILE: example/everything/main.go
  function main (line 41) | func main() {
  function requireEnv (line 168) | func requireEnv(key string) string {
  function defaultEnv (line 177) | func defaultEnv(key, def string) string {

FILE: example/minimal/main.go
  function main (line 27) | func main() {

FILE: example/repl/main.go
  function main (line 31) | func main() {

FILE: hal/asciitable.go
  function AsciiTable (line 29) | func AsciiTable(hdr []string, rows [][]string) string {

FILE: hal/asciitable_test.go
  function TestAsciiTable (line 24) | func TestAsciiTable(t *testing.T) {

FILE: hal/broker.go
  type Broker (line 20) | type Broker interface

FILE: hal/cmd.go
  type Cmd (line 49) | type Cmd struct
    method ListSubCmds (line 233) | func (c *Cmd) ListSubCmds() []*SubCmd {
    method _kvparams (line 242) | func (c *Cmd) _kvparams() []*KVParam {
    method _boolparams (line 251) | func (c *Cmd) _boolparams() []*BoolParam {
    method _idxparams (line 260) | func (c *Cmd) _idxparams() map[int]*IdxParam {
    method Aliases (line 269) | func (c *Cmd) Aliases() []string {
    method assertZeroIdxParams (line 278) | func (c *Cmd) assertZeroIdxParams() {
    method assertZeroKeyParams (line 286) | func (c *Cmd) assertZeroKeyParams() {
    method AddKVParam (line 296) | func (c *Cmd) AddKVParam(key string, required bool) *KVParam {
    method AddBoolParam (line 310) | func (c *Cmd) AddBoolParam(key string, required bool) *BoolParam {
    method AddIdxParam (line 325) | func (c *Cmd) AddIdxParam(position int, name string, required bool) *I...
    method AddAlias (line 400) | func (c *Cmd) AddAlias(alias string) *Cmd {
    method Parent (line 416) | func (c *Cmd) Parent() *Cmd {
    method MustSubCmd (line 421) | func (c *Cmd) MustSubCmd() bool {
    method Usage (line 426) | func (c *Cmd) Usage() string {
    method SetUsage (line 461) | func (c *Cmd) SetUsage(usage string) *Cmd {
    method Cmd (line 767) | func (c *Cmd) Cmd() *Cmd {
    method Token (line 775) | func (c *Cmd) Token() string {
    method AddSubCmd (line 780) | func (c *Cmd) AddSubCmd(token string) *SubCmd {
    method GetKVParam (line 790) | func (c *Cmd) GetKVParam(key string) *KVParam {
    method GetBoolParam (line 800) | func (c *Cmd) GetBoolParam(key string) *BoolParam {
    method GetIdxParam (line 811) | func (c *Cmd) GetIdxParam(idx int) *IdxParam {
    method HasKVParam (line 821) | func (c *Cmd) HasKVParam(key string) bool {
    method HasBoolParam (line 831) | func (c *Cmd) HasBoolParam(key string) bool {
    method HasIdxParam (line 841) | func (c *Cmd) HasIdxParam(idx int) bool {
    method SubCmds (line 848) | func (c *Cmd) SubCmds() []*SubCmd {
    method GetSubCmd (line 853) | func (c *Cmd) GetSubCmd(token string) *SubCmd {
    method Process (line 871) | func (c *Cmd) Process(argv []string) (*CmdInst, error) {
    method HasSubCmdToken (line 1244) | func (c *Cmd) HasSubCmdToken(token string) bool {
    method HasKeyParam (line 1260) | func (c *Cmd) HasKeyParam(key string) bool {
    method ListNamedParams (line 1282) | func (c *Cmd) ListNamedParams() []NamedParam {
  type SubCmd (line 61) | type SubCmd struct
    method AddKVParam (line 347) | func (c *SubCmd) AddKVParam(key string, required bool) *KVParam {
    method AddBoolParam (line 362) | func (c *SubCmd) AddBoolParam(key string, required bool) *BoolParam {
    method AddIdxParam (line 378) | func (c *SubCmd) AddIdxParam(position int, name string, required bool)...
    method AddAlias (line 405) | func (s *SubCmd) AddAlias(alias string) *SubCmd {
    method SetUsage (line 467) | func (s *SubCmd) SetUsage(usage string) *SubCmd {
    method SubCmd (line 771) | func (s *SubCmd) SubCmd() *SubCmd {
  type CmdInst (line 66) | type CmdInst struct
    method Usage (line 473) | func (c *CmdInst) Usage() string {
    method listSubCmdInst (line 1221) | func (c *CmdInst) listSubCmdInst() []*SubCmdInst {
    method SubCmdToken (line 1308) | func (c *CmdInst) SubCmdToken() string {
    method SubCmdInst (line 1324) | func (c *CmdInst) SubCmdInst() *SubCmdInst {
    method HasKVParamInst (line 1328) | func (c *CmdInst) HasKVParamInst(key string) bool {
    method HasKVParam (line 1338) | func (c *CmdInst) HasKVParam(key string) bool {
    method HasBoolParamInst (line 1346) | func (c *CmdInst) HasBoolParamInst(key string) bool {
    method HasBoolParam (line 1356) | func (c *CmdInst) HasBoolParam(key string) bool {
    method HasIdxParamInst (line 1360) | func (c *CmdInst) HasIdxParamInst(idx int) bool {
    method HasIdxParam (line 1366) | func (c *CmdInst) HasIdxParam(idx int) bool {
    method GetKVParamInst (line 1375) | func (c *CmdInst) GetKVParamInst(key string) *KVParamInst {
    method GetKVParam (line 1416) | func (c *CmdInst) GetKVParam(key string) *KVParam {
    method GetBoolParamInst (line 1437) | func (c *CmdInst) GetBoolParamInst(key string) *BoolParamInst {
    method GetBoolParam (line 1478) | func (c *CmdInst) GetBoolParam(key string) *BoolParam {
    method GetIdxParamInst (line 1499) | func (c *CmdInst) GetIdxParamInst(idx int) *IdxParamInst {
    method GetIdxParamInstByName (line 1538) | func (c *CmdInst) GetIdxParamInstByName(name string) *IdxParamInst {
    method GetIdxParam (line 1561) | func (c *CmdInst) GetIdxParam(idx int) *IdxParam {
    method appendKVParamInst (line 1579) | func (c *CmdInst) appendKVParamInst(pi *KVParamInst) {
    method appendBoolParamInst (line 1583) | func (c *CmdInst) appendBoolParamInst(pi *BoolParamInst) {
    method appendIdxParamInst (line 1587) | func (c *CmdInst) appendIdxParamInst(pi *IdxParamInst) {
    method ListKVParamInsts (line 1593) | func (c *CmdInst) ListKVParamInsts() []*KVParamInst {
    method ListBoolParamInsts (line 1602) | func (c *CmdInst) ListBoolParamInsts() []*BoolParamInst {
    method mapIdxParamInsts (line 1611) | func (c *CmdInst) mapIdxParamInsts() map[int]*IdxParamInst {
    method ListIdxParamInsts (line 1619) | func (c *CmdInst) ListIdxParamInsts() []*IdxParamInst {
    method Remainder (line 1631) | func (c *CmdInst) Remainder() []string {
  type SubCmdInst (line 75) | type SubCmdInst struct
    method SubCmdToken (line 1316) | func (c *SubCmdInst) SubCmdToken() string {
    method HasKVParam (line 1342) | func (c *SubCmdInst) HasKVParam(key string) bool {
    method HasIdxParam (line 1370) | func (c *SubCmdInst) HasIdxParam(idx int) bool {
    method GetKVParamInst (line 1396) | func (c *SubCmdInst) GetKVParamInst(key string) *KVParamInst {
    method GetKVParam (line 1426) | func (c *SubCmdInst) GetKVParam(key string) *KVParam {
    method GetBoolParamInst (line 1458) | func (c *SubCmdInst) GetBoolParamInst(key string) *BoolParamInst {
    method GetBoolParam (line 1488) | func (c *SubCmdInst) GetBoolParam(key string) *BoolParam {
    method GetIdxParamInst (line 1518) | func (c *SubCmdInst) GetIdxParamInst(idx int) *IdxParamInst {
    method GetIdxParamInstByName (line 1550) | func (c *SubCmdInst) GetIdxParamInstByName(name string) *IdxParamInst {
    method GetIdxParam (line 1570) | func (c *SubCmdInst) GetIdxParam(idx int) *IdxParam {
  type KVParam (line 81) | type KVParam struct
    method AddAlias (line 411) | func (p *KVParam) AddAlias(alias string) *KVParam {
    method Usage (line 477) | func (p *KVParam) Usage() string {
    method SetUsage (line 490) | func (p *KVParam) SetUsage(usage string) *KVParam {
    method SetDefault (line 507) | func (p *KVParam) SetDefault(def string) *KVParam {
    method Key (line 525) | func (p *KVParam) Key() string {
    method Name (line 551) | func (p *KVParam) Name() string {
    method IsRequired (line 567) | func (p *KVParam) IsRequired() bool {
    method Cmd (line 578) | func (p *KVParam) Cmd() *Cmd {
    method SubCmd (line 604) | func (p *KVParam) SubCmd() *SubCmd {
    method newInst (line 628) | func (p *KVParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, is...
    method Aliases (line 1640) | func (p *KVParam) Aliases() []string {
  type BoolParam (line 93) | type BoolParam struct
    method Usage (line 481) | func (p *BoolParam) Usage() string {
    method SetUsage (line 496) | func (p *BoolParam) SetUsage(usage string) *BoolParam {
    method SetDefault (line 513) | func (p *BoolParam) SetDefault(def bool) *BoolParam {
    method Key (line 529) | func (p *BoolParam) Key() string {
    method Name (line 557) | func (p *BoolParam) Name() string {
    method IsRequired (line 570) | func (p *BoolParam) IsRequired() bool {
    method Cmd (line 587) | func (p *BoolParam) Cmd() *Cmd {
    method SubCmd (line 612) | func (p *BoolParam) SubCmd() *SubCmd {
    method newInst (line 639) | func (p *BoolParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, ...
  type IdxParam (line 105) | type IdxParam struct
    method Usage (line 485) | func (p *IdxParam) Usage() string {
    method SetUsage (line 502) | func (p *IdxParam) SetUsage(usage string) *IdxParam {
    method SetDefault (line 519) | func (p *IdxParam) SetDefault(def string) *IdxParam {
    method Idx (line 533) | func (p *IdxParam) Idx() int {
    method Name (line 563) | func (p *IdxParam) Name() string {
    method IsRequired (line 573) | func (p *IdxParam) IsRequired() bool {
    method Cmd (line 596) | func (p *IdxParam) Cmd() *Cmd {
    method SubCmd (line 620) | func (p *IdxParam) SubCmd() *SubCmd {
    method newInst (line 650) | func (p *IdxParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, i...
  type KVParamInst (line 117) | type KVParamInst struct
    method Key (line 537) | func (p *KVParamInst) Key() string {
    method Cmd (line 663) | func (p *KVParamInst) Cmd() *Cmd {
    method SubCmdInst (line 689) | func (p *KVParamInst) SubCmdInst() *SubCmdInst {
    method Found (line 713) | func (p *KVParamInst) Found() bool {
    method Required (line 725) | func (p *KVParamInst) Required() bool {
    method Param (line 737) | func (p *KVParamInst) Param() *KVParam {
    method errParam (line 751) | func (p *KVParamInst) errParam() NamedParam {
    method Value (line 1648) | func (p *KVParamInst) Value() string {
    method Name (line 1662) | func (p *KVParamInst) Name() string {
    method String (line 1679) | func (p *KVParamInst) String() (string, error) {
    method Int (line 1725) | func (p *KVParamInst) Int() (int, error) {
    method Float (line 1747) | func (p *KVParamInst) Float() (float64, error) {
    method Bool (line 1772) | func (p *KVParamInst) Bool() (bool, error) {
    method Duration (line 1815) | func (p *KVParamInst) Duration() (time.Duration, error) {
    method Time (line 1858) | func (p *KVParamInst) Time() (time.Time, error) {
    method MustString (line 1868) | func (p *KVParamInst) MustString() string {
    method DefString (line 1911) | func (p *KVParamInst) DefString(def string) string {
    method DefInt (line 1938) | func (p *KVParamInst) DefInt(def int) int {
  type BoolParamInst (line 129) | type BoolParamInst struct
    method Key (line 541) | func (p *BoolParamInst) Key() string {
    method Cmd (line 672) | func (p *BoolParamInst) Cmd() *Cmd {
    method SubCmdInst (line 697) | func (p *BoolParamInst) SubCmdInst() *SubCmdInst {
    method Found (line 717) | func (p *BoolParamInst) Found() bool {
    method Required (line 729) | func (p *BoolParamInst) Required() bool {
    method Param (line 741) | func (p *BoolParamInst) Param() *BoolParam {
    method errParam (line 756) | func (p *BoolParamInst) errParam() NamedParam {
    method Value (line 1652) | func (p *BoolParamInst) Value() bool {
    method Name (line 1668) | func (p *BoolParamInst) Name() string {
    method String (line 1688) | func (p *BoolParamInst) String() (string, error) {
  type IdxParamInst (line 141) | type IdxParamInst struct
    method Idx (line 545) | func (p *IdxParamInst) Idx() int {
    method Cmd (line 681) | func (p *IdxParamInst) Cmd() *Cmd {
    method SubCmdInst (line 705) | func (p *IdxParamInst) SubCmdInst() *SubCmdInst {
    method Found (line 721) | func (p *IdxParamInst) Found() bool {
    method Required (line 733) | func (p *IdxParamInst) Required() bool {
    method Param (line 745) | func (p *IdxParamInst) Param() *IdxParam {
    method errParam (line 761) | func (p *IdxParamInst) errParam() NamedParam {
    method Value (line 1656) | func (p *IdxParamInst) Value() string {
    method Name (line 1674) | func (p *IdxParamInst) Name() string {
    method String (line 1701) | func (p *IdxParamInst) String() (string, error) {
    method Int (line 1729) | func (p *IdxParamInst) Int() (int, error) {
    method Float (line 1751) | func (p *IdxParamInst) Float() (float64, error) {
    method Bool (line 1776) | func (p *IdxParamInst) Bool() (bool, error) {
    method Duration (line 1819) | func (p *IdxParamInst) Duration() (time.Duration, error) {
    method Time (line 1862) | func (p *IdxParamInst) Time() (time.Time, error) {
    method MustString (line 1877) | func (p *IdxParamInst) MustString() string {
    method DefString (line 1915) | func (p *IdxParamInst) DefString(def string) string {
    method DefInt (line 1942) | func (p *IdxParamInst) DefInt(def int) int {
  type tmpParamInst (line 153) | type tmpParamInst struct
    method attachKeyParam (line 1141) | func (tmp *tmpParamInst) attachKeyParam(whatever cmdorsubcmd) {
    method findAndAttachKeyParam (line 1211) | func (tmp *tmpParamInst) findAndAttachKeyParam(sub *SubCmdInst) {
  type stringValuedParamInst (line 164) | type stringValuedParamInst interface
  type cmdorsubcmd (line 177) | type cmdorsubcmd interface
  type NamedParam (line 189) | type NamedParam interface
  type SubCmdNotFound (line 195) | type SubCmdNotFound struct
    method Error (line 199) | func (e SubCmdNotFound) Error() string {
  type RequiredParamNotFound (line 206) | type RequiredParamNotFound struct
    method Error (line 211) | func (e RequiredParamNotFound) Error() string {
  type UnsupportedTimeFormatError (line 217) | type UnsupportedTimeFormatError struct
    method Error (line 222) | func (e UnsupportedTimeFormatError) Error() string {
  function NewCmd (line 227) | func NewCmd(token string, mustsubcmd bool) *Cmd {
  function looksLikeBool (line 1116) | func looksLikeBool(val string) bool {
  function looksLikeParam (line 1131) | func looksLikeParam(key string) bool {
  function intParam (line 1712) | func intParam(p stringValuedParamInst) (int, error) {
  function floatParam (line 1735) | func floatParam(p stringValuedParamInst) (float64, error) {
  function boolParam (line 1759) | func boolParam(p stringValuedParamInst) (bool, error) {
  function durationParam (line 1785) | func durationParam(p stringValuedParamInst) (time.Duration, error) {
  function timeParam (line 1829) | func timeParam(p stringValuedParamInst) (time.Time, error) {
  function defStringParam (line 1891) | func defStringParam(p stringValuedParamInst, def string) string {
  function defIntParam (line 1920) | func defIntParam(p stringValuedParamInst, def int) int {
  function defFloatParam (line 1947) | func defFloatParam(p stringValuedParamInst, def float64) float64 {
  function defBoolParam (line 1966) | func defBoolParam(p stringValuedParamInst, def bool) bool {

FILE: hal/cmd_test.go
  function TestCmd (line 24) | func TestCmd(t *testing.T) {

FILE: hal/counter.go
  constant CounterTable (line 23) | CounterTable = `
  function GetCounter (line 31) | func GetCounter(key string) (value int, err error) {
  function SetCounter (line 47) | func SetCounter(key string, value int) error {
  function IncrementCounter (line 61) | func IncrementCounter(key string) error {
  function DecrementCounter (line 75) | func DecrementCounter(key string) error {

FILE: hal/directory.go
  type directory (line 30) | type directory struct
    method exec (line 102) | func (dir *directory) exec(sql string, params ...interface{}) error {
    method query (line 113) | func (dir *directory) query(sql string, params ...interface{}) ([][]st...
    method Put (line 150) | func (dir *directory) Put(key, kind string, attrs map[string]string, e...
    method PutNode (line 183) | func (dir *directory) PutNode(key, kind string) error {
    method HasNode (line 188) | func (dir *directory) HasNode(key, kind string) (bool, error) {
    method DelNode (line 203) | func (dir *directory) DelNode(key, kind string) error {
    method PutNodeAttr (line 208) | func (dir *directory) PutNodeAttr(key, kind, attr, value string) error {
    method GetAttrNodes (line 215) | func (dir *directory) GetAttrNodes(attr, value string) ([][2]string, e...
    method HasEdge (line 232) | func (dir *directory) HasEdge(keyA, kindA, keyB, kindB string) (bool, ...
    method PutEdge (line 247) | func (dir *directory) PutEdge(keyA, kindA, keyB, kindB string) error {
    method DelEdge (line 252) | func (dir *directory) DelEdge(keyA, kindA, keyB, kindB string) error {
    method GetNeighbors (line 257) | func (dir *directory) GetNeighbors(key, kind string) ([][2]string, err...
    method GetEdges (line 278) | func (dir *directory) GetEdges() ([]DirEdge, error) {
    method GetNodes (line 303) | func (dir *directory) GetNodes() ([]DirNode, error) {
    method GetNodeAttrs (line 328) | func (dir *directory) GetNodeAttrs() ([]DirNodeAttr, error) {
  constant DirNodeTable (line 34) | DirNodeTable = `
  type DirNode (line 42) | type DirNode struct
  constant DirNodeAttrTable (line 48) | DirNodeAttrTable = `
  type DirNodeAttr (line 60) | type DirNodeAttr struct
  constant DirEdgeTable (line 68) | DirEdgeTable = `
  type DirEdge (line 82) | type DirEdge struct
  function Directory (line 92) | func Directory() *directory {

FILE: hal/event.go
  type Evt (line 32) | type Evt struct
    method Clone (line 61) | func (e *Evt) Clone() Evt {
    method Reply (line 89) | func (e *Evt) Reply(msg string) {
    method Replyf (line 127) | func (e *Evt) Replyf(msg string, a ...interface{}) {
    method Error (line 135) | func (e *Evt) Error(err error) {
    method ReplyTable (line 143) | func (e *Evt) ReplyTable(hdr []string, rows [][]string) {
    method ReplyDM (line 155) | func (e *Evt) ReplyDM(msg string) {
    method ReplyToRoom (line 163) | func (e *Evt) ReplyToRoom(msg string) {
    method BrokerName (line 175) | func (e *Evt) BrokerName() string {
    method FindPrefs (line 182) | func (e *Evt) FindPrefs() Prefs {
    method InstanceSettings (line 190) | func (e *Evt) InstanceSettings() Prefs {
    method AsPref (line 211) | func (e *Evt) AsPref() Pref {
    method BodyAsArgv (line 234) | func (e *Evt) BodyAsArgv() []string {
    method ForceToRoom (line 257) | func (e *Evt) ForceToRoom() Evt {
    method ForceToUser (line 266) | func (e *Evt) ForceToUser() Evt {
    method String (line 272) | func (e *Evt) String() string {

FILE: hal/event_test.go
  function TestEvtBodyAsArgv (line 24) | func TestEvtBodyAsArgv(t *testing.T) {

FILE: hal/kv.go
  constant KVTable (line 25) | KVTable = `
  function kvLazyInit (line 37) | func kvLazyInit() {
  function kvCleanup (line 44) | func kvCleanup() {
  function ExistsKV (line 58) | func ExistsKV(key string) bool {
  function GetKV (line 75) | func GetKV(key string) (string, bool) {
  function SetKV (line 94) | func SetKV(key, value string, ttl time.Duration) (err error) {

FILE: hal/logger.go
  type Logger (line 28) | type Logger struct
    method SetPrefix (line 100) | func (l *Logger) SetPrefix(prefix string) {
    method Printf (line 105) | func (l *Logger) Printf(msg string, a ...interface{}) {
    method Println (line 120) | func (l *Logger) Println(a ...interface{}) {
    method Debugf (line 136) | func (l *Logger) Debugf(msg string, a ...interface{}) {
    method Fatalf (line 154) | func (l *Logger) Fatalf(msg string, a ...interface{}) {
    method Panic (line 177) | func (l *Logger) Panic(msg string) {
    method Panicf (line 187) | func (l *Logger) Panicf(msg string, a ...interface{}) {
    method IsDebug (line 202) | func (l *Logger) IsDebug() bool {
    method EnableDebug (line 207) | func (l *Logger) EnableDebug() {
    method DisableDebug (line 212) | func (l *Logger) DisableDebug() {
    method NewLogSink (line 218) | func (l *Logger) NewLogSink() chan LogEntry {
    method NewDebugSink (line 230) | func (l *Logger) NewDebugSink() chan LogEntry {
    method DisableLogStdout (line 241) | func (l *Logger) DisableLogStdout() {
    method DisableDbgStdout (line 254) | func (l *Logger) DisableDbgStdout() {
  type LogEntry (line 32) | type LogEntry struct
    method String (line 59) | func (l *LogEntry) String() string {
  type logger (line 40) | type logger struct
    method initialize (line 71) | func (l *logger) initialize() {
    method fwdStdout (line 91) | func (l *logger) fwdStdout(src chan LogEntry, closed chan struct{}) {
  function IsDebug (line 197) | func IsDebug() bool {

FILE: hal/logger_test.go
  function TestLogger (line 23) | func TestLogger(t *testing.T) {

FILE: hal/periodic.go
  type PeriodicFunc (line 25) | type PeriodicFunc struct
    method initialize (line 67) | func (pf *PeriodicFunc) initialize() {
    method loop (line 78) | func (pf *PeriodicFunc) loop() {
    method runFunc (line 110) | func (pf *PeriodicFunc) runFunc(t time.Time) {
    method Register (line 120) | func (pf *PeriodicFunc) Register() {
    method Start (line 134) | func (pf *PeriodicFunc) Start() {
    method Stop (line 160) | func (pf *PeriodicFunc) Stop() {
    method Bump (line 170) | func (pf *PeriodicFunc) Bump() {
    method Status (line 179) | func (pf *PeriodicFunc) Status() string {
    method Last (line 188) | func (pf *PeriodicFunc) Last() time.Time {
    method randSleep (line 198) | func (pf *PeriodicFunc) randSleep() {
  function init (line 47) | func init() {
  function GetPeriodicFunc (line 53) | func GetPeriodicFunc(name string) *PeriodicFunc {

FILE: hal/persist_plugins.go
  constant PLUGIN_INST_TABLE (line 19) | PLUGIN_INST_TABLE = `
  method LoadInstances (line 35) | func (pr *pluginRegistry) LoadInstances() error {
  method SaveInstances (line 105) | func (pr *pluginRegistry) SaveInstances() error {

FILE: hal/plugins.go
  type pluginRegistry (line 27) | type pluginRegistry struct
    method PluginList (line 216) | func (pr *pluginRegistry) PluginList() []*Plugin {
    method InstanceList (line 226) | func (pr *pluginRegistry) InstanceList() []*Instance {
    method GetPlugin (line 240) | func (pr *pluginRegistry) GetPlugin(name string) (*Plugin, error) {
    method FindInstances (line 255) | func (pr *pluginRegistry) FindInstances(roomId, bname, plugin string) ...
    method ActivePluginList (line 271) | func (pr *pluginRegistry) ActivePluginList() []*Plugin {
    method InactivePluginList (line 295) | func (pr *pluginRegistry) InactivePluginList() []*Plugin {
  type Plugin (line 41) | type Plugin struct
    method Register (line 74) | func (p *Plugin) Register() error {
    method Unregister (line 92) | func (p *Plugin) Unregister() error {
    method Instance (line 116) | func (p *Plugin) Instance(roomId string, broker Broker) *Instance {
    method String (line 316) | func (p *Plugin) String() string {
  type Instance (line 53) | type Instance struct
    method Register (line 128) | func (inst *Instance) Register() error {
    method Unregister (line 158) | func (inst *Instance) Unregister() error {
    method LoadSettingsFromPrefs (line 184) | func (inst *Instance) LoadSettingsFromPrefs() {
    method SaveSettingsToPrefs (line 205) | func (inst *Instance) SaveSettingsToPrefs() {
    method String (line 320) | func (inst *Instance) String() string {
  function PluginRegistry (line 64) | func PluginRegistry() *pluginRegistry {

FILE: hal/prefs.go
  constant PrefsTable (line 33) | PrefsTable = `
  type Pref (line 72) | type Pref struct
    method Get (line 165) | func (in *Pref) Get() Pref {
    method GetPrefs (line 189) | func (in *Pref) GetPrefs() Prefs {
    method get (line 193) | func (in *Pref) get() Prefs {
    method Set (line 243) | func (in *Pref) Set() error {
    method Delete (line 272) | func (in *Pref) Delete() error {
    method Find (line 304) | func (p Pref) Find() Prefs {
    method FindKey (line 308) | func (p Pref) FindKey(key string) Prefs {
    method find (line 319) | func (p Pref) find(keyRequired bool) Prefs {
    method SetKey (line 438) | func (pref Pref) SetKey(key string) Pref {
    method SetUser (line 444) | func (pref Pref) SetUser(user string) Pref {
    method SetBroker (line 450) | func (pref Pref) SetBroker(broker string) Pref {
    method precedence (line 562) | func (p *Pref) precedence() int {
    method String (line 584) | func (p *Pref) String() string {
  type Prefs (line 85) | type Prefs
    method Clone (line 413) | func (prefs Prefs) Clone() Prefs {
    method One (line 427) | func (prefs Prefs) One() Pref {
    method User (line 458) | func (prefs Prefs) User(user string) Prefs {
    method Room (line 472) | func (prefs Prefs) Room(room string) Prefs {
    method Broker (line 485) | func (prefs Prefs) Broker(broker string) Prefs {
    method Plugin (line 498) | func (prefs Prefs) Plugin(plugin string) Prefs {
    method Key (line 511) | func (prefs Prefs) Key(key string) Prefs {
    method Value (line 524) | func (prefs Prefs) Value(value string) Prefs {
    method Table (line 537) | func (prefs Prefs) Table() [][]string {
    method Len (line 558) | func (ps Prefs) Len() int           { return len(ps) }
    method Swap (line 559) | func (ps Prefs) Swap(i, j int)      { ps[i], ps[j] = ps[j], ps[i] }
    method Less (line 560) | func (ps Prefs) Less(i, j int) bool { return ps[i].precedence() < ps[j...
    method String (line 600) | func (p *Prefs) String() string {
  function GetPref (line 91) | func GetPref(user, broker, room, plugin, key, def string) Pref {
  function SetPref (line 112) | func SetPref(user, broker, room, plugin, key, value string) error {
  function GetPrefs (line 131) | func GetPrefs(user, broker, room, plugin string) Prefs {
  function FindPrefs (line 143) | func FindPrefs(user, broker, room, plugin, key string) Prefs {
  function RmPrefId (line 155) | func RmPrefId(id int) error {
  function FindKey (line 314) | func FindKey(key string) Prefs {

FILE: hal/router.go
  type RouterCTX (line 27) | type RouterCTX struct
    method AddBroker (line 71) | func (r *RouterCTX) AddBroker(b Broker) {
    method Send (line 90) | func (r *RouterCTX) Send(evt Evt) {
    method GetBroker (line 95) | func (r *RouterCTX) GetBroker(name string) Broker {
    method Brokers (line 108) | func (r *RouterCTX) Brokers() []Broker {
    method Quit (line 122) | func (r *RouterCTX) Quit() {
    method Route (line 129) | func (r *RouterCTX) Route() {
    method inEvent (line 145) | func (r *RouterCTX) inEvent(evt *Evt) {
    method outEvent (line 242) | func (r *RouterCTX) outEvent(evt *Evt) {
  type fwdBroker (line 37) | type fwdBroker struct
  function Router (line 46) | func Router() *RouterCTX {
  function forwardChan (line 60) | func forwardChan(from, to chan *Evt) {

FILE: hal/secrets.go
  type SecretStore (line 30) | type SecretStore struct
    method SetEncryptionKey (line 77) | func (ss *SecretStore) SetEncryptionKey(in []byte) {
    method Get (line 95) | func (ss *SecretStore) Get(key string) string {
    method Exists (line 108) | func (ss *SecretStore) Exists(key string) bool {
    method Set (line 120) | func (ss *SecretStore) Set(key, value string) {
    method Put (line 128) | func (ss *SecretStore) Put(key, value string) {
    method Delete (line 134) | func (ss *SecretStore) Delete(key string) {
    method Dump (line 144) | func (ss *SecretStore) Dump() map[string]string {
    method LoadFromDB (line 161) | func (ss *SecretStore) LoadFromDB() {
    method SaveToDB (line 206) | func (ss *SecretStore) SaveToDB() {
    method initTable (line 254) | func (ss *SecretStore) initTable() {
    method WipeDB (line 263) | func (ss *SecretStore) WipeDB() {
    method InitDB (line 267) | func (ss *SecretStore) InitDB() {
    method mkNonce (line 272) | func (ss *SecretStore) mkNonce() ([]byte, error) {
    method getGCM (line 285) | func (ss *SecretStore) getGCM() cipher.AEAD {
  constant SECRETS_TABLE (line 43) | SECRETS_TABLE = `
  type ssRec (line 53) | type ssRec struct
  constant KEY_SIZE (line 60) | KEY_SIZE = 32
  constant NONCE_SIZE (line 61) | NONCE_SIZE = 12
  function Secrets (line 64) | func Secrets() *SecretStore {

FILE: hal/secrets_test.go
  function TestSecretsBasic (line 25) | func TestSecretsBasic(t *testing.T) {

FILE: hal/sqldb.go
  constant SECRETS_KEY_DSN (line 32) | SECRETS_KEY_DSN = "hal.dsn"
  function SqlDB (line 35) | func SqlDB() *sql.DB {
  function ForceSqlDBHandle (line 66) | func ForceSqlDBHandle(db *sql.DB) {
  function SqlInit (line 77) | func SqlInit(sqlTxt string) error {

FILE: hal/text2image.go
  type FontChar (line 48) | type FontChar struct
  type FontData (line 56) | type FontData struct
    method StringToChars (line 82) | func (fd *FontData) StringToChars(want string) []*FontChar {
    method StringToImages (line 102) | func (fd *FontData) StringToImages(want string, clr color.Color) []ima...
    method StringToImage (line 127) | func (fd *FontData) StringToImage(want string, clr color.Color) image....
    method StringsToImage (line 147) | func (fd *FontData) StringsToImage(want []string, clr color.Color) ima...
    method ParseColor (line 6090) | func (fd *FontData) ParseColor(in string, def color.Color) color.Color {
  function charRow (line 71) | func charRow(in string) (out uint8) {
  function FixedFont (line 175) | func FixedFont() *FontData {

FILE: hal/text2image_test.go
  function Testtext2image (line 25) | func Testtext2image(t *testing.T) {

FILE: hal/ttlcache.go
  type ttlCache (line 27) | type ttlCache struct
    method Get (line 55) | func (cache *ttlCache) Get(key string, v interface{}) (time.Duration, ...
    method Set (line 90) | func (cache *ttlCache) Set(key string, v interface{}, ttl time.Duratio...
    method Delete (line 99) | func (cache *ttlCache) Delete(key string) {
    method Exists (line 108) | func (cache *ttlCache) Exists(key string) bool {
    method Age (line 118) | func (cache *ttlCache) Age(key string) time.Duration {
    method Ttl (line 131) | func (cache *ttlCache) Ttl(key string) time.Duration {
  function Cache (line 37) | func Cache() *ttlCache {

FILE: hal/ttlcache_test.go
  type Whatever (line 26) | type Whatever struct
  function TestTtlCache (line 32) | func TestTtlCache(t *testing.T) {

FILE: hal/utf8table.go
  function Utf8Table (line 27) | func Utf8Table(hdr []string, rows [][]string) string {

FILE: hal/utf8table_test.go
  function TestUtf8Table (line 24) | func TestUtf8Table(t *testing.T) {

FILE: plugins/archive/plugin.go
  type ArchiveEntry (line 33) | type ArchiveEntry struct
  constant ArchiveTable (line 46) | ArchiveTable = `
  constant ReactionTable (line 57) | ReactionTable = `
  function Register (line 68) | func Register() {
  function archiveRecorder (line 92) | func archiveRecorder(evt hal.Evt) {
  function archiveReaction (line 112) | func archiveReaction(evt hal.Evt) {
  function insertReaction (line 134) | func insertReaction(ts time.Time, id, user, room, broker, reaction strin...
  function deleteReaction (line 142) | func deleteReaction(id, user, room, broker, reaction string) {
  function httpGetArchive (line 151) | func httpGetArchive(w http.ResponseWriter, r *http.Request) {
  function FetchArchive (line 168) | func FetchArchive(limit int) ([]*ArchiveEntry, error) {

FILE: plugins/blabber/plugin.go
  type wncRow (line 34) | type wncRow struct
  type qFrag (line 40) | type qFrag struct
  constant BLABBERWORDS_TABLE (line 48) | BLABBERWORDS_TABLE = `
  function Register (line 59) | func Register() {
  function bwCounter (line 77) | func bwCounter(evt hal.Evt) {
  function blab (line 128) | func blab(evt hal.Evt) {
  function nextWord (line 171) | func nextWord(current wncRow, userFrag, roomFrag qFrag) wncRow {
  function rowsToString (line 198) | func rowsToString(rows []wncRow) string {
  function getRows (line 208) | func getRows(sql string, params []interface{}) []wncRow {
  function firstWord (line 234) | func firstWord(userFrag, roomFrag qFrag) wncRow {
  function mkQueryFragment (line 263) | func mkQueryFragment(col string, list []string) qFrag {
  function extractArgs (line 281) | func extractArgs(argv []string, i int) []string {

FILE: plugins/cross_the_streams/plugin.go
  function Register (line 29) | func Register() {
  function crossStreams (line 45) | func crossStreams(evt hal.Evt) {

FILE: plugins/docker/plugin.go
  constant Name (line 27) | Name = "docker"
  constant Usage (line 29) | Usage = `
  function Register (line 36) | func Register() {
  function docker (line 46) | func docker(evt hal.Evt) {
  function run (line 72) | func run(evt hal.Evt, argv []string) {
  function images (line 83) | func images(evt hal.Evt) {

FILE: plugins/google_calendar/google.go
  constant oauthJsonKey (line 30) | oauthJsonKey = `google-calendar-oauth-client-json`
  type CalEvent (line 33) | type CalEvent struct
  type GoogleError (line 41) | type GoogleError struct
    method Error (line 45) | func (e GoogleError) Error() string {
  type PrefMissingError (line 49) | type PrefMissingError struct
    method Error (line 51) | func (e PrefMissingError) Error() string {
  type SecretMissingError (line 56) | type SecretMissingError struct
    method Error (line 58) | func (e SecretMissingError) Error() string {
  function getEvents (line 62) | func getEvents(calendarId string, now time.Time) ([]CalEvent, error) {

FILE: plugins/google_calendar/plugin.go
  constant Usage (line 33) | Usage = `!gcal (silence|status|expire|reload)
  constant DefaultTz (line 60) | DefaultTz = "America/Los_Angeles"
  constant DefaultMsg (line 61) | DefaultMsg = "Calendar event: %q"
  type Config (line 63) | type Config struct
    method getCachedCalEvents (line 307) | func (c *Config) getCachedCalEvents(now time.Time) ([]CalEvent, error) {
    method LoadFromPrefs (line 329) | func (c *Config) LoadFromPrefs() error {
    method expireCaches (line 356) | func (c *Config) expireCaches() {
    method loadBoolPref (line 361) | func (c *Config) loadBoolPref(key string) bool {
  function init (line 81) | func init() {
  function Register (line 86) | func Register() {
  function initData (line 97) | func initData(inst *hal.Instance) {
  function handleEvt (line 119) | func handleEvt(evt hal.Evt) {
  function nospamReplyf (line 203) | func nospamReplyf(evt *hal.Evt, msg string, a ...interface{}) {
  function handleCommand (line 219) | func handleCommand(evt *hal.Evt) {
  function getSpamKey (line 263) | func getSpamKey(scope, id string) string {
  function updateCachedCalEvents (line 267) | func updateCachedCalEvents(roomId string) {
  function getCachedConfig (line 292) | func getCachedConfig(roomId string, now time.Time) *Config {

FILE: plugins/guys/plugin.go
  function Register (line 25) | func Register() {
  function guys (line 41) | func guys(evt hal.Evt) {

FILE: plugins/inspect/plugin.go
  function Register (line 23) | func Register() {
  function getid (line 41) | func getid(evt hal.Evt) {
  function leave (line 61) | func leave(evt hal.Evt) {

FILE: plugins/mark/plugin.go
  type Mark (line 31) | type Mark struct
  constant MarkTable (line 39) | MarkTable = `
  function Register (line 49) | func Register() {
  function mark (line 62) | func mark(evt hal.Evt) {
  function listMarks (line 86) | func listMarks(evt hal.Evt) {
  function httpGetMarks (line 102) | func httpGetMarks(w http.ResponseWriter, r *http.Request) {
  function FetchMarks (line 118) | func FetchMarks(room string, limit int) ([]Mark, error) {

FILE: plugins/pagerduty/helpers.go
  function init (line 31) | func init() {
  function authenticatedGet (line 36) | func authenticatedGet(geturl, token string) (*http.Response, error) {
  function authenticatedPost (line 55) | func authenticatedPost(token, postUrl string, body []byte) (*http.Respon...
  function pagedUrl (line 71) | func pagedUrl(resource string, offset, limit int, params map[string][]st...

FILE: plugins/pagerduty/oncall_plugin.go
  constant OncallUsage (line 30) | OncallUsage = `!oncall <alias>
  constant DefaultTopicInterval (line 39) | DefaultTopicInterval = "10m"
  function init (line 44) | func init() {
  function oncall (line 52) | func oncall(msg hal.Evt) {
  function getTeamOncalls (line 177) | func getTeamOncalls(token string, team Team) []Oncall {
  function getOncallCache (line 211) | func getOncallCache(token string, forceUpdate bool) []Oncall {
  function getCacheFreq (line 251) | func getCacheFreq() time.Duration {
  function getTopicFreq (line 261) | func getTopicFreq(roomId string) time.Duration {
  function oncallInit (line 271) | func oncallInit(i *hal.Instance) {
  function pollOncalls (line 301) | func pollOncalls(token string) {
  function topicUpdater (line 312) | func topicUpdater(token, roomId, brokerName string) {
  function topicFuncName (line 368) | func topicFuncName(roomId string) string {
  type OncallsByLevel (line 373) | type OncallsByLevel
    method Len (line 375) | func (a OncallsByLevel) Len() int      { return len(a) }
    method Swap (line 376) | func (a OncallsByLevel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
    method Less (line 377) | func (a OncallsByLevel) Less(i, j int) bool {
  function formatOncallReply (line 386) | func formatOncallReply(wanted string, exactMatchFound bool, matches map[...
  function getMutex (line 426) | func getMutex(token string) sync.Mutex {
  function clean (line 438) | func clean(in string) string {

FILE: plugins/pagerduty/page_plugin.go
  constant PageUsage (line 26) | PageUsage = `!page <alias> [optional message]
  constant PageDefaultMessage (line 42) | PageDefaultMessage = `your presence is requested in the chat room`
  function page (line 44) | func page(msg hal.Evt) {
  function pageAlias (line 74) | func pageAlias(evt hal.Evt, parts []string) {
  function addAlias (line 142) | func addAlias(msg hal.Evt, parts []string) {
  function rmAlias (line 164) | func rmAlias(msg hal.Evt, parts []string) {
  function listAlias (line 178) | func listAlias(msg hal.Evt) {
  function aliasKey (line 186) | func aliasKey(alias string) string {

FILE: plugins/pagerduty/pd_events_v1.go
  constant V1EventEndpoint (line 27) | V1EventEndpoint = `https://events.pagerduty.com/generic/2010-04-15/creat...
  type Context (line 30) | type Context interface
  type ContextLink (line 34) | type ContextLink struct
    method GetType (line 177) | func (c *ContextLink) GetType() string {
  type ContextImage (line 40) | type ContextImage struct
    method GetType (line 181) | func (c *ContextImage) GetType() string {
  type Event (line 47) | type Event struct
    method Send (line 112) | func (e *Event) Send(token string) (*Response, error) {
    method respond (line 150) | func (e *Event) respond(status, message string) *Response {
    method checkRequired (line 154) | func (e *Event) checkRequired() error {
  type Error (line 58) | type Error struct
  type ErrorResponse (line 64) | type ErrorResponse struct
  type Response (line 68) | type Response struct
  function NewEvent (line 78) | func NewEvent(serviceKey, eventType, description string) *Event {
  function NewTrigger (line 88) | func NewTrigger(serviceKey, description string) *Event {
  function NewAck (line 92) | func NewAck(serviceKey, description string) *Event {
  function NewResolve (line 96) | func NewResolve(serviceKey, description string) *Event {
  function NewResponse (line 100) | func NewResponse(status, message, incidentKey string) *Response {

FILE: plugins/pagerduty/pd_events_v2.go
  constant V2EventEndpoint (line 27) | V2EventEndpoint = `https://events.pagerduty.com/v2/enqueue`
  type EventPayload (line 31) | type EventPayload struct
  type EventImage (line 41) | type EventImage struct
  type EventBody (line 47) | type EventBody struct
    method Send (line 82) | func (eb *EventBody) Send(token string) (EventResult, error) {
    method checkFields (line 134) | func (eb *EventBody) checkFields() error {
  type EventResult (line 57) | type EventResult struct
  function NewV2Event (line 64) | func NewV2Event(routingKey string) *EventBody {

FILE: plugins/pagerduty/pd_oncall.go
  function GetOncalls (line 27) | func GetOncalls(token string, query map[string][]string) ([]Oncall, erro...

FILE: plugins/pagerduty/pd_policy.go
  function GetEscalationPolicies (line 26) | func GetEscalationPolicies(token string, params map[string][]string) ([]...

FILE: plugins/pagerduty/pd_schedule.go
  type scheduleOncallUsersResponse (line 25) | type scheduleOncallUsersResponse struct
  function GetScheduleOncalls (line 30) | func GetScheduleOncalls(token, id string) ([]User, error) {
  function GetSchedules (line 52) | func GetSchedules(token string, params map[string][]string) ([]Schedule,...

FILE: plugins/pagerduty/pd_service.go
  function GetServices (line 26) | func GetServices(token string, params map[string][]string) ([]Service, e...
  function GetService (line 62) | func GetService(token, id string) (Service, error) {

FILE: plugins/pagerduty/pd_team.go
  function GetTeams (line 26) | func GetTeams(token string, params map[string][]string) ([]Team, error) {

FILE: plugins/pagerduty/pd_types.go
  type ContactMethod (line 23) | type ContactMethod struct
  type NotificationRule (line 31) | type NotificationRule struct
  type Team (line 40) | type Team struct
  type TeamRef (line 50) | type TeamRef struct
  type TeamsResponse (line 58) | type TeamsResponse struct
  type Schedule (line 66) | type Schedule struct
  type ScheduleRef (line 82) | type ScheduleRef struct
  type SchedulesResponse (line 90) | type SchedulesResponse struct
  type ScheduleLayer (line 98) | type ScheduleLayer struct
  type SubSchedule (line 115) | type SubSchedule struct
  type ScheduleLayerEntry (line 121) | type ScheduleLayerEntry struct
  type Restriction (line 127) | type Restriction struct
  type EscalationPolicy (line 133) | type EscalationPolicy struct
  type EscalationPolicyResponse (line 148) | type EscalationPolicyResponse struct
  type EscalationRule (line 156) | type EscalationRule struct
  type EscalationPolicyRef (line 162) | type EscalationPolicyRef struct
  type EscalationTarget (line 170) | type EscalationTarget struct
  type PolicyService (line 175) | type PolicyService struct
  type Integration (line 183) | type Integration struct
  type IncidentCounts (line 198) | type IncidentCounts struct
  type Service (line 206) | type Service struct
  type ServiceRef (line 226) | type ServiceRef struct
  type ServicesResponse (line 234) | type ServicesResponse struct
  type User (line 242) | type User struct
  type UserRef (line 265) | type UserRef struct
  type UsersResponse (line 273) | type UsersResponse struct
  type Oncall (line 281) | type Oncall struct
  type OncallsResponse (line 290) | type OncallsResponse struct
  type Override (line 298) | type Override struct

FILE: plugins/pagerduty/pd_user.go
  function GetUsersOncall (line 26) | func GetUsersOncall(token string) ([]Oncall, error) {
  function GetUsers (line 61) | func GetUsers(token string, params map[string][]string) ([]User, error) {

FILE: plugins/pagerduty/plugin.go
  constant PagerdutyTokenKey (line 27) | PagerdutyTokenKey = `pagerduty.token`
  constant CacheKey (line 30) | CacheKey = `pagerduty.policy_cache`
  constant cacheExpire (line 32) | cacheExpire = time.Minute * 10
  constant DefaultCacheInterval (line 34) | DefaultCacheInterval = "1h"
  function Register (line 36) | func Register() {
  function getSecrets (line 64) | func getSecrets() (token string, err error) {

FILE: plugins/pagerduty/poller.go
  function pollerHandler (line 28) | func pollerHandler(evt hal.Evt) {
  function pollerInit (line 32) | func pollerInit(inst *hal.Instance) {
  function ingestPagerdutyAccount (line 43) | func ingestPagerdutyAccount() {
  function ingestPDusers (line 56) | func ingestPDusers(token string) {
  function ingestPDteams (line 91) | func ingestPDteams(token string) {
  function ingestPDservices (line 110) | func ingestPDservices(token string) {
  function ingestPDschedules (line 154) | func ingestPDschedules(token string) {
  function logit (line 182) | func logit(err error) {

FILE: plugins/pluginmgr/plugin.go
  constant NAME (line 30) | NAME = "pluginmgr"
  constant HELP (line 33) | HELP = `
  constant PluginGroupTable (line 51) | PluginGroupTable = `
  type PluginGroupRow (line 59) | type PluginGroupRow struct
    method Save (line 331) | func (pgr *PluginGroupRow) Save() error {
    method Delete (line 340) | func (pgr *PluginGroupRow) Delete() error {
  type PluginGroup (line 65) | type PluginGroup
  function Register (line 70) | func Register() {
  function pluginmgr (line 120) | func pluginmgr(evt hal.Evt) {
  function listPlugins (line 204) | func listPlugins(evt hal.Evt) {
  function listInstances (line 222) | func listInstances(evt hal.Evt, roomId string) {
  function savePlugins (line 248) | func savePlugins(evt hal.Evt) {
  function attachPlugin (line 259) | func attachPlugin(evt hal.Evt, pluginName, roomId, regex string) {
  function detachPlugin (line 279) | func detachPlugin(evt hal.Evt, plugin, roomId string) {
  function GetPluginGroup (line 297) | func GetPluginGroup(group string) (PluginGroup, error) {
  function listGroupPlugin (line 348) | func listGroupPlugin(evt hal.Evt, group string) {
  function addGroupPlugin (line 363) | func addGroupPlugin(evt hal.Evt, group, pluginName string) {
  function delGroupPlugin (line 387) | func delGroupPlugin(evt hal.Evt, group, plugin string) {

FILE: plugins/prefmgr/http.go
  function prefHandler (line 26) | func prefHandler(w http.ResponseWriter, r *http.Request) {
  function getPrefHandler (line 44) | func getPrefHandler(w http.ResponseWriter, r *http.Request) {
  function putPrefHandler (line 58) | func putPrefHandler(w http.ResponseWriter, r *http.Request) {
  function patchPrefHandler (line 61) | func patchPrefHandler(w http.ResponseWriter, r *http.Request) {
  function deletePrefHandler (line 64) | func deletePrefHandler(w http.ResponseWriter, r *http.Request) {

FILE: plugins/prefmgr/plugin.go
  constant NAME (line 29) | NAME = "prefmgr"
  constant HELP (line 31) | HELP = `Listing keys with no filter will list all keys visible to the ac...
  function init (line 41) | func init() {
  function init (line 45) | func init() {
  function Register (line 92) | func Register() {
  function prefmgr (line 104) | func prefmgr(evt hal.Evt) {
  function cmd2pref (line 135) | func cmd2pref(req *hal.SubCmdInst, evt *hal.Evt) (*hal.Pref, error) {
  function cliList (line 169) | func cliList(req *hal.SubCmdInst, evt *hal.Evt) {
  function cliFind (line 205) | func cliFind(req *hal.SubCmdInst, evt *hal.Evt) {
  function cliSet (line 217) | func cliSet(req *hal.SubCmdInst, evt *hal.Evt) {
  function cliRm (line 245) | func cliRm(req *hal.SubCmdInst, evt *hal.Evt) {
  function stripAutoLinks (line 259) | func stripAutoLinks(in string) string {

FILE: plugins/roster/plugin.go
  type RosterUser (line 30) | type RosterUser struct
  constant ROSTER_TABLE (line 37) | ROSTER_TABLE = `
  function Register (line 46) | func Register() {
  function rostertracker (line 69) | func rostertracker(msg hal.Evt) {
  function rosterlast (line 92) | func rosterlast(msg hal.Evt) {
  function webroster (line 109) | func webroster(w http.ResponseWriter, r *http.Request) {
  function GetRoster (line 125) | func GetRoster() ([]*RosterUser, error) {

FILE: plugins/seppuku/plugin.go
  function Register (line 28) | func Register() {
  function seppuku (line 47) | func seppuku(evt hal.Evt) {
  function zombie (line 58) | func zombie(evt hal.Evt) {

FILE: plugins/spam/plugin.go
  function Register (line 25) | func Register() {
  function spam (line 33) | func spam(evt hal.Evt) {

FILE: plugins/uptime/plugin.go
  function init (line 29) | func init() {
  function Register (line 33) | func Register() {
  function uptime (line 42) | func uptime(evt hal.Evt) {
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (549K chars).
[
  {
    "path": ".gitignore",
    "chars": 385,
    "preview": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture spe"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "NOTICE",
    "chars": 185,
    "preview": "The font in hal/text2image.go is derived from the IBM VGA 8x16 font\nfrom http://int10h.org/oldschool-pc-fonts/ which is "
  },
  {
    "path": "OSSMETADATA",
    "chars": 20,
    "preview": "osslifecycle=active\n"
  },
  {
    "path": "README.md",
    "chars": 5723,
    "preview": "# Hal-9001\n\nHal-9001 is a Go library that offers a number of facilities for creating a bot\nand its plugins.\n\n# Goals\n\n* "
  },
  {
    "path": "brokers/console/broker.go",
    "chars": 6103,
    "preview": "package console\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "brokers/hipchat/broker.go",
    "chars": 7069,
    "preview": "package hipchat\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "brokers/slack/broker.go",
    "chars": 19793,
    "preview": "package slack\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licens"
  },
  {
    "path": "example/demos/colorparser.go",
    "chars": 1040,
    "preview": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, V"
  },
  {
    "path": "example/demos/imgtable.go",
    "chars": 1591,
    "preview": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, V"
  },
  {
    "path": "example/demos/utf8table.go",
    "chars": 1295,
    "preview": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, V"
  },
  {
    "path": "example/docker-repl/main.go",
    "chars": 1675,
    "preview": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "example/everything/main.go",
    "chars": 5644,
    "preview": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "example/minimal/main.go",
    "chars": 977,
    "preview": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "example/repl/main.go",
    "chars": 1284,
    "preview": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "hal/asciitable.go",
    "chars": 2788,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/asciitable_test.go",
    "chars": 1339,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/broker.go",
    "chars": 1255,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/cmd.go",
    "chars": 47089,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/cmd_test.go",
    "chars": 6351,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/counter.go",
    "chars": 2105,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/directory.go",
    "chars": 10996,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/event.go",
    "chars": 8920,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/event_test.go",
    "chars": 1494,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/kv.go",
    "chars": 2846,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/logger.go",
    "chars": 6508,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/logger_test.go",
    "chars": 1172,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/periodic.go",
    "chars": 4645,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/persist_plugins.go",
    "chars": 4103,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/plugins.go",
    "chars": 8822,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/prefs.go",
    "chars": 14659,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/router.go",
    "chars": 6818,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/secrets.go",
    "chars": 7635,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/secrets_test.go",
    "chars": 1099,
    "preview": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lic"
  },
  {
    "path": "hal/sqldb.go",
    "chars": 3278,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/text2image.go",
    "chars": 132230,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/text2image_test.go",
    "chars": 1489,
    "preview": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lic"
  },
  {
    "path": "hal/ttlcache.go",
    "chars": 3653,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/ttlcache_test.go",
    "chars": 1285,
    "preview": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lic"
  },
  {
    "path": "hal/utf8table.go",
    "chars": 3905,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "hal/utf8table_test.go",
    "chars": 2310,
    "preview": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\""
  },
  {
    "path": "plugins/archive/plugin.go",
    "chars": 6981,
    "preview": "package archive\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "plugins/blabber/plugin.go",
    "chars": 6796,
    "preview": "// blabber records events as word pairs with counts and can\n// use that data to generate text\n// This is an experiment a"
  },
  {
    "path": "plugins/cross_the_streams/plugin.go",
    "chars": 1876,
    "preview": "// cross_the_streams replicates messages between brokers\npackage cross_the_streams\n\n/*\n * Copyright 2016-2017 Netflix, I"
  },
  {
    "path": "plugins/docker/plugin.go",
    "chars": 2310,
    "preview": "// Package docker allows users to attach a Docker image to a room and interact\n// with it over its stdin/stdout.\npackage"
  },
  {
    "path": "plugins/google_calendar/google.go",
    "chars": 4860,
    "preview": "package google_calendar\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (t"
  },
  {
    "path": "plugins/google_calendar/plugin.go",
    "chars": 10096,
    "preview": "package google_calendar\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (t"
  },
  {
    "path": "plugins/guys/plugin.go",
    "chars": 1822,
    "preview": "package guys\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "plugins/inspect/plugin.go",
    "chars": 1930,
    "preview": "package inspect\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "plugins/mark/plugin.go",
    "chars": 3865,
    "preview": "package mark\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "plugins/pagerduty/helpers.go",
    "chars": 2481,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/oncall_plugin.go",
    "chars": 12477,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/page_plugin.go",
    "chars": 5467,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_events_v1.go",
    "chars": 4836,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_events_v2.go",
    "chars": 4021,
    "preview": "package pagerduty\n\n/*\n * Copyright 2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "plugins/pagerduty/pd_oncall.go",
    "chars": 1576,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_policy.go",
    "chars": 1522,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_schedule.go",
    "chars": 2101,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_service.go",
    "chars": 1945,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_team.go",
    "chars": 1384,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_types.go",
    "chars": 10983,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/pd_user.go",
    "chars": 2016,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/plugin.go",
    "chars": 1904,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pagerduty/poller.go",
    "chars": 5621,
    "preview": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "plugins/pluginmgr/plugin.go",
    "chars": 9756,
    "preview": "// Package pluginmgr is a plugin manager for hal that allows users to\n// manage plugins from inside chat or over REST.\np"
  },
  {
    "path": "plugins/prefmgr/http.go",
    "chars": 1708,
    "preview": "package prefmgr\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "plugins/prefmgr/plugin.go",
    "chars": 7212,
    "preview": "// prefmgr exposes hal's preferences as a bot command and over REST\npackage prefmgr\n\n/*\n * Copyright 2016-2017 Netflix, "
  },
  {
    "path": "plugins/roster/plugin.go",
    "chars": 3770,
    "preview": "package roster\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Licen"
  },
  {
    "path": "plugins/seppuku/plugin.go",
    "chars": 2007,
    "preview": "package seppuku\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "plugins/spam/plugin.go",
    "chars": 1032,
    "preview": "package spam\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License"
  },
  {
    "path": "plugins/uptime/plugin.go",
    "chars": 1012,
    "preview": "// uptime: the simplest useful plugin possible\npackage uptime\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed un"
  }
]

About this extraction

This page contains the full source code of the Netflix/hal-9001 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (467.1 KB), approximately 141.9k tokens, and a symbol index with 690 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!