SlackのIncoming Webhooksを使い倒す

この記事は[ピクシブ株式会社 AdventCalendar 2017](https://qiita.com/advent-calendar/2017/pixiv)の14日目の記事です。
昨日は @Ragg による [CSSをRailsとゆるふわにお付き合いさせる話](https://qiita.com/Ragg/items/b96cdfabd93a762c3ec0)でした。

こんにちは、普段はpixivの決済基盤のシステムを担当しているエンジニアのikです。
(※今回は決済システムの話はしません)

---

# Slack Incoming Webhooksとは
Slackには、様々なAPIや外部サービスとの連携機構があります。Incoming Webhooksはその中の1つで「外部サービスからSlackにメッセージを送信する」ための機能です。

この機能を使うと、外部からHTTPリクエストでメッセージを送ることができます。本質はHTTPリクエストを送るだけなのでライブラリ等を導入する必要もなく、HTTP通信ができる言語・環境であればどこからでも送ることができます。

# Incoming Webhooksのエンドポイントを取得する
この機能を使うには、まず送信先のエンドポイントをSlackで作成する必要があります。

[Slackのアプリケーション追加ページ](https://slack.com/apps)より `incoming-webhooks` を検索・選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/20a1d29d-82dc-df91-1a5f-6b8d468e09ba.png)

Incoming Webhooksの設定画面が開かれるので、 `Add Configuration` から設定を追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/53ac581a-674c-443c-7263-985a2015a538.png)

メッセージを送信するチャンネルを選ぶ画面が表示されるので、選択して `Add incoming WebHooks integration` をクリックすると、webhookのエンドポイントが発行されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/7b32a569-86cc-bc14-88d0-ec76a849bf38.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/005b1eba-921f-d1d7-45d7-53bba93a273c.png)
(注: このURLを知っていればだれでも今追加したチャンネルにメッセージをPOSTできてしまうので、関係ない第三者が見れないように注意してください。)

また、このページでデフォルトで表示されるbot名・アイコン、Descriptionを設定することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/cc82fe47-8660-a2f7-7aad-42e0f02d3c71.png)
設定した値は、このURLに対してPOSTしたメッセージを表示するときに使用されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/15286af4-1e3a-cb84-c581-2af32f1b7a77.png)

# テキストをPOSTする
単純なメッセージを送信するには、以下のようなJSONを `payload`パラメータとして、エンドポイントに対してPOSTします。

```json
{"text": "TEST"}
```

たとえばこのメッセージをシェル上でcurlを使って送信する場合は以下のようにします。

```sh
$ curl -X POST --data-urlencode "payload={\"text\": \"TEST\" }" https://hooks.slack.com/services/TAAAAAAAA/BBBBBBBBBB/ZZZZZZZZZZZZZZZZZZZZZ
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/5b2b97e0-b72a-9751-56cb-276fe0a5cd10.png)

## 装飾をしたメッセージ群を送る
先程の例では、シンプルなテキストのみを送りましたが、より複雑な装飾をしたメッセージを送ることもできます。

例えば以下のJSONをPOSTすると以下の画像のようなメッセージとして表示されます。

```json
{
   "attachments":[
      {
         "fallback":"fallback Test",
         "pretext":"attachments Test",
         "color":"#D00000",
         "fields":[
            {
               "title":"attachment01",
               "value":"This is attachment"
            }
         ]
      }
   ]
}	
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/08aeedc8-6181-3c4a-e44f-e3f2243c86f4.png)

この時、表示されていない `fallback` はattachmentsが表示できない環境(プッシュ通知の文言等)の表示に使用されます。

また、attachmentsは複数同時に渡すことも可能です。

```json
{
   "attachments":[
      {
         "fallback":"fallback Test",
         "pretext":"attachments Test",
         "color":"#D00000",
         "fields":[
            {
               "title":"attachment01",
               "value":"This is attachment"
            }
         ]
      },
      {
         "fallback":"fallback Test",
         "pretext":"attachments Test02",
         "color":"#00FF00",
         "fields":[
            {
               "title":"attachment02",
               "value":"This is attachment02"
            }
         ]
      }
   ]
}	
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/aa0a9ecf-e799-557b-3a28-2456b91bb8c7.png)

attachmentsには、ここで書いた以外にも様々な表示オプションがあり、それらを使用すれば以下のようなメッセージも作成できます。

```json
{
   "attachments":[
      {
         "author_name": "ik-fib",
         "author_link": "https://twitter.com/ik11235",
         "author_icon": "https://s.gravatar.com/avatar/ce68aabafe314bb9524700960fe7b6dc?s=80",
         "fallback":"ik-fibがツイートしました",
         "pretext":"ik-fibがツイートしました",
         "color":"#D00000",
         "fields":[
            {
               "title":"Twitter",
               "value":"なう"
            }
         ],
         "footer": "Slack API",
         "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png",
         "ts": 123456789
      }
   ]
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/ee01898c-f701-aa25-810f-ea917c6a4a47.png)

attachmentsのオプションはこれ以外にもいくつもあり、以下の公式ドキュメントでも確認できます。

- [Attaching content and links to messages \| Slack](https://api.slack.com/docs/message-attachments) 

# Tips
## Slackでの表示を簡単に確認する
webhookを使ってメッセージ送信botを作る際に、思っていたフォーマットになるかどうかを毎回JSONをPOSTして確認するのはなかなかに面倒な作業です。
しかし、JSONを書くとそれがどのようなSlackメッセージになるかをすぐにプレビューできる機能が公式から提供されています

[Message Formatting \| Slack](https://api.slack.com/docs/messages/builder)

こちらではテキストエリアに記載したJSONをすぐにslackのメッセージ形式でプレビューできます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/a68c2da5-84cb-4c98-ee07-fff1112bffe9.png)

ただし、後述するアイコンの絵文字指定機能でカスタム絵文字を指定した場合は解釈されずに文字のまま処理されたり、一部は完全には動きません。(カスタム絵文字まで処理を期待するのは流石に厳しいですが、昔は一部正常オプションも解釈してくれずエラー扱いになった記憶があるので…)

### ポストごとに送信するチャンネルを変える
基本的にwebhookでメッセージが送信されるチャンネルは最初の設定で行ったチャンネルですが、`channel` にチャンネル名を指定することでポストごとに送信するチャンネルを変えることも可能です。

```json
{
    "channel": "#new_channel",
    "text": "I am a test message http://slack.com"
}
```

ただし、この方法でチャンネル指定をしている場合、その送信先のチャンネル名を変更した場合、その名前をもつチャンネルがなくなるのでエラーになります。(webhookの設定で指定している場合は自動で対応される)
コード上も変更が必要になるため、同一botで可変にする必要がある場合や、テスト送信をsandboxのチャンネルに向けるなど一時的な使い方以外にはあまりおすすめしません。

## ポストごとにbotの表示名を変える
基本的にbotメッセージの表示名は設定で指定したものになりますが、 `username` で可変的に指定することが可能です。

```json
{
    "username": "スーパーwebhookおじさん",
    "text": "test message"
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/8b2f5171-b369-e224-d09f-5b7b83481f82.png)

## ポストごとにbotのアイコンを変える
基本的にbotメッセージのアイコンは設定で指定したものになりますが、 `icon_emoji` で可変的に指定することが可能です。

```json
{
    "icon_emoji": ":bow:",
    "text": "Test message"
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/4a9978fd-6758-8eb5-c762-e9636e31fcc7.png)

また、これはslackに最初から設定されている絵文字以外にも[カスタム絵文字機能](https://get.slack.help/hc/ja/articles/206870177-%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%A0%E7%B5%B5%E6%96%87%E5%AD%97%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)を使って追加した絵文字も指定可能です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/71edc534-d5b7-e963-7cb7-3b985bc40082.png)

```json
{
    "icon_emoji": ":cool_emoji:",
    "text": "カスタム絵文字も指定できます"
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/d146a49a-9fc1-4c7b-708f-21ac66868d96.png)

## ユーザーにメンションを送る
webhookに@つきでテキストを書いてもそのままではユーザーへのメンションになりません。

```json
{
    "text": "Hello @pixiv",
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/a053806e-d16b-3bbb-b8e6-4dd8c5bba26c.png)

これをメンションにするには、 `link_names` を指定します。

```json
{
    "text": "Hello @pixiv",
    "link_names": 1
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/c391709d-cc5e-ee5f-d3c3-8f92ea16f7e9.png)

他の方法としては、`<>`で囲むことでも有効になります。

```json
{
    "text": "good night <@pixiv>"
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/bcea34c8-f69a-6d82-d3eb-f78234d0fb8d.png)

ただし、この書式で`@here`や`@channel`指定をする場合は、`@`ではなく`!`で指定する必要があります。

```json
{
    "text": "come on <@here> <@channel>"
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/32f65091-6403-1241-00e5-a285d051766b.png)

```json
{
    "text": "come on <!here> <!channel>"
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/fe56f25a-1cf8-514a-756f-4e566504c48f.png)

ただし有料プランで使える[ユーザーグループ](https://get.slack.help/hc/ja/articles/212906697-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)に対してのメンションはこれらの方式ではできず、グループのID(slack側で自動で割り振られている固有の英数字)を調べて指定する必要があります。

- 参考:[Basic message formatting \| Slack](https://api.slack.com/docs/message-formatting#variables)

## URLリンクを特定の文字列につける
メッセージ内にURLを含めたら、自動的にリンクとして処理してくれますが、長いURLの場合や文章内に入れる場合には、特定のテキストに対して、リンクを付けたい場合もあります、
その場合、 `<URL|特定の文字列>` として表記することでリンクとしてメッセージになります。

```json
{
    "text": "<https://pixiv.net/|This is pixiv link>"
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/c2a12250-56d4-8234-a148-d8e957b01c24.png)

## attachments内にslackのメッセージ書式設定を適用する
slackには、[Markdownに似たメッセージ書式設定](https://get.slack.help/hc/ja/articles/202288908-%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%81%AE%E6%9B%B8%E5%BC%8F%E8%A8%AD%E5%AE%9A)があり、webhookによるメッセージでも使えますが、attachmentsの中ではそのままでは有効になりません。

```json
{
    "text": "ここでは \`何も指定しなくても\` *メッセージ* _書式指定_ が反映されるけど、",
    "attachments": [
        {
            "text": "ここではそのままでは *使えない*"
        }
    ]
}
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/d729ceee-8028-0651-e582-e76fed798173.png)

attachments内部の要素にこの記法を適用するには、`mrkdwn_in` で適用したい要素を指定する必要があります。

```json
{
    "text": "ここでは \`何も指定しなくても\` *メッセージ* _書式指定_ が反映されるけど、",
    "attachments": [
        {
            "text": "ここで使うには _以下の_ *指定が必要* ",
            "mrkdwn_in": [
                "text"
            ]
        }
    ]
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/39736/8a691a29-1568-e4fe-14ea-cdaf57074e32.png)

# 利用例
ここからは実際にピクシブの業務でも使っているSlack webhookの利用例を紹介します。

## Jenkinsのビルドの成否を通知する
ピクシブでは一部のプロジェクトでJenkinsを使用しています。

その中で、Jenkinsのビルド結果によって異なる内容をSlackに通知するのを、シェルスクリプトで実行しています。
(「[Slack Notification Plugin](https://plugins.jenkins.io/slack)」 というJenkinsプラグインもありますが、プロジェクトごとやビルドごとに異なる内容を柔軟に変えたいため、シェルスクリプトで実行しています。(プラグインを使っているプロジェクトもあります))

```sh
WEBHOOK_URL="https://hooks.slack.com/services/TAAAAAAAA/BBBBBBBBBB/ZZZZZZZZZZZZZZZZZZZZZ"

POST_DATA=`cat << EOF
    payload={
    "text": "ジョブ開始 <${BUILD_URL}|${BUILD_TAG}>"
  }
EOF`
curl --data-urlencode "${POST_DATA}" ${WEBHOOK_URL}

# jenkinsで実行したい処理本体
php batch/job.php &&:
RET=$?

if [ ${RET} -eq 0 ]; then
  POST_TEXT="ジョブ成功"
else
  POST_TEXT="@channel ジョブ失敗！ <${BUILD_URL}|失敗したジョブ>"
fi

POST_DATA=`cat << EOF
    payload={
    "text": "${POST_TEXT}",
    "link_names": 1
  }
EOF`
curl --data-urlencode "${POST_DATA}" ${WEBHOOK_URL}

exit $RET
```

バッチ処理本体の本体部分を `&&:` しているのは、jenkinsが返り値が0以外の場合、即時エラーとして処理を停止 & ビルド失敗とするため、後続の失敗通知を処理してもらうために使用しています。

これ以外にも該当箇所のみ　`set +e` を指定して0以外の返り値でも処理を続行するように指定する方法もありますが、このようにしないとビルド本体の終了コードを元に処理を書くのが困難です。

## rubyの集計スクリプト結果をPOSTする
webhookは単純なPOSTリクエストでしかないので、インターネットにPOSTできる環境であれば、どこからでもPOSTできます。
例えば、Slackのサーバからアクセスして通知するような連携だと、外部からアクセス不可能なネットワークにあるデータを取得することはできませんが、
webhookで通知するスクリプトと内部のネットワーク内部で動かしてSlackにpostすることでそのようなデータでも通知可能です。(外部サーバにPOSTするので、扱うデータのポリシーは所属する組織・団体のルールに合わせて注意してください。)

```ruby
require 'net/https'
require 'json'

# 通知したいデータの集計や整形処理

result = #通知したいデータを処理した結果

uri = URI.parse("https://hooks.slack.com/services/TAAAAAAAA/BBBBBBBBBB/ZZZZZZZZZZZZZZZZZZZZZ")

request = Net::HTTP::Post.new(uri.request_uri)
request.body = {text: "処理結果: #{result}"}.to_json

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

http.start do |h|
  puts h.request(request).body
end
```

rubyには、SlackAPIを扱えるgemもありますが、単純な通知だけなら上記のように、gemを使わずに単純なPOSTだけでも可能です。

# まとめ
Slackには、様々なアプリ連携機能がありますが、Webhookを使えば対応していないサービスや自社サービス、自前のPCからもメッセージが多種多様・好きな形式で送れるので、用途に合わせた通知をすることができます。

ここで紹介した以外にも、webhookでインタラクティブなボタン付きのメッセージを送るなどもできるので、ぜひ試してみてください。

---

[ピクシブ株式会社](https://www.pixiv.co.jp/)では、botを駆使して業務を効率化できるエンジニアを募集しています。

明日は @orekyuu がなんとかJの話をしてくれるかもしれないので、乞うご期待