Discord Bot を複数個起動させる方法（Python）

同じものを何度も書くのが面倒だったので、1つだけファイルを編集したら全ての Bot の起動ができるようにした。今後増やしていく際にも楽になりそう。

## この記事で分かること

- Bot を同時に起動できるようにする方法
- `client.start(token)` の活用例
- おまけ
  - openAI を使用してメンションを飛ばしたら返答が返るようにする方法


### 完成イメージ

メンションを飛ばしたら MBTI を擬人化したキャラが返信してくれる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630025/0cd54897-b284-6b0c-af8c-8da22be215fb.png)



## ポイント

https://discordpy.readthedocs.io/ja/latest/api.html?highlight=run#discord.Client.run

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1630025/f6f111db-8f6e-1f56-24f3-0b7bd59147af.png)


`client.run` を実行するとその後に呼び出したものは実行されなくなってしまう。いくらループさせようが、1回呼ばれてしまうとその後に処理は呼び出されなさそう…。

<br>

なので、代わりに `client.start` を実行して、複数の Bot を非同期に起動させた。

``` python:bot_manager.py の抜粋
...
    async def start(self):
        await self.client.start(self.token)

async def start_bot(bot: Bot):
    bot_manager = BotManager(bot)
    bot_manager.setup_events()
    await bot_manager.start()

```

``` python:main.py
if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    for bot in allBots:
        loop.create_task(start_bot(bot=bot))
    
    loop.run_forever() # イベントループを無限に実行し、Bot を常に稼働し続ける

    loop.close()
```

## 全体のコード

token とプロンプトのテキストファイルはそれぞれ用意している前提

``` python:bot.py
from dataclasses import dataclass

@dataclass
class Bot:
    mebti_type: str # token で使用
    mbti_file_name: str # プロンプトの取得で使用

# 以下に Bot を増やせばそれだけで追加可能
allBots = [
    Bot(
        mebti_type="ENTP",
        mbti_file_name="entp-kosho",
    ),
    Bot(
        mebti_type="INTP",
        mbti_file_name="intp-nagi",
    )
]
```

``` python:bot_manager.py
import os
import discord
from dotenv import load_dotenv
from openai import OpenAI
from bot import Bot

# .envファイルから環境変数を読み込む
load_dotenv()

# OpenAIのAPIキーを環境変数から取得
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
openAiClient = OpenAI(api_key=OPENAI_API_KEY)

class BotManager:
    # token とプロンプトの読み込みが必要なので、bot を活用する
    def __init__(self, bot: Bot):
        self.bot = bot

        # 各 Discord の token 名を以下のようにしている
        # 例: DISCORD_ENTP_TOKEN
        self.token = os.getenv(f'DISCORD_{bot.mebti_type}_TOKEN')

        # Intentsを設定
        intents = discord.Intents.default()
        intents.messages = True

        # Discordクライアントの作成（intentsを指定）
        self.client = discord.Client(intents=intents)

    def setup_events(self):
        @self.client.event
        async def on_ready():
            print(f'Logged in as {self.client.user}')

        @self.client.event
        async def on_message(message):
            # 自分のBotのメッセージには反応しない
            if message.author == self.client.user:
                return

            # メンションされた場合に反応
            if self.client.user.mentioned_in(message):
                user_message = message.content.replace(f'<@!{self.client.user.id}>', '').strip()

                # キャラのプロンプトを読み込む
                with open(f'mbti-prompt/{self.bot.mbti_file_name}.txt', 'r', encoding='utf-8') as file:
                    mbti_prompt = file.read()

                if user_message:
                    # OpenAIにメッセージを送信して返答を取得
                    # ref: https://platform.openai.com/docs/guides/text%EF%BC%8Dgeneration
                    completion = openAiClient.chat.completions.create(
                        model="gpt-4o-mini",
                        messages=[
                            {"role": "developer", "content": mbti_prompt},
                            {
                                "role": "user",
                                "content": user_message,
                            }
                        ]
                    )

                    # 生成された返答を送信
                    await message.channel.send(completion.choices[0].message.content)
                else:
                    await message.channel.send("Hello! How can I assist you today?")

    # POINT: client.run ではなく、client.start を活用する
    async def start(self):
        await self.client.start(self.token)

async def start_bot(bot: Bot):
    bot_manager = BotManager(bot)
    bot_manager.setup_events()
    await bot_manager.start()
```

``` python:main.py
import asyncio
from bot_manager import start_bot
from bot import allBots

if __name__ == "__main__":
    loop = asyncio.get_event_loop()

    for bot in allBots:
        loop.create_task(start_bot(bot=bot))
    
    loop.run_forever()

    loop.close()
```

## 参考

https://discordpy.readthedocs.io/ja/latest/api.html?highlight=run#discord.Client.run


https://docs.python.org/ja/3.13/library/asyncio-eventloop.html#asyncio.loop.run_forever

https://2basa.acceleland.net/blog/discordbot1process/