GAS+Slackで自主練管理

大学の部活で自主練を管理したかったので、以下のようなものを作った。

  1. 毎日、明日自主練を行くかどうかを確認するメッセージをSlackに送信
  2. そのメッセージにリアクションをするとスプレッドシート上のカレンダーの該当メンバー、該当日時に色を付ける
  3. 月の変わり目にはカレンダーを新しく生成する。

デモ動画

定期的にメッセージを送信

Google SpreadSheetのGoogle App Script(GAS)にあるトリガーという機能を使用する。

SpreadSheetを開いたら「拡張機能」→「Apps Script」

これでSpread Sheetに連動したコードを記述できる。

実際に投稿を行うコード

function postSlackbot() {

  var todaydata = new Date();
    var data = new Date(todaydata);
  data.setDate(todaydata.getDate() + 1);
  var year = data.getFullYear();
  var month = data.getMonth() + 1;
  var date = data.getDate();
  var day = data.getDay();
  var days = ["日", "月", "火", "水", "木", "金", "土"];
  var message = "明日: " + month + "月" + date + "日" + "(" + days[day] + ")" + "の自主練に参加する人はリアクションをしてください。10:30-12:00の人は:参加:のリアクションを、19:00-20:30の人は:参加青:のリアクション。";

  var url = "https://slack.com/api/chat.postMessage";

  var payload = {
    "channel": channelId,
    "text" : message
  };

  var options = {
    "method" : "post",
    "contentType" : "application/json",
    "headers" : {"Authorization" : "Bearer " + token},
    "payload" : JSON.stringify(payload)
  };

  // メッセージを投稿
  var response = UrlFetchApp.fetch(url, options);
  var responseData = JSON.parse(response.getContentText());


  
  if (responseData.ok) {
    var ts = responseData.ts; // メッセージのタイムスタンプを取得
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("posts")
    sheet.appendRow([ts, year, month, date])
    // リアクションを追加するための関数を呼び出す
    addReaction(token, channelId, ts, "参加"); // 10:30-12:00の人のリアクション
    addReaction(token, channelId, ts, "参加青"); // 19:00-20:30の人のリアクション
  } else {
    Logger.log("Error posting message: " + responseData.error);
  }
}
function addReaction(token, channelId, timestamp, reaction) {
  var url = "https://slack.com/api/reactions.add";
  var payload = {
    "channel": channelId,
    "name": reaction,
    "timestamp": timestamp
  };

  var options = {
    "method" : "post",
    "contentType" : "application/json",
    "headers" : {"Authorization" : "Bearer " + token},
    "payload" : JSON.stringify(payload)
  };

  var response = UrlFetchApp.fetch(url, options);
  var responseData = JSON.parse(response.getContentText());
  if (!responseData.ok) {
    Logger.log("Error adding reaction: " + responseData.error);
  }
}

この後、リアクションが起きたときにその投稿がいつの日付の募集なのかをハあっくするためにtime stampをスプレッドシート上のpostシートに記録しておく。

slackのほうでもあらかじめいくつか設定をしておく必要がある。

  • ワークスペースごとにある「SlackAPI: ワークスペース名」のようなページから「OAth&…」を選択
  • 「channel:read」など自分の行いたい処理に合わせて権限を許可
  • ワークスペースにインストールを押すと、トークンが発行される
  • このトークンをもとにポストリクエストを送ることで処理ができる。

定期的に送信するために、GASのほうで「トリガー」を指定する。

リアクションを検知

スラック側で何かイベントが起きた時にSpread Sheet側で処理が走るようにしたい。

doPost()関数は、Postリクエストが送られてきたときに実行される関数。

以下では、doPost()の処理、ポストリクエストを受け取るためにすることを簡単に示す。

doPost()の処理

function doPost(e) {


  var params = JSON.parse(e.postData.getDataAsString());


  //https://api.slack.com/events/url_verification 対応。セキュリティのため検証後はコメントする。
  if(params.type === 'url_verification') {
    return ContentService.createTextOutput(params.challenge);
  }
  

  if (params.event && (params.event.type == 'reaction_added' || params.event.type == 'reaction_removed')) {
    var user = params.event.user;
    var reaction = params.event.reaction;
    var item = params.event.item;
    var timestamp = params.ts;
    var item_ts = item.ts;


    var dateInfo = findDate(item_ts)
    debug_sheet.appendRow([item_ts])

    var color = null
    if(params.event.type == "reaction_added"){
      if(reaction == "参加"){
        color = "#ff6d01"
      }else if(reaction == "参加青"){
        color = "#4a86e8"
      }
    }
    colorCell(dateInfo[0], dateInfo[1], dateInfo[2], user, color)
  }

}
function colorCell(year, month, day, userId, color){
  var name = getUserRepresentation(userId)  
  var date = new Date(year, month - 1, day);
  var dateStr = Utilities.formatDate(date, Session.getScriptTimeZone(), "MM/dd");
  var sheetName = year + "年" + month + "月";
  var sheet =  SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);

  var firstRowRange = sheet.getRange(2, 1, 1, sheet.getLastColumn());
  var firstRowValues = firstRowRange.getValues()[0]

  // 1列目のヘッダーを取得
  var firstColumnRange = sheet.getRange(1, 1, sheet.getLastRow(), 1);
  var firstColValues = firstColumnRange.getValues().map(function(row) { return row[0]; });

  var targetRow = -1;

  for(var i = 0; i < firstRowValues.length; i++){
    if(firstRowValues[i] == dateStr){
      targetRow = i + 1;
    }
  }
  var targetCol = -1;
  for(var i = 0; i < firstColValues.length; i++){
    if(firstColValues[i] == name){
      targetCol = i + 1;
    }
  }


  if(targetRow == -1 || targetCol == -1){
    return
  }

  var range = sheet.getRange(targetCol, targetRow)
  range.setBackground(color)
  return
}
function getUserRepresentation(userId){
  var sheet =  SpreadsheetApp.getActiveSpreadsheet().getSheetByName("members");
  var range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 2)
  var data = range.getValues()
    for (var i = 0; i < data.length; i++) {
      if(userId == data[i][0]){
        return data[i][1];
      }
  }
  return "";
}
function findDate(ts){
  if(debug){
    ts = "1720972992.68831"
  }
  var sheet =  SpreadsheetApp.getActiveSpreadsheet().getSheetByName("posts");
  var range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 4)
  var data = range.getValues();

    for (var i = 0; i < data.length; i++) {
      if(Math.abs(ts - data[i][0] < 0.00001)){
        return [data[i][1], data[i][2], data[i][3]];
      }else{
      }
  }
  return [];
}

あらかじめ、membersというシートにUserIDと表示したい名前を対応付けておく。

また、先ほどのステップでpostsシートに記録した、タイムスタンプと日付の対応付けをもとにスプレッドシート上で書き込みたい日付を取得する。

浮動小数なので、値の一致の確認は差分の絶対値がε以下とする。(==だと等しくなくなる)

手動で1回だけ実行すればよいコード

function getUserIdNameMap(){
  var url = "https://slack.com/api/conversations.members?channel=" + channelId;
  var options = {
    "method": "get",
    "headers": {
      "Authorization": "Bearer " + token
    }
  };
  
  var response = UrlFetchApp.fetch(url, options);
  var usersInfo = JSON.parse(response.getContentText());

  if (usersInfo.ok) {
    var users = usersInfo.members;

    // スプレッドシートの準備
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
    sheet.clear(); // 既存のデータをクリア
    sheet.appendRow(["User ID", "User Name"]); // ヘッダー行を追加

    for (var i = 0; i < users.length; i++) {
      var user = users[i];
      var user_name = getUserName(user)
      sheet.appendRow([user, user_name])
    }
    return;
  } else {
    Logger.log("Error fetching user list: " + usersInfo.error);
    return {};
  }
}

月の変わり目に新しくカレンダーを作成

function createSheet(year, month){
  var people = getMembers();
  var sheetName = year + "年" + month + "月";
  var ss = SpreadsheetApp.getActiveSpreadsheet();

  // 既存のシート名と重複しないように確認
  var sheet = ss.getSheetByName(sheetName);
  if (sheet) {
    Logger.log("シート " + sheetName + " は既に存在します。");
    return;
  }

  // 新しいシートを作成
  sheet = ss.insertSheet(sheetName);
  Logger.log("シート " + sheetName + " を作成しました。");

  for (var i = 0; i < people.length; i++) {
    sheet.getRange(i + 3, 1).setValue(people[i]); // 3行目から人名を設定
  }
  
  // 横軸(日付と曜日)を設定
  var days = ["日", "月", "火", "水", "木", "金", "土"];
  var finishDate = new Date(year, month, 0)
  var finishDay = finishDate.getDate();

  var curDate = new Date(year, month - 1, 0)
  var date = -1;
  var j = 0;
  while(date != finishDay) {
    curDate.setDate(curDate.getDate() + 1);
    date = curDate.getDate()
    j++;
    var dayOfWeek = days[curDate.getDay()];
    var dateStr = Utilities.formatDate(curDate, Session.getScriptTimeZone(), "MM/dd");
    sheet.getRange(1, j + 1).setValue(dayOfWeek); // 1行目に曜日を設定
    sheet.getRange(2, j + 1).setValue("'" + dateStr); // 2行目に日付を設定
    sheet.setColumnWidth(j + 1, 50)
  }
  
  // ヘッダーのスタイルを設定
  sheet.getRange(1, 2, 2, j).setBackground("#cccccc").setFontWeight("bold");
  
  // 人名列のスタイルを設定
  sheet.getRange(3, 1, people.length - 1, 1).setBackground("#eeeeee").setFontWeight("bold");
}

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です