はじめに
この記事はTDU21 Advent Calendar 2021に参加しています!
この記事は音ゲーサークルと課題曲botの話の後半に該当しますが、botの作り方自体は単体でも読めるようになっています。
後半に出てくる音ゲー用語については前編(※gistに飛びます)を読んでください。
この記事では課題曲botについてと、discord.pyを用いた簡単なdiscordbotの作り方を紹介したいと思います。簡単な応答をするコマンドを作ったり、言葉に反応したり、DMを送ったりできます。
botの作り方自体には備忘録的な意味もあったり、私でもちゃんとわかってない部分が割とあったりするので、非効率的だったり、変なコードになっていることもあるかもしれないです。一応記事内のコードの動作確認はしているつもりです。記事内のコードはここにまとめてあります。
botの作り方と銘打ってるもののメインはコードで、手順をまとめるだけになっています。類似記事もいっぱいあるので、何とかなると思います。(丸投げ)
discord.pyは既に開発終了しており、ちゃんと動く保証がありません!!
この記事をもとに作成したbotによって発生た損害について、責任は負いかねます。
discordbotについて
今回はdiscord.pyというAPIラッパーを用いて作っていきます。ざっくり言うとPythonというプログラミング言語を使ってメッセージを検知したり、自動的に送信したり、discordの機能をあらかた使用できます。
discordbotを作る
ここから本題に入っていきます。
1. トークンの取得 & サーバー加入
このページを参考にトークンを取得して、自分で適当なテスト用discordサーバーを作ってそこに入れてください。
2 環境構築
Pythonとdiscord.pyのインストールを済ませておいてください
Pythonのインストールについてはここに書くまでもないので、各自調べてください。
Pythonのインストールができていれば
python3 -m pip install -U discord.py
でdiscord.pyのインストールが出来るはずです。
3. コードを書く
#discord.pyの大事な部分をimport
from discord.ext import commands
#ここにトークンを入れる
# (Github等で管理する際にはトークンを入れたファイルを別で用意して
# gitignoreを使うなどしてPublicなリポジトリにはプッシュしないようにする)
TOKEN="X0X0N1_T0K3NW0_1R3T3N3_G1THU8N1_4G3N41D3N3"
#botのオブジェクトを作成(コマンドのトリガーを!に)
bot=commands.Bot(command_prefix='!')
#コマンドを設定
@bot.command()
#"!hello"と送信された時
async def hello(ctx):
await ctx.send("hello!")#送信された場所に"hello!"と送り返す
#イベントを検知
@bot.event
#botの起動が完了したとき
async def on_ready():
print("Hello!")#コマンドラインにHello!と出力
#起動
bot.run(TOKEN)
これだけです。
これだけで最低限のbot起動が出来るはずです。
コード内にもある通り、注意が一つだけあります。PublicなGitHubリポジトリ等、トークンを公開しないように最大限注意してください。トークンを公開してしまうとbotを悪用されかねないので、公開しないよう気を付けてください。
これらの手順で詰まるところとしてはトークンの取得と環境構築だと思います。
が、discord.py とかで調べるだけで先駆者の記事がうじゃうじゃ出てくると思うので、どうにかなると思います(丸投げ)
とりあえず、たったこれだけでdiscordbotの原型ができます。
細かい処理などについては少しPythonを理解する必要がありますが、簡単なメッセージを返す程度ならかなり簡単にできると思います。
3.5 いろいろな処理について
ここからはこういうことができるよ!という紹介です。この節のコードは基本先ほどのコードのbotの定義とrunの間に置けば動くはずです。
コマンドの構造
@bot.command() #↓任意のコマンド名(foo)
async def foo(ctx):#←引数ctxは必須(情報を渡すため)
#任意の処理
await ctx.send("foo!")#任意のメッセージを返す
@bot.command() #↓任意のコマンド名(bar)
async def bar(ctx,arg):#←ctxの後に追加することで引数を取れる
#任意の処理
await ctx.send(f"***{arg}***")#discordの書式に則っていればテキストの修飾もできる
これで新しいコマンドをじゃんじゃん追加できます。これらのコマンドはbot作成時に指定したcommand_prefix(今回は!)と設定したコマンドで動作します。("!foo"、"!bar xxx"のようにして使用)
async、awaitの話はめんどくさいので省きますが、とにかく送信するときはawaitを使えば、たいてい動きます。
もちろん引数(処理に必要な情報)もとることができ、スペース区切りで入力されたデータが引数のctxに続く部分に入れられていきます。(もちろん入力とかみ合わなければエラーを吐きます)
コマンドから情報を取る
#ctx.authorでコマンド送信者の情報が取れる
@bot.command()
async def info(ctx):
info=f"this guild:{ctx.guild}\n"
info+=f"this channel:{ctx.channel}\n"
info+=f"your user id:{ctx.author.id}\n"
info+=f"your user name:{ctx.author}"
await ctx.send(info)
#送信者にDMを返す
@bot.command()
async def dm(ctx):
await ctx.author.send("Direct Message!!")
送信されたチャンネルや、ユーザーの情報は引数ctxとして渡されます。
ctx.authorでユーザーの情報が取れるので、そのままDMを送ったりできます。
ユーザーの情報を格納しておき、必要になった際に送れます。
参考:
discord.py APIリファレンス "Context"
discord.py APIリファレンス "User"
#何らかのメッセージが送られた時に発動
@bot.event
async def on_message(message):#引数は上記のctxみたいなもの、これはMessageクラス
if message.author.bot:
pass
elif "おはよう" in message.content:#メッセージの内容に"おはよう"という文字列が含まれているとき、
await message.channel.send("おはよう!!!")#メッセージが送られたチャンネルに送信
ここまではすべてコマンド(!hello など意図的に動作させるもの)でしたが、特定の文章に反応するようなものも可能です。
このような何かの動作(イベント)に反応して、botを動作させることもできます。何ができるかは以下のリファレンスを見ればおおよそわかると思います。ここでは、botの発言にbotが反応して無限ループを防ぐためにbotの発言をスルーするようにしています。
参考:
discord.py APIリファレンス "イベントリファレンス"
discord.py APIリファレンス "Message"
指定時間に動作
from discord.ext import commands,tasks
まずtasksを追加でimportします。(import部分を書き換えてください)
#ユーザーの情報を格納するset
users=set()
#アラーム設定用コマンド
@bot.command()
async def alarm(ctx):
users.add(ctx.author)
print(f"{ctx.author}のアラームを登録しました。")
await ctx.send("アラーム登録しました!")
#60秒ごとに実行
@tasks.loop(seconds=60)
async def morning():
now=time.localtime()#現在時刻を取得
if now.tm_hour==4 and now.tm_min==0:#4時なら
for user in users:#アラームを登録している全ユーザーに対して
await user.send("おはよう!朝4時に何してるんだい?")#おはよう!朝4時に何してるんだい?
#ループの開始
morning.start()
一定時間ごとに行うループには引数がないので、送信先をこちらで用意しておく必要があります。ここではアラーム設定用コマンドを用意し、ユーザーの情報をsetに格納しています。そして、60秒ごとに現在時刻を取り、指定時刻なら各ユーザーのDMにメッセージを送信しています。もちろんチャンネルの情報を持っていればそこに送ったりもできます。
指定時刻に実行するためのtimeってやつがあるっぽいんですが、ねーよボケ(loop() got an unexpected keyword argument 'time')と怒られたのでこの方式を取っています。ほかにもループ用の引数がいくつかあります。(参考参照)
そして最後の行にある通り、スタートさせないと動きません。
参考:
discord.py "discord.ext.tasks"
4.実際に動かす
自分のPCでも動かしっぱなしにすれば問題なく使用できますが、ずっと動かしっぱなしにできるPCがある人も多くはないと思います。
そこで、Herokuという開発したアプリケーションを動かしてくれるサービスを使います。
Herokuに動かす方法はいくつかあるので、自分で調べてください(GitHubのリポジトリと連携するのが簡単だと思いますが、その場合はトークンを悪用されかねないのでリポジトリを絶対にPublicにしないでください!)
一応参考ページを節の下の方に載せておきます。
その際にいくつかファイルが必要になるので、その中身の例だけ紹介します。
まず、必要になるファイルは最初の構造で触れたとおりの以下の3つです。
- Procfile(何を動かすか)
- requirements.txt(必要なモジュールのインストール用)
- runtime.txt(実行環境)
discordbot: python discordbot.py
discord.py
python-3.8.5
参考:
個人でdiscordのbotを作成し常時稼働させる方法を紹介します Herokuでの設定
Cogを使う
Cogとは、歯車の意で、botのパーツとなります。Cogを使ってbotを分割することで、ファイルや機能の管理がしやすくなったり、一つだけ停止させてメンテナンスできたり(この記事では扱いませんが)、メリットが多いです。
discordbot
│ discordbot.py
│
└─cogs
cogA.py
cogB.py
cogC.py
このようなファイル構造だとします。もちろんコグもPythonファイルです。
#botの定義とrunの間に書く
cogfolder="cogs."
cogs=["cogA","cogB","cogC"]
for c in cogs:
bot.load_extension(cogfolder+c)
まず、メインファイル(discordbot.py)のrunの前にコグを読み込む部分を記述します。必要なコグをリストにまとめておき、oad_extension()でコグをロードしています。
そしてコグの中身はcommands.Cogを継承したクラスとして記述します。
from discord.ext import commands
#checkcogの本体
class checkcog(commands.Cog):
def __init__(self,bot):
self.bot=bot
#コマンドの記述
@command.command()
async def cog_command(self,ctx):
await ctx.send("using cog!")
#Cogとして使うのに必要なsetup関数
def setup(bot):
print("checkcog OK")
return bot.add_cog(checkcog(bot))
コマンドの記述の中以外はほぼ定型文みたいなものです。(正確にどのタイミングで実行されてるかは知らないんですが)起動時にクラス外の記述もちゃんと実行されるので、データの読み込みもできます。コマンドの追加はクラスの中にどんどんできます。
正直ファイル分ける以外上手く使えている感じはしないんですが、課題曲botでもcogを使っています。ということで課題曲botのお話をここからしていきたいと思います。
課題曲bot
ここから音ゲーの話がバリバリ入ってきます。本編ですが話は薄いです。
基本構造
assignment_song_bot
│ .gitignore #gitで無視するファイルを記述する
│ discordbot.py #メインファイル
│ Procfile #herokuで動作させるためのやつ(動かすファイルの記述)
│ README.md #Readme
│ requirements.txt #herokuで動作させるためのやつ(必要なモジュールの記述)
│ runtime.txt #herokuで動作させるためのやつ(実行環境の記述)
│
├─cogs #コグのフォルダ
│ chucog.py #チュウニズム用のコグ
│ constread.py #定数読み込み用の自作モジュール
│ gekicog.py #オンゲキ用のコグ
│ maicog.py #maimai用のコグ
│ mycog.py #個人用のコグ
│ noscog.py #ノスタルジア用のコグ
│ ochincog.py #[自主規制]のコグ
│ searchcog.py #検索用のコグ
│
├─csv #楽曲データのcsv
│ chu.csv
│ geki.csv
│ mai.csv
│
└─ratingmanage #レート管理シート
├─CHUNITHM #チュウニズム用
│ crys.html #レーティング対象楽曲のページのhtmlファイル
│ crystal.py #レート管理シートを生成するやつ
│ crystal.xlsx #生成されたレート管理シート
│ crystalsample.xlsx #レート管理シートのベース
│
└─ONGEKI #オンゲキ用
geki.html #レーティング対象楽曲のページのhtmlファイル
gekipageread.py #攻略サイトから定数データスクレイピングするやつ
gekisample.xlsx #レート管理シートのベースになるもの
ongeki.py #レート管理シートを生成するやつ
ongeki.xlsx #生成されたレート管理シート
メインのdiscordbot.pyを中心に、コグを使用し機能を分けています。
そして各音ゲーの譜面データは攻略サイトからスクレイピングしてcsvにしてあります。起動時に読み込みます。いずれこの辺も自動更新できるようにしたりいろいろしたいですね(ただ自動更新については何かあると譜面データが壊れて動作に支障をきたしそうなので迷っています)。
ここからの話でピックアップするのは以下の5ファイルです。
constread.py #定数読み込み用の自作モジュール
chucog.py #チュウニズム用のコグ
gekicog.py #オンゲキ用のコグ
maicog.py #maimai用のコグ
searchcog.py #検索用のコグ
個人用コグを除いて、メインで使用するこの4つ+モジュール1つになります。
そもそもチュウニズム、オンゲキ、maimaiの3つのゲームはセガ製のゲームで、難易度やレーティングのシステムがかなり似ています。なので、一つ作れば、軽い改変で3つ分作れることになります。これらのコグで共通している機能は、
- 課題曲を提示する(定数指定、定数範囲指定、レベル指定)
- 指定したレーティングの値から課題曲と目標スコアを提示
- 指定した定数の楽曲リストを表示
- レーティング計算
になります。定数指定部分を読む用のモジュールを作っておいたので、課題曲の提示自体は定数指定→範囲を返す→その範囲のリストからランダムに取るという手順で、指定したレーティングの値から課題曲とスコアを提示する部分は、レーティング値から達成可能な定数範囲を逆算して、課題曲の提示と同様にその範囲のリストからランダムに取り、その譜面の定数をもとに入力されたレーティング値を達成可能なスコアを計算して表示しています。レーティング計算関連も使いまわしまくってます。つまり、目標スコア表示はただの課題曲提示にひと手間加えただけで、必要な操作をまとめて作っているので、実はあんまり手がかかってなかったりします。ちなみに、楽曲データは定数をキーにしてdictに格納しています。そのうち楽曲データをちゃんとしたデータベースに入れてこねこね出来るようにしたいです。
楽曲検索も、各コグの定数データを使いまわして検索可能にしています。
使いまわし機能盛り万歳!!!!!
このbotは音ゲーサークルのdiscordサーバーで動かしてます、が各機種の楽曲データ更新が出来てないので若干古いです。そして、チュウニズムに大型アップデートが入ったので、定数とレーティングの色々を丸っと書き換える必要が発生しています。まだちょっと中身をちゃんと人に見せられる状態ではないので、近いうちに機能追加とリファクタリングを予定しています。(本来この記事の公開と同時にお披露目する予定だった……)
レーティング管理シート
先述したセガ系音ゲーのレーティングの計算は、スコアと定数があれば出来るようになっています。レーティングの計算には数十曲のデータが必要なので、Excelのシートで数式を使って管理することがあります。友人からそのシートをいただいたのですが、曲名とスコアと定数のデータの入力がめんどくさくてあんまり使っていませんでした。なので、曲名から定数を自動入力できるようにして、その音ゲーの公式サイトでレーティングの計算対象の楽曲名とスコアを取ってきて、レーティング管理シートを自動生成するようにしました。このシートを先ほどのbotとくっつけてdiscordで見れるようにしました(mycog) 。なのでコマンドを探せば私のデータをこっそり見れます。
終わりに
投稿予定の20分前に焦りながらこれを書いています。
気合入れてdiscordbotの作り方書いてたらそっちの方が分厚くなっちゃいました……
そして記事書いてて色々恥ずかしくなってきたので、ちゃんと作り直したいなってなってます……
これを見て、何か作ってみようとなった方がいればうれしいかなと思います。(何かこの記事の内容で困ったことがあれば私のTwitterまで!)
まだまだ書きたいことが多いので、追記か書き直しをするかもしれないです。
ここでは、チャットの機能ベースで紹介しましたが、discordで使える機能が大体使えるので、興味があればいろいろやってみるといいと思います!開発終了してしまいましたが、資料はかなり豊富な方だと思います。
拙い文章およびコードでしたが、ここまで読んでいただいた方も、読んでいただけていない方も、ありがとうございました!