Slack-botから送られてきたメッセージを削除する

# はじめに
Slack-botを開発していると，テストチャンネルがBotからのメッセージで溢れたり，またはボットのメッセージが作成した本人でも権限がなくて消せなかったりする際にAPI経由で消すと楽だったのでメモとして残しておきたいと思います．
（入退室管理のボットを作った程度の初心者です．．．）

また，アプリの作成や必要な権限などについては明記してませんが，適宜APIのリンクから確認していただければと思います．
さらに，APIを呼ぶときのPythonのコードも記載するのですが，今回はいつもお世話になっているPythonの[Bolt](https://slack.dev/bolt-python)のWebClient経由で呼んでいます．結局は[Slack-SDK](https://github.com/slackapi/python-slack-sdk)のWebClientで送ってるのでBoltを使ってないときはSDKのWebClinetやAPIに直接POSTする方法などに置き換えてもらえたら幸いです．


## 実行環境
```
Python: 3.9.5 (>=3.6)
必要なモジュール: slack_bolt
```

# Botのメッセージを削除する
まずBotが送信したメッセージを削除するときは[chat.deleteメソッド](https://api.slack.com/methods/chat.delete)を使います．
必要なのはBotのトークン（``xoxb-xxxx-xxxx-xxxx``）とチャンネルのID，メッセージのタイムスタンプです．この3点がわかればいいので，一番簡単な方法としてはメッセージのリンク（``https://hogehoge.slack.com/archives/<channnelID>/p<timestamp>``）をコピーしてそのリンクを元にリクエストを投げるという形です．

タイムスタンプは後ろから6桁目にピリオドを付与したものを用います．例えば``1234567891011120``の場合は``1234567891.011120``をリクエストで使います．
試す方法は[chat.deleteメソッドのテスター](https://api.slack.com/methods/chat.delete)に記述するのが手っ取り早いですが，個人的にチャンネルIDやタイムスタンプを手作業で抜き出すのが面倒だったので以下のようなPythonスクリプトを使っています．（リンクを直接指定したいときとかは結構便利かもしれません）

```deleteMessage.py
import os, sys
from slack_bolt import App

args = sys.argv
try:
    url = args[1]
    # スレッド時のクエリを削除
    if url.find("?") != -1:
        url = url[:url.find("?")]
    split_url = url.split("/")
    channel_id = split_url[-2]
    timestamp_orginal = split_url[-1].lstrip("p")
    timestamp = "{0}.{1}".format(timestamp[:-6], timestamp[-6:])
except Exception as e:
    print("ERROR")
    print(e)
    sys.exit(1)

app = App(token=os.environ["SLACK_BOT_TOKEN"])
res = app.client.chat_delete(
    channel=channel_id,
    ts=timestamp,
)

print(res)

# $ python3 deleteMessage.py 
# $ https://hogehoge.slack.com/archives/<channnelID>/p<timestamp>
# >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp>'} 
```

# 直近に送信したBOTのメッセージを削除する
リンクをコピーしてPythonスクリプトを実行するというのは特定のメッセージを削除するときに有用です．しかし，それ以外のときは楽なようで全く楽じゃなくて，例えば「直近から3件削除したい」みたいなときにいちいちリンクをコピーして実行を繰り返すのは流石に手間です．そこで，チャンネルのメッセージIDすら自動で取得して消せたら楽なのにと思いAPIを探しました．

ここで使うのは，[conversations.historyメソッド](https://api.slack.com/methods/conversations.history)と[conversations.repliesメソッド](https://api.slack.com/methods/conversations.replies)の2つです．
後者は，ボットが生成したスレッド（ボットのみが返信をしているスレッド）を削除するときに使うのですが，以下のような流れで作成されているときを想定しています．

```
例．入退室管理ボット
（チャンネルへ投稿されたメッセージ...）
　|
　部屋解錠メッセージ
　|    |--- メッセージ1　[入室]　Aさん
　|    |--- メッセージ2　[退室]　Aさん
　|    |--- メッセージ3  部屋施錠メッセージ
　|
　部屋施錠メッセージ（reply_broadcast）
```

また，**conversations.historyメソッド**と**conversations.repliesメソッド**を呼び出す上で最低限必要なのはボットのトークンとチャンネルのIDだけです．これらのメソッドを使って取得したメッセージに含まれるタイムスタンプを元に**chat.deleteメソッド**にリクエストを送りメッセージを削除します．

## 通常のメッセージのみを削除する
ここでの通常のメッセージとは，上記の例における``部屋解錠メッセージ``や``部屋施錠メッセージ（reply_broadcast）``などのチャンネルに送信されたメッセージを指します．スレッドが生えている場合，通常メッセージを削除してもそのスレッドへの返信までは削除されず残ります．

**conversations.historyメソッド**を使えばチャンネルに送信されたメッセージの履歴を取得できます．これを利用して最新のメッセージのタイムスタンプを取得することで**chat.deleteメソッド**にリクエストを送ります．
特に，**conversations.historyメソッド**のAPIに記載されている通り，取得するメッセージの件数（limit）はデフォルトで100になっているので，これを1に設定することで最新のメッセージのみを取得します．
取得したメッセージのJSONの中にある``ts``キーがメッセージのタイムスタンプなので，これを用いて**chat.deleteメソッド**にリクエストを投げます．

### conversations.historyメソッドに関する余談
limitを1に設定することで最新を取得すると記載しましたが，limitをどの値に設定したとしてもmessagesというキーの配列の中にメッセージが格納されているので必須というわけではありません．しかし，今回最新のメッセージ以外はすべて不要なので1に設定することをお勧めします．

また，APIを見るとlatestやoldestというオプションがあります，これをうまく使えばより柔軟な操作ができます．

## スレッドのメッセージを削除する
スレッドを削除する場合は，はじめに記載した通り，通常メッセージを削除するだけではスレッドに生えたリプライまでは削除されません．
そこで．**conversations.repliesメソッド**を用いて親のメッセージを起点としてスレッドに対して返信されたメッセージのタイムスタンプを取得し，それらのタイムスタンプを使って**chat.deleteメソッド**を呼び出すというアプローチをとります．そうすることで，スレッドに関係したメッセージも含めて丸ごと削除できます．
ここでスレッドに関係したメッセージというのは，スレッドの親となるメッセージ，スレッドに返信されたメッセージ，スレッドに返信しかつチャンネルにも送信されたメッセージ（``reply_broadcast=True``として送信したメッセージ）を指します．


流れとしては以下のような手順です．
1. **conversations.historyメソッド**を使って最新のメッセージを取得する
2. スレッドに関係したメッセージかどうか判断する
  　- スレッドに関係ないならばメッセージのタイムスタンプを使って**chat.deleteメソッド**にリクエストを投げる
  　- スレッドに関係したメッセージならば**conversations.repliesメソッド**を用いてスレッドに関係したすべてのメッセージを取得する
3. スレッドに関係したすべてのメッセージそれぞれのタイムスタンプを使って**chat.deleteメソッド**にリクエストを投げる

スレッドに関係したメッセージかどうかを判断するためには，今回は``reply_count``キーと``subtype``キーに注目しました．``reply_count``キーはスレッドが生えている親のメッセージに対して付与されているキーで，``subtype``キーは``reply_broadcast=True``として送信されたメッセージに対して付与されるキーです．これらがメッセージの情報に含まれている場合にはスレッドに関係したメッセージとして判断します．


## 上記の二つの削除をまとめたPythonコード

```deleteMessageNow.py
import os, time
from slack_bolt import App

channel_id = os.environ["SLACK_CHANNEL_ID"]
app = App(token=os.environ["SLACK_BOT_TOKEN"])

def deleteMessage(channel, ts):
    return app.client.chat_delete(
        channel=channel_id,
        ts=ts,
    )
    
def deleteThreadMessage(channel, thread_ts):
    # https://api.slack.com/methods/conversations.replies
    messages = app.client.conversations_replies(
        channel=channel_id,
        ts = thread_ts,
    )["messages"]
    
    responses = list()
    # 後ろから順番に削除していく
    for message in reversed(messages):
        responses.append(deleteMessage(
            channel, 
            message["ts"],
        ))
        time.sleep(0.5) # 一応待ちます．
    return responses

def run():
    # https://api.slack.com/methods/conversations.history
    result = app.client.conversations_history(channel=channel_id, limit=1)

    message = result["messages"][0]
    # スレッドに関係したメッセージかどうか
    if "reply_count" in message.keys() or "subtype" in message.keys():
        # thread_tsが親のタイムスタンプ
        return deleteThreadMessage(channel_id, message["thread_ts"])
    
    # それ以外は通常メッセージなのでそのまま消す
    ts = message["ts"]
    return [deleteMessage(channel_id, ts)]

if __name__ == "__main__":
    responses = run()
    for res in responses:
        print(res)

# スレッドの場合
# $ python3 deleteMessageNow.py
# >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp3>'} 
# >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp2>'} 
# >> {'ok': True, 'channel': '<channnelID>', 'ts': '<dotted timestamp1>'} 
```

## シェルスクリプトと上記のPythonコードを組み合わせる
直近から3個削除したいときに``deleteMessageNow.py``を3回実行するのは面倒ですので，シェルスクリプトと組み合わせることでとっても楽に消せるようになります．

```run_deleteMessageNow.sh
#!/bin/sh

echo "直近何回分消しますか？":

read num
echo "OK! running deleteMessageNow.py [$num]"

# 入力された回数だけloopを回す
for i in `seq 1 $num`
do
    python3 deleteMessageNow.py
done
```


# 参考URL
slack-api
　- [chat.delete](https://api.slack.com/methods/chat.delete) 
　- [conversations.history](https://api.slack.com/methods/conversations.history)
　- [conversations.replies](https://api.slack.com/methods/conversations.replies)
Qiita記事
　- [[Slack]自作したBotのメッセージを削除する](https://qiita.com/mgmgmogumi/items/4171143d5d2811d37f1a)
　- [Slack APIのchannels.historyが使えなくなる対策](https://qiita.com/yomori/items/d9242129ebbae1f1c6ff)
その他
　- [Bolt for Python](https://slack.dev/bolt-python)
　- [Bolt for Python (Github)](https://github.com/slackapi/bolt-python)
　- [Python Slack SDK](https://slack.dev/python-slack-sdk/)