🐡

便利だって言われたGAS

2023/11/17に公開

エンジニアではありませんが初心者なりに勉強しながらGASで業務改善をちまちまやっております。DELTAの伊藤です。
往々にして欲しいと言われて作ったのに使われないなんてこともありますが、便利と言われたGASを一つ紹介します。


作ったもの

任意のGoogleカレンダーから予定の情報を取得して、自動でMTG議事録用のファイルを作ってくれるGAS

仕様
毎日定時に起動し、[初回] タグがついているカレンダーイベントの情報を取得して、スプレッドシートに情報を追記。合わせてMTG用フォルダを作成し、slackに通知する。
※トリガーを始業時間付近で設定しています。

便利になったところ

  • 初めてMTGする相手の基本情報を自動でDB化
  • 初めてMTGするための議事録用ファイルなどを自動で用意
  • MTG当日の朝に誰と初回MTGかを通知


初め画像みたいな感じのカレンダーの予定から、自動でMTG情報がスプレッドシートに溜まっていくようなのGASで出来ない?という相談を受けました。

MTG毎に毎回議事録用のドキュメントファイルなどを用意していたため、そこまで自動化できるのでは?ということで、以前書いたフォルダの複製の機能を追加しました。


スプレッドシート

H1セルに読み込みたいカレンダーのメールアドレス(ID)を入力する。

カレンダー入力ルール

  • 予定タイトルの"[初回]"をトリガーに起動
  • 分類はカレンダー予定の説明一行目に"A"または、"B"または、両方を入力
  • タイトルは「【場所】MTG相手[初回]」の形式
    注意
    ※毎日10-11時の間に起動するため、それよりも後に作成された予定は取得できません

コードと解説

コード全体

※雑な作りなのでなんでも許せる人向けです

function checkEventTitle() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  const mailaddress = sheet.getRange("H1").getValue();
  const calendar = CalendarApp.getCalendarById(mailaddress);
  const events = calendar.getEvents(new Date(), new Date(Date.now() + (4 * 24 * 60 * 60 * 1000))); // 1週間分の予定を取得
  let startTime = "";
  let endTime = "";
  let title = "";
  for (let i = 0; i < events.length; i++) {
    title = events[i].getTitle();
    startTime = Utilities.formatDate(events[i].getStartTime(), 'JST', 'yy/M/d HH:mm');
    endTime = Utilities.formatDate(events[i].getEndTime(), 'JST', 'HH: mm');
    let comment = "";
    //[初回]タグ
    if (title.includes("初回") && !title.includes('[初回]')) {
      comment +=
        "・[初回]タグを正しく変更してください。(予定当日の場合、フォルダは作成されていません) " + '\n'
    }
    if (comment !== "") {
      comment += "*" + "日時: " + startTime + " - " + endTime + " " + title + "*" + '\n' + "https://calendar.google.com/";
      slackNotice(comment);
    }
  }
  getCalendarEvent(sheet, mailaddress);
}

function getCalendarEvent(sheet, mail) {
  const lastRow = sheet.getRange(sheet.getMaxRows(), 2).getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
  const finishedEvents = [];
  for (let i = 2; i <= lastRow; i++) {
    const finishedEvent = sheet.getRange(i, 6).getValue();
    finishedEvents.push(finishedEvent);
  }

  const today = new Date();
  const myEvents = CalendarApp.getCalendarById(mail).getEventsForDay(today);

  // [初回]が含まれる予定のみを取得
  const filteredEvents = myEvents.filter(event => event.getTitle().includes("[初回]"));
  const date = Utilities.formatDate(today, 'JST', 'yy/MM/dd');
  // filteredEventsに対して操作を行う(例えばタイトルを修正するなど)
  for (let i = 0; i < filteredEvents.length; i++) {
    const eventTitle = filteredEvents[i].getTitle();
    const lastRow = sheet.getRange(sheet.getMaxRows(), 3).getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
    console.log(eventTitle)
    if (finishedEvents.includes(eventTitle)) {
      let comment =
        '【フォルダ作成失敗】同一タイトルの予定があります。本日のカレンダーの予定を確認してください。' + '\n'
        + date + " " + "『" + eventTitle + "』";
      slackNotice(comment);
    } else {
      const removes = ['[初回]'];

      const regex_place = /(.*?)/;
      const matches = eventTitle.match(regex_place);
      if (matches) {
        removes.push(matches[0]);
      }
      const regex_other = /\[(.*?)\]/g;
      const matches_other = eventTitle.match(regex_other);
      const other = [];
      if (matches_other !== null) {
        matches_other.forEach(match => {
          if (match !== '[初回]') {
            other.push(match);
            removes.push(match);
          }
        });
      }
      console.log(other);
      console.log(removes);

      const others = other.join(', ');

      let partner_name = eventTitle;
      removes.forEach(removeItem => {
        const regex = new RegExp(`\\${removeItem}`, 'g');
        partner_name = partner_name.replace(regex, '');
      });
      console.log(partner_name);

      const description = filteredEvents[i].getDescription();
      const lines = description.split('\n');
      const firstLine = lines[0];

      if (firstLine.match('A') && firstLine.match('B')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類A,分類B");
      } else if (firstLine.match('A')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類A");
      } else if (firstLine.match('B')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類B");
      } else {
        sheet.getRange(lastRow + 1, 3).setValue("不明");
      }
      sheet.getRange(lastRow + 1, 2).setValue(partner_name);
      sheet.getRange(lastRow + 1, 4).setValue(others);
      sheet.getRange(lastRow + 1, 5).setValue(date);
      sheet.getRange(lastRow + 1, 6).setValue(eventTitle);
      folderCreate(partner_name, date);
    }
  }
}

function folderCreate(partner_name, date) {
  //新規フォルダ作成
  const folderIdSrc = 'コピー元のフォルダid';

  const folderSrc = DriveApp.getFolderById(folderIdSrc);//元のフォルダ
  const folderDest = folderSrc.getParents().next().createFolder(partner_name);//新しい名前で一番外側のフォルダを作る
  copyFolder(folderSrc, folderDest);//copyFolderに渡す

  function copyFolder(src, dest) {
    const folders = src.getFolders();//元のフォルダを取得
    const files = src.getFiles();    //元のファイルを取得

    //---------内部ファイルをコピー--------//
    while (files.hasNext()) {
      const file = files.next();      //ファイルを取得して、
      const newfile = file.makeCopy(file.getName(), dest);//コピーを作成
      var copy_filename = newfile.getName();  //すべてのファイル名を取得
      const newFileName = copy_filename.replace("〇〇", partner_name).replace("yy/mm/dd", date);//置き換える
      newfile.setName(newFileName);         //新しいタイトルをセットする
    }
    //---------内部フォルダをコピー--------//
    while (folders.hasNext()) {
      const subFolder = folders.next();   //フォルダを取得
      const folderName = subFolder.getName();//フォルダ名を取得
      const folderDest = dest.createFolder(folderName);//新しい名前でフォルダを作る
      copyFolder(subFolder, folderDest);    //copyFolderに渡す
    }
  }
  const folderURL = folderDest.getUrl();
  let comment = '【本日初回】フォルダを作成しました' + ':' + "『" + partner_name + "』" + " 様" + '\n'
    + folderURL;
  slackNotice(comment);
}

function slackNotice(comment) {
  var postUrl = '通知したいslackチャンネルのIncoming WebhooksURL';
  var username = 'カレンダー予定通知';
  var icon = ':spiral_calendar_pad:';

  let jsonData =
  {
    "username": username,
    "icon_emoji": icon,
    "text": comment
  };
  var payload = JSON.stringify(jsonData);

  var options =
  {
    "method": "post",
    "contentType": "application/json",
    "payload": payload
  };
  UrlFetchApp.fetch(postUrl, options);
}

参考にしたページ

https://for-dummies.net/gas-noobs/how-to-get-events-from-calendar-by-gas/


上から関数の説明をするとこんな感じ。

  • checkEventTitle()⇒イベントタイトルが正しいか確認する
  • getCalendarEvent(sheet, mail)⇒[初回]タグをつけたイベントの情報を取得する
  • folderCreate(partner_name, date)⇒getCalendarEventで取得した情報をもとに新規フォルダを作成する
  • slackNotice(comment)⇒フォルダの作成やイベントタイトルの確認をslackのチャンネルに飛ばす

checkEventTitle()

function checkEventTitle() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  const mailaddress = sheet.getRange("H1").getValue();
  const calendar = CalendarApp.getCalendarById(mailaddress);

CalendarApp.getCalendarById();で読み込みたいカレンダーを設定します。
個人のカレンダーの場合はメールアドレスがカレンダーIDになるため、あらかじめスプレッドシートに入力してあるメールアドレスを取得しています。

  const events = calendar.getEvents(new Date(), new Date(Date.now() + (4 * 24 * 60 * 60 * 1000))); // 1週間分の予定を取得
  let startTime = "";
  let endTime = "";
  let title = "";
  for (let i = 0; i < events.length; i++) {
    title = events[i].getTitle();
    startTime = Utilities.formatDate(events[i].getStartTime(), 'JST', 'yy/M/d HH:mm');
    endTime = Utilities.formatDate(events[i].getEndTime(), 'JST', 'HH: mm');
    let comment = "";
    //[初回]タグ
    if (title.includes("初回") && !title.includes('[初回]')) {
      comment +=
        "・[初回]タグを正しく変更してください。(予定当日の場合、フォルダは作成されていません) " + '\n'
    }
    if (comment !== "") {
      comment += "*" + "日時: " + startTime + " - " + endTime + " " + title + "*" + '\n' + "https://calendar.google.com/";
      slackNotice(comment);
    }
  }
  getCalendarEvent(sheet, mailaddress);
}

[初回]タグが(初回)とか【初回】のような形になっている場合は、フィルターに引っかからないため一週間先まで見て、事前にタグが正しいかチェックします。

変更しなければならないイベントがどれか分かりやすいように、親切で時間まで表示しています。

ここの部分だけ別のgsファイルを作っても良かったんですが、checkEventTitle()が終わってからgetCalendarEvent()が起動するようにしています。
当日のイベントだった場合にフォルダが作成されないことをお知らせできるようにです。


getCalendarEvent()

function getCalendarEvent(sheet, mail) {
  const lastRow = sheet.getRange(sheet.getMaxRows(), 2).getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
  const finishedEvents = [];
  for (let i = 2; i <= lastRow; i++) {
    const finishedEvent = sheet.getRange(i, 6).getValue();
    finishedEvents.push(finishedEvent);
  }

スプレッドシートに入力されている終了済のイベント(finishedEvent)を取得します。

  const today = new Date();
  const myEvents = CalendarApp.getCalendarById(mail).getEventsForDay(today);
  // [初回]が含まれる予定のみを取得
  const filteredEvents = myEvents.filter(event => event.getTitle().includes("[初回]"));

毎日定時に起動するため、今日のイベントのなかで [初回]タグがついてるイベントのみをフィルターします。

  const date = Utilities.formatDate(today, 'JST', 'yy/MM/dd');
  // filteredEventsに対して操作を行う(例えばタイトルを修正するなど)
  for (let i = 0; i < filteredEvents.length; i++) {
    const eventTitle = filteredEvents[i].getTitle();
    const lastRow = sheet.getRange(sheet.getMaxRows(), 3).getNextDataCell(SpreadsheetApp.Direction.UP).getRow();
    console.log(eventTitle)
    if (finishedEvents.includes(eventTitle)) {
      let comment =
        '【フォルダ作成失敗】同一タイトルの予定があります。本日のカレンダーの予定を確認してください。' + '\n'
        + date + " " + "『" + eventTitle + "』";
      slackNotice(comment);

イベントをコピーして作成した時を想定して、まったく同じイベントタイトルがある場合は本当に初回か確認する通知をslackに投げるようにしてます。

それ以外(被りのタイトルがなかった)の場合に、スプレッドシートへイベント情報を入力していきます。

      const removes = ['[初回]'];

      const regex_place = /(.*?)/;
      const matches = eventTitle.match(regex_place);
      if (matches) {
        removes.push(matches[0]);
      }
      const regex_other = /\[(.*?)\]/g;
      const matches_other = eventTitle.match(regex_other);
      const other = [];
      if (matches_other !== null) {
        matches_other.forEach(match => {
          if (match !== '[初回]') {
            other.push(match);
            removes.push(match);
          }
        });
      }
      console.log(other);
      console.log(removes);

      const others = other.join(', ');

      let partner_name = eventTitle;
      removes.forEach(removeItem => {
        const regex = new RegExp(`\\${removeItem}`, 'g');
        partner_name = partner_name.replace(regex, '');
      });
      console.log(partner_name);

イベントタイトルからスプレッドシートに記入する情報を抜き出すため試行錯誤した結果、なんだかかなり非効率なやり方をしている気がしますが、今の限界です。。

      const description = filteredEvents[i].getDescription();
      const lines = description.split('\n');
      const firstLine = lines[0];

      if (firstLine.match('A') && firstLine.match('B')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類A,分類B");
      } else if (firstLine.match('A')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類A");
      } else if (firstLine.match('B')) {
        sheet.getRange(lastRow + 1, 3).setValue("分類B");
      } else {
        sheet.getRange(lastRow + 1, 3).setValue("不明");
      }
      sheet.getRange(lastRow + 1, 2).setValue(partner_name);
      sheet.getRange(lastRow + 1, 4).setValue(others);
      sheet.getRange(lastRow + 1, 5).setValue(date);
      sheet.getRange(lastRow + 1, 6).setValue(eventTitle);
      folderCreate(partner_name, date);

イベント説明一行目から分類を取得します。
その後スプレッドシートに情報を記入し、フォルダ作成に移ります。


folderCreate()
フォルダ作成部分は以前に公開したフォルダ複製GASの記事に詳しく書いています。システム的に特別変更した部分などはないため、ここでは割愛。
テンプレートフォルダの中身
自動で新規作成されるファイル。タイトルがちゃんと変更されてます


slackNotice()

  let jsonData =
  {
    "username": username,
    "icon_emoji": icon,
    "text": comment
  };

slack通知の機能は死活監視の記事でちらっと触れていますが、今回は複数種類の通知をslackNotice()でまとめて通知出来るように、jsonData内のtextに直接通知内容を入力するのではなく、それぞれの関数内で通知内容を定義してcommentとしてslackNotice()に渡すようにしています。

感想

この機能を作ったのは3ヶ月くらい前になるので、忘れてしまってる部分もあるんですが、なんかいい感じに適当につけた予定タイトルも編集してくれて勝手に情報収集してくれて…みたいな機能にしたくて奮闘してました。なんの形式もない状態なのに”なんかいい感じ”にするのはかなり難しいと気付けたのはいい経験です。
わざわざ作ってくれて良かったとか、助かるみたいに言われること自体貴重なので、割とピンポイントで優先度とか難易度は高くないけど、自動に出来るたら手間が減る。みたいなものがGASの需要なんだなとしみじみ感じました。

We're hiring!

DELTAではチームの一員になっていただける仲間を募集中です!
下記フォームよりお気軽にご連絡ください!
http://bit.ly/3Y2818U

DELTAテックブログ

Discussion