discord.pyのTextInput

# 初めに

- WSL, Ubuntu22.04.1
- Python 3.10.6
- discord.py             2.1.0

という環境で行っています．


# TextInput

[TextInput](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.ui.TextInput)は，[Modal](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#modal)においてテキスト入力をするためのコンポーネントです．

Modalとは，

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/366f1fe1-0986-de37-2c69-770e7fcf7b56.png)


こんな感じのポップアップウィンドウです．アクティビティを退出するときに出てくる評価してねみたいなやつもModalポップアップウィンドウです．
これにテキスト入力できる欄を追加するのがTextInputです．

とりあえず，上の画像のようなModalを作ってみましょう

## 実験

Modalに対しTextInputを追加するには，Viewと同じように，Modalの[`add_item`メソッド](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.ui.Modal.add_item)を利用します．また，フォームが送信されたときに処理をしたい場合はModalのサブクラスを作り，そこで[`on_submit`メソッド](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.ui.Modal.on_submit)をオーバーライドします．


```diff_python: python
import discord
from discord import Interaction, TextStyle
from discord.app_commands import CommandTree
from discord.ui import TextInput, View, Modal
import os


intents = discord.Intents.default()
client = MyClient(intents=intents)



class MyClient(discord.Client):
    def __init__(self, *, intents: discord.Intents):
        super().__init__(intents=intents)
        self.tree = CommandTree(self)

    async def setup_hook(self) -> None:
        await super().setup_hook()
        commands = await self.tree.sync()
        print(commands)

    async def on_ready(self):
        print(f"Logged in as {client.user} (ID: {client.user.id})")
        print("------")


class Questionnaire(Modal):
    def __init__(self, title: str) -> None:
        super().__init__(title=title)
        self.answer = TextInput(label="favorite point", style=TextStyle.long)
        self.add_item(self.answer)

    async def on_submit(self, interaction: Interaction) -> None:
        await interaction.response.send_message("thanks!")
        await interaction.followup.send(
            f"your favarite point is {self.answer.value}", ephemeral=True
        )


@client.tree.command(name="questionnaire")
async def app_questionnaire(interaction: Interaction):
    modal = Questionnaire("application questionnaire")
    await interaction.response.send_modal(modal)


client.run(os.getenv("TOKEN"))
```

動かしてみると上記の画像のようなModalが表示されます．

TextInputには[テキストボックスの長さ](https://discordpy.readthedocs.io/ja/latest/interactions/api.html#discord.TextStyle)をstyleとして指定できます，shortが1行だけ，paragraphあるいはlongを指定すると複数行のフォームとなります．paragraphの別名がlongなだけなので2種類のみです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953650/1f950617-599b-3753-95e0-29cb5e6c816f.png)

## 注意点

注意点として，TextInputはModalにしか使えません．つまりViewには追加できません．例えばcontext_menuによるメッセージの転送[^forward]をしようとするときに，メッセージにタイトルをつけようとしてTextInputを使おうかなと思っても使うことができません．下記のようなエラーが発生します．

[^forward]: [メッセージを転送しようという記事](https://qiita.com/halglobe0108/items/d6794e2104b6c90580a6)を書いてます．見ていただけると嬉しいです．


```bash
discord.app_commands.errors.CommandInvokeError: Command 'what_input' raised an exception: HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body
In data.components.0.components.0: Value of field "type" must be one of (2, 3, 5, 6, 7, 8).
```

意訳すると，data.components.0.components.0にあるtypeに指定する値は(2, 3, 5, 6, 7, 8)のどれかにしてくれ，というエラーです．まずdata.components.0.components.0というのは，[DEVELOPER PORTALのExample Component](https://discord.com/developers/docs/interactions/message-components#component-object-example-component)のところを見ると，Requestを送る際のデータ構造のようです．全体がdataという名前で，それのcomponentsの0行目の中にあるcomponentsの0行目にtypeが入っているようです．

そして，このtypeは何者かという話ですが，[DEVELOPER PORTALのWhat is a Componentという項目](https://discord.com/developers/docs/interactions/message-components#what-is-a-component)によると，どのUIコンポーネントかを示すもののようです．もしButtonを追加してたら，そのtypeには2が入るようです．TextInputは4です．つまり，ViewにおいてTextInputは使えないということです．

また，[DEVELOPER PORTALのText Inputsの欄](https://discord.com/developers/docs/interactions/message-components#text-inputs)を見るとModalでrenderされるよって書いてあると思います．なので，discord.pyのapiリファレンスだけじゃなく，ときおりDEVELOPER PORTALの方のapiリファレンスも見た方がいいとおもいました

メッセージの転送でタイトルをつけようとしたら，追加でボタンを作成してそのボタンを押したらtitleをつけるためModalを表示させるようなことをするか，そもそもタイトルの表示を諦めるか，context_menuではなくcommandにしてメッセージのIDを取得してもらってそれをcommandの引数で指定してもらう，などといったことが考えられるかもしれません．まあ，そこまでしてユーザーの入力が欲しいのかを聞かれると困ってしまいますので諦めた方がいいかもしれません．


以上となります．discord.pyのTextInputを紹介しました．