Slackの@メンションをGmailで転送する

Slackでの投稿をメールに転送するための実装です．
Gmail->Slackへの転送はいくつか記事にまとめていただいているものを見つけましたが，逆は見つからなかったので2024年8月時点の実装方法の備忘録を残します．

* 個別メンションされた場合は，メンション相手にだけ転送する
* @ channelでメンションされた場合は，メーリングリストに転送する

Google App Script（以下GAS）のリミットレートが50件/minであることetcから，少人数のワークスペースでしか流用できないかもしれません．また**とりあえず動けば何でもいいの精神で実装した**ので，あんまりちゃんとしたフローではないです．

# 1. Slack APIの設定

1.1. Slack Appの作成
* [Slack API](https://api.slack.com/)にアクセスして右上の「Your Apps」をクリック
* 「Create New App」をクリックして「From scratch」を選択
* アプリ名（なんでもよい）を入力して，導入したいワークスペースを選択して「Create App」をクリック

1.2. OAuth & Permissionsの設定
* 左側のメニューから「OAuth & Permissions」を選択
* 「Scopes」のセクションで、以下のスコープ（権限）を追加
    * channels:history: チャンネル内のメッセージの履歴を読み取る
    * channels:read: ワークスペース内のチャンネル情報を取得する
    * users:read: ワークスペース内のユーザー情報を取得する
    * users:read.email ワークスペース内のユーザーのメールアドレスを取得する
    * files:read: Slackにアップロードされたファイルを読み取

1.3.	変更を保存後に「Install App to Workspace」のセクションでアプリをワークスペースにインストール
1.4.	表示される「OAuth Access Token」をコピーしておく


# 2. GASの設定

2.1. [GAS](https://script.google.com/)にアクセスして新しいプロジェクトを作成
※ここでログインしているGoogleアカウントのGmailが転送メールの送信元になる

2.2. プロジェクト名を設定（なんでもよい）して，以下のコードを貼り付ける

<INS>以下の３箇所はそれぞれ書き換える<INS>
* *自分のSlack Botトークン*
* *ワークスペース上のユーザーのメアド*
* *メンバー全員が入っているメーリングリスト*

```Javascript:Javascript
function doPost(e) {
  let data = JSON.parse(e.postData.contents);
  
  if (data.challenge) {
    return ContentService.createTextOutput(data.challenge);
  }

  try {
    if (data.event && data.event.type === "message") {
    //*1
      var token = '自分のSlack Botトークンを貼り付け'
      var userRealName = getUserRealName(data.event.user, token);
      var channelName = getChannelName(data.event.channel, token);

      var inputData = {
        data: data.event.text,
        name: userRealName,
        label: channelName,
        URL: `https://slack.com/archives/${data.event.channel}/p${data.event.ts.replace('.', '')}`
      };

      sendSlackNotification(inputData, token);  // tokenを渡す
    }
  } catch (error) {
    return ContentService.createTextOutput("Error occurred: " + error.message);
  }

  return ContentService.createTextOutput("ok");
}


function getUserRealName(userId, token) {
  var url = `https://slack.com/api/users.info?user=${userId}`;
  var options = {
    headers: {
      'Authorization': `Bearer ${token}`
    },
    muteHttpExceptions: true 
  };
  
  var response = UrlFetchApp.fetch(url, options);
  var userData = JSON.parse(response.getContentText());

  if (userData.ok) {
    return userData.user.name;
  } else {
    throw new Error("Failed to fetch user real name: " + userData.error);
  }
}


function getChannelName(channelId, token) {
  var url = `https://slack.com/api/conversations.info?channel=${channelId}`;
  var options = {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  };
  
  var response = UrlFetchApp.fetch(url, options);
  var channelData = JSON.parse(response.getContentText());

  if (channelData.ok) {
    return channelData.channel.name;
  } else {
    return "Unknown Channel";
  }
}


function sendSlackNotification(inputData, token) {
  var inputMsg = inputData.data;
  var name = inputData.name;
  var label = inputData.label;
  var slackURL = inputData.URL;
  
  var to_addresses = []; 
  var subject = `Slack Message: ${label}`;
  var content;

  // 添付ファイルのリストを準備
  var attachments = [];

  // 投稿にファイルが添付されているか確認
  if (inputData.files && inputData.files.length > 0) {
    for (var i = 0; i < inputData.files.length; i++) {
      var file = inputData.files[i];
      var fileUrl = file.url_private;
      
      // ファイルをダウンロード
      var options = {
        headers: {
          'Authorization': 'Bearer ' + token
        }
      };
      var response = UrlFetchApp.fetch(fileUrl, options);
      var blob = response.getBlob().setName(fileName);  // ファイル名を設定
      
      // 添付ファイルリストに追加
      attachments.push(blob);
    }
  }

  if (inputMsg.includes('!channel')) {
    to_addresses.push('メンバー全員が入っているメーリングリストを入れる');
    content = `
${name}さんからSlackで@channelにメンションがありました。
*****
${inputMsg}
*****
${slackURL}
    `;
  } else if (inputMsg.includes('@')) {

    // *2
    // ここにワークスペース上のユーザーのメアドを入れる
    var wordList = [
      "example_1@gmail.com", 
      "example_2@gmail.com", 
      ...
      "example_N@gmail.com"
    ];

    inputMsg = inputMsg.replace(/<@([A-Z0-9]+)>/g, function(match, userId) {
      var realName = getUserRealName(userId, token);
  
      for (var i = 0; i < wordList.length; i++) {
        if (wordList[i].startsWith(realName + '@')) {
          to_addresses.push(wordList[i]);
          break;
        }
      }
  
      return '@' + realName;
    });

    content = `
${name}さんからSlackでメンションがありました。
*****
${inputMsg}
*****
${slackURL}
    `;
  }

  var to_address = null;
  if (to_addresses.length > 0) {
    to_address = to_addresses.join(',');
  }

  if (to_address) {
    if (attachments.length > 0) {
      // ファイルを添付してメールを送信
      GmailApp.sendEmail(to_address, subject, content, {
        attachments: attachments,
        name: `Slack転送 + ${name}`
      });
    } else {
      // 添付ファイルがない場合のメール送信
      GmailApp.sendEmail(to_address, subject, content, {
        name: `Slack転送 + ${name}`
      });
    }
  }
}
```

注釈
* *1　自分のSlack Botトークンは1.4.でコピーしたもの．
**実際はコードに直書きせずシークレットキーにすることがセキュリティ上推奨されます．**
実装する場合の参考：[GASでトークン等を保存しておけるプロパティサービスについてまとめてみた](https://qiita.com/zumi0/items/85ca400d57f60728a7c7)

* *2　Slackのチャンネル「メンバー -> 設定 -> メンバーのメールアドレスをコピーする」で一覧を取得できる．**これもコードに直書きせずシークレット化することが推奨されます．**

* ログ書き出しの関数を残していますが，GASは外部からHTTPリクエストでdoPost関数を呼び出すときに実行履歴にログが残りません．ログを監視したい場合は，例えばスプレッドシートと連携してログを書き出すなどで解決できる．
実装する場合の参考：[Google Apps ScriptでdoPost関数のログを出力する方法3選【GAS】](https://prtn-life.com/blog/gas-log-gcp)

2.3. GASプロジェクトのデプロイ

* 右上の「デプロイ」->「公開」->「ウェブアプリケーションとして導入」を選択
* 表示される「ウェブアプリケーションのURL」をコピーしておく

# 3. Slackイベントの設定
3.1 Slack AppとGASの連携
* Slack App設定ページの「Event Subscriptions」を選択
* 「Enable Events」を有効にする
* 「Request URL」に2.3でコピーしたGASの「ウェブアプリケーションのURL」を貼り付けて保存

3.2 Botをチャンネルに招待

Slackのワークスペースを開いて，メール転送を行いたい各チャンネルで「メンバー -> インテグレーション -> App -> アプリを追加する」から1.1で決めた名前のアプリを追加する．
* 今回の実装では，プライベートチャンネルのメッセージは転送できない

# 4. 動作確認
うまく行っていれば，こんな感じのメールが届きます
![スクリーンショット 2024-08-12 16.49.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3859391/8eae0360-94e7-7bbf-715b-339a323cf9d8.png)




# 5. MEMO
* ファイルの転送を行いたい気持ちだけコードに残っていますが，機能していません．このままでもファイルが転送できないだけで本文の転送はできるので一旦残しておいて今後解決したい．
* そもそもメールアドレスのリストをGAS側で保存しなくても，API叩くときにメンバーのメール一覧を取得できないはずなさそうだと直感的には思う．メンバーが増えるたびに手動での操作が必要になるのは面倒なので，暇なときに要検討．