5
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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

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

1.1 アプリを作成

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

001.png

1.2 スコープを設定

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

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

を追加してください。下の画像のようになればOKです。
002.PNG

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

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

003.png

1.4 トークンを確認

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

004.png

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

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

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

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

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

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

007.png

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

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

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

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

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

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

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

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

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

005.PNG

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

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

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

008.PNG

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

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

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

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

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

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

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

3.2 SlackのInteractivityの設定をする

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

010.PNG

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

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

011.PNG

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

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

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

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

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

012.PNG

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

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


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

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

5
10
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
5
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?