discord.pyでスラッシュコマンドを実装する。

こんにちは。beatbox4108です。

今日は、Discord.py 2.0.0で~~実装予定の~~ **(実装されました)** スラッシュコマンドを実際に触ってみて説明したいと思います。

今の所まだ自分がコマンドグループについて理解していないのでそれについての説明はしません。

# Discord.pyのインストール
リリースされたので、次のような通常のpipコマンドでインストールできます。

```bash
$ pip install discord
```
ただ、古いバージョンが入っていると更新されない場合があるので、その場合は次のようにしてください。
```bash
$ pip install discord==2.0.0
```

リリースされた内容については、discordでの告知の翻訳版があるので、それを参考にしてください。

https://qiita.com/beatbox4108/items/45800290eb1e88515c1b

<details>
<summary>古い内容</summary>

今の所まだPyPIにはリリースされていないのでgit経由でインストールします。
```bash
$ pip install git+https://github.com/Rapptz/discord.py.git
```

</details>

# 早速触ってみる
スラッシュコマンドの実装方法は数種類ありますが、今回は`CommandTree`を使った方法で実装します。
```python3
import discord
import discord.app_commands

token = "" #Your TOKEN
client = discord.Client()
tree = discord.app_commands.CommandTree(client) #←ココ

@client.event
async def on_ready():
    print("ready")
client.run(token)
```
これが基本の形です。
大事なのはこれです。
```python3
tree = discord.app_commands.CommandTree(client)
```
この部分で`CommandTree`というインスタンスを作成しています。
これは、スラッシュコマンドの登録に使います。
## コマンドを登録する
コマンドの登録には`CommandTree.command`デコレーターを使います。
```python3
import discord
import discord.app_commands

token = "" # Your TOKEN
client = discord.Client()
tree = discord.app_commands.CommandTree(client)

@tree.command(
    name="hello",#コマンド名
    description="Send Hello world."#コマンドの説明
)
async def hoge(ctx:discord.Interaction):
    await ctx.response.send_message("Hello world")

@client.event
async def on_ready():
    print("ready")
client.run(token)
```
ですが、これだけだとDiscord側でコマンドを確認しても登録されていません。
Discord側とコマンドの同期を行うには、`CommandTree.sync`関数を実行します。
```python3
await tree.sync()
```
~~`on_ready`の中などDiscordとの接続ができてから実行しましょう。~~

:::note warn
### 2022/12/16追記。
Discordのコマンド同期の申請制限に引っかかる恐れがあるので、テストなどの際はサーバー限定コマンドとして登録するようにして、`on_ready`内部に入れるなどむやみに`sync`をしないようにしましょう。
:::

# インタラクション
> インタラクションは、ユーザーに返信をする必要のあるアクションが行われた際に発生します。現在の例はスラッシュコマンドとコンポーネントです。

([Discord.py インタラクションAPIリファレンスより](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#interaction:~:text=%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AF%E3%80%81%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AB%E8%BF%94%E4%BF%A1%E3%82%92%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%AE%E3%81%82%E3%82%8B%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8C%E8%A1%8C%E3%82%8F%E3%82%8C%E3%81%9F%E9%9A%9B%E3%81%AB%E7%99%BA%E7%94%9F%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%E7%8F%BE%E5%9C%A8%E3%81%AE%E4%BE%8B%E3%81%AF%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A8%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7%E3%81%99%E3%80%82))

インタラクションはスラッシュコマンドの最初の引数として渡されます。
Botはこれに対して返答などをすることになります。

この節で書く`ctx`は`discord.Interaction`オブジェクトです。

## `response`を使った返答
インタラクションには`response`で返答できます。

:::note warn 
responseでの返答は**一度しかできません**。
2度目からは、webhookで返答することになります。
:::

### メッセージで返答する
```python3
await ctx.response.send_message("メッセージ")
```
基本的に`send`と変わりません。
引数として`ephemeral=True`を渡すと、コマンドを実行したユーザーにのみ返答が表示されます。
### 返答の遅延
重い処理や時間がかかる処理などですぐに返答できない場合は、次の関数を実行します。
```python3
await ctx.response.defer()
```
:::note warn 
**これも返答に数えられる**ので注意してください。
:::
ほかにもいくつかの返答方法があるので [リファレンス](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.InteractionResponse) を読んでみてください。
## `followup`での返答
2回目以降の返答はWebhookを通じて行います。
`ctx.followup`に格納されます。
### メッセージの返答
```python3
await ctx.followup.send()
```
普通の`send`ですが、引数として`ephemeral=True`を渡せます。
# 引数をとる
Discordのコマンドは引数をとれます。
基本的に関数に引数を追加するときは型アノテーションをつけたほうがいいです。

:::note info
`discord.User`など、自動補完されるようになったり、
自動で型変換してくれたりします。
:::
## 説明をつける
`discord.app_commands`の`describe`デコレーターを使います。
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello." # 引数名=説明
)
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
上の例のように`引数名=説明`の形式で指定します。

:::note info
コマンドを定義しているデコレーターよりも前には記述しないでください。
:::

## 引数を改名する
`discord.app_commands`の`rename`デコレーターを使います。
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name" # 引数名=名前
)
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
上の例のように`引数名=名前`の形式で指定します。

:::note warn
`describe`などで名前を指定するときは、元々の名前を使ってください。
:::

## 変換候補を出す
いくつか方法があります。
### デコレーター
`discord.app_commands`の`choices`デコレーターを使います。
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
@discord.app_commands.choices(
    text=[
        discord.app_commands.Choice(name="Discord",value="Discord"),
        discord.app_commands.Choice(name="World",value="World"),
        discord.app_commands.Choice(name="Asia",value="Asia"),
        discord.app_commands.Choice(name="Japan",value="Japan")
    ]
)
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
上のように、`引数名=Choiceのリスト`形式で設定します。
### `enum.Enum`を使う方法
```python3
class Names(enum.Enum):
    Discord = "Discord"
    World = "World"
    Asia = "Asia"
    Japan = "Japan"

@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
async def hoge(ctx:discord.Interaction,text:Names):
    await ctx.response.send_message(f"Hello, {text}!")
```
引数の型アノテーションに`Enum`を指定します。
### `typing.Literal`
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
async def hoge(ctx:discord.Interaction,text:typing.Literal["Discord","World","Asia","Japan"]):
    await ctx.response.send_message(f"Hello, {text}!")
```
`Enum`と同じように、型アノテーションで指定します。
## サーバー限定コマンド(ギルドコマンド)にする
:::note warn
ここで解説するのは**コマンドをグローバルに登録しない方法**です。
DMなどで使用できないようにしたい場合は次で解説します。
:::
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
@discord.app_commands.guilds(
    #サーバーID
)
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
`discord.app_commands`の`guilds`デコレーターを使います。
引数にはサーバーIDを渡します。
## コマンドをサーバーでのみ使えるようにする
:::note warn
ここで解説するのは**DMなどでコマンドが実行できないようにする方法**です。
ギルドコマンドとして登録したい場合は前の項で解説しています。
:::
`discord.app_commands`の`guild_only`デコレーターを使います。
```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
@discord.app_commands.guild_only()
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
引数はありません。

:::note info
デコレーターに括弧をつけていますが、次のように省略できます。
```python3
@discord.app_commands.guild_only
```
:::
## 権限があるかを確認する
:::note alert
> これは ヒント として機能し、メンバーはこれらのコマンドを実際に実行するのに権限を持つ必要は ありません 。メンバーが必要な権限を有することを確かめたい場合は、代わりに has_permissions() を使用してみてください。

[discord.py インタラクションAPIリファレンスより](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#decorators:~:text=%E3%81%93%E3%82%8C%E3%81%AF%20%E3%83%92%E3%83%B3%E3%83%88%20%E3%81%A8%E3%81%97%E3%81%A6%E6%A9%9F%E8%83%BD%E3%81%97%E3%80%81%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E3%81%AF%E3%81%93%E3%82%8C%E3%82%89%E3%81%AE%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%82%92%E5%AE%9F%E9%9A%9B%E3%81%AB%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B%E3%81%AE%E3%81%AB%E6%A8%A9%E9%99%90%E3%82%92%E6%8C%81%E3%81%A4%E5%BF%85%E8%A6%81%E3%81%AF%20%E3%81%82%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93%20%E3%80%82%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E6%A8%A9%E9%99%90%E3%82%92%E6%9C%89%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%92%E7%A2%BA%E3%81%8B%E3%82%81%E3%81%9F%E3%81%84%E5%A0%B4%E5%90%88%E3%81%AF%E3%80%81%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%AB%20has_permissions()%20%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%A6%E3%81%BF%E3%81%A6%E3%81%8F%E3%81%A0%E3%81%95%E3%81%84%E3%80%82)
:::

```python3
@tree.command(
    name="hello",
    description="Send Hello world."
)
@discord.app_commands.describe(
    text="Text to say hello."
)
@discord.app_commands.rename(
    text="name"
)
@discord.app_commands.default_permissions(
    administrator=True
)
async def hoge(ctx:discord.Interaction,text:str):
    await ctx.response.send_message(f"Hello, {text}!")
```
引数は`権限名=True/False`の形式で指定します。権限は[ここ](https://discordpy.readthedocs.io/ja/latest/api.html#discord.Permissions)のパラメータから指定します。



#
いかがだったでしょうか?
正式リリースは近いのでよかったらみんなもやってみてください!