GASでSlackのDMをBotから社員（有料）ユーザーだけに送る

この記事は、[リンクバルアドベントカレンダー2023](https://qiita.com/advent-calendar/2023/linkbal)の1日目の記事です。

## はじめに
Slackで同じ内容のDMを複数のユーザーに送りたいことってありますよね？
30人程度までなら手動でもいいですが、それ以上だとめんどうなので、GAS(Google Apps Script)を使って送る方法を紹介します。

GASでDMを送る方法は検索したりChatGPTに聞けばいろいろ出てきます。
が、今回は以下の要件があったので、少しスプレッドシートで工夫します。

- 社員ユーザーにだけ送りたい
    - 取引先のユーザーなどには送らない
- その他記事の本筋からずれる要件
    - スプレッドシートにメッセージのテキストを用意したい
    - 用意したテキストをランダムに送りたい
    - 定期的に送りたい

コードだけ知りたい方は[こちら](#コード)まで飛んでください！

## 準備
### 1 Slack Appの作成
https://api.slack.com/apps の 右上の `Create New App` ボタン から作成できます。

詳細は公式のヘルプや、以下のような記事を参考にしてください！

#### 参考記事
https://slack.com/intl/ja-jp/help/articles/13345326945043-Slack-%E3%81%AE%E9%96%8B%E7%99%BA%E8%80%85%E7%94%A8%E3%83%84%E3%83%BC%E3%83%AB%E3%81%A7%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E6%A7%8B%E7%AF%89%E3%81%99%E3%82%8B

https://dev.classmethod.jp/articles/post-messages-to-slack-channel/

https://zenn.dev/nyancat/articles/20211219-create-slack-app


#### botに必要なスコープ
- chat:write
- im:write

あとはtokenが必要なので、控えておいてください。


### 2 DM送信対象者のUser IDの取得
botからDMを送るには、送信対象ユーザーのSlackのUser IDが必要です。
Slackの画面から個別に取得することもできますが、人数が多いとしんどすぎるので、Analytics画面からCSVを取得します。

#### 1 Analytics画面のMembersを開く
https://linkbal.slack.com/stats#members です。

#### 2 `Edit Columns` で、必要な項目をチェックする＆不要な項目のチェックを外す
![Screenshot 2023-11-24 at 13.03.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/2ed12f36-8443-3cdf-2804-d754a7d41cd4.png)

最低限 `User ID` さえあれば送信できます
今回は管理と社員だけに送るために、画像のようにチェックしています。

![Screenshot 2023-11-24 at 13.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/7f67efd8-3674-2d4f-8730-7c6b7c7b0710.png)

`Included in bill` は、有料ユーザーがどうかです。当社の場合は、社員以外は基本的に有料ユーザーがいないので、このデータで社員判定をします。

もし社員以外にも有料ユーザーがいたり、有料ユーザーだけど社員でない人がいる場合などは、例えばスプレッドシートで特定のドメインがEmail列に含まれているか、など工夫しましょう。

#### 3 `Export CSV` でCSVをダウンロードする
データ量次第ですが、早ければ1,2秒でSlack BotからDMで送られます

![Screenshot 2023-11-24 at 13.03.21 copy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/ea72160e-9864-f874-bd77-9c5782700129.png)


### 3 スプレッドシートとGASの作成
なんでもいいのでスプレッドシートを1つ準備してください。

#### 1 先ほどのCSVをアップロードする
メニューの `ファイル` > `インポート` > `アップロード` でできます。

![Screenshot 2023-11-24 at 13.14.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/eff2a473-7655-b482-ae00-57aed49c8be5.png)

画像のモーダルが出るので、操作した状況に合わせて適切なものを選択してください。
基本的に `新しいシートを挿入する` か `現在のシートを置換する` のどちらかになります。

![Screenshot 2023-11-24 at 13.09.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/c10b9a2e-5982-243c-5562-c37e8d38177a.png)

#### 2 社員ユーザーだけに対象を絞る
CSVをインポートしたシートの名前が `Slackインポート用` だとします。

1つシートを追加して、A1に以下の関数を入れます。

```
=QUERY(
'Slackインポート用'!B:G,
"select
  C
where
  G = 1",
1
)
```

![Screenshot 2023-11-24 at 13.23.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/b17d88f2-db24-5364-dee7-a91c1ee31a9c.png)


やってることは単純で、QUERY関数で、CSVデータからG列が1の行のC列を抽出しているだけです。
最後の1は、ヘッダー行があることを表しています。

今回のCSVは、C列がUser IDで、G列が `Included in bill` です。

コメントをつけるとこんな感じです。

```
=QUERY(
'Slackインポート用'!B:G, // 対象範囲
"select
  C // 取得する列
where
  G = 1", // 取得する行の条件。G列が1の行
1 // ヘッダー行があることを明示
)
```

QUERY関数の公式ドキュメント
https://support.google.com/docs/answer/3093343?hl=ja

2 シートと紐づくGASのプロジェクトを作成する
メニューの `拡張機能` > `Apps Script` で、スプレッドシートに紐づいたGASのプロジェクトを作成しましょう。

![Screenshot 2023-11-24 at 13.14.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/007b977d-5ea8-a909-21a4-6f73c98133fb.png)


## GASの作成
記事の主題からはそれますが、、、
前提として、画像のようなデータから、分類をランダムで選び、その分類のテキストからランダムに送るようになっています。

![Screenshot 2023-11-24 at 13.18.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/90ef82d3-72e6-f83f-30ad-0af6c8408a4f.png)


### やること
1. GASのエディタに以下のコードを貼り付ける
    - 最初からある `myFunctio` などの3行のコードは不要なので消してOKです
1. ショートカットのctrl+sやcmd+sか、フロッピーディスクのアイコンで保存する

### コード
コメントで説明しています。

```js
// 特定ユーザーだけにしたテスト実行用
function doTest() {
  main('テスト用Slack_UserID抽出'); // 名前は実際のシートに合わせる
}

function main(targetSheetName) {
  const spreadsheet = SpreadsheetApp.getActive(); // スプレッドシートからスクリプト作成した前提

  const slackUserSheet = spreadsheet.getSheetByName(targetSheetName); // SlackのUserID記載のシート名を指定
  const targetUserIds = slackUserSheet.getDataRange().getValues().slice(1); // ヘッダー行を対象から外す

  // ========== ここから：ランダムにテキストを送るための実装 ==========
  // スプレッドシートからテキストデータを取得
  const textSheet = spreadsheet.getSheetByName('テキストシート名'); // 名前は実際のシートに合わせる
  const textRange = textSheet.getDataRange().getValues().slice(1); // ヘッダー行を対象から外す
  
  // 大分類ごとにテキストをグループ化
  const classifiedTextObjects = createClassifiedTextObjects(textRange);
  // ========== ここまで：ランダムにテキストを送るための実装 ==========

  targetUserIds.forEach(function(row) {
    const targetUserId = row[0];
    if (!targetUserId) return; // 空のセルは無視

    sendSlackDM(targetUserId, getSlackMessage(classifiedTextObjects)); // 定型文を送るだけなら、この第二引数に文字列をそのまま渡せばOK
  });
}

// メンバーにDMを送信する
function sendSlackDM(targetUserId, message) {
  const payload = {
    token: 'xxxxx', // 作成したAPIのtokne
    channel: targetUserId,
    text: message
  }

  const slackEndPoint = 'https://slack.com/api/chat.postMessage'
  const option = {
    "method" : "post",
    "payload" : payload
  }

  const response = UrlFetchApp.fetch(slackEndPoint, option)
  // Logger.log(response) // デバッグ
}

// 全UserID 誤って実行しないよう注意！！！！！
function doProd() {
  main('Slack_UserID抽出'); // 名前は実際のシートに合わせる
}


// ========== 以下はランダムにテキストを送るための実装 ==========

// テキストの種類とテキストそのもののオブジェクトを作成する
function createClassifiedTextObjects(range) {
  const classifiedTextObjects = {};

  range.forEach(function(row) {
    const textType = row[0];
    const textSentence = row[1];

    if (!classifiedTextObjects[textType]) {
      classifiedTextObjects[textType] = [];
    }
    classifiedTextObjects[textType].push(textSentence);
  });
  return classifiedTextObjects;
}

// テキストをランダムに返す
function getRandomTextType(classifiedTextObjects) {
  const textTypes = Object.keys(classifiedTextObjects);
  return textTypes[Math.floor(Math.random() * textTypes.length)];
}

// textType のテキストをランダムに返す
function getRandomTextSentence(classifiedTextObjects, textType) {
  const textSentences = classifiedTextObjects[textType];
  return textSentences[Math.floor(Math.random() * textSentences.length)];
}

function getSlackMessage(classifiedTextObjects) {
  const randomTextType = getRandomTextType(classifiedTextObjects);
  const randomTextSentence = getRandomTextSentence(classifiedTextObjects, randomTextType);

  const message = `定型文
${randomTextType}
${randomTextSentence}
定型文`;
  // Logger.log(message); デバッグ

  return message;
}
```

## テスト実行
`doTest` というテスト用の関数を作っています。

私は誤って2回テストのメッセージを全員に送ってしまったので、上記作成しました。
`doTest` のシートのA2とA3に、自分のSlackのUser IDを入れておきましょう。
2行入れておけば、DMが2通来たらテスト実行だとわかります。1通しか来なかったらミスってます...

画像の部分が `doTest` になっていることを確認したら、`実行` ボタンを押しましょう。

![Screenshot 2023-11-24 at 13.32.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/ef0e0160-3dd8-9c72-4c9d-f03cbdd40a58.png)

作成したSlack Appから、自分にDMが2つ来るはずです！

来ない場合は、以下を確認しましょう。

- シートの作り
- Slack Appの権限が足りているか
- Slack AppのTokneがGASに設定されたいるか
- GASでエラーが出ていないか

GAS初見だったりプログラミング未経験の方はトラブルシューティングが難しいので、どうしてもわからないときはプロフィール記載のSMSにメッセージなり送っていただければ、（多分）回答します！


## 本番実行（定期実行）
サイドメニューの `トリガー` > 右下の `トリガーを追加` > 画像のように設定して `保存`

![Screenshot 2023-11-24 at 13.36.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/ce17a742-44d7-9707-00ec-2d53a3de23f5.png)

![Screenshot 2023-11-24 at 13.37.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130946/4bf4a45e-5aa3-f52b-7d42-07f159b2c7d7.png)

重要なポイント

- `実行する関数` は `doProd` を選択すること
- `イベントのソースを選択` やそれ以降の設定は、適切なものを選択すること

こうすれば、設定した内容で定期実行されます。

## おわりに
おつかれさまでした！