📍

Slack ゲストアカウントの棚卸・通知をGASで作ってみた

2022/08/16に公開約12,900字

はじめに

本業・副業共にコミュニケーションツールはSlackを使用しているのですが、ゲストアカウントの棚卸って結構課題だったりすることが多いと思います。自分も多分に漏れず、「後で効率化したいな〜、何かScript書こうかな〜」と思いながら1年以上経過してしまいました。。。

過去の経緯から有効期限を設定してないものもあったり、IT統制などでアカウントの棚卸を効率的に行いたいなど、みんな似たような課題感はあるのかなと思い、Slackチャンネルへ通知してくれるScriptを作成してみました

公式に機能なかったっけ?

はい、公式にも期限切れ通知の機能はあります。ただし、

  • 設定した管理者 + 対象者にのみ通知
  • 2日前に通知(固定)

と、使い勝手は良くはありません...

本当は...Slackコネクトしたいよね

Slackコネクトしている場合であれば、相手側のアカウント管理まで実施する必要はありません。
とはいえ、相手側がSlackを使っていなかったり無料プランだったりすると、ゲストユーザーとして招待するしかないのが現状です。

ゲストユーザーとSlackコネクトの違いについては下記を参照ください

https://note.com/humihumi_infosys/n/ne8a5695d1aea

https://slack.com/intl/ja-jp/help/articles/202518103-Slack-でのゲストのメンバー種別を理解する

完成形

さっそく、「こんな感じでできたよ!」 というのを披露したいと思います〜

Slack側

1ユーザーのみの場合

複数ユーザーいる場合

スプレッドシート側

作成手順

01.Slackアプリの作成

  1. Slackのホーム画面の左メニューから、[App 管理] をクリックする

  2. 右上の [ビルド] をクリックする

  3. [Create New App] > [From scratch] をクリックする

  4. アプリ名と対象のワークスペースを選択して、[Create App] をクリックする

  5. Basic Informationの画面をスクロールし、「Display Information」の情報を入力し、[Save Changes] をクリックする

  6. 左メニューの [Incoming Webhooks] > 「Activate Incoming Webhooks」のトグルを [On] にする

  7. 左メニューの [App Home] > [Edit] > Display NameとDefault usernameを入力して [Add] をクリックする

  8. 左メニューの [OAuth & Permissions] > Scopes から、下記のScopeを追加する

  9. [OAuth & Permissions]の上部に戻り、[Instal to Workspace] をクリックする

  10. インストール先のチャンネルを指定して、[許可する] をクリックする

  11. [Bot Useer OAuth Token] をコピーする

  12. [Webhook URL] をコピーする

02.GAS(Google Apps Script)の作成

  1. スプレッドシートを新規作成する

  2. メニューの [拡張機能] > [Apps Script] をクリックする

  3. プロジェクトの設定画面から、[スクリプト プロパティを編集] をクリックし、以下のように設定する

プロパティ
BOT_TOKEN 1-11でコピーした値
WEBHOOK_URL 1-12でコピーした値

  1. エディタから、scriptファイルを2つ (functsions.gs / main.gs) を作成する

  2. 以下のソースを貼り付ける

functions.gs
function getSlackGuestUsersInfo() {

  // Bot User OAuth Token:スクリプトプロパティの情報を取得する
  const token = PropertiesService.getScriptProperties().getProperty("BOT_TOKEN");

  // ユーザー情報を取得する
  const members = getUserList(token);

  // 変数の初期化
  const membersInfo   = [];
  let ultraRestricted = "";
  let isInvitedUser   = "";
  let guestInvitedBy  = "";
  let guestExpiryDate = "";
  let guestUserName   = "";

  members.forEach(function(member) {

    // 削除済みユーザー or ゲストユーザー以外 は除外
    if (member["deleted"] === true || member["is_restricted"] === false) { return; };

    // ゲストユーザーがシングル or マルチ?
    ultraRestricted = member["is_ultra_restricted"] === true ? "シングル" : "マルチ";

    // 招待ステータスがtrue?undifined?
    isInvitedUser = member["is_invited_user"] === undefined ? "" : "true";

    // ゲストの有効期限が入力されてたら、UnixTime → JSTに変換
    guestExpiryDate = member["profile"]["guest_expiration_ts"] === undefined ? "" : changeJSTfromUnixTime(member["profile"]["guest_expiration_ts"]);

    // 招待したユーザーIDがundifinedの場合 → 空白に変換
    guestInvitedBy = member["profile"]["guest_invited_by"] === undefined ? "" : member["profile"]["guest_invited_by"];

    // 招待したユーザーID → ユーザー名に変換
    guestUserName = getUserNamefromInvitedId(guestInvitedBy, token);

    // ユーザー情報をオブジェクトに入れる
    membersInfo.push([
      member["id"],               // ユーザーID
      member["real_name"],        // 氏名
      member["profile"]["email"], // メールアドレス
      ultraRestricted,            // ゲストの種類
      isInvitedUser,              // true:招待中
      guestExpiryDate,            // ゲストの有効期限
      guestInvitedBy,             // 招待したユーザーID
      guestUserName,              // 招待したユーザー名
    ]);
  });

  return membersInfo;
 
}

function writeSheet(membersInfo) {

  //  スプレッドシート読み込み
  const spreadsheet = SpreadsheetApp.openById("xxxxxxxxxxxx");
  const sheet       = spreadsheet.getSheetByName("xxxx");
  const lastRow     = sheet.getLastRow();
  const lastCol     = sheet.getLastColumn();
  
  // 一度既存の情報をクリアする
  sheet.getRange(2, 2, lastRow - 1, lastCol - 1).clearContent();

  // ユーザー情報をセットする
  sheet.getRange(2, 2, membersInfo.length, membersInfo[0].length).setValues(membersInfo);

}

function postSlack(body) {
 
    // Webhook URL:スクリプトプロパティから情報を取得
    const url = PropertiesService.getScriptProperties().getProperty("WEBHOOK_URL");

    // 送信オプション
    const options = {
      method     : 'post',
      contentType: 'application/json',
      payload    : JSON.stringify(body),
    };
  
    // 送信処理
    UrlFetchApp.fetch(url, options);

}


function getUserList(token) {

  const options = {
    "method"     : "get",
    "contentType": "application/x-www-form-urlencoded",
    "payload": { 
      "token": token
    }
  };
  
  const url = "https://slack.com/api/users.list";
  const response = UrlFetchApp.fetch(url, options);
  
  const userList = JSON.parse(response).members;

  return userList;

}

function changeJSTfromUnixTime(unixTime) {

  const jstTime = Utilities.formatDate(new Date( unixTime * 1000 ), "JST", "yyyy/MM/dd HH:mm:ss");
  return jstTime;

}

function getUserNamefromInvitedId(guestInvitedBy, token) {

  if (guestInvitedBy == "") { return };

  const options = {
    "method"     : "get",
    "contentType": "application/x-www-form-urlencoded",
    "payload": { 
      "token": token,
      "user" : guestInvitedBy
    }
  };
  
  const url      = "https://slack.com/api/users.info";
  const response = UrlFetchApp.fetch(url, options);
  const userName = JSON.parse(response).user["name"] === undefined ? "" : JSON.parse(response).user["name"];

  return userName;
  
}

function checkExpiryDate(guestExpiryDate) {

  const expiryDate = new Date(guestExpiryDate);
  const now        = new Date();
  const termDay    = (expiryDate - now) / 86400000;

  return termDay;

}
if文を使用する場合はこちら(getSlackGuestUsersInfo())
function getSlackGuestUsersInfo() {

  // Bot User OAuth Token:スクリプトプロパティの情報を取得する
  const token = PropertiesService.getScriptProperties().getProperty("BOT_TOKEN");

  // ユーザー情報を取得する
  const members = getUserList(token);

  // 変数の初期化
  const membersInfo   = [];
  let ultraRestricted = "";
  let isInvitedUser   = "";
  let guestInvitedBy  = "";
  let guestExpiryDate = "";
  let guestUserName   = "";

  members.forEach(function(member) {

    // 削除済みユーザー or ゲストユーザー以外 は除外
    if (member["deleted"] === true || member["is_restricted"] === false) { return; };

    // ゲストユーザーがシングル or マルチ?
    if (member["is_ultra_restricted"] === true ) {
      ultraRestricted = "シングル";
    } else {
      ultraRestricted = "マルチ";
    }

    // 招待ステータスがtrue?undifined?   
    if (member["is_invited_user"] === undefined) {
      isInvitedUser = "";
    }  else {
      isInvitedUser = "true";
    }  

    // ゲストの有効期限が入力されてたら、UnixTime → JSTに変換
    if (member["profile"]["guest_expiration_ts"] === undefined) {
      guestExpiry = "";
    } else {
      guestExpiry = changeJSTfromUnixTime(member["profile"]["guest_expiration_ts"]);
    }

    // 招待したユーザーIDがundifinedの場合 → 空白に変換    
    if (member["profile"]["guest_invited_by"] === undefined) {
      guestInvitedBy = "";
    } else {
      guestInvitedBy = member["profile"]["guest_invited_by"];
    }

    // 招待したユーザーID → ユーザー名に変換
    guestUserName = getUserNamefromInvitedId(guestInvitedBy, token);

    // ユーザー情報をオブジェクトに入れる
    membersInfo.push([
      member["id"],               // ユーザーID
      member["real_name"],        // 氏名
      member["profile"]["email"], // メールアドレス
      ultraRestricted,            // ゲストの種類
      isInvitedUser,              // true:招待中
      guestExpiryDate,            // ゲストの有効期限
      guestInvitedBy,             // 招待したユーザーID
      guestUserName,              // 招待したユーザー名
    ]);
  });

  return membersInfo;
 
}
main.gs
function main() {

  // ユーザー情報の取得
  const membersInfo = getSlackGuestUsersInfo();

  // スプシに書き込み
  writeSheet(membersInfo);

  // 本文(その1:メッセージ部分)作成
  const prebody =
  {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "<!channel>" + "\n"
            + "Slackのゲストユーザーの有効期限をお知らせするよ!:emoji:",
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "有効期限を延長する場合は <https://xxxx.slack.com/admin|ユーザー管理> から〜 :black_cat:" + "\n"
            + "最終ログイン日の確認は <https://xxxx.slack.com/admin/stats|アナリティクス> から〜 :mag:"
        },
      },
    ]
  }

  // Slackへ通知(その1)
  postSlack(prebody);

  // 本文(その2:通知ユーザー部分)作成
  membersInfo.forEach(function(member) {
    
    // 有効期限が15日より先の場合はスルー
    if (member[5] == "" || checkExpiryDate(member[5]) > 15) { return; };
 
    // 本文
    const mainbody =
    {
      "blocks": [
        {
          "type": "divider"
        },
      ],
      "attachments" : [
        {
          "fallback": "Slackのゲストユーザーの有効期限をお知らせするよ!:emoji:",
          "color": "#fec20f",
          "fields": [
            {
              "title": "`ゲストアカウント名`",
              "value": member[1],
              "short": true
            },
            {
              "title": "`メールアドレス`",
              "value": member[2],
              "short": true
            },
            {
              "title": "`有効期限`",
              "value": member[5],
              "short": true
            },
            {
              "title": "招待者",
              "value": member[7],
              "short": true
            },
          ],
        }
      ],
    }

    // Slackへ通知(その2)
    postSlack(mainbody);
  
  });
}
  1. 送信テストとして、[▷実行] をクリックする

  2. [トリガー] > [トリガーを追加] をクリックする

  3. 任意の設定をして [保存] をクリックする

考慮した点

  • BotトークンやWebhookURLのプロパティ類は、ソースコードに記載せずに、GASのスクリプトプロパティ欄に記載
  • デフォルトの「users.list」だけだと、招待した人のユーザー名を取得できなかったので、取得処理を追加
  • 弊社環境では、謎に招待したユーザーIDが「undifined」になるパターンがあった為、処理を追加
  • 弊社環境では、ゲストの有効期限を一定統一しているため、複数ユーザーいる場合は、前半のメッセージ部分とユーザー情報部分を分けて通知しています
  • 内部統制関連で、棚卸情報の一覧化用としてスプレッドシートへも書き込んでいます
    • 今回の書き込み先は一時的なものなので、必要なタイミングで値をコピーしたりするなど、適切に切り出してあげると良いかも
  • その他、雰囲気...

妥協した点

  • Slack上で有効期限を延長(無効化)させたい
    • どうやらAPIが、Enterprise Gridのみ利用可能ということで今回は諦め
  • 最終アクティブ日時の取得
    • ぱっと取得できるものがなさそうだったので妥協

参考にしたもの

その1

最初のきっかけはこれ。kajinariさんの記事を見ていいなぁと思ったところから。

https://medium.com/kajinari/slack-communication-with-collaborator-d4417992c03f

その2

文章や現状のProプランで可能な実装範囲を踏まえた運用の参考に。

同タイミングでヤスムラさんも同様の記事を上げていますが、コード見てみると結構違いがあって面白いです

https://note.com/yasuym1/n/n9a0fcc9439dc

さいごに

最近だと、BUNDLEジョーシス のようなSaaSの一元管理サービスなどが出ており、その中でもチェックできたりしますよね。

とはいえ、弊社では導入予定はなかったり、やっぱり「Slackに通知してくれないと中々気づけん!」みたいな人もいるかなと思います(僕ですね...

作ってみてやはり、Enterprise Gridいいなぁと思いました。ホント導入してる企業羨ましい...

Discussion

ログインするとコメントできます