Slackの「Interactive buttons」を使ってアンケートをしてみた

お久しぶりです。本間です。
またSlackのBotを作ったので紹介したいと思います。 今回はまだ使ったことのない「Interactive buttons」を使ってアンケートができるBotを作ってみました。 sadf

使い方

  1. HubotのいるチャンネルにJSON形式で作成したアンケートを投稿します。
    f:id:AdwaysEngineerBlog:20170210154538p:plain

  2. ダイレクトメッセージでアンケートが届きます。
    f:id:AdwaysEngineerBlog:20170210154546p:plain

  3. ボタンを押してアンケートに答えます。
    f:id:AdwaysEngineerBlog:20170210154553p:plain

  4. Hubotのいるチャンネルでアンケートの集計を行います。
    集計結果はJSONで出力されます。
    f:id:AdwaysEngineerBlog:20170210154602p:plain

事前知識

Slack Appの作成

ここからSlack Appを作成します。
次に、Bot Userのtokenを発行するために。認証後のリダイレクトURLを設定します。
f:id:AdwaysEngineerBlog:20170210154610p:plain

Bot Userの名前とオンラインの設定を行います。
f:id:AdwaysEngineerBlog:20170210154620p:plain

Bot Userのtokenを発行

ここの手順に従ってtokenを発行します。

1.認証 下記URLを作成して認証を行います。

https://slack.com/oauth/authorize?client_id={client_id}&scope=bot&redirect_uri={redirect_uri}

2.token発行 1で取得したコードを利用してtokenを発行します。 下記URLにアクセスするとtokenを取得できます。

https://slack.com/api/oauth.access?client_id={client_id}&client_secret={client_secret}&code={code}&redirect_uri={redirect_uri}

redirect_uriは先ほど設定したリダイレクトURLを使用してください。
client_idとclient_secretはSlack Appから取得できます。
f:id:AdwaysEngineerBlog:20170210154629p:plain

Slack Appのエンドポイントの作成

今回はNode.jsのExpressを使って実装します。
このエンドポイントには「Interactive buttons」が押される度にリクエストがきます。
エンドポイントのURLはSlack Appに設定します。
f:id:AdwaysEngineerBlog:20170210154640p:plain

JSON形式のアンケートから「Interactive buttons」を作成する処理

アンケートの作成はHubotを通してSlackから行います。

メイン処理

module.exports = (robot) => {
  // ここの正規表現でJSONを取得します。
  robot.respond(/enquete[\s\S]```[\s\S]((?:[\s\S]?.*)+)[\s\S]```/i, async (res) => {
    let slack = new Slack(config.slack);
    let inquiryJson = res.match[1];
    try {
      let inquiry = JSON.parse(inquiryJson);
      // ダイレクトメッセージのチャンネルを作成します。
      // 今回はアンケート作成者にアンケートを投稿します。
      let channel_id = (await slack.openDirectMessage(res.envelope.user.id)).channel.id;
      // アンケート回答用のAttachmentsを投稿します。
      await slack.postAttachments(Inquiry.object2attachments(inquiry), channel_id);
      // アンケート集計用のAttachmentsを投稿します。
      await slack.postAttachments(Inquiry.getAggregateAttachments(), res.envelope.room);
      res.send();
    } catch(e) {
      console.log(e);
    }
  });
}

JSONからAttachmentsに変換する処理と集計用のAttachmentsを作成する処理

export class Inquiry {
  static object2attachments(object) {
    return object.questions.map( (question, index) => { return {
      fallback: object.notice,
      title: question.title,
      callback_id: index,
      color: '#0000FF',
      attachment_type: 'default',
      actions: question.choices.map( (choice, index) => { return {
          name: index,
          text: choice,
          type: 'button',
          value: choice
      }})
    }});
  }
  static getAggregateAttachments() {
    return [
      {
        fallback: '集計する',
        title: '集計する',
        callback_id: 'aggregate',
        color: '#0000FF',
        attachment_type: 'default',
        actions: [
          {
            name: 'aggregate',
            text: '集計する',
            type: 'button',
            value: 'aggregate'
          }
        ]
      }
    ];
  }
}

アンケート回答時の処理

アンケート回答時の処理は作成したエンドポイントで行います。

メイン処理

let answers = {};
router.post('/', async (req, res) => {
  let params = JSON.parse(req.body.payload);

  // 集計時の処理
  if (/^aggregate$/.test(params.callback_id)) {
    await aggregateProcess(params);
  }
  // アンケート回答時の処理
  else {
    await answerProcess(params);
  }
  res.send();
});

アンケート回答時の処理

async function answerProcess(params) {
  let index = params.callback_id;
  let original_attachments = params.original_message.attachments;
  // アンケートの質問を取得します。
  let question = original_attachments[index].title;
  // アンケートの回答を取得します。
  let answer   = params.actions[0].value;
  
  // アンケート結果をオブジェクトに保持しておきます。
  let userAnswers = answers[params.user.name] || {};
  userAnswers[question] = answer;
  answers[params.user.name] = userAnswers;

  // アンケート回答箇所だけSlackの表示を書き換えます。
  original_attachments[index] = {
    title: question,
    text: answer,
    color: '#00FF00'
  }
  // Slack Web APIにリクエストを送ってSlackのメッセージを更新します。
  await new Slack(config.slack).updateAttachments(
    params.message_ts,
    original_attachments,
    params.channel.id
  );
}

アンケート集計時の処理

アンケート集計のトリガーを「Interactive buttons」にしているので、集計の処理は作成したエンドポイントで行います。

メイン処理

let answers = {};
router.post('/', async (req, res) => {
  let params = JSON.parse(req.body.payload);

  // 集計時の処理
  if (/^aggregate$/.test(params.callback_id)) {
    await aggregateProcess(params);
  }
  // アンケート回答時の処理
  else {
    await answerProcess(params);
  }
  res.send();
});

アンケート集計時の処理

async function aggregateProcess(params) {
  // JSONを整形してSlackに投稿します。
  await new Slack(config.slack).postMessage(
    "```\n" + JSON.stringify(answers, null, '  ') + "\n```",
    params.channel.id
  );
  answers = {};
}

おわりに

今回はHubotとExpressで実装をしましたが、HubotはHTTPのリッスンもできるのでHubotにまとめたいと思いました。しかし、HubotのtokenとSlack Appのtokenが別ものなので個人的にあまりしっくりきません。。(Hubotから「Interactive buttons」を投稿する方法をご存知の方がいたら教えていただきたいです)
SlackのBot作成フレームワークにはHubotの他にもBotkitというものがあり、こちらはBot Userのtokenを使用するのでSlack Appのtoken一つでできそうです。また、HTTPのリッスンもHubot同様にできるので一つにまとまってすごいしっくりきます。今度はBotkitを使ってこのアンケートBotをさらに使いやすくしてみたです。