backlog webhookを使ってpull-requestをslackに通知してみる


## はじめに
- ゆるふわです

## あわせて読みたい
- backlogのgitからjenkinsをbuild - Qiita http://qiita.com/shinderuman@github/items/eff9f3567cbd11c03ee2

## ゴール
- backlogのwebhookのイベント(pull-request作成、更新、コメント)をサーバー上のgolangで受け付けてslackのincoming webhookにリクエストする

## 公式ドキュメントでjson定義探す
- http://www.backlog.jp/help/adminsguide/webhook-setting/userguide2493.html

> Webhookでは、指定されたURLにデータをJSON形式でPUSHで送信します。JSONデータの属性は一部の共通部分を除き、イベントによって異なるためテスト実行等で確認して下さい。

\><

## 自鯖に流して確認
### pull-request作成
```json
{
    "content": {
        "changes": [], 
        "comment": null, 
        "description": "XXX", 
        "diff": null, 
        "id": 111, 
        "issue": {
            "description": "XXX", 
            "id": 111, 
            "key_id": 111, 
            "summary": "XXX"
        }, 
        "number": 1, 
        "repository": {
            "description": "", 
            "id": 111, 
            "name": "test"
        }, 
        "summary": "XXX"
    }, 
    "created": "2016-04-14T14:31:33Z", 
    "createdUser": {
        "id": 111, 
        "mailAddress": null, 
        "name": "XXX", 
        "nulabAccount": null, 
        "roleType": 1, 
        "userId": null
    }, 
    "id": 111, 
    "notifications": [
        {
            "alreadyRead": false, 
            "id": 111, 
            "reason": 111, 
            "resourceAlreadyRead": false, 
            "user": {
                "id": 111, 
                "mailAddress": null, 
                "name": "XXX", 
                "nulabAccount": null, 
                "roleType": 111, 
                "userId": null
            }
        }, 
        {
            "alreadyRead": false, 
            "id": 111, 
            "reason": 111, 
            "resourceAlreadyRead": false, 
            "user": {
                "id": 111, 
                "mailAddress": null, 
                "name": "XXX", 
                "nulabAccount": {
                    "name": "XXX", 
                    "nulabId": "XXX", 
                    "uniqueId": "XXX"
                }, 
                "roleType": 111, 
                "userId": null
            }
        }
    ], 
    "project": {
        "archived": false, 
        "chartEnabled": true, 
        "id": 111, 
        "name": "XXX", 
        "projectKey": "XXX", 
        "subtaskingEnabled": true
    }, 
    "type": 111
}
```

### pull-request更新
```json
{
    "content": {
        "changes": [
            {
                "field": "description", 
                "new_value": "XXX", 
                "old_value": "XXX"
            }
        ], 
        "comment": {
            "content": "", 
            "id": 111
        }, 
        "description": "XXX", 
        "diff": "", 
        "id": 111, 
        "issue": {
            "description": "XXX", 
            "id": 111, 
            "key_id": 111, 
            "summary": "XXX"
        }, 
        "number": 111, 
        "repository": {
            "description": "", 
            "id": 111, 
            "name": "XXX"
        }, 
        "summary": "XXX"
    }, 
    "created": "2016-04-14T14:32:30Z", 
    "createdUser": {
        "id": 111, 
        "mailAddress": null, 
        "name": "XXX", 
        "nulabAccount": null, 
        "roleType": 111, 
        "userId": null
    }, 
    "id": 111, 
    "notifications": [], 
    "project": {
        "archived": false, 
        "chartEnabled": true, 
        "id": 111, 
        "name": "XXX", 
        "projectKey": "XXX", 
        "subtaskingEnabled": true
    }, 
    "type": 111
}
```

### pull-requestコメント

```json
{
    "content": {
        "changes": [], 
        "comment": {
            "content": "XXX", 
            "id": 111
        }, 
        "description": "XXX", 
        "diff": null, 
        "id": 111, 
        "issue": {
            "description": "XXX", 
            "id": 111, 
            "key_id": 111, 
            "summary": "XXX"
        }, 
        "number": 111, 
        "repository": {
            "description": "", 
            "id": 111, 
            "name": "XXX"
        }, 
        "summary": "XXX"
    }, 
    "created": "2016-04-14T14:32:04Z", 
    "createdUser": {
        "id": 111, 
        "mailAddress": null, 
        "name": "XXX", 
        "nulabAccount": null, 
        "roleType": 111, 
        "userId": null
    }, 
    "id": 111, 
    "notifications": [
        {
            "alreadyRead": false, 
            "id": 111, 
            "reason": 111, 
            "resourceAlreadyRead": false, 
            "user": {
                "id": 111, 
                "mailAddress": null, 
                "name": "XXX", 
                "nulabAccount": null, 
                "roleType": 111, 
                "userId": null
            }
        }, 
        {
            "alreadyRead": false, 
            "id": 111, 
            "reason": 111, 
            "resourceAlreadyRead": false, 
            "user": {
                "id": 111, 
                "mailAddress": null, 
                "name": "XXX", 
                "nulabAccount": {
                    "name": "XXX", 
                    "nulabId": "XXX", 
                    "uniqueId": "XXX"
                }, 
                "roleType": 111, 
                "userId": null
            }
        }
    ], 
    "project": {
        "archived": false, 
        "chartEnabled": true, 
        "id": 111, 
        "name": "XXX", 
        "projectKey": "XXX", 
        "subtaskingEnabled": true
    }, 
    "type": 111
}
```

### 上記3つをmergeしたstruct

```go
type Backlog struct {
	Content struct {
		Changes []interface{} `json:"changes"`
		Comment struct {
			Content string `json:"content"`
			ID      int    `json:"id"`
		} `json:"comment"`
		Description string      `json:"description"`
		Diff        interface{} `json:"diff"`
		ID          int         `json:"id"`
		Issue       struct {
			Description string `json:"description"`
			ID          int    `json:"id"`
			KeyID       int    `json:"key_id"`
			Summary     string `json:"summary"`
		} `json:"issue"`
		Number     int `json:"number"`
		Repository struct {
			Description string `json:"description"`
			ID          int    `json:"id"`
			Name        string `json:"name"`
		} `json:"repository"`
		Summary string `json:"summary"`
	} `json:"content"`
	Created     string `json:"created"`
	CreatedUser struct {
		ID           int         `json:"id"`
		MailAddress  interface{} `json:"mailAddress"`
		Name         string      `json:"name"`
		NulabAccount interface{} `json:"nulabAccount"`
		RoleType     int         `json:"roleType"`
		UserID       interface{} `json:"userId"`
	} `json:"createdUser"`
	ID            int `json:"id"`
	Notifications []struct {
		AlreadyRead         bool `json:"alreadyRead"`
		ID                  int  `json:"id"`
		Reason              int  `json:"reason"`
		ResourceAlreadyRead bool `json:"resourceAlreadyRead"`
		User                struct {
			ID           int         `json:"id"`
			MailAddress  interface{} `json:"mailAddress"`
			Name         string      `json:"name"`
			NulabAccount interface{} `json:"nulabAccount"`
			RoleType     int         `json:"roleType"`
			UserID       interface{} `json:"userId"`
		} `json:"user"`
	} `json:"notifications"`
	Project struct {
		Archived          bool   `json:"archived"`
		ChartEnabled      bool   `json:"chartEnabled"`
		ID                int    `json:"id"`
		Name              string `json:"name"`
		ProjectKey        string `json:"projectKey"`
		SubtaskingEnabled bool   `json:"subtaskingEnabled"`
	} `json:"project"`
	Type int `json:"type"`
}
```

## 実装

```go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

type Slack struct {
	Channel   string `json:"channel"`
	IconEmoji string `json:"icon_emoji"`
	IconUrl   string `json:"icon_url"`
	Text      string `json:"text"`
	Username  string `json:"username"`
}

type Backlog struct {
	Content struct {
		Changes []interface{} `json:"changes"`
		Comment struct {
			Content string `json:"content"`
			ID      int    `json:"id"`
		} `json:"comment"`
		Description string      `json:"description"`
		Diff        interface{} `json:"diff"`
		ID          int         `json:"id"`
		Issue       struct {
			Description string `json:"description"`
			ID          int    `json:"id"`
			KeyID       int    `json:"key_id"`
			Summary     string `json:"summary"`
		} `json:"issue"`
		Number     int `json:"number"`
		Repository struct {
			Description string `json:"description"`
			ID          int    `json:"id"`
			Name        string `json:"name"`
		} `json:"repository"`
		Summary string `json:"summary"`
	} `json:"content"`
	Created     string `json:"created"`
	CreatedUser struct {
		ID           int         `json:"id"`
		MailAddress  interface{} `json:"mailAddress"`
		Name         string      `json:"name"`
		NulabAccount interface{} `json:"nulabAccount"`
		RoleType     int         `json:"roleType"`
		UserID       interface{} `json:"userId"`
	} `json:"createdUser"`
	ID            int `json:"id"`
	Notifications []struct {
		AlreadyRead         bool `json:"alreadyRead"`
		ID                  int  `json:"id"`
		Reason              int  `json:"reason"`
		ResourceAlreadyRead bool `json:"resourceAlreadyRead"`
		User                struct {
			ID           int         `json:"id"`
			MailAddress  interface{} `json:"mailAddress"`
			Name         string      `json:"name"`
			NulabAccount interface{} `json:"nulabAccount"`
			RoleType     int         `json:"roleType"`
			UserID       interface{} `json:"userId"`
		} `json:"user"`
	} `json:"notifications"`
	Project struct {
		Archived          bool   `json:"archived"`
		ChartEnabled      bool   `json:"chartEnabled"`
		ID                int    `json:"id"`
		Name              string `json:"name"`
		ProjectKey        string `json:"projectKey"`
		SubtaskingEnabled bool   `json:"subtaskingEnabled"`
	} `json:"project"`
	Type int `json:"type"`
}

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		var b Backlog
		err := json.NewDecoder(r.Body).Decode(&b)
		if !(err != nil && err == io.EOF) {

			s := Slack{Text: fmt.Sprintf("https://{YOUR_BACKLOG_DOMAIN}.backlog.jp/git/%s/%s/pullRequests/%d", b.Project.ProjectKey, b.Content.Repository.Name, b.Content.Number)}
			j, _ := json.Marshal(s)
			http.PostForm("https://hooks.slack.com/services/XXXXX/XXXXX/XXXXXXX", url.Values{"payload": {string(j)}})
		}
	})
	http.ListenAndServe(":80", nil)
}
```

URL送ったところで力尽きたorz

## 感想戦
- json調べるのにほぼほぼの時間を使った
- プロジェクト配下のすべてのリポジトリのpull-requestがwebhookで送られてくるので不要なものを無視するのがひと手間
- コメントごとにslackに通知が来るのはだいぶノイズなのでn分間隔でpoolして重複は除去して通知とかやりたい(やるとはいってない