よくある質問

これは discord.py 及び拡張モジュールの利用に際し、よくある質問をまとめたものです。気兼ねなく質問やプルリクエストを提出してください。

質問

コルーチン

コルーチンとasyncioに関する質問。

コルーチンとはなんですか。

coroutine とは await または yield from から呼び出さなければならない関数です。Pythonは await の付いた命令を処理する際、その時点で一度その関数の実行を停止し、他の作業を実行します。 これは await のついた命令の作業が終了し、再度停止したポイントに戻ってくるまで続きます。 これにより、スレッドや複雑なマルチプロセッシングを用いずに複数の処理を並列実行することができます。

コルーチンにawaitを記述し忘れた場合、コルーチンは実行されません。awaitの記述を忘れないように注意してください。

await はどこで使用することができますか。

awaitasync def で定義される関数の中でのみ使用できます。

「ブロッキング」とはなんですか。

非同期プログラミングにおけるブロッキングとは、関数内の await 修飾子がないコードすべてを指します。 しかし、全てのブロッキングが悪いというわけではありません。ブロッキングを使用することは避けられませんが、ブロックの発生は出来るだけ少なくする必要があります。長時間のブロックが発生すると、関数の実行が停止せず他の命令を処理することができないため、長時間Botがフリーズすることになることを覚えておきましょう。

ログの出力を有効にしている場合、ライブラリはブロッキングが起きていることを Heartbeat blocked for more than N seconds. というメッセージで警告します。ログの出力を有効にするには、 ログの設定 を参照してください。

長時間のブロッキングの原因として一般的なのは time.sleep() などです。 これは使用せず、下記の例のように asyncio.sleep() を使用してください。

# bad
time.sleep(10)

# good
await asyncio.sleep(10)

また、これだけでなく、有名なモジュール Requests: HTTP for Humans™ のHTTPリクエストも長時間ブロックの原因になります。 Requests: HTTP for Humans™ モジュールは同期プログラミングにおいては素晴らしいモジュールですが、特定のリクエストがイベントループを長時間ブロックする可能性があるため、 asyncio には適していません。 代わりにこのライブラリと一緒にインストールされた aiohttp を使用してください。

次の例を見てみましょう。

# bad
r = requests.get('http://aws.random.cat/meow')
if r.status_code == 200:
    js = r.json()
    await channel.send(js['file'])

# good
async with aiohttp.ClientSession() as session:
    async with session.get('http://aws.random.cat/meow') as r:
        if r.status == 200:
            js = await r.json()
            await channel.send(js['file'])

一般

ライブラリの使用に関する一般的な質問。

使用例はどこで確認できますか。

サンプルコードはレポジトリの examplesフォルダ で確認できます。

「プレイ中」状態の設定をするにはどうすればいいですか。

キーワード引数である activityClient のコンストラクタ、または Client.change_presence()Activity を渡すことで設定できます。

静的なアクティビティを設定する際はコンストラクタ、実行時にアクティビティを更新する際には Client.change_presence() が利用できます。

警告

on_ready()Client.change_presence() やその他APIの呼び出しを行うことは推奨できません。このイベントは実行中に一度ではなく複数回呼び出される可能性があるためです。

また、接続後すぐにプレゼンスを変更すると高確率で切断されます。

ステータスタイプ(プレイ中、再生中、配信中、視聴中)は列挙型の ActivityType を指定することで設定が可能です。メモリの最適化のため、一部のアクティビティはスリム化して提供しています。

これらの情報をまとめると以下のようになります。

client = discord.Client(activity=discord.Game(name='my game'))

# or, for watching:
activity = discord.Activity(name='my activity', type=discord.ActivityType.watching)
client = discord.Client(activity=activity)

特定のチャンネルにメッセージを送るにはどうすればいいですか。

チャンネルを直接取得してから、適切なメソッドの呼び出しを行う必要があります。以下がその例です。

channel = client.get_channel(12324234183172)
await channel.send('hello')

DMを送るにはどうすればいいですか。

UserMember を取得し、 abc.Messageable.send() を呼び出してください。以下は使用例です。

user = client.get_user(381870129706958858)
await user.send('👀')

on_message() などのイベントで応答を返したい場合は、 Message.author を介して User オブジェクトが取得できます。

await message.author.send('👋')

送信したメッセージのIDを取得するにはどうすればいいですか。

abc.Messageable.send() は送信したメッセージの Message を返します。メッセージのIDには Message.id でアクセスできます。

message = await channel.send('hmm…')
message_id = message.id

画像をアップロードするにはどうすればいいですか。

Discordに何かをアップロードする際には File オブジェクトを使用する必要があります。

File は二つのパラメータがあり、ファイルライクなオブジェクト(または、そのファイルパス)と、ファイル名を渡すことができます。

画像をアップロードするだけなら、以下のように簡単に行なえます。

await channel.send(file=discord.File('my_file.png'))

もし、ファイルライクなオブジェクトがあるなら、以下のような実装が可能です。

with open('my_file.png', 'rb') as fp:
    await channel.send(file=discord.File(fp, 'new_filename.png'))

複数のファイルをアップロードするには、 file の代わりに files を使用しましょう。

my_files = [
    discord.File('result.zip'),
    discord.File('teaser_graph.png'),
]
await channel.send(files=my_files)

URLから何かをアップロードする場合は、 aiohttp のHTTPリクエストを使用し、 io.BytesIO インスタンスを File に渡す必要があります。

import io
import aiohttp

async with aiohttp.ClientSession() as session:
    async with session.get(my_url) as resp:
        if resp.status != 200:
            return await channel.send('Could not download file...')
        data = io.BytesIO(await resp.read())
        await channel.send(file=discord.File(data, 'cool_image.png'))

メッセージにリアクションをつけるにはどうすればいいですか。

Message.add_reaction() を使用してください。

Unicodeの絵文字を使用する場合は、文字列内の有効なUnicodeのコードポイントを渡す必要があります。 例を挙げると、このようになります。

  • '👍'

  • '\U0001F44D'

  • '\N{THUMBS UP SIGN}'

簡単な例:

emoji = '\N{THUMBS UP SIGN}'
# or '\U0001f44d' or '👍'
await message.add_reaction(emoji)

メッセージに含まれる絵文字を使用したい場合は、特になにをするでもなく、コンテンツ内のコードポイントを取得しています。また、 ':thumbsup:' のような簡略化したものを送信することは できません

カスタム絵文字については、 Emoji のインスタンスを渡してください。 '<:名前:ID>' 形式の文字列も渡せますが、その絵文字が使えるなら、 Client.get_emoji() でIDから絵文字を取得したり、 Client.emojisGuild.emojis に対して utils.find()/ utils.get() を利用することで取得ができるはずです。

カスタム絵文字の名前とIDをクライアント側で知るには、 :カスタム絵文字: の頭にバックスラッシュ(円記号)をつけます。たとえば、メッセージ \:python3: を送信すると、結果は <:python3:232720527448342530> になります。

簡単な例:

# if you have the ID already
emoji = client.get_emoji(310177266011340803)
await message.add_reaction(emoji)

# no ID, do a lookup
emoji = discord.utils.get(guild.emojis, name='LUL')
if emoji:
    await message.add_reaction(emoji)

# if you have the name and ID of a custom emoji:
emoji = '<:python3:232720527448342530>'
await message.add_reaction(emoji)

音楽プレイヤーの「後処理」関数にコルーチンを渡すにはどうすればいいですか。

ライブラリの音楽プレーヤーは別のスレッドで起動するもので、コルーチン内で実行されるものではありません。しかし、 after にコルーチンが渡せないというわけではありません。コルーチンを渡すためには、いくつかの機能を包括した呼び出し可能コードで渡す必要があります。

コルーチンを呼び出すという動作はスレッドセーフなものではないということを最初に理解しておく必要があります。技術的に別スレッドなので、スレッドセーフに呼び出す際には注意が必要です。幸運にも、 asyncio には asyncio.run_coroutine_threadsafe() という関数があります。これを用いることで、別スレッドからコルーチンを呼び出すことが可能です。

しかし、この関数は Future を返すので、実際にはそこから結果を読み出す必要があります。これをすべてまとめると、次のことができます。

def my_after(error):
    coro = some_channel.send('Song is done!')
    fut = asyncio.run_coroutine_threadsafe(coro, client.loop)
    try:
        fut.result()
    except:
        # an error happened sending the message
        pass

voice.play(discord.FFmpegPCMAudio(url), after=my_after)

バックグラウンドで何かを動かすにはどうすればいいですか。

background_task.pyの例を参照してください。

特定のモデルを取得するにはどうすればいいですか。

方法は複数ありますが、特定のモデルのIDがわかっていれば、以下の方法が使えます。

以下はHTTPリクエストを使用します。

上記の関数を使えない状況の場合、 utils.find() または utils.get() が役に立つでしょう。

簡単な例:

# find a guild by name
guild = discord.utils.get(client.guilds, name='My Server')

# make sure to check if it's found
if guild is not None:
    # find a channel by name
    channel = discord.utils.get(guild.text_channels, name='cool-channel')

Webリクエストはどうやって作ればよいですか。

リクエストを送るには、ノンブロッキングのライブラリを使わなければなりません。このライブラリは、リクエストを作成するのにサードパーティー製の aiohttp を必要とします。

簡単な例:

async with aiohttp.ClientSession() as session:
    async with session.get('http://aws.random.cat/meow') as r:
        if r.status == 200:
            js = await r.json()

詳細は aiohttpの完全なドキュメント を参照してください。

Embedの画像にローカルの画像を使用するにはどうすればいいですか。

特殊なケースとして、画像が別々に表示されないようDiscordにembedを用いてアップロードする際、画像は代わりにembedのサムネイルや画像、フッター、製作者アイコンに表示されます。

これを行うには、通常通り abc.Messageable.send() を用いて画像をアップロードし、Embedの画像URLに attachment://image.png を設定します。 image.png は送信する画像のファイル名になります。

簡単な例:

file = discord.File("path/to/my/image.png", filename="image.png")
embed = discord.Embed()
embed.set_image(url="attachment://image.png")
await channel.send(file=file, embed=embed)

監査ログのエントリが作成されるイベントはありますか。

Discordはゲートウェイでこの情報をディスパッチしないため、ライブラリによってこの情報を提供することはできません。これはDiscord側の制限によるものです。

コマンド拡張

discord.ext.commands に関する質問。

on_message を使うとコマンドが動作しなくなります。どうしてですか。

デフォルトで提供されている on_message をオーバーライドすると、コマンドが実行されなくなります。これを修正するには on_message の最後に bot.process_commands(message) を追加してみてください。

@bot.event
async def on_message(message):
    # do some extra stuff here

    await bot.process_commands(message)

別の方法として、 on_message に書きたい処理を リスナー として登録することもできます。この方法では bot.process_commands() を呼び出す必要はありません。また、この方法ではメッセージの受信に対して複数の処理を非同期に実行することができます。使用例:

@bot.listen('on_message')
async def whatever_you_want_to_call_it(message):
    # do stuff here
    # do not process commands here

コマンドの引数に引用符が必要なのはなぜですか。

次の簡単なコマンドを見てみましょう。

@bot.command()
async def echo(ctx, message: str):
    await ctx.send(message)

このコマンドを ?echo a b c のように実行したとき、コマンドに渡されるのは最初の引数だけです。その後の引数はすべて無視されます。これを正常に動かすためには ?echo "a b c" のようにしてコマンドを実行するか、コマンドの引数を下記の例のようにしてみましょう。

@bot.command()
async def echo(ctx, *, message: str):
    await ctx.send(message)

これにより、クォーテーションなしで ?echo a b c を使用することができます。

元の message を取得するにはどうすればよいですか。

Context は元のメッセージを取得するための属性である message を持っています。

例:

@bot.command()
async def length(ctx):
    await ctx.send(f'Your message is {len(ctx.message.content)} characters long.')

サブコマンドを作るにはどうすればいいですか。

group() デコレータを使います。これにより、コールバックが Group に変換され、groupに「サブコマンド」として動作するコマンドを追加できます。これらのグループは、ネストすることもできます。

例:

@bot.group()
async def git(ctx):
    if ctx.invoked_subcommand is None:
        await ctx.send('Invalid git command passed...')

@git.command()
async def push(ctx, remote: str, branch: str):
    await ctx.send(f'Pushing to {remote} {branch}')

これは ?git push origin master のように使うことができます。

ビューとモーダル

discord.ui.Viewdiscord.ui.Modal 、およびボタン、選択メニューなどそれらのコンポーネントに関する質問。

タイムアウト時にすべての項目を無効にするにはどうすればよいですか?

これには3つのステップが必要です。

  1. send() の戻り値の型を使用して、または Interaction.original_response() から取得した View にメッセージを添付します。

  2. on_timeout() 内で、ビュー内のすべての項目をループし無効にします。

  3. ステップ1で取得したメッセージを新しく変更したビューで編集します。

すべてをまとめると、テキストコマンドではこのように行うことができます。

class MyView(discord.ui.View):
    async def on_timeout(self) -> None:
        # Step 2
        for item in self.children:
            item.disabled = True

        # Step 3
        await self.message.edit(view=self)

    @discord.ui.button(label='Example')
    async def example_button(self, interaction: discord.Interaction, button: discord.ui.Button):
        await interaction.response.send_message('Hello!', ephemeral=True)

@bot.command()
async def timeout_example(ctx):
    """An example to showcase disabling buttons on timing out"""
    view = MyView()
    # Step 1
    view.message = await ctx.send('Press me!', view=view)

アプリケーションコマンドは InteractionResponse.send_message() で応答したときメッセージを返さないため、信頼性の高い方法で行うには Interaction.original_response() を使用してメッセージを取得すべきです。

以前のビューの定義を使用して、すべてをまとめます。

@tree.command()
async def more_timeout_example(interaction):
    """Another example to showcase disabling buttons on timing out"""
    view = MyView()
    await interaction.response.send_message('Press me!', view=view)

    # Step 1
    view.message = await interaction.original_response()

アプリケーションコマンド

一般的に「スラッシュコマンド」や「コンテキストメニューコマンド」と呼ばれている、Discordの新しいアプリケーションコマンドに関する質問。

私のボットのコマンドが一覧に表示されません!

  1. コマンドを sync() しましたか?コマンドは出現する前に同期する必要があります。

  2. 正しい権限でBotを招待しましたか? bot スコープに加えて、 applications.commands スコープ付きでBotを招待する必要があります。例えば、以下の URL でBotを招待します: https://discord.com/oauth2/authorize?client_id=<client id>&scope=applications.commands+bot 。 あるいは、 utils.oauth_url() を使用している場合は、 oauth_url(<other options>, scopes=("bot", "applications.commands")) のように関数を呼び出すことができます。