📑

GASで業務自動化した際によく使ったスニペットと踏み抜いたGASのバグ

2021/12/24に公開1

この記事はリアズ Advent Calendar 2021 の24日目の記事です。


今日は阪急電車のmoku TRAINレゴ デュプロとくるま図鑑を夜間配達します和田です🎅

今年は育休(2年ぶり2回目)を取るため、コーポレートIT系の作業を諸々自動化すべくGoogle Apps Scriptを結構書きました。

作ったもの

  • Google Workspaceアカウントの作成処理
    • Googleフォームから依頼されたらSlack上で承認してアカウントを自動作成する Google Workspaceアカウントの作成bot Slackからボタンぽちーでアカウント発行可能に
  • 退職者が出たら退職日にGoogle・Slack・Zoomのアカウントを一括停止する(自動デプロビジョニングは一部しかできてないのだ…)
  • Slackチャンネルのリネームするスラッシュコマンド(一般ユーザーでは出来ないため[1]
  • オンライン社員総会のためのSlack特設チャンネルに参加者を一括inviteするスラッシュコマンド
    • 応用してSlack上で災害時の安否確認にも使えるようにした
  • Zoomのライセンスに残余がある時は、Slack上でライセンス割当が出来るように
  • 複合機でFAX受信したら自動でGoogleドライブにPDF保存されるようにした
  • 取引先より請求書をEメールで受領したら自動でGoogleドライブに保存するようにした
  • etc...

その中で頻用したGASスニペット(関数)をメモとして書いておきます。

よく使うスニペット(ビギナー向け)

Slack APIにリクエストする

/**
 * Slack APIにPOSTする
 * @param {string} apiMethod - 使用するAPI
 * @param {Object} payload - tokenやパラメータ
 * @returns {GoogleAppsScript.URL_Fetch.HTTPResponse}
 */
function postSlackApi(apiMethod, payload) {
  // PARAMS
  const PARAMS = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded',
    'payload': payload
  };
  // リクエスト処理
  return UrlFetchApp.fetch(`https://slack.com/api/${apiMethod}`, PARAMS);
}

Slack中心の生活を送っているので、どうしてもSlack APIを叩くことが多いです。Slack APIドキュメントではメソッドが指定されてるけれど、実際は全部POSTでいけるようなので基本これです。
Tokenは他のパラメータと一緒にpayloadとして渡してます。

EメールアドレスからSlackIDを取得する

/**
 * EメールアドレスからSlackのuserオブジェクトを取得する
 * @param {string} email
 * @param {string} slackToken
 * @returns {GoogleAppsScript.URL_Fetch.HTTPResponse} Slack User Object
 */
function getSlackUserByEmail(email, slackToken) {
  // SlackのユーザーIDを取得する
  const apiMethod = "users.lookupByEmail";
  const apiParams = {
    "token": slackToken,
    "email": email
  }; // リクエストパラメータ
  return postSlackApi(apiMethod, apiParams);
}

個人を色んなサービスを横断して取り扱う時にどうしてもEメールアドレスがキーとして使いがち。だけどSlackでメンションしたりするにはSlack IDが欲しくなりますね。公式で便利APIが用意されているのでこれを使います。ユーザーオブジェクトが取れるので名前なんかも一緒に取得出来ます。 リクエストは1つめの関数を使います。
ただ、GASでAPIを使う、特に外部APIを使うと時間かかるので複数ユーザーを取り扱う時は users.list で取得してスプレッドシートに保存を定期実行して読み込んだ方が多少速いです。

営業日かどうか判定する(平日か土日祝か)

/**
 * 日付をパラメータとして与えると営業日(土日祝日ではない)かどうか真偽値で答える
 * @param {Date} requestDate
 * @returns {boolean} 営業日か否か
 */
function isBusinessday(requestDate) {
  // 土日なら
  if (requestDate.getDay() === 6 || requestDate.getDay() === 0) {
    return false;
  }
  // 日本の祝日カレンダーに予定があればfalse
  const JAPANESE_HOLIDAYS = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com');
  const SPECIFIC_DAY_SCHEDULE = JAPANESE_HOLIDAYS.getEventsForDay(requestDate);
  return SPECIFIC_DAY_SCHEDULE.length === 0;
}

ベタですがGoogleカレンダーの「日本の祝日」を参照することで、数年分の祝日データを作っておいて祝日法改正に合わせて修正するような面倒くさい作業から解放されて[2]、GASはシンプルに書けて好きです。
1日1回定期実行トリガーも冒頭にこの関数を呼んでfalseならメイン処理せずに終了とさせて便利です。

スプレッドシートの列に特定の値と一致する行を探す

/**
 * ターゲット列に値が一致する行番号を配列で返す
 * @param sheet
 * @param targetValue
 * @param {number} targetColumn
 * @returns {Array<number>} Corresponding Row
 */
function findRows(sheet, targetValue, targetColumn) {
  // シートのデータを二次元配列に取得
  const SHEET_VALUES = sheet.getDataRange().getValues();
  // 指定列の上から順に値がマッチするか調べる
  let targetArray = [];
  for (let i = 1; i < SHEET_VALUES.length; i++) {
    if (SHEET_VALUES[i][targetColumn - 1] === targetValue) {
      targetArray.push(i + 1);
    }
  }
  return targetArray;
}

スプレッドシートに保存しているデータから、指定列に検索値と一致する行をすべて出すというときに使います。 例えば3列目がtrueとなっている行(レコード)はどれだ?という時など。
最初に一致した行だけでなく該当する行全て欲しいので配列化しました。

踏み抜いたGASのバグ2選

スクリプトからトリガーを作っても実行されない

GASとSlackの連携は特に実行時間がシビアで、スラッシュコマンドなどでは3秒以内に応答しないといけません。ですがGASは基本的に非同期処理が出来ないのでトリガーを作って別のタイミングで実行させるのが肝要なのですが、どうもスクリプトからトリガーを作っても実行されず無効になります。
stack overflow経由で下記にたどり着きました。
Installed triggers are disabled when created from another trigger function in V8 [150756612]

どうやらV8ランタイムでのバグのようです。Rhinoにダウングレードしたら動くようですがconst/let使いまくってるし、テンプレートリテラルも使ってるし…もどせず絶望。
結局時間内になんとか収まるように頑張る、一部タイムアウトエラーを許容するという形に。

Webアプリ用公開URLの取得がおかしい

ScriptApp.getService().getUrl() doesn't work for versioned deployments [170799249]

Webアプリ(API)として公開すると変更の都度デプロイがいるのですが、GASの場合URLが毎回変わります。 Slackのインタラクティブメッセージの応答先URLとして自動的に出力したいのに、なぜかdev用のURLになってしまうバグ。
書き換えのたびに毎回手で変更するはめに… 😭

どちらのバグもGoogleとしては直す気がなさそうな感じでつらいですね。 いずれにしろ本格的な処理にはGASはしんどいですが、シュッと書いて出せるのが良いところですね。
2022年はパブリッククラウドのサーバレスサービスと組み合わせてもっと楽していきたいと思います。

それではメリークリスマス!🎄

脚注
  1. チャンネルオーナーなら出来る ↩︎

  2. 2019〜2021年は即位とかオリンピックなどでイレギュラーな祝日もありましたね。 ↩︎

Discussion

Yusuke WADAYusuke WADA

スクリプトからトリガーを作っても実行されない

これは修正されたようです。