개발 강좌/기타 강좌

디스코드봇(Components) - 02ㅣComponents를 통하여 버튼을 만들어보자!

건유1019 2021. 6. 1. 02:42

안녕하세요. 이번 강의에서는 디스코드 봇(Components)을 통하여 버튼을 만들어 보도록 하겠습니다!

우선 오늘 배우는 것은 discord.py를 사용할 거지만, discord.py를 굳이 사용하지 않고, aiohttp나 requests 등을 통하여 HTTP 세션을 보낼 수 있다면, 충분히 가능합니다

 

우선 이번 강좌도 저번 강좌에 예고했듯이 난이도가 있는 편입니다. 본인이 "네트워크에 대한 지식이 없다", "프로그래밍 언어에 대한 기초 지식이 없다.", "디스코드 봇을 모른다" 그러면 많은 어려움이 따를 수 있습니다. 최소한 네트워크 지식을 알고 하시는 것을 추천합니다.

 

우선 Components를 활용한 버튼을 만드는 작업은 정식적으로 discord.py에서 지원해주지 않기 때문에 API를 통해야 합니다. 우선 Discord API 사용법에 대하여 알아봅시다.


Discord API 사용법?

우선 우리가 알아야 할 부분에 대해서만 집중적으로 다루겠습니다. 

Discord API를 다루기 위해선 HTTP 세션을 Discord에 보내주어야만 합니다.

 

Discord API 주소는 <BASE URL>/v <버전>/<path>로 구성되어 있습니다.

 

우선 Discord API의 BASE URL는 "https://discord.com/api"입니다.

버전은 Components는 최근에 추가되었기 때문에 9 버전을 택하도록 하겠습니다.

따라서 디스코드 API를 사용하기 위해선 앞에 "https://discord.com/api/v9"을 입력해야 합니다.

 

다음은 우리는 무슨 path를 보낼지 정해야 합니다.

우리는 메시지를 보낼 것이기 때문에 https://discord.com/developers/docs/resources/channel#create-message를 참고해서 사용해야만 합니다.

내용이 많이 복잡하지만 천천히 읽으면 모두 이해할 수 있는 사항입니다.

method는 POST 형태로, path는 "/channels/{채널 ID}/messages"를 통하여 메시지를 보낼 수 있다고 기재되어 있습니다.

 

Limitations(한계)에서는 아래와 같이 분석할 수 있습니다.

  • 보낼 봇이 "메시지 보내기" 권한이 필요합니다.
  • TTS 메시지를 보낼 경우에는 "TTS 메시지 보내기" 권한이 필요합니다.
  • 회신(답변)을 하기 위해서는 "메시지 역사 보기" 권한이 필요합니다.
  • 메시지의 최대 전송 크기는 8MB로 제한됩니다.
  • 임베드를 설정할 때에는 type에 rich 값을 안 주어도 rich 값으로 자동으로 설정합니다.
  • 파일은 "multipart/form-data" 형태의 데이터 타입만 수신할 수 있습니다.

그리고 최소 파일, 메시지, 임베드(embed)를 하나 이상 제공해야 한다고도 하네요.

 

다음은 필요한 Params를 알아보도록 합시다. 위에 있는 표를 참고하면 됩니다. 위에 있는 표를 아래에 번역해 두도록 하겠습니다.

필드(키) 타입 설명 필수 여부
content string(문자열) 메시지 내용 (최대 2000 자) content, tts, file 중 하나는 필수로 작성해야합니다.
tts boolean TTS 활성화 여부 기본값이 True로 설정되며, 필수가 아닙니다.
file file contents 파일 콘텐츠 content, tts, file 중 하나는 필수로 작성해야합니다.
embed embed 모델 임베드 콘텐츠
payload_json string (문자열) 파일이 아닌 매개 변수의 json 인코딩 본문 multipart/form-data만 받을 수 있습니다
allowed_mentions allowed_mentions 모델 맨션 허가 여부  필수가 아닙니다.
message_reference message_reference 모델 답장(회신)을 하기위한 메시지 콘텐츠 필수가 아닙니다.
components components 모델 components 내용 필수가 아닙니다.

문서 내에 일부 누락되어 있지만, "components"에 대한 내용을 추가하였습니다.

 

따라서, Postman 등을 통하여 직접 보낼 때에는 아래처럼 보내야 합니다.

물론 그대로 보내게 된다면, 아래의 에러가 리턴될 겁니다.

{
    "message": "401: Unauthorized",
    "code": 0
}

이 문제는 토큰 값이 저장되어 있지 않기 때문에 사용자가 누군지 확인할 수 없어서 발생한 현상입니다.

그래서 우리는 우리가 누구인지 증명을 해줘야 합니다. 따라서 Headers 값에 "authorization"를 추가해줘야 합니다.

따라서 봇을 통하여 사용할 때에는 저렇게 작성해주시고 <token> 값에는 디스코드 봇의 토큰 값을 작성해줍니다.

(저기에 들어가는 값에 따라 유저 봇이라는 사용자가 봇이 되는 것도 가능하지만, 이용약관에 위배되는 사항이므로 다루지는 않겠습니다)

 

사실 이 과정은 discord.py가 없어도, 충분히 가능한 사항입니다. 다음은 discord.py를 통하여 메시지를 출력하는 방법을 알려드리겠습니다.


discord.py를 통하여 "직접 메시지"를 보내기

discord.py를 통하여 메시지를 보낸다고 하신 다면 어떤 사람들은 아래처럼 보내면 될 거라고도 합니다.

@bot.event
async def on_message(msg):
	await msg.channel.send("블라블라~")
    
@command.commands(name="명령어")
async def command(ctx):
	await ctx.send("블라블라~~")

그러나 최근에 지원을 하기 시작한 Components는 위 방법을 통하여 보내 줄 수 없기 때문에 "직접" 보내줘야 합니다.

 

우선 이 과정을 알기 위해서는 discord.py를 뜯어볼 필요가 있다고 봅니다. 우리는 우리의 목표를 충족하기 위해서 우리가 사용하는 코드의 일부를 보고 분석해야 할 능력이 있어야 합니다. 

따라서 discord.py가 배포 중인 "https://github.com/Rapptz/discord.py"를 가 보록 합시다.

우리가 저렇게 ~. send()를 통하여 보낼 때 discord.py에는 아래의 함수가 실행되어 보내집니다.

discord/abc.py의 1172줄

이 함수를 집중적으로 분석해 봅시다. 참고로 views라는 매개변수는 아직 정식으로 추가된 것은 아니지만 Components와 관련된 매개변수라는 점을 또 알 수 있습니다. (아직 discord.py v1.7을 사용하는 개발자는 사용할 수 없습니다.)

 

코드를 분석해보자면 아래의 부분을 발견하게 됩니다.

이 부분은 파일을 제외한 tts, content, embed 등이 이곳을 통하여 보내진 다는 것을 알 수 있습니다. 저 함수의 위치를 다시 찾게 된다면 아래에 있다는 것을 알게 됩니다.

discord/http.py의 349줄

우리는 Route라는 함수에 위에 다루었던 method와 path를 볼 수 있습니다. 그렇습니다. 사실 discord.py도 Discord API를 거치고 있는 겁니다. 따라서 우리는 http.py에 있는 Route를 꺼내서, path와 method를 지정한 뒤에 HTTP 클래스에 있는 request 함수를 통하여 보내야 합니다.

 

HTTP 클래스는 Client 또는 Cog(Bot)을 사용하든 아래의 방법으로 꺼낼 수 있습니다.

http = Client.http # Client를 사용할 때, 대부분 Client이라는 변수를 사용한다는 전재
http = Bot.http # Cog를 사용할 때, 대부분 Bot이라는 변수를 사용한다는 전재

위에서 서술한 헤더(Headers)와 Base URI, 버전은 discord.py에서 자동으로 설정해주기 때문에, 굳이 신경을 쓰실 필요는 없습니다.


 

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
    if msg.content == "!프로그래밍":
    	return

우선 저는 discord.py에 있는 Client를 기준으로 서술해보겠습니다. cog도 이번에는 과정이 동일하기 때문에 충분히 따라오시면 가능할 겁니다.

 

우선 discord를 불러옵니다. 그리고 Route 클래스는 discord에 기본 모듈에는 없기 때문에 "직접" 불러와야 합니다.

다음은 우리는 Route를 불러와야 합니다.

r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
    if msg.content == "!프로그래밍":
    	r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
    	return

r값에 Route에서 불러온 클래스를 넣어 주었습니다.

우리는 시험 삼아 "파이썬 최고!"라는 메시지를 보내도록 하겠습니다.

 

위에 설명했던 content라는 키를 사용하여 보내게 됩니다. 파이썬에서는 params 데이터는 대부분 dict를 통하여 전송할 수 있습니다.

payload = {
	"content": "파이썬 최고!"
}
import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
    if msg.content == "!프로그래밍":
    	r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
        payload = {
			"content": "파이썬 최고!"
		}
    	return

이제 우리는 이 메시지를 보내주어야만 합니다. 위에서 불러온 http 변수에 있는 request 함수를 통하여 가능하다고 했기 때문에 아래의 코드를 추가해주면, 우리는 ~. send()를 직접 구현한 것입니다.

http.request(r, json=payload)
import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
    if msg.content == "!프로그래밍":
    	r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
        payload = {
			"content": "파이썬 최고!"
		}
        http.request(r, json=payload)
    	return

이렇게 discord.py를 통하여 메시지를 "직접" 보내는 방법에 대하여 알아보았습니다.


Components 사용법에 대하여 알아보자

다음은 Components를 보내보도록 합시다. 위 코드를 이어서 설명해보도록 하겠습니다.

우리는 Components를 사용하기 전에 사용 방법을 알아야 합니다.

https://discord.com/developers/docs/interactions/message-components를 통하여 Components 사용법을 알 수 있으며, (영어를 잘해야 되는 이유) 영어를 하실 줄 아신다면 충분히 저 내용만으로 만 이해하실 수 있을 겁니다.

 

우선 위에 있던 components라는 키값 안에 저 내용이 들어가야 하는 것이며, content에 있던 것처럼 넣어주시면 됩니다.

{
    "content": "This is a message with components",
    "components": [
        {
            "type": 1,
            "components": []
        }
    ]
}

문서 내에 있는 예제 문처럼 보내면 됩니다. 이렇게 하시면 components는 성공적으로 불러왔습니다.

이제 버튼을 만들어봐야 합니다.

버튼은 components안에 있는 components에 만들어야 합니다.

 

여기서 우리는 아래 서술되어 있는 components 모델에 대하여 확인해보도록 하겠습니다.

위 내용은 영어로 되어 있기 때문에, 아래에 표로 번역해두겠습니다.

필드(키) 타입 설명
type int(정수형) 컴포넌트 타입(아래 서술)
style int(정수형) 스타일 타입(아래 서술)
lable string(문자열) 내용(아래 서술)
emoji 이모지 모델 이모지 콘텐츠
custom_id string(문자열)  ID
url string(문자열)  주소(아래 서술)
disabled boolean 비활성화 여부

여기서 type은 필수로 사용해야 하며, style, label, custom_id는 버튼을 생성하기 위해 필수로 사용해야 하는 것들입니다.

 

그리고 위에 작성되어 있는 컴포넌트 타입은 위와 같습니다.

필드(키) 이름 설명
1 ActionRow 컴포넌트 가로 줄
2 버튼 버튼

따라서 type값을 2로 지정하셔야 버튼을 사용할 수 있습니다.

 

다음은 style에 대하여 서술해보겠습니다.

스타일은 아래와 같이 Primary-CTA(파란색), Primary-success(초록색), Secondary, Destructve(빨간색), Link로 구성할 수 있으며, 1~5의 값을 대입합니다. 따라서 Primary-CTA(파란색) 버튼을 만들고 싶다면, 1이란 값을 style에 넣어주시면 됩니다.

 

추가로 5번(Link 버튼)을 만들 때에는 위에 서술되어 있는 url 값을 무조건 넣어줘야 합니다.

disabled 키값에 True(참)을 넣게 될 경우 위 사진에 있는 것처럼 되며, 버튼을 누를 수 없도록 설정됩니다.

 

버튼을 넣어주게 된다면, 아래의 예시 코드가 구성될 것입니다.

{
    "content": "This is a message with components",
    "components": [
        {
            "type": 1,
            "components": [
                {
                    "type": 2,
                    "label": "버튼",
                    "style": 1,
                    "custom_id": "<찾을 버튼의 ID>"
                }
            ]

        }
    ]
}

그리고 버튼을 여러 개 만드신다면, components라는 리스트('[]') 안에 계속해서 넣어주시면 됩니다.

주의하실 점은 저 리스트 안에는 최대 5개의 버튼밖에 들어갈 수 없습니다. 따라서 버튼을 6개 이상을 한 줄에 넣을 수는 없다는 소리입니다.


지금까지 배운 것을 응용해봅시다!

이제 우리가 지금까지 알아온 위 내용을 토대로 1편에 있는 버튼을 만들어 보도록 합시다.

우리가 만들어 볼 디스코드 봇은 이렇습니다. embed안에 Python, Kotlin, C언어, C++, Java 중 최고의 프로그래밍 언어를 선정하는 프로그램입니다.

 

embed(임베드)의 title(주제) 값은 "최고의 프로그래밍 언어"라는 것을 알 수 있습니다.

 

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		return

우리는 위에 있는 코드가 있는 상태에서 다시 시작해보도록 하겠습니다.

 

기본적으로 Route와 http.requests를 설정해주신 뒤에, 위에서 서술했듯이 content, embed, file값은 무조건 하나 이상 포함하고 있어야 한다고 했기 때문에 우리는 embed값을 만들 어 줄 예정입니다.

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
		payload = {}
		http.request(r, json=payload)
		return

위 코드에서 위에서 다루었던, Route와 payload, http.requests를 모두 사용한다는 것을 알 수 있습니다.

embed는 discord.py에서 이미 지원하는 기능이기 때문에, 이미 지원하고 있는 embed를 통하여 만들어 보겠습니다.

 

embed = discord.Embed(
	title="최고의 프로그래밍 언어",
	description="""<:python:847876880257908757> Python: 0표
		<:kotlin:847876848662216714> Kotlin: 0표
		C언어: 0표
		<:cpp:847876987778629722> C++: 0표
		:java:847876915619954708> Java: 0표""",
	colour=0x0080ff
)
import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: 0표
				<:kotlin:847876848662216714> Kotlin: 0표
				C언어: 0표
				<:cpp:847876987778629722> C++: 0표
				<:java:847876915619954708> Java: 0표""",
			colour=0x0080ff
		)
        
		r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
		payload = {}
		http.request(r, json=payload)
		return

 

embed라는 이름에 저장된 embed 값은. to_dict()를 통하여 dict형태의 embed 문을 불러올 수 있습니다. 따라서 payload에 "embed"라는 키값으로 embed.to_dict()라는 값을 넣어줍니다.

 

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: 0표
				<:kotlin:847876848662216714> Kotlin: 0표
				C언어: 0표
				<:cpp:847876987778629722> C++: 0표
				<:java:847876915619954708> Java: 0표""",
			colour=0x0080ff
		)
        
		r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
		payload = {
			"embed": embed.to_dict()
		}
		http.request(r, json=payload)
		return

 

embed를 이렇게 해서 payload라는 곳에 성공적으로 넣을 수 있습니다. 다음은 components 작업을 이어서 하도록 하겠습니다.

 

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: 0표
				<:kotlin:847876848662216714> Kotlin: 0표
				C언어: 0표
				<:cpp:847876987778629722> C++: 0표
				<:java:847876915619954708> Java: 0표""",
			colour=0x0080ff
		)
        
		r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
		payload = {
			"embed": embed.to_dict()
			"components": default_components
		}
		http.request(r, json=payload)
		return

 

컴포넌트의 내용은 사실상 매우 길어질 수 있기 때문에 따로 default_components라는 변수 안에 만들어 넣겠습니다.

default_components 에는 우선 우리가 위해서 서술했듯 Action Raw형태의 컴포넌트를 미리 넣어줘야 합니다.

 

default_components = [
	{
		"type": 1,
		"components": []
	}
]

 

그다음은 components안에 버튼을 넣어줘야 합니다. 하나하나 차근식 넣어주시면 됩니다.

 

{
	"type": 2,
	"label": "Python",
	"style": 2,
	"custom_id": "python",
	"emoji": {
		"id": "847876880257908757",
		"name": "python"
	}
}

 

이렇게 생긴 것을 총 5개 넣어주시면 됩니다. custom_id는 추후 우리가 식별한 id값을 넣어주시면 되고, label 값 안에는 디스코드 버튼의 이름을 넣어 주시면 됩니다.

 

여기서 우리가 강의 중에서 서술되지 않은 "emoji"부분이 있는데요, emoji 모델은 id와 name으로 구성되어 있으며, 이모지의 ID값과 이모지의 이름을 통하여 불러올 수 있습니다.

 

저 문장을 반복해서 총 5개의 버튼을 만들어주면 아래의 코드가 탄생합니다.

 

default_components = [
    {
        "type": 1,
        "components": [
            {
                "type": 2,
                "label": "Python",
                "style": 2,
                "custom_id": "python",
                "emoji": {
                    "id": "847876880257908757",
                    "name": "python"
                }
            }, {
                "type": 2,
                "label": "Kotlin",
                "style": 2,
                "custom_id": "kotlin",
                "emoji": {
                    "id": "847876848662216714",
                    "name": "kotlin"
                }
            }, {
                "type": 2,
                "label": "C언어",
                "style": 2,
                "custom_id": "c"
            }, {
                "type": 2,
                "label": "C++",
                "style": 2,
                "custom_id": "cpp",
                "emoji": {
                    "id": "847876987778629722",
                    "name": "cpp"
                }
            }, {
                "type": 2,
                "label": "Java",
                "style": 2,
                "custom_id": "java",
                "emoji": {
                    "id": "847876915619954708",
                    "name": "java"
                }
            }
        ]

    }
]

 

이렇게 default_components의 변수를 만들어 주었습니다. 이제 메인코드에 삽입해줍니다.

 

import discord
from discord.http import Route

bot = discord.Client()
http = bot.http

default_components = [
    {
        "type": 1,
        "components": [
            {
                "type": 2,
                "label": "Python",
                "style": 2,
                "custom_id": "python",
                "emoji": {
                    "id": "847876880257908757",
                    "name": "python"
                }
            }, {
                "type": 2,
                "label": "Kotlin",
                "style": 2,
                "custom_id": "kotlin",
                "emoji": {
                    "id": "847876848662216714",
                    "name": "kotlin"
                }
            }, {
                "type": 2,
                "label": "C언어",
                "style": 2,
                "custom_id": "c"
            }, {
                "type": 2,
                "label": "C++",
                "style": 2,
                "custom_id": "cpp",
                "emoji": {
                    "id": "847876987778629722",
                    "name": "cpp"
                }
            }, {
                "type": 2,
                "label": "Java",
                "style": 2,
                "custom_id": "java",
                "emoji": {
                    "id": "847876915619954708",
                    "name": "java"
                }
            }
        ]

    }
]

@bot.event
async def on_message(msg: discord.Message):
	if msg.content == "!프로그래밍":
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: 0표
				<:kotlin:847876848662216714> Kotlin: 0표
				C언어: 0표
				<:cpp:847876987778629722> C++: 0표
				<:java:847876915619954708> Java: 0표""",
			colour=0x0080ff
		)
        
		r = Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id)
		payload = {
			"embed": embed.to_dict()
			"components": default_components
		}
		http.request(r, json=payload)
		return
       
bot.run("<토큰>")

 

이렇게 코드를 완성하였습니다. 아무래도 discord.py에서는 공식적으로 지원하는 것도 아니고, 직접 구현하다 보니 생각보다 난이도가 있었을 거라고 생각합니다.  그럼에도 불구하고 여기까지 따라오셨다면 나중에 discord.py를 안 써보고도 디스코드 봇을 만들 수 있다는 사람입니다. discord.py를 사용하지 않고, 디스코드 봇 만들기 꼭 한번 해보시는 것을 추천드립니다.

 


이렇게 2편을 끝내게 되었습니다. 사실은 총 3편의 구성으로 알차게 작성하려 했지만, 아직 다루어야 할 부분이 조금 남아있어서 4편으로 끝내게 될 것 같다고 봅니다. 이번 강의 내용이 많이 길었지만 잘 따라와 주셔서 감사합니다. 

 

다음 강의에서는 Components에 있는 버튼을 클릭하면 반응하도록 하는 부분에 대하여 작성해보도록 하겠습니다.

다시 한 번 긴 글을 읽어주신 여러분들 진심으로 감사합니다.