自動化厨のプログラミングメモブログ │ CODE:LIFE

Python/ExcelVBA/JavaScript/Raspberry Piなどで色んなことを自動化

プライベートチャンネル自動生成 GASでSlackスラッシュコマンド(後編)

前回、GASでSlackのスラッシュコマンドのテストを行いました。 無事に応答が返ってくるのも確認できたので実際にプライベートチャンネルを作成する処理を書いていきます。 codelife.cafe

動作概要の確認

  1. スラッシュコマンド /soudan を実行
  2. プライベートチャンネル private_soudan_<ユーザ名>を生成
  3. チャンネルにコマンド実行ユーザとスプレッドシートで指定したユーザを追加
  4. チャンネル作成完了のメッセージを返す
  5. エラーが発生した場合はエラーコードを返す

こんなかんじで動きます。

Slack App の設定

Slash Commandを変更

前回 /test というコマンドをGASに紐づけていましたが、相談用チャンネル作成コマンドとして /soudan に変更しておきます。

f:id:maru0014:20210117221731p:plain

OAuth Tokenを取得

外部からAppの権限を用いてユーザやチャンネルを操作するためには認証情報としてOAuth Tokenが必要です。

管理画面左メニューの「OAuth & Permissions」を選択して表示される Bot User OAuth Access Token をコピーしておきましょう。

f:id:maru0014:20210117221738p:plain

権限スコープを設定

現時点ではスラッシュコマンドの権限しかありません。

追加で「ユーザ情報の参照」「プライベートチャンネルの作成・変更」の権限が必要なので設定します。先程の「OAuth & Permissions」の下部に権限設定があるので users:read channels:manage groups:write im:write mpim:writeを追加します。

※追加したあとは再度インストールする必要があるので「Reinstall to Workspace」をクリック

f:id:maru0014:20210117221747p:plain

Google Apps Script を作成

お先に完成形はこちら。先程コピーしておいたトークンを slack_app_token にセットします。

実運用時にはうっかりトークンが流出するのを防ぐためにもPropertyServiceに格納しましょう。

※2021/01/17時点で 新しいIDE環境ではPropertyServiceを設定する画面が存在しません。右上の「以前のエディタを使用」から旧IDEに変更する必要があるようです(T_T)

f:id:maru0014:20210117221800p:plain

// 共有設定
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("users");
const slack_app_token = PropertiesService.getScriptProperties().getProperty("slack_app_token");
const slash_command_token = PropertiesService.getScriptProperties().getProperty("slash_command_token");

/**
 * SlashCommandからのリクエスト受付
 * @param {object} e リクエストオブジェクト
 * @return {string} 実行結果をSlackに返却
 */
function doPost(e) {
  console.log(JSON.stringify(e));
  let result = "";
  
  // トークンがSlashCommandのリクエストと一致する場合のみ処理
  if(e.parameters.token == slash_command_token){
    const user_id = e.parameters.user_id;
    const user_name = e.parameters.user_name;

    // プライベートチャンネル作成
    const channel_name = `private_soudan_${user_name}`;
    const channel_res = createSlackGroups(channel_name);
    console.log(JSON.stringify(channel_res));

    if(channel_res["ok"]){
      // 成功時のメッセージを追加
      result += `相談用チャンネル「${channel_name}(${channel_res["channel"]["id"]})」を作成しました。\n`;

      // 各ユーザをチャンネルに追加
      const user_list = `${user_id},${getInviteSlackUsers()}`;
      const invite_res = inviteSlackUser(channel_res["channel"]["id"], user_list);
      console.log(JSON.stringify(invite_res));

      if(invite_res["ok"]){
        // 成功時のメッセージを追加
        result += `相談用チャンネル「${channel_name}(${channel_res["channel"]["id"]})」にユーザを追加しました。`;
      } else {
        // 失敗時はエラーを返す
        result += `ユーザの追加に失敗しました。 [エラーコード: ${invite_res["error"]}]`;
      }
    } else {
      // 失敗時はエラーを返す
      result += `チャンネルの作成に失敗しました。[エラーコード: ${channel_res["error"]}]`;
    }
    
    return ContentService.createTextOutput(result);
    
  } else {
    
    result = `無効なトークンです ${e.parameters.token}  ${slash_command_token}`
    return ContentService.createTextOutput(result);
    
  }
}

/**
 * プライベートチャンネル作成
 * @param {string} channel_name 作成するチャンネル名
 * @return {object} 実行結果
 */
function createSlackGroups(channel_name){
  const url = "https://slack.com/api/conversations.create";
  const options = {
    "method" : "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token,
      "name": channel_name,
            "is_private": true
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  return json;
}

/**
 * チャンネルにユーザを追加
 * @param {string} channel_id チャンネルID
 * @param {string} user_id ユーザーIDをカンマ区切りで指定
 * @return {object} 実行結果
 */
function inviteSlackUser(channel_id, user_id){
  const url = "https://slack.com/api/conversations.invite";
  const options = {
    "method" : "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token,
      "channel": channel_id,
      "users": user_id
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  return json;
}

/**
 * 全ユーザの名前とIDを取得してスプレッドシートに出力
 * @return {string} チャンネルに参加させるユーザリスト(カンマ区切り)
 */
function getInviteSlackUsers() {
  let result = [];
  const users = sheet.getRange(2, 1, sheet.getLastRow(), 3).getValues();
  
  for (let i = 0; i < users.length; i++) {
    if (users[i][2] !== "") {
      result.push(users[i][1]);
    }
  }
  
  console.log(result.join(","));
  return result.join(",");
  
}

/**
 * 全ユーザの名前とIDを取得してスプレッドシートに出力
 */
function getSlackUsers() {
  const url = "https://slack.com/api/users.list";
  const options = {
    "method" : "get",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response);
  const members = json.members;
  
  let table = [["ユーザー名", "ユーザーID", "参加"]];
  
  for (const member of members) {
    
    //削除済、botユーザー、Slackbotを除く
    if (!member.deleted && !member.is_bot && member.id !== "USLACKBOT") {
      let id = member.id;
      let real_name = member.real_name; //氏名(※表示名ではない)
      table.push([real_name, id, ""]);
    }
    
  }
  
  //スプレッドシートに書き込み
  sheet.getRange(1, 1, sheet.getMaxRows()-1, 3).clearContent();
  sheet.getRange(1, 1, table.length, table[0].length).setValues(table);
}

getSlackUsers() ユーザ情報を一括取得

こちらを参考に作成しました。実際に運用する段階ではトークンをPropertyServiceに保管して呼び出す形にしましょう。

【GAS×SlackAPI】ワークスペースのユーザー名とIDをスプレッドシートに取得する|もりさんのプログラミング手帳

設定用シートとして「users」というシートを作成しておき、以下のスクリプトを実行してユーザリストを取得します。認証トークンはグローバル変数 slack_app_token を用います。

/**
 * 全ユーザの名前とIDを取得してスプレッドシートに出力
 */
function getSlackUsers() {
  const url = "https://slack.com/api/users.list";
  const options = {
    "method" : "get",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response);
  const members = json.members;
  
  let table = [["ユーザー名", "ユーザーID", "参加"]];
  
  for (const member of members) {
    
    //削除済、botユーザー、Slackbotを除く
    if (!member.deleted && !member.is_bot && member.id !== "USLACKBOT") {
      let id = member.id;
      let real_name = member.real_name; //氏名(※表示名ではない)
      table.push([real_name, id, ""]);
    }
    
  }
  
  //スプレッドシートに書き込み
  sheet.getRange(1, 1, sheet.getMaxRows()-1, 3).clearContent();
  sheet.getRange(1, 1, table.length, table[0].length).setValues(table);
}

チャンネルに自動参加させたいユーザに「○」を付けます。

※空欄ではないという条件にしてあるので文字はなんでもいいです。

f:id:maru0014:20210117221844p:plain

createSlackGroups(channel_name) プライベートチャンネル作成

channels.create は2021年2月で廃止らしいので conversations.create を利用します。

公式ドキュメント: https://api.slack.com/methods/groups.create

/**
 * プライベートチャンネル作成
 * @param {string} channel_name 作成するチャンネル名
 * @return {object} 実行結果
 */
function createSlackGroups(channel_name){
  const url = "https://slack.com/api/conversations.create";
  const options = {
    "method" : "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token,
      "name": channel_name,
            "is_private": true
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  return json;
}

inviteSlackUser(channel_id, user_id) チャンネルにユーザを追加

groups.invite は2021年2月で廃止らしいので conversations.invite を利用します。

公式ドキュメント: https://api.slack.com/methods/groups.invite

/**
 * チャンネルにユーザを追加
 * @param {string} channel_id チャンネルID
 * @param {string} user_id ユーザーIDをカンマ区切りで指定
 * @return {object} 実行結果
 */
function inviteSlackUser(channel_id, user_id){
  const url = "https://slack.com/api/conversations.invite";
  const options = {
    "method" : "post",
    "contentType": "application/x-www-form-urlencoded",
    "payload" : { 
      "token": slack_app_token,
      "channel": channel_id,
      "users": user_id
    }
  };  
  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  return json;
}

getInviteSlackUsers() 参加ユーザリストを取得

スプレッドシートから「参加」列が空ではないユーザIDを取得してカンマ区切りで返す関数です。

/**
 * 設定用シートから参加対象ユーザを取得
 * @return {string} チャンネルに参加させるユーザリスト(カンマ区切り)
 */
function getInviteSlackUsers() {
  let result = [];
  const users = sheet.getRange(2, 1, sheet.getLastRow(), 3).getValues();
  
  for (let i = 0; i < users.length; i++) {
    if (users[i][2] !== "") {
      result.push(users[i][1]);
    }
  }
  
  console.log(result.join(","));
  return result.join(",");
  
}

doPost(e) SlashCommandからのリクエスト受付

現状はURLさえわかればどこからでもリクエスト可能な状態になっているので、SlashCommandに含まれるtoken文字列と比較して一致している場合のみ処理するようにします。

tokenは前回のテストコマンドの要領で事前に取得してPropertiesServiceにセット、グローバル変数 slash_command_token として取得して利用します。

/**
 * SlashCommandからのリクエスト受付
 * @param {object} e リクエストオブジェクト
 * @return {string} 実行結果をSlackに返却
 */
function doPost(e) {
  console.log(JSON.stringify(e));
  let result = "";
  
  // トークンがSlashCommandのリクエストと一致する場合のみ処理
  if(e.parameters.token == slash_command_token){
    const user_id = e.parameters.user_id;
    const user_name = e.parameters.user_name;

    // プライベートチャンネル作成
    const channel_name = `private_soudan_${user_name}`;
    const channel_res = createSlackGroups(channel_name);
    console.log(JSON.stringify(channel_res));

    if(channel_res["ok"]){
      // 成功時のメッセージを追加
      result += `相談用チャンネル「${channel_name}(${channel_res["channel"]["id"]})」を作成しました。\n`;

      // 各ユーザをチャンネルに追加
      const user_list = `${user_id},${getInviteSlackUsers()}`;
      const invite_res = inviteSlackUser(channel_res["channel"]["id"], user_list);
      console.log(JSON.stringify(invite_res));

      if(invite_res["ok"]){
        // 成功時のメッセージを追加
        result += `相談用チャンネル「${channel_name}(${channel_res["channel"]["id"]})」にユーザを追加しました。`;
      } else {
        // 失敗時はエラーを返す
        result += `ユーザの追加に失敗しました。 [エラーコード: ${invite_res["error"]}]`;
      }
    } else {
      // 失敗時はエラーを返す
      result += `チャンネルの作成に失敗しました。[エラーコード: ${channel_res["error"]}]`;
    }
    
    return ContentService.createTextOutput(result);
    
  } else {
    
    result = `無効なトークンです`
    return ContentService.createTextOutput(result);
    
  }
}

検証

GASを変更したあとは忘れずに保存→デプロイ→新しいデプロイで反映しておきます。

さて、再度スラッシュコマンドを使ってみると... 無事にプライベートチャンネルが作成されてユーザ追加も正常に実行されました! 意外と3秒以内に完了できるもんなんですね。

f:id:maru0014:20210117222454g:plain

エラー処理の確認

既にチャンネルを作成済みの状態で再度実行するとチャンネル作成エラーとしてエラーコードが返ってきます。今回の相談チャンネルは1ユーザ1チャンネルを想定していたためこの動作で問題ないですが、使い捨て前提で複数作れるようにする場合はチャンネル名に日時やランダムな文字列を追加するなどの工夫が必要でしょう。

f:id:maru0014:20210117221914g:plain

まとめ

  • 想像してたよりは簡単
  • Slackの3秒ルール以内には処理完了できる
  • group.createなどは2021年2月廃止予定なので注意
  • GASの新IDEはスクリプトのプロパティ設定画面がない(今後の改善に期待)

職場でSlack環境使っている訳ではないので今のところ出番無いですが、相談用チャンネルに限らずイベントのメンバー振り分けや、今は行かないでしょうけどランチメンバーのランダムセッティングなど応用できそうですね。

他にこんなことできますかーなどご意見いただければ検証してみるので、コメントまたはTwitterなどでもお気軽にご相談ください!