🐄

discord.pyを利用した作業管理bot 開発メモ

2022/12/31に公開

作業管理botを作った

研究室のアルバイトなどをしていて、discord上で作業時間の管理できたらいいなぁと思ったのをきっかけに作ってみました。せっかくなので作業ログを残しておきます。雑めです。

ちょうど後輩くんがアドベントカレンダーでdiscord bot使った記事を載せていたのでたくさん参考にしました。ありがとう後輩くん。この後ちょくちょく引用します。

作ったもの

要件

  • プロジェクトごとに作業時間を管理できる
    • 作業内容も任意でのこせると嬉しい
  • 後から作業時間の確認ができる
    • 累計作業時間
    • 作業時間の履歴が見れても嬉しい

作成したコマンド

  • \start <project_name> <description>
    • プロジェクトの作業を開始
  • \stop <project_name>
    • プロジェクトの作業を終了
  • \projects
    • プロジェクトの累計時間の一覧
  • \download_file
    • プロジェクトの累計時間・作業内容の一覧ファイルをダウンロード

実装メモ

今回はdiscord.pyを利用してdiscord botを作成しました。
ここには今回の実装で必要になった機能の実装方法や参考記事などを残しておきます。

botをオンラインに

まずはbotをdiscordのサーバーにログインさせ、オンラインにするところから。これは後輩くんの記事discord botを作成する流れ を参考にしました。

コマンド登録

ログインできたら、次はコマンド実装!これも後輩くんの記事discord botへのコマンド登録にお世話になりました。

任意の引数

任意の引数を設定したい場合は、以下のように引数の型にNoneを当てます。

app.py
@client.tree.command()
@app_commands.describe(project="プロジェクト名", description="作業内容(任意)")
async def start(
    interaction: Interaction,
    project: str,
    description: str = None,
):
    # 処理

ファイルの送信

ファイルを返したい場合はsend_message()といったメソッドの引数にfile=File(filepath)を渡すことでファイルを返せるようになります。

app.py
@client.tree.command()
async def download_file(interaction: Interaction):
    filepath = "./hoge/hoge.json"
    await interaction.response.send_message(file=File(filepath))

helpコマンドにEmbedを使いたい

discord botの\helpで表示されるものって以下のようなことが多いですよね。これをEmbedと言います。この作成にはDiscord.pyでEmbedを扱う(メモ)を参考にしました。

引数入力の候補を出したい

以下のように引数の選択候補を出したい場合は@app_commands.choices()を使用します。

choices()の引数に与えたい選択肢を配列にして渡します。ここでもメソッドの引数のtypeにNoneを渡すことで引数の入力を任意にできます。
選択した値は引数名.name, 引数名.valueで取得することができます。今回の場合はcommands.value, commands.nameです。

app.py
@client.tree.command()
@app_commands.describe(commands="コマンド名(任意)")
@app_commands.choices(commands=[
    Choice(name="start", value="start"),
    Choice(name="stop", value="stop"),
    Choice(name="projects", value="projects"),
    Choice(name="download_file", value="download_file"),
])
async def help(interaction: Interaction, commands: Choice[str] = None):
    if commands is None:
        # 基本のhelpメニューの表示
        # embed =
        await interaction.response.send_message(embed=embed)
    else:
        # コマンドに応じたelpメニューの表示
        # commands.value, commands.nameで選択した値が取れる
        # embed = 
        await interaction.response.send_message(embed=embed)

botを常駐させたい

Discord Botを作ってGoogleComputeEngineに配置を参考に、ゼミのGoogleComputeEngineに配置しました。

いやー、作っててデプロイどうしようかな〜ってdiscordで呟いてると公開鍵寄越せって言ってくれるゼミの先生に感謝ですね。

ゼミサーバーのbotなのでデータの方もゼミのデータベースに保存させてもらいました。

詰まったところ

discord.pyを動かそうとしたら CERTIFICATE_VERIFY_FAILED

いざdiscord上のbotをオンラインにするぞ!ワクワク!としていたら、以下のエラーが発生

Cannot connect to host discord.com:443 ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1123)')]

macOS用公式インストーラーのPython 3.6でCERTIFICATE_VERIFY_FAILEDとなる問題 の記事を参考に以下のファイルを実行したところエラーは解消されました。

$ /Applications/Python/3.9/Install/Certificates.command

上記の記事によると

macOSに標準でインストールされているOpenSSLが古すぎるため、Python 3.6以降ではmacOS用のインストーラーにはOpenSSLが同梱され、システムのOpenSSLは参照されなくなりました。
これによって、OSにインストールされたルート証明書も参照されず、インストール直後の状態ではルート証明書が含まれていません1。このため、TLSサーバー証明書の検証に失敗します。

とのことらしいです。なるほど。

クライアントは3秒以内に応答することが期待されている件について

続いて、APIからデータを取得するという処理を数回行ったところ、実行時間が3秒を超えてしまい、以下のエラーが発生。

discord.app_commands.errors.CommandInvokeError: Command 'start' raised an exception: NotFound: 404 Not Found (error code: 10062): Unknown interaction

はじめ何が原因が全くわからず右往左往してましたが、Discord Developer Portal Interactionsにちゃんと書いてありました。

Interaction tokens are valid for 15 minutes and can be used to send followup messages but you must send an initial response within 3 seconds of receiving the event. If the 3 second deadline is exceeded, the token will be invalidated.

followupを使うことでレスポンスまでの時間を15分まで伸ばすことができるみたいですね。今回はfollowupと合わせてdeferを使用しました。

次のようにdefer()でインタラクションの応答を遅らせ、followup.send()でその後にメッセージを送る処理を行います。

@client.tree.command()
async def defer(interaction: Interaction):
    await interaction.response.defer()
    await asyncio.sleep(4)
    await interaction.followup.send("finish")

また、defer()の引数にephemeralがあるのですが、これをTrueにすると以下のようにコマンドを使用したユーザーにのみメッセージが表示されるようになります。デフォルト値はFalseになっています。

Discussion