Merge pull request #125 from glower/hipchat-notifications
added support for hipchat notificationspull/128/head
commit
843868351a
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*.test
|
|
@ -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
|
|
@ -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.
|
|
@ -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:** [](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.
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
*.swp
|
||||
examples/hiptest
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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
|
|
@ -0,0 +1,58 @@
|
|||
# hipchat-go
|
||||
|
||||
Go client library for the [HipChat API v2](https://www.hipchat.com/docs/apiv2).
|
||||
|
||||
[](https://godoc.org/github.com/tbruyelle/hipchat-go/hipchat)
|
||||
[](https://travis-ci.org/tbruyelle/hipchat-go)
|
||||
|
||||
Currently only a small part of the API is implemented, so pull requests are welcome.
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
import "github.com/tbruyelle/hipchat-go/hipchat"
|
||||
```
|
||||
|
||||
Build a new client, then use the `client.Room` service to spam all the rooms you have access to (not recommended):
|
||||
|
||||
```go
|
||||
c := hipchat.NewClient("<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).
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
hipfile
|
|
@ -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!"
|
||||
```
|
||||
|
BIN
vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png
generated
vendored
Normal file
BIN
vendor/github.com/tbruyelle/hipchat-go/examples/hipfile/gopher.png
generated
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -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 !")
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
hiplol
|
|
@ -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>
|
||||
```
|
|
@ -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 !")
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
hipshow
|
|
@ -0,0 +1,11 @@
|
|||
hipshow
|
||||
=====
|
||||
|
||||
Prints all rooms from your group
|
||||
|
||||
##### Usage
|
||||
|
||||
```bash
|
||||
go build
|
||||
./hipshow --token=<your auth token>
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
hiptail
|
|
@ -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>
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
}
|
1
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore
generated
vendored
Normal file
1
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hipwebhooks
|
21
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md
generated
vendored
Normal file
21
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/README.md
generated
vendored
Normal 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"
|
109
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go
generated
vendored
Normal file
109
vendor/github.com/tbruyelle/hipchat-go/examples/hipwebhooks/main.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
82
vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go
generated
vendored
Normal file
82
vendor/github.com/tbruyelle/hipchat-go/hipchat/room_webhook_test.go
generated
vendored
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue