Discord.pyでカスタムPrefix ～改～【初心者向け】

# 最初に

前回にも同じような記事([Custom prefix を導入しよう！！](https://qiita.com/Req_/items/f64def8e057739ef15a3))を書きましたが、こちらの記事で紹介しているコードだと Github + Heroku で動かしているBotには対応していないので、その動かし方でもちゃんと動くように改良しました。
※ それ以外のところも多少改良されています

# 開発環境
> OS - Windows10 home
エディタ - Atom 1.54.0
実行シェル - コマンドプロンプト
Python - 3.9.1
PIP - 20.3.3
discord.py - 1.6.0

# 本題

## コード

※ 基本的にどのコードでも同じ書き方になるものは `テンプレ` とコメントが打ってあります
※ `テンプレ` 部分の解説は省略させていただきます

<details><summary>コードは長いのでこの表示方法をとらせていただきます</summary><div>

```py
# テンプレ
import discord
from discord.ext import commands


BACKUP_CHANNEL_ID =
DEFAULT_PREFIX = ''
TOKEN = ''


def _change_command_prefix(bot: commands.Bot, msg: discord.Message):
    if str(msg.guild.id) in prefix_dict.keys():
        return prefix_dict[str(msg.guild.id)]
    else:
        return DEFAULT_PREFIX

# テンプレ
bot = commands.Bot(command_prefix=_change_command_prefix, intents=discord.Intents.all())


@bot.event
async def on_ready():
    global backup_ch
    global prefix_dict

    backup_ch = await bot.fetch_channel(BACKUP_CHANNEL_ID)
    prefix_dict = {}

    async for m in backup_ch.history():
        splited = m.content.split(' ', 1)
        prefix_dict[splited[0]] = splited[1]

    print('ready')


@bot.command(aliases=['cp'])
async def change_prefix(ctx, new_prefix: str = None):
    guild_id = str(ctx.guild.id)

    if new_prefix == None:
        if guild_id in prefix_dict.keys():
            old_prefix = prefix_dict[guild_id]
            async for m in backup_ch.history():
                if m.content.startswith(guild_id):
                    await m.delete()
            prefix_dict.pop(guild_id)
            await ctx.send(embed=discord.Embed(title='This server\'s prefix was reseted', description=f'{old_prefix} -> default({DEFAULT_PREFIX})'))
            return

        else:
            return

    if guild_id in prefix_dict.keys():
        async for m in backup_ch.history():
            if m.content.startswith(guild_id):
                await m.edit(content=f'{guild_id} {new_prefix}')
                break
        old_prefix = prefix_dict[guild_id]
        prefix_dict.pop(guild_id)
        prefix_dict[guild_id] = new_prefix
        await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'{old_prefix} -> {prefix_dict[guild_id]}'))
        return

    else:
        await backup_ch.send(f'{guild_id} {new_prefix}')
        prefix_dict[guild_id] = new_prefix
        await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'default({DEFAULT_PREFIX}) -> {prefix_dict[guild_id]}'))
        return


# テンプレ
bot.run(TOKEN)
```

</div></details>

## 解説

### 定数の定義

```py
BACKUP_CHANNEL_ID =
DEFAULT_PREFIX = ''
TOKEN = ''
```

上から
・カスタムPrefix情報をバックアップしておくためのメッセージを送信するチャンネルのチャンネルID
└ 作成者とBotのみが閲覧できるような設定にしておくことをお勧めします
・デフォルトとするPrefix
・BotのTOKEN

### サーバーごとのPrefixを返す関数

```py
def _change_command_prefix(bot: commands.Bot, msg: discord.Message):
    if str(msg.guild.id) in prefix_dict.keys():
        return prefix_dict[str(msg.guild.id)]
    else:
        return DEFAULT_PREFIX
```

これは、
prefix_dictという辞書型変数のKeyにコマンドを実行したサーバーのサーバーIDが入っているかどうかを確認し、
├ 入っていれば対応するPrefixを返し、
└ 入っていなければデフォルトのPrefixを返す

という関数になっています。

### on_ready内

```py
@bot.event
async def on_ready():
    global backup_ch
    global prefix_dict

    backup_ch = await bot.fetch_channel(BACKUP_CHANNEL_ID)
    prefix_dict = {}

    async for m in backup_ch.history():
        splited = m.content.split(' ', 1)
        prefix_dict[splited[0]] = splited[1]

    print('ready')
```

```py
global backup_ch
global prefix_dict
```

他の関数(change_prefix)でも使用する変数をグローバル変数として定義する

```py
backup_ch = await bot.fetch_channel(BACKUP_CHANNEL_ID)
```

カスタムPrefix情報をバックアップしておくためのメッセージを送信するチャンネルのチャンネルオブジェクトを取得する

```py
prefix_dict = {}
```

サーバーごとのPrefixを保存するための辞書型変数の定義

```py
async for m in backup_ch.history():
    splited = m.content.split(' ', 1)
    prefix_dict[splited[0]] = splited[1]
```

上のほうで取得した、カスタムPrefix情報をバックアップしておくためチャンネルの履歴からサーバーごとのPrefixを保存する辞書を構成する
※ あまりにも数が多すぎると取得に時間がかかる可能性がありますが、Bot起動時に発動する関数なのであまり影響はないと思います。

以下、各々でカスタム可能


### change_prefix内

```py
@bot.command(aliases=['cp'])
async def change_prefix(ctx, new_prefix: str = None):
    guild_id = str(ctx.guild.id)

    if new_prefix == None:
        if guild_id in prefix_dict.keys():
            old_prefix = prefix_dict[guild_id]
            async for m in backup_ch.history():
                if m.content.startswith(guild_id):
                    await m.delete()
            prefix_dict.pop(guild_id)
            await ctx.send(embed=discord.Embed(title='This server\'s prefix was reseted', description=f'{old_prefix} -> default({DEFAULT_PREFIX})'))
            return

        else:
            return

    if guild_id in prefix_dict.keys():
        async for m in backup_ch.history():
            if m.content.startswith(guild_id):
                await m.edit(content=f'{guild_id} {new_prefix}')
                break
        old_prefix = prefix_dict[guild_id]
        prefix_dict.pop(guild_id)
        prefix_dict[guild_id] = new_prefix
        await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'{old_prefix} -> {prefix_dict[guild_id]}'))
        return

    else:
        await backup_ch.send(f'{guild_id} {new_prefix}')
        prefix_dict[guild_id] = new_prefix
        await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'default({DEFAULT_PREFIX}) -> {prefix_dict[guild_id]}'))
        return
```

※ 一応エイリアスとして `cp` が設定されていますが、なくても大丈夫です

```py
guild_id = str(ctx.guild.id)
```

`guild_id` という変数にstr型に変換したサーバーIDを格納する

```py
if new_prefix == None:
    if guild_id in prefix_dict.keys():
        old_prefix = prefix_dict[guild_id]
        async for m in backup_ch.history():
            if m.content.startswith(guild_id):
                await m.delete()
        prefix_dict.pop(guild_id)
        await ctx.send(embed=discord.Embed(title='This server\'s prefix was reseted', description=f'{old_prefix} -> default({DEFAULT_PREFIX})'))
        return

    else:
        return
```

引数として渡される `new_prefix` が指定されていなければ(==None)
　├ コマンドを実行したサーバーのサーバーIDが `prefix_dict` のKeyに入っていれば(==カスタムPrefixが設定されている)
　│　└ ¹古いPrefixを `old_prefix` 変数に格納
　│　　　↓
　│　　バックアップチャンネルの履歴からコマンドを実行したサーバーのPrefix情報が格納されているメッセージを削除
　│　　　↓
　│　　`prefix_dict` からコマンドを実行したサーバーの情報を削除
　│　　　↓
　│　　²デフォルトのPrefixに戻したという旨のメッセージを送信
　└ それ以外
　　　└ 何もしない

※ 1 - 2で送信するメッセージに使用
※ 1, 2 - Prefix変更のメッセージのため、省略可能

```py
if guild_id in prefix_dict.keys():
    async for m in backup_ch.history():
        if m.content.startswith(guild_id):
            await m.edit(content=f'{guild_id} {new_prefix}')
            break
    old_prefix = prefix_dict[guild_id]
    prefix_dict.pop(guild_id)
    prefix_dict[guild_id] = new_prefix
    await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'{old_prefix} -> {prefix_dict[guild_id]}'))
    return
```

コマンドを実行したサーバーのサーバーIDが `prefix_dict` のKeyに入っていれば(==カスタムPrefixが設定されている)
　└ バックアップチャンネルの履歴からコマンドを実行したサーバーのPrefix情報が格納されているメッセージの内容を変数
　　　↓
　　¹古いPrefixを `old_prefix` 変数に格納
　　　↓
　　`prefix_dict` からサーバーIDに対応する要素を削除
　　　↓
　　`prefix_dict` にサーバーのPrefix情報を格納
　　　↓
　　²Prefixを変更したという旨のメッセージを送信

※ 1 - 2で送信するメッセージに使用
※ 1, 2 - Prefix変更のメッセージのため、省略可能

```py
else:
    await backup_ch.send(f'{guild_id} {new_prefix}')
    prefix_dict[guild_id] = new_prefix
    await ctx.send(embed=discord.Embed(title='This server\'s prefix was changed', description=f'default({DEFAULT_PREFIX}) -> {prefix_dict[guild_id]}'))
    return
```

コマンドを実行したサーバーのサーバーIDが prefix_dict のKeyに入ってなければ(==カスタムPrefixが設定されていない)
　└ バックアップチャンネルに新しくサーバーのPrefix情報を送信
　　　↓
　　`prefix_dict` にサーバーのPrefix情報を格納
　　　↓
　　¹Prefixをデフォルトからカスタムに変更したという旨のメッセージを送信

※ 1 - Prefix変更のメッセージのため、省略可能


# 後書き
今回は少し書き方を変えてみました。どうでしょうか？見やすかったらうれしいです。
今のところ不具合は確認されていませんが、もしうまく動かない等ありましたらコメントにてその旨をお伝えください。
今回はPythonの基礎は理解できているていで解説しています。わからないことがあれば、少し調べればでてくるようなものしか使っていないので各自調べて下さい。