GAS

フォームのデータを取得できませんでした。に対処したもう1つのシンプルな方法

0.はじめに

以前、こんな記事を書きました。
「フォームのデータを取得できませんでした。のエラーに対処した話」

内容としては、おそらくGoogle側の問題が原因で発生するエラー、
Exception: フォームのデータを取得できませんでした。しばらくしてからもう一度お試しください。
に対処する方法についてでした。
GASのフォーム送信時トリガーで回答情報を取得しようとする際に起こるエラーですね。

ただ、エラーを防止する方法は無く、
エラーが起きても後から再実行できる方法を提案するものでした。

記事の投稿から時間が経ち、私が採用しているコードも変わりました。
相変わらずエラーを防止する方法はありませんが、
上記の記事はいまだに結構アクセスがありますので、
最新版をお届けしようと思います。

 

1.コード

// フォーム送信時トリガー
function main(e) {
  let datas = {status:false, msg:"初期値"};
  //フォームデータ取得を20回トライする
  for(let i = 1; i<=20; i++){
    datas = getFormDatas(e);
    if(datas.status) break;
    Utilities.sleep(3000);
  }
  if(!datas.status){
    //前回記事の方法やエラーメール送信などを書く
  };
}

//フォームデータの取得
function getFormDatas(e){
  try{
    // 必要な処理をする
    const formTitle = FormApp.getActiveForm().getTitle();
    const itemresponses = e.response.getItemResponses();
    const address = e.response.getRespondentEmail();
    const responseId = e.response.getId();
    return {status:true,formTitle:formTitle,itemresponses:itemresponses,address:address,category:category,responseId:responseId};
  }
  catch(er){
    console.log(er);
    return {status:false, msg:er.message};
  }
}

 

2.簡易解説

フォーム回答を取得する部分について、getFormDatasという関数にしています。
これを、成功するまで20回を上限に実行しています。ごくシンプルですね。
高速でループしてもサーバーエラーが解消しない限り無意味なので、
気分的にループ毎に3秒のwaitを入れています。
このwaitに本当に意味があるかは分かりません!

 

3.検証結果

このコードを書いた当時、SeleniumでGoogleフォームを1,500回、回答してテストしました。
※seleniumとは、ブラウザを自動操作するツールです。pythonやC#で使えます。
エラーが出ない時期もありますが、出る時期に行いました。
結果、2回以上のループに入るケースが2割ほどありましたが、
20ループを超えてエラーを吐くケースは1%未満に留まりました。

 

4.ループ回数は何回が適切なのか

ループしすぎて処理がタイムアウトにならなければいいのではないでしょうか。
あまり気にせず適当に決めていいと思いますが、
エラーが出る時期は5ループとか10ループだと結構突破してくる印象があります。

 

5.最後に

いかがでしたでしょうか。
単に試行回数を増やすという強引な対処ですが、
ランダム性のあるこの手のエラーには1つの有効な手立てとなります。
ぜひ、前回記事と合わせて活用してみてください。

AdminDirectoryでエイリアスを追加/削除する方法 – GAS

0.はじめに

エイリアスを操作しようと思っていざ調べてみると、
思いのほか情報が少なく、誤情報もあり困ったため、方法を共有します。

 

1.追加する方法


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
//AdminDirectory.Users.Aliases.insert({"alias":"エイリアスメールアドレス"}, "ユーザのプライマリメールアドレス");

 

2.削除する方法


AdminDirectory.Users.Aliases.remove("userMail@~~~~", "エイリアス@~~~~");
//AdminDirectory.Users.Aliases.remove("ユーザのプライマリメールアドレス", "エイリアスメールアドレス");

 

3.一括更新できないのか?

色々と検証してみたところ、ループを回すしか方法が無さそうです。
メソッド1発でできる方法があれば是非教えてください。

AdminDirectoryで管理者が自アカウントを変更すると403になる罠

0.はじめに

GoogleAppsScriptでAdminDirectory系のメソッドを実行した際に403が出て若干ハマったので共有します。

 

1.結論

結論:管理者アカウントを操作しようとしたからです。

 

通常、AdminDirectoryServiceはそれなりの権限がないと実行できません。
なぜならユーザー情報やグループ情報を変更したり削除したりできてしまうからです。
「運用中の通常ユーザーの情報は触りたくないから自分のアカウントで試すか~」
とやると、403が返ってきます。
あなたのアカウントは管理者アカウントではありませんか?
AdminユーザーでもAdminユーザーを変更する事ができないようです。


API call to directory.users.aliases.insert failed with error: Not Authorized to access this resource/api

「AdminDirectory 403」等で調べると、スコープや権限の話が出てきますが、
スコープやユーザー変更権限があっても実行できません。
「Groups.list()とかUsers.insert()とかできるのになんで?」となりますが、
管理者アカウントは操作できる対象ではありません。

 

2.ちなみに実行したコードはこちら

ちなみに私が実行したコードはこれでした。
自分のメールエイリアスを設定しようとしたものです。
Users.Aliasesの情報が少なく「何かコードがおかしいんだ」と悩みましたが、
変更対象アカウントを一般ユーザーにしたところ普通に動作しました。


AdminDirectory.Users.Aliases.insert({"alias":"~~~~@~~~~~"}, "userMail@~~~~");
AdminDirectory.Users.Aliases.remove(userKey, "userMail@~~~~");

GASでドライブに日毎、月毎のフォルダを一気に作成する方法【コピペでOK】

0.はじめに

ドライブに日毎のフォルダを一気に作成したい!
ということをGASで実現する記事です。

ちまちま作成している人はパパっとコピペして活用しちゃってください!

1.フォルダ作成のコード

//エディタ左からライブラリday.jsを追加してください。スクリプトIDは下記。
//1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
function main() {
  //親フォルダを定義
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  let monthFolder = "";

  //開始日と最終日を設定
  let targetDate = dayjs.dayjs("2023/1/1");
  let endDate = dayjs.dayjs("2024/12/31");

  //最終日を超えるまでtargetDateを加算しつつ繰り返し処理
  while(targetDate.isBefore(endDate.add(1,"day"),"day")){
    //日付が1日or月フォルダ未指定の場合はフォルダ作成(root内に当月フォルダが存在するか確認するとよりgood)
    if(targetDate.date()==1 || monthFolder==""){
      //月フォルダを作成
      monthFolder = rootFolder.createFolder(targetDate.format("YYYY/MM"));
    }
    //月フォルダ内に日フォルダを作成
    monthFolder.createFolder(targetDate.format("YYYY/MM/DD"));
    //targetDateに1日加算
    targetDate = targetDate.add(1,"day");
  }
}

コード内にコメントで書いている通り、day.jsを使用するコードになっています。
エディタ左側のライブラリのところから、追加してください。

day.jsのスクリプトIDはこちら
1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
day.js公式

実行結果としてはこんな感じになります。
↓↓↓

親フォルダ内に月フォルダが並び・・・

生成済みの月フォルダたち

月フォルダの中には日毎のフォルダが並びます。

月フォルダ内に日毎のフォルダが並ぶ画像

尚、注意点ですが、少し処理時間を要します。
なぜなら1つずつフォルダを順次作成していくからです。

上記の例、丸2年分を私の環境で実行すると、2分15秒かかりました。
手で作成するよりは断然速いですが、あまり大量に作成するとタイムアウトしますので
ある程度は区切って処理していくことをオススメします。

2.当日になったらフォルダを削除したい、そんな時のコード

作って使うだけなら楽ですが、管理上そうもいかない場合が多いです。
そんな時、日次で夜実行のトリガーを設定しておいて当日分を削除するようなコードがあれば便利ですよね。
それがこちらです。月末日の場合は月フォルダも削除します。


//本日のフォルダを削除
function deleteTodayFolder(){
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  const today = dayjs.dayjs();
  const monthFolder = rootFolder.getFoldersByName(today.format("YYYY/MM")).next();
  
  if(monthFolder){
    //月フォルダが見つかったら本日フォルダを検索し、見つかったら削除する
    let todayFolder = monthFolder.getFoldersByName(today.format("YYYY/MM/DD")).next();
    if(todayFolder) todayFolder.removeFolder();

    //翌日が1日だったら月末なので、月フォルダも削除する
    if(today.add(1,"day").date()==1) monthFolder.removeFolder();
  }
}

こちらもday.jsを使いますので事前に追加しておいてくださいね。

3.昨日分を削除したい方へのコード

別に当日分を23時~24時のトリガーで日次削除してもいいけど、
できれば厳密に日が変わってから昨日分を削除したい方はこちらをどうぞ。


//昨日のフォルダを削除
function deleteYesterdayFolder(){
  const rootFolder = DriveApp.getFolderById("親フォルダID");
  const yesterday = dayjs.dayjs().subtract(1,"day");
  const monthFolder = rootFolder.getFoldersByName(yesterday.format("YYYY/MM")).next();
  
  if(monthFolder){
    //月フォルダが見つかったら昨日フォルダを検索し、見つかったら削除する
    let yesterdayFolder = monthFolder.getFoldersByName(yesterday.format("YYYY/MM/DD")).next();
    if(yesterdayFolder) yesterdayFolder.removeFolder();

    //本日が1日であれば先月の月フォルダも削除する
    if(dayjs.dayjs().date()==1) monthFolder.removeFolder();
  }
}

こちらもday.jsです。便利ですね。
私が実務で使っているのも、このパターンです。

4.さいごに

いかかでしたでしょうか。結構シンプルなコードだと思いませんか?
これだけのコードですが、手で作業するとなると莫大な時間を要します。

ぜひGASを活用して効率化、高速化を実現してきましょう!
ご活用いただけると幸いです。それではまた。

GASで契約管理! 管理シートを自動参照&通知日に自動メール通知!

0.はじめに

期日管理、自動化していますか?

「期日の〇ヶ月前になったら通知する」等は定型的で非常に自動化しやすい部分です。
その一例としてシート例とコード例をご紹介します。
もし業務の補助になるなら、ぜひチャレンジしてみてください。

1.スプレッドシート

こんな構成にしました。

契約管理更新シートのサンプル画像

E2セルにはこんな関数が入っています。

=ARRAYFORMULA(IF((D2:D=””)+(F2:F=””)+(F2:F=”-“),””,EDATE(D2:D,F2:F*-1)))

IF文でD列が空白orF列が空白orF列が-の場合は空に、
それ以外の場合はD列からF列の月数をマイナスした日付が入ります。

GASを学ぶとつい全てコードでやってしまいそうになりますが、
関数を適切に活用することでコードがシンプルになり可読性や保守性が向上します。

2.コード

//最初にライブラリdayjsを追加しておくこと
//スクリプトIDは1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
//メイン処理
function alertContractMain() {
  const targets = getAlertTargets();
  if(targets.length == 0) return;

  for(let t of targets){
    let body = getHtmlBody(t);
    alert(t,body);
  }
}

//アラートメール送信
function alert(target,body){
  let mailArgs = {htmlBody : body};
  GmailApp.sendEmail(target.AlertTo, `契約更新通知 ${target.Title}`,"",mailArgs);
}

//HTMLメール本文を作成する
function getHtmlBody(target){
  const body = `下記、契約更新通知の時期となりましたため通知いたします。<br>
                <br>
                対象:${target.Title}<br>
                カテゴリ:${target.Type}<br>
                契約満了日:${dayjs.dayjs(target.EndDate).format("YYYY/MM/DD")}<br>
                自動更新有無:${target.AutoUpdate}<br>
                コメント:${target.Detail}<br>
                <br>
                必要に応じて更新検討および更新手続きをお願いします。`;
  return body;
}

//本日アラート日のものを返す
function getAlertTargets(){
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName("契約管理");
  const vals = sh.getRange("A2:H").getValues().filter(x => x[0]);
  const obj = vals.map(x => new Contract(x));

  return obj.filter(x => dayjs.dayjs(x.AlertDate).isSame(dayjs.dayjs(),"day"));
}

//SS情報をオブジェクト化するためのクラス
class Contract{
  constructor(row){
    this.Title      = row[0]; //項目名
    this.Type       = row[1]; //カテゴリ
    this.AutoUpdate = row[2]; //自動更新
    this.EndDate    = row[3]; //契約満了日
    this.AlertDate  = row[4]; //通知日
    this.AlertTiming= row[5]; //通知時期
    this.AlertTo    = row[6]; //通知先
    this.Detail     = row[7]; //詳細
  }
}

 

3.ポイント

3-1.day.js

ライブラリ、day.jsを使っています。
使わなくても日付比較はできますが、使うと非常に楽ですので使用をオススメします。

使い方は、エディタ左側のライブラリのところで「+」を押し
下記のスクリプトIDを入力、追加してください。

1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB
day.js公式

3-2.トリガー設定

通知日=本日のデータがないかチェックしてメール送信する仕組みですので、
毎日起動する必要があります。

業務都合に合うお好きな時間帯を日次設定してください。

3-3.テンプレートリテラルについて

htmlBodyを作成する際、変数の値と文字列を組み合わせて作成しています。
文字列と変数を+で結合しても問題ありません。
ただ、当コードで使用しているテンプレートリテラルを活用すると
途中で柔軟に改行できることから可読性と保守性が増す場合が多いです。

使用方法としては、まず文字列を ` で囲います。
変数や定数の値を使用したい場合は`の中で${変数名}と書くと展開されます。

今回のような長めの文字列を扱う場合、有効ですので是非この機会に慣れておいてください。

4.さいごに

結構シンプルですぐ作れる内容でありながら、
業務にはバッチリ貢献してくれそうな例だと思います。

期日管理系ですと類似のものでこんな記事もありますので興味があれば参照してみてください。
GASでシートの入社日や退職日に合わせて自動でメールを送信する事例

ご活用いただけますと幸いです。ありがとうございました。

GASでシートの入社日や退職日に合わせて自動でメールを送信する事例

0.はじめに

従業員の入社や退職に際して自動的に連絡をしたい、
そんな時に活用できる例をご紹介します。

1.一覧スプレッドシートについて

今回使用するスプレッドシートはこんな形式だとします。
列構成を変えたい場合などもコードを少し変更すれば対応可能ですので柔軟に考えてください。
1行に1名の情報が入っていると楽です。

入社管理スプレッドシート

 

2.設定シートについて

メールの宛先や件名、内容などを簡単に管理できるように
スプレッドシート上に設定情報を持たせておきます。

メール設定シート

これをやると簡単に設定変更が可能になり汎用的なコードに近づきますが、
必要に応じてシートの編集権限を絞る、等の工夫が必要な場合もあるかと思います。

3.コード

ではこれらの内容を取得して、送信に繋げていきます。

//最初にライブラリdayjsを追加しておくこと
//スクリプトIDは1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB

/**
 * メイン処理
 * 入社日が本日のものだけを対象にするため、日次トリガーを設定する必要がある
 */
function sendEntryMail() {
  //シート情報を取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const targets = getEntryDatas(ss);
  if(targets.length == 0) return; //本日入社が無ければ終了
  
  //メール設定を取得
  const mailSetting = getMailSetting(ss, "入社");
  
  //1人ずつ送信
  for(let person of targets){
    //送信設定情報を作成
    let mailArgs = { cc: mailSetting.Cc,
                     bcc: mailSetting.Bcc,
                     from: mailSetting.FromAddress,
                     name: mailSetting.FromName,
                     htmlBody: mailSetting.Body.replace(/%対象者%/g,person.Name) //置換
                    };
    //送信
    GmailApp.sendEmail(person.Mail,mailSetting.Subject,"",mailArgs);
  }
}

/**
 * 本日入社のデータを取得する
 * @param {object} ss スプレッドシートオブジェクト
 * @return {object} todayEntry 本日入社のデータ 無ければ空配列 
 */
function getEntryDatas(ss){
  //スプレッドシートデータを取得
  const sh = ss.getSheetByName("入社");
  const datas = sh.getRange("A2:G").getValues().filter(x => x[0]);
  const obj = datas.map(x => new Entry(x));
  
  //本日入社分にフィルタ
  const todayEntry = obj.filter(x => dayjs.dayjs(x.Date).isSame(dayjs.dayjs(),"day"));
  return todayEntry;
}

//入社者のデータを整理するためのクラス
class Entry{
  /**
   * @param {array} row 入社シート1行分の配列
   */
  constructor(row){
    this.Date = row[0];
    this.Number = row[1];
    this.Mail = row[2];
    this.Name = row[3];
    this.Div = row[4];
    this.Prace = row[5];
    this.Notes = row[6];
  }
}

/**
 * 引数で指定したメール設定を返す
 * @param {object} ss スプレッドシートオブジェクト
 * @param {string} type 設定シートのどの項目を取得するか文字列で指定
 * @return {object} typeに応じた設定object
 */
function getMailSetting(ss,type){
  const sh = ss.getSheetByName("メール設定");
  const datas = sh.getRange("A2:H").getValues().filter(x => x[0]);
  const obj = datas.map(x => new MailSetting(x));

  const targetObj = obj.find(x => x.Type == type);
  return targetObj;
}

//メール設定を整理するためのクラス
class MailSetting{
  /**
   * @param {array} row メール設定シート1行分の配列
   */
  constructor(row){
    this.Type        = row[0];
    this.Cc          = row[1];
    this.Bcc         = row[2];
    this.FromAddress = row[3];
    this.FromName    = row[4];
    this.Subject     = row[5];
    this.Body        = row[6];
    this.Notes       = row[7];
  }
}

ポイントをいくつか挙げていきます。

3-1.day.jsについて

ライブラリ、day.jsを使っています。
使わなくても日付比較はできますが、使うと非常に楽ですので使用をオススメします。

使い方は、エディタ左側のライブラリのところで「+」を押し
下記のスクリプトIDを入力、追加してください。

1ShsRhHc8tgPy5wGOzUvgEhOedJUQD53m-gd8lG2MOgs-dXC_aCZn9lFB

day.js公式

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

本日入社者がいないかチェックしてメール送信する仕組みですので、
毎日起動する必要があります。

社風によって適切な時間帯が異なると思いますので
お好きな時間帯を設定してください。

3-3.クラスとmapについて

スプレッドシート情報を取得した後、mapの中でnewしています。
各行の内容を用いてインスタンスオブジェクトを作成することで
効率的にキー付きの扱いやすいオブジェクトを作成できます。

非常に便利で、メイン処理を汚さないコードを書きやすくなります。
今回のコードはかなりシンプルで分かりやすいため、
クラスの理解がまだの方はじっくりと動きを確かめてみてください。

3-4.エラー処理について

この手の処理はあまりサーバー系のエラーが起きないので何もしていません。
が、こういう油断が足元をすくうのでtry catchで囲っておくべきです。

「サーバーエラーの場合try catchでも拾えないじゃない」と思う方は、
実行ログを作成して履行チェックを実施するGASを作成してください。
履行チェックGASは下記記事で使っていますので、よければ参照してみてください。
フォームのデータを取得できませんでした。のエラーに対処した話

4.さいごに

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

私の会社ではシート上にもっと情報が多く、処理項目も多いため
メール送信部分だけを切り出してご紹介しました。

日次に応じてひな形に沿った対応を行う、という業務は
非常に自動化しやすい業務ですので、見つけたらチャンスです。

ぜひ活用してみてください。

GASでGoogleグループのエイリアス一覧を取得するコードと解説

0.はじめに

前回記事、
GASでGoogleグループのメンバー一覧を取得するコードと解説
のエイリアスバージョンです。

Admin画面からエイリアス設定を参照するためには、
少し操作ステップが必要になります。
スプレッドシートに一覧化しておけば参照が楽になり、
また編集履歴から古い情報も確認可能になります。

今回もドメイン名とシート名以外はコピペで使えます。
ぜひ参考にして、活用してみてください。

1.コード

function getGroupListAll(){  
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sh = ss.getSheetByName('test');

  let output = ([["グループ名", "アドレス", "エイリアス"]]);
  let domainList = ["ドメインを入力","管理ドメインが複数ある場合は続けて入力"];
  //ドメインに@は不要

  domainList.forEach(x=> pushArray(output,getGroupInfo(x)));
  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 g of groupsList.groups){
        let groupName = g.name//グループ名
        let groupMail = g.email//メールアドレス
        
        let nonEditable = g.nonEditableAliases; //自動付与設定されているエイリアス
        let editable = g.aliases; //自動付与以外に個別設定したエイリアス
        let concatedAliase = concatAliase(nonEditable, editable); //結合

        let forPush = [groupName,groupMail];
        result.push(forPush.concat(concatedAliase));
        }
      }
      token = groupsList.nextPageToken
    }while(token)
  return result
}


//エイリアス配列を必要に応じて結合して返す
function concatAliase(nonEditable, editable){
  if(nonEditable && editable){
    return nonEditable.concat(editable);
  }else if(nonEditable && !editable){
    return nonEditable;
  }else if(!nonEditable && editable){
    return editable;
  }else{
    return [];
  }
}


//破壊的に二次元配列に二次元配列を結合する
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('');
    }
  }
}

 

2.出力結果

こんな感じでスプレッドシートに出力されます。

エイリアスリスト取得結果

それぞれのエイリアス設定件数に応じて横に伸びていく形になります。

3.解説

3-1.AdminDirectory

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

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

3-2.構成

以下のような流れになっています。

ヘッダ、ドメインリストを作成

グループ情報を取得する中でエイリアス情報も一緒に取得
└2種類のエイリアスを結合

ヘッダにグループ情報を結合

行ごとの長さが異なるジャグ配列なので、これを貼り付け可能な二次元配列に整形

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

3-3.ポイント

3-3-1.取得上限

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

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

3-3-2.nonEditableAliaseとaliaseについて

エイリアスには2種類あります。
アカウント作成と同時に固定的に生成するエイリアスを設定している場合、
そのエイリアスはnonEditableAliaseとして扱われます。

尚、nonEditableAliaseもaliaseも0件の場合はundefinedが返ってきますので、
concatAliase()でundefinedを除外して結合するようにしています。

4.活用方法

オススメの活用方法は「日次トリガーで参照用マスタにする」です。
Admin画面からエイリアス設定を参照するのは少し手間がかかりますし、
Admin画面を参照できる権限者にしかできない事です。

毎日深夜に更新されるよう設定しておけば工数ゼロなので、是非どうぞ。

5.さいごに

エイリアス参照が楽になる方法をお伝えしてきました。

今回の方法は、エイリアスを大量に設定していない限りは
グループメンバー一覧化と異なり情報量が少ないと思うので
タイムアウトやセル数上限についても気にしなくて良いかと思います。

ぜひ気軽に導入してみてくださいね。

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でGoogleドキュメントを効率化!書き込む時の重要操作方法一覧!

0.はじめに

GAS活用ではシートやドライブ、フォームにスポットが当たりがちですが
Googleドキュメントも扱えます。

基本的な書き込みや改ページ、書式操作などをご紹介します。
また活用事例も簡単にご紹介します。

1.最初にbodyを取得

必ず最初にこの手順を行ってください。

基本的にここで取得したbodyに対して変更を重ねることになります。
また上書き保存や改ページはdocに対して実行します。

const doc = DocumentApp.openById(docid);
let body = doc.getBody();

2.ドキュメント内容を削除する

1つのドキュメントを繰り返しリセットして編集する場合、処理の頭で実行しておきましょう。

body.clear();

3.末尾に行を追加

ドキュメント末尾にテキストを追加します。

body.appendParagraph("追記テキスト");

4.右揃えや中央揃えを設定する

//右揃え
body.appendParagraph("追記テキスト").setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
//左揃え
body.appendParagraph("追記テキスト").setAlignment(DocumentApp.HorizontalAlignment.LEFT);

//中央揃え
body.appendParagraph("追記テキスト").setAlignment(DocumentApp.HorizontalAlignment.CENTER);

5.文字サイズを設定する

body.appendParagraph("追記テキスト").editAsText().setFontSize('14');

6.太字・下線を設定する

//太字
body.appendParagraph("追記テキスト").editAsText().setBold(true);
//下線
body.appendParagraph("追記テキスト").editAsText().setUnderline(true);

7.文字色・背景色を設定する

他のメソッドと違って引数が複数あるので注意です。
開始位置、終了位置、色コードを設定します。

//文字色 1文字目~3文字目を黄色に
body.appendParagraph("text").editAsText().setForegroundColor(0,2,"#ffff00");

//背景色 2文字目~5文字目を赤背景に
body.appendParagraph("text").editAsText().setBackgroundColor(1,4,"#e60033");

8.ドキュメントに改ページを挿入する

項1で定義したドキュメントオブジェクトに対して実行します。

body.appendPageBreak();

9.ドキュメントを保存して閉じる

編集が終わった後に保存する必要がある場合、実行してください。

doc.saveAndClose()

10.メソッドチェーン

「太字にしてサイズ変えて右揃えにして・・・」を一気に書けます。

const text = "テストですよ";
body.appendParagraph(text)
         .setAlignment(DocumentApp.HorizontalAlignment.RIGHT)
         .editAsText().setFontSize('18')
         .editAsText().setForegroundColor(0,text.length-1,"#e60033")
         .editAsText().setBackgroundColor(0,text.length-1,"#ffff00")
         .editAsText().setBold(true)
         .editAsText().setUnderline(true);

11.番外:PDF保存

作成したドキュメントをPDFとしてドライブに保存します。
スプレッドシートをシート指定してPDF化するより安定します。

//ドキュメントをPDF形式でエクスポートするURL
const url = `https://docs.google.com/document/d/${ドキュメントのID}/export?format=pdf`;

//Access Tokenを取得
const token = ScriptApp.getOAuthToken();
    
//PDF形式でドライブに保存
const res = UrlFetchApp.fetch(url, {headers: {'Authorization': 'Bearer ' +  token}}).getBlob().setName("保存PDF名称.pdf");
const folder = DriveApp.getFolderById("保存先フォルダID");
const pdfFile = folder.createFile(res);

12.活用例

  • 安定したPDF作成の仕組みを作る(SS⇒PDF作成より安定する)
  • 定型フォーマットのDocを作成するGASを作り、可変部分は変数にしておき
    その時々で必要な値を入力した定型のドキュメントをワンクリックで作成可能にする

私も実務で1つ目のパターンを活用しています。
スプレッドシートからシート指定でPDF作成すると本当に安定しないのでこれで代用しています。

13.さいごに

ドキュメント作成に関するGoogle公式リファレンスはこちらです。
もっと細かく設定したい方は、公式情報も参照しつつ色々試してみてください!

GASでAsanaAPIを使ってタスクを自動的に作成するためのコード

0.はじめに

タスク管理ツールAsanaに対して
GASからAPIを叩いてタスク作成する方法をご紹介します。

1.コードサンプル

まずコードをどうぞ。

/**
 * asanaAPIでタスク登録
 */
function test(){
  //個人アクセストークンの取得方法はこちらhttps://asana.com/ja/guide/help/api/api
  const headers = {
    "Authorization": "Bearer " + "実行するアカウントの個人アクセストークン"
  };

  //Asanaに登録する基本的な内容 他の項目や詳細はこちらのcreate-a-task https://developers.asana.com/docs/create-a-task
  const payload = {
    "data":{
        "workspace"  : "ワークスペースのID",             //ワークスペースID
        "projects"   : ["プロジェクトID"],               //プロジェクトのURLにあります
        "name"       : "title",                         //タスクタイトル
        "assignee"   : "担当者メールアドレス",            //タスク担当者メールアドレス
        "html_notes" : "body",                    //htmlで本文作成したい場合bodyタグで括る必要あり
        "due_on"     : "2023-01-23",                    //タスク期日 YYYY-MM-DD形式
        "followers"  : ["~~~~@ドメイン.jp"]            //コラボレーターアドレス 配列でいいはず 組織外アドレスは追加できなさそうです
    }
  };

  const options = {
      "method"              : "post",
      "contentType"         : "application/json",
      "headers"             : headers,
      "payload"             : JSON.stringify(payload),
      "muteHttpExceptions"  : true
  };

  //タスク生成
  const response = UrlFetchApp.fetch("https://app.asana.com/api/1.0/tasks", options);  
  
  //これで実行結果が取れます
  console.log(response.getContentText());

  //実行結果からタスクIDを取得、タスクURLを作成 このURLをSlackに飛ばすなり何なり
  const taskId  = JSON.parse(response).data.gid;
  const taskUrl  = "https://app.asana.com/0/" + "プロジェクトID" +  "/" + taskId;
}

2.事前取得すべき情報について

いくつか事前に情報を取得する必要があります。

2-1.AsanaワークスペースIDの取得

契約している会社ごとのIDの事です。コードの最初の方で使っていますね。
ご自身の会社のIDを取得する必要があります。

Asanaにログインして、その状態で下記のリンクに飛んでください。
https://app.asana.com/api/1.0/workspaces

そうするとシンプルなテキストだけのページが表示されます。
その中のgidがワークスペースIDになります。

2-2.アクセストークンの取得方法

こちらは下記のリンクを参照しながら値を取得してみてください。
アクセストークンの取得方法

2-3.リクエストやレスポンスについて

リクエストの細かい設定方法やレスポンス内容の公式リファレンスはこちらです。
create-a-taskに関する公式情報

3.注意すべき点

上記のリンクやコードを細かく見た方はお分かりになると思いますが、
htmlでタスクbodyを作成する時のルールや、
組織外のアドレスを追加できない仕様など細かい条件があります。

上手くいかない場合、その辺りの条件が正しいか公式情報を確認してみてください。

4.活用アイデア

Googleフォームで依頼を受けて自動的にタスク化する

Asanaというとプロジェクト管理等で使われれる印象です。
が、このサイトはバックオフィスがテーマなので、バックオフィス寄りの提案です。

Googleフォームの回答があった際にGASを自動発動できるので、
その設定を利用してGASでフォームの情報をAsanaに飛ばします。
フォームからは回答者のメールアドレスや回答内容を取得可能なので、
回答者をコラボレーターに追加し、自部署担当者をAsanaタスク担当者に設定しましょう。

依頼種別ごとに複数のGoogleフォームを作成し、
内容によってタスク担当者やコラボレーターが変わるようにすると更に活用範囲が広がります。

フォームの種類があまりにも多い場合、それぞれのフォーム内のGASに対して
担当者情報等をハードコードするのはオススメできません。
担当者が変更になる時のメンテナンスコストが非常に高くなるからです。
どのフォームに誰を紐づけるか管理するスプレッドシートを用意し、
全てのフォーム⇒AsanaのGASがそのシートを参照してからタスクを作成する。
そんな仕組みにすると良いでしょう。

5.おわりに

フォームからAsanaに飛ばしたり、シート情報を元に生成したり、
GASで作成したwebアプリからタスク生成したり、色々できそうですよね。

Saasツールはこういう機能をフル活用してこそ、効果が上がると感じます。
ご活用いただけると幸いです。