Discordで通話中のみのチャットが作りたい！

はじめまして。Qiita初投稿です。

Discordを使用してるとき、通話中の人だけでチャットしたい事ってありませんか？
例えばゲームの招待コードとか、、
話してることに関連したリンクとか、、、
ボットのコマンドを打ちたいときとか、、、、

今回はそんなときに便利な通話中のみ使用できるチャットルームを[discord.py](https://github.com/Rapptz/discord.py)を用いて作っていきます。


# 完成品
![chatbot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/347998/4da886fc-35bf-9c9d-8077-b06d54352fac.gif)


# 準備
まずは、ボットプログラムを作るための準備を行いましょう。

投稿時の環境
- Python 3.8.3
- discord.py 1.3.3

このボットにはチャンネルを閲覧できる権限、ロール管理の権限とチャンネル管理の権限が必要です。

こちらでは細かな説明を省きます、以下の記事を参考にどうぞ。
[Pythonで簡単なDiscord Botの作り方](https://qiita.com/PinappleHunter/items/af4ccdbb04727437477f)


# 処理の流れ
処理の内容をまとめていきます。

1. メンバーが最初にボイスチャットに参加する
2. Botがボイスチャットに対応するテキストチャットルームを作成
3. 通話メンバーの参加・退室に応じてテキストチャットルームへ参加・退室させる
4. 通話終了時、テキストチャンネルを削除

今回は楽さ重視でテキストチャンネルを終了時に削除する方向で作っていきます。

プライベートさ重視でチャット履歴も非表示とか、テキストチャンネルはボイスチャットに完全に紐づけてしまって、最初に作成してからそのまま残して使うのも全然ありかなと思います。


# 実装
```python:bot.py
import discord
client = discord.Client()

# テキストチャンネルの先頭につける文字
CHANNEL_PREFIX = "private_"
# botたちのロール名 (botはテキストチャンネルに参加していてほしい)
BOT_ROLE_NAME = "bot"


# 接続できたときに実行される
@client.event
async def on_ready():
    # 初期化処理などが行えるよ
    print("On ready")


# ボイスチャンネル内の状態が変化したときに実行される
@client.event
async def on_voice_state_update(member, before, after):
    # チャンネルを移動していない場合処理をしない
    if before.channel == after.channel:
        return

    # チャンネルから退出してきた場合
    if before.channel is not None:
        # ボイスチャンネルに誰もいなくなった場合
        if len(before.channel.members) == 0:
            # テキストチャンネルを削除する
            await _channel_delete(member, before.channel)
        else:
            # テキストチャンネルから退出させる
            await _channel_exit(member, before.channel)

    # ボイスチャンネルに参加してきた場合
    if after.channel is not None:
        # 参加したチャンネルの1人目だった場合
        if len(after.channel.members) == 1:
            # テキストチャンネルを作成する
            await _channel_create(member, after.channel)
        else:
            # テキストチャンネルに参加させる
            await _channel_join(member, after.channel)

        # 入室時にメンションでチャンネルに案内
        await _channel_send_join(member, after.channel)

    print("fin voice state update event")


# テキストチャンネルを検索する関数
def _channel_find(voiceChannel):
    text_channels = voiceChannel.guild.text_channels
    channel_name = CHANNEL_PREFIX + str(voiceChannel.id)
    # 名前からチャンネルオブジェクトを取得する
    return discord.utils.get(text_channels, name=channel_name)


# チャンネル作成時の権限リストを返す
def _init_overwrites(guild, member):
    overwrites = {
        # デフォルトのユーザーはメッセージを見れないように
        guild.default_role: discord.PermissionOverwrite(read_messages=False),
        # 参加したメンバーは見ることができるように
        member: discord.PermissionOverwrite(read_messages=True)
    }

    bots_role = discord.utils.get(guild.roles, name=BOT_ROLE_NAME)
    if bots_role is not None:
        # Botもメッセージを見れるように
        bot_overwrite = {
            bots_role: discord.PermissionOverwrite(read_messages=True)
        }
        overwrites.update(bot_overwrite)

    return overwrites


# テキストチャンネルを作成する関数
async def _channel_create(member, voiceChannel):
    guild = voiceChannel.guild

    channel_name = CHANNEL_PREFIX + str(voiceChannel.id)
    overwrites = _init_overwrites(guild, member)
    category = voiceChannel.category

    # テキストチャンネルを作成
    await guild.create_text_channel(
        channel_name, overwrites=overwrites, category=category)


# テキストチャンネルを削除する関数
async def _channel_delete(member, voiceChannel):
    target = _channel_find(voiceChannel)
    if target is not None:
        await target.delete()


# テキストチャンネルに参加させる関数
async def _channel_join(member, voiceChannel):
    target = _channel_find(voiceChannel)
    if target is not None:
        overwrites = discord.PermissionOverwrite(read_messages=True)
        # 該当メンバーに読み取り権限を付与
        await target.set_permissions(member, overwrite=overwrites)


# テキストチャンネルから退出させる関数
async def _channel_exit(member, voiceChannel):
    target = _channel_find(voiceChannel)
    if target is not None:
        # 該当メンバーの読取権限を取り消し
        await target.set_permissions(member, overwrite=None)


# 入室時にメンションを飛ばして案内したい
async def _channel_send_join(member, voiceChannel):
    target = _channel_find(voiceChannel)
    if target is not None:
        await target.send(member.mention + "通話中のチャットはこちらをお使いください")


if __name__ == "__main__":
    client.run("あなたのbotのトークン")
```

上記のコードを実行し、ボイスチャンネルに入室したら'private_[チャンネルid]'という名前のチャンネルが自動で作られ、参加できました！
退室したらしっかりとチャンネルが削除されています。

# まとめ
意外と簡単にDiscord Botってつくれます。

discord.pyはじめたい！もっと詳しく知りたい！という場合は
[公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/)や[discord.py入門(1)](https://qiita.com/sizumita/items/9d44ae7d1ce007391699) などをおススメします。

欲しい機能をどんどん追加して、よいDiscordライフを！


# 参考
[Pythonで簡単なDiscord Botの作り方](https://qiita.com/PinappleHunter/items/af4ccdbb04727437477f)
[discord.pyドキュメント](https://discordpy.readthedocs.io/ja/latest/)
[discord.py入門(1)](https://qiita.com/sizumita/items/9d44ae7d1ce007391699)