Profile picture

[Python] 디스코드(Discord) 봇 구현하기 - discord.py

JaehyoJJAng2024년 09월 17일

개요

image

discord.py를 사용하여 디스코드 봇을 구현하는 방법에 대해서 기록해보려고 한다.


사전 준비

디스코드 봇을 원활하게 활성화하기 위해서는 몇 가지의 사전 준비 작업이 필요하다!

아래 단계들을 차근차근 따라하여 사전 준비를 완료해보자.


앱(봇) 생성

우선 디스코드 개발자 포털에 들어가서 새로운 애플리케이션(Bot)을 생성해주도록 하자.
(디스코드 가입이 되어있지 않다면 가입 먼저)
image


토큰 발급

애플리케이션을 생성하였다면 해당 애플리케이션의 토큰을 발급 받아야한다.
아래의 사진과 같이 해당 앱의 설정 - 봇 메뉴에서 'Reset Token'을 실행하여 토큰을 얻도록 하자.
image

💥 해당 토큰은 절대로 외부에 공유하지 않도록 하고, 노출되었다면 'Reset Token'으로 재갱신 해주도록 하자.


권한 부여

Bot이 메시지 콘텐츠를 수신하기 위해 권한 설정이 필요하다.

Privileged Gateway Intents 영역에 MESSAGE CONTENT INTENT를 활성화하고 [Save Changes]를 클릭하자.
image
Discord Privileged Gateway Intents는 Bot과 Application이 특정 민감한 데이터 및 기능에 액세스 할 수 있도록 허용하는 권한 시스템이다.


Gateway Intents 시스템은 2018년 Discord에서 도입했으며 Bot과 Application에서 명시적으로 요청하지 않는 한 특정 데이터 및 이벤트에 대한 액세스를 제한하여 서버 성능을 개선하고 사용자 개인 정보를 보호하도록 설계되었다.

Gateway Intent에는 "privileged"과 "unprivileged" 두 가지 유형이 존재한다.

privileged에는 서버 구성원 존재 및 구성원 데이터가 포함되며 unprivileged에는 비구성원 데이터 및 이벤트가 포함된다.

기본적으로 Discord Bot은 privileged에 액세스 할 수 없으며 Discord 개발자 포털을 통해 명시적으로 액세스를 요청해야 가능하다.

discord.errors.PrivilegedIntentsRequired: Shard ID None is requesting privileged intents that have not been explicitly enabled in the developer portal. It is recommended to go to https://discord.com/developers/applications/ and explicitly enable the privileged intents within your application's page. If this is not possible, then consider disabling the privileged intents instead.

만약 위와 같은 오류 메시지가 나왔다면 권한 설정이 필요하므로, 위 과정을 실시해주도록 하자.


봇 초대하기

방금 생성한 봇을 내 채널에 초대해보자.

초대할 앱의 설정 - OAuth2 - URL Generator 메뉴에서 'bot'을 선택하자.

개발할 bot에 필요한 기능들을 체크하고, 화면 하단에 보이는 GENERATED URL을 복사하여 브라우저에서 실행해주자. 그러면 봇을 초대할 디스코드 서버를 선택할 수 있다.
image
image
image


연결이 완료되었다.
image


기본적인 Discord 봇 구조

이번 챕터에서는 간단하게 디스코드 봇 구현 방법에 대해서 알아볼거다.


먼저 실습 전에 아래 명령어를 실행하여 discord.py를 설치해주자.

pip install discord.py

이제 디스코드에서 사용자로부터 /hello/echo에 대한 입력이 들어왔을 때, 적절한 명령을 수행하는 코드를 작성해보자.

bot.py

import discord
from discord.ext import commands

# 봇의 접두사 설정 (명령어 앞에 사용할 문자열)
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True  # 메시지 내용 접근 권한
bot = commands.Bot(command_prefix='/', intents=intents)

# 봇이 준비되었을 때 실행되는 이벤트
@bot.event
async def on_ready() -> None:
    print(f'봇이 실행되었습니다. 이름: {bot.user}')

# 메시지 명령어 처리
@bot.command()
async def hello(ctx) -> None:
    await ctx.send(f'안녕하세요, {ctx.author.name}님!')

# 에코 기능 (입력한 메시지를 그대로 반환)
@bot.command()
async def echo(ctx, *, content: str) -> None:
    await ctx.send(content)

# 봇 실행
bot.run('YOUR_BOT_TOKEN')

위 코드는 메시지를 감지하고, 특정 명령어(/hello, /echo)에 응답하는 간단한 봇 코드이다.


봇 테스트

디스코드 서버에서 /hello 또는 echo [내용]을 입력해 봇의 응답을 테스트해보자.
image



Cog 모듈화


Cog가 뭔데?

Cog는 discord.py에서 모듈이나 익스텐션을 뜻하는 용어이다.

위에서 기본적인 디스코드 봇 구조에 대해서 간략하게 알아봤는데

Cog로 기능들을 모듈화하면 카테고리별로 묶어 관리하는 것이 가능해진다.

이를 통해 코드의 가독성이 향상되고 유지 관리가 용이해지는 것이다.



discord.pycogs를 활용하면 봇의 기능을 모듈화하여 관리하기 쉬운 구조로 만들 수 있다.

이렇게 구조를 모듈화하면 추후에 새로운 기능 추가나 유지보수에 대해서도 매우 유연하게 대처가 가능하다!

Cog를 디렉토리 구조로 분리하여 모듈화와 관리가 쉬워지도록 변경해주자!


프로젝트 구조는 대강 다음과 같이 변경된다.

project/
│
├── bot.py           # 메인 봇 실행 파일
├── cogs/            # 기능별 Cog 모듈 폴더
│   ├── __init__.py  # 패키지 초기화 파일 (비워둬도 됨)
│   ├── general.py   # 일반 명령어를 관리하는 Cog
│   └── ...          # 다른 기능별 Cog 파일 추가 가능
└── requirements.txt # 설치할 패키지 리스트 (선택사항)

기능별 구현 코드는 cogs 디렉토리 하위에 구현하고 싶은 기능의 이름을 따서 파이썬 파일을 작성하고,

그 다음 기능에 맞는 코드를 구현하면 된다.


아래 예시는 general.py 파일에 일반적인 명령어를 관리하는 Cog 클래스를 정의한 구현 코드이다.


cogs/general.py

import discord
from discord.ext import commands

class General(commands.Cog):
    def __init__(self, bot) -> None:
        self.bot = bot

    # General Cog 실행
    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print("General is going ...")

    # hello 명령어
    @commands.command()
    async def hello(self, ctx) -> None:
        await ctx.send(f'안녕하세요, {ctx.author.name}님!')

    # echo 명령어
    @commands.command()
    async def echo(self, ctx, *, content: str) -> None:
        await ctx.send(content)

# Cog를 등록하는 함수
async def setup(bot) -> None:
    await bot.add_cog(General(bot))

Cogdiscord.ext.commands 확장 라이브러리에 속해있기 때문에

commands.Cog의 subclass를 생성해줬다.


앞서 이벤트를 확인할 때는 @bot.event 데코레이터를 사용하여 이벤트에 대응하는 함수를 직접 생성했었는데,

Cog의 경우에는 @commands.Cog.listener 데코레이터로 사용해야 한다.

🟢 `@bot.command` 데코레이터를 사용하는 일반 명령어는 `@commands.command`로 대신 사용한다.
🟠 Cog에 추가한 명령어와 실행 모듈에 있는 명령어의 이름이 같으면 오류가 발생한다.


메인 모듈에 Cog 연결하기

그리고 이렇게 Cog를 모듈화하면 기존의 bot.py 코드도 cogs 디렉토리의 Cog들을 자동으로 로드될 수 있도록

메인 모듈에 Cog를 연결해야 한다.

import os
import discord
import sys
import asyncio
from discord.ext import commands

# 봇의 접두사 설정 (명령어 앞에 사용할 문자열)
intents = discord.Intents.default()
intents.messages = True
intents.message_content = True  # 메시지 내용 접근 권한
bot: commands.Bot = commands.Bot(command_prefix="/", intents=intents)

# 봇 준비 이벤트
@bot.event
async def on_ready() -> None:
    print(f"봇이 실행되었음: {bot.user}")

# Cog 로드 함수
async def load_cogs() -> None:
    if not os.path.exists("./cogs"):
        print(f"cogs/ 디렉토리가 존재하지 않습니다!")
        sys.exit()

    for filename in os.listdir("./cogs"):
        if filename.endswith(".py"):
            no_ext_filename, ext = os.path.splitext(filename)
            await bot.load_extension(
                name=f"cogs.{no_ext_filename}"
            )  # 확장자 제외하고 로드

# 토큰 지정
_TOKEN: str = ""

# 봇 실행
async def main() -> None:
    async with bot:
        await load_cogs()  # cog 로드
        await bot.start(token=_TOKEN)

if __name__ == "__main__":
    # 비동기 메인 실행
    asyncio.run(main=main())

주요 변경 사항

Cog 클래스 생성

  • cogs/general.py에서 commands.Cog를 상속받는 클래스를 생성하였다.
  • 기능별로 클래스를 나누어 모듈화가 가능하기 때문에 추후 유지보수나 기능 추가가 매우 편리해진다!

Cog 로드 방식

  • bot.load_extension()을 사용하여 cogs 폴더에 있는 파일들을 자동으로 로드하도록 수정하였다.
  • 만약, 파일 이름이 cogs/general.py인 경우, cogs.general로 로드하게 된다.

비동기 setup 함수

  • Cog 파일에서 반드시 setup 함수를 정의해야 하며, 이를 통해 Cog를 봇에 등록해야하는 걸 잊지 말자!

새로운 기능 추가

만약에 새로운 Cog를 추가하려면 cogs/ 디렉토리에 새로운 .py 파일을 생성하고 기능을 구현한 뒤에

위와 동일하게 setup 함수를 정의하면 된다!

그리고 python bot.py로 프로그램을 실행하면 끝!

이런식으로 기능별 파일을 cogs 디렉토리 하위에 생성해 점점 확장해 나갈 수 있다!

bot.py에 때려박았을 때보다 디렉토리 가독성이 좋고, 훨씬 간편하지 않은가?



더 다양한 기능 구현해보기

이번 챕터에서는 discord.py에서 자주 사용되는 유용하고 다양한 기능들을 구현하는 방법에 대해서 기록해보려고 한다.

  • 슬래시 명령어
  • Embed 메세지
  • 상태 설정
  • 반복 작업 (Task)
  • 생성형 버튼
  • Form 생성
  • Ephemeral 메시지
  • 자동 삭제 메시지

등등 주요 기능을 다뤄보자.


1. Slash Command 구현하기

discord.app_commands 모듈을 활용해 디스코드의 최신 슬래시 명령어를 구현할 수 있다.

사용자는 / 를 입력하여 명령어를 실행한다.


cogs 디렉토리 하위에 slash_command.py 라는 파일을 생성한 후, 코드를 작성해보자.


cogs/slash_command.py

import discord
from discord.ext import commands
from discord import app_commands

class SlashCommand(commands.Cog):
    def __init__(self, bot: commands.Bot) -> None:
        super().__init__()
        self.bot = bot

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")

    @app_commands.command(
        name="sayhello", description="사용자에게 인사를 건네는 명령어입니다!"
    )
    async def say_hello(self, interaction: discord.Interaction):
        await interaction.response.send_message(f"Hello, {interaction.user.name}!")

# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(SlashCommand(bot=bot))

그리고 bot.py 메인 모듈 파일에서 명령어를 동기화시켜야 한다.

...
# 봇 준비 이벤트
@bot.event
async def on_ready() -> None:
    print(f"봇이 실행되었음: {bot.user}")
    await bot.tree.sync()  # tree 동기화
...

봇이 실행되면 on_ready 이벤트에서 bot.tree.sync()를 호출하여 Slash Command를 디스코드 서버와 동기화한다.


테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
image
명령어가 정상적으로 등록된 것이 보인다!



2. 스타일 있는 Embed 메시지 만들기

discord.embed를 사용하면 깔끔하고 가독성 좋은 메시지 생성이 가능하다!

cogs 디렉토리 하위에 embed_command.py 라는 파일을 생성한 후, 코드를 작성해보자.


cogs/embed_command.py

import discord
from discord.ext import commands
from discord import app_commands

class EmbedCommand(commands.Cog):
    def __init__(self, bot: commands.Bot) -> None:
        super().__init__()
        self.bot = bot

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")

    @app_commands.command(
        name="embed", description="embed를 테스트하기 위한 명령어입니다."
    )
    async def pretty_embed(self, interaction: discord.Interaction):
        embed = discord.Embed(
            title="봇 정보",
            description="이 봇은 다양한 기능을 제공합니다",
            color=discord.Color.blue(),
        )
        embed.add_field(name="버전", value="1.0", inline=True)
        embed.add_field(name="개발자", value="Jaehyo Lee", inline=True)
        embed.set_footer(text="이 메시지는 임베드로 작성되었습니다")
        await interaction.response.send_message(embed=embed)


# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(EmbedCommand(bot=bot))

테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
image


3. 반복 작업 처리: task

discord.ext.tasks 모듈을 사용하면 특정 작업을 주기적으로 실행할 수 있다.

예를 들어, 매일 알림을 보내는 봇도 task를 활용하여 구현이 가능하다

cogs 디렉토리 하위에 embed_command.py 라는 파일을 생성한 후, 코드를 작성해보자.


cogs/task_command.py

import discord
from discord.ext import commands, tasks
from discord import app_commands


class TaskCommand(commands.Cog):
    def __init__(self, bot: commands.Bot) -> None:
        super().__init__()
        self.bot = bot
        self.test_task.start()  # task 시작

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")

    @tasks.loop(seconds=10)
    async def test_task(self) -> None:
        channel_id: int = 0
        channel = self.bot.get_channel(channel_id)
        if channel:
            await channel.send("10초마다 실행되는 작업이에요~")

    @test_task.before_loop
    async def before_test_task(self) -> None:
        await self.bot.wait_until_ready()


# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(TaskCommand(bot=bot))

테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
image


4. 봇의 상태 설정: Activity Type


봇의 활동 상태를 설정하여 사용자에게 현재 무슨 작업을 하는지 표시할 수 있다.

이번에는 cog 서브클래스를 생성하는 것이 아닌 메인 모듈(bot.py)에서 작업해보도록 하겠다.


메인 모듈(bot.py)의 on_ready 이벤트에 아래와 같이 Activity를 명시해주면 된다.

# 봇 준비 이벤트
@bot.event
async def on_ready() -> None:
    ...
    activity = discord.Game(name="게임") # 또는 discord.Activity(type=discord.ActivityType.watching, name="YouTube")
    await bot.change_presence(status=discord.Status.online, activity=activity)

테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
image


5. 생성형 버튼: 유저 상호작용 강화하기

버튼을 활용하면 유저와의 상호작용을 한층 더 개선할 수 있다!

cogs 디렉토리 하위에 button_command.py 라는 파일을 생성한 후, 코드를 작성해보자.


cogs/button_command.py

import discord
from discord.ext import commands
from discord import app_commands, ButtonStyle
from discord.ui import Button, View


class ButtonView(View):
    def __init__(self) -> None:
        super().__init__()

    @discord.ui.button(label="button1", style=ButtonStyle.green, custom_id="button1")
    async def button_callback1(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ) -> None:
        button.callback = self.button_callback1
        await interaction.response.send_message(content="버튼1 클릭됨")

    @discord.ui.button(label="button2", style=ButtonStyle.green, custom_id="button2")
    async def button_callback2(
        self, interaction: discord.Interaction, button: discord.ui.Button
    ) -> None:
        button.callback = self.button_callback2
        await interaction.response.send_message(content="버튼2 클릭됨")


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

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")

        channel_id: int = 0
        channel = self.bot.get_channel(channel_id)
        if channel:
            await channel.send("버튼을 클릭해보세요.", view=ButtonView())


# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(ButtonSet(bot=bot))
  • 1. discord.ui_View 클래스를 사용하여 버튼을 포함한 뷰를 생성해준다.
  • 2. discord.ui.button 데코레이터로 버튼을 정의하고, button_callback 함수에서 버튼 클릭 시 수행할 작업을 설정해준다.
  • 3. ButtonSet 서브 클래스의 on_ready 이벤트에서 채널 ID를 지정하고 해당 채널로 메시지를 보낼 때 view=view를 전달하여 버튼을 함께 보낸다.

테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
discord_button



6. 사용자 입력받기: Form 생성 (Modal)

discord.ui.Modal을 활용하면 사용자에게 입력받을 수 있는 Form 생성이 가능하다!

cogs 디렉토리 하위에 form_command.py 라는 파일을 생성한 후, 코드를 작성해보자.


cogs/form_command.py

import discord
from discord.ui import Modal, TextInput
from discord.ext import commands
from discord import app_commands


class MyModal(Modal):
    def __init__(self) -> None:
        super().__init__(title="Form Test")
        self.name = TextInput(label="이름", placeholder="당신의 이름을 입력하시오.")
        self.age = TextInput(label="나이", placeholder="당신의 나이를 입력하시오.")
        self.add_item(self.name)
        self.add_item(self.age)

    async def on_submit(self, interaction: discord.Interaction) -> None:
        await interaction.response.send_message(
            f"이름: {self.name.value}, 나이: {self.age.value}"
        )

class FormCommand(commands.Cog):
    def __init__(self, bot: commands.Bot) -> None:
        super().__init__()
        self.bot = bot

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")

    @app_commands.command(
        name="form", description="Form를 테스트하기 위한 명령어입니다."
    )
    async def test_form(self, interaction: discord.Interaction):
        await interaction.response.send_modal(MyModal())


# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(FormCommand(bot=bot))

테스트

코드를 실행하고 디스코드로 이동하여 CTRL + R을 클릭해 디스코드를 새로고침 해주자.
form


7. Ephemeral 메시지: 개인 메시지 보내기

ephemeral 옵션으로 특정 사용자에게만 보이는 비공개 메시지를 전송할 수 있다.

@discord.app_commands.command(name="secret")
async def secret_message(interaction: discord.Interaction):
    await interaction.response.send_message("이 메시지는 당신에게만 보입니다.", ephemeral=True)

8. 자동 삭제 메시지: delete_after

delete_after 옵션을 사용하면 메시지를 일정 시간 후 자동으로 삭제할 수 있다.

@commands.command(name="tempmsg")
async def temp_message(ctx):
    await ctx.send("이 메시지는 5초 후에 삭제됩니다.", delete_after=5)

9. 채널 메시지 삭제하기

채널에 전송된 메시지를 삭제하려면 discord.message.Message 객체를 가져와 delete() 메소드로 삭제해주면 된다.

cogs 디렉토리 하위에 channel_message_delete.py 라는 파일을 생성한 후, 코드를 작성해보자.


아래 코드는 channel_id에 지정한 채널 ID를 가진 채널에서 전송된 메시지 중 최대 3개를 삭제하는 코드이다.


cogs/channel_message_delete.py

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

    @commands.Cog.listener()
    async def on_ready(self) -> None:
        print(f"{self.__class__.__name__} Cog is ready.")
        channel_id: int = 0

        # 체널 가져오기
        channel = self.bot.get_channel(channel_id)

        # 채널 메시지 전송하기
        if channel:
            # 채널의 최근 메시지 삭제하기
            try:
                async for message in channel.history(limit=3):
                    await message.delete()
            except discord.Forbidden:
                print("Bot lacks permission to delete message.")
            except discord.HTTPException as e:
                print(f"Failed to delete message: {e}")

            # 메시지 전송
            await channel.send("거래소 관련 채널입니다.", view=MarketButtonView())
        else:
            print(f"{self.__class__.__name__}: Channel Error")

# Cog 등록 함수
async def setup(bot: commands.Bot):
    await bot.add_cog(Button(bot=bot))

매개변수 타입힌트 (TypeHint) 지정하기

코드를 작성하다보면 매개변수들의 타입힌트(Typehint)을 지정하지 않아서 자동완성이 되지 않는 불편한 상황을 겪을 수 있다.


이럴 때에는 내가 궁금한 매개변수들의 타입을 type() 함수로 출력하여 어떤 타입이 반환되는지 확인하고

해당 반환 타입을 매개변수의 타입힌트로 지정해주면 된다.


그러면 Cog 클래스에서 ctx 매개변수의 타입이 궁금하다면 어떻게 해야할까?

cogs/general.py Cog를 기준으로 설명해보겠다.

import discord
from discord.ext import commands

class General(commands.Cog):
    def __init__(self, bot) -> None:
        self.bot = bot

    # ctx 매개변수 타입 확인을 위해 /hello 입력 시 타입이 출력되도록 코드 수정
    @commands.command()
    async def hello(self, ctx) -> None:
        print(f"ctx -> {type(ctx)})

    # echo 명령어
    @commands.command()
    async def echo(self, ctx, *, content: str) -> None:
        await ctx.send(content)

# Cog를 등록하는 함수
async def setup(bot) -> None:
    await bot.add_cog(General(bot))

이렇게하면 /hello 입력 시 매개변수 ctx에 대한 타입이 출력될 것이다.


출력해보니 다음과 같은 타입이 출력된다.

봇이 실행되었음: DevBot#0737
General is going ...
<discord.ext.commands.context.Context object at 0x000001D175995040> # 출력된 ctx 매개변수의 타입

이를 기반으로 매개변수에 다음과 같이 타입힌트를 지정해주면 된다.

import discord
from discord.ext import commands


class General(commands.Cog):
    ...
    @commands.command()
    async def hello(self, ctx: commands.Context) -> None:
        ctx.send(f"안녕하세요, {ctx.author.name}님!")
    ...

채널 ID 확인 방법

Discord 채널 ID는 디스코드 서버 각 채널에 할당되는 고유 식별값이다.

Discord를 이용하는데에는 채널 ID를 몰라도 문제될 게 없지만,

다양한 API 사용이나 명령, Bot 활용 등을 위해서는 필요할 수 있다.

해당 챕터에서는 디스코드에서 채널 ID를 찾는 방법에 대해서 간단하게 설명해보겠다.


1. 개발자 모드 활성화

먼저 개발자 모드가 활성화되어 있는지 확인이 필요하다. 사용자 설정을 클릭해보자.
image


비활성화 상태라면 고급 -> 개발자 모드를 클릭하여 활성화해주자.
image


2. 채널 ID 복사

다시 서버로 돌아와서 원하는 채널을 마우스 우클릭 후 메뉴에서 "ID 복사하기"를 클릭하자. 이제 필요한 곳에 붙여넣기를 하면된다.
image


Loading script...