Skip to content

Instantly share code, notes, and snippets.

@eggplants
Forked from Rapptz/dpy_development_plans.md
Last active April 24, 2022 20:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eggplants/3b32e1918e7bf9d80e94331784e58222 to your computer and use it in GitHub Desktop.
Save eggplants/3b32e1918e7bf9d80e94331784e58222 to your computer and use it in GitHub Desktop.
discord.pyの開発再開に際して作者(Rapptz)が発表した声明文(英語)を日本語に完訳したもの。(Updated at: 2022-04-25)

discord.pyの開発再開について

エコシステムからの離脱を発表してから6ヶ月経ちましたが、その間に多くのことが起こりました。

この2週間の間に、多くの貢献者と私は一緒になって、Python Discord ボットのエコシステムがほとんど燃えている状態であることを確認しました。他の人の助けを借りて熟考した後、私は開発を再開すべきだという結論に達しました。この2週間の間に、多くの作業がdiscord.pyプロジェクトに追いつき、多くのことを実装し、最終的にv2.0リリースを開始するために費やされました。我々は締め切りに追われていますが、多くの仕事を成し遂げることができました。

なぜ戻ってきたのですか?

3週間ほど前、DiscordはAPIバージョン6と7を2022年5月1日に廃止することを発表しました。現在のベータ版であるv2.0がバージョン9であるのに対し、現在の安定版であるdiscord.pyのv1.7.3はAPIのバージョン7である。つまり、Discordのv6と7の廃止計画により、2022年5月1日をもって安定版のdiscord.py上のボットはすべて動作しなくなる。これは、こんなに早く起こるとは想定していませんでした。

私はDiscordと廃止の影響について議論しようとしましたが、彼らは私の懸念を認めたものの、廃止日が延期されるという保証は何もありませんでした。もし廃止が計画通りに進めば、当初の特権的なメッセージの意図の要求よりも、エコシステムにとってはるかに壊滅的な打撃となるでしょう。この変更のダメージを最小限にするために、私はライブラリの更新が必要だと感じました。

同様に、時間が経つにつれて、Python Botのエコシステムがあまりにも断片的であることがますます明らかになってきました。私は、半年後には、ほとんどのユーザーが透過的に移行できる明確な代替ライブラリが存在することを期待していましたが、結局それは実現されなかったようです。エコシステムがこれ以上混乱しないようにするためには、現在の不幸な状況を改善することが賢明でした。

今、Discordは改善されていると思いますか?

4月30日の期限が近づいていますが、Discordがエコシステム全体の移行について十分な計画を立てているとはまだ思えませんし、模範的な仕事をしているとも思えません。この6ヶ月の間、DiscordのAPIに追加されたものはそれほど多くありません:

  • メンバーのタイムアウト
    • 28日間に限定されますが、全体的に適切な機能です。
  • ロールアイコン
    • ブーストのみ
  • サーバーのアバターと経歴
    • ブーストのみ
  • スラッシュコマンドのファイル添付
    • この機能は当初問題がありましたが、現在では修正されていると思います。
  • 添付ファイルのエフェメラルサポート。
    • これは良いですね。
  • コンポーネントモダル
    • 現在の形では信じられないほど制限されており、テキストコンポーネントのみ使用可能です。
  • オートコンプリート
    • この機能はかなりクールです。
  • 添付ファイルの Alt テキスト
  • メッセージ上の添付ファイルの編集

基本的にはこれで終わりです。期限まで2ヶ月ありますが、パーミッションやいわゆる「スレートv2」などの主要なものは、将来のどこかの時点でテストする予定ではあるものの、まだ欠落している状態です。

もちろん、いわゆる5日間のSLAを約束したにもかかわらず、1~3ヶ月待ちでメッセージコンテンツのインテント申請をしている人がいることは言うまでもありません。すべてが混乱したままであり、そうでないふりをすることは、助長しているように思えてならないのです。

Discordとの個人的なコミュニケーションという点では、開発をやめてから改善されたとは言い難く、むしろコミュニケーションは苦しくなりました。開発中止後、私はDiscordの開発者と話をすることはありませんでしたし、彼らも私と話をすることはありませんでした。唯一の接触は、私が廃止の話をするために彼らに連絡したことです。

注: "Permissions v2", "Slate v2", "Slash Command Localisation"は、今後リリースを予定している機能です。

新機能のご紹介

以下が、この2週間、私たちが一生懸命に取り組んできたことです。

メンバーのタイムアウト

Member に新しい属性 timed_out_until が追加されました。これは Member.edit で動作し、タイムアウトさせることができます。また、 Member.is_timed_out もあり、メンバーがタイムアウトしているかどうかを確認することができます。

ロールアイコン

同じようにシンプルに Role.iconRole.display_icon です。 display_icon を変更したい場合は、Role.edit がサポートされています。

添付ファイルへのAltテキスト

これもシンプルで、 Filedescription というクワーグを渡すだけでよいです。また、 Attachment で読み込むためのプロパティでもあります。

API v10

このライブラリは Intents.message_content フラグをサポートし、デフォルトで API v10 を使用するようになりました。4月30日までの期限までにメッセージ内容の意図をユーザーに伝える必要があるため、意外と禁止されていることがわかりました。ボットを機能させるために必要であれば、ボットとボットアプリケーションページの両方で Intents.message_content を有効にすることを推奨します。

重要なことなので繰り返しますが、APIのバージョンが上がっているため、メッセージコンテンツが必要なボットを機能させるには、あなたのコードと開発者ポータルの両方でメッセージコンテンツのインテントを有効にする必要があります。この要件は残念ながらDiscordによって課されたもので、私のコントロールの及ばないものです。

モーダル

discord.ui.TextInputdiscord.ui.Modal のサポートが追加されました。この構文は discord.ui.View と似ていますが、各コンポーネントが個別のコールバックを持つことができないので、若干の違いがあります。簡単な例です:

import discord
from discord import ui

class Questionnaire(ui.Modal, title='Questionnaire Response'):
    name = ui.TextInput(label='Name')
    answer = ui.TextInput(label='Answer', style=discord.TextStyle.paragraph)

    async def on_submit(self, interaction: discord.Interaction):
        await interaction.response.send_message(f'Thanks for your response, {self.name}!', ephemeral=True)

モーダルを送信するには、特別なインタラクションレスポンスタイプを必要とするため、 Interaction.response.send_modal が使用されます。メッセージとモーダルを同時に送信することはできません。それが望ましいのであれば、 Interaction.followup.send を使用することを検討してください。

インタラクションの改善

  • Interaction.client が追加され、万が一に備えてクライアントを取得できるようになりました。
  • Interaction.response.deferthinking=Truethinking=False をサポートするようになりました。UI が必要な場合に使用します。これは InteractionType.deferred_channel_message に相当します。
  • Interaction.response.send_message がファイルの送信をサポートするようになりました。これはエフェメラルファイル(ephemeral files, 投稿後短時間で自動的に削除されるファイル)もサポートします。
  • アプリケーションコマンドのオートコンプリートレスポンス用に、低レベルの Interaction.response.autocomplete ヘルパーを追加しました。

スラッシュコマンドとコンテキストメニューコマンド

いくつかの設計作業と熟考の後、私は discord.ext.commands パッケージのサブセットであるシンタックスを使用してスラッシュコマンドを実装しました。これらは新しい名前空間である discord.app_commands に存在し、ほとんど同じように機能します。コマンドを登録するためには、 app_commands.CommandTree という新しい型が必要で、これは作成時に Client を唯一の引数として受け取ります:

import discord
from discord import app_commands

intents = discord.Intents.default()
intents.message_content = True

client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

ツリーの設定後、コマンドの追加は、コマンド拡張とほとんど同じです:

@tree.command(guild=discord.Object(id=MY_GUILD_ID))
async def slash(interaction: discord.Interaction, number: int, string: str):
    await interaction.response.send_message(f'{number=} {string=}', ephemeral=True)

# パラメータの説明...
@tree.command(guild=discord.Object(id=MY_GUILD_ID))
@app_commands.describe(attachment='The file to upload')
async def upload(interaction: discord.Interaction, attachment: discord.Attachment):
    await interaction.response.send_message(f'Thanks for uploading {attachment.filename}!', ephemeral=True)

キーワード引数 guild を省略することで、代わりにグローバルコマンドとして追加されます。もし、デコレータを使いたくない場合は、以下のコードと同等になります:

@app_commands.command()
async def slash(interaction: discord.Interaction, number: int, string: str):
    await interaction.response.send_message(f'{number=} {string=}', ephemeral=True)

# ここで guild を指定することも可能ですが、この例では指定しないことにしています。
tree.add_command(slash)

グループ

グループでの作業は、身近にある歯車(cogs, UI上の設定メニューのことか)と同じような機能です:

class Permissions(app_commands.Group):
    """メンバーの権限を管理します。"""

    def get_permissions_embed(self, permissions: discord.Permissions) -> discord.Embed:
        embed = discord.Embed(title='Permissions', colour=discord.Colour.blurple())
        permissions = [
            (name.replace('_', ' ').title(), value)
            for name, value in permissions
        ]

        allowed = [name for name, value in permissions if value]
        denied = [name for name, value in permissions if not value]

        embed.add_field(name='Granted', value='\n'.join(allowed), inline=True)
        embed.add_field(name='Denied', value='\n'.join(denied), inline=True)
        return embed

    @app_commands.command()
    @app_commands.describe(target='The member or role to get permissions of')
    async def get(self, interaction: discord.Interaction, target: Union[discord.Member, discord.Role]):
        """メンバーまたはロールの権限を取得する。"""
        
        if isinstance(target, discord.Member):
            assert target.resolved_permissions is not None
            embed = self.get_permissions_embed(target.resolved_permissions)
            embed.set_author(name=target.display_name, url=target.display_avatar)
        else:
            embed = self.get_permissions_embed(target.permissions)

        await interaction.response.send_message(embed=embed)

    @app_commands.command(name='in')
    @app_commands.describe(channel='The channel to get permissions in')
    @app_commands.describe(member='The member to get permissions of')
    async def _in(
        self, 
        interaction: discord.Interaction, 
        channel: Union[discord.TextChannel, discord.VoiceChannel],
        member: Optional[discord.Member] = None,
    ):
        """特定のチャンネルで、自分または他のメンバーの権限を取得します。"""
        embed = self.get_permissions_embed(channel.permissions_for(member or interaction.user))
        await interaction.response.send_message(embed=embed)


# ツリーにグループを追加するには...
tree.add_command(Permissions(), guild=discord.Object(id=MY_GUILD_ID))

ただし、Discordの制限により、グループにコールバックを付けて呼び出すことはできません:

class Tag(app_commands.Group):
    """タグの名前から取得する。"""

    stats = app_commands.Group(name='stats', description='Get tag statistics')

    @app_commands.command(name='get')
    @app_commands.describe(name='the tag name')
    async def tag_get(self, interaction: discord.Interaction, name: str):
        """名前でタグを取得する。"""
        await interaction.response.send_message(f'tag get {name}', ephemeral=True)

    @app_commands.command()
    @app_commands.describe(name='the tag name', content='the tag content')
    async def create(self, interaction: discord.Interaction, name: str, content: str):
        """タグを作成する。"""
        await interaction.response.send_message(f'tag create {name} {content}', ephemeral=True)

    @app_commands.command(name='list')
    @app_commands.describe(member='the member to get tags of')
    async def tag_list(self, interaction: discord.Interaction, member: discord.Member):
        """ユーザーのタグ一覧を取得する。"""
        await interaction.response.send_message(f'tag list {member}', ephemeral=True)

    @stats.command(name='server')
    async def stats_guild(self, interaction: discord.Interaction):
        """サーバーのタグの統計情報を取得する。"""
        await interaction.response.send_message(f'tag stats server', ephemeral=True)

    @stats.command(name='member')
    @app_commands.describe(member='the member to get stats of')
    async def stats_member(self, interaction: discord.Interaction, member: discord.Member):
        """メンバーのタグの統計情報を取得する。"""
        await interaction.response.send_message(f'tag stats member {member}', ephemeral=True)

tree.add_command(Tag())

コンテクストメニュー

コンテキストメニューも簡単で、関数に discord.Member または discord.Message というアノテーションを付けるだけです:

@tree.context_menu(guild=discord.Object(id=MY_GUILD_ID))
async def bonk(interaction: discord.Interaction, member: discord.Member):
    await interaction.response.send_message('Bonk', ephemeral=True)

@tree.context_menu(name='Translate with Google', guild=discord.Object(id=MY_GUILD_ID))
async def translate(interaction: discord.Interaction, message: discord.Message):
    if not message.content:
        await interaction.response.send_message('No content!', ephemeral=True)
        return

    text = await google_translate(message.content)  # Exercise for the reader!
    await interaction.response.send_message(text, ephemeral=True)

範囲

指定した範囲で数値を制限するには、app_commands.Rangeアノテーションを使用します:

@tree.command(guild=discord.Object(id=MY_GUILD_ID))
async def range(interaction: discord.Interaction, value: app_commands.Range[int, 1, 100]):
    await interaction.response.send_message(f'Your value is {value}', ephemeral=True)

選択

また、選択肢は3つの異なるフレーバーでサポートされています。1つ目は最もシンプルなもので、typing.Literalを使用します:

@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
async def fruit(interaction: discord.Interaction, fruits: Literal['apple', 'banana', 'cherry']):
    await interaction.response.send_message(f'Your favourite fruit is {fruits}.')

もし、値に名前を付けたいのであれば、enum.Enum派生クラスを使うのが次のステップアップです:

class Fruits(enum.Enum):
    apple = 1
    banana = 2
    cherry = 3

@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
async def fruit(interaction: discord.Interaction, fruits: Fruits):
    await interaction.response.send_message(f'Your favourite fruit is {fruits}.')

もし、実際の選択肢のリストをもっとコントロールしたいのであれば、 app_commands.choices デコレーターがあります:

from discord.app_commands import Choice

@app_commands.command()
@app_commands.describe(fruits='fruits to choose from')
@app_commands.choices(fruits=[
    Choice(name='apple', value=1),
    Choice(name='banana', value=2),
    Choice(name='cherry', value=3),
])
async def fruit(interaction: discord.Interaction, fruits: Choice[int]):
    await interaction.response.send_message(f'Your favourite fruit is {fruits.name}.')

名前を気にしないのであれば、ここでアノテーションとして素の int を使うこともできることに注意してほしい。

オートコンプリート(自動補完)

また、discord.pyは2種類のデコレーター構文によるオートコンプリートをサポートしました:

@app_commands.command()
async def fruits(interaction: discord.Interaction, fruits: str):
    await interaction.response.send_message(f'Your favourite fruit seems to be {fruits}')

@fruits.autocomplete('fruits')
async def fruits_autocomplete(
    interaction: discord.Interaction,
    current: str,
    namespace: app_commands.Namespace
) -> List[app_commands.Choice[str]]:
    fruits = ['Banana', 'Pineapple', 'Apple', 'Watermelon', 'Melon', 'Cherry']
    return [
        app_commands.Choice(name=fruit, value=fruit)
        for fruit in fruits if current.lower() in fruit.lower()
    ]

あるいは、その代わりに:

@app_commands.command()
@app_commands.autocomplete(fruits=fruits_autocomplete)
async def fruits(interaction: discord.Interaction, fruits: str):
    await interaction.response.send_message(f'Your favourite fruit seems to be {fruits}')

async def fruits_autocomplete(
    interaction: discord.Interaction,
    current: str,
    namespace: app_commands.Namespace
) -> List[app_commands.Choice[str]]:
    fruits = ['Banana', 'Pineapple', 'Apple', 'Watermelon', 'Melon', 'Cherry']
    return [
        app_commands.Choice(name=fruit, value=fruit)
        for fruit in fruits if current.lower() in fruit.lower()
    ]

同期

discord.pyでは自動同期は行ってません。これはユーザーの責任です。コマンドを同期させるには、以下のようにします:

await tree.sync(guild=discord.Object(id=MY_GUILD_ID))

# または、グローバルコマンドを同期させる場合:
await tree.sync()

すべてのギルドコマンドを同期する方法は明らかに存在しないことに注意してください。これは、リクエストが多くなりすぎるためです。同様に、discord.pyはユーザーから明示的に指示されない限り、バックグラウンドでHTTPリクエストを行わないように意識的に取り組んでいます。

その他

例えば、トランスフォーマー (コンバーターに相当) や on_error ハンドラなど、他にもたくさんのものがありますが、このセクションはすでに長くなりすぎています。

将来的な計画

まだやるべきことはたくさんあります。私たちはワーキンググループを結成し、基本的にdiscord.pyをよりアクセスしやすく、学びやすいものにするためのガイドを作ることにしています。もしあなたがこういったことに参加したいのであれば、どうぞご遠慮なく。大きな取り組みなので、手助けが得られると思います。

これらは現在計画されている大きなものですが、実際に実現されるかどうかは不明です。

  • より散文的なページと分かりやすいドキュメントを持つ、discord.pyのガイドに取り組んでいます。
  • ループオブジェクトを人質に取る代わりに、asyncio.runを使用できるようにリファクタリングします。
    • これにより、discord.pyがよりモダンなasyncioのデザインに対応できるようになります。
  • イベントを、マルチパラメーターではなく、シングルパラメーターの "イベントオブジェクト" を受け取るようにリファクタリングしました。
    • これは、リッチなAPIのためのヘルパーメソッドを持つ、すべてのイベントを "生のイベント "にするようなものです。
  • 新しい app_commands 名前空間との整合性を保つために、View パラメータの順番を変更しました。

私たちはまだ締め切りに迫われています!また、説明した機能は、フィードバックにより変更される可能性がありますことをご了承ください。

謝辞

何らかの形でご協力いただいた関係者の皆様に深く感謝申し上げます(a-z順):

  • devon#4089
  • Eviee#0666
  • Gobot1234#2435
  • Imayhaveborkedit#6049
  • Jackenmen#6607
  • Josh#6734
  • Kaylynn#0001
  • Kowlin#2536
  • LostLuma#7931
  • Maya#9000
  • mniip#9046
  • NCPlayz#7941
  • Orangutan#9393
  • Palm__#0873
  • Predä#1001
  • SebbyLaw#2597
  • TrustyJAID#0001
  • Twentysix#5252
  • Umbra#0009
  • Vaskel#0001

これは間違いなく、あなたたちの協力なしには実現できなかったことで、とても感謝しています :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment