GASを使ってslackユーザーデータを取得する(1000件以上対応)
お久しぶりです。カインです。
転職して1年が経過し、色々なGASやSlackアプリを作ったのでぼちぼち記事にしてきたいと思います。
Slackのユーザー情報の呼び出しAPI(users.list)、何かと便利ですよね。
最初はユーザー情報が欲しい時はusers.list APIで毎回呼び出して使用していましたが、実行時間を削減したりスプレッドシートで活用したく、取って来たデータをJSONやスプレッドシートに保存して使っています。
今回は、users.listの呼び出し、JSONファイルの保存、スプレッドシートへの書き込みの3つ記事に分けて紹介していきます。
この記事で紹介すること
- slack API users.list の使い方
続きの記事
- slackユーザーデータをJSONファイルとしてGoogleドライブに保存する - シンプルに暮らしたい情シスのブログ
- 保存したJSONを読み込み、スプレッドシートへ書き込む(執筆予定)
slack API users.listの使い方
slackアプリの作成・権限設定
まずはslackアプリを作成し、users.listに必要な権限設定を行います。
アプリの作成
slack左上のメニュー>設定と管理>アプリを管理する
ビルドをクリック
Create New Appをクリック
From scratchをクリック
App Nameに任意の名前を入れて、Pick a workspace to develop your app inにインストール先のワークスペースを選択します。
ここでusers.listに必要な権限を確認します。
https://api.slack.com/methods/users.listを開き、
ここを確認すると、users:read が必要なようです。
が…!これには罠があって、読み進めていくと、
メールアドレスを取得するにはusers:read.emailも必要とのこと!
Required scopesのところに書いておいて欲しいですねぇ…。
権限の設定
OAuth & Permissionsをクリック
少し下がったところにあるScopes>Bot Token Scopesの下にあるAdd an OAuth Scopeをクリック
users:readを探してクリック
users:read.emailを探してクリック
OAuth & Permissionsのページ上部にあるInstall to Workspaceをクリック
内容を確認して、許可するをクリック
これでslackアプリの準備は完了です。
後ほどGASで使用する情報を確認しておきます。
OAuth & Permissionsのページ上部にBot User OAuth Token(xoxb-から始まるもの)があるので控えておきます。
GASの作成・実行
コード作成の前に
ここでusers.listをもう一度確認します。
Argumentsのところで、tokenのみRequiredが付いているので、users.listはtokenだけあれば実行は可能です。
ただし、読み進めていると、
一度に1000件までしか取得できないようで、1000件を超える場合はcursorが必要になるようです。
弊社は1000ユーザーを超えているので、cursorも含めたコードを作成します。
処理の流れ
- 空のcursorと配列を定義
- users.listを呼び出し、配列にuser情報を格納
- responseのnext_cursorに値があれば、再度users.listを実行(1000件を超えている場合next_cursorに値が入る)
- next_cursorが空であれば繰り返しを終了
トークンの設定
コード内に直接トークンを記載するのはリスクがあるため、スクリプトプロパティ内に設定します。
歯車アイコンをクリック
ページ下部のスクリプト プロパティを追加をクリック
プロパティは任意ですが、bot_tokenとしておきます。
値には、slackアプリの設定で控えた、Bot User OAuth Tokenを貼り付けて保存します。
コードの構成
コード
function Main() { let cursor = ""; let users_data = []; // users.listは1度で1000件までしか取得できないので、1000件を超える場合(next_cursorに値がある場合)繰り返す do { let response = callUsersList(cursor); cursor = response["response_metadata"]["next_cursor"]; // こちらの説明は後述 users_data.push(...response["members"]); // こちらの説明は後述 } while (cursor != ""); // next_cursorに値が無い場合=全て取得し終わった場合、ループを終了する // ユーザーデータをJSON文字列に変換 const users_data_str = JSON.stringify(users_data); Logger.log(users_data_str); } function callUsersList(cursor) { // スクリプトプロパティに設定したbot_tokenを呼び出す const token = PropertiesService.getScriptProperties().getProperty("bot_token") const url = "https://slack.com/api/users.list" //APIのURL // payloadを設定 const payload = { "token": token, "cursor": cursor } // optionsを設定 const options = { "method": "GET", "payload": payload, "headers": { "contentType": "x-www-form-urlencoded", } } // APIリクエストの結果をresponseに格納 const response = UrlFetchApp.fetch(url, options); // responseの文字列をJSON解析しオブジェクト化して返す return JSON.parse(response); }
一部解説
next_cursorの取り出し
users.listのresponseデータ(以下参照)の中に以下の部分があります。
"response_metadata": { "next_cursor": "dXNlcjpVMEc5V0ZYTlo=" }
この場合は、response_metadata.next_cursorに値があるので、
do~while内の
cursor = response["response_metadata"]["next_cursor"];
この部分でcursorにdXNlcjpVMEc5V0ZYTlo=が入ります。
cursor=""ではないため、cursorを引数としてcallUsersListに渡して再度APIを呼び出します。
続きが無い場合、response_metadata.next_cursorは
"response_metadata": { "next_cursor": "" }
と返ってくるため、cursorには""が入り、do~whileループから抜けます。
メンバー情報の取り出し・格納
do~while内に以下の部分があります。
users_data.push(...response["members"]);
user_dataは最初は空の配列で、1回APIを呼び出す毎に取得したユーザーデータ(members)を格納しています。
response["members"]の前にある...は、簡単に言うと配列の[]を外すような処理をします。
サンプル
test_dataがresponse["members"]と思ってください。
function sample() { let arr1 = []; let arr2 = []; const test_data = [ { "name": "a" }, { "name": "b" }, { "name": "c" } ]; arr1.push(test_data); arr2.push(...test_data); Logger.log(arr1); Logger.log(arr2); }
実行結果
(参考)responseデータ(JSON)
※users.listから引用
{ "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=" } }