DiscordのBotの実装にdiscord.ext.commandsを利用したら中身がスッキリした

2021/05/13に公開

はじめに

この記事は前回の記事の実質的な続きとなる記事です。
前回実装したBotプログラムをベースに、メッセージを受け取ってそこからコマンドを吸い出す形式からdiscord.ext.commandsを利用して受け取ったコマンドから対応する関数を引き出す実装に置き換えたという事例を解説します。

何をするのか

前回はon_messageでbotから見える全てのメッセージを受け取って、それについて内容やDMかどうかの識別などをしてふるいにかけ処理を進める形式を取っていましたが、今回は処理内容の識別とDMかどうかの識別の二点をdiscord.ext.commandsの機能を用いて書き換えます。

準備

今回、discord.ext.commandsを使用するため冒頭に一行だけ加えます。

from discord.ext import commands

また、discordのクライアントの準備も異なります。

- client = discord.Client()
+ bot = commands.Bot(command_prefix="$")

今までのコードでは@client.eventのデコレータからon_messageなどを用いてメッセージを受け取ったりしていましたが、今回はdiscord.ext.commandsとしてコマンドを受け取る準備としてcommands.Bot()を用意します。
command_prefixの値でコマンドの接頭詞を指定します。この場合では$なので、$hogeと入力されればBot側はhogeというコマンドを受け取る形になります。
変数の名前がclientからbotになっているのは変更箇所をわかりやすくするためだったので、clientから変える必要はないです。(いずれにせよどちらか片方しか使わないため)

書き換える前の仕様について

前回実装したBotでは、コンマ区切りで「収入か支出か」「記録する名称」「金額」の3つをDMでBotに送ることでそれらを記録するという形式を取っています。
まず最初に、メッセージを受け取り処理する関数として、引数としてメッセージ内容を受け取るon_messageの準備をします。

@client.event
async def on_message(message):

そして、

if type(message.channel) == discord.DMChannel : 

この記述で、ここから先の処理はDMチャンネル以外だった場合は進めないようシャットアウトしています。

この後、ここでは省きますが、シートのURLを返す為に受け取ったメッセージが「シート」だった場合にはその処理を、といった処理をいくつか行った後に、

receipt = message.content.split(',')

        if len(receipt) != 3 :                      #支出、収入の入力がフォーマットに沿ってなかったら弾く
            await message.channel.send('入力が無効')
            return
        receipt[2] = receipt[2].replace('円','')     #金額に円と付いてたらその部分を取り除く

このような形で受け取ったメッセージ(message.content)をsplitで配列として分けて、パラメータの数が合わなかったらその時点で弾くといった挙動を行っています。
この後、最初の要素の値が「支出」か「収入」かをif文で判断し、処理を分岐させて収支の記録を行っています。
この処理をこれからdiscord.ext.commandsを利用したものに書き換えて行きます。

コマンドの定義

discord.ext.commandsでは指定した接頭詞を元にコマンドを受け取った場合、対応する名称の関数が呼び出されます。
この関数の定義の仕方は以下の通りです。

@bot.command()
async def hoge(ctx,prm,):

この場合、$hogeと入力された場合に関数hogeが呼び出されます。
引数ctxには受け取ったメッセージのcontextが収まり、メッセージを返したりする場合はこちらを利用することになます。
引数prm及びそれ以降の引数は、接頭詞のついたコマンドに付随して書かれたメッセージ(=コマンドの引数)が収まります。引数として用意した変数の数とメッセージに書かれた引数が食い違う場合はエラーが起きますが、以下の書き方を行うことで引数の数に関係なく配列で受け取ることが出来ます。

@bot.command()
async def hoge(ctx,*prm):

この書き方をした場合、引数の数によってエラーが起きることはなく、幾つ引数を受け取っても全て確保することが出来ます。

DMでのみ動くコマンドを作る

この際、discord.ext.commandsに含まれるデコレータを用いて、コマンドを受け取る時点でDM以外を弾くことが出来ます。

@bot.command()
+ @commands.dm_only()
async def ...

このように、@commands.dm_only()のデコレータを付け加えるだけでDM以外でコマンドを実行しようとした場合には関数が実行されないようにすることができます。

処理を書き換えて行く

以前の処理ではon_message()でメッセージを受け取りその中身から判別していましたが、今回はそれぞれの処理にそれぞれのコマンドを割り当てることになります。
余談ですが、前回実装していた収支を確認する処理について、実際運用してみるとシートを直接見に行くことが圧倒的に多かったためカットしました。

まず、シートを返す処理からです。
以前の処理では、受け取ったメッセージが「シート」だった場合にシートのURLを返しています。

        if message.content == 'シート' :
            await message.channel.send(SHEET_URL)
            return

書き換えた処理では、$sheetコマンドでシートのURLを返すようにしています。

@bot.command()                          #シートのURL確認
@commands.dm_only()                     #DMのみを受け取る
async def sheet(ctx):
    await ctx.send(SHEET_URL)           #シートのURLを返す
    return

SHEET_URLには環境変数を用いて引っ張ってきたシートのURLが収まっています。
受け取ったcontextからsend()でメッセージを返すシンプルな処理です。
デコレータによるDM以外シャットアウトも機能しているので、シートのURLを確認する処理はこれで完璧です。
引き続きURLだけが返される味気ない状態ですが……。

次に、収入と支出の記録です。
収入と支出の処理の差については受け取った引数を渡す先の関数が異なるだけなので、今回は収入の方だけ記述します。
以前の処理では、コンマ区切りで実質的に引数として受け取った配列を元に関数へ引数を渡して書き込みをしています。
予め定義した関数add_incomeにシート、名称、金額をそれぞれ引数で渡すことで、スプレッドシートに書き込みを行います。ここで渡している変数worksheetには、収支等で処理が分岐する前にシートを予め取得してあります。
関数check_totalでは、シートに新たに情報が書き込まれただけでは更新されていない収支合計などの部分の更新を行っています。

if receipt[0] == '収入' :
            add_income(worksheet,str(receipt[1]),int(receipt[2]))   #収入を書き込む
            check_total(worksheet)  #収支の合計をチェックし入力させる
            await message.channel.send(''+receipt[1]+'による収入'+receipt[2]+'円を記録しました。\r\n記録後の今月の収入は'+str(int(check_income(worksheet)))+'円です。')
            return

書き換えた処理では、$incomeとして関数を定義し、この時点で記録する名称と金額を引数として個別に受け取っていきます。
引数は名称、金額の順に受け取るため、それぞれname,amountのふたつの変数で受け取っています。

@bot.command()
@commands.dm_only()                     #DMのみを受け取る
async def income(ctx,name,amount):

今回、処理ごとに関数が異なるためスプレッドシートを取得する処理も関数ごとに行っていきます。次に、金額をスプレッドシートに数値として書き込む為に、金額を受け取った引数を数値型にキャストし、そのまま予め定義した変数add_incomeにシート、名称、金額を渡してシートへの書き込みを行います。
その後、関数check_totalにて収支合計の更新を行い、メッセージをDMチャンネルに返して処理を終了します。

    worksheet = monthcheck()            #シートの取得
    amount = int(amount)
    add_income(worksheet,name,amount)   #収入の記録
    check_total(worksheet)              #シートの整理
    await ctx.send(''+name+'による収入'+str(amount)+'円を記録しました。\r\n記録後の今月の収入は'+str(int(check_income(worksheet)))+'円です。')
    return

支出の場合、関数がadd_incomeからadd_spendingに置き換わるだけなので、ほぼコピペで終了です。

感想など

今回の処理の書き換えで変化した部分を大雑把にまとめるとこうなります

  • DMの判別が、受け取ったメッセージをif文で判定してそれ以下に処理を収める形から、デコレータを1行添えるだけで済むようになった。
  • 受け取る引数を配列ではなく別々に受け取れるようになったので、処理の見た目がかなりスッキリした。
  • 一つの関数(on_message)にオールインなのではなく処理ごとに個別の関数を用意できた為、それぞれの処理がわかりやすくなった。
    以前の処理法は、三つ目の一つの関数に全て収まっていたことなどが要因で見た目のゴチャゴチャ感がかなり強く、幸い書き込み等の処理を関数で別けていたからギリギリなんとかなっていたものの、扱っている変数などのややこしさなどが濃いと感じていて、処理の仕方を見直すにもいい機会になったと思いました。
    今までdiscord.ext.commandsのことをそもそも知らないというところに居たので、コメントをして頂いたLa様には感謝の気持でいっぱいです。
    手元にある個人的に運用しているbotも色々とスッキリした形に書き換えられそうなので、色々と取り掛かって行きたいと思っています。

参考にしたリンクなど

Discord.py公式ドキュメント discord.ext.commands -- ボットコマンドのフレームワーク
discord.py - commandsフレームワークへの移行

Discussion