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",
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
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.