はじめに
この記事は discord.py を用いた Discord bot 開発についての記事です。雑にCogを使うことを目的としています。あまり詳しい説明はしません。また、1つのBotインスタンスで複数サーバに対応させることをしています。参考になれば幸いです
更新箇所(2020/10/6)
- 記事の内容を大幅に更新
更新箇所(2019/12/18)
- Wikiコマンドのバグ修正
- 回答時にWikipediaのページを表示する
ソートなぞなぞとは
初めに、ある単語をソートしたものが問題として与えられます。元になった単語を推測するゲームです。非自明な面白さがあります。
問題 | 答え |
---|---|
えつぴん | えんぴつ |
ペボルンー | ボールペン |
aelpp | apple |
こちらからBotを招待することで遊ぶことができます
https://discord.com/api/oauth2/authorize?client_id=761582339557556245&permissions=2048&scope=bot
細かい話はどうでもいいから遊んでみたいという方はこちらからクローンして遊んでみてください。(ライブラリのインポートや、環境に合わせてファイルを多少いじる必要があります。)
動作環境
- Python 3.8.2
- discord.py 1.5.0
Botアカウントの作成
こちらでBotアカウントを作成してください。その際にトークン(CLIENT SECRET)もコピーしておいてください。直後に使います。
Botの作成と起動
GitHubのREADMEの通りです。config.ini
にBotのトークンを置いておきます
$ git clone https://github.com/t4t5u0/sort-riddle-v2.git
$ cd sort-riddle-v2
$ echo -e '[TOKEN]\ntoken=Botのトークン'>config.ini
$ pip install -r requirements.txt
$ nohup python main.py &
以下実装の話
まずライブラリをインストールします
pip install discord
次にコードの解説です
import configparser
import discord
from discord.ext.commands import Bot
config = configparser.ConfigParser()
config.read('./config.ini')
TOKEN = config['TOKEN']['token']
bot = Bot(command_prefix='!')
bot.load_extension('cog.sort_riddle')
@bot.event
async def on_ready():
# Bot起動時にターミナルに通知を出す
print('-'*20)
print('ログインしました')
print('-'*20)
await bot.change_presence(activity=discord.Game(name='!help'))
if __name__ == "__main__":
bot.run(TOKEN)
bot.load_extension
で読み込んだCogを追加していきます
最小構成は以下になります
from discord.ext import commands
class SortRiddleCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.command()
async def neko(self, ctx):
await ctx.send(f'{ctx.author.mention} にゃーん')
def setup(bot):
return bot.add_cog(SortRiddleCog(bot))
- Cogのクラスを建てる
- クラスを初期化する。Botを引数に取ること
- コマンドを追加する。
commands.command()
デコレータを使用します - Cogを登録する。
setup
関数がないと動きません
かわいいですね
コマンド追加
このままではゲームとしては成り立たないのでコマンドを追加していきます。ソースはこちらにあります。
コマンド | 内容 |
---|---|
!start | 出題 |
!answer | 解答 |
!hint | 答えの1文字目を表示 |
!giveup | ギブアップし答えを表示 |
の4つを追加します
複数サーバへの対応
サーバ情報を保存するためのファイルを作ります。JSON配列に対しての探索は極力したくないので、探索用にサーバのIDのリストを持ちました。このリストに対して2分探索を行い、計算量の削減を行っています。現在は、小規模ということでJSONをメモリに持っていますが、将来的に規模が大きくなる場合はDBへの移行を考えています。csvモジュールはpandasに置き換えると高速化できそうです
import bisect
import csv
import json
import re
from datetime import datetime
import discord
import requests
from discord.ext import commands
class SortRiddleCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.sort_riddle_data = []
self.guild_id_list = []
with open('./data/guild_id_list.csv') as f:
self.guild_id_list = [int(y) for x in csv.reader(f) for y in x]
with open('./data/sort_riddle_data.json') as f:
self.sort_riddle_data = json.load(f)
@commands.Cog.listener()
async def on_guild_join(self, guild):
to_insert_index = bisect.bisect_left(self.guild_id_list, guild.id)
info = {
"guild_id": guild.id,
"guild_name": guild.name,
"channel_id": None,
"answer": None,
"question": None,
"start_time": None
}
self.sort_riddle_data.insert(to_insert_index, info)
with open('./data/sort_riddle_data.json', 'w') as f:
json.dump(self.sort_riddle_data, f, indent=4)
bisect.insort(self.guild_id_list, guild.id)
with open('./data/guild_id_list.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow(self.guild_id_list)
@commands.Cog.listener()
async def on_guild_remove(self, guild):
to_delete_index = bisect.bisect_left(self.guild_id_list, guild.id)
self.sort_riddle_data.pop(to_delete_index)
with open('./data/sort_riddle_data.json', 'w') as f:
json.dump(self.sort_riddle_data, f, indent=4)
self.guild_id_list.pop(to_delete_index)
with open('./data/guild_id_list.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow(self.guild_id_list)
commands.Cog.listner()
デコレータでイベントを拾っていきます。Botがサーバに入った時に情報を追加する処理と、退出した時に削除する処理を書いています。
startコマンドの実装
@commands.command(aliases=['s'])
async def start(self, ctx):
if 'guild' not in dir(ctx.author):
await ctx.send('**!start** はDM限定だにゃ')
return
# guild_id が登録されていなかったときの処理
index = bisect.bisect_left(self.guild_id_list, ctx.author.guild.id)
if ctx.author.guild.id not in self.guild_id_list:
info = {
"guild_id": ctx.author.guild.id,
"guild_name": ctx.author.guild.name,
"channel_id": None,
"answer": None,
"question": None,
"start_time": None
}
self.sort_riddle_data.insert(index, info)
with open('./data/sort_riddle_data.json', 'w') as f:
json.dump(self.sort_riddle_data, f, indent=4)
bisect.insort(self.guild_id_list, ctx.author.guild.id)
with open('./data/guild_id_list.csv', 'w') as f:
writer = csv.writer(f)
writer.writerow(self.guild_id_list)
if (q := self.sort_riddle_data[index]['question']) is not None:
await ctx.send(f'問題は **{q}** だにゃ')
return
link = 'https://ja.wikipedia.org/w/api.php?action=query&list=random&format=json&rnnamespace=0&rnlimit=1'
response = requests.get(link)
json_data = response.json()
"""
json_data
{'batchcomplete': '',
'continue': {'continue': '-||',
'rncontinue': '0.269787212577|0.269787813864|1126887|0'},
'query': {'random': [{'id': 2226876,
'ns': 10,
'title': 'Template:Country alias 岐阜県'}]}}
"""
a = json_data['query']['random'][0]['title'].replace(' ', '_')
self.sort_riddle_data[index]['answer'] = a
q = ''.join(sorted(list(a)))
self.sort_riddle_data[index]['question'] = q
await ctx.send(f'問題は **{q}** だにゃ')
self.sort_riddle_data[index]['start_time'] = re.split('[-|:|.|\s]',str(datetime.now()))
# 諸々を書き込み
with open('./data/sort_riddle_data.json', 'w') as f:
json.dump(self.sort_riddle_data, f, indent=4)
時刻を取るところは、実装し終わったあとにライブラリに関数があることに気づきました。(アホ) 早期リターンを心がけました
おわりに
初投稿です。また、プログラミング歴半年足らずの初心者です。ガバがありましたら容赦なくマサカリを投げつけてください。
初投稿から1年経ったのをキッカケにリファクタリングをしてみました。過去の自分が書いたプログラムって見たくないものですね…
ソースはこちらに置いてあります。ぜひ。
参考にした記事・サイト
discord.py へようこそ。 — discord.py 1.3.0a ドキュメント
Pythonで実用Discord Bot(discordpy解説) - Qiita
discord.pyのBot Commands Frameworkを用いたBot開発 - Qiita