文系でも分かる、GASによる授業通知のLINE bot の作成 その④ 〜setTriggerを使って、指定の時間にプッシュ通知を送る〜


文系でも分かる、GASによる授業通知のLINE bot の作成 その③の続きです。
今回で最後になります。

  1. 文系でも分かる、GASによる授業通知のLINE bot の作成 その① 〜GASを使ったLINE botの作成〜
  2. 文系でも分かる、GASによる授業通知のLINE bot の作成 その② 〜GASとスプレッドシートの連携、日付のフォーマット〜
  3. 文系でも分かる、GASによる授業通知のLINE bot の作成 その③
  4. 文系でも分かる、GASによる授業通知のLINE bot の作成 その④ 〜setTriggerを使って、指定の時間にプッシュ通知を送る〜 ←今回

setTriggerというものを使い、授業開始10分前に通知が来るようにします。

この記事でわかること

  • GASのsetTrigger の使い方
  • Messaging APIのプッシュ通知の送り方

今回やること

  • 要件④授業の10分前に授業を通知する機能の追加

要件定義

  • push通知を送るfunctionの作成
  • push通知で送る授業情報を取得するfunctionを作成
  • 上のfunctionが指定の時間に実行されるためのfunctionを作成

push通知を送るfunctionの作成

Code.gs
function push(text, zoom) {
  //メッセージを送信(push)する時に必要なurlでこれは、皆同じなので、修正する必要ありません。
  //この関数は全て基本コピペで大丈夫です。
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  //toのところにメッセージを送信したいユーザーのIDを指定します。(toは最初の方で自分のIDを指定したので、linebotから自分に送信されることになります。)
  //textの部分は、送信されるメッセージが入ります。createMessageという関数で定義したメッセージがここに入ります。
  var postData = {
    "to" : user_id,
    "messages" : [
      {
        'type':'text',
        'text':text,
      },
      {
        'type':'text',
        'text':zoom,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

"messages" : [{}{}] の形にすることで、パスワードが別のメッセージとして送られてくるようにしています(上の画像参照)。こうすることで、パスワードのコピーが容易になっています。

push通知で送る授業情報を取得するfunctionを作成

Code.gs
function pushClassInfo() {
 //function findNextClassを実行
  var classInfos = findNextClass();
  var today = new Date();
  //あとで、現在より15分後の日時を取得します。
  var quarterAfter = new Date();
  var day = today.getDay();
  var array = ["日", "月", "火", "水", "木", "金", "土"];
 //quarterAfterに入っている日時が、現在より15分後になりました。
  quarterAfter.setMinutes(quarterAfter.getMinutes() + 15);
  var hhmmToday = Utilities.formatDate( today, 'Asia/Tokyo', 'HH:mm');
  var hhmmQuarter = Utilities.formatDate( quarterAfter, 'Asia/Tokyo', 'HH:mm');
  console.log(classInfos);
  console.log(classInfos.startTime <= hhmmQuarter); //授業が始まるのは、今から15分後より前、つまりもうすぐ授業が始まる。
 //findNextClassで取得した授業が現在の曜日のものか
  if(classInfos.classDay == array[day] && classInfos.startTime <= hhmmQuarter ){ 
    var message = "もうすぐ次の授業です。\n" + classInfos.classDay + "曜" + classInfos.classNum + "限 (" + classInfos.startTime + "-" + classInfos.endTime + 
      ")\n授業名:" + classInfos.className + 
        "\nZoomID:" + classInfos.zoomID + 
          "\nPass:" + classInfos.zoomPass;
    console.log(message);
   //function pushを実行
    push(message, classInfos.zoomPass);
  } else {console.log("not upcoming one ");}
}

流れとしては、fuction findNextClassで1番近い授業を取得して、それが始まるのが今から15分以内かどうか判別し、trueならクラスの情報をpushするという感じです。

指定時間に通知が送られてくるようにする

コード(コピペで動きます)

Code.gs
//授業時間が設定されている時、その授業時間の10分前にpushClassInfoを実行するタイマーをセットする
function setTrigger(){
  var today = new Date();
  var year = today.getFullYear();
  var month = today.getMonth();
  var date = today.getDate();
  for(let i=3; i <= 27; i+=4) {
    if(sheet.getRange(i, 1).getValue()){
      var classStart = sheet.getRange(i, 1).getValue();
      var startMinutes = classStart.getMinutes();
      classStart.setFullYear(year);
      classStart.setMonth(month);
      classStart.setDate(date);
      classStart.setMinutes(startMinutes - 10);
      console.log(classStart);
      ScriptApp.newTrigger('pushClassInfo').timeBased().at(classStart).create();
    }
  }
}

function delTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for(let i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == "pushClassInfo") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

トリガーの使い方

ScriptApp.newTrigger('function名').timeBased().at(時間).create()
でトリガーというものをセットすることがでます。このトリガーによって、指定した日時にfunctionを実行させることができます。
ただ、セットしたトリガーは残り続けてしまうので、 deleleTriggerで削除します。
あとは、function setTriggerを早朝に、delTriggerを深夜に実行する設定をすれば完了です。

setTriggerとdelTriggerを毎日呼び出す

https://tonari-it.com/gas-trigger-set/#toc4
こちらの記事の、「5. 毎日指定の時刻のDateオブジェクトを作成する」を参考に、setTriggerを早朝(1限より前の時間)に、delTriggerを深夜(7限よりあとの時間)にセットしてください。つまり、記事の作業を2回おこないます。
ちなみに1〜4もScriptApp.newTriggerについて詳しく解説してくれているので、読むと勉強になります。

これで完成です!
最後に更新を忘れないようにしましょう。

最終的なコードの全体像

全て書き終えた後のコードは以下のようになります

Code.gs
var access_token = "アクセストークン"
// 自分のユーザーIDを指定します。LINE Developersの「Your user ID」の部分です。
var user_id = "ユーザーID"

//★★スプレッドシートID★★
var ss = SpreadsheetApp.openById("スプレッドシートID");
//★★シート名★★
var sheet = ss.getSheetByName("シート名");

function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0];
  var returnMessage = "曜日と時限(半角:1〜7)を指定してね!\n次の授業が知りたいときは、「次」と入力してね!";
  if(event.source.userId == user_id){
    //返信するためのトークン取得
    var reply_token= event.replyToken;
    if (typeof reply_token === 'undefined') {
      return;
    }
    var message = event.message.text;
    for(let i=3; i<=7; i ++) {
      var dateColumn = i;
      var day = sheet.getRange(1, i).getValue();
      if(message.includes(day)){
        for(let j=2; j<=26; j += 4) {
          var classNumRow = j;
          var classNum = sheet.getRange(j, 1).getValue();
          if(message.includes(classNum) && sheet.getRange(classNumRow, dateColumn).getValue()){
            var classInfos = getClassInfo(classNumRow, dateColumn);
            var returnMessage = classInfos.classDay + "曜" + classInfos.classNum + "限 (" + classInfos.startTime + "-" + classInfos.endTime + 
              ")\n授業名:" + classInfos.className + 
                "\nZoomID: " + classInfos.zoomID + 
                  "\nPass: " + classInfos.zoomPass;
            reply(reply_token, returnMessage);
          } else if(message.includes(classNum) && !sheet.getRange(classNumRow, dateColumn).getValue()) {
            var returnMessage = "授業はありません。"
            reply(reply_token, returnMessage);
          }
        }
      }
    }

    if(message.includes("次")){
      var classInfos = findNextClass();
      var returnMessage = classInfos.classDay + "曜" + classInfos.classNum + "限 (" + classInfos.startTime + "-" + classInfos.endTime + 
        ")\n授業名:" + classInfos.className + 
          "\nZoomID: " + classInfos.zoomID + 
            "\nPass: " + classInfos.zoomPass;
      reply(reply_token, returnMessage);
    } else {reply(reply_token, returnMessage);}
  }
}

function reply(reply_token, returnMessage) {
  var reply_url = 'https://api.line.me/v2/bot/message/reply';

  // メッセージを返信 
  UrlFetchApp.fetch(reply_url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + access_token,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': [{
        'type': 'text',
        'text': returnMessage,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}


function push(text, zoom) {
  //メッセージを送信(push)する時に必要なurlでこれは、皆同じなので、修正する必要ありません。
  //この関数は全て基本コピペで大丈夫です。
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  //toのところにメッセージを送信したいユーザーのIDを指定します。(toは最初の方で自分のIDを指定したので、linebotから自分に送信されることになります。)
  //textの部分は、送信されるメッセージが入ります。createMessageという関数で定義したメッセージがここに入ります。
  var postData = {
    "to" : user_id,
    "messages" : [
      {
        'type':'text',
        'text':text,
      },
      {
        'type':'text',
        'text':zoom,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function pushClassInfo() {
  var classInfos = findNextClass();
  var today = new Date();
  var quarterAfter = new Date();
  var day = today.getDay();
  var array = ["日", "月", "火", "水", "木", "金", "土"];
  quarterAfter.setMinutes(quarterAfter.getMinutes() + 15);
  var hhmmToday = Utilities.formatDate( today, 'Asia/Tokyo', 'HH:mm');
  var hhmmQuarter = Utilities.formatDate( quarterAfter, 'Asia/Tokyo', 'HH:mm');
  console.log(classInfos);
  console.log(classInfos.startTime <= hhmmQuarter); //授業が始まるのは、今から15分後より前、つまりもうすぐ授業が始まる。
  if(classInfos.classDay == array[day] && classInfos.startTime <= hhmmQuarter ){ 
    var message = "もうすぐ次の授業です。\n" + classInfos.classDay + "曜" + classInfos.classNum + "限 (" + classInfos.startTime + "-" + classInfos.endTime + 
      ")\n授業名:" + classInfos.className + 
        "\nZoomID:" + classInfos.zoomID + 
          "\nPass:" + classInfos.zoomPass;
    console.log(message);
    push(message, classInfos.zoomPass);
  } else {console.log("not upcoming one ");}
}

function findNextClass() {
  var today = new Date();
  var hour = today.getHours();
  var minutes = today.getMinutes();
  console.log(today);
  for(let i=0; i <= 6; i++) {
    var day = (today.getDay() + i) % 7;
    var dateColumn = day + 3;
    var array = ['日','月','火','水','木', '金', '土'];
    console.log("曜日:" + array[day]);

    var searchHour = 0;
    if(day == today.getDay()){searchHour = hour;} else { searchHour = 6;}
    for(var j=0; j < 24-searchHour; j++) {
      console.log(searchHour + "時");
      for(let k=2; k <= 26; k+=4){
        var classNumRow = k;
        var startTimeRow = k + 1;  
        //始業時間が設定されている場合、始業時間を取得
        if(sheet.getRange(startTimeRow, 1).getValue()) {
          var startTime = sheet.getRange(startTimeRow, 1).getValue();
          var startHour = startTime.getHours();
          var startMinutes = startTime.getMinutes();
          console.log("start hour: " + startHour);
          //検索した時、今と検索時間の日付と時間が一致していても、今の分が始業の分を超えている場合は情報を取得しない          
          if(searchHour === startHour && today.getDay() === day && hour === searchHour && minutes > startMinutes){
            console.log("\nalready orver\n");
            //時間が一致し、授業が存在する場合、情報を取得
          } else if(searchHour === startHour && sheet.getRange(classNumRow, dateColumn).getValue()){
            var classInfos = getClassInfo(classNumRow, dateColumn);
            return classInfos;
          }
        }
      }
      searchHour += 1;
    }
  }  
}

function getClassInfo(rowNum, dateColumn){
  var className = sheet.getRange(rowNum, dateColumn).getValue();
  var classDay = sheet.getRange(1, dateColumn).getValue();
  var classNum = sheet.getRange(rowNum, 1).getValue();
  var startTime = Utilities.formatDate( sheet.getRange(rowNum + 1, 1).getValue(), 'Asia/Tokyo', 'HH:mm');
  var endTime = Utilities.formatDate( sheet.getRange(rowNum + 3, 1).getValue(), 'Asia/Tokyo', 'HH:mm');
  var zoomID = sheet.getRange(rowNum + 1, dateColumn).getValue();
  var zoomPass = sheet.getRange(rowNum + 2, dateColumn).getValue();
  var classInfos = {className: className, classDay: classDay, classNum: classNum, startTime: startTime, endTime: endTime, zoomID: zoomID, zoomPass: zoomPass};
  return classInfos;
}

function setTrigger(){
  var today = new Date();
  var year = today.getFullYear();
  var month = today.getMonth();
  var date = today.getDate();
  for(let i=3; i <= 27; i+=4) {
    if(sheet.getRange(i, 1).getValue()){
      var classStart = sheet.getRange(i, 1).getValue();
      var startMinutes = classStart.getMinutes();
      classStart.setFullYear(year);
      classStart.setMonth(month);
      classStart.setDate(date);
      classStart.setMinutes(startMinutes - 10);
      console.log(classStart);
      ScriptApp.newTrigger('pushClassInfo').timeBased().at(classStart).create();
    }
  }
}

function delTrigger() {
  var triggers = ScriptApp.getProjectTriggers();
  for(let i=0; i < triggers.length; i++) {
    if (triggers[i].getHandlerFunction() == "pushClassInfo") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}

最後に

いかがだったでしょうか?
僕はこれを作った時、文系でもこんなものが作れるんだと感動しました。
文系目線でなるべくわかりやすく解説を入れたつもりなので、少しでも役に立てば嬉しいです。
なお、未経験文系大学生が書いているので、誤り等あるかもしれませんがご了承ください。
最後まで取り組んでくださってありがとうございました!