Discord BOTにライブラリ非依存で返信機能を持たせる

Advent Calendar 12日目の記事です。

初めての参加となります。よろしくお願いします。

現地時間11月16日より、Discordで返信機能が実装されました。

さぞかしBOTに「返信機能を付けてみたい！」と思う方もいるかと思います。

今回はそれについて書いていきます。

# 返信機能は特別なものではない
[Discordの公式APIリファレンス](https://discord.com/developers/docs/resources/channel#create-message)を見ると、新しくmessage_referenceという項目ができているのが分かります。

ここにメッセージIDを指定してやればあっという間に返信が実現できます。

# ライブラリを使わずにどうやるか？
私達は普段直接APIを触ることなく、それぞれの言語に用意されたフレームワーク(Discord.pyやDiscord.jsなど)を使いプログラミングしています。しかし、実際はAPIを扱いやすくしたに過ぎないため、素の状態(APIを直接触ること)でやり取りすることもできます。

この記事を書いた時点ではまだ返信機能をサポートしていないライブラリが大半です。ここでは、直接APIとやり取りしてメッセージを送信する方法を解説します。

## エンドポイント

まず、メッセージを送信するためのエンドポイントは

```
https://discord.com/api/channels/[チャンネルID]/messages
```

となっているので、そんなに難しくありません。メッセージオブジェクトのチャンネルIDを当てはめればURLは完成です。

## ヘッダーとデータ
APIとはJSONを使ってやり取りします。

そのため、ヘッダのContent-Typeは```Content-Type: application/json```とする必要があります。

APIを使うにあたって、ヘッダ部分に認証情報を記載する必要があります。

「え、これはどうすれば…」と思った方。もちろんBOTのトークンが使えるので大丈夫です。

言語別に記載しませんが、リクエストヘッダに書くときは

```
Authorization: Bot [G6WtkBk9nHW4AlhB3wbCBSzx.nDUuhSMp02k9Okjs5DwRn4QRt9SvFo36m5]
```

となります。トークンは適宜ご自身のものに変えてください。[ ]で囲んだ部分にトークンを入れます。

次に送信するデータです。

基本形は

```json
{
 "content": "Hello world"
}
```

です。contentにメッセージを入れます。
Webhookみたいにとてもシンプルですね。

ここに、```message_reference```プロパティを追加して「返信だよ！」という情報を追加します。

公式リファレンスには

|FIELD|DESCRIPTION (和訳)|
|--------------|---------------|
|message_id|元のメッセージのID|
|channel_id?|元のメッセージのチャンネルID|
|guild_id?|元のメッセージのサーバーID|

と書かれています。FIELDの最後についている**?**は「パラメータは必須ではない、省略可」という意になっているので、message_idだけ渡せばよいということが分かります。

message_idには、メッセージオブジェクトのメッセージIDを渡せばいいのです。

ここまでのをまとめると、送信するJSONはこうなります。

```json
{
 "content": "Hello world!",
 "message_reference": {
  "message_id": 1234567890123456
 }
}
```

となります。

これらをヘッダと組み合わせて送信してやれば、返信機能が実現できます！
試しに実験サーバーで何かメッセージを送信してみて、チャンネルIDをURLにセットして、message_idのところを送信したメッセージIDに書き換えて

```
curl -X POST -H "Content-Type: application/json" -H "Authorization: Bot YourToken.PUT.Here" -d "{\"content\":\"Reply\" , \"message_reference\":{\"message_id\":YOUR_MESSAGE_ID}}" https://discord.com/api/channels/YOUR_CHANNEL_ID/messages
```

を端末で実行して

![Screenshot_20201122-205144~2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196404/3ca0eee5-b67e-d68a-68e0-af4a1d0e1836.png)

というふうになったことを確認できれば、完了です。

### メンションOFFにする
ただ、このままだと相手にメンションが飛んでしまいます。
メンションするか、しないかは通常だとアプリの設定で変えられます。

APIも同様で「メンションしないで」という情報を送ればよいのです。

先程のJSONに付け足して

```json
{
 "content": "Hello world!",
 "message_reference": {
  "message_id": 1234567890123456
 },
 "allowed_mentions": {
  "replied_user": false
 }
}
```

というふうになります。

詳しくは[Allowed mentions object](https://discord.com/developers/docs/resources/channel#allowed-mentions-object)を参照してください。


これのデータを先程のcurlで送るとメンションなしで返信できるのを確認できると思います。

# 関数化させよう
ここまでcurlを使いましたが、プログラムで使うのにはとても不都合です。

関数化させて、比較的便利に使えるようにしましょう。

Python用にあらかじめ用意した関数を貼っておきます。
使いやすいようにアレンジしてお使いください。

```py
async def reply(basemsg, message, send_mention, **kwargs):
   url = f'https://discord.com/api/channels/{basemsg.channel.id}/messages'
   heads = {"Content-Type":"application/json", "Authorization":f"Bot {TOKEN}"}
   data = {"content":message, "message_reference": {"message_id":basemsg.id}, "allowed_mentions": {"replied_user":send_mention}}
   if kwargs.get('embed'):
      data['embed']=kwargs.get('embed').to_dict()
   async with aiohttp.ClientSession() as session:
     async with session.post(url, data=json.dumps(data), headers=heads) as req:
        if req.status!=200:
           raise Exception(f'Gateway returned an error')
           return False
   return True
```

使い方は

```py
await reply(message, '返信した！', False)
```

となります。左から1番目にはメッセージオブジェクトを、2番目には送信したいテキスト、3番目は相手にメンションするかどうかをTrue or Falseで指定します。

また、任意で```embed```というキーワード付き引数にdiscord.Embedのオブジェクトを渡すと埋め込みも送信できます。

```py
em = discord.Embed(title='埋め込みもできるよ')
await reply(message, '返信した！', False, embed=em)
```


なお、この関数はrequests、json、aiohttpモジュールを使用します。事前にインポートしてください。

# まとめ
返信をするときは```message_reference```の```message_id```プロパティに返信先メッセージIDを指定する
@メンションをしないときは```allowed_mentions```の```replied_user```をFalseにする

本当であればライブラリのアップデートを待つのが一番だし、一番簡単です。

あくまでも参考として、早く使ってみたい方向けです。

その点についてご理解ください。