백준 관련해서 디스코드 봇을 만들고 있는데, 만드는 도중 몇 가지 상황에 대해서 유저 입력을 받아야 할 때, 어떻게 처리해야 하는지 고민이었다. 특히, 디스코드 봇을 아예 처음 제작하는 상황이라 맨땅에 헤딩 수준으로 들이 박아야 했다.
뭘 되게 많이 쓰긴 했지만, 그냥 요약하면 다음과 같다.
"디코 봇이랑 티키타카 어떻게 함?"
이게 말만 들으면 "엥? 그냥 보통 서버에서도 이벤트 듣는 .listen()
처럼 대충 비슷한 기법으로 하면 되는 거 아님?" 이럴 수 있지만, 잘 생각해야 하는게, 디스코드 봇 서버는 하나만 돌아가고 디코봇을 사용한 사람은 여러명이다.
한마디로, 디코봇에게 메시지를 보낸 사람과 메시지 내용을 구별할 줄 알아야 하고, 그에 따른 응답을 할 줄 알아야 한다. 자, 그럼 한 가지 질문이 더 있다.
"디코 봇은 사용자가 말할 때까지 어떻게 기다리지?"
상황을 가정하면 다음과 같다.
이걸 이제 어떻게 하느냐 이말이다. 웹 서버라면 사용자의 ip를 바탕으로 소캣을 이용하여 listen()
을 통해 하면 될 것이다. 근데 이건 '디코봇'이다. 웹으로 뭘 띄우는 게 아니라 디스코드를 통해 채팅을 해야 한다는 것이다. 과연, 단일 서버가 이걸 처리할 수 있을까? 다행히 discord.js는 놀랍게도 이를 구현하는 메소드가 있다.
10.18) 지금 다시 읽어보니, 저 밑에 말한 Collectors도 그냥 소켓의 일종이다...ㅋㅋ...애초에 디스코드 봇이 웹소켓 방식으로 동작하는 것이니...글은 이미 썼으니, 밑의 설명들은 그냥 소켓의 설명이라고 생각하면 된다..
답은 Collectors
를 이용해 사용자의 응답을 대신 듣는 객체를 디스코드 서버에 생성하는 것이다. 그림을 그리면 다음과 같다.
A와 B가 동시에 '!아이디등록' 이라는 명령어를 입력하면, 디스코드 봇은 각 사용자의 채팅 내용을 대신 기록할 MessageCollector 객체를 생성한다. MessageCollector은 이름 그대로 메시지를 수집하는 클래스다. 만일 각 Collector가 메시지를 수집하면, Collector은 채팅 내용을 반환한다.
코드를 한번 보자. 다음은 개발 중인 백준 봇의 코드 중, 사용자의 백준 ID를 입력하는 코드의 일부다.
async function registerId(conn, message) {
message.reply("아이디를 입력해주세요.");
const botFilter = m => !m.author.bot && m.author.id === message.author.id && !m.content.startsWith('!');
const idCollector = message.channel.createMessageCollector({filter: botFilter,max:1, time: 20000});
idCollector.on('collect', async msg => {
const bojId = msg.content;
const response = discord_util.addBojId(conn, message.author.id, bojId);
if (response){
message.reply("정상적으로 등록되었습니다.")
}else{
message.reply("알 수 없는 오류가 발생했습니다.")
}
})
idCollector.on('end', collected => {
if (collected.size === 0){
message.reply("입력 시간이 만료되었습니다.")
}
})
}
아까 말했던 MessageCollector
객체가 쓰인 곳을 천천히 뜯어보겠다.
const botFilter = m => !m.author.bot && m.author.id === message.author.id && !m.content.startsWith('!');
const idCollector = message.channel.createMessageCollector({filter: botFilter,max:1, time: 20000});
MessageCollector
은 기본적으로 메시지 필터링 기능이 있다. 즉, 위에 있는 그림에서 '특정 누군가의 메시지'만을 수집하려고 할 때 쓰인다. 물론, 특정 메시지 만을 수집할 때도 쓰일 수 있다.
botfilter
변수는 필터링 변수로, 봇 자체가 보내는 메시지를 무시하고, 메시지를 (채팅을) 보낸 사용자와 명령어를 입력한 사용자의 ID가 같을 때만 메시지를 수집한다. 뒤에는 다른 명령어 (!로 시작하는 명령어)를 무시하기 위해 쓴 조건문이다.
그 아래에는 MessageCollector을 디스코드 서버 스레드에 생성하기 위해 message.channel.createMessageCollector()
를 사용하고, 파라미터로 위에서 생성한 필터링 변수와, 최대 얼만큼 메시지를 수집할 건지 (max:1
), 그리고 얼마동안 대기할 것인지 (time: 20000
)를 넘겨준다.
그렇게 되면 디스코드 봇에 명령어를 입력할 때 디스코드 봇에 MessageCollector
객체가 생성된다. 작동 모습을 보자.
!register을 입력하면 저렇게 이미 등록되었다고 나오고 저 셋 중 하나를 입력하라고 한다. 이 순간, 디스코드 봇은 MessageCollector
을 하나 생성한 것이다. 그럼 여기서 쓰인 필터링 변수는 뭐였을까?
당연히 '변경', '삭제', '취소'만을 응답으로 받도록 필터링 변수가 선언되었을 것이다. 이렇게 말이다.
const botFilter = m => !m.author.bot &&
m.author.id === message.author.id &&
!m.content.startsWith('!') &&
(m.content === '변경' || m.content === '삭제' || m.content === '취소');
웃긴건, 만약 !m.author.bot
조건을 안 넣어주면 지가 묻고 지가 답하는 웃긴 상황이 벌어진다. 어쨌든 봇이 보낸 메시지 또한 메시지로 인식되기 때문이다. 이 코드 때문에 그렇다.
client.on('messageCreate', message => {
...
}
이건 디코봇이 디스코드 스레드 내에서 메시지가 생성되었을 때 발생하는 이벤트다. 디코봇은 디스코드 스레드에서 메시지 생성 이벤트를 감지하고 코드 내용을 실행한다. 어쨌든, 봇이 보낸 메시지는 꼭 무시하도록 하자.
이어서 작동 상황을 보겠다.
아이디를 입력하라 해서 잘 입력헀더니 정상적으로 변경이 되었다고 한다. 자, 문제를 하나 주겠다. 저기 "synoti211" 메시지는 누가 감지한 것일까? 디스코드 봇일까, 아니면 MessageCollector일까?
내 글을 잘 읽었다면 주저없이 MessageCollector을 택했을 것이다. 내가 말했듯, 디스코드 봇은 자기 대신 메시지를 들어줄 객체로 MessageCollector를 생성한다고 했다. 따라서, 이 MessageCollector 역시 event listener이 존재한다.
idCollector.on('collect', async msg => {
const bojId = msg.content;
const response = discord_util.addBojId(conn, message.author.id, bojId);
if (response){
message.reply("정상적으로 등록되었습니다.")
}else{
message.reply("알 수 없는 오류가 발생했습니다.")
}
})
idCollector.on('end', collected => {
if (collected.size === 0){
message.reply("입력 시간이 만료되었습니다.")
}
})
idCollector.on('collect')
는 MessageCollector 객체가 메시지를 수집하는 이벤트가 발생됐을 때, 즉 콜렉터가 메시지를 수집했을 때 실행되는 함수다. 그렇게 함수가 실행되면 사용자에게 정상적으로 등록되었다고 답장을 보낸다.
또한, idCollector.on('end')
는 MessageCollector이 메시지 수집을 완료했을 때 실행되는 함수다. 이는 위에서 설명한 필터링 변수에서 timeout
을 설정했을 때도 실행된다. 이렇게 말이다.
지금까지 글을 잘 읽었는지 확인하기 위해 문제를 주겠다. 다음 상황에서 각각의 메시지는 누가 보낸 걸까?
"알 수 없는 명령어입니다."는 디스코드 봇이, "입력 시간이 만료되었습니다"는 MessageCollector
이 보낸 것이다. 그 이유는 아까 설명했던 botfilter
에 있다.
const botFilter = m => !m.author.bot &&
m.author.id === message.author.id &&
!m.content.startsWith('!') &&
(m.content === '변경' || m.content === '삭제' || m.content === '취소');
이 필터링 변수에는 !m.content.startsWith('!')
라는 조건문이 있다. 즉, !로 시작하는 명령어는 듣지 않겠다는 것이다. 그럼 저 답장은 어떻게 된걸까? 저 메시지는 MessageCollector
이 아닌, 디스코드 봇 메인 객체client
가 보낸 것이다.
그림을 다시 보면 이해가 될 것이다. 디스코드 봇은 Collector 객체를 생성하고, 채팅을 수집하는 역할을 위임했다. 즉, 디스코드 봇은 사용자의 채팅을 수집하는 역할을 하지 않고, 그 외의 기능을 수행하고 있었기에, 만약 명령어가 이상한 명령어일 경우 경고를 보내는 자체 함수가 실행된 것이다.
지금까지 내가 디스코드 봇을 만들면서 티키타카를 이루기 위해 사용했던 MessageCollector
에 대해 설명했다. 이 글을 읽는 사람도 자신만의 디스코드 봇을 만들 때 티키타카가 필요한 경우, 유용하게 사용하기 바란다.