개요
discord.py
를 사용하여 디스코드 봇을 구현하는 방법에 대해서 기록해보려고 한다.
사전 준비
디스코드 봇을 원활하게 활성화하기 위해서는 몇 가지의 사전 준비 작업이 필요하다!
아래 단계들을 차근차근 따라하여 사전 준비를 완료해보자.
앱(봇) 생성
우선 디스코드 개발자 포털에 들어가서 새로운 애플리케이션(Bot)을 생성해주도록 하자.
(디스코드 가입이 되어있지 않다면 가입 먼저)
토큰 발급
애플리케이션을 생성하였다면 해당 애플리케이션의 토큰을 발급 받아야한다.
아래의 사진과 같이 해당 앱의 설정 - 봇 메뉴에서 'Reset Token'을 실행하여 토큰을 얻도록 하자.
💥 해당 토큰은 절대로 외부에 공유하지 않도록 하고, 노출되었다면 'Reset Token'으로 재갱신 해주도록 하자.
권한 부여
Bot이 메시지 콘텐츠를 수신하기 위해 권한 설정이 필요하다.
Privileged Gateway Intents 영역에 MESSAGE CONTENT INTENT를 활성화하고 [Save Changes]를 클릭하자.
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을 복사하여 브라우저에서 실행해주자. 그러면 봇을 초대할 디스코드 서버를 선택할 수 있다.
연결이 완료되었다.
기본적인 Discord 봇 구조
discord.py
공식 문서
이번 챕터에서는 간단하게 디스코드 봇 구현 방법에 대해서 알아볼거다.
먼저 실습 전에 아래 명령어를 실행하여 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 # 메시지 내용 접근 권한
intents.members = 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 [내용]
을 입력해 봇의 응답을 테스트해보자.
Cog 모듈화
Cog는 discord.py
에서 모듈이나 익스텐션을 뜻하는 용어이다.
위에서 기본적인 디스코드 봇 구조에 대해서 간략하게 알아봤는데
Cog로 기능들을 모듈화하면 카테고리별로 묶어 관리하는 것이 가능해진다.
이를 통해 코드의 가독성이 향상되고 유지 관리가 용이해지는 것이다.
discord.py
의 cogs
를 활용하면 봇의 기능을 모듈화하여 관리하기 쉬운 구조로 만들 수 있다.
이렇게 구조를 모듈화하면 추후에 새로운 기능 추가나 유지보수에 대해서도 매우 유연하게 대처가 가능하다!
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))
Cog
는 discord.ext.commands
확장 라이브러리에 속해있기 때문에
commands.Cog
의 subclass를 생성해줬다.
앞서 이벤트를 확인할 때는 @bot.event
데코레이터를 사용하여 이벤트에 대응하는 함수를 직접 생성했었는데,
Cog의 경우에는 @commands.Cog.listener
데코레이터로 사용해야 한다.
메인 모듈에 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))
특정 채널에서만 /sayhello
명령어가 실행 가능하도록 하려면 다음과 같이 코드를 작성하면 된다.
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
self.channel_id: int = 000000 # 명령어 사용이 가능한 채널의 채널 ID 입력
@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):
# 특정 채널인지 확인
if interaction.channel.id != self.channel_id:
await interaction.response.send_message(f"이 명령어는 <#{self.channel_id}> 채널에서만 사용 가능합니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!")
# Cog 등록 함수
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
또한 어드민 권한이 있는 유저만 /sayhello
명령어 사용이 가능하도록 변경하려면 다음과 같이 코드를 작성해주자.
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):
# 어드민 권한 확인
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f"{interaction.user.name} 님은 이 명령어를 사용할 권한이 없습니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!")
# Cog 등록 함수
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
명령어에 인자를 받아서 처리하고 싶다면 @app_commands.command
에 인자를 정의해주면 된다.
예를 들어, 사용자가 /명령어 <내용>
과 같은 명령어 사용이 가능하게끔 해준다.
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="사용자에게 인사를 건네는 명령어입니다!"
)
@app_commands.describe(content="공지할 점검 내용을 입력하세요.")
async def say_hello(self, interaction: discord.Interaction, content: str):
# 어드민 권한 확인
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(f"{interaction.user.name} 님은 이 명령어를 사용할 권한이 없습니다!")
return
await interaction.response.send_message(f"Hello, {interaction.user.name}!\nyour content is: {content}")
# Cog 등록 함수
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
주의할 점으로는 @app_commands.describe
데코레이터에서 받는 content
매개 변수와 say_hello
메소드에서 받는 content
매개 변수의 이름이 동일해야 한다.
슬래시 명령어에 대한 Cog 작성이 끝났다면,
마지막으로 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
을 클릭해 디스코드를 새로고침 해주자.
명령어가 정상적으로 등록된 것이 보인다!
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
을 클릭해 디스코드를 새로고침 해주자.
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
을 클릭해 디스코드를 새로고침 해주자.
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
을 클릭해 디스코드를 새로고침 해주자.
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
을 클릭해 디스코드를 새로고침 해주자.
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))
만약 discord.ui.TextInput
에서 여러 줄을 입력할 수 있는 멀티라인을 구현하고 싶다면
discord.TextStyle.paragraph
를 활용하면 된다.
....
class MyModal(Modal):
def __init__(self) -> None:
super().__init__(title="Form Test")
self.name = TextInput(label="이름", placeholder="당신의 이름을 입력하시오.", style=discord.TextStyle.paragraph)
self.age = TextInput(label="나이", placeholder="당신의 나이를 입력하시오.", style=discord.TextStyle.paragraph)
self.add_item(self.name)
self.add_item(self.age)
....
테스트
코드를 실행하고 디스코드로 이동하여 CTRL
+ R
을 클릭해 디스코드를 새로고침 해주자.
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))
10. 응답 지연 해결하기 - Interaction.response.defer()
Interaction.response.defer()
는 디스코드 상호작용(Interaction)의 응답을 지연(defer)시키는 메서드이다.
특정 명령어 처리에 시간이 걸릴 경우, Discord는 3초 내에 응답을 받지 못하면 에러를 반환한다.
이를 방지하기 위해 응답을 미리 지연 처리하고, 이후에 결과를 제공할 수 있도록 설계된 것이 바로 defer()
이다.
cogs
디렉토리 하위에 test_defer
라는 파일을 생성한 후, 코드를 작성해보자.
아래 코드는 Interaction.respons.defer()
를 활용한 간단한 예시 코드이다.
cogs/test_defer.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="longtask", description="defer 테스트를 위한 명령어입니다."
)
async def long_task(self, interaction: discord.Interaction):
# 응답 지연
await interaction.response.defer()
# 시간이 걸리는 작업 수행
import time
time.sleep(5)
# 최종 결과 응답
await interaction.followup.send("작업 완료!")
# Cog 등록 함수
async def setup(bot: commands.Bot):
await bot.add_cog(SlashCommand(bot=bot))
참고 사항
- 1.
defer()
호출 후 추가 응답 필수:defer()
는 응답을 지연시키기만 하며, 이후followup.send()
또는edit_original_response()
등으로 최종 응답을 제공해야 한다. - 2. 3초 이내 호출 필요:
defer()
는 Interaction 이벤트 발생 후 3초 이내에 호출해야 Discord의 응답 제한 준수가 가능하다.
매개변수 타입힌트 (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}님!")
...
응용 가능한 심화 기능 구현해보기
문의(티켓) 생성 기능
서버 멤버들이 직접 문의를 남길 수 있는 티켓 기반의 문의 기능을 구현해보는 기능을 구현해보자.
이번 심화 챕터에서 구현해보려는 봇의 기능은 다음과 같다.
- 1. 고객센터 안내 메시지: "채널권한"이라는 텍스트 채널에 이용 안내 메시지를 표시하고, 사용자가 선택할 수 있는 버튼을 제공한다.
- 2. 문의 버튼: 사용자가 버튼을 클릭하면 개인 티켓 채널이 생성된다.
- 3. 자동 종료 타이머: 티켓 채널에서 3분간 활동(메시지)가 없으면 자동으로 채널이 삭제된다.
- 4. 권한 설정: 티켓 채널은 해당 사용자와 관리자만 접근이 가능해야 한다.
주요 구현 사항
1. 봇 설정 및 기본 구조
bot.py
를 다음과 같이 작성한다.
bot.py
import os
import discord
import sys
import asyncio
from discord.ext import commands
from dotenv import load_dotenv
# 봇의 접두사 설정 (명령어 앞에 사용할 문자열)
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:
activity = discord.Activity(type=discord.ActivityType.playing, name="Game")
await bot.change_presence(status=discord.Status.online, activity=activity)
await bot.tree.sync() # tree 동기화
# Cog 로드 함수
async def load_cogs() -> None:
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}"
) # 확장자 제외하고 로드
# 봇 실행
load_dotenv(dotenv_path=".env")
_TOKEN: str = os.getenv("TOKEN")
async def main() -> None:
async with bot:
await load_cogs() # cog 로드
await bot.start(token=_TOKEN)
if __name__ == "__main__":
# 비동기 메인 실행
asyncio.run(main=main())
티켓 생성 같은 기능 구현 로직들은 cogs
디렉토리 하위에다가 기능에 맞는 파일이름.py
형식으로 파일을 만들어 진행할거다.
2. 티켓 생성 버튼 구현
cogs
디렉토리 하위에 ticket.py
이라는 파일을 생성하고 다음과 같은 코드를 작성해주자.
cogs/ticket.py
class TicketView(View):
def __init__(self, bot: commands.Bot) -> None:
super().__init__(timeout=None) # 타임아웃 제거
self.bot = bot
@discord.ui.button(
label="문의하기", style=ButtonStyle.blurple, custom_id="create_ticket"
)
async def create_ticket(
self,
interaction: discord.Interaction,
button: discord.ui.Button,
) -> None:
guild = interaction.guild
user = interaction.user
# 카테고리 추출
category = discord.utils.get(guild.categories, name="고객센터")
# 카테고리가 없는 경우 새롭게 생성
if not category:
category = await guild.create_category("고객센터")
# 카테고리에 있는 채널 목록 추출
for category_channel in category.channels:
# 요청 유저에 대한 티켓이 생성되었는지 분기 작성
check_ticket_channel = f"문의-{user.name}"
if check_ticket_channel == category_channel.name:
await interaction.response.send_message("이미 티켓이 열려 있습니다!", ephemeral=True)
return
# 티켓 채널 생성
overwirtes = {
guild.default_role: discord.PermissionOverwrite(read_messages=False),
user: discord.PermissionOverwrite(read_messages=True, send_messages=True),
}
channel = await guild.create_text_channel(
name=f"문의-{user.name}", category=category, overwrites=overwirtes
)
# 티켓 생성 문구 사용자에게 출력해주기
await interaction.response.send_message(
f"티켓이 생성되었습니다: {channel.mention}", ephemeral=True
)
# 임베드 작성하기
problem_embed = discord.Embed(
title=f"{user.name} 님의 Ticket이 활성화 되었습니다.",
description=(
f"**{user.name}님! 안녕하세요, 저희 디스코드 채널에 문의할 내용이 있으신가요?**\n\n"
),
)
# 임베드 메시지 생성된 채널로 전송하기
await channel.send(embed=problem_embed)
# 자동종료 타이머
await self.start_inactivity_timer(channel=channel, user=user)
await interaction.response.defer()
async def start_inactivity_timer(
self, channel: discord.channel.TextChannel, user: discord.member.Member
) -> None:
try:
def check(m):
return m.channel == channel and m.author == user
await self.bot.wait_for("message", check=check, timeout=180.0)
except asyncio.TimeoutError:
await channel.delete()
Button View를 생성하는 TicketView
클래스를 작성해주었다.
3. wait_for / def check(..)
ticket.py
에서 작성된 def check(m): ..
는 어떤 상황에서 사용되는 걸까?
check
함수는 위 공식 문서 예제 코드에서도 나와있듯이
discord.py
의 wait_for
메소드에서 사용된다.
이벤트 필터링 역할을 한다고 보면 될 것 같다.
check()
함수는 조건부로 이벤트 처리할 때 유용하다.
예를 들어, 특정 사용자가 특정 채널에서 메시지를 보낼 때만 작업을 수행하도록 설정할 수 있다.
@bot.event
async def on_message(message):
if message.content == "!start":
await message.channel.send("숫자를 입력해주세요.")
def check(msg):
return msg.author == message.author and msg.channel == message.channel and msg.content.isdigit()
try:
response = await bot.wait_for("message", check=check, timeout=30.0)
await message.channel.send(f"입력된 숫자는 {response.content}입니다.")
except asyncio.TimeoutError:
await message.channel.send("시간 초과되었습니다. 다시 시도해주세요.")
해당 예제는 사용자가 !start
명령을 입력하면, 숫자 입력을 대기하고, 숫자를 입력하지 않거나 시간이 초과되면 오류 메시지를 반환한다.
4. Cog 등록
마지막으로 Cog를 등록해주기 위해서 ticket.py
에 다음과 같은 코드를 추가해주자.
class Ticket(commands.Cog):
def __init__(self, bot: commands.Bot):
super().__init__()
self.bot = bot
@commands.Cog.listener()
async def on_ready(self) -> None:
print(f"Ticket Command is running ...")
channel_id: int = 0000
channel = self.bot.get_channel(channel_id)
if channel:
embed = discord.Embed(
title="고객센터 안내",
description=(
"- 고객센터 운영 시간은 정해져 있지 않습니다\n"
"- 생성된 채널은 3분 동안 아무런 문의가 없는 경우 무통보 종료됩니다.\n"
"- 문의와 건의가 아닌 폭언과 비아냥은 무통보 차단됩니다.\n"
),
color=discord.Color.blue(),
)
view = TicketView(bot=self.bot)
await channel.send(embed=embed, view=view)
# Cog 등록 함수
async def setup(bot: commands.Bot):
await bot.add_cog(Ticket(bot=bot))
Ticket Cog가 동작할 때 channel_id
에 명시된 채널로 버튼이 생성될 것이다.
트러블슈팅
버튼 클릭 시 타임아웃(timeout) 문제
discord.py
에서 discord.ui.View
를 사용하여 생성된 버튼의 기본 수명은
디폴트로 3분(180초) 이다.
그로 인해서 디스코드 봇을 실행하고,
일정 시간이 지난 다음 버튼을 클릭하면 다음과 같이 "상호작용 실패" 라는 에러가 발생하게 된다.
관련된 오류 내용은 https://stackoverflow.com/questions/72312099/discord-py-button-responses-interaction-failed-after-a-certain-time에서 자세하게 확인이 가능하다.
1. timeout 설정 늘리기
discord.ui.View
를 생성할 때 timeout
값을 명시적으로 설정해 타임아웃을 해제하거나 연장할 수 있다.
import discord
from discord.ext import commands
class MyView(discord.ui.View):
def __init__(self, timeout=600): # 10분 (600초)으로 설정
super().__init__(timeout=timeout)
bot = commands.Bot(command_prefix="!")
@bot.command()
async def button(ctx):
view = MyView()
button = discord.ui.Button(label="클릭!", style=discord.ButtonStyle.green)
async def button_callback(interaction):
await interaction.response.send_message("버튼 눌렀음!")
button.callback = button_callback
view.add_item(button)
await ctx.send("버튼1", view=view)
bot.run("디스코드봇토큰입력")
이렇게 하면 버튼의 수명이 timeout
값에 따라 늘어나거나 해제된다.
2. View 재등록
타임아웃 이후에도 버튼을 계속 사용할 수 있도록, 새로운 View를 만들어 메시지에 업데이트하거나 버튼을 다시 등록해야 한다.
특정 시간을 간격으로 View를 재등록한다면 다음과 같은 코드를 작성해볼 수 있다.
@bot.command()
async def persistent_button(ctx):
view = MyView()
async def update_view():
while True:
await asyncio.sleep(300) # 5분마다 View를 새로고침
await ctx.edit(view=MyView())
button = discord.ui.Button(label="지속 버튼", style=discord.ButtonStyle.blurple)
async def button_callback(interaction):
await interaction.response.send_message("지속 버튼이 눌렸습니다!")
button.callback = button_callback
view.add_item(button)
await ctx.send("이 버튼은 지속적으로 작동합니다!", view=view)
bot.loop.create_task(update_view())
3. Persistent View 사용
만약 봇이 재시작되어도 버튼이 되기를 원한다면 Discord의 Persistent View
기능을 사용하면 된다.
이를 위해서는 버튼에 다음과 같이 custom_id
를 설정해야 한다.
class PersistentView(discord.ui.View):
def __init__(self):
super().__init__(timeout=None) # 타임아웃 없음
@discord.ui.button(label="Persistent Button", style=discord.ButtonStyle.green, custom_id="persistent_button")
async def persistent_button(self, button: discord.ui.Button, interaction: discord.Interaction):
await interaction.response.send_message("이 버튼은 재시작 후에도 유지됩니다!", ephemeral=True)
@bot.event
async def on_ready():
# 봇이 시작되면 View를 등록
bot.add_view(PersistentView())
print(f"Logged in as {bot.user}!")
채널 ID 확인 방법
Discord 채널 ID는 디스코드 서버 각 채널에 할당되는 고유 식별값이다.
Discord를 이용하는데에는 채널 ID를 몰라도 문제될 게 없지만,
다양한 API 사용이나 명령, Bot 활용 등을 위해서는 필요할 수 있다.
해당 챕터에서는 디스코드에서 채널 ID를 찾는 방법에 대해서 간단하게 설명해보겠다.
1. 개발자 모드 활성화
먼저 개발자 모드가 활성화되어 있는지 확인이 필요하다. 사용자 설정을 클릭해보자.
비활성화 상태라면 고급 -> 개발자 모드를 클릭하여 활성화해주자.
2. 채널 ID 복사
다시 서버로 돌아와서 원하는 채널을 마우스 우클릭 후 메뉴에서 "ID 복사하기"를 클릭하자. 이제 필요한 곳에 붙여넣기를 하면된다.