GASでSlackのユーザーグループのメンバーを自動更新する

# はじめに
チーム内でシステムのアラート当番を回しており、何かしらのエラーを検知したら当番が対応するという運用をしています。
エラーの通知のメンション先はSlackの当番用のユーザーグループなので、当番が変わるタイミングで手動で更新していたのですが、地味に面倒でした。
そこでGASを使ってスプレッドシートで作成した当番表に従ってユーザーグループ定期的に自動更新するようにしました。

# 概要
**Google Spreadsheet**
以下のような当番表のシートを作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2747426/d5c5f30b-d12d-e89e-014f-48802a5191b7.png)



**GAS**
- 毎日決まった時間に動くようにトリガーを設定して定期的に実行する。
- 現在日とスプレッドシートの当番開始日が一致するレコードがあったら
その担当者のユーザIDを取得し、ユーザーグループを更新する。
- 更新したらSlack通知でお知らせする。

# 使用するSlack API
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します。

# スクリプト

```javascript:main.gs
const TOKEN = 'TOKEN';
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";
const USER_GROUP_NAME = "slackのユーザーグループ名";
const SPREADSHEET = SpreadsheetApp.getActiveSpreadsheet();
const SHEET_USERS = SPREADSHEET.getSheetByName('当番表'); // ここでは「当番表」というシート名から取得する

/** ここはコードでベタ書きしたが、別シートにまとめてそこから取得しても良いかも */
const ADDRESS_LIST = {
  "担当者名1":"member1@hogehoge.com",
  "担当者名2":"member2@hogehoge.com",
  "担当者名3":"member3@hogehoge.com",
  "担当者名4":"member4@hogehoge.com"
}

/** UserGroupIDを求める */
function getUserGroupID() {
  Logger.log("START getUserGroupID");
  let usergroup_id = '';

  // 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 == USER_GROUP_NAME);
  if (hit_usergroup === undefined){
    throw new Error(":" + USER_GROUP_NAME + "は見つかりませんでした。登録済みか確認してください");
  }
  Logger.log(hit_usergroup);

  // 取得結果
  Logger.log('UserGroupID:' + hit_usergroup['id']);

  return hit_usergroup['id'];
}

/** カンマ区切りのUserIDリストを返す */
function getUserIDs(targerRow){
  Logger.log("START getUserIDs");
  let error_flg = false;
  let user_id_list = [];
  let name_1 = SHEET_USERS.getRange("C"+targerRow).getValues();
  let name_2 = SHEET_USERS.getRange("D"+targerRow).getValues();
  let email_1 = ADDRESS_LIST[name_1];
  let email_2 = ADDRESS_LIST[name_2];
  let emailList = [email_1, email_2];
  
  let user_id = '';
  let requests = [];
  let responses =[];
  let response = '';
  const options = {
	"headers": { 'Authorization': 'Bearer ' + TOKEN }
  };

  for(let i = 0; i < 2; i++){
    requests=API_USERS_LOOKUPBYEMAIL + "?&email=" +  emailList[i];
    
    responses = UrlFetchApp.fetch(requests,options);
    response = JSON.parse(responses.getContentText("UTF-8"));
    user_id =  response['user']['id'];
    user_id_list.push(user_id);
  }

  Logger.log('user_id_list:' + user_id_list);

  return user_id_list.join(',');
}

/** ユーザーグループの更新 */
function updateUserGroup(usergroup_id, user_id_list) {
  Logger.log("START updateUserGroup");

  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);
  Logger.log(response);
}

/** SlackAPIの呼び出し */
function callApi(url, params){
  let response = UrlFetchApp.fetch(url, params);
  let resjson = JSON.parse(response);

  if (resjson.ok != true) {
    Logger.log('エラー：' + resjson['error']);
    throw new Error(resjson['error']);
  }
  return resjson;
}

function findRow(sheet,val,col){
  const dat = sheet.getDataRange().getValues(); //受け取ったシートのデータを二次元配列に取得
  for(let i=1;i<dat.length;i++){
    Logger.log('value:'+dat[i][col-1]);
    if(dat[i][col-1] === val){
      return i+1;
    }
  }
  return 0;
}

/** メイン処理 */
function main(){
  Logger.log("START");

  const today = new Date();
  todayFormatted = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy-MM-dd');
  Logger.log('today:' + todayFormatted);

  const targerRow = findRow(SPREADSHEET,todayFormatted,1);

  if (targerRow == 0) {
    Logger.log('not exists target');
    return;
  }

  Logger.log('targerRow:'+targerRow);

  // UserGroupIDを求める
  const usergroup_id = getUserGroupID();
  // 各行のUserIDを求める
  const user_id_list = getUserIDs(targerRow);
  // Update実施
  updateUserGroup(usergroup_id, user_id_list);
  // slack通知
  sendNotice();
  Logger.log("END");
}

```
```javascript:sendNotice.gs
function sendNotice() {
  const webhookURL = 'https://hooks.slack.com/services/~';

  const data = {
    'username' : '当番チームのメンバーを変更してくれるくん',
    'channel' : '#チャンネル名',
    'icon_emoji': ':smile:',
    'attachments': [{
      'color': '#008000',
      'text' : '当番交代しました！',
    }],
  };

  const payload = JSON.stringify(data);
  const options = {
    'method' : 'POST',
    'contentType' : 'application/json',
    'payload' : payload
  };

  UrlFetchApp.fetch(webhookURL, options);
}

```

# トリガー設定
毎日決まった時間に動くようにGASの画面からトリガーを設定する。