GAS

GASでGoogleグループのメンバー一覧を取得するコードと解説

0.はじめに

GoogleWorkspaceを管理されている方々なら
「Googleグループを一覧化したい、マスタ管理したい」
そう思う事が絶対にあると思います。

そんな方に向けて、グループメンバーを一覧化するコードをご紹介します。
簡単に解説もしていきますので、ぜひ参考にしてみてくださいね。

1.コード

サンプルコードです。


//メイン処理
function getGroupListAll(){  
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('test');

  let domainList = ["ドメインを入力","ドメインを入力"];
  let output = ([["グループ名", "メールアドレス", "メンバー数", "グループメンバー"]]);

  //outputに対して一気にグループ情報とメンバー情報を付加
  domainList.forEach(x=> pushArray(output,getGroupInfo(x)));

  //2次元配列で貼り付けるための整形
  inputSpace(output);

  //シートクリア
  sh.clear();

  //貼り付け
  sh.getRange(1,1,output.length,output[0].length).setValues(output);
}


//ドメインのグループを全件取得する
function getGroupInfo(domain){
  let maxResults = 200;  
  let token = '';
  let result = []
  do{
    if(token==""){
      var groupsList = AdminDirectory.Groups.list({domain: domain, maxResults: maxResults})
    }else{
      var groupsList = AdminDirectory.Groups.list({domain: domain, maxResults: maxResults, pageToken: token})
    }
    if(groupsList.groups) {
      for(let i in groupsList.groups){
        let groupName = groupsList.groups[i].name//グループ名
        let groupMail = groupsList.groups[i].email//メールアドレス
        let groupNumber = groupsList.groups[i].directMembersCount//メンバー数
        let groupMember = getMemb(groupsList.groups[i].email)
        let forPush = [groupName,groupMail,groupNumber]
        result.push(forPush.concat(groupMember))
        }
      }
      token = groupsList.nextPageToken
    }while(token)
  return result
}


//addressのメンバーを1次元配列で取得
function getMemb(address){
  let rv = [];
  let member = ''
  let token = '';

  do{
    if(token == ''){
      member = AdminDirectory.Members.list(address,{maxResults:200});
    }else{
      member = AdminDirectory.Members.list(address,{maxResults:200, pageToken: token});
    }
    if(member.members){
      member.members.forEach(x => rv.push(x.email));
    }
  token = member.nextPageToken;
  }while(token)

  return rv;
}


//破壊的に二次元配列に二次元配列を結合する
function pushArray(array1, array2){
  for(let arr2 of array2){
    array1.push(arr2);
  }
}


//長さの違う2次元配列に空白を入力する
function inputSpace(array){
  let wide = 0;
  for(let arr of array){
    if(arr.length>wide){
      wide = arr.length;
    }
  }
  for(let arr of array){
    while(arr.length<wide){
      arr.push('');
    }
  }
}

多分もっと分かりやすく、綺麗に書く事もできると思います。
ちなみにAIチャットボットChatGPTに書いてもらうとこんな感じになります。
AIチャットボットにGASを書いてもらったら実用レベルで絶望した件

2.出力結果

こんな感じにスプレッドシートに出力されます。
メンバーリストの出力結果画像

3.解説

3-1.AdminDirectory

グループ情報取得、メンバー情報取得の両方の処理でAdminDirectoryを使用します。
エディタ左側の「サービス」から「AdminSDK」を追加しないと使えません。

Groups.listの公式リファレンス
Members.listの公式リファレンス

3-2.構成

大きな流れとしては、こんな感じになっています。

貼り付け用の二次元配列とヘッダを用意

各ドメインをループし全グループを取得
└ループ内で各グループのメンバーを取得

行ごとに長さの違うジャグ配列になっているため、
列数を揃えるためにinputSpace()で整形

シートをクリアして貼り付け

3-3.ポイント

Groups.list、Members.list両方とも1リクエスト当たりの上限があります。
200件が上限のため200件以上の場合はトークンを取得して
次ページ分の情報取得へ進んでいく必要があります。

コード内で.nextPageToken()してトークンを取得、
do while(token)でトークンがある場合は次のページを取得しています。

4.注意点

4-1.セル数上限に注意

「3000人入っているグループが5000件ある・・・」等、
ボリュームがある場合はスプレッドシートのセル数上限に注意してください。

セル数上限は昨年引き上げられてスプレッドシート当たり1000万になりましたが、
人数分、右方向にセルを使う仕様のため要注意です。

対策したい場合、1行100列までにして次の行に折り返す工夫などが必要です。

4-2.タイムアウトに注意

グループを全件取得し、更にメンバーを全件取得しています。
グループ2000×メンバー3000だと単純に掛け算して6,000,000。
処理数が多くなるので時間がかかります。

対策としては、ループをラムダ式に置き換え高速化を図るか、
タイムアウトしそうになったら一度処理を終了して
続きから実行するトリガーを設定するような手法が必要になります。

何にせよ、一度ご自身の環境で実行して対策の要否を検討してみてください。
私の環境では2000グループ×2000名ほどですが、何とかなっています。

5.さいごに

いかがでしたでしょうか。
これがあると、Admin画面を見られない方にも参照して頂けます。
メンバーをユーザー名で表示したい場合、
こちらの記事も参照してユーザーリストと照合する仕組みにしてください。
GASで全ユーザーのリストをスプレッドシートに日次取得する方法
ぜひ活用してみてくださいね。

脱ハードコード!問い合わせ削減!GASで設定をシート上に持つ方法

0.はじめに

GASをある程度深めた方。
管理の質に着目した事はありますか?

例えばノンプログラマーに対して業務ツールを提供したとします。
自動メールのテンプレートやCC宛先を変更するために、毎回ユーザーから依頼が入ります。
これ、運用管理者側で変更してもらえると楽ですよね。

今回はその1つとして、スプレッドシート上で設定値を保持して
それらの情報を使用する際に取得する
ところまでご紹介します。

1.【事例1】メールテンプレート設定

1-1.スプレッドシート側

まず、スプレッドシート側の具体例です。
画像をご確認ください。
設定シート

1-2.GAS側

ではこの設定情報をどうやって取得するか、GASコードを見ていきます。

function myFunction(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName("setting");
  const vals = sh.getRange("A2:H").getValues().filter(x => x[0]); //A列が空でないvaluesにfilter
  const settingObj = vals.map(x => new MailSetting(x));
  console.log(settingObj);
  /**
   * settingObjの中身
  [ 
    { Id: '01',
      Subject: '【test1】住所変更申請を受け付けました【%日時%】',
      Body: '長いので省略しますが、本文列の内容が入ってます。',
      To: [ '%申請者%' ],
      Cc: [ 'jusho_sys@test.test.jp', 'jibunnoemail@jibunnoemail.com' ],
      Bcc: [],
      FromName: '住所申請システム',
      FromAddress: 'jusho_sys@test.test.jp' },
    { Id: '02',
      Subject: '【test1】緊急連絡先変更申請を受け付けました【%日時%】',
      Body: '長いので省略しますが、本文列の内容が入ってます。',
      To: [ '%申請者%' ],
      Cc: [ 'kinkyu_sys@test.test.jp', 'jibunnoemail@jibunnoemail.com' ],
      Bcc: [],
      FromName: '緊急連絡先申請システム',
      FromAddress: 'kinkyu_sys@test.test.jp' } ]
   */

  //settingObjの中身から使う時は
  const setting02 = settingObj.find(x => x.Id == "02");

  //body等の%申請者%や%日次%はreplaceを使って置換します。
  const body = setting02.Body.replace("%申請者%","山中田村丸太郎乃介");
}

class MailSetting{
  constructor(rowArray){
    this.Id          = rowArray[0];
    this.Subject     = rowArray[1];
    this.Body        = rowArray[2];
    this.To          = rowArray[3].split(",").filter(x => x);
    this.Cc          = rowArray[4].split(",").filter(x => x);
    this.Bcc         = rowArray[5].split(",").filter(x => x);
    this.FromName    = rowArray[6];
    this.FromAddress = rowArray[7];
  }
}

お分かりいただけたでしょうか。
クラスとmapを活用して一気にobject配列を作成しています。

Googleフォームに対して自動返信をするとした時、
全てのGoogleフォームがこのスプレッドシートを見て自動返信してくれたとしたら、
非常に管理が楽になりますよね。

コードを直接触る必要も無くなり、メンテミスの可能性が下がります。
本文を編集するには若干htmlの知識が必要ですが、
それでも大部分は運用管理者が実務に合わせてメンテナンスできると思います。
「システム担当にわざわざ頼むの面倒・・・」という事もなくなりますね。

2.【事例2】発動日を指定したい

2-1.スプレッドシート側

超シンプルです。こんなのもアリです。

設定シート

2-2.GAS側

function myFunction2(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName("setting2");
  const targetDates = sh.getRange("B2").getValue().split(',').filter(x => x);

  //day.jsを使います 左のライブラリに追加してください
  //スクリプトID 1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
  const today = dayjs.dayjs();
  //毎日
  if(targetDates.find(x => x == today.date())){
    //毎日トリガーで実行した上で、設定シート上の日付と一致する場合だけメイン処理を実行する、等
  }
}

コード欄にもコメントで説明を書いていますが、
毎日起動するトリガー設定をした上で、設定シート上の日付の場合だけ処理を行うようなコードです。

ライブラリday.jsを使用しているので、コピペで使う場合はライブラリに追加してください。
追加に必要なIDは「1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB」です。

3.おわりに

いかがでしたでしょうか。

GASは当然Googleサービスとの繋がりが強いので、
設定を外部化するための1つの方法としてスプレッドシート活用が有効な候補になります。
小規模開発でもメンテナンス性を上げるために、活用しない手はないでしょう。

また今回の記事で活用したmapやfindについてこちらの記事で解説しています。
GAS初心者がfor文やif文の次に学ぶと急激にレベルアップする技術7選
興味のある方は併せてご覧ください。

それではまた。ありがとうございました。

GASで全カレンダーリソースの名称とアドレスをスプレッドシートに取得する方法

1.はじめに

GoogleWorkspaceを使っている方で、
「当社管理のカレンダーリソースを全件取得したい!」
などと思っている方、居られるのではないでしょうか。

そんな方々へ、簡単コピペで実現できるGASコードをご紹介します。
これらのデータの活用方法も交えながら解説していきます!

2.コード

ではコードをどうぞ。
実行すると、シートA列に指名、B列に対応するメールアドレスがズラっと羅列されます。
シート名やアドレスはご自身の環境に合った内容に変更してくださいね。

function getResourcesAll(){

  const customerId = AdminDirectory.Users.get("あなたのメールアドレス").customerId;
  let pageToken = '';
  let result = [["名称","アドレス"]];

  do{
      let resources = AdminDirectory.Resources.Calendars.list(customerId, {pageToken: pageToken});
      let items = resources.items;

      for(let item of items){
        result.push([item.resourceName, item.resourceEmail]);
        /**
         * itemから取得可能なプロパティは以下の通り
         * resourceEmail
         * capacity
         * etags
         * resourceName
         * resourceCategory
         * generatedResourceName
         * resourceDescription
         * resourceId
         * buildingId
         * kind
         */
      }
      pageToken = resources.nextPageToken;      
    }while(pageToken);
  
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  sheet.clear();
  SpreadsheetApp.flush();
  sheet.getRange(1,1,result.length, result[0].length).setValues(result);
}

3.解説-前提

3-1.スクリプトエディタについて

まず、リストを載せるスプレッドシートを用意してください。
スプレッドシートの拡張機能タブからエディタを開いてください。
拡張機能タブからAppsScriptを選択

3-2.権限について

前回記事でユーザーリストを取得した時と同じ内容になります。

まず、AdminDirectoryを利用できるユーザーでないと実行できません。
GoogleWorkspaceの特権管理者だと実行可能ですが、
部分的に権限を持つことも可能です。
ここは社内の管理者に付与してもらうしかありません。

なぜなら、AdminDirectoryはリソース情報の編集など、影響の大きい行為を実行できるからです。
誰しもがそんな権限を行使できたら・・・恐ろしいですよね。

ということで、情シスの方に
「GASでAdminDirectoryっての使いたいんだけど、権限もらえる?」
と聞いてみてください。相手が管理者なら分かるはずです。

3-3.AdminDirectoryサービスについて

こちらも前回記事と同じですね。

スクリプトエディタの左側に「サービス」という項目があります。
こちらに「AdminDirectory」との表示がないと、このGASは動きません。

サービスの横の+ボタンをクリックし、
「Admin SDK API」を選択、追加ボタンをクリックしてください。
すると、サービスの欄に追加されます。
AdminSDKを追加

3-4.トリガーについて

日次更新したい場合、日次のトリガー設定を行ってください。
後でも解説しますが、毎回データを削除して再取得した内容に洗い替えるコードになっています。

4.解説-コード

4-1.customerIdについて

最初で定義するcustomerId、ご自身のメールアドレスから取得と書いていますが、
必要なカレンダーリソースをGoogleカレンダーに登録しているアカウントにしてください。

実際に実務で検証してみたところ、
そのアカウントがカレンダー上に表示可能なリソースとして登録している分だけを取得できるようです。

例えば会議室A,B,Cがあったとして、
自身の「マイカレンダー」「その他のカレンダー」にAとBしか登録していなかった場合、
customerIdに自身を設定するとCの情報は取得できません。
必要なカレンダーを予め「マイカレンダー」か「その他のカレンダー」に登録しておくか、
必要なカレンダーが登録されているアカウントをcustomerIdに設定してください。
「マイカレンダー」「その他のカレンダー」の設定方法はこちらです。

4-2.各リソースから取得可能な値について

for文内のコメントに記載していますが、
各カレンダーリソースから様々な情報を取得可能です。

例えばcapacity。
会議室に収容人数を設定していた場合、その値が取得できます。

他にもbuildingIdなど、活用すると拠点情報と紐づけできそうです。
例えばGASを使って空き状況を可視化したい場合などを想定すると、
拠点ごとに検索をする必要がありますから、ビルディングの設定は必須です。

取得する項目を変更する場合、冒頭で定義しているヘッダ項目名を変更するなど
二次元配列の列構成に気を付けてコードを変更してくださいね。

4-3.シートクリアについて

貼り付ける前にシート内容を「sheet.clear()」でクリアしています。
もしコードを書き替えて、差分情報だけ貼り付けしたい場合などは
この部分を削除してください。

5.活用方法のアイデア

様々な活用方法があると思います。

5-1.社員に見せるためのマスタ情報とする

「リソースのブラウジング」という機能があるので、
一般ユーザーが普通にカレンダー登録する際には特にマスタは必要ありません。

ですが例えばcapacity属性に登録した収容人数情報がシートに自動連携され、
一覧性のある状態になっていれば便利そうじゃないですか?

また各社員での開発が盛んな企業の場合、
リソースIDを公開しておくことにも意義があります。

5-2.空き会議室検索ツール用のマスタ

実務で作成しました。
開始時刻と終了時刻、対象の拠点を選択して、
その時間帯に空いている会議室を表示するアプリを開発できます。

Googleカレンダー上でも予定を作成する画面上で空き部屋を検索可能ですが、
「予定作成画面をわざわざ開かずに見たい」というユーザーには好評です。
まあ、UI/UXの好みの問題でしょうね。

5-3.リソース管理/更新用アプリを開発する

マスタ上に「ビルディング/リソースの作成/変更/削除」機能をGASで持たせましょう。
GoogleWorkspace上で操作しなくても、マスタを参照しながらマスタシート上で
管理/更新を一括管理するアプリを開発できます。

あまり手間をかけずに更に拡張するなら、
総務系の拠点管理担当からリソース作成等の依頼を飛ばせる依頼用UIを開発しましょう。
その依頼内容を以て、承認できるようなUIも作成。
承認したら作成等の処理とマスタ更新が走ります。
更新メニューを開く事のできるアカウントを制限するとより良いでしょう。
GASでのwebアプリ開発経験者にとっては、あまり難しくないと思います。

6.さいごに

いかがでしたでしょうか。
たかがカレンダーリソースですが、
固く運用するのであれば開発のし甲斐がありそうですよね。

ご参考になれば幸いです。ありがとうございました。

GASで全ユーザーのリストをスプレッドシートに日次取得する方法

1.はじめに

GoogleWorkspaceを使っている方で、
「当社ドメインのユーザーアドレスを全件取得したい!」
「ユーザーアドレスのマスタを日次更新したい!」

などと思っている方、居られるのではないでしょうか。

そんな方々へ、簡単コピペで実現できるGASコードをご紹介します。

2.GASコード

早速コードをどうぞ。
実行すると、シートA列に指名、B列に対応するメールアドレスがズラっと羅列されます。

function listAllUsers(){
  let pageToken;
  let page;
  let result = [];
  
  do {
    page = AdminDirectory.Users.list({
      domain: 'leverages.jp',
      orderBy: 'givenName',
      maxResults: 500,
      pageToken: pageToken
    });
    let users = page.users;
    if (users) {
      users.forEach(x=> result.push([x.name.fullName,x.primaryEmail]))
    }
    pageToken = page.nextPageToken;
  }while (pageToken)
  
  const sh = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  sh.clear();
  SpreadsheetApp.flush();
  const menu = ["氏名","アドレス"];
  result.unshift(menu);
  sh.getRange(1,1,result.length,result[0].length).setValues(result);      
  SpreadsheetApp.flush();
}

3.解説-前提

3-1.スクリプトエディタについて

まず、リストを載せるスプレッドシートを用意してください。
スプレッドシートの拡張機能タブからエディタを開いてください。
拡張機能タブからAppsScriptを選択

3-2.権限について

まず、AdminDirectoryを利用できるユーザーでないと実行できません。
GoogleWorkspaceの特権管理者だと実行可能ですが、
部分的に権限を持つことも可能です。
ここは社内の管理者に付与してもらうしかありません。

なぜそんな面倒か、と言いますと、
AdminDirectoryはユーザー情報の編集など、影響の大きい行為を実行できるからです。
誰しもがそんな権限を行使できたら・・・恐ろしいですよね。

ということで、情シスの方に
「GASでAdminDirectoryっての使いたいんだけど、権限もらえる?」
と聞いてみてください。相手が管理者なら分かるはずです。

3-3.AdminDirectoryサービスについて

スクリプトエディタの左側に「サービス」という項目があります。
こちらに「AdminDirectory」との表示がないと、このGASは動きません。

サービスの横の+ボタンをクリックし、
「Admin SDK API」を選択、追加ボタンをクリックしてください。
すると、サービスの欄に追加されます。
AdminSDKを追加

3-4.トリガーについて

日次更新したい場合、日次のトリガー設定を行ってください。
後でも解説しますが、毎回データを削除して再取得した内容に洗い替えるコードになっています。

4.解説-コード

4-1.上限突破について

メイン処理のAdminDirectory.Users.listですが、
1回の取得上限が500件までと制限されています。
ですので、do while文を使ってループしています。

page.nextPageTokenで次ページの存在を確認し、
存在確認が取れなくなるまで500件ずつ取得しています。
これで1000件2000件と、500件以上のユーザー数にも対応できます。

4-2.日次更新について

毎日更新することを前提にしているため、
ユーザーリストを取得して貼り付けする前に
シートのデータを全削除する、というコードになっています。

sh.clear() の部分で、シート情報を全てクリアしています。
もしコードを書き替えて、差分情報だけ貼り付けしたい場合などは
この部分を削除してください。

4-3.取得する情報の種類について

今回はA列に指名、B列にメールアドレスを取得しています。
ですが、GoogleWorkspaceからは他にも情報を取得できます。

例えば、nameでもfamilyNameを取得したり、
GoogleWorkspaceの管理者なのかどうかを取得したり、
エイリアスのアドレスを取得したり、です。

取得できる項目は公式ドキュメントにありますので、参照してみて下さい。

5.Usersの活用アイデア

AdminDirectory.Usersの活用アイデアを提案します。
実際に私の環境で使っているものもあります。

5-1.入退社リストと紐づける

入退社リストにアドレス列と処理状態列を作成します。
処理状態に「作成」「削除」をプルダウン入力可能にします。
プルダウン状態を取得し、作成/削除し状態列を作成完了/削除完了にするGASを開発します。
実行時にユーザーマスタシートも最新情報に更新します。

尚、同姓同名の場合にアドレス被りが発生するとエラーになりますので、
エラー時にはアドレス末尾に_1を付けて再実行、
それもダメだったら_2を付けて再度、等の工夫をすると良いでしょう。

5-2.Googleグループマスタと関連づける

メーリングリストとして活用できるGoogleグループ。そのマスタがあったとします。
GASでGoogleグループからメンバ取得する際、メールアドレスしか取得できません。
GASでユーザー名まで取得したい場合、adminDirectory.Usersの情報を紐づけましょう。

関数が得意な場合は、グループマスタとユーザーマスタを関数で紐づけても良さそうです。
件数が多い場合は処理が重くなるので、基本的にはGASでの日次取得がオススメです。
この方法は私も実務で活用しています。

5-3.管理権限等のマスタ及び操作ツールを開発する

取得したUserについて、様々な属性を付与または取得する事ができます。
例えばisAdminを取得すれば、そのアカウントが特権管理者かどうか分かります。
同様にarchivedを取得すれば、アーカイブ状態かどうかが分かります。

また、それらの情報は、updateメソッドによって更新可能です。
よって、GoogleWorkspace上のユーザー情報を管理、編集するための
GASによるwebアプリを開発できるという事になります。

取得可能な情報と実行可能なメソッドはこちら
ユーザー情報の更新についてはこちらに載っています。
Googleの公式ドキュメントです。

 

6.さいごに

いかがでしたでしょうか。
ぶわっと結果が表示された瞬間、ワクワクしませんか?
「何かのマスタがある」という状態は、様々な改善への第一歩となります。
ここから関数やGASを派生させて他の何かを管理するも自由です。
ぜひチャレンジしてみてください。

またチャレンジの過程で分からないことがあれば、コメントください。
ありがとうございました。

フォームのデータを取得できませんでした。のエラーに対処した話

2024/3/12 追記

強引に発生確率を下げる方法について、2024年の記事で解説しています。
両記事ともある程度有効な手段かと思いますので、あわせてご参照ください。
フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法

0.はじめに

2022年12月頃から、Googleフォームの送信時トリガーで
以下のようなエラーが発生し不安定な状態となっているようです。
フォームのデータを取得できませんでした。のエラー画像
私の環境でも発生していたため、
一助になれればと思い対応した方法を記載しておきます。
※エラーそのものを発生させなくする方法ではありません。

 

1.何が起こったのか

フォーム送信時トリガーを設定した時、
送信時作成のイベントから中身を取り出そうとする段階でエラーを吐きます。

エラーメッセージ:Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。

ただ毎回起こるわけではなく、不定期に発生するのが厄介です。

2.根本解決の方法はあるのか

調査したところ、根本的に当エラーが発生しなくする方法は見つけられませんでした。
なので一旦「エラーが起きても問題ない形」をご提案します。

 

 

3.即時リトライを試みる方法

3-1.コード

//フォーム送信時トリガーを設定
function formRes(e) {
  try{  //通常処理
    const itemresponses = e.response.getItemResponses(); //ここで対象のエラーが発生する 
    const address = e.response.getRespondentEmail();
    main(itemresponses, address);
  }catch(e){
    //エラー時
    console.log(e);

    //対象のフォームを取得 
    const form = FormApp.getActiveForm();
    //対象フォームの回答を過去分から全て取得する
    const allItemresponses = form.getResponses();
    //最新分は配列末尾に入っているため最新分を取得 
    /** 精度重視であれば、allItemresponsesの中身をgetTimestamp()して日次比較、最新分を取得する方がベターでしょう。*/ 
    const recentResponse = allItemresponses[allItemresponses.length-1]; 
    
    //recentResponseは、tryの中のe.responseと似た扱いができる
    const itemresponses = recentResponse.getItemResponses(); 
    const address = recentResponse.getRespondentEmail();
    main(itemresponses, address); 
  }
}               
                 
function main(itemresponses, address){
  //フォーム回答を使って行いたい処理 
  //メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
  console.log("main"); 
}

 

3-2.解説

エラーが起きるところをtryに入れ、該当のエラー発生時にcatchします。
catchしたら無理に送信時トリガーでのフォーム情報を使わず、
formオブジェクト.getResponses()で対象フォームの回答全件を取得します。
そこから最新の回答を絞り込み、本来処理に持ち込む形です。

 

3-3.デメリット

3-3-1.ほぼ同時に複数の回答があった場合の精度が保証できない

リトライ処理中に他の回答があった場合、
最新側を取得して処理に入ってしまう可能性があります。

 

3-3-2.即時でない

当然、本来のフォーム送信時トリガーよりは反応が遅くなってしまいます。

 

3-3-3.リトライ処理自体も失敗することがある

このコードをテストしている時、リトライ処理自体も1回エラーを吐きました。
Exception: Failed to retrieve form data. Please wait and try again.
失敗した直後のcatchでリトライ処理を試みると、
結構な確率でFormにアクセスする段階でエラーを吐く印象です。
「リトライ処理自体は失敗しても5回まで繰り返す」等の対策があった方が良さそうです。

 

3-4.類似の方法

似ている方法として、フォーム回答を蓄積するスプレッドシートにGASを仕込み、
シート更新に応じて最新データを取得、処理する方法があります。

デメリットとしては、フォームの設問内容を追加/削除した際、
過去の質問項目もシートに残るため、正確に情報を抜き出すことが難しそうです。
「回答があったよ!」等の情報だけが必要であれば、これで良いかもしれませんね。

4.日次で処理漏れを検知、再実行する方法

4.1コード

//フォーム送信時トリガーを設定
function formRes(e) {
  try{
    //通常処理
    const itemresponses = e.response.getItemResponses();
    const address = e.response.getRespondentEmail();
    main(itemresponses, address);

    //ログシートを取得できれば方法は何でもいい
    const logSheet = SpreadsheetApp.openById("SSのID").getSheetByName("フォーム後処理ログ");
    const lastRow = logSheet.getLastRow();
    const responseId = e.response.getId();
    const timeStamp = e.response.getTimestamp();

    //ログ書き込み
    logSheet.getRange(lastRow+1,1,1,2).setValues([timeStamp, responseId]);

  }catch(e){
    //失敗時 一応タイムリーにエラーキャッチしたいならメール飛ばす等を実装    
  }
}

function main(itemresponses, address){
  //フォーム回答を使って行いたい処理 メール送信するなりチャットツールに通知するなりデータ加工して二次利用するなり
  console.log("main");
}

//昨日分の履行をチェックする 日次トリガーにする
function dailyCheck(){
  //昨日のdayjsオブジェクト
  const yesterday = dayjs.dayjs().subtract(1,"day");

  //昨日の回答を全件取得する
  //ライブラリdayjsを使用しているが、timestampが昨日であるかを確認できれば方法は何でもいい
  const form = FormApp.openById("フォームID");
  const yesterdayResponses = form.getResponses()
                            .filter(x => dayjs.dayjs(x.getTimestamp()).isSame(yesterday,"day"));
  //昨日のフォーム回答が0件だったらここで処理終了
  if(yesterdayResponses.length==0) return;

  //ログシートからログを全件取得し、空白を除外、A列に入力されているtimestampが昨日のものを抽出
  const logSheet = SpreadsheetApp.openById("シートID").getSheetByName("フォーム後処理ログ");
  const logs = logSheet.getRange("A1:A").getValues().filter(x => x)
                .filter(x => dayjs.dayjs(x[0]).isSame(yesterday,"day"));
  
  //ログシートの情報と、フォームから直接取得した昨日の回答全件を照らし合わせ、
  //ログシートに記載のないものを抽出する=送信時にtryが失敗していたものを抽出
  const targets = yesterdayResponses.filter(x => !logs.find(y => y[1]==x.response.getId())); 
  if(targets.length == 0) return;

  //メイン処理を実行する
  for(let target of targets){
    let itemresponses = target.getItemResponses();
    let address = target.getRespondentEmail();
    main(itemresponses, address);
  }
}

 

4-2.解説

仕組み自体が少々まどろっこしいかもしれません。ゆっくり読んでください。

 

4-2-1.実行ログを取る

まず、本来の送信時トリガーでの処理時に必ず実行ログを取るようにします。
発動時に取得するイベントオブジェクト(e)から、
「回答ID」を取得してログシートに入力する仕組みです。
※シートはどこでもいいですが、回答記録されるSS上にシートを作っておくと管理上楽でしょう。

 

4-2-2.ログシートの内容と、フォーム回答全件を比較する

さて、そうすると、当該エラーを吐いた時には「回答ID」はログには残りません。
この状況を利用します。

毎日0時~1時に、対象フォームの昨日分回答を全件取得して、
その内容とログシートの内容が一致していれば、
エラーは発生していなかったという事になります。
逆に一致していなければ、ログ記録未実行=失敗しているものがあります。

コードと一緒に見ていきましょう。

 

4-2-3.実行が漏れていた分の処理を実行する

エラーで処理が実行されていなかった分は、本来処理を改めて実行しましょう。
実行するための回答情報は、前項で取得した未実行分の「回答ID」を使って取得します。
コードですと、この部分になります。
フォーム送信時トリガーと扱いが同じオブジェクトを取得できちゃいます。

4-3.メリット・デメリット

4-3-1.【メリット】即時リトライよりは精度が上がる

ほぼ同時に複数の回答があった場合の、回答の取り違えが発生しません。
これは項3で紹介した方法より良い部分ですね。

 

4-3-2.【デメリット】回答からのタイムラグが大きい

今回のケースですと、分かりやすく日次トリガーを設定して
前日分の動作に問題が無かったかを検知しますので、遅いです。
午前9時にエラーを吐いた場合、その回答への対応は翌日になります。

フォームの用途上、タイムラグが許されない場合は
数時間毎に検知できるように工夫してみたり、
エラー時にメール等で通知し日中帯は即時フォローできるようにするなど、
工夫をすればある程度カバーできる問題かと思います。

 

4-3-3.【デメリット】日次処理自体が失敗する可能性がある

項3の方法で触れましたが、こちらでも似たような処理を実行しているため
何度か実行を試みるような処理を追加した方が無難だと思います。

 

4-3-4.【デメリット】処理が煩雑になる

最後はやっぱりこれに尽きますね。地味に面倒くさい。
「修正してくれ~」と思ってしまいますね。
ですが、GASやSaaS系ツールはサーバー側の問題で不安定になりますから、
それも見越して厚めのエラー処理を当たり前に仕込むべきなのかもしれませんね。

 

5.さいごに

いかがでしたでしょうか。
私はというと、2つ目の方法を採用して日次で管理しています。

ちなみに私の環境では送信時トリガーのフォームを20個ほど管理していますが、
昨年12月中旬からポツポツとエラーが発生し、2023/1/16以来発生していません。
皆さんはいかがでしょうか?よければコメントください。
⇒2023年2月現在、やはりたまに発生するようです。

もしかすると解消されたのかもしれませんが、
Google側が不安定になると同様の事が起こるかもしれません。
時間がある際に対処しておく事をおすすめします。

また、強引にエラーハンドリングを行う方法として他でも使えるかもしれません。
参考にしていただけると幸いです。

Googleフォームでメール自動返信!GASとHTMLのコード実例をご紹介

1.はじめに

「Googleフォーム回答に対してカスタマイズした形で自動返信したい!」
そんな方へ向けてGASでの解決方法を解説した記事になります。
ぜひ参考にしてみてください。

2.作成方法

2-0.フォームを用意する

業務本番で使っているフォームにいきなり仕込むのは危険です。
本番フォームをコピーしたり、テスト用に新設して用意してください。

2-1.スクリプトエディタを開く

自動回答を仕込みたいGoogleフォームの編集画面上で、
スクリプトエディタを開いてください。
この画面にコードを書き込んでいきます。
  フォーム右上のメニューからスクリプトエディタを開く

2-2.gsファイルとhtmlファイルをエディタに作成する

今回はGAS用ファイル1つとhtmlファイル1つを使用します。
画面左側からファイルをスクリプトファイルとhtmlファイルを作成してください。
エディタ内でスクリプトファイルとHTMLファイルを作成する

2-2.コードを書く

今回私が作成したコードはこちらです。
後ほど細かく解説していきます。

//GASコード

/**
 * メイン処理
 * @param {object} e    イベントオブジェクト 質問文や回答内容を取り出せる
 */
function sendFormResponse(e){
  //フォーム回答情報をeから取得しitemresponsesに格納
  const itemResponses = e.response.getItemResponses();

  //フォームタイトルを取得
  const formTitle = FormApp.getActiveForm().getTitle();

  //回答者アドレスを収集するフォーム設定の場合、回答者アドレスを取得できる
  //収集せず固定宛先に送る場合はaddress="~~~~~~@~~~.~~"とする
  const address = e.response.getRespondentEmail();
  
  //メール設定を取得する
  const mailSettings = getMailSettings(itemResponses, formTitle, address);

  //sendMail関数を発動 メール設定を渡す
  sendMail(mailSettings);
}

/**
 * メール設定を作成
 * @param {object} itemResponses  フォーム情報
 * @param {string} formTitle    フォーム名称
 * @param {string} address     回答者メールアドレス
 * @return {object} {body:本文html, subject:メールタイトル文字列, mailTo:メールToアドレス文字列} 
 */
function getMailSettings(itemResponses, formTitle, address){
  //回答内容を「質問文:[改行][スペース]回答内容[改行]質問:・・・」となるようhtml文字列を生成
  const qaHtml = itemResponses.map(res => 
         `${res.getItem().getTitle()}:<br> ${res.getResponse()}`)
                 .join("<br><br>");

  //メール.htmlの内容に、qaHtmlとformTitleの内容を適用
  const mailBody = HtmlService.createHtmlOutputFromFile("メール.html").getContent()
                   .replace("@replace_qa@",qaHtml)
                   .replace("@replace_formtitle@", formTitle);

  const subject = `${formTitle}の回答を受け付けました。`;
  const mailTo = address;
  return {body:mailBody, subject:subject, mailTo:mailTo};
}

/**
 * メール送信
 * @param {object} mailSettings {body:本文html, subject:メールタイトル文字列, mailTo:メールToアドレス文字列}
 */
function sendMail(mailSettings){
  GmailApp.sendEmail(mailSettings.mailTo,mailSettings.subject,"",{htmlBody:mailSettings.body});
}
フォーム「@replace_formtitle@」の回答を受け付けました。<br>
後のご対応については〇〇にて回答いたします。<br>
回答まで今しばらくお待ちください。<br>
<br>
@replace_qa@
<br>
※このメールは自動返信です。<br>
3営業日以内に連絡がない場合、お手数ですが下記へお問い合わせください。<br>
問合せ先:〇〇〇@〇〇〇〇.jp

※コード内にもかなり細かく解説コメントを書いています。
邪魔になる場合はお手数ですが削除してください。

2-3.認証を通しておく

スクリプトがフォーム回答等にアクセスするので、
それを予めOKかどうか許可しておく必要があります。
一度、スクリプト起動して許可しましょう。
権限を許可します

2-4.トリガー設定

この設定を行うと、フォーム回答時にスクリプトが発動するようになります。
これで、フォーム回答すると自動的に反応するようになります。

画面左のメニューから「トリガー」を選択、
「トリガーを追加」をクリックします。

トリガーを作成します

メイン処理をフォーム送信時起動の設定にして保存しましょう。
メイン処理をフォーム回答時起動に設定して保存

3.コード解説

3-1.sendFormResponse(e)

3-1-0.この関数について

Googleフォーム回答時に自動で呼び出される関数です。

3-1-1.eについて

イベントの略称でeとしています。
「フォーム回答」というイベントが発生すると、
フォーム回答に関する情報が発生し、自動的にeに格納されると思ってください。

3-1-2.const itemResponses = e.response.getItemResponses();

eの中から各質問ごとの詳細を取り出します。
itemResponses[0].getItem().getTitle()とすれば1問目の質問文を、
itemResponses[0].getResponse()とすれば1問目の回答を得られます。
公式ドキュメント

3-1-3.const formTitle = FormApp.getActiveForm().getTitle();

フォームタイトルを取得します。
getActiveとありますが、このスクリプトと紐づいているフォームを取得します。

3-1-4.const address = e.response.getRespondentEmail();

eの中から、回答者のメールアドレスを取得します。
フォーム設定で回答者アドレスを記録する設定の場合のみ、有効です。
公式ドキュメント

3-2.getMailSettings(itemResponses, formTitle, address)

3-2-0.この関数について

メール設定情報を作成するための関数です。

3-2-1.const qaHtml = itemResponses.map…

質問と回答を、綺麗な文字列になるように処理しています。
brタグはhtmlで改行を意味します。

3-2-2.HtmlService.createHtmlOutputFromFile(“メール.html”).getContent()

メール.htmlの内容をテキストとして取得します。

3-2-3.replace(“@replace_qa@”,qaHtml)

メール.htmlの@replace_qa@部分を、qaHtmlに置き換えます。

3-2-4.return {body:mailBody, subject:subject, mailTo:mailTo}

呼び出し元のsendFormResponse関数へ情報を返します。
返った情報はmailSettingsに格納されます。

3-3.sendMail(mailSettings)

3-3-0.この関数について

mailSettingsを用いてメールを送信します。

3-3-1.GmailApp.sendEmail();

フォームのオーナーアカウントからGmailでメールを送信します。
()の中に宛先やタイトル、本文情報など必要情報を入力します。
公式:

4.まとめ

いかがでしたでしょうか。 GoogleフォームとGASは使いこなせばかなり業務で役立ちます。
ぜひコードを書いてみて、活用してみてください。

また今回のコードは、全くの初心者の方には少し難しく、
玄人の方は「こうした方がいい」と思うコードだと思います。
ご自身なりに試行錯誤しながらアレンジしてみてください。

今後も、初心者の方がコードを理解しやすいような記事や、
改善事例、コード例などを発信していきますのでチェックしてみてください。