개요
서버 멤버들이 직접 문의를 남길 수 있는 티켓 기반의 문의 기능을 구현해보는 과정을 기록해보려고 한다.
이번 글에서 구현해보려는 봇의 기능은 다음과 같다.
- 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):
super().__init__(timeout=None)
self.add_item(Button(label="문의", style=discord.ButtonStyle.green, custom_id="create_ticket_inquiry"))
async def create_ticket(self, interaction: discord.Interaction):
"""
이 메소드는 사용자가 버튼을 클릭했을 때 호출되며, 새로운 티켓 채널을 생성하고 초기 설정을 수행한다..
1. 티켓 채널의 읽기/쓰기 권한을 설정한다.
2. 사용자가 이미 티켓을 생성한 경우 이를 방지한다.
3. 생성된 티켓 채널에 초기 안내 메시지를 보낸다.
"""
guild = interaction.guild
user = interaction.user
if user.id in ticket_channels:
await interaction.response.send_message("이미 티켓이 열려 있습니다!", ephemeral=True)
return
overwrites = {
guild.default_role: discord.PermissionOverwrite(read_messages=False),
user: discord.PermissionOverwrite(read_messages=True, send_messages=True),
}
# "고객센터" 객체를 가져온다.
category = discord.utils.get(guild.categories, name="고객센터")
if not category: # 만약 고객센터 카테고리가 없는 경우 새로 생성한다.
category = await guild.create_category("고객센터")
channel = await guild.create_text_channel(name=f"ticket-{user.name}", category=category, overwrites=overwrites)
ticket_channels[user.id] = channel.id
await interaction.response.send_message(f"티켓이 생성되었습니다: {channel.mention}", ephemeral=True)
await channel.send(f"{user.mention} 님의 티켓입니다. 문제를 설명해주세요. 3분 동안 활동이 없으면 자동 종료됩니다.")
await self.start_inactivity_timer(channel, user)
async def start_inactivity_timer(self, channel, user):
"""
이 메소드는 생성된 티켓 채널에서 활동을 모니터링하고, 3분 동안 사용자의 메시지가 없으면 채널을 삭제하도록 한다..
내부적으로 check 함수를 사용하여 메시지가 다음 조건을 만족하는지 확인합니다:
1. 메시지가 해당 티켓 채널에서 작성되었는가?
2. 메시지 작성자가 티켓을 생성한 사용자와 동일한가?
"""
try:
def check(m):
# 이 함수는 특정 조건을 확인하는 데 사용됩니다.
# 조건:
# 1. 메시지가 티켓 채널에서 작성되었는가?
# 2. 메시지 작성자가 티켓을 생성한 사용자와 동일한가?
return m.channel == channel and m.author == user
await bot.wait_for("message", check=check, timeout=180.0)
except asyncio.TimeoutError:
await channel.send("활동이 없어 티켓이 종료됩니다.")
del ticket_channels[user.id]
await channel.delete()
Button View를 생성하는 TicketView
클래스를 작성해주었다.
메소드별 동작 배경은 주석으로 처리해두었으니 참고해보면 될 것 같다.
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
명령을 입력하면, 숫자 입력을 대기하고, 숫자를 입력하지 않거나 시간이 초과되면 오류 메시지를 반환한다.
마지막으로 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
에 명시된 채널로 버튼이 생성될 것이다.