LoginSignup
57
87

More than 3 years have passed since last update.

Slack App+GASでボタン選択式botを作ろう

Last updated at Posted at 2019-06-28

はじめに

大学の研究室で交代制のゴミ出し当番が決まっているのですが、出し方のルールがよくわからないことを理由にゴミ出しをサボる人がいてゴミが溢れかえってます。そこで「ゴミの捨て方を優しく教えてくれちゃうbotを研究室Slackに導入して言い訳を封じよう!!みんなの役に立とう!!」と思い、ウルトラ優秀サイトであるQiitaとかを参考にちょろちょろ〜っと作ってみるか!と意気込んだものの、意外と苦戦したので, ゼロ知識からSlack App + Google Apps Script (GAS)でbotを作るまでに詰まったポイントと解決方法, 参考になった記事をまとめます。

SlackAppの開発は初めてで、GASどころかJavascriptも書いたことなく、2週間程度で作ったので何か間違っている点などありましたら是非ご指摘お願いします!

本記事の目的

  • 裏側の処理をGoogle Apps Scriptで行う
  • Slackチャンネル内のスラッシュコマンドに反応して応答
  • 選択肢ボタンをユーザに提示し, 押されたボタンに応じて応答を変える
  • チャンネルがbotメッセージに埋もれないようにbotを呼び出した人にのみメッセージを表示
  • Google Driveに置いた画像を添付する

を実現するbotの作成を目的とします。だいたいこんな感じの構造です。
qiita_1_1.png

Slack Appの設定(前半)

まずはbotを導入をしたいSlackにAppを作成します。基本的な設定方法は省略します。

パーミッションの設定

Slack Appに持たせる権限を設定します。Appのメニューの Features > OAuth&Permissionsを選択、Select Permission Scopesで下記の4種を追加しておきましょう。

スクリーンショット 2019-06-30 20.45.40.png

Send messages as (App名):
botとしてメッセージを送信するために必要なパーミッションっぽいです。
Send messages as user:
ユーザーとしてメッセージを送信するために必要なパーミッションっぽいです。今回はbotとしてメッセージを送信するだけなので必要ないかもしれないです。(未検証)
Post to specific channels in Slack:
Incoming Webhookを使わないので必要ないかもしれないです。(未検証)
Add slash commands and add actions to messages (and view related content):
スラッシュコマンドを利用するために必要な権限です。

チャンネルへのインストールをした後でこの設定を変更すると、権限が変わるので再インストールを求められます。指示通りに再インストールをしましょう。

Verification Tokenの取得

後に触れるGASはWebアプリケーション用URLを発行すると、URLを知っている人であればだれでも実行することができます。可能性は低いですが、このURLを第三者に知られるといたずら(ひたすらbotを勝手に動かされるなど)をすることが可能なので、特定のSlack Appからのみアクセスできるように照合用のトークンを取得しましょう。

作成したSlack AppのapiでサイドメニューからSettings > Basic Informationを選択し App Credentials のVerification Tokenを確認しておきます。

スクリーンショット 2019-06-27 18.02.55.png

とりあえずSlack App側はこれで放置します。

Google Driveの設定

今回は応答メッセージに画像を添付しますが、画像データをGoogleドライブに置き、Slackアプリからアクセスして利用する方法を採用します。画像をWeb上に置く方法はいろいろありますがGoogle使うのがなんとなく安心感あるので()

画像公開用URLの取得

Googleドライブの任意の場所に画像をアップロードし、右クリックで共有可能なリンクの取得を選択、画像のステータスを公開に切り替えてURLを取得します(勝手にクリップボードにコピーされます)。 Google Driveですが、別にGASと同じアカウントである必要はありません
スクリーンショット 2019-06-28 3.20.22.png
URLはhttps://drive.google.com/open?id=hogehogehogehogehoge
みたいな形になると思います。これによりドライブの持ち主以外もこの画像を閲覧できるようになるのですが、Slack AppではこのURLから画像を挿入することができません。

参照用URLの作成

そこでこちらの記事を参考にSlack Appが参照できるURLを作ります。

https://drive.google.com/uc?id=hogehogehogehogehoge

このように取得したURLのid=以降のほげほげ部分を同様に挿入すれば参照用アドレスの出来上がりです。

Google Apps Scriptの設定

Slack側ではプログラムを組んで処理をすることができないので、なんらかの外部サービスで処理を行う必要があります。Webアプリとして使えるプラットフォームにGoogle Cloud Function(有料)やAWS lambda(だいたい有料)やがありますが、今回は処理が軽いので無料で使えるGoogle Apps Script (GAS)を使って裏処理を行います。無料サイコー!!フゥー(👆 ՞ਊ ՞)👆

Goodle Apps Scriptの作成とWebアプリケーション化方法の確認

基本のGASの作成方法とWebアプリ化の方法は省略します。GASを使ったことのない人でもこちらのサイトを参考に導入から順に学べます。

Slack用ライブラリの導入

GASでSlack Appの処理をする場合はライブラリが必要となります。こちらの記事の著者様が作ったライブラリを導入しましょう。

メニューの リソース > ライブラリ を選択し、ライブラリキーM3W5Ut3Q39AaIwLquryEPMwV62A3znfOOでライブラリを追加します。

bot用Webアプリの作成

では具体的にWebアプリケーションを作ってみましょう。だいたいの構造はこちらの記事を参考にしました。

以下のアプリ①とアプリ②はそれぞれ別のプロジェクトとしてして作成します。

スラッシュコマンドを受け取るアプリ : アプリ①

全体コード

Slack Appがスラッシュコマンド\gomibotを受け付けたことによって、リクエストURL(= アプリ①の公開URL)を叩きます。

アプリ①のコード(クリックして展開)
slash_receive.gs
function doPost(e) {
  var slack_token = 'トークン'; // Verification Tokenで取得したトークン
  // 指定したチャンネルからの命令しか受け付けない
  if (slack_token != e.parameter.token) {
    throw new Error(e.parameter.token);
  }

  // 返答データ本体
  var data = {
    "text": "Hello! I'm Gomi-bot. I'll tell you how to take out trash.", //アタッチメントではない通常メッセージ
    "response_type":"ephemeral", // ここを"ephemeral"から"in_chanel"に変えると他の人にも表示されるらしい(?)
    //アタッチメント部分
    "attachments": [{
      "title": "Language Select",// アタッチメントのタイトル
      "text": "Please select language.",//アタッチメント内テキスト
      "fallback": "Yeeeeeeeeeeah!!!",//ボタン表示に対応してない環境での表示メッセージ. 
      "callback_id": "callback_button",
      "color": "#00bfff", //左の棒の色を指定する
      "attachment_type": "default",
      // ボタン部分
      "actions": [
        //ボタン1
        {
          "name": "eng",
          "text": "English",
          "type": "button",//
          "value": "language"
        },
        //ボタン2
        {
          "name": "jpn",
          "text": "日本語",
          "type": "button",
          "value": "language"
        }
        ]
      }]
  };
  //  botを呼び出した人にのみ表示する
  //   返信データをJSON形式に変換してチャンネルに返す
  return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);
}

Slack Appからのメッセージの受信


function doPost(e) {
//中略
}

function doPost(e)はスラックからのメッセージをeで受け取り、実行される関数です。

トークンの認証

var slack_token = 'トークン'; // Verification Tokenで取得したトークン
// 指定したチャンネルからの命令しか受け付けない
if (slack_token != e.parameter.token) {
  throw new Error(e.parameter.token);
}

叩かれたアプリ①は先ほど確認したVerification Token('トークン'部分にはこのトークンが入ります)とリクエスト元のトークンe.parameter.tokenが一致していることを確認し、一致しない場合はエラー終了します。

送信データの設定

次に選択肢付きのメッセージデータをvar dataで定義します。


var data = {
    "text": "Hello! I'm Gomi-bot. I'll tell you how to take out trash.", //アタッチメントではない通常メッセージ
    "response_type":"ephemeral", // ここを"ephemeral"から"in_chanel"に変えると他の人にも表示されるらしい(?)
    //アタッチメント部分
    "attachments": [{
      "title": "Language Select",// アタッチメントのタイトル
      "text": "Please select language.",//アタッチメント内テキスト
      "fallback": "Yeeeeeeeeeeah!!!",//ボタン表示に対応してない環境での表示メッセージ. 
      "callback_id": "callback_button",
      "color": "#00bfff", //左の棒の色を指定する
      "attachment_type": "default",
      // ボタン部分
      "actions": [
        //ボタン1
        {
          "name": "eng",
          "text": "English",
          "type": "button",
          "value": "language"
        },
        //ボタン2
        {
          "name": "jpn",
          "text": "日本語",
          "type": "button",
          "value": "language"
        }
        ]
      }]
  };
"text"
通常のメッセージ部分です。
"response_type"
投稿を他人にも見せるか否かを決めるオプションです。デフォルトで他人には見えない"ephemeral"となっているので書かなくても良いです。"in_chanel"に変えると他人にも表示されるらしい(?)です。
"attachment"
通常メッセージよりも装飾をつけられて豪華な感じ(語彙力)のアタッチメントというメッセージを定義できます。1つのメッセージに複数のアタッチメントを添付することも可能です。詳しいことはSlack API attachmentsチートシートが参考になります。
"title"
アタッチメントのタイトル部分です。太いです。
"text"
アタッチメント内メッセージ部分です。
"fallback"
アタッチメント表示に対応してない環境で表示される代替テキストらしいです。スマホのプッシュ通知とかでしょうか。
"color"
アタッチメントの横に付く棒の色をカラーコードで指定できます。
"actions"
とりあえず今回はボタンを定義する部分と考えます。"actions"内の{}部分を増やすことでボタンを増やすことができますが、私が試してみたところボタンメッセージとして送信できるのは最大5個までです。(エラーにはなりませんが6個目以降は表示されません。)
"name"
なんかよくわからないですが識別用の名前です。この部分はメッセージでは見えません。
"text"
この部分が実際にボタンに表示される文章です
"type"
今回はボタンタイプなので"button"とします。
"value"
選択肢の識別用の名前です。応答を分岐させるとき用の任意の識別名です。ここもメッセージでは見えません。

この内容だと下図のような送信メッセージになります。
スクリーンショット 2019-06-27 15.14.29.png

チャンネルへのメッセージ送信

作った送信データをJSON形式に変換してチャンネルへ送り返します。

return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);

以上がスラッシュコマンドを受け取った際に実行されるアプリ①の内容です。次は選択肢ボタンを受け取るアプリ②について説明します。


選択肢を受け取るアプリ : アプリ②

全体コード

ユーザーがボタン付きメッセージを受信し、ボタンをタップまたはクリックすると、このアプリ②の公開URLが叩かれます。英語がなんだか変です。

アプリ②のコード(クリックして展開)
button_receive.gs
function doPost(e) {
  // ペイロード部分の取り出し
  var payload = JSON.parse(e["parameter"]["payload"]);
  var name = payload["actions"][0]["name"];
  var value = payload["actions"][0]["value"];

  // nameの値についてswitch分岐(nameを言語モードの分岐条件にしている)
  switch (name) {
    // 英語モードの場合
    case 'eng':
      var head_text = "English Mode";
      var quest_attachment = {
        "title": "Question Select",
        "text": "What do you want to know about?",
        "fallback": "Opps",
        "callback_id": "callback_button",
        "color": "#00bfff",
        "attachment_type": "default",
        "actions": [
          {
            "name": "eng",
            "text": "Trash list of each weekdays",
            "type": "button",
            "value": "week"
          },
          {
            "name": "eng",
            "text": "How to take out trash",
            "type": "button",
            "value": "howgomi"
          },
          {
            "name": "eng",
            "text": "Where are new plastic bags?",
            "type": "button",
            "value": "gomibag"
          },
          {
            "name": "eng",
            "text": "Which key corresponds to each room?",
            "type": "button",
            "value": "roomkey"
          },
          {
            "name": "eng",
            "text": "Exit",
            "type": "button",
            "value": "quit"
          }
        ]
      };
      break;
    // 日本語モードの場合
    case 'jpn':
      var head_text = '日本語モード';
      // 2段階目の選択肢ボタン用アタッチメント
      var quest_attachment = {
        "title": '質問選択',
        "text": '何について調べますか?',
        "fallback": "ほえ〜",
        "callback_id": "callback_button",
        "color": "#00bfff",
        "attachment_type": "default",
        "actions": [
          {
            "name": "jpn",
            "text": "各曜日のごみは?",
            "type": "button",
            "value": "week"
          },
          {
            "name": "jpn",
            "text": "ごみの捨て方",
            "type": "button",
            "value": "howgomi"
          },
          {
            "name": "jpn",
            "text": "新しいゴミ袋はどこ?",
            "type": "button",
            "value": "gomibag"
          },
          {
            "name": "jpn",
            "text": "部屋の鍵がわからない",
            "type": "button",
            "value": "roomkey"
          },
          {
            "name": "jpn",
            "text": "終了",
            "type": "button",
            "value": "quit"
          }
        ]
      }
      break;
  }

  // 選択肢に応じた応答をするためにvalueでswitch分岐する
  switch (value) {
    case 'week':
      var image = '画像URL'; // 添付画像のURL
      // 言語モードに応じた答えに分岐する
      switch (name) {
        case 'eng':
          var r_text = "Trash list of each weekdays";
          var exp_text = "Monday&Thursday's garbage is \"burning garbage\"\n Wednesday's garbage is \"non-burnable garbage\", \"pet bottle\" and \"bottle and can\" Please take out the garbage between 8:30 am to 9:30 am";
          break;
        case 'jpn':
          var r_text = "各曜日のごみ";
          var exp_text = "月曜日・木曜日のごみは「もえるごみ」です. \n 水曜日のごみは「もえないごみ」「ペットボトル」「ビン・カン」です. \n AM8:30~9:30の間にゴミを出してください.";
          break;
      }
      break;
    case 'howgomi':
      var image = '画像URL';
      switch (name) {
        case 'eng':
          var r_text = "How to take out trash";
          var exp_text = "Make sure that \"XXX laboratory\" is written on the trash bag, and set a new trash bag in the trash box.";
          break;
        case 'jpn':
          var r_text = "ごみの捨てかた";
          var exp_text = "ゴミ袋に「〇〇研究室」が記入されているか確認し, 新しいゴミ袋をゴミ箱にセットします.";
          break;
      }
      break;
    case 'gomibag':
      var image = '画像URL';
      switch (name) {
        case 'eng':
          var r_text = "Where are new plastic bags?";
          var exp_text = "In the closet under the sink in the staff room";
          break;
        case 'jpn':
          var r_text = "新しいゴミ袋はどこ?";
          var exp_text = "スタッフルームの流しの下の戸棚にあります";
          break;
      }
      break;
    case 'roomkey':
      var image = '画像URL';
      switch (name) {
        case 'eng':
          var r_text = "Which key corresponds to each room?";
          var exp_text = "Correspondence of the key of each room is as this photo";
          break;
        case 'jpn':
          var r_text = "各部屋の鍵の対応";
          var exp_text = "各部屋の鍵の対応は写真の通りです";
          break;

      }
      break;
    case 'language':
      var image = '画像URL';
      var r_text = "Notification";
      switch (name) {
        case 'eng':
          var exp_text = "English-Mode was selected.";
          break;
        case 'jpn':
          var exp_text = "日本語モードが選択されました.";
          break;
      }
      break;
  }
  // 質問に応じたアタッチメントの定義
  var response = {
    "title": r_text,
    "text": exp_text,
    "image_url": image,
    "color": "#ffa500"
  };
  // 送信されるメッセージの定義
  var new_rep = {
    "text": head_text,
    "attachments": [
      response,
      quest_attachment
    ]
  };

  // 「終了」という選択肢が選ばれた時のみ異なる処理をしてボタンを消す(ボタン無しメッセージで上書きする)
  if (value == "quit"){
    // 終了時
    var reply = {
      "attachments": [
        {
          "text": "Clean the room and have a nice day !!",
          "color": "00bfff"
        }
      ]
    };
    return ContentService.createTextOutput(JSON.stringify(reply)).setMimeType(ContentService.MimeType.JSON);
  } else{
    // それ以外の場合は選択肢メッセージを出し続ける
    return ContentService.createTextOutput(JSON.stringify(new_rep)).setMimeType(ContentService.MimeType.JSON);
  }

}

以降アプリ①と共通する説明は省略します。

ペイロードの取り出し

ボタンをユーザーがタップすると、ボタンメッセージに付随した情報("actions"内の要素)が送信されます。以下の部分では受信系列から"name""value"の要素を取り出しています。

// ペイロード部分の取り出し
var payload = JSON.parse(e["parameter"]["payload"]);
var name = payload["actions"][0]["name"];
var value = payload["actions"][0]["value"];

画像をアタッチメントに添付

"image_url"という要素が増えてます!! ここには先ほどGoogle Driveの項で用意した画像URLを入れます。これによりGASが直接画像を扱うことなく、Slack App側でURLから画像を取得して表示することができます。

// 画像の参照URLの指定
var image = '画像URL';

// 質問に応じたアタッチメントの定義
var response = {
   "title": r_text,
   "text": exp_text,
   "image_url": image,
   "color": "#ffa500"
};

参考

メッセージ例

こんな感じで表示されます。かっこいいでしょ。
選択肢を選ぶごとに前のメッセージに上書きされて表示されます。
スクリーンショット 2019-06-27 17.59.07.png


Webアプリ化

アプリ①とアプリ②をWebアプリとして公開し、公開URLをそれぞれ取得します。こちらのサイトがまたまた参考になります。

https://script.google.com/macros/s/hogehogehogehogehoge/execのような形で得られるはずです。

Slack Appの設定(後半)

GASの設定が終わり、Webアプリケーションとしての公開URLをゲットしました。しかしこのままではSlack側での設定が終わっていません。スラッシュコマンドInteractive Componentsの設定を行います。

スラッシュコマンドの設定

再びapiメニューよりFeaturesのSlash Commandを選択し、Create New Commandを選択、コマンドの設定を行います。

スクリーンショット 2019-06-28 4.05.21.png

command
コマンドの名前を決めます。スラッシュに続く任意の名前をつけましょう。ここでは\gomibotとしました。
Request URL
アプリ①のURLを入れます。
Short Description
短い説明を入れます。
Usage Hint
今回はスラッシュコマンド単体で使う前提なのでなくてもいいです。

Interactive Componentsの設定

同じくFeaturesのInteractive Componentsを選択し、InteractivityRequest URLアプリ②の公開URLを入れます。

スクリーンショット 2019-06-28 3.59.54.png

完成!

以上でSlack App, Google Apps Script, Googleドライブの設定が全て完了です!
できたbotに質問しまくってみましょう!
今回のまとめは、

  • Slack側でスラッシュコマンドを受け付けて、GASのURLを叩く
  • ボタンや画像を含んだアタッチメント付きメッセージデータをGASから送信する
  • 利用者の押したボタンに応じた応答をGASから行う
  • 利用者自身にしか見えないメッセージにしてチャンネルを汚さず利用できる
  • Google Drive上の画像を取得してSlackで表示する

ということがSlack App + GASで実現できました。この基本的なbotを応用すれば、アタッチメント内のコンテンンツを変えたり、Googleカレンダーやスプレッドシートをさらに加えるなどで幅広い活用ができると思います。調べていてすごい記事があったので貼っておきます。

以上でこの記事は終わりです。読んでくださりありがとうございました!

番外編 : 詰まったポイント

GASプロジェクトのバージョンアップ

コードを変更した際に、アプリケーションとして変更を反映するには「ウェブアプリケーションとして導入」をする必要がありますが、ここでバージョンを「New」にしないと反映されないという罠が!!
しかもデフォルトでは現在のバージョンのまま更新という形になります。
スクリーンショット 2019-06-28 13.21.53.png

ここが新しいバージョン更新されていないと、いくらコードを変えてもエラーメッセージの内容が変わらない、なんていう泥沼にハマります。試行錯誤で作っていたときにほんとに困りました。
こちらの記事が見つからなかったら今でも詰まってた可能性が高いです。

更新のたびにバージョンアップしてたらバージョンがめちゃくちゃ大きな数字になりそうですね。

57
87
2

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
57
87