개발 강좌/기타 강좌

디스코드봇(Components) - 03ㅣComponents를 상호작용해보자.

건유1019 2021. 6. 13. 03:40

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

저번 강의가 생각보다 내용이 많았던 것 같았어요. 사실 이번 강의는 2편에 넣으려고 했던 내용이었지만, 내용이 길어지다 보니 어쩔 수 없이 분리하게 됐습니다. 그래서 그렇게 저번처럼 매우 길어지지는 않을 거라고 봅니다. 저번 강의에서는 디스코드 봇(Componnents)을 통하여 버튼을 만들어 보았더라면, 이번 강의에서는 버튼을 클릭한 여부, 즉 상호작용을 해보도록 하겠습니다.

 

이번 강의는 discord.py(Python)을 기준으로 강의를 진행할 예정이며, discord.py(v1.7 기준) 내에는 기본적으로 지원하지 않기 때문에, 저희는 discord.py 내에 있는 웹소켓을 받아서 직접 상호작용을 진행할 예정입니다.

 


우리는 디스코드의 통신 방법을 알아야 합니다.

우리는 지금까지 on_message 등의 다양한 이벤트 핸들러와, cogs를 통하여 메시지를 받고자 하였습니다.

그러나, 우리는 지금까지 on_message와 cogs의 수신 방법을 아시는 분들도 있겠지만, 모르는 사람이 대부분 일 겁니다.

 

discord.py 내에서는 사실 간단한 코드 일지라도 많은 처리 프로세스가 동작하고 있는 것 다들 알고 계셨나요?

Discord API에서는 어떠한 이벤트가 발생했을 때, 웹소켓 통신 방법을 활용하여 아래와 비슷한 코드를 리턴합니다.

{
  t: 'GUILD_MEMBERS_CHUNK',
  s: 3,
  op: 0,
  d: {
    not_found: [ 123123 ],
    members: [],
    guild_id: '308994132968210433'
  }
}

우선 저 위 내용과 관련된 내용은 아래의 Discord API 문서에서 확인하실 수 있습니다.

https://discord.com/developers/docs/topics/gateway

필드(키) 타입 설명
op int(정수형) opcode에 대한 값
d mixed(혼합형, JSON) 이벤트 데이터
s int(정수형) 세션 및 하트 비트 재개에 사용되는 시퀀스 번호
t string(문자열) 페이로드의 이벤트 이름

위 정보를 파악한다면, 't'라는 곳에는 이벤트 이름이 들어가 있으며, 'd'에는 관련된 데이터 값이 들어 있다는 것을 알 수 있습니다.

 

이로써 discord.py가 처리하기 전에 받는 데이터를 대하여 분석해보았습니다.

discord.py 안에서는 socket_response라는 이벤트 함수를 통하여 Discord API와 서로 간 상호작용하는 내용 중 Discord API가 발신하는 되는 모든 정보를 읽어올 수 있습니다.

async def on_socket_response(payload):
	print(payload)

위 함수를 추가한다면, 웹소켓을 통하여 받는 모든 내용을 읽을 수 있습니다. 위 소스를 통하여 불러온 payload를 중에서 우리가 익숙한 두 가지를 분석해보고자 합니다.

+ 추가 (2022-02-18)
discord.py v2.0 이상의 버전에서는 더 이상 "on_socket_response" 이벤트를 통하여 상호작용하는 내역을 수집할 수 없습니다. 따라서 아래의 과정을 추가함으로써, discord.py v2.0 이상의 버전에서도 디스코드와 상호작용한 값을 수집할 수 있습니다.

1. Client에 enable_debug_events 인자를 True 값으로 설정합니다.
client = discord.Client(enable_debug_events=True)​

이 방법은 discord.AutoShardedClient, command.Bot, command.AutoShardedBot에도 동일하게 사용됩니다.


2. "on_socket_response" 대신 "on_socket_raw_receive"를 사용합니다.
대신 해당 함수를 사용하게 되면, 압축 상태의 값이 들어오기 때문에, 압축 해체(decompress) 과정이 필요합니다.
Discord 공식 문서(#)에서도 서술되어 있으며, 아래의 소스 코드를 통해 동일한 값을 얻으실 수 있습니다.
# Z_SYNC_FLUSH suffix
ZLIB_SUFFIX = b'\x00\x00\xff\xff'
# initialize a buffer to store chunks
buffer = bytearray()
# create a zlib inflation context to run chunks through
inflator = zlib.decompressobj()

# ...
@client.event()
def on_socket_raw_receive(msg):
  # always push the message data to your cache
  buffer.extend(msg)

  # check if the last four bytes are equal to ZLIB_SUFFIX
  if len(msg) < 4 or msg[-4:] != ZLIB_SUFFIX:
    return

  # if the message *does* end with ZLIB_SUFFIX,
  # get the full message by decompressing the buffers
  # NOTE: the message is utf-8 encoded.
  msg = inflator.decompress(buffer)
  buffer = bytearray()

  # here you can treat `msg` as either JSON or ETF encoded,
  # depending on your `encoding` param​

이 소스코드를 다소 보기좋게 수정한다면 아래의 소스코드를 대신 적용해도 됩니다.

_zlib = zlib.decompressobj()
_buffer = bytearray()

@client.event()
def on_socket_raw_receive(msg):
	if type(msg) is bytes:
		_buffer.extend(msg)
		if len(msg) < 4 or msg[-4:] != b'\x00\x00\xff\xff':
			return
		msg = _zlib.decompress(self.__buffer)
		msg = msg.decode('utf-8')
		_buffer = bytearray()
	msg = json.loads(msg)

물론 discord.py v2.0 에서부터는 컴포넌트 수신을 지원하기 때문에, 꼭 이 방법을 적용하실 필요는 없지만, 혹시나 필요하신 분을 위해 작성해드렸습니다.

 


위에 설명한 것을 기반으로 아래의 두 가지 데이터를 분석해보도록 하겠습니다. 두 데이터는 우리가 디스코드 봇을 운영한다면 무조건 받은 payload입니다. 한번 차근차근 분석해보도록 하겠습니다.

  • on_ready (READY)
{
  't': 'READY',
  's': 1,
  'op': 0,
  'd': {
    'v': 6,
    'user_settings': {},
    'user': {
      'verified': True,
      'username': '<name>',
      'mfa_enabled': True,
      'id': '<ID>',
      'flags': 0,
      'email': None,
      'discriminator': '<tag>',
      'bot': True,
      'avatar': '<avatar>'
    },
    'session_id': '<Session ID>',
    'relationships': [],
    'private_channels': [],
    'presences': [],
    'guilds': ['guild'],
    'guild_join_requests': [],
    'geo_ordered_rtc_regions': ['south-korea', 'japan', 'hongkong', 'singapore', 'india'],
    'application': {
      'id': '<ID>',
      'flags': 303104
    },
    '_trace': []
    'shard_id': None
  }
}

해당 기능은 't'값에 READY라는 내용이 있는 것을 보아, 준비되었다는 것을 알리기 위한 이벤트라고 보입니다.

실제로 해당 payload가 들어오고 나서 on_ready() 이벤트 함수가 작동하게 됩니다.

 

우리는 이때, 성공적으로 실행됐다는 것만 알 수 있지만, 사실은 들어가 있는 길드 정보, 서버 정보, 앱 정보, 봇 사용자 정보 등 다양한 정보가 들어가 있었습니다.

 

  • on_message (MESSAGE CREATE)
{
  't': 'MESSAGE_CREATE',
  's': 4,
  'op': 0,
  'd': {
    'type': 0,
    'tts': False,
    'timestamp': '<time>',
    'referenced_message': None,
    'pinned': False,
    'nonce': '<nonce ID>',
    'mentions': [],
    'mention_roles': [],
    'mention_everyone': False,
    'member': {
      'roles': ['<role ID>'],
      'premium_since': None,
      'pending': False,
      'nick': None,
      'mute': False,
      'joined_at': '<joined_at>',
      'is_pending': False,
      'hoisted_role': '<role>',
      'deaf': False,
      'avatar': None,
      'user': {
        'username': '<name>',
        'id': <User ID>,
        'avatar': '<avatar>',
        'discriminator': '<tag>',
        'bot': False
      }
    },
    'id': '<message ID>',
    'flags': 0,
    'embeds': [],
    'edited_timestamp': None,
    'content': '.',
    'components': [],
    'channel_id': '<channel ID>',
    'author': {
      'username': '<name>',
      'public_flags': 131136,
      'id': '<User ID>',
      'discriminator': '<tag>',
      'avatar': '<avatar>'
    },
    'attachments': [],
    'guild_id': '<guild ID>'
  }
}

다음은 위에 있는 예제를 분석해보도록 하겠습니다. 't'의 값이 "MESSAGE CREATE"라는 것을 보아, 메시지가 만들어졌을 때 발생하는 이벤트 함수인 것을 알 수 있습니다.

 

'd'라는 키값에 데이터 값이 상당히 많다는 것을 알 수 있는데요. 수신받은 메시지, 임베드, 자료(attachments), 서버 정보, 채널 정보, 유저 정보, 맨션 유무 등의 상당한 많은 양에 데이터가 들어 있음을 알 수 있습니다. 이 데이터들이 discord.py에서는 message라는 데이터 모델(Data Model) 안에 들어가게 되어, 보다 쉽게 수집할 수 있었습니다.

 

그러면 버튼을 눌렀을 때도 이벤트가 발생한다는 것을 대충 눈치채셨을까요? 맞습니다. 버튼을 만들었을 때에도, "INTERACTION_CREATE"라는 이벤트가 발생합니다.

{
  't': 'INTERACTION_CREATE',
  's': 7,
  'op': 0,
  'd': {
    'version': 1,
    'type': 3,
    'token': '<token>',
    'message': '<message>',
    'member': {
      'user': {
        'username': '<name>',
        'public_flags': 131136,
        'id': '<id>',
        'discriminator': '<tag>',
        'avatar': '<avatar>'
      },
      'roles': ['<role>'],
      'premium_since': None,
      'permissions': '<permissions>',
      'pending': False,
      'nick': None,
      'mute': False,
      'joined_at': '<joined_at>',
      'is_pending': False,
      'deaf': False,
      'avatar': None
    },
    'id': '<id>',
    'guild_id': '<guild_id>',
    'data': {
      'custom_id': 'python',
      'component_type': 2
    },
    'channel_id': '<channel_id>',
    'application_id': '<application_id>'
  }
}

이렇게 생긴 payload가 들어오며, 사실 상당히 긴 내용의 데이터지만 일부 중복되는 데이터를 축약하여 알려드리겠습니다. 우선 member에는 버튼을 누른 사용자의 데이터가 들어가 있었습니다. 그리고, message에는 INTERACTION(상호작용)이 발생한 메시지 데이터가 들어 있었습니다. data 값에는 사용자가 누른 버튼의 데이터와 ID값이 들어 있습니다.

 

여기서 ID 값은 저번 2편에서 다루었던 'custom_id'라는 것을 알 수 있었습니다. 즉, 우리는 'custom_id'값이 버튼을 찾기 위한 고유의 키값이라는 것을 또 알 수 있습니다.


버튼을 받아볼까요?

이제 한번 사용자가 택한 버튼을 읽어보고 분석해보도록 하겠습니다.

이번 강의는 저번 편에 이어서 진행할 예정입니다. 저번 편에 있던 마지막 소스를 이어서 나갈 예정이니 지난 강의를 보지 못한 분들은 지난 강의를 보고 오시는 것이 편할 것입니다.

2021.06.01 - [개발 강좌/기타 강좌] - 디스코드봇(Components) - 02ㅣComponents를 통하여 버튼을 만들어보자!

 

우선 위에 말했듯 discord.py에서는 기본적으로 상호작용에 대한 이벤트가 존재하지 않기 때문에 직접 만들어야 합니다.

@client.event
async def on_socket_response(payload):
	if payload.get("t", "") == "INTERACTION_CREATE":
		return

그다음은 INTERACTION_CREATE이라는 이벤트만 받아야 하기 때문에 위 조건문을 추가해야 합니다.

+ 추가 (2021-06-13)
해당 이벤트는 버튼을 누르는 경우 외에도, "/" 명령어라는 빗금 명령어를 사용해도 발생합니다. 따라서 type를 통하여 구분을 할 필요가 있습니다.

상호작용 이벤트가 발생하는 타입(type)에는 총 3가지가 있으며, 아래의 표와 같습니다.

해당 내용을 아래의 표에 추가로 정리해두겠습니다.
이름
1
빗금 명령어('/') 2
메시지 컴포넌트 3
따라서 data 값내에 있는 type 에서 "3" 값일 경우에만 작동하도록 만들어야 합니다.

@client.event
async def on_socket_response(payload):
	if payload.get("t", "") == "INTERACTION_CREATE" and payload.get("d", {}).get("type") == 3:
		return​

 

 

왜냐하면 해당 이벤트를 통하여, 상호작용이라는 이벤트뿐만 아닌, 다른 다양한 이벤트가 발생하기 때문에 우리는 우리가 필요한 이벤트만 잡아줄 필요가 있습니다.

 

't에 이벤트 이름이 들어 있으니, 't' 값을 통하여 이벤트를 필터링해줍니다. 그다음 우리는 해당 이벤트에서 제공하는 데이터 값을 수집해야 합니다. 우선 사용자가 무엇을 선택했는지에 대한 정보와, 메시지 정보를 직접 수집해봅시다.

참고로, discord.py에서는 해당 값들이 모두 Data Class안에 내장되어 보다 쉽게 불러올 수 있지만, 직접 수집할 때에는 payload라는 변수 안에 dict형태의 데이터가 들어 있으므로 직접 수집해줘야 합니다.

d = payload.get("d", {})
message = d.get("message", {})
custom_id = d.get("data", {}).get("custom_id")

data 값을 불러오지 않고, custom_id를 바로 불러왔냐면, 굳이 Button 인 것을 아는데, 불러올 필요가 있을 까라는 생각이 들어서였습니다. 물론 data라는 변수가 없어서는 안 되겠지만, 만약에 없을 경우 None값을 불러오기 때문에 에러를 유발할 수 있습니다. 따라서, 우리는 dict의 기본값을 넣어주어서 값을 확인합니다.

 

마지막으로 print()를 사용하여, 사용자가 선택받은 값을 읽어보도록 합시다.

@client.event
async def on_socket_response(payload):
	if payload.get("t", "") == "INTERACTION_CREATE":
		d = payload.get("d", {})
		message = d.get("message", {})
		custom_id = d.get("data", {}).get("custom_id")
		print(custom_id)
	return

그런데 말입니다. 어찌 버튼을 눌러도 이상한 점을 발견하지 않으셨나요? 분명히 값은 잘 불러와지는데, 아래 사진과 같은 현상이 발생합니다.

상호작용 실패? 분명히 잘 받았는데, 왜 상호작용에 실패하였다는 메시지가 뜰까요? 그것은 디스코드 봇이 올바르게 버튼을 수신받았는지 알 수 없기 때문입니다. 따라서, 상호작용을 성공적으로 맞추었다는 내용을 Discord API에 발신해야 합니다.


상호작용(Ineraction Callback) 성공해보자!

나는 올바르게 수신받았다! 를 전달하기 위해선 메시지를 보내는 것처럼 HTTP 클래스를 활용해야 합니다.

사용방법은 아래의 디스코드 API 공식 문서에 작성돼 있긴 하지만, 이 강의에서 차근차근 다루어볼 것입니다.

https://discord.com/developers/docs/interactions/slash-commands#create-interaction-response

내용을 참조하자면 /interactions/{상호작용 ID}/{상호작용 token}/callback에 POST를 보내야 합니다.

방식은 2편에서 다루었던 메시지를 보내는 방법과 동일합니다. 근데 의문점이 발생합니다. 도대체 어디서 상호작용 ID와 토큰 값을 얻어오는 걸까요?

 

바로 위 이벤트에서 받았던 데이터 안에 포함되어 있습니다. 따라서 ID값과 토큰 값도 불러와야 합니다.

interaction_id = d.get("id")
interaction_token = d.get("token")

그러면 무슨 데이터를 보내야 할까요? 상호작용 ID 만과 토큰으로는 충분하지 않을 텐데 무엇이 더 필요할까요?

상호작용 유형(Type)과 그에 알맞은 데이터가 필요합니다. 우선 유형을 지정해볼까요?

우선 위 사진을 참조해보면 총 5가지의 콜백 유형이 존재합니다.

해당 표를 아래에 번역해보도록 하겠습니다.

이름 설명
퐁(Pong) 1 ACK 핑(아직 무슨 기능인지는 잘 모릅니다. ㅎㅎ)
ChannelMessageWithSource 4 새로운 메시지를 전송합니다. 이 메시지가 사용자만 볼 수 있는 메시지 인지 선택할 수도 있습니다.
DeferredChannelMessageWithSource 5 "생각 중 입니다."라는 메시지를 보냅니다. 만약에 처리하는데 시간이 걸리는 상호작용이라면 해당 값을 리턴시키는 것을 추천드립니다.
DeferredUpdateMessage 6 우선 성공적으로 상호작용에 성공했다는 메시지를 보냅니다. 그러나 추후 메시지 수정이 필요합니다.
UpdateMessage 7 data 내에 첨부된 내용을 기반으로 메시지를 수정합니다.

6번과 7번은 특히 컴포넌트(Components)를 사용했을 때만 사용이 가능한 기능인 점도 아래에 서술되어 있습니다.

5번과 6번을 리턴할 경우에는 추가적으로 데이터를 제공할 필요는 없지만, 4번과 7번을 보냈을 경우에는 리턴되는 데이터가 data라는 값 안에 포함되어야 합니다.

 

data에 전송해야 되는 값은 2편에서 다루었던 메시지를 출력하는 방식과 방식이 동일하지만 Message 안에 있는 flag값이 0일 경우 그리고 64 일 경우에 따라, 메시지를 공개할 것인지, 본인만 볼 수 있는지를 고를 수 있습니다.

flag값이 64일 경우 이렇게 리턴됩니다.

다시 우리가 개발하고 있었던 소스코드에 돌아와서 위 문서를 참조해준 뒤, 저는 사용자에게 ㅇㅇ을 골랐다고 알려주는 메시지를 추가하였습니다.

await bot.http.request(
	Route("POST", f"/interactions/{interaction_id}/{interaction_token}/callback"),
	json={"type": 4, "data": {
		"content": "당신은 {}를 고르셨군요!".format(custom_id),
		"flags": 64
	}},
)

이렇게 상호작용을 하는 방법까지 알아보았습니다.


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

이번 강의에서 배운 상호작용(콜백)과 상호작용 수신을 토대로 어제 다루었던 프로젝트를 이어서 나가보도록 하겠습니다. 해당 내용은 이전 강의 글을 이어서 나가기 때문에 이전에 다루었던 코드가 필요합니다!

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("<토큰>")

 

(2편에서 다루었던 코드)를 편의상 가져와보았습니다. 이곳에 오늘 배운 소스를 모두 추가해줄 예정입니다.

 

우선, 위에서 말했듯이 이벤트를 저희가 직접 받아서 만들어 줘야 합니다.

 

@bot.event
async def on_socket_response(payload: dict):
	return

 

그다음 우리는 특정 이벤트 일 경우에만 작동할 수 있도록 필터링을 해준 뒤, 데이터 값과 이벤트 이름을 변수에 저장해둡니다.

 

@bot.event
async def on_socket_response(payload: dict):
	d = payload.get("d", {})
	t = payload.get("t")
	if t == "INTERACTION_CREATE" and d.get("type") == 3:
		return
	return

 

그다음, 사용자가 선택한 값을 불러옵니다. 그리고 메시지 값 또한, payload 데이터에서 꺼낸 뒤 변수에 저장해 둡니다. 해당 값은 추후 사용할 필요가 있습니다.

 

# + custom_id = d.get("data", {}).get("custom_id")
# + message = d.get("message")
@bot.event
async def on_socket_response(payload: dict):
	d = payload.get("d", {})
	t = payload.get("t")
	if t == "INTERACTION_CREATE" and d.get("type") == 3:
		custom_id = d.get("data", {}).get("custom_id")
		message = d.get("message")
	return

 

우선 저희는 최고의 프로그래밍 언어를 선택하는 프로그램을 만들 예정이기 때문에, 투표한 데이터 값을 저장해야 합니다.

voted_data = {}

 

맨 위에 해당 변수를 선언하여 전역 변수를 만들어 주고 나서, 아래처럼 메시지를 보내고 난 뒤에도 데이터를 저장할 수 있도록 값을 저장해 둡니다.

 

voted_data[response.get("id")] = {
	"python": 0, "kotlin": 0, "java": 0, "cpp": 0, "c": 0
}
@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
		)

		component1 = {
			"embed": embed.to_dict(),
			"components": default_components
		}

		response = await http.request(
			Route('POST', '/channels/{channel_id}/messages', channel_id=msg.channel.id), json=component1
		)
		voted_data[response.get("id")] = {
			"python": 0, "kotlin": 0, "java": 0, "cpp": 0, "c": 0
		}

 

그러고 나서, 사용자가 선택한 값을 변수에 저장해둔 뒤에 embed를 재생성합니다.

 

embed = discord.Embed(
	title="최고의 프로그래밍 언어",
	description="""<:python:847876880257908757> Python: {}표
				<:kotlin:847876848662216714> Kotlin: {}표
				C언어: {}표
				<:cpp:847876987778629722> C++: {}표
				<:java:847876915619954708> Java: {}표""".format(
		voted_data[message.get("id", 0)]['python'], voted_data[message.get("id", 0)]['kotlin'],
		voted_data[message.get("id", 0)]['c'], voted_data[message.get("id", 0)]['cpp'],
		voted_data[message.get("id", 0)]['java']),
	colour=0x0080ff
)
@bot.event
async def on_socket_response(payload: dict):
	d = payload.get("d", {})
	t = payload.get("t")
	if t == "INTERACTION_CREATE" and d.get("type") == 3:
		custom_id = d.get("data", {}).get("custom_id")
		message = d.get("message")
		
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: {}표
						<:kotlin:847876848662216714> Kotlin: {}표
						C언어: {}표
						<:cpp:847876987778629722> C++: {}표
						<:java:847876915619954708> Java: {}표""".format(
				voted_data[message.get("id", 0)]['python'], voted_data[message.get("id", 0)]['kotlin'],
				voted_data[message.get("id", 0)]['c'], voted_data[message.get("id", 0)]['cpp'],
				voted_data[message.get("id", 0)]['java']),
			colour=0x0080ff
		)
	return

 

그러고 나서 변경한 메시지를 사용자에게 전달해 주어야 합니다. 투표 후 수정된 값을 전송해 줍니다. 이 방법 역시 메시지를 송신할 때와 방법이 동일합니다.

 

component2 = {
	"embed": embed.to_dict(),
	"components": default_components
}

await http.request(
	Route('PATCH', '/channels/{channel_id}/messages/{message_id}',
	channel_id=message.get("channel_id"), message_id=message.get('id')), json=component2
)
@bot.event
async def on_socket_response(payload: dict):
	d = payload.get("d", {})
	t = payload.get("t")
	if t == "INTERACTION_CREATE" and d.get("type") == 3:
		custom_id = d.get("data", {}).get("custom_id")
		message = d.get("message")
		
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: {}표
						<:kotlin:847876848662216714> Kotlin: {}표
						C언어: {}표
						<:cpp:847876987778629722> C++: {}표
						<:java:847876915619954708> Java: {}표""".format(
				voted_data[message.get("id", 0)]['python'], voted_data[message.get("id", 0)]['kotlin'],
				voted_data[message.get("id", 0)]['c'], voted_data[message.get("id", 0)]['cpp'],
				voted_data[message.get("id", 0)]['java']),
			colour=0x0080ff
		)

		component2 = {
			"embed": embed.to_dict(),
			"components": default_components
		}

		await http.request(
			Route('PATCH', '/channels/{channel_id}/messages/{message_id}',
			channel_id=message.get("channel_id"), message_id=message.get('id')), json=component2
		)
	return

 

여기서 메시지는 "PATCH"라는 method와 "/channels/{채널 ID}/messages/{메시지 ID}"(https://discord.com/api/v9/channels/{채널 ID}/messages/{메시지 ID})로 수정할 수 있음을 알 수 있습니다. 채널 ID, 메시지 ID는 위에서 불러온 message 변수에서 꺼내왔습니다.

 

이제 다 왔습니다. 이제 사용자가 정상적으로 수신했다는 메시지만 얻어오면 됩니다.

 

interaction_id = d.get("id")
interaction_token = d.get("token")
		
await bot.http.request(
	Route("POST", f"/interactions/{interaction_id}/{interaction_token}/callback"),
	json={"type": 4, "data": {
		"content": "당신은 {}를 고르셨군요!".format(custom_id),
		"flags": 64
	}},
)
@bot.event
async def on_socket_response(payload: dict):
	d = payload.get("d", {})
	t = payload.get("t")
	if t == "INTERACTION_CREATE" and d.get("type") == 3:
		custom_id = d.get("data", {}).get("custom_id")
		message = d.get("message")
		
		embed = discord.Embed(
			title="최고의 프로그래밍 언어",
			description="""<:python:847876880257908757> Python: {}표
						<:kotlin:847876848662216714> Kotlin: {}표
						C언어: {}표
						<:cpp:847876987778629722> C++: {}표
						<:java:847876915619954708> Java: {}표""".format(
				voted_data[message.get("id", 0)]['python'], voted_data[message.get("id", 0)]['kotlin'],
				voted_data[message.get("id", 0)]['c'], voted_data[message.get("id", 0)]['cpp'],
				voted_data[message.get("id", 0)]['java']),
			colour=0x0080ff
		)

		component2 = {
			"embed": embed.to_dict(),
			"components": default_components
		}

		await http.request(
			Route('PATCH', '/channels/{channel_id}/messages/{message_id}',
			channel_id=message.get("channel_id"), message_id=message.get('id')), json=component2
		)
        
		interaction_id = d.get("id")
		interaction_token = d.get("token")
		
		await bot.http.request(
			Route("POST", f"/interactions/{interaction_id}/{interaction_token}/callback"),
			json={"type": 4, "data": {
				"content": "당신은 {}를 고르셨군요!".format(custom_id),
				"flags": 64
			}},
		)
	return

 

이 중에 사용자만 무엇을 택했는지 확인할 수 있도록 flags 값을 64로 제공하였습니다. 위에서 설명했듯이 0을 주면 전부 다 메시지를 볼 수 있지만, 64를 주면 버튼을 누른 사용자만 메시지를 확인하게 만들 수 있습니다.

 

이렇게 사용자가 버튼을 누른 값을 확인하고 이에 알맞은 값을 보내는 방법까지 알아보았습니다.

앞서 말한 코드는 Github Gist를 통하여 배포하고 있습니다. 저 위에 있는 코드가 잘보이지 않는다면 아래의 소스코드를 써보는 것도 나쁘지 않다고 생각합니다.

https://gist.github.com/gunyu1019/39bb26eccd23da99f2d8f7c6d5f22308


이렇게 이번 강의에서는 사용자가 버튼을 누른 값을 확인하고, 정상적으로 상호작용을 했다는 콜백 부분에 대하여 알아보았습니다. 사실은 이렇게 하면 디스코드 봇 버튼 만드는 과정은 마스터하신 거지만, 강의의 내용이 아직 하나 남아 있습니다. 다음 마지막 편에서는 아직 배포되지 않은 숨겨진 기능, Select에 대하여 다루어 보도록 하겠습니다.

 

이번 강의도 길이가 만만치 않은 편인데, 끝까지 읽어주신 분들께 감사인사를 드립니다. 모두들 수고하셨습니다!