From bb856c1323822be93a4eb3077748801cd41b8cdb Mon Sep 17 00:00:00 2001 From: Matthew Rothenberg Date: Wed, 2 Mar 2016 19:00:47 -0500 Subject: [PATCH] switch from glide to godep glide generates the vendor directory with git sub repositories, which makes a lot of sense, however this results in the files not being included on a clone without a sub update. going back to the oldskool method here, sigh. --- Godeps/Godeps.json | 20 + Godeps/Readme | 5 + Makefile | 7 +- vendor/github.com/codegangsta/cli | 1 - vendor/github.com/codegangsta/cli/.travis.yml | 19 + vendor/github.com/codegangsta/cli/LICENSE | 21 + vendor/github.com/codegangsta/cli/README.md | 352 +++++++++++ vendor/github.com/codegangsta/cli/app.go | 349 +++++++++++ .../github.com/codegangsta/cli/appveyor.yml | 16 + vendor/github.com/codegangsta/cli/cli.go | 40 ++ vendor/github.com/codegangsta/cli/command.go | 250 ++++++++ vendor/github.com/codegangsta/cli/context.go | 388 ++++++++++++ vendor/github.com/codegangsta/cli/flag.go | 546 ++++++++++++++++ vendor/github.com/codegangsta/cli/help.go | 248 ++++++++ vendor/github.com/nlopes/slack | 1 - vendor/github.com/nlopes/slack/.gitignore | 2 + vendor/github.com/nlopes/slack/.travis.yml | 19 + vendor/github.com/nlopes/slack/LICENSE | 23 + vendor/github.com/nlopes/slack/README.md | 72 +++ vendor/github.com/nlopes/slack/TODO.txt | 3 + vendor/github.com/nlopes/slack/admin.go | 190 ++++++ vendor/github.com/nlopes/slack/attachments.go | 31 + vendor/github.com/nlopes/slack/backoff.go | 57 ++ vendor/github.com/nlopes/slack/channels.go | 254 ++++++++ vendor/github.com/nlopes/slack/chat.go | 161 +++++ vendor/github.com/nlopes/slack/comment.go | 10 + .../github.com/nlopes/slack/conversation.go | 38 ++ vendor/github.com/nlopes/slack/emoji.go | 27 + vendor/github.com/nlopes/slack/files.go | 242 ++++++++ vendor/github.com/nlopes/slack/groups.go | 286 +++++++++ vendor/github.com/nlopes/slack/history.go | 33 + vendor/github.com/nlopes/slack/im.go | 123 ++++ vendor/github.com/nlopes/slack/info.go | 206 ++++++ vendor/github.com/nlopes/slack/item.go | 75 +++ vendor/github.com/nlopes/slack/messageID.go | 30 + vendor/github.com/nlopes/slack/messages.go | 128 ++++ vendor/github.com/nlopes/slack/misc.go | 119 ++++ vendor/github.com/nlopes/slack/oauth.go | 54 ++ vendor/github.com/nlopes/slack/pagination.go | 20 + vendor/github.com/nlopes/slack/pins.go | 79 +++ vendor/github.com/nlopes/slack/reactions.go | 247 ++++++++ vendor/github.com/nlopes/slack/rtm.go | 39 ++ vendor/github.com/nlopes/slack/search.go | 137 ++++ vendor/github.com/nlopes/slack/slack.go | 77 +++ vendor/github.com/nlopes/slack/stars.go | 135 ++++ vendor/github.com/nlopes/slack/users.go | 139 +++++ vendor/github.com/nlopes/slack/websocket.go | 94 +++ .../nlopes/slack/websocket_channels.go | 72 +++ .../github.com/nlopes/slack/websocket_dm.go | 23 + .../nlopes/slack/websocket_files.go | 49 ++ .../nlopes/slack/websocket_groups.go | 49 ++ .../nlopes/slack/websocket_internals.go | 92 +++ .../nlopes/slack/websocket_managed_conn.go | 424 +++++++++++++ .../github.com/nlopes/slack/websocket_misc.go | 117 ++++ .../github.com/nlopes/slack/websocket_pins.go | 16 + .../nlopes/slack/websocket_proxy.go | 83 +++ .../nlopes/slack/websocket_reactions.go | 15 + .../nlopes/slack/websocket_stars.go | 14 + .../nlopes/slack/websocket_teams.go | 33 + .../nlopes/slack/websocket_utils.go | 49 ++ vendor/golang.org/x/net | 1 - vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/websocket/client.go | 113 ++++ vendor/golang.org/x/net/websocket/hybi.go | 586 ++++++++++++++++++ vendor/golang.org/x/net/websocket/server.go | 113 ++++ .../golang.org/x/net/websocket/websocket.go | 412 ++++++++++++ 67 files changed, 7714 insertions(+), 9 deletions(-) create mode 100644 Godeps/Godeps.json create mode 100644 Godeps/Readme delete mode 160000 vendor/github.com/codegangsta/cli create mode 100644 vendor/github.com/codegangsta/cli/.travis.yml create mode 100644 vendor/github.com/codegangsta/cli/LICENSE create mode 100644 vendor/github.com/codegangsta/cli/README.md create mode 100644 vendor/github.com/codegangsta/cli/app.go create mode 100644 vendor/github.com/codegangsta/cli/appveyor.yml create mode 100644 vendor/github.com/codegangsta/cli/cli.go create mode 100644 vendor/github.com/codegangsta/cli/command.go create mode 100644 vendor/github.com/codegangsta/cli/context.go create mode 100644 vendor/github.com/codegangsta/cli/flag.go create mode 100644 vendor/github.com/codegangsta/cli/help.go delete mode 160000 vendor/github.com/nlopes/slack create mode 100644 vendor/github.com/nlopes/slack/.gitignore create mode 100644 vendor/github.com/nlopes/slack/.travis.yml create mode 100644 vendor/github.com/nlopes/slack/LICENSE create mode 100644 vendor/github.com/nlopes/slack/README.md create mode 100644 vendor/github.com/nlopes/slack/TODO.txt create mode 100644 vendor/github.com/nlopes/slack/admin.go create mode 100644 vendor/github.com/nlopes/slack/attachments.go create mode 100644 vendor/github.com/nlopes/slack/backoff.go create mode 100644 vendor/github.com/nlopes/slack/channels.go create mode 100644 vendor/github.com/nlopes/slack/chat.go create mode 100644 vendor/github.com/nlopes/slack/comment.go create mode 100644 vendor/github.com/nlopes/slack/conversation.go create mode 100644 vendor/github.com/nlopes/slack/emoji.go create mode 100644 vendor/github.com/nlopes/slack/files.go create mode 100644 vendor/github.com/nlopes/slack/groups.go create mode 100644 vendor/github.com/nlopes/slack/history.go create mode 100644 vendor/github.com/nlopes/slack/im.go create mode 100644 vendor/github.com/nlopes/slack/info.go create mode 100644 vendor/github.com/nlopes/slack/item.go create mode 100644 vendor/github.com/nlopes/slack/messageID.go create mode 100644 vendor/github.com/nlopes/slack/messages.go create mode 100644 vendor/github.com/nlopes/slack/misc.go create mode 100644 vendor/github.com/nlopes/slack/oauth.go create mode 100644 vendor/github.com/nlopes/slack/pagination.go create mode 100644 vendor/github.com/nlopes/slack/pins.go create mode 100644 vendor/github.com/nlopes/slack/reactions.go create mode 100644 vendor/github.com/nlopes/slack/rtm.go create mode 100644 vendor/github.com/nlopes/slack/search.go create mode 100644 vendor/github.com/nlopes/slack/slack.go create mode 100644 vendor/github.com/nlopes/slack/stars.go create mode 100644 vendor/github.com/nlopes/slack/users.go create mode 100644 vendor/github.com/nlopes/slack/websocket.go create mode 100644 vendor/github.com/nlopes/slack/websocket_channels.go create mode 100644 vendor/github.com/nlopes/slack/websocket_dm.go create mode 100644 vendor/github.com/nlopes/slack/websocket_files.go create mode 100644 vendor/github.com/nlopes/slack/websocket_groups.go create mode 100644 vendor/github.com/nlopes/slack/websocket_internals.go create mode 100644 vendor/github.com/nlopes/slack/websocket_managed_conn.go create mode 100644 vendor/github.com/nlopes/slack/websocket_misc.go create mode 100644 vendor/github.com/nlopes/slack/websocket_pins.go create mode 100644 vendor/github.com/nlopes/slack/websocket_proxy.go create mode 100644 vendor/github.com/nlopes/slack/websocket_reactions.go create mode 100644 vendor/github.com/nlopes/slack/websocket_stars.go create mode 100644 vendor/github.com/nlopes/slack/websocket_teams.go create mode 100644 vendor/github.com/nlopes/slack/websocket_utils.go delete mode 160000 vendor/golang.org/x/net create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/websocket/client.go create mode 100644 vendor/golang.org/x/net/websocket/hybi.go create mode 100644 vendor/golang.org/x/net/websocket/server.go create mode 100644 vendor/golang.org/x/net/websocket/websocket.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..905b36e --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,20 @@ +{ + "ImportPath": "github.com/mroth/slacknimate", + "GoVersion": "go1.6", + "Deps": [ + { + "ImportPath": "github.com/codegangsta/cli", + "Comment": "1.2.0-215-g0ab42fd", + "Rev": "0ab42fd482c27cf2c95e7794ad3bb2082c2ab2d7" + }, + { + "ImportPath": "github.com/nlopes/slack", + "Comment": "v0.0.1-76-ga6a2e43", + "Rev": "a6a2e430fd2e1bc1f91c0fd538e5c2d0c898f5dc" + }, + { + "ImportPath": "golang.org/x/net/websocket", + "Rev": "8968c61983e8f51a91b8c0ef25bf739278c89634" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Makefile b/Makefile index bff84cf..b4984a0 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,11 @@ .PHONY: build_deps build goinstall gouninstall package_deps package clobber .DEFAULT_GOAL := build -vendor: - @type glide >/dev/null 2>&1 || \ - { echo >&2 "I require glide but it is not installed. Aborting."; exit 1; } - glide install - build_deps: @type go >/dev/null 2>&1 || \ { echo >&2 "I require go but it is not installed. Aborting."; exit 1; } -build: vendor build_deps +build: build_deps go build -o bin/slacknimate diff --git a/vendor/github.com/codegangsta/cli b/vendor/github.com/codegangsta/cli deleted file mode 160000 index 5db7419..0000000 --- a/vendor/github.com/codegangsta/cli +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5db74198dee1cfe60cf06a611d03a420361baad6 diff --git a/vendor/github.com/codegangsta/cli/.travis.yml b/vendor/github.com/codegangsta/cli/.travis.yml new file mode 100644 index 0000000..87ba52f --- /dev/null +++ b/vendor/github.com/codegangsta/cli/.travis.yml @@ -0,0 +1,19 @@ +language: go +sudo: false + +go: +- 1.0.3 +- 1.1.2 +- 1.2.2 +- 1.3.3 +- 1.4.2 +- 1.5.1 +- tip + +matrix: + allow_failures: + - go: tip + +script: +- go vet ./... +- go test -v ./... diff --git a/vendor/github.com/codegangsta/cli/LICENSE b/vendor/github.com/codegangsta/cli/LICENSE new file mode 100644 index 0000000..5515ccf --- /dev/null +++ b/vendor/github.com/codegangsta/cli/LICENSE @@ -0,0 +1,21 @@ +Copyright (C) 2013 Jeremy Saenz +All Rights Reserved. + +MIT LICENSE + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/codegangsta/cli/README.md b/vendor/github.com/codegangsta/cli/README.md new file mode 100644 index 0000000..ae0a4ca --- /dev/null +++ b/vendor/github.com/codegangsta/cli/README.md @@ -0,0 +1,352 @@ +[![Coverage](http://gocover.io/_badge/github.com/codegangsta/cli?0)](http://gocover.io/github.com/codegangsta/cli) +[![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli) +[![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) + +# cli.go + +`cli.go` is simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line applications in an expressive way. + +## Overview + +Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. + +**This is where `cli.go` comes into play.** `cli.go` makes command line programming fun, organized, and expressive! + +## Installation + +Make sure you have a working Go environment (go 1.1+ is *required*). [See the install instructions](http://golang.org/doc/install.html). + +To install `cli.go`, simply run: +``` +$ go get github.com/codegangsta/cli +``` + +Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: +``` +export PATH=$PATH:$GOPATH/bin +``` + +## Getting Started + +One of the philosophies behind `cli.go` is that an API should be playful and full of discovery. So a `cli.go` app can be as little as one line of code in `main()`. + +``` go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + cli.NewApp().Run(os.Args) +} +``` + +This app will run and show help text, but is not very useful. Let's give an action to execute and some help documentation: + +``` go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "boom" + app.Usage = "make an explosive entrance" + app.Action = func(c *cli.Context) { + println("boom! I say!") + } + + app.Run(os.Args) +} +``` + +Running this already gives you a ton of functionality, plus support for things like subcommands and flags, which are covered below. + +## Example + +Being a programmer can be a lonely job. Thankfully by the power of automation that is not the case! Let's create a greeter app to fend off our demons of loneliness! + +Start by creating a directory named `greet`, and within it, add a file, `greet.go` with the following code in it: + +``` go +package main + +import ( + "os" + "github.com/codegangsta/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "greet" + app.Usage = "fight the loneliness!" + app.Action = func(c *cli.Context) { + println("Hello friend!") + } + + app.Run(os.Args) +} +``` + +Install our command to the `$GOPATH/bin` directory: + +``` +$ go install +``` + +Finally run our new command: + +``` +$ greet +Hello friend! +``` + +`cli.go` also generates neat help text: + +``` +$ greet help +NAME: + greet - fight the loneliness! + +USAGE: + greet [global options] command [command options] [arguments...] + +VERSION: + 0.0.0 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS + --version Shows version information +``` + +### Arguments + +You can lookup arguments by calling the `Args` function on `cli.Context`. + +``` go +... +app.Action = func(c *cli.Context) { + println("Hello", c.Args()[0]) +} +... +``` + +### Flags + +Setting and querying flags is simple. + +``` go +... +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, +} +app.Action = func(c *cli.Context) { + name := "someone" + if len(c.Args()) > 0 { + name = c.Args()[0] + } + if c.String("lang") == "spanish" { + println("Hola", name) + } else { + println("Hello", name) + } +} +... +``` + +You can also set a destination variable for a flag, to which the content will be scanned. + +``` go +... +var language string +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, +} +app.Action = func(c *cli.Context) { + name := "someone" + if len(c.Args()) > 0 { + name = c.Args()[0] + } + if language == "spanish" { + println("Hola", name) + } else { + println("Hello", name) + } +} +... +``` + +See full list of flags at http://godoc.org/github.com/codegangsta/cli + +#### Alternate Names + +You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, +} +``` + +That flag can then be set with `--lang spanish` or `-l spanish`. Note that giving two different forms of the same flag in the same command invocation is an error. + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVar`. e.g. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, +} +``` + +The `EnvVar` may also be given as a comma-delimited "cascade", where the first environment variable that resolves is used as the default. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, +} +``` + +### Subcommands + +Subcommands can be defined for a more git-like command line app. + +```go +... +app.Commands = []cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) { + println("added task: ", c.Args().First()) + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + }, + { + Name: "template", + Aliases: []string{"r"}, + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) { + println("new task template: ", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) { + println("removed task template: ", c.Args().First()) + }, + }, + }, + }, +} +... +``` + +### Bash Completion + +You can enable completion commands by setting the `EnableBashCompletion` +flag on the `App` object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. + +```go +... +var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} +app := cli.NewApp() +app.EnableBashCompletion = true +app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if len(c.Args()) > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, + } +} +... +``` + +#### To Enable + +Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while +setting the `PROG` variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + +#### To Distribute + +Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename +it to the name of the program you wish to add autocomplete support for (or +automatically install it there if you are distributing a package). Don't forget +to source the file to make it active in the current shell. + +``` +sudo cp src/bash_autocomplete /etc/bash_completion.d/ +source /etc/bash_completion.d/ +``` + +Alternatively, you can just document that users should source the generic +`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set +to the name of their program (as above). + +## Contribution Guidelines + +Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. + +If you have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. + +If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. diff --git a/vendor/github.com/codegangsta/cli/app.go b/vendor/github.com/codegangsta/cli/app.go new file mode 100644 index 0000000..1ea3fd0 --- /dev/null +++ b/vendor/github.com/codegangsta/cli/app.go @@ -0,0 +1,349 @@ +package cli + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path" + "time" +) + +// App is the main structure of a cli application. It is recommended that +// an app be created with the cli.NewApp() function +type App struct { + // The name of the program. Defaults to path.Base(os.Args[0]) + Name string + // Full name of command for help, defaults to Name + HelpName string + // Description of the program. + Usage string + // Text to override the USAGE section of help + UsageText string + // Description of the program argument format. + ArgsUsage string + // Version of the program + Version string + // List of commands to execute + Commands []Command + // List of flags to parse + Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command + HideHelp bool + // Boolean to hide built-in version flag + HideVersion bool + // An action to execute when the bash-completion flag is set + BashComplete func(context *Context) + // An action to execute before any subcommands are run, but after the context is ready + // If a non-nil error is returned, no subcommands are run + Before func(context *Context) error + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After func(context *Context) error + // The action to execute when no subcommands are specified + Action func(context *Context) + // Execute this function if the proper command cannot be found + CommandNotFound func(context *Context, command string) + // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. + // This function is able to replace the original error messages. + // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. + OnUsageError func(context *Context, err error, isSubcommand bool) error + // Compilation date + Compiled time.Time + // List of all authors who contributed + Authors []Author + // Copyright of the binary if any + Copyright string + // Name of Author (Note: Use App.Authors, this is deprecated) + Author string + // Email of Author (Note: Use App.Authors, this is deprecated) + Email string + // Writer writer to write output to + Writer io.Writer +} + +// Tries to find out when this binary was compiled. +// Returns the current time if it fails to find it. +func compileTime() time.Time { + info, err := os.Stat(os.Args[0]) + if err != nil { + return time.Now() + } + return info.ModTime() +} + +// Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. +func NewApp() *App { + return &App{ + Name: path.Base(os.Args[0]), + HelpName: path.Base(os.Args[0]), + Usage: "A new cli application", + UsageText: "", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + Writer: os.Stdout, + } +} + +// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination +func (a *App) Run(arguments []string) (err error) { + if a.Author != "" || a.Email != "" { + a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) + } + + newCmds := []Command{} + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCmds = append(newCmds, c) + } + a.Commands = newCmds + + // append help to commands + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + if (HelpFlag != BoolFlag{}) { + a.appendFlag(HelpFlag) + } + } + + //append version/help flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + + if !a.HideVersion { + a.appendFlag(VersionFlag) + } + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err = set.Parse(arguments[1:]) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, nil) + if nerr != nil { + fmt.Fprintln(a.Writer, nerr) + ShowAppHelp(context) + return nerr + } + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err := a.OnUsageError(context, err, false) + return err + } else { + fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + ShowAppHelp(context) + return err + } + } + + if !a.HideHelp && checkHelp(context) { + ShowAppHelp(context) + return nil + } + + if !a.HideVersion && checkVersion(context) { + ShowVersion(context) + return nil + } + + if a.After != nil { + defer func() { + if afterErr := a.After(context); afterErr != nil { + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + err = a.Before(context) + if err != nil { + fmt.Fprintf(a.Writer, "%v\n\n", err) + ShowAppHelp(context) + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + a.Action(context) + return nil +} + +// Another entry point to the cli app, takes care of passing arguments and error handling +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags +func (a *App) RunAsSubcommand(ctx *Context) (err error) { + // append help to commands + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + if (HelpFlag != BoolFlag{}) { + a.appendFlag(HelpFlag) + } + } + } + + newCmds := []Command{} + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCmds = append(newCmds, c) + } + a.Commands = newCmds + + // append flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err = set.Parse(ctx.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, ctx) + + if nerr != nil { + fmt.Fprintln(a.Writer, nerr) + fmt.Fprintln(a.Writer) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } + return nerr + } + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err = a.OnUsageError(context, err, true) + return err + } else { + fmt.Fprintf(a.Writer, "%s\n\n", "Incorrect Usage.") + ShowSubcommandHelp(context) + return err + } + } + + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } + } + + if a.After != nil { + defer func() { + afterErr := a.After(context) + if afterErr != nil { + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + err := a.Before(context) + if err != nil { + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + a.Action(context) + + return nil +} + +// Returns the named command on App. Returns nil if the command does not exist +func (a *App) Command(name string) *Command { + for _, c := range a.Commands { + if c.HasName(name) { + return &c + } + } + + return nil +} + +func (a *App) hasFlag(flag Flag) bool { + for _, f := range a.Flags { + if flag == f { + return true + } + } + + return false +} + +func (a *App) appendFlag(flag Flag) { + if !a.hasFlag(flag) { + a.Flags = append(a.Flags, flag) + } +} + +// Author represents someone who has contributed to a cli project. +type Author struct { + Name string // The Authors name + Email string // The Authors email +} + +// String makes Author comply to the Stringer interface, to allow an easy print in the templating process +func (a Author) String() string { + e := "" + if a.Email != "" { + e = "<" + a.Email + "> " + } + + return fmt.Sprintf("%v %v", a.Name, e) +} diff --git a/vendor/github.com/codegangsta/cli/appveyor.yml b/vendor/github.com/codegangsta/cli/appveyor.yml new file mode 100644 index 0000000..3ca7afa --- /dev/null +++ b/vendor/github.com/codegangsta/cli/appveyor.yml @@ -0,0 +1,16 @@ +version: "{build}" + +os: Windows Server 2012 R2 + +install: + - go version + - go env + +build_script: + - cd %APPVEYOR_BUILD_FOLDER% + - go vet ./... + - go test -v ./... + +test: off + +deploy: off diff --git a/vendor/github.com/codegangsta/cli/cli.go b/vendor/github.com/codegangsta/cli/cli.go new file mode 100644 index 0000000..31dc912 --- /dev/null +++ b/vendor/github.com/codegangsta/cli/cli.go @@ -0,0 +1,40 @@ +// Package cli provides a minimal framework for creating and organizing command line +// Go applications. cli is designed to be easy to understand and write, the most simple +// cli application can be written as follows: +// func main() { +// cli.NewApp().Run(os.Args) +// } +// +// Of course this application does not do much, so let's make this an actual application: +// func main() { +// app := cli.NewApp() +// app.Name = "greet" +// app.Usage = "say a greeting" +// app.Action = func(c *cli.Context) { +// println("Greetings") +// } +// +// app.Run(os.Args) +// } +package cli + +import ( + "strings" +) + +type MultiError struct { + Errors []error +} + +func NewMultiError(err ...error) MultiError { + return MultiError{Errors: err} +} + +func (m MultiError) Error() string { + errs := make([]string, len(m.Errors)) + for i, err := range m.Errors { + errs[i] = err.Error() + } + + return strings.Join(errs, "\n") +} diff --git a/vendor/github.com/codegangsta/cli/command.go b/vendor/github.com/codegangsta/cli/command.go new file mode 100644 index 0000000..bbf42ae --- /dev/null +++ b/vendor/github.com/codegangsta/cli/command.go @@ -0,0 +1,250 @@ +package cli + +import ( + "fmt" + "io/ioutil" + "strings" +) + +// Command is a subcommand for a cli.App. +type Command struct { + // The name of the command + Name string + // short name of the command. Typically one character (deprecated, use `Aliases`) + ShortName string + // A list of aliases for the command + Aliases []string + // A short description of the usage of this command + Usage string + // Custom text to show on USAGE section of help + UsageText string + // A longer explanation of how the command works + Description string + // A short description of the arguments of this command + ArgsUsage string + // The function to call when checking for bash command completions + BashComplete func(context *Context) + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before func(context *Context) error + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After func(context *Context) error + // The function to call when this command is invoked + Action func(context *Context) + // Execute this function, if an usage error occurs. This is useful for displaying customized usage error messages. + // This function is able to replace the original error messages. + // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. + OnUsageError func(context *Context, err error) error + // List of child commands + Subcommands []Command + // List of flags to parse + Flags []Flag + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Boolean to hide built-in help command + HideHelp bool + + // Full name of command for help, defaults to full command name, including parent commands. + HelpName string + commandNamePath []string +} + +// Returns the full name of the command. +// For subcommands this ensures that parent commands are part of the command path +func (c Command) FullName() string { + if c.commandNamePath == nil { + return c.Name + } + return strings.Join(c.commandNamePath, " ") +} + +// Invokes the command given the context, parses ctx.Args() to generate command-specific flags +func (c Command) Run(ctx *Context) (err error) { + if len(c.Subcommands) > 0 { + return c.startApp(ctx) + } + + if !c.HideHelp && (HelpFlag != BoolFlag{}) { + // append help to flags + c.Flags = append( + c.Flags, + HelpFlag, + ) + } + + if ctx.App.EnableBashCompletion { + c.Flags = append(c.Flags, BashCompletionFlag) + } + + set := flagSet(c.Name, c.Flags) + set.SetOutput(ioutil.Discard) + + if !c.SkipFlagParsing { + firstFlagIndex := -1 + terminatorIndex := -1 + for index, arg := range ctx.Args() { + if arg == "--" { + terminatorIndex = index + break + } else if arg == "-" { + // Do nothing. A dash alone is not really a flag. + continue + } else if strings.HasPrefix(arg, "-") && firstFlagIndex == -1 { + firstFlagIndex = index + } + } + + if firstFlagIndex > -1 { + args := ctx.Args() + regularArgs := make([]string, len(args[1:firstFlagIndex])) + copy(regularArgs, args[1:firstFlagIndex]) + + var flagArgs []string + if terminatorIndex > -1 { + flagArgs = args[firstFlagIndex:terminatorIndex] + regularArgs = append(regularArgs, args[terminatorIndex:]...) + } else { + flagArgs = args[firstFlagIndex:] + } + + err = set.Parse(append(flagArgs, regularArgs...)) + } else { + err = set.Parse(ctx.Args().Tail()) + } + } else { + if c.SkipFlagParsing { + err = set.Parse(append([]string{"--"}, ctx.Args().Tail()...)) + } + } + + if err != nil { + if c.OnUsageError != nil { + err := c.OnUsageError(ctx, err) + return err + } else { + fmt.Fprintln(ctx.App.Writer, "Incorrect Usage.") + fmt.Fprintln(ctx.App.Writer) + ShowCommandHelp(ctx, c.Name) + return err + } + } + + nerr := normalizeFlags(c.Flags, set) + if nerr != nil { + fmt.Fprintln(ctx.App.Writer, nerr) + fmt.Fprintln(ctx.App.Writer) + ShowCommandHelp(ctx, c.Name) + return nerr + } + context := NewContext(ctx.App, set, ctx) + + if checkCommandCompletions(context, c.Name) { + return nil + } + + if checkCommandHelp(context, c.Name) { + return nil + } + + if c.After != nil { + defer func() { + afterErr := c.After(context) + if afterErr != nil { + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if c.Before != nil { + err := c.Before(context) + if err != nil { + fmt.Fprintln(ctx.App.Writer, err) + fmt.Fprintln(ctx.App.Writer) + ShowCommandHelp(ctx, c.Name) + return err + } + } + + context.Command = c + c.Action(context) + return nil +} + +func (c Command) Names() []string { + names := []string{c.Name} + + if c.ShortName != "" { + names = append(names, c.ShortName) + } + + return append(names, c.Aliases...) +} + +// Returns true if Command.Name or Command.ShortName matches given name +func (c Command) HasName(name string) bool { + for _, n := range c.Names() { + if n == name { + return true + } + } + return false +} + +func (c Command) startApp(ctx *Context) error { + app := NewApp() + + // set the name and usage + app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + if c.HelpName == "" { + app.HelpName = c.HelpName + } else { + app.HelpName = app.Name + } + + if c.Description != "" { + app.Usage = c.Description + } else { + app.Usage = c.Usage + } + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + app.HideHelp = c.HideHelp + + app.Version = ctx.App.Version + app.HideVersion = ctx.App.HideVersion + app.Compiled = ctx.App.Compiled + app.Author = ctx.App.Author + app.Email = ctx.App.Email + app.Writer = ctx.App.Writer + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the actions + app.Before = c.Before + app.After = c.After + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + + for index, cc := range app.Commands { + app.Commands[index].commandNamePath = []string{c.Name, cc.Name} + } + + return app.RunAsSubcommand(ctx) +} diff --git a/vendor/github.com/codegangsta/cli/context.go b/vendor/github.com/codegangsta/cli/context.go new file mode 100644 index 0000000..0513d34 --- /dev/null +++ b/vendor/github.com/codegangsta/cli/context.go @@ -0,0 +1,388 @@ +package cli + +import ( + "errors" + "flag" + "strconv" + "strings" + "time" +) + +// Context is a type that is passed through to +// each Handler action in a cli application. Context +// can be used to retrieve context-specific Args and +// parsed command-line options. +type Context struct { + App *App + Command Command + flagSet *flag.FlagSet + setFlags map[string]bool + globalSetFlags map[string]bool + parentContext *Context +} + +// Creates a new context. For use in when invoking an App or Command action. +func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { + return &Context{App: app, flagSet: set, parentContext: parentCtx} +} + +// Looks up the value of a local int flag, returns 0 if no int flag exists +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + +// Looks up the value of a local float64 flag, returns 0 if no float64 flag exists +func (c *Context) Float64(name string) float64 { + return lookupFloat64(name, c.flagSet) +} + +// Looks up the value of a local bool flag, returns false if no bool flag exists +func (c *Context) Bool(name string) bool { + return lookupBool(name, c.flagSet) +} + +// Looks up the value of a local boolT flag, returns false if no bool flag exists +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + +// Looks up the value of a local string flag, returns "" if no string flag exists +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// Looks up the value of a local string slice flag, returns nil if no string slice flag exists +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// Looks up the value of a local int slice flag, returns nil if no int slice flag exists +func (c *Context) IntSlice(name string) []int { + return lookupIntSlice(name, c.flagSet) +} + +// Looks up the value of a local generic flag, returns nil if no generic flag exists +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// Looks up the value of a global int flag, returns 0 if no int flag exists +func (c *Context) GlobalInt(name string) int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt(name, fs) + } + return 0 +} + +// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) GlobalDuration(name string) time.Duration { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} + +// Looks up the value of a global bool flag, returns false if no bool flag exists +func (c *Context) GlobalBool(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +// Looks up the value of a global string flag, returns "" if no string flag exists +func (c *Context) GlobalString(name string) string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupString(name, fs) + } + return "" +} + +// Looks up the value of a global string slice flag, returns nil if no string slice flag exists +func (c *Context) GlobalStringSlice(name string) []string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupStringSlice(name, fs) + } + return nil +} + +// Looks up the value of a global int slice flag, returns nil if no int slice flag exists +func (c *Context) GlobalIntSlice(name string) []int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupIntSlice(name, fs) + } + return nil +} + +// Looks up the value of a global generic flag, returns nil if no generic flag exists +func (c *Context) GlobalGeneric(name string) interface{} { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupGeneric(name, fs) + } + return nil +} + +// Returns the number of flags set +func (c *Context) NumFlags() int { + return c.flagSet.NFlag() +} + +// Determines if the flag was actually set +func (c *Context) IsSet(name string) bool { + if c.setFlags == nil { + c.setFlags = make(map[string]bool) + c.flagSet.Visit(func(f *flag.Flag) { + c.setFlags[f.Name] = true + }) + } + return c.setFlags[name] == true +} + +// Determines if the global flag was actually set +func (c *Context) GlobalIsSet(name string) bool { + if c.globalSetFlags == nil { + c.globalSetFlags = make(map[string]bool) + ctx := c + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + for ; ctx != nil && c.globalSetFlags[name] == false; ctx = ctx.parentContext { + ctx.flagSet.Visit(func(f *flag.Flag) { + c.globalSetFlags[f.Name] = true + }) + } + } + return c.globalSetFlags[name] +} + +// Returns a slice of flag names used in this context. +func (c *Context) FlagNames() (names []string) { + for _, flag := range c.Command.Flags { + name := strings.Split(flag.GetName(), ",")[0] + if name == "help" { + continue + } + names = append(names, name) + } + return +} + +// Returns a slice of global flag names used by the app. +func (c *Context) GlobalFlagNames() (names []string) { + for _, flag := range c.App.Flags { + name := strings.Split(flag.GetName(), ",")[0] + if name == "help" || name == "version" { + continue + } + names = append(names, name) + } + return +} + +// Returns the parent context, if any +func (c *Context) Parent() *Context { + return c.parentContext +} + +type Args []string + +// Returns the command line arguments associated with the context. +func (c *Context) Args() Args { + args := Args(c.flagSet.Args()) + return args +} + +// Returns the nth argument, or else a blank string +func (a Args) Get(n int) string { + if len(a) > n { + return a[n] + } + return "" +} + +// Returns the first argument, or else a blank string +func (a Args) First() string { + return a.Get(0) +} + +// Return the rest of the arguments (not the first one) +// or else an empty string slice +func (a Args) Tail() []string { + if len(a) >= 2 { + return []string(a)[1:] + } + return []string{} +} + +// Checks if there are any arguments present +func (a Args) Present() bool { + return len(a) != 0 +} + +// Swaps arguments at the given indexes +func (a Args) Swap(from, to int) error { + if from >= len(a) || to >= len(a) { + return errors.New("index out of range") + } + a[from], a[to] = a[to], a[from] + return nil +} + +func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + for ; ctx != nil; ctx = ctx.parentContext { + if f := ctx.flagSet.Lookup(name); f != nil { + return ctx.flagSet + } + } + return nil +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + val, err := strconv.Atoi(f.Value.String()) + if err != nil { + return 0 + } + return val + } + + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + val, err := time.ParseDuration(f.Value.String()) + if err == nil { + return val + } + } + + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return val + } + + return 0 +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + return f.Value.String() + } + + return "" +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*StringSlice)).Value() + + } + + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*IntSlice)).Value() + + } + + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + return f.Value + } + return nil +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return val + } + + return false +} + +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return true + } + return val + } + + return false +} + +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case *StringSlice: + default: + set.Set(name, ff.Value.String()) + } +} + +func normalizeFlags(flags []Flag, set *flag.FlagSet) error { + visited := make(map[string]bool) + set.Visit(func(f *flag.Flag) { + visited[f.Name] = true + }) + for _, f := range flags { + parts := strings.Split(f.GetName(), ",") + if len(parts) == 1 { + continue + } + var ff *flag.Flag + for _, name := range parts { + name = strings.Trim(name, " ") + if visited[name] { + if ff != nil { + return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) + } + ff = set.Lookup(name) + } + } + if ff == nil { + continue + } + for _, name := range parts { + name = strings.Trim(name, " ") + if !visited[name] { + copyFlag(name, ff, set) + } + } + } + return nil +} diff --git a/vendor/github.com/codegangsta/cli/flag.go b/vendor/github.com/codegangsta/cli/flag.go new file mode 100644 index 0000000..e951c2d --- /dev/null +++ b/vendor/github.com/codegangsta/cli/flag.go @@ -0,0 +1,546 @@ +package cli + +import ( + "flag" + "fmt" + "os" + "runtime" + "strconv" + "strings" + "time" +) + +// This flag enables bash-completion for all commands and subcommands +var BashCompletionFlag = BoolFlag{ + Name: "generate-bash-completion", +} + +// This flag prints the version for the application +var VersionFlag = BoolFlag{ + Name: "version, v", + Usage: "print the version", +} + +// This flag prints the help for all commands and subcommands +// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand +// unless HideHelp is set to true) +var HelpFlag = BoolFlag{ + Name: "help, h", + Usage: "show help", +} + +// Flag is a common interface related to parsing flags in cli. +// For more advanced flag parsing techniques, it is recommended that +// this interface be implemented. +type Flag interface { + fmt.Stringer + // Apply Flag settings to the given flag set + Apply(*flag.FlagSet) + GetName() string +} + +func flagSet(name string, flags []Flag) *flag.FlagSet { + set := flag.NewFlagSet(name, flag.ContinueOnError) + + for _, f := range flags { + f.Apply(set) + } + return set +} + +func eachName(longName string, fn func(string)) { + parts := strings.Split(longName, ",") + for _, name := range parts { + name = strings.Trim(name, " ") + fn(name) + } +} + +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// GenericFlag is the flag type for types implementing Generic +type GenericFlag struct { + Name string + Value Generic + Usage string + EnvVar string +} + +// String returns the string representation of the generic flag to display the +// help text to the user (uses the String() method of the generic flag to show +// the value) +func (f GenericFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage)) +} + +func (f GenericFlag) FormatValueHelp() string { + if f.Value == nil { + return "" + } + s := f.Value.String() + if len(s) == 0 { + return "" + } + return fmt.Sprintf("\"%s\"", s) +} + +// Apply takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) Apply(set *flag.FlagSet) { + val := f.Value + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + val.Set(envVal) + break + } + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) +} + +func (f GenericFlag) GetName() string { + return f.Name +} + +// StringSlice is an opaque type for []string to satisfy flag.Value +type StringSlice []string + +// Set appends the string value to the list of values +func (f *StringSlice) Set(value string) error { + *f = append(*f, value) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *StringSlice) String() string { + return fmt.Sprintf("%s", *f) +} + +// Value returns the slice of strings set by this flag +func (f *StringSlice) Value() []string { + return *f +} + +// StringSlice is a string flag that can be specified multiple times on the +// command-line +type StringSliceFlag struct { + Name string + Value *StringSlice + Usage string + EnvVar string +} + +// String returns the usage +func (f StringSliceFlag) String() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f StringSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + newVal.Set(s) + } + f.Value = newVal + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &StringSlice{} + } + set.Var(f.Value, name, f.Usage) + }) +} + +func (f StringSliceFlag) GetName() string { + return f.Name +} + +// StringSlice is an opaque type for []int to satisfy flag.Value +type IntSlice []int + +// Set parses the value into an integer and appends it to the list of values +func (f *IntSlice) Set(value string) error { + tmp, err := strconv.Atoi(value) + if err != nil { + return err + } else { + *f = append(*f, tmp) + } + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *IntSlice) String() string { + return fmt.Sprintf("%d", *f) +} + +// Value returns the slice of ints set by this flag +func (f *IntSlice) Value() []int { + return *f +} + +// IntSliceFlag is an int flag that can be specified multiple times on the +// command-line +type IntSliceFlag struct { + Name string + Value *IntSlice + Usage string + EnvVar string +} + +// String returns the usage +func (f IntSliceFlag) String() string { + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s [%v]\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f IntSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + err := newVal.Set(s) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + } + } + f.Value = newVal + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &IntSlice{} + } + set.Var(f.Value, name, f.Usage) + }) +} + +func (f IntSliceFlag) GetName() string { + return f.Name +} + +// BoolFlag is a switch that defaults to false +type BoolFlag struct { + Name string + Usage string + EnvVar string + Destination *bool +} + +// String returns a readable representation of this value (for usage defaults) +func (f BoolFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f BoolFlag) Apply(set *flag.FlagSet) { + val := false + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.BoolVar(f.Destination, name, val, f.Usage) + return + } + set.Bool(name, val, f.Usage) + }) +} + +func (f BoolFlag) GetName() string { + return f.Name +} + +// BoolTFlag this represents a boolean flag that is true by default, but can +// still be set to false by --some-flag=false +type BoolTFlag struct { + Name string + Usage string + EnvVar string + Destination *bool +} + +// String returns a readable representation of this value (for usage defaults) +func (f BoolTFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f BoolTFlag) Apply(set *flag.FlagSet) { + val := true + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + break + } + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.BoolVar(f.Destination, name, val, f.Usage) + return + } + set.Bool(name, val, f.Usage) + }) +} + +func (f BoolTFlag) GetName() string { + return f.Name +} + +// StringFlag represents a flag that takes as string value +type StringFlag struct { + Name string + Value string + Usage string + EnvVar string + Destination *string +} + +// String returns the usage +func (f StringFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s %v\t%v", prefixedNames(f.Name), f.FormatValueHelp(), f.Usage)) +} + +func (f StringFlag) FormatValueHelp() string { + s := f.Value + if len(s) == 0 { + return "" + } + return fmt.Sprintf("\"%s\"", s) +} + +// Apply populates the flag given the flag set and environment +func (f StringFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + f.Value = envVal + break + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.StringVar(f.Destination, name, f.Value, f.Usage) + return + } + set.String(name, f.Value, f.Usage) + }) +} + +func (f StringFlag) GetName() string { + return f.Name +} + +// IntFlag is a flag that takes an integer +// Errors if the value provided cannot be parsed +type IntFlag struct { + Name string + Value int + Usage string + EnvVar string + Destination *int +} + +// String returns the usage +func (f IntFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f IntFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err == nil { + f.Value = int(envValInt) + break + } + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.IntVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Int(name, f.Value, f.Usage) + }) +} + +func (f IntFlag) GetName() string { + return f.Name +} + +// DurationFlag is a flag that takes a duration specified in Go's duration +// format: https://golang.org/pkg/time/#ParseDuration +type DurationFlag struct { + Name string + Value time.Duration + Usage string + EnvVar string + Destination *time.Duration +} + +// String returns a readable representation of this value (for usage defaults) +func (f DurationFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f DurationFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValDuration, err := time.ParseDuration(envVal) + if err == nil { + f.Value = envValDuration + break + } + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.DurationVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Duration(name, f.Value, f.Usage) + }) +} + +func (f DurationFlag) GetName() string { + return f.Name +} + +// Float64Flag is a flag that takes an float value +// Errors if the value provided cannot be parsed +type Float64Flag struct { + Name string + Value float64 + Usage string + EnvVar string + Destination *float64 +} + +// String returns the usage +func (f Float64Flag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s \"%v\"\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +// Apply populates the flag given the flag set and environment +func (f Float64Flag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + for _, envVar := range strings.Split(f.EnvVar, ",") { + envVar = strings.TrimSpace(envVar) + if envVal := os.Getenv(envVar); envVal != "" { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err == nil { + f.Value = float64(envValFloat) + } + } + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Float64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Float64(name, f.Value, f.Usage) + }) +} + +func (f Float64Flag) GetName() string { + return f.Name +} + +func prefixFor(name string) (prefix string) { + if len(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} + +func prefixedNames(fullName string) (prefixed string) { + parts := strings.Split(fullName, ",") + for i, name := range parts { + name = strings.Trim(name, " ") + prefixed += prefixFor(name) + name + if i < len(parts)-1 { + prefixed += ", " + } + } + return +} + +func withEnvHint(envVar, str string) string { + envText := "" + if envVar != "" { + prefix := "$" + suffix := "" + sep := ", $" + if runtime.GOOS == "windows" { + prefix = "%" + suffix = "%" + sep = "%, %" + } + envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(strings.Split(envVar, ","), sep), suffix) + } + return str + envText +} diff --git a/vendor/github.com/codegangsta/cli/help.go b/vendor/github.com/codegangsta/cli/help.go new file mode 100644 index 0000000..15916f8 --- /dev/null +++ b/vendor/github.com/codegangsta/cli/help.go @@ -0,0 +1,248 @@ +package cli + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + "text/template" +) + +// The text template for the Default help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .Flags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} + {{if .Version}} +VERSION: + {{.Version}} + {{end}}{{if len .Authors}} +AUTHOR(S): + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +COMMANDS: + {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{end}}{{if .Flags}} +GLOBAL OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{end}}{{if .Copyright }} +COPYRIGHT: + {{.Copyright}} + {{end}} +` + +// The text template for the command help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var CommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .Flags}} + +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{ end }} +` + +// The text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}} command{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + +COMMANDS: + {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{if .Flags}} +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{end}} +` + +var helpCommand = Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowAppHelp(c) + } + }, +} + +var helpSubcommand = Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowSubcommandHelp(c) + } + }, +} + +// Prints help for the App or Command +type helpPrinter func(w io.Writer, templ string, data interface{}) + +var HelpPrinter helpPrinter = printHelp + +// Prints version for the App +var VersionPrinter = printVersion + +func ShowAppHelp(c *Context) { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) +} + +// Prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + for _, command := range c.App.Commands { + for _, name := range command.Names() { + fmt.Fprintln(c.App.Writer, name) + } + } +} + +// Prints help for the given command +func ShowCommandHelp(ctx *Context, command string) { + // show the subcommand help for a command with subcommands + if command == "" { + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) + return + } + + for _, c := range ctx.App.Commands { + if c.HasName(command) { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + return + } + } + + if ctx.App.CommandNotFound != nil { + ctx.App.CommandNotFound(ctx, command) + } else { + fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command) + } +} + +// Prints help for the given subcommand +func ShowSubcommandHelp(c *Context) { + ShowCommandHelp(c, c.Command.Name) +} + +// Prints the version number of the App +func ShowVersion(c *Context) { + VersionPrinter(c) +} + +func printVersion(c *Context) { + fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) +} + +// Prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// Prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil && c.BashComplete != nil { + c.BashComplete(ctx) + } +} + +func printHelp(out io.Writer, templ string, data interface{}) { + funcMap := template.FuncMap{ + "join": strings.Join, + } + + w := tabwriter.NewWriter(out, 0, 8, 1, '\t', 0) + t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) + err := t.Execute(w, data) + if err != nil { + // If the writer is closed, t.Execute will fail, and there's nothing + // we can do to recover. We could send this to os.Stderr if we need. + return + } + w.Flush() +} + +func checkVersion(c *Context) bool { + found := false + if VersionFlag.Name != "" { + eachName(VersionFlag.Name, func(name string) { + if c.GlobalBool(name) || c.Bool(name) { + found = true + } + }) + } + return found +} + +func checkHelp(c *Context) bool { + found := false + if HelpFlag.Name != "" { + eachName(HelpFlag.Name, func(name string) { + if c.GlobalBool(name) || c.Bool(name) { + found = true + } + }) + } + return found +} + +func checkCommandHelp(c *Context, name string) bool { + if c.Bool("h") || c.Bool("help") { + ShowCommandHelp(c, name) + return true + } + + return false +} + +func checkSubcommandHelp(c *Context) bool { + if c.GlobalBool("h") || c.GlobalBool("help") { + ShowSubcommandHelp(c) + return true + } + + return false +} + +func checkCompletions(c *Context) bool { + if (c.GlobalBool(BashCompletionFlag.Name) || c.Bool(BashCompletionFlag.Name)) && c.App.EnableBashCompletion { + ShowCompletions(c) + return true + } + + return false +} + +func checkCommandCompletions(c *Context, name string) bool { + if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCommandCompletions(c, name) + return true + } + + return false +} diff --git a/vendor/github.com/nlopes/slack b/vendor/github.com/nlopes/slack deleted file mode 160000 index a6a2e43..0000000 --- a/vendor/github.com/nlopes/slack +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a6a2e430fd2e1bc1f91c0fd538e5c2d0c898f5dc diff --git a/vendor/github.com/nlopes/slack/.gitignore b/vendor/github.com/nlopes/slack/.gitignore new file mode 100644 index 0000000..dd2440d --- /dev/null +++ b/vendor/github.com/nlopes/slack/.gitignore @@ -0,0 +1,2 @@ +*.test +*~ diff --git a/vendor/github.com/nlopes/slack/.travis.yml b/vendor/github.com/nlopes/slack/.travis.yml new file mode 100644 index 0000000..53f636b --- /dev/null +++ b/vendor/github.com/nlopes/slack/.travis.yml @@ -0,0 +1,19 @@ +language: go + +go: + - 1.4 + - tip + +before_install: + - export PATH=$HOME/gopath/bin:$PATH + +script: + - go test -race ./... + - go test -cover ./... + +matrix: + allow_failures: + - go: tip + +git: + depth: 10 diff --git a/vendor/github.com/nlopes/slack/LICENSE b/vendor/github.com/nlopes/slack/LICENSE new file mode 100644 index 0000000..5145171 --- /dev/null +++ b/vendor/github.com/nlopes/slack/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Norberto Lopes +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/nlopes/slack/README.md b/vendor/github.com/nlopes/slack/README.md new file mode 100644 index 0000000..5637182 --- /dev/null +++ b/vendor/github.com/nlopes/slack/README.md @@ -0,0 +1,72 @@ +Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack) +=============== + +This library supports most if not all of the `api.slack.com` REST +calls, as well as the Real-Time Messaging protocol over websocket, in +a fully managed way. + + +Note: If you just updated from master and it broke your implementation, please check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) + +## Installing + +### *go get* + + $ go get github.com/nlopes/slack + +## Example + +### Getting all groups + + import ( + "fmt" + + "github.com/nlopes/slack" + ) + + func main() { + api := slack.New("YOUR_TOKEN_HERE") + // If you set debugging, it will log all requests to the console + // Useful when encountering issues + // api.SetDebug(true) + groups, err := api.GetGroups(false) + if err != nil { + fmt.Printf("%s\n", err) + return + } + for _, group := range groups { + fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) + } + } + +### Getting User Information + + import ( + "fmt" + + "github.com/nlopes/slack" + ) + + func main() { + api := slack.New("YOUR_TOKEN_HERE") + user, err := api.GetUserInfo("U023BECGF") + if err != nil { + fmt.Printf("%s\n", err) + return + } + fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) + } + +## Minimal RTM usage: + +See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go + + +## Contributing + +You are more than welcome to contribute to this project. Fork and +make a Pull Request, or create an Issue if you see any problem. + +## License + +BSD 2 Clause license diff --git a/vendor/github.com/nlopes/slack/TODO.txt b/vendor/github.com/nlopes/slack/TODO.txt new file mode 100644 index 0000000..8607960 --- /dev/null +++ b/vendor/github.com/nlopes/slack/TODO.txt @@ -0,0 +1,3 @@ +- Add more tests!!! +- Add support to have markdown hints + - See section Message Formatting at https://api.slack.com/docs/formatting diff --git a/vendor/github.com/nlopes/slack/admin.go b/vendor/github.com/nlopes/slack/admin.go new file mode 100644 index 0000000..393b288 --- /dev/null +++ b/vendor/github.com/nlopes/slack/admin.go @@ -0,0 +1,190 @@ +package slack + +import ( + "errors" + "fmt" + "net/url" +) + +type adminResponse struct { + OK bool `json:"ok"` + Error string `json:"error"` +} + +func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) { + adminResponse := &adminResponse{} + err := parseAdminResponse(method, teamName, values, adminResponse, debug) + if err != nil { + return nil, err + } + + if !adminResponse.OK { + return nil, errors.New(adminResponse.Error) + } + + return adminResponse, nil +} + +// DisableUser disabled a user account, given a user ID +func (api *Client) DisableUser(teamName string, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("setInactive", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err) + } + + return nil +} + +// InviteGuest invites a user to Slack as a single-channel guest +func (api *Client) InviteGuest( + teamName string, + channel string, + firstName string, + lastName string, + emailAddress string, +) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "ultra_restricted": {"1"}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to invite single-channel guest: %s", err) + } + + return nil +} + +// InviteRestricted invites a user to Slack as a restricted account +func (api *Client) InviteRestricted( + teamName string, + channel string, + firstName string, + lastName string, + emailAddress string, +) error { + values := url.Values{ + "email": {emailAddress}, + "channels": {channel}, + "first_name": {firstName}, + "last_name": {lastName}, + "restricted": {"1"}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to restricted account: %s", err) + } + + return nil +} + +// InviteToTeam invites a user to a Slack team +func (api *Client) InviteToTeam( + teamName string, + firstName string, + lastName string, + emailAddress string, +) error { + values := url.Values{ + "email": {emailAddress}, + "first_name": {firstName}, + "last_name": {lastName}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("invite", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to invite to team: %s", err) + } + + return nil +} + +// SetRegular enables the specified user +func (api *Client) SetRegular(teamName string, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("setRegular", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) + } + + return nil +} + +// SendSSOBindingEmail sends an SSO binding email to the specified user +func (api *Client) SendSSOBindingEmail(teamName string, user string) error { + values := url.Values{ + "user": {user}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("sendSSOBind", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) + } + + return nil +} + +// SetUltraRestricted converts a user into a single-channel guest +func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { + values := url.Values{ + "user": {uid}, + "channel": {channel}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("setUltraRestricted", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to ultra-restrict account: %s", err) + } + + return nil +} + +// SetRestricted converts a user into a restricted account +func (api *Client) SetRestricted(teamName, uid string) error { + values := url.Values{ + "user": {uid}, + "token": {api.config.token}, + "set_active": {"true"}, + "_attempts": {"1"}, + } + + _, err := adminRequest("setRestricted", teamName, values, api.debug) + if err != nil { + return fmt.Errorf("Failed to restrict account: %s", err) + } + + return nil +} diff --git a/vendor/github.com/nlopes/slack/attachments.go b/vendor/github.com/nlopes/slack/attachments.go new file mode 100644 index 0000000..eb92d68 --- /dev/null +++ b/vendor/github.com/nlopes/slack/attachments.go @@ -0,0 +1,31 @@ +package slack + +// AttachmentField contains information for an attachment field +// An Attachment can contain multiple of these +type AttachmentField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// Attachment contains all the information for an attachment +type Attachment struct { + Color string `json:"color,omitempty"` + Fallback string `json:"fallback"` + + AuthorName string `json:"author_name,omitempty"` + AuthorSubname string `json:"author_subname,omitempty"` + AuthorLink string `json:"author_link,omitempty"` + AuthorIcon string `json:"author_icon,omitempty"` + + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Pretext string `json:"pretext,omitempty"` + Text string `json:"text"` + + ImageURL string `json:"image_url,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + + Fields []AttachmentField `json:"fields,omitempty"` + MarkdownIn []string `json:"mrkdwn_in,omitempty"` +} diff --git a/vendor/github.com/nlopes/slack/backoff.go b/vendor/github.com/nlopes/slack/backoff.go new file mode 100644 index 0000000..e555a1a --- /dev/null +++ b/vendor/github.com/nlopes/slack/backoff.go @@ -0,0 +1,57 @@ +package slack + +import ( + "math" + "math/rand" + "time" +) + +// This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go + +// Backoff is a time.Duration counter. It starts at Min. After every +// call to Duration() it is multiplied by Factor. It is capped at +// Max. It returns to Min on every call to Reset(). Used in +// conjunction with the time package. +type backoff struct { + attempts int + //Factor is the multiplying factor for each increment step + Factor float64 + //Jitter eases contention by randomizing backoff steps + Jitter bool + //Min and Max are the minimum and maximum values of the counter + Min, Max time.Duration +} + +// Returns the current value of the counter and then multiplies it +// Factor +func (b *backoff) Duration() time.Duration { + //Zero-values are nonsensical, so we use + //them to apply defaults + if b.Min == 0 { + b.Min = 100 * time.Millisecond + } + if b.Max == 0 { + b.Max = 10 * time.Second + } + if b.Factor == 0 { + b.Factor = 2 + } + //calculate this duration + dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts)) + if b.Jitter == true { + dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min) + } + //cap! + if dur > float64(b.Max) { + return b.Max + } + //bump attempts count + b.attempts++ + //return as a time.Duration + return time.Duration(dur) +} + +//Resets the current value of the counter back to Min +func (b *backoff) Reset() { + b.attempts = 0 +} diff --git a/vendor/github.com/nlopes/slack/channels.go b/vendor/github.com/nlopes/slack/channels.go new file mode 100644 index 0000000..a579f24 --- /dev/null +++ b/vendor/github.com/nlopes/slack/channels.go @@ -0,0 +1,254 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +type channelResponseFull struct { + Channel Channel `json:"channel"` + Channels []Channel `json:"channels"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInChannel bool `json:"not_in_channel"` + History + SlackResponse +} + +// Channel contains information about the channel +type Channel struct { + groupConversation + IsChannel bool `json:"is_channel"` + IsGeneral bool `json:"is_general"` + IsMember bool `json:"is_member"` +} + +func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) { + response := &channelResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// ArchiveChannel archives the given channel +func (api *Client) ArchiveChannel(channel string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + _, err := channelRequest("channels.archive", values, api.debug) + if err != nil { + return err + } + return nil +} + +// UnarchiveChannel unarchives the given channel +func (api *Client) UnarchiveChannel(channel string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + _, err := channelRequest("channels.unarchive", values, api.debug) + if err != nil { + return err + } + return nil +} + +// CreateChannel creates a channel with the given name and returns a *Channel +func (api *Client) CreateChannel(channel string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "name": {channel}, + } + response, err := channelRequest("channels.create", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// GetChannelHistory retrieves the channel history +func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + response, err := channelRequest("channels.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetChannelInfo retrieves the given channel +func (api *Client) GetChannelInfo(channel string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + response, err := channelRequest("channels.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// InviteUserToChannel invites a user to a given channel and returns a *Channel +func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "user": {user}, + } + response, err := channelRequest("channels.invite", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// JoinChannel joins the currently authenticated user to a channel +func (api *Client) JoinChannel(channel string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "name": {channel}, + } + response, err := channelRequest("channels.join", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil +} + +// LeaveChannel makes the authenticated user leave the given channel +func (api *Client) LeaveChannel(channel string) (bool, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + response, err := channelRequest("channels.leave", values, api.debug) + if err != nil { + return false, err + } + if response.NotInChannel { + return response.NotInChannel, nil + } + return false, nil +} + +// KickUserFromChannel kicks a user from a given channel +func (api *Client) KickUserFromChannel(channel, user string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "user": {user}, + } + _, err := channelRequest("channels.kick", values, api.debug) + if err != nil { + return err + } + return nil +} + +// GetChannels retrieves all the channels +func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) { + values := url.Values{ + "token": {api.config.token}, + } + if excludeArchived { + values.Add("exclude_archived", "1") + } + response, err := channelRequest("channels.list", values, api.debug) + if err != nil { + return nil, err + } + return response.Channels, nil +} + +// SetChannelReadMark sets the read mark of a given channel to a specific point +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls +// (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A +// timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. +func (api *Client) SetChannelReadMark(channel, ts string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "ts": {ts}, + } + _, err := channelRequest("channels.mark", values, api.debug) + if err != nil { + return err + } + return nil +} + +// RenameChannel renames a given channel +func (api *Client) RenameChannel(channel, name string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "name": {name}, + } + // XXX: the created entry in this call returns a string instead of a number + // so I may have to do some workaround to solve it. + response, err := channelRequest("channels.rename", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil + +} + +// SetChannelPurpose sets the channel purpose and returns the purpose that was +// successfully set +func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "purpose": {purpose}, + } + response, err := channelRequest("channels.setPurpose", values, api.debug) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetChannelTopic sets the channel topic and returns the topic that was successfully set +func (api *Client) SetChannelTopic(channel, topic string) (string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "topic": {topic}, + } + response, err := channelRequest("channels.setTopic", values, api.debug) + if err != nil { + return "", err + } + return response.Topic, nil +} diff --git a/vendor/github.com/nlopes/slack/chat.go b/vendor/github.com/nlopes/slack/chat.go new file mode 100644 index 0000000..9af5c15 --- /dev/null +++ b/vendor/github.com/nlopes/slack/chat.go @@ -0,0 +1,161 @@ +package slack + +import ( + "encoding/json" + "errors" + "net/url" + "strings" +) + +const ( + DEFAULT_MESSAGE_USERNAME = "" + DEFAULT_MESSAGE_ASUSER = false + DEFAULT_MESSAGE_PARSE = "" + DEFAULT_MESSAGE_LINK_NAMES = 0 + DEFAULT_MESSAGE_UNFURL_LINKS = false + DEFAULT_MESSAGE_UNFURL_MEDIA = true + DEFAULT_MESSAGE_ICON_URL = "" + DEFAULT_MESSAGE_ICON_EMOJI = "" + DEFAULT_MESSAGE_MARKDOWN = true + DEFAULT_MESSAGE_ESCAPE_TEXT = true +) + +type chatResponseFull struct { + Channel string `json:"channel"` + Timestamp string `json:"ts"` + Text string `json:"text"` + SlackResponse +} + +// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request +type PostMessageParameters struct { + Text string + Username string + AsUser bool + Parse string + LinkNames int + Attachments []Attachment + UnfurlLinks bool + UnfurlMedia bool + IconURL string + IconEmoji string + Markdown bool `json:"mrkdwn,omitempty"` + EscapeText bool +} + +// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set +func NewPostMessageParameters() PostMessageParameters { + return PostMessageParameters{ + Username: DEFAULT_MESSAGE_USERNAME, + AsUser: DEFAULT_MESSAGE_ASUSER, + Parse: DEFAULT_MESSAGE_PARSE, + LinkNames: DEFAULT_MESSAGE_LINK_NAMES, + Attachments: nil, + UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, + UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, + IconURL: DEFAULT_MESSAGE_ICON_URL, + IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, + Markdown: DEFAULT_MESSAGE_MARKDOWN, + EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, + } +} + +func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) { + response := &chatResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// DeleteMessage deletes a message in a channel +func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "ts": {messageTimestamp}, + } + response, err := chatRequest("chat.delete", values, api.debug) + if err != nil { + return "", "", err + } + return response.Channel, response.Timestamp, nil +} + +func escapeMessage(message string) string { + replacer := strings.NewReplacer("&", "&", "<", "<", ">", ">") + return replacer.Replace(message) +} + +// PostMessage sends a message to a channel. +// Message is escaped by default according to https://api.slack.com/docs/formatting +// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. +func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) { + if params.EscapeText { + text = escapeMessage(text) + } + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "text": {text}, + } + if params.Username != DEFAULT_MESSAGE_USERNAME { + values.Set("username", string(params.Username)) + } + if params.AsUser != DEFAULT_MESSAGE_ASUSER { + values.Set("as_user", "true") + } + if params.Parse != DEFAULT_MESSAGE_PARSE { + values.Set("parse", string(params.Parse)) + } + if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { + values.Set("link_names", "1") + } + if params.Attachments != nil { + attachments, err := json.Marshal(params.Attachments) + if err != nil { + return "", "", err + } + values.Set("attachments", string(attachments)) + } + if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { + values.Set("unfurl_links", "true") + } + if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { + values.Set("unfurl_media", "false") + } + if params.IconURL != DEFAULT_MESSAGE_ICON_URL { + values.Set("icon_url", params.IconURL) + } + if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { + values.Set("icon_emoji", params.IconEmoji) + } + if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { + values.Set("mrkdwn", "false") + } + + response, err := chatRequest("chat.postMessage", values, api.debug) + if err != nil { + return "", "", err + } + return response.Channel, response.Timestamp, nil +} + +// UpdateMessage updates a message in a channel +func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "text": {escapeMessage(text)}, + "ts": {timestamp}, + } + response, err := chatRequest("chat.update", values, api.debug) + if err != nil { + return "", "", "", err + } + return response.Channel, response.Timestamp, response.Text, nil +} diff --git a/vendor/github.com/nlopes/slack/comment.go b/vendor/github.com/nlopes/slack/comment.go new file mode 100644 index 0000000..7d1c0d4 --- /dev/null +++ b/vendor/github.com/nlopes/slack/comment.go @@ -0,0 +1,10 @@ +package slack + +// Comment contains all the information relative to a comment +type Comment struct { + ID string `json:"id,omitempty"` + Created JSONTime `json:"created,omitempty"` + Timestamp JSONTime `json:"timestamp,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/vendor/github.com/nlopes/slack/conversation.go b/vendor/github.com/nlopes/slack/conversation.go new file mode 100644 index 0000000..51b993d --- /dev/null +++ b/vendor/github.com/nlopes/slack/conversation.go @@ -0,0 +1,38 @@ +package slack + +// Conversation is the foundation for IM and BaseGroupConversation +type conversation struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + IsOpen bool `json:"is_open"` + LastRead string `json:"last_read,omitempty"` + Latest *Message `json:"latest,omitempty"` + UnreadCount int `json:"unread_count,omitempty"` + UnreadCountDisplay int `json:"unread_count_display,omitempty"` +} + +// GroupConversation is the foundation for Group and Channel +type groupConversation struct { + conversation + Name string `json:"name"` + Creator string `json:"creator"` + IsArchived bool `json:"is_archived"` + Members []string `json:"members"` + NumMembers int `json:"num_members,omitempty"` + Topic Topic `json:"topic"` + Purpose Purpose `json:"purpose"` +} + +// Topic contains information about the topic +type Topic struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} + +// Purpose contains information about the purpose +type Purpose struct { + Value string `json:"value"` + Creator string `json:"creator"` + LastSet JSONTime `json:"last_set"` +} diff --git a/vendor/github.com/nlopes/slack/emoji.go b/vendor/github.com/nlopes/slack/emoji.go new file mode 100644 index 0000000..776c4a5 --- /dev/null +++ b/vendor/github.com/nlopes/slack/emoji.go @@ -0,0 +1,27 @@ +package slack + +import ( + "errors" + "net/url" +) + +type emojiResponseFull struct { + Emoji map[string]string `json:"emoji"` + SlackResponse +} + +// GetEmoji retrieves all the emojis +func (api *Client) GetEmoji() (map[string]string, error) { + values := url.Values{ + "token": {api.config.token}, + } + response := &emojiResponseFull{} + err := post("emoji.list", values, response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.Emoji, nil +} diff --git a/vendor/github.com/nlopes/slack/files.go b/vendor/github.com/nlopes/slack/files.go new file mode 100644 index 0000000..6a082b7 --- /dev/null +++ b/vendor/github.com/nlopes/slack/files.go @@ -0,0 +1,242 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" + "strings" +) + +const ( + // Add here the defaults in the siten + DEFAULT_FILES_USER = "" + DEFAULT_FILES_TS_FROM = 0 + DEFAULT_FILES_TS_TO = -1 + DEFAULT_FILES_TYPES = "all" + DEFAULT_FILES_COUNT = 100 + DEFAULT_FILES_PAGE = 1 +) + +// File contains all the information for a file +type File struct { + ID string `json:"id"` + Created JSONTime `json:"created"` + Timestamp JSONTime `json:"timestamp"` + + Name string `json:"name"` + Title string `json:"title"` + Mimetype string `json:"mimetype"` + ImageExifRotation int `json:"image_exif_rotation"` + Filetype string `json:"filetype"` + PrettyType string `json:"pretty_type"` + User string `json:"user"` + + Mode string `json:"mode"` + Editable bool `json:"editable"` + IsExternal bool `json:"is_external"` + ExternalType string `json:"external_type"` + + Size int `json:"size"` + + URL string `json:"url"` // Deprecated - never set + URLDownload string `json:"url_download"` // Deprecated - never set + URLPrivate string `json:"url_private"` + URLPrivateDownload string `json:"url_private_download"` + + OriginalH int `json:"original_h"` + OriginalW int `json:"original_w"` + Thumb64 string `json:"thumb_64"` + Thumb80 string `json:"thumb_80"` + Thumb160 string `json:"thumb_160"` + Thumb360 string `json:"thumb_360"` + Thumb360Gif string `json:"thumb_360_gif"` + Thumb360W int `json:"thumb_360_w"` + Thumb360H int `json:"thumb_360_h"` + Thumb480 string `json:"thumb_480"` + Thumb480W int `json:"thumb_480_w"` + Thumb480H int `json:"thumb_480_h"` + Thumb720 string `json:"thumb_720"` + Thumb720W int `json:"thumb_720_w"` + Thumb720H int `json:"thumb_720_h"` + Thumb960 string `json:"thumb_960"` + Thumb960W int `json:"thumb_960_w"` + Thumb960H int `json:"thumb_960_h"` + Thumb1024 string `json:"thumb_1024"` + Thumb1024W int `json:"thumb_1024_w"` + Thumb1024H int `json:"thumb_1024_h"` + + Permalink string `json:"permalink"` + PermalinkPublic string `json:"permalink_public"` + + EditLink string `json:"edit_link"` + Preview string `json:"preview"` + PreviewHighlight string `json:"preview_highlight"` + Lines int `json:"lines"` + LinesMore int `json:"lines_more"` + + IsPublic bool `json:"is_public"` + PublicURLShared bool `json:"public_url_shared"` + Channels []string `json:"channels"` + Groups []string `json:"groups"` + IMs []string `json:"ims"` + InitialComment Comment `json:"initial_comment"` + CommentsCount int `json:"comments_count"` + NumStars int `json:"num_stars"` + IsStarred bool `json:"is_starred"` +} + +// FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request +type FileUploadParameters struct { + File string + Content string + Filetype string + Filename string + Title string + InitialComment string + Channels []string +} + +// GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request +type GetFilesParameters struct { + User string + TimestampFrom JSONTime + TimestampTo JSONTime + Types string + Count int + Page int +} + +type fileResponseFull struct { + File `json:"file"` + Paging `json:"paging"` + Comments []Comment `json:"comments"` + Files []File `json:"files"` + + SlackResponse +} + +// NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set +func NewGetFilesParameters() GetFilesParameters { + return GetFilesParameters{ + User: DEFAULT_FILES_USER, + TimestampFrom: DEFAULT_FILES_TS_FROM, + TimestampTo: DEFAULT_FILES_TS_TO, + Types: DEFAULT_FILES_TYPES, + Count: DEFAULT_FILES_COUNT, + Page: DEFAULT_FILES_PAGE, + } +} + +func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) { + response := &fileResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetFileInfo retrieves a file and related comments +func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { + values := url.Values{ + "token": {api.config.token}, + "file": {fileID}, + "count": {strconv.Itoa(count)}, + "page": {strconv.Itoa(page)}, + } + response, err := fileRequest("files.info", values, api.debug) + if err != nil { + return nil, nil, nil, err + } + return &response.File, response.Comments, &response.Paging, nil +} + +// GetFiles retrieves all files according to the parameters given +func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { + values := url.Values{ + "token": {api.config.token}, + } + if params.User != DEFAULT_FILES_USER { + values.Add("user", params.User) + } + // XXX: this is broken. fix it with a proper unix timestamp + if params.TimestampFrom != DEFAULT_FILES_TS_FROM { + values.Add("ts_from", params.TimestampFrom.String()) + } + if params.TimestampTo != DEFAULT_FILES_TS_TO { + values.Add("ts_to", params.TimestampTo.String()) + } + if params.Types != DEFAULT_FILES_TYPES { + values.Add("types", params.Types) + } + if params.Count != DEFAULT_FILES_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_FILES_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + response, err := fileRequest("files.list", values, api.debug) + if err != nil { + return nil, nil, err + } + return response.Files, &response.Paging, nil +} + +// UploadFile uploads a file +func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { + // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More + // investigation needed, but for now this will do. + _, err = api.AuthTest() + if err != nil { + return nil, err + } + response := &fileResponseFull{} + values := url.Values{ + "token": {api.config.token}, + } + if params.Filetype != "" { + values.Add("filetype", params.Filetype) + } + if params.Filename != "" { + values.Add("filename", params.Filename) + } + if params.Title != "" { + values.Add("title", params.Title) + } + if params.InitialComment != "" { + values.Add("initial_comment", params.InitialComment) + } + if len(params.Channels) != 0 { + values.Add("channels", strings.Join(params.Channels, ",")) + } + if params.Content != "" { + values.Add("content", params.Content) + err = post("files.upload", values, response, api.debug) + } else if params.File != "" { + err = postWithMultipartResponse("files.upload", params.File, values, response, api.debug) + } + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return &response.File, nil +} + +// DeleteFile deletes a file +func (api *Client) DeleteFile(fileID string) error { + values := url.Values{ + "token": {api.config.token}, + "file": {fileID}, + } + _, err := fileRequest("files.delete", values, api.debug) + if err != nil { + return err + } + return nil + +} diff --git a/vendor/github.com/nlopes/slack/groups.go b/vendor/github.com/nlopes/slack/groups.go new file mode 100644 index 0000000..77fcbf8 --- /dev/null +++ b/vendor/github.com/nlopes/slack/groups.go @@ -0,0 +1,286 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +// Group contains all the information for a group +type Group struct { + groupConversation + IsGroup bool `json:"is_group"` +} + +type groupResponseFull struct { + Group Group `json:"group"` + Groups []Group `json:"groups"` + Purpose string `json:"purpose"` + Topic string `json:"topic"` + NotInGroup bool `json:"not_in_group"` + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + AlreadyInGroup bool `json:"already_in_group"` + Channel Channel `json:"channel"` + History + SlackResponse +} + +func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) { + response := &groupResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// ArchiveGroup archives a private group +func (api *Client) ArchiveGroup(group string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + _, err := groupRequest("groups.archive", values, api.debug) + if err != nil { + return err + } + return nil +} + +// UnarchiveGroup unarchives a private group +func (api *Client) UnarchiveGroup(group string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + _, err := groupRequest("groups.unarchive", values, api.debug) + if err != nil { + return err + } + return nil +} + +// CreateGroup creates a private group +func (api *Client) CreateGroup(group string) (*Group, error) { + values := url.Values{ + "token": {api.config.token}, + "name": {group}, + } + response, err := groupRequest("groups.create", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// CreateChildGroup creates a new private group archiving the old one +// This method takes an existing private group and performs the following steps: +// 1. Renames the existing group (from "example" to "example-archived"). +// 2. Archives the existing group. +// 3. Creates a new group with the name of the existing group. +// 4. Adds all members of the existing group to the new group. +func (api *Client) CreateChildGroup(group string) (*Group, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + response, err := groupRequest("groups.createChild", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// CloseGroup closes a private group +func (api *Client) CloseGroup(group string) (bool, bool, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + response, err := imRequest("groups.close", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyClosed, nil +} + +// GetGroupHistory fetches all the history for a private group +func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + response, err := groupRequest("groups.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// InviteUserToGroup invites a specific user to a private group +func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "user": {user}, + } + response, err := groupRequest("groups.invite", values, api.debug) + if err != nil { + return nil, false, err + } + return &response.Group, response.AlreadyInGroup, nil +} + +// LeaveGroup makes authenticated user leave the group +func (api *Client) LeaveGroup(group string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + _, err := groupRequest("groups.leave", values, api.debug) + if err != nil { + return err + } + return nil +} + +// KickUserFromGroup kicks a user from a group +func (api *Client) KickUserFromGroup(group, user string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "user": {user}, + } + _, err := groupRequest("groups.kick", values, api.debug) + if err != nil { + return err + } + return nil +} + +// GetGroups retrieves all groups +func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) { + values := url.Values{ + "token": {api.config.token}, + } + if excludeArchived { + values.Add("exclude_archived", "1") + } + response, err := groupRequest("groups.list", values, api.debug) + if err != nil { + return nil, err + } + return response.Groups, nil +} + +// GetGroupInfo retrieves the given group +func (api *Client) GetGroupInfo(group string) (*Group, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + response, err := groupRequest("groups.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.Group, nil +} + +// SetGroupReadMark sets the read mark on a private group +// Clients should try to avoid making this call too often. When needing to mark a read position, a client should set a +// timer before making the call. In this way, any further updates needed during the timeout will not generate extra +// calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live +// channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout. +func (api *Client) SetGroupReadMark(group, ts string) error { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "ts": {ts}, + } + _, err := groupRequest("groups.mark", values, api.debug) + if err != nil { + return err + } + return nil +} + +// OpenGroup opens a private group +func (api *Client) OpenGroup(group string) (bool, bool, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + } + response, err := groupRequest("groups.open", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyOpen, nil +} + +// RenameGroup renames a group +// XXX: They return a channel, not a group. What is this crap? :( +// Inconsistent api it seems. +func (api *Client) RenameGroup(group, name string) (*Channel, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "name": {name}, + } + // XXX: the created entry in this call returns a string instead of a number + // so I may have to do some workaround to solve it. + response, err := groupRequest("groups.rename", values, api.debug) + if err != nil { + return nil, err + } + return &response.Channel, nil + +} + +// SetGroupPurpose sets the group purpose +func (api *Client) SetGroupPurpose(group, purpose string) (string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "purpose": {purpose}, + } + response, err := groupRequest("groups.setPurpose", values, api.debug) + if err != nil { + return "", err + } + return response.Purpose, nil +} + +// SetGroupTopic sets the group topic +func (api *Client) SetGroupTopic(group, topic string) (string, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {group}, + "topic": {topic}, + } + response, err := groupRequest("groups.setTopic", values, api.debug) + if err != nil { + return "", err + } + return response.Topic, nil +} diff --git a/vendor/github.com/nlopes/slack/history.go b/vendor/github.com/nlopes/slack/history.go new file mode 100644 index 0000000..18467d0 --- /dev/null +++ b/vendor/github.com/nlopes/slack/history.go @@ -0,0 +1,33 @@ +package slack + +const ( + DEFAULT_HISTORY_LATEST = "" + DEFAULT_HISTORY_OLDEST = "0" + DEFAULT_HISTORY_COUNT = 100 + DEFAULT_HISTORY_INCLUSIVE = false +) + +// HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs +type HistoryParameters struct { + Latest string + Oldest string + Count int + Inclusive bool +} + +// History contains message history information needed to navigate a Channel / Group / DM history +type History struct { + Latest string `json:"latest"` + Messages []Message `json:"messages"` + HasMore bool `json:"has_more"` +} + +// NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set +func NewHistoryParameters() HistoryParameters { + return HistoryParameters{ + Latest: DEFAULT_HISTORY_LATEST, + Oldest: DEFAULT_HISTORY_OLDEST, + Count: DEFAULT_HISTORY_COUNT, + Inclusive: DEFAULT_HISTORY_INCLUSIVE, + } +} diff --git a/vendor/github.com/nlopes/slack/im.go b/vendor/github.com/nlopes/slack/im.go new file mode 100644 index 0000000..2533f0d --- /dev/null +++ b/vendor/github.com/nlopes/slack/im.go @@ -0,0 +1,123 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +type imChannel struct { + ID string `json:"id"` +} + +type imResponseFull struct { + NoOp bool `json:"no_op"` + AlreadyClosed bool `json:"already_closed"` + AlreadyOpen bool `json:"already_open"` + Channel imChannel `json:"channel"` + IMs []IM `json:"ims"` + History + SlackResponse +} + +// IM contains information related to the Direct Message channel +type IM struct { + conversation + IsIM bool `json:"is_im"` + User string `json:"user"` + IsUserDeleted bool `json:"is_user_deleted"` +} + +func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) { + response := &imResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// CloseIMChannel closes the direct message channel +func (api *Client) CloseIMChannel(channel string) (bool, bool, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + response, err := imRequest("im.close", values, api.debug) + if err != nil { + return false, false, err + } + return response.NoOp, response.AlreadyClosed, nil +} + +// OpenIMChannel opens a direct message channel to the user provided as argument +// Returns some status and the channel ID +func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) { + values := url.Values{ + "token": {api.config.token}, + "user": {user}, + } + response, err := imRequest("im.open", values, api.debug) + if err != nil { + return false, false, "", err + } + return response.NoOp, response.AlreadyOpen, response.Channel.ID, nil +} + +// MarkIMChannel sets the read mark of a direct message channel to a specific point +func (api *Client) MarkIMChannel(channel, ts string) (err error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + "ts": {ts}, + } + _, err = imRequest("im.mark", values, api.debug) + if err != nil { + return err + } + return +} + +// GetIMHistory retrieves the direct message channel history +func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) { + values := url.Values{ + "token": {api.config.token}, + "channel": {channel}, + } + if params.Latest != DEFAULT_HISTORY_LATEST { + values.Add("latest", params.Latest) + } + if params.Oldest != DEFAULT_HISTORY_OLDEST { + values.Add("oldest", params.Oldest) + } + if params.Count != DEFAULT_HISTORY_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Inclusive != DEFAULT_HISTORY_INCLUSIVE { + if params.Inclusive { + values.Add("inclusive", "1") + } else { + values.Add("inclusive", "0") + } + } + response, err := imRequest("im.history", values, api.debug) + if err != nil { + return nil, err + } + return &response.History, nil +} + +// GetIMChannels returns the list of direct message channels +func (api *Client) GetIMChannels() ([]IM, error) { + values := url.Values{ + "token": {api.config.token}, + } + response, err := imRequest("im.list", values, api.debug) + if err != nil { + return nil, err + } + return response.IMs, nil +} diff --git a/vendor/github.com/nlopes/slack/info.go b/vendor/github.com/nlopes/slack/info.go new file mode 100644 index 0000000..5f0e541 --- /dev/null +++ b/vendor/github.com/nlopes/slack/info.go @@ -0,0 +1,206 @@ +package slack + +import ( + "fmt" + "time" +) + +// UserPrefs needs to be implemented +type UserPrefs struct { + // "highlight_words":"", + // "user_colors":"", + // "color_names_in_list":true, + // "growls_enabled":true, + // "tz":"Europe\/London", + // "push_dm_alert":true, + // "push_mention_alert":true, + // "push_everything":true, + // "push_idle_wait":2, + // "push_sound":"b2.mp3", + // "push_loud_channels":"", + // "push_mention_channels":"", + // "push_loud_channels_set":"", + // "email_alerts":"instant", + // "email_alerts_sleep_until":0, + // "email_misc":false, + // "email_weekly":true, + // "welcome_message_hidden":false, + // "all_channels_loud":true, + // "loud_channels":"", + // "never_channels":"", + // "loud_channels_set":"", + // "show_member_presence":true, + // "search_sort":"timestamp", + // "expand_inline_imgs":true, + // "expand_internal_inline_imgs":true, + // "expand_snippets":false, + // "posts_formatting_guide":true, + // "seen_welcome_2":true, + // "seen_ssb_prompt":false, + // "search_only_my_channels":false, + // "emoji_mode":"default", + // "has_invited":true, + // "has_uploaded":false, + // "has_created_channel":true, + // "search_exclude_channels":"", + // "messages_theme":"default", + // "webapp_spellcheck":true, + // "no_joined_overlays":false, + // "no_created_overlays":true, + // "dropbox_enabled":false, + // "seen_user_menu_tip_card":true, + // "seen_team_menu_tip_card":true, + // "seen_channel_menu_tip_card":true, + // "seen_message_input_tip_card":true, + // "seen_channels_tip_card":true, + // "seen_domain_invite_reminder":false, + // "seen_member_invite_reminder":false, + // "seen_flexpane_tip_card":true, + // "seen_search_input_tip_card":true, + // "mute_sounds":false, + // "arrow_history":false, + // "tab_ui_return_selects":true, + // "obey_inline_img_limit":true, + // "new_msg_snd":"knock_brush.mp3", + // "collapsible":false, + // "collapsible_by_click":true, + // "require_at":false, + // "mac_ssb_bounce":"", + // "mac_ssb_bullet":true, + // "win_ssb_bullet":true, + // "expand_non_media_attachments":true, + // "show_typing":true, + // "pagekeys_handled":true, + // "last_snippet_type":"", + // "display_real_names_override":0, + // "time24":false, + // "enter_is_special_in_tbt":false, + // "graphic_emoticons":false, + // "convert_emoticons":true, + // "autoplay_chat_sounds":true, + // "ss_emojis":true, + // "sidebar_behavior":"", + // "mark_msgs_read_immediately":true, + // "start_scroll_at_oldest":true, + // "snippet_editor_wrap_long_lines":false, + // "ls_disabled":false, + // "sidebar_theme":"default", + // "sidebar_theme_custom_values":"", + // "f_key_search":false, + // "k_key_omnibox":true, + // "speak_growls":false, + // "mac_speak_voice":"com.apple.speech.synthesis.voice.Alex", + // "mac_speak_speed":250, + // "comma_key_prefs":false, + // "at_channel_suppressed_channels":"", + // "push_at_channel_suppressed_channels":"", + // "prompted_for_email_disabling":false, + // "full_text_extracts":false, + // "no_text_in_notifications":false, + // "muted_channels":"", + // "no_macssb1_banner":false, + // "privacy_policy_seen":true, + // "search_exclude_bots":false, + // "fuzzy_matching":false +} + +// UserDetails contains user details coming in the initial response from StartRTM +type UserDetails struct { + ID string `json:"id"` + Name string `json:"name"` + Created JSONTime `json:"created"` + ManualPresence string `json:"manual_presence"` + Prefs UserPrefs `json:"prefs"` +} + +// JSONTime exists so that we can have a String method converting the date +type JSONTime int64 + +// String converts the unix timestamp into a string +func (t JSONTime) String() string { + tm := t.Time() + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +// Time returns a `time.Time` representation of this value. +func (t JSONTime) Time() time.Time { + return time.Unix(int64(t), 0) +} + +// Team contains details about a team +type Team struct { + ID string `json:"id"` + Name string `json:"name"` + Domain string `json:"domain"` +} + +// Icons XXX: needs further investigation +type Icons struct { + Image48 string `json:"image_48"` +} + +// Bot contains information about a bot +type Bot struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Icons Icons `json:"icons"` +} + +// Info contains various details about Users, Channels, Bots and the authenticated user. +// It is returned by StartRTM or included in the "ConnectedEvent" RTM event. +type Info struct { + URL string `json:"url,omitempty"` + User *UserDetails `json:"self,omitempty"` + Team *Team `json:"team,omitempty"` + Users []User `json:"users,omitempty"` + Channels []Channel `json:"channels,omitempty"` + Groups []Group `json:"groups,omitempty"` + Bots []Bot `json:"bots,omitempty"` + IMs []IM `json:"ims,omitempty"` +} + +type infoResponseFull struct { + Info + WebResponse +} + +// GetBotByID returns a bot given a bot id +func (info Info) GetBotByID(botID string) *Bot { + for _, bot := range info.Bots { + if bot.ID == botID { + return &bot + } + } + return nil +} + +// GetUserByID returns a user given a user id +func (info Info) GetUserByID(userID string) *User { + for _, user := range info.Users { + if user.ID == userID { + return &user + } + } + return nil +} + +// GetChannelByID returns a channel given a channel id +func (info Info) GetChannelByID(channelID string) *Channel { + for _, channel := range info.Channels { + if channel.ID == channelID { + return &channel + } + } + return nil +} + +// GetGroupByID returns a group given a group id +func (info Info) GetGroupByID(groupID string) *Group { + for _, group := range info.Groups { + if group.ID == groupID { + return &group + } + } + return nil +} diff --git a/vendor/github.com/nlopes/slack/item.go b/vendor/github.com/nlopes/slack/item.go new file mode 100644 index 0000000..89af4eb --- /dev/null +++ b/vendor/github.com/nlopes/slack/item.go @@ -0,0 +1,75 @@ +package slack + +const ( + TYPE_MESSAGE = "message" + TYPE_FILE = "file" + TYPE_FILE_COMMENT = "file_comment" + TYPE_CHANNEL = "channel" + TYPE_IM = "im" + TYPE_GROUP = "group" +) + +// Item is any type of slack message - message, file, or file comment. +type Item struct { + Type string `json:"type"` + Channel string `json:"channel,omitempty"` + Message *Message `json:"message,omitempty"` + File *File `json:"file,omitempty"` + Comment *Comment `json:"comment,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// NewMessageItem turns a message on a channel into a typed message struct. +func NewMessageItem(ch string, m *Message) Item { + return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} +} + +// NewFileItem turns a file into a typed file struct. +func NewFileItem(f *File) Item { + return Item{Type: TYPE_FILE, File: f} +} + +// NewFileCommentItem turns a file and comment into a typed file_comment struct. +func NewFileCommentItem(f *File, c *Comment) Item { + return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} +} + +// NewChannelItem turns a channel id into a typed channel struct. +func NewChannelItem(ch string) Item { + return Item{Type: TYPE_CHANNEL, Channel: ch} +} + +// NewIMItem turns a channel id into a typed im struct. +func NewIMItem(ch string) Item { + return Item{Type: TYPE_IM, Channel: ch} +} + +// NewGroupItem turns a channel id into a typed group struct. +func NewGroupItem(ch string) Item { + return Item{Type: TYPE_GROUP, Channel: ch} +} + +// ItemRef is a reference to a message of any type. One of FileID, +// CommentId, or the combination of ChannelId and Timestamp must be +// specified. +type ItemRef struct { + Channel string `json:"channel"` + Timestamp string `json:"timestamp"` + File string `json:"file"` + Comment string `json:"file_comment"` +} + +// NewRefToMessage initializes a reference to to a message. +func NewRefToMessage(channel, timestamp string) ItemRef { + return ItemRef{Channel: channel, Timestamp: timestamp} +} + +// NewRefToFile initializes a reference to a file. +func NewRefToFile(file string) ItemRef { + return ItemRef{File: file} +} + +// NewRefToComment initializes a reference to a file comment. +func NewRefToComment(comment string) ItemRef { + return ItemRef{Comment: comment} +} diff --git a/vendor/github.com/nlopes/slack/messageID.go b/vendor/github.com/nlopes/slack/messageID.go new file mode 100644 index 0000000..a17472b --- /dev/null +++ b/vendor/github.com/nlopes/slack/messageID.go @@ -0,0 +1,30 @@ +package slack + +import "sync" + +// IDGenerator provides an interface for generating integer ID values. +type IDGenerator interface { + Next() int +} + +// NewSafeID returns a new instance of an IDGenerator which is safe for +// concurrent use by multiple goroutines. +func NewSafeID(startID int) IDGenerator { + return &safeID{ + nextID: startID, + mutex: &sync.Mutex{}, + } +} + +type safeID struct { + nextID int + mutex *sync.Mutex +} + +func (s *safeID) Next() int { + s.mutex.Lock() + defer s.mutex.Unlock() + id := s.nextID + s.nextID++ + return id +} diff --git a/vendor/github.com/nlopes/slack/messages.go b/vendor/github.com/nlopes/slack/messages.go new file mode 100644 index 0000000..effcf22 --- /dev/null +++ b/vendor/github.com/nlopes/slack/messages.go @@ -0,0 +1,128 @@ +package slack + +// OutgoingMessage is used for the realtime API, and seems incomplete. +type OutgoingMessage struct { + ID int `json:"id"` + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + Type string `json:"type,omitempty"` +} + +// Message is an auxiliary type to allow us to have a message containing sub messages +type Message struct { + Msg + SubMessage *Msg `json:"message,omitempty"` +} + +// Msg contains information about a slack message +type Msg struct { + // Basic Message + Type string `json:"type,omitempty"` + Channel string `json:"channel,omitempty"` + User string `json:"user,omitempty"` + Text string `json:"text,omitempty"` + Timestamp string `json:"ts,omitempty"` + IsStarred bool `json:"is_starred,omitempty"` + PinnedTo []string `json:"pinned_to, omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` + Edited *Edited `json:"edited,omitempty"` + + // Message Subtypes + SubType string `json:"subtype,omitempty"` + + // Hidden Subtypes + Hidden bool `json:"hidden,omitempty"` // message_changed, message_deleted, unpinned_item + DeletedTimestamp string `json:"deleted_ts,omitempty"` // message_deleted + EventTimestamp string `json:"event_ts,omitempty"` + + // bot_message (https://api.slack.com/events/message/bot_message) + BotID string `json:"bot_id,omitempty"` + Username string `json:"username,omitempty"` + Icons *Icon `json:"icons,omitempty"` + + // channel_join, group_join + Inviter string `json:"inviter,omitempty"` + + // channel_topic, group_topic + Topic string `json:"topic,omitempty"` + + // channel_purpose, group_purpose + Purpose string `json:"purpose,omitempty"` + + // channel_name, group_name + Name string `json:"name,omitempty"` + OldName string `json:"old_name,omitempty"` + + // channel_archive, group_archive + Members []string `json:"members,omitempty"` + + // file_share, file_comment, file_mention + File *File `json:"file,omitempty"` + + // file_share + Upload bool `json:"upload,omitempty"` + + // file_comment + Comment *Comment `json:"comment,omitempty"` + + // pinned_item + ItemType string `json:"item_type,omitempty"` + + // https://api.slack.com/rtm + ReplyTo int `json:"reply_to,omitempty"` + Team string `json:"team,omitempty"` +} + +// Icon is used for bot messages +type Icon struct { + IconURL string `json:"icon_url,omitempty"` + IconEmoji string `json:"icon_emoji,omitempty"` +} + +// Edited indicates that a message has been edited. +type Edited struct { + User string `json:"user,omitempty"` + Timestamp string `json:"ts,omitempty"` +} + +// Event contains the event type +type Event struct { + Type string `json:"type,omitempty"` +} + +// Ping contains information about a Ping Event +type Ping struct { + ID int `json:"id"` + Type string `json:"type"` +} + +// Pong contains information about a Pong Event +type Pong struct { + Type string `json:"type"` + ReplyTo int `json:"reply_to"` +} + +// NewOutgoingMessage prepares an OutgoingMessage that the user can +// use to send a message. Use this function to properly set the +// messageID. +func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage { + id := rtm.idGen.Next() + return &OutgoingMessage{ + ID: id, + Type: "message", + Channel: channel, + Text: text, + } +} + +// NewTypingMessage prepares an OutgoingMessage that the user can +// use to send as a typing indicator. Use this function to properly set the +// messageID. +func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage { + id := rtm.idGen.Next() + return &OutgoingMessage{ + ID: id, + Type: "typing", + Channel: channel, + } +} diff --git a/vendor/github.com/nlopes/slack/misc.go b/vendor/github.com/nlopes/slack/misc.go new file mode 100644 index 0000000..03ef9dc --- /dev/null +++ b/vendor/github.com/nlopes/slack/misc.go @@ -0,0 +1,119 @@ +package slack + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "time" +) + +type WebResponse struct { + Ok bool `json:"ok"` + Error *WebError `json:"error"` +} + +type WebError string + +func (s WebError) Error() string { + return string(s) +} + +func fileUploadReq(path, fpath string, values url.Values) (*http.Request, error) { + fullpath, err := filepath.Abs(fpath) + if err != nil { + return nil, err + } + file, err := os.Open(fullpath) + if err != nil { + return nil, err + } + defer file.Close() + + body := &bytes.Buffer{} + wr := multipart.NewWriter(body) + + ioWriter, err := wr.CreateFormFile("file", filepath.Base(fullpath)) + if err != nil { + wr.Close() + return nil, err + } + bytes, err := io.Copy(ioWriter, file) + if err != nil { + wr.Close() + return nil, err + } + // Close the multipart writer or the footer won't be written + wr.Close() + stat, err := file.Stat() + if err != nil { + return nil, err + } + if bytes != stat.Size() { + return nil, errors.New("could not read the whole file") + } + req, err := http.NewRequest("POST", path, body) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", wr.FormDataContentType()) + req.URL.RawQuery = (values).Encode() + return req, nil +} + +func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error { + response, err := ioutil.ReadAll(body) + if err != nil { + return err + } + + // FIXME: will be api.Debugf + if debug { + log.Printf("parseResponseBody: %s\n", string(response)) + } + + err = json.Unmarshal(response, &intf) + if err != nil { + return err + } + + return nil +} + +func postWithMultipartResponse(path string, filepath string, values url.Values, intf interface{}, debug bool) error { + req, err := fileUploadReq(SLACK_API+path, filepath, values) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + return parseResponseBody(resp.Body, &intf, debug) +} + +func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error { + resp, err := http.PostForm(endpoint, values) + if err != nil { + return err + } + defer resp.Body.Close() + + return parseResponseBody(resp.Body, &intf, debug) +} + +func post(path string, values url.Values, intf interface{}, debug bool) error { + return postForm(SLACK_API+path, values, intf, debug) +} + +func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error { + endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix()) + return postForm(endpoint, values, intf, debug) +} diff --git a/vendor/github.com/nlopes/slack/oauth.go b/vendor/github.com/nlopes/slack/oauth.go new file mode 100644 index 0000000..33c3ed2 --- /dev/null +++ b/vendor/github.com/nlopes/slack/oauth.go @@ -0,0 +1,54 @@ +package slack + +import ( + "errors" + "net/url" +) + +type OAuthResponseIncomingWebhook struct { + URL string `json:"url"` + Channel string `json:"channel"` + ConfigurationURL string `json:"configuration_url"` +} + +type OAuthResponseBot struct { + BotUserID string `json:"bot_user_id"` + BotAccessToken string `json:"bot_access_token"` +} + +type OAuthResponse struct { + AccessToken string `json:"access_token"` + Scope string `json:"scope"` + TeamName string `json:"team_name"` + TeamID string `json:"team_id"` + IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"` + Bot OAuthResponseBot `json:"bot"` + SlackResponse +} + +// GetOAuthToken retrieves an AccessToken +func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) { + response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug) + if err != nil { + return "", "", err + } + return response.AccessToken, response.Scope, nil +} + +func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) { + values := url.Values{ + "client_id": {clientID}, + "client_secret": {clientSecret}, + "code": {code}, + "redirect_uri": {redirectURI}, + } + response := &OAuthResponse{} + err = post("oauth.access", values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} diff --git a/vendor/github.com/nlopes/slack/pagination.go b/vendor/github.com/nlopes/slack/pagination.go new file mode 100644 index 0000000..87dd136 --- /dev/null +++ b/vendor/github.com/nlopes/slack/pagination.go @@ -0,0 +1,20 @@ +package slack + +// Paging contains paging information +type Paging struct { + Count int `json:"count"` + Total int `json:"total"` + Page int `json:"page"` + Pages int `json:"pages"` +} + +// Pagination contains pagination information +// This is different from Paging in that it contains additional details +type Pagination struct { + TotalCount int `json:"total_count"` + Page int `json:"page"` + PerPage int `json:"per_page"` + PageCount int `json:"page_count"` + First int `json:"first"` + Last int `json:"last"` +} diff --git a/vendor/github.com/nlopes/slack/pins.go b/vendor/github.com/nlopes/slack/pins.go new file mode 100644 index 0000000..b95efbb --- /dev/null +++ b/vendor/github.com/nlopes/slack/pins.go @@ -0,0 +1,79 @@ +package slack + +import ( + "errors" + "net/url" +) + +type listPinsResponseFull struct { + Items []Item + Paging `json:"paging"` + SlackResponse +} + +// AddPin pins an item in a channel +func (api *Client) AddPin(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("pins.add", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// RemovePin un-pins an item from a channel +func (api *Client) RemovePin(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("pins.remove", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// ListPins returns information about the items a user reacted to. +func (api *Client) ListPins(channel string) ([]Item, *Paging, error) { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + response := &listPinsResponseFull{} + err := post("pins.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.Items, &response.Paging, nil +} diff --git a/vendor/github.com/nlopes/slack/reactions.go b/vendor/github.com/nlopes/slack/reactions.go new file mode 100644 index 0000000..d68fb4a --- /dev/null +++ b/vendor/github.com/nlopes/slack/reactions.go @@ -0,0 +1,247 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +// ItemReaction is the reactions that have happened on an item. +type ItemReaction struct { + Name string `json:"name"` + Count int `json:"count"` + Users []string `json:"users"` +} + +// ReactedItem is an item that was reacted to, and the details of the +// reactions. +type ReactedItem struct { + Item + Reactions []ItemReaction +} + +// GetReactionsParameters is the inputs to get reactions to an item. +type GetReactionsParameters struct { + Full bool +} + +// NewGetReactionsParameters initializes the inputs to get reactions to an item. +func NewGetReactionsParameters() GetReactionsParameters { + return GetReactionsParameters{ + Full: false, + } +} + +type getReactionsResponseFull struct { + Type string + M struct { + Reactions []ItemReaction + } `json:"message"` + F struct { + Reactions []ItemReaction + } `json:"file"` + FC struct { + Reactions []ItemReaction + } `json:"comment"` + SlackResponse +} + +func (res getReactionsResponseFull) extractReactions() []ItemReaction { + switch res.Type { + case "message": + return res.M.Reactions + case "file": + return res.F.Reactions + case "file_comment": + return res.FC.Reactions + } + return []ItemReaction{} +} + +const ( + DEFAULT_REACTIONS_USER = "" + DEFAULT_REACTIONS_COUNT = 100 + DEFAULT_REACTIONS_PAGE = 1 + DEFAULT_REACTIONS_FULL = false +) + +// ListReactionsParameters is the inputs to find all reactions by a user. +type ListReactionsParameters struct { + User string + Count int + Page int + Full bool +} + +// NewListReactionsParameters initializes the inputs to find all reactions +// performed by a user. +func NewListReactionsParameters() ListReactionsParameters { + return ListReactionsParameters{ + User: DEFAULT_REACTIONS_USER, + Count: DEFAULT_REACTIONS_COUNT, + Page: DEFAULT_REACTIONS_PAGE, + Full: DEFAULT_REACTIONS_FULL, + } +} + +type listReactionsResponseFull struct { + Items []struct { + Type string + Channel string + M struct { + *Message + Reactions []ItemReaction + } `json:"message"` + F struct { + *File + Reactions []ItemReaction + } `json:"file"` + FC struct { + *Comment + Reactions []ItemReaction + } `json:"comment"` + } + Paging `json:"paging"` + SlackResponse +} + +func (res listReactionsResponseFull) extractReactedItems() []ReactedItem { + items := make([]ReactedItem, len(res.Items)) + for i, input := range res.Items { + item := ReactedItem{} + item.Type = input.Type + switch input.Type { + case "message": + item.Channel = input.Channel + item.Message = input.M.Message + item.Reactions = input.M.Reactions + case "file": + item.File = input.F.File + item.Reactions = input.F.Reactions + case "file_comment": + item.File = input.F.File + item.Comment = input.FC.Comment + item.Reactions = input.FC.Reactions + } + items[i] = item + } + return items +} + +// AddReaction adds a reaction emoji to a message, file or file comment. +func (api *Client) AddReaction(name string, item ItemRef) error { + values := url.Values{ + "token": {api.config.token}, + } + if name != "" { + values.Set("name", name) + } + if item.Channel != "" { + values.Set("channel", string(item.Channel)) + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("reactions.add", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// RemoveReaction removes a reaction emoji from a message, file or file comment. +func (api *Client) RemoveReaction(name string, item ItemRef) error { + values := url.Values{ + "token": {api.config.token}, + } + if name != "" { + values.Set("name", name) + } + if item.Channel != "" { + values.Set("channel", string(item.Channel)) + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("reactions.remove", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// GetReactions returns details about the reactions on an item. +func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) { + values := url.Values{ + "token": {api.config.token}, + } + if item.Channel != "" { + values.Set("channel", string(item.Channel)) + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + if params.Full != DEFAULT_REACTIONS_FULL { + values.Set("full", strconv.FormatBool(params.Full)) + } + response := &getReactionsResponseFull{} + if err := post("reactions.get", values, response, api.debug); err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response.extractReactions(), nil +} + +// ListReactions returns information about the items a user reacted to. +func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) { + values := url.Values{ + "token": {api.config.token}, + } + if params.User != DEFAULT_REACTIONS_USER { + values.Add("user", params.User) + } + if params.Count != DEFAULT_REACTIONS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_REACTIONS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + if params.Full != DEFAULT_REACTIONS_FULL { + values.Add("full", strconv.FormatBool(params.Full)) + } + response := &listReactionsResponseFull{} + err := post("reactions.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.extractReactedItems(), &response.Paging, nil +} diff --git a/vendor/github.com/nlopes/slack/rtm.go b/vendor/github.com/nlopes/slack/rtm.go new file mode 100644 index 0000000..f3552c5 --- /dev/null +++ b/vendor/github.com/nlopes/slack/rtm.go @@ -0,0 +1,39 @@ +package slack + +import ( + "fmt" + "net/url" +) + +// StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info +// block. +// +// To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` +// on it. +func (api *Client) StartRTM() (info *Info, websocketURL string, err error) { + response := &infoResponseFull{} + err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug) + if err != nil { + return nil, "", fmt.Errorf("post: %s", err) + } + if !response.Ok { + return nil, "", response.Error + } + + // websocket.Dial does not accept url without the port (yet) + // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3 + // but slack returns the address with no port, so we have to fix it + api.Debugln("Using URL:", response.Info.URL) + websocketURL, err = websocketizeURLPort(response.Info.URL) + if err != nil { + return nil, "", fmt.Errorf("parsing response URL: %s", err) + } + + return &response.Info, websocketURL, nil +} + +// NewRTM returns a RTM, which provides a fully managed connection to +// Slack's websocket-based Real-Time Messaging protocol./ +func (api *Client) NewRTM() *RTM { + return newRTM(api) +} diff --git a/vendor/github.com/nlopes/slack/search.go b/vendor/github.com/nlopes/slack/search.go new file mode 100644 index 0000000..ab3c5da --- /dev/null +++ b/vendor/github.com/nlopes/slack/search.go @@ -0,0 +1,137 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_SEARCH_SORT = "score" + DEFAULT_SEARCH_SORT_DIR = "desc" + DEFAULT_SEARCH_HIGHLIGHT = false + DEFAULT_SEARCH_COUNT = 100 + DEFAULT_SEARCH_PAGE = 1 +) + +type SearchParameters struct { + Sort string + SortDirection string + Highlight bool + Count int + Page int +} + +type CtxChannel struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type CtxMessage struct { + User string `json:"user"` + Username string `json:"username"` + Text string `json:"text"` + Timestamp string `json:"ts"` + Type string `json:"type"` +} + +type SearchMessage struct { + Type string `json:"type"` + Channel CtxChannel `json:"channel"` + User string `json:"user"` + Username string `json:"username"` + Timestamp string `json:"ts"` + Text string `json:"text"` + Permalink string `json:"permalink"` + Previous CtxMessage `json:"previous"` + Previous2 CtxMessage `json:"previous_2"` + Next CtxMessage `json:"next"` + Next2 CtxMessage `json:"next_2"` +} + +type SearchMessages struct { + Matches []SearchMessage `json:"matches"` + Paging `json:"paging"` + Pagination `json:"pagination"` + Total int `json:"total"` +} + +type SearchFiles struct { + Matches []File `json:"matches"` + Paging `json:"paging"` + Pagination `json:"pagination"` + Total int `json:"total"` +} + +type searchResponseFull struct { + Query string `json:"query"` + SearchMessages `json:"messages"` + SearchFiles `json:"files"` + SlackResponse +} + +func NewSearchParameters() SearchParameters { + return SearchParameters{ + Sort: DEFAULT_SEARCH_SORT, + SortDirection: DEFAULT_SEARCH_SORT_DIR, + Highlight: DEFAULT_SEARCH_HIGHLIGHT, + Count: DEFAULT_SEARCH_COUNT, + Page: DEFAULT_SEARCH_PAGE, + } +} + +func (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) { + values := url.Values{ + "token": {api.config.token}, + "query": {query}, + } + if params.Sort != DEFAULT_SEARCH_SORT { + values.Add("sort", params.Sort) + } + if params.SortDirection != DEFAULT_SEARCH_SORT_DIR { + values.Add("sort_dir", params.SortDirection) + } + if params.Highlight != DEFAULT_SEARCH_HIGHLIGHT { + values.Add("highlight", strconv.Itoa(1)) + } + if params.Count != DEFAULT_SEARCH_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_SEARCH_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + response = &searchResponseFull{} + err := post(path, values, response, api.debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil + +} + +func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) { + response, err := api._search("search.all", query, params, true, true) + if err != nil { + return nil, nil, err + } + return &response.SearchMessages, &response.SearchFiles, nil +} + +func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) { + response, err := api._search("search.files", query, params, true, false) + if err != nil { + return nil, err + } + return &response.SearchFiles, nil +} + +func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) { + response, err := api._search("search.messages", query, params, false, true) + if err != nil { + return nil, err + } + return &response.SearchMessages, nil +} diff --git a/vendor/github.com/nlopes/slack/slack.go b/vendor/github.com/nlopes/slack/slack.go new file mode 100644 index 0000000..cc8f000 --- /dev/null +++ b/vendor/github.com/nlopes/slack/slack.go @@ -0,0 +1,77 @@ +package slack + +import ( + "errors" + "log" + "net/url" +) + +/* + Added as a var so that we can change this for testing purposes +*/ +var SLACK_API string = "https://slack.com/api/" +var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s" + +type SlackResponse struct { + Ok bool `json:"ok"` + Error string `json:"error"` +} + +type AuthTestResponse struct { + URL string `json:"url"` + Team string `json:"team"` + User string `json:"user"` + TeamID string `json:"team_id"` + UserID string `json:"user_id"` +} + +type authTestResponseFull struct { + SlackResponse + AuthTestResponse +} + +type Client struct { + config struct { + token string + } + info Info + debug bool +} + +func New(token string) *Client { + s := &Client{} + s.config.token = token + return s +} + +// AuthTest tests if the user is able to do authenticated requests or not +func (api *Client) AuthTest() (response *AuthTestResponse, error error) { + responseFull := &authTestResponseFull{} + err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug) + if err != nil { + return nil, err + } + if !responseFull.Ok { + return nil, errors.New(responseFull.Error) + } + return &responseFull.AuthTestResponse, nil +} + +// SetDebug switches the api into debug mode +// When in debug mode, it logs various info about what its doing +// If you ever use this in production, don't call SetDebug(true) +func (api *Client) SetDebug(debug bool) { + api.debug = debug +} + +func (api *Client) Debugf(format string, v ...interface{}) { + if api.debug { + log.Printf(format, v...) + } +} + +func (api *Client) Debugln(v ...interface{}) { + if api.debug { + log.Println(v...) + } +} diff --git a/vendor/github.com/nlopes/slack/stars.go b/vendor/github.com/nlopes/slack/stars.go new file mode 100644 index 0000000..cc12e6e --- /dev/null +++ b/vendor/github.com/nlopes/slack/stars.go @@ -0,0 +1,135 @@ +package slack + +import ( + "errors" + "net/url" + "strconv" +) + +const ( + DEFAULT_STARS_USER = "" + DEFAULT_STARS_COUNT = 100 + DEFAULT_STARS_PAGE = 1 +) + +type StarsParameters struct { + User string + Count int + Page int +} + +type StarredItem Item + +type listResponseFull struct { + Items []Item `json:"items"` + Paging `json:"paging"` + SlackResponse +} + +// NewStarsParameters initialises StarsParameters with default values +func NewStarsParameters() StarsParameters { + return StarsParameters{ + User: DEFAULT_STARS_USER, + Count: DEFAULT_STARS_COUNT, + Page: DEFAULT_STARS_PAGE, + } +} + +// AddStar stars an item in a channel +func (api *Client) AddStar(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("stars.add", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// RemoveStar removes a starred item from a channel +func (api *Client) RemoveStar(channel string, item ItemRef) error { + values := url.Values{ + "channel": {channel}, + "token": {api.config.token}, + } + if item.Timestamp != "" { + values.Set("timestamp", string(item.Timestamp)) + } + if item.File != "" { + values.Set("file", string(item.File)) + } + if item.Comment != "" { + values.Set("file_comment", string(item.Comment)) + } + response := &SlackResponse{} + if err := post("stars.remove", values, response, api.debug); err != nil { + return err + } + if !response.Ok { + return errors.New(response.Error) + } + return nil +} + +// ListStars returns information about the stars a user added +func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) { + values := url.Values{ + "token": {api.config.token}, + } + if params.User != DEFAULT_STARS_USER { + values.Add("user", params.User) + } + if params.Count != DEFAULT_STARS_COUNT { + values.Add("count", strconv.Itoa(params.Count)) + } + if params.Page != DEFAULT_STARS_PAGE { + values.Add("page", strconv.Itoa(params.Page)) + } + response := &listResponseFull{} + err := post("stars.list", values, response, api.debug) + if err != nil { + return nil, nil, err + } + if !response.Ok { + return nil, nil, errors.New(response.Error) + } + return response.Items, &response.Paging, nil +} + +// GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should +// be looking at according to what is in the Type. +// for _, item := range items { +// switch c.Type { +// case "file_comment": +// log.Println(c.Comment) +// case "file": +// ... +// +// } +// This function still exists to maintain backwards compatibility. +// I exposed it as returning []StarredItem, so it shall stay as StarredItem +func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) { + items, paging, err := api.ListStars(params) + if err != nil { + return nil, nil, err + } + starredItems := make([]StarredItem, len(items)) + for i, item := range items { + starredItems[i] = StarredItem(item) + } + return starredItems, paging, nil +} diff --git a/vendor/github.com/nlopes/slack/users.go b/vendor/github.com/nlopes/slack/users.go new file mode 100644 index 0000000..63691d6 --- /dev/null +++ b/vendor/github.com/nlopes/slack/users.go @@ -0,0 +1,139 @@ +package slack + +import ( + "errors" + "net/url" +) + +// UserProfile contains all the information details of a given user +type UserProfile struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + RealName string `json:"real_name"` + RealNameNormalized string `json:"real_name_normalized"` + Email string `json:"email"` + Skype string `json:"skype"` + Phone string `json:"phone"` + Image24 string `json:"image_24"` + Image32 string `json:"image_32"` + Image48 string `json:"image_48"` + Image72 string `json:"image_72"` + Image192 string `json:"image_192"` + ImageOriginal string `json:"image_original"` + Title string `json:"title"` +} + +// User contains all the information of a user +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Deleted bool `json:"deleted"` + Color string `json:"color"` + RealName string `json:"real_name"` + TZ string `json:"tz,omitempty"` + TZLabel string `json:"tz_label"` + TZOffset int `json:"tz_offset"` + Profile UserProfile `json:"profile"` + IsBot bool `json:"is_bot"` + IsAdmin bool `json:"is_admin"` + IsOwner bool `json:"is_owner"` + IsPrimaryOwner bool `json:"is_primary_owner"` + IsRestricted bool `json:"is_restricted"` + IsUltraRestricted bool `json:"is_ultra_restricted"` + Has2FA bool `json:"has_2fa"` + HasFiles bool `json:"has_files"` + Presence string `json:"presence"` +} + +// UserPresence contains details about a user online status +type UserPresence struct { + Presence string `json:"presence,omitempty"` + Online bool `json:"online,omitempty"` + AutoAway bool `json:"auto_away,omitempty"` + ManualAway bool `json:"manual_away,omitempty"` + ConnectionCount int `json:"connection_count,omitempty"` + LastActivity JSONTime `json:"last_activity,omitempty"` +} + +type userResponseFull struct { + Members []User `json:"members,omitempty"` // ListUsers + User `json:"user,omitempty"` // GetUserInfo + UserPresence // GetUserPresence + SlackResponse +} + +func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) { + response := &userResponseFull{} + err := post(path, values, response, debug) + if err != nil { + return nil, err + } + if !response.Ok { + return nil, errors.New(response.Error) + } + return response, nil +} + +// GetUserPresence will retrieve the current presence status of given user. +func (api *Client) GetUserPresence(user string) (*UserPresence, error) { + values := url.Values{ + "token": {api.config.token}, + "user": {user}, + } + response, err := userRequest("users.getPresence", values, api.debug) + if err != nil { + return nil, err + } + return &response.UserPresence, nil +} + +// GetUserInfo will retrive the complete user information +func (api *Client) GetUserInfo(user string) (*User, error) { + values := url.Values{ + "token": {api.config.token}, + "user": {user}, + } + response, err := userRequest("users.info", values, api.debug) + if err != nil { + return nil, err + } + return &response.User, nil +} + +// GetUsers returns the list of users (with their detailed information) +func (api *Client) GetUsers() ([]User, error) { + values := url.Values{ + "token": {api.config.token}, + } + response, err := userRequest("users.list", values, api.debug) + if err != nil { + return nil, err + } + return response.Members, nil +} + +// SetUserAsActive marks the currently authenticated user as active +func (api *Client) SetUserAsActive() error { + values := url.Values{ + "token": {api.config.token}, + } + _, err := userRequest("users.setActive", values, api.debug) + if err != nil { + return err + } + return nil +} + +// SetUserPresence changes the currently authenticated user presence +func (api *Client) SetUserPresence(presence string) error { + values := url.Values{ + "token": {api.config.token}, + "presence": {presence}, + } + _, err := userRequest("users.setPresence", values, api.debug) + if err != nil { + return err + } + return nil + +} diff --git a/vendor/github.com/nlopes/slack/websocket.go b/vendor/github.com/nlopes/slack/websocket.go new file mode 100644 index 0000000..36d9695 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket.go @@ -0,0 +1,94 @@ +package slack + +import ( + "encoding/json" + "errors" + "log" + "time" + + "golang.org/x/net/websocket" +) + +const ( + // MaxMessageTextLength is the current maximum message length in number of characters as defined here + // https://api.slack.com/rtm#limits + MaxMessageTextLength = 4000 +) + +// RTM represents a managed websocket connection. It also supports +// all the methods of the `Client` type. +// +// Create this element with Client's NewRTM(). +type RTM struct { + idGen IDGenerator + pings map[int]time.Time + + // Connection life-cycle + conn *websocket.Conn + IncomingEvents chan RTMEvent + outgoingMessages chan OutgoingMessage + killChannel chan bool + forcePing chan bool + rawEvents chan json.RawMessage + wasIntentional bool + isConnected bool + + // Client is the main API, embedded + Client + websocketURL string + + // UserDetails upon connection + info *Info +} + +// NewRTM returns a RTM, which provides a fully managed connection to +// Slack's websocket-based Real-Time Messaging protocol. +func newRTM(api *Client) *RTM { + return &RTM{ + Client: *api, + IncomingEvents: make(chan RTMEvent, 50), + outgoingMessages: make(chan OutgoingMessage, 20), + pings: make(map[int]time.Time), + isConnected: false, + wasIntentional: true, + killChannel: make(chan bool), + forcePing: make(chan bool), + rawEvents: make(chan json.RawMessage), + idGen: NewSafeID(1), + } +} + +// Disconnect and wait, blocking until a successful disconnection. +func (rtm *RTM) Disconnect() error { + if !rtm.isConnected { + return errors.New("Invalid call to Disconnect - Slack API is already disconnected") + } + rtm.killChannel <- true + return nil +} + +// Reconnect only makes sense if you've successfully disconnectd with Disconnect(). +func (rtm *RTM) Reconnect() error { + log.Println("RTM::Reconnect not implemented!") + return nil +} + +// GetInfo returns the info structure received when calling +// "startrtm", holding all channels, groups and other metadata needed +// to implement a full chat client. It will be non-nil after a call to +// StartRTM(). +func (rtm *RTM) GetInfo() *Info { + return rtm.info +} + +// SendMessage submits a simple message through the websocket. For +// more complicated messages, use `rtm.PostMessage` with a complete +// struct describing your attachments and all. +func (rtm *RTM) SendMessage(msg *OutgoingMessage) { + if msg == nil { + rtm.Debugln("Error: Attempted to SendMessage(nil)") + return + } + + rtm.outgoingMessages <- *msg +} diff --git a/vendor/github.com/nlopes/slack/websocket_channels.go b/vendor/github.com/nlopes/slack/websocket_channels.go new file mode 100644 index 0000000..fed1061 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_channels.go @@ -0,0 +1,72 @@ +package slack + +// ChannelCreatedEvent represents the Channel created event +type ChannelCreatedEvent struct { + Type string `json:"type"` + Channel ChannelCreatedInfo `json:"channel"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// ChannelCreatedInfo represents the information associated with the Channel created event +type ChannelCreatedInfo struct { + ID string `json:"id"` + IsChannel bool `json:"is_channel"` + Name string `json:"name"` + Created int `json:"created"` + Creator string `json:"creator"` +} + +// ChannelJoinedEvent represents the Channel joined event +type ChannelJoinedEvent struct { + Type string `json:"type"` + Channel Channel `json:"channel"` +} + +// ChannelInfoEvent represents the Channel info event +type ChannelInfoEvent struct { + // channel_left + // channel_deleted + // channel_archive + // channel_unarchive + Type string `json:"type"` + Channel string `json:"channel"` + User string `json:"user,omitempty"` + Timestamp *JSONTimeString `json:"ts,omitempty"` +} + +// ChannelRenameEvent represents the Channel rename event +type ChannelRenameEvent struct { + Type string `json:"type"` + Channel ChannelRenameInfo `json:"channel"` + Timestamp string `json:"event_ts"` +} + +// ChannelRenameInfo represents the information associated with a Channel rename event +type ChannelRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created *JSONTimeString `json:"created"` +} + +// ChannelHistoryChangedEvent represents the Channel history changed event +type ChannelHistoryChangedEvent struct { + Type string `json:"type"` + Latest JSONTimeString `json:"latest"` + Timestamp JSONTimeString `json:"ts"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// ChannelMarkedEvent represents the Channel marked event +type ChannelMarkedEvent ChannelInfoEvent + +// ChannelLeftEvent represents the Channel left event +type ChannelLeftEvent ChannelInfoEvent + +// ChannelDeletedEvent represents the Channel deleted event +type ChannelDeletedEvent ChannelInfoEvent + +// ChannelArchiveEvent represents the Channel archive event +type ChannelArchiveEvent ChannelInfoEvent + +// ChannelUnarchiveEvent represents the Channel unarchive event +type ChannelUnarchiveEvent ChannelInfoEvent diff --git a/vendor/github.com/nlopes/slack/websocket_dm.go b/vendor/github.com/nlopes/slack/websocket_dm.go new file mode 100644 index 0000000..98bf6f8 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_dm.go @@ -0,0 +1,23 @@ +package slack + +// IMCreatedEvent represents the IM created event +type IMCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel ChannelCreatedInfo `json:"channel"` +} + +// IMHistoryChangedEvent represents the IM history changed event +type IMHistoryChangedEvent ChannelHistoryChangedEvent + +// IMOpenEvent represents the IM open event +type IMOpenEvent ChannelInfoEvent + +// IMCloseEvent represents the IM close event +type IMCloseEvent ChannelInfoEvent + +// IMMarkedEvent represents the IM marked event +type IMMarkedEvent ChannelInfoEvent + +// IMMarkedHistoryChanged represents the IM marked history changed event +type IMMarkedHistoryChanged ChannelInfoEvent diff --git a/vendor/github.com/nlopes/slack/websocket_files.go b/vendor/github.com/nlopes/slack/websocket_files.go new file mode 100644 index 0000000..cf491ef --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_files.go @@ -0,0 +1,49 @@ +package slack + +// FileActionEvent represents the File action event +type fileActionEvent struct { + Type string `json:"type"` + EventTimestamp JSONTimeString `json:"event_ts"` + File File `json:"file"` + // FileID is used for FileDeletedEvent + FileID string `json:"file_id,omitempty"` +} + +// FileCreatedEvent represents the File created event +type FileCreatedEvent fileActionEvent + +// FileSharedEvent represents the File shared event +type FileSharedEvent fileActionEvent + +// FilePublicEvent represents the File public event +type FilePublicEvent fileActionEvent + +// FileUnsharedEvent represents the File unshared event +type FileUnsharedEvent fileActionEvent + +// FileChangeEvent represents the File change event +type FileChangeEvent fileActionEvent + +// FileDeletedEvent represents the File deleted event +type FileDeletedEvent fileActionEvent + +// FilePrivateEvent represents the File private event +type FilePrivateEvent fileActionEvent + +// FileCommentAddedEvent represents the File comment added event +type FileCommentAddedEvent struct { + fileActionEvent + Comment Comment `json:"comment"` +} + +// FileCommentEditedEvent represents the File comment edited event +type FileCommentEditedEvent struct { + fileActionEvent + Comment Comment `json:"comment"` +} + +// FileCommentDeletedEvent represents the File comment deleted event +type FileCommentDeletedEvent struct { + fileActionEvent + Comment string `json:"comment"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_groups.go b/vendor/github.com/nlopes/slack/websocket_groups.go new file mode 100644 index 0000000..eb88985 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_groups.go @@ -0,0 +1,49 @@ +package slack + +// GroupCreatedEvent represents the Group created event +type GroupCreatedEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel ChannelCreatedInfo `json:"channel"` +} + +// XXX: Should we really do this? event.Group is probably nicer than event.Channel +// even though the api returns "channel" + +// GroupMarkedEvent represents the Group marked event +type GroupMarkedEvent ChannelInfoEvent + +// GroupOpenEvent represents the Group open event +type GroupOpenEvent ChannelInfoEvent + +// GroupCloseEvent represents the Group close event +type GroupCloseEvent ChannelInfoEvent + +// GroupArchiveEvent represents the Group archive event +type GroupArchiveEvent ChannelInfoEvent + +// GroupUnarchiveEvent represents the Group unarchive event +type GroupUnarchiveEvent ChannelInfoEvent + +// GroupLeftEvent represents the Group left event +type GroupLeftEvent ChannelInfoEvent + +// GroupJoinedEvent represents the Group joined event +type GroupJoinedEvent ChannelJoinedEvent + +// GroupRenameEvent represents the Group rename event +type GroupRenameEvent struct { + Type string `json:"type"` + Group GroupRenameInfo `json:"channel"` + Timestamp string `json:"ts"` +} + +// GroupRenameInfo represents the group info related to the renamed group +type GroupRenameInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Created string `json:"created"` +} + +// GroupHistoryChangedEvent represents the Group history changed event +type GroupHistoryChangedEvent ChannelHistoryChangedEvent diff --git a/vendor/github.com/nlopes/slack/websocket_internals.go b/vendor/github.com/nlopes/slack/websocket_internals.go new file mode 100644 index 0000000..2a8abe6 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_internals.go @@ -0,0 +1,92 @@ +package slack + +import ( + "fmt" + "time" +) + +/** + * Internal events, created by this lib and not mapped to Slack APIs. + */ + +// ConnectedEvent is used for when we connect to Slack +type ConnectedEvent struct { + ConnectionCount int // 1 = first time, 2 = second time + Info *Info +} + +// ConnectionErrorEvent contains information about a connection error +type ConnectionErrorEvent struct { + Attempt int + ErrorObj error +} + +func (c *ConnectionErrorEvent) Error() string { + return c.ErrorObj.Error() +} + +// ConnectingEvent contains information about our connection attempt +type ConnectingEvent struct { + Attempt int // 1 = first attempt, 2 = second attempt + ConnectionCount int +} + +// DisconnectedEvent contains information about how we disconnected +type DisconnectedEvent struct { + Intentional bool +} + +// LatencyReport contains information about connection latency +type LatencyReport struct { + Value time.Duration +} + +// InvalidAuthEvent is used in case we can't even authenticate with the API +type InvalidAuthEvent struct{} + +// UnmarshallingErrorEvent is used when there are issues deconstructing a response +type UnmarshallingErrorEvent struct { + ErrorObj error +} + +func (u UnmarshallingErrorEvent) Error() string { + return u.ErrorObj.Error() +} + +// MessageTooLongEvent is used when sending a message that is too long +type MessageTooLongEvent struct { + Message OutgoingMessage + MaxLength int +} + +func (m *MessageTooLongEvent) Error() string { + return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength) +} + +// OutgoingErrorEvent contains information in case there were errors sending messages +type OutgoingErrorEvent struct { + Message OutgoingMessage + ErrorObj error +} + +func (o OutgoingErrorEvent) Error() string { + return o.ErrorObj.Error() +} + +// IncomingEventError contains information about an unexpected error receiving a websocket event +type IncomingEventError struct { + ErrorObj error +} + +func (i *IncomingEventError) Error() string { + return i.ErrorObj.Error() +} + +// AckErrorEvent i +type AckErrorEvent struct { + ErrorObj error +} + +func (a *AckErrorEvent) Error() string { + return a.ErrorObj.Error() +} diff --git a/vendor/github.com/nlopes/slack/websocket_managed_conn.go b/vendor/github.com/nlopes/slack/websocket_managed_conn.go new file mode 100644 index 0000000..6986c73 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_managed_conn.go @@ -0,0 +1,424 @@ +package slack + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + "time" + + "golang.org/x/net/websocket" +) + +// ManageConnection can be called on a Slack RTM instance returned by the +// NewRTM method. It will connect to the slack RTM API and handle all incoming +// and outgoing events. If a connection fails then it will attempt to reconnect +// and will notify any listeners through an error event on the IncomingEvents +// channel. +// +// If the connection ends and the disconnect was unintentional then this will +// attempt to reconnect. +// +// This should only be called once per slack API! Otherwise expect undefined +// behavior. +// +// The defined error events are located in websocket_internals.go. +func (rtm *RTM) ManageConnection() { + var connectionCount int + for { + connectionCount++ + // start trying to connect + // the returned err is already passed onto the IncomingEvents channel + info, conn, err := rtm.connect(connectionCount) + // if err != nil then the connection is sucessful - otherwise it is + // fatal + if err != nil { + return + } + rtm.info = info + rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{ + ConnectionCount: connectionCount, + Info: info, + }} + + rtm.conn = conn + rtm.isConnected = true + + keepRunning := make(chan bool) + // we're now connected (or have failed fatally) so we can set up + // listeners + go rtm.handleIncomingEvents(keepRunning) + + // this should be a blocking call until the connection has ended + rtm.handleEvents(keepRunning, 30*time.Second) + + // after being disconnected we need to check if it was intentional + // if not then we should try to reconnect + if rtm.wasIntentional { + return + } + // else continue and run the loop again to connect + } +} + +// connect attempts to connect to the slack websocket API. It handles any +// errors that occur while connecting and will return once a connection +// has been successfully opened. +func (rtm *RTM) connect(connectionCount int) (*Info, *websocket.Conn, error) { + // used to provide exponential backoff wait time with jitter before trying + // to connect to slack again + boff := &backoff{ + Min: 100 * time.Millisecond, + Max: 5 * time.Minute, + Factor: 2, + Jitter: true, + } + + for { + // send connecting event + rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{ + Attempt: boff.attempts + 1, + ConnectionCount: connectionCount, + }} + // attempt to start the connection + info, conn, err := rtm.startRTMAndDial() + if err == nil { + return info, conn, nil + } + // check for fatal errors - currently only invalid_auth + if sErr, ok := err.(*WebError); ok && sErr.Error() == "invalid_auth" { + rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}} + return nil, nil, sErr + } + // any other errors are treated as recoverable and we try again after + // sending the event along the IncomingEvents channel + rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{ + Attempt: boff.attempts, + ErrorObj: err, + }} + // get time we should wait before attempting to connect again + dur := boff.Duration() + rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err) + rtm.Debugln(" -> reconnecting in", dur) + time.Sleep(dur) + } +} + +// startRTMAndDial attemps to connect to the slack websocket. It returns the +// full information returned by the "rtm.start" method on the slack API. +func (rtm *RTM) startRTMAndDial() (*Info, *websocket.Conn, error) { + info, url, err := rtm.StartRTM() + if err != nil { + return nil, nil, err + } + + conn, err := websocketProxyDial(url, "http://api.slack.com") + if err != nil { + return nil, nil, err + } + return info, conn, err +} + +// killConnection stops the websocket connection and signals to all goroutines +// that they should cease listening to the connection for events. +// +// This should not be called directly! Instead a boolean value (true for +// intentional, false otherwise) should be sent to the killChannel on the RTM. +func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error { + rtm.Debugln("killing connection") + if rtm.isConnected { + close(keepRunning) + } + rtm.isConnected = false + rtm.wasIntentional = intentional + err := rtm.conn.Close() + rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}} + return err +} + +// handleEvents is a blocking function that handles all events. This sends +// pings when asked to (on rtm.forcePing) and upon every given elapsed +// interval. This also sends outgoing messages that are received from the RTM's +// outgoingMessages channel. This also handles incoming raw events from the RTM +// rawEvents channel. +func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + // catch "stop" signal on channel close + case intentional := <-rtm.killChannel: + _ = rtm.killConnection(keepRunning, intentional) + return + // send pings on ticker interval + case <-ticker.C: + err := rtm.ping() + if err != nil { + _ = rtm.killConnection(keepRunning, false) + return + } + case <-rtm.forcePing: + err := rtm.ping() + if err != nil { + _ = rtm.killConnection(keepRunning, false) + return + } + // listen for messages that need to be sent + case msg := <-rtm.outgoingMessages: + rtm.sendOutgoingMessage(msg) + // listen for incoming messages that need to be parsed + case rawEvent := <-rtm.rawEvents: + rtm.handleRawEvent(rawEvent) + } + } +} + +// handleIncomingEvents monitors the RTM's opened websocket for any incoming +// events. It pushes the raw events onto the RTM channel rawEvents. +// +// This will stop executing once the RTM's keepRunning channel has been closed +// or has anything sent to it. +func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) { + for { + // non-blocking listen to see if channel is closed + select { + // catch "stop" signal on channel close + case <-keepRunning: + return + default: + rtm.receiveIncomingEvent() + } + } +} + +// sendOutgoingMessage sends the given OutgoingMessage to the slack websocket. +// +// It does not currently detect if a outgoing message fails due to a disconnect +// and instead lets a future failed 'PING' detect the failed connection. +func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) { + rtm.Debugln("Sending message:", msg) + if len(msg.Text) > MaxMessageTextLength { + rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{ + Message: msg, + MaxLength: MaxMessageTextLength, + }} + return + } + err := websocket.JSON.Send(rtm.conn, msg) + if err != nil { + rtm.IncomingEvents <- RTMEvent{"outgoing_error", &OutgoingErrorEvent{ + Message: msg, + ErrorObj: err, + }} + // TODO force ping? + } +} + +// ping sends a 'PING' message to the RTM's websocket. If the 'PING' message +// fails to send then this returns an error signifying that the connection +// should be considered disconnected. +// +// This does not handle incoming 'PONG' responses but does store the time of +// each successful 'PING' send so latency can be detected upon a 'PONG' +// response. +func (rtm *RTM) ping() error { + id := rtm.idGen.Next() + rtm.Debugln("Sending PING ", id) + rtm.pings[id] = time.Now() + + msg := &Ping{ID: id, Type: "ping"} + err := websocket.JSON.Send(rtm.conn, msg) + if err != nil { + rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error()) + return err + } + return nil +} + +// receiveIncomingEvent attempts to receive an event from the RTM's websocket. +// This will block until a frame is available from the websocket. +func (rtm *RTM) receiveIncomingEvent() { + event := json.RawMessage{} + err := websocket.JSON.Receive(rtm.conn, &event) + if err == io.EOF { + // EOF's don't seem to signify a failed connection so instead we ignore + // them here and detect a failed connection upon attempting to send a + // 'PING' message + + // trigger a 'PING' to detect pontential websocket disconnect + rtm.forcePing <- true + return + } else if err != nil { + rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{ + ErrorObj: err, + }} + // force a ping here too? + return + } else if len(event) == 0 { + rtm.Debugln("Received empty event") + return + } + rtm.Debugln("Incoming Event:", string(event[:])) + rtm.rawEvents <- event +} + +// handleRawEvent takes a raw JSON message received from the slack websocket +// and handles the encoded event. +func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) { + event := &Event{} + err := json.Unmarshal(rawEvent, event) + if err != nil { + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return + } + switch event.Type { + case "": + rtm.handleAck(rawEvent) + case "hello": + rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}} + case "pong": + rtm.handlePong(rawEvent) + default: + rtm.handleEvent(event.Type, rawEvent) + } +} + +// handleAck handles an incoming 'ACK' message. +func (rtm *RTM) handleAck(event json.RawMessage) { + ack := &AckMessage{} + if err := json.Unmarshal(event, ack); err != nil { + rtm.Debugln("RTM Error unmarshalling 'ack' event:", err) + rtm.Debugln(" -> Erroneous 'ack' event:", string(event)) + return + } + if ack.Ok { + rtm.IncomingEvents <- RTMEvent{"ack", ack} + } else { + rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}} + } +} + +// handlePong handles an incoming 'PONG' message which should be in response to +// a previously sent 'PING' message. This is then used to compute the +// connection's latency. +func (rtm *RTM) handlePong(event json.RawMessage) { + pong := &Pong{} + if err := json.Unmarshal(event, pong); err != nil { + rtm.Debugln("RTM Error unmarshalling 'pong' event:", err) + rtm.Debugln(" -> Erroneous 'ping' event:", string(event)) + return + } + if pingTime, exists := rtm.pings[pong.ReplyTo]; exists { + latency := time.Since(pingTime) + rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}} + delete(rtm.pings, pong.ReplyTo) + } else { + rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event)) + } +} + +// handleEvent is the "default" response to an event that does not have a +// special case. It matches the command's name to a mapping of defined events +// and then sends the corresponding event struct to the IncomingEvents channel. +// If the event type is not found or the event cannot be unmarshalled into the +// correct struct then this sends an UnmarshallingErrorEvent to the +// IncomingEvents channel. +func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) { + v, exists := eventMapping[typeStr] + if !exists { + rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event)) + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return + } + t := reflect.TypeOf(v) + recvEvent := reflect.New(t).Interface() + err := json.Unmarshal(event, recvEvent) + if err != nil { + rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event)) + err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event)) + rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}} + return + } + rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent} +} + +// eventMapping holds a mapping of event names to their corresponding struct +// implementations. The structs should be instances of the unmarshalling +// target for the matching event type. +var eventMapping = map[string]interface{}{ + "message": MessageEvent{}, + "presence_change": PresenceChangeEvent{}, + "user_typing": UserTypingEvent{}, + + "channel_marked": ChannelMarkedEvent{}, + "channel_created": ChannelCreatedEvent{}, + "channel_joined": ChannelJoinedEvent{}, + "channel_left": ChannelLeftEvent{}, + "channel_deleted": ChannelDeletedEvent{}, + "channel_rename": ChannelRenameEvent{}, + "channel_archive": ChannelArchiveEvent{}, + "channel_unarchive": ChannelUnarchiveEvent{}, + "channel_history_changed": ChannelHistoryChangedEvent{}, + + "im_created": IMCreatedEvent{}, + "im_open": IMOpenEvent{}, + "im_close": IMCloseEvent{}, + "im_marked": IMMarkedEvent{}, + "im_history_changed": IMHistoryChangedEvent{}, + + "group_marked": GroupMarkedEvent{}, + "group_open": GroupOpenEvent{}, + "group_joined": GroupJoinedEvent{}, + "group_left": GroupLeftEvent{}, + "group_close": GroupCloseEvent{}, + "group_rename": GroupRenameEvent{}, + "group_archive": GroupArchiveEvent{}, + "group_unarchive": GroupUnarchiveEvent{}, + "group_history_changed": GroupHistoryChangedEvent{}, + + "file_created": FileCreatedEvent{}, + "file_shared": FileSharedEvent{}, + "file_unshared": FileUnsharedEvent{}, + "file_public": FilePublicEvent{}, + "file_private": FilePrivateEvent{}, + "file_change": FileChangeEvent{}, + "file_deleted": FileDeletedEvent{}, + "file_comment_added": FileCommentAddedEvent{}, + "file_comment_edited": FileCommentEditedEvent{}, + "file_comment_deleted": FileCommentDeletedEvent{}, + + "pin_added": PinAddedEvent{}, + "pin_removed": PinRemovedEvent{}, + + "star_added": StarAddedEvent{}, + "star_removed": StarRemovedEvent{}, + + "reaction_added": ReactionAddedEvent{}, + "reaction_removed": ReactionRemovedEvent{}, + + "pref_change": PrefChangeEvent{}, + + "team_join": TeamJoinEvent{}, + "team_rename": TeamRenameEvent{}, + "team_pref_change": TeamPrefChangeEvent{}, + "team_domain_change": TeamDomainChangeEvent{}, + "team_migration_started": TeamMigrationStartedEvent{}, + + "manual_presence_change": ManualPresenceChangeEvent{}, + + "user_change": UserChangeEvent{}, + + "emoji_changed": EmojiChangedEvent{}, + + "commands_changed": CommandsChangedEvent{}, + + "email_domain_changed": EmailDomainChangedEvent{}, + + "bot_added": BotAddedEvent{}, + "bot_changed": BotChangedEvent{}, + + "accounts_changed": AccountsChangedEvent{}, + + "reconnect_url": ReconnectUrlEvent{}, +} diff --git a/vendor/github.com/nlopes/slack/websocket_misc.go b/vendor/github.com/nlopes/slack/websocket_misc.go new file mode 100644 index 0000000..2e33656 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_misc.go @@ -0,0 +1,117 @@ +package slack + +import ( + "encoding/json" + "fmt" +) + +// AckMessage is used for messages received in reply to other messages +type AckMessage struct { + ReplyTo int `json:"reply_to"` + Timestamp string `json:"ts"` + Text string `json:"text"` + RTMResponse +} + +// RTMResponse encapsulates response details as returned by the Slack API +type RTMResponse struct { + Ok bool `json:"ok"` + Error *RTMError `json:"error"` +} + +// RTMError encapsulates error information as returned by the Slack API +type RTMError struct { + Code int + Msg string +} + +func (s RTMError) Error() string { + return fmt.Sprintf("Code %d - %s", s.Code, s.Msg) +} + +// MessageEvent represents a Slack Message (used as the event type for an incoming message) +type MessageEvent Message + +// RTMEvent is the main wrapper. You will find all the other messages attached +type RTMEvent struct { + Type string + Data interface{} +} + +// HelloEvent represents the hello event +type HelloEvent struct{} + +// PresenceChangeEvent represents the presence change event +type PresenceChangeEvent struct { + Type string `json:"type"` + Presence string `json:"presence"` + User string `json:"user"` +} + +// UserTypingEvent represents the user typing event +type UserTypingEvent struct { + Type string `json:"type"` + User string `json:"user"` + Channel string `json:"channel"` +} + +// PrefChangeEvent represents a user preferences change event +type PrefChangeEvent struct { + Type string `json:"type"` + Name string `json:"name"` + Value json.RawMessage `json:"value"` +} + +// ManualPresenceChangeEvent represents the manual presence change event +type ManualPresenceChangeEvent struct { + Type string `json:"type"` + Presence string `json:"presence"` +} + +// UserChangeEvent represents the user change event +type UserChangeEvent struct { + Type string `json:"type"` + User User `json:"user"` +} + +// EmojiChangedEvent represents the emoji changed event +type EmojiChangedEvent struct { + Type string `json:"type"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// CommandsChangedEvent represents the commands changed event +type CommandsChangedEvent struct { + Type string `json:"type"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// EmailDomainChangedEvent represents the email domain changed event +type EmailDomainChangedEvent struct { + Type string `json:"type"` + EventTimestamp JSONTimeString `json:"event_ts"` + EmailDomain string `json:"email_domain"` +} + +// BotAddedEvent represents the bot added event +type BotAddedEvent struct { + Type string `json:"type"` + Bot Bot `json:"bot"` +} + +// BotChangedEvent represents the bot changed event +type BotChangedEvent struct { + Type string `json:"type"` + Bot Bot `json:"bot"` +} + +// AccountsChangedEvent represents the accounts changed event +type AccountsChangedEvent struct { + Type string `json:"type"` +} + +// ReconnectUrlEvent represents the receiving reconnect url event +type ReconnectUrlEvent struct { + Type string `json:"type"` + URL string `json:"url"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_pins.go b/vendor/github.com/nlopes/slack/websocket_pins.go new file mode 100644 index 0000000..463bf38 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_pins.go @@ -0,0 +1,16 @@ +package slack + +type pinEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item Item `json:"item"` + Channel string `json:"channel_id"` + EventTimestamp JSONTimeString `json:"event_ts"` + HasPins bool `json:"has_pins,omitempty"` +} + +// PinAddedEvent represents the Pin added event +type PinAddedEvent pinEvent + +// PinRemovedEvent represents the Pin removed event +type PinRemovedEvent pinEvent diff --git a/vendor/github.com/nlopes/slack/websocket_proxy.go b/vendor/github.com/nlopes/slack/websocket_proxy.go new file mode 100644 index 0000000..440015d --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_proxy.go @@ -0,0 +1,83 @@ +package slack + +import ( + "crypto/tls" + "errors" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + + "golang.org/x/net/websocket" +) + +// Taken and reworked from: https://gist.github.com/madmo/8548738 +func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) { + p, err := net.Dial("tcp", proxy) + if err != nil { + return nil, err + } + + turl, err := url.Parse(urlString) + if err != nil { + return nil, err + } + + req := http.Request{ + Method: "CONNECT", + URL: &url.URL{}, + Host: turl.Host, + } + + cc := httputil.NewProxyClientConn(p, nil) + cc.Do(&req) + if err != nil && err != httputil.ErrPersistEOF { + return nil, err + } + + rwc, _ := cc.Hijack() + + return rwc, nil +} + +func websocketProxyDial(urlString, origin string) (ws *websocket.Conn, err error) { + if os.Getenv("HTTP_PROXY") == "" { + return websocket.Dial(urlString, "", origin) + } + + purl, err := url.Parse(os.Getenv("HTTP_PROXY")) + if err != nil { + return nil, err + } + + config, err := websocket.NewConfig(urlString, origin) + if err != nil { + return nil, err + } + + client, err := websocketHTTPConnect(purl.Host, urlString) + if err != nil { + return nil, err + } + + switch config.Location.Scheme { + case "ws": + case "wss": + tlsClient := tls.Client(client, &tls.Config{ + ServerName: strings.Split(config.Location.Host, ":")[0], + }) + err := tlsClient.Handshake() + if err != nil { + tlsClient.Close() + return nil, err + } + client = tlsClient + + default: + return nil, errors.New("invalid websocket schema") + } + + return websocket.NewClient(config, client) +} diff --git a/vendor/github.com/nlopes/slack/websocket_reactions.go b/vendor/github.com/nlopes/slack/websocket_reactions.go new file mode 100644 index 0000000..50e4534 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_reactions.go @@ -0,0 +1,15 @@ +package slack + +type reactionEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item ReactedItem `json:"item"` + Reaction string `json:"reaction"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// ReactionAddedEvent represents the Reaction added event +type ReactionAddedEvent reactionEvent + +// ReactionRemovedEvent represents the Reaction removed event +type ReactionRemovedEvent reactionEvent diff --git a/vendor/github.com/nlopes/slack/websocket_stars.go b/vendor/github.com/nlopes/slack/websocket_stars.go new file mode 100644 index 0000000..2132ab7 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_stars.go @@ -0,0 +1,14 @@ +package slack + +type starEvent struct { + Type string `json:"type"` + User string `json:"user"` + Item StarredItem `json:"item"` + EventTimestamp JSONTimeString `json:"event_ts"` +} + +// StarAddedEvent represents the Star added event +type StarAddedEvent starEvent + +// StarRemovedEvent represents the Star removed event +type StarRemovedEvent starEvent diff --git a/vendor/github.com/nlopes/slack/websocket_teams.go b/vendor/github.com/nlopes/slack/websocket_teams.go new file mode 100644 index 0000000..66964a5 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_teams.go @@ -0,0 +1,33 @@ +package slack + +// TeamJoinEvent represents the Team join event +type TeamJoinEvent struct { + Type string `json:"type"` + User *User `json:"user,omitempty"` +} + +// TeamRenameEvent represents the Team rename event +type TeamRenameEvent struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` + EventTimestamp *JSONTimeString `json:"event_ts,omitempty"` +} + +// TeamPrefChangeEvent represents the Team preference change event +type TeamPrefChangeEvent struct { + Type string `json:"type"` + Name string `json:"name,omitempty"` + Value []string `json:"value,omitempty"` +} + +// TeamDomainChangeEvent represents the Team domain change event +type TeamDomainChangeEvent struct { + Type string `json:"type"` + URL string `json:"url"` + Domain string `json:"domain"` +} + +// TeamMigrationStartedEvent represents the Team migration started event +type TeamMigrationStartedEvent struct { + Type string `json:"type"` +} diff --git a/vendor/github.com/nlopes/slack/websocket_utils.go b/vendor/github.com/nlopes/slack/websocket_utils.go new file mode 100644 index 0000000..7822108 --- /dev/null +++ b/vendor/github.com/nlopes/slack/websocket_utils.go @@ -0,0 +1,49 @@ +package slack + +import ( + "fmt" + "log" + "net" + "net/url" + "strconv" + "time" +) + +// JSONTimeString is an auxiliary type to allow us to format the time as we wish +type JSONTimeString string + +// String converts the unix timestamp into a string +func (t JSONTimeString) String() string { + tm := t.Time() + if tm.IsZero() { + return "" + } + return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) +} + +// Time converts the timestamp string to time.Time +func (t JSONTimeString) Time() time.Time { + if t == "" { + return time.Time{} + } + floatN, err := strconv.ParseFloat(string(t), 64) + if err != nil { + log.Println("ERROR parsing a JSONTimeString!", err) + return time.Time{} + } + return time.Unix(int64(floatN), 0) +} + +var portMapping = map[string]string{"ws": "80", "wss": "443"} + +func websocketizeURLPort(orig string) (string, error) { + urlObj, err := url.ParseRequestURI(orig) + if err != nil { + return "", err + } + _, _, err = net.SplitHostPort(urlObj.Host) + if err != nil { + return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil + } + return orig, nil +} diff --git a/vendor/golang.org/x/net b/vendor/golang.org/x/net deleted file mode 160000 index 4599ae7..0000000 --- a/vendor/golang.org/x/net +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4599ae7937fce9b670ce32b8ad32bbb7ae726b3e diff --git a/vendor/golang.org/x/net/LICENSE b/vendor/golang.org/x/net/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/vendor/golang.org/x/net/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/net/PATENTS b/vendor/golang.org/x/net/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/net/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google 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, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/net/websocket/client.go b/vendor/golang.org/x/net/websocket/client.go new file mode 100644 index 0000000..20d1e1e --- /dev/null +++ b/vendor/golang.org/x/net/websocket/client.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "crypto/tls" + "io" + "net" + "net/http" + "net/url" +) + +// DialError is an error that occurs while dialling a websocket server. +type DialError struct { + *Config + Err error +} + +func (e *DialError) Error() string { + return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() +} + +// NewConfig creates a new WebSocket config for client connection. +func NewConfig(server, origin string) (config *Config, err error) { + config = new(Config) + config.Version = ProtocolVersionHybi13 + config.Location, err = url.ParseRequestURI(server) + if err != nil { + return + } + config.Origin, err = url.ParseRequestURI(origin) + if err != nil { + return + } + config.Header = http.Header(make(map[string][]string)) + return +} + +// NewClient creates a new WebSocket client connection over rwc. +func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + err = hybiClientHandshake(config, br, bw) + if err != nil { + return + } + buf := bufio.NewReadWriter(br, bw) + ws = newHybiClientConn(config, buf, rwc) + return +} + +// Dial opens a new client connection to a WebSocket. +func Dial(url_, protocol, origin string) (ws *Conn, err error) { + config, err := NewConfig(url_, origin) + if err != nil { + return nil, err + } + if protocol != "" { + config.Protocol = []string{protocol} + } + return DialConfig(config) +} + +var portMap = map[string]string{ + "ws": "80", + "wss": "443", +} + +func parseAuthority(location *url.URL) string { + if _, ok := portMap[location.Scheme]; ok { + if _, _, err := net.SplitHostPort(location.Host); err != nil { + return net.JoinHostPort(location.Host, portMap[location.Scheme]) + } + } + return location.Host +} + +// DialConfig opens a new client connection to a WebSocket with a config. +func DialConfig(config *Config) (ws *Conn, err error) { + var client net.Conn + if config.Location == nil { + return nil, &DialError{config, ErrBadWebSocketLocation} + } + if config.Origin == nil { + return nil, &DialError{config, ErrBadWebSocketOrigin} + } + switch config.Location.Scheme { + case "ws": + client, err = net.Dial("tcp", parseAuthority(config.Location)) + + case "wss": + client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig) + + default: + err = ErrBadScheme + } + if err != nil { + goto Error + } + + ws, err = NewClient(config, client) + if err != nil { + client.Close() + goto Error + } + return + +Error: + return nil, &DialError{config, err} +} diff --git a/vendor/golang.org/x/net/websocket/hybi.go b/vendor/golang.org/x/net/websocket/hybi.go new file mode 100644 index 0000000..60bbc84 --- /dev/null +++ b/vendor/golang.org/x/net/websocket/hybi.go @@ -0,0 +1,586 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +// This file implements a protocol of hybi draft. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17 + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/binary" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +const ( + websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + closeStatusNormal = 1000 + closeStatusGoingAway = 1001 + closeStatusProtocolError = 1002 + closeStatusUnsupportedData = 1003 + closeStatusFrameTooLarge = 1004 + closeStatusNoStatusRcvd = 1005 + closeStatusAbnormalClosure = 1006 + closeStatusBadMessageData = 1007 + closeStatusPolicyViolation = 1008 + closeStatusTooBigData = 1009 + closeStatusExtensionMismatch = 1010 + + maxControlFramePayloadLength = 125 +) + +var ( + ErrBadMaskingKey = &ProtocolError{"bad masking key"} + ErrBadPongMessage = &ProtocolError{"bad pong message"} + ErrBadClosingStatus = &ProtocolError{"bad closing status"} + ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"} + ErrNotImplemented = &ProtocolError{"not implemented"} + + handshakeHeader = map[string]bool{ + "Host": true, + "Upgrade": true, + "Connection": true, + "Sec-Websocket-Key": true, + "Sec-Websocket-Origin": true, + "Sec-Websocket-Version": true, + "Sec-Websocket-Protocol": true, + "Sec-Websocket-Accept": true, + } +) + +// A hybiFrameHeader is a frame header as defined in hybi draft. +type hybiFrameHeader struct { + Fin bool + Rsv [3]bool + OpCode byte + Length int64 + MaskingKey []byte + + data *bytes.Buffer +} + +// A hybiFrameReader is a reader for hybi frame. +type hybiFrameReader struct { + reader io.Reader + + header hybiFrameHeader + pos int64 + length int +} + +func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) { + n, err = frame.reader.Read(msg) + if err != nil { + return 0, err + } + if frame.header.MaskingKey != nil { + for i := 0; i < n; i++ { + msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4] + frame.pos++ + } + } + return n, err +} + +func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode } + +func (frame *hybiFrameReader) HeaderReader() io.Reader { + if frame.header.data == nil { + return nil + } + if frame.header.data.Len() == 0 { + return nil + } + return frame.header.data +} + +func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil } + +func (frame *hybiFrameReader) Len() (n int) { return frame.length } + +// A hybiFrameReaderFactory creates new frame reader based on its frame type. +type hybiFrameReaderFactory struct { + *bufio.Reader +} + +// NewFrameReader reads a frame header from the connection, and creates new reader for the frame. +// See Section 5.2 Base Framing protocol for detail. +// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2 +func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) { + hybiFrame := new(hybiFrameReader) + frame = hybiFrame + var header []byte + var b byte + // First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0 + for i := 0; i < 3; i++ { + j := uint(6 - i) + hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0 + } + hybiFrame.header.OpCode = header[0] & 0x0f + + // Second byte. Mask/Payload len(7bits) + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + mask := (b & 0x80) != 0 + b &= 0x7f + lengthFields := 0 + switch { + case b <= 125: // Payload length 7bits. + hybiFrame.header.Length = int64(b) + case b == 126: // Payload length 7+16bits + lengthFields = 2 + case b == 127: // Payload length 7+64bits + lengthFields = 8 + } + for i := 0; i < lengthFields; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits + b &= 0x7f + } + header = append(header, b) + hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b) + } + if mask { + // Masking key. 4 bytes. + for i := 0; i < 4; i++ { + b, err = buf.ReadByte() + if err != nil { + return + } + header = append(header, b) + hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b) + } + } + hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length) + hybiFrame.header.data = bytes.NewBuffer(header) + hybiFrame.length = len(header) + int(hybiFrame.header.Length) + return +} + +// A HybiFrameWriter is a writer for hybi frame. +type hybiFrameWriter struct { + writer *bufio.Writer + + header *hybiFrameHeader +} + +func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) { + var header []byte + var b byte + if frame.header.Fin { + b |= 0x80 + } + for i := 0; i < 3; i++ { + if frame.header.Rsv[i] { + j := uint(6 - i) + b |= 1 << j + } + } + b |= frame.header.OpCode + header = append(header, b) + if frame.header.MaskingKey != nil { + b = 0x80 + } else { + b = 0 + } + lengthFields := 0 + length := len(msg) + switch { + case length <= 125: + b |= byte(length) + case length < 65536: + b |= 126 + lengthFields = 2 + default: + b |= 127 + lengthFields = 8 + } + header = append(header, b) + for i := 0; i < lengthFields; i++ { + j := uint((lengthFields - i - 1) * 8) + b = byte((length >> j) & 0xff) + header = append(header, b) + } + if frame.header.MaskingKey != nil { + if len(frame.header.MaskingKey) != 4 { + return 0, ErrBadMaskingKey + } + header = append(header, frame.header.MaskingKey...) + frame.writer.Write(header) + data := make([]byte, length) + for i := range data { + data[i] = msg[i] ^ frame.header.MaskingKey[i%4] + } + frame.writer.Write(data) + err = frame.writer.Flush() + return length, err + } + frame.writer.Write(header) + frame.writer.Write(msg) + err = frame.writer.Flush() + return length, err +} + +func (frame *hybiFrameWriter) Close() error { return nil } + +type hybiFrameWriterFactory struct { + *bufio.Writer + needMaskingKey bool +} + +func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) { + frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType} + if buf.needMaskingKey { + frameHeader.MaskingKey, err = generateMaskingKey() + if err != nil { + return nil, err + } + } + return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil +} + +type hybiFrameHandler struct { + conn *Conn + payloadType byte +} + +func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) { + if handler.conn.IsServerConn() { + // The client MUST mask all frames sent to the server. + if frame.(*hybiFrameReader).header.MaskingKey == nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } else { + // The server MUST NOT mask all frames. + if frame.(*hybiFrameReader).header.MaskingKey != nil { + handler.WriteClose(closeStatusProtocolError) + return nil, io.EOF + } + } + if header := frame.HeaderReader(); header != nil { + io.Copy(ioutil.Discard, header) + } + switch frame.PayloadType() { + case ContinuationFrame: + frame.(*hybiFrameReader).header.OpCode = handler.payloadType + case TextFrame, BinaryFrame: + handler.payloadType = frame.PayloadType() + case CloseFrame: + return nil, io.EOF + case PingFrame, PongFrame: + b := make([]byte, maxControlFramePayloadLength) + n, err := io.ReadFull(frame, b) + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + io.Copy(ioutil.Discard, frame) + if frame.PayloadType() == PingFrame { + if _, err := handler.WritePong(b[:n]); err != nil { + return nil, err + } + } + return nil, nil + } + return frame, nil +} + +func (handler *hybiFrameHandler) WriteClose(status int) (err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame) + if err != nil { + return err + } + msg := make([]byte, 2) + binary.BigEndian.PutUint16(msg, uint16(status)) + _, err = w.Write(msg) + w.Close() + return err +} + +func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) { + handler.conn.wio.Lock() + defer handler.conn.wio.Unlock() + w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + return n, err +} + +// newHybiConn creates a new WebSocket connection speaking hybi draft protocol. +func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + if buf == nil { + br := bufio.NewReader(rwc) + bw := bufio.NewWriter(rwc) + buf = bufio.NewReadWriter(br, bw) + } + ws := &Conn{config: config, request: request, buf: buf, rwc: rwc, + frameReaderFactory: hybiFrameReaderFactory{buf.Reader}, + frameWriterFactory: hybiFrameWriterFactory{ + buf.Writer, request == nil}, + PayloadType: TextFrame, + defaultCloseStatus: closeStatusNormal} + ws.frameHandler = &hybiFrameHandler{conn: ws} + return ws +} + +// generateMaskingKey generates a masking key for a frame. +func generateMaskingKey() (maskingKey []byte, err error) { + maskingKey = make([]byte, 4) + if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil { + return + } + return +} + +// generateNonce generates a nonce consisting of a randomly selected 16-byte +// value that has been base64-encoded. +func generateNonce() (nonce []byte) { + key := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + panic(err) + } + nonce = make([]byte, 24) + base64.StdEncoding.Encode(nonce, key) + return +} + +// removeZone removes IPv6 zone identifer from host. +// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080" +func removeZone(host string) string { + if !strings.HasPrefix(host, "[") { + return host + } + i := strings.LastIndex(host, "]") + if i < 0 { + return host + } + j := strings.LastIndex(host[:i], "%") + if j < 0 { + return host + } + return host[:j] + host[i:] +} + +// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of +// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string. +func getNonceAccept(nonce []byte) (expected []byte, err error) { + h := sha1.New() + if _, err = h.Write(nonce); err != nil { + return + } + if _, err = h.Write([]byte(websocketGUID)); err != nil { + return + } + expected = make([]byte, 28) + base64.StdEncoding.Encode(expected, h.Sum(nil)) + return +} + +// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17 +func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) { + bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n") + + // According to RFC 6874, an HTTP client, proxy, or other + // intermediary must remove any IPv6 zone identifier attached + // to an outgoing URI. + bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n") + bw.WriteString("Upgrade: websocket\r\n") + bw.WriteString("Connection: Upgrade\r\n") + nonce := generateNonce() + if config.handshakeData != nil { + nonce = []byte(config.handshakeData["key"]) + } + bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n") + bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n") + + if config.Version != ProtocolVersionHybi13 { + return ErrBadProtocolVersion + } + + bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n") + if len(config.Protocol) > 0 { + bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + err = config.Header.WriteSubset(bw, handshakeHeader) + if err != nil { + return err + } + + bw.WriteString("\r\n") + if err = bw.Flush(); err != nil { + return err + } + + resp, err := http.ReadResponse(br, &http.Request{Method: "GET"}) + if err != nil { + return err + } + if resp.StatusCode != 101 { + return ErrBadStatus + } + if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" || + strings.ToLower(resp.Header.Get("Connection")) != "upgrade" { + return ErrBadUpgrade + } + expectedAccept, err := getNonceAccept(nonce) + if err != nil { + return err + } + if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) { + return ErrChallengeResponse + } + if resp.Header.Get("Sec-WebSocket-Extensions") != "" { + return ErrUnsupportedExtensions + } + offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol") + if offeredProtocol != "" { + protocolMatched := false + for i := 0; i < len(config.Protocol); i++ { + if config.Protocol[i] == offeredProtocol { + protocolMatched = true + break + } + } + if !protocolMatched { + return ErrBadWebSocketProtocol + } + config.Protocol = []string{offeredProtocol} + } + + return nil +} + +// newHybiClientConn creates a client WebSocket connection after handshake. +func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn { + return newHybiConn(config, buf, rwc, nil) +} + +// A HybiServerHandshaker performs a server handshake using hybi draft protocol. +type hybiServerHandshaker struct { + *Config + accept []byte +} + +func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) { + c.Version = ProtocolVersionHybi13 + if req.Method != "GET" { + return http.StatusMethodNotAllowed, ErrBadRequestMethod + } + // HTTP version can be safely ignored. + + if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || + !strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") { + return http.StatusBadRequest, ErrNotWebSocket + } + + key := req.Header.Get("Sec-Websocket-Key") + if key == "" { + return http.StatusBadRequest, ErrChallengeResponse + } + version := req.Header.Get("Sec-Websocket-Version") + switch version { + case "13": + c.Version = ProtocolVersionHybi13 + default: + return http.StatusBadRequest, ErrBadWebSocketVersion + } + var scheme string + if req.TLS != nil { + scheme = "wss" + } else { + scheme = "ws" + } + c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI()) + if err != nil { + return http.StatusBadRequest, err + } + protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol")) + if protocol != "" { + protocols := strings.Split(protocol, ",") + for i := 0; i < len(protocols); i++ { + c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i])) + } + } + c.accept, err = getNonceAccept([]byte(key)) + if err != nil { + return http.StatusInternalServerError, err + } + return http.StatusSwitchingProtocols, nil +} + +// Origin parses the Origin header in req. +// If the Origin header is not set, it returns nil and nil. +func Origin(config *Config, req *http.Request) (*url.URL, error) { + var origin string + switch config.Version { + case ProtocolVersionHybi13: + origin = req.Header.Get("Origin") + } + if origin == "" { + return nil, nil + } + return url.ParseRequestURI(origin) +} + +func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) { + if len(c.Protocol) > 0 { + if len(c.Protocol) != 1 { + // You need choose a Protocol in Handshake func in Server. + return ErrBadWebSocketProtocol + } + } + buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n") + buf.WriteString("Upgrade: websocket\r\n") + buf.WriteString("Connection: Upgrade\r\n") + buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n") + if len(c.Protocol) > 0 { + buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n") + } + // TODO(ukai): send Sec-WebSocket-Extensions. + if c.Header != nil { + err := c.Header.WriteSubset(buf, handshakeHeader) + if err != nil { + return err + } + } + buf.WriteString("\r\n") + return buf.Flush() +} + +func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiServerConn(c.Config, buf, rwc, request) +} + +// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol. +func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn { + return newHybiConn(config, buf, rwc, request) +} diff --git a/vendor/golang.org/x/net/websocket/server.go b/vendor/golang.org/x/net/websocket/server.go new file mode 100644 index 0000000..0895dea --- /dev/null +++ b/vendor/golang.org/x/net/websocket/server.go @@ -0,0 +1,113 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package websocket + +import ( + "bufio" + "fmt" + "io" + "net/http" +) + +func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { + var hs serverHandshaker = &hybiServerHandshaker{Config: config} + code, err := hs.ReadHandshake(buf.Reader, req) + if err == ErrBadWebSocketVersion { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if err != nil { + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.WriteString(err.Error()) + buf.Flush() + return + } + if handshake != nil { + err = handshake(config, req) + if err != nil { + code = http.StatusForbidden + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + } + err = hs.AcceptHandshake(buf.Writer) + if err != nil { + code = http.StatusBadRequest + fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) + buf.WriteString("\r\n") + buf.Flush() + return + } + conn = hs.NewServerConn(buf, rwc, req) + return +} + +// Server represents a server of a WebSocket. +type Server struct { + // Config is a WebSocket configuration for new WebSocket connection. + Config + + // Handshake is an optional function in WebSocket handshake. + // For example, you can check, or don't check Origin header. + // Another example, you can select config.Protocol. + Handshake func(*Config, *http.Request) error + + // Handler handles a WebSocket connection. + Handler +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.serveWebSocket(w, req) +} + +func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { + rwc, buf, err := w.(http.Hijacker).Hijack() + if err != nil { + panic("Hijack failed: " + err.Error()) + } + // The server should abort the WebSocket connection if it finds + // the client did not send a handshake that matches with protocol + // specification. + defer rwc.Close() + conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) + if err != nil { + return + } + if conn == nil { + panic("unexpected nil conn") + } + s.Handler(conn) +} + +// Handler is a simple interface to a WebSocket browser client. +// It checks if Origin header is valid URL by default. +// You might want to verify websocket.Conn.Config().Origin in the func. +// If you use Server instead of Handler, you could call websocket.Origin and +// check the origin in your Handshake func. So, if you want to accept +// non-browser clients, which do not send an Origin header, set a +// Server.Handshake that does not check the origin. +type Handler func(*Conn) + +func checkOrigin(config *Config, req *http.Request) (err error) { + config.Origin, err = Origin(config, req) + if err == nil && config.Origin == nil { + return fmt.Errorf("null origin") + } + return err +} + +// ServeHTTP implements the http.Handler interface for a WebSocket +func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s := Server{Handler: h, Handshake: checkOrigin} + s.serveWebSocket(w, req) +} diff --git a/vendor/golang.org/x/net/websocket/websocket.go b/vendor/golang.org/x/net/websocket/websocket.go new file mode 100644 index 0000000..da0dd96 --- /dev/null +++ b/vendor/golang.org/x/net/websocket/websocket.go @@ -0,0 +1,412 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package websocket implements a client and server for the WebSocket protocol +// as specified in RFC 6455. +package websocket + +import ( + "bufio" + "crypto/tls" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "sync" + "time" +) + +const ( + ProtocolVersionHybi13 = 13 + ProtocolVersionHybi = ProtocolVersionHybi13 + SupportedProtocolVersion = "13" + + ContinuationFrame = 0 + TextFrame = 1 + BinaryFrame = 2 + CloseFrame = 8 + PingFrame = 9 + PongFrame = 10 + UnknownFrame = 255 +) + +// ProtocolError represents WebSocket protocol errors. +type ProtocolError struct { + ErrorString string +} + +func (err *ProtocolError) Error() string { return err.ErrorString } + +var ( + ErrBadProtocolVersion = &ProtocolError{"bad protocol version"} + ErrBadScheme = &ProtocolError{"bad scheme"} + ErrBadStatus = &ProtocolError{"bad status"} + ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"} + ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"} + ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"} + ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"} + ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"} + ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"} + ErrBadFrame = &ProtocolError{"bad frame"} + ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"} + ErrNotWebSocket = &ProtocolError{"not websocket protocol"} + ErrBadRequestMethod = &ProtocolError{"bad method"} + ErrNotSupported = &ProtocolError{"not supported"} +) + +// Addr is an implementation of net.Addr for WebSocket. +type Addr struct { + *url.URL +} + +// Network returns the network type for a WebSocket, "websocket". +func (addr *Addr) Network() string { return "websocket" } + +// Config is a WebSocket configuration +type Config struct { + // A WebSocket server address. + Location *url.URL + + // A Websocket client origin. + Origin *url.URL + + // WebSocket subprotocols. + Protocol []string + + // WebSocket protocol version. + Version int + + // TLS config for secure WebSocket (wss). + TlsConfig *tls.Config + + // Additional header fields to be sent in WebSocket opening handshake. + Header http.Header + + handshakeData map[string]string +} + +// serverHandshaker is an interface to handle WebSocket server side handshake. +type serverHandshaker interface { + // ReadHandshake reads handshake request message from client. + // Returns http response code and error if any. + ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) + + // AcceptHandshake accepts the client handshake request and sends + // handshake response back to client. + AcceptHandshake(buf *bufio.Writer) (err error) + + // NewServerConn creates a new WebSocket connection. + NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn) +} + +// frameReader is an interface to read a WebSocket frame. +type frameReader interface { + // Reader is to read payload of the frame. + io.Reader + + // PayloadType returns payload type. + PayloadType() byte + + // HeaderReader returns a reader to read header of the frame. + HeaderReader() io.Reader + + // TrailerReader returns a reader to read trailer of the frame. + // If it returns nil, there is no trailer in the frame. + TrailerReader() io.Reader + + // Len returns total length of the frame, including header and trailer. + Len() int +} + +// frameReaderFactory is an interface to creates new frame reader. +type frameReaderFactory interface { + NewFrameReader() (r frameReader, err error) +} + +// frameWriter is an interface to write a WebSocket frame. +type frameWriter interface { + // Writer is to write payload of the frame. + io.WriteCloser +} + +// frameWriterFactory is an interface to create new frame writer. +type frameWriterFactory interface { + NewFrameWriter(payloadType byte) (w frameWriter, err error) +} + +type frameHandler interface { + HandleFrame(frame frameReader) (r frameReader, err error) + WriteClose(status int) (err error) +} + +// Conn represents a WebSocket connection. +type Conn struct { + config *Config + request *http.Request + + buf *bufio.ReadWriter + rwc io.ReadWriteCloser + + rio sync.Mutex + frameReaderFactory + frameReader + + wio sync.Mutex + frameWriterFactory + + frameHandler + PayloadType byte + defaultCloseStatus int +} + +// Read implements the io.Reader interface: +// it reads data of a frame from the WebSocket connection. +// if msg is not large enough for the frame data, it fills the msg and next Read +// will read the rest of the frame data. +// it reads Text frame or Binary frame. +func (ws *Conn) Read(msg []byte) (n int, err error) { + ws.rio.Lock() + defer ws.rio.Unlock() +again: + if ws.frameReader == nil { + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return 0, err + } + ws.frameReader, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return 0, err + } + if ws.frameReader == nil { + goto again + } + } + n, err = ws.frameReader.Read(msg) + if err == io.EOF { + if trailer := ws.frameReader.TrailerReader(); trailer != nil { + io.Copy(ioutil.Discard, trailer) + } + ws.frameReader = nil + goto again + } + return n, err +} + +// Write implements the io.Writer interface: +// it writes data as a frame to the WebSocket connection. +func (ws *Conn) Write(msg []byte) (n int, err error) { + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType) + if err != nil { + return 0, err + } + n, err = w.Write(msg) + w.Close() + if err != nil { + return n, err + } + return n, err +} + +// Close implements the io.Closer interface. +func (ws *Conn) Close() error { + err := ws.frameHandler.WriteClose(ws.defaultCloseStatus) + err1 := ws.rwc.Close() + if err != nil { + return err + } + return err1 +} + +func (ws *Conn) IsClientConn() bool { return ws.request == nil } +func (ws *Conn) IsServerConn() bool { return ws.request != nil } + +// LocalAddr returns the WebSocket Origin for the connection for client, or +// the WebSocket location for server. +func (ws *Conn) LocalAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Origin} + } + return &Addr{ws.config.Location} +} + +// RemoteAddr returns the WebSocket location for the connection for client, or +// the Websocket Origin for server. +func (ws *Conn) RemoteAddr() net.Addr { + if ws.IsClientConn() { + return &Addr{ws.config.Location} + } + return &Addr{ws.config.Origin} +} + +var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn") + +// SetDeadline sets the connection's network read & write deadlines. +func (ws *Conn) SetDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetDeadline(t) + } + return errSetDeadline +} + +// SetReadDeadline sets the connection's network read deadline. +func (ws *Conn) SetReadDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetReadDeadline(t) + } + return errSetDeadline +} + +// SetWriteDeadline sets the connection's network write deadline. +func (ws *Conn) SetWriteDeadline(t time.Time) error { + if conn, ok := ws.rwc.(net.Conn); ok { + return conn.SetWriteDeadline(t) + } + return errSetDeadline +} + +// Config returns the WebSocket config. +func (ws *Conn) Config() *Config { return ws.config } + +// Request returns the http request upgraded to the WebSocket. +// It is nil for client side. +func (ws *Conn) Request() *http.Request { return ws.request } + +// Codec represents a symmetric pair of functions that implement a codec. +type Codec struct { + Marshal func(v interface{}) (data []byte, payloadType byte, err error) + Unmarshal func(data []byte, payloadType byte, v interface{}) (err error) +} + +// Send sends v marshaled by cd.Marshal as single frame to ws. +func (cd Codec) Send(ws *Conn, v interface{}) (err error) { + data, payloadType, err := cd.Marshal(v) + if err != nil { + return err + } + ws.wio.Lock() + defer ws.wio.Unlock() + w, err := ws.frameWriterFactory.NewFrameWriter(payloadType) + if err != nil { + return err + } + _, err = w.Write(data) + w.Close() + return err +} + +// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v. +func (cd Codec) Receive(ws *Conn, v interface{}) (err error) { + ws.rio.Lock() + defer ws.rio.Unlock() + if ws.frameReader != nil { + _, err = io.Copy(ioutil.Discard, ws.frameReader) + if err != nil { + return err + } + ws.frameReader = nil + } +again: + frame, err := ws.frameReaderFactory.NewFrameReader() + if err != nil { + return err + } + frame, err = ws.frameHandler.HandleFrame(frame) + if err != nil { + return err + } + if frame == nil { + goto again + } + payloadType := frame.PayloadType() + data, err := ioutil.ReadAll(frame) + if err != nil { + return err + } + return cd.Unmarshal(data, payloadType, v) +} + +func marshal(v interface{}) (msg []byte, payloadType byte, err error) { + switch data := v.(type) { + case string: + return []byte(data), TextFrame, nil + case []byte: + return data, BinaryFrame, nil + } + return nil, UnknownFrame, ErrNotSupported +} + +func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + switch data := v.(type) { + case *string: + *data = string(msg) + return nil + case *[]byte: + *data = msg + return nil + } + return ErrNotSupported +} + +/* +Message is a codec to send/receive text/binary data in a frame on WebSocket connection. +To send/receive text frame, use string type. +To send/receive binary frame, use []byte type. + +Trivial usage: + + import "websocket" + + // receive text frame + var message string + websocket.Message.Receive(ws, &message) + + // send text frame + message = "hello" + websocket.Message.Send(ws, message) + + // receive binary frame + var data []byte + websocket.Message.Receive(ws, &data) + + // send binary frame + data = []byte{0, 1, 2} + websocket.Message.Send(ws, data) + +*/ +var Message = Codec{marshal, unmarshal} + +func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) { + msg, err = json.Marshal(v) + return msg, TextFrame, err +} + +func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) { + return json.Unmarshal(msg, v) +} + +/* +JSON is a codec to send/receive JSON data in a frame from a WebSocket connection. + +Trivial usage: + + import "websocket" + + type T struct { + Msg string + Count int + } + + // receive JSON type T + var data T + websocket.JSON.Receive(ws, &data) + + // send JSON type T + websocket.JSON.Send(ws, data) +*/ +var JSON = Codec{jsonMarshal, jsonUnmarshal}