🔰

Discord Botでメンバー募集機能を作成する

に公開

はじめに

こんにちは。Discordはゲーマーにとって欠かせない存在になっていると思います。ロールでメンションすればやりたいゲームの募集が一瞬でできて便利です。
Valorantをフルパでやりたくて募集した時,集まったらやりたいからできる人リアクションしてって感じで募集してました。でもこれだと集まってるか確認する方法はアプリを開いてリアクションの人数を見ることだけでした。集まったら通知してくれたらいいのになと思ってメンバー募集機能を作ってみました。Botの招待はこちら↓

https://discord.com/oauth2/authorize?client_id=814465495914119218&permissions=1759218604441591&integration_type=0&scope=bot+applications.commands

完成したもの

欲しかった機能

  • ボタンで参加,辞退できる
  • 誰が参加してるか一目で確認できる
  • 募集人数と現在の参加人数がわかる
  • 募集人数に達した時にメンションされる

初めはこれらの機能が欲しいと思い開発を始めました。
ただ,作っている間に,

  • 人数を指定せずに(無制限に)募集
    もできるようにしたいと考えました。

完成したコード

コードは募集用の埋め込みを作成して送信する部分と,ボタンが押された時の処理の二つです

埋め込みを作成して募集メッセージを送信する

    @app_commands.command(name='r', description='募集を行います')
    async def r(self, ctx: discord.Interaction, title: str, detail: str = None, max: int = None):
        """募集を行う
        Args:
            title (str): 募集タイトル
            detail (str): 募集詳細
            max (int): 募集人数
        """
        embed = discord.Embed(title=title, description=detail, color=0x00ff00)
        name = '参加者'
        name += f' (1/{max})' if max else ' (1)'
        embed.add_field(name=name, value=f'<@{ctx.user.id}>', inline=False)
        view = discord.ui.View()
        view.add_item(discord.ui.Button(
            label='参加', style=discord.ButtonStyle.primary, custom_id="join"))
        view.add_item(discord.ui.Button(
            label='辞退', style=discord.ButtonStyle.primary, custom_id="cancel"))
        view.add_item(discord.ui.Button(
            label='終了', style=discord.ButtonStyle.red, custom_id="delete"))

        await ctx.response.send_message(embed=embed, view=view)

コードの説明

まず,募集コマンドはタイトルと募集詳細,募集人数を受け取ります。
タイトルは必須で詳細と人数は任意の引数にするためtitle=Noneのようにします。
タイトルと詳細を入れて埋め込みを作成したら,参加人数と参加者を表示するフィールドを作成します。
フィールドは名前と値を持ち,埋め込みの中に25個まで挿入できます(一個しか使ってないけど)

name = '参加者'
name += f' (1/{max})' if max else ' (1)'

で最大人数が設定されているかどうかで表示を分けます。
valueは<@募集したユーザーのid>とすることでメンションの形で表示されるようにしています。
なんでctx.user.mention使わないんだって思った方へ。知らなかっただけです。後から気づいたけどそのままにしてます。

ボタンはviewを作成してその中に追加していく形で作ります。ボタンのラベルと色,custom_idを指定して追加します。custom_idの説明はあとで。

あとはctx.response.send_message()にembedとviewを指定して送信するだけです。

ボタンが押された時の処理

class Event(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
        self.level = level.Level(self.bot)

    @commands.Cog.listener()
    async def on_button_click(self, ctx: discord.Interaction):
        def find_num(s: str) -> list:
            """募集人数を取得する
            Args:
                s (str): 募集内容
            Returns:
                list: [現在の参加人数, 募集人数]
            """
            start_index1 = s.find("/") + 1
            end_index1 = s.find(")")
            max_number = int(s[start_index1:end_index1])
            start_index2 = s.find("(") + 1
            end_index2 = s.find("/")
            number = int(s[start_index2:end_index2])
            return [number, max_number]

        custom_id = ctx.data['custom_id']
        message_id = ctx.message.id
        message = await ctx.channel.fetch_message(message_id)
        embed = message.embeds[0]
        embed_dict = embed.to_dict()
        member_str = embed_dict['fields'][0]['value']
        member_list = member_str.split('\n')
        name = embed_dict['fields'][0]['name']

        # --参加ボタンが押されたとき--
        if custom_id == 'join':
            # *すでに参加しているとき
            if f'<@{ctx.user.id}>' in member_list:
                await ctx.response.send_message(content=f'<@{ctx.user.id}> すでに参加しています', ephemeral=True)
                return
            # *参加してないとき
            member_str += f'\n<@{ctx.user.id}>'  # 参加者リストに追加
            # *募集人数が設定されているとき
            if '/' in name:
                num_list = find_num(name)  # 現在の参加人数と募集人数を取得
                num_now = num_list[0] + 1  # 現在の参加人数を更新
                num_max = num_list[1]  # 募集人数を取得
                name = f'参加者 ({num_now}/{num_max})'
                # *募集人数に達したとき
                if num_now == num_max:
                    embed.color = discord.Color.red()
                    embed.set_field_at(
                        0, name=name, value=member_str, inline=False)
                    view = discord.ui.View()
                    await message.edit(embed=embed, view=view)
                    await ctx.response.send_message(content=f'\n募集内容:{embed.title}\n{member_str}')
                    return
            else:
                # *人数が0から1になるとき len(member_list) + 1 は2になってしまうのでその処理
                if len(member_list) == 1 and member_list[0] == '':
                    name = f'参加者 (1)'
                else:
                    # 参加者数を更新 リストの長さは参加者数より1少ない
                    name = f'参加者 ({len(member_list) + 1})'
            embed.set_field_at(0, name=name, value=member_str, inline=False)
            await message.edit(embed=embed)  # メッセージを更新
            await ctx.response.send_message(content=f'<@{ctx.user.id}> 参加しました', ephemeral=True)

        # --辞退ボタンが押されたとき--
        if custom_id == 'cancel':
            # *参加していないとき
            if f'<@{ctx.user.id}>' not in member_list:
                await ctx.response.send_message(content=f'<@{ctx.user.id}> まだ参加していません', ephemeral=True)
                return
            # *参加しているとき
            member_list.remove(f'<@{ctx.user.id}>')  # 参加者リストから削除
            member_str = '\n'.join(member_list)
            # *募集人数が設定されているとき
            if '/' in name:
                num_list = find_num(name)
                num_now = num_list[0] - 1
                num_max = num_list[1]
                name = f'参加者 ({num_now}/{num_max})'
            else:
                name = f'参加者 ({len(member_list)})'  # 参加者数を更新 リストの長さはすでに1引かれている
            embed.set_field_at(0, name=name, value=member_str, inline=False)
            await message.edit(embed=embed)
            await ctx.response.send_message(content=f'<@{ctx.user.id}> 辞退しました', ephemeral=True)

        # --終了ボタンが押されたとき--
        if custom_id == 'delete':
            # *ボタンを押した人が募集者かオーナーのとき
            if f'<@{ctx.user.id}>' == member_list[0] or ctx.user.id == ctx.guild.owner.id:
                embed.color = discord.Color.red()
                view = discord.ui.View()
                await message.edit(embed=embed, view=view)
                await ctx.response.send_message(content=f'募集を終了しました\n募集内容:{embed.title}\n{member_str}', ephemeral=False)
            # *募集者でもオーナーでもないとき
            else:
                await ctx.response.send_message(content='募集者のみが募集を終了できます', ephemeral=True)
 

    # インタラクションの中からボタンのクリックとセレクトを選別
    @commands.Cog.listener()
    async def on_interaction(self, ctx: discord.Interaction) -> None:
        try:
            if ctx.data['component_type'] == 2:
                await self.on_button_click(ctx)

        except KeyError:
            pass

コードの説明

ボタンが押されるとon_interaction()が実行されます。
イベント検知にはon_button_clickが用意されていないので,インタラクションからボタンを選別しています。

on_button_click()ではまず,どのボタンが押されたかを確かめるためにcustom_id,どのメッセージのボタンが押されたか確かめるためにmessage_idを取得しています。
message_idからMessageオブジェクトを取得し,その中のEmbedを辞書型に変換し,募集の内容や参加人数,現在の参加者を取得します。あとは押された内容で分岐してそれぞれ処理してます。

おわり

現在は参加した人にロールを付与する機能や,新しいテキストチャンネルを作成して募集する機能も追加していますが,その紹介は別の機会にするとします。
Botの宣伝程度で記事書いてるのでコードの説明は少なめでした。
そもそも別にそんな難しいコードじゃないから説明することそんなになかったです。もっと綺麗なコード描けるようになりたいです。Bot使ってフィードバックいただけたら嬉しいです。

Discussion