ChatGPT ✕ Googleスプレッドシート ~未入力セルをSlackに通知したい編~

Web制作・開発
2024.06.18
栗原
Webディレクター

こんにちは。ディレクターの栗原です。

前回の記事(ChatGPT ✕ Googleスプレッドシート ~セルの更新日時を自動入力したい編~)を書いて以来、「GAS(Google Apps Script)ってめちゃくちゃ便利じゃん」ということに気付いてしまった僕ですが、その後もGASを使うとほかに何ができるのかといったことを仕事の合間に調べていました。
そんな中で見つけたのが、「GASを使えばGoogleスプレッドシートの更新情報をSlackに通知させることができる」という情報。更新情報を通知できるなら、逆に更新がない場合、つまり「対象のセル(列)に入力がない場合に、Slackへ通知する(アラートを送る)」こともできるのではと思い至り、今回もChatGPTの力を借りてスクリプトを作ってみました。

この記事では、「Googleスプレッドシートに記載された締め切りの1時間前になっても、指定した列に未入力のセルを含む行の情報をSlackに通知する方法」をご紹介します。少し長いので、お急ぎの方は必要なところだけお読みください。

目次

    想定される用途

    今回紹介するスクリプトの使い道としては、「●月●日の何時までに回答がもらえないと次の工程に影響が出てしまう」状況下で毎日回答を催促しなければならないケースや、単純に社内のタスクの管理に際して作業が遅れがちな人にアラートを送る、なんなら自分自身へのリマインダーとしての活用を想定しています。複数人で運用している記事サイトとかでも使えるかもしれないです。

    記事の作成にあたって課題管理表のような形式でGoogleスプレッドシートにサンプルの表を作ってみましたが、用途に合わせて作ったものや既に使用しているスプレッドシート内の表をそのままコピペしてChatGPTに伝えてあげれば、きちんとそれぞれの形式に合わせたスクリプトを作ってくれると思います(理由は後述)。

    図:とりあえずサンプルとして作ってみた表

    まずはincoming Webhookを設定する

    「準備段階その1」がスプレッドシート上に表を用意することだとすると、「その2」はSlackにincoming Webhookを追加すること。
    上記URLへアクセスして、incoming Webhookを検索するか、Slackの左側メニューの「その他」→「自動化」→「App」と移動して検索したら導入用のページにたどり着けます。 設定方法はインターネット上にたくさん転がっているのでさらっと流してしまいますが、ここで必須の作業は通知先のSlackチャンネルの設定と、Webhook URLの取得。ほかはあとからでも変更できます。
    取得したWebhook URLはどこかにメモっておいてください。ちなみに通知先のSlackチャンネルはDMでもよいみたいです。
    図:incoming Webhookの導入用ページ
    図:通知時のアイコンなどもこの画面から変更できるが、重要なのはあくまでも配信先のチャンネルの設定とWebhook URL

    ChatGPTと相談しながらGASに設定するスクリプトを作る

    もともと作っていた表の形式が悪かったのか、正確に動作するスクリプトをChatGPTが吐き出してくれるまで今回は何度も試行錯誤をすることになりました。
    同じ行が複数回にわたって通知されるのも鬱陶しいし、既に締め切りが過ぎたものを今更通知されても謝ることしかできないしなあというので、通知条件を細かく設定しようとしたことや、絞り込みや視認性を考えて日付と時間の列をわけたことも原因となって、いろいろうまくいかなかったようです。
    とはいえ、なんとかきっちり動いてくれたスクリプトがこちら。

    
    function sendSlackNotifications() {
      // シート名を指定
      const sheetName = 'ここに対象のシート名を記載する';
      
      // スプレッドシートの指定されたシートを取得
      const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
      if (!sheet) {
        Logger.log(`シート名 ""${sheetName}"" が見つかりません。`);
        return;
      }
      
      // シートの全データを取得
      const data = sheet.getDataRange().getValues();
      const now = new Date(); // 現在の日時を取得
      const notifications = []; // 通知メッセージを格納する配列
    
      // データの各行をループ処理(ヘッダー行は除く)
      for (let i = 1; i < data.length; i++) {
        const noValue = data[i][0]; // No.(A列)の値
        const importantFlag = data[i][2]; // 重要フラグ(C列)
        const deadlineDate = data[i][4]; // 期日(E列)
        const deadlineTime = data[i][5]; // 締め切り時間(F列)
        const question = data[i][6]; // 質問内容(G列)
        const responseStatus = data[i][7]; // 返答ステータス(H列)
        const slackNotified = data[i][9]; // Slack通知済みフラグ(J列)
    
        // 重要フラグのチェック(空欄でない場合を重要フラグと認識)
        const isImportant = importantFlag !== '';
    
        // 日付や時間が有効かチェックする
        if (isValidDate(deadlineDate) && isValidTime(deadlineTime)) {
          // 通知条件を満たすかどうかチェック
          const deadlineDateTime = createDateTime(deadlineDate, deadlineTime);
          if (isImportant && !slackNotified && responseStatus === '' && isDeadlineApproaching(deadlineDateTime, now)) {
            // 通知メッセージを配列に追加
            notifications.push(`No.${noValue}:${question}`);
            // 通知済みフラグをシートにセット
            sheet.getRange(i + 1, 10).setValue('通知済み'); // J列(index 9)に設定
          }
        } else {
          Logger.log(`無効な日付または時間: ${i + 1}行目 - 日付: ${deadlineDate}, 時間: ${deadlineTime}`);
        }
      }
    
      // 通知メッセージが存在する場合、Slackに送信
      if (notifications.length > 0) {
        sendToSlack(notifications.join('\n'));
      }
    }
    
    // 日付が有効かチェックする関数
    function isValidDate(date) {
      return date instanceof Date && !isNaN(date.getTime());
    }
    
    // 時間が有効かチェックする関数
    function isValidTime(time) {
      // 時間が日付オブジェクトの場合、時刻部分を抽出
      if (time instanceof Date) {
        return true;
      }
      // 時間が文字列かどうかをチェック
      if (typeof time !== 'string') {
        return false;
      }
      const timeParts = time.split(':');
      return timeParts.length === 2 && !isNaN(timeParts[0]) && !isNaN(timeParts[1]);
    }
    
    // 日付と時間を結合してDateオブジェクトを作成する関数
    function createDateTime(date, time) {
      const dateTime = new Date(date);
      if (time instanceof Date) {
        dateTime.setHours(time.getHours());
        dateTime.setMinutes(time.getMinutes());
        dateTime.setSeconds(time.getSeconds());
        dateTime.setMilliseconds(time.getMilliseconds());
      } else {
        const [hour, minute] = time.split(':').map(Number);
        dateTime.setHours(hour);
        dateTime.setMinutes(minute);
        dateTime.setSeconds(0);
        dateTime.setMilliseconds(0);
      }
      return dateTime;
    }
    
    // 指定された期日と締め切り時間が現在時刻から1時間以内かどうかを判定する関数
    function isDeadlineApproaching(deadline, now) {
      const oneHourFromNow = new Date(now.getTime() + 60 * 60 * 1000);
      return deadline >= now && deadline <= oneHourFromNow;
    }
    
    // Slackにメッセージを送信する関数
    function sendToSlack(message) {
      const Webhook URL = 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'; // Slack Webhook URL
      const payload = {
        text: `<!channel>\n【1時間以内に締め切りの課題】\n${message}` // 送信するメッセージ
      };
    
      const options = {
        method: 'post', // HTTPメソッドはPOST
        contentType: 'application/json', // コンテンツタイプはJSON
        payload: JSON.stringify(payload) // ペイロードをJSON形式に変換
      };
    
      // Slackにリクエストを送信
      UrlFetchApp.fetch(Webhook URL, options);
    }
    
    // スクリプトの定期実行トリガーを作成する関数
    function createTimeDrivenTriggers() {
      ScriptApp.newTrigger('sendSlackNotifications') // 実行する関数を指定
        .timeBased() // 時間ベースのトリガー
        .everyMinutes(10) // 10分おきに実行
        .create(); // トリガーを作成
    }
    
    

    何度もやり取りしている中でスクリプトだけでなく表自体も形式が若干変わってしまいましたが、新しく「通知済み」列を追加して、一度通知したものにはスクリプト側でフラグを立ててもらうように変更。おかげで通知済みの行に対するお知らせを何度も受け取らずに済むようになっています。
    漫然とお知らせを垂れ流すのも意味がなさそうだったので、最終的に@channelを付けてチャンネル全体に通知するような記述も追加しました(個人宛にするには少し記述の仕方が異なるようなので注意)。

    図:最初に貼ったキャプチャと比較すると、右端に「通知済み」の列が増えた

    ChatGPTには表の細かい中身を伝えるのがポイント

    紆余曲折を経て、最終版の完成に至ったChatGPTへの依頼は下記の内容になりました。
    実際に流用する場合はこの記事を読んでいる方のやりたいことに合わせて調整する必要が出てくる部分なので、あくまでも参考程度に見てもらえたらと思います。
    スプレッドシート内の表の範囲を丸ごとコピーして貼り付ければ、ChatGPT側できちんと表として認識してくれるみたいです(下図参照)。前半部分の謎の文字列は、上の章に画像として貼り付けた表が文字だけになったものとして捉えてください。

    栗原

    No.    記入日    重要フラグ    記入者    期日     締め切り時間     質問     返答     ステータス    Slack通知済み
    1 2024/05/17 ◎ 山田 2024/05/21 11:00 質問1です。どういう状況なのか教えてください。    確認中
    2 2024/05/18    山田 2024/05/21 11:00 質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2    確認中
    3 2024/05/19 ◎ 山田 2024/05/21 11:00 質問3は簡単な質問です。答えてください。 わかりました。 記載済み
    4 2024/05/20    山田 2024/05/21 12:00 特に質問はありませんでしたが、質問する必要があるので記載することにしました。中身はありません 返答は以上となります。よろしくお願いします。 記載済み
    5 2024/05/17 ◎ 山田 2024/05/21 14:00 ああああああああああああ    確認中
    6 2024/05/18 ◎ 山田 2024/05/21 13:00 いいいいいいいいいいいいい    確認中
    7 2024/05/19 ◎ 山田 2024/05/21 11:00 質問3は簡単な質問です。答えてください。 わかりました。 記載済み
    8 2024/05/20    山田 2024/05/21 13:00 特に質問はありませんでしたが、質問する必要があるので記載することにしました。中身はありません 返答は以上となります。よろしくお願いします。 記載済み

    ・Googleスプレッドシート上に作られた上記の表組みに関して、C列に何かしらの文字が入っている場合、E列とF列の期日が近づいたものをSlackへ通知する仕組みを作りたいと思っています。
    ・例外として、H列に既に値が入っているものについては、通知を行わないものとします。
    ・通知タイミングはE列とF列の期日の1時間前(1時間以内にE列F列の時刻が到達するものを通知する)。10分おきに通知タイミングが発生するように設定したうえで、多少の時間のズレは許容したいと思います。
    ・また、既に通知したものに関しては、以降は通知を行わず、期日を超過したものについても通知を行いません。
    ・通知メッセージにはG列の内容を載せたいです。同タイミングで通知されるものについては、ひとまとめにしてください。また、「No.●」の形式で●部分にA列の値も入力してください。
    ・通知メッセージの先頭には@channelを入れて、Slackチャンネルの全体へ通知が届くようにしてください。

    以下が通知メッセージ例文です。
    【1時間以内に締め切りの課題】
    No.6::質問1です。どういう状況なのか教えてください。
    No.9:質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2質問2
    No.11:いいいいいいいいいいいいい

    生成されたGAS(Google Apps Script)用のスクリプトのWebhook URLを入れる場所とシート名を入れる場所がわからなければ、それもChatGPTへの依頼に盛り込んでしまえば大丈夫です。

    図:表として認識してくれる。すごい

    トリガーを設定する

    スクリプトが完成したら、あとはトリガーの設定です。このスクリプトによって通知が来てほしいタイミングは「スプレッドシートの対象の列が更新されたとき」ではなく、「スプレッドシートの対象の列が更新されなかったとき」。
    前回の記事では「編集時」をトリガーとしていましたが、今回作ったGAS(Google Apps Script)がGoogleスプレッドシート上で動くようにするためには、また違う種類のトリガーを設定することがポイントになります。

    手順は次の通りです。

    1. メニュー内の「拡張機能」から「Apps Script」を選択。
    2. コード.gsのエディタ部分にChatGPTが作ったスクリプトを貼り付けて、フロッピーみたいな形状のアイコンを押して保存。
    3. 左側のメニューから目覚ましアイコンみたいな「トリガー」を選択。
    4. 右下の「トリガーを追加」を選択。
    5. 「イベントのソースを選択」から「時間主導型」を選択
    6. 「時間ベースのトリガーのタイプを選択」で分ベースのタイマーを選択
    7. 「時間の間隔を選択(分)」はお好みで。10分とか15分おきでいいと思います。
    8. 「保存」を押すと、ポップアップが開いてサインインを求められるので、サインインしてそのまま進み、「Allow」を押せばOK。
      ※ サインイン後に「Google hasn’t verified this app」みたいな警告が出るかもしれないですが、左下の「Advanced」を押して「Go to 【Apps Scriptのプロジェクト名】 (unsafe)」を躊躇なく押していけば進めます。
    9. 設定完了!
    図:更新されなかった場合用なので、トリガーは時間主導型。まとめて通知してほしい場合は1時間おきにもできる。

    最後に

    「何度も通知させない」「締め切りを過ぎたものは無視」など、わりとニッチというか、若干汎用性が低いかもしれないカスタマイズを施してしまってはいますが、サンプルのスクリプト自体をChatGPTに渡したうえで、「この表でこういうことがしたいです。このスクリプトを元に考えてください」などと伝えてあげたら、きっとうまいことやってくれると思います(もしくは上記の依頼内容例をコピペして投げる場合、該当箇所を削除しておく)。
    スクリプト実行時にエラーが出た場合も、エラー内容をChatGPTにコピペして送りつけたら何かしらの対応策を講じた新しいスクリプトを提示してくれたので、「この状態になっているものをこうしたときに、こうなっていてほしい」くらいの丁寧さで、どうしてほしいのかを具体的に言語化してChatGPTに伝えさえすれば、最新のAIがきっとなんとかしてくれるはず。

    前回の記事で作った「更新日時を自動で入力してくれるスクリプト」と組み合わせて「更新日時が●時以前(以降)のものを除外する」等の細かい指定もできる気がしています。

    設定しても正常に動かない、コメントアウトを入れてくれない(入れてと言ったら入れてくれた)、なぜかトリガーが増殖するなどのトラブルがあり、目論見通りの挙動をするスクリプトへたどり着くまでに時間はかかってしまったものの、最終的には意図したスクリプトが完成してよかったです。非エンジニアでも、実例を交えつつAIにやりたいことを粘り強く伝えれば、GAS(Google Apps Script)のスクリプトくらいならなんとかなる、なんならVBAもなんとかなるということがよくわかりました。
    皆さんもぜひご活用ください!

    図:実際の通知画面はこんな感じ