diff --git a/cmd/keel/main.go b/cmd/keel/main.go
index d653d66d..31d3f717 100644
--- a/cmd/keel/main.go
+++ b/cmd/keel/main.go
@@ -30,6 +30,7 @@ import (
"github.com/keel-hq/keel/version"
// extensions
+ _ "github.com/keel-hq/keel/extension/notification/hipchat"
_ "github.com/keel-hq/keel/extension/notification/slack"
_ "github.com/keel-hq/keel/extension/notification/webhook"
diff --git a/constants/constants.go b/constants/constants.go
index e263cdd2..e64caffa 100644
--- a/constants/constants.go
+++ b/constants/constants.go
@@ -11,10 +11,14 @@ const WebhookEndpointEnv = "WEBHOOK_ENDPOINT"
// slack bot/token
const (
- EnvSlackToken = "SLACK_TOKEN"
- EnvSlackBotName = "SLACK_BOT_NAME"
- EnvSlackChannels = "SLACK_CHANNELS"
+ EnvSlackToken = "SLACK_TOKEN"
+ EnvSlackBotName = "SLACK_BOT_NAME"
+ EnvSlackChannels = "SLACK_CHANNELS"
EnvSlackApprovalsChannel = "SLACK_APPROVALS_CHANNEL"
+
+ EnvHipchatToken = "HIPCHAT_TOKEN"
+ EnvHipchatBotName = "HIPCHAT_BOT_NAME"
+ EnvHipchatChannels = "HIPCHAT_CHANNELS"
)
// EnvNotificationLevel - minimum level for notifications, defaults to info
diff --git a/extension/notification/hipchat/hipchat.go b/extension/notification/hipchat/hipchat.go
new file mode 100644
index 00000000..af117d2d
--- /dev/null
+++ b/extension/notification/hipchat/hipchat.go
@@ -0,0 +1,96 @@
+package hipchat
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+
+ "github.com/keel-hq/keel/constants"
+ "github.com/keel-hq/keel/extension/notification"
+ "github.com/keel-hq/keel/types"
+
+ log "github.com/Sirupsen/logrus"
+)
+
+type sender struct {
+ hipchatClient *hipchat.Client
+ channels []string
+ botName string
+}
+
+func init() {
+ notification.RegisterSender("hipchat", &sender{})
+}
+
+func (s *sender) Configure(config *notification.Config) (bool, error) {
+ var token string
+
+ if os.Getenv(constants.EnvHipchatToken) != "" {
+ token = os.Getenv(constants.EnvHipchatToken)
+ } else {
+ return false, nil
+ }
+ if os.Getenv(constants.EnvHipchatBotName) != "" {
+ s.botName = os.Getenv(constants.EnvHipchatBotName)
+ } else {
+ s.botName = "keel"
+ }
+
+ if os.Getenv(constants.EnvHipchatChannels) != "" {
+ channels := os.Getenv(constants.EnvHipchatChannels)
+ s.channels = strings.Split(channels, ",")
+ } else {
+ s.channels = []string{"general"}
+ }
+
+ s.hipchatClient = hipchat.NewClient(token)
+
+ log.WithFields(log.Fields{
+ "name": "hipchat",
+ "channels": s.channels,
+ }).Info("extension.notification.hipchat: sender configured")
+
+ return true, nil
+}
+
+func (s *sender) Send(event types.EventNotification) error {
+ msg := fmt.Sprintf("%s
%s", event.Type.String(), event.Message)
+
+ notification := &hipchat.NotificationRequest{
+ Color: getHipchatColor(event.Level.String()),
+ Message: msg,
+ Notify: true,
+ From: s.botName,
+ }
+
+ for _, channel := range s.channels {
+ _, err := s.hipchatClient.Room.Notification(channel, notification)
+ if err != nil {
+ log.WithFields(log.Fields{
+ "error": err,
+ "channel": channel,
+ }).Error("extension.notification.hipchat: failed to send notification")
+ }
+ }
+
+ return nil
+}
+
+func getHipchatColor(eventLevel string) hipchat.Color {
+ switch eventLevel {
+ case "error":
+ return "red"
+ case "info":
+ return "gray"
+ case "success":
+ return "green"
+ case "fatal":
+ return "purple"
+ case "warn":
+ return "yellow"
+ default:
+ return "gray"
+ }
+}
diff --git a/hack/deployment.sample.yml b/hack/deployment.sample.yml
index d893025f..569ec13d 100644
--- a/hack/deployment.sample.yml
+++ b/hack/deployment.sample.yml
@@ -25,7 +25,7 @@ spec:
value: "1"
# Enable/disable Helm provider
# - name: HELM_PROVIDER
- # value: "1"
+ # value: "1"
- name: PROJECT_ID
value: "my-project-id"
# - name: WEBHOOK_ENDPOINT
@@ -36,6 +36,10 @@ spec:
# value: general
# - name: SLACK_APPROVALS_CHANNEL
# value: approvals
+ # - name: HIPCHAT_TOKEN
+ # value: your-token-here
+ # - name: HIPCHAT_CHANNELS
+ # value: keel-bot
name: keel
command: ["/bin/keel"]
ports:
diff --git a/vendor/github.com/google/go-querystring/.gitignore b/vendor/github.com/google/go-querystring/.gitignore
new file mode 100644
index 00000000..9ed3b07c
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/.gitignore
@@ -0,0 +1 @@
+*.test
diff --git a/vendor/github.com/google/go-querystring/CONTRIBUTING.md b/vendor/github.com/google/go-querystring/CONTRIBUTING.md
new file mode 100644
index 00000000..51cf5cd1
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/CONTRIBUTING.md
@@ -0,0 +1,67 @@
+# How to contribute #
+
+We'd love to accept your patches and contributions to this project. There are
+a just a few small guidelines you need to follow.
+
+
+## Contributor License Agreement ##
+
+Contributions to any Google project must be accompanied by a Contributor
+License Agreement. This is not a copyright **assignment**, it simply gives
+Google permission to use and redistribute your contributions as part of the
+project.
+
+ * If you are an individual writing original source code and you're sure you
+ own the intellectual property, then you'll need to sign an [individual
+ CLA][].
+
+ * If you work for a company that wants to allow you to contribute your work,
+ then you'll need to sign a [corporate CLA][].
+
+You generally only need to submit a CLA once, so if you've already submitted
+one (even if it was for a different project), you probably don't need to do it
+again.
+
+[individual CLA]: https://developers.google.com/open-source/cla/individual
+[corporate CLA]: https://developers.google.com/open-source/cla/corporate
+
+
+## Submitting a patch ##
+
+ 1. It's generally best to start by opening a new issue describing the bug or
+ feature you're intending to fix. Even if you think it's relatively minor,
+ it's helpful to know what people are working on. Mention in the initial
+ issue that you are planning to work on that bug or feature so that it can
+ be assigned to you.
+
+ 1. Follow the normal process of [forking][] the project, and setup a new
+ branch to work in. It's important that each group of changes be done in
+ separate branches in order to ensure that a pull request only includes the
+ commits related to that bug or feature.
+
+ 1. Go makes it very simple to ensure properly formatted code, so always run
+ `go fmt` on your code before committing it. You should also run
+ [golint][] over your code. As noted in the [golint readme][], it's not
+ strictly necessary that your code be completely "lint-free", but this will
+ help you find common style issues.
+
+ 1. Any significant changes should almost always be accompanied by tests. The
+ project already has good test coverage, so look at some of the existing
+ tests if you're unsure how to go about it. [gocov][] and [gocov-html][]
+ are invaluable tools for seeing which parts of your code aren't being
+ exercised by your tests.
+
+ 1. Do your best to have [well-formed commit messages][] for each change.
+ This provides consistency throughout the project, and ensures that commit
+ messages are able to be formatted properly by various git tools.
+
+ 1. Finally, push the commits to your fork and submit a [pull request][].
+
+[forking]: https://help.github.com/articles/fork-a-repo
+[golint]: https://github.com/golang/lint
+[golint readme]: https://github.com/golang/lint/blob/master/README
+[gocov]: https://github.com/axw/gocov
+[gocov-html]: https://github.com/matm/gocov-html
+[well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
+[squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
+[pull request]: https://help.github.com/articles/creating-a-pull-request
diff --git a/vendor/github.com/google/go-querystring/LICENSE b/vendor/github.com/google/go-querystring/LICENSE
new file mode 100644
index 00000000..ae121a1e
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2013 Google. 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/github.com/google/go-querystring/README.md b/vendor/github.com/google/go-querystring/README.md
new file mode 100644
index 00000000..03e93703
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/README.md
@@ -0,0 +1,39 @@
+# go-querystring #
+
+go-querystring is Go library for encoding structs into URL query parameters.
+
+
+**Documentation:**
+**Build Status:** [](https://drone.io/github.com/google/go-querystring/latest)
+
+## Usage ##
+
+```go
+import "github.com/google/go-querystring/query"
+```
+
+go-querystring is designed to assist in scenarios where you want to construct a
+URL using a struct that represents the URL query parameters. You might do this
+to enforce the type safety of your parameters, for example, as is done in the
+[go-github][] library.
+
+The query package exports a single `Values()` function. A simple example:
+
+```go
+type Options struct {
+ Query string `url:"q"`
+ ShowAll bool `url:"all"`
+ Page int `url:"page"`
+}
+
+opt := Options{ "foo", true, 2 }
+v, _ := query.Values(opt)
+fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
+```
+
+[go-github]: https://github.com/google/go-github/commit/994f6f8405f052a117d2d0b500054341048fbb08
+
+## License ##
+
+This library is distributed under the BSD-style license found in the [LICENSE](./LICENSE)
+file.
diff --git a/vendor/github.com/google/go-querystring/query/encode.go b/vendor/github.com/google/go-querystring/query/encode.go
new file mode 100644
index 00000000..37080b19
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/query/encode.go
@@ -0,0 +1,320 @@
+// Copyright 2013 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 query implements encoding of structs into URL query parameters.
+//
+// As a simple example:
+//
+// type Options struct {
+// Query string `url:"q"`
+// ShowAll bool `url:"all"`
+// Page int `url:"page"`
+// }
+//
+// opt := Options{ "foo", true, 2 }
+// v, _ := query.Values(opt)
+// fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2"
+//
+// The exact mapping between Go values and url.Values is described in the
+// documentation for the Values() function.
+package query
+
+import (
+ "bytes"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var timeType = reflect.TypeOf(time.Time{})
+
+var encoderType = reflect.TypeOf(new(Encoder)).Elem()
+
+// Encoder is an interface implemented by any type that wishes to encode
+// itself into URL values in a non-standard way.
+type Encoder interface {
+ EncodeValues(key string, v *url.Values) error
+}
+
+// Values returns the url.Values encoding of v.
+//
+// Values expects to be passed a struct, and traverses it recursively using the
+// following encoding rules.
+//
+// Each exported struct field is encoded as a URL parameter unless
+//
+// - the field's tag is "-", or
+// - the field is empty and its tag specifies the "omitempty" option
+//
+// The empty values are false, 0, any nil pointer or interface value, any array
+// slice, map, or string of length zero, and any time.Time that returns true
+// for IsZero().
+//
+// The URL parameter name defaults to the struct field name but can be
+// specified in the struct field's tag value. The "url" key in the struct
+// field's tag value is the key name, followed by an optional comma and
+// options. For example:
+//
+// // Field is ignored by this package.
+// Field int `url:"-"`
+//
+// // Field appears as URL parameter "myName".
+// Field int `url:"myName"`
+//
+// // Field appears as URL parameter "myName" and the field is omitted if
+// // its value is empty
+// Field int `url:"myName,omitempty"`
+//
+// // Field appears as URL parameter "Field" (the default), but the field
+// // is skipped if empty. Note the leading comma.
+// Field int `url:",omitempty"`
+//
+// For encoding individual field values, the following type-dependent rules
+// apply:
+//
+// Boolean values default to encoding as the strings "true" or "false".
+// Including the "int" option signals that the field should be encoded as the
+// strings "1" or "0".
+//
+// time.Time values default to encoding as RFC3339 timestamps. Including the
+// "unix" option signals that the field should be encoded as a Unix time (see
+// time.Unix())
+//
+// Slice and Array values default to encoding as multiple URL values of the
+// same name. Including the "comma" option signals that the field should be
+// encoded as a single comma-delimited value. Including the "space" option
+// similarly encodes the value as a single space-delimited string. Including
+// the "semicolon" option will encode the value as a semicolon-delimited string.
+// Including the "brackets" option signals that the multiple URL values should
+// have "[]" appended to the value name. "numbered" will append a number to
+// the end of each incidence of the value name, example:
+// name0=value0&name1=value1, etc.
+//
+// Anonymous struct fields are usually encoded as if their inner exported
+// fields were fields in the outer struct, subject to the standard Go
+// visibility rules. An anonymous struct field with a name given in its URL
+// tag is treated as having that name, rather than being anonymous.
+//
+// Non-nil pointer values are encoded as the value pointed to.
+//
+// Nested structs are encoded including parent fields in value names for
+// scoping. e.g:
+//
+// "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
+//
+// All other values are encoded using their default string representation.
+//
+// Multiple fields that encode to the same URL parameter name will be included
+// as multiple URL values of the same name.
+func Values(v interface{}) (url.Values, error) {
+ values := make(url.Values)
+ val := reflect.ValueOf(v)
+ for val.Kind() == reflect.Ptr {
+ if val.IsNil() {
+ return values, nil
+ }
+ val = val.Elem()
+ }
+
+ if v == nil {
+ return values, nil
+ }
+
+ if val.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
+ }
+
+ err := reflectValue(values, val, "")
+ return values, err
+}
+
+// reflectValue populates the values parameter from the struct fields in val.
+// Embedded structs are followed recursively (using the rules defined in the
+// Values function documentation) breadth-first.
+func reflectValue(values url.Values, val reflect.Value, scope string) error {
+ var embedded []reflect.Value
+
+ typ := val.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ sf := typ.Field(i)
+ if sf.PkgPath != "" && !sf.Anonymous { // unexported
+ continue
+ }
+
+ sv := val.Field(i)
+ tag := sf.Tag.Get("url")
+ if tag == "-" {
+ continue
+ }
+ name, opts := parseTag(tag)
+ if name == "" {
+ if sf.Anonymous && sv.Kind() == reflect.Struct {
+ // save embedded struct for later processing
+ embedded = append(embedded, sv)
+ continue
+ }
+
+ name = sf.Name
+ }
+
+ if scope != "" {
+ name = scope + "[" + name + "]"
+ }
+
+ if opts.Contains("omitempty") && isEmptyValue(sv) {
+ continue
+ }
+
+ if sv.Type().Implements(encoderType) {
+ if !reflect.Indirect(sv).IsValid() {
+ sv = reflect.New(sv.Type().Elem())
+ }
+
+ m := sv.Interface().(Encoder)
+ if err := m.EncodeValues(name, &values); err != nil {
+ return err
+ }
+ continue
+ }
+
+ if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
+ var del byte
+ if opts.Contains("comma") {
+ del = ','
+ } else if opts.Contains("space") {
+ del = ' '
+ } else if opts.Contains("semicolon") {
+ del = ';'
+ } else if opts.Contains("brackets") {
+ name = name + "[]"
+ }
+
+ if del != 0 {
+ s := new(bytes.Buffer)
+ first := true
+ for i := 0; i < sv.Len(); i++ {
+ if first {
+ first = false
+ } else {
+ s.WriteByte(del)
+ }
+ s.WriteString(valueString(sv.Index(i), opts))
+ }
+ values.Add(name, s.String())
+ } else {
+ for i := 0; i < sv.Len(); i++ {
+ k := name
+ if opts.Contains("numbered") {
+ k = fmt.Sprintf("%s%d", name, i)
+ }
+ values.Add(k, valueString(sv.Index(i), opts))
+ }
+ }
+ continue
+ }
+
+ for sv.Kind() == reflect.Ptr {
+ if sv.IsNil() {
+ break
+ }
+ sv = sv.Elem()
+ }
+
+ if sv.Type() == timeType {
+ values.Add(name, valueString(sv, opts))
+ continue
+ }
+
+ if sv.Kind() == reflect.Struct {
+ reflectValue(values, sv, name)
+ continue
+ }
+
+ values.Add(name, valueString(sv, opts))
+ }
+
+ for _, f := range embedded {
+ if err := reflectValue(values, f, scope); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// valueString returns the string representation of a value.
+func valueString(v reflect.Value, opts tagOptions) string {
+ for v.Kind() == reflect.Ptr {
+ if v.IsNil() {
+ return ""
+ }
+ v = v.Elem()
+ }
+
+ if v.Kind() == reflect.Bool && opts.Contains("int") {
+ if v.Bool() {
+ return "1"
+ }
+ return "0"
+ }
+
+ if v.Type() == timeType {
+ t := v.Interface().(time.Time)
+ if opts.Contains("unix") {
+ return strconv.FormatInt(t.Unix(), 10)
+ }
+ return t.Format(time.RFC3339)
+ }
+
+ return fmt.Sprint(v.Interface())
+}
+
+// isEmptyValue checks if a value should be considered empty for the purposes
+// of omitting fields with the "omitempty" option.
+func isEmptyValue(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return v.Int() == 0
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+ return v.Uint() == 0
+ case reflect.Float32, reflect.Float64:
+ return v.Float() == 0
+ case reflect.Interface, reflect.Ptr:
+ return v.IsNil()
+ }
+
+ if v.Type() == timeType {
+ return v.Interface().(time.Time).IsZero()
+ }
+
+ return false
+}
+
+// tagOptions is the string following a comma in a struct field's "url" tag, or
+// the empty string. It does not include the leading comma.
+type tagOptions []string
+
+// parseTag splits a struct field's url tag into its name and comma-separated
+// options.
+func parseTag(tag string) (string, tagOptions) {
+ s := strings.Split(tag, ",")
+ return s[0], s[1:]
+}
+
+// Contains checks whether the tagOptions contains the specified option.
+func (o tagOptions) Contains(option string) bool {
+ for _, s := range o {
+ if s == option {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/google/go-querystring/query/encode_test.go b/vendor/github.com/google/go-querystring/query/encode_test.go
new file mode 100644
index 00000000..0f26a775
--- /dev/null
+++ b/vendor/github.com/google/go-querystring/query/encode_test.go
@@ -0,0 +1,328 @@
+// Copyright 2013 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 query
+
+import (
+ "fmt"
+ "net/url"
+ "reflect"
+ "testing"
+ "time"
+)
+
+type Nested struct {
+ A SubNested `url:"a"`
+ B *SubNested `url:"b"`
+ Ptr *SubNested `url:"ptr,omitempty"`
+}
+
+type SubNested struct {
+ Value string `url:"value"`
+}
+
+func TestValues_types(t *testing.T) {
+ str := "string"
+ strPtr := &str
+ timeVal := time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)
+
+ tests := []struct {
+ in interface{}
+ want url.Values
+ }{
+ {
+ // basic primitives
+ struct {
+ A string
+ B int
+ C uint
+ D float32
+ E bool
+ }{},
+ url.Values{
+ "A": {""},
+ "B": {"0"},
+ "C": {"0"},
+ "D": {"0"},
+ "E": {"false"},
+ },
+ },
+ {
+ // pointers
+ struct {
+ A *string
+ B *int
+ C **string
+ D *time.Time
+ }{
+ A: strPtr,
+ C: &strPtr,
+ D: &timeVal,
+ },
+ url.Values{
+ "A": {str},
+ "B": {""},
+ "C": {str},
+ "D": {"2000-01-01T12:34:56Z"},
+ },
+ },
+ {
+ // slices and arrays
+ struct {
+ A []string
+ B []string `url:",comma"`
+ C []string `url:",space"`
+ D [2]string
+ E [2]string `url:",comma"`
+ F [2]string `url:",space"`
+ G []*string `url:",space"`
+ H []bool `url:",int,space"`
+ I []string `url:",brackets"`
+ J []string `url:",semicolon"`
+ K []string `url:",numbered"`
+ }{
+ A: []string{"a", "b"},
+ B: []string{"a", "b"},
+ C: []string{"a", "b"},
+ D: [2]string{"a", "b"},
+ E: [2]string{"a", "b"},
+ F: [2]string{"a", "b"},
+ G: []*string{&str, &str},
+ H: []bool{true, false},
+ I: []string{"a", "b"},
+ J: []string{"a", "b"},
+ K: []string{"a", "b"},
+ },
+ url.Values{
+ "A": {"a", "b"},
+ "B": {"a,b"},
+ "C": {"a b"},
+ "D": {"a", "b"},
+ "E": {"a,b"},
+ "F": {"a b"},
+ "G": {"string string"},
+ "H": {"1 0"},
+ "I[]": {"a", "b"},
+ "J": {"a;b"},
+ "K0": {"a"},
+ "K1": {"b"},
+ },
+ },
+ {
+ // other types
+ struct {
+ A time.Time
+ B time.Time `url:",unix"`
+ C bool `url:",int"`
+ D bool `url:",int"`
+ }{
+ A: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
+ B: time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC),
+ C: true,
+ D: false,
+ },
+ url.Values{
+ "A": {"2000-01-01T12:34:56Z"},
+ "B": {"946730096"},
+ "C": {"1"},
+ "D": {"0"},
+ },
+ },
+ {
+ struct {
+ Nest Nested `url:"nest"`
+ }{
+ Nested{
+ A: SubNested{
+ Value: "that",
+ },
+ },
+ },
+ url.Values{
+ "nest[a][value]": {"that"},
+ "nest[b]": {""},
+ },
+ },
+ {
+ struct {
+ Nest Nested `url:"nest"`
+ }{
+ Nested{
+ Ptr: &SubNested{
+ Value: "that",
+ },
+ },
+ },
+ url.Values{
+ "nest[a][value]": {""},
+ "nest[b]": {""},
+ "nest[ptr][value]": {"that"},
+ },
+ },
+ {
+ nil,
+ url.Values{},
+ },
+ }
+
+ for i, tt := range tests {
+ v, err := Values(tt.in)
+ if err != nil {
+ t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
+ }
+
+ if !reflect.DeepEqual(tt.want, v) {
+ t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
+ }
+ }
+}
+
+func TestValues_omitEmpty(t *testing.T) {
+ str := ""
+ s := struct {
+ a string
+ A string
+ B string `url:",omitempty"`
+ C string `url:"-"`
+ D string `url:"omitempty"` // actually named omitempty, not an option
+ E *string `url:",omitempty"`
+ }{E: &str}
+
+ v, err := Values(s)
+ if err != nil {
+ t.Errorf("Values(%q) returned error: %v", s, err)
+ }
+
+ want := url.Values{
+ "A": {""},
+ "omitempty": {""},
+ "E": {""}, // E is included because the pointer is not empty, even though the string being pointed to is
+ }
+ if !reflect.DeepEqual(want, v) {
+ t.Errorf("Values(%q) returned %v, want %v", s, v, want)
+ }
+}
+
+type A struct {
+ B
+}
+
+type B struct {
+ C string
+}
+
+type D struct {
+ B
+ C string
+}
+
+type e struct {
+ B
+ C string
+}
+
+type F struct {
+ e
+}
+
+func TestValues_embeddedStructs(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ want url.Values
+ }{
+ {
+ A{B{C: "foo"}},
+ url.Values{"C": {"foo"}},
+ },
+ {
+ D{B: B{C: "bar"}, C: "foo"},
+ url.Values{"C": {"foo", "bar"}},
+ },
+ {
+ F{e{B: B{C: "bar"}, C: "foo"}}, // With unexported embed
+ url.Values{"C": {"foo", "bar"}},
+ },
+ }
+
+ for i, tt := range tests {
+ v, err := Values(tt.in)
+ if err != nil {
+ t.Errorf("%d. Values(%q) returned error: %v", i, tt.in, err)
+ }
+
+ if !reflect.DeepEqual(tt.want, v) {
+ t.Errorf("%d. Values(%q) returned %v, want %v", i, tt.in, v, tt.want)
+ }
+ }
+}
+
+func TestValues_invalidInput(t *testing.T) {
+ _, err := Values("")
+ if err == nil {
+ t.Errorf("expected Values() to return an error on invalid input")
+ }
+}
+
+type EncodedArgs []string
+
+func (m EncodedArgs) EncodeValues(key string, v *url.Values) error {
+ for i, arg := range m {
+ v.Set(fmt.Sprintf("%s.%d", key, i), arg)
+ }
+ return nil
+}
+
+func TestValues_Marshaler(t *testing.T) {
+ s := struct {
+ Args EncodedArgs `url:"arg"`
+ }{[]string{"a", "b", "c"}}
+ v, err := Values(s)
+ if err != nil {
+ t.Errorf("Values(%q) returned error: %v", s, err)
+ }
+
+ want := url.Values{
+ "arg.0": {"a"},
+ "arg.1": {"b"},
+ "arg.2": {"c"},
+ }
+ if !reflect.DeepEqual(want, v) {
+ t.Errorf("Values(%q) returned %v, want %v", s, v, want)
+ }
+}
+
+func TestValues_MarshalerWithNilPointer(t *testing.T) {
+ s := struct {
+ Args *EncodedArgs `url:"arg"`
+ }{}
+ v, err := Values(s)
+ if err != nil {
+ t.Errorf("Values(%q) returned error: %v", s, err)
+ }
+
+ want := url.Values{}
+ if !reflect.DeepEqual(want, v) {
+ t.Errorf("Values(%q) returned %v, want %v", s, v, want)
+ }
+}
+
+func TestTagParsing(t *testing.T) {
+ name, opts := parseTag("field,foobar,foo")
+ if name != "field" {
+ t.Fatalf("name = %q, want field", name)
+ }
+ for _, tt := range []struct {
+ opt string
+ want bool
+ }{
+ {"foobar", true},
+ {"foo", true},
+ {"bar", false},
+ {"field", false},
+ } {
+ if opts.Contains(tt.opt) != tt.want {
+ t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
+ }
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/.gitignore
new file mode 100644
index 00000000..97f5c520
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/.gitignore
@@ -0,0 +1,2 @@
+*.swp
+examples/hiptest
diff --git a/vendor/github.com/tbruyelle/hipchat-go/.travis.yml b/vendor/github.com/tbruyelle/hipchat-go/.travis.yml
new file mode 100644
index 00000000..e3ea49fc
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/.travis.yml
@@ -0,0 +1,17 @@
+language: go
+sudo: false
+go:
+ - 1.6
+ - 1.7
+ - 1.8
+
+install: go get -v ./hipchat
+script:
+ - go get -u github.com/golang/lint/golint
+ - golint ./...
+ - test `gofmt -l . | wc -l` = 0
+ - make
+
+matrix:
+ allow_failures:
+ go: tip
diff --git a/vendor/github.com/tbruyelle/hipchat-go/LICENSE b/vendor/github.com/tbruyelle/hipchat-go/LICENSE
new file mode 100644
index 00000000..e06d2081
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/LICENSE
@@ -0,0 +1,202 @@
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/vendor/github.com/tbruyelle/hipchat-go/Makefile b/vendor/github.com/tbruyelle/hipchat-go/Makefile
new file mode 100644
index 00000000..d2a34c6a
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/Makefile
@@ -0,0 +1,9 @@
+SRC_DIR=./hipchat
+
+include checks.mk
+
+default: test checks
+
+# test runs the unit tests and vets the code
+test:
+ go test -v $(SRC_DIR) $(TESTARGS) -timeout=30s -parallel=4
diff --git a/vendor/github.com/tbruyelle/hipchat-go/README.md b/vendor/github.com/tbruyelle/hipchat-go/README.md
new file mode 100644
index 00000000..003a5dd0
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/README.md
@@ -0,0 +1,58 @@
+# hipchat-go
+
+Go client library for the [HipChat API v2](https://www.hipchat.com/docs/apiv2).
+
+[](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat)
+[](https://travis-ci.org/tbruyelle/hipchat-go)
+
+Currently only a small part of the API is implemented, so pull requests are welcome.
+
+### Usage
+
+```go
+import "github.com/tbruyelle/hipchat-go/hipchat"
+```
+
+Build a new client, then use the `client.Room` service to spam all the rooms you have access to (not recommended):
+
+```go
+c := hipchat.NewClient("")
+
+opt := &hipchat.RoomsListOptions{IncludePrivate: true, IncludeArchived: true}
+rooms, _, err := c.Room.List(opt)
+if err != nil {
+ panic(err)
+}
+
+notifRq := &hipchat.NotificationRequest{Message: "Hey there!"}
+
+for _, room := range rooms.Items {
+ _, err := c.Room.Notification(room.Name, notifRq)
+ if err != nil {
+ panic(err)
+ }
+}
+```
+
+### Testing the auth token
+
+HipChat allows to [test the auth token](https://www.hipchat.com/docs/apiv2/auth#auth_test) by adding the `auth_test=true` param, into any API endpoints.
+
+You can do this with `hipchat-go` by setting the global var `hipchat.AuthTest`. Because the server response will be different from the one defined in the API endpoint, you need to check another global var `AuthTestReponse` to see if the authentication succeeds.
+
+```go
+hipchat.AuthTest = true
+
+client.Room.Get(42)
+
+_, ok := hipchat.AuthTestResponse["success"]
+fmt.Println("Authentification succeed :", ok)
+// Dont forget to reset the variable, or every other API calls
+// will be impacted.
+hipchat.AuthTest = false
+```
+
+---
+The code architecture is hugely inspired by [google/go-github](http://github.com/google/go-github).
+
+
diff --git a/vendor/github.com/tbruyelle/hipchat-go/checks.mk b/vendor/github.com/tbruyelle/hipchat-go/checks.mk
new file mode 100644
index 00000000..40af798b
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/checks.mk
@@ -0,0 +1,18 @@
+checks:
+ go test -race $(SRC_DIR)
+ @$(call checkbin,go tool vet,golang.org/x/tools/cms/vet)
+ go tool vet $(SRC_DIR)
+ @$(call checkbin,golint,github.com/golang/lint/golint)
+ golint -set_exit_status $(SRC_DIR)
+ @$(call checkbin,errcheck,github.com/kisielk/errcheck)
+ errcheck -ignore 'Close' -ignoretests $(SRC_DIR)
+ @$(call checkbin,structcheck,github.com/opennota/check/cmd/structcheck)
+ structcheck $(SRC_DIR)
+ @$(call checkbin,varcheck,github.com/opennota/check/cmd/varcheck)
+ varcheck $(SRC_DIR)
+
+checkbin = $1 2> /dev/null; if [ $$? -eq 127 ]; then\
+ echo "Retrieving missing tool $1...";\
+ go get $2; \
+ fi;
+
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore
new file mode 100644
index 00000000..a8292681
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/.gitignore
@@ -0,0 +1 @@
+hipfile
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md
new file mode 100644
index 00000000..44f3b717
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/README.md
@@ -0,0 +1,21 @@
+hipfile
+=====
+
+Sends the given file to the specified room or user.
+
+##### Usage
+
+```bash
+go build
+./hipfile --token= --room= --path=
+```
+
+##### Example
+
+Give it a try with the gopher.png file
+
+```bash
+go build
+./hipfile --token= --room= --path=gopher.png --message="Check out this one!"
+```
+
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png
new file mode 100644
index 00000000..1eb81f0b
Binary files /dev/null and b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png differ
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go
new file mode 100644
index 00000000..dc3e75c0
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/main.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+)
+
+var (
+ token = flag.String("token", "", "The HipChat AuthToken")
+ roomId = flag.String("room", "", "The HipChat room id")
+ userId = flag.String("user", "", "The HipChat user id")
+ path = flag.String("path", "", "The file path")
+ message = flag.String("message", "", "The message")
+ filename = flag.String("filename", "", "The name of the file")
+)
+
+func main() {
+ flag.Parse()
+ if *token == "" || *path == "" || ((*roomId == "") && (*userId == "")) {
+ flag.PrintDefaults()
+ return
+ }
+ c := hipchat.NewClient(*token)
+
+ shareFileRq := &hipchat.ShareFileRequest{Path: *path, Message: *message, Filename: *filename}
+
+ if *roomId != "" {
+ resp, err := c.Room.ShareFile(*roomId, shareFileRq)
+
+ if err != nil {
+ fmt.Printf("Error during room file share %q\n", err)
+ fmt.Printf("Server returns %+v\n", resp)
+ return
+ }
+ }
+
+ if *userId != "" {
+ resp, err := c.User.ShareFile(*userId, shareFileRq)
+
+ if err != nil {
+ fmt.Printf("Error during user file share %q\n", err)
+ fmt.Printf("Server returns %+v\n", resp)
+ return
+ }
+ }
+
+ fmt.Println("File sent !")
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore
new file mode 100644
index 00000000..b70bde05
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/.gitignore
@@ -0,0 +1 @@
+hiplol
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md
new file mode 100644
index 00000000..8455041b
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/README.md
@@ -0,0 +1,11 @@
+lol
+=====
+
+Prints the `(lol)` emoticon in the room in parameter.
+
+##### Usage
+
+```bash
+go build
+./hiplol --token= --room=
+```
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go
new file mode 100644
index 00000000..d500ae9c
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiplol/main.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+)
+
+var (
+ token = flag.String("token", "", "The HipChat AuthToken")
+ roomId = flag.String("room", "", "The HipChat room id")
+ test = flag.Bool("t", false, "Enable auth_test parameter")
+)
+
+func main() {
+ flag.Parse()
+ if *token == "" || *roomId == "" {
+ flag.PrintDefaults()
+ return
+ }
+ hipchat.AuthTest = *test
+
+ c := hipchat.NewClient(*token)
+
+ notifRq := &hipchat.NotificationRequest{Message: "Hey there!"}
+
+ resp, err := c.Room.Notification(*roomId, notifRq)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error during room notification %q\n", err)
+ fmt.Fprintf(os.Stderr, "Server returns %+v\n", resp)
+ return
+ }
+
+ if hipchat.AuthTest {
+ _, ok := hipchat.AuthTestResponse["success"]
+ fmt.Println("Authentification succeed :", ok)
+ } else {
+ fmt.Println("Lol sent !")
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore
new file mode 100644
index 00000000..9ad9e2c8
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/.gitignore
@@ -0,0 +1 @@
+hipshow
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md
new file mode 100644
index 00000000..e91be606
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/README.md
@@ -0,0 +1,11 @@
+hipshow
+=====
+
+Prints all rooms from your group
+
+##### Usage
+
+```bash
+go build
+./hipshow --token=
+```
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go
new file mode 100644
index 00000000..38476dee
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipshow/main.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+)
+
+var (
+ token = flag.String("token", "", "The HipChat AuthToken")
+ maxResults = flag.Int("maxResults", 5, "Max results per request")
+ includePrivate = flag.Bool("includePrivate", false, "Include private rooms?")
+ includeArchived = flag.Bool("includeArchived", false, "Include archived rooms?")
+)
+
+func main() {
+ flag.Parse()
+ if *token == "" {
+ flag.PrintDefaults()
+ return
+ }
+ c := hipchat.NewClient(*token)
+ startIndex := 0
+ totalRequests := 0
+ var allRooms []hipchat.Room
+
+ for {
+ opt := &hipchat.RoomsListOptions{
+ ListOptions: hipchat.ListOptions{StartIndex: startIndex, MaxResults: *maxResults},
+ IncludePrivate: *includePrivate,
+ IncludeArchived: *includeArchived}
+
+ rooms, resp, err := c.Room.List(opt)
+
+ if err != nil {
+ fmt.Printf("Error during room list req %q\n", err)
+ fmt.Printf("Server returns %+v\n", resp)
+ return
+ }
+
+ totalRequests++
+
+ allRooms = append(allRooms, rooms.Items...)
+ if rooms.Links.Next != "" {
+ startIndex += *maxResults
+ } else {
+ break
+ }
+ }
+
+ fmt.Printf("Your group has %d rooms, it took %d requests to retrieve all of them:\n",
+ len(allRooms), totalRequests)
+ for _, r := range allRooms {
+ fmt.Printf("%d %s \n", r.ID, r.Name)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore
new file mode 100644
index 00000000..5ec375c7
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/.gitignore
@@ -0,0 +1 @@
+hiptail
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md
new file mode 100644
index 00000000..6031abd2
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/README.md
@@ -0,0 +1,11 @@
+hiptail
+=====
+
+Prints recent messages in the room in parameter.
+
+##### Usage
+
+```bash
+go build
+./hiptail --token= --room=
+```
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go
new file mode 100644
index 00000000..dd6eebed
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hiptail/main.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "strings"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+)
+
+const (
+ maxMsgLen = 128
+ moreString = " [MORE]"
+)
+
+var (
+ token = flag.String("token", "", "The HipChat AuthToken")
+ roomId = flag.String("room", "", "The HipChat room id")
+)
+
+func main() {
+ flag.Parse()
+ if *token == "" || *roomId == "" {
+ flag.PrintDefaults()
+ return
+ }
+ c := hipchat.NewClient(*token)
+ hist, resp, err := c.Room.History(*roomId, &hipchat.HistoryOptions{})
+ if err != nil {
+ fmt.Printf("Error during room history req %q\n", err)
+ fmt.Printf("Server returns %+v\n", resp)
+ return
+ }
+ for _, m := range hist.Items {
+ from := ""
+ switch m.From.(type) {
+ case string:
+ from = m.From.(string)
+ case map[string]interface{}:
+ f := m.From.(map[string]interface{})
+ from = f["name"].(string)
+ }
+ msg := m.Message
+ if len(m.Message) > (maxMsgLen - len(moreString)) {
+ msg = fmt.Sprintf("%s%s", strings.Replace(m.Message[:len(m.Message)], "\n", " - ", -1), moreString)
+ }
+ fmt.Printf("%s [%s]: %s\n", from, m.Date, msg)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore
new file mode 100644
index 00000000..dd5f0150
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore
@@ -0,0 +1 @@
+hipwebhooks
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md
new file mode 100644
index 00000000..90f0d2eb
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md
@@ -0,0 +1,21 @@
+Get a list of the currently active webhooks for each room or for a specific room
+
+List all rooms:
+
+ $ go run main.go -token="$TOKEN"
+
+List a specific room:
+
+ $ go run main.go -token="$TOKEN" -room="$ROOM"
+
+Delete a webhook:
+
+ $ go run main.go -token="$TOKEN" -room="$ROOM" -action="delete" -webhook="$WEBHOOK"
+
+Create a webhook:
+
+ $ go run main.go -token="$TOKEN" -room="$ROOM" -action="delete" \
+ -name="$NAME" \
+ -event="$EVENT" \
+ -pattern="$PATTERN" \
+ -url="$URL"
diff --git a/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go
new file mode 100644
index 00000000..6a612e85
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+
+ "github.com/tbruyelle/hipchat-go/hipchat"
+)
+
+var (
+ token = flag.String("token", "", "The HipChat AuthToken")
+ roomId = flag.String("room", "", "A specific Room ID")
+ action = flag.String("action", "", "An action to take. Currently supported: create, delete")
+
+ // delete
+ webhookId = flag.String("webhook", "", "A specific Room ID")
+
+ // create
+ name = flag.String("name", "", "With action: create, name for the new webhook")
+ event = flag.String("event", "", "With action: create, event for the new webhook (enter, exit, message, notification, topic_change)")
+ pattern = flag.String("pattern", "", "With action: create, pattern for the new webhook")
+ url = flag.String("url", "", "With action: create, target URL for the new webhook")
+)
+
+func main() {
+ flag.Parse()
+ if *token == "" {
+ flag.PrintDefaults()
+ return
+ }
+ c := hipchat.NewClient(*token)
+
+ if *action == "" {
+ if *roomId == "" {
+ // If no room is given, look up all rooms and all of their webhooks
+ rooms, resp, err := c.Room.List(&hipchat.RoomsListOptions{})
+ handleRequestError(resp, err)
+
+ for _, room := range rooms.Items {
+ fmt.Printf("%-25v%10v\n", room.Name, room.ID)
+
+ hooks, resp, err := c.Room.ListWebhooks(room.ID, nil)
+ handleRequestError(resp, err)
+
+ for _, webhook := range hooks.Webhooks {
+ fmt.Printf(" %v %v\t%v\t%v\t%v\n", webhook.Name, webhook.ID, webhook.Event, webhook.URL, webhook.Links.Self)
+ }
+
+ fmt.Println("---")
+ }
+ } else {
+ // If room is given, just get the webhooks for that room
+ hooks, resp, err := c.Room.ListWebhooks(*roomId, nil)
+ handleRequestError(resp, err)
+
+ for _, webhook := range hooks.Webhooks {
+ fmt.Printf(" %v %v\t%v\t%v\t%v\n", webhook.Name, webhook.ID, webhook.Event, webhook.URL, webhook.Links.Self)
+ }
+ }
+ } else if *action == "create" {
+ if *roomId == "" {
+ fmt.Println("roomId is required for webhook creation")
+ flag.PrintDefaults()
+ return
+ }
+
+ webhook, resp, err := c.Room.CreateWebhook(*roomId, &hipchat.CreateWebhookRequest{
+ Name: *name,
+ Event: "room_" + *event,
+ Pattern: *pattern,
+ URL: *url,
+ })
+ handleRequestError(resp, err)
+ fmt.Printf("%v\t%v\t%v\t%v\n", webhook.Name, webhook.Event, webhook.URL, webhook.Links.Self)
+ } else if *action == "delete" {
+ if *roomId == "" {
+ fmt.Println("roomId is required for webhook deletion")
+ flag.PrintDefaults()
+ return
+ }
+
+ if *webhookId == "" {
+ fmt.Println("webhookId is required for webhook deletion")
+ flag.PrintDefaults()
+ return
+ }
+
+ resp, err := c.Room.DeleteWebhook(*roomId, *webhookId)
+ handleRequestError(resp, err)
+
+ fmt.Println("Deleted webhook with id", *webhookId)
+ }
+
+}
+
+func handleRequestError(resp *http.Response, err error) {
+ if err != nil {
+ if resp != nil {
+ fmt.Printf("Request Failed:\n%+v\n", resp)
+ body, _ := ioutil.ReadAll(resp.Body)
+ fmt.Printf("%+v\n", body)
+ } else {
+ fmt.Printf("Request failed, response is nil")
+ }
+ panic(err)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go
new file mode 100644
index 00000000..a8d2539a
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon.go
@@ -0,0 +1,52 @@
+package hipchat
+
+import (
+ "net/http"
+)
+
+// EmoticonService gives access to the emoticon related part of the API.
+type EmoticonService struct {
+ client *Client
+}
+
+// Emoticons represents a list of hipchat emoticons.
+type Emoticons struct {
+ Items []Emoticon `json:"items"`
+ StartIndex int `json:"startIndex"`
+ MaxResults int `json:"maxResults"`
+ Links PageLinks `json:"links"`
+}
+
+// Emoticon represents a hipchat emoticon.
+type Emoticon struct {
+ ID int `json:"id"`
+ URL string `json:"url"`
+ Links Links `json:"links"`
+ Shortcut string `json:"shortcut"`
+}
+
+// EmoticonsListOptions specifies the optionnal parameters of the EmoticonService.List
+// method.
+type EmoticonsListOptions struct {
+ ListOptions
+
+ // The type of emoticons to get (global, group or all)
+ Type string `url:"type,omitempty"`
+}
+
+// List returns the list of all the emoticons
+//
+// HipChat api docs : https://www.hipchat.com/docs/apiv2/method/get_all_emoticons
+func (e *EmoticonService) List(opt *EmoticonsListOptions) (*Emoticons, *http.Response, error) {
+ req, err := e.client.NewRequest("GET", "emoticon", opt, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ emoticons := new(Emoticons)
+ resp, err := e.client.Do(req, emoticons)
+ if err != nil {
+ return nil, resp, err
+ }
+ return emoticons, resp, nil
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go
new file mode 100644
index 00000000..aeef06a8
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/emoticon_test.go
@@ -0,0 +1,43 @@
+package hipchat
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestEmoticonList(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/emoticon", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "start-index": "1",
+ "max-results": "100",
+ "type": "type",
+ })
+ fmt.Fprintf(w, `{
+ "items": [{"id":1, "url":"u", "shortcut":"s", "links":{"self":"s"}}],
+ "startIndex": 1,
+ "maxResults": 1,
+ "links":{"self":"s", "prev":"p", "next":"n"}
+ }`)
+ })
+ want := &Emoticons{
+ Items: []Emoticon{{ID: 1, URL: "u", Shortcut: "s", Links: Links{Self: "s"}}},
+ StartIndex: 1,
+ MaxResults: 1,
+ Links: PageLinks{Links: Links{Self: "s"}, Prev: "p", Next: "n"},
+ }
+
+ opt := &EmoticonsListOptions{ListOptions{1, 100}, "type"}
+ emos, _, err := client.Emoticon.List(opt)
+ if err != nil {
+ t.Fatalf("Emoticon.List returned an error %v", err)
+ }
+ if !reflect.DeepEqual(want, emos) {
+ t.Errorf("Emoticon.List returned %+v, want %+v", emos, want)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go
new file mode 100644
index 00000000..bd1288ff
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat.go
@@ -0,0 +1,435 @@
+// Package hipchat provides a client for using the HipChat API v2.
+package hipchat
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "mime"
+ "net/http"
+ "net/url"
+ "os"
+ "os/user"
+ "path/filepath"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/google/go-querystring/query"
+)
+
+const (
+ defaultBaseURL = "https://api.hipchat.com/v2/"
+)
+
+// HTTPClient is an interface that allows overriding the http behavior
+// by providing custom http clients
+type HTTPClient interface {
+ Do(req *http.Request) (res *http.Response, err error)
+}
+
+// LimitData contains the latest Rate Limit or Flood Control data sent with every API call response.
+//
+// Limit is the number of API calls per period of time
+// Remaining is the current number of API calls that can be done before the ResetTime
+// ResetTime is the UTC time in Unix epoch format for when the full Limit of API calls will be restored.
+type LimitData struct {
+ Limit int
+ Remaining int
+ ResetTime int
+}
+
+// Client manages the communication with the HipChat API.
+//
+// LatestFloodControl contains the response from the latest API call's response headers X-Floodcontrol-{Limit, Remaining, ResetTime}
+// LatestRateLimit contains the response from the latest API call's response headers X-Ratelimit-{Limit, Remaining, ResetTime}
+// Room gives access to the /room part of the API.
+// User gives access to the /user part of the API.
+// Emoticon gives access to the /emoticon part of the API.
+type Client struct {
+ authToken string
+ BaseURL *url.URL
+ client HTTPClient
+ LatestFloodControl LimitData
+ LatestRateLimit LimitData
+ Room *RoomService
+ User *UserService
+ Emoticon *EmoticonService
+}
+
+// Links represents the HipChat default links.
+type Links struct {
+ Self string `json:"self"`
+}
+
+// PageLinks represents the HipChat page links.
+type PageLinks struct {
+ Links
+ Prev string `json:"prev"`
+ Next string `json:"next"`
+}
+
+// ID represents a HipChat id.
+// Use a separate struct because it can be a string or a int.
+type ID struct {
+ ID string `json:"id"`
+}
+
+// ListOptions specifies the optional parameters to various List methods that
+// support pagination.
+//
+// For paginated results, StartIndex represents the first page to display.
+// For paginated results, MaxResults reprensents the number of items per page. Default value is 100. Maximum value is 1000.
+type ListOptions struct {
+ StartIndex int `url:"start-index,omitempty"`
+ MaxResults int `url:"max-results,omitempty"`
+}
+
+// ExpandOptions specifies which Hipchat collections to automatically expand.
+// This functionality is primarily used to reduce the total time to receive the data.
+// It also reduces the sheer number of API calls from 1+N, to 1.
+//
+// cf: https://developer.atlassian.com/hipchat/guide/hipchat-rest-api/api-title-expansion
+type ExpandOptions struct {
+ Expand string `url:"expand,omitempty"`
+}
+
+// Color is set of hard-coded string values for the HipChat API for notifications.
+// cf: https://www.hipchat.com/docs/apiv2/method/send_room_notification
+type Color string
+
+const (
+ // ColorYellow is the color yellow
+ ColorYellow Color = "yellow"
+ // ColorGreen is the color green
+ ColorGreen Color = "green"
+ // ColorRed is the color red
+ ColorRed Color = "red"
+ // ColorPurple is the color purple
+ ColorPurple Color = "purple"
+ // ColorGray is the color gray
+ ColorGray Color = "gray"
+ // ColorRandom is the random "surprise me!" color
+ ColorRandom Color = "random"
+)
+
+// AuthTest can be set to true to test an auth token.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/auth#auth_test
+var AuthTest = false
+
+// AuthTestResponse will contain the server response of any
+// API calls if AuthTest=true.
+var AuthTestResponse = map[string]interface{}{}
+
+// RetryOnRateLimit can be set to true to automatically retry the API call until it succeeds,
+// subject to the RateLimitRetryPolicy settings. This behavior is only active when the API
+// call returns 429 (StatusTooManyRequests).
+var RetryOnRateLimit = false
+
+// RetryPolicy defines a RetryPolicy.
+//
+// MaxRetries is the maximum number of attempts to make before returning an error
+// MinDelay is the initial delay between attempts. This value is multiplied by the current attempt number.
+// MaxDelay is the largest delay between attempts.
+// JitterDelay is the amount of random jitter to add to the delay.
+// JitterBias is the amount of jitter to remove from the delay.
+//
+// The use of Jitter avoids inadvertant and undesirable synchronization of network
+// operations between otherwise unrelated clients.
+// cf: https://brooker.co.za/blog/2015/03/21/backoff.html and https://www.awsarchitectureblog.com/2015/03/backoff.html
+//
+// Using the values of JitterDelay = 250 milliseconds and a JitterBias of negative 125 milliseconds,
+// would result in a uniformly distributed Jitter between -125 and +125 milliseconds, centered
+// around the current trial Delay (between MinDelay and MaxDelay).
+//
+//
+type RetryPolicy struct {
+ MaxRetries int
+ MinDelay time.Duration
+ MaxDelay time.Duration
+ JitterDelay time.Duration
+ JitterBias time.Duration
+}
+
+// NoRateLimitRetryPolicy defines the "never retry an API call" policy's values.
+var NoRateLimitRetryPolicy = RetryPolicy{0, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond}
+
+// DefaultRateLimitRetryPolicy defines the "up to 300 times, 1 second apart, randomly adding an additional up-to-500 milliseconds of delay" policy.
+var DefaultRateLimitRetryPolicy = RetryPolicy{300, 1 * time.Second, 1 * time.Second, 500 * time.Millisecond, 0 * time.Millisecond}
+
+// RateLimitRetryPolicy can be set to a custom RetryPolicy's values,
+// or to one of the two predefined ones: NoRateLimitRetryPolicy or DefaultRateLimitRetryPolicy
+var RateLimitRetryPolicy = DefaultRateLimitRetryPolicy
+
+// NewClient returns a new HipChat API client. You must provide a valid
+// AuthToken retrieved from your HipChat account.
+func NewClient(authToken string) *Client {
+ baseURL, err := url.Parse(defaultBaseURL)
+ if err != nil {
+ panic(err)
+ }
+
+ c := &Client{
+ authToken: authToken,
+ BaseURL: baseURL,
+ client: http.DefaultClient,
+ }
+ c.Room = &RoomService{client: c}
+ c.User = &UserService{client: c}
+ c.Emoticon = &EmoticonService{client: c}
+ return c
+}
+
+// SetHTTPClient sets the http client for performing API requests.
+// This method allows overriding the default http client with any
+// implementation of the HTTPClient interface. It is typically used
+// to have finer control of the http request.
+// If a nil httpClient is provided, http.DefaultClient will be used.
+func (c *Client) SetHTTPClient(httpClient HTTPClient) {
+ if httpClient == nil {
+ c.client = http.DefaultClient
+ } else {
+ c.client = httpClient
+ }
+}
+
+// NewRequest creates an API request. This method can be used to performs
+// API request not implemented in this library. Otherwise it should not be
+// be used directly.
+// Relative URLs should always be specified without a preceding slash.
+func (c *Client) NewRequest(method, urlStr string, opt interface{}, body interface{}) (*http.Request, error) {
+ rel, err := addOptions(urlStr, opt)
+ if err != nil {
+ return nil, err
+ }
+
+ if AuthTest {
+ // Add the auth_test param
+ values := rel.Query()
+ values.Add("auth_test", strconv.FormatBool(AuthTest))
+ rel.RawQuery = values.Encode()
+ }
+
+ u := c.BaseURL.ResolveReference(rel)
+
+ buf := new(bytes.Buffer)
+ if body != nil {
+ err := json.NewEncoder(buf).Encode(body)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ req, err := http.NewRequest(method, u.String(), buf)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Authorization", "Bearer "+c.authToken)
+ req.Header.Add("Content-Type", "application/json")
+ return req, nil
+}
+
+// NewFileUploadRequest creates an API request to upload a file.
+// This method manually formats the request as multipart/related with a single part
+// of content-type application/json and a second part containing the file to be sent.
+// Relative URLs should always be specified without a preceding slash.
+func (c *Client) NewFileUploadRequest(method, urlStr string, v interface{}) (*http.Request, error) {
+ rel, err := url.Parse(urlStr)
+ if err != nil {
+ return nil, err
+ }
+
+ u := c.BaseURL.ResolveReference(rel)
+
+ shareFileReq, ok := v.(*ShareFileRequest)
+ if !ok {
+ return nil, errors.New("ShareFileRequest corrupted")
+ }
+ path := shareFileReq.Path
+ message := shareFileReq.Message
+
+ // Resolve home path
+ if strings.HasPrefix(path, "~") {
+ usr, _ := user.Current()
+ path = strings.Replace(path, "~", usr.HomeDir, 1)
+ }
+
+ // Check if file exists
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ return nil, err
+ }
+
+ // Read file and encode to base 64
+ file, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ b64 := base64.StdEncoding.EncodeToString(file)
+ contentType := mime.TypeByExtension(filepath.Ext(path))
+
+ // Set proper filename
+ filename := shareFileReq.Filename
+ if filename == "" {
+ filename = filepath.Base(path)
+ } else if filepath.Ext(filename) != filepath.Ext(path) {
+ filename = filepath.Base(filename) + filepath.Ext(path)
+ }
+
+ // Build request body
+ body := "--hipfileboundary\n" +
+ "Content-Type: application/json; charset=UTF-8\n" +
+ "Content-Disposition: attachment; name=\"metadata\"\n\n" +
+ "{\"message\": \"" + message + "\"}\n" +
+ "--hipfileboundary\n" +
+ "Content-Type: " + contentType + " charset=UTF-8\n" +
+ "Content-Transfer-Encoding: base64\n" +
+ "Content-Disposition: attachment; name=file; filename=" + filename + "\n\n" +
+ b64 + "\n" +
+ "--hipfileboundary\n"
+
+ b := &bytes.Buffer{}
+ _, err = b.Write([]byte(body))
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(method, u.String(), b)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Authorization", "Bearer "+c.authToken)
+ req.Header.Add("Content-Type", "multipart/related; boundary=hipfileboundary")
+
+ return req, err
+}
+
+// Do performs the request, the json received in the response is decoded
+// and stored in the value pointed by v.
+// Do can be used to perform the request created with NewRequest, which
+// should be used only for API requests not implemented in this library.
+func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
+ var policy = NoRateLimitRetryPolicy
+ if RetryOnRateLimit {
+ policy = RateLimitRetryPolicy
+ }
+
+ resp, err := c.doWithRetryPolicy(req, policy)
+ if err != nil {
+ return nil, err
+ }
+
+ if AuthTest {
+ // If AuthTest is enabled, the reponse won't be the
+ // one defined in the API endpoint.
+ err = json.NewDecoder(resp.Body).Decode(&AuthTestResponse)
+ } else {
+ if c := resp.StatusCode; c < 200 || c > 299 {
+ return resp, fmt.Errorf("Server returns status %d", c)
+ }
+
+ if v != nil {
+ defer resp.Body.Close()
+ if w, ok := v.(io.Writer); ok {
+ _, err = io.Copy(w, resp.Body)
+ } else {
+ err = json.NewDecoder(resp.Body).Decode(v)
+ }
+ }
+ }
+ return resp, err
+}
+
+func (c *Client) doWithRetryPolicy(req *http.Request, policy RetryPolicy) (*http.Response, error) {
+ currentTry := 0
+
+ for willContinue(currentTry, policy) {
+ currentTry = currentTry + 1
+ resp, err := c.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ c.captureRateLimits(resp)
+ if http.StatusTooManyRequests == resp.StatusCode {
+ resp.Body.Close()
+ if willContinue(currentTry, policy) {
+ sleep(currentTry, policy)
+ }
+ } else {
+ return resp, nil
+ }
+ }
+ return nil, fmt.Errorf("max retries exceeded (%d)", policy.MaxRetries)
+}
+
+func willContinue(currentTry int, policy RetryPolicy) bool {
+ return currentTry <= policy.MaxRetries
+}
+
+func sleep(currentTry int, policy RetryPolicy) {
+ jitter := time.Duration(rand.Int63n(2*int64(policy.JitterDelay))) - policy.JitterBias
+ linearDelay := time.Duration(currentTry)*policy.MinDelay + jitter
+ if linearDelay > policy.MaxDelay {
+ linearDelay = policy.MaxDelay
+ }
+ time.Sleep(time.Duration(linearDelay))
+}
+
+func setIfPresent(src string, dest *int) {
+ if len(src) > 0 {
+ v, err := strconv.Atoi(src)
+ if err != nil {
+ *dest = v
+ }
+ }
+}
+
+func (c *Client) captureRateLimits(resp *http.Response) {
+ // BY DESIGN:
+ // if and only if the HTTP Response headers contain the header are the values updated.
+ // The Floodcontrol limits are orthogonal to the API limits.
+ // API Limits are consumed for each and every API call.
+ // The default value for API limits are 500 (app token) or 100 (user token).
+ // Flood Control limits are consumed only when a user message, room message, or room notification is sent.
+ // The default value for Flood Control limits is 30 per minute per user token.
+ setIfPresent(resp.Header.Get("X-Ratelimit-Limit"), &c.LatestRateLimit.Limit)
+ setIfPresent(resp.Header.Get("X-Ratelimit-Remaining"), &c.LatestRateLimit.Remaining)
+ setIfPresent(resp.Header.Get("X-Ratelimit-Reset"), &c.LatestRateLimit.ResetTime)
+ setIfPresent(resp.Header.Get("X-Floodcontrol-Limit"), &c.LatestFloodControl.Limit)
+ setIfPresent(resp.Header.Get("X-Floodcontrol-Remaining"), &c.LatestFloodControl.Remaining)
+ setIfPresent(resp.Header.Get("X-Floodcontrol-Reset"), &c.LatestFloodControl.ResetTime)
+}
+
+// addOptions adds the parameters in opt as URL query parameters to s. opt
+// must be a struct whose fields may contain "url" tags.
+func addOptions(s string, opt interface{}) (*url.URL, error) {
+ u, err := url.Parse(s)
+ if err != nil {
+ return nil, err
+ }
+ if opt == nil {
+ return u, nil
+ }
+
+ v := reflect.ValueOf(opt)
+ if v.Kind() == reflect.Ptr && v.IsNil() {
+ // No query string to add
+ return u, nil
+ }
+
+ qs, err := query.Values(opt)
+ if err != nil {
+ return nil, err
+ }
+
+ u.RawQuery = qs.Encode()
+ return u, nil
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go
new file mode 100644
index 00000000..52383022
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/hipchat_test.go
@@ -0,0 +1,210 @@
+package hipchat
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "reflect"
+ "testing"
+)
+
+var (
+ mux *http.ServeMux
+ server *httptest.Server
+ client *Client
+)
+
+// setup sets up a test HTTP server and a hipchat.Client configured to talk
+// to that test server.
+// Tests should register handlers on mux which provide mock responses for
+// the API method being tested.
+func setup() {
+ // test server
+ mux = http.NewServeMux()
+ server = httptest.NewServer(mux)
+
+ // github client configured to use test server
+ client = NewClient("AuthToken")
+ url, _ := url.Parse(server.URL)
+ client.BaseURL = url
+}
+
+// teardown closes the test HTTP server.
+func teardown() {
+ server.Close()
+}
+
+func testMethod(t *testing.T, r *http.Request, want string) {
+ if got := r.Method; got != want {
+ t.Errorf("Request method: %v, want %v", got, want)
+ }
+}
+
+type values map[string]string
+
+func testFormValues(t *testing.T, r *http.Request, values values) {
+ want := url.Values{}
+ for k, v := range values {
+ want.Add(k, v)
+ }
+
+ r.ParseForm()
+ if got := r.Form; !reflect.DeepEqual(got, want) {
+ t.Errorf("Request parameters: %v, want %v", got, want)
+ }
+}
+
+func testHeader(t *testing.T, r *http.Request, header string, want string) {
+ if got := r.Header.Get(header); got != want {
+ t.Errorf("Header.Get(%q) returned %s, want %s", header, got, want)
+ }
+}
+
+func TestNewClient(t *testing.T) {
+ authToken := "AuthToken"
+
+ c := NewClient(authToken)
+
+ if c.authToken != authToken {
+ t.Errorf("NewClient authToken %s, want %s", c.authToken, authToken)
+ }
+ if c.BaseURL.String() != defaultBaseURL {
+ t.Errorf("NewClient BaseURL %s, want %s", c.BaseURL.String(), defaultBaseURL)
+ }
+ if c.client != http.DefaultClient {
+ t.Errorf("SetHTTPClient client %v, want %p", c.client, http.DefaultClient)
+ }
+}
+
+func TestSetHTTPClient(t *testing.T) {
+ c := NewClient("AuthToken")
+
+ httpClient := new(http.Client)
+ c.SetHTTPClient(httpClient)
+
+ if c.client != httpClient {
+ t.Errorf("SetHTTPClient client %v, want %p", c.client, httpClient)
+ }
+}
+
+type customHTTPClient struct{}
+
+func (c customHTTPClient) Do(*http.Request) (*http.Response, error) {
+ return nil, nil
+}
+
+func TestSetCustomHTTPClient(t *testing.T) {
+ c := NewClient("AuthToken")
+
+ httpClient := new(customHTTPClient)
+ c.SetHTTPClient(httpClient)
+
+ if c.client != httpClient {
+ t.Errorf("SetHTTPClient client %v, want %p", c.client, httpClient)
+ }
+}
+
+func TestSetHTTPClient_NilHTTPClient(t *testing.T) {
+ c := NewClient("AuthToken")
+
+ c.SetHTTPClient(nil)
+
+ if c.client != http.DefaultClient {
+ t.Errorf("SetHTTPClient client %v, want %p", c.client, http.DefaultClient)
+ }
+}
+
+func TestNewRequest(t *testing.T) {
+ c := NewClient("AuthToken")
+
+ inURL, outURL := "foo", defaultBaseURL+"foo?max-results=100&start-index=1"
+ opt := &ListOptions{StartIndex: 1, MaxResults: 100}
+ inBody, outBody := &NotificationRequest{Message: "Hello"}, `{"message":"Hello"}`+"\n"
+ r, _ := c.NewRequest("GET", inURL, opt, inBody)
+
+ if r.URL.String() != outURL {
+ t.Errorf("NewRequest URL %s, want %s", r.URL.String(), outURL)
+ }
+ body, _ := ioutil.ReadAll(r.Body)
+ if string(body) != outBody {
+ t.Errorf("NewRequest body %s, want %s", body, outBody)
+ }
+ authorization := r.Header.Get("Authorization")
+ if authorization != "Bearer "+c.authToken {
+ t.Errorf("NewRequest authorization header %s, want %s", authorization, "Bearer "+c.authToken)
+ }
+ contentType := r.Header.Get("Content-Type")
+ if contentType != "application/json" {
+ t.Errorf("NewRequest Content-Type header %s, want application/json", contentType)
+ }
+}
+
+func TestNewRequest_AuthTestEnabled(t *testing.T) {
+ AuthTest = true
+ defer func() { AuthTest = false }()
+ c := NewClient("AuthToken")
+
+ inURL, outURL := "foo", defaultBaseURL+"foo?auth_test=true"
+ r, _ := c.NewRequest("GET", inURL, nil, nil)
+
+ if r.URL.String() != outURL {
+ t.Errorf("NewRequest URL %s, want %s", r.URL.String(), outURL)
+ }
+}
+
+func TestDo(t *testing.T) {
+ setup()
+ defer teardown()
+
+ type foo struct {
+ Bar int
+ }
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if m := "GET"; m != r.Method {
+ t.Errorf("Request method = %v, want %v", r.Method, m)
+ }
+ fmt.Fprintf(w, `{"Bar":1}`)
+ })
+ req, _ := client.NewRequest("GET", "/", nil, nil)
+ body := new(foo)
+
+ _, err := client.Do(req, body)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := &foo{Bar: 1}
+ if !reflect.DeepEqual(body, want) {
+ t.Errorf("Response body = %v, want %v", body, want)
+ }
+}
+
+func TestDo_AuthTestEnabled(t *testing.T) {
+ AuthTest = true
+ defer func() { AuthTest = false }()
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ if m := "GET"; m != r.Method {
+ t.Errorf("Request method = %v, want %v", r.Method, m)
+ }
+ if r.URL.Query().Get("auth_test") == "true" {
+ fmt.Fprintf(w, `{"success":{ "code": 202, "type": "Accepted", "message": "This auth_token has access to use this method." }}`)
+ } else {
+ fmt.Fprintf(w, `{"Bar":1}`)
+ }
+ })
+ req, _ := client.NewRequest("GET", "/", nil, nil)
+
+ _, err := client.Do(req, nil)
+
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := AuthTestResponse["success"]; !ok {
+ t.Errorf("Response body = %v, want succeed", AuthTestResponse)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go
new file mode 100644
index 00000000..7211ba80
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go
@@ -0,0 +1,107 @@
+package hipchat
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+// ClientCredentials represents the OAuth2 client ID and secret for an integration
+type ClientCredentials struct {
+ ClientID string
+ ClientSecret string
+}
+
+// OAuthAccessToken represents a newly created Hipchat OAuth access token
+type OAuthAccessToken struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn uint32 `json:"expires_in"`
+ GroupID uint32 `json:"group_id"`
+ GroupName string `json:"group_name"`
+ Scope string `json:"scope"`
+ TokenType string `json:"token_type"`
+}
+
+// CreateClient creates a new client from this OAuth token
+func (t *OAuthAccessToken) CreateClient() *Client {
+ return NewClient(t.AccessToken)
+}
+
+// GenerateToken returns back an access token for a given integration's client ID and client secret
+//
+// HipChat API documentation: https://www.hipchat.com/docs/apiv2/method/generate_token
+func (c *Client) GenerateToken(credentials ClientCredentials, scopes []string) (*OAuthAccessToken, *http.Response, error) {
+ rel, err := url.Parse("oauth/token")
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ u := c.BaseURL.ResolveReference(rel)
+
+ params := url.Values{"grant_type": {"client_credentials"},
+ "scope": {strings.Join(scopes, " ")}}
+ req, err := http.NewRequest("POST", u.String(), strings.NewReader(params.Encode()))
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req.SetBasicAuth(credentials.ClientID, credentials.ClientSecret)
+ req.Header.Set("Content-type", "application/x-www-form-urlencoded")
+
+ resp, err := c.client.Do(req)
+
+ if err != nil {
+ return nil, resp, err
+ }
+
+ if resp.StatusCode != 200 {
+ content, readerr := ioutil.ReadAll(resp.Body)
+
+ if readerr != nil {
+ content = []byte("Unknown error")
+ }
+
+ return nil, resp, fmt.Errorf("Couldn't retrieve access token: %s", content)
+ }
+
+ content, err := ioutil.ReadAll(resp.Body)
+
+ var token OAuthAccessToken
+ err = json.Unmarshal(content, &token)
+
+ return &token, resp, err
+}
+
+const (
+ // ScopeAdminGroup - Perform group administrative tasks
+ ScopeAdminGroup = "admin_group"
+
+ // ScopeAdminRoom - Perform room administrative tasks
+ ScopeAdminRoom = "admin_room"
+
+ // ScopeImportData - Import users, rooms, and chat history. Only available for select add-ons.
+ ScopeImportData = "import_data"
+
+ // ScopeManageRooms - Create, update, and remove rooms
+ ScopeManageRooms = "manage_rooms"
+
+ // ScopeSendMessage - Send private one-on-one messages
+ ScopeSendMessage = "send_message"
+
+ // ScopeSendNotification - Send room notifications
+ ScopeSendNotification = "send_notification"
+
+ // ScopeViewGroup - View users, rooms, and other group information
+ ScopeViewGroup = "view_group"
+
+ // ScopeViewMessages - View messages from chat rooms and private chats you have access to
+ ScopeViewMessages = "view_messages"
+
+ // ScopeViewRoom - View room information and participants, but not history
+ ScopeViewRoom = "view_room"
+)
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go
new file mode 100644
index 00000000..f9bbac8c
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth_test.go
@@ -0,0 +1,79 @@
+package hipchat
+
+import (
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestGetAccessToken(t *testing.T) {
+ setup()
+ defer teardown()
+
+ clientID := "client-abcdef"
+ clientSecret := "secret-12345"
+
+ mux.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.String() != "/oauth/token" {
+ t.Errorf("Incorrect URL = %v, want %v", r.URL, "/oauth/token")
+ }
+
+ testMethod(t, r, "POST")
+ testHeader(t, r, "Authorization", "Basic Y2xpZW50LWFiY2RlZjpzZWNyZXQtMTIzNDU=")
+ testFormValues(t, r, values{
+
+ "grant_type": "client_credentials",
+ "scope": "send_notification view_room",
+ })
+ fmt.Fprintf(w, `
+ {
+ "access_token": "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456",
+ "expires_in": 3599,
+ "group_id": 123456,
+ "group_name": "TestGroup",
+ "scope": "send_notification view_room",
+ "token_type": "bearer"
+ }
+ `)
+ })
+ want := &OAuthAccessToken{
+ AccessToken: "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456",
+ ExpiresIn: 3599,
+ GroupID: 123456,
+ GroupName: "TestGroup",
+ Scope: "send_notification view_room",
+ TokenType: "bearer",
+ }
+
+ credentials := ClientCredentials{ClientID: clientID, ClientSecret: clientSecret}
+
+ token, _, err := client.GenerateToken(credentials, []string{ScopeSendNotification, ScopeViewRoom})
+ if err != nil {
+ t.Fatalf("Client.GetAccessToken returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, token) {
+ t.Errorf("Client.GetAccessToken returned %+v, want %+v", token, want)
+ }
+}
+
+func TestCreateClientFromAccessToken(t *testing.T) {
+ token := OAuthAccessToken{
+ AccessToken: "q0M8p3UrBL96uHb79x4qdR2r6oEnCeajcg123456",
+ ExpiresIn: 3599,
+ GroupID: 123456,
+ GroupName: "TestGroup",
+ Scope: "send_notification view_room",
+ TokenType: "bearer",
+ }
+
+ client := token.CreateClient()
+
+ if client.authToken != token.AccessToken {
+ t.Fatalf(
+ "Client auth token does not match access token: %v != %v",
+ client.authToken,
+ token.AccessToken,
+ )
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go
new file mode 100644
index 00000000..50e1a6d6
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go
@@ -0,0 +1,659 @@
+package hipchat
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// RoomService gives access to the room related methods of the API.
+type RoomService struct {
+ client *Client
+}
+
+// Rooms represents a HipChat room list.
+type Rooms struct {
+ Items []Room `json:"items"`
+ StartIndex int `json:"startIndex"`
+ MaxResults int `json:"maxResults"`
+ Links PageLinks `json:"links"`
+}
+
+// Room represents a HipChat room.
+type Room struct {
+ ID int `json:"id"`
+ Links RoomLinks `json:"links"`
+ Name string `json:"name"`
+ XMPPJid string `json:"xmpp_jid"`
+ Statistics RoomStatistics `json:"statistics"`
+ Created string `json:"created"`
+ IsArchived bool `json:"is_archived"`
+ Privacy string `json:"privacy"`
+ IsGuestAccessible bool `json:"is_guess_accessible"`
+ Topic string `json:"topic"`
+ Participants []User `json:"participants"`
+ Owner User `json:"owner"`
+ GuestAccessURL string `json:"guest_access_url"`
+}
+
+// RoomStatistics represents the HipChat room statistics.
+type RoomStatistics struct {
+ Links Links `json:"links"`
+ MessagesSent int `json:"messages_sent,omitempty"`
+ LastActive string `json:"last_active,omitempty"`
+}
+
+// CreateRoomRequest represents a HipChat room creation request.
+type CreateRoomRequest struct {
+ Topic string `json:"topic,omitempty"`
+ GuestAccess bool `json:"guest_access,omitempty"`
+ Name string `json:"name,omitempty"`
+ OwnerUserID string `json:"owner_user_id,omitempty"`
+ Privacy string `json:"privacy,omitempty"`
+}
+
+// UpdateRoomRequest represents a HipChat room update request.
+type UpdateRoomRequest struct {
+ Name string `json:"name"`
+ Topic string `json:"topic"`
+ IsGuestAccess bool `json:"is_guest_accessible"`
+ IsArchived bool `json:"is_archived"`
+ Privacy string `json:"privacy"`
+ Owner ID `json:"owner"`
+}
+
+// RoomLinks represents the HipChat room links.
+type RoomLinks struct {
+ Links
+ Webhooks string `json:"webhooks"`
+ Members string `json:"members"`
+ Participants string `json:"participants"`
+}
+
+// NotificationRequest represents a HipChat room notification request.
+type NotificationRequest struct {
+ Color Color `json:"color,omitempty"`
+ Message string `json:"message,omitempty"`
+ Notify bool `json:"notify,omitempty"`
+ MessageFormat string `json:"message_format,omitempty"`
+ From string `json:"from,omitempty"`
+ Card *Card `json:"card,omitempty"`
+}
+
+// RoomMessageRequest represents a Hipchat room message request.
+type RoomMessageRequest struct {
+ Message string `json:"message"`
+}
+
+// Card is used to send information as messages to Hipchat rooms
+type Card struct {
+ Style string `json:"style"`
+ Description CardDescription `json:"description"`
+ Format string `json:"format,omitempty"`
+ URL string `json:"url,omitempty"`
+ Title string `json:"title"`
+ Thumbnail *Thumbnail `json:"thumbnail,omitempty"`
+ Activity *Activity `json:"activity,omitempty"`
+ Attributes []Attribute `json:"attributes,omitempty"`
+ ID string `json:"id,omitempty"`
+ Icon *Icon `json:"icon,omitempty"`
+}
+
+const (
+ // CardStyleFile represents a Card notification related to a file
+ CardStyleFile = "file"
+
+ // CardStyleImage represents a Card notification related to an image
+ CardStyleImage = "image"
+
+ // CardStyleApplication represents a Card notification related to an application
+ CardStyleApplication = "application"
+
+ // CardStyleLink represents a Card notification related to a link
+ CardStyleLink = "link"
+
+ // CardStyleMedia represents a Card notiifcation related to media
+ CardStyleMedia = "media"
+)
+
+// CardDescription represents the main content of the Card
+type CardDescription struct {
+ Format string
+ Value string
+}
+
+// MarshalJSON serializes a CardDescription into JSON
+func (c CardDescription) MarshalJSON() ([]byte, error) {
+ if c.Format == "" {
+ return json.Marshal(c.Value)
+ }
+
+ obj := make(map[string]string)
+ obj["format"] = c.Format
+ obj["value"] = c.Value
+
+ return json.Marshal(obj)
+}
+
+// UnmarshalJSON deserializes a JSON-serialized CardDescription
+func (c *CardDescription) UnmarshalJSON(data []byte) error {
+ // Compact the JSON to make it easier to process below
+ buffer := bytes.NewBuffer([]byte{})
+ err := json.Compact(buffer, data)
+ if err != nil {
+ return err
+ }
+ data = buffer.Bytes()
+
+ // Since Description can be either a string value or an object, we
+ // must check and deserialize appropriately
+
+ if data[0] == 123 { // == }
+ obj := make(map[string]string)
+
+ err = json.Unmarshal(data, &obj)
+ if err != nil {
+ return err
+ }
+
+ c.Format = obj["format"]
+ c.Value = obj["value"]
+ } else {
+ c.Format = ""
+ err = json.Unmarshal(data, &c.Value)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// Icon represents an icon
+type Icon struct {
+ URL string `json:"url"`
+ URL2x string `json:"url@2x,omitempty"`
+}
+
+// Thumbnail represents a thumbnail image
+type Thumbnail struct {
+ URL string `json:"url"`
+ URL2x string `json:"url@2x,omitempty"`
+ Width uint `json:"width,omitempty"`
+ Height uint `json:"height,omitempty"`
+}
+
+// Attribute represents an attribute on a Card
+type Attribute struct {
+ Label string `json:"label,omitempty"`
+ Value AttributeValue `json:"value"`
+}
+
+// AttributeValue represents the value of an attribute
+type AttributeValue struct {
+ URL string `json:"url,omitempty"`
+ Style string `json:"style,omitempty"`
+ Type string `json:"type,omitempty"`
+ Label string `json:"label,omitempty"`
+ Value string `json:"value,omitempty"`
+ Icon *Icon `json:"icon,omitempty"`
+}
+
+// Activity represents an activity that occurred
+type Activity struct {
+ Icon *Icon `json:"icon,omitempty"`
+ HTML string `json:"html,omitempty"`
+}
+
+// ShareFileRequest represents a HipChat room file share request.
+type ShareFileRequest struct {
+ Path string `json:"path"`
+ Filename string `json:"filename,omitempty"`
+ Message string `json:"message,omitempty"`
+}
+
+// History represents a HipChat room chat history.
+type History struct {
+ Items []Message `json:"items"`
+ StartIndex int `json:"startIndex"`
+ MaxResults int `json:"maxResults"`
+ Links PageLinks `json:"links"`
+}
+
+// Message represents a HipChat message.
+type Message struct {
+ Date string `json:"date"`
+ From interface{} `json:"from"` // string | obj <- weak
+ ID string `json:"id"`
+ Mentions []User `json:"mentions"`
+ Message string `json:"message"`
+ MessageFormat string `json:"message_format"`
+ Type string `json:"type"`
+}
+
+// SetTopicRequest represents a hipchat update topic request
+type SetTopicRequest struct {
+ Topic string `json:"topic"`
+}
+
+// InviteRequest represents a hipchat invite to room request
+type InviteRequest struct {
+ Reason string `json:"reason"`
+}
+
+// AddMemberRequest represents a HipChat add member request
+type AddMemberRequest struct {
+ Roles []string `json:"roles,omitempty"`
+}
+
+// GlanceRequest represents a HipChat room ui glance
+type GlanceRequest struct {
+ Key string `json:"key"`
+ Name GlanceName `json:"name"`
+ Target string `json:"target"`
+ QueryURL string `json:"queryUrl,omitempty"`
+ Icon Icon `json:"icon"`
+ Conditions []*GlanceCondition `json:"conditions,omitempty"`
+}
+
+// GlanceName represents a glance name
+type GlanceName struct {
+ Value string `json:"value"`
+ I18n string `json:"i18n,omitempty"`
+}
+
+// GlanceCondition represents a condition to determine whether a glance is displayed
+type GlanceCondition struct {
+ Condition string `json:"condition"`
+ Params map[string]string `json:"params"`
+ Invert bool `json:"invert"`
+}
+
+// GlanceUpdateRequest represents a HipChat room ui glance update request
+type GlanceUpdateRequest struct {
+ Glance []*GlanceUpdate `json:"glance"`
+}
+
+// GlanceUpdate represents a component of a HipChat room ui glance update
+type GlanceUpdate struct {
+ Key string `json:"key"`
+ Content GlanceContent `json:"content"`
+}
+
+// GlanceContent is a component of a Glance
+type GlanceContent struct {
+ Status *GlanceStatus `json:"status,omitempty"`
+ Metadata interface{} `json:"metadata,omitempty"`
+ Label AttributeValue `json:"label"` // AttributeValue{Type, Label}
+}
+
+// GlanceStatus is a status field component of a GlanceContent
+type GlanceStatus struct {
+ Type string `json:"type"` // "lozenge" | "icon"
+ Value interface{} `json:"value"` // AttributeValue{Type, Label} | Icon{URL, URL2x}
+}
+
+// UnmarshalJSON deserializes a JSON-serialized GlanceStatus
+func (gs *GlanceStatus) UnmarshalJSON(data []byte) error {
+ // Compact the JSON to make it easier to process below
+ buffer := bytes.NewBuffer([]byte{})
+ err := json.Compact(buffer, data)
+ if err != nil {
+ return err
+ }
+ data = buffer.Bytes()
+
+ // Since Value can be either an AttributeValue or an Icon, we
+ // must check and deserialize appropriately
+ obj := make(map[string]interface{})
+
+ err = json.Unmarshal(data, &obj)
+ if err != nil {
+ return err
+ }
+
+ for _, field := range []string{"type", "value"} {
+ if obj[field] == nil {
+ return fmt.Errorf("missing %s field", field)
+ }
+ }
+
+ gs.Type = obj["type"].(string)
+ val := obj["value"].(map[string]interface{})
+
+ valueMap := map[string][]string{
+ "lozenge": []string{"type", "label"},
+ "icon": []string{"url", "url@2x"},
+ }
+
+ if valueMap[gs.Type] == nil {
+ return fmt.Errorf("invalid GlanceStatus type: %s", gs.Type)
+ }
+
+ for _, field := range valueMap[gs.Type] {
+ if val[field] == nil {
+ return fmt.Errorf("%s missing %s field", gs.Type, field)
+ }
+ _, ok := val[field].(string)
+ if !ok {
+ return fmt.Errorf("could not convert %s field %s to string", gs.Type, field)
+ }
+ }
+
+ // Can safely perform type coercion
+ switch gs.Type {
+ case "lozenge":
+ gs.Value = AttributeValue{Type: val["type"].(string), Label: val["label"].(string)}
+ case "icon":
+ gs.Value = Icon{URL: val["url"].(string), URL2x: val["url@2x"].(string)}
+ }
+
+ return nil
+}
+
+// AddAttribute adds an attribute to a Card
+func (c *Card) AddAttribute(mainLabel, subLabel, url, iconURL string) {
+ attr := Attribute{Label: mainLabel}
+ attr.Value = AttributeValue{Label: subLabel, URL: url, Icon: &Icon{URL: iconURL}}
+
+ c.Attributes = append(c.Attributes, attr)
+}
+
+// RoomsListOptions specifies the optional parameters of the RoomService.List
+// method.
+type RoomsListOptions struct {
+ ListOptions
+ ExpandOptions
+
+ // Include private rooms in the result, API defaults to true
+ IncludePrivate bool `url:"include-private,omitempty"`
+
+ // Include archived rooms in the result, API defaults to false
+ IncludeArchived bool `url:"include-archived,omitempty"`
+}
+
+// List returns all the rooms authorized.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_rooms
+func (r *RoomService) List(opt *RoomsListOptions) (*Rooms, *http.Response, error) {
+ req, err := r.client.NewRequest("GET", "room", opt, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ rooms := new(Rooms)
+ resp, err := r.client.Do(req, rooms)
+ if err != nil {
+ return nil, resp, err
+ }
+ return rooms, resp, nil
+}
+
+// Get returns the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_room
+func (r *RoomService) Get(id string) (*Room, *http.Response, error) {
+ req, err := r.client.NewRequest("GET", fmt.Sprintf("room/%s", id), nil, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ room := new(Room)
+ resp, err := r.client.Do(req, room)
+ if err != nil {
+ return nil, resp, err
+ }
+ return room, resp, nil
+}
+
+// GetStatistics returns the room statistics pecified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_room_statistics
+func (r *RoomService) GetStatistics(id string) (*RoomStatistics, *http.Response, error) {
+ req, err := r.client.NewRequest("GET", fmt.Sprintf("room/%s/statistics", id), nil, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ roomStatistics := new(RoomStatistics)
+ resp, err := r.client.Do(req, roomStatistics)
+ if err != nil {
+ return nil, resp, err
+ }
+ return roomStatistics, resp, nil
+}
+
+// Notification sends a notification to the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/send_room_notification
+func (r *RoomService) Notification(id string, notifReq *NotificationRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/notification", id), nil, notifReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// Message sends a message to the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/send_message
+func (r *RoomService) Message(id string, msgReq *RoomMessageRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/message", id), nil, msgReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// ShareFile sends a file to the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/share_file_with_room
+func (r *RoomService) ShareFile(id string, shareFileReq *ShareFileRequest) (*http.Response, error) {
+ req, err := r.client.NewFileUploadRequest("POST", fmt.Sprintf("room/%s/share/file", id), shareFileReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// Create creates a new room.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_room
+func (r *RoomService) Create(roomReq *CreateRoomRequest) (*Room, *http.Response, error) {
+ req, err := r.client.NewRequest("POST", "room", nil, roomReq)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ room := new(Room)
+ resp, err := r.client.Do(req, room)
+ if err != nil {
+ return nil, resp, err
+ }
+ return room, resp, nil
+}
+
+// Delete deletes an existing room.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_room
+func (r *RoomService) Delete(id string) (*http.Response, error) {
+ req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s", id), nil, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// Update updates an existing room.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/update_room
+func (r *RoomService) Update(id string, roomReq *UpdateRoomRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s", id), nil, roomReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// HistoryOptions represents a HipChat room chat history request.
+type HistoryOptions struct {
+ ListOptions
+ ExpandOptions
+
+ // Either the latest date to fetch history for in ISO-8601 format, or 'recent' to fetch
+ // the latest 75 messages. Paging isn't supported for 'recent', however they are real-time
+ // values, whereas date queries may not include the most recent messages.
+ Date string `url:"date,omitempty"`
+
+ // Your timezone. Must be a supported timezone
+ Timezone string `url:"timezone,omitempty"`
+
+ // Reverse the output such that the oldest message is first.
+ // For consistent paging, set to 'false'.
+ Reverse bool `url:"reverse,omitempty"`
+
+ // Either the earliest date to fetch history for the ISO-8601 format string,
+ // or leave blank to disable this filter.
+ // to be effective, the API call requires Date also be filled in with an ISO-8601 format string.
+ EndDate string `url:"end-date,omitempty"`
+
+ // Include records about deleted messages into results (body of a message isn't returned). Set to 'true'.
+ IncludeDeleted bool `url:"include_deleted,omitempty"`
+}
+
+// History fetches a room's chat history.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_room_history
+func (r *RoomService) History(id string, opt *HistoryOptions) (*History, *http.Response, error) {
+ u := fmt.Sprintf("room/%s/history", id)
+ req, err := r.client.NewRequest("GET", u, opt, nil)
+ h := new(History)
+ resp, err := r.client.Do(req, &h)
+ if err != nil {
+ return nil, resp, err
+ }
+ return h, resp, nil
+}
+
+// LatestHistoryOptions represents a HipChat room chat latest history request.
+type LatestHistoryOptions struct {
+
+ // The maximum number of messages to return.
+ MaxResults int `url:"max-results,omitempty"`
+
+ // Your timezone. Must be a supported timezone.
+ Timezone string `url:"timezone,omitempty"`
+
+ // The id of the message that is oldest in the set of messages to be returned.
+ // The server will not return any messages that chronologically precede this message.
+ NotBefore string `url:"not-before,omitempty"`
+}
+
+// Latest fetches a room's chat history.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_recent_room_history
+func (r *RoomService) Latest(id string, opt *LatestHistoryOptions) (*History, *http.Response, error) {
+ u := fmt.Sprintf("room/%s/history/latest", id)
+ req, err := r.client.NewRequest("GET", u, opt, nil)
+ h := new(History)
+ resp, err := r.client.Do(req, &h)
+ if err != nil {
+ return nil, resp, err
+ }
+ return h, resp, nil
+}
+
+// SetTopic sets Room topic.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/set_topic
+func (r *RoomService) SetTopic(id string, topic string) (*http.Response, error) {
+ topicReq := &SetTopicRequest{Topic: topic}
+
+ req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/topic", id), nil, topicReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// Invite someone to the Room.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/invite_user
+func (r *RoomService) Invite(room string, user string, reason string) (*http.Response, error) {
+ reasonReq := &InviteRequest{Reason: reason}
+
+ req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%s/invite/%s", room, user), nil, reasonReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// CreateGlance creates a glance in the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_room_glance
+func (r *RoomService) CreateGlance(id string, glanceReq *GlanceRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/extension/glance/%s", id, glanceReq.Key), nil, glanceReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// DeleteGlance deletes a glance in the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_room_glance
+func (r *RoomService) DeleteGlance(id string, glanceReq *GlanceRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s/extension/glance/%s", id, glanceReq.Key), nil, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// UpdateGlance sends a glance update to the room specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/room_addon_ui_update
+func (r *RoomService) UpdateGlance(id string, glanceUpdateReq *GlanceUpdateRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("POST", fmt.Sprintf("addon/ui/room/%s", id), nil, glanceUpdateReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// AddMember adds a member to a private room and sends member's unavailable presence to all room members asynchronously.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/add_member
+func (r *RoomService) AddMember(roomID string, userID string, addMemberReq *AddMemberRequest) (*http.Response, error) {
+ req, err := r.client.NewRequest("PUT", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, addMemberReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
+
+// RemoveMember removes a member from a private room
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/remove_member
+func (r *RoomService) RemoveMember(roomID string, userID string) (*http.Response, error) {
+ req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%s/member/%s", roomID, userID), nil, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return r.client.Do(req, nil)
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go
new file mode 100644
index 00000000..59bcf1ef
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_test.go
@@ -0,0 +1,734 @@
+package hipchat
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "reflect"
+ "testing"
+)
+
+func TestRoomGet(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ fmt.Fprintf(w, `
+ {
+ "id":1,
+ "name":"n",
+ "links":{"self":"s"},
+ "Participants":[
+ {"Name":"n1"},
+ {"Name":"n2"}
+ ],
+ "Owner":{"Name":"n1"}
+ }`)
+ })
+ want := &Room{
+ ID: 1,
+ Name: "n",
+ Links: RoomLinks{Links: Links{Self: "s"}},
+ Participants: []User{{Name: "n1"}, {Name: "n2"}},
+ Owner: User{Name: "n1"},
+ }
+
+ room, _, err := client.Room.Get("1")
+ if err != nil {
+ t.Fatalf("Room.Get returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, room) {
+ t.Errorf("Room.Get returned %+v, want %+v", room, want)
+ }
+}
+
+func TestRoomList(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "start-index": "1",
+ "max-results": "10",
+ "expand": "expansion",
+ "include-private": "true",
+ "include-archived": "true",
+ })
+ fmt.Fprintf(w, `
+ {
+ "items": [{"id":1,"name":"n"}],
+ "startIndex":1,
+ "maxResults":1,
+ "links":{"Self":"s"}
+ }`)
+ })
+ want := &Rooms{Items: []Room{{ID: 1, Name: "n"}}, StartIndex: 1, MaxResults: 1, Links: PageLinks{Links: Links{Self: "s"}}}
+ opt := &RoomsListOptions{ListOptions{1, 10}, ExpandOptions{"expansion"}, true, true}
+ rooms, _, err := client.Room.List(opt)
+ if err != nil {
+ t.Fatalf("Room.List returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, rooms) {
+ t.Errorf("Room.List returned %+v, want %+v", rooms, want)
+ }
+}
+
+func TestRoomGetStatistics(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/statistics", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ fmt.Fprintf(w, `
+ {
+ "messages_sent":1,
+ "last_active":"2016-02-29T09:03:53+00:00"
+ }`)
+ })
+ want := &RoomStatistics{
+ MessagesSent: 1,
+ LastActive: "2016-02-29T09:03:53+00:00",
+ }
+
+ roomStatistics, _, err := client.Room.GetStatistics("1")
+ if err != nil {
+ t.Fatalf("Room.GetStatistics returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, roomStatistics) {
+ t.Errorf("Room.GetStatistics returned %+v, want %+v", roomStatistics, want)
+ }
+}
+
+func TestRoomNotification(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &NotificationRequest{Color: "red", Message: "m", MessageFormat: "text"}
+
+ mux.HandleFunc("/room/1/notification", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(NotificationRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.Room.Notification("1", args)
+ if err != nil {
+ t.Fatalf("Room.Notification returns an error %v", err)
+ }
+}
+
+func TestRoomNotificationCardWithThumbnail(t *testing.T) {
+ setup()
+ defer teardown()
+
+ thumbnail := &Thumbnail{URL: "http://foo.com", URL2x: "http://foo2x.com", Width: 1, Height: 2}
+ description := CardDescription{Format: "format", Value: "value"}
+ card := &Card{Style: "style", Description: description, Title: "title", Thumbnail: thumbnail}
+ args := &NotificationRequest{Card: card}
+
+ mux.HandleFunc("/room/2/notification", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(NotificationRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.Room.Notification("2", args)
+ if err != nil {
+ t.Fatalf("Room.Notification returns an error %v", err)
+ }
+}
+
+func TestRoomMessage(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &RoomMessageRequest{Message: "m"}
+
+ mux.HandleFunc("/room/1/message", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(RoomMessageRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.Room.Message("1", args)
+ if err != nil {
+ t.Fatalf("Room.Message returns an error %v", err)
+ }
+}
+
+func TestRoomShareFile(t *testing.T) {
+ setup()
+ defer teardown()
+
+ tempFile, err := ioutil.TempFile(os.TempDir(), "hipfile")
+ tempFile.WriteString("go gophers")
+ defer os.Remove(tempFile.Name())
+
+ want := "--hipfileboundary\n" +
+ "Content-Type: application/json; charset=UTF-8\n" +
+ "Content-Disposition: attachment; name=\"metadata\"\n\n" +
+ "{\"message\": \"Hello there\"}\n" +
+ "--hipfileboundary\n" +
+ "Content-Type: charset=UTF-8\n" +
+ "Content-Transfer-Encoding: base64\n" +
+ "Content-Disposition: attachment; name=file; filename=hipfile\n\n" +
+ "Z28gZ29waGVycw==\n" +
+ "--hipfileboundary\n"
+
+ mux.HandleFunc("/room/1/share/file", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+
+ body, _ := ioutil.ReadAll(r.Body)
+
+ if string(body) != want {
+ t.Errorf("Request body \n%+v\n,want \n\n%+v", string(body), want)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ args := &ShareFileRequest{Path: tempFile.Name(), Message: "Hello there", Filename: "hipfile"}
+ _, err = client.Room.ShareFile("1", args)
+ if err != nil {
+ t.Fatalf("Room.ShareFile returns an error %v", err)
+ }
+}
+
+func TestRoomCreate(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &CreateRoomRequest{Name: "n", Topic: "t"}
+
+ mux.HandleFunc("/room", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(CreateRoomRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ fmt.Fprintf(w, `{"id":1,"links":{"self":"s"}}`)
+ })
+ want := &Room{ID: 1, Links: RoomLinks{Links: Links{Self: "s"}}}
+
+ room, _, err := client.Room.Create(args)
+ if err != nil {
+ t.Fatalf("Room.Create returns an error %v", err)
+ }
+ if !reflect.DeepEqual(room, want) {
+ t.Errorf("Room.Create returns %+v, want %+v", room, want)
+ }
+}
+
+func TestRoomDelete(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "DELETE")
+ })
+
+ _, err := client.Room.Delete("1")
+ if err != nil {
+ t.Fatalf("Room.Delete returns an error %v", err)
+ }
+}
+
+func TestRoomUpdate(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &UpdateRoomRequest{Name: "n", Topic: "t"}
+
+ mux.HandleFunc("/room/1", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "PUT")
+ v := new(UpdateRoomRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ })
+
+ _, err := client.Room.Update("1", args)
+ if err != nil {
+ t.Fatalf("Room.Update returns an error %v", err)
+ }
+}
+
+func TestRoomHistory(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/history", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "start-index": "1",
+ "max-results": "100",
+ "expand": "expansion",
+ "date": "date",
+ "timezone": "tz",
+ "reverse": "true",
+ "end-date": "end-date",
+ "include_deleted": "true",
+ })
+ fmt.Fprintf(w, `
+ {
+ "items": [
+ {
+ "date": "2014-11-23T21:23:49.807578+00:00",
+ "from": "Test Testerson",
+ "id": "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed",
+ "mentions": [],
+ "message": "Hey there!",
+ "message_format": "html",
+ "type": "notification"
+ }
+ ],
+ "links": {
+ "self": "https://api.hipchat.com/v2/room/1/history"
+ },
+ "maxResults": 100,
+ "startIndex": 0
+ }`)
+ })
+
+ opt := &HistoryOptions{
+ ListOptions{1, 100}, ExpandOptions{"expansion"}, "date", "tz", true, "end-date", true,
+ }
+ hist, _, err := client.Room.History("1", opt)
+ if err != nil {
+ t.Fatalf("Room.History returns an error %v", err)
+ }
+
+ want := &History{Items: []Message{{Date: "2014-11-23T21:23:49.807578+00:00", From: "Test Testerson", ID: "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", Mentions: []User{}, Message: "Hey there!", MessageFormat: "html", Type: "notification"}}, StartIndex: 0, MaxResults: 100, Links: PageLinks{Links: Links{Self: "https://api.hipchat.com/v2/room/1/history"}}}
+ if !reflect.DeepEqual(want, hist) {
+ t.Errorf("Room.History returned %+v, want %+v", hist, want)
+ }
+}
+
+func TestRoomLatest(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/history/latest", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "max-results": "100",
+ "timezone": "tz",
+ "not-before": "notbefore",
+ })
+ fmt.Fprintf(w, `
+ {
+ "items": [
+ {
+ "date": "2014-11-23T21:23:49.807578+00:00",
+ "from": "Test Testerson",
+ "id": "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed",
+ "mentions": [],
+ "message": "Hey there!",
+ "message_format": "html",
+ "type": "notification"
+ }
+ ],
+ "links": {
+ "self": "https://api.hipchat.com/v2/room/1/history/latest"
+ },
+ "maxResults": 100
+ }`)
+ })
+
+ opt := &LatestHistoryOptions{
+ 100, "tz", "notbefore",
+ }
+ hist, _, err := client.Room.Latest("1", opt)
+ if err != nil {
+ t.Fatalf("Room.Latest returns an error %v", err)
+ }
+ want := &History{Items: []Message{{Date: "2014-11-23T21:23:49.807578+00:00", From: "Test Testerson", ID: "f058e668-c9c0-4cd5-9ca5-e2c42b06f3ed", Mentions: []User{}, Message: "Hey there!", MessageFormat: "html", Type: "notification"}}, MaxResults: 100, Links: PageLinks{Links: Links{Self: "https://api.hipchat.com/v2/room/1/history/latest"}}}
+ if !reflect.DeepEqual(want, hist) {
+ t.Errorf("Room.Latest returned %+v, want %+v", hist, want)
+ }
+}
+func TestRoomGlanceCreate(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &GlanceRequest{
+ Key: "abc",
+ Name: GlanceName{Value: "Test Glance"},
+ Target: "target",
+ QueryURL: "qu",
+ Icon: Icon{URL: "i", URL2x: "i"},
+ }
+
+ mux.HandleFunc(fmt.Sprintf("/room/1/extension/glance/%s", args.Key), func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "PUT")
+ v := new(GlanceRequest)
+ json.NewDecoder(r.Body).Decode(v)
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.Room.CreateGlance("1", args)
+ if err != nil {
+ t.Fatalf("Room.CreateGlance returns an error %v", err)
+ }
+}
+
+func TestRoomGlanceDelete(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &GlanceRequest{
+ Key: "abc",
+ }
+
+ mux.HandleFunc(fmt.Sprintf("/room/1/extension/glance/%s", args.Key), func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "DELETE")
+ })
+
+ _, err := client.Room.DeleteGlance("1", args)
+ if err != nil {
+ t.Fatalf("Room.DeleteGlance returns an error %v", err)
+ }
+}
+
+func TestRoomGlanceUpdate(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &GlanceUpdateRequest{
+ Glance: []*GlanceUpdate{
+ &GlanceUpdate{
+ Key: "abc",
+ Content: GlanceContent{
+ Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ Label: AttributeValue{Type: "html", Value: "hello"},
+ },
+ },
+ },
+ }
+
+ mux.HandleFunc("/addon/ui/room/1", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(GlanceUpdateRequest)
+ json.NewDecoder(r.Body).Decode(v)
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.Room.UpdateGlance("1", args)
+ if err != nil {
+ t.Fatalf("Room.UpdateGlance returns an error %v", err)
+ }
+}
+
+func TestSetTopic(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &SetTopicRequest{Topic: "t"}
+
+ mux.HandleFunc("/room/1/topic", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "PUT")
+ v := new(SetTopicRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ })
+
+ _, err := client.Room.SetTopic("1", "t")
+ if err != nil {
+ t.Fatalf("Room.SetTopic returns an error %v", err)
+ }
+}
+
+func TestInvite(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &InviteRequest{Reason: "r"}
+
+ mux.HandleFunc("/room/1/invite/user", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(InviteRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ })
+
+ _, err := client.Room.Invite("1", "user", "r")
+ if err != nil {
+ t.Fatalf("Room.Invite returns an error %v", err)
+ }
+}
+
+func TestCardDescriptionJSONEncodeWithString(t *testing.T) {
+ description := CardDescription{Value: "This is a test"}
+ expected := `"This is a test"`
+
+ encoded, err := json.Marshal(description)
+ if err != nil {
+ t.Errorf("Encoding of CardDescription failed")
+ }
+
+ if string(encoded) != expected {
+ t.Fatalf("Encoding of CardDescription failed: %s", encoded)
+ }
+}
+
+func TestCardDescriptionJSONDecodeWithString(t *testing.T) {
+ encoded := []byte(`"This is a test"`)
+ expected := CardDescription{Format: "", Value: "This is a test"}
+
+ var actual CardDescription
+
+ err := json.Unmarshal(encoded, &actual)
+ if err != nil {
+ t.Errorf("Decoding of CardDescription failed: %v", err)
+ }
+
+ if actual.Value != expected.Value {
+ t.Fatalf("Unexpected CardDescription.Value: %v", actual.Value)
+ }
+
+ if actual.Format != expected.Format {
+ t.Fatalf("Unexpected CardDescription.Format: %v", actual.Format)
+ }
+}
+
+func TestCardDescriptionJSONEncodeWithObject(t *testing.T) {
+ description := CardDescription{Format: "html", Value: "This is a test"}
+ expected := `{"format":"html","value":"\u003cstrong\u003eThis is a test\u003c/strong\u003e"}`
+
+ encoded, err := json.Marshal(description)
+ if err != nil {
+ t.Errorf("Encoding of CardDescription failed")
+ }
+
+ if string(encoded) != expected {
+ t.Fatalf("Encoding of CardDescription failed: %s", encoded)
+ }
+}
+
+func TestCardDescriptionJSONDecodeWithObject(t *testing.T) {
+ encoded := []byte(`{"format":"html","value":"\u003cstrong\u003eThis is a test\u003c/strong\u003e"}`)
+ expected := CardDescription{Format: "html", Value: "This is a test"}
+
+ var actual CardDescription
+
+ err := json.Unmarshal(encoded, &actual)
+ if err != nil {
+ t.Errorf("Decoding of CardDescription failed: %v", err)
+ }
+
+ if actual.Value != expected.Value {
+ t.Fatalf("Unexpected CardDescription.Value: %v", actual.Value)
+ }
+
+ if actual.Format != expected.Format {
+ t.Fatalf("Unexpected CardDescription.Format: %v", actual.Format)
+ }
+}
+
+func TestGlanceUpdateRequestJSONEncodeWithString(t *testing.T) {
+ gr := GlanceUpdateRequest{
+ Glance: []*GlanceUpdate{
+ &GlanceUpdate{
+ Key: "abc",
+ Content: GlanceContent{
+ Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ Label: AttributeValue{Type: "html", Value: "hello"},
+ },
+ },
+ },
+ }
+ expected := `{"glance":[{"key":"abc","content":{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}}]}`
+
+ encoded, err := json.Marshal(gr)
+ if err != nil {
+ t.Errorf("Encoding of GlanceUpdateRequest failed")
+ }
+
+ if string(encoded) != expected {
+ t.Fatalf("Encoding of GlanceUpdateRequest failed: %s", encoded)
+ }
+}
+
+func TestGlanceContentJSONEncodeWithString(t *testing.T) {
+ gcTests := []struct {
+ gc GlanceContent
+ expected string
+ }{
+ {
+ GlanceContent{
+ Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ Label: AttributeValue{Type: "html", Value: "hello"},
+ },
+ `{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}`,
+ },
+ }
+
+ for _, tt := range gcTests {
+ encoded, err := json.Marshal(tt.gc)
+ if err != nil {
+ t.Errorf("Encoding of GlanceContent failed")
+ }
+
+ if string(encoded) != tt.expected {
+ t.Fatalf("Encoding of GlanceContent failed: %s", encoded)
+ }
+ }
+}
+
+func TestGlanceContentJSONDecodeWithObject(t *testing.T) {
+ gcTests := []struct {
+ gc GlanceContent
+ encoded string
+ }{
+ {
+ GlanceContent{
+ Status: &GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ Label: AttributeValue{Type: "html", Value: "hello"},
+ },
+ `{"status":{"type":"lozenge","value":{"type":"default","label":"something"}},"label":{"type":"html","value":"hello"}}`,
+ },
+ }
+
+ for _, tt := range gcTests {
+ var actual GlanceContent
+
+ err := json.Unmarshal([]byte(tt.encoded), &actual)
+ if err != nil {
+ t.Errorf("Decoding of GlanceContent failed: %v", err)
+ }
+
+ if !reflect.DeepEqual(actual.Status, tt.gc.Status) {
+ t.Fatalf("Unexpected GlanceContent.Status: %+v, want %+v", actual.Status, tt.gc.Status)
+ }
+
+ if actual.Label != tt.gc.Label {
+ t.Fatalf("Unexpected GlanceStatus.Label: %v", actual.Label)
+ }
+
+ if actual.Metadata != tt.gc.Metadata {
+ t.Fatalf("Unexpected GlanceStatus.Metadata %v", actual.Metadata)
+ }
+ }
+}
+
+func TestGlanceStatusJSONEncodeWithString(t *testing.T) {
+ gsTests := []struct {
+ gs *GlanceStatus
+ expected string
+ }{
+ {&GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ `{"type":"lozenge","value":{"type":"default","label":"something"}}`},
+ {&GlanceStatus{Type: "icon", Value: Icon{URL: "z", URL2x: "x"}},
+ `{"type":"icon","value":{"url":"z","url@2x":"x"}}`},
+ }
+
+ for _, tt := range gsTests {
+ encoded, err := json.Marshal(tt.gs)
+ if err != nil {
+ t.Errorf("Encoding of GlanceStatus failed")
+ }
+
+ if string(encoded) != tt.expected {
+ t.Fatalf("Encoding of GlanceStatus failed: %s", encoded)
+ }
+ }
+}
+
+func TestGlanceStatusJSONDecodeWithObject(t *testing.T) {
+ gsTests := []struct {
+ gs *GlanceStatus
+ encoded string
+ }{
+ {&GlanceStatus{Type: "lozenge", Value: AttributeValue{Type: "default", Label: "something"}},
+ `{"type":"lozenge","value":{"type":"default","label":"something"}}`},
+ {&GlanceStatus{Type: "icon", Value: Icon{URL: "z", URL2x: "x"}},
+ `{"type":"icon","value":{"url":"z","url@2x":"x"}}`},
+ }
+
+ for _, tt := range gsTests {
+ var actual GlanceStatus
+
+ err := json.Unmarshal([]byte(tt.encoded), &actual)
+ if err != nil {
+ t.Errorf("Decoding of GlanceStatus failed: %v", err)
+ }
+
+ if actual.Type != tt.gs.Type {
+ t.Fatalf("Unexpected GlanceStatus.Type: %v", actual.Type)
+ }
+
+ if actual.Value != tt.gs.Value {
+ t.Fatalf("Unexpected GlanceStatus.Value: %v", actual.Value)
+ }
+ }
+}
+
+func TestAddMember(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &AddMemberRequest{Roles: []string{"room_member"}}
+
+ mux.HandleFunc("/room/1/member/user", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "PUT")
+ v := new(AddMemberRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ })
+
+ _, err := client.Room.AddMember("1", "user", args)
+ if err != nil {
+ t.Fatalf("Room.AddMember returns an error %v", err)
+ }
+}
+
+func TestRemoveMember(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/member/user", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "DELETE")
+ })
+
+ _, err := client.Room.RemoveMember("1", "user")
+ if err != nil {
+ t.Fatalf("Room.RemoveMember returns an error %v", err)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go
new file mode 100644
index 00000000..233296d6
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook.go
@@ -0,0 +1,99 @@
+// handling Webhook data
+
+package hipchat
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// Response Types
+
+// Webhook represents a HipChat webhook.
+type Webhook struct {
+ Links Links `json:"links"`
+ Name string `json:"name"`
+ Key string `json:"key,omitempty"`
+ Event string `json:"event"`
+ Pattern string `json:"pattern"`
+ URL string `json:"url"`
+ ID int `json:"id,omitempty"`
+}
+
+// WebhookList represents a HipChat webhook list.
+type WebhookList struct {
+ Webhooks []Webhook `json:"items"`
+ StartIndex int `json:"startIndex"`
+ MaxResults int `json:"maxResults"`
+ Links PageLinks `json:"links"`
+}
+
+// Request Types
+
+// ListWebhooksOptions represents options for ListWebhooks method.
+type ListWebhooksOptions struct {
+ ListOptions
+}
+
+// CreateWebhookRequest represents the body of the CreateWebhook method.
+type CreateWebhookRequest struct {
+ Name string `json:"name"`
+ Key string `json:"key,omitempty"`
+ Event string `json:"event"`
+ Pattern string `json:"pattern"`
+ URL string `json:"url"`
+}
+
+// ListWebhooks returns all the webhooks for a given room.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_webhooks
+func (r *RoomService) ListWebhooks(id interface{}, opt *ListWebhooksOptions) (*WebhookList, *http.Response, error) {
+ u := fmt.Sprintf("room/%v/webhook", id)
+ req, err := r.client.NewRequest("GET", u, opt, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ whList := new(WebhookList)
+
+ resp, err := r.client.Do(req, whList)
+ if err != nil {
+ return nil, resp, err
+ }
+ return whList, resp, nil
+}
+
+// DeleteWebhook removes the given webhook.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/delete_webhook
+func (r *RoomService) DeleteWebhook(id interface{}, webhookID interface{}) (*http.Response, error) {
+ req, err := r.client.NewRequest("DELETE", fmt.Sprintf("room/%v/webhook/%v", id, webhookID), nil, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := r.client.Do(req, nil)
+ if err != nil {
+ return resp, err
+ }
+
+ return resp, nil
+}
+
+// CreateWebhook creates a new webhook.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/create_webhook
+func (r *RoomService) CreateWebhook(id interface{}, roomReq *CreateWebhookRequest) (*Webhook, *http.Response, error) {
+ req, err := r.client.NewRequest("POST", fmt.Sprintf("room/%v/webhook", id), nil, roomReq)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ wh := new(Webhook)
+
+ resp, err := r.client.Do(req, wh)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return wh, resp, nil
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go
new file mode 100644
index 00000000..4ec5ad5d
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go
@@ -0,0 +1,82 @@
+package hipchat
+
+import (
+ // "encoding/json"
+ "fmt"
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestWebhookList(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/webhook", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "max-results": "100",
+ "start-index": "1",
+ })
+ fmt.Fprintf(w, `
+ {
+ "items":[
+ {"name":"a", "key": "a", "pattern":"a", "event":"message_received", "url":"h", "id":1, "links":{"self":"s"}},
+ {"name":"b", "key": "b", "pattern":"b", "event":"message_received", "url":"h", "id":2, "links":{"self":"s"}}
+ ],
+ "links":{"self":"s", "prev":"a", "next":"b"},
+ "startIndex":0,
+ "maxResults":10
+ }`)
+ })
+
+ want := &WebhookList{
+ Webhooks: []Webhook{
+ {
+ Name: "a",
+ Key: "a",
+ Pattern: "a",
+ Event: "message_received",
+ URL: "h",
+ ID: 1,
+ Links: Links{Self: "s"},
+ },
+ {
+ Name: "b",
+ Key: "b",
+ Pattern: "b",
+ Event: "message_received",
+ URL: "h",
+ ID: 2,
+ Links: Links{Self: "s"},
+ },
+ },
+ StartIndex: 0,
+ MaxResults: 10,
+ Links: PageLinks{Links: Links{Self: "s"}, Prev: "a", Next: "b"},
+ }
+
+ opt := &ListWebhooksOptions{ListOptions{1, 100}}
+
+ actual, _, err := client.Room.ListWebhooks("1", opt)
+ if err != nil {
+ t.Fatalf("Room.ListWebhooks returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, actual) {
+ t.Errorf("Room.ListWebhooks returned %+v, want %+v", actual, want)
+ }
+}
+
+func TestWebhookDelete(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/room/1/webhook/2", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "DELETE")
+ })
+
+ _, err := client.Room.DeleteWebhook("1", "2")
+ if err != nil {
+ t.Fatalf("Room.Update returns an error %v", err)
+ }
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go
new file mode 100644
index 00000000..c45872ec
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go
@@ -0,0 +1,155 @@
+package hipchat
+
+import (
+ "fmt"
+ "net/http"
+)
+
+// MessageRequest represents a HipChat private message to user.
+type MessageRequest struct {
+ Message string `json:"message,omitempty"`
+ Notify bool `json:"notify,omitempty"`
+ MessageFormat string `json:"message_format,omitempty"`
+}
+
+// UserPresence represents the HipChat user's presence.
+type UserPresence struct {
+ Status string `json:"status"`
+ Idle int `json:"idle"`
+ Show string `json:"show"`
+ IsOnline bool `json:"is_online"`
+}
+
+const (
+ // UserPresenceShowAway show status away
+ UserPresenceShowAway = "away"
+
+ // UserPresenceShowChat show status available to chat
+ UserPresenceShowChat = "chat"
+
+ // UserPresenceShowDnd show status do not disturb
+ UserPresenceShowDnd = "dnd"
+
+ // UserPresenceShowXa show status xa?
+ UserPresenceShowXa = "xa"
+)
+
+// UpdateUserRequest represents a HipChat user update request body.
+type UpdateUserRequest struct {
+ Name string `json:"name"`
+ Presence UpdateUserPresenceRequest `json:"presence"`
+ MentionName string `json:"mention_name"`
+ Email string `json:"email"`
+}
+
+// UpdateUserPresenceRequest represents the HipChat user's presence update request body.
+type UpdateUserPresenceRequest struct {
+ Status string `json:"status"`
+ Show string `json:"show"`
+}
+
+// User represents the HipChat user.
+type User struct {
+ XMPPJid string `json:"xmpp_jid"`
+ IsDeleted bool `json:"is_deleted"`
+ Name string `json:"name"`
+ LastActive string `json:"last_active"`
+ Title string `json:"title"`
+ Presence UserPresence `json:"presence"`
+ Created string `json:"created"`
+ ID int `json:"id"`
+ MentionName string `json:"mention_name"`
+ IsGroupAdmin bool `json:"is_group_admin"`
+ Timezone string `json:"timezone"`
+ IsGuest bool `json:"is_guest"`
+ Email string `json:"email"`
+ PhotoURL string `json:"photo_url"`
+ Links Links `json:"links"`
+}
+
+// Users represents the API return of a collection of Users plus metadata
+type Users struct {
+ Items []User `json:"items"`
+ StartIndex int `json:"start_index"`
+ MaxResults int `json:"max_results"`
+ Links Links `json:"links"`
+}
+
+// UserService gives access to the user related methods of the API.
+type UserService struct {
+ client *Client
+}
+
+// ShareFile sends a file to the user specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/share_file_with_user
+func (u *UserService) ShareFile(id string, shareFileReq *ShareFileRequest) (*http.Response, error) {
+ req, err := u.client.NewFileUploadRequest("POST", fmt.Sprintf("user/%s/share/file", id), shareFileReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return u.client.Do(req, nil)
+}
+
+// View fetches a user's details.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/view_user
+func (u *UserService) View(id string) (*User, *http.Response, error) {
+ req, err := u.client.NewRequest("GET", fmt.Sprintf("user/%s", id), nil, nil)
+
+ userDetails := new(User)
+ resp, err := u.client.Do(req, &userDetails)
+ if err != nil {
+ return nil, resp, err
+ }
+ return userDetails, resp, nil
+}
+
+// Message sends a private message to the user specified by the id.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/private_message_user
+func (u *UserService) Message(id string, msgReq *MessageRequest) (*http.Response, error) {
+ req, err := u.client.NewRequest("POST", fmt.Sprintf("user/%s/message", id), nil, msgReq)
+ if err != nil {
+ return nil, err
+ }
+
+ return u.client.Do(req, nil)
+}
+
+// UserListOptions specified the parameters to the UserService.List method.
+type UserListOptions struct {
+ ListOptions
+ ExpandOptions
+ // Include active guest users in response.
+ IncludeGuests bool `url:"include-guests,omitempty"`
+ // Include deleted users in response.
+ IncludeDeleted bool `url:"include-deleted,omitempty"`
+}
+
+// List returns all users in the group.
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/get_all_users
+func (u *UserService) List(opt *UserListOptions) ([]User, *http.Response, error) {
+ req, err := u.client.NewRequest("GET", "user", opt, nil)
+
+ users := new(Users)
+ resp, err := u.client.Do(req, &users)
+ if err != nil {
+ return nil, resp, err
+ }
+ return users.Items, resp, nil
+}
+
+// Update a user
+//
+// HipChat API docs: https://www.hipchat.com/docs/apiv2/method/update_user
+func (u *UserService) Update(id string, user *UpdateUserRequest) (*http.Response, error) {
+ req, err := u.client.NewRequest("PUT", fmt.Sprintf("user/%s", id), nil, user)
+ if err != nil {
+ return nil, err
+ }
+
+ return u.client.Do(req, nil)
+}
diff --git a/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go
new file mode 100644
index 00000000..9e245673
--- /dev/null
+++ b/vendor/github.com/tbruyelle/hipchat-go/hipchat/user_test.go
@@ -0,0 +1,216 @@
+package hipchat
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "reflect"
+ "testing"
+)
+
+func TestUserShareFile(t *testing.T) {
+ setup()
+ defer teardown()
+
+ tempFile, err := ioutil.TempFile(os.TempDir(), "hipfile")
+ tempFile.WriteString("go gophers")
+ defer os.Remove(tempFile.Name())
+
+ want := "--hipfileboundary\n" +
+ "Content-Type: application/json; charset=UTF-8\n" +
+ "Content-Disposition: attachment; name=\"metadata\"\n\n" +
+ "{\"message\": \"Hello there\"}\n" +
+ "--hipfileboundary\n" +
+ "Content-Type: charset=UTF-8\n" +
+ "Content-Transfer-Encoding: base64\n" +
+ "Content-Disposition: attachment; name=file; filename=hipfile\n\n" +
+ "Z28gZ29waGVycw==\n" +
+ "--hipfileboundary\n"
+
+ mux.HandleFunc("/user/1/share/file", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+
+ body, _ := ioutil.ReadAll(r.Body)
+
+ if string(body) != want {
+ t.Errorf("Request body \n%+v\n,want \n\n%+v", string(body), want)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ args := &ShareFileRequest{Path: tempFile.Name(), Message: "Hello there", Filename: "hipfile"}
+ _, err = client.User.ShareFile("1", args)
+ if err != nil {
+ t.Fatalf("User.ShareFile returns an error %v", err)
+ }
+}
+
+func TestUserMessage(t *testing.T) {
+ setup()
+ defer teardown()
+
+ args := &MessageRequest{Message: "m", MessageFormat: "text"}
+
+ mux.HandleFunc("/user/@FirstL/message", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "POST")
+ v := new(MessageRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, args) {
+ t.Errorf("Request body %+v, want %+v", v, args)
+ }
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ _, err := client.User.Message("@FirstL", args)
+ if err != nil {
+ t.Fatalf("User.Message returns an error %v", err)
+ }
+}
+
+func TestUserView(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/user/@FirstL", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ fmt.Fprintf(w, `
+ {
+ "created": "2013-11-07T17:57:11+00:00",
+ "email": "user@example.com",
+ "group": {
+ "id": 1234,
+ "links": {
+ "self": "https://api.hipchat.com/v2/group/1234"
+ },
+ "name": "Example"
+ },
+ "id": 1,
+ "is_deleted": false,
+ "is_group_admin": true,
+ "is_guest": false,
+ "last_active": "1421029691",
+ "links": {
+ "self": "https://api.hipchat.com/v2/user/1"
+ },
+ "mention_name": "FirstL",
+ "name": "First Last",
+ "photo_url": "https://bitbucket-assetroot.s3.amazonaws.com/c/photos/2014/Mar/02/hipchat-pidgin-theme-logo-571708621-0_avatar.png",
+ "presence": {
+ "client": {
+ "type": "http://hipchat.com/client/mac",
+ "version": "151"
+ },
+ "is_online": true,
+ "show": "chat"
+ },
+ "timezone": "America/New_York",
+ "title": "Test user",
+ "xmpp_jid": "1@chat.hipchat.com"
+ }`)
+ })
+ want := &User{XMPPJid: "1@chat.hipchat.com",
+ IsDeleted: false,
+ Name: "First Last",
+ LastActive: "1421029691",
+ Title: "Test user",
+ Presence: UserPresence{Show: "chat", IsOnline: true},
+ Created: "2013-11-07T17:57:11+00:00",
+ ID: 1,
+ MentionName: "FirstL",
+ IsGroupAdmin: true,
+ Timezone: "America/New_York",
+ IsGuest: false,
+ Email: "user@example.com",
+ PhotoURL: "https://bitbucket-assetroot.s3.amazonaws.com/c/photos/2014/Mar/02/hipchat-pidgin-theme-logo-571708621-0_avatar.png",
+ Links: Links{Self: "https://api.hipchat.com/v2/user/1"}}
+
+ user, _, err := client.User.View("@FirstL")
+ if err != nil {
+ t.Fatalf("User.View returns an error %v", err)
+ }
+ if !reflect.DeepEqual(want, user) {
+ t.Errorf("User.View returned %+v, want %+v", user, want)
+ }
+}
+
+func TestUserList(t *testing.T) {
+ setup()
+ defer teardown()
+
+ mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "GET")
+ testFormValues(t, r, values{
+ "start-index": "1",
+ "max-results": "100",
+ "expand": "expansion",
+ "include-guests": "true",
+ "include-deleted": "true",
+ })
+ fmt.Fprintf(w, `
+ {
+ "items": [
+ {
+ "id": 1,
+ "links": {
+ "self": "https:\/\/api.hipchat.com\/v2\/user\/1"
+ },
+ "mention_name": "FirstL",
+ "name": "First Last"
+ }
+ ],
+ "startIndex": 0,
+ "maxResults": 100,
+ "links": {
+ "self": "https:\/\/api.hipchat.com\/v2\/user"
+ }
+ }`)
+ })
+ want := []User{
+ {
+ ID: 1,
+ Name: "First Last",
+ MentionName: "FirstL",
+ Links: Links{Self: "https://api.hipchat.com/v2/user/1"},
+ },
+ }
+
+ opt := &UserListOptions{
+ ListOptions{StartIndex: 1, MaxResults: 100},
+ ExpandOptions{"expansion"},
+ true, true,
+ }
+
+ users, _, err := client.User.List(opt)
+ if err != nil {
+ t.Fatalf("User.List returned an error %v", err)
+ }
+ if !reflect.DeepEqual(want, users) {
+ t.Errorf("User.List returned %+v, want %+v", users, want)
+ }
+}
+
+func TestUserUpdate(t *testing.T) {
+ setup()
+ defer teardown()
+
+ pArgs := UpdateUserPresenceRequest{Status: "status", Show: UserPresenceShowDnd}
+ userArgs := &UpdateUserRequest{Name: "n", Presence: pArgs, MentionName: "mn", Email: "e"}
+
+ mux.HandleFunc("/user/@FirstL", func(w http.ResponseWriter, r *http.Request) {
+ testMethod(t, r, "PUT")
+ v := new(UpdateUserRequest)
+ json.NewDecoder(r.Body).Decode(v)
+
+ if !reflect.DeepEqual(v, userArgs) {
+ t.Errorf("Request body %+v, want %+v", v, userArgs)
+ }
+ })
+
+ _, err := client.User.Update("@FirstL", userArgs)
+ if err != nil {
+ t.Fatalf("User.Update returns an error %v", err)
+ }
+}