discord.js-buttonsを使ってButtonで操作出来るDiscordBotを作ってみた

# 経緯
このBotを作ろうとした理由と記事を書いた理由は下記の通りです。

- Discordに新しく追加されたインタラクションであるButtonsを使ってみたかった
- [discord.js-buttons](https://www.npmjs.com/package/discord.js-buttons)に関する情報がなかった

似たようなことをしたい人の参考になれば良いなと思います。
なお、[discord.js-buttons](https://www.npmjs.com/package/discord.js-buttons)はリポジトリが削除されているため今後無くなる可能性がありますが  
discord.js自体のmasterブランチにButtonsに関するソースコードが追加されている為(2021/06/08現在)　　
discord.jsが今後のバージョンでButtonsに対応すると思われますのでそちらを待ってもいいかもしれません。

# ソースコード
GitHubにあります。
[noriokun4649/DiscordSoundEffectBot](https://github.com/noriokun4649/DiscordSoundEffectBot)
利用してるライブラリなどの依存関係はpackage.jsonに書いてありますが下記の通りです。

* "@discordjs/opus": "^0.5.0"
* "opusscript": "^0.0.8"
* "config": "^3.3.6"
* "discord.js": "^12.5.3"
* "discord.js-buttons": "^1.0.0"
* "ffmpeg-static": "^4.3.0"
* "fs": "^0.0.1-security"

# discord.js-buttons の使い方
[discord.js-buttons](https://www.npmjs.com/package/discord.js-buttons) の使い方について書いていきます。
## discord.js-buttonsのセットアップ
```js
const discord = require('discord.js');
const client = new discord.Client();
const discordbtn = require('discord.js-buttons')(client);
```
これでdiscord.jsからButtonsを利用する準備は完了です。

##Buttonのコンポーネントを作成
上記のセットアップで定義した```discordbtn```から```MessageButton()```を呼びだし新しいインスタンスを作ることで作成できます。

```js:シンプルなbuttonコンポーネント
const button = new discordbtn.MessageButton()
    .setStyle('green')
    .setLabel('ファイル更新・読込')
    .setID('scan');
```
シンプルな例だとこのようになり、次のようなボタンが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/9401bc4e-1720-54c6-d67c-ce7087a7c6bf.png)
そして、このボタンを押したときのButtonIDは```'scan'```になります。
このButtonIDは後述する「[buttonのイベントを受け取る](#buttonのイベントを受け取る)」で利用するので覚えておきましょう。
```MessageButton()```に設定できるパラメータは幾つかあるので例を挙げてみます。

```js:無効化されたbuttonコンポーネント
const disabledbutton = new discordbtn.MessageButton()
    .setStyle('green')
    .setLabel('ファイル更新・読込')
    .setID('scan')
    .setDisabled(); //このボタンを無効化された状態に設定する
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/71b6f66c-7676-c795-f3b4-1732dbab2735.png)

```js:URLのbuttonコンポーネント
const urlbutton = new discordbtn.MessageButton()
    .setStyle('url') //ボタンのスタイルをurlにする
    .setLabel('ファイル更新・読込')
    .setURL('https://hoge.hoge');
    //スタイルがurlの場合setIDではなくsetURLを指定するので注意
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/dfbd6833-7c3e-e91f-0672-5736dcefabf0.png)

指定できるスタイルに関しては[DiscordのAPIドキュメント](https://discord.com/developers/docs/interactions/message-components#buttons-button-styles)を参照してください。
```setStyle```と```setLabel```と```setID```若しくは```setURL```は必須の項目なので注意が必要です。
ボタンの無効化は既に送信済みのボタンが無効化されるわけではないため、既に送信済みのメッセージを編集して無効化したボタンに置き換える時などがユーズケースだと思われます。

##メッセージの送信時にButtonを追加する
ここでは早速メッセージ送信時にButtonを付ける方法に触れていきます。

```js:1つのbuttonを追加
message.channel.send('1つのボタンがついてるメッセージ', button);
```
意外に簡単です。この例では下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/e66c97df-4142-9458-c566-227ef17c702f.png)

```js:1つのbuttonを追加(返信)
message.reply('1つのボタンがついてる返信メッセージ', button);
```
このように返信を送る際にも同様の方法でボタンを追加出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/36cbff13-601c-eba4-3790-0fce97bcf30b.png)

そして、このbuttonはDiscordAPI仕様によりメッセージと一緒に送る必要があります。
また、同じくDiscordAPI仕様によりメッセージは空にできない為buttonだけを送信する方法は今のところありません。

さて、このbuttonですが1つのメッセージに対して最大で5個まで追加する事ができます。

```js:複数のbuttonを追加
message.reply('複数のボタンがついてる返信メッセージ', { buttons: [button, disabledbutton, urlbutton] });
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/04a47a47-a898-3fe4-82c2-5812237a40e5.png)
discord.jsから送信したメッセージのシンタックスハイライトを有効にする際などで使う文法をそのまま使えるので第2引数の連想配列には、```buttons```という名前がついたbuttonコンポーネントの配列を渡してあげれば大丈夫です。

## buttonのイベントを受け取る
ここではbuttonがおされた後の処理を行います。

```js:buttonが押された際のイベントを受け取るハンドラ
client.on('clickButton', async (button) => {

});
```

このように書き、discord.jsにおけるmessageイベントを受け取るときと同じ書き方が出来ます。
この時、仮引数である```button```には[DiscordAPIドキュメントのインタラクション](https://discord.com/developers/docs/interactions/slash-commands#interaction)に書かれているフィールドに加えて、レスポンスを返す為の関数が用意されています。

```ts:仮引数(button)に用意されてるフィールドと関数
    client: Client;
    id: string;
    version: number;
    token: string;
    discordID: Snowflake;
    applicationID: Snowflake;
    guild: Guild;
    channel: Channel;
    clicker: {};
    message: Message;
    webhook: WebhookClient;
    replied: boolean;
    deferred: boolean;
    defer(ephemeral: boolean): Promise<void>;
    think(ephemeral: boolean): Promise<void>;
    get reply(): {
        send: (content: string, options?: object) => Promise<void>;
        fetch: () => Promise<string>;
        edit: (content: any, options?: object) => Promise<any>;
        delete: () => Promise<void>;
    };
```

### フィールドについて
フィールドについては特筆すべきことは無いと思いますがあるとすれば```id: string```です。
これはbuttonコンポーネントが押されたときの押されたbuttonと紐付くButtonIDの文字列が入っています。
あとは```clicker: {}```ですが、これにはbuttonをおしたユーザーの情報が入っています。

```js:clickerフィールドの利用例
const guild = button.guild;
const member = guild.member(button.clicker.user);
```
このように使うことで、buttonをおしたユーザのGuildMemberインスタンスを取得できます。
```replied```と```deferred```フィールドに関しては次の「[関数について](#関数について)」で触れる関数が既に使われているかどうかを判定します。これは関数を使った後にすぐ状態が変化するわけではなく、もう一度buttonがおされた際に変化するので注意が必要です。

### 関数について

このとき```think(boolean)```関数はユーザに処理中若しくは考え中を表示させる関数です。```defer(boolean)```関数はユーザに処理を先延ばしにする旨を伝える関数です。先延ばしに関する処理はデスクトップ版Discordがバグっているのかわかりませんが、モバイル版のDiscordのみボタンが押せない状態で処理が先延ばしされます。
この2つの関数において引数になるbooleanはこの考え中や先延ばしというBot側のメッセージをbuttonを押したユーザーだけに伝えるかどうかを指定できます。```true```の場合はbuttonを押したユーザーだけに伝えます。　　
```reply.send()```ではbuttonのついたメッセージへ返信出来ます。
```reply.delete()```では返信を削除できます。これは```think(boolean)```によって送信された「考え中・・・」というメッセージも対処です。
```reply.edit()```では返信を編集出来ます。
```reply.fetch()```では返信されているメッセージを取得出来ます。

そして注意が必要なのは、これらの関数のうちいずれかを利用しないと、Discord上で「インタラクションに失敗しました」というメッセージが表示される事です。そのため、buttonが押され正常な処理が出来る場合にはいずれかの関数を使いレスポンスを返してあげましょう。

では早速、具体的に機能を書いていきます。

```js
const listButton = new disbut.MessageButton() //buttonコンポーネントを定義しておく
    .setStyle('green')
    .setLabel('再生可能ファイル一覧を表示')
    .setID('list');

client.on('clickButton', async (button) => { //button押された時に実行される
    await button.think(false); //botが考え中ということを全員にしらせる
    if (button.id === 'scan') { //ButtonIDがscanの場合
        const fileList = readfile(); //ファイル読み込み関数を呼ぶ(buttonには関係ない処理)
        if (fileList.length > 0) { //ファイルが1以上だったら
            btn = createButtns(fileList);
            button.reply.edit('ファイル更新・読込に成功しました。', { code: true, buttons: [listButton]         }); 
            //考え中の返信を「ファイル更新・読込に成功しました。」というメッセージに編集する。
        } else {
            button.reply.edit('ファイル更新・読込に失敗しました。', { code: true });
            //考え中の返信を「ファイル更新・読込に失敗しました。」というメッセージに編集する。
        }
});
```
![GIF 2021-06-08 5-42-58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/b77df4d3-1f00-60a5-6e43-8fe56bc78537.gif)![GIF 2021-06-08 5-39-27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/01845168-844d-edcf-62e7-a7c577d82b74.gif)

このようになります。わかりづらいかも知れませんが「考え中・・・」と出た後に書き換えられてるかと思います。
因みに上記プログラムの```button.think(false);```を```button.think(true);```に変更すると
![GIF 2021-06-08 5-45-49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/447593/8f6c9756-fd1e-bdb3-4502-456efede35d4.gif)
こうなります。「これらはあなただけに表示されています」という風になりますね。

## discord.js-buttonsの使い方おわり
以上がdiscord.js-buttonsを使ったButtonsの使い方です。使いこなせるようになると非常にUXに優れたBotが作成できると思います。
Buttonsを多用しすぎてメッセージチャンネルをあらさないような配慮も考えつつ活用して頂ければ幸いです。

# 最後に
最後に、私が今回作ったBotについて軽く触れて終わりにします。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Discordに追加されたButtonのインタラクションを使って、効果音流せるBotつくってみた <a href="https://t.co/VyZ7uGsxL1">pic.twitter.com/VyZ7uGsxL1</a></p>&mdash; #𝒊𝒏𝒄𝒍𝒖𝒅𝒆 &quot;🍢🐟🍢煮込みすぎたおでん🍢🐟🍢.𝒉&quot; (@noriokun_blog) <a href="https://twitter.com/noriokun_blog/status/1401373911128809473?ref_src=twsrc%5Etfw">June 6, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

今回作ったのはDiscordのボイスチャンネル上でサウンドエフェクトを再生できるBotです。
Buttonsを多用して非常に使いやすくできたと思っていますのでよろしければ使ってみてください。
[ソースコードに飛ぶ](#ソースコード)

# リンク
 - [discord.js-buttons - npm](https://www.npmjs.com/package/discord.js-buttons)