LoginSignup
13
9

More than 5 years have passed since last update.

Slash Commands と Slack Interactive Message 入門

Posted at

つくるもの

  • Slash interactive message を使ったインタラクティブな Slack

    • 今回は slash command ですが、もちろん、 outgoing と組み合わせたり bot にしても大丈夫です
  • GolangでSlack Interactive Messageを使ったBotを書く ではコードはかけると思うのですが、 Slack 上での設定が多すぎて迷うと思ったので書きました。

  • 実装側は Deeeet さんので十分です

やること

  • Slack 上での登録/設定
  • Global から Request できる Web サーバ (API)

補足

  • 本番環境は適宜設定するとして、開発環境は ngrok を使うとよいです(が自社のセキュリティルールに合わせてください)
  • mac の場合は brew cask install ngrok で入ります

Slack 上での登録/設定

事前に ngrok http 3000 などを実行し、エンドポイントを取得しておいてください

  1. https://api.slack.com/apps を開く
  2. 「Create New App」で App を作成 image.png
    1. Slack Team を選ぶ
    2. あとで配ったりもできるので、手元のテスト環境用の team で大丈夫です
  3. 「Basic information」->「Add features and functionality」から「Slash Commands」を選択し、下記を設定
    1. Command: Slack 上で 「/XXX」 と入力して app を呼び出すコマンドを決めます
    2. Request URL: app の呼び出しエンドポイントを登録します
    3. その他は適当に
  4. 「Interactive Messages」を設定
    1. Request URL: Button / Menu を選択したときに呼び出されるエンドポイントを登録します

API サーバを書く

  • 今回は適当に node で書いていますが、好きな言語で書いてください。

Slash Command を受けて Button / Menu を返す

  • slash コマンドからの API 呼び出しは POST で来ます
{
  "token":"XXXXXXXXXXXXXXXXX",
  "team_id":"XXXXXXXXXXXX",
  "team_domain":"muddydixon",
  "channel_id":"XXXXXXXXXXX",
  "channel_name":"general",
  "user_id":"XXXXXXXXXXXX",
  "user_name":"muddydixon",
  "command":"/deploy",
  "text":"",
  "response_url":"https://hooks.slack.com/commands/XXXXXXXXX/XXXXXXX/XXXXXXXXXXX
}
  • command を確認して適切な処理に振り分ければ1アプリで複数のコマンドを受けることができます
  • Interactive Message は現時点で Button と Menu を使うことができます。
    • Interactive Messages にサンプルの JSON も載っていますので参考にしてください
  • Button と Menu の場合で返却する JSON が異なるので、適当に messageBuilder を作って簡略化しておきました
  • Slack の該当 channel で設定した slack command をうち、うまくいくと下記のように応答が返ってきます

image.png

Interactive Message のユーザ応答を受ける

  • Interactive Message からの API 呼び出しも POST できます
  • body.payload の中身は文字列化された JSON で parse が必要です (下記には parse した JSON を記載しておきます)
{
  "actions":[{"name": "version", "type": "button", "value": "0.0.2"}],
  "callback_id": "version",
  "team": {"id": "XXXXXXX", "domain": "muddydixon"},
  "channel": {"id": "XXXXXXXXX", "name": "general"},
  "user": {"id": "XXXXXXXXX", "name": "muddydixon"},
  "action_ts": "1499824071.755716",
  "message_ts": "1499824068.033282",
  "attachment_id": "1",
  "token": "XXXXXXXXXXXXXXXXX",
  "is_app_unfurl": false,
  "original_message": {
    "text": "デプロイするバージョン",
    "bot_id": "XXXXXXXXX",
    "attachments": [
      {
        "callback_id": "version",
        "fallback": "choose version",
        "id": 1,
        "actions": [
          {
            "id": "1", "name": "version", "text": "0.0.1",
            "type": "button", "value": "0.0.1", "style": ""
          }, {
            // 
          }
        ]
      }
    ],
    "type": "message",
    "subtype": "bot_message",
    "ts": "1499824068.033282"
  },
  "response_url": "https: //hooks.slack.com/actions/XXXXXXXXXXXX/XXXXXXXXXXXXXXX/XXXXXXXXXXXXXXX"
}
  • レスポンスのルールとしてはきちんと {text: "XXX"}を返して、履歴を残すようにしましょう

サンプルコード

const http = require("http");
const express = require("express");
const BodyParser = require("body-parser");

const app = express();
app.use(BodyParser.json());
app.use(BodyParser.urlencoded({extended: true}));

// body を確認するため
app.use((req, res, next)=>{
  console.log(`${req.path}, ${req.method}, ${JSON.stringify(req.query)}, ${JSON.stringify(req.body)}`);
  next();
});

// 返却する json を組み上げるのが面倒なので builder
const messageBuilder = (text = "", subtext = "", type = "button", name = "choice", actions = [], options = {})=>{
  if(["button", "select"].indexOf(type) === -1) return null;
  return {
    text,
    response_type: options.responseType || "in_channel",
    attachments: [
      {
        text: subtext,
        fallback: options.fallback || `choose ${name}`,
        callback_id: options.callback_id || `${name}`,
        color: options.color || "info",
        attachment_type: "default",
        actions: type === "button" ? actions.map((action)=>{
          return {
            name,
            text: action,
            type,
            value: action.toLowerCase()
          };
        }) : [{
          name,
          type,
          text: `pick a ${name}`,
          options: actions.map((action)=>{
            return {
              text: action,
              value: action
            };
          })
        }]
      }
    ]
  };
};

// commands
const commands = {
  "/deploy": ()=>{
    // ここでなんかの処理を書く
    // ansible 叩くとか

    return messageBuilder(
      "デプロイするバージョン", "", "button", "version",
      ["0.0.1", "0.0.2", "0.0.3"]);
  }
};

Object.keys(commands).forEach((command)=>{
  app.post(`${command}`, (req, res, next)=>{
    if(!req.body) return null;
    if(req.body.command){
      return res.json(commands[req.body.command]());
    }else if(req.body.payload){
      const payload = JSON.parse(req.body.payload);
      const action = payload.actions[0];
      const type = action.type;
      return res.json({
        text: `you choose ${type === "button" ? action.value : action.selected_options[0].value}`
      });
    }
    return res.json({text: "invalid message"});
  });
  app.post(`${command}/options`, (req, res, next)=>{
    res.json({});
  });
});


const server = http.createServer(app);
const port = process.env.PORT || 3000;
server.listen(port);
server.on("listening", ()=>{
  console.log(`listening on ${port}`);
});
server.on("error", (err)=>{
  console.error(err);
});
13
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9