Merge pull request #125 from glower/hipchat-notifications

added support for hipchat notifications
pull/128/head
Karolis Rusenas 2017-12-15 16:21:01 +00:00 committed by GitHub
commit 843868351a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 4455 additions and 4 deletions

View File

@ -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"

View File

@ -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

View File

@ -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("<b>%s</b><br>%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"
}
}

View File

@ -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:

1
vendor/github.com/google/go-querystring/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
*.test

View File

@ -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

27
vendor/github.com/google/go-querystring/LICENSE generated vendored Normal file
View File

@ -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.

39
vendor/github.com/google/go-querystring/README.md generated vendored Normal file
View File

@ -0,0 +1,39 @@
# go-querystring #
go-querystring is Go library for encoding structs into URL query parameters.
**Documentation:** <http://godoc.org/github.com/google/go-querystring/query>
**Build Status:** [![Build Status](https://drone.io/github.com/google/go-querystring/status.png)](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.

320
vendor/github.com/google/go-querystring/query/encode.go generated vendored Normal file
View File

@ -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
}

View File

@ -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)
}
}
}

2
vendor/github.com/tbruyelle/hipchat-go/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
*.swp
examples/hiptest

17
vendor/github.com/tbruyelle/hipchat-go/.travis.yml generated vendored Normal file
View File

@ -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

202
vendor/github.com/tbruyelle/hipchat-go/LICENSE generated vendored Normal file
View File

@ -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.

9
vendor/github.com/tbruyelle/hipchat-go/Makefile generated vendored Normal file
View File

@ -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

58
vendor/github.com/tbruyelle/hipchat-go/README.md generated vendored Normal file
View File

@ -0,0 +1,58 @@
# hipchat-go
Go client library for the [HipChat API v2](https://www.hipchat.com/docs/apiv2).
[![GoDoc](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat?status.svg)](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat)
[![Build Status](https://travis-ci.org/tbruyelle/hipchat-go.svg??branch=master)](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("<your AuthToken here>")
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).

18
vendor/github.com/tbruyelle/hipchat-go/checks.mk generated vendored Normal file
View File

@ -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;

View File

@ -0,0 +1 @@
hipfile

View File

@ -0,0 +1,21 @@
hipfile
=====
Sends the given file to the specified room or user.
##### Usage
```bash
go build
./hipfile --token=<your auth token> --room=<room id> --path=<file path>
```
##### Example
Give it a try with the gopher.png file
```bash
go build
./hipfile --token=<your auth token> --room=<room id> --path=gopher.png --message="Check out this one!"
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -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 !")
}

View File

@ -0,0 +1 @@
hiplol

View File

@ -0,0 +1,11 @@
lol
=====
Prints the `(lol)` emoticon in the room in parameter.
##### Usage
```bash
go build
./hiplol --token=<your auth token> --room=<room id>
```

View File

@ -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 !")
}
}

View File

@ -0,0 +1 @@
hipshow

View File

@ -0,0 +1,11 @@
hipshow
=====
Prints all rooms from your group
##### Usage
```bash
go build
./hipshow --token=<your auth token>
```

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
hiptail

View File

@ -0,0 +1,11 @@
hiptail
=====
Prints recent messages in the room in parameter.
##### Usage
```bash
go build
./hiptail --token=<your auth token> --room=<room id>
```

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
hipwebhooks

View File

@ -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"

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

107
vendor/github.com/tbruyelle/hipchat-go/hipchat/oauth.go generated vendored Normal file
View File

@ -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"
)

View File

@ -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,
)
}
}

659
vendor/github.com/tbruyelle/hipchat-go/hipchat/room.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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: "<strong>This is a test</strong>"}
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: "<strong>This is a test</strong>"}
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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

155
vendor/github.com/tbruyelle/hipchat-go/hipchat/user.go generated vendored Normal file
View File

@ -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)
}

View File

@ -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)
}
}