slackで単純なボタン付きメッセージを送る

この記事では
- slackの任意のチャンネルにボタン付きメッセージを送る
- ボタンの操作結果をwebサーバーで受け取る
という単純なサンプルについてメモを書いておきます

前提
- slackをすでに利用している
- httpsの利用可能なwebサーバーを公開できる

公式 Document
https://api.slack.com/docs/message-buttons
https://api.slack.com/interactive-messages
https://api.slack.com/docs/message-guidelines


#1. slackのアプリを作成する
ボタン付きメッセージを送るだけならアプリを作成する必要はありませんが、ボタンをクリックした結果を任意のwebサーバー側で受け取る場合はアプリを作成する必要があります。

##1-1. アプリの作成
https://api.slack.com/apps
にアクセスし、create new app からアプリを作成します。
「App Name｣に任意の名前
「Development Slack Team」にはボタン付きメッセージを送りたいチャンネルのあるチームを選択します。

##1-2. Incoming Webhooksの設定
Incoming Webhooksを設定すると任意のslackチャンネルにメッセージを送ることができるようになります。

メニューのIncoming Webhooksをクリックし、Activate Incoming Webhooksの右にあるボタンをクリックしてActiavteをOnにします。

次に Incoming Webhooksのページ下部にある「Add New Webhook to Team」のボタンをクリックします。
「Post to」にメッセージを送りたいチャンネル名を入力し、「install」をクリックします。
installが終わるとIncoming Webhooksのページに戻り、設定したチャンネルへメッセージを送るためのWebhook URLが表示されます。

##1-3. Interactive Messagesの設定
Interactive Messagesを設定するとslackメッセージのボタン操作結果を任意のWebサーバーで受け取ることができます。
メニューのInteractive Messagesをクリックし、「Enable Interactive Messages」のボタンをクリックします。
Request URLに任意のURLを入力して「Enable Interactive Messages」のボタンをクリックします。設定したURLにslackでボタンを押した結果がリクエスト(http post)されるようになります。

#2. ボタン付きメッセージの送信
slackにボタン付きメッセージを送信します。宛先のチャンネルは1-2で設定したチャンネルとなります。

##2-1 curlを使ったサンプル
curlを使ったサンプル(下記はURLの一部伏字にしてますが、1-2で設定したWebhook URLとなります)

```
curl  https://hooks.slack.com/services/***/***/*** -d'payload=
{
    "text": "Slack Message Sample Text",
    "attachments": [
        {
            "fallback": "fallback string",
            "title": "title string",
            "callback_id": "callback_id value",
            "color": "#FF0000",
            "attachment_type": "default",
            "actions": [
                {
                    "name": "btn1Name",
                    "text": "btn1",
                    "type": "button",
                    "style":"default",
                    "value": "btn1Value"
                },
                {
                    "name": "btn2Name",
                    "text": "btn2",
                    "type": "button",
                    "style":"danger",
                    "value": "btn2Value"
                },
                {
                    "name": "btn3Name",
                    "text": "btn3",
                    "type": "button",
                    "style":"primary",
                    "value": "btn2Value"
                }
            ]
        }
    ]
}'
```

成功すると下記のようなメッセージが送信されます
<img width="447" alt="スクリーンショット 2017-06-24 15.08.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/188809/579666f7-89be-161d-ba35-c9c6604dab4d.png">

#3. ボタンクリックの結果をハンドリング
ボタンを押した結果はWebhook URLに http POSTされてきます。

##3-1 ボタンクリック時のリクエストをログに出してみる
下記のようなJavaのコントローラを用意し、リクエストを受け取ってログ出力してみます。


```java:SlackReceiveController
package org.hoge.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("slackReceive")
public class SlackReceiveController {

    private Logger logger = LoggerFactory.getLogger(SlackReceiveController.class);
    
    @PostMapping(value = "", consumes = "application/x-www-form-urlencoded")
    ResponseEntity buttonAction(HttpServletRequest request, @RequestParam(value = "payload", required = true) String json) {
        logger.info(json);
        return new ResponseEntity(HttpStatus.OK);
    }
}
```

ログの結果は以下です(btn1をクリックしています)
みづらいので整形した内容を記載します（idは全て伏字)

```json:result.json
{
  "actions": [
    {
      "name": "btn1Name",
      "type": "button",
      "value": "btn1Value"
    }
  ],
  "callback_id": "callback_id value",
  "team": {
    "id": "*****",
    "domain": "*****"
  },
  "channel": {
    "id": "*****",
    "name": "******"
  },
  "user": {
    "id": "*****",
    "name": "*****"
  },
  "action_ts": "1498286982.378198",
  "message_ts": "1498286913.724083",
  "attachment_id": "1",
  "token": "*****",
  "is_app_unfurl": false,
  "original_message": {
    "text": "Slack Message Sample Text",
    "bot_id": "*****",
    "attachments": [
      {
        "callback_id": "callback_id value",
        "fallback": "fallback string",
        "title": "title string",
        "id": 1,
        "color": "FF0000",
        "actions": [
          {
            "id": "1",
            "name": "btn1Name",
            "text": "btn1",
            "type": "button",
            "value": "btn1Value",
            "style": "default"
          },
          {
            "id": "2",
            "name": "btn2Name",
            "text": "btn2",
            "type": "button",
            "value": "btn2Value",
            "style": "danger"
          },
          {
            "id": "3",
            "name": "btn3Name",
            "text": "btn3",
            "type": "button",
            "value": "btn2Value",
            "style": "primary"
          }
        ]
      }
    ],
    "type": "message",
    "subtype": "bot_message",
    "ts": "1498286913.724083"
  },
  "response_url": "https://hooks.slack.com/actions/***/***/***"
}
```
actionsにクリックしたボタンの情報がありますので、こちらを利用することで処理の分岐が可能です。

##3-2 ボタンクリック時のリクエストを受けて元のメッセージを編集する
ボタンクリック時のリクエストに対し、webサーバー側(SlackReceiveController)からのresponse bodyにjsonを設定することで元のメッセージを書き換えることが可能です。
クリックしたボタンに応じて元のメッセージを書き換えるサンプルは下記の通り

```java:SlackReceiveController
package org.hoge.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.cloudfoundry.com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

@Controller
@RequestMapping("slackReceive")
public class SlackReceiveController {

    private Logger logger = LoggerFactory.getLogger(SlackReceiveController.class);

    @PostMapping(value = "", consumes = "application/x-www-form-urlencoded")
    ResponseEntity buttonAction(HttpServletRequest request, @RequestParam(value = "payload", required = true) String json) {

        //json文字列を雑にMap objectに変換
        ObjectMapper mapper = new ObjectMapper();
        HashMap<String, Object> payloadMap = null;
        try {
            payloadMap = mapper.readValue(json, HashMap.class);
        } catch (IOException e) {
            return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        //クリックされたactionsの取り出し
        List<HashMap<String, String>> actions = (List<HashMap<String, String>>) payloadMap.get("actions");
        String clickedBtn = actions.get(0).get("name");

        //オリジナルメッセージの取り出しと書き換え
        LinkedHashMap<String, Object> original_message;
        original_message = (LinkedHashMap) payloadMap.get("original_message");
        original_message.put("text", "Clicked: " + clickedBtn);

        //bodyを設定
        return new ResponseEntity(original_message, HttpStatus.OK);
    }
}

```
元々のメッセージ
<img width="364" alt="スクリーンショット 2017-06-24 16.25.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/188809/a296a5a5-bd6d-89dc-8793-24f9910e0eaa.png">

btn1 クリック後
<img width="382" alt="スクリーンショット 2017-06-24 16.25.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/188809/566701ae-fb6f-bb18-b2ac-ab3e37b5827c.png">

btn2 クリック後
<img width="317" alt="スクリーンショット 2017-06-24 16.26.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/188809/aa79aeda-9051-5d2a-a9f8-65ed73c84907.png">

btn3 クリック後
<img width="305" alt="スクリーンショット 2017-06-24 16.27.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/188809/18e671f9-7894-e984-91b3-2a4b4876d9ae.png">