Google Apps ScriptでContactsをPeople APIで弄る【GAS】

Androidの連絡帳でもあり、メールの送信先管理でも使うGoogle Contact。これを扱うGoogle Apps ScriptのクラスがContactsAppだったのですが、これ2022年1月19日に廃止されていて現在は非推奨となっています。またSunset Scheduleにも記載が追加されており、今更これで構築するのはオススメできません。

現在はPeople APIへと置き換えられているようですが、必ずしもContactsAppと同じことができるわけでもないようなので、今回このAPIについて調べてみました。

図:連絡先を弄るためのAPIです

今回使用するスプレッドシート等

概要

People APIはAdvanced Serviceから有効化してから使う仕組みなのですが、Google Apps Scriptから使うリファレンスが充実しておらず、ContactsAppで構築していた人は結構書き直すのに苦労するのではないかなと思います。

Google Contactsについて

また、AdminのDirectoryも表示はされていますが、iPhoneなどから接続して参照するのは連絡先の項目だけなので、ここをきちんと整備しておかないと、組織で使うiPhoneの電話帳リストが古いままということになります。

また、この連絡帳はGoogle WorkspaceのDirectoryとはリンクしていないので、管理者が整備しておいて皆が参照するといった使い方が出来ません。個別に作業をして入れておく必要があります。1つのマスターを参照して連絡帳を構築したい場合はLDAPやCalDav、icsファイルなどを使う必要があります。

今回のファイルは各個人が最新の連絡先情報として更新させる為に使うものになります。

APIの独特の仕様

今回のAPIを利用するに当たって独特の2つのポイントがあります。

1つはetag。これは手動自動問わず編集をすると変わると特性があり、これを書き込み時に指定をしないと更新が出来ず、値が現在のetagの値と一致してない場合エラーになります。一覧取得で値を取り出し即座に編集時に利用します。

2つ目はリソースネーム。people/meで自分自身を表すリソースネームとなります。これは各人のメールアドレスと一対になっているもので、これを持って連絡帳の対象者を特定しています。メアドを変更するとリソースネームの値は変わってしまいますが、それ以外を編集しても変化しません。各個人に割り当てられてるリソースネームは重複しないユニークな値になっています。

このようにちょっと今までのAPIと違った特性があるので、ここを意識しないとうまく実装することが出来ません。

People APIの制限

People APIの既定のリミットは以下のような感じ。GCPの対象のAPIの割当に記述があります。結構わかりにくい。引っかかりそうなくらい大量にリクエストを送るような場合は、適度にSleepを入れて回避する必要がありそうですが、課金されるようなAPIではないので、超えても429エラーになるだけです。

右側にある「割当の編集」を開いて割当の増加を申し込むをクリックすると、Google Formが開いて上限引き上げを申し込むことも可能です。デフォルト値が1分辺り180件なので、300名いるような組織ならば2分にわけて更新するようなウェイトで十分行けるでしょう。

※このQuotaの値が組織によって異なるようです。自分は180だけれど別の組織だと90だったり・・・

図:ここにQuotaの記述があります。

図:割当上限引き上げが可能

iPhoneの連絡先として使う

iPhoneの連絡先は電話アプリを開き、連絡先を開くと色々と連絡先リストが出てきます。ここでGoogle Contactsの連絡先を追加して利用することが可能です。利用する手順は以下の通り。今回のスクリプトで更新するとこの中身が入れ替わる仕組みです。Androidの場合は標準のGoogle Contacts(連絡帳)というアプリが入ってるので、使用時には既にリンクされています。

  1. 設定アプリを開き、連絡先を開く
  2. アカウントをタップする
  3. アカウントを追加をタップする
  4. 「Google」をタップする
  5. GoogleアカウントにログインをするとそのアカウントのContactsとリンクします
  6. 2.の画面に戻って追加されたGmailのアカウントを開き、連絡先のスイッチがオンになってるか確認
  7. 電話アプリを起動し、連絡先を開く
  8. Gmailが出てくるので開いてみる
  9. Google Contactsのリストがズラっと出てくる

図:連絡先にアカウントを追加する

図:電話アプリからContactsのリストを見る

図:Androidの場合

事前準備

本APIを利用する為には、GCP側でAPIをオンにして、尚且Google Apps Script側でもappsscript.jsonの編集が必要です。

GCP側の準備

こちらの作業はGCP側でモニタをしたり、外部メンバーを交えてのテストなどでは必要になりますが、通常の利用では行う必要はありません

GASのプロジェクトを紐付けする

外部のメンバー参加の場合には、Google Apps ScriptとCloud Consoleのプロジェクトを紐付けする作業が必要です。以下の手順でプロジェクトの変更を行います。前述のプロジェクトの控えておいた番号を利用します。そうでない場合はプロジェクト移動をせずとも利用が出来ました(内部メンバーのみの場合)。

  1. Cloud Console側のプロジェクトのホームを開き、「プロジェクト番号」を控えておく
  2. Google Apps Scriptのスクリプトエディタを開く
  3. サイドバーからプロジェクト設定を開く
  4. GCPプロジェクトの「プロジェクトを変更」をクリック
  5. GCPのプロジェクト番号に、1.の番号を入力して、プロジェクトを設定をクリック
  6. これで紐付けが完了しました。

図:プロジェクトの移動も必須の作業です

APIを有効にする

Google Cloud Console側でもAPIを有効化する必要性があります。

  1. GCPのプロジェクトを開く
  2. 左サイドバーからAPIとサービスにて、「APIとサービスの有効化」をクリックする
  3. peopleと検索すると出てくるので、クリックします。
  4. 有効化をクリックします。
  5. 認証情報の作成は不要です

図:有効化をしておくだけでOK

GAS側の準備

appsscript.jsonに記述を追加する

スクリプトエディタの左サイドバーから「プロジェクト設定」を開き、「appsscript.json」マニフェスト ファイルをエディタで表示するにチェックを入れて、appsscript.jsonを表示する。その後そのファイルを開き、以下のように記述を行います。必須の作業です。

{
        ...
       "oauthScopes": [
              "https://www.googleapis.com/auth/contacts",
              "https://www.googleapis.com/auth/spreadsheets",
              "https://www.googleapis.com/auth/userinfo.profile"
       ],
       ...
}

People APIで使うScopeを上記のように指定する必要があります。これを指定しておかないと403エラー Permission Denied等のエラーが発生してしまいます。

サービスの追加

スクリプトエディタのサイドバーにある「サービス」の横の+ボタンを押して、リストから「Peopleapi」を探し出して追加する必要があります。これにより、GASからPeople APIが利用可能になります。

図:これを追加しておく必要性があります。

初回認証をしておく

ここまでの準備で適当な関数を用意して実行すると認証が始まります。連絡先の表示、編集、ダウンロード、完全な削除というスコープが出てくるはずです。

図:認証を実行するとこの画面が出る

ソースコード

通常の単発の取得、新規追加、更新、削除のコードを使っています。また一括でそれらを行うBatchGetだけ今回のサンプルに入れてありますが更新や追加、削除にもBatchのメソッドがあり、速度を稼ぐ場合に利用しますが制限があります。

スプシのデータで更新し無ければ追加する

スプレッドシートを基準にしてループで1件ずつ更新します。連絡帳に無い連絡先は新規に追加するようにしある場合には更新をします。ただし今回はスプレッドシートに無いデータを連絡帳から削除するといったコードは追加していません。削除してはならない個別の連絡帳を手動で追加してるケースも有り得るので、例えば退職者の連絡先を消したい場合には、retireシートに記録しておいて、後述の連絡先の削除を実行すると良いでしょう。

スプレッドシートとWebアプリ側からのアクセスで処理を分岐する用にargmanの引数を付けています。

//スプレッドシート情報を元に連絡帳のアプデと削除を行う
function contactUpdate(argman = 1){
  //現在の一覧をまず取得しておく
  let array = personskun();

  //スプレッドシートデータを取得する
  if(argman == 1){
    var ui = SpreadsheetApp.getUi();
  }else{
    //何もつけない
  }
  
  let ssid = PropertiesService.getScriptProperties().getProperty("sheetid")
  let sheet = SpreadsheetApp.openById(ssid)
  let meibo = sheet.getSheetByName("contacts").getRange("A2:E").getValues();

  //エラー捕捉
  try{
    //スプレッドシートのデータをループで走査
    for(let i = 0;i<meibo.length;i++){
      //追加フラグを用意
      let addflg = true;

      //レコードを取り出す
      let rec = meibo[i];

      //空データの場合はスルーする
      if(rec[0] == "" || rec[0] == undefined){
        continue;
      }

      //氏名を取得する
      let username = rec[1];

      //電話番号と部署を取り出す
      let telnum = rec[2];
      let busyo = rec[3];

      //追加ラベルを取得
      let cGroup = rec[4]

      //メールアドレスを取得
      let mailman = rec[0];

      //メアドでarrayの中身を調査
      for(let j = 0;j<array.length;j++){
        //レコードを一個取り出す
        let recs = array[j];

        //メールアドレスを取得
        let tempmail = recs[0];

        //一致するものがあったら処理をする
        if(tempmail == mailman){
          //etagとresourcenameを取得する
          let resname = recs[1];
          let etag = recs[2];

          //電話番号と部署で相違がある場合だけアップデート
          let temptel = recs[3];
          let tempbusyo = recs[4];

          if(temptel == telnum){
            if(busyo == tempbusyo){
              //更新せずにスルー
              addflg = false;
              break;
            }
          }

          //リクエストボディを構築する
          var resource = {
            "etag": etag,
            "organizations": {
              'name': busyo
            },
            "phoneNumbers": [{
              'value': telnum
            }],
          };

          //コンタクトを更新
          People.People.updateContact(resource,resname,{updatePersonFields: "organizations,phoneNumbers"});

          //Sleep処理
          Utilities.sleep(50)

          //追加フラグを変更
          addflg = false;
        }
      }

      //追加フラグが立っていたら新規追加
      if(addflg == true){
        //ラベル指定で連絡先を新規追加
        createContact(username,telnum,mailman,busyo,cGroup);
      }
    }

    if(argman == 1){
      //終了処理
      ui.alert("連絡帳のリストを更新しました。");
    }else{
      //返す
      return 0;
    }

  }catch(e){
    //エラーメッセージを返す
    return e.message;
  }
}
  • personkunでまずは一覧でetagと個人のリソースネーム、メアド一覧を取得しておきます。
  • スプレッドシートのメアドと一致するものがあった場合には、リクエスト内容構築します。この時リクエストには必ずetagを指定する必要があります。この値は編集後に別の値に変わる特性があるので使いまわしは出来ません。
  • そして、People.updateContactにて、リクエストと相手のリソース名、更新する場所をupdatePersonFieldsで指定します。
  • 電話番号と部署だけ更新するので、差異がなければ処理をスルーするようにしています(Quota回避と速度向上の為

単一の連絡先をラベル付きで新規追加する

指定のラベルで連絡帳に新規追加します。指定のラベルが無い場合にはラベルも作成するという仕様になっています。今回は名前、電話番号、メアド、所属組織でメンバーを新規追加しています。

usernameは半角スペースで区切られた姓名を入れておく必要があり、半角スペースで区切りそれぞれを、givenName(名前)、familyName(姓)に分けて入れるようにしています。

※ただし新規追加はとても遅いのでBatchで追加するか?初回だけはCSVでインポートのほうが望ましいです(6分の制限を超えてしまう可能性が)。

//単一の連絡先を作成する
function createContact(username,telnum,mailman,busyo,cGroup) {
  //usernameを半角スペースで分解する
  let userman = username.split(" ");

  //作成するリソースの中身
  var contactResource = {
    "names": [{
      "displayNameLastFirst": username,
      "givenName": userman[1],
      "familyName": userman[0],
    }],
    "phoneNumbers": [{
      'value': telnum
    }],
    "emailAddresses": [{
      'value': mailman
    }],
    "organizations": {
      'name': busyo
    },
  }
  var contactResourceName = People.People.createContact(contactResource)["resourceName"];

  //コンタクトグループ(ラベル)を指定
  var groupName = cGroup;
  var groups = People.ContactGroups.list()["contactGroups"];
  var group = groups.find(group => group["name"] === groupName);

  //グループ名が無い場合には作成する
  if (!group) {
    var groupResource = {
      contactGroup: {
        name: groupName
      }
    }
    group = People.ContactGroups.create(groupResource);
  }
  var groupResourceName = group["resourceName"];

  //コンタクトリストに追加する
  var membersResource = {
    "resourceNamesToAdd": [
      contactResourceName
    ]
  }
  People.ContactGroups.Members.modify(membersResource, groupResourceName); 

  //sleep処理
  Utilities.sleep(200)

  return "OK"; 
}

バッチリクエストで一括インポート

個別で1件1件ループで登録は非常に遅いです。とても数分で終わらないので、こういった場合にはバッチリクエストでcreateContactを実行出来ます。500件ほどで試してみましたが、非常に高速で追加が可能です。

ただし1度にバッチで送り込めるのは200人分までなので、送信するデータが200人を超える場合には複数回に分割してバッチリクエストをする必要があります。バッチ結果からresourceNameを取り出してcontactGroups.members.modifyでラベルをつければ完璧です。

※また圧倒的にリクエスト回数を減らせるのでQuota回避が容易になるので、必須のテクニックです。

図:一括インポートはバッチが必須

//BatchCreateで一括インポート
//arrayはシートデータの2次元配列をそのまま入れておく
function batchContactImport(array){
  //レコード格納用
  let record = [];

  //インポートデータを作成する
  for(var i = 0;i<array.length;i++){
    //レコードデータを取得する
    let rec = array[i];

    //一時配列を用意する
    let temparr = {};

    //名前データを分割する
    let tempname = rec[1];
    let username = tempname.split(" ");

    //リクエストデータを連想配列で作成
    temparr.contactPerson = {
      emailAddresses: [{ value: rec[0]}],
      names: [
        { familyName: username[0], givenName: username[1] },
      ],
      "phoneNumbers": [{
        'value': rec[2]
      }],
      "emailAddresses": [{
        'value': rec[0]
      }],
      "organizations": {
        'name': rec[3]
      }
    }

    //配列に追加
    record.push(temparr);
  }

  //200件で割ってリクエスト回数を計算
  var loopman = Math.ceil(record.length / 200);

  //リクエストを投げる
  for(var j = 0;j<loopman;j++){
    //200件分取り出す
    temparray = record.splice(0,200);

    //リクエストボディを作成する
    let obj = {
      contacts: temparray,
      readMask: "emailAddresses,names,phoneNumbers,organizations"
    };

    //BatchCreate Request
    let response = People.People.batchCreateContacts(obj)["createdPeople"];
    
    //resourceNameを抽出してラベルを付ける
    let resarr = []
    for(var k = 0;k<response.length;k++){
      resarr.push(response[k].requestedResourceName);
    }

    //コンタクトグループ(ラベル)を指定
    var groupName = "通常社内";
    var groups = People.ContactGroups.list()["contactGroups"];
    var group = groups.find(group => group["name"] === groupName);

    //グループ名が無い場合には作成する
    if (!group) {
      var groupResource = {
        contactGroup: {
          name: groupName
        }
      }
      group = People.ContactGroups.create(groupResource);
    }
    var groupResourceName = group["resourceName"];

    //コンタクトリストに追加する
    var membersResource = {
      "resourceNamesToAdd": [
        resarr
      ]
    }
    People.ContactGroups.Members.modify(membersResource, groupResourceName); 
  }

  //処理終了
  return 0;
}

バッチリクエストで一括アップデート

前述のコードは一括インポートですが、次は一括アップデート。同様にQuotaの回避や実行時間の削減の為にバッチリクエストで連絡帳のアップデートを高速に行うことが出来ます。

ただし1度にバッチで更新できるのは200人分までなので、送信するデータが200人を超える場合には複数回に分割してバッチリクエストをする必要があります。またリクエストボディの作り方がちょっと特殊で、おまけにetagの値も必要な上に、前述のようにcontactsに入れるのは配列なのではなく、resourceNameをキーに使ったオブジェクトとして構築しなければならないので、かなり厄介です。

※バッチインポートとバッチアップデートの2つを使えば高速に追加と更新が出来るようになるので、このテクニックも必須です。

//バッチアップデート
function contactUpdate2(argman = 1){
  //現在の一覧をまず取得しておく
  let array = personskun();

  //0件の場合処理を終了
  if(array.length == 0){
    return 0;
  }

  //スプレッドシートデータを取得する
  if(argman == 1){
    var ui = SpreadsheetApp.getUi();
  }else{
    //何もつけない
  }
  
  let ssid = PropertiesService.getScriptProperties().getProperty("sheetid")
  let sheet = SpreadsheetApp.openById(ssid)
  let meibo = sheet.getSheetByName("contacts").getRange("A2:E").getValues();

  //エラー捕捉
  let addarr = [];

  //スプレッドシートのデータをループで走査
  for(let i = 0;i<meibo.length;i++){
    //レコードを取り出す
    let rec = meibo[i];

    //空データの場合はスルーする
    if(rec[0] == "" || rec[0] == undefined){
      continue;
    }

    //電話番号と部署を取り出す
    let telnum = rec[2];
    let busyo = rec[3];

    //メールアドレスを取得
    let mailman = rec[0];

    //メアドでarrayの中身を調査
    for(let j = 0;j<array.length;j++){
      //レコードを一個取り出す
      let recs = array[j];

      //メールアドレスを取得
      let tempmail = recs[0];

      //一致するものがあったら処理をする
      if(tempmail == mailman){
        //etagとresourcenameを取得する
        let resname = recs[1];
        let etag = recs[2];

        //電話番号と部署で相違がある場合だけアップデート
        let temptel = recs[3];
        let tempbusyo = recs[4];

        if(temptel == telnum){
          if(busyo == tempbusyo){
            //更新せずにスルー
            addflg = false;
            break;
          }
        }

        //resnameとetagをpushする
        rec.push(resname);
        rec.push(etag)
        
        //更新対象レコードをpushする
        addarr.push(rec)
      }
    }
  }

  //addarrが0件の場合はbatchUpdateはスルー
  if(addarr.length == 0){
    //何もしないでスルーする
  }else{
    //バッチインポートを実行
    let res = batchContactUpdate(addarr);
  }

  if(argman == 1){
    //終了処理
    ui.alert("連絡帳のリストを更新しました。");
  }else{
    //返す
    return 0;
  }
}

//バッチリクエストで更新する
function batchContactUpdate(array){
  //200件で割ってリクエスト回数を計算
  var loopman = Math.ceil(array.length / 200);

  //カウンターを用意
  var counter = 0;

  //レコード格納用
  var obj = {};

  //インポートデータを作成する
  for(var i = 0;i<loopman;i++){
    //オブジェクトを初期化
    obj = {};

    //200個ずつ切り出す
    for(var z = 0;z<200;z++){
      //レコードデータを取得する
      let rec = array[counter];
      
      //値がなくなったらループを抜ける
      if(rec == undefined){
        break;
      }

      //名前データを分割する
      let tempname = rec[1];
      let username = tempname.split(" ");

      //リクエストデータを連想配列で作成
      let temparr = {
        "etag": rec[6],
        "organizations": {
          'name': rec[3]
        },
        "phoneNumbers": [{
          'value': rec[2]
        }],
      }
      
      //リソース名をキーにオブジェクトとしてobjへ追加する
      let key = rec[5];
      obj[key] = temparr;

      //カウンターを回す
      counter = counter + 1;
    }

    //リクエストボディを作成する
    let obj2 = {
      contacts: obj,
      updateMask: "phoneNumbers,organizations",
      readMask: "phoneNumbers,organizations",
    };

    //アップデートを実行
    let res = People.People.batchUpdateContacts(obj2);

  }

  //処理終了
  return 0;
}

ちなみに、リクエストボディは実際に構築すると以下のようなスタイルになります。people/7575757といったユーザのリソースネームをキーにして、複数contactsに加えていくスタイルなので、配列ではない点をobj[key] = temparrで実現しています。こうすることで、変数をキー名に使って連想配列を連結していくことが可能です。

{ contacts: 
   { 'people/c75757575757575757': 
      { etag: '%EgoBAgkLDC43PT4/GgQBAgUHIgxKMTczZ0xQNHRHOD0=',
        organizations: [Object],
        phoneNumbers: [Object] } },
  updateMask: 'phoneNumbers,organizations',
  readMask: 'phoneNumbers,organizations' }

連絡帳のメンバー一覧を取得する

連絡帳のメンバーを全取得します。ただし仕様上各データにはラベルに関するデータがなく、ラベルは別管理扱いとなっているようです。1ターンで取れる人数は最大で1000までです。それを超える場合にはpageTokenで次のページを指定することで次の1000名が取れるという仕様です。

今回は名前、メアド、所属組織を取得します(personFieldsに指定してる内容がそれに該当する)。

連絡先が0件の場合にそなえて、その場合空の配列を返すように加えてあります。

//連絡帳の一覧を取得する
function personskun(){
  //pageTokenの変数
  let pageToken = null

  //一覧用の配列
  let array = [];

  do{
    //一覧を取得
    let people = People.People.Connections.list('people/me', {
        personFields: 'names,emailAddresses,organizations,phoneNumbers',
        pageSize: 500,
        pageToken: pageToken
    });

    //リストを取得する
    let target = people.connections;

    //連絡先が0件の場合
    if(target == undefined){
      return array;
    }

    //ターゲットデータを処理する
    for(var i = 0;i<target.length;i++){
      //レコードを取り出す
      let rec = target[i];

      //一時配列を用意
      let temparr = [];

      //emailAddressesが無い場合はスルーする
      let mailaddress = ""
      try{
        mailaddress = rec.emailAddresses[0].value
      }catch(e){
        mailaddress = ""
      }

      //一覧から必要な項目を取得する
      temparr.push(mailaddress);
      temparr.push(rec.resourceName);
      temparr.push(rec.etag);
      temparr.push(rec.phoneNumbers[0].value);
      temparr.push(rec.organizations[0].name);


      //一覧用配列に追加
      array.push(temparr);
    }

    //nextPageTokenを取得する
    pageToken = people.nextPageToken

  }while(pageToken !== undefined);

  //一覧データを返す
  return array
}

その他のコード

自分自身のリソースネームを知る

通常はpeople/meで自分自身のリソースネームを意味します。ですが、あえて指定したい場合には自分自身のリソースネームを知りたい場合があります。其の場合には、以下のような形でコードを記述します。personFieldsで指定した領域の特定の値も合わせて取得が可能です。

//自分自身のpeople/resourcenameを取得する
function getmyresname(){
  const people = People.People.get('people/me', {
    personFields: 'names,emailAddresses'
  });

  return people.resourceName;
}

連絡先を削除する

単発で1名分の連絡先を削除したい場合は、対象者のリソースネームを調べてから以下のようなコードを実行すれば、その1名だけ削除することが可能です。

//コンタクトをDeleteする
function deleteContact(resname){
  People.People.deleteContact("people/" + resname)
}

特定のラベルのメンバーをバッチで取得

特定のラベルをつけたメンバーをまとめて取得します。一括で取得する場合BatchGetというのを利用するのですが、これが使い勝手が悪く最大200名までしか取得出来ませんし、nextPageTokenといったものもありません。People.ContactGroups.getでまずガッツリ取得して、そこから200人ずつをPeople.People.getBatchGetのresourceNamesに突っ込んで順番に取る必要があります。

また、この際にラベルのリソースネーム(例:contactGroups/00011110000ff)を指定する必要があるのですが、この値はラベルをクリックした時のURLに含まれています。

図:グループリソースネームの場所

//特定のコンタクトグループからリストをBatchGetで取得
function getConnections() {
  var group = People.ContactGroups.get('contactGroups/ここにグループリソースネーム', {
    maxMembers: 300
  });

  //BatchGetは最大200名までしか取得できない
  var group_contacts = People.People.getBatchGet({
    resourceNames: group.memberResourceNames,
    personFields: "emailAddresses"
  });

  console.log(group_contacts)
}

特定のラベルのメンバーをバッチで削除

前述の特定のラベルのメンバーをバッチで取得するをさらに応用して、そのラベル内のメンバーを一括で削除する方法になります。削除はなぜか1度にバッチで500件まで削除が可能です。続けて、特定ラベル付きでメンバーを追加すればメンバー入れ替え更新という形が出来ます。

//バッチでメンバーを削除する
function batchContactDelete(){
  try{
    var groupName = "組織アドレス";
    var groups = People.ContactGroups.list()["contactGroups"];
    var group = groups.find(group => group["name"] === groupName);

    //リソース名を取得する
    var resname = group.resourceName;

    //特定リソースの情報を取得
    var groupman = People.ContactGroups.get(resname, {
      maxMembers: 500
    });

    //取得したリストをもとにbatchDeleteする
    var deleteman = People.People.batchDeleteContacts({
      resourceNames: groupman.memberResourceNames,
    })
  }catch(e){
    console.log(e.message)
  }finally{
    return 0;
  }
}

ウェブUI

スプレッドシートから実行する分には、スプレッドシートにアクセスしてる人(社内の人間)の権限で動作することになるので、各々の連絡帳に対して処理が実行されます。しかしこれをウェブアプリケーション上のボタンから実行させたい場合は以下の点に注意が必要です。

  1. 次のユーザとして実行では、デプロイする場合はウェブアプリケーションにアクセスしているユーザで設定
  2. アクセスできるユーザは組織内のユーザとして指定する
  3. 組織外のユーザに使わせたい場合、GCPのOAuth同意画面のテストユーザにアドレスを追加する必要があります(場合によってはGCPのプロジェクトユーザとして追加が必要な場合も)
  4. 内部メンバーだけの場合にはOAuthにメンバー追加などをする必要はありません。

ウェブ側のソースコードは以下のようなスタイルです。

<!DOCTYPE html>
<html>
<head>
  <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">

  <!-- Vue-TooltipとVueLoaderのCSS-->
  <?!= HtmlService.createHtmlOutputFromFile('vueloading').getContent(); ?>

  <script>
    //処理が完了したら報告
    function onSuccess(data){
      //ローダー停止
      vm.isLoading = false;

      //エラー処理
      if(data == 0){
        //メッセージ表示
        snackman("更新完了しました。");
      }else{
        //エラーメッセージ表示
        snackman(data);
      }
    }

    //スナックバー表示
    function snackman(msg){
      vm.text = msg;
      vm.snackbar = true;
    }
    
  </script>

</head>
<body>
  <v-app id="app">
    <v-row align="center" justify="space-around">
      <v-btn depressed color="primary" @Click="onStart">
        連絡帳の更新
      </v-btn>
    </v-row>

    <v-dialog v-model="dialog">
      <!-- カードで整形する -->
      <v-card>
        <v-card-title class="headline">
          最新の連絡帳データを反映しますか?
        </v-card-title>
        <v-card-text>実行すると既にあるデータは更新され、不足してるデータは追加されます。</v-card-text>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn
            color="blue darken-1"
            text
            @click="onApprove"
          >
            実行
          </v-btn>
          <v-btn
            color="red darken-1"
            text
            @click="onReject"
          >
            キャンセル
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <!-- ローディングサークル -->
    <loading
      :active.sync="isLoading"
      :can-cancel="true"
      :on-cancel="onCancel"
      :is-full-page="fullPage">
    </loading>

    <!-- メッセージ Toast-->
    <v-snackbar v-model="snackbar" :timeout="timeout">
      {{ text }}
      <template v-slot:action="{ attrs }">
        <v-btn color="blue"	text v-bind="attrs" @click="snackbar = false">
          閉じる
        </v-btn>
      </template>
    </v-snackbar>
  </v-app>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/vue-loading-overlay@3"></script>

  <script>
    let vm = "";

    //VueLoadingを組み込む
    Vue.use(VueLoading)
    const Loading = window.VueLoading;

    vm = new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      data () {
        return {
          dialog: false,
          isLoading: false,
					fullPage: true,
          snackbar:false,
          timeout:-1,
          text:"",
        }
      },
      methods:{
        //確認ダイアログ
        onStart(){
          this.dialog = true;
        },

        //ローダーキャンセル時処理
        onCancel:function() {
          let self = this;  
          self.isLoading = false;
				},

        onApprove(){
          //ローダーを開始
          this.isLoading = true;

          //GAS側のコードを実行する
          google.script.run.withSuccessHandler(onSuccess).contactUpdate(0);
          this.dialog = false;  //dialogを閉じる
        },
        onReject(){
          alert("却下しました");
          this.dialog = false; //dialogを閉じる
        },        
      },
      components: {
        "loading":Loading
      },
    })
  </script>
</body>
</html>

図:各人の権限で実行するようにデプロイ

図:社外の人に同時に使わせるには色々手順が必要

図:シンプルにボタン1個だけ

Directory APIで実現する

概要と注意点

People APIは組織に登録されている「人」に関するデータの入出力を行うことが可能ですが、Admin SDKにも元々Directory APIという似たようなAPIが用意されています。管理者権限で実行する必要がありますが、People API同様にAdmin Consoleに登録されてるユーザに関するデータを取得が可能です。

しかし、1点問題があります

そのユーザの画像サムネイルへのURLが取得出来るのですが、この値のURLにアクセスするとリダイレクトされて別のURLに移動した後に画像が表示されます。この値をHTMLの画像のURLに使うと表示されないことがあります。しかし、リダイレクト先のURLを取得出来ない為、これを利用して画像を表示が難しいです(其の場合、リダイレクトするとこの画像が返ってきたりします)。

よって、Directory APIを使った場合には、user.id(People APIで言うところのuserのresourceName)を取得しこれを元にPeople APIに投げて情報を再取得すると、こちらは「リダイレクト後のサムネイルへのURLが取得可能」なので、こちらを利用します。People APIでも十分情報は取れるので、ユーザ情報はDirectory APIではなく、People APIを利用しましょう。

※いっそのこと、people.listDirectoryPeopleを使ってDirectoryのlistをPeople APIで取得しちゃうのもアリかも。この場合連絡帳にアップしてるユーザのリストではなくDirectoryにアクセスして取ってきてくれます。

図:Directoryに登録されてるユーザ情報を取得出来る

ソースコード

Directory APIを使った場合のユーザ情報の取得について記述しています。user.thumbnailPhotoUrlは使えないURLが返ってくるので、そこだけPeople APIにて別にサムネイル画像へのURLを取得させています。

function getDirectoryUser(domain){
  let pageToken = null;

  //ユーザリストを取得する
  do {
    //リクエスト
    page = AdminDirectory.Users.list({
      domain: domain,
      orderBy: 'givenName',
      maxResults: 500,
      pageToken: pageToken
    });

    //取得したユーザリストが空の場合
    const users = page.users;
    if (!users) {
      console.log('ユーザがいませんでした。');
      return;
    }

    //ユーザ情報を書き出し
    var array = [];
    for (const user of users) {
      //エラートラップ
      let phonenum = "";
      try{
        phonenum = user.phones[0].value
      }catch(e){
        phonenum = "";
      }

      let orgvalue = "";
      try{
        orgvalue = user.organizations[0].department;
        console.log(orgvalue)
      }catch(e){
        orgvalue = "";
      }
      
      //thumbnailPhotoUrlだとリダイレクトしてしまい画像が取れないので
      let photos = "";
      if(user.thumbnailPhotoUrl == "" || user.thumbnailPhotoUrl == undefined){
        //何もしない
      }else{
        //People APIから画像のURLを取得する
        photos = getmyresname(user.id)
      }

      //一時配列
      let temparr = [
        user.id,
        user.primaryEmail,
        user.name.fullName,
        orgvalue,
        String(phonenum),
        photos,
        user.isAdmin
      ]

      //書き出し用配列に追加
      array.push(temparr);
    }

    //次のトークンを取得
    pageToken = page.nextPageToken;

  } while (pageToken);

  //取得したデータを表示
  console.log(array)
}  
  
 //自分自身のpeople/resourcenameを取得する
function getmyresname(uid){
  let photourl = "";

  try{
    const people = People.People.get('people/' + uid, {
      personFields: 'photos'
    });

    photourl = people.photos[0].url;
  }catch(e){

  }

  return photourl;
}

People APIでディレクトリにアクセス

通常、People APIは連絡帳のデータにアクセスする為のものであり、ディレクトリにアクセスする為のAPIではないのですが、Admin SDKを使わずにディレクトリのユーザデータも実は取ることが可能なので、Directory APIを使わずにメンバーのデータを取得することが可能です。それが、People.listDirectoryPeople

利用する場合には、appsscript.jsonのoauthScopesに対して「"https://www.googleapis.com/auth/directory.readonly"」を追加する必要があります。

実際に取得するソースコードは以下の通りで、1回のリクエストで最大1000人まで取得可能。それ以上はpageTokenで次の1000名を取得するスタイルです。

//DirectoryからPeople APIで取得する
function getDirectoryPeople(){
  //pageTokenの変数
  let pageToken = null

  do{
    let src = 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'
    let people = People.People.listDirectoryPeople({
      readMask:'names,emailAddresses,phoneNumbers,organizations',
      pageSize:1000,
      sources:src,
    })

    console.log(JSON.stringify(people))

    //nextPageTokenを取得する
    pageToken = people.nextPageToken

  }while(pageToken !== undefined);

}

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)