🎮

GASを使って調整さんの集約結果をDiscordへ投稿してみる

2021/09/20に公開

経緯

現在私はFF14というオンラインゲームで絶アルテマというコンテンツを友人たちと遊んでいます。
このコンテンツはFF14における高難易度コンテンツの一つであり、長さにしてたった16分であるにも関わらず、その攻略には数週間から場合によっては数ヶ月を要するかなりの長期戦となっています。
※ちなみにFF14はもっと気軽に楽しめるコンテンツも山程あります。高難易度コンテンツ遊んでるマゾい人間は極一部です。
今回あつまった私を含む攻略メンバーはそのほとんどが社会人であるため、当然一緒に遊べる時間もバラバラです。ですので、攻略メンバーのリーダーが毎週調整さんという日程調整ツールを使って遊べる日程を調整してくれています。日程調整のフローは以下のとおりです。

  1. リーダーが調整さんの集約用ページを作成(1週間分)
  2. 各メンバーは集約用ページに自身が参加可能な日程を記入
  3. 全員が集約用ページに予定を入力した後、全員が参加可能な日程をDiscordサーバーに投稿する

上記フローの通り、リーダーは毎週作成した予定表への入力の催促や入力結果の集約を行うわけですが、これをGASを使って簡略化できないかな?と思い、今回GASを使った調整さんの内容をDiscordへ投稿する機能を作成しました。

参考にさせていただいたサイト

https://qiita.com/iroha71/items/b2a473898d6c9b4b4ae7
https://strategy-conference.com/analysis/spreadsheet/1045/

機能要件

  • 参加者全員が調整さんに入力できている場合は、対応可能な日程をDiscordに投稿する
  • 出席可否が全員「○」である場合は、日程確定扱いとして当該日程を列挙する。
  • 出席可否に「△」が含まれている場合は、開始時間に変更がある可能性がある旨を付記した上で日程を列挙する。
  • 出席可否に「✕」が含まれている場合は、その日程は除外する
  • もしすべての候補日に「✕」が含まれていた場合は、対応可能な日程が無い旨を投稿する。
  • いずれかの参加者が調整さんに入力できていない場合は、入力できているメンバー名をDiscordに投稿する。

作成方法

DiscordのWebHookURLを取得

下記サイトを参考に、投稿先のDiscordサーバーのWebHookURLを取得します。この時、Discordチャンネルはメッセージの投稿先のチャンネルを指定してやるようにしてください。
https://qiita.com/iroha71/items/b2a473898d6c9b4b4ae7

調整さんでイベントを作成

調整さんで任意の形式でイベントを作成します。

イベント作成後、URLをコピーして別に保存しておきます。

作成したイベントに対し、参加者が参加可能な予定を入力すると、以下のような感じになります。

調整さんで作成したイベントの情報をスクレイピング

今回はSpreadsheetに各種パラメータ、及びスクレイピング結果を保存することにします。
まず、任意の名前でSpreadsheetを作成し、シート名を「入力」とし、下図の通り入力項目を作成してください。

その後、最大人数の項目に先程調整さんで作成したイベントに参加予定の人数を入力します(GASの項で使います)。

次に、調整さんURLの項目(B2セル)に先程調整さんで作成したイベントのURLを貼り付けます。

すると、以下のようになると思います(最大人数は今回4人としました)。

これで入力シートの設定は完了です。

次に、調整さんで作成したイベントの「日程表」シートを作成し、「日程表」シートのA1セルに、

=ImportHTML('入力'!B2, "table", 1)

と入力してください。そうすると、指定したURL(今回はB2セルを指定しているので、調整さんのイベントページ)からTableを取得してくれます。

標準関数一つでスクレイピングが可能とは、流石Googleさんですね。

ただし、このままだと同じスクレイピング結果が残り続けることになりますので、Spreadsheetのファイル>Spreadsheetの設定 から再計算の頻度を「変更時と毎時」としておきましょう。

これで1時間毎にスクレイピングの結果が更新されるようになりました。

GASでDiscordへの投稿用文章を作成する

あとは取得したスクレイピング結果を元に、投稿用の文章を作成するだけです。
Spreadsheetのツール>スクリプトエディタ からGASのエディタを開き、以下のコードを入力します。

function send_schedule_to_discord_server() {

  // 日程の集約結果を管理するクラス
  class Schedule {
    constructor(date, attendance, status) {
      this.date = date;                 //候補日時
      this.attendance = attendance;     //各員の出席状況
      this.status = status;             //日程dateの確度
    }
  }

  // Discordへの送信用クラス
  class DiscordBot{

    // discord側で作成したボットのウェブフックURL
    constructor(webhookURL){
      this.webhookURL = webhookURL;
    }

    // メッセージ送信メソッド
    sendMessage(text){

      const message = {
        "content": text, // チャット本文
        "tts": false  // ロボットによる読み上げ機能を無効化
      }

      const param = {
        "method": "POST",
        "headers": { 'Content-type': "application/json" },
        "payload": JSON.stringify(message)
      }

      UrlFetchApp.fetch(this.webhookURL, param);

    }
  }

  const ss = SpreadsheetApp.getActiveSpreadsheet();

  //コンフィグの取得
  const configsheet = ss.getSheetByName("入力");
  const [participantsNum,tyoseiURL,discordWebHookURL] = configsheet.getRange(2, 1, 1, 4).getValues()[0];

  //discordBotをインスタンス化
  const discordBot = new DiscordBot(discordWebHookURL);

  //調整さんからスクレイピングした予定表の取得
  const schedulesheet = ss.getSheetByName("日程表");
  const scheduleList = schedulesheet.getDataRange().getValues();
  const checkSchedule = [];

  //参加者が全員予定を入力していれば、以下の対応可能日時の取得処理を実行
  if (scheduleList[0].length === (participantsNum + 1)){

    // 日程表の内容を抽出、整形
    // 最初と最後の行以外を取得(参加者名、コメント行は不要であるため)
    for (let i = 1; i < scheduleList.length - 1; i++) {
      const date = scheduleList[i][0];
      const attendance = scheduleList[i].slice(1, scheduleList[i].length);

      let status;

      if (attendance.includes("×")) {
        status = "NotAvailable";
      } else if(attendance.includes("△")){
        status = "Possible";
      } else if(attendance.includes("○")){
        status = "Confirm";
      }
      checkSchedule.push(new Schedule(date, attendance, status));
      console.log(date, attendance, status);
    }

    //対応可能な日程を抽出
    let possibleStr = "開始時間に変更が発生する可能性がある日程は以下のとおりです\n"
    let confirmStr = "実施確定となった日程は以下のとおりです\n";

    const initialPossibleStr = possibleStr;
    const initialConfirmStr = confirmStr;

    for(const schedule of checkSchedule){

      switch(schedule.status){
        case "Possible":
          possibleStr += schedule.date + "\n";
          break;
        case "Confirm":
          confirmStr += schedule.date + "\n";
      }

    }

    // Discordへメッセージを送信
    if(possibleStr === initialPossibleStr && confirmStr === initialConfirmStr){
      discordBot.sendMessage("対応可能な日程はありません");
    }else{
      discordBot.sendMessage(confirmStr);
      discordBot.sendMessage(possibleStr);
    }

  }else{
    // 参加者全員が調整さんに入力していない場合、入力済みの参加者名をDiscordに送信する
    let enteredMember = "調整さんに入力していない人がいます。\n入力済みのメンバーは下記のとおりです。\n";
    for(let i=1;i<scheduleList[0].length;i++){
      enteredMember += "・" + scheduleList[0][i] + "\n";
    }
    discordBot.sendMessage(enteredMember);    
  }

}

コード入力後、エディタからコードを実行するとセキュリティのポップアップが表示されるかと思いますが、例によってすべてOKとしてください。

実行結果

  • 参加者全員が調整さんへ入力済みの場合

    対応可能な日程がある場合はそのリストがDiscordサーバに投稿されます

    対応可能な日程がなかった場合、その旨が投稿されます

  • 参加者のうち誰かが調整さんへ入力していない場合は、調整さんへ入力済みの参加者名が投稿されます(今回はテスト3が入力していない)。

実行用のボタンを作成

ここまでで投稿機能の作成は完了しましたので、あとは実行用ボタンを作成して完成です。
Spreadsheetの挿入>描画図形 を選択し、任意の図形を作成します。


作成したボタンに対し、押下時にsend_schedule_to_discord_serverを実行するように設定すれば完成です。

おわりに

以上でGASを使った調整さんの内容をDiscordへ投稿する機能の作成は終了です。
もう少し手を加えれば、指定日時に自動で投稿する機能なんかも作れるかと思います。

なお、もしFF14に興味のある方がいらっしゃるようでしたら、フリートライアルという無料体験版がございますのでぜひダウンロードしてみてください(フリートライアルだけでもそのへんのゲーム2本分くらい楽しめます)
https://www.finalfantasyxiv.com/freetrial/?&M

オンラインゲームなんて興味ないぜ!って人は、FF14のプロデューサーである吉田さんのコラムだけでも読んでみてください。オンラインゲームに興味のない方でも楽しんで読んでいただけると思います。
https://www.asahi.com/and/article/20191120/7323398/

Discussion