【discord.py】アプリケーションコマンドを実装する

# はじめに

discord.pyは近年アップデート続きで情報が不確かで、日本語の文献が少なかったため書いてみました。

この記事では簡単にアプリケーションコマンド(スラッシュコマンド)を実装するための方法を解説します。

## この記事の対象者

- ある程度Pythonの知識があり、discord.pyでアプリケーションコマンドを実装したい。

## 前提知識

- 基本的なPythonの記法と仕様を理解している
- discord.pyの基本的な使い方を知っている

# 実装

## 基本的な実装

以下のコードは引数を取らない基本的な実装です:
```python
from os import environ

import discord
from discord.ext import commands

intents = discord.Intents.all()
bot = commands.Bot(intents=intents, command_prefix="!")
tree = bot.tree

@bot.event
async def on_ready():
    print(f"Logged in as {bot.user.name}.")
    await tree.sync()
    print("Synced slash commands.")


@tree.command(name="ping", description="レイテンシを計測します")
async def ping(ctx: discord.Interaction):
    text = f'Pong! {round(self.bot.latency*1000)}ms'
    embed = discord.Embed(title='Latency', description=text)
    await ctx.response.send_message(embed=embed)

bot.run(environ["TOKEN"])
```
変数`tree`には`bot.tree`(型は[CommandTree](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#commandtree))が渡され、これがアプリケーションとのインタラクションを扱う変数になります。

`@tree.command()`デコレータを使ってアプリケーションコマンドが実装できます。

### `tree.command()` デコレータの引数

- `name`: アプリケーションコマンドの名前を指定する引数
- `description`: アプリケーションコマンドの説明文を指定する引数(optional)
その他の引数は[公式リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.app_commands.CommandTree.command)を参照してください。

`ctx`(コンテキスト)には[discord.Interaction](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#interaction)が入り、これの`response.send_message()`などのメソッドを利用してインタラクションへの応答などの処理が可能です。

## 引数をとるコマンドの実装

### 基本的な引数の実装

```python
@tree.command(name="add", description="引数の総合値を算出します")
async def add(ctx: discord.Interaction, val1, val2, val3):
    try:
        result = str(int(val1) + int(val2) + int(val3)) # 基本的に引数はstr型で渡されます
    except Exception as e:
        result = "Error: " + str(e)
    text = f'`{}`'
    embed = Embed(title='Result:', description=text)
    await ctx.response.send_message(embed=embed)
```
`@tree.command()`デコレータの引数を増やすことでユーザーから引数を受け取ることができるようになります。

### 引数に説明を付与する

```python
from discord.app_commands import describe

@tree.command(name="add", description="引数の総合値を算出します")
@describe(val1="第1引数")
@describe(val2="第2引数")
@describe(val3="第3引数")
async def add(ctx: discord.Interaction, val1, val2, val3):
    try:
        result = str(int(val1) + int(val2) + int(val3))
    except Exception as e:
        result = "Error: " + str(e)
    text = f'`{}`'
    embed = Embed(title='Result:', description=text)
    await ctx.response.send_message(embed=embed)
```
`@discord.app_command.describe()`デコレータを利用してアプリケーションコマンドの引数に説明を付与することができます。
構文は`describe(引数名=説明)`です。

### 引数をオプショナル(任意)にする
```python
from discord.app_commands import describe

@tree.command(name="add", description="引数の総合値を算出します")
@describe(val1="第1引数")
@describe(val2="第2引数")
@describe(val3="第3引数")
async def add(ctx: discord.Interaction, val1, val2 = 0, val3 = 0):
    try:
        result = str(int(val1) + int(val2) + int(val3))
    except Exception as e:
        result = "Error: " + str(e)
    text = f'`{}`'
    embed = Embed(title='Result:', description=text)
    await ctx.response.send_message(embed=embed)
```
デコレータ内の引数を予め指定しておくことにより、アプリケーションコマンド上でもデフォルトの値が指定できます。

### 引数の型を指定する
```python
from discord.app_commands import describe

@tree.command(name="add", description="引数の総合値を算出します")
@describe(val1="第1引数")
@describe(val2="第2引数")
@describe(val3="第3引数")
async def add(ctx: discord.Interaction, val1: int, val2: int = 0, val3: int = 0):
    try:
        result = str(int(val1) + int(val2) + int(val3))
    except Exception as e:
        result = "Error: " + str(e)
    text = f'`{}`'
    embed = Embed(title='Result:', description=text)
    await ctx.response.send_message(embed=embed)
```
型アノテーションを追加することで引数の型を指定できます。(str=文字列, int=整数値, float=浮動小数点数)

引数のデフォルト値と型の指定を併用する際、デフォルト値はその型と同じである必要があります。

例:
```py
val1: int = 0
val2: str = ""
val3: float = .0
val4: discord.Member = None # NoneTypeはどの型に対しても有効
```

### 数値を指定する引数に範囲を指定する
```python
from discord.app_commands import Range, describe

r = Range[int, -100, 100]

@tree.command(name="add", description="引数の総合値を算出します")
@describe(val1="第1引数")
@describe(val2="第2引数")
@describe(val3="第3引数")
async def add(ctx: discord.Interaction, val1: r, val2: r = 0, val3: r = 0):
    try:
        result = str(val1 + val2 + val3)
    except Exception as e:
        result = "Error: " + str(e)
    text = f'`{result}`'
    embed = Embed(title='Result:', description=text)
    await ctx.response.send_message(embed=embed)
```

`discord.app_command.Range`クラスを使ってアノテーションをすることで範囲を指定できます。
[公式リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#range)からの引用:
- `Range[int, 10]` は最小値10、最大値なしを意味します
- `Range[int, None, 10]` は最小値なし、最大値10を意味します
- `Range[int, 1, 10]` は最小値1、最大値10を意味します
- `Range[float, 1.0, 5.0]` は最小値1.0、最大値5.0を意味します
- `Range[str, 1, 10]` は最小1文字、最大10文字を意味します

# 終わりに
最後まで読んで頂きありがとうございます。不備があれば是非コメントで指摘して下さい。