Googleのグループを、自動でメンテするようにしたロボ

[ウェルネスをテクノロジーで届ける企業、TENTIAL](https://corp.tential.jp/)のCFO酒井です。
意外とこの手のスクリプト世の中になかったのでポストしておきます。

# Googleのグループって何？美味しいの？
昔で言うところのメーリスです。
ユーザーグループはリストの性質があり、グループをカレンダーやドライブ上のファイルに招待することで、グループ内のメンバーに動的に権限を付与できます。

- 入社したばっかのひと、全社イベントに招待するの忘れてた
- 異動の際に、ファイルの権限いちいち削除・付与するの面倒…

みたいな悩みを持っている人は、この記事おすすめです。

ちなみに動的グループっていうネイティブの機能もあるので、それで対応できないもので使うのがおすすめです。
https://support.google.com/a/answer/10286834?hl=ja

# 技術
GoogleAppsScript(GAS)でやりきります。
もちろん、Python等他言語・環境でも似たようなことはできますが、Googleの世界に閉じている今回のようなケースはGASによる実装ほぼ一択です。

AdminDirectory
AdminGroupsSettings
の両API/ライブラリを「サービス」としてインポートして活用しています。便利。

# 構成

注:GASの実行者は、グループの作成や調整ができるAdmin権限が必要です。
スプシはこんなかんじで用意して下さい。
https://docs.google.com/spreadsheets/d/1ZWB3aKAb4J9McSCrxXwCVqQOkiR3xTrVkx-89SrAOo0/edit?usp=sharing

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214098/1ae5e88e-031e-cbac-e273-a8aea4ff26ba.png)

Slack通知はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214098/08f6f0bf-1b83-7ddc-bef2-896a1ccf48c1.png)



# コード
※ 拡張子はほんとは.gs


```maintain.js
function try_(func, ...args) {
  try {
    return func(...args)
  } catch(e) {
    Logger.log(`name：　${e.name}`)
    Logger.log(`message：　${e.message}`)
  }
}

// Memberの削除および追加
function refreshMembers(members, groupEmail) {
  const curtMembers = try_(AdminDirectory.Members.list, groupEmail)
  if (!curtMembers) return
  const oldMembers = curtMembers.filter(cm => !members.find(m => cm.email === m))
  const newMembers = members.filter(m => !curtMembers.find(cm => m === cm.email))
  // Memberの削除
  oldMembers.forEach(om => {
    const resRemove = try_(AdminDirectory.Members.remove, groupEmail, om.email)
    if (resRemove) {
      notifySlack(`:wave::skin-tone-2:${om} を ${groupEmail} から削除したロボ…`)
      Logger.log(resRemove)
    }
  })
  // Memberの追加
  newMembers.forEach(nm => {
    const resInsert = try_(AdminDirectory.Members.insert, {email: nm}, groupEmail)
    if (resInsert) {
      notifySlack(`:warning:${nm} を ${groupEmail} に追加したロボ！`)
      Logger.log(resInsert)
    }
  })
}

function groupSettings(name) {
  // アクセスタイプの設定
  // https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups
  return {
    name: name,
    whoCanContactOwner: 'ALL_IN_DOMAIN_CAN_CONTACT', // オーナーに連絡 組織全体
    whoCanViewMembership: 'ALL_IN_DOMAIN_CAN_VIEW', // メンバーを表示 組織全体
    whoCanViewGroup: 'ALL_MEMBERS_CAN_VIEW', // トピックを表示 グループのメンバー
    whoCanPostMessage: 'ALL_MEMBERS_CAN_POST', // 投稿を公開 グループのメンバー
    archiveOnly: 'false',
    whoCanModerateMembers: 'OWNERS_AND_MANAGERS', // メンバーを管理 グループの管理者 
    whoCanJoin: 'INVITED_CAN_JOIN', // Candidates for membership can be invited to join.
    allowExternalMembers: 'false' // 組織外のメンバーの許可 いいえ
  }
} 

// Group作成
function insertGroup(email, name) {
  // グループ作成
  const resGroup = try_(AdminDirectory.Groups.insert, {email: email})
  if (resGroup) {
    notifySlack(`:loudspeaker:${email} ってグループを作ったロボ。`)
    Logger.log(resGroup)
  }
  // 設定更新
  const resSettings = try_(AdminGroupsSettings.Groups.patch, groupSettings(name), email)
  if (resGroup) {
    Logger.log(resSettings)
  }
}

function maintainGroups(sheet){
  const lastRow = sheet.getLastRow()
  const groupName = sheet.getRange('H1').getValue()
  // getValuesの戻り値の各要素は要素数1のListで格納されている
  const members = sheet.getRange(2, 1, lastRow - 1).getValues().map(m => m[0])
  const groupEmail = sheet.getSheetName()

  //membersのクレンジング
  insertGroup(groupEmail, groupName)
  refreshMembers(members, groupEmail)
}
  
function main(){
  const spreadsheet = SpreadsheetApp.openById([your-sheet-id])
  spreadsheet.getSheets().forEach(sht => {
    Logger.log(`===${sht.getName()}のメンテナンス開始===`)
    maintainGroups(sht)
    Logger.log('===終了===')
  })
}
```

```notify.js
function notifySlack(message) {
 const postUrl  = "[your-slack-webhook-url]"

 const userName = "メンテナーbot"   // Slackに通知する時の名前になります
 const icon     = ":robot_face:"    // 表示されるアイコン
 
 const jsonData = {
   "username" : userName,  // username(通知者の名前)
   "icon_emoji" : icon, // icon_emoji(アイコン)
   "text" : message  // text (送信するメッセージ)
 }  
 const payload = JSON.stringify(jsonData)
 const options = {
   "method" : "post",
   "contentType" : "application/json",
   "payload" : payload
 }

 UrlFetchApp.fetch(postUrl, options)
}
```

# もっとこうしたいな
- SmartHRで従業員の所属情報を充実化し、API叩いて人事異動にダイナミックに連動するようにしたい。
  - (2022-02-24追記)こちらの3部作で対応しました。https://qiita.com/piyoSakai/items/2982fb7511331e56f8fb
- Slackの実行結果通知が厳密でない。(ジョブが「正常に失敗」する可能性を考慮していない)。
- Already existsはシステム的にエラーは正常な実行結果。なのでcatchでreturnしていないが、ほんとはもっと丁寧に処理すべき。

# 良い点
- 便利
- メンテ結果がSlack通知されるので、変な権限追加等あったら気付ける
- bot語尾が可愛い。

# 宣伝
コーポレートでこういうことしたい方、当社はあなたにぴったりです。
話だけでもOKなので、気軽にTwitter( https://twitter.com/rsakaiii )にDMください。
ダイレクト応募も歓迎です、オープンポジションからどうぞ。
https://herp.careers/v1/tential/sImi2n5yLrP3

当社はテクノロジーに強みを持っているウェルネスD2C/EC企業です。
AltJSで開発してますので、JSが好き、型が好きなあなたにおすすめです。
僕と違って、型に厳格に開発しています。
求人一覧 → https://tential.v1.herp.cloud/ats/p/requisitions?sortedBy=NameAsc
CTO → https://twitter.com/Hey_icchiman