discord.py commandsフレームワークの小ネタ: クールダウン

皆さんこんにちは、daima3629と申します。
DiscordBotのTipsということで、8日目はDiscordAPIのPython用ラッパーライブラリである`discord.py`のcommandsフレームワークの小ネタを紹介していきたいと思います。
~~あれ...？8日目...？なにかおかしいnおっと誰か来たようだ~~
今回ご紹介致しますのは、その名も「**クールダウン**」！(商品紹介風)

## クールダウンとは？
「一定時間内に指定した回数以上コマンドを入力するとそれ以上のコマンドは受け付けなくなる」です。
まあ言葉で書き連ねてもよくわかんないと思うので、GIF画像をご用意したのでどういう挙動をするのかご覧ください。![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/637800/9a6087a0-ea06-da37-c1b1-50a7231a4042.gif)
こんな感じです。
なんとなくわかったとこで実際にどのようなコードを書けばいいのかお教えしましょう！

## 今回使用するコード
```python
# コマンドの実装部分のみ書いてあり、その他の部分は省略してあります

@bot.command()
@commands.cooldown(2, 10, type=discord.BucketType.user)
async def hello(ctx):
    await ctx.send("hello!")

@bot.event
async def on_command_error(ctx, err):
    if isinstance(err, commands.CommandOnCooldown):
        return await ctx.send("クールダウン中だよ！")
```

### とりあえずクールダウンに直接関わる部分
とりあえずコードを2つのブロックに分けて、１つ目を見ていきましょう！

```python
@bot.command()
@commands.cooldown(2, 10, type=discord.BucketType.user)
async def hello(ctx):
    await ctx.send("hello!")
```

#### [commands.cooldown](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.ext.commands.cooldown)
今回の記事の本命です。
`@commands.cooldown(rate, per, type)`このようにして使います。
引数名の説明は以下の通り。

|  引数名  |  説明  |
| :-------: | :------: |
|  rate  |  一定時間内に何回コマンドを打ったら<br>クールダウンに入るのか数字で指定  |
|  per  |  一定時間を指定  |
|  type  |  クールダウンタイプを指定  |

`type`引数に指定するものはちょっと変わったものです。
これは[リファレンス](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.discord.ext.commands.BucketType)を見たほうがわかりやすいかもしれません。
今回のコード例では`BucketType.user`を使用しているのでユーザーごとのクールダウンとなります。

これをもとに今回のクールダウンを見てみると、「10秒間の間に2回以上はコマンドを受け付けないユーザーごとのクールダウン」と読み取れますね！

### [on_command_error](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.on_command_error)
もう一つ残っているコードを見てみましょう。

```python
@bot.event
async def on_command_error(ctx, err):
    if isinstance(err, commands.CommandOnCooldown):
        return await ctx.send("クールダウン中だよ！")
```
ここで出ました`on_command_error`。
早速見ていきましょう。

#### クールダウン時はエラーが出る
クールダウンのときコマンドを実行すると`CommandOnCooldown`というエラー(例外)が発生します。
これを利用して、コードではクールダウン時にメッセージを発するようにしています。
`on_command_error`は他にもコマンドが存在しないときのエラーもキャッチできますよね。
割と使う要素です。

#### エラーはオブジェクトの一種

Python自体の知識ですが、エラーは`discord.User`とかと同じオブジェクトの一種です。
つまり、なにかしら情報が入っていると。
では[リファレンス](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.ext.commands.CommandOnCooldown)を見てみましょう。

|  要素名  |  説明  |
| :------: | :---: |
|  retry_after  |  再びコマンドが使えるようになるまでにかかる秒数(float型)  |
|  cooldown  |  クールダウンオブジェクト  |

`retry_after`は整数(int型)ではなく小数(float型)であることに注意です。
また`cooldown`は`cooldown.rate`のようにアクセスすることでデコレータで指定した情報を取得するために用意されています。


## クールダウン応用編
これまで学んできたことを生かして、少し応用を効かせたコードを書いてみましょう！

### クールダウンが終わるまでどのくらいかかるか表示する！
クールダウンの残り時間がわからないほどイライラすることはありません。
ユーザーのイライラを解消するためにも残り時間を表示してあげましょう。

#### 小数点以下を切り捨てる
`retry_after`を使うわけですが、先程も言ったようにこれは小数なので、そのまま表示すると「あと3.1548秒待ってね！」のように表示されてしまうので、切り捨てが必要です。
今回はint型に変換することで切り捨てます。

#### 実装してみよう
今回は秒数が大きくなる可能性も考慮して、「~分…秒」の形で表示させることにします。

```python
@bot.event
async def on_command_error(ctx, err):
    if isinstance(err, commands.CommandOnCooldown):
        retry_after_int = int(err.retry_after)
        retry_minute = retry_after_int // 60
        retry_second = retry_after_int % 60
        return await ctx.send(f"クールダウン中だよ！あと{retry_minute}分{retry_second}秒待ってね！")
```

今回のポイントは先程説明した小数点切り捨てに加え、2つの演算子、`//`と`%`です。
`//`は割り算したとき小数になった場合に小数点以下を切り捨てた数値を返す演算子で、
`%`は割り算したときの余りを返す演算子です。
これを組み合わせることで「~秒」を「~分...秒」の形にしています。

## おまけ: Cogに組み込んでみる
もちろんクールダウンはCogの中のコマンドにも組み込めます。
使用例だけ記しておきます。
細かい解説は省きます。

```python
class CooldownCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @commands.command()
    @commands.cooldown(2, 10, type=discord.BucketType.user)
    async def hello(ctx):
        await ctx.send("hello!")

    @commands.Cog.listner()
    async def on_command_error(self, ctx, err):
        if isinstance(err, commands.CommandOnCooldown):
            return await ctx.send("クールダウン中だよ！")
```

## まとめ

- `commands.cooldown`を使うことでコマンドにクールダウンを実装させることができる！
- `on_command_error`を使うことでクールダウンに応じて文章を送ることができる！

というわけで、みなさんもクールダウンを使って一歩先のBot開発者になりませんか!?
この記事が役に立ったのなら幸いです。