🤖

【GAS】Slack App(Bot)へのDMを他のチャンネルに自動転送する

2024/11/10に公開

はじめに

Slack Botを利用して、DMで受け取ったメッセージや画像を特定のチャンネルに自動転送する機能を実装しました。この記事では、実装方法と必要なSlack Botの権限設定について解説します。

前提条件

  • SlackワークスペースへのBotアプリ作成済み
  • DMを転送したいチャンネルにBotを追加済み
  • BotにDMを送れるようにSlack App ページ内で設定済み
    • AppHome > Show Tabs > Messages Tab > Allow users to send Slash commands and messages from the messages tab にチェック

必要な権限

Slack APIを利用するために、Botに以下のOAuth権限を付与します。

  • channels:read : チャンネル情報の取得
  • channels:join : チャンネルへの参加
  • chat:write : チャンネルへのメッセージ投稿
  • files:write : ファイルの投稿
  • files:read : ファイルの取得
  • im:read : DMメッセージの取得

これらの権限を追加することで、BotはDMで受信したメッセージや画像を指定したチャンネルに転送できるようになります。

コードの説明

以下は、Slack BotがDMで受信したメッセージや画像を特定のチャンネルに転送するコードです。

1. 初期設定

まず、Slack BotのOAuthトークンを取得し、Slack APIのライブラリを設定します。

main.gs
// Slack Bot User OAuth Token
const SLACK_API_TOKEN = 'xoxb-XXXXXXXXX-XXXXXXXXX-XXXXXXXXX';

2. チャンネルのID設定

DMからのメッセージを転送するチャンネルのIDを設定します。

main.gs
const ChannelId_Bot = 'D0XXXXXXXXX'; // SlackAppチャンネル
const ChannelId_Trf = 'C0XXXXXXXXX'; // DM転送先チャンネル

3. メインのイベントハンドリング関数

doPost関数は、Slackからのイベント(メッセージ受信など)を処理します。DMからメッセージを受け取った場合、そのメッセージやファイルを指定したチャンネルに転送します。

main.gs
function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const event = json.event;

  const text         = event.text;
  const eventType    = event.type;
  const eventChannel = event.channel;
  const channelType  = event.channel_type;
  
  // キャッシュ処理: 同一メッセージの重複処理防止
  const cache = CacheService.getScriptCache();
  if (cache.get(json.event.client_msg_id) === 'done') {
    return ContentService.createTextOutput();
  } else {
    cache.put(json.event.client_msg_id, 'done', 600); // キャッシュを600秒保持
  }

 // メッセージイベントかつ特定のチャンネルでのDMイベントの場合に処理
  if (eventType === 'message' && eventChannel === ChannelId_Bot && channelType === 'im') {
      postToSlack(text, event.files);
  }
  return ContentService.createTextOutput();
}

4. メッセージとファイルの転送関数

postToSlack関数は、メッセージやファイルを転送先チャンネルに投稿するための関数です。ファイルの場合は、ファイルのダウンロードとアップロードURLの取得、再アップロード処理を行います。

modules.gs
function postToSlack(text, files) {
  if (files && files.length > 0) {
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      const fileUrl = file.url_private;

      // step1:ファイルをSlackからダウンロード
      const response = UrlFetchApp.fetch(fileUrl, {
        headers: {
          'Authorization': 'Bearer ' + SLACK_API_TOKEN
        }
      });
      const blob = response.getBlob();
      const fileSize = blob.getBytes().length;

      // step2:アップロードURLを取得
      const uploadUrlResponse = UrlFetchApp.fetch(`https://slack.com/api/files.getUploadURLExternal?filename=${encodeURIComponent(file.name)}&length=${fileSize}`, {
        method: 'get',
        headers: {
          'Authorization': 'Bearer ' + SLACK_API_TOKEN,
          'Content-Type': 'application/json; charset=utf-8'
        },
        muteHttpExceptions: true
      });
      const uploadUrlResult = JSON.parse(uploadUrlResponse.getContentText());

      if (!uploadUrlResult.ok) {
        continue;
      }
      const uploadUrl = uploadUrlResult.upload_url;
      const fileId = uploadUrlResult.file_id;

      // step3:ファイルをアップロード
      const uploadFileResponse = UrlFetchApp.fetch(uploadUrl, {
        method: 'post',
        payload: blob,
        muteHttpExceptions: true
      });

      if (uploadFileResponse.getResponseCode() !== 200) {
        continue;
      }

      // step4:アップロード完了通知
      const completeUploadResponse = UrlFetchApp.fetch('https://slack.com/api/files.completeUploadExternal', {
        method: 'post',
        headers: {
          'Authorization': 'Bearer ' + SLACK_API_TOKEN,
          'Content-Type': 'application/json; charset=utf-8'
        },
        payload: JSON.stringify({
          files: [{ id: fileId, title: file.name }],
          channel_id: ChannelId_Trf,
        }),
        muteHttpExceptions: true
      });

      const completeUploadResult = JSON.parse(completeUploadResponse.getContentText());

      if (!completeUploadResult.ok) {
        Logger.log('ファイルのアップロード完了通知に失敗しました: ', completeUploadResult.error);
      }
    }

  } else { // ファイルがない場合はテキストを転送
    const messageResponse = UrlFetchApp.fetch('https://slack.com/api/chat.postMessage', {
      method: 'post',
      contentType: 'application/json',
      headers: {
        'Authorization': 'Bearer ' + SLACK_API_TOKEN
      },
      payload: JSON.stringify({
        channel: ChannelId_Trf,
        text: text
      })
    });

    const messageResult = JSON.parse(messageResponse.getContentText());
    if (!messageResult.ok) {
      Logger.log('メッセージの投稿に失敗しました: ', messageResult.error);
    }
  }
}

まとめ

このコードを利用することで、Slack BotはDMで受け取ったメッセージや画像を特定のチャンネルに転送できるようになります。私はこれで匿名で話せるチャンネルを作りました。やはり、言いにくいことを言える場を作るのも大事だと思います。

生成AIでこのコードをほとんど書いたのですが、使えないfiles.upload APIを紹介してきたり、step3:ファイルのアップロードのmethodをなぜかputにしてきたりしました。生成AIによるコーディングでエラー吐いたら、ちゃんと公式ドキュメントを読むようにしましょう...

参考

https://zenn.dev/slack/articles/7ce5065cc4daa7
https://api.slack.com/methods

Discussion