naroSEC
article thumbnail

들어가기 앞서

Discord에서는 봇 운영을 위한 API를 제공하며, 이를 쉽게 활용할 수 있도록 Python, JavaScript, Java 등 다양한 언어로 라이브러리가 개발되어 있다. 이번 포스팅에서는 그중에서도 Python에서 가장 널리 쓰이는 "discord" 라이브러리의 기본 사용법을 알아보려고 한다.


discord 라이브러리

Install

discord 라이브러리는 Python 내장 라이브러리가 아니므로, 별도의 설치가 필요하다. 관련 프로젝트는 아래의 레포지토리에서 확인할 수 있다.

 

GitHub - Rapptz/discord.py: An API wrapper for Discord written in Python.

An API wrapper for Discord written in Python. Contribute to Rapptz/discord.py development by creating an account on GitHub.

github.com

 

discord 라이브러리는 두 가지 설치 모드를 제공한다. 하나는 음성 기능을 지원 하지 않는 버전이고, 또 다른 하나는 음성 기능을 지원하는 버전이다. 여기서 말하는 음성 지원이란 디스코드 서버의 음성 채널 보이스 기능을 말한다.

만약, 본인이 디스코드 음악 재생, 라디오 등의 봇을 만들 예정이라면 음성 기능을 지원하는 버전을 설치해야 한다.

# 음성 지원X
pip install -U discord.py
# 음성 지원O
pip install -U discord.py[voice]

 

Manual

discord 라이브러리는 기본적으로 비동기 프로그래밍 방식으로 동작한다. 이전 버전에서는 동기 방식으로도 제공하였는데 효율적 작업 처리를 위해 비동기 방식으로 변경되었다.


비동기 방식이란 작업이 완료될 때까지 기다리지 않고, 동시에 여러 작업을 처리할 수 있게 해 주는 방식이다.

 

Base

아래 코드는 discord 라이브러리의 가장 기본적인 사용 방법으로, 데코레이터를 통한 이벤트 핸들러를 제공한다.

  • on_ready: 봇이 Discord에 성공적으로 연결되었을 때, 한 번 실행되는 이벤트 함수로, 봇의 초기 설정, 시작 메시지 출력, 특정 작업 시작 등을 설정할 때 사용한다.
  • on_connect: 봇이 Discord 서버에 연결될 때, 호출된다. on_ready와의 차이점은 준비되기 전에 호출될 수 있다. 주로, 연결 상태나 API 상태를 검사할 때 사용한다.
  • on_disconnect: 봇이 Dicord 서버와의 연결이 끊어졌을 때, 호출된다. 연결 문제를 추적하거나, 재연결 작업을 설정할 때 사용한다.
  • on_message: 서버에서 사용자가 메시지를 전송될 때 호출된다. 채널 내 봇이 대화에 반응할 때 사용된다.
import discord
import os
from pprint import pprint
from dotenv import load_dotenv

load_dotenv() # .env로 설정한 환경 변수를 불러온다.
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY") # 환경 변수로 설정한 Discord API KEY를 가져온다.
client = discord.Client(intents=discord.Intents.all())

@client.event
async def on_ready():
    pprint("Ready to connect to discord ")

@client.event
async def on_message(message):
    if message.author == client.user:
        return
        
    if message.content == "!hello" or message.content == "!hi":
        await message.channel.send("Hello!")

@client.event
async def on_connect():
    pprint("connect to discord")

@client.event
async def on_disconnect():
    pprint("disconnect to discord")

if __name__ == "__main__":
	# run() 인자로 Discord API KEY를 넘겨준다.
    client.run(DISCORD_API_KEY)

 

Command

discord 라이브러리에서 사용자가 입력한 메시지를 처리할 때, 주로 on_message() 이벤트 핸들러를 사용한다. 하지만 이 방법은 모든 메시지를 수신하여 처리해야 하므로, 정규 표현식을 이용한 필터 설정이 필요해 번거로울 수 있다. 이를 간소화하기 위해 라이브러리에서는 명령어만 수신 받는 command() 이벤트 핸들러를 제공한다.

command()를 사용하려면 봇 명령어를 처리할 수 있는 Bot 객체를 생성해야 하며, 이때 commands.when_mentioned_or("!") 함수를 사용해 기본 접두사를 설정하면 !명령어 형식으로 명령어를 수신 받을 수 있다.

 

명령어 이름은 함수 이름과 동일하다. 아래 코드를 예시로 들자면, @bot.command() 데코레이터로 등록된 test 함수가 명령어 이름이 되는 것이다. ex) !test

import discord
import os
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
# intents: 봇 권한 설정 all은 모든 권한을 부여한다.
intents = discord.Intents.all()

# command_prefix: 명령어 접두사 설정
bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "TEST Settings",
    intents = intents
)

@bot.command()
async def test(msg):
    await msg.send("TEST Message") # 봇이 서버에 메시지를 전송할 때, 사용된다.

bot.run(DISCORD_API_KEY, reconnect=True)

 

Command를 통한 채널 메시지 삭제 및 명령어 오류 처리

command 데코레이터로 등록된 함수 및 이벤트 핸들러들은 기본적으로 Context 인자를 넘겨 받는다. 해당 인자는 discord.ext.commands.Context 객체로, 명령어가 호출된 상황에 대한 다양한 정보를 포함하고 있으며, 이를 통해 명령어를 호출한 사용자, 메시지, 채널, 서버 등의 정보를 얻을 수 있다.

 

Context 객체의 channel.purge 함수는 요청된 채널의 모든 메시지를 삭제하는 기능을 수행한다. 또한, @commands.has_role() 데코레이터를 사용하면 특정 역할을 가진 사용자만 이 명령어를 사용할 수 있도록 설정할 수 있다.

※ 참고로, @bot.command()를 사용할 때 함수 이름을 그대로 명령어 이름으로 사용할 수도 있지만, 아래 코드처럼 name 인자를 통해 명령어 이름을 지정할 수도 있습니다.
import discord
import os
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
intents = discord.Intents.all()

bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "Bot TEST Settings",
    intents = intents
)

@bot.command(name="delete")
@commands.has_role("MessageManager")
async def delete_message(ctx):
    await ctx.channel.purge(limit=None)

@bot.event
async def on_command_error(ctx, error):
    pprint("Command error")

bot.run(DISCORD_API_KEY, reconnect=True)

 

discord.ext.commands.Context 객체의 주요 속성과 메서드

앞서 설명한바와 같이 Context 객체에는 디스코드 서버와 상호 작용하기 위한 여러 정보들을 담고 있다. 아래는 Context 객체의 주요 속성과 함수에 대한 설명이다.

@bot.command(name="TEST")
async def delete_message(ctx):
    ctx 객체의 주요 속성과 메서드
    ctx.message: 명령어를 호출한 메시지 객체로, 메시지 내용, 작성자 등 다양한 정보를 포함한다.
    ctx.author: 명령어를 호출한 사용자(작성자) 정보가 들어있는 Member 객체이다.
    ctx.channel: 명령어가 실행된 채널(텍스트 채널)에 대한 정보가 들어있는 TextChannel 객체이다.
    ctx.guild: 명령어가 호출된 서버에 대한 정보가 들어있는 Guild 객체로, DM에서 호출된 경우 None이 된다.
    ctx.send(): 현재 채널에 메시지를 보낼 수 있는 함수이다.
    ctx.reply(): 호출한 메시지에 답장 형태로 메시지를 보낼 수 있는 함수이다.
    ctx.command: 호출된 명령어 객체 자체를 포함하며, 명령어 이름이나 설명 등을 참조할 때 유용하다.

 

discord.Embed() 객체를 이요한 임베드 메시지 생성

discord.Embed() 객체는 Discord 메시지에 다양한 정보를 포함할 수 있는 임베드 메시지를 생성하는데 사용된다.

[그림 1]과 같은 메시지 형태를 임베드 메시지라고 하며, 이미지 및 영상 첨부도 가능하다.

[그림 1] 임베드 메시지 예시

 

discord.Embed() 객체는 여러 가지 속성을 통해 임베드 메시지를 커스터마이징할 수 있으며, 주요 속성은 아래와 같다.

  • title: 임베드의 제목을 설정한다.
  • description: 임베드의 본문 내용을 설정한다.
  • color: 임베드의 왼쪽 테두리 색상을 설정한다.(discord.Color를 사용하여 색상을 지정할 수 있다.)
  • url: 임베드 제목을 클릭했을 때 이동할 URL을 설정한다.
  • timestamp: 임베드에 현재 시간을 추가할 수 있다. 일반적으로 메시지의 생성 시간을 표시하는 데 사용된다.
  • footer: 임베드의 하단에 표시될 텍스트와 아이콘을 설정한다.
    • text: 하단 텍스트
    • icon_url: 아이콘 이미지 URL
  • author: 임베드의 작성자 정보를 설정합니다.
    • name: 작성자 이름
    • url: 작성자 이름 클릭 시 이동할 URL
    • icon_url: 작성자 아이콘 이미지 URL
  • fields: 여러 개의 필드를 추가하여 정보를 정리할 수 있으며, 각 필드는 제목과 내용을 가질 수 있다.
    • inline: 인라인 설정을 적용(True) 하거나 미적용(False) 할 수 있다.
import discord
import os
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
intents = discord.Intents.all()

bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "Bot TEST Settings",
    intents = intents
)

@bot.command(name="em")
async def embed_test(ctx):
    embed = discord.Embed(
        title="Embed 제목",
        description="Embed 설명",
        color=discord.Color.blue(),
    )

    embed.add_field(name="필드1", value="필드 1의 값", inline=False)
    embed.add_field(name="필드2", value="필드 2의 값", inline=True)
    embed.add_field(name="필드3", value="필드 3의 값", inline=True)
    embed.set_thumbnail(url="이미지 주소")
    embed.set_image(url="이미지 주소")
    embed.set_footer(text="꼬리말", icon_url="이미지 주소")
    embed.set_author(name="작성자", url="개인 도메인 주소", icon_url="이미지 주소")
    embed.timestamp = ctx.message.created_at
	
    # 임베드 메시지 전송 시 embed 인자로 생성한 Embed 객체를 넘겨주면 된다.
    await ctx.send(embed=embed)

bot.run(DISCORD_API_KEY, reconnect=True)

 

dicord.ui.View, dicord.ui.Button을 통한 버튼 인터페이스 추가하기

discord.ui.View와 discord.ui.Button은 discord.py의 UI 모듈을 통해 디스코드 봇에 인터랙티브한 사용자 인터페이스 요소를 추가하는데 사용된다. 이 기능을 통해 사용자는 버튼을 클릭하거나 선택할 수 있으며, 봇은 이에 대한 반응을 처리할 수 있다.

[그림 2]는 discord.ui.Button 객체로 구현한 버튼이다.

[그림 2] 디스코드 버튼 예시

discord.ui.View는 여러 UI 요소(예: 버튼, 선택 메뉴 등)를 그룹화하여 디스코드 메시지와 함께 표시하는 클래스로, 사용자가 버튼을 클릭할 때 어떤 행동을 취하도록 하는 이벤트 핸들러를 설정할 수 있다.

 

사용 방법은 간단하다. 먼저, View 객체를 생성한 후, 버튼과 같은 UI 요소를 추가하고, 이를 send 함수의 view 인자 값으로 함께 전송하면 된다. 

아래 예제 코드에서는 MyView 클래스가 discord.ui.View를 상속 받도록 정의했으며, @discord.ui.button 데코레이터를 통해 버튼 객체를 생성할 수 있다.

또한, 데코레이터로 등록된 핸들러 함수는 버튼이 클릭될 때 호출되며, 여기서는 사용자가 버튼 클릭 시 "Button clicked!"라는 메시지를 전송하게 된다.

 

아래는 discord.ui.Button 객체의 주요 속성이다.

  • label: 버튼에 표시될 텍스트이다.
  • style: 버튼의 스타일을 설정한다. ButtonStyle을 사용하여 다양한 스타일을 적용할 수 있다.(예: primary, secondary, success, danger 등).
  • emoji: 이모지를 설정할 수 있다.
  • custom_id: 버튼의 고유 ID로, 특정 버튼을 식별하는데 사용된다.
import discord
import os
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
intents = discord.Intents.all()

bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "Bot TEST Settings",
    intents = intents
)

# View 클래스를 상속 받아 사용할 수도 있으며, view = discord.ui.View()와 같이 직접 객체를 생성해도 무방하다.
class MyView(discord.ui.View):
    @discord.ui.button(label="Click Me!", style=discord.ButtonStyle.primary, emoji="", custom_id="btn_1")
    async def button_callback(self, button, interaction): # 생성한 버튼을 클릭했을 때, 호출할 함수를 지정할 수 있다.
        await interaction.response.send_message("Button clicked!")

@bot.command(name="button")
async def interactive(ctx):
    view = MyView()
    await ctx.send("Click the button below:", view=view)

bot.run(DISCORD_API_KEY, reconnect=True)

 

Cogs를 이용한 명령어 모듈화

cogs는 봇의 명령어와 이벤트를 여러 파일로 분리하여 관리할 수 있게 해 주는 모듈화 기능이다. cogs를 사용하면 코드를 더 구조적으로 구성하고, 특정 기능을 쉽게 추가, 수정 또는 제거할 수 있다. 대다수 봇 개발자들은 cogs 기능을 이용하여 코드를 관리하다.

 

cogs 기능을 사용하려면 별도의 디렉터리를 만들어 해당 디렉터리 내에 Python 스크립트 파일을 생성해주면 된다. 이렇게 생성된 파일을 Cog  파일이라고 부른다.

 

아래의 예제 코드는 "현재 경로/cogs/hello.py"로 생성한 스크립트 파일이다. 코드를 보면 알 수 있듯이 Cog  파일은 모두 class 작성되어야 하며, setup() 함수도 반드시 설정해줘야 한다.

 

setup() 함수는 메인 스크립트 파일에서 Cog 파일을 로드할 때, 자동으로 후출되는 함수로 Cog 클래스의 인스턴스를 생성하고, 이를 봇에 추가하여 Cog의 명령어 및 이벤트를 사용할 수 있도록 해준다.

import discord
from discord.ext import commands

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

    @commands.command(name="hello", aliases=["하이", "헬로우"], help="봇이 인사합니다.")
    async def hello_command(self, ctx):
        await ctx.send("Hello, world!")

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

 

아래 예제 코드는 앞서 작성한 Cog 파일을 로드하는 메인 스크립트이다. Cog 파일을 불러올 때는 Bot 객체의 load_extension() 함수를 사용해야 한다. 이때 주의할 점은 파일을 불러올 때 확장자를 제외하고, "cogs."라는 접두사를 추가해야 한다는 것이다.

 

더불어, Cog 파일은 봇이 실행되기 이전에 로드되어야 한다. 때문에, 아래 예제에서는 별도의 main() 함수를 생성하여, Cog 파일을 먼저 불러오고 이후에 봇이 실행 될 수 있도록 작성했다.

 

또한, 예제에서 작성된 load_cogs()와 bot.start() 함수는 모두 비동기 방식으로 동작하기 때문에, main() 함수를 호출할 때도 비동기 방식으로 작성해야 한다. 따라서 main() 함수를 호출하기 위해 별도로 asyncio 라이브러리를 사용했다.

import discord
import os
import asyncio
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
intents = discord.Intents.all()

bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "Bot TEST Settings",
    intents = intents,
    help_command=None
)

async def load_cogs():
    current_dir_path = os.path.dirname(__file__)
    for filename in os.listdir(f"{current_dir_path}/cogs"):
        if filename.endswith('.py'):
            pprint(f"Loading extension: {filename}")
            # cog 파일을 로드할 때는 확장자를 제외하고 파일 이름 앞에 cogs. 을 붙여줘야 한다.
            await bot.load_extension(f'cogs.{filename[:-3]}')

async def main():
    await load_cogs()
    await bot.start(DISCORD_API_KEY, reconnect=True)

asyncio.run(main())

 

asyncio 라이브러리를 사용하고 싶지 않은 경우에는 아래 예제와 같이 on_ready() 이벤트 핸들러가 호출될 때, Cog 파일을 불러오면 된다.

import discord
import os
from discord.ext import commands
from pprint import pprint
from dotenv import load_dotenv

load_dotenv()
DISCORD_API_KEY = os.getenv("DISCORD_API_KEY")
intents = discord.Intents.all()

bot = commands.Bot(
    command_prefix = commands.when_mentioned_or("!"),
    description = "Bot TEST Settings",
    intents = intents,
    help_command=None
)

@bot.event
async def on_ready():
    pprint("Ready to connect to discord")
    await load_extensions()

async def load_extensions():
    current_dir_path = os.path.dirname(__file__)
    for filename in os.listdir(f"{current_dir_path}/cogs"):
        if filename.endswith('.py'):
            pprint(f"Loading extension: {filename}")
            await bot.load_extension(f'cogs.{filename[:-3]}')

if __name__ == '__main__':
	bot.run(DISCORD_API_KEY, reconnect=True)

 

profile

naroSEC

@naroSEC

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...