Slackの古い投稿を自動定期削除する

# やりたいこと
Slackの特定のチャンネルに投稿された発言を定期的に削除したい

# 仕様とか
* 投稿から一定時間経過したSlackの発言を削除する
* 一度に削除できる投稿数には上限がある (https://api.slack.com/lang/ja-jp/rate-limit)
* サーバーレス (GASを利用)
* パブリック・プライベートチャンネル両方対象
* ユーザー権限で実行するためSlackAppのTokenを発行するユーザー自身に投稿削除権限が必要

# 手順
## Slack側の設定

### 1) Appの作成

Slack API のサイトに行き、右上の `Your apps` からApp管理ページを開く
https://api.slack.com/
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/c3b39631-16f8-ecac-8e7f-64d4e52cbe1e.png" width=800">

`Create New App` からAppの作成画面を開く
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/a351488b-f9f0-007f-6a66-875ab8cf3a82.png" width=800">

`From scratch` を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/a58f1ef9-fb41-99a0-4485-d80e58666fed.png" width=500">

App Name は適当に
`Pick a workspace to develop your app in:` でAppを追加したいワークスペースを選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/1245c38d-a6b7-647f-a975-70a609f62b77.png" width=500">

### 2) 必要な権限の追加

`Basic Information` の `Add features and functionality` のプルダウンの中に `Permissions` というのがあるので選択する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/af59a0a6-e82c-d0bf-7ccf-e666ab8f3591.png" width=500">

`Scopes` の項で `User Token Scopes` の `Add an OAuth Scope` を選択する
**※Botではない方**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/4d6c5f06-08a8-a923-3eda-e7c4036d9480.png" width=500">
プルダウンから必要な権限を選んで追加していく

* channels:history
* channels:read
* chat:write
* groups:history
* groups:read

権限が追加されたことを確認
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/a032cbb2-0536-d2f0-f6cd-64b258a17aab.png" width=500">

### 3) Appのインストール

権限が追加できたら左カラムのメニューから `Basic Information` を選択して基本情報画面に戻ります
`Install your app` のプルダウンを開き `Install to Workspace` を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/cfca5dc7-3bec-ece2-444e-3b7c501683f5.png" width=800">

確認画面にリダイレクトされるので `許可する` を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/f19ab63c-556f-0164-437e-47dfbc8c7a45.png" width=500">

`Install your app` の項目にチェックが付けばOK
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/dd3da88b-7b47-a186-53db-1cd9f7084415.png" width=500">

### 4) Tokenの取得

Appのインストールが完了したら左カラムのメニューから `OAuth & Permissions` へ遷移する
`OAuth Tokens for Your Workspace` という項目に先ほど作成した `User Token Scopes` の `OAuth Token` があるのでエディタなどにコピーして控えておく
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/4dbf7f86-65b4-1750-dfc8-9d95df66ccfa.png" width=800">

## Google Spreadsheet側の設定

### 1) 自動削除したいチャンネル一覧を作成する

Google Spreadsheetで新しいスプレッドシートを作成する
https://docs.google.com/spreadsheets/
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/50069581-5a96-f595-33c3-ee5710fa193a.png" width=200">

自動削除をしたいチャンネルを記入する
シート名はなんでもOK
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/d5900a1e-e2f9-c2f7-91d0-5a1c55bf81e6.png" width="300">

### 2) スクリプトの作成

メニューから `ツール` > `スクリプト エディタ` を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/4380f4bb-cb8e-5f57-52b2-2c8f6951ceff.png" width="400">

スクリプトに適当なファイル名をつける
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/9d0b3b60-f077-7d97-ff21-c2cce5049e12.png" width="400">

以下のコードを貼り付ける
1行目の `token` にはSlackの設定で作成した `User OAuth Token` をセットする

```javascript:cleaner.gs

const token = 'xoxp-23********177-23********317-23********648-e6bd********e7********a********c';
const hours = 8; // 何時間遡るか
const limit = 3; // 最大追加取得回数
const second = 0.8; // 何秒ごとに削除を実行するか

/**
 * main
 */
function main() {
  const values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();

  const channelNames = [];
  for (const value of values) {
    channelNames.push(value[0]);
  }
  for (const channelName of channelNames) {
    Logger.log("[%s]",channelName)
    cleanChannel(token, channelName);
  }
}

/**
 * チャンネル内投稿削除
 */
function cleanChannel(token, channelName) {
  Logger.log('*** cleanChannel start');

  const channelId = getChannelId(token, channelName);
  Logger.log("channelId: #" + channelName + ' | ' + channelId)
  if (channelId.length === 0) {
    return;
  }

  const date = new Date();
  const timestamp = Math.round(date.setHours(date.getHours()-hours) / 1000); // 設定した時間前
  Logger.log("timestamp: " + timestamp + ' | ' + new Date(timestamp * 1000).toString());

  let result;
  let count = 0;
  do {
    result = getChannelHistory(token, channelId, timestamp);
    count++;
    if (result.ok) {
      for (const message of result.messages.reverse()) {
        const deleteResult = deleteChat(token, channelId, message.ts);
        if (deleteResult.ok) {
          Logger.log("delete success");
        }
        Utilities.sleep(second * 1000); // error429回避のため一定時間sleep
      }
    }
  } while (result.ok && result.has_more && count < limit)
}

/**
 * チャンネル名 -> チャンネルID取得
 */
function getChannelId(token, channelName) {
  Logger.log('*** getChannelId start');

  const result = getChannelList(token);
  if (result.ok) {
    for (const channel of result.channels) {
      if (channel.name === channelName) {
        return channel.id;
      }
    }
  }
  return '';
}

/**
 * チャンネル一覧取得
 */
function getChannelList(token) {
  Logger.log('*** getChannelList start');

  const endpoint = 'https://slack.com/api/conversations.list';
  const options = {
    method: 'post',
    payload: {
      token: token,
      types: 'public_channel,private_channel'
    }
  };
  const response = UrlFetchApp.fetch(endpoint, options);
  Logger.log(JSON.parse(response));
  return JSON.parse(response);
}

/**
 * チャンネル内投稿取得
 */
function getChannelHistory(token, channelId, timestamp) {
  Logger.log('*** getChannelHistory start');

  const endpoint = 'https://slack.com/api/conversations.history';
  const options = {
    method: 'post',
    payload: {
      token: token,
      channel: channelId,
      latest: timestamp + '.00000',
      inclusive: true
    }
  };
  const response = UrlFetchApp.fetch(endpoint, options);
  Logger.log(JSON.parse(response));
  return JSON.parse(response);
}

/**
 * 投稿削除
 */
function deleteChat(token, channelId, timestamp) {
  Logger.log('*** deleteChat target: ' + timestamp);
  const endpoint = 'https://slack.com/api/chat.delete';
  const options = {
    method: 'post',
    payload: {
      token: token,
      channel: channelId,
      ts: timestamp
    }
  };
  let response = UrlFetchApp.fetch(endpoint, options);
  Logger.log(JSON.parse(response));

  // error429が出た場合は指定秒数sleepさせる
  while (!response.ok && response.status == 429) {
    const retryAfter = response.headers['retry-after'];
    Utilities.sleep(retryAfter * 1000);
    response = UrlFetchApp.fetch(endpoint, options);
    Logger.log(JSON.parse(response));
  }
  return JSON.parse(response);
}

```

### 3) トリガーの設定

左のメニューから `トリガー` を選択する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/43187530-ba93-260e-99de-b040882e36e3.png" width="300">

画面右下の `+ トリガーを追加` ボタンを押す
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/dd63ca7f-a869-398c-c15b-7b1375db9e86.png" width="200">

定期実行の設定を行う
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54083/ebd031f1-ef70-e02b-56ed-962aae0d3431.png" width="700">


# 参考にした情報
基本的なロジックは以下を参考にさせていただき、現在のAPI仕様に対応していなかったり一部動作しない箇所を修正しました。
https://qiita.com/hideo-works/items/21c2a6784b7f6caa4c06
https://tech.naturalmindo.com/notwork_slackapi/


以上