Discord のリアクションに反応し通知する Bot


## はじめに
Discord の返信や応答などに使われるリアクションに気づかないことがあり不便なため、リアクションに反応して通知してくれる Bot を作りました。ユーザがリアクションすると Bot がメッセージ主にメンションする流れになっています。なお、NITRO に入ることで使えるようになる他サーバのカスタム絵文字には対応していません。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/54a2de5e-a96a-3964-c84e-80a5daf9b0b7.png)
以下、ソースコードになります。

https://github.com/Pumi1a/ReactionDetect

## 環境
* Raspberry Pi 4 Model B
    * CentOS Stream 8
        * Python 3.11.0
            * discord.py 2.1.0
* <a href="https://replit.com/">Repl.it</a>
    * Ubuntu 20.04.2 LTS
        * Python 3.8.12
            * discord.py 2.1.0

Raspberry Pi に CentOS Stream 8 を入れて Bot を動かしています（2023年2月現在、品薄状態でかなり高騰しています）。自前サーバを持っていない方は、無料のホスティングサービスを選択するのがいいでしょう。Raspberry Pi のセットアップ方法と、ホスティングサービスの使い方を記事にまとめたので参考にしてみて下さい。

https://qiita.com/Pumila/items/4f73263025ffbc427e90

https://qiita.com/Pumila/items/29f26fb349d5592046ae


## 導入

### Bot アカウントの作成
<a href="https://discord.com/developers">Discord Developer Portal</a> より Bot アカウントを作成しましょう。詳細な手順については既にまとまっている記事があるため省きます。以下の記事を参考にして下さい。
実装においてアクセストークンが必要になります。メモしておきましょう。

https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf

### Botアカウントの設定
メッセージ取得のために Bot の追加設定が必要になります。<font color="red"> この設定を行わないとメッセージが取得できません。</font><a href="https://discord.com/developers">Discord Developer Portal</a> から対象の App を選択し、[Bot] の項目の `Privileged Gateway Intents` より `MESSAGE CONTENT INTENT` を有効化します。設定を保存するのを忘れないようにしましょう。
<img width="600" alt="キャプチャ.PNG]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/77c9d2c6-1651-0f2a-7c09-fe47a454af5f.png">


### サーバの設定
リアクションに反応しメンションを送るたびに、メッセージと同じチャンネルにメンションが送られるのはサーバを運用する上で不便かと思います。そこで Bot 専用のチャンネルを作成し、そこにメンションを送信するようにしました。さらに、そのチャンネルの通知設定を`@メンションのみ`にすることで、自分宛てのリアクションのみ通知させることが可能になります（他人へのリアクションは通知しない）。
実装において該当のチャンネル ID が必要になります。チャンネル ID を取得するためには、Discord の詳細設定より`開発者モード`を有効化し、チャンネルを右クリックすることで取得できます。メモしておきましょう。
![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1115291/4eb8e0f0-a9f6-e6bc-e6df-d7b9698e2a1c.png)

### コードの作成と実行
ここからは Python のソースコードになります。まずは <a href="https://github.com/Rapptz/discord.py"> discord.py</a> をインストールします。
```console:discord.pyのインストール
$ python3 -m pip install -U discord.py
```

実装に関してですが二通りの方法があります。違いとしてはキャッシュを利用するかしないかになります。それぞれ、`on_reaction_add()` メソッドと `on_raw_reaction_add()` メソッドになります。これらのメソッドはメッセージにリアクションが追加された時に呼ばれるメソッドです。
まずはキャッシュを利用する `on_reaction_add()` メソッドについて説明していきましょう。このメソッドは、Bot のキャッシュに保存されているメッセージに対しリアクションされた時に呼び出されます。キャッシュに保存されるメッセージは Bot が起動した以降のものになります。つまり、Bot が起動する以前のメッセージに対するリアクションには反応できません。__定期的に Bot が再起動されるホスティングサービスでは実用的ではないでしょう__ （Heroku, Repl.it など）。また、保存されるメッセージキャッシュの最大数はデフォルトで 1,000 になります。
以下に `on_raw_reaction_add()` を利用したコードを載せます。注意として discord.py ver2.0 以降からメッセージ取得のために `intents.message_content = True` とする必要があります（キャッシュを利用する場合のみコードの記述が必要な模様）。適宜変更して下さい。


```python:reaction-detect.py
#!/usr/bin/env python3
import discord

# 作成した Bot のアクセストークンに置き換えて下さい
TOKEN = 'YourAccsessToken'
# クライアント接続
intents = discord.Intents.default()
# discord.py Ver2.0 以前ではコメントアウトして下さい
intents.message_content = True
client = discord.Client(intents=intents)

# 起動時の処理（無くても問題ない）
@client.event
async def on_ready():
    print('ログインしました')

# リアクションされた時に動作する処理
@client.event
async def on_reaction_add(reaction, user):
# リアクションされたメッセージ
    message = reaction.message
# 自分自身に対するリアクションは通知しない
    if (message.author == user):
        return
# 左からリアクションされたメッセージの投稿主、リアクション、リアクションしたユーザの表示名、
# メッセージの内容、メッセージへのリンク
    msg = f"{message.author.mention} {reaction}\nFrom:{user.display_name} \
          \nMessage:{message.content}\n{message.jump_url}"
# Bot 専用のチャンネル ID に置き換えて下さい
    CHANNEL_ID = 435155995647209
    channel = client.get_channel(CHANNEL_ID)
# メンションを指定のチャンネルに投稿
    await channel.send(msg)

client.run(TOKEN)
```
メッセージ内容は f 文字列を使用しています。適宜改変して下さい。追加したい情報などがある場合は、<a href="https://discordpy.readthedocs.io/ja/latest/api.html">Discord.py API リファレンス</a>を参照してみて下さい。
このコードを実行すると、ユーザがメッセージにリアクションした場合、指定したチャンネルにメンションが送られます。

続いて、キャッシュを利用しない方法について説明していきます。`on_raw_reaction_add()` メソッドは、キャッシュに関係なく全てのリアクションに対し呼び出されます。つまり、Bot を稼働させているサーバが定期的に再起動するような場合でも、リアクションに反応することができます（再起動している間のリアクションには反応できませんが…）。キャッシュではなく生のデータを使用しているという意味で "raw" という単語が含まれています。引数には <a href="https://discordpy.readthedocs.io/ja/latest/api.html#discord.RawReactionActionEvent">`RawReactionActionEvent`</a> というクラスのオブジェクトを受け取ります。このクラスで定義されている属性は以下のとおりです。
* channel_id
* emoji
* event_type
* guild_id
* member
* message_id
* user_id
`on_reaction_add()` メソッドの引数と違い、こちらの引数では message オブジェクトを直接取得することができません（必要最低限の情報しか持っていないため）。そこで、`message_id` を用いて非同期処理を行い、message オブジェクトを取得する必要があります。手順としては以下の通りです。
```python:message オブジェクトの取得
async def on_raw_reaction_add(payload):
    txt_channel = client.get_channel(payload.channel_id)
    message = await txt_channel.fetch_message(payload.message_id)
```

以下に `on_raw_reaction_add()` メソッドを利用したコードを載せます。

```python:reaction-detect2.py
#!/usr/bin/env python3
import discord

TOKEN = 'YourAccsessToken'

intents=discord.Intents.none()
intents.reactions = True
intents.guilds = True
client = discord.Client(intents=intents)

@client.event
async def on_ready():
    print('ログインしました')

@client.event
async def on_raw_reaction_add(payload):
# リアクションされたメッセージのチャンネル
    txt_channel = client.get_channel(payload.channel_id)
# リアクションされたメッセージ
    message = await txt_channel.fetch_message(payload.message_id)
# リアクションしたユーザ
    user = payload.member
# 自分自身に対するリアクションは通知しない
    if (message.author == user):
        return
    msg = f"{message.author.mention} {payload.emoji}\nFrom:{user.display_name} \
          \nMessage:{message.content}\n{message.jump_url}"
# Bot 専用のチャンネルIDに置き換えて下さい
    CHANNEL_ID = 435155995647209
    channel = client.get_channel(CHANNEL_ID)
    await channel.send(msg)

client.run(TOKEN)
```
クライアント接続における Intents は必要最低限にしています。Intents については記事にまとめたので参考にしてみて下さい

https://qiita.com/Pumila/items/8b8e14f60fd7f266872b

このコードを実行すると全てのメッセージを対象に、ユーザがリアクションした場合、指定したチャンネルにメンションが送られます。

### どちらのメソッドを利用すべきなのか
__結論から言うと、キャッシュを利用しない `on_raw_reaction_add()` メソッドの方が都合がいいと思います。__ キャッシュを利用する場合と利用しない場合とを比較すると、利用しない場合メッセージ取得のために非同期処理を余計に 1 つ行っていることが分かります（await 部分）。その分処理に時間がかかりますが（私の環境では 0.25 秒程でした）、そこまでの負荷ではないでしょう。
また `on_reaction_add()` メソッドの場合、Bot の再起動のタイミング次第では、"リアクションしたのに通知されなかった"という問題が発生します（Bot 起動以前のメッセージには反応できないため）。その点、`on_raw_reaction_add()` メソッドでは過去まで遡り全てのリアクションに反応してくれます。特定のメッセージに対するリアクションを検知して何かを実行することも可能です（リアクションロールといったメッセージにリアクションすると自動でロールを振り分けてくれる機能など）。さらに、気軽にサーバを再起動できるという点でも優れています。これには定期的に再起動が行われるホスティングサービスを利用する場合でも有効でしょう。
以上より、大規模なサーバで運用を行いリアクションが頻繁に起こる場合や、サーバを再起動しない場合を除いてキャッシュを利用しない `on_raw_reaction_add()` メソッドを使用した方が運用しやすいと思います。

## service への登録
この Bot を常時稼働させておくために、サービスとして登録しておきましょう（Linux サーバを用いて Bot を運用する場合）。登録するためには root 権限が必要になります。まずは以下ディレクトリに移動してください。
```console
[root@localhost ~]# cd /etc/systemd/system/
```
その後、以下のコードを `reaction-detect.service` として保存して下さい。
```bash:reaction-detect.service
[Unit]
Description=Reaction Detect

[Install]
WantedBy=multi-user.target

[Service]
# 実行したいユーザに置き換えて下さい
User=web
# 実行するコードのディレクトリに置き換えて下さい
ExecStart=/home/web/discord/reaction-detect/reaction-detect2.py
Restart=always
```
最後にサーバ起動時にサービスも自動起動してくれるように設定します。
```console:サービスの自動起動
[root@localhost system]# systemctl start reaction-detect.service 
[root@localhost system]# systemctl enable reaction-detect.service 
Created symlink /etc/systemd/system/multi-user.target.wants/reaction-detect.service → /etc/systemd/system/reaction-detect.service.
```
以下コマンドを実行して `Active: active (running)` と表示されていれば問題ありません。
```console:サービスの状態
[root@localhost system]# systemctl status reaction-detect.service 
● reaction-detect.service - Reaction Detect
   Loaded: loaded (/etc/systemd/system/reaction-detect.service; enabled; ve>
   Active: active (running) since Sat 2023-01-28 09:40:49 JST; 2 days ago
 Main PID: 1204 (python3)
    Tasks: 22 (limit: 23888)
   CGroup: /system.slice/reaction-detect.service
           └─1204 python3 /home/web/reaction-detect/reaction-detect2.py

Jan 28 09:40:49 localhost.localdomain systemd[1]: Started Reaction Detectio
```

## 最後に
ゲーム募集などをする時に、リアクションされたかどうかを逐一確認する必要がありました。Bot を導入してからはそのような手間がかからなくなりました。より気軽にリアクションを使えるようになったと思います。

## 参考
<a href="https://qiita.com/PinappleHunter/items/db1c6e07d309c6b258ca">Discordの投稿につけたリアクションを通知するボット - Qiita</a>
<a href="https://qiita.com/1ntegrale9/items/9d570ef8175cf178468f">Pythonで実用Discord Bot(discordpy解説) - Qiita</a>