🗄️

discord.py - MongoDBをdiscord.pyで使う

10 min read

はじめに

この記事はdiscord.pyでMongoDBを使ったプロフィールBotを作ることを目標にしています。
間違いを見つけたらどんどん指摘してください。

環境

- Python v3.9.2-final
- discord.py v1.7.1-final
- aiohttp v3.6.3
- system info: Windows 10 10.0.17134

python -m discord -vの情報)

1. MongoDBのアカウントを作る

もうCollectionを作成した人は3. ライブラリのインストールまで飛ばして下さい。

https://mongodb.com
にアクセスし、アカウントを作ります。
今回はGoogleアカウントで作ります。
Sign Inを押し、
Login
ここのSign In with GoogleからGoogleアカウントでログインします。
規約に同意し、以下のような画面になったら成功です。
Organizations

エラーが発生した場合

ここにアクセスします。
それでもダメな場合はもう一度試してみて下さい。

2. Clusterを作る

Organizationを作る

Create an Organizationを押し、新しいOrganizationを作ります。
MongoDB Atlas
Organization Nameに名前を入力し、Nextを押します。
Select Cloud ServiceはMongoDB Atlasで大丈夫です。
Create Organization
メンバーを招待する画面に移ります。
もし他の開発者がいるなら招待しましょう。
今回は無視し、Create Organizationを押します。

Projectを作る

New Project
この画面になったらNew Projectで新しいProjectを作って下さい。
Project Nameで名前を指定
Project Nameで名前を指定します。
Create Project
先ほどのように、他の開発者がいるなら招待しましょう。
今回は無視し、Create Projectを押します。

Clusterを作る

Create Cluster
Build a ClusterでClusterを作ります。
Shared Clusters
プランが出てくるので、Shared Clustersを選択します。

設定
ここで色々な設定が出来ます。
Google Cloud > Tokyoで日本のサーバーを使えます。
設定出来たら、Create ClusterでClusterを作成して下さい。

待つ
押すとClusterが作成されるので1-3分ほど待ちます。

接続を設定する

Connectを押す
Clusterが出来たらConnectで接続設定を出します。

設定
上でアクセスできるIPを制限できます。Herokuなどでホストしている場合はデプロイ毎にIPが変わるため、Allow Access from Anywhere(すべてのIPを許可)を使います。
今回はIPを制限しません。
下でユーザーを作成します。
今回はBotというユーザー名で作成します。パスワードはAutogenerate Secure Password(パスワードを自動生成)で作成した方が安全です。
生成したらパスワード欄のSHOWでパスワードを表示し、コピーします。

Connect Your Applicationを押します。
Connect Your Applicationを押します。

Connection Stringをコピー
ウィンドウの下に書いてあるURL(Connection String)をコピーします。

mongodb+srv://<名前>:<パスワード>@hitoniyotte.kawaru.mongodb.net/<データベース名>?retryWrites=true&w=majority

(これは例です。)
<名前><パスワード>は設定した物に置き換えて下さい。データベース名はお好みで。

以上で準備は終わりです。お疲れ様でした。

3. ライブラリのインストール

MongoDBを使うには一般的にはpymongoを使いますが、これはブロッキング(discord.pyではBot全体のフリーズ、詳細はdiscord.pyのFAQにて)を引き起こしてしまいます。
そのため、今回はmotorを使います。
また、dnspythonも必要なのでインストールします。

py -3 -m pip install motor dnspython

または

python3 -m pip install motor dnspython

をしてください。

4. データベースを読み書きする

以下のコードから始めます:

import discord
from discord.ext import commands

intents = discord.Intents.default()
intents.members = True  # Member Intentsを有効化する

allowed_mentions = discord.AllowedMentions(replied_user=False)  # 返信のメンションをデフォルトで無効化

bot = commands.Bot(command_prefix="p ", intents=intents, allowed_mentions=allowed_mentions)


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")

bot.run("Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked")

クライアントを作成する

motor.motor_asyncio.AsyncIOMotorClientを作成します。

  from discord.ext import commands
+ from motor import motor_asyncio as motor  # motor_asyncioをインポート

================================================================

  bot = commands.Bot(command_prefix="p ", intents=intents, allowed_mentions=allowed_mentions)
+ dbclient = motor.AsyncIOMotorClient("mongodb+srv://<名前>:<パスワード>@hitoniyotte.kawaru.mongodb.net/<データベース名>?retryWrites=true&w=majority")  # AsyncIOMotorClientを作成

Connection Stringは置き換えて下さい。
また、流出を防ぐためにdotenvなどを使うことをお勧めします。

データベースオブジェクトを作成する

クライアント["データベース名"]、またはクライアント.データベース名でデータベースオブジェクトを作成できます。

  dbclient = motor.AsyncIOMotorClient("mongodb+srv://<名前>:<パスワード>@hitoniyotte.kawaru.mongodb.net/<データベース名>?retryWrites=true&w=majority")
+ db = dbclient["ProfileBot"]
+ # または:db = dbclient.ProfileBot

コレクションオブジェクトを作成する

コレクションはテーブルのような物で、データを保存する場所です。
データベース["コレクション名"]、またはデータベース.コレクション名でコレクションオブジェクトを作成できます。

  db = dbclient["ProfileBot"]
+ profiles_collection = db.profiles
+ # または:profiles_collection = db["profiles"]

コレクションを読み書きする

書き込み

insert_oneでデータを1つ、insert_manyで複数のデータを書き込むことが出来ます。

p set プロフィールで設定出来るようにします。

@bot.command(name="set")
async def set_profile(ctx, *, text):  # setだと変数名がかぶるので変更、*,で空白を無視
    await profiles_collection.insert_one({
        "userid": ctx.author.id,
	"text": text
    })
    await ctx.reply("設定が完了しました。")

読み込み

find_oneで条件に一致するデータを1つ、findで複数のデータを検索できます。

p show ユーザーで表示出来るようにします。

@bot.command(name="show")
async def show_profile(ctx, target: discord.User):  # ユーザーを指定
    profile = await profiles_collection.find_one({
        "userid": target.id
    }, {
        "_id": False  # 内部IDを取得しないように
    })
    if profile is None:
        return await ctx.reply("見付かりませんでした。")
    embed = discord.Embed(title=f"`{target}`のプロフィール", description=profile["text"])  # 埋め込みを作成
    return await ctx.reply(embed=embed)  # 埋め込みを送信

正常
良い感じです。

削除

delete_oneで条件に合うデータを1つ、delete_manyで全て削除できます。
また、返り値のpymongo.results.DeleteResultのdeleted_countで削除した個数を取得できます。
p deletep delで削除出来るようにします。

@bot.command(name="delete", aliases=["del"])
async def delete_profile(ctx):  # ユーザーを指定
    result = await profiles_collection.delete_one({
        "userid": ctx.author.id  # useridで条件を指定
    })
    if result.deleted_count == 0:  # 削除できなかったら
        return await ctx.reply("見付かりませんでした。")
    return await ctx.reply("削除しました。")

削除
しっかり削除できています。

置き換え

replace_oneで条件に合うデータを1つ指定の物に置き換えます。

登録時に1度置き換えを試し、出来なかったら追加するようにします。

  async def set_profile(ctx, *, text):
+     new_data = {
+         "userid": ctx.author.id,
+         "text": text
+     }
+     result = await profiles_collection.replace_one({
+         "userid": ctx.author.id  # useridで条件を指定
+     }, new_data)
+     if result.matched_count == 0:  # 見付からなかったら
+         await profiles_collection.insert_one(new_data)  # データを追加
-     await profiles_collection.insert_one({
-         "userid": ctx.author.id,
-         "text": text
-     })

正常
しっかり置き換えられています。

他にも色々なことが出来ますが、それらについてはリファレンスを読んで下さい。

最終コード

import discord
from discord.ext import commands
from motor import motor_asyncio as motor

intents = discord.Intents.default()
intents.members = True

allowed_mentions = discord.AllowedMentions(replied_user=False)

bot = commands.Bot(command_prefix="p ", intents=intents, allowed_mentions=allowed_mentions)
dbclient = motor.AsyncIOMotorClient("mongodb+srv://<名前>:<パスワード>@hitoniyotte.kawaru.mongodb.net/<データベース名>?retryWrites=true&w=majority")
db = dbclient["ProfileBot"]
profiles_collection = db.profiles


@bot.event
async def on_ready():
    print(f"Logged in as {bot.user}")


@bot.command(name="set")
async def set_profile(ctx, *, text):
    new_data = {
        "userid": ctx.author.id,
        "text": text
    }
    result = await profiles_collection.replace_one({
        "userid": ctx.author.id
    }, new_data)
    if result.matched_count == 0:
        await profiles_collection.insert_one(new_data)
    await ctx.reply("設定が完了しました。")


@bot.command(name="show")
async def show_profile(ctx, target: discord.User):
    profile = await profiles_collection.find_one({
        "userid": target.id
    }, {
        "_id": False
    })
    if profile is None:
        return await ctx.reply("見付かりませんでした。")
    embed = discord.Embed(title=f"`{target}`のプロフィール", description=profile["text"])
    return await ctx.reply(embed=embed)


@bot.command(name="delete", aliases=["del"])
async def delete_profile(ctx):
    result = await profiles_collection.delete_one({
        "userid": ctx.author.id
    })
    if result.deleted_count == 0:
        return await ctx.reply("見付かりませんでした。")
    return await ctx.reply("削除しました。")
bot.run("Th1sIsN0tT0k3n.B3cause.1fiShowB0tWillG3tH4cked")

終わりに

この記事が役に立てば幸いです。
読んでいただきありがとうございました。

Discussion

ログインするとコメントできます