Slackのユーザーグループにユーザーを一括登録するGoogle Spreadsheet

# はじめに
Google Spreadsheetに記載したユーザーを
一気にユーザーグループに登録するGASを作ってみました。
以下のような機能を持たせています。

+ シートに記入したユーザー一覧を指定したユーザーグループに一括登録する
+ 1ファイルで1ユーザーグループを管理
+ 参加させるユーザーの特定にはメールアドレスを使う
+ あらかじめユーザーグループ自体は作成済みであることを前提とする


# 概要
Slack APIの[usergroups.users.update](https://api.slack.com/methods/usergroups.users.update)でユーザーグループ所属ユーザーの登録ができます。
このAPIにはユーザーグループのIDとユーザーのIDが必要であるため、
それぞれ[usergroups.list](https://api.slack.com/methods/usergroups.list)と[users.lookupByEmail](https://api.slack.com/methods/users.lookupByEmail)でIDを検索します。

これらのAPIは、SlackのアプリのTokenを使って実行します。

# Slackの準備
[Slack apiのYour Appsのページ](https://api.slack.com/apps)からSlackアプリを作成します。
各apiのドキュメントに必要な権限が記載されているので、それぞれ必要な権限を付与します。
今回の場合ですと以下になります。

+ usergroups:write 
+ usergroups:read 
+ users:read.email 

作成したらWorkspaceにインストールします。
後で使うため、**OAuth Access Token** をCopyします。

# Spreadsheet
CONFIG, usersの2つのシートを用意します。

## CONFIGシート  
更新対象のユーザーグループを指定するシート。
![CONFIG.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436247/0305a43a-a843-650b-d0e0-9960a5d98762.png)
英語名欄にユーザーグループの英語名を入力して、更新ボタンを押すと、
usersシートに記載されたユーザーが登録される。という動作です。

更新ボタンにはスクリプトのmain()を割り当てます。
割り当て方法は[こちら](https://www.acrovision.jp/service/gas/?p=269)を参照。

何らかのエラーが発生すると、実行結果欄にエラーメッセージが表示されます。

## usersシート  
登録対象のユーザー一覧のシート。
![users.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436247/d5ffed55-b0b8-2329-0013-96c11ade5ede.png)
API(usergroups.users.update)の仕様上、差分更新ではなく、このリスト上のユーザーにDel&Insされます。
各ユーザーのメールアドレスからSlackIDを探すのですが、見つからなかった場合などはエラー欄にメッセージが表示されます。


# スクリプト

<details><summary>コード.gs</summary><div>

```Javascript
const SPREADSHEET = SpreadsheetApp.getActiveSpreadsheet();
const SHEET_USERS = SPREADSHEET.getSheetByName('users');
const SHEET_CONFIG = SPREADSHEET.getSheetByName('CONFIG');
const CELL_USER_GROUP_NAME = SHEET_CONFIG.getRange(1, 2); // Group名
const CELL_USER_GROUP_ID = SHEET_CONFIG.getRange(2, 2);   // Group ID
const CELL_TOKEN = SHEET_CONFIG.getRange(3, 2);           // Token
const CELL_LAST_TIME = SHEET_CONFIG.getRange(4, 2);       // 最終実行時刻
const CELL_RESULT = SHEET_CONFIG.getRange(5, 2);          // 処理結果
const CELL_PROCESSING_SEC = SHEET_CONFIG.getRange(6, 2);  // 処理時間
const IND_NAME = 0;
const IND_EMAIL = 1;
const IND_USER_ID = 2;
const IND_ERROR = 3;

const TOKEN = CELL_TOKEN.getValue();
const SLACK_API_URL = "https://slack.com/api/";
const API_USERGROUPS_LIST = SLACK_API_URL + "usergroups.list";
const API_USERS_LOOKUPBYEMAIL = SLACK_API_URL + "users.lookupByEmail";
const API_USERGROUPS_USERS_UPDATE = SLACK_API_URL + "usergroups.users.update";


let start_time = new Date();

/** UserGroupIDを求める */
function getUserGroupID() {
  console.log("START getUserGroupID");
  let usergroup_name = CELL_USER_GROUP_NAME.getValue();
  let usergroup_id = CELL_USER_GROUP_ID.getValue();

  // GroupIDがわかっている場合はこのまま終了
  if (usergroup_id != ""){
    return usergroup_id
  }
  // 全UserGroupを取得して、そこから該当のものを探す
  let params = {'method': 'post', 'payload': {'token': TOKEN}};
  let resjson = callApi(API_USERGROUPS_LIST, params);
  if (resjson == {}) {return "";}
  let usergroups = resjson.usergroups;
  
  let hit_usergroup = usergroups.find((v) => v.handle == usergroup_name);
  if (hit_usergroup === undefined){
    throw new Error(":" + usergroup_name + "は見つかりませんでした。登録済みか確認してください");
  }
  console.log(hit_usergroup);
  
  // 取得結果を記入
  CELL_USER_GROUP_ID.setValue(hit_usergroup['id']);
  
  return hit_usergroup['id'];
}


/** カンマ区切りのUserIDリストを返す */
function getUserIDs(usergroup_id){
  console.log("START getUserIDs");
  let error_flg = false;
  let user_id_list = [];
  // 現状を取得
  let last_row = SHEET_USERS.getLastRow();
  let user_infos = SHEET_USERS.getRange(2, 1, last_row - 1, 4).getValues();
  console.log(user_infos);

  // UserIDが不明なものはメアドから検索
  let requests = [];
  user_infos.forEach(function(user_info){
    console.log(user_info);
    // user_idがわかっているものは飛ばす
    if(user_info[IND_USER_ID] == ""){
      requests.push(genRequest(user_info[IND_EMAIL]));
    }
  });
  let responses = UrlFetchApp.fetchAll(requests);
  
  // 検索結果をもとに全SlackIDを求める
  let i = 0;
  user_infos.forEach(function(user_info){
    console.log(user_info);
    // user_idがわかっているものはそのまま追加
    if(user_info[IND_USER_ID] == ""){
      let response = JSON.parse(responses[i].getContentText("UTF-8"));
      console.log(response);
      if (response['ok'] == true) {
        console.log(response['user']['id'] + " " + response['user']['name'] + ' ' + response['user']['profile']['email']);
        user_info[IND_USER_ID] = response['user']['id'];
      } else {
        user_info[IND_ERROR] = response['error'];
        error_flg = true;
      }
      i++;
    }
    user_id_list.push(user_info[IND_USER_ID]);
  });
  console.log(user_infos);

  // 書き込み
  SHEET_USERS.getRange(2, 1, last_row - 1, 4).setValues(user_infos);
  
  if (error_flg) {
    throw new Error(':UserID特定処理にてエラーが発生しました')
  }
  return user_id_list.join(',');

}


/** fetchAllで投げるためのAPI_USERS_LOOKUPBYEMAILリクエストを生成 */
function genRequest(email){
  let request = {
    'url': API_USERS_LOOKUPBYEMAIL + "?token=" + TOKEN + "&email=" +  email,
    'contentType': 'application/x-www-form-urlencoded',
    'method': 'get'
  }
  return request;
}


/** ユーザーグループの更新 */
function updateUserGroup(usergroup_id, user_id_list) {
  console.log("START updateUserGroup");
  console.log(usergroup_id +"," + user_id_list);
  
  let payload = {
    'token': TOKEN,
    'usergroup': usergroup_id,
    'users': user_id_list,
  }
  let params = {'method': 'post', 'contentType': 'application/x-www-form-urlencoded', 'payload': payload};
  let response = callApi(API_USERGROUPS_USERS_UPDATE, params);
  console.log(response);
}


/** SlackAPIの呼び出し */
function callApi(url, params){
  let response = UrlFetchApp.fetch(url, params);
  let resjson = JSON.parse(response);
  console.log(resjson);
  if (resjson.ok != true) {
    console.log('エラー：' + resjson['error']);
    throw new Error(resjson['error']);
  }
  return resjson;
}


/** 処理結果の書き込み */
function writeResult(message, isEnd) {
  // 処理自体終了の場合は処理時間なども記入
  if(isEnd){
    let end_time = new Date();
    let last_date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
    let processing_sec = (end_time - start_time)/1000;
    CELL_LAST_TIME.setValue(last_date);
    CELL_PROCESSING_SEC.setValue(processing_sec);
  }

  CELL_RESULT.setValue(message);
}


/** メイン処理 */
function main(){
  writeResult("実行中", false);
  try {
    // UserGroupIDを求める
    let usergroup_id = getUserGroupID();
    // 各行のUserIDを求める
    let user_id_list = getUserIDs(usergroup_id);
    // Update実施
    updateUserGroup(usergroup_id, user_id_list);
  } catch (e){
    writeResult(e.name + " " + e.message, true);
    return;
  }
  writeResult("終了", true);
}
```
</div></details>

## ポイント
+ Token  
説明のためにSlackアプリのTokenをわかるようにしていますが、本来は隠しましょう。
+ UserGroupIDの特定   
getUserGroupID()にて、usergroups.listですべてのユーザーグループを取得してそこから更新対象のものを探しています。  
何らかの方法で一発で特定できないものか、、、
+ メールアドレスでの特定
ユーザーは必ずメールアドレスを登録する運用のため、メールアドレスからSlackIDを特定するようにしています。
+ 処理時間の壁 
GASには **1実行あたり6分まで** という制限があります。
getUserIDs()で各ユーザーのSlack IDを求めるところでは、 [UrlFetchApp.fetchALL()](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchAll(Object))を使うことで、多くのユーザを登録する場合でも制限に引っかからないようにしています。
+ 更新されたことの通知  
参考にしたこちら([Slackで動的なユーザグループを作成しようと試行錯誤した物語](https://qiita.com/sin_tanaka/items/77f7b6bf2c11d41617fc))では、ユーザーグループに更新があるたびに通知が飛んだので要望を出したといった内容が記載されてますが、要望が通ったのか2021年1月現在では飛びませんでした。