SlackのBlockKitとGAS、スプレッドシートでタスク管理アプリを作る

Slack上で動作するタスク管理アプリを作成します。機能は以下の通りです。

- 毎週月曜日に、今月が期限のタスクをスプレッドシートから取得してSlackに送信
- Slackでボタンを押すと、タスクのステータス（未対応→対応中→完了）が更新される

やや手順が多いですが、開発経験が無い方でも作れるように詳細に説明していきます。

## 手順1 Slackにアプリを作成する

### 1.1 アプリを作成

https://api.slack.com/apps から**Create an App**を選択、作成するアプリ名（自由）を入力、ワークスペースを選択して**Create App**。

![001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/a0ae7d19-1373-c49f-a7eb-99d7369ff4b8.png)

### 1.2 スコープを設定

Basic Informationのページに移動するので左のメニューから**OAuth & Permissions**を選択。下にスクロールして

**Bot Token Scopes**に
「chat:write」・・・チャンネルでの発言に必要
「users:read」・・・タスクを更新したユーザーの確認に必要

を追加してください。下の画像のようになればOKです。
![002.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/26c0c7fd-546e-5988-5e29-fcc01d9142d9.png)

### 1.3 ワークスペースにインストール

ページを上にスクロールして**Install App to Workspace**を選択、下の確認画面で許可を押すとワークスペースにアプリがインストールされます。

![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/61a07884-92ff-c3e8-3cce-729633d28e5c.png)

### 1.4 トークンを確認

OAuth & Permissionsのページに**Tokens for Your Workspace**が表示されています。**Bot User OAuth Access Token**をコピーして控えておきます（漏洩のないように気を付けてください）。

![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/f0ba1bd6-4abd-f290-0ad5-878163d501f5.png)

### 1.5 送信先チャンネルのIDを調べる

Slackのデスクトップアプリから、タスクの通知を出したいチャンネルを右クリック>その他のオプション>リンクをコピーを選択してください。

`https://hogehoger5rin.slack.com/archives/《ここ》`

コピーしたリンクの《》内が**チャンネルのID**です。これを控えておいてください。

### 1.6 Slackのチャンネルにアプリを追加する

タスクの通知を出したいチャンネルにこのアプリを追加してください。UIが頻繁に変わるため詳細の説明は省きますが、おそらくチャンネルの詳細画面から追加できます。

![007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/fd1e00d7-d014-0630-7af8-eeeda965cb38.png)

## 手順2 GoogleAppsScriptのアプリを作成する

### 2.1 タスクを入力するスプレッドシートを作成する

https://docs.google.com/spreadsheets/d/1EVr5WJcuTUHPKF-iB2FrvGkUw44Qdxq_hX3KKxLBX2k/edit?usp=sharing

上のスプレッドシートをコピーして使用してください。
画面下の「タスク管理」のタブの▽を押して**別のワークブックにコピー**>**新しいスプレッドシート**で作成できます。

スプレッドシートを作成できたら、そのURLを確認してください。

`https://docs.google.com/spreadsheets/d/《ここ》/edit#gid=《数字》`

《ここ》の英数字が**スプレッドシートのID**です。これを控えておいてください。

### 2.2 GoogleAppsScriptでタスクの送信プログラムを作成する

作成したスプレッドシートの**ツール**>**スクリプトエディタ**を開いてください。

「無題のプロジェクト」となっている場所をクリックして、適当な名前を付けて保存してください。
また、左のタブのコード.gsの▽をクリックして、こちらは「タスク送信」という名前に変更してください。

![005.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/d27ebff3-e19c-128e-6e84-820511fdb129.png)

上の画像のようになったら準備OKです。エディタに以下のコードをコピペして、《**Bot User Access Token**》《**チャンネルのID**》、《**スプレッドシートのID**》を先ほど控えたものに置き換えてください。

```Javascript
function taskSearch() {
  var sheetId = "《スプレッドシートのID》";
  var channel = "《チャンネルのID》";
  var token = "《Bot User Access Token》";

  // シートから未対応or対応中で期限が今月or今月より前のものを取得
  var sheet = SpreadsheetApp.openById(sheetId).getSheetByName("タスク管理");
  var data = sheet.getRange(3,2,sheet.getLastRow()-2,9).getValues();
  var dt = new Date(); 
  var transferData = [];
  
  for (var i in data){
    var deadMonth = new Date(data[i][7]).getMonth();
    if ((deadMonth == dt.getMonth() && data[i][6] != "完了") || (new Date(data[i][7]) <= dt && data[i][6] != "完了")){
      transferData.push(data[i]);
      }
    }

  // 配列blockKitにsectionを追加していく
  var blockKit = []
  if (transferData == []){
    blockKit.push({
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "今月が期限の残存タスクはありません。"
        }
      });
    execute('chat.postMessage', {
      token: token,
      channel: channel,
      blocks : JSON.stringify(blockKit),
      });
    return;
	}
    
  else{
    blockKit.push({
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "今月が期限の残存タスクは以下の通りです。"
		}
      });
    blockKit.push({"type": "divider"});
    
    // タスク名にシートへのリンクを設置
    var titleLink = SpreadsheetApp.openById(sheetId).getUrl();

    for (var i in transferData){
      // blockKitのtypeがmrkdownのtextはリンクとかのマークダウンが可能
	  blockKit.push({
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "*<" + titleLink + "|" + transferData[i][2] + ">*\n"+ transferData[i][8]
          }
       });
      
      // fieldsはPCで2列、モバイルで1列で表示される attachmentみたいにshortの指定はできないっぽい
      blockKit.push({
        "type": "section",
        "fields": [
          {"type": "mrkdwn","text": "*起票日:* " + Utilities.formatDate(new Date(transferData[i][1]), "JST", "yyyy/MM/dd")},
          {"type": "mrkdwn","text": "*緊急度:* " + transferData[i][3]},
          {"type": "mrkdwn","text": "*分類:* " + transferData[i][4]},
          {"type": "mrkdwn","text": "*担当者:* " + transferData[i][5]},
          {"type": "mrkdwn","text": "*ステータス:* " + transferData[i][6]},
          {"type": "mrkdwn","text": "*期限:* " + Utilities.formatDate(new Date(transferData[i][7]), "JST", "yyyy/MM/dd")},
          ]
        });
          
      if (transferData[i][6] == "未対応"){
        // actionsでの反応は設定したRequestURLにHTTPPostが送られる。
        // EventSubscriptionとの違いはe.parameter.payloadの有無
        blockKit.push({
          "type": "actions",
          "elements": [{
            "type": "button",
            "text": {"type": "plain_text", "text": "対応中に変更"},
            "value": "task_" + transferData[i][0] + "_advance"
            },
            {
            "type": "button",
            "text": {"type": "plain_text", "text": "タスクを完了"},
            "style": "primary",
            "value": "task_" + transferData[i][0] + "_complete"
            }]
          });
        }
      else {
        blockKit.push({
          "type": "actions",
          "elements": [{
            "type": "button",
            "text": {"type": "plain_text", "text": "タスクを完了"},
            "style": "primary",
            "value": "task_" + transferData[i][0] + "_complete"
            }]
          });
        }
      blockKit.push({"type": "divider"});
      } //endfor
  
      execute('chat.postMessage', {
        token: token,
        channel: channel,
        blocks : JSON.stringify(blockKit),
      });
      return;
    } //endif
}

// apiを実行
function execute(apiName, params){
  var options = {
    'method': 'POST',
    'payload': params,
  }
  var res = UrlFetchApp.fetch('https://slack.com/api/' + apiName,options);
  return JSON.parse(res.getContentText());
}
```

### 2.3 GoogleAppsScriptの認証をする

エディタ画面の上のタブの「**関数を選択**」のプルダウンから「**taskSearch**」を選択して左の実行ボタン（三角形）を押してください。

![006.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/5a49a260-27b4-d5a5-f0ca-32ea9aeedbe4.png)


上のようなウィンドウが出るので、許可を確認>Googleアカウントを選択>詳細>《プロジェクト名》（安全ではないページに移動）>許可 と進んでください。

![008.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/cd9e0eda-b5b3-34ac-af17-5803da464b2e.png)

上のようなメッセージが投稿されれば成功です。次の手順3ではボタンからスプレッドシートのステータスを変更できるようにします。

## 手順3 ボタンでステータスの変更をできるようにする

### 3.1 GoogleAppsScriptでボタンを受信するプログラムを作成する

スクリプトエディタの左上のタブのファイル>New>スクリプトファイルを選択し、「ステータス変更」という名前で新規作成します。

作成できたら、以下のコードをコピペしてください。
先ほどと同様に、《》内を置き換えてください。（今回はチャンネルのIDは不要です。）

```Javascript
function doPost(e){
  var sheetId = "《スプレッドシートのID》";
  var token = "《Bot User Access Token》";
  
  if (e.parameter.payload){
    var react = JSON.parse(e.parameter.payload);
    console.log(react);
    
    // valueの中身は"task_番号_変更後ステータス"
    if (react.actions[0].value.substr(0, 4) == "task"){
      // 多重送信対策
      var cache = CacheService.getScriptCache();
      if (cache.get("タスク完了") == "hoge"){
        throw new Error("えらーだよ");
        }
      cache.put("タスク完了","hoge", 10);
      
      var sheet = SpreadsheetApp.openById(sheetId).getSheetByName("タスク管理");
      taskStatusChange(react,token,sheet) 
      }
    }
  // RequestURLの検証用
  else{
    var react = JSON.parse(e.postData.getDataAsString());
    if(react.type === "url_verification"){
      return ContentService.createTextOutput(react.challenge);
      }
    }
  }
  
function taskStatusChange(react,token,sheet){
  
  var arr = react.actions[0].value.split("_");
  var taskNum = arr[1];
  var taskStatus = arr[2];
  
  // ボタンを押したユーザーの名前を取得
  var userName = execute('users.info',{
    token: token,
    user: react.user.id
    }).user.profile.real_name;
  
  for (var i = 0; i < react.message.blocks.length; i++){
    // block_idはブロックに固有
    // 押されたボタンのあるブロックが配列の何番目かを調べる
    if (react.message.blocks[i].block_id == react.actions[0].block_id){
      
      // 元のブロックの配列を複製、ボタンが押されたブロックを除去
      var newBlock = react.message.blocks;
      newBlock.splice(i,1);
      
      // 完了ボタンなら、メッセージのブロックを追加
      if (taskStatus == "complete"){
        newBlock.splice(i,0,{
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm") + " " + userName + "がタスクを完了しました。お疲れ様でした！"}
		  });
          // シートのステータスを変更
          sheet.getRange(Number(taskNum) + 2,8).setValue("完了");
          sheet.getRange(Number(taskNum) + 2,11).setValue(new Date());
        }
      // 対応中ボタンなら、完了ボタンとメッセージのブロックを追加
      if (taskStatus == "advance"){
        newBlock.splice(i,0,{
          "type": "section",
          "text": {
            "type": "mrkdwn",
            "text": Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd HH:mm") + " " + userName + "がタスクを対応中にしました。頑張ってね！"}
		  });
        newBlock.splice(i,0,{
          "type": "actions",
          "elements": [{
            "type": "button",
            "text": {"type": "plain_text", "text": "タスクを完了"},
            "style": "primary",
            "value": "task_" + taskNum + "_complete"
            }]
          });
          // シートのステータスを変更
          sheet.getRange(Number(taskNum) + 2,8).setValue("対応中");
        }
      }     
    }
    // メッセージを編集
    execute('chat.update', {
      token: token,
      channel: react.channel.id,
      ts: react.message.ts,
      blocks: JSON.stringify(newBlock)
      });
  }
```

Ctrl+Sで保存したら、左上のタブの公開>ウェブアプリケーションとして導入を選択してください。

Project versionが**New**、中段が自分のGoogleアカウント、下段が**Anyone, even anonymous**になっているのを確認して**更新**、あるいは**Deploy**となっているボタンを押してください。

![009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/ae64fac3-a7f8-2f38-59c8-dcb582e18924.png)

次の画面で**末尾が/execのURL**が表示されるので、それを控えておいてください。

### 3.2 SlackのInteractivityの設定をする

手順1.4のページから、左のメニューの**Interactivity & Shortcuts**を選択して、OffになっているスイッチをOnにしてください。
（もしブラウザを閉じてしまっていたら、<https://api.slack.com/apps>から先ほど作成したアプリを選んで進んでください）

![010.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/5b5147f9-e911-4065-d7f7-7bb8b1ab7004.png)

このフィールドに先ほどのURLを入力したら、**画面下に表示されるセーブボタンを忘れずに押して**ください。

その後、Slackに戻って先ほど送信されたボタンを押してみてください。

![011.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/e971cbc2-1c8b-dfc1-5b47-d80f9634ab3c.png)

上のように、ボタンが変化してメッセージが表示されたら成功です。スプレッドシートのステータスや完了日も変化しているので確認してみてください。


## 手順4 GoogleAppsScriptのトリガーを設定する

タスク送信プログラムの自動実行を設定します。

スクリプトエディタの**編集**>**現在のプロジェクトのトリガー**を開き、**トリガーを追加**を押してください。

実行する関数は**taskSearch**を選択してください。週ベースのタイマーを選択すると、毎週タスク送信プログラムが実行されます。曜日や時刻は適宜設定してください。

![012.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419349/12583ebf-4102-2019-a735-407534af198d.png)

保存すると、設定された時間にプログラムが自動で実行されて、その月が期限の残りのタスクがチャンネルに通知されます。

作業は以上です。お疲れさまでした。

---

BlockKitとchat.updateの組み合わせが便利でいいですね！

プログラムの不具合などありましたら、コメントお願いします。