Discord.pyの開発が2021年に終了してしまったため、現在一番人気があると思われる後継の「pycord」ライブラリを使ってDiscordのbotを作っていたところ、思わぬところで躓いた。


import discord

from discord.commands import Option

TOKEN = 'botのトークン'
test_channel_id = 'チャンネルid'
test_server_id = 'サーバーid'
bot = discord.Bot()

#ログイン時にコンソールにログを流す
@bot.listen()
async def on_ready():
    print('Logged in as\n' + bot.user.name + "/n" + bot.user.id + "/n------")

#descriptionの引数で与えた文字列をbotが返信する
@bot.slash_command(guild_ids=[test_server_id])
async def example_command(
    ctx,
    description: Option(str, 'botが発言するメッセージ')
):
    await ctx.respond(str) #botがコマンドに対して返信する

#特定チャンネルにメッセージを送信する
@bot.slash_command(guild_ids=[test_server_id])
async def example_command2(ctx):
    #チャンネルを取得する(ここでつまづいた)
    channel = bot.get_partial_messageable(test_channel_id)
    await channel.send("これはテスト用のメッセージ")

bot.run(TOKEN)

2022/02/27更新:メソッド名が同じになっていたため少し修正

結果からすると、こうだ。

普通は21行目のようにctx.respondを使ってコマンドに対してメッセージをbotに返信させる、といった使い方をするらしいのだが、グローバルチャット(共有チャット)的な機構を作るときに「特定のチャンネルにメッセージを送信したい」といったシーンで対応できなかった。

client = discord.client()
#clientはあらかじめ用意しておく
await client.wait_until_ready()

を使うことでキャッシュを読み込むまで待機する的な説明をRedditやらStackOverFlowやらどっかで拾った記憶があるのだが、pycordでこれを使った場合そこで処理が無限に続いてしまった。

そもそもpycordではリファレンスを見てもclientを使ってるような感じがしない。discord.Bot()にすべてを含んでいる、的な説明も観た覚えがあるので基本的にはこれにどうこうする形でいいんじゃなかろうか。

そしてもっとも簡単なのがctxを使ったctx.respondを使った返信方法なのだが、

チャンネルを取得して実行したい!というとき、リファレンス通り、もしくはDiscord.pyの書き方をまねるとこれに至るだろう。

channel = bot.get_channel(test_channel_id)

もしくはclientを宣言してclient.get_channelとしたとか。

しかし、これだと

AttributeError: 'NoneType' object has no attribute 'send'

が帰ってくる。これはget_channelが引数として受け取ったチャンネルidを探して、存在しない場合にNoneを返すからだ。チャンネルオブジェクトでない、ただのNoneに対してsendしているためsendがNoneTypeに存在しないというエラーが帰ってくる。

チャンネルは実在するし、botの管理範囲内のハズなのに、だ。StackOverFlowで助けを求めていた人の中には、プログラムの構文がおかしくてbot.runを実行する前にチャンネルを取得しようとしてNoneを返されていたというパターンはあった。これは構文ミスなので一応見直しを。

しかしチャンネルが存在するのにget_channelでid指定してもチャンネルを取得できない。これで2000×10^-3時間くらい悩んだ。

そもそもなぜチャンネルオブジェクトに対してsend出来ていないのかということに初心者ながら気づかなかった。そもそもNoneTypeオブジェクトであったということに。

では、チャンネルidが存在しないというNoneが何故帰ってくるのか。

リファレンスを見てみる。

get_channel(id, /)

Returns a channel or thread with the given ID.

Parameters id (int) – The ID to search for. Returns The returned channel or None if not found.

https://docs.pycord.dev/en/master/api.html#discord.Client.get_channel

idを渡せばチャンネルを返すと説明してあるのに、チャンネルidが存在しないわけでもないのにNoneを返してくる。何故なのか一切不明。

あてずっぽうで調べ調べでやっていて、チャンネルを取得できたのがこっち。

get_partial_messageable(id, *, type=None)

Returns a partial messageable with the given channel ID.

This is useful if you have a channel_id but don’t want to do an API call to send messages to it. New in version 2.0. Parameters

id (int) – The channel ID to create a partial messageable for.

type (Optional[ChannelType]) – The underlying channel type for the partial messageable.

https://docs.pycord.dev/en/master/api.html#discord.Client.get_partial_messageable

直訳すれば「部分的にメッセージ可能」を返すとのこと。

翻訳すると、「channel_idがあり、それにメッセージを送信するためにAPI呼び出しを実行したくない場合に役立ちます。」とのことだ。

API呼び出しがよく分からないが、get_channelで取得する事が出来なかったのでこっちを使うと、ちゃんと取得する事が出来た。

これで29,30行目のようにチャンネルIDからチャンネルを取得してメッセージを送信させる事が出来たわけだ。

おそらくctxからチャンネルidとかも取得できるだろうし、そっからコマンドへの変身という形ではなく単純にメッセージを送信したいときにも使えるのかな。(ctxに直でsendでも行けるかもしれないけど、そこらへんは試してないから各自で調べてね(‘ω’))

ちなみにここまで来て、ctxの意味が全く分かっていないのである。

メッセージに含まれるサーバーid、ユーザーid、その他情報的な何かかな?とぼんやり程度しか思ってないが・・・これもリファレンスに書いてあるのかな。見つけきれなかった。

・・・という感じで、get_channelでチャンネルを取得できないときの対処法を紹介した。恐らくこれが役に立った!って人は少ない(直でチャンネルID指定するシーンがほぼ無いから)と思うけど、参考になれば。

pycordのリファレンス

CAPTCHA