エコシステムからの離脱を発表してから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のエコシステムがあまりにも断片的であることがますます明らかになってきました。私は、半年後には、ほとんどのユーザーが透過的に移行できる明確な代替ライブラリが存在することを期待していましたが、結局それは実現されなかったようです。エコシステムがこれ以上混乱しないようにするためには、現在の不幸な状況を改善することが賢明でした。
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.icon
と Role.display_icon
です。 display_icon
を変更したい場合は、Role.edit
がサポートされています。
これもシンプルで、 File
に description
というクワーグを渡すだけでよいです。また、 Attachment
で読み込むためのプロパティでもあります。
このライブラリは Intents.message_content
フラグをサポートし、デフォルトで API v10 を使用するようになりました。4月30日までの期限までにメッセージ内容の意図をユーザーに伝える必要があるため、意外と禁止されていることがわかりました。ボットを機能させるために必要であれば、ボットとボットアプリケーションページの両方で Intents.message_content
を有効にすることを推奨します。
重要なことなので繰り返しますが、APIのバージョンが上がっているため、メッセージコンテンツが必要なボットを機能させるには、あなたのコードと開発者ポータルの両方でメッセージコンテンツのインテントを有効にする必要があります。この要件は残念ながらDiscordによって課されたもので、私のコントロールの及ばないものです。
discord.ui.TextInput
と discord.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.defer
がthinking=True
とthinking=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
のデザインに対応できるようになります。
- これにより、discord.pyがよりモダンな
- イベントを、マルチパラメーターではなく、シングルパラメーターの "イベントオブジェクト" を受け取るようにリファクタリングしました。
- これは、リッチな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
これは間違いなく、あなたたちの協力なしには実現できなかったことで、とても感謝しています :)