Slack×GAS スプレッドシートにユーザーリストを一括出力して管理したり色々する

corp-engr 情シスSlack(コーポレートエンジニア x 情シス) Advent Calendar 2020#1 の24日目の記事です。

どうもbarusuです。知らない方は初めまして。
いつもお世話になっている情シスSlackのAdvent Calendar#1にて記事を寄稿させていただきます。

さて今日の内容ですが、Slackからゲストアカウント含めた全ユーザーをスプレッドシートに出力するScriptをご紹介します。

前半は初心者向けに画像多くマニュアル的に記載し、
後半は慣れてる人向けの解説を書きました。

忙しい人のための欄

スクリプトだけほしい人はこちらからどーぞ。


function getSlackUser(cursor) {
 
 const slack_app_token = "xoxb-your-token";
 const limit =500;
 const options = {
   "method" : "get",
   "contentType": "application/x-www-form-urlencoded",
   "payload" : { 
     "token": slack_app_token,
     "cursor": cursor,
     "limit":limit
   }
 };
 
 const url = "https://slack.com/api/users.list";
 const response = UrlFetchApp.fetch(url, options);
 
 const members = JSON.parse(response).members;
 
 let activeArr = [];
 let inActiveArr = [];

   for (const member of members) {
       let deleted = member.deleted;
       let id = member.id;
       let real_name = member.profile.real_name; //氏名
       let name = member.name; //mei
       let is_primary_owner = member.is_primary_owner; //プライマリオーナー
       let is_owner = member.is_owner; //オーナー
       let is_admin  = member.is_admin; //管理者アカウント
       let is_restricted = member.is_restricted; //Trueならばゲスト
       let is_ultra_restricted = member.is_ultra_restricted; //true ならばシングルゲストチャンネル,Falseかつis_restrictedがTrueならばマルチチャンネルゲスト
       let is_bot = member.is_bot; //botユーザー
       let is_app_user = member.is_app_user; // アプリユーザー
       let is_invited_user = member.is_invited_user;// 招待中
       let guest_invited_by = member.guest_invited_by ; //アカウント有効期限

if (!member.deleted) {
       activeArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}
if (member.deleted) {
inActiveArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}
   }


 //スプレッドシートに書き込み
 const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  if( cursor ==null){ 
   sheet.clear();
     sheet.appendRow(['ID','削除済','氏名','ユーザー名','プライマリオーナー','オーナー','管理者アカウント','ゲストアカウント','シングルorマルチ','botユーザー','アプリユーザー','招待中','アカウント有効期限']);
   }
try{
 sheet.getRange( sheet.getLastRow()+1,1, activeArr.length, activeArr[0].length).setValues(activeArr);
 sheet.getRange( sheet.getLastRow()+1,1, inActiveArr.length, inActiveArr[0].length).setValues(inActiveArr);
}
catch(e){
}

   if( responseMeta = JSON.parse(response).response_metadata.next_cursor != ''){
   getSlackUser(JSON.parse(response).response_metadata.next_cursor);
 }
}

以降、使い方の説明を記載していきます。

事前準備:Slackアクセストークンを発行する

SlackAPIを使うので、アクセスする権利=トークンを発行する必要があります。

1. [設定と管理] → [アプリを管理する]の順にクリック

2. ビルドをクリック

画像2

3. [Create New App]を選択

画像3

4. AppNameを入力しWorkspaceを選択し[CreateApp]をクリック

画像4

5. [OAuth & Permissions] をクリック

画像5

6. 下へページをスクロールし[Bot Token Scopes]の欄で[users:read]を追加

画像7

7. ページ上部の[OAuth Tokens & Redirect URLs]欄にある[Install to Workspace]をクリック

画像7

8. [許可する]をクリック

画像8

9. 次の画面でTokenが発行されているので、コピーして控えておく

画像9

※このトークンは外部に漏らさないようにしましょう

以上で事前準備は完了です。

GASを書く

1. 適当にスプレッドシートを作成する

画像10

2. [ツール]→[スクリプトエディタ]の順にクリック

画像11

3. エディタ欄に以下コードを貼り付け



function getSlackUser(cursor) {
 
 const slack_app_token = "xoxb-your-token";
 const limit =500;
 const options = {
   "method" : "get",
   "contentType": "application/x-www-form-urlencoded",
   "payload" : { 
     "token": slack_app_token,
     "cursor": cursor,
     "limit":limit
   }
 };
 
 const url = "https://slack.com/api/users.list";
 const response = UrlFetchApp.fetch(url, options);
 
 const members = JSON.parse(response).members;
 
 let activeArr = [];
 let inActiveArr = [];

   for (const member of members) {
       let deleted = member.deleted;
       let id = member.id;
       let real_name = member.profile.real_name; //氏名
       let name = member.name; //mei
       let is_primary_owner = member.is_primary_owner; //プライマリオーナー
       let is_owner = member.is_owner; //オーナー
       let is_admin  = member.is_admin; //管理者アカウント
       let is_restricted = member.is_restricted; //Trueならばゲスト
       let is_ultra_restricted = member.is_ultra_restricted; //true ならばシングルゲストチャンネル,Falseかつis_restrictedがTrueならばマルチチャンネルゲスト
       let is_bot = member.is_bot; //botユーザー
       let is_app_user = member.is_app_user; // アプリユーザー
       let is_invited_user = member.is_invited_user;// 招待中
       let guest_invited_by = member.guest_invited_by ; //アカウント有効期限

if (!member.deleted) {
       activeArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}
if (member.deleted) {
inActiveArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}
   }


 //スプレッドシートに書き込み
 const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  if( cursor ==null){ 
   sheet.clear();
     sheet.appendRow(['ID','削除済','氏名','ユーザー名','プライマリオーナー','オーナー','管理者アカウント','ゲストアカウント','シングルorマルチ','botユーザー','アプリユーザー','招待中','アカウント有効期限']);
   }
try{
 sheet.getRange( sheet.getLastRow()+1,1, activeArr.length, activeArr[0].length).setValues(activeArr);
 sheet.getRange( sheet.getLastRow()+1,1, inActiveArr.length, inActiveArr[0].length).setValues(inActiveArr);
}
catch(e){
}

   if( responseMeta = JSON.parse(response).response_metadata.next_cursor != ''){
   getSlackUser(JSON.parse(response).response_metadata.next_cursor);
 }
}

画像12

4. 事前準備で発行したトークンをxoxb-your-tokenの部分に貼り付け

画像13

5. 実行をクリック

画像14

6. 権限を確認をクリック

画像15

7.アカウントを選択

画像16

8. [許可]をクリック

画像17

実行するとこうなる

画像18

スプレッドシートにユーザーリストが出力されます。

補足:ゲストアカウントの扱いについて

H列:ゲストアカウント
  TRUE  :シングルチャンネルorマルチチャンネルゲスト
  FALSE :ユーザーアカウント
I列:シングルorマルチ 
  TRUE  :シングルチャンネルゲスト  
  FALSE :(H列がTRUEならば)マルチチャンネルゲスト

となります。GASで判別するように後日改修するかもです。

使い方の説明は以上です。
以降はそこそこ技術的な解説をしていきます。

Script解説

 const slack_app_token = "xoxb-your-token";
 const limit =500;
 const options = {
   "method" : "get",
   "contentType": "application/x-www-form-urlencoded",
   "payload" : { 
     "token": slack_app_token,
     "cursor": cursor,
     "limit":limit
   }
 };
 
  const url = "https://slack.com/api/users.list";

この段落では、SlackAPIにリクエストする内容を定義している。

limit:一度に取得する量を設定。ユーザー数が1000とかいる組織の場合は複数回に分けて取得する必要がある。複数回に分ける方法は後述。

cursor:sqlのcursorとほぼ同じ。SlackAPIから取得したデータをlimit数分ずつ取得するときに「いまどこからどこまでのデータを取得してます」を示す目印みたいなもの。初回はnull。

url:SlackAPI のurlを指定。

 
 const response = UrlFetchApp.fetch(url, options);
 const members = JSON.parse(response).members;

ここでSlackAPIにパラメータを渡す処理。

response :SlackAPIにパラメータを渡した後、戻ってきたモノを格納している。
戻ってきたモノの具体的な値は以下。

{
   "ok": true,
   "members": [
       {
           "id": "W012A3CDE",
           "team_id": "T012AB3C4",
           "name": "spengler",
           "deleted": false,
           "color": "9f69e7",
           "real_name": "spengler",
           "tz": "America/Los_Angeles",
           "tz_label": "Pacific Daylight Time",
           "tz_offset": -25200,
           "profile": {
               "avatar_hash": "ge3b51ca72de",
               "status_text": "Print is dead",
               "status_emoji": ":books:",
               "real_name": "Egon Spengler",
               "display_name": "spengler",
               "real_name_normalized": "Egon Spengler",
               "display_name_normalized": "spengler",
               "email": "spengler@ghostbusters.example.com",
               "image_24": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "image_32": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "image_48": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "image_72": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "image_192": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "image_512": "https://.../avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
               "team": "T012AB3C4"
           },
           "is_admin": true,
           "is_owner": false,
           "is_primary_owner": false,
           "is_restricted": false,
           "is_ultra_restricted": false,
           "is_bot": false,
           "updated": 1502138686,
           "is_app_user": false,
           "has_2fa": false
       },
       {
           "id": "W07QCRPA4",
           "team_id": "T0G9PQBBK",
           "name": "glinda",
           "deleted": false,
           "color": "9f69e7",
           "real_name": "Glinda Southgood",
           "tz": "America/Los_Angeles",
           "tz_label": "Pacific Daylight Time",
           "tz_offset": -25200,
           "profile": {
               "avatar_hash": "8fbdd10b41c6",
               "image_24": "https://a.slack-edge.com...png",
               "image_32": "https://a.slack-edge.com...png",
               "image_48": "https://a.slack-edge.com...png",
               "image_72": "https://a.slack-edge.com...png",
               "image_192": "https://a.slack-edge.com...png",
               "image_512": "https://a.slack-edge.com...png",
               "image_1024": "https://a.slack-edge.com...png",
               "image_original": "https://a.slack-edge.com...png",
               "first_name": "Glinda",
               "last_name": "Southgood",
               "title": "Glinda the Good",
               "phone": "",
               "skype": "",
               "real_name": "Glinda Southgood",
               "real_name_normalized": "Glinda Southgood",
               "display_name": "Glinda the Fairly Good",
               "display_name_normalized": "Glinda the Fairly Good",
               "email": "glenda@south.oz.coven"
           },
           "is_admin": true,
           "is_owner": false,
           "is_primary_owner": false,
           "is_restricted": false,
           "is_ultra_restricted": false,
           "is_bot": false,
           "updated": 1480527098,
           "has_2fa": false
       }
   ],
   "cache_ts": 1498777272,
   "response_metadata": {
       "next_cursor": "dXNlcjpVMEc5V0ZYTlo="
   }
}

JSON.parse(response).members :Jsonだと使いにくいので、Javascriptでアクセスしやすい形(オブジェクト)に変更している

    for (const member of members) {
       let deleted = member.deleted;
       let id = member.id;
       let real_name = member.profile.real_name; //氏名
       let name = member.name; //mei
       let is_primary_owner = member.is_primary_owner; //プライマリオーナー
       let is_owner = member.is_owner; //オーナー
       let is_admin  = member.is_admin; //管理者アカウント
       let is_restricted = member.is_restricted; //Trueならばゲスト
       let is_ultra_restricted = member.is_ultra_restricted; //true ならばシングルゲストチャンネル,Falseかつis_restrictedがTrueならばマルチチャンネルゲスト
       let is_bot = member.is_bot; //botユーザー
       let is_app_user = member.is_app_user; // アプリユーザー
       let is_invited_user = member.is_invited_user;// 招待中
       let guest_invited_by = member.guest_invited_by ; //アカウント有効期限

取得したユーザーリストの行数分ループする。
必要なデータをletで定義している。
Object.keys(members).forEach(function(key){} で全要素を取る方法もあるのだけど、追記削除しやすいこちらを選んだ。

if (!member.deleted) {
       activeArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}
if (member.deleted) {
inActiveArr.push([ id, deleted,real_name, name, is_primary_owner, is_owner, is_admin , is_restricted, is_ultra_restricted, is_bot, is_app_user, is_invited_user,guest_invited_by]);
}

削除済ユーザーがHitした場合は別の配列に入れるようにした。
スプレッドシートに出力する際、削除済ユーザーと有効ユーザーが混在するのを避けるため。
activeArr.sort のメソッドでも良いのだが...
削除ユーザーだけ別のシートにしたいとか、有効なユーザーの場合は別途XXしたい
など応用することがあり得るかな?と考えたので分離しておいた。

  if( cursor ==null){ 
   sheet.clear();
   sheet.appendRow(['ID','削除済','氏名','ユーザー名','プライマリオーナー','オーナー','管理者アカウント','ゲストアカウント','シングルorマルチ','botユーザー','アプリユーザー','招待中','アカウント有効期限']);
   }

初回の実行の場合、シートをクリアしてヘッダ行を追加する。

try{
 sheet.getRange( sheet.getLastRow()+1,1, activeArr.length, activeArr[0].length).setValues(activeArr);
 sheet.getRange( sheet.getLastRow()+1,1, inActiveArr.length, inActiveArr[0].length).setValues(inActiveArr);
}
catch(e){
}

スプレッドシートへの書き込みをする。
場合によっては削除済ユーザーが0の場合はエラーでスクリプトが止まるのでエラー回避のため、スプレッドシート貼り付けの処理はTry-Catch文にした。

   if( responseMeta = JSON.parse(response).response_metadata.next_cursor != ''){
   getSlackUser(JSON.parse(response).response_metadata.next_cursor);
 }

next_cursor に値がある場合は未取得のユーザーリストがあるので、再度自分自身を実行する。

最後に

この記事を書く前にググってみたんですが、ネットに転がってるやつだと
・ゲストかどうかとかの判別がない
・ページネーションに対応していない
とかで、大きい組織では使えない感じなソースが多い印象でした。

そこらへんの課題をケアして作成したので色々と応用が利くと思います。
取得するusers.list のパラメータを変更すれば条件に一致したユーザーにDMを送るとか
ゲストの期限が設定されていないものを毎日検知し管理者に通知するとか
色々できそうですね。

こんなことができませんかね?とか
今困ってることがあります!とか
アイデア、要望があればぜひぜひコメント/DMください!

どうせ年末年始も暇なんで。

馬なんで。

以上、ここまでお付き合いくださりありがとうございました。

PS.
SFDC×異世界 の話は近日まとめて公開予定です。

この記事が気に入ったらサポートをしてみませんか?