🤖

Slackの新着Timesチャンネルをお知らせするボットをつくりました

に公開

こんにちは、TRUSTDOCKのよもぎたです。

弊社はリモートワーク主体で、主なコミュニケーションの手段はSlackやZoomになります。Slackでは各人が自由に自分のTimesチャンネルを作成できます。今回は、新しくTimesチャンネルが作成されたらお知らせするbotを作成しました。これをどのように作ったか、記事にまとめます。

なお、本件は人事の方の「こういうのあったらいいかもね」という発言でアイディアを頂き、ノリと勢いが大切ということで即日コードを書いて、翌日自分のTimesチャンネルにリリースしてます。退勤後とお昼休みにやってます。仕事中に遊んでいるわけではありません。

アプローチ

概要

弊社はSlackのルールが定められており、Timesチャンネルは'z-times-'という接頭辞を付けるという命名規則があります。そこで、Slackのチャンネル一覧からその接頭辞で始まるチャンネルを抽出し、さらに前回との差分を比較して新しく追加された分だけをSlackメッセージとして送信することにします。

弊社の標準のオフィススイートはGoogle Workspaceで、「前回のTimesチャンネル一覧」の保管場所にGoogleスプレッドシートがちょうどいいので、Google Apps Scriptで実装します。

Timesチャンネル一覧の取得

Slackのconversations.list APIを使用して一覧を取得します。戻り値からアーカイブされたチャンネルを除外することはできるので、その機能を有効にします。チャンネル名でフィルタすることはできないようなので、一覧取得後にフィルタすることにします。

前回実行時との差分比較

ここは前回実行時の結果をGoogleスプレッドシートに保管し、実行時に取り出して比較することにします。当然、新しいTimesチャンネルがあったときは、実行の最後でスプレッドシートの内容をアップデートしていきます。

Slackチャンネルへのメッセージ送信

Slackのcat.postMessage APIを使ってメッセージを送信します。

条件、制限

1回のconversations.list API実行で取得できるチャンネル数は1000件となります。当社の規模では問題にならないので、ここはいったんスルーすることにします。

実行結果

こんな感じになります。

ボットを実行した結果

実装

Slackアプリの準備

ここは過去私が投稿したこちらの記事を参考にしました。

アプリに付与した権限は、

になります。

今回はアプリからチャンネルにメッセージを投稿するので、チャンネルにアプリを追加する必要があります。次の手順で行えます。

  1. アプリを追加したいSlackチャンネルを右クリックしてチャンネルの詳細を表示するをクリックします。
  2. 開いたダイアログでインテグレーションタブを選択します。
  3. Appセクションでアプリを追加するをクリックします。
  4. 追加したいアプリの追加ボタンをクリックします

スプレッドシートの準備

次のようなスプレッドシートを作成します。チャンネル一覧に変化があったときにここの一覧をアップデートするようにします。

チャンネル一覧を保持するGoogleスプレッドシート

いざGASで実装

何となく今回はmain()から各関数を呼びに行くスタイルになりました。あとconst宣言多めです。コメントでコードの解説をするスタイルで行きます。

main()

早速main()関数を見ていきます。

function main(){

  // SlackアプリのOAuth Tokenを代入します
  const slack_oauth_token = 'xoxb-deadbeef...';

  // 前回実行時のTimesチャンネル一覧を格納しているスプレッドシートのIDを代入します
  const channel_list_spreadsheet_id = '...';

  // 上記スプレッドシートで前回実行時のTimesチャンネル一覧を格納しているシートの名前を代入します
  const ss_sheet_name = '...';

  // メッセージを送信するチャンネルのIDを代入します
  // いまは自分のTimesチャンネルのIDを代入しています
  const dest_channel_id = 'C...';

  // 現在のTimesチャンネルの一覧を取得します
  const current_times_channels = get_current_times_channel_list(slack_oauth_token);

  // 前回実行時のTimesチャンネル一覧を取得します
  const prev_times_channels = get_prev_times_channels(channel_list_spreadsheet_id, ss_sheet_name);

  // チャンネルに送信するメッセージを組み立てます
  // この中で、前回と今回のTimesチャンネル一覧を比較しています
  let message = build_message(current_times_channels, prev_times_channels, slack_oauth_token);

  // messageの長さが0より大きい時は新着Timesチャンネルがあるので、メッセージを送信します
  if(message.length > 0){

    // 新着Timesチャンネルお知らせであることをメッセージに追加します
    message = '新着Timesチャンネルのお知らせです\n' + message;

    // 指定したチャンネルにメッセージを送信します
    post_message(slack_oauth_token, message, dest_channel_id);

    // Timesチャンネル一覧のスプレッドシートを更新します
    update_spreadsheet(channel_list_spreadsheet_id, ss_sheet_name, current_times_channels);
  }
}

get_current_times_channels()

現在のTimesチャンネルの一覧を取得します。

function get_current_times_channel_list(slack_oauth_token){

  // SlackのConversations.list APIのURLを代入します
  const slack_conversations_api_url = 'https://slack.com/api/conversations.list?exclude_archived=true&limit=1000';

  // API呼び出し時のヘッダをオプションとして代入します
  const headers = {'Authorization': 'Bearer ' + slack_oauth_token};
  const options = {'headers': headers};

  // 現在のチャンネル一覧を取得します
  // この時点では、Timesチャンネル以外のチャンネルも含まれています
  const channels = JSON.parse(UrlFetchApp.fetch(slack_conversations_api_url, options).toString()).channels;

  // Timesチャンネルの情報を取り出して格納する配列を宣言します
  let times_channesls = [];

  channels.forEach((channel) =>
  {
    // チャンネルの名前がTimesチャンネルの条件にマッチした時に、配列に情報を追加します
    if(channel.name.match(/^z-times-/)){
      // チャンネルのID、名前、作成者の配列を格納した二次元配列にしています
      // これは、もちろんそうする必要があったからですが、
      // こうすることで後でスプレッドシートに値をセットするときにそのまま利用できます
      times_channesls.push([channel.id, channel.name, channel.creator]);
    }
  }
  )

  // 関数の戻り値として現在のTimesチャンネルの一覧を返します
  return times_channesls;
}

get_prev_times_channels()

前回実行時のTimesチャンネルの一覧を取得します。

function get_prev_times_channels(channel_list_spreadsheet_id, ss_sheet_name){

  // 前回のTimesチャンネルの一覧を格納しているスプレッドシートを開きます
  const ssApp = SpreadsheetApp.openById(channel_list_spreadsheet_id);

  // その中から、Timesチャンネルの一覧を格納しているシートを取得します
  const sheet = ssApp.getSheetByName(ss_sheet_name);

  // 前回のTimesチャンネルの一覧を配列に格納します
  const prev_times_channel_list = sheet.getRange('A1:A' + sheet.getLastRow()).getValues().flat();

  // 前回実行時のTimesチャンネル一覧を返します
  return prev_times_channel_list;
}

build_message()

送信するメッセージを組み立てます。新着Timesチャンネルが無い場合は空文字列を返します。

function build_message(current_times_channels, prev_times_channels, slack_oauth_token){

  // 組み立てたメッセージを格納する変数を宣言します
  let message = '';

  current_times_channels.forEach((channel) =>
  {
    // チャンネルの情報のうち、IDと作成者を使うので、それぞれそれっぽい名前の変数に代入します
    let channel_id = channel[0];
    let channel_creator = channel[2];

    // チャンネルIDが前回実行時のTimesチャンネルのID一覧に無ければ新着としてメッセージを更新します
    if(!(prev_times_channels.indexOf(channel_id) > 0)){

      // メッセージに使うチャンネル作成者のDisplayNameを取得します
      let user_dispaly_name = get_user_display_name(slack_oauth_token, channel_creator);

      // メッセージを更新します
      message = message + user_dispaly_name + 'さんが <#' + channel_id + '> を開設されたようです\n';
    }
  }
  )

  // 組み立てたメッセージを戻り値として返します
  // 新着Timesチャンネルが無いときは空文字列です
  return message;
}

get_user_display_name

メッセージに使用するユーザーのDisplay nameを取得します。

function get_user_display_name(slack_oauth_token, slack_user_id){

  // SlackのUsers.info APIのURLを代入します
  const slackusers_info_api_url = 'https://slack.com/api/users.info?user=' + slack_user_id;

  // API呼び出し時のヘッダをオプションとして代入します
  const headers = {'Authorization': 'Bearer ' + slack_oauth_token};
  const options = {'headers': headers};

  // APIを呼び出し結果を代入します
  const response = JSON.parse(UrlFetchApp.fetch(slackusers_info_api_url, options).toString());

  // API呼び出し結果からDisplay Nameを取り出します
  user_display_name = response.user.profile.display_name;

  // Display Nameが未設定だったときは、代わりにReal Nameを取得して代入します
  if(user_display_name.length == 0){
    user_display_name = response.user.profile.real_name;
  }

  // 取得したDisplay Nameを返します
  return user_display_name;
}

Timesチャンネルを開設した方へのメンションにならないよう、Display Nameを使用しています。突然知らないチャンネルでメンションされたらびっくりしますからね。

post_message()

メッセージを指定されたチャンネルに送信します。

function post_message(slack_oauth_token, message, dest_channel_id){

  // SlackのChat.postMessage APIのURLを代入します
  const slack_api_url = 'https://slack.com/api/chat.postMessage';

  // API呼び出し時のヘッダをオプションとして代入します
  const headers = {'Authorization': 'Bearer ' + slack_oauth_token, 'Content-types': 'application/json; charset=UTF-8'};
  const payload = {'channel': dest_channel_id, 'text': message};
  const options = {'method': 'POST', 'headers': headers, 'payload': payload};

  // メッセージを送信します
  UrlFetchApp.fetch(slack_api_url, options);

  // この関数は戻り値を返しません
}

update_spreadsheet()

Timesチャンネルの一覧をGoogleスプレッドシートに格納(スプレッドシートを更新)します。

function update_spreadsheet(channel_list_spreadsheet_id, ss_sheet_name, current_times_channels){

  // 更新するスプレッドシートを開きます
  const ssApp = SpreadsheetApp.openById(channel_list_spreadsheet_id);

  // 更新するシートを取得します
  const sheet = ssApp.getSheetByName(ss_sheet_name);

  // 更新するシートの中身をclear()します
  // 新着を追記、ではなくて全部を更新してます
  sheet.clear();

  // シートの見出し行をセットします
  sheet.getRange('A1:C1').setValues([['channel_id', 'channel_name', 'creator']]);

  // シートに今回のTimesチャンネル一覧を格納します
  sheet.getRange('A2:C' + (current_times_channels.length + 1)).setValues(current_times_channels);

  // この関数は戻り値を返しません
}

コードは以上です。

最後に

弊社では活発に採用をしていて、おかげさまで毎月数人の方にご入社頂いています。
したがってTimesチャンネルは日々増えるのですが、今までは能動的に探しに行く必要がありました。
このbotがあれば、新着Timesチャンネルを教えてもらえるので、探しに行く必要が無くなります。やったね。

実はこれを作ったのは先週前半の話です。記事にするまでになんで時間がかかったかというと、やはり人に見せられる最低限のコードにしてたからですねー。最初に勢いで書いたコードは人に見せられません。黒歴史です。

最後までお読みいただきありがとうございました。

TRUSTDOCK テックブログ

Discussion