인터랙션 봇을 만들기 위한 봇 생성 과정과 초대 과정을 거쳤어요.

기존의 시스템으로는 유저가 명령어를 사용하는 메시지를 보냈을 때 메시지가 명령어인지, 어떤 명령어를 사용했는지를 거쳐서 사용 했지만, 인터랙션에서 지원하는 슬래시 커맨드를 사용하게 되면 메시지가 명령어인지 검증할 필요가 없고 (해당 봇에 보내는 명령어 일때만 요청을 보내게 되어서) 또한 기존 메시지로 명령어를 처리했을 때 다른 봇들과의 충돌을 막기 위한 접두사 설정이 필요없어져요.

이제 직접 개발을 해서 실제로 동작하는 봇을 만들어볼게요!

이전 글을 아직 못 보셨나요?
MSA로 Discord 봇 구축하기 (3) - 인터랙션

직접 해보기

3. 슬래시 커맨드 추가하기

슬래시 커맨드를 통해 입력을 받기 위해서는 먼저 슬래시 커맨드를 추가해야 합니다.

언어에 상관 없이 (심지어는 http 클라이어트를 통해서 요청을 보내는 것 만으로도) 명령어를 추가할 수 있어요.

ping이라는 명령어를 추가해볼게요.

만약에 global command를 추가하는 경우, 적용 되는 데에 5분 정도 소요 될 수 있어서 guild command를 만들도록 할게요.

bot_token에는 개발자 포털에서 복사해둔 봇 토큰을 입력하면 되고, application_id, guild_id도 맞게 채워주세요.

1
2
3
4
5
6
7
8
9
## Request
curl -X "POST" "https://discord.com/api/v10/applications/<application_id>/guilds/<guild_id>/commands" \
     -H 'Authorization: Bot <bot_token>' \
     -H 'Content-Type: application/json' \
     -d $'{
  "name": "ping",
  "type": 1,
  "description": "ping! pong!"
}'

이렇게 하면 /ping 명령어가 추가 된거에요.

하지만 아직 동작을 어떻게 하는지 설명해주지 않았으니 돌아가지 않겠죠?

4. 기본 코드 만들기

이제 동작하는 코드를 작성해볼게요.

팀 피클에서는 봇의 전반적인 운용을 Go 언어를 사용하고 있어요. AWS Lambda가 지원하는 언어라면 모두 구현할 수 있어요.

디스코드 문서의 Receiving and Responding에 나와있는대로 응답을 하면 돼요.

라이브러리로는 arikawa를 사용중입니다.
단순한 HTTP 응답을 구성하는 것인데에도 라이브러리를 쓰는 이유는 클라이언트와 관련된 작업이 필요한 것이 아닌 인터랙션 이벤트 타입이 편해서 그렇습니다.

우선 가장 프로젝트 세팅하고 두가지 모듈을 설치해볼게요.

1
2
3
go mod init <module name>
go get github.com/diamondburned/arikawa/v3
go get github.com/aws/aws-lambda-go/lambda

main.go파일을 만들고 lambda에서 사용할 수 있는 기본 구조를 만들게요.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
	"context"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
	return events.LambdaFunctionURLResponse{}, nil
}

func main() {
	lambda.Start(handler)
}

고언어로 람다를 사용하기 위한 가장 기본적인 구조에요.

5. 람다 생성하기

람다를 생성해보고 사용을 해볼게요.

AWS Lambda페이지에서 Create Function버튼을 눌러주세요.

여기서 Funciton name과 Runtime을 설정해주세요. (아쉽게도 Go언어로는 arm64를 사용할 수 없어요 ㅜㅜ)

아래에 있는 Advanced settings에서 외부에서 바로 사용이 가능한 function url을 생성해줍니다.

그리고 Create Function을 누르면 됩니다.

생성이 되었다면 Function URL로 들어갔을 때 Hello from Lambda!라고 떠요.

이제 디스코드 봇에서 적용할 수 있게끔 두가지 요구사항을 만족시켜볼게요.

6. 보안 설정

인터랙션을 실행하기 전에, 정말 이게 디스코드 서버에서 보낸 요청이 맞는지 확인해야 돼요. 1차적으로 Function URL이 외부로 노출이 된다면 좋지 않은 상황이지만, 노출이 되더라도 보안적으로 막아주기는 해야하기 때문이에요.

그러기 위해서는 개발자 포털에 있는 PUBLIC KEY가 필요해요.

디스코드가 가지고 있는 프라이빗 키를 사용해서 시그니쳐를 생성하고, 람다 내에서 퍼블릭 키를 사용해서 정말 디스코드가 보낸 요청이 맞는지 검증하는 과정을 거칠거에요.

이 PUBLIC KEY를 복사해둘게요.

Go언어에는 검증을 하기 위한 nacl모듈을 고언어에서 제공하고 있기 때문에 그것을 바로 사용하면 돼요.

헤더에 있는 두가지 정보와 요청 바디를 사용해서 검증합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import (
  // ...
  "golang.org/x/crypto/nacl/sign"
  // ...
)

func handleRequest(ctx context.Context, request events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
  signature := request.Headers["x-signature-ed25519"]
  timestamp := request.Headers["x-signature-timestamp"]
  strBody := request.Body

  signedMessage, _ := hex.DecodeString(signature)
  publicKeyByteSlice, _ := hex.DecodeString("<PUBLIC KEY>")

  signedMessage = append(signedMessage, []byte(timestamp+strBody)...)
  _, isVerified := sign.Open(nil, signedMessage, (*[32]byte)(publicKeyByteSlice))
  if !isVerified {
    return events.LambdaFunctionURLResponse{
      StatusCode: 401,
      Body:       "Signature verification failed",
    }, nil
  }
  // ...
}

만약에 디스코드에서 보냈다는 것을 확인하지 못하면 401 에러를 반환해야 합니다.

7. Ping 신호에 응답하기

Ping (1번) 신호에 응답하는 과정이 필요합니다.

arikawa 라이브러리를 사용하고 있기 때문에 그 라이브러리에서 지원하는 타입을 이용해 인터렉션을 뜯어볼게요.

1
2
3
body := []byte(strBody)
var interaction discord.InteractionEvent
interaction.UnmarshalJSON(body)

이렇게 하면 interaction 변수에 인터랙션이 들어가요.

그리고 핑 신호에 이렇게 응답하면 돼요.

1
2
3
4
5
6
if interaction.Data.InteractionType() == discord.PingInteractionType {
  return events.LambdaFunctionURLResponse{
    StatusCode: 200,
    Body:       "{\"type\": 1}",
  }, nil
}

8. 답장하기

webhook을 사용하는 경우, 인터랙션에 response만으로 답장을 할 수 있어요.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if interaction.Data.InteractionType() == discord.CommandInteractionType {
  _ = interaction.Data.(*discord.CommandInteraction)
  interactionResponse := api.InteractionResponse{
    Type: api.MessageInteractionWithSource,
    Data: &api.InteractionResponseData{
      Content: option.NewNullableString("Pong!"),
    },
  }
  messageBytes, _ := json.Marshal(interactionResponse)
  return events.LambdaFunctionURLResponse{
    StatusCode: 200,
    Body:       string(messageBytes),
  }, nil
}

그리고 어떤 경우에도 해당하지 않으면 404응답으로 마무리 하면 돼요.

1
return events.APIGatewayProxyResponse{StatusCode: 404}, nil

9. 람다 배포하기

지금까지 만든 봇을 빌드하고, aws에 올려서 배포하면 되겠죠?

아래 명령어를 차례대로 입력해주시면 돼요.

1
2
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o hello
zip function.zip hello

CLI로 배포 하기

aws cli를 사용중인 경우 아래 명령어로 쉽게 배포할 수 있어요.

1
aws lambda update-function-code --function-name <람다 이름> --zip-file fileb://function.zip

콘솔에서 배포하기

Upload from -> .zip file을 눌러서 압축한 function.zip 파일을 업로드 하면 돼요.

10. function url 등록하기

보이는 것처럼 Function url을 개발자 포털에서 등록하면 됩니다.

이런 초록색 메시지가 뜨면 성공한거에요!

11. 결과


CraftyDragon678's profile
CraftyDragon678
보안과 인공지능에 관심이 많은 개발자입니다.