Profile picture

[Python] 디스코드 봇 - Cog로 코드를 모듈화하기 (nextcord)

JaehyoJJAng2024년 01월 19일

◾️ 개요

Cog를 사용하면 코드를 모듈화하고 기능을 논리적으로 분리할 수 있습니다. 이를 통해 코드의 가독성이 향상되고 유지 관리가 용이해집니다. 예를 들어, 각 Cog는 특정 기능 또는 목적에 대응하며, 필요할 때 Cog를 추가하거나 제거하여 봇의 기능을 조정할 수 있습니다. 또한, 여러 명의 개발자가 동시에 작업할 때 각자의 Cog를 관리하고 개발할 수 있으므로 협업이 용이합니다.


◾️ 앱(봇) 생성

다음 게시글 참고 👉 디스코드 봇 구현하기 - WTT Devlog


◾️ 봇 개발하기

아래 패키지 사전 설치

pip install -r requirements.txt

requirements.txt

nextcord==2.6.0

▪️ 코드


{% include codeHeader.html name="main.py" %}

import nextcord
from nextcord.ext import commands
import os

intents = nextcord.Intents.all()
bot = commands.Bot(command_prefix='/', intents=intents, help_command=None)
_token = '<TOKEN>'

@bot.event
async def on_ready():
    print(f'{bot.user} is ready!')
    # 봇의 현재 활동 상태 지정하기
    activity = nextcord.Activity(type=nextcord.ActivityType.playing, name='Palworld')
    await bot.change_presence(activity=activity)

# Error Handling
@bot.event
async def on_command_error(ctx, error):
    if isinstance(error, commands.CommandNotFound):
        await ctx.send('This command does not exist.')
    elif isinstance(error, commands.MissingPermissions):
        await ctx.send("You don't have the required permissions to use this command.")
    elif isinstance(error, commands.MissingRequiredArgument):
        await ctx.send("You are missing a required argument.")
    elif isinstance(error, commands.CommandOnCooldown):
        await ctx.send("This command is on cooldown. Please try again later.")
    else:
        await ctx.send(f'An error occured: {error}')

if __name__ == '__main__':
    for filename in os.listdir("cogs"):
        if filename.endswith(".py"):
            bot.load_extension(f"cogs.{filename[:-3]}")
    bot.run(_token)

코드 설명

  • 먼저 코드의 시작 부분에서 필요한 모듈들을 가져옵니다.
    • nextcord: 디스코드 봇을 만들기 위한 모듈입니다.
    • nextcord.ext.commands: 명령어 기반의 디스코드 봇을 작성하기 위한 부가적인 모듈입니다.
    • os: 운영 체제와 상호 작용하기 위한 모듈입니다.
  • 다음으로, intents 변수를 설정합니다.
    • nextcord.Intents.all(): 모든 인텐트를 활성화합니다. 인텐트는 디스코드 봇이 수신하는 이벤트를 제어하는 데 사용됩니다.
  • 그 후, commands.Bot 클래스를 사용하여 봇을 초기화합니다.
    • command_prefix=config.bot_prefix: 봇의 명령어 접두사를 설정합니다.
    • intents=intents: 방금 설정한 인텐트를 전달하여 봇이 어떤 이벤트를 받을지 결정합니다.
    • help_command=None: 기본 도움말 명령어를 비활성화합니다.
  • on_ready 이벤트 핸들러를 정의합니다.
    • 봇이 준비되면 콘솔에 메시지를 출력하고, 설정된 활동(playing, watching 등)을 설정합니다.
  • 오류 처리를 위한 on_command_error 이벤트 핸들러를 정의합니다.
    • 다양한 종류의 오류에 대한 메시지를 클라이언트에게 전송합니다.
  • __name__ == '__main__'을 사용하여 스크립트가 직접 실행될 때만 특정 작업을 수행하도록 합니다.
    • "cogs" 디렉토리에서 .py 확장자로 끝나는 파일을 모두 가져와서 각각을 Cog로서 불러옵니다.
  • bot.run(_token): 설정된 토큰을 사용하여 봇을 실행합니다.

이제 cogs 디렉토리 하위에 디스코드 봇에 추가할 기능들을 구현해주면 된다.

간단한 예시로 'hey'라고 전송하면 'hello'가 응답오도록 cogs/hello.py에 해당 로직을 구현해보자.

cogs/hello.py

from nextcord.ext import commands, tasks
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    @commands.command(name="hey")
    async def hello(self, ctx:commands.Context):
        await ctx.send("hello")

def setup(bot:commands.Bot):
    bot.add_cog(HelloCog(bot))

image


• slash_command

  • slash_command: 슬래시 커맨드를 사용하면 위의 HelloCog 예제처럼 사용자가 일일이 명령어를 기억하지 않아도 된다.

cogs/example.py

from nextcord.ext import commands
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
    
    @nextcord.slash_command(name='say',description='Say it!')
    async def hello(self,interaction: nextcord.Interaction) -> None:
        text: str = 'Hello!'
        interaction.response.send_message(text)

def setup(bot:commands.Bot):
    bot.add_cog(PlayerBanCog(bot))

또한, 명령어를 어드민만 사용 가능하도록 명령어에 대한 권한을 지정할 수도 있다

from nextcord.ext import commands
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
    
    @nextcord.slash_command(name='say',description='Say it!',default_member_permissions=nextcord.Permissions(administrator=True))
    async def hello(self,interaction: nextcord.Interaction) -> None:
        text: str = 'Hello!'
        interaction.response.send_message(text)

def setup(bot:commands.Bot):
    bot.add_cog(PlayerBanCog(bot))

일반 유저도 사용할 수 있도록 하려면 아래처럼 권한을 수정하면 된다.

from nextcord.ext import commands
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
    
    @nextcord.slash_command(name='say',description='Say it!',default_member_permissions=nextcord.Permissions(permissions=1))
    async def hello(self,interaction: nextcord.Interaction) -> None:
        text: str = 'Hello!'
        interaction.response.send_message(text)

def setup(bot:commands.Bot):
    bot.add_cog(PlayerBanCog(bot))

명령어에 인자를 입력 받아볼 수도 있다.

from nextcord.ext import commands
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot
    
    @nextcord.slash_command(name='say',description='Say it!',default_member_permissions=nextcord.Permissions(administrator=True))
    async def hello(self,interaction: nextcord.Interaction, arg: str) -> None:
        print(arg)

def setup(bot:commands.Bot):
    bot.add_cog(PlayerBanCog(bot))

arg 변수의 경우 명령어에서 입력 받고싶은 문자열로 지정해주면 된다.


예를 들어, steamid라는 인자를 받도록 설정하려면 아래와 같이 인자명을 변경해주면 된다.

...
    async def hello(self,interaction: nextcord.Interaction, steamid: str) -> None:
        print(steamid)

그리고 인자에 대한 설명도 붙여줄 수 있다.

....
    async def hello(self,interaction: nextcord.Interaction, arg: str=nextcord.SlashOption(description='아무 텍스트나 입력하세요!',autocomplete=True)) -> None:
        print(arg)

이렇게 하면 사용자가 해당 인자에 대해서 어떤 값을 넣어야하는지 쉽게 알 수 있게된다.


• embed

  • nextcord에서 자체적으로 지원하는 embed를 사용해보자.

{% include codeHeader.html name="cogs/embed.py" %}

import nextcord
from nextcord.ext import commands

class HelloCog(commands.Cog):
    def __init__(self,bot) -> None:
        self.bot: commands.Bot = bot
    
    @nextcord.slash_command(name='say',description='Say it!',default_member_permissions=nextcord.Permissions(administrator=True))
    async def hello(self,interaction: nextcord.Interaction, arg: str) -> None:
        embed: nextcord.Embed = nextcord.Embed()
        embed.title = '인삿말'
        embed.description = '인사를 합니다'
        embed.color = 2003199
        embed.add_field(name='', value=arg, inline=True)
        interaction.response.send_message(embed=embed)

• tasks

  • 매 시간마다 함수가 동작하도록 task를 등록해보자.

cogs/tasks.py

from nextcord.ext import commands, tasks
import nextcord

class HelloCog(commands.Cog):
    def __init__(self, bot):
        self.bot: commands.Bot = bot

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print('Hello Cog Startomg ...')
        self.send_message.start()
    
    @tasks.loop(minutes=10)
    async def send_message(self) -> None:
        text: str | None = '안녕하세요'

        if text:
            embed: nextcord.Embed = nextcord.Embed()
            embed.title = '인삿말'
            embed.description = '인사를 합니다'
            embed.color = 2003199
            channel = self.bot.get_channel(<channel_id>)
            await channel.send(embed=embed)

def setup(bot:commands.Bot):
    bot.add_cog(ShowPlayersCog(bot))

특정 채널에 10분마다 embed 메시지가 전송됨.


• ActivityType

봇의 활동 상태또한 지정 가능하다.

from src.rcon_show_players import get_users
from src.server import check_container_status
from nextcord.ext import commands,tasks
import config.config as config
import nextcord
import os

intents = nextcord.Intents.all()
bot = commands.Bot(command_prefix=config.bot_prefix, intents=intents, help_command=None)

@bot.event
async def on_ready() -> None:
    # activity_name 지정
    activity_name: str = f'Palworld'

    # 봇 상태를 playing으로 전환
    activity = nextcord.Activity(type=nextcord.ActivityType.playing, name=activity_name)
    await bot.change_presence(activity=activity)

def main() -> None:
    for filename in os.listdir('cogs'):
        if filename.endswith('.py'):
            print(f'cogs.{filename[:-3]}')
            bot.load_extension(f'cogs.{filename[:-3]}')
    bot.run(config.bot_token)
    
if __name__ == '__main__':
    main()

그 외에 다른 ActivityType이 궁금하다면 아래 문서를 확인해보자.
https://docs.nextcord.dev/en/stable/api.html


‣ nextcord.ui.View

  • 버튼형 UI 생성 방법

image
nextcord.ui.button 이라는 데코레이터를 사용하여 버튼을 생성할 수 있다.

예시로, nextcord.ui.View를 상속하여 커스텀하는 MyView라는 클래스를 하나 만들어 사용해보자.

이렇게 하면 View에 더 많은 기능을 추가하거나, 뷰의 동작을 사용자가 정의할 수 있게된다.

class ButtonView(nextcord.ui.View):
    def __init__(self, bot: commands.Bot):
        super().__init__()
        self.bot = bot
    
    @nextcord.ui.button(label='테스트 버튼', style=nextcord.ButtonStyle.danger)
    async def test_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
        message = await interaction.response.send_message('테스트1 로직 수행 시작!', ephemeral=True)
        
        # 사용자가 읽은 후 몇 분 후에 메시지가 사라짐.
        await asyncio.sleep(10)
        await message.delete()

코드 설명

ButtonView 클래스를 정의하고 있다. 이 클래스는 nextcord.ui.View 클래스를 상속받는다. 즉, 디스코드 UI 구성 요소를 만들 수 있는 클래스이다.

class ButtonView(nextcord.ui.View)

ButtonView 클래스의 생성자입니다. bot이라는 인자를 받는다.
여기서는 commands.Bot 객체가 예상되는데, 이는 디스코드 봇을 나타낸다.
super().__init__()을 통해 부모 클래스의 생성자를 호출하고, self.bot 속성에 전달된 bot 객체를 할당한다.

def __init__(self, bot: commands.Bot):
    super().__init__()
    self.bot = bot

'테스트 버튼'을 정의하는 부분이다.
@nextcord.ui.button 데코레이터는 이 메서드가 버튼을 만들 때 사용됨을 나타낸다.
label 매개변수에는 버튼에 표시될 텍스트를, style에는 버튼의 스타일을 설정한다.
여기서는 '테스트 버튼'이라는 텍스트와 위험한(danger) 스타일을 사용하고 있다.
이 메서드는 버튼이 클릭되면 실행되는 콜백 함수이다.
사용자가 버튼을 누르면 '테스트1 로직 수행 시작!'이라는 메시지가 봇에 의해 보내지고, 그 메시지는 10초 후에 삭제된다.

@nextcord.ui.button(label='테스트 버튼', style=nextcord.ButtonStyle.danger)
async def test_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
    message = await interaction.response.send_message('테스트1 로직 수행 시작!', ephemeral=True)
    
    # 사용자가 읽은 후 몇 분 후에 메시지가 사라짐.
    await asyncio.sleep(10)
    await message.delete()

마지막으로 버튼은 하나가 아닌 여러 개의 버튼을 생성할 수 있다.

class ButtonView(nextcord.ui.View):
    def __init__(self, bot: commands.Bot):
        super().__init__()
        self.bot = bot
    
    @nextcord.ui.button(label='테스트 버튼', style=nextcord.ButtonStyle.danger)
    async def test_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
        message = await interaction.response.send_message('테스트1 로직 수행 시작!', ephemeral=True)
        
        # 사용자가 읽은 후 몇 분 후에 메시지가 사라짐.
        await asyncio.sleep(10)
        await message.delete()

    @nextcord.ui.button(label='테스트 버튼2', style=nextcord.ButtonStyle.danger)
    async def test_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
        message = await interaction.response.send_message('테스트2 로직 수행 시작!', ephemeral=True)

• 상호작용 실패 문제

버튼을 활성화하고 난 뒤로부터, 일정 시간이 지나고 다시 버튼을 클릭하면 아래와 같이 '상호작용 실패'가 발생하게 된다.

일반적으로 버튼의 경우 timeout = none이더라도 최대 15분간 유효하고, 또는 봇이 재부팅 된 경우 버튼이 정상적으로 동작하지 않을 수 있다.

이런 상황이 발생하지 않게 하려면, persistent_view를 설정해줘야 한다.

자세한 코드는 위 링크를 참조해보면 되고, 나의 경우에는 아래와 같이 설정해보았다.

class ButtonView(nextcord.ui.View):
    def __init__(self, bot: commands.Bot):
        super().__init__(timeout=None) # timeout을 None으로 지정.
        ...
    
    # custon_id를 명시
    @nextcord.ui.button(label='공지사항', style=nextcord.ButtonStyle.danger, custom_id='persistent_view:danger')
    async def notice_button(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
        # 이전 메시지 삭제
        await self.delete_previous_message(user_id=interaction.user.id)
        
        # 메시지 전송        
        message = await interaction.response.send_message('공지사항 로직 수행 시작!', ephemeral=True)
        
        # 메시지 기록
        self.result_message[interaction.user.id] = message
        
        # 사용자가 읽은 후 몇 분 후에 메시지가 사라짐.
        await asyncio.sleep(10)
        await message.delete()

‣ ephemeral

ephemeral=True 이는 메시지가 특정 사용자에게만 보이고 다른 사용자에게는 보이지 않도록 하는 옵션이다. (나에게만 보이기)

message = await interaction.response.send_message(embed=embed, ephemeral=True)

‣ delete_after

delete_after 옵션은 Nextcord에서 메시지를 특정 시간이 지난 후 자동으로 삭제되도록 하는 옵션이다.

이 옵션에 초 단위의 값을 설정하면, 해당 시간이 지난 후 메시지가 자동으로 삭제된다. 이는 일회성 알림이나 임시 메시지를 관리하는데 유용하다.

예를 들어, delete_after=10으로 설정하면 메시지가 전송된 지 10초 후에 해당 메시지가 자동 삭제된다.

message = await interaction.response.send_message(embed=embed, delete_after=10)

이 예시에서는 임베드된 메시지가 전송된 후 10초 후에 자동으로 삭제된다.


Loading script...